From 9cff19228579220796b968cc630d28181f42d2cc Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 14:15:56 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20keep=20workflow=20pro?= =?UTF-8?q?gress=20visible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tools/WorkflowRunToolCall.test.tsx | 54 ++++++++++++++++ .../features/Tools/WorkflowRunToolCall.tsx | 34 +++++----- src/node/services/streamManager.test.ts | 62 ++++++++++++++++++ src/node/services/streamManager.ts | 63 ++++++++++++++++--- 4 files changed, 188 insertions(+), 25 deletions(-) diff --git a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx index 9278b4201e..813eb6028e 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx @@ -1197,6 +1197,60 @@ describe("WorkflowRunToolCall", () => { }); }); + test("shows workflow events before invocation arguments", () => { + const runningRun = { + id: "wfr_event_priority", + 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:event-priority", + args: { topic: "workflow cards" }, + status: "running" as const, + createdAt: "2026-05-29T00:00:00.000Z", + updatedAt: "2026-05-29T00:00:02.000Z", + events: [ + { + sequence: 1, + type: "action" as const, + at: "2026-05-29T00:00:01.000Z", + stepId: "collect-sources", + name: "github.issue.get", + status: "completed" as const, + effect: "read" as const, + details: { issue: 149 }, + }, + ], + steps: [], + }; + + const view = renderWithStickyToolProviders( + + ); + + const eventsTitle = view.getByText("Workflow events (1)"); + const argumentsTitle = view.getByText("Arguments"); + expect(Boolean(eventsTitle.compareDocumentPosition(argumentsTitle) & 4)).toBe(true); + expect(view.getByText("collect-sources / github.issue.get / completed")).toBeTruthy(); + }); + test("renders executing foreground workflow status before the durable run is discovered", () => { const view = render( diff --git a/src/browser/features/Tools/WorkflowRunToolCall.tsx b/src/browser/features/Tools/WorkflowRunToolCall.tsx index cff4a6c5d2..0d615ea7c0 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.tsx @@ -1483,23 +1483,6 @@ export const WorkflowRunToolCall: React.FC = ({ )} - {/* Large workflow payloads stay collapsed so completed runs remain scannable. */} - - - - - {run?.definitionSource && ( - -
- -
-
- )} - {(canInterrupt || canResume || canRetryFromCheckpoint || canPromote) && (
{canInterrupt && ( @@ -1627,6 +1610,23 @@ export const WorkflowRunToolCall: React.FC = ({ )} + {/* Keep detailed workflow payloads below live progress so short viewports show actions first. */} + + + + + {run?.definitionSource && ( + +
+ +
+
+ )} + {structuredOutput !== undefined && ( diff --git a/src/node/services/streamManager.test.ts b/src/node/services/streamManager.test.ts index 60310cee13..f9ab75f875 100644 --- a/src/node/services/streamManager.test.ts +++ b/src/node/services/streamManager.test.ts @@ -3,6 +3,7 @@ import * as fs from "node:fs/promises"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { StreamEndEventSchema } from "@/common/orpc/schemas/stream"; +import type { CompletedMessagePart } from "@/common/types/stream"; import { Ok, Err } from "@/common/types/result"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; import { @@ -200,6 +201,7 @@ function createStreamInfoForTests( startTime: now, lastPartTimestamp: now, toolCompletionTimestamps: new Map(), + pendingWorkflowRunAttachments: new Map(), model, metadataModel: overrides.metadataModel ?? model, historySequence: 1, @@ -266,6 +268,66 @@ describe("StreamManager - workflow run attachments", () => { timestamp: timestamp + 1, }); }); + + test("persists workflow attachments that arrive before the tool part", async () => { + const streamManager = new StreamManager(historyService); + const workspaceId = "workflow-attachment-race-workspace"; + const messageId = "workflow-attachment-race-message"; + const timestamp = Date.now(); + const streamInfo = createStreamInfoForTests({ + messageId, + lastPartialWriteTime: timestamp, + parts: [], + }); + + getWorkspaceStreamsForTests(streamManager).set(workspaceId, streamInfo); + + const attached = await streamManager.attachWorkflowRunToToolCall({ + type: "workflow-run-attached", + workspaceId, + messageId, + toolCallId: "workflow-call-race", + runId: "wfr_race", + timestamp: timestamp + 1, + }); + + expect(attached).toBe(true); + expect(await historyService.readPartial(workspaceId)).toBeNull(); + + const appendPartAndEmit = getPrivateMethodForTests< + ( + workspaceId: string, + streamInfo: Record, + part: CompletedMessagePart, + schedulePartialWrite?: boolean + ) => Promise + >(streamManager, "appendPartAndEmit"); + + await appendPartAndEmit.call( + streamManager, + workspaceId, + streamInfo, + { + type: "dynamic-tool", + toolCallId: "workflow-call-race", + toolName: "workflow_run", + input: { name: "deep-research", args: {} }, + state: "input-available", + timestamp: timestamp + 2, + }, + false + ); + + const partial = await historyService.readPartial(workspaceId); + const part = partial?.parts[0]; + if (part?.type !== "dynamic-tool") { + throw new Error("Expected workflow tool part in persisted partial"); + } + expect(part.workflowRun).toEqual({ + runId: "wfr_race", + timestamp: timestamp + 1, + }); + }); }); describe("StreamManager - createTempDirForStream", () => { diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index 0e101bd825..2185eafe5f 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -463,6 +463,9 @@ function hasIncompleteToolCallPart(parts: CompletedMessagePart[]): boolean { return parts.some((part) => part.type === "dynamic-tool" && part.state !== "output-available"); } +type DynamicToolCompletedMessagePart = Extract; +type WorkflowRunToolAttachment = NonNullable; + // Comprehensive stream info interface WorkspaceStreamInfo { state: StreamState; @@ -484,6 +487,10 @@ interface WorkspaceStreamInfo { // original start timestamp even after they gain output. toolCompletionTimestamps: Map; + // Workflow tools can create the durable run before their stream part is stored. Keep the exact + // attachment and apply it as soon as the matching dynamic-tool part lands. + pendingWorkflowRunAttachments: Map; + model: string; /** Metadata model resolved from provider mapping for cost/token metadata lookups. */ metadataModel: string; @@ -646,6 +653,25 @@ export class StreamManager extends EventEmitter { streamInfo.toolModelUsages.push(clonePersistedToolModelUsage(event)); } + private getWorkflowRunAttachment(event: WorkflowRunAttachedEvent): WorkflowRunToolAttachment { + return { + runId: event.runId, + ...(event.run != null ? { run: event.run } : {}), + timestamp: event.timestamp, + }; + } + + private takePendingWorkflowRunAttachment( + streamInfo: WorkspaceStreamInfo, + toolCallId: string + ): WorkflowRunToolAttachment | undefined { + const attachment = streamInfo.pendingWorkflowRunAttachments.get(toolCallId); + if (attachment != null) { + streamInfo.pendingWorkflowRunAttachments.delete(toolCallId); + } + return attachment; + } + async attachWorkflowRunToToolCall(event: WorkflowRunAttachedEvent): Promise { const workspaceId = event.workspaceId as WorkspaceId; const streamInfo = this.workspaceStreams.get(workspaceId); @@ -656,11 +682,13 @@ export class StreamManager extends EventEmitter { return false; } + const attachment = this.getWorkflowRunAttachment(event); const partIndex = streamInfo.parts.findIndex( (part) => part.type === "dynamic-tool" && part.toolCallId === event.toolCallId ); if (partIndex === -1) { - return false; + streamInfo.pendingWorkflowRunAttachments.set(event.toolCallId, attachment); + return true; } const part = streamInfo.parts[partIndex]; @@ -668,13 +696,10 @@ export class StreamManager extends EventEmitter { return false; } + streamInfo.pendingWorkflowRunAttachments.delete(event.toolCallId); streamInfo.parts[partIndex] = { ...part, - workflowRun: { - runId: event.runId, - ...(event.run != null ? { run: event.run } : {}), - timestamp: event.timestamp, - }, + workflowRun: attachment, }; await this.flushPartialWrite(workspaceId, streamInfo); @@ -1164,8 +1189,22 @@ export class StreamManager extends EventEmitter { await this.emitPartAsEvent(workspaceId, streamInfo.messageId, part); } finally { // Always persist the part in-memory (and to partial.json, if enabled), even if emit fails. - streamInfo.parts.push(part); - if (schedulePartialWrite) { + let partToPersist = part; + let attachedWorkflowRun = false; + if (part.type === "dynamic-tool") { + const pendingAttachment = this.takePendingWorkflowRunAttachment( + streamInfo, + part.toolCallId + ); + if (pendingAttachment != null) { + partToPersist = { ...part, workflowRun: pendingAttachment }; + attachedWorkflowRun = true; + } + } + streamInfo.parts.push(partToPersist); + if (attachedWorkflowRun) { + await this.flushPartialWrite(workspaceId, streamInfo); + } else if (schedulePartialWrite) { void this.schedulePartialWrite(workspaceId, streamInfo); } } @@ -1563,6 +1602,7 @@ export class StreamManager extends EventEmitter { startTime, lastPartTimestamp: startTime, toolCompletionTimestamps: new Map(), + pendingWorkflowRunAttachments: new Map(), model: modelString, metadataModel, thinkingLevel, @@ -1626,8 +1666,13 @@ export class StreamManager extends EventEmitter { if (existingPartIndex !== -1) { const existingPart = streamInfo.parts[existingPartIndex]; if (existingPart.type === "dynamic-tool") { + const pendingAttachment = this.takePendingWorkflowRunAttachment( + streamInfo, + existingPart.toolCallId + ); streamInfo.parts[existingPartIndex] = { ...existingPart, + ...(pendingAttachment != null ? { workflowRun: pendingAttachment } : {}), state: "output-available" as const, output, }; @@ -1636,6 +1681,7 @@ export class StreamManager extends EventEmitter { // Fallback: if the matching tool-call part is missing, still persist output so the UI // does not stay stuck in input-available. Input may be missing for provider-native tools. const toolCall = toolCalls.get(toolCallId); + const pendingAttachment = this.takePendingWorkflowRunAttachment(streamInfo, toolCallId); streamInfo.parts.push({ type: "dynamic-tool" as const, toolCallId, @@ -1644,6 +1690,7 @@ export class StreamManager extends EventEmitter { input: toolCall?.input ?? null, output, timestamp: nextPartTimestamp(streamInfo), + ...(pendingAttachment != null ? { workflowRun: pendingAttachment } : {}), }); } From 033adfb2ab17ea28d88b9be4bcb09f654b6f2bd1 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 14:23:02 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20dedupe=20workflo?= =?UTF-8?q?w=20attachment=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deduplicate the pending workflow run attachment lookup in StreamManager completeToolCall while preserving existing and fallback attachment behavior. --- src/node/services/streamManager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index 2185eafe5f..9d04ec53e8 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -1662,14 +1662,11 @@ export class StreamManager extends EventEmitter { const existingPartIndex = streamInfo.parts.findIndex( (p) => p.type === "dynamic-tool" && p.toolCallId === toolCallId ); + const pendingAttachment = this.takePendingWorkflowRunAttachment(streamInfo, toolCallId); if (existingPartIndex !== -1) { const existingPart = streamInfo.parts[existingPartIndex]; if (existingPart.type === "dynamic-tool") { - const pendingAttachment = this.takePendingWorkflowRunAttachment( - streamInfo, - existingPart.toolCallId - ); streamInfo.parts[existingPartIndex] = { ...existingPart, ...(pendingAttachment != null ? { workflowRun: pendingAttachment } : {}), @@ -1681,7 +1678,6 @@ export class StreamManager extends EventEmitter { // Fallback: if the matching tool-call part is missing, still persist output so the UI // does not stay stuck in input-available. Input may be missing for provider-native tools. const toolCall = toolCalls.get(toolCallId); - const pendingAttachment = this.takePendingWorkflowRunAttachment(streamInfo, toolCallId); streamInfo.parts.push({ type: "dynamic-tool" as const, toolCallId, From d1973a478817ac01ae4d4d16b3213329f577f8cb Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 14:33:32 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20replay=20queued=20wor?= =?UTF-8?q?kflow=20run=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/streamManager.test.ts | 18 ++++++++++- src/node/services/streamManager.ts | 41 ++++++++++++++++++++----- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/node/services/streamManager.test.ts b/src/node/services/streamManager.test.ts index f9ab75f875..29f1013072 100644 --- a/src/node/services/streamManager.test.ts +++ b/src/node/services/streamManager.test.ts @@ -3,7 +3,7 @@ import * as fs from "node:fs/promises"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { StreamEndEventSchema } from "@/common/orpc/schemas/stream"; -import type { CompletedMessagePart } from "@/common/types/stream"; +import type { CompletedMessagePart, WorkflowRunAttachedEvent } from "@/common/types/stream"; import { Ok, Err } from "@/common/types/result"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; import { @@ -303,6 +303,11 @@ describe("StreamManager - workflow run attachments", () => { ) => Promise >(streamManager, "appendPartAndEmit"); + const replayedAttachments: WorkflowRunAttachedEvent[] = []; + streamManager.on("workflow-run-attached", (event: WorkflowRunAttachedEvent) => { + replayedAttachments.push(event); + }); + await appendPartAndEmit.call( streamManager, workspaceId, @@ -318,6 +323,17 @@ describe("StreamManager - workflow run attachments", () => { false ); + expect(replayedAttachments).toEqual([ + { + type: "workflow-run-attached", + workspaceId, + messageId, + toolCallId: "workflow-call-race", + runId: "wfr_race", + timestamp: timestamp + 1, + }, + ]); + const partial = await historyService.readPartial(workspaceId); const part = partial?.parts[0]; if (part?.type !== "dynamic-tool") { diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index 9d04ec53e8..11800bff56 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -672,6 +672,23 @@ export class StreamManager extends EventEmitter { return attachment; } + private emitWorkflowRunAttachedFromAttachment(input: { + workspaceId: WorkspaceId; + messageId: string; + toolCallId: string; + attachment: WorkflowRunToolAttachment; + }): void { + this.emit("workflow-run-attached", { + type: "workflow-run-attached", + workspaceId: input.workspaceId as string, + messageId: input.messageId, + toolCallId: input.toolCallId, + runId: input.attachment.runId, + ...(input.attachment.run != null ? { run: input.attachment.run } : {}), + timestamp: input.attachment.timestamp, + } satisfies WorkflowRunAttachedEvent); + } + async attachWorkflowRunToToolCall(event: WorkflowRunAttachedEvent): Promise { const workspaceId = event.workspaceId as WorkspaceId; const streamInfo = this.workspaceStreams.get(workspaceId); @@ -1190,20 +1207,22 @@ export class StreamManager extends EventEmitter { } finally { // Always persist the part in-memory (and to partial.json, if enabled), even if emit fails. let partToPersist = part; - let attachedWorkflowRun = false; + let pendingAttachment: WorkflowRunToolAttachment | undefined; if (part.type === "dynamic-tool") { - const pendingAttachment = this.takePendingWorkflowRunAttachment( - streamInfo, - part.toolCallId - ); + pendingAttachment = this.takePendingWorkflowRunAttachment(streamInfo, part.toolCallId); if (pendingAttachment != null) { partToPersist = { ...part, workflowRun: pendingAttachment }; - attachedWorkflowRun = true; } } streamInfo.parts.push(partToPersist); - if (attachedWorkflowRun) { + if (pendingAttachment != null && part.type === "dynamic-tool") { await this.flushPartialWrite(workspaceId, streamInfo); + this.emitWorkflowRunAttachedFromAttachment({ + workspaceId, + messageId: streamInfo.messageId, + toolCallId: part.toolCallId, + attachment: pendingAttachment, + }); } else if (schedulePartialWrite) { void this.schedulePartialWrite(workspaceId, streamInfo); } @@ -1695,6 +1714,14 @@ export class StreamManager extends EventEmitter { // read partial.json via commitPartial. Without this await, there's a race condition // where the partial is read before the tool result is written, causing "amnesia". await this.flushPartialWrite(workspaceId, streamInfo); + if (pendingAttachment != null) { + this.emitWorkflowRunAttachedFromAttachment({ + workspaceId, + messageId: streamInfo.messageId, + toolCallId, + attachment: pendingAttachment, + }); + } // Emit tool-call-end event (listeners can now safely read partial) const completionTimestamp = nextPartTimestamp(streamInfo); From e78b633205fa549a833cbca966e1c2d1f9a1b04b Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 14:43:48 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20forward=20replayed=20?= =?UTF-8?q?workflow=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/aiService.test.ts | 23 +++++++++++++++++++++++ src/node/services/aiService.ts | 1 + 2 files changed, 24 insertions(+) diff --git a/src/node/services/aiService.test.ts b/src/node/services/aiService.test.ts index 2a1647b145..b27152af4e 100644 --- a/src/node/services/aiService.test.ts +++ b/src/node/services/aiService.test.ts @@ -52,6 +52,7 @@ import type { RuntimeStatusEvent, StreamAbortEvent, StreamEndEvent, + WorkflowRunAttachedEvent, } from "@/common/types/stream"; import { log } from "./log"; import type { SessionUsageService } from "./sessionUsageService"; @@ -680,6 +681,28 @@ describe("AIService.setupStreamEventForwarding", () => { expect(internals.pendingDevToolsRunMetadataByMessageId.has("message-1")).toBe(true); }); + it("forwards workflow-run-attached events", async () => { + using harness = createForwardingHarness("ai-service-workflow-run-attached-forwarding"); + const { service, internals } = harness; + const event: WorkflowRunAttachedEvent = { + type: "workflow-run-attached", + workspaceId: "workspace-1", + messageId: "message-1", + toolCallId: "workflow-call-1", + runId: "wfr_forwarded", + timestamp: Date.now(), + }; + + const forwardedPromise = new Promise((resolve) => { + service.once("workflow-run-attached", (forwarded) => + resolve(forwarded as WorkflowRunAttachedEvent) + ); + }); + internals.streamManager.emit("workflow-run-attached", event); + + expect(await forwardedPromise).toEqual(event); + }); + it.each([ { name: "stream error", diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 01bf973c21..eb9dd54fc2 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -631,6 +631,7 @@ export class AIService extends EventEmitter { "tool-call-end", "reasoning-delta", "reasoning-end", + "workflow-run-attached", "usage-delta", ] as const) { this.streamManager.on(event, (data) => this.emit(event, data)); From 82eb3926a11d4bf8b08e037d69599f2e0bfade8a Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 14:57:56 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20tolerate=20legacy=20s?= =?UTF-8?q?tream=20info=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/streamManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index 11800bff56..e9d2e4095b 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -665,9 +665,10 @@ export class StreamManager extends EventEmitter { streamInfo: WorkspaceStreamInfo, toolCallId: string ): WorkflowRunToolAttachment | undefined { - const attachment = streamInfo.pendingWorkflowRunAttachments.get(toolCallId); + const pendingAttachments = (streamInfo.pendingWorkflowRunAttachments ??= new Map()); + const attachment = pendingAttachments.get(toolCallId); if (attachment != null) { - streamInfo.pendingWorkflowRunAttachments.delete(toolCallId); + pendingAttachments.delete(toolCallId); } return attachment; } @@ -704,7 +705,7 @@ export class StreamManager extends EventEmitter { (part) => part.type === "dynamic-tool" && part.toolCallId === event.toolCallId ); if (partIndex === -1) { - streamInfo.pendingWorkflowRunAttachments.set(event.toolCallId, attachment); + (streamInfo.pendingWorkflowRunAttachments ??= new Map()).set(event.toolCallId, attachment); return true; } From fb8385819fd7e4da44a27bb47080102cbf54b60a Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 15:06:32 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20guard=20workflow=20at?= =?UTF-8?q?tachment=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/streamManager.test.ts | 1 + src/node/services/streamManager.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/services/streamManager.test.ts b/src/node/services/streamManager.test.ts index 29f1013072..c0ef40d381 100644 --- a/src/node/services/streamManager.test.ts +++ b/src/node/services/streamManager.test.ts @@ -234,6 +234,7 @@ describe("StreamManager - workflow run attachments", () => { const streamInfo = createStreamInfoForTests({ messageId, lastPartialWriteTime: timestamp, + pendingWorkflowRunAttachments: undefined, parts: [ { type: "dynamic-tool", diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index e9d2e4095b..238e1d427b 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -714,7 +714,7 @@ export class StreamManager extends EventEmitter { return false; } - streamInfo.pendingWorkflowRunAttachments.delete(event.toolCallId); + streamInfo.pendingWorkflowRunAttachments?.delete(event.toolCallId); streamInfo.parts[partIndex] = { ...part, workflowRun: attachment, From 3de382f6417bdf87148ef7f0c664f02c36b301e7 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 17 Jun 2026 15:44:31 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20restore=20workflow=20?= =?UTF-8?q?details=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tools/WorkflowRunToolCall.test.tsx | 8 +++-- .../features/Tools/WorkflowRunToolCall.tsx | 34 +++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx index 813eb6028e..016f2aaeff 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.test.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.test.tsx @@ -1197,7 +1197,7 @@ describe("WorkflowRunToolCall", () => { }); }); - test("shows workflow events before invocation arguments", () => { + test("shows invocation arguments and definition source before workflow events", () => { const runningRun = { id: "wfr_event_priority", workspaceId: TEST_WORKSPACE_ID, @@ -1245,9 +1245,11 @@ describe("WorkflowRunToolCall", () => { /> ); - const eventsTitle = view.getByText("Workflow events (1)"); const argumentsTitle = view.getByText("Arguments"); - expect(Boolean(eventsTitle.compareDocumentPosition(argumentsTitle) & 4)).toBe(true); + const definitionSourceTitle = view.getByText("Definition source"); + const eventsTitle = view.getByText("Workflow events (1)"); + expect(Boolean(argumentsTitle.compareDocumentPosition(eventsTitle) & 4)).toBe(true); + expect(Boolean(definitionSourceTitle.compareDocumentPosition(eventsTitle) & 4)).toBe(true); expect(view.getByText("collect-sources / github.issue.get / completed")).toBeTruthy(); }); diff --git a/src/browser/features/Tools/WorkflowRunToolCall.tsx b/src/browser/features/Tools/WorkflowRunToolCall.tsx index 0d615ea7c0..cff4a6c5d2 100644 --- a/src/browser/features/Tools/WorkflowRunToolCall.tsx +++ b/src/browser/features/Tools/WorkflowRunToolCall.tsx @@ -1483,6 +1483,23 @@ export const WorkflowRunToolCall: React.FC = ({ )} + {/* Large workflow payloads stay collapsed so completed runs remain scannable. */} + + + + + {run?.definitionSource && ( + +
+ +
+
+ )} + {(canInterrupt || canResume || canRetryFromCheckpoint || canPromote) && (
{canInterrupt && ( @@ -1610,23 +1627,6 @@ export const WorkflowRunToolCall: React.FC = ({ )} - {/* Keep detailed workflow payloads below live progress so short viewports show actions first. */} - - - - - {run?.definitionSource && ( - -
- -
-
- )} - {structuredOutput !== undefined && (