From bef911adc70fedcdda2572458eff678405c0a511 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 16 Jun 2026 06:37:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20auto-collapse=20workf?= =?UTF-8?q?low=20lookup=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - Auto-collapse completed workflow list, workflow read, and workflow action list tool cards. - Preserve explicit user expansion/collapse intent and update tests/stories for the collapsed default. Validation: - bun test src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx src/browser/features/Tools/WorkflowActionListToolCall.test.tsx - make typecheck - make lint - make fmt-check - Storybook dogfood with screenshots and video capture --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `205253{MUX_COSTS_USD:-unknown}`_ --- .../Tools/WorkflowActionListToolCall.test.tsx | 14 +++++++++- .../Tools/WorkflowActionListToolCall.tsx | 10 +++---- .../WorkflowDefinitionToolCall.stories.tsx | 9 +++++++ .../Tools/WorkflowDefinitionToolCall.test.tsx | 20 ++++++++++++-- .../Tools/WorkflowDefinitionToolCall.tsx | 26 +++++++++++++++++-- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx b/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx index a5cdb2baa1..dc4b328826 100644 --- a/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx @@ -16,6 +16,12 @@ function renderWithTooltip(ui: React.ReactElement) { ); } +function clickToolHeader(view: ReturnType, label: string) { + const header = view.getByText(label).closest('[data-scroll-intent="ignore"]'); + expect(header).toBeTruthy(); + fireEvent.click(header as HTMLElement); +} + describe("WorkflowActionListToolCall", () => { let originalWindow: typeof globalThis.window; let originalDocument: typeof globalThis.document; @@ -37,7 +43,7 @@ describe("WorkflowActionListToolCall", () => { globalThis.localStorage = originalLocalStorage; }); - test("renders action rows with effect and blocked badges", () => { + test("renders action rows with effect and blocked badges after manual expansion", () => { const view = renderWithTooltip( { ); expect(view.getByText("2 actions")).toBeTruthy(); + expect(view.queryByText("git.changedFiles")).toBeNull(); + expect(view.queryByText("Project is not trusted")).toBeNull(); + + clickToolHeader(view, "2 actions"); + expect(view.getByText("git.changedFiles")).toBeTruthy(); expect(view.getByText("read")).toBeTruthy(); expect(view.getByText("blocked")).toBeTruthy(); @@ -104,6 +115,7 @@ describe("WorkflowActionListToolCall", () => { expect(view.queryByText("Input schema")).toBeNull(); + clickToolHeader(view, "1 action"); fireEvent.click(view.getByRole("button", { expanded: false })); expect(view.getByText("/__mux_builtin_workflow_actions__/git/changedFiles.js")).toBeTruthy(); diff --git a/src/browser/features/Tools/WorkflowActionListToolCall.tsx b/src/browser/features/Tools/WorkflowActionListToolCall.tsx index eff0056c00..b714e15ad1 100644 --- a/src/browser/features/Tools/WorkflowActionListToolCall.tsx +++ b/src/browser/features/Tools/WorkflowActionListToolCall.tsx @@ -18,18 +18,14 @@ import { ToolIcon, ToolName, } from "./Shared/ToolPrimitives"; -import { - getStatusDisplay, - isToolErrorResult, - type ToolStatus, - useToolExpansion, -} from "./Shared/toolUtils"; +import { getStatusDisplay, isToolErrorResult, type ToolStatus } from "./Shared/toolUtils"; import { WorkflowBadge, WorkflowJsonBlock, WorkflowKindBadge, WorkflowLoadingState, WorkflowSection, + useAutoCollapsingWorkflowLookup, } from "./WorkflowDefinitionToolCall"; interface WorkflowActionListToolCallProps { @@ -167,7 +163,7 @@ export const WorkflowActionListToolCall: React.FC { - const { expanded, toggleExpanded } = useToolExpansion(true); + const { expanded, toggleExpanded } = useAutoCollapsingWorkflowLookup(status); const errorResult = isToolErrorResult(result) ? result : null; const successResult = isWorkflowActionListSuccessResult(result) ? result : null; const actions = successResult?.actions ?? []; diff --git a/src/browser/features/Tools/WorkflowDefinitionToolCall.stories.tsx b/src/browser/features/Tools/WorkflowDefinitionToolCall.stories.tsx index 9b4eb2e6e4..38ef023560 100644 --- a/src/browser/features/Tools/WorkflowDefinitionToolCall.stories.tsx +++ b/src/browser/features/Tools/WorkflowDefinitionToolCall.stories.tsx @@ -28,6 +28,13 @@ function NarrowContainerDecorator(Story: ComponentType) { ); } +function expandToolCard(canvasElement: HTMLElement, summaryText: string) { + const canvas = within(canvasElement); + const header = canvas.getByText(summaryText).closest('[data-scroll-intent="ignore"]'); + if (header == null) throw new Error(`Could not find tool header for "${summaryText}"`); + (header as HTMLElement).click(); +} + /** * Assert the narrow list layout engaged: the description must wrap onto its own * grid row below the name instead of sharing the single-line wide layout. @@ -165,6 +172,7 @@ export const WorkflowActionListNarrow: Story = { chromatic: { modes: { "dark-mobile": { theme: "dark", viewport: "mobile1", hasTouch: true } } }, }, play: async ({ canvasElement }) => { + expandToolCard(canvasElement, "4 actions"); await expectDescriptionBelowName( canvasElement, "git.changedFiles", @@ -218,6 +226,7 @@ export const WorkflowListNarrow: Story = { chromatic: { modes: { "dark-mobile": { theme: "dark", viewport: "mobile1", hasTouch: true } } }, }, play: async ({ canvasElement }) => { + expandToolCard(canvasElement, "3 definitions"); await expectDescriptionBelowName(canvasElement, "deep-research", /Coordinate staged research/); }, }; diff --git a/src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx b/src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx index 0b0f3e442d..3a5ea3f21a 100644 --- a/src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { GlobalWindow } from "happy-dom"; -import { cleanup, render } from "@testing-library/react"; +import { cleanup, fireEvent, render } from "@testing-library/react"; import type React from "react"; @@ -27,6 +27,12 @@ function expectWorkflowHeaderBadge(view: ReturnType, label: strin expect(headerText.indexOf("Workflow")).toBeLessThan(headerText.indexOf(label)); } +function clickToolHeader(view: ReturnType, label: string) { + const header = view.getByText(label).closest('[data-scroll-intent="ignore"]'); + expect(header).toBeTruthy(); + fireEvent.click(header as HTMLElement); +} + describe("WorkflowDefinitionToolCall", () => { let originalWindow: typeof globalThis.window; let originalDocument: typeof globalThis.document; @@ -66,11 +72,16 @@ describe("WorkflowDefinitionToolCall", () => { ); expectWorkflowHeaderBadge(view, "deep-research"); + expect(view.queryByText("Deep research")).toBeNull(); + expect(view.container.textContent).not.toContain("return agent"); + + clickToolHeader(view, "deep-research"); + expect(view.getByText("Deep research")).toBeTruthy(); expect(view.container.textContent).toContain("return agent"); }); - test("renders workflow_list as definition cards", () => { + test("renders workflow_list as definition cards after manual expansion", () => { const view = renderWithTooltip( { expectWorkflowHeaderBadge(view, "list"); expect(view.getByText("2 definitions")).toBeTruthy(); + expect(view.queryByText("blocked")).toBeNull(); + expect(view.queryByText("Project is not trusted")).toBeNull(); + + clickToolHeader(view, "2 definitions"); + expect(view.queryByText("executable")).toBeNull(); expect(view.getByText("blocked")).toBeTruthy(); expect(view.getByText("Project is not trusted")).toBeTruthy(); diff --git a/src/browser/features/Tools/WorkflowDefinitionToolCall.tsx b/src/browser/features/Tools/WorkflowDefinitionToolCall.tsx index 2dc46598f5..380662cf95 100644 --- a/src/browser/features/Tools/WorkflowDefinitionToolCall.tsx +++ b/src/browser/features/Tools/WorkflowDefinitionToolCall.tsx @@ -222,6 +222,28 @@ function isWorkflowReadSuccessResult( return value != null && !isToolErrorResult(value); } +const AUTO_COLLAPSE_WORKFLOW_LOOKUP_STATUSES = new Set(["completed"]); + +export function useAutoCollapsingWorkflowLookup(status: ToolStatus) { + const { expanded, setExpanded, toggleExpanded } = useToolExpansion(true); + const userToggledExpansionRef = React.useRef(false); + + const toggleAutoCollapsingExpanded = () => { + userToggledExpansionRef.current = true; + toggleExpanded(); + }; + + React.useLayoutEffect(() => { + // Completed workflow lookup/action payloads can be bulky; collapse them once + // the result arrives for transcript scanability, but keep explicit user intent. + if (AUTO_COLLAPSE_WORKFLOW_LOOKUP_STATUSES.has(status) && !userToggledExpansionRef.current) { + setExpanded(false); + } + }, [status, setExpanded]); + + return { expanded, toggleExpanded: toggleAutoCollapsingExpanded }; +} + export function WorkflowLoadingState() { return (
@@ -235,7 +257,7 @@ export const WorkflowListToolCall: React.FC = ({ result, status = "pending", }) => { - const { expanded, toggleExpanded } = useToolExpansion(true); + const { expanded, toggleExpanded } = useAutoCollapsingWorkflowLookup(status); const errorResult = isToolErrorResult(result) ? result : null; const successResult = isWorkflowListSuccessResult(result) ? result : null; const workflows = successResult?.workflows ?? []; @@ -276,7 +298,7 @@ export const WorkflowReadToolCall: React.FC = ({ result, status = "pending", }) => { - const { expanded, toggleExpanded } = useToolExpansion(true); + const { expanded, toggleExpanded } = useAutoCollapsingWorkflowLookup(status); const errorResult = isToolErrorResult(result) ? result : null; const successResult = isWorkflowReadSuccessResult(result) ? result : null; From e593119a1b3a80027efe173c8dc10160396edbfa Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 16 Jun 2026 06:57:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20avoid=20sticky=20pref?= =?UTF-8?q?erence=20writes=20on=20auto-collapse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - Move auto-collapse layering into shared tool expansion utilities. - Keep automatic workflow card collapse local to each card instead of persisting it as user sticky preference. - Add regression coverage for workflow lookup cards and workflow run cards. Validation: - bun test src/browser/features/Tools/WorkflowDefinitionToolCall.test.tsx src/browser/features/Tools/WorkflowActionListToolCall.test.tsx src/browser/features/Tools/WorkflowRunToolCall.test.tsx - make fmt-check && make static-check Simplify workflow: - Applied actionable finding from simplify workflow run wfr_9ab2221bb2f4656b. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `365675{MUX_COSTS_USD:-unknown}`_ --- .../features/Tools/Shared/toolUtils.tsx | 65 +++++++ .../Tools/WorkflowActionListToolCall.test.tsx | 58 +++++++ .../Tools/WorkflowDefinitionToolCall.test.tsx | 53 ++++++ .../Tools/WorkflowDefinitionToolCall.tsx | 26 +-- .../Tools/WorkflowRunToolCall.test.tsx | 164 ++++++++++++++++++ .../features/Tools/WorkflowRunToolCall.tsx | 49 ++---- 6 files changed, 366 insertions(+), 49 deletions(-) diff --git a/src/browser/features/Tools/Shared/toolUtils.tsx b/src/browser/features/Tools/Shared/toolUtils.tsx index 1b5152d8b7..96d7b59f35 100644 --- a/src/browser/features/Tools/Shared/toolUtils.tsx +++ b/src/browser/features/Tools/Shared/toolUtils.tsx @@ -32,6 +32,71 @@ export function useToolExpansion(initialExpanded = false, options?: UseStickyExp return useStickyExpand("tools", initialExpanded, options); } +interface AutoCollapsingToolExpansionOptions { + autoCollapsed: boolean; + resetKey: string | undefined; +} + +/** + * Tool expansion with a non-persisted presentation-only auto-collapse layer. + * Header toggles still go through useToolExpansion and are the only path that + * updates the sticky per-tool preference. + */ +export function useAutoCollapsingToolExpansion( + initialExpanded: boolean, + options: AutoCollapsingToolExpansionOptions +) { + const { expanded: stickyExpanded, setExpanded: setStickyExpanded } = + useToolExpansion(initialExpanded); + const [userInteraction, setUserInteraction] = React.useState<{ + key: string | undefined; + interacted: boolean; + }>(() => ({ key: options.resetKey, interacted: false })); + const [localExpanded, setLocalExpandedState] = React.useState<{ + key: string | undefined; + expanded: boolean; + } | null>(null); + const localExpandedValue = + localExpanded != null && localExpanded.key === options.resetKey ? localExpanded.expanded : null; + const userInteracted = + localExpandedValue != null || + (userInteraction.interacted && userInteraction.key === options.resetKey); + const expanded = + options.autoCollapsed && !userInteracted ? false : (localExpandedValue ?? stickyExpanded); + + const expandedRef = React.useRef(expanded); + expandedRef.current = expanded; + const resetKeyRef = React.useRef(options.resetKey); + resetKeyRef.current = options.resetKey; + + // These callbacks intentionally keep stable identities: WorkflowRunToolCall registers + // command-palette actions from an effect and relies on the expansion setter not changing + // unless the underlying sticky setter changes. + const markInteracted = React.useCallback((): void => { + setUserInteraction({ key: resetKeyRef.current, interacted: true }); + }, []); + const setExpanded = React.useCallback( + (next: boolean): void => { + markInteracted(); + setLocalExpandedState(null); + setStickyExpanded(next); + }, + [markInteracted, setStickyExpanded] + ); + const setLocalExpanded = React.useCallback( + (next: boolean): void => { + markInteracted(); + setLocalExpandedState({ key: resetKeyRef.current, expanded: next }); + }, + [markInteracted] + ); + const toggleExpanded = React.useCallback(() => { + setExpanded(!expandedRef.current); + }, [setExpanded]); + + return { expanded, setExpanded, setLocalExpanded, toggleExpanded, markInteracted }; +} + /** * Get display element for tool status */ diff --git a/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx b/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx index dc4b328826..be5f03430a 100644 --- a/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowActionListToolCall.test.tsx @@ -6,8 +6,13 @@ import type React from "react"; import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; import { ThemeProvider } from "@/browser/contexts/ThemeContext"; +import { MessageListProvider } from "@/browser/features/Messages/MessageListContext"; +import { ToolNameProvider } from "@/browser/features/Messages/ToolNameContext"; +import { getAutoExpandPrefsKey } from "@/common/constants/storage"; import { WorkflowActionListToolCall } from "./WorkflowActionListToolCall"; +const TEST_WORKSPACE_ID = "workflow-action-list-tool-test"; + function renderWithTooltip(ui: React.ReactElement) { return render( @@ -16,6 +21,22 @@ function renderWithTooltip(ui: React.ReactElement) { ); } +function renderWithStickyToolProviders(ui: React.ReactElement) { + return render( + + + + {ui} + + + + ); +} + +function getStoredPrefs(): string | null { + return globalThis.localStorage.getItem(getAutoExpandPrefsKey(TEST_WORKSPACE_ID)); +} + function clickToolHeader(view: ReturnType, label: string) { const header = view.getByText(label).closest('[data-scroll-intent="ignore"]'); expect(header).toBeTruthy(); @@ -43,6 +64,43 @@ describe("WorkflowActionListToolCall", () => { globalThis.localStorage = originalLocalStorage; }); + test("auto-collapses completed action lists without mutating sticky preferences", () => { + const completedView = renderWithStickyToolProviders( + + ); + + expect(completedView.getByText("1 action")).toBeTruthy(); + expect(completedView.queryByText("git.changedFiles")).toBeNull(); + expect(getStoredPrefs()).toBeNull(); + completedView.unmount(); + + const executingView = renderWithStickyToolProviders( + + ); + + expect(executingView.container.textContent).toContain("Waiting for workflow result"); + expect(getStoredPrefs()).toBeNull(); + }); + test("renders action rows with effect and blocked badges after manual expansion", () => { const view = renderWithTooltip( @@ -21,6 +26,22 @@ function renderWithTooltip(ui: React.ReactElement) { ); } +function renderWithStickyToolProviders(ui: React.ReactElement, toolName: string) { + return render( + + + + {ui} + + + + ); +} + +function getStoredPrefs(): string | null { + return globalThis.localStorage.getItem(getAutoExpandPrefsKey(TEST_WORKSPACE_ID)); +} + function expectWorkflowHeaderBadge(view: ReturnType, label: string) { const workflowBadge = view.getByText("Workflow"); const headerText = workflowBadge.closest('[data-scroll-intent="ignore"]')?.textContent ?? ""; @@ -54,6 +75,38 @@ describe("WorkflowDefinitionToolCall", () => { globalThis.localStorage = originalLocalStorage; }); + test("auto-collapses completed workflow_read without mutating sticky preferences", () => { + const completedView = renderWithStickyToolProviders( + , + "workflow_read" + ); + + expect(completedView.queryByText("Deep research")).toBeNull(); + expect(completedView.container.textContent).not.toContain("return agent"); + expect(getStoredPrefs()).toBeNull(); + completedView.unmount(); + + const executingView = renderWithStickyToolProviders( + , + "workflow_read" + ); + + expect(executingView.container.textContent).toContain("Waiting for workflow result"); + expect(getStoredPrefs()).toBeNull(); + }); + test("renders workflow_read metadata and highlighted source", () => { const view = renderWithTooltip( (["completed"]); export function useAutoCollapsingWorkflowLookup(status: ToolStatus) { - const { expanded, setExpanded, toggleExpanded } = useToolExpansion(true); - const userToggledExpansionRef = React.useRef(false); - - const toggleAutoCollapsingExpanded = () => { - userToggledExpansionRef.current = true; - toggleExpanded(); - }; - - React.useLayoutEffect(() => { - // Completed workflow lookup/action payloads can be bulky; collapse them once - // the result arrives for transcript scanability, but keep explicit user intent. - if (AUTO_COLLAPSE_WORKFLOW_LOOKUP_STATUSES.has(status) && !userToggledExpansionRef.current) { - setExpanded(false); - } - }, [status, setExpanded]); - - return { expanded, toggleExpanded: toggleAutoCollapsingExpanded }; + // Completed workflow lookup/action payloads can be bulky, so collapse them for + // transcript scanability without writing that automatic presentation choice to + // the user's sticky expansion preference. Header clicks still persist intent. + return useAutoCollapsingToolExpansion(true, { + autoCollapsed: AUTO_COLLAPSE_WORKFLOW_LOOKUP_STATUSES.has(status), + resetKey: undefined, + }); } export function WorkflowLoadingState() { diff --git a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx index d56b4799df..c3a41c644a 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx @@ -23,6 +23,9 @@ import { } from "@/browser/contexts/CommandRegistryContext"; import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; import { ThemeProvider } from "@/browser/contexts/ThemeContext"; +import { MessageListProvider } from "@/browser/features/Messages/MessageListContext"; +import { ToolNameProvider } from "@/browser/features/Messages/ToolNameContext"; +import { getAutoExpandPrefsKey } from "@/common/constants/storage"; import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore"; function createWorkflowTaskWorkspaceMetadata(workspaceId: string): FrontendWorkspaceMetadata { return { @@ -117,6 +120,77 @@ function APIHarness(props: { client: unknown; children: ReactNode }) { ); } +const TEST_WORKSPACE_ID = "workflow-run-tool-test"; + +function withStickyToolProviders(ui: ReactElement, toolName = "workflow_run") { + return ( + + + + {ui} + + + + ); +} + +function renderWithStickyToolProviders(ui: ReactElement, toolName = "workflow_run") { + return render(withStickyToolProviders(ui, toolName)); +} + +function getStoredPrefs(): string | null { + return globalThis.localStorage.getItem(getAutoExpandPrefsKey(TEST_WORKSPACE_ID)); +} + +function createWorkflowRunForExpansionTest(input: { + id: string; + status: "running" | "completed"; + reportMarkdown?: string; +}) { + return { + id: input.id, + workspaceId: TEST_WORKSPACE_ID, + definition: { + name: "deep-research", + description: "Deep research", + scope: "built-in" as const, + executable: true, + }, + definitionSource: "export default function workflow() { return null; }", + definitionHash: "sha256:test", + args: { topic: "workflow cards" }, + status: input.status, + createdAt: "2026-05-29T00:00:00.000Z", + updatedAt: + input.status === "completed" ? "2026-05-29T00:00:02.000Z" : "2026-05-29T00:00:01.000Z", + events: + input.status === "completed" + ? [ + { + sequence: 1, + type: "result" as const, + at: "2026-05-29T00:00:02.000Z", + result: { reportMarkdown: input.reportMarkdown ?? `${input.id} result` }, + }, + { + sequence: 2, + type: "status" as const, + at: "2026-05-29T00:00:02.000Z", + status: "completed" as const, + }, + ] + : [ + { + sequence: 1, + type: "status" as const, + at: "2026-05-29T00:00:01.000Z", + status: "running" as const, + }, + ], + steps: [], + }; +} + function CommandActionCapture(props: { onActions: (actions: CommandAction[]) => void }) { const registry = useCommandRegistry(); useEffect(() => { @@ -157,6 +231,96 @@ describe("WorkflowRunToolCall", () => { globalThis.localStorage = originalLocalStorage; }); + test("auto-collapses completed workflow runs without mutating sticky preferences", () => { + const completedRun = createWorkflowRunForExpansionTest({ + id: "wfr_auto_collapse", + status: "completed", + reportMarkdown: "auto result", + }); + const completedView = renderWithStickyToolProviders( + + ); + + expect(completedView.queryByText("wfr_auto_collapse")).toBeNull(); + expect(completedView.queryByText("auto result")).toBeNull(); + expect(getStoredPrefs()).toBeNull(); + completedView.unmount(); + + const runningRun = createWorkflowRunForExpansionTest({ id: "wfr_running", status: "running" }); + const executingView = renderWithStickyToolProviders( + + ); + + expect(executingView.getByText("wfr_running")).toBeTruthy(); + expect(getStoredPrefs()).toBeNull(); + }); + + test("resets completed workflow auto-collapse interaction for a new run id", () => { + const firstRun = createWorkflowRunForExpansionTest({ id: "wfr_first", status: "completed" }); + const secondRun = createWorkflowRunForExpansionTest({ id: "wfr_second", status: "completed" }); + const view = renderWithStickyToolProviders( + + ); + + expect(view.queryByText("wfr_first")).toBeNull(); + fireEvent.click(getWorkflowHeader(view)); + expect(view.getByText("wfr_first")).toBeTruthy(); + expect(JSON.parse(getStoredPrefs() ?? "{}")).toEqual({ tools: { workflow_run: true } }); + + view.rerender( + withStickyToolProviders( + + ) + ); + + expect(view.queryByText("wfr_second")).toBeNull(); + expect(view.queryByText("second result")).toBeNull(); + expect(JSON.parse(getStoredPrefs() ?? "{}")).toEqual({ tools: { workflow_run: true } }); + }); + test("renders workflow run phases, linked task ids, and final report", async () => { const view = render( diff --git a/src/browser/features/Tools/WorkflowRunToolCall.tsx b/src/browser/features/Tools/WorkflowRunToolCall.tsx index abaf358a73..8d2c8bbb90 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.tsx @@ -1,11 +1,4 @@ -import React, { - useContext, - useEffect, - useLayoutEffect, - useRef, - useState, - useSyncExternalStore, -} from "react"; +import React, { useContext, useEffect, useRef, useState, useSyncExternalStore } from "react"; import { APIContext, type APIClient } from "@/browser/contexts/API"; import { @@ -50,7 +43,7 @@ import { getStatusDisplay, isToolErrorResult, type ToolStatus, - useToolExpansion, + useAutoCollapsingToolExpansion, } from "./Shared/toolUtils"; import { HighlightedCode } from "./Shared/HighlightedCode"; import { @@ -1039,9 +1032,6 @@ export const WorkflowRunToolCall: React.FC = ({ }) => { const apiState = useContext(APIContext); const commandRegistry = useOptionalCommandRegistry(); - const { expanded, setExpanded, toggleExpanded } = useToolExpansion(true); - const userToggledExpansionRef = useRef(false); - const autoCollapseRunIdRef = useRef(undefined); const registerCommandSource = commandRegistry?.registerSource; const errorResult = isToolErrorResult(result) ? result : null; const successResult = isWorkflowRunSuccessResult(result) ? result : null; @@ -1075,22 +1065,21 @@ export const WorkflowRunToolCall: React.FC = ({ const displayRows = getWorkflowDisplayRows(events); const headerStatus = toToolStatus(displayStatus); const workspaceStore = useWorkspaceStoreRaw(); + const { + expanded, + setLocalExpanded, + toggleExpanded, + markInteracted: markExpansionInteracted, + } = useAutoCollapsingToolExpansion(true, { + // Completed workflow runs can contain large reports and event logs. Collapse them for + // scanability without persisting that automatic presentation choice as user intent. + autoCollapsed: AUTO_COLLAPSE_WORKFLOW_STATUSES.has(displayStatus), + resetKey: runId, + }); const toggleWorkflowExpanded = () => { - userToggledExpansionRef.current = true; toggleExpanded(); }; - useLayoutEffect(() => { - if (autoCollapseRunIdRef.current !== runId) { - autoCollapseRunIdRef.current = runId; - userToggledExpansionRef.current = false; - } - // Completed workflow runs can contain large reports and event logs. Collapse them once for - // scanability, but never override an explicit user expansion/collapse choice. - if (AUTO_COLLAPSE_WORKFLOW_STATUSES.has(displayStatus) && !userToggledExpansionRef.current) { - setExpanded(false); - } - }, [displayStatus, runId, setExpanded]); const [actionError, setActionError] = useState(null); const [promotedDefinition, setPromotedDefinition] = useState( @@ -1291,8 +1280,7 @@ export const WorkflowRunToolCall: React.FC = ({ section: "Workflows", keywords: ["workflow", "save", "project", "scratch", displayName, runId], run: () => { - userToggledExpansionRef.current = true; - setExpanded(true); + setLocalExpanded(true); saveScratchWorkflowRef.current("project"); }, }, @@ -1303,8 +1291,7 @@ export const WorkflowRunToolCall: React.FC = ({ section: "Workflows", keywords: ["workflow", "save", "global", "scratch", displayName, runId], run: () => { - userToggledExpansionRef.current = true; - setExpanded(true); + setLocalExpanded(true); saveScratchWorkflowRef.current("global"); }, } @@ -1325,7 +1312,7 @@ export const WorkflowRunToolCall: React.FC = ({ registerCommandSource, run?.workspaceId, runId, - setExpanded, + setLocalExpanded, ]); useEffect(() => { @@ -1578,10 +1565,10 @@ export const WorkflowRunToolCall: React.FC = ({ steps={run?.steps ?? []} onNavigate={(taskId) => workspaceStore.navigateToWorkspace(taskId)} onOpenReport={() => { - userToggledExpansionRef.current = true; + markExpansionInteracted(); }} onInspectStructuredOutput={() => { - userToggledExpansionRef.current = true; + markExpansionInteracted(); }} /> );