Skip to content

Commit 2eaf4ce

Browse files
committed
fix(thinking): show thinking during subagents
1 parent 5c9a5a0 commit 2eaf4ce

2 files changed

Lines changed: 43 additions & 46 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { describe, expect, it } from 'vitest'
55
import type { ContentBlock } from '../../types'
66
import {
7-
assistantMessageHasRunningWork,
7+
assistantMessageHasVisibleExecutingTool,
88
parseBlocks,
99
shouldShowTrailingThinking,
1010
shouldSmoothTextSegment,
@@ -14,10 +14,6 @@ function subagentStart(name: string, spanId: string, parentSpanId: string): Cont
1414
return { type: 'subagent', content: name, spanId, parentSpanId, timestamp: 1 }
1515
}
1616

17-
function subagentEnd(spanId: string): ContentBlock {
18-
return { type: 'subagent_end', spanId, timestamp: 2 }
19-
}
20-
2117
function subagentToolCall(
2218
id: string,
2319
name: string,
@@ -489,41 +485,46 @@ describe('parseBlocks legacy — thinking between top-level tools', () => {
489485
})
490486
})
491487

492-
describe('assistantMessageHasRunningWork', () => {
493-
it('reads an open subagent lane as running work', () => {
494-
expect(assistantMessageHasRunningWork([subagentStart('workflow', 'S1', 'main')])).toBe(true)
495-
})
496-
497-
it('reads a lane closed by its paired subagent_end (live path) as settled', () => {
498-
const blocks: ContentBlock[] = [subagentStart('workflow', 'S1', 'main'), subagentEnd('S1')]
499-
expect(assistantMessageHasRunningWork(blocks)).toBe(false)
500-
})
501-
502-
it('reads a lane closed by a stamped endedAt (persisted path) as settled', () => {
503-
const closed: ContentBlock = { ...subagentStart('workflow', 'S1', 'main'), endedAt: 5 }
504-
expect(assistantMessageHasRunningWork([closed])).toBe(false)
488+
describe('assistantMessageHasVisibleExecutingTool', () => {
489+
it('does not treat an open subagent lane as an executing tool row', () => {
490+
expect(assistantMessageHasVisibleExecutingTool([subagentStart('workflow', 'S1', 'main')])).toBe(
491+
false
492+
)
505493
})
506494

507-
it('keeps an executing tool as running work after every lane closed', () => {
495+
it('keeps a visible executing tool as active work', () => {
508496
const blocks: ContentBlock[] = [
509497
subagentStart('workflow', 'S1', 'main'),
510-
subagentEnd('S1'),
511498
{
512499
type: 'tool_call',
513-
toolCall: { id: 't1', name: 'grep', status: 'executing' },
500+
toolCall: { id: 't1', name: 'grep', status: 'executing', calledBy: 'workflow' },
501+
spanId: 'S1',
514502
timestamp: 3,
515503
},
516504
]
517-
expect(assistantMessageHasRunningWork(blocks)).toBe(true)
505+
expect(assistantMessageHasVisibleExecutingTool(blocks)).toBe(true)
518506
})
519507

520-
it('tracks parallel lanes independently — one still-open lane keeps the turn running', () => {
508+
it('does not let open parallel lanes suppress the single turn-level indicator', () => {
521509
const blocks: ContentBlock[] = [
522510
subagentStart('workflow', 'S1', 'main'),
523511
subagentStart('search', 'S2', 'main'),
524-
subagentEnd('S1'),
525512
]
526-
expect(assistantMessageHasRunningWork(blocks)).toBe(true)
527-
expect(assistantMessageHasRunningWork([...blocks, subagentEnd('S2')])).toBe(false)
513+
expect(assistantMessageHasVisibleExecutingTool(blocks)).toBe(false)
514+
})
515+
516+
it('ignores the executing dispatch tool represented by its subagent lane', () => {
517+
const blocks: ContentBlock[] = [
518+
{
519+
type: 'tool_call',
520+
toolCall: { id: 'dispatch-1', name: 'workspace_file', status: 'executing' },
521+
timestamp: 1,
522+
},
523+
{
524+
...subagentStart('file', 'S1', 'main'),
525+
parentToolCallId: 'dispatch-1',
526+
},
527+
]
528+
expect(assistantMessageHasVisibleExecutingTool(blocks)).toBe(false)
528529
})
529530
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -761,27 +761,23 @@ export function assistantMessageHasRenderableContent(
761761
return segments.length > 0
762762
}
763763

764-
/**
765-
* True while any wire-declared work in the turn is still running: a tool row
766-
* still `executing`, or a subagent lane that has not closed. A lane closes via
767-
* its paired `subagent_end` block (live serializer) or a stamped `endedAt`
768-
* (Sim-backend persistence / turn-terminal sweep). The live path never stamps
769-
* `endedAt` on `subagent` blocks, so checking `endedAt` alone reads every
770-
* finished subagent as still running for the rest of the turn — permanently
771-
* suppressing the between-steps thinking indicator after the first delegation.
772-
*/
773-
export function assistantMessageHasRunningWork(blocks: ContentBlock[]): boolean {
774-
const closedSpanIds = new Set<string>()
764+
/** True when the transcript is already rendering an executing tool row. */
765+
export function assistantMessageHasVisibleExecutingTool(blocks: ContentBlock[]): boolean {
766+
const subagentDispatchCallIds = new Set<string>()
775767
for (const block of blocks) {
776-
if (block.type === 'subagent_end' && block.spanId) closedSpanIds.add(block.spanId)
768+
if (block.type === 'subagent' && block.parentToolCallId) {
769+
subagentDispatchCallIds.add(block.parentToolCallId)
770+
}
777771
}
778-
return blocks.some(
779-
(block) =>
780-
block.toolCall?.status === 'executing' ||
781-
(block.type === 'subagent' &&
782-
block.endedAt === undefined &&
783-
!(block.spanId && closedSpanIds.has(block.spanId)))
784-
)
772+
773+
return blocks.some((block) => {
774+
const toolCall = block.toolCall
775+
if (!toolCall || toolCall.status !== 'executing') return false
776+
if (isHiddenToolCall(toolCall.name)) return false
777+
if (toolCall.name === ReadTool.id && isToolResultRead(toolCall.params)) return false
778+
if (SUBAGENT_KEYS.has(toolCall.name)) return false
779+
return !subagentDispatchCallIds.has(toolCall.id)
780+
})
785781
}
786782

787783
export function shouldSmoothTextSegment({
@@ -897,7 +893,7 @@ function MessageContentInner({
897893
// not suppress the turn-level indicator: once its latest visible chunk has
898894
// settled, the loader can bridge the wait until that lane (or a parallel
899895
// sibling) emits again.
900-
const hasExecutingTool = blocks.some((b) => b.toolCall?.status === 'executing')
896+
const hasExecutingTool = assistantMessageHasVisibleExecutingTool(blocks)
901897
const showTrailingThinking = shouldShowTrailingThinking({
902898
isStreaming: phase === 'streaming',
903899
isStreamIdle,

0 commit comments

Comments
 (0)