mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Keep fast Assistant tool rows visibly running
This commit is contained in:
parent
3bbbee323f
commit
ddd9aaddba
5 changed files with 50 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (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<ToolExecutionBlockProps> = (props) =>
|
|||
<div
|
||||
class="my-1 inline-flex max-w-full items-start gap-2 rounded-md border border-border-subtle bg-surface-alt px-2.5 py-1.5 text-[11px] text-muted"
|
||||
role="status"
|
||||
aria-label="Assistant completed tool activity"
|
||||
aria-label={
|
||||
settlingFastCompletion()
|
||||
? 'Assistant tool running'
|
||||
: 'Assistant completed tool activity'
|
||||
}
|
||||
title={inputSummary()}
|
||||
>
|
||||
<CheckCircleIcon
|
||||
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-300"
|
||||
aria-label="completed"
|
||||
/>
|
||||
<Show
|
||||
when={!settlingFastCompletion()}
|
||||
fallback={
|
||||
<LoaderCircleIcon
|
||||
class="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin text-blue-500 dark:text-blue-400"
|
||||
aria-label="running"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CheckCircleIcon
|
||||
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-300"
|
||||
aria-label="completed"
|
||||
/>
|
||||
</Show>
|
||||
<span class="shrink-0 font-mono text-[9px] font-semibold uppercase tracking-wider">
|
||||
{toolLabel()}
|
||||
</span>
|
||||
<span class="shrink-0 text-[10px] font-medium">{statusLabel()}</span>
|
||||
<span class="min-w-0 truncate text-base-content">{inputSummary()}</span>
|
||||
<Show when={durationLabel()}>
|
||||
<span class="shrink-0 text-[10px]" aria-label={`Tool duration ${durationLabel()}`}>
|
||||
|
|
|
|||
|
|
@ -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<ToolCancellation>): 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(() => (
|
||||
<ToolExecutionBlock
|
||||
tool={makeTool({ output: 'up 42 days' })}
|
||||
|
|
@ -186,13 +189,22 @@ describe('ToolExecutionBlock', () => {
|
|||
/>
|
||||
));
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue