From 76ab362701693139440b11a3600cb629b4810b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Fri, 11 Sep 2026 14:46:04 +0100 Subject: [PATCH] fix(ui): require explicit handoff context selection (#3601) --- .../web/components/task/handoff-types.test.ts | 10 +- apps/web/components/task/handoff-types.ts | 6 +- .../task/new-session-dialog.test.tsx | 153 ++++++++++++++-- .../components/task/new-session-dialog.tsx | 14 -- .../e2e/tests/session/mobile-handoff.spec.ts | 91 ++++++++- .../e2e/tests/session/session-handoff.spec.ts | 64 ++++++- .../agent-launch-prompt-composer/plan.md | 7 + .../task-01-reuse-shared-composer.md | 4 + .../task-02-agent-launch-e2e.md | 4 + docs/plans/handoff-blank-context/plan.md | 172 ++++++++++++++++++ .../task-01-explicit-context.md | 170 +++++++++++++++++ docs/public/sessions-and-review.md | 4 +- docs/specs/ui/README.md | 2 + .../agent-launch-prompt-composer.md | 24 ++- .../agent-launch-prompt-composer.md | 87 +++++++++ 15 files changed, 760 insertions(+), 52 deletions(-) create mode 100644 docs/plans/handoff-blank-context/plan.md create mode 100644 docs/plans/handoff-blank-context/task-01-explicit-context.md create mode 100644 docs/specs/ui/system-design/agent-launch-prompt-composer.md diff --git a/apps/web/components/task/handoff-types.test.ts b/apps/web/components/task/handoff-types.test.ts index 69eb59e4b5..123e657d4b 100644 --- a/apps/web/components/task/handoff-types.test.ts +++ b/apps/web/components/task/handoff-types.test.ts @@ -1,19 +1,15 @@ import { describe, expect, it } from "vitest"; -import { buildHandoffInitialState, summarizeContextValue } from "./handoff-types"; +import { buildHandoffInitialState } from "./handoff-types"; describe("handoff-types", () => { - it("buildHandoffInitialState selects target profile and summarize context", () => { + it("buildHandoffInitialState selects target profile and blank context", () => { const result = buildHandoffInitialState({ sourceSessionId: "session-a", targetProfileId: "profile-b", }); expect(result).toEqual({ selectedProfileId: "profile-b", - contextValue: "summarize:session-a", + contextValue: "blank", }); }); - - it("summarizeContextValue prefixes session id", () => { - expect(summarizeContextValue("session-123")).toBe("summarize:session-123"); - }); }); diff --git a/apps/web/components/task/handoff-types.ts b/apps/web/components/task/handoff-types.ts index 8336d82ed6..8d17226e93 100644 --- a/apps/web/components/task/handoff-types.ts +++ b/apps/web/components/task/handoff-types.ts @@ -9,10 +9,6 @@ export function buildHandoffInitialState(handoff: HandoffPreset): { } { return { selectedProfileId: handoff.targetProfileId, - contextValue: summarizeContextValue(handoff.sourceSessionId), + contextValue: "blank", }; } - -export function summarizeContextValue(sessionId: string): string { - return `summarize:${sessionId}`; -} diff --git a/apps/web/components/task/new-session-dialog.test.tsx b/apps/web/components/task/new-session-dialog.test.tsx index e118725fe9..f7b3a1bcfe 100644 --- a/apps/web/components/task/new-session-dialog.test.tsx +++ b/apps/web/components/task/new-session-dialog.test.tsx @@ -14,7 +14,10 @@ const mockRecordRecentUse = vi.fn(); let mockAgentSelectorValue: string | undefined; let mockAgentSelectorOnChange: ((value: string) => void) | undefined; let mockExecutorProfile: ExecutorProfile | null = null; +let mockContextSelectValue: string | undefined; const PLUGIN_COMPOSER_LABEL = "Plugin composer action"; +const DESCRIPTION_INPUT_TEST_ID = "task-description-input"; +const TYPED_HANDOFF_PROMPT = "typed handoff prompt"; const BASE_PROFILE: AgentProfileOption = { id: "profile-1", @@ -144,7 +147,7 @@ vi.mock("@/components/task-create-dialog-selectors", async () => { React.Fragment, null, React.createElement("textarea", { - "data-testid": "task-description-input", + "data-testid": DESCRIPTION_INPUT_TEST_ID, placeholder: "Describe what you want the agent to do... (@ to insert a saved prompt)", value, onChange: (event: React.ChangeEvent) => @@ -224,13 +227,29 @@ vi.mock("@/components/enhance-prompt-button", () => ({ vi.mock("./session-dialog-shared", () => ({ EnvironmentBadges: () => null, AttachButton: () => null, - ContextSelect: ({ onValueChange }: { onValueChange: (value: string) => void }) => ( -
- -
- ), + ContextSelect: ({ + value, + onValueChange, + }: { + value: string; + onValueChange: (value: string) => void; + }) => { + mockContextSelectValue = value; + return ( +
+ {value} + + + +
+ ); + }, useDialogAttachments: () => ({ attachments: [], isDragging: false, @@ -257,6 +276,7 @@ describe("NewSessionDialog", () => { mockExecutorProfile = null; mockAgentSelectorValue = undefined; mockAgentSelectorOnChange = undefined; + mockContextSelectValue = undefined; }); beforeEach(() => { @@ -274,13 +294,13 @@ describe("NewSessionDialog", () => { fireEvent.click(screen.getByRole("button", { name: "Copy initial prompt" })); await waitFor(() => - expect((screen.getByTestId("task-description-input") as HTMLTextAreaElement).value).toBe( + expect((screen.getByTestId(DESCRIPTION_INPUT_TEST_ID) as HTMLTextAreaElement).value).toBe( "seed prompt", ), ); }); - it("writes the handoff summary into the fresh-open dialog prompt", async () => { + it("opens handoff with blank context without summarizing", () => { render( { />, ); + expect(mockContextSelectValue).toBe("blank"); + expect((screen.getByTestId(DESCRIPTION_INPUT_TEST_ID) as HTMLTextAreaElement).value).toBe(""); + expect( + (screen.getByRole("button", { name: "Start Agent" }) as HTMLButtonElement).disabled, + ).toBe(true); + expect(screen.getByText("Profile 1")).toBeTruthy(); + expect(mockSummarize).not.toHaveBeenCalled(); + }); + + it("summarizes only after an explicit session selection", async () => { + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Summarize source session" })); + await waitFor(() => expect(mockSummarize).toHaveBeenCalledWith("session-9")); await waitFor(() => - expect((screen.getByTestId("task-description-input") as HTMLTextAreaElement).value).toBe( + expect((screen.getByTestId(DESCRIPTION_INPUT_TEST_ID) as HTMLTextAreaElement).value).toBe( "summary text", ), ); + expect(mockContextSelectValue).toBe("summarize:session-9"); + }); + + it("summarizes the explicitly chosen alternate session", async () => { + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Summarize alternate session" })); + + await waitFor(() => expect(mockSummarize).toHaveBeenCalledWith("session-10")); + expect(mockSummarize).toHaveBeenCalledTimes(1); + expect(mockContextSelectValue).toBe("summarize:session-10"); + }); + + it("resets handoff context to blank when reopened", async () => { + const props = { + onOpenChange: vi.fn(), + taskId: "task-1", + handoff: { sourceSessionId: "session-9", targetProfileId: "profile-1" }, + } as const; + const { rerender } = render(); + + fireEvent.click(screen.getByRole("button", { name: "Summarize source session" })); + await waitFor(() => + expect((screen.getByTestId(DESCRIPTION_INPUT_TEST_ID) as HTMLTextAreaElement).value).toBe( + "summary text", + ), + ); + + rerender(); + rerender(); + + expect(mockContextSelectValue).toBe("blank"); + expect((screen.getByTestId(DESCRIPTION_INPUT_TEST_ID) as HTMLTextAreaElement).value).toBe(""); + expect(mockSummarize).toHaveBeenCalledTimes(1); + }); + + it("preserves a typed handoff draft across ordinary rerenders", () => { + const { rerender } = render( + , + ); + const prompt = screen.getByTestId(DESCRIPTION_INPUT_TEST_ID); + fireEvent.change(prompt, { target: { value: TYPED_HANDOFF_PROMPT } }); + + rerender( + , + ); + + expect((prompt as HTMLTextAreaElement).value).toBe(TYPED_HANDOFF_PROMPT); + expect(mockSummarize).not.toHaveBeenCalled(); + }); + + it("launches a typed handoff prompt without summarizing", async () => { + render( + , + ); + fireEvent.change(screen.getByTestId(DESCRIPTION_INPUT_TEST_ID), { + target: { value: TYPED_HANDOFF_PROMPT }, + }); + + fireEvent.click(screen.getByRole("button", { name: "Start Agent" })); + + await waitFor(() => expect(mockLaunchSession).toHaveBeenCalledTimes(1)); + expect(mockBuildStartRequest).toHaveBeenCalledWith( + "task-1", + "profile-1", + expect.objectContaining({ prompt: TYPED_HANDOFF_PROMPT }), + ); + expect(mockSummarize).not.toHaveBeenCalled(); }); it("submits text a plugin composer action inserted into a blank composer", async () => { diff --git a/apps/web/components/task/new-session-dialog.tsx b/apps/web/components/task/new-session-dialog.tsx index 1100f23930..48b05954ad 100644 --- a/apps/web/components/task/new-session-dialog.tsx +++ b/apps/web/components/task/new-session-dialog.tsx @@ -151,19 +151,6 @@ function isMissingCompatibleProfile( return !hasCompatibleProfiles; } -function useHandoffAutoSummarize( - handoff: HandoffPreset | undefined, - contextValue: string, - onContextChange: (value: string) => void, -) { - const started = useRef(false); - useEffect(() => { - if (!handoff || started.current) return; - started.current = true; - void onContextChange(contextValue); - }, [handoff, contextValue, onContextChange]); -} - export function useSessionPromptController( promptRef: RefObject, taskId: string, @@ -396,7 +383,6 @@ function NewSessionForm({ setContextValue, setHasPrompt, }); - useHandoffAutoSummarize(handoff, handoffInitial?.contextValue ?? "blank", handleContextChange); const handleSubmit = useSessionLaunchSubmit({ promptRef, diff --git a/apps/web/e2e/tests/session/mobile-handoff.spec.ts b/apps/web/e2e/tests/session/mobile-handoff.spec.ts index efc2d2cf2e..6db0706ea4 100644 --- a/apps/web/e2e/tests/session/mobile-handoff.spec.ts +++ b/apps/web/e2e/tests/session/mobile-handoff.spec.ts @@ -1,7 +1,26 @@ import { test, expect } from "../../fixtures/test-base"; +import { dwell } from "../../helpers/causal-waits"; import { SessionPage } from "../../pages/session-page"; const DONE_STATES = ["COMPLETED", "WAITING_FOR_INPUT"]; +const HANDOFF_SUMMARY = "Mobile handoff summary: completed the prior session work."; + +async function mockSummarizeUtility(testPage: import("@playwright/test").Page) { + let summarizeRequestCount = 0; + await testPage.route("**/api/v1/utility/execute", async (route) => { + const request = route.request().postDataJSON() as { utility_agent_id?: string }; + if (request.utility_agent_id === "builtin-summarize-session") summarizeRequestCount += 1; + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + success: true, + response: HANDOFF_SUMMARY, + }), + }); + }); + return () => summarizeRequestCount; +} async function createProfiles( apiClient: InstanceType, @@ -27,6 +46,7 @@ test.describe("Session handoff on mobile", () => { test.setTimeout(120_000); const { profileA, profileB, agentName } = await createProfiles(apiClient); + const getSummarizeRequestCount = await mockSummarizeUtility(testPage); const task = await apiClient.createTaskWithAgent( seedData.workspaceId, @@ -65,8 +85,75 @@ test.describe("Session handoff on mobile", () => { await session.openMobileHandoffDialog(session1Id, profileB.id); + const handoffDialog = session.handoffDialog(); + await expect(handoffDialog).toBeVisible({ timeout: 5_000 }); + await expect(handoffDialog).toContainText("Hand off to"); + await expect(handoffDialog).toContainText("Mobile Handoff B"); + + const prompt = session.newSessionPromptInput(); + const contextTrigger = handoffDialog.locator("button").filter({ hasText: "Blank" }); + await expect(contextTrigger).toBeVisible(); + await expect(prompt).toHaveValue(""); + await expect(session.newSessionStartButton()).toBeDisabled(); + await expect + .poll(() => + testPage.evaluate( + () => document.documentElement.scrollWidth <= document.documentElement.clientWidth, + ), + ) + .toBe(true); + await dwell( + testPage, + 500, + "negative-assertion", + "prove opening a mobile handoff does not invoke the summary utility", + ); + expect(getSummarizeRequestCount()).toBe(0); + + await contextTrigger.tap(); + const summaryOption = testPage.getByRole("option", { name: /Mobile Handoff A/ }); + await expect(summaryOption).toBeVisible(); + await summaryOption.tap(); + await expect(prompt).toHaveValue(HANDOFF_SUMMARY, { timeout: 15_000 }); + await expect.poll(getSummarizeRequestCount).toBe(1); + + await handoffDialog.getByRole("button", { name: "Cancel" }).tap(); + await expect(handoffDialog).not.toBeVisible({ timeout: 5_000 }); + await testPage.keyboard.press("Escape"); + await expect(testPage.getByRole("dialog", { name: "Sessions" })).not.toBeVisible(); + + await session.openMobileHandoffDialog(session1Id, profileB.id); await expect(session.handoffDialog()).toBeVisible({ timeout: 5_000 }); - await expect(session.handoffDialog()).toContainText("Hand off to"); - await expect(session.handoffDialog()).toContainText("Mobile Handoff B"); + await expect(session.newSessionPromptInput()).toHaveValue(""); + await expect( + session.handoffDialog().locator("button").filter({ hasText: "Blank" }), + ).toBeVisible(); + expect(getSummarizeRequestCount()).toBe(1); + + await session.newSessionPromptInput().fill("/e2e:simple-message"); + await session.newSessionStartButton().tap(); + await expect(session.handoffDialog()).not.toBeVisible({ timeout: 15_000 }); + + let handoffSessionId: string | undefined; + await expect + .poll( + async () => { + const { sessions: updated } = await apiClient.listTaskSessions(task.id); + const handoffSession = updated.find(({ id }) => id !== session1Id); + handoffSessionId = handoffSession?.id; + if (!handoffSession) return null; + + const { messages } = await apiClient.listSessionMessages(handoffSession.id); + return messages.find((message) => message.author_type === "user")?.content ?? null; + }, + { timeout: 30_000, message: "Waiting for mobile handoff session to be created" }, + ) + .toBe("/e2e:simple-message"); + + const { sessions: updated } = await apiClient.listTaskSessions(task.id); + const handoffSession = updated.find(({ id }) => id !== session1Id); + expect(handoffSession?.id).toBe(handoffSessionId); + expect(handoffSession?.agent_profile_id).toBe(profileB.id); + expect(getSummarizeRequestCount()).toBe(1); }); }); diff --git a/apps/web/e2e/tests/session/session-handoff.spec.ts b/apps/web/e2e/tests/session/session-handoff.spec.ts index 8f58cf8ef7..d49f57e973 100644 --- a/apps/web/e2e/tests/session/session-handoff.spec.ts +++ b/apps/web/e2e/tests/session/session-handoff.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "../../fixtures/test-base"; +import { dwell } from "../../helpers/causal-waits"; import { KanbanPage } from "../../pages/kanban-page"; import { SessionPage } from "../../pages/session-page"; @@ -7,7 +8,10 @@ const DONE_STATES = ["COMPLETED", "WAITING_FOR_INPUT"]; const HANDOFF_SUMMARY = "Handoff summary: completed the prior session work."; async function mockSummarizeUtility(testPage: import("@playwright/test").Page) { + let summarizeRequestCount = 0; await testPage.route("**/api/v1/utility/execute", async (route) => { + const request = route.request().postDataJSON() as { utility_agent_id?: string }; + if (request.utility_agent_id === "builtin-summarize-session") summarizeRequestCount += 1; await route.fulfill({ status: 200, contentType: "application/json", @@ -17,6 +21,7 @@ async function mockSummarizeUtility(testPage: import("@playwright/test").Page) { }), }); }); + return () => summarizeRequestCount; } async function createProfiles( @@ -43,7 +48,7 @@ test.describe("Session handoff", () => { test.setTimeout(120_000); const { profileA, profileB } = await createProfiles(apiClient); - await mockSummarizeUtility(testPage); + const getSummarizeRequestCount = await mockSummarizeUtility(testPage); const task = await apiClient.createTaskWithAgent( seedData.workspaceId, @@ -83,25 +88,68 @@ test.describe("Session handoff", () => { await session.openHandoffDialog(session1Id, profileB.id); - await expect(session.handoffDialog()).toBeVisible({ timeout: 5_000 }); - await expect(session.handoffDialog()).toContainText("Hand off to"); - await expect(session.handoffDialog()).toContainText("Handoff Profile B"); + const handoffDialog = session.handoffDialog(); + await expect(handoffDialog).toBeVisible({ timeout: 5_000 }); + await expect(handoffDialog).toContainText("Hand off to"); + await expect(handoffDialog).toContainText("Handoff Profile B"); const prompt = session.newSessionPromptInput(); - await expect(prompt).toHaveValue(HANDOFF_SUMMARY, { timeout: 15_000 }); + const contextTrigger = handoffDialog.locator("button").filter({ hasText: "Blank" }); + await expect(contextTrigger).toBeVisible(); + await expect(prompt).toHaveValue(""); + await expect(session.newSessionStartButton()).toBeDisabled(); + await dwell( + testPage, + 500, + "negative-assertion", + "prove opening a handoff does not invoke the summary utility", + ); + expect(getSummarizeRequestCount()).toBe(0); - await prompt.fill(`${HANDOFF_SUMMARY}\n/e2e:simple-message`); + await contextTrigger.click(); + const summaryOption = testPage.getByRole("option", { + name: /Handoff Profile A/, + }); + await expect(summaryOption).toBeVisible(); + await summaryOption.click(); + await expect(prompt).toHaveValue(HANDOFF_SUMMARY, { timeout: 15_000 }); + await expect.poll(getSummarizeRequestCount).toBe(1); + + await handoffDialog.getByRole("button", { name: "Cancel" }).click(); + await expect(handoffDialog).not.toBeVisible({ timeout: 5_000 }); + + await session.openHandoffDialog(session1Id, profileB.id); + await expect(session.handoffDialog()).toBeVisible({ timeout: 5_000 }); + await expect(session.newSessionPromptInput()).toHaveValue(""); + await expect( + session.handoffDialog().locator("button").filter({ hasText: "Blank" }), + ).toBeVisible(); + expect(getSummarizeRequestCount()).toBe(1); + + await prompt.fill("/e2e:simple-message"); await session.newSessionStartButton().click(); await expect(session.handoffDialog()).not.toBeVisible({ timeout: 15_000 }); + let handoffSessionId: string | undefined; await expect .poll( async () => { const { sessions: updated } = await apiClient.listTaskSessions(task.id); - return updated.length; + const handoffSession = updated.find(({ id }) => id !== session1Id); + handoffSessionId = handoffSession?.id; + if (!handoffSession) return null; + + const { messages } = await apiClient.listSessionMessages(handoffSession.id); + return messages.find((message) => message.author_type === "user")?.content ?? null; }, { timeout: 30_000, message: "Waiting for handoff session to be created" }, ) - .toBe(2); + .toBe("/e2e:simple-message"); + + const { sessions: updated } = await apiClient.listTaskSessions(task.id); + const handoffSession = updated.find(({ id }) => id !== session1Id); + expect(handoffSession?.id).toBe(handoffSessionId); + expect(handoffSession?.agent_profile_id).toBe(profileB.id); + expect(getSummarizeRequestCount()).toBe(1); }); }); diff --git a/docs/plans/agent-launch-prompt-composer/plan.md b/docs/plans/agent-launch-prompt-composer/plan.md index 72142d3530..a8799e063b 100644 --- a/docs/plans/agent-launch-prompt-composer/plan.md +++ b/docs/plans/agent-launch-prompt-composer/plan.md @@ -6,6 +6,13 @@ status: complete # Implementation Plan: Agent Launch Prompt Composer +## Later context-default correction + +The [Blank handoff context package](../handoff-blank-context/plan.md) supersedes +this completed package's automatic-summary preservation expectation. Its new +regressions require explicit summary selection. The results below describe the +original delivery and are retained as historical evidence. + ## Overview The New Agent and handoff flows currently render a bespoke `SessionPromptField`, while task creation renders `TaskFormInputs`. The bespoke field independently implements attachments and enhancement but never installs `useTaskCreatePromptMention` or the voice control, so `@saved-prompt` remains literal text in the affected dialog. The repair replaces that duplicate field with `TaskFormInputs` in session mode and adapts the surrounding context, enhancement, and launch hooks to its existing `TaskFormInputsHandle` contract before adding desktop and mobile regressions. diff --git a/docs/plans/agent-launch-prompt-composer/task-01-reuse-shared-composer.md b/docs/plans/agent-launch-prompt-composer/task-01-reuse-shared-composer.md index c1b481ce05..e533817ea9 100644 --- a/docs/plans/agent-launch-prompt-composer/task-01-reuse-shared-composer.md +++ b/docs/plans/agent-launch-prompt-composer/task-01-reuse-shared-composer.md @@ -10,6 +10,10 @@ spec: "../../specs/ui/requirements/agent-launch-prompt-composer.md" # Task 01: Reuse shared launch composer +The automatic-summary preservation criterion below records the original completed +delivery. It is superseded by [Blank handoff context](../handoff-blank-context/plan.md). +The shared-composer acceptance and historical results remain applicable. + ## Acceptance - New Agent and handoff render `TaskFormInputs` in session mode, exposing saved-prompt autocomplete, attachments, enhancement recovery, and voice without the bespoke `SessionPromptField`. diff --git a/docs/plans/agent-launch-prompt-composer/task-02-agent-launch-e2e.md b/docs/plans/agent-launch-prompt-composer/task-02-agent-launch-e2e.md index 91085ce6ef..0d391b48ca 100644 --- a/docs/plans/agent-launch-prompt-composer/task-02-agent-launch-e2e.md +++ b/docs/plans/agent-launch-prompt-composer/task-02-agent-launch-e2e.md @@ -10,6 +10,10 @@ spec: "../../specs/ui/requirements/agent-launch-prompt-composer.md" # Task 02: Cover agent launch flows +The [Blank handoff context package](../handoff-blank-context/plan.md) adds explicit +handoff-context coverage. The completed saved-prompt scenarios and results below +remain historical evidence; they do not verify the new context default. + ## Acceptance - Desktop E2E proves saved-prompt keyboard selection inserts content without launching and explicit Start Agent creates and activates the second session. diff --git a/docs/plans/handoff-blank-context/plan.md b/docs/plans/handoff-blank-context/plan.md new file mode 100644 index 0000000000..74196221ae --- /dev/null +++ b/docs/plans/handoff-blank-context/plan.md @@ -0,0 +1,172 @@ +--- +created: 2026-09-11 +status: complete +requirements: + - REQ-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001 +system_design: + - ../../specs/ui/system-design/agent-launch-prompt-composer.md +legacy_specs: [] +--- + +# Implementation Plan: Blank handoff context + +## Overview + +Start every handoff with Blank context and an empty prompt. Generate summaries +only after explicit context selection. One sequential work order owns the small +correction, regression coverage, and public instructions. + +## Confirmed root cause and reproduction + +On 2026-09-11, a read-only Node 24 invocation imported the real +`apps/web/components/task/handoff-types.ts` and called +`buildHandoffInitialState({sourceSessionId: "session-a", targetProfileId: "profile-b"})`. +It returned `{selectedProfileId: "profile-b", contextValue: "summarize:session-a"}`, +which conflicts with the requested Blank default. + +Source trace: `NewSessionForm` initializes from that helper, then +`useHandoffAutoSummarize` calls `handleContextChange` in a mount effect. +`useSessionContextChange` calls `summarize` for the prefixed value. The summary +hook fetches the transcript and can invoke the summary utility immediately. +The unit test `buildHandoffInitialState selects target profile and summarize context`, +the component test `writes the handoff summary into the fresh-open dialog prompt`, +and desktop `session-handoff.spec.ts` all encode this automatic behavior. + +Smallest UI reproduction: open a completed task session, choose Handoff and a +compatible target profile, then observe summary selection/loading without any +context action. A configured utility and nonempty transcript allow generation. +The supplied screenshot shows Blank at capture time; it does not establish the +preceding transition. The helper execution and source trace confirm the defect. +No browser or live-instance reproduction was run for this design-only package. + +Classification: intended product behavior changes. The existing requirement's +handoff scenario expressly preserved automatic summaries. The user has now +replaced that expectation; AC-001.9 through AC-001.11 specify the correction. + +## Scope + +### In scope + +- Blank initialization, no summary side effect on opening, and fresh-open reset. +- Explicit summary selection, compatible target preservation, and typed launch. +- Focused desktop/mobile regressions and updated public handoff instructions. + +### Out of scope + +- Backend APIs, utility configuration, summary content quality, and persistence. +- Profile precedence, environment reuse, subtask and Quick Chat behavior. +- Dialog redesign, new copy/translation keys, or asynchronous summary cancellation. + +## Technical approach + +In `handoff-types.ts`, return `blank` from `buildHandoffInitialState` while +retaining the target profile. Remove `summarizeContextValue` if the confirmed +reference search still shows only its helper/test consumers. +In `new-session-dialog.tsx`, delete `useHandoffAutoSummarize` and its call; +retain the keyed fresh-open reset and existing profile resolver. Keep +`useSessionContextChange` attached to user selection in `ContextSelect`. +Do not replace the effect with an effect that clears the prompt on mount. + +Update both handoff descriptions in `docs/public/sessions-and-review.md` +(a how-to guide): opening selects Blank and the target profile; the user may +type or explicitly choose a session summary before Start Agent. + +The completed [composer package](../agent-launch-prompt-composer/plan.md) and its +two done work orders were inventoried. Preserve their historical results; +their automatic-summary preservation scope is superseded by this package. +There is no pending companion work to merge. The ADR index contains no separate +handoff-context decision requiring amendment. This local default correction +does not require a new architectural boundary or ADR. + +## ASCII UI preview + +UI-01: Handoff context on fresh opening. Desktop enters from session-tab actions; +phone enters from the mobile sessions action menu. Both use the existing dialog. + +```text +Before (source-confirmed): After (desktop and phone): +Hand off to Hand off to +Environment / Agent Profile Environment / Agent Profile +Context [Summarizing... v] Context [Blank v] +Prompt [generated summary] Prompt [empty ] + [Cancel] [Start Agent: disabled] + +After explicit summary choice: +Context [Summarizing... v] -> [Summarize v] +Prompt [summary text, editable] +[Cancel] [Start Agent] +``` + +The Before prompt represents successful completion of the automatic request. +Blank, empty prompt, target preservation, and explicit selection are required +structure (AC-001.6, .9-.11); spacing is illustrative. No geometry changes are +planned. Phone retains the same vertical form and existing footer/scroll +behavior, with touch selection through its current session entry. The focused +phone test checks reachable actions, containment, and no horizontal overflow. +Manual summary failure retains existing Blank recovery and error feedback. + +## Tests + +All criteria below belong to REQ-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001. + +| Criteria | File under apps/web | Planned evidence | +| --- | --- | --- | +| .9, .11 | components/task/handoff-types.test.ts | `buildHandoffInitialState selects target profile and blank context` fails before correction | +| .6, .9-.11 | components/task/new-session-dialog.test.tsx | `opens handoff with Blank context without summarizing`; empty prompt, disabled launch, target retained; close/reopen and source/target changes reset; rerender/session hydration does not summarize or erase typed draft | +| .4, .10 | components/task/new-session-dialog.test.tsx | Explicit source and other-session summary choices invoke only the chosen session; copy and Blank remain manual | +| .4-.6 | components/task/new-session-form-actions.test.ts; components/task/session-context-summary.test.ts | Retain manual summary result/failure and current prompt submission coverage | + +## E2E tests + +Update existing `apps/web/e2e/tests/session/session-handoff.spec.ts` in `chromium` +and `mobile-handoff.spec.ts` in `mobile-chrome`. Arm summary utility request +counting before opening. Assert Blank, empty prompt, correct target, disabled +Start Agent, and no summary execution before any context selection. Type and +launch without a summary. Separately reopen after a summary choice and prove +Blank reset; explicitly select the source summary and prove one request plus +editable summary text. Mobile uses touch selection and proves launch outcome. +These scenarios cover AC-001.6, .7, and .9-.11. + +Count only `builtin-summarize-session` requests, so unrelated utility traffic +does not fail the test. Use the existing causal-wait helpers for selected-summary +responses and documented negative assertions. Preserve existing unhealthy-profile +coverage, which remains a focused compatibility check. + +## Work orders + +- [done] [Task 01: Make handoff context explicit](task-01-explicit-context.md) + +## Verification results + +Implementation evidence is complete. The helper and mount-effect causes were +removed, explicit context actions remain manual, and the public handoff guide +now describes the delivered Blank default. + +- TDD RED: the helper expectation failed against `summarize:session-a`; the + unchanged automatic-summary dialog test then failed after the helper was + corrected, confirming both causes before the final production change. +- `pnpm install --frozen-lockfile`: passed from `apps`. +- Targeted Vitest suite: 5 files, 38 tests passed for the implementation; + the post-review fixup suite passed with 5 files and 39 tests. +- `pnpm run typecheck`: passed. +- Targeted ESLint: the implementation passed with 0 errors and 2 duplicate- + string warnings in `new-session-dialog.test.tsx`; the final fixup passed + with 0 errors and 0 warnings after deduplicating those test values. +- `pnpm run i18n:ratchet`: passed; 0 new violations and guard allowlist intact. +- Desktop managed E2E: 2 tests passed, including handoff and unhealthy-profile + coverage. +- Mobile managed E2E: 1 test passed on `mobile-chrome`, including touch launch + and the no-horizontal-overflow assertion. +- Public-doc validation: 62 tests passed and 46 published pages validated. +- Specification tests: 36 passed; all specification files passed lint. +- `git diff --check`: passed. +- The managed E2E runners completed isolated cleanup without repository or + instance artifacts. The tracked work-order package is the only plan output. +- Owning UI index: 14,745 bytes, within the configured 16 KiB limit. + +## Risks + +- Changing only the displayed default leaves the mount action in place; remove both. +- Existing automatic-summary tests must become explicit-choice tests, not be dropped. +- Keep compatible target selection independent from Blank context initialization. +- Public docs must ship with implementation, not claim pending behavior is live. diff --git a/docs/plans/handoff-blank-context/task-01-explicit-context.md b/docs/plans/handoff-blank-context/task-01-explicit-context.md new file mode 100644 index 0000000000..cd2080e700 --- /dev/null +++ b/docs/plans/handoff-blank-context/task-01-explicit-context.md @@ -0,0 +1,170 @@ +--- +id: "01-explicit-context" +title: "Make handoff context explicit" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001 +acceptance_criteria: + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.4 + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.6 + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.7 + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.9 + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.10 + - AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.11 +system_design: + - ../../specs/ui/system-design/agent-launch-prompt-composer.md +--- + +# Task 01: Make handoff context explicit + +## Summary + +Initialize handoff context as Blank and remove automatic context-action execution. +Preserve explicit context actions and target-profile selection on both viewports. + +## In scope + +- Implement the helper and mount-effect correction described in the plan. +- Update the existing unit/component and desktop/mobile handoff regressions. +- Update both public handoff descriptions with the delivered behavior. + +## Out of scope + +Backend changes, stored preferences, profile precedence, dialog layout changes, +new translations, and summary cancellation or stale-result redesign. + +## Acceptance + +- Fresh opening, reopening, and new handoff identity show Blank with an empty + prompt, preserve compatible target choice, and perform no automatic summary. + Ordinary rerenders and data hydration preserve typed text without summary work. +- Explicit context selection retains copy, Blank, selected-session summary, + loading/failure behavior, and launch guards. Typed launch needs no summary. +- Desktop and phone E2E prove the outcomes in the plan's scenario matrix; + public instructions describe the delivered default and optional summary action. + +## ASCII UI preview + +UI-01 from the [full preview](plan.md#ascii-ui-preview), AC-001.6 and .9-.11. +Shared desktop/phone form; phone entry uses the existing mobile session actions. + +```text +Hand off to +Environment / Agent Profile +Context [Blank v] +Prompt [empty] +[Cancel] [Start Agent: disabled] +``` + +Only user selection changes context to a session summary. Retain the current +phone form geometry and touch treatment. Compare both rendered views with this +structure during the focused E2E runs. + +## Verification + +Start with TDD: change the helper expectation to Blank and replace the automatic +component test with `opens handoff with Blank context without summarizing`. +Confirm a behavioral RED (actual summarize value/call), then remove both causes. +Add explicit-summary, reopen, alternate-session, hydration, and typed-launch +coverage specified by the plan. Extend the existing context-selector mock to +expose selected value and explicit summary actions if needed. + +Run from repository root. Install dependencies once before package commands. +Managed E2E rebuilds production assets and owns isolated instance cleanup. + +```bash +(cd apps && pnpm install --frozen-lockfile) +(cd apps/web && pnpm exec vitest run components/task/handoff-types.test.ts components/task/new-session-dialog.test.tsx components/task/new-session-form-actions.test.ts components/task/session-context-summary.test.ts components/task/new-session-profile-selection.test.ts) +(cd apps/web && pnpm run typecheck) +(cd apps/web && pnpm exec eslint components/task/handoff-types.ts components/task/handoff-types.test.ts components/task/new-session-dialog.tsx components/task/new-session-dialog.test.tsx e2e/tests/session/session-handoff.spec.ts e2e/tests/session/mobile-handoff.spec.ts) +(cd apps/web && pnpm run i18n:ratchet) +(cd apps/web && pnpm e2e:run --project chromium tests/session/session-handoff.spec.ts tests/session/session-handoff-unhealthy-profile.spec.ts) +(cd apps/web && pnpm e2e:run --project mobile-chrome tests/session/mobile-handoff.spec.ts) +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +python3 scripts/lint-spec-files.test.py +python3 scripts/lint-spec-files.py --all +git diff --check +git status --short -- docs/plans/handoff-blank-context +``` + +If supporting test helpers change, include them in targeted lint and test checks. +Record RED and final results, discovered test counts, rendered comparison, and +cleanup. Do not mark done if a required command fails or no tests are discovered. + +## Files likely touched + +- `apps/web/components/task/handoff-types.ts` +- `apps/web/components/task/handoff-types.test.ts` +- `apps/web/components/task/new-session-dialog.tsx` +- `apps/web/components/task/new-session-dialog.test.tsx` +- `apps/web/e2e/tests/session/session-handoff.spec.ts` +- `apps/web/e2e/tests/session/mobile-handoff.spec.ts` +- `docs/public/sessions-and-review.md` +- This work order and `plan.md` for execution status and results. + +## Dependencies + +None. Start after the user's explicit implementation request. + +## Risks + +The old tests encode automatic summaries. Retain their summary-delivery coverage +by selecting context explicitly. Keep session/profile initialization and the +existing form key intact. Count only summary utility requests in browser checks. + +## Parallelism + +`sequential` + +## Inputs + +- [Requirement](../../specs/ui/requirements/agent-launch-prompt-composer.md), AC-001.4, .6-.7, .9-.11. +- [Design](../../specs/ui/system-design/agent-launch-prompt-composer.md), Context lifecycle and Responsive behavior. +- `new-session-form-actions.ts`, `session-dialog-shared.tsx`, and `hooks/use-summarize-session.ts` for explicit-action behavior. +- Existing handoff E2E files and `e2e/pages/session-page.ts` for desktop/mobile entry helpers. +- Use `/tdd`, `/e2e`, and `/docs-maintainer` during execution. + +## Results + +Implemented the correction in the shared handoff composer. `buildHandoffInitialState` +now preserves the target profile while selecting `blank`; the automatic mount +effect was removed, so only an explicit ContextSelect action can request a +summary. Desktop and phone regressions cover fresh open, explicit summary +selection, reopen reset, typed launch, target preservation, and phone overflow. + +TDD evidence: + +- RED: the updated helper test failed before the helper change because the + actual value was `summarize:session-a`. +- RED: after the helper change, the unchanged dialog test failed because the + old mount effect no longer received an automatic summary call. +- GREEN: the implementation targeted suite passed with 5 files and 38 tests; + the post-review fixup suite passed with 5 files and 39 tests. + +Validation: + +- `pnpm install --frozen-lockfile`: passed from `apps`. +- `pnpm exec vitest run ...`: 5 files, 38 tests passed for the implementation. +- Post-review fixup targeted suite: 5 files, 39 tests passed after adding the + alternate-session summary regression. +- `pnpm run typecheck`: passed. +- Targeted ESLint: the implementation passed with 0 errors and 2 duplicate- + string warnings in `new-session-dialog.test.tsx`; the final fixup passed + with 0 errors and 0 warnings after deduplicating those test values. +- `pnpm run i18n:ratchet`: passed with 0 new violations. +- The exact desktop command passed with 2 tests: handoff and + unhealthy-profile compatibility. +- The mobile command passed with 1 `mobile-chrome` test, including touch + selection, typed launch, and horizontal-overflow checks. +- `node --test scripts/validate-public-docs.test.mjs`: 62 tests passed. +- `node scripts/validate-public-docs.mjs`: 46 published pages validated. +- `python3 scripts/lint-spec-files.test.py`: 36 tests passed. +- `python3 scripts/lint-spec-files.py --all`: passed. +- `git diff --check`: passed. + +The E2E runs used the managed isolated runner and left no instance artifacts. +The public documentation correction is included in this implementation package. diff --git a/docs/public/sessions-and-review.md b/docs/public/sessions-and-review.md index e62b0c9df2..298a3d2ce4 100644 --- a/docs/public/sessions-and-review.md +++ b/docs/public/sessions-and-review.md @@ -40,7 +40,7 @@ The profile picker shows only profiles compatible with the task executor. If non | **Copy initial prompt** | Copies the first user message from the currently active session into the editable prompt field | A parallel approach; it is not guaranteed to be the task's original description, so inspect and edit it before launch | | **Summarize a session** | Inserts a utility-agent summary of the selected conversation into the editable prompt field | Continue or branch from work already discussed | -**Handoff** from an existing session opens the same dialog and selects a summary of that session. Summarization requires a working `summarize-session` utility agent. Review generated summaries: they can omit constraints or decisions. +**Handoff** from an existing session opens the same dialog with Blank context and an empty prompt. Select a session summary when you need earlier discussion. Summarization requires a working `summarize-session` utility agent. Review generated summaries: they can omit constraints or decisions. Prompts support pasted, dropped, or selected attachments. A prompt can contain at most 10 files, with a limit of 10 MiB per file and 20 MiB in total. The prompt itself is required. @@ -56,7 +56,7 @@ Right-click an agent tab on desktop to manage it. Available actions depend on it | **Resume** | Attempts to continue a completed, failed, or cancelled session | | **Delete** | Permanently removes the conversation; if it was primary, another session is promoted when possible. The task workspace and its files are kept; a later session reuses them | | **Share** | Opens the publishing preview for an eligible session | -| **Handoff** | Starts another session with a generated summary of this conversation | +| **Handoff** | Opens the launch dialog with Blank context. Select a summary when you want to include this conversation | | **Close Others** | Closes other visible agent panels without deleting their sessions | Stopping is not deletion. Resume succeeds only while the executor still has the session record needed to continue. A removed worktree, expired remote environment, restarted executor, removed profile, or missing runtime record can force a fresh session instead. The failure banner offers **Start fresh** when continuation is unavailable. diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md index 134ce3d4b7..14d1bb23ee 100644 --- a/docs/specs/ui/README.md +++ b/docs/specs/ui/README.md @@ -154,6 +154,8 @@ runtime, and workspace lifecycle/state ownership. - [WebSocket Connectivity Warning](requirements/ws-connectivity-warning.md) ### Design + +- [Agent launch prompt composer](system-design/agent-launch-prompt-composer.md) - [Control sizing](system-design/control-sizing.md) - [Adaptive Kanban](system-design/adaptive-kanban.md) - [Clarification submit feedback](system-design/clarification-submit-feedback.md) diff --git a/docs/specs/ui/requirements/agent-launch-prompt-composer.md b/docs/specs/ui/requirements/agent-launch-prompt-composer.md index 9fb8cd33b7..ccdf256dc8 100644 --- a/docs/specs/ui/requirements/agent-launch-prompt-composer.md +++ b/docs/specs/ui/requirements/agent-launch-prompt-composer.md @@ -2,7 +2,7 @@ status: active system: ui created: 2026-08-02 -updated: 2026-08-23 +updated: 2026-09-11 owners: - kandev --- @@ -28,6 +28,9 @@ Starting another agent inside an existing task is another prompt-authoring flow, - **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.6:** The Start Agent action remains disabled while the prompt is empty, while context is being summarized, while launch is in progress, or when no compatible agent profile is available. - **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.7:** Desktop and phone layouts expose the same capabilities and launch result. On phone, the existing mobile sessions entry point opens the dialog; the prompt menu follows the shared [composer suggestion overlay requirements](composer-suggestion-overlays.md), and the controls remain touch reachable without horizontal document overflow. - **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.8:** Agent launch continues to use the existing session launch contract, task environment, context selection, profile compatibility rules, and guarded enhancement delivery. +- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.9:** Each fresh opening of New Agent or handoff shall select Blank with an empty prompt. Reopening after a previous context choice shall also start Blank. Opening shall not generate a summary or launch an agent. +- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.10:** A session summary shall start only when the user explicitly selects a session summary option. The selected session supplies the summary. Opening, profile selection, and session-list updates shall not select context or start summarization. +- **AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.11:** On desktop and phone, handoff shall retain the selected compatible target profile while starting with Blank context. Users can type a prompt and launch without configuring or invoking summarization. ## Migrated source detail @@ -62,10 +65,25 @@ Starting another agent inside an existing task is another prompt-authoring flow, - **GIVEN** voice mode is configured, **WHEN** the user dictates in the launch composer, **THEN** the transcript is inserted at the caret and configured auto-send starts the agent only after non-empty text was inserted. - **GIVEN** a prior task prompt or session summary, **WHEN** the user selects that context, **THEN** the resulting text appears in the shared composer and remains editable with saved-prompt, attachment, enhancement, and voice controls available. - **GIVEN** a phone viewport, **WHEN** the user opens New Agent from the mobile session controls, inserts a saved prompt by touch, and starts the agent, **THEN** the dialog remains viewport-contained without document horizontal overflow and the new session becomes active. -- **GIVEN** a session handoff, **WHEN** the handoff dialog opens, **THEN** it exposes the same shared launch composer while preserving the existing automatic summary and target-profile behavior. +- **GIVEN** a session handoff, **WHEN** the handoff dialog opens, **THEN** it exposes an empty shared launch composer with Blank context and preserves compatible target-profile selection. Summary generation requires a subsequent explicit context selection. + +## Compatibility + +The 2026-09-11 correction replaces automatic handoff summaries with explicit +context selection, as requested by the user. Existing summary generation, +copy-prompt selection, profile compatibility, and launch contracts remain available. + +## System design + +- [Agent launch prompt composer](../system-design/agent-launch-prompt-composer.md) + +## Implementation Plans + +- [Shared composer delivery](../../../plans/agent-launch-prompt-composer/plan.md) records the completed composer integration. +- [Blank handoff context](../../../plans/handoff-blank-context/plan.md) delivers AC-001.9 through AC-001.11 and supersedes the earlier automatic-summary expectation. ## Out of scope - Changing saved-prompt storage, mention search, attachment limits, utility-agent enhancement, voice settings, or the session launch API. -- Changing task creation, subtask creation, task chat, Quick Chat, context selection semantics, agent profile selection, or environment reuse. +- Changing task creation, subtask creation, task chat, Quick Chat, explicit context-action results, agent profile selection, or environment reuse. - Adding rich inline chips or external `#` entity references to the plain launch prompt. diff --git a/docs/specs/ui/system-design/agent-launch-prompt-composer.md b/docs/specs/ui/system-design/agent-launch-prompt-composer.md new file mode 100644 index 0000000000..0e93de3070 --- /dev/null +++ b/docs/specs/ui/system-design/agent-launch-prompt-composer.md @@ -0,0 +1,87 @@ +--- +status: current +system: ui +created: 2026-09-11 +updated: 2026-09-11 +owners: + - kandev +requirements: + - REQ-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001 +--- + +# Agent launch prompt composer + +## Boundary + +This design extends the existing UI-owned launch-composer interaction contract. +It owns prompt initialization and user context selection in New Agent and handoff. +Task environment reuse and agent profile eligibility remain owned by their +[task](../../tasks/system-design/additional-session-workspace-reuse.md) and +[agent](../../agents/system-design/profile-recent-use.md) contracts. + +## Components + +`NewSessionDialog` in `apps/web/components/task/new-session-dialog.tsx` renders +`NewSessionForm` keyed by task, open state, and handoff source/target identity. +The form renders shared `TaskFormInputs` in session mode and reads its current +prompt and attachments through `TaskFormInputsHandle`. + +`buildHandoffInitialState` in `handoff-types.ts` returns the target profile and +`contextValue: "blank"`. The source session remains part of `HandoffPreset` and +the form identity; it does not authorize summary generation. +`useSessionProfileSelection` independently preserves the existing compatible +handoff target and fallback behavior. + +## Context lifecycle + +The form initializes Blank and an empty composer for every fresh opening. +Keep the keyed reset on closing/reopening or changing handoff identity. +Ordinary rerenders and session/profile data hydration preserve the current draft. + +Remove `useHandoffAutoSummarize` and its invocation. Mounting the form must not +call `useSessionContextChange`'s returned action. Only `ContextSelect` user +selection invokes that action: + +- `blank` clears the composer and prompt-presence state. +- `copy_prompt` inserts the existing initial prompt. +- `summarize:` invokes `useSummarizeSession` for that selected session. + +The summary hook reads the selected transcript and calls the existing +`builtin-summarize-session` utility. Opening the form may load session options, +but must not request a transcript for automatic summary generation or execute +the summary utility. There is no stored context preference or migration. + +## Composer, launch, and failure behavior + +Retain `useSessionPromptController` enhancement delivery and recovery. +`useSessionLaunchSubmit` reads the current shared handle and uses the existing +launch service. Empty-prompt, busy, attachment-upload, and compatible-profile +guards remain in force. Manual summary loading and +`applySummarizeSessionResult` success/failure behavior remain in force, including +Blank recovery and the existing error toast on failure. Broader asynchronous +summary cancellation or stale-result handling is outside this change. + +## Responsive behavior + +Desktop session actions and phone session actions open the same `NewSessionDialog`. +The nearest shipped phone exemplar is `mobile-handoff.spec.ts`, using +`SessionPage.openMobileHandoffDialog` through the mobile sessions controls. +This correction changes shared initialization only. It retains the existing +dialog, control order, scroll behavior, and touch presentation. Explicit summary +selection and typed-prompt launch receive desktop and Pixel 5 coverage. +Reuse the existing localized Blank and summary labels. + +## Requirement mapping and evidence + +| Acceptance criteria | Design boundary | Evidence | +| --- | --- | --- | +| AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.1 through .5 | Shared composer and launch | Existing composer and action tests | +| AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.6 through .8 | Guards and responsive launch | Dialog tests and desktop/mobile session E2E | +| AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.9 | Fresh-open initialization | Helper and dialog regressions, reopen E2E | +| AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.10 | Explicit context action | Summary spy/request counting and manual selection | +| AC-UI-AGENT-LAUNCH-PROMPT-COMPOSER-001.11 | Independent profile selection | Target preservation and typed-prompt launch on both viewports | + +## Implementation Plans + +[Blank handoff context](../../../plans/handoff-blank-context/plan.md) records the +completed correction and its implementation evidence.