mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
fix(conversations): reset agentic task insights override on turn settle (#5008)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user