the task tray shows the newest first, and forgets the oldest
Three things about the background-task strip, all the same complaint: the chip you want is the one you cannot see. Newest first, because the strip scrolls horizontally and the far end is off-screen — chronological order put every task you just started exactly where you had to scroll to reach it. Finished chips are capped at five, so a long session stops turning the tray into a history; running ones are never counted or dropped, since watching them is the whole point. And the default horizontal scrollbar is ~15px tall and laid out inside the row, which is what made the chips read as squeezed — it is thin now, and the row and chips have the padding back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@@ -123,12 +123,12 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Duck brand hex colors */
|
||||
--duck-yellow: #F4C430;
|
||||
--duck-orange: #EA580C;
|
||||
--duck-teal: #0891B2;
|
||||
--duck-yellow: #f4c430;
|
||||
--duck-orange: #ea580c;
|
||||
--duck-teal: #0891b2;
|
||||
--duck-forest: #166534;
|
||||
--duck-dark: #14532D;
|
||||
--duck-beige: #E7D4B5;
|
||||
--duck-dark: #14532d;
|
||||
--duck-beige: #e7d4b5;
|
||||
|
||||
/* Page background */
|
||||
--pixel-grid-color: rgba(20, 83, 45, 0.1);
|
||||
@@ -328,14 +328,39 @@
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.file-grid:has([data-state="open"]) [data-file-item]:not([data-state="open"]) {
|
||||
/* Chat — the background-task strip. A default horizontal scrollbar is ~15px tall and is laid out INSIDE
|
||||
the row, so it stole most of the chips' breathing room and made the tray read as squeezed. Thin, and
|
||||
neutral enough to sit on either theme. */
|
||||
.task-strip {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(128 128 128 / 0.3) transparent;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task-strip::-webkit-scrollbar-thumb {
|
||||
background: rgb(128 128 128 / 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.task-strip:hover::-webkit-scrollbar-thumb {
|
||||
background: rgb(128 128 128 / 0.5);
|
||||
}
|
||||
|
||||
.file-grid:has([data-state='open']) [data-file-item]:not([data-state='open']) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-grid:has([data-state="open"]) [data-file-item]:not([data-state="open"]) .file-item-ellipsis {
|
||||
.file-grid:has([data-state='open']) [data-file-item]:not([data-state='open']) .file-item-ellipsis {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.file-grid [data-file-item][data-state="open"] .file-item-ellipsis {
|
||||
.file-grid [data-file-item][data-state='open'] .file-item-ellipsis {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ export const BackgroundTaskTray = ({ messages }: BackgroundTaskTrayProps) => {
|
||||
<div className="mb-2 rounded-lg border border-duck-dark/10 bg-duck-dark/[0.03]">
|
||||
{open && <TaskPanel task={open} onClose={() => setOpenId(null)} />}
|
||||
|
||||
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||
<div className="flex items-center gap-2 px-2 py-2">
|
||||
<span className="shrink-0 text-[10px] uppercase tracking-wider text-duck-dark/40">
|
||||
{running.length > 0 ? `${running.length} running` : 'Background'}
|
||||
</span>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto">
|
||||
{/* pb-1.5 keeps the scrollbar clear of the chips; the matching -mb-1.5 hides that pad from layout,
|
||||
so it draws into the container's own padding and the label and X still centre on the chips. */}
|
||||
<div className="task-strip -mb-1.5 flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto pb-1.5">
|
||||
{tasks.map((task) => (
|
||||
<TaskChip
|
||||
key={task.taskId}
|
||||
@@ -82,7 +84,7 @@ const TaskChip = ({ task, active, onClick }: TaskChipProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs transition-colors cursor-pointer ${
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs transition-colors cursor-pointer ${
|
||||
active
|
||||
? 'border-duck-teal/40 bg-duck-teal/10 text-duck-dark/80'
|
||||
: 'border-duck-dark/10 bg-background/60 text-duck-dark/60 hover:border-duck-dark/20 hover:text-duck-dark/80'
|
||||
|
||||
@@ -14,18 +14,43 @@ export type BackgroundTask = Extract<ChatMessage, { role: 'task' }>;
|
||||
*
|
||||
* Finished tasks stay in the tray until dismissed rather than vanishing on completion: the notification
|
||||
* lands while you are reading something else, and a chip that removes itself the instant it succeeds is a
|
||||
* chip you never saw.
|
||||
* chip you never saw. They do not stay forever, though — see FINISHED_KEPT.
|
||||
*
|
||||
* NEWEST FIRST. The tray is a horizontal strip, so the far end of it is off-screen, and the task you want
|
||||
* is almost always the one that just started. Chronological order put it exactly where you could not see
|
||||
* it without scrolling.
|
||||
*/
|
||||
|
||||
/**
|
||||
* How many finished tasks the tray keeps behind the running ones.
|
||||
*
|
||||
* Enough that a notification you missed is still there when you look up, few enough that the strip does
|
||||
* not become a scrolling history of the whole conversation. Running tasks are never counted or dropped:
|
||||
* the tray's job is watching them, and a busy session could legitimately have more than this many.
|
||||
*/
|
||||
const FINISHED_KEPT = 5;
|
||||
|
||||
export function useBackgroundTasks(messages: ChatMessage[]) {
|
||||
const [dismissed, setDismissed] = useState<string[]>([]);
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
const byId = new Map<string, BackgroundTask>();
|
||||
for (const m of messages) if (m.role === 'task') byId.set(m.taskId, m);
|
||||
return [...byId.values()];
|
||||
return [...byId.values()].reverse();
|
||||
}, [messages]);
|
||||
|
||||
const visible = tasks.filter((t) => !dismissed.includes(t.taskId));
|
||||
const visible = useMemo(() => {
|
||||
let finished = 0;
|
||||
// `tasks` is already newest-first, so counting forwards drops the oldest finished chips and keeps the
|
||||
// ones whose notifications are still recent enough to be worth reading.
|
||||
return tasks.filter((t) => {
|
||||
if (dismissed.includes(t.taskId)) return false;
|
||||
if (!t.status) return true;
|
||||
finished += 1;
|
||||
return finished <= FINISHED_KEPT;
|
||||
});
|
||||
}, [tasks, dismissed]);
|
||||
|
||||
const running = visible.filter((t) => !t.status);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user