fix(conversations): reset agentic task insights override on turn settle (#5008)

This commit is contained in:
Cyrus Gray
2026-07-17 17:11:30 +05:30
committed by GitHub
parent 1cb1861f96
commit 34b0fbff5d
2 changed files with 129 additions and 7 deletions
@@ -1,5 +1,5 @@
import createDebug from 'debug';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import WorktreeActions from '../../../components/worktree/WorktreeActions';
import { useT } from '../../../lib/i18n/I18nContext';
@@ -499,6 +499,26 @@ export function ToolTimelineBlock({
// turn), so a plain `useState` already survives every turn it needs to.
const [userOverrideOpen, setUserOverrideOpen] = useState<boolean | null>(null);
// Whether *any* entry is currently running — computed here (ahead of the
// `entries.length === 0` early return below) purely so the reset effect
// that follows is an unconditional hook call every render; order doesn't
// matter for this existence check, unlike `latestRunningEntryId` further
// down, which needs the seq-sorted order to pick a specific "latest" row.
const isRunning = entries.some(entry => entry.status === 'running');
// Reset the user's manual open/close override on the running→settled edge
// (a turn just finished) so the auto-collapse applies to the just-settled
// turn. The override only sticks WITHIN a turn — preventing involuntary
// mid-feedback collapse (#4942) — not permanently across turns.
const prevIsRunningRef = useRef(false);
useEffect(() => {
if (prevIsRunningRef.current && !isRunning) {
log('agent-task-insights: turn settled (running→done), resetting user override');
setUserOverrideOpen(null);
}
prevIsRunningRef.current = isRunning;
}, [isRunning]);
if (entries.length === 0) return null;
// The rows + the parent's streaming response — shared by both the collapsible
@@ -513,7 +533,6 @@ export function ToolTimelineBlock({
// but sorts earlier (e.g. seq [2, 0, 1]) gets treated as "latest" and the
// wrong step stays expanded/linked in compact chat mode.
const latestRunningEntryId = [...ordered].reverse().find(entry => entry.status === 'running')?.id;
const isRunning = latestRunningEntryId != null;
const titleLabel = (
<span className="text-[13px] font-medium text-content-muted">
@@ -396,7 +396,7 @@ describe('ToolTimelineBlock — agentic task insights surface', () => {
// earlier. These tests simulate that same "new turn's entries land on an
// already-mounted block" shape via `rerender` rather than remounting.
describe('agentic task insights — sticky user expand/collapse across turns', () => {
it('keeps an explicit user expand across a new turn that starts and settles', () => {
it('resets a user expand when a new turn settles', () => {
const turn1Settled: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'success' },
];
@@ -408,7 +408,9 @@ describe('ToolTimelineBlock — agentic task insights surface', () => {
fireEvent.click(screen.getByText('Agentic task insights'));
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// A new turn/feedback starts streaming onto the SAME mounted block.
// A new turn/feedback starts streaming onto the SAME mounted block. The
// override still wins WHILE it runs — the user's choice isn't clobbered
// mid-turn (#4942).
const turn2Running: ToolTimelineEntry[] = [
...turn1Settled,
{ id: 't2', name: 'file_read', round: 2, seq: 1, status: 'running' },
@@ -420,8 +422,9 @@ describe('ToolTimelineBlock — agentic task insights surface', () => {
);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// ...and settles. Without the fix this is exactly where it would
// involuntarily re-collapse, wiping out the user's choice.
// ...and settles. The override only sticks WITHIN a turn — once this
// turn finishes, the auto-collapse applies to it, so the panel
// collapses instead of permanently overriding every future turn.
const turn2Settled: ToolTimelineEntry[] = [
...turn1Settled,
{ id: 't2', name: 'file_read', round: 2, seq: 1, status: 'success' },
@@ -431,7 +434,7 @@ describe('ToolTimelineBlock — agentic task insights surface', () => {
<ToolTimelineBlock entries={turn2Settled} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
});
it('leaves the default open-while-running/collapsed-when-settled behaviour unchanged absent any user interaction', () => {
@@ -476,6 +479,106 @@ describe('ToolTimelineBlock — agentic task insights surface', () => {
);
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
});
it('auto-collapses when a turn finishes even if the user had expanded it', () => {
const turn1Settled: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'success' },
];
const { rerender } = renderInStore(<ToolTimelineBlock entries={turn1Settled} />);
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
// User expands the settled turn1 panel.
fireEvent.click(screen.getByText('Agentic task insights'));
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// A new turn starts running — stays open (both the override and the
// auto rule agree here).
const turn2Running: ToolTimelineEntry[] = [
...turn1Settled,
{ id: 't2', name: 'file_read', round: 2, seq: 1, status: 'running' },
];
rerender(
<Provider store={store}>
<ToolTimelineBlock entries={turn2Running} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// It settles — the override is cleared on this running→settled edge,
// so the panel auto-collapses instead of staying pinned open forever.
const turn2Settled: ToolTimelineEntry[] = [
...turn1Settled,
{ id: 't2', name: 'file_read', round: 2, seq: 1, status: 'success' },
];
rerender(
<Provider store={store}>
<ToolTimelineBlock entries={turn2Settled} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
});
it('does not collapse mid-stream when new entries arrive on a running turn', () => {
const turn1Running: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'running' },
];
const { rerender } = renderInStore(<ToolTimelineBlock entries={turn1Running} />);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// More entries stream in while the turn is still running — the
// running→settled edge never fires, so the panel must stay open.
const turn1StillRunning: ToolTimelineEntry[] = [
...turn1Running,
{ id: 't1b', name: 'file_read', round: 1, seq: 1, status: 'running' },
];
rerender(
<Provider store={store}>
<ToolTimelineBlock entries={turn1StillRunning} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
const turn1MoreRunning: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'success' },
{ id: 't1b', name: 'file_read', round: 1, seq: 1, status: 'running' },
];
rerender(
<Provider store={store}>
<ToolTimelineBlock entries={turn1MoreRunning} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
});
it('respects a manual expand during an active turn, resets on settle', () => {
const turn1Running: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'running' },
];
const { rerender } = renderInStore(<ToolTimelineBlock entries={turn1Running} />);
// Auto-open while running (no override yet).
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// User explicitly collapses it mid-run...
fireEvent.click(screen.getByText('Agentic task insights'));
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
// ...then explicitly re-expands it — a manual expand during the still-
// active turn — and it must stick while the turn keeps running.
fireEvent.click(screen.getByText('Agentic task insights'));
expect(screen.getByTestId('agent-task-insights')).toHaveAttribute('open');
// The turn settles — the manual override resets on this edge, and
// since the run is done the auto rule collapses the panel.
const turn1Settled: ToolTimelineEntry[] = [
{ id: 't1', name: 'web_search', round: 1, seq: 0, status: 'success' },
];
rerender(
<Provider store={store}>
<ToolTimelineBlock entries={turn1Settled} />
</Provider>
);
expect(screen.getByTestId('agent-task-insights')).not.toHaveAttribute('open');
});
});
it('renders the tool result output inside the expanded row', () => {