diff --git a/.changeset/planning-thinking-all-steps.md b/.changeset/planning-thinking-all-steps.md new file mode 100644 index 0000000000..da5fa72332 --- /dev/null +++ b/.changeset/planning-thinking-all-steps.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Every Planning Mode generation step now streams AI thinking/output, not just the first turn. +category: fix +dev: The planning workspace loader (follow-up turns — next question, refine, contextual comments, question regeneration) reuses the initial loading view's thinking container/toggle and mirrors the generation-activity label. diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index cf0a36499b..8ea9be9d94 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -3975,12 +3975,38 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat {view.type === "loading" && (
- {t("planning.generatingPlan", "Generating plan…")} + + {generationActivity === "question" + ? t("planning.generatingQuestion", "Generating next question…") + : t("planning.generatingPlan", "Generating plan…")} + {generationStartTime && {t("planning.thinkingElapsed", "Thinking… ({{seconds}}s)", { seconds: elapsedSeconds })}} + {/* + FNXC:PlanningThinkingVisibility 2026-07-23-22:45: + Every Planning Mode generation step must stream the model's thinking/output to + the operator, not only the first (pre-summary) turn. This workspace loader + covers all follow-up turns — next question, refine, contextual comments, and + question regeneration — and previously showed only a spinner with elapsed + time. Reuse the same thinking container + toggle as the initial loading view. + */} +
+ + {showThinking && streamingOutput && ( +
+
{streamingOutput}
+
+ )} +
)} diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx index 95dd7315a3..2ec81efe4a 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -842,6 +842,30 @@ describe("PlanningModeModal sequential flow", () => { expect(await screen.findByText("Thinking… (7s)")).toBeInTheDocument(); dateNow.mockRestore(); }); + /* + FNXC:PlanningThinkingVisibility 2026-07-23-22:45: + Every generation step must stream thinking/output to the operator. Follow-up turns render + the workspace loader (summary present), which previously showed only a spinner + elapsed + time; this pins the streamed thinking pane there too. + */ + it("streams thinking in the workspace loader during follow-up generations", async () => { + mockFetchAiSession.mockResolvedValue({ + ...base, + status: "generating", + currentQuestion: null, + result: JSON.stringify(summaryWithRefinements), + inputPayload: JSON.stringify({ generationPurpose: "plan_update", generationStartedAt: new Date().toISOString() }), + }); + renderSession(); + + await waitFor(() => expect(mockConnectPlanningStream).toHaveBeenCalledWith("session-1", "project-1", expect.any(Object))); + const handlers = mockConnectPlanningStream.mock.calls[0]?.[2]; + act(() => handlers?.onThinking?.("Weighing the tradeoffs between approaches…")); + + expect(await screen.findByText("Weighing the tradeoffs between approaches…")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Hide thinking" })); + expect(screen.queryByText("Weighing the tradeoffs between approaches…")).toBeNull(); + }); it("returns to the prior question without an error when generation is stopped", async () => { const priorQuestion = { id: "q-prior", type: "text", question: "What should change?" }; mockFetchAiSession.mockResolvedValue({