| author | |
| committer | |
| log | 010ba70561cff75af0188213e87f8c418b0c3b81 |
| tree | d80e0ae417b2bda10b884561b72deb68b4de3256 |
| parent | 6ee057e25bd4f4dad85445571ffb0cc1ce3c6980 |
- 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 behavior3 files changed, 60 insertions(+), 7 deletions(-)
apps/web/src/components/ChatView.tsx+12-7| ... | @@ -92,6 +92,7 @@ import { | ... | @@ -92,6 +92,7 @@ import { |
| 92 | buildPlanImplementationPrompt, | 92 | buildPlanImplementationPrompt, |
| 93 | buildProposedPlanMarkdownFilename, | 93 | buildProposedPlanMarkdownFilename, |
| 94 | proposedPlanTitle, | 94 | proposedPlanTitle, |
| 95 | resolvePlanFollowUpSubmission, | ||
| 95 | } from "../proposedPlan"; | 96 | } from "../proposedPlan"; |
| 96 | import { truncateTitle } from "../truncateTitle"; | 97 | import { truncateTitle } from "../truncateTitle"; |
| 97 | import { | 98 | import { |
| ... | @@ -2318,19 +2319,18 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2318,19 +2319,18 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2318 | } | 2319 | } |
| 2319 | const trimmed = prompt.trim(); | 2320 | const trimmed = prompt.trim(); |
| 2320 | if (showPlanFollowUpPrompt && activeProposedPlan) { | 2321 | if (showPlanFollowUpPrompt && activeProposedPlan) { |
| 2321 | const followUpText = | 2322 | const followUp = resolvePlanFollowUpSubmission({ |
| 2322 | trimmed.length > 0 | 2323 | draftText: trimmed, |
| 2323 | ? trimmed | 2324 | planMarkdown: activeProposedPlan.planMarkdown, |
| 2324 | : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown); | 2325 | }); |
| 2325 | const nextInteractionMode = trimmed.length > 0 ? "plan" : "default"; | ||
| 2326 | promptRef.current = ""; | 2326 | promptRef.current = ""; |
| 2327 | clearComposerDraftContent(activeThread.id); | 2327 | clearComposerDraftContent(activeThread.id); |
| 2328 | setComposerHighlightedItemId(null); | 2328 | setComposerHighlightedItemId(null); |
| 2329 | setComposerCursor(0); | 2329 | setComposerCursor(0); |
| 2330 | setComposerTrigger(null); | 2330 | setComposerTrigger(null); |
| 2331 | await onSubmitPlanFollowUp({ | 2331 | await onSubmitPlanFollowUp({ |
| 2332 | text: followUpText, | 2332 | text: followUp.text, |
| 2333 | interactionMode: nextInteractionMode, | 2333 | interactionMode: followUp.interactionMode, |
| 2334 | }); | 2334 | }); |
| 2335 | return; | 2335 | return; |
| 2336 | } | 2336 | } |
| ... | @@ -2795,6 +2795,10 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2795,6 +2795,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2795 | interactionMode: nextInteractionMode, | 2795 | interactionMode: nextInteractionMode, |
| 2796 | }); | 2796 | }); |
| 2797 | 2797 | ||
| 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 | |||
| 2798 | await api.orchestration.dispatchCommand({ | 2802 | await api.orchestration.dispatchCommand({ |
| 2799 | type: "thread.turn.start", | 2803 | type: "thread.turn.start", |
| 2800 | commandId: newCommandId(), | 2804 | commandId: newCommandId(), |
| ... | @@ -2840,6 +2844,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2840,6 +2844,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2840 | selectedModel, | 2844 | selectedModel, |
| 2841 | selectedModelOptionsForDispatch, | 2845 | selectedModelOptionsForDispatch, |
| 2842 | selectedProvider, | 2846 | selectedProvider, |
| 2847 | setComposerDraftInteractionMode, | ||
| 2843 | setThreadError, | 2848 | setThreadError, |
| 2844 | settings.enableAssistantStreaming, | 2849 | settings.enableAssistantStreaming, |
| 2845 | ], | 2850 | ], |
apps/web/src/proposedPlan.test.ts+27| ... | @@ -5,6 +5,7 @@ import { | ... | @@ -5,6 +5,7 @@ import { |
| 5 | buildPlanImplementationPrompt, | 5 | buildPlanImplementationPrompt, |
| 6 | buildProposedPlanMarkdownFilename, | 6 | buildProposedPlanMarkdownFilename, |
| 7 | proposedPlanTitle, | 7 | proposedPlanTitle, |
| 8 | resolvePlanFollowUpSubmission, | ||
| 8 | } from "./proposedPlan"; | 9 | } from "./proposedPlan"; |
| 9 | 10 | ||
| 10 | describe("proposedPlanTitle", () => { | 11 | describe("proposedPlanTitle", () => { |
| ... | @@ -25,6 +26,32 @@ describe("buildPlanImplementationPrompt", () => { | ... | @@ -25,6 +26,32 @@ describe("buildPlanImplementationPrompt", () => { |
| 25 | }); | 26 | }); |
| 26 | }); | 27 | }); |
| 27 | 28 | ||
| 29 | describe("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 | |||
| 28 | describe("buildPlanImplementationThreadTitle", () => { | 55 | describe("buildPlanImplementationThreadTitle", () => { |
| 29 | it("uses the plan heading when building the implementation thread title", () => { | 56 | it("uses the plan heading when building the implementation thread title", () => { |
| 30 | expect(buildPlanImplementationThreadTitle("# Integrate RPC\n\nBody")).toBe( | 57 | expect(buildPlanImplementationThreadTitle("# Integrate RPC\n\nBody")).toBe( |
apps/web/src/proposedPlan.ts+21| ... | @@ -16,6 +16,27 @@ export function buildPlanImplementationPrompt(planMarkdown: string): string { | ... | @@ -16,6 +16,27 @@ export function buildPlanImplementationPrompt(planMarkdown: string): string { |
| 16 | return `PLEASE IMPLEMENT THIS PLAN:\n${planMarkdown.trim()}`; | 16 | return `PLEASE IMPLEMENT THIS PLAN:\n${planMarkdown.trim()}`; |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | export 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 | |||
| 19 | export function buildPlanImplementationThreadTitle(planMarkdown: string): string { | 40 | export function buildPlanImplementationThreadTitle(planMarkdown: string): string { |
| 20 | const title = proposedPlanTitle(planMarkdown); | 41 | const title = proposedPlanTitle(planMarkdown); |
| 21 | if (!title) { | 42 | if (!title) { |