From ebbb5948ab653eb8b9c150ecead2aa907f2d493f Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 00:56:47 -0700 Subject: [PATCH] FN-8529: add contextual plan review comments Enable operators to submit selected-plan suggestions through the existing Planning Mode revision flow. - Capture text selections, batch suggestions, and retain retryable comment submissions in the Planning Mode UI. - Validate bounded comment payloads and format them for the existing planning session update path. - Document the workflow and add coverage for ordering, retry behavior, and selection boundaries. Files changed: .changeset/fn-8529-planning-comments.md | 7 + docs/dashboard-guide.md | 4 + packages/dashboard/app/api/legacy.ts | 1 + packages/dashboard/app/api/planning.ts | 9 +- .../dashboard/app/components/PlanningModeModal.css | 86 ++++++++++++ .../dashboard/app/components/PlanningModeModal.tsx | 144 ++++++++++++++++++++- .../PlanningModeModal.planning-flow.test.tsx | 39 ++++++ .../PlanningModeModal.ui-interactions.test.tsx | 10 ++ .../__tests__/planning-infinite-interview.test.ts | 62 +++++++++ packages/dashboard/src/planning.ts | 76 ++++++++++- .../src/routes/register-planning-subtask-routes.ts | 27 +++- 11 files changed, 455 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-8529 Fusion-Task-Lineage: c7064d76-effd-4597-a2b4-03f344dd59b9 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8529-planning-comments.md | 7 + docs/dashboard-guide.md | 4 + packages/dashboard/app/api/legacy.ts | 1 + packages/dashboard/app/api/planning.ts | 9 +- .../app/components/PlanningModeModal.css | 86 +++++++++++ .../app/components/PlanningModeModal.tsx | 144 +++++++++++++++++- .../PlanningModeModal.planning-flow.test.tsx | 39 +++++ ...PlanningModeModal.ui-interactions.test.tsx | 10 ++ .../planning-infinite-interview.test.ts | 62 ++++++++ packages/dashboard/src/planning.ts | 76 ++++++++- .../register-planning-subtask-routes.ts | 27 +++- 11 files changed, 455 insertions(+), 10 deletions(-) create mode 100644 .changeset/fn-8529-planning-comments.md diff --git a/.changeset/fn-8529-planning-comments.md b/.changeset/fn-8529-planning-comments.md new file mode 100644 index 0000000000..a78ff23b54 --- /dev/null +++ b/.changeset/fn-8529-planning-comments.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add contextual comments to Planning Mode plan reviews. +category: feature +dev: Batches selected plan quotes and suggestions into the existing plan-update generation. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 1731e427a1..cf027bd054 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2254,3 +2254,7 @@ Productivity duration uses total agent-active time: planning (`cumulativePlannin ### Custom workflow column descriptions Custom workflow authors can add optional explanatory copy beneath each column name in the workflow editor. The description appears on selected, aggregate, and archived workflow board columns. Clearing it removes the custom metadata; columns then continue to use the standard lifecycle description when one exists. + +## Planning Mode contextual comments + +In plan review, select text inside the rendered plan and choose **Add comment to selection**. Enter a suggestion to capture the selected quote and suggestion as a pending contextual comment. You can remove individual comments before choosing **Submit comments**; Fusion sends the ordered batch through the existing Planning Mode revision generation, so the agent revises the quoted areas while preserving unaffected plan content. A successful revised-plan update clears the batch; a failed submission retains it for retry. diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 710ac2ec96..bc5dcb0c02 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -613,6 +613,7 @@ export { export type { PlanningSession, PlanningResponse, + PlanningContextualComment, PlanningStreamEvent, AgentOnboardingSummary, OnboardingMode, diff --git a/packages/dashboard/app/api/planning.ts b/packages/dashboard/app/api/planning.ts index 6f1f23cd90..de97f33d84 100644 --- a/packages/dashboard/app/api/planning.ts +++ b/packages/dashboard/app/api/planning.ts @@ -155,10 +155,15 @@ export function updatePlanningSessionTitle(sessionId: string, title: string, pro }); } -/** Submit a response to the current planning question */ +export interface PlanningContextualComment { + quote: string; + suggestion: string; +} + +/** Submit a response to the current planning question or a bounded contextual-comment batch. */ export function respondToPlanning( sessionId: string, - responses: Record, + responses: Record | { contextualComments: PlanningContextualComment[] }, projectId?: string, ): Promise { return api(withProjectId("/planning/respond", projectId), { diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index dafef36be6..03201e7ed1 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -1761,6 +1761,78 @@ Tablet (769–1024) keeps the same two-pane shell as desktop but previously fell min-width: 0; } +/* +FNXC:PlanningComments 2026-07-23-12:00: +Contextual comments remain transient review controls: render the capture/editor only after a +selection, and render the tray only after a valid comment exists so an empty plan review keeps +its established Refine and Proceed action hierarchy. +*/ +.planning-add-comment { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + margin-top: var(--space-lg); +} + +.planning-comment-editor, +.planning-comment-tray { + display: flex; + flex-direction: column; + gap: var(--space-md); + min-width: 0; +} + +.planning-comment-editor { + margin-top: var(--space-lg); + padding: var(--space-lg); + border: solid var(--btn-border-width) var(--border); + border-radius: var(--radius-lg); + background: var(--card); +} + +.planning-comment-quote, +.planning-comment-tray blockquote, +.planning-comment-tray p { + margin: 0; + overflow-wrap: anywhere; +} + +.planning-comment-quote, +.planning-comment-tray blockquote { + padding-inline-start: var(--space-md); + border-inline-start: solid var(--btn-border-width) var(--todo); + color: var(--text-muted); +} + +.planning-comment-tray { + flex: 1; +} + +.planning-comment-tray ul { + display: flex; + flex-direction: column; + gap: var(--space-sm); + margin: 0; + padding: 0; + list-style: none; +} + +.planning-comment-tray li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-sm); + align-items: start; +} + +.planning-comment-tray li > p { + grid-column: 1; +} + +.planning-comment-tray .btn-icon { + grid-column: 2; + grid-row: 1 / span 2; +} + .planning-summary-actions-right { display: flex; gap: var(--space-sm); @@ -1787,6 +1859,10 @@ Tablet (769–1024) keeps the same two-pane shell as desktop but previously fell min-height: calc(var(--space-2xl) + var(--space-md)); } + .planning-comment-tray { + grid-column: 1 / -1; + } + .planning-refine-menu { right: var(--space-lg); left: var(--space-lg); @@ -1803,6 +1879,16 @@ Tablet embedded Planning keeps the desktop two-pane shell. The question Next act Refine/Proceed actions share one compact bottom baseline; equal footer/button heights, non-wrapping plan actions, and a token-sized bottom inset keep all three controls inline without extra space. */ +@media (max-width: 768px) { + .planning-comment-tray li { + grid-template-columns: minmax(0, 1fr) auto; + } + + .planning-comment-editor { + padding: var(--space-md); + } +} + @media (min-width: 769px) and (max-width: 1024px) { /* FNXC:PlanningModeMobile 2026-07-20-10:30: tablet uses the same compact list destination as phone and short shells, so summary/compose detail cannot remain beside saved session rows. */ .planning-modal-body--show-list { diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 975f5d425a..9231cf3fc5 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -42,6 +42,7 @@ import { type ModelInfo, type ConversationHistoryEntry, type AiSessionSummary, + type PlanningContextualComment, } from "../api"; import { subscribeSse } from "../sse-bus"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; @@ -646,6 +647,27 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat non-whitespace text before submission. */ const [refinementPrompt, setRefinementPrompt] = useState(""); + const [contextualComments, setContextualComments] = useState([]); + const [selectedPlanQuote, setSelectedPlanQuote] = useState(null); + const [commentDraft, setCommentDraft] = useState(""); + const [isCommentEditorOpen, setIsCommentEditorOpen] = useState(false); + const contextualCommentInFlightRef = useRef(false); + const contextualCommentSubmissionRef = useRef(false); + const planDocumentRef = useRef(null); + const commentInputRef = useRef(null); + const addCommentTriggerRef = useRef(null); + + useEffect(() => { + // A batch belongs to one visible session; never carry comments into another plan. + setContextualComments([]); + setSelectedPlanQuote(null); + setCommentDraft(""); + setIsCommentEditorOpen(false); + }, [selectedSessionId]); + + useEffect(() => { + if (isCommentEditorOpen) commentInputRef.current?.focus(); + }, [isCommentEditorOpen]); useEffect(() => { if (isMobile && workspaceQuestion) { @@ -1167,6 +1189,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat */ runningSummaryRef.current = normalizedSummary; setRunningSummary(normalizedSummary); + setSelectedPlanQuote(null); + setIsCommentEditorOpen(false); + if (contextualCommentSubmissionRef.current) { + contextualCommentSubmissionRef.current = false; + setContextualComments([]); + } setView((previous) => previous.type === "question" ? { ...previous, session: { ...previous.session, summary: normalizedSummary } } : previous); @@ -2561,6 +2589,61 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat } }, [connectToPlanningStream, projectId, refinementInstructions, t, view, workspaceQuestion]); + const capturePlanSelection = useCallback(() => { + const selection = window.getSelection(); + const root = planDocumentRef.current; + if (!selection || selection.rangeCount === 0 || !root || !root.contains(selection.anchorNode) || !root.contains(selection.focusNode)) { + setSelectedPlanQuote(null); + return; + } + const quote = selection.toString().replace(/\s+/g, " ").trim(); + setSelectedPlanQuote(quote || null); + }, []); + + const handleAddContextualComment = useCallback(() => { + const quote = selectedPlanQuote; + const suggestion = commentDraft.trim(); + if (!quote || !suggestion) return; + setContextualComments((comments) => [...comments, { quote, suggestion }]); + setCommentDraft(""); + setSelectedPlanQuote(null); + setIsCommentEditorOpen(false); + window.getSelection()?.removeAllRanges(); + addCommentTriggerRef.current?.focus(); + }, [commentDraft, selectedPlanQuote]); + + const handleSubmitContextualComments = useCallback(async () => { + const sessionId = currentSessionIdRef.current; + const summary = runningSummaryRef.current; + if (!sessionId || !summary || contextualComments.length === 0 || contextualCommentInFlightRef.current) return; + /* FNXC:PlanningComments 2026-07-23-12:00: A synchronous guard makes one batch one established plan-update turn despite rapid pointer or keyboard activation. */ + contextualCommentInFlightRef.current = true; + contextualCommentSubmissionRef.current = true; + setError(null); + setGenerationActivity("plan_update"); + setGenerationStartTime(Date.now()); + setView({ type: "loading" }); + if (!streamConnectionRef.current?.isConnected()) connectToPlanningStream(sessionId); + try { + const response = await respondToPlanning(sessionId, { contextualComments }, projectId); + const nextSummary = "type" in response ? null : response.summary; + if (nextSummary) { + const normalized = normalizePlanningSummary(nextSummary); + runningSummaryRef.current = normalized; + setRunningSummary(normalized); + contextualCommentSubmissionRef.current = false; + setContextualComments([]); + setView({ type: "plan_review", session: { sessionId, currentQuestion: null, summary: normalized }, summary: normalized }); + } + } catch (err) { + contextualCommentSubmissionRef.current = false; + setError(getErrorMessage(err) || t("planning.failedSubmitResponse", "Failed to submit comments")); + setView({ type: "plan_review", session: { sessionId, currentQuestion: null, summary }, summary }); + } finally { + contextualCommentInFlightRef.current = false; + } + }, [connectToPlanningStream, contextualComments, projectId, t]); + const handleProceedWithPlan = useCallback(async () => { const sessionId = currentSessionIdRef.current; const summary = runningSummaryRef.current; @@ -2793,14 +2876,65 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
- + {/* + FNXC:PlanningComments 2026-07-23-13:00: + Contextual quotes must originate exclusively in rendered plan Markdown. The editor and + action controls remain outside this selection root so typing or selecting a suggestion + cannot replace the captured plan quote. + */} +
+ +
+ {selectedPlanQuote && !isCommentEditorOpen && ( + + )} + {isCommentEditorOpen && selectedPlanQuote && ( +
+

{selectedPlanQuote}

+