authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 23:00:32-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log010ba70561cff75af0188213e87f8c418b0c3b81
treed80e0ae417b2bda10b884561b72deb68b4de3256
parent6ee057e25bd4f4dad85445571ffb0cc1ce3c6980

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

3 files changed, 60 insertions(+), 7 deletions(-)

apps/web/src/components/ChatView.tsx+12-7
......@@ -92,6 +92,7 @@ import {
9292 buildPlanImplementationPrompt,
9393 buildProposedPlanMarkdownFilename,
9494 proposedPlanTitle,
95 resolvePlanFollowUpSubmission,
9596} from "../proposedPlan";
9697import { truncateTitle } from "../truncateTitle";
9798import {
......@@ -2318,19 +2319,18 @@ export default function ChatView({ threadId }: ChatViewProps) {
23182319 }
23192320 const trimmed = prompt.trim();
23202321 if (showPlanFollowUpPrompt && activeProposedPlan) {
2321 const followUpText =
2322 trimmed.length > 0
2323 ? trimmed
2324 : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown);
2325 const nextInteractionMode = trimmed.length > 0 ? "plan" : "default";
2322 const followUp = resolvePlanFollowUpSubmission({
2323 draftText: trimmed,
2324 planMarkdown: activeProposedPlan.planMarkdown,
2325 });
23262326 promptRef.current = "";
23272327 clearComposerDraftContent(activeThread.id);
23282328 setComposerHighlightedItemId(null);
23292329 setComposerCursor(0);
23302330 setComposerTrigger(null);
23312331 await onSubmitPlanFollowUp({
2332 text: followUpText,
2333 interactionMode: nextInteractionMode,
2332 text: followUp.text,
2333 interactionMode: followUp.interactionMode,
23342334 });
23352335 return;
23362336 }
......@@ -2795,6 +2795,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
27952795 interactionMode: nextInteractionMode,
27962796 });
27972797
2798 // Keep the mode toggle and plan-follow-up banner in sync immediately
2799 // while the same-thread implementation turn is starting.
2800 setComposerDraftInteractionMode(threadIdForSend, nextInteractionMode);
2801
27982802 await api.orchestration.dispatchCommand({
27992803 type: "thread.turn.start",
28002804 commandId: newCommandId(),
......@@ -2840,6 +2844,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
28402844 selectedModel,
28412845 selectedModelOptionsForDispatch,
28422846 selectedProvider,
2847 setComposerDraftInteractionMode,
28432848 setThreadError,
28442849 settings.enableAssistantStreaming,
28452850 ],
apps/web/src/proposedPlan.test.ts+27
......@@ -5,6 +5,7 @@ import {
55 buildPlanImplementationPrompt,
66 buildProposedPlanMarkdownFilename,
77 proposedPlanTitle,
8 resolvePlanFollowUpSubmission,
89} from "./proposedPlan";
910
1011describe("proposedPlanTitle", () => {
......@@ -25,6 +26,32 @@ describe("buildPlanImplementationPrompt", () => {
2526 });
2627});
2728
29describe("resolvePlanFollowUpSubmission", () => {
30 it("switches to default mode when implementing the ready plan without extra text", () => {
31 expect(
32 resolvePlanFollowUpSubmission({
33 draftText: " ",
34 planMarkdown: "## Ship it\n\n- step 1\n",
35 }),
36 ).toEqual({
37 text: "PLEASE IMPLEMENT THIS PLAN:\n## Ship it\n\n- step 1",
38 interactionMode: "default",
39 });
40 });
41
42 it("stays in plan mode when the user adds a follow-up prompt", () => {
43 expect(
44 resolvePlanFollowUpSubmission({
45 draftText: "Refine step 2 first",
46 planMarkdown: "## Ship it\n\n- step 1\n",
47 }),
48 ).toEqual({
49 text: "Refine step 2 first",
50 interactionMode: "plan",
51 });
52 });
53});
54
2855describe("buildPlanImplementationThreadTitle", () => {
2956 it("uses the plan heading when building the implementation thread title", () => {
3057 expect(buildPlanImplementationThreadTitle("# Integrate RPC\n\nBody")).toBe(
apps/web/src/proposedPlan.ts+21
......@@ -16,6 +16,27 @@ export function buildPlanImplementationPrompt(planMarkdown: string): string {
1616 return `PLEASE IMPLEMENT THIS PLAN:\n${planMarkdown.trim()}`;
1717}
1818
19export function resolvePlanFollowUpSubmission(input: {
20 draftText: string;
21 planMarkdown: string;
22}): {
23 text: string;
24 interactionMode: "default" | "plan";
25} {
26 const trimmedDraftText = input.draftText.trim();
27 if (trimmedDraftText.length > 0) {
28 return {
29 text: trimmedDraftText,
30 interactionMode: "plan",
31 };
32 }
33
34 return {
35 text: buildPlanImplementationPrompt(input.planMarkdown),
36 interactionMode: "default",
37 };
38}
39
1940export function buildPlanImplementationThreadTitle(planMarkdown: string): string {
2041 const title = proposedPlanTitle(planMarkdown);
2142 if (!title) {