From 010ba70561cff75af0188213e87f8c418b0c3b81 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 23:00:32 -0800 Subject: [PATCH] Fix plan follow-up mode switching and submission text - Extract follow-up submission resolution into `resolvePlanFollowUpSubmission` - Send implementation prompt in default mode when follow-up text is empty - Keep composer mode toggle synced when starting same-thread implementation turns - Add tests for follow-up submission behavior --- apps/web/src/components/ChatView.tsx | 19 ++++++++++++------- apps/web/src/proposedPlan.test.ts | 27 +++++++++++++++++++++++++++ apps/web/src/proposedPlan.ts | 21 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 94d10ee5bc2ce3c2ced1cb6bc2d4fbe9197ca763..63def586487c3f4bfa05683cae42fcf1165a411b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -92,6 +92,7 @@ import { buildPlanImplementationPrompt, buildProposedPlanMarkdownFilename, proposedPlanTitle, + resolvePlanFollowUpSubmission, } from "../proposedPlan"; import { truncateTitle } from "../truncateTitle"; import { @@ -2318,19 +2319,18 @@ export default function ChatView({ threadId }: ChatViewProps) { } const trimmed = prompt.trim(); if (showPlanFollowUpPrompt && activeProposedPlan) { - const followUpText = - trimmed.length > 0 - ? trimmed - : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown); - const nextInteractionMode = trimmed.length > 0 ? "plan" : "default"; + const followUp = resolvePlanFollowUpSubmission({ + draftText: trimmed, + planMarkdown: activeProposedPlan.planMarkdown, + }); promptRef.current = ""; clearComposerDraftContent(activeThread.id); setComposerHighlightedItemId(null); setComposerCursor(0); setComposerTrigger(null); await onSubmitPlanFollowUp({ - text: followUpText, - interactionMode: nextInteractionMode, + text: followUp.text, + interactionMode: followUp.interactionMode, }); return; } @@ -2795,6 +2795,10 @@ export default function ChatView({ threadId }: ChatViewProps) { interactionMode: nextInteractionMode, }); + // Keep the mode toggle and plan-follow-up banner in sync immediately + // while the same-thread implementation turn is starting. + setComposerDraftInteractionMode(threadIdForSend, nextInteractionMode); + await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), @@ -2840,6 +2844,7 @@ export default function ChatView({ threadId }: ChatViewProps) { selectedModel, selectedModelOptionsForDispatch, selectedProvider, + setComposerDraftInteractionMode, setThreadError, settings.enableAssistantStreaming, ], diff --git a/apps/web/src/proposedPlan.test.ts b/apps/web/src/proposedPlan.test.ts index 02e4e78aed5763b1c7fa3b7144c08b85ef44651e..b8431bdbcc4d4dfafacb2707dca4129444fdd46b 100644 --- a/apps/web/src/proposedPlan.test.ts +++ b/apps/web/src/proposedPlan.test.ts @@ -5,6 +5,7 @@ import { buildPlanImplementationPrompt, buildProposedPlanMarkdownFilename, proposedPlanTitle, + resolvePlanFollowUpSubmission, } from "./proposedPlan"; describe("proposedPlanTitle", () => { @@ -25,6 +26,32 @@ describe("buildPlanImplementationPrompt", () => { }); }); +describe("resolvePlanFollowUpSubmission", () => { + it("switches to default mode when implementing the ready plan without extra text", () => { + expect( + resolvePlanFollowUpSubmission({ + draftText: " ", + planMarkdown: "## Ship it\n\n- step 1\n", + }), + ).toEqual({ + text: "PLEASE IMPLEMENT THIS PLAN:\n## Ship it\n\n- step 1", + interactionMode: "default", + }); + }); + + it("stays in plan mode when the user adds a follow-up prompt", () => { + expect( + resolvePlanFollowUpSubmission({ + draftText: "Refine step 2 first", + planMarkdown: "## Ship it\n\n- step 1\n", + }), + ).toEqual({ + text: "Refine step 2 first", + interactionMode: "plan", + }); + }); +}); + describe("buildPlanImplementationThreadTitle", () => { it("uses the plan heading when building the implementation thread title", () => { expect(buildPlanImplementationThreadTitle("# Integrate RPC\n\nBody")).toBe( diff --git a/apps/web/src/proposedPlan.ts b/apps/web/src/proposedPlan.ts index fdfe859726b95051f41a278093ecfa305345ebd1..1550eb7de1edc8c1c7b2d0e27f97c1d9ea9325d2 100644 --- a/apps/web/src/proposedPlan.ts +++ b/apps/web/src/proposedPlan.ts @@ -16,6 +16,27 @@ export function buildPlanImplementationPrompt(planMarkdown: string): string { return `PLEASE IMPLEMENT THIS PLAN:\n${planMarkdown.trim()}`; } +export function resolvePlanFollowUpSubmission(input: { + draftText: string; + planMarkdown: string; +}): { + text: string; + interactionMode: "default" | "plan"; +} { + const trimmedDraftText = input.draftText.trim(); + if (trimmedDraftText.length > 0) { + return { + text: trimmedDraftText, + interactionMode: "plan", + }; + } + + return { + text: buildPlanImplementationPrompt(input.planMarkdown), + interactionMode: "default", + }; +} + export function buildPlanImplementationThreadTitle(planMarkdown: string): string { const title = proposedPlanTitle(planMarkdown); if (!title) { -- 2.54.0