From 5624ed8e66fa751fdc76868b41375159692a1926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 20:38:16 +0000 Subject: [PATCH] dismiss background task chips one at a time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the tray only had a bulk clear, so getting rid of one finished chip meant clearing all of them. each finished chip now carries its own close control. the pill becomes a div wrapping two buttons — a button nested inside a button is invalid and the browser eats one of the two clicks. running chips stay undismissable: the tray is the only handle on work still going. --- .../Chat/components/BackgroundTaskTray.tsx | 68 +++++++++++++------ .../src/apps/Chat/useBackgroundTasks.ts | 13 +++- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx b/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx index c0c00ae9..28d0b577 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/BackgroundTaskTray.tsx @@ -28,7 +28,7 @@ function statusTone(status: BackgroundTask['status']): Tone { * tail of a backgrounded shell's log, both read from the file Claude Code streams the task into. */ export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => { - const { tasks, running, dismissFinished } = useBackgroundTasks(messages); + const { tasks, running, dismiss, dismissFinished } = useBackgroundTasks(messages); const [openId, setOpenId] = useState(null); // Derived, not stored: dismissing the chip that is open closes the panel without a second piece of state @@ -54,6 +54,9 @@ export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => { task={task} active={task.taskId === openId} onClick={() => setOpenId(task.taskId === openId ? null : task.taskId)} + // Finished only. A running chip is the only handle on work still going on, so closing one + // would hide something you cannot get back to — the bulk clear draws the same line. + onDismiss={task.status ? () => dismiss(task.taskId) : undefined} /> ))} @@ -61,8 +64,8 @@ export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => { + {onDismiss && ( + // Always drawn, not revealed on hover: the strip is as much a touch target as a pointer one, and + // a control you can only find by hovering is one half the devices cannot find at all. + )} - {task.description || 'Background task'} - + ); }; diff --git a/src/workspaces/officerdev/src/apps/Chat/useBackgroundTasks.ts b/src/workspaces/officerdev/src/apps/Chat/useBackgroundTasks.ts index 788e7e7c..2589bec0 100644 --- a/src/workspaces/officerdev/src/apps/Chat/useBackgroundTasks.ts +++ b/src/workspaces/officerdev/src/apps/Chat/useBackgroundTasks.ts @@ -56,8 +56,17 @@ export function useBackgroundTasks(messages: ChatMessage[]) { return { tasks: visible, running, - /** Clear the finished chips. Running ones are never dismissable — they'd leave nothing watching them. */ - dismissFinished: () => setDismissed(tasks.filter((t) => t.status).map((t) => t.taskId)), + /** + * Close one chip. Only ever called for a finished task — running ones are not dismissable, because the + * tray is the only thing watching them and a hidden running task is one you have no way back to. + * + * Dismissing the newest of a capped set promotes the next-oldest into view rather than leaving a gap: + * `FINISHED_KEPT` is how many finished chips are shown, not which ones survive. + */ + dismiss: (taskId: string) => setDismissed((prev) => (prev.includes(taskId) ? prev : [...prev, taskId])), + /** Clear every finished chip at once. Additive, so chips closed one at a time are not resurrected. */ + dismissFinished: () => + setDismissed((prev) => [...new Set([...prev, ...tasks.filter((t) => t.status).map((t) => t.taskId)])]), }; }