Skip to content

Commit cdd438b

Browse files
🤖 fix: show nested tool calls during code_execution streaming (#1366)
## Problem When using `code_execution` (PTC), nested tool calls (e.g., `mux.bash()`, `mux.file_read()`) didn't appear in the UI until they completed. Users expected to see "executing..." while long-running nested tools were in progress, just like regular top-level tool calls. ## Root Cause Two issues combined: ### 1. MessageId Mismatch The backend was emitting nested tool events with the wrong `messageId`: - `aiService.ts` created `assistantMessageId` for history and the `emitNestedEvent` callback - `streamManager.ts` created a **separate** `messageId` for all stream events - Frontend aggregator looked up messages by `messageId` - nested events with `assistantMessageId` couldn't find the message stored under `streamInfo.messageId` - Events were silently dropped ### 2. React Reactivity Even after fixing the messageId, the UI didn't update when nested tools completed: - `handleToolCallEnd` mutated `nestedCall` objects in place - React didn't detect changes because object references stayed the same - Fix: use immutable update pattern (create new array + new objects) ## Solution 1. **Thread messageId through**: Pass `assistantMessageId` from `aiService.ts` to `streamManager.startStream()` so both use the same ID 2. **Immutable updates**: Replace in-place mutation with immutable update pattern in `handleToolCallEnd` ## Testing - All existing tests pass - Manual testing confirms nested tools now show "executing..." and update to completed state in real-time --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_
1 parent 1c10fa2 commit cdd438b

File tree

4 files changed

+22
-7
lines changed

4 files changed

+22
-7
lines changed

src/browser/utils/messages/StreamingMessageAggregator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,15 +1269,23 @@ export class StreamingMessageAggregator {
12691269
if (message) {
12701270
// If nested, update in parent's nestedCalls array
12711271
if (data.parentToolCallId) {
1272-
const parentPart = message.parts.find(
1272+
const parentIndex = message.parts.findIndex(
12731273
(part): part is DynamicToolPart =>
12741274
part.type === "dynamic-tool" && part.toolCallId === data.parentToolCallId
12751275
);
1276+
const parentPart = message.parts[parentIndex] as DynamicToolPart | undefined;
12761277
if (parentPart?.nestedCalls) {
1277-
const nestedCall = parentPart.nestedCalls.find((nc) => nc.toolCallId === data.toolCallId);
1278-
if (nestedCall) {
1279-
nestedCall.state = "output-available";
1280-
nestedCall.output = data.result;
1278+
const nestedIndex = parentPart.nestedCalls.findIndex(
1279+
(nc) => nc.toolCallId === data.toolCallId
1280+
);
1281+
if (nestedIndex !== -1) {
1282+
// Create new objects to trigger React re-render (immutable update pattern)
1283+
const updatedNestedCalls = parentPart.nestedCalls.map((nc, i) =>
1284+
i === nestedIndex
1285+
? { ...nc, state: "output-available" as const, output: data.result }
1286+
: nc
1287+
);
1288+
message.parts[parentIndex] = { ...parentPart, nestedCalls: updatedNestedCalls };
12811289
this.invalidateCache();
12821290
return;
12831291
}

src/node/services/aiService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,7 @@ export class AIService extends EventEmitter {
17351735
historySequence,
17361736
systemMessage,
17371737
runtime,
1738+
assistantMessageId, // Shared messageId ensures nested tool events match stream events
17381739
abortSignal,
17391740
tools,
17401741
{

src/node/services/streamManager.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => {
9090
1,
9191
"You are a helpful assistant",
9292
runtime,
93+
"test-msg-1",
9394
undefined,
9495
{}
9596
);
@@ -108,6 +109,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => {
108109
2,
109110
"You are a helpful assistant",
110111
runtime,
112+
"test-msg-2",
111113
undefined,
112114
{}
113115
);
@@ -280,6 +282,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => {
280282
1,
281283
"system",
282284
runtime,
285+
"test-msg-1",
283286
undefined,
284287
{}
285288
),
@@ -291,6 +294,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => {
291294
2,
292295
"system",
293296
runtime,
297+
"test-msg-2",
294298
undefined,
295299
{}
296300
),
@@ -302,6 +306,7 @@ describe("StreamManager - Concurrent Stream Prevention", () => {
302306
3,
303307
"system",
304308
runtime,
309+
"test-msg-3",
305310
undefined,
306311
{}
307312
),

src/node/services/streamManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export class StreamManager extends EventEmitter {
620620
abortSignal: AbortSignal | undefined,
621621
system: string,
622622
historySequence: number,
623+
messageId: string,
623624
tools?: Record<string, Tool>,
624625
initialMetadata?: Partial<MuxMetadata>,
625626
providerOptions?: Record<string, unknown>,
@@ -720,8 +721,6 @@ export class StreamManager extends EventEmitter {
720721
throw error;
721722
}
722723

723-
const messageId = `assistant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
724-
725724
const streamInfo: WorkspaceStreamInfo = {
726725
state: StreamState.STARTING,
727726
streamResult,
@@ -1512,6 +1511,7 @@ export class StreamManager extends EventEmitter {
15121511
historySequence: number,
15131512
system: string,
15141513
runtime: Runtime,
1514+
messageId: string,
15151515
abortSignal?: AbortSignal,
15161516
tools?: Record<string, Tool>,
15171517
initialMetadata?: Partial<MuxMetadata>,
@@ -1568,6 +1568,7 @@ export class StreamManager extends EventEmitter {
15681568
abortSignal,
15691569
system,
15701570
historySequence,
1571+
messageId,
15711572
tools,
15721573
initialMetadata,
15731574
providerOptions,

0 commit comments

Comments
 (0)