| author | |
| committer | |
| log | e259dd23c7ef67a7f215389762a1ef5622c79088 |
| tree | 4bcae525c9094cae664e8fce2189d872653ae1c1 |
| parent | df78cda8bf9c0971300e1bf35251774d9fbc833a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>6 files changed, 163 insertions(+), 20 deletions(-)
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+18-4| ... | @@ -42,7 +42,8 @@ import { | ... | @@ -42,7 +42,8 @@ import { |
| 42 | restoreComposerDraftSnapshot, | 42 | restoreComposerDraftSnapshot, |
| 43 | type ComposerDraft, | 43 | type ComposerDraft, |
| 44 | } from "../../state/use-composer-drafts"; | 44 | } from "../../state/use-composer-drafts"; |
| 45 | import { useProjects } from "../../state/entities"; | 45 | import { useEnvironmentServerConfig, useProjects } from "../../state/entities"; |
| 46 | import { resolveSelectableModelSelection } from "../../lib/modelOptions"; | ||
| 46 | import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn"; | 47 | import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn"; |
| 47 | import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; | 48 | import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; |
| 48 | import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox"; | 49 | import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox"; |
| ... | @@ -90,6 +91,9 @@ export function NewTaskDraftScreen(props: { | ... | @@ -90,6 +91,9 @@ export function NewTaskDraftScreen(props: { |
| 90 | const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10); | 91 | const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10); |
| 91 | const { logicalProjects, selectedProject, setProject } = flow; | 92 | const { logicalProjects, selectedProject, setProject } = flow; |
| 92 | const { connectedEnvironments } = useRemoteConnectionStatus(); | 93 | const { connectedEnvironments } = useRemoteConnectionStatus(); |
| 94 | const selectedEnvironmentServerConfig = useEnvironmentServerConfig( | ||
| 95 | selectedProject?.environmentId ?? null, | ||
| 96 | ); | ||
| 93 | const environmentConnected = | 97 | const environmentConnected = |
| 94 | selectedProject !== null && | 98 | selectedProject !== null && |
| 95 | connectedEnvironments.find( | 99 | connectedEnvironments.find( |
| ... | @@ -795,7 +799,14 @@ export function NewTaskDraftScreen(props: { | ... | @@ -795,7 +799,14 @@ export function NewTaskDraftScreen(props: { |
| 795 | return; | 799 | return; |
| 796 | } | 800 | } |
| 797 | const draft = getComposerDraftSnapshot(draftKey); | 801 | const draft = getComposerDraftSnapshot(draftKey); |
| 798 | const modelSelection = draft.modelSelection ?? flow.selectedModel; | 802 | // Snapshot read keeps just-typed selector state; the availability gate |
| 803 | // still applies so a stored selection on a disabled provider falls back | ||
| 804 | // to the flow's resolved model. | ||
| 805 | const modelSelection = | ||
| 806 | resolveSelectableModelSelection( | ||
| 807 | selectedEnvironmentServerConfig, | ||
| 808 | draft.modelSelection ?? null, | ||
| 809 | ) ?? flow.selectedModel; | ||
| 799 | const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode; | 810 | const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode; |
| 800 | const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName; | 811 | const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName; |
| 801 | const selectedWorktreePath = | 812 | const selectedWorktreePath = |
| ... | @@ -847,7 +858,10 @@ export function NewTaskDraftScreen(props: { | ... | @@ -847,7 +858,10 @@ export function NewTaskDraftScreen(props: { |
| 847 | if (editingPendingTask) { | 858 | if (editingPendingTask) { |
| 848 | flow.finishEditingPendingTask(); | 859 | flow.finishEditingPendingTask(); |
| 849 | } else { | 860 | } else { |
| 850 | clearComposerDraftContent(draftKey); | 861 | // Drop the workspace selection with the content: the next task should |
| 862 | // re-resolve mode/branch/origin from the server's configured defaults | ||
| 863 | // instead of resurrecting this task's picks. | ||
| 864 | clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true }); | ||
| 851 | } | 865 | } |
| 852 | navigation.getParent()?.goBack(); | 866 | navigation.getParent()?.goBack(); |
| 853 | return; | 867 | return; |
| ... | @@ -905,7 +919,7 @@ export function NewTaskDraftScreen(props: { | ... | @@ -905,7 +919,7 @@ export function NewTaskDraftScreen(props: { |
| 905 | } | 919 | } |
| 906 | flow.finishEditingPendingTask(); | 920 | flow.finishEditingPendingTask(); |
| 907 | } else { | 921 | } else { |
| 908 | clearComposerDraftContent(draftKey); | 922 | clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true }); |
| 909 | } | 923 | } |
| 910 | navigation.dispatch( | 924 | navigation.dispatch( |
| 911 | StackActions.replace("Thread", { | 925 | StackActions.replace("Thread", { |
apps/mobile/src/features/threads/new-task-flow-provider.tsx+37-12| ... | @@ -22,7 +22,11 @@ import { useEnvironmentServerConfig, useProjects, useThreadShells } from "../../ | ... | @@ -22,7 +22,11 @@ import { useEnvironmentServerConfig, useProjects, useThreadShells } from "../../ |
| 22 | import type { TurnCommandMetadata } from "../../lib/commandMetadata"; | 22 | import type { TurnCommandMetadata } from "../../lib/commandMetadata"; |
| 23 | import type { DraftComposerImageAttachment } from "../../lib/composerImages"; | 23 | import type { DraftComposerImageAttachment } from "../../lib/composerImages"; |
| 24 | import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; | 24 | import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; |
| 25 | import { buildModelOptions, groupByProvider } from "../../lib/modelOptions"; | 25 | import { |
| 26 | buildModelOptions, | ||
| 27 | groupByProvider, | ||
| 28 | resolveSelectableModelSelection, | ||
| 29 | } from "../../lib/modelOptions"; | ||
| 26 | import { groupProjectsByRepository } from "../../lib/repositoryGroups"; | 30 | import { groupProjectsByRepository } from "../../lib/repositoryGroups"; |
| 27 | import { scopedProjectKey } from "../../lib/scopedEntities"; | 31 | import { scopedProjectKey } from "../../lib/scopedEntities"; |
| 28 | import { appAtomRegistry } from "../../state/atom-registry"; | 32 | import { appAtomRegistry } from "../../state/atom-registry"; |
| ... | @@ -347,7 +351,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -347,7 +351,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 347 | const selectedProjectDraft = useComposerDraft(selectedProjectDraftKey); | 351 | const selectedProjectDraft = useComposerDraft(selectedProjectDraftKey); |
| 348 | const prompt = selectedProjectDraft.text; | 352 | const prompt = selectedProjectDraft.text; |
| 349 | const attachments = selectedProjectDraft.attachments; | 353 | const attachments = selectedProjectDraft.attachments; |
| 350 | const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local"; | 354 | // The server's configured default decides the mode until the user picks one |
| 355 | // explicitly — same resolution web uses for new draft threads. | ||
| 356 | const defaultWorkspaceMode: WorkspaceMode = | ||
| 357 | selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ?? "local"; | ||
| 358 | const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode; | ||
| 351 | const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null; | 359 | const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null; |
| 352 | const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null; | 360 | const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null; |
| 353 | // Keep the user's explicit choice separate from the resolved display value: | 361 | // Keep the user's explicit choice separate from the resolved display value: |
| ... | @@ -361,22 +369,29 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -361,22 +369,29 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 361 | const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; | 369 | const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; |
| 362 | const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; | 370 | const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; |
| 363 | 371 | ||
| 372 | // Stored selections (draft and project default) only count while their | ||
| 373 | // provider is usable on the server; otherwise the server's default model | ||
| 374 | // wins instead of silently targeting a disabled provider. | ||
| 375 | const draftModelSelection = resolveSelectableModelSelection( | ||
| 376 | selectedEnvironmentServerConfig, | ||
| 377 | selectedProjectDraft.modelSelection ?? null, | ||
| 378 | ); | ||
| 379 | const projectDefaultModelSelection = resolveSelectableModelSelection( | ||
| 380 | selectedEnvironmentServerConfig, | ||
| 381 | selectedProject?.defaultModelSelection ?? null, | ||
| 382 | ); | ||
| 364 | const modelOptions = useMemo( | 383 | const modelOptions = useMemo( |
| 365 | () => | 384 | () => |
| 366 | buildModelOptions( | 385 | buildModelOptions( |
| 367 | selectedEnvironmentServerConfig, | 386 | selectedEnvironmentServerConfig, |
| 368 | selectedProjectDraft.modelSelection ?? selectedProject?.defaultModelSelection ?? null, | 387 | draftModelSelection ?? projectDefaultModelSelection, |
| 369 | ), | 388 | ), |
| 370 | [ | 389 | [selectedEnvironmentServerConfig, draftModelSelection, projectDefaultModelSelection], |
| 371 | selectedEnvironmentServerConfig, | ||
| 372 | selectedProject?.defaultModelSelection, | ||
| 373 | selectedProjectDraft.modelSelection, | ||
| 374 | ], | ||
| 375 | ); | 390 | ); |
| 376 | 391 | ||
| 377 | const selectedModel = | 392 | const selectedModel = |
| 378 | selectedProjectDraft.modelSelection ?? | 393 | draftModelSelection ?? |
| 379 | selectedProject?.defaultModelSelection ?? | 394 | projectDefaultModelSelection ?? |
| 380 | modelOptions.find((option) => option.isDefault)?.selection ?? | 395 | modelOptions.find((option) => option.isDefault)?.selection ?? |
| 381 | modelOptions[0]?.selection ?? | 396 | modelOptions[0]?.selection ?? |
| 382 | null; | 397 | null; |
| ... | @@ -675,12 +690,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -675,12 +690,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 675 | } | 690 | } |
| 676 | const draft = getComposerDraftSnapshot(selectedProjectDraftKey); | 691 | const draft = getComposerDraftSnapshot(selectedProjectDraftKey); |
| 677 | const text = draft.text.trim(); | 692 | const text = draft.text.trim(); |
| 678 | const draftModelSelection = draft.modelSelection ?? selectedModel; | 693 | // Same availability gate the composer display applies: a stored |
| 694 | // selection targeting a disabled provider must not ride into the queue. | ||
| 695 | const draftModelSelection = | ||
| 696 | resolveSelectableModelSelection( | ||
| 697 | selectedEnvironmentServerConfig, | ||
| 698 | draft.modelSelection ?? null, | ||
| 699 | ) ?? selectedModel; | ||
| 679 | if (text.length === 0 || !draftModelSelection) { | 700 | if (text.length === 0 || !draftModelSelection) { |
| 680 | return null; | 701 | return null; |
| 681 | } | 702 | } |
| 682 | const workspaceSelection = draft.workspaceSelection; | 703 | const workspaceSelection = draft.workspaceSelection; |
| 683 | const mode = workspaceSelection?.mode ?? "local"; | 704 | // Fall back to the resolved mode (server default) so queued tasks drain |
| 705 | // with the same mode the composer displayed. | ||
| 706 | const mode = workspaceSelection?.mode ?? workspaceMode; | ||
| 684 | // When the selection is the stand-in built from the queued snapshot, | 707 | // When the selection is the stand-in built from the queued snapshot, |
| 685 | // persist the original (possibly absent) snapshot values — the | 708 | // persist the original (possibly absent) snapshot values — the |
| 686 | // stand-in's placeholder title/workspaceRoot must never be written back | 709 | // stand-in's placeholder title/workspaceRoot must never be written back |
| ... | @@ -722,10 +745,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { | ... | @@ -722,10 +745,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { |
| 722 | [ | 745 | [ |
| 723 | editingPendingProject, | 746 | editingPendingProject, |
| 724 | editingPendingTask, | 747 | editingPendingTask, |
| 748 | selectedEnvironmentServerConfig, | ||
| 725 | selectedModel, | 749 | selectedModel, |
| 726 | selectedProject, | 750 | selectedProject, |
| 727 | selectedProjectDraftKey, | 751 | selectedProjectDraftKey, |
| 728 | startFromOrigin, | 752 | startFromOrigin, |
| 753 | workspaceMode, | ||
| 729 | ], | 754 | ], |
| 730 | ); | 755 | ); |
| 731 | 756 |
apps/mobile/src/lib/modelOptions.test.ts+43-1| ... | @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; | ... | @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | ||
| 3 | import { ProviderInstanceId, type ServerConfig } from "@t3tools/contracts"; | 3 | import { ProviderInstanceId, type ServerConfig } from "@t3tools/contracts"; |
| 4 | 4 | ||
| 5 | import { buildModelOptions } from "./modelOptions"; | 5 | import { buildModelOptions, resolveSelectableModelSelection } from "./modelOptions"; |
| 6 | 6 | ||
| 7 | describe("mobile model options", () => { | 7 | describe("mobile model options", () => { |
| 8 | it("normalizes a legacy fallback selection against current capabilities", () => { | 8 | it("normalizes a legacy fallback selection against current capabilities", () => { |
| ... | @@ -49,4 +49,46 @@ describe("mobile model options", () => { | ... | @@ -49,4 +49,46 @@ describe("mobile model options", () => { |
| 49 | expect(option?.capabilities?.optionDescriptors?.[0]?.id).toBe("serviceTier"); | 49 | expect(option?.capabilities?.optionDescriptors?.[0]?.id).toBe("serviceTier"); |
| 50 | expect(option?.selection.options).toEqual([{ id: "serviceTier", value: "default" }]); | 50 | expect(option?.selection.options).toEqual([{ id: "serviceTier", value: "default" }]); |
| 51 | }); | 51 | }); |
| 52 | |||
| 53 | it("rejects stored selections whose provider is not usable", () => { | ||
| 54 | const config = { | ||
| 55 | providers: [ | ||
| 56 | { | ||
| 57 | instanceId: "codex", | ||
| 58 | driver: "codex", | ||
| 59 | enabled: true, | ||
| 60 | installed: true, | ||
| 61 | auth: { status: "authenticated" }, | ||
| 62 | models: [], | ||
| 63 | }, | ||
| 64 | { | ||
| 65 | instanceId: "claudeAgent", | ||
| 66 | driver: "claudeAgent", | ||
| 67 | enabled: false, | ||
| 68 | installed: true, | ||
| 69 | auth: { status: "authenticated" }, | ||
| 70 | models: [], | ||
| 71 | }, | ||
| 72 | ], | ||
| 73 | } as unknown as ServerConfig; | ||
| 74 | |||
| 75 | const usable = { | ||
| 76 | instanceId: ProviderInstanceId.make("codex"), | ||
| 77 | model: "gpt-5.6-sol", | ||
| 78 | }; | ||
| 79 | const disabled = { | ||
| 80 | instanceId: ProviderInstanceId.make("claudeAgent"), | ||
| 81 | model: "claude-sonnet-5", | ||
| 82 | }; | ||
| 83 | const removed = { | ||
| 84 | instanceId: ProviderInstanceId.make("codex_personal"), | ||
| 85 | model: "gpt-5.6-sol", | ||
| 86 | }; | ||
| 87 | |||
| 88 | expect(resolveSelectableModelSelection(config, usable)).toBe(usable); | ||
| 89 | expect(resolveSelectableModelSelection(config, disabled)).toBeNull(); | ||
| 90 | expect(resolveSelectableModelSelection(config, removed)).toBeNull(); | ||
| 91 | // No config (environment offline) — nothing to validate against. | ||
| 92 | expect(resolveSelectableModelSelection(null, disabled)).toBe(disabled); | ||
| 93 | }); | ||
| 52 | }); | 94 | }); |
apps/mobile/src/lib/modelOptions.ts+25| ... | @@ -58,6 +58,31 @@ function normalizeSelectionOptions( | ... | @@ -58,6 +58,31 @@ function normalizeSelectionOptions( |
| 58 | }; | 58 | }; |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | /** | ||
| 62 | * A stored model selection is only usable when its provider instance is | ||
| 63 | * currently enabled, installed, and authenticated on the server. Returns the | ||
| 64 | * selection unchanged when usable, otherwise `null` so callers fall through to | ||
| 65 | * the server's default model. A missing config (environment offline) cannot be | ||
| 66 | * validated, so stored selections pass through untouched. | ||
| 67 | */ | ||
| 68 | export function resolveSelectableModelSelection( | ||
| 69 | config: T3ServerConfig | null | undefined, | ||
| 70 | selection: ModelSelection | null, | ||
| 71 | ): ModelSelection | null { | ||
| 72 | if (!selection || !config) { | ||
| 73 | return selection; | ||
| 74 | } | ||
| 75 | const provider = config.providers.find( | ||
| 76 | (candidate) => candidate.instanceId === selection.instanceId, | ||
| 77 | ); | ||
| 78 | return provider && | ||
| 79 | provider.enabled && | ||
| 80 | provider.installed && | ||
| 81 | provider.auth.status !== "unauthenticated" | ||
| 82 | ? selection | ||
| 83 | : null; | ||
| 84 | } | ||
| 85 | |||
| 61 | export function buildModelOptions( | 86 | export function buildModelOptions( |
| 62 | config: T3ServerConfig | null | undefined, | 87 | config: T3ServerConfig | null | undefined, |
| 63 | fallbackModelSelection: ModelSelection | null, | 88 | fallbackModelSelection: ModelSelection | null, |
apps/mobile/src/state/use-composer-drafts.test.ts+30| ... | @@ -119,6 +119,36 @@ describe("mobile composer drafts", () => { | ... | @@ -119,6 +119,36 @@ describe("mobile composer drafts", () => { |
| 119 | }); | 119 | }); |
| 120 | }); | 120 | }); |
| 121 | 121 | ||
| 122 | it("drops the workspace selection when clearing a sent new-task draft", () => { | ||
| 123 | const draftKey = "new-task:environment-1:project-1"; | ||
| 124 | const draft: ComposerDraft = { | ||
| 125 | text: "send this", | ||
| 126 | attachments: [], | ||
| 127 | modelSelection: { | ||
| 128 | instanceId: ProviderInstanceId.make("codex"), | ||
| 129 | model: "gpt-5.4", | ||
| 130 | }, | ||
| 131 | workspaceSelection: { | ||
| 132 | mode: "worktree", | ||
| 133 | branch: "main", | ||
| 134 | worktreePath: null, | ||
| 135 | startFromOrigin: false, | ||
| 136 | }, | ||
| 137 | }; | ||
| 138 | |||
| 139 | expect( | ||
| 140 | clearComposerDraftContentState({ [draftKey]: draft }, draftKey, { | ||
| 141 | clearWorkspaceSelection: true, | ||
| 142 | }), | ||
| 143 | ).toEqual({ | ||
| 144 | [draftKey]: { | ||
| 145 | modelSelection: draft.modelSelection, | ||
| 146 | text: "", | ||
| 147 | attachments: [], | ||
| 148 | }, | ||
| 149 | }); | ||
| 150 | }); | ||
| 151 | |||
| 122 | it("reads the latest selector state synchronously for send", () => { | 152 | it("reads the latest selector state synchronously for send", () => { |
| 123 | const draftKey = "environment-1:thread-1"; | 153 | const draftKey = "environment-1:thread-1"; |
| 124 | const selectedDraft: ComposerDraft = { | 154 | const selectedDraft: ComposerDraft = { |
apps/mobile/src/state/use-composer-drafts.ts+10-3| ... | @@ -372,14 +372,18 @@ export function updateComposerDraftSettings( | ... | @@ -372,14 +372,18 @@ export function updateComposerDraftSettings( |
| 372 | export function clearComposerDraftContentState( | 372 | export function clearComposerDraftContentState( |
| 373 | current: Record<string, ComposerDraft>, | 373 | current: Record<string, ComposerDraft>, |
| 374 | draftKey: string, | 374 | draftKey: string, |
| 375 | options?: { readonly clearWorkspaceSelection?: boolean }, | ||
| 375 | ): Record<string, ComposerDraft> { | 376 | ): Record<string, ComposerDraft> { |
| 376 | const existing = current[draftKey]; | 377 | const existing = current[draftKey]; |
| 377 | if (!existing) { | 378 | if (!existing) { |
| 378 | return current; | 379 | return current; |
| 379 | } | 380 | } |
| 380 | const { importedShareIds: _importedShareIds, ...retained } = existing; | 381 | const { importedShareIds: _importedShareIds, workspaceSelection, ...retained } = existing; |
| 381 | const draft = { | 382 | const draft = { |
| 382 | ...retained, | 383 | ...retained, |
| 384 | ...(options?.clearWorkspaceSelection || workspaceSelection === undefined | ||
| 385 | ? {} | ||
| 386 | : { workspaceSelection }), | ||
| 383 | text: "", | 387 | text: "", |
| 384 | attachments: [], | 388 | attachments: [], |
| 385 | }; | 389 | }; |
| ... | @@ -526,8 +530,11 @@ export async function restoreComposerDraftSnapshot( | ... | @@ -526,8 +530,11 @@ export async function restoreComposerDraftSnapshot( |
| 526 | await persistenceQueue.run(() => writePersistedComposerDrafts(next)); | 530 | await persistenceQueue.run(() => writePersistedComposerDrafts(next)); |
| 527 | } | 531 | } |
| 528 | 532 | ||
| 529 | export function clearComposerDraftContent(draftKey: string): void { | 533 | export function clearComposerDraftContent( |
| 530 | updateComposerDrafts((current) => clearComposerDraftContentState(current, draftKey)); | 534 | draftKey: string, |
| 535 | options?: { readonly clearWorkspaceSelection?: boolean }, | ||
| 536 | ): void { | ||
| 537 | updateComposerDrafts((current) => clearComposerDraftContentState(current, draftKey, options)); | ||
| 531 | } | 538 | } |
| 532 | 539 | ||
| 533 | export function clearComposerDraft(draftKey: string): void { | 540 | export function clearComposerDraft(draftKey: string): void { |