Skip to content
19 changes: 13 additions & 6 deletions packages/cli/src/ui/hooks/useToolScheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ describe('useToolScheduler', () => {
expect(toolCalls2.every((t) => t.responseSubmittedToGemini)).toBe(true);
});

it('ignores TOOL_CALLS_UPDATE from non-root schedulers when no tools await approval', async () => {
it('shows Executing tool calls from non-root schedulers for subagent visibility', async () => {
const { result } = await renderHook(() =>
useToolScheduler(
vi.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -413,7 +413,11 @@ describe('useToolScheduler', () => {
} as ToolCallsUpdateMessage);
});

expect(result.current[0]).toHaveLength(0);
// Executing calls from non-root schedulers are now visible so the
// task-tree can build the full parentCallId hierarchy for subagent work.
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0].request.callId).toBe('call-sub');
expect(result.current[0][0].status).toBe(CoreToolCallStatus.Executing);
});

it('allows TOOL_CALLS_UPDATE from non-root schedulers when tools are awaiting approval', async () => {
Expand Down Expand Up @@ -504,7 +508,8 @@ describe('useToolScheduler', () => {
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0].status).toBe(CoreToolCallStatus.Executing);

// Background tool should not be shown
// New executing tool from the same subagent — now visible so the
// task-tree can display the full hierarchy of subagent work.
const backgroundTool = {
status: CoreToolCallStatus.Executing as const,
request: {
Expand All @@ -527,9 +532,11 @@ describe('useToolScheduler', () => {
} as ToolCallsUpdateMessage);
});

// The subagent list should now be empty because the previously approved tool
// is gone from the current list, and the new tool doesn't need approval.
expect(result.current[0]).toHaveLength(0);
// The previously approved tool (call-sub) is gone from the update, but the
// new Executing tool (call-background) is now shown for subagent visibility.
expect(result.current[0]).toHaveLength(1);
expect(result.current[0][0].request.callId).toBe('call-background');
expect(result.current[0][0].status).toBe(CoreToolCallStatus.Executing);
});

it('adapts success/error status to executing when a tail call is present', async () => {
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/ui/hooks/useToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,27 @@ export function useToolScheduler(
const prevCalls = prev[event.schedulerId] ?? [];
const prevCallIds = new Set(prevCalls.map((tc) => tc.request.callId));

// For non-root schedulers, we only show tool calls that:
// For non-root schedulers, we show tool calls that:
// 1. Are currently awaiting approval.
// 2. Were previously shown (e.g., they are now executing or completed).
// This prevents "thinking" tools (reads/searches) from flickering in the UI
// unless they specifically required user interaction.
// 2. Are actively executing — two distinct reasons:
// a. Interactive shell calls (pid present) need to surface so the
// user can see PTY prompts. (See also: PR #21268 which fixes the
// narrower case of pid-bearing shell calls for issue #21052.)
// b. All other executing subagent calls (reads, searches, MCP, etc.)
// must be present in state so the task-tree can build the full
// parentCallId hierarchy. Without them, subagent work is invisible
// in the tree regardless of nesting depth.
// 3. Were previously shown (now completing or completed).
//
// Calls still in Scheduled/Validating that have never been shown and
// don't require approval remain hidden to prevent transient flicker in
// the flat-list UI.
const filteredToolCalls = isRoot
? event.toolCalls
: event.toolCalls.filter(
(tc) =>
tc.status === CoreToolCallStatus.AwaitingApproval ||
tc.status === CoreToolCallStatus.Executing ||
prevCallIds.has(tc.request.callId),
Comment thread
TravisHaa marked this conversation as resolved.
);

Expand Down