diff --git a/packages/app/src/screens/new-workspace-screen.test.tsx b/packages/app/src/screens/new-workspace-screen.test.tsx index 955ce16ab..ea4fe13ce 100644 --- a/packages/app/src/screens/new-workspace-screen.test.tsx +++ b/packages/app/src/screens/new-workspace-screen.test.tsx @@ -15,6 +15,7 @@ const { mergeWorkspacesMock, navigateMock, saveDraftInputMock, + clearDraftInputMock, queueDraftSubmissionMock, createdAgent, createdWorkspace, @@ -116,6 +117,7 @@ const { mergeWorkspacesMock: vi.fn(), navigateMock: vi.fn(), saveDraftInputMock: vi.fn(), + clearDraftInputMock: vi.fn(), queueDraftSubmissionMock: vi.fn(), createdAgent, createdWorkspace, @@ -228,6 +230,7 @@ vi.mock("@/stores/draft-store", () => ({ useDraftStore: { getState: () => ({ saveDraftInput: saveDraftInputMock, + clearDraftInput: clearDraftInputMock, }), }, })); @@ -494,6 +497,7 @@ beforeEach(() => { mockClient.createPaseoWorktree.mockClear(); mockClient.createAgent.mockClear(); saveDraftInputMock.mockClear(); + clearDraftInputMock.mockClear(); queueDraftSubmissionMock.mockClear(); navigateMock.mockClear(); initialAttachments.length = 0; @@ -646,6 +650,13 @@ describe("NewWorkspaceScreen picker payload", () => { target: { kind: "draft", draftId: "draft-new-workspace" }, navigationMethod: "replace", }); + expect(clearDraftInputMock).toHaveBeenCalledWith({ + draftKey: "new-workspace:server:/repo", + lifecycle: "sent", + }); + expect(navigateMock.mock.invocationCallOrder[0]).toBeLessThan( + clearDraftInputMock.mock.invocationCallOrder[0], + ); expect(document.querySelector("textarea")).toHaveProperty("disabled", true); }); @@ -877,6 +888,7 @@ describe("NewWorkspaceScreen picker payload", () => { expect(queryByTestId("message-input-attach-button")).toHaveProperty("disabled", true); expect(queryByTestId("new-workspace-ref-picker-trigger")).toHaveProperty("disabled", true); expect(queryByTestId("new-workspace-ref-picker-branch-dev")).toHaveProperty("disabled", true); + expect(clearDraftInputMock).not.toHaveBeenCalled(); createWorktree.reject(new Error("Create worktree failed")); await flush(); diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index b0a6c2904..c44d6dd53 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -135,8 +135,9 @@ export function NewWorkspaceScreen({ const isPending = pendingAction !== null; const client = useHostRuntimeClient(serverId); const isConnected = useHostRuntimeIsConnected(serverId); + const draftKey = `new-workspace:${serverId}:${sourceDirectory}`; const chatDraft = useAgentInputDraft({ - draftKey: `new-workspace:${serverId}:${sourceDirectory}`, + draftKey, initialCwd: sourceDirectory, composer: { initialServerId: serverId || null, @@ -370,6 +371,7 @@ export function NewWorkspaceScreen({ target: { kind: "draft", draftId }, navigationMethod: "replace", }); + useDraftStore.getState().clearDraftInput({ draftKey, lifecycle: "sent" }); } catch (error) { const message = toErrorMessage(error); setPendingAction(null); @@ -377,7 +379,7 @@ export function NewWorkspaceScreen({ toast.error(message); } }, - [composerState, ensureWorkspace, serverId, toast], + [composerState, draftKey, ensureWorkspace, serverId, toast], ); const workspaceTitle = diff --git a/packages/app/src/screens/workspace/workspace-draft-agent-tab.test.tsx b/packages/app/src/screens/workspace/workspace-draft-agent-tab.test.tsx new file mode 100644 index 000000000..3f35a9d03 --- /dev/null +++ b/packages/app/src/screens/workspace/workspace-draft-agent-tab.test.tsx @@ -0,0 +1,270 @@ +/** + * @vitest-environment jsdom + */ +import React from "react"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ComposerAttachment } from "@/attachments/types"; +import type { MessagePayload } from "@/components/message-input"; +import { useCreateFlowStore } from "@/stores/create-flow-store"; +import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store"; +import type { StreamItem } from "@/types/stream"; +import { WorkspaceDraftAgentTab } from "./workspace-draft-agent-tab"; + +const { + createAgentMock, + latestComposerText, + latestComposerDisabled, + latestStreamText, + onCreatedMock, +} = vi.hoisted(() => ({ + createAgentMock: vi.fn(), + latestComposerText: { current: null as string | null }, + latestComposerDisabled: { current: null as boolean | null }, + latestStreamText: { current: null as string | null }, + onCreatedMock: vi.fn(), +})); + +vi.mock("react-native-unistyles", () => ({ + StyleSheet: { + create: (factory: unknown) => + typeof factory === "function" + ? factory({ + colors: { + surface0: "#000", + surface2: "#222", + destructive: "#f00", + }, + spacing: { 2: 8, 3: 12, 4: 16, 6: 24 }, + borderRadius: { md: 6 }, + fontSize: { sm: 13 }, + }) + : factory, + }, +})); + +vi.mock("react-native-safe-area-context", () => ({ + useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }), +})); + +vi.mock("@/constants/platform", () => ({ + isWeb: true, + isNative: false, +})); + +vi.mock("@/runtime/host-runtime", () => ({ + useHostRuntimeClient: () => ({ + createAgent: createAgentMock, + }), + useHostRuntimeIsConnected: () => true, +})); + +vi.mock("@/stores/session-store-hooks", () => ({ + useWorkspaceExecutionAuthority: () => ({ + ok: true, + authority: { + workspaceId: "workspace-1", + workspaceDirectory: "/repo/.paseo/worktrees/workspace-1", + }, + }), +})); + +vi.mock("@/hooks/use-agent-input-draft", () => ({ + useAgentInputDraft: () => { + const [text, setText] = React.useState("please review this change"); + const [attachments, setAttachments] = React.useState([]); + return { + text, + setText, + attachments, + setAttachments, + cwd: "/repo/.paseo/worktrees/workspace-1", + clear: () => { + setText(""); + setAttachments([]); + }, + isHydrated: true, + composerState: { + providerDefinitions: [{ id: "codex", label: "Codex" }], + selectedProvider: "codex", + selectedMode: "", + modeOptions: [], + selectedModel: "gpt-5.4", + availableModels: [{ id: "gpt-5.4", label: "GPT-5.4" }], + allProviderModels: {}, + isAllModelsLoading: false, + isModelLoading: false, + effectiveModelId: "gpt-5.4", + effectiveThinkingOptionId: "", + featureValues: undefined, + statusControls: { + providerDefinitions: [{ id: "codex", label: "Codex" }], + selectedProvider: "codex", + modeOptions: [], + selectedMode: "", + models: [{ id: "gpt-5.4", label: "GPT-5.4" }], + selectedModel: "gpt-5.4", + thinkingOptions: [], + selectedThinkingOptionId: "", + isModelLoading: false, + allProviderModels: {}, + isAllModelsLoading: false, + features: [], + }, + commandDraftConfig: undefined, + persistFormPreferences: vi.fn(), + }, + }; + }, +})); + +vi.mock("@/utils/encode-images", () => ({ + encodeImages: vi.fn(async () => []), +})); + +vi.mock("@/components/composer-attachments", () => ({ + splitComposerAttachmentsForSubmit: (attachments: ComposerAttachment[]) => ({ + images: [], + attachments, + }), +})); + +vi.mock("@/components/composer", () => ({ + Composer: ({ + value, + isSubmitLoading, + }: { + onSubmitMessage: (payload: MessagePayload) => Promise; + value: string; + isSubmitLoading?: boolean; + }) => { + latestComposerText.current = value; + latestComposerDisabled.current = isSubmitLoading ?? false; + return ( +