fix(ui): require explicit handoff context selection (#3601)
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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<HTMLTextAreaElement>) =>
|
||||
@@ -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 }) => (
|
||||
<div>
|
||||
<button type="button" onClick={() => void onValueChange("copy_prompt")}>
|
||||
Copy initial prompt
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
ContextSelect: ({
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
}) => {
|
||||
mockContextSelectValue = value;
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="context-value">{value}</span>
|
||||
<button type="button" onClick={() => void onValueChange("copy_prompt")}>
|
||||
Copy initial prompt
|
||||
</button>
|
||||
<button type="button" onClick={() => void onValueChange("summarize:session-9")}>
|
||||
Summarize source session
|
||||
</button>
|
||||
<button type="button" onClick={() => void onValueChange("summarize:session-10")}>
|
||||
Summarize alternate session
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
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(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
@@ -290,12 +310,123 @@ describe("NewSessionDialog", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
taskId="task-1"
|
||||
handoff={{ sourceSessionId: "session-9", targetProfileId: "profile-1" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
taskId="task-1"
|
||||
handoff={{ sourceSessionId: "session-9", targetProfileId: "profile-1" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(<NewSessionDialog open={true} {...props} />);
|
||||
|
||||
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(<NewSessionDialog open={false} {...props} />);
|
||||
rerender(<NewSessionDialog open={true} {...props} />);
|
||||
|
||||
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(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
taskId="task-1"
|
||||
handoff={{ sourceSessionId: "session-9", targetProfileId: "profile-1" }}
|
||||
/>,
|
||||
);
|
||||
const prompt = screen.getByTestId(DESCRIPTION_INPUT_TEST_ID);
|
||||
fireEvent.change(prompt, { target: { value: TYPED_HANDOFF_PROMPT } });
|
||||
|
||||
rerender(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
taskId="task-1"
|
||||
handoff={{ sourceSessionId: "session-9", targetProfileId: "profile-1" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect((prompt as HTMLTextAreaElement).value).toBe(TYPED_HANDOFF_PROMPT);
|
||||
expect(mockSummarize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("launches a typed handoff prompt without summarizing", async () => {
|
||||
render(
|
||||
<NewSessionDialog
|
||||
open={true}
|
||||
onOpenChange={vi.fn()}
|
||||
taskId="task-1"
|
||||
handoff={{ sourceSessionId: "session-9", targetProfileId: "profile-1" }}
|
||||
/>,
|
||||
);
|
||||
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 () => {
|
||||
|
||||
@@ -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<TaskFormInputsHandle | null>,
|
||||
taskId: string,
|
||||
@@ -396,7 +383,6 @@ function NewSessionForm({
|
||||
setContextValue,
|
||||
setHasPrompt,
|
||||
});
|
||||
useHandoffAutoSummarize(handoff, handoffInitial?.contextValue ?? "blank", handleContextChange);
|
||||
|
||||
const handleSubmit = useSessionLaunchSubmit({
|
||||
promptRef,
|
||||
|
||||
@@ -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<typeof import("../../helpers/api-client").ApiClient>,
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <target> Hand off to <target>
|
||||
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 <chosen session> 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.
|
||||
@@ -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 <target>
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:<sessionId>` 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.
|
||||
Reference in New Issue
Block a user