Keep completed Assistant tool output inspectable

This commit is contained in:
rcourtman
2026-06-07 23:41:32 +01:00
parent a4c5a85bef
commit 98ee858f7f
3 changed files with 47 additions and 26 deletions
@@ -65,10 +65,12 @@ updates running tool metadata from the execution context in
`packages/opencode/src/session/tools.ts` lines 53-66, and suppresses completed
successful tool detail when `showDetails` is off in
`packages/tui/src/routes/session/index.tsx` lines 1704-1779. Pulse adapts that
by compacting only successful completed tool rows while the same Assistant turn
is still streaming and newer concrete activity has arrived. Pending tools,
skipped/canceled tools, failed tools, and completed-turn tool details must
remain visible/inspectable.
by keeping successful completed tool rows command/status visible, compacting
them while the same Assistant turn is still streaming and newer concrete
activity has arrived, and keeping successful raw output behind the details
disclosure by default once the turn completes. Pending tools, skipped/canceled
tools, failed tools, and completed-turn tool details must remain
visible/inspectable; failed output may render inline because it is actionable.
Assistant live workflow status follows OpenCode's current-state timeline
model: history may persist for audit/session state, but live transcript rows
@@ -344,11 +344,17 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
return preview;
});
const outputPreview = createMemo(() => formatOutputPreview(outputText()));
const showInlineOutputPreview = createMemo(
() => props.tool.success === false && outputPreview().length > 0,
);
const hiddenOutputSummary = createMemo(() =>
outputPreview() ? '' : formatHiddenOutputSummary(outputText()),
showInlineOutputPreview() ? '' : formatHiddenOutputSummary(outputText()),
);
const hiddenOutputBadgeSummary = createMemo(() =>
settlingFastCompletion() ? '' : hiddenOutputSummary(),
);
const hiddenOutputBadgeLabel = createMemo(() =>
hiddenOutputSummary() ? 'output available' : '',
hiddenOutputBadgeSummary() ? 'output available' : '',
);
const hasInput = createMemo(() => detailInputText().trim().length > 0);
const hasOutput = createMemo(() => hasReadableToolOutput(outputText()));
@@ -441,11 +447,11 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
{durationLabel()}
</span>
</Show>
<Show when={hiddenOutputSummary()}>
<Show when={hiddenOutputBadgeSummary()}>
<span
class="shrink-0 rounded border border-border-subtle bg-surface px-1.5 py-0.5 text-[9px] font-medium"
title="Open the completed turn to inspect tool output"
aria-label={`Tool output available: ${hiddenOutputSummary()}`}
aria-label={`Tool output available: ${hiddenOutputBadgeSummary()}`}
>
{hiddenOutputBadgeLabel()}
</span>
@@ -527,11 +533,11 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
aria-hidden="true"
/>
</Show>
<Show when={hiddenOutputSummary()}>
<Show when={hiddenOutputBadgeSummary()}>
<span
class="shrink-0 rounded border border-border-subtle bg-surface-alt px-1.5 py-0.5 text-[9px] font-medium text-muted"
title="Open tool details to inspect output"
aria-label={`Tool output available: ${hiddenOutputSummary()}`}
aria-label={`Tool output available: ${hiddenOutputBadgeSummary()}`}
>
{hiddenOutputBadgeLabel()}
</span>
@@ -544,7 +550,7 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
</div>
</div>
<Show when={outputPreview()}>
<Show when={showInlineOutputPreview()}>
<pre
class="border-t border-border-subtle bg-surface-alt px-3 py-2 font-mono text-[11px] leading-5 text-base-content whitespace-pre-wrap break-words"
aria-label="Tool output preview"
@@ -195,6 +195,7 @@ describe('ToolExecutionBlock', () => {
expect(runningRow).toHaveTextContent('uptime');
expect(screen.getByLabelText('running')).toBeInTheDocument();
expect(screen.queryByLabelText(/Tool duration/)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Tool output available/)).not.toBeInTheDocument();
await vi.advanceTimersByTimeAsync(FAST_TOOL_SETTLE_TEST_MS);
@@ -204,6 +205,9 @@ describe('ToolExecutionBlock', () => {
expect(row).toHaveTextContent('uptime');
expect(row).toHaveTextContent('<1s');
expect(screen.getByLabelText('completed')).toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 10 chars output')).toHaveTextContent(
'output available',
);
expect(screen.queryByLabelText('Assistant tool running')).not.toBeInTheDocument();
});
@@ -319,7 +323,10 @@ describe('ToolExecutionBlock', () => {
expect(screen.getByText('Inspect devices on current resource')).toBeInTheDocument();
expect(screen.getByLabelText('Tool command')).toHaveTextContent('$ ls /dev | wc -l');
expect(screen.getByLabelText('Tool output preview')).toHaveTextContent('42');
expect(screen.queryByLabelText('Tool output preview')).not.toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 2 chars output')).toHaveTextContent(
'output available',
);
expect(screen.queryByText(/"target_host"/)).not.toBeInTheDocument();
});
@@ -484,13 +491,15 @@ describe('ToolExecutionBlock', () => {
// --- Output display ---
it('previews successful short plain-text output while keeping full details available', () => {
it('keeps successful short plain-text output behind details by default', () => {
render(() => <ToolExecutionBlock tool={makeTool({ output: 'hello world' })} />);
expect(screen.getByLabelText('Tool output preview')).toHaveTextContent('hello world');
expect(screen.queryByText('11 chars output')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Tool output preview')).not.toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 11 chars output')).toHaveTextContent(
'output available',
);
fireEvent.click(getToolDetailsTrigger());
expect(screen.getByText('Output')).toBeInTheDocument();
expect(screen.getAllByText('hello world')).toHaveLength(2);
expect(screen.getByText('hello world')).toBeInTheDocument();
});
it('does not preview structured JSON output by default', () => {
@@ -511,7 +520,7 @@ describe('ToolExecutionBlock', () => {
expect(getToolDetailsTrigger()).toBeInTheDocument();
});
it('previews successful plain-text output with a bounded visible snippet', () => {
it('keeps successful multi-line plain-text output behind details by default', () => {
const { container } = render(() => (
<ToolExecutionBlock
tool={makeTool({
@@ -521,21 +530,22 @@ describe('ToolExecutionBlock', () => {
));
const text = container.textContent || '';
expect(screen.getByLabelText('Tool output preview')).toHaveTextContent(
'line 1 line 2 line 3 line 4 ...',
expect(screen.queryByLabelText('Tool output preview')).not.toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 5 lines output')).toHaveTextContent(
'output available',
);
expect(text).not.toContain('line 1');
expect(text).not.toContain('line 5');
expect(screen.queryByText('5 lines output')).not.toBeInTheDocument();
expect(getToolDetailsTrigger()).toBeInTheDocument();
});
it('previews long successful single-line output without showing raw output counts', () => {
it('keeps long successful single-line output behind details by default', () => {
render(() => <ToolExecutionBlock tool={makeTool({ output: 'x'.repeat(160) })} />);
const preview = screen.getByLabelText('Tool output preview');
expect(preview).toHaveTextContent(/\.\.\.$/);
expect(preview.textContent || '').not.toHaveLength(160);
expect(screen.queryByText('160 chars output')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Tool output preview')).not.toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 160 chars output')).toHaveTextContent(
'output available',
);
});
it('previews failed plain-text output while keeping full details available', () => {
@@ -562,7 +572,10 @@ describe('ToolExecutionBlock', () => {
expect(screen.getByText('Inspect devices on current resource')).toBeInTheDocument();
expect(screen.getByLabelText('Tool command')).toHaveTextContent('$ ls /dev | wc -l');
expect(screen.getByLabelText('Tool output preview')).toHaveTextContent('42');
expect(screen.queryByLabelText('Tool output preview')).not.toBeInTheDocument();
expect(screen.getByLabelText('Tool output available: 2 chars output')).toHaveTextContent(
'output available',
);
});
it('hides output that is only whitespace', () => {