fix(dashboard): stream AI thinking on every Planning Mode generation step
The workspace loader that covers all follow-up turns (next question, refine, contextual comments, question regeneration) showed only a spinner and elapsed time — streamed thinking/output was visible only on the first pre-summary turn. Reuse the initial loading view's thinking container and toggle there, and mirror the generation-activity label instead of a hardcoded "Generating plan…". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/planning-thinking-all-steps.md
Normal file
7
.changeset/planning-thinking-all-steps.md
Normal file
@@ -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.
|
||||
@@ -3975,12 +3975,38 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
{view.type === "loading" && (
|
||||
<div className="planning-workspace-loader" role="status" aria-live="polite">
|
||||
<Loader2 size={40} className="spin" />
|
||||
<strong>{t("planning.generatingPlan", "Generating plan…")}</strong>
|
||||
<strong>
|
||||
{generationActivity === "question"
|
||||
? t("planning.generatingQuestion", "Generating next question…")
|
||||
: t("planning.generatingPlan", "Generating plan…")}
|
||||
</strong>
|
||||
{generationStartTime && <span>{t("planning.thinkingElapsed", "Thinking… ({{seconds}}s)", { seconds: elapsedSeconds })}</span>}
|
||||
<button className="btn planning-stop-btn" type="button" onClick={() => void handleStopGeneration()}>
|
||||
<StopCircle size={14} />
|
||||
<span className="icon-ml-6">{t("planning.stop", "Stop")}</span>
|
||||
</button>
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<div className="planning-thinking-container">
|
||||
<button
|
||||
className="planning-thinking-toggle"
|
||||
onClick={() => setShowThinking(!showThinking)}
|
||||
type="button"
|
||||
>
|
||||
{showThinking ? t("planning.hideThinking", "Hide thinking") : t("planning.showThinking", "Show thinking")}
|
||||
</button>
|
||||
{showThinking && streamingOutput && (
|
||||
<div className="planning-thinking-output" ref={thinkingOutputRef}>
|
||||
<pre>{streamingOutput}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user