Skip to content

Commit 08f6b2c

Browse files
committed
fix: prevent task list flash on session restore and during conversation
- Initialize autoHidden to true when all tasks are already completed on mount (e.g. session restore), instead of showing for 5 seconds then hiding - Add shallow comparison in onTasksChange callback to skip state updates when tasks haven't materially changed (same id + status), preventing unnecessary re-renders and autoHidden resets that cause flickering - Update tests to reflect new immediate-hide-on-mount behavior and add dedicated test for the all-completed-on-mount case
1 parent de83d65 commit 08f6b2c

3 files changed

Lines changed: 55 additions & 11 deletions

File tree

packages/code/src/components/TaskList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ export const TaskList: React.FC = () => {
5959
const autoHideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
6060
null,
6161
);
62-
const [autoHidden, setAutoHidden] = React.useState(false);
62+
const [autoHidden, setAutoHidden] = React.useState(() => {
63+
// If all tasks are already completed on mount (e.g. session restore),
64+
// start hidden immediately instead of flashing for 5 seconds.
65+
const active = tasks.filter((t) => t.status !== "deleted");
66+
return active.length > 0 && active.every((t) => t.status === "completed");
67+
});
6368
const [, setTick] = React.useState(0);
6469

6570
const now = Date.now();

packages/code/src/contexts/useChat.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
382382
const runs = agentRef.current?.getWorkflowRuns();
383383
if (runs) setWorkflowRuns([...runs]);
384384
},
385-
onTasksChange: (tasks) => {
386-
setTasks([...tasks]);
385+
onTasksChange: (newTasks) => {
386+
setTasks((prev) => {
387+
if (
388+
prev.length === newTasks.length &&
389+
prev.every(
390+
(t, i) =>
391+
t.id === newTasks[i].id && t.status === newTasks[i].status,
392+
)
393+
) {
394+
return prev;
395+
}
396+
return [...newTasks];
397+
});
387398
},
388399
onPermissionModeChange: (mode) => {
389400
setPermissionModeState(mode);

packages/code/tests/components/TaskList.test.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,42 +232,70 @@ describe("TaskList", () => {
232232

233233
it("should auto-hide after all tasks completed", () => {
234234
vi.useFakeTimers();
235+
// Start with an in-progress task so the list is visible
235236
const mockTasks: Task[] = [
236-
makeTask({ id: "1", subject: "Done 1", status: "completed" }),
237-
makeTask({ id: "2", subject: "Done 2", status: "completed" }),
237+
makeTask({ id: "1", subject: "Working", status: "in_progress" }),
238+
makeTask({ id: "2", subject: "Done", status: "completed" }),
238239
];
239240
vi.mocked(useTasks).mockReturnValue(mockTasks);
240241

241242
const { lastFrame, rerender } = render(<TaskList />);
242243
expect(lastFrame()).toBeTruthy();
243244

245+
// Complete the remaining task
246+
vi.mocked(useTasks).mockReturnValue([
247+
makeTask({ id: "1", subject: "Working", status: "completed" }),
248+
makeTask({ id: "2", subject: "Done", status: "completed" }),
249+
]);
250+
rerender(<TaskList />);
251+
expect(lastFrame()).toBeTruthy();
252+
244253
vi.advanceTimersByTime(5000);
245254
rerender(<TaskList />);
246255
expect(lastFrame()).toBeFalsy();
247256

248257
vi.useRealTimers();
249258
});
250259

251-
it("should reappear when new task added after auto-hide", () => {
252-
vi.useFakeTimers();
253-
const completedTasks: Task[] = [
260+
it("should be hidden immediately when mounted with all completed tasks", () => {
261+
const mockTasks: Task[] = [
254262
makeTask({ id: "1", subject: "Done 1", status: "completed" }),
255263
makeTask({ id: "2", subject: "Done 2", status: "completed" }),
256264
];
257-
vi.mocked(useTasks).mockReturnValue(completedTasks);
265+
vi.mocked(useTasks).mockReturnValue(mockTasks);
266+
267+
const { lastFrame } = render(<TaskList />);
268+
// Should be hidden immediately, no 5-second flash
269+
expect(lastFrame()).toBeFalsy();
270+
});
271+
272+
it("should reappear when new task added after auto-hide", () => {
273+
vi.useFakeTimers();
274+
// Start with in-progress so list is visible initially
275+
const initialTasks: Task[] = [
276+
makeTask({ id: "1", subject: "Working", status: "in_progress" }),
277+
makeTask({ id: "2", subject: "Done", status: "completed" }),
278+
];
279+
vi.mocked(useTasks).mockReturnValue(initialTasks);
258280

259281
const { lastFrame, rerender, unmount } = render(<TaskList />);
260282
expect(lastFrame()).toBeTruthy();
261283

284+
// Complete all tasks → auto-hide after delay
285+
vi.mocked(useTasks).mockReturnValue([
286+
makeTask({ id: "1", subject: "Working", status: "completed" }),
287+
makeTask({ id: "2", subject: "Done", status: "completed" }),
288+
]);
289+
rerender(<TaskList />);
262290
vi.advanceTimersByTime(5000);
263-
// Rerender to pick up the state change from the timer
264291
rerender(<TaskList />);
265292
expect(lastFrame()).toBeFalsy();
266293
unmount();
267294

268295
// Add a new pending task and mount fresh
269296
vi.mocked(useTasks).mockReturnValue([
270-
...completedTasks,
297+
makeTask({ id: "1", subject: "Working", status: "completed" }),
298+
makeTask({ id: "2", subject: "Done", status: "completed" }),
271299
makeTask({ id: "3", subject: "New Task", status: "pending" }),
272300
]);
273301
const { lastFrame: lastFrame2 } = render(<TaskList />);

0 commit comments

Comments
 (0)