From ddd9aaddba732dc8de81fd21b6cba52219ce63ca Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 7 Jun 2026 21:48:12 +0100 Subject: [PATCH] Keep fast Assistant tool rows visibly running --- .../v6/internal/subsystems/ai-runtime.md | 10 ++++++- .../components/AI/Chat/ToolExecutionBlock.tsx | 27 ++++++++++++++----- .../__tests__/ToolExecutionBlock.test.tsx | 22 +++++++++++---- .../AI/Chat/__tests__/useChat.test.ts | 3 ++- .../AI/Chat/streamActivityTiming.ts | 2 +- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 9e2011e3e..023cc29fe 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -225,7 +225,15 @@ timers when the stream already has a newer workflow/tool state. exposes a provider action in the same dialog. Pulse adapts that by wiring the Assistant model chooser's provider action to the governed Assistant & Patrol provider settings route instead of hiding provider repair behind a - separate slash command or silently switching model routes. Stop is the explicit + separate slash command or silently switching model routes. The same + 2026-06-07 activity-visibility pass rechecked OpenCode + `packages/app/src/pages/session/message-timeline.tsx` and + `message-timeline.data.ts`: active turns are represented as session timeline + rows, with busy/retry state kept visible while assistant parts continue to + arrive. Pulse adapts that by keeping fast command/tool completions in a + transient running state long enough for the compact transcript row to paint, + while still collapsing large command output behind the existing tool-details + affordance. Stop is the explicit interruption path: it must abort the active stream, clear queued follow-ups and pending tool/approval/question affordances, preserve any partial model text, return focus to the composer, and render a neutral transcript marker diff --git a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx index 2e297c1e3..35f42f0d8 100644 --- a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx +++ b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx @@ -354,7 +354,7 @@ export const ToolExecutionBlock: Component = (props) => const hasOutput = createMemo(() => hasReadableToolOutput(outputText())); const hasDetails = createMemo(() => hasInput() || hasOutput()); createEffect(() => { - if (props.compact || !props.tool.success) { + if (!props.tool.success) { setSettlingFastCompletion(false); return; } @@ -410,16 +410,31 @@ export const ToolExecutionBlock: Component = (props) =>
- + + } + > + + {toolLabel()} + {statusLabel()} {inputSummary()} diff --git a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx index 1b9cea1bb..5008e09bc 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx @@ -8,6 +8,7 @@ import { PendingToolsList, ToolExecutionsList, } from '../ToolExecutionBlock'; +import { ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS } from '../streamActivityTiming'; import type { ToolExecution, PendingTool, ToolCancellation } from '../types'; afterEach(() => { @@ -47,7 +48,7 @@ function makeCancellation(overrides?: Partial): ToolCancellati } const getToolDetailsTrigger = () => screen.getByTitle('Show tool details'); -const FAST_TOOL_SETTLE_TEST_MS = 500; +const FAST_TOOL_SETTLE_TEST_MS = ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS + 80; // ============================================================ // ToolExecutionBlock @@ -161,21 +162,23 @@ describe('ToolExecutionBlock', () => { tool={makeTool()} startedAt={9_900} completedAt={9_940} - settleUntil={10_380} + settleUntil={10_000 + ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS - 40} /> )); expect(screen.getByLabelText('Assistant tool running')).toBeInTheDocument(); expect(screen.getByText('running')).toBeInTheDocument(); - await vi.advanceTimersByTimeAsync(380); + await vi.advanceTimersByTimeAsync(FAST_TOOL_SETTLE_TEST_MS); expect(screen.queryByLabelText('Assistant tool running')).not.toBeInTheDocument(); expect(screen.getByText('completed')).toBeInTheDocument(); expect(screen.getByLabelText('Tool duration <1s')).toHaveTextContent('<1s'); }); - it('renders compact completed activity without the running settle state', () => { + it('briefly presents compact fresh live fast completions as running', async () => { + vi.useFakeTimers(); + render(() => ( { /> )); + const runningRow = screen.getByRole('status', { name: 'Assistant tool running' }); + expect(runningRow).toHaveTextContent('cmd'); + expect(runningRow).toHaveTextContent('running'); + expect(runningRow).toHaveTextContent('uptime'); + expect(screen.getByLabelText('running')).toBeInTheDocument(); + expect(screen.queryByLabelText(/Tool duration/)).not.toBeInTheDocument(); + + await vi.advanceTimersByTimeAsync(FAST_TOOL_SETTLE_TEST_MS); + const row = screen.getByRole('status', { name: 'Assistant completed tool activity' }); expect(row).toHaveTextContent('cmd'); + expect(row).toHaveTextContent('completed'); expect(row).toHaveTextContent('uptime'); expect(row).toHaveTextContent('<1s'); expect(screen.getByLabelText('completed')).toBeInTheDocument(); expect(screen.queryByLabelText('Assistant tool running')).not.toBeInTheDocument(); - expect(screen.queryByText('running')).not.toBeInTheDocument(); }); it('does not defer failed fast completions behind a running state', () => { diff --git a/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts index 346b7804f..f7130ab5c 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts @@ -32,6 +32,7 @@ vi.mock('@/utils/logger', () => ({ })); import { useChat } from '../hooks/useChat'; +import { ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS } from '../streamActivityTiming'; import { AIChatAPI, type ChatMention, type StreamEvent } from '@/api/aiChat'; import { notificationStore } from '@/stores/notifications'; @@ -2211,7 +2212,7 @@ describe('useChat', () => { toolId: 'tool-1', startedAt: 20_000, updatedAt: 20_040, - settleUntil: 20_420, + settleUntil: 20_000 + ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS, }), ); expect(assistant.streamEvents?.map((event) => event.type)).toEqual(['tool', 'content']); diff --git a/frontend-modern/src/components/AI/Chat/streamActivityTiming.ts b/frontend-modern/src/components/AI/Chat/streamActivityTiming.ts index 83e6959a7..3dc735d95 100644 --- a/frontend-modern/src/components/AI/Chat/streamActivityTiming.ts +++ b/frontend-modern/src/components/AI/Chat/streamActivityTiming.ts @@ -1,4 +1,4 @@ -export const ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS = 420; +export const ASSISTANT_FAST_TOOL_COMPLETION_SETTLE_MS = 900; export const getAssistantFastToolCompletionSettleUntil = ( startedAt: number | undefined,