| 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 | 92 | buildPlanImplementationPrompt, |
| 93 | 93 | buildProposedPlanMarkdownFilename, |
| 94 | 94 | proposedPlanTitle, |
| 95 | resolvePlanFollowUpSubmission, | |
| 95 | 96 | } from "../proposedPlan"; |
| 96 | 97 | import { truncateTitle } from "../truncateTitle"; |
| 97 | 98 | import { |
| ... | ... | @@ -2318,19 +2319,18 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2318 | 2319 | } |
| 2319 | 2320 | const trimmed = prompt.trim(); |
| 2320 | 2321 | 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 | }); | |
| 2326 | 2326 | promptRef.current = ""; |
| 2327 | 2327 | clearComposerDraftContent(activeThread.id); |
| 2328 | 2328 | setComposerHighlightedItemId(null); |
| 2329 | 2329 | setComposerCursor(0); |
| 2330 | 2330 | setComposerTrigger(null); |
| 2331 | 2331 | await onSubmitPlanFollowUp({ |
| 2332 | text: followUpText, | |
| 2333 | interactionMode: nextInteractionMode, | |
| 2332 | text: followUp.text, | |
| 2333 | interactionMode: followUp.interactionMode, | |
| 2334 | 2334 | }); |
| 2335 | 2335 | return; |
| 2336 | 2336 | } |
| ... | ... | @@ -2795,6 +2795,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2795 | 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 | 2802 | await api.orchestration.dispatchCommand({ |
| 2799 | 2803 | type: "thread.turn.start", |
| 2800 | 2804 | commandId: newCommandId(), |
| ... | ... | @@ -2840,6 +2844,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2840 | 2844 | selectedModel, |
| 2841 | 2845 | selectedModelOptionsForDispatch, |
| 2842 | 2846 | selectedProvider, |
| 2847 | setComposerDraftInteractionMode, | |
| 2843 | 2848 | setThreadError, |
| 2844 | 2849 | settings.enableAssistantStreaming, |
| 2845 | 2850 | ], |
apps/web/src/proposedPlan.test.ts+27| ... | ... | @@ -5,6 +5,7 @@ import { |
| 5 | 5 | buildPlanImplementationPrompt, |
| 6 | 6 | buildProposedPlanMarkdownFilename, |
| 7 | 7 | proposedPlanTitle, |
| 8 | resolvePlanFollowUpSubmission, | |
| 8 | 9 | } from "./proposedPlan"; |
| 9 | 10 | |
| 10 | 11 | describe("proposedPlanTitle", () => { |
| ... | ... | @@ -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 | 55 | describe("buildPlanImplementationThreadTitle", () => { |
| 29 | 56 | it("uses the plan heading when building the implementation thread title", () => { |
| 30 | 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 | 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 | 40 | export function buildPlanImplementationThreadTitle(planMarkdown: string): string { |
| 20 | 41 | const title = proposedPlanTitle(planMarkdown); |
| 21 | 42 | if (!title) { |