diff --git a/app/sidebar.css b/app/sidebar.css index 0b8f66a69e..a5251aa139 100644 --- a/app/sidebar.css +++ b/app/sidebar.css @@ -174,6 +174,10 @@ background: #0891b2; } +.session-tree-pinned-dot.is-awaiting { + background: #d97706; +} + /* Session row: title, branch, time; actions take the time's place on hover. Hover and selection are a rounded box inset from the sidebar's edges, never a full-width band. The title starts in the text column, @@ -654,6 +658,7 @@ a.session-tree-main { } .session-tree-summary-running, +.session-tree-summary-awaiting, .session-tree-summary-unread { display: inline-flex; align-items: center; @@ -672,6 +677,10 @@ a.session-tree-main { background: #0891b2; } +.session-tree-summary-dot.is-awaiting { + background: #d97706; +} + .session-tree-group-actions { display: none; flex: none; diff --git a/components/SessionTree.test.mjs b/components/SessionTree.test.mjs index fe38e40509..5d8489f123 100644 --- a/components/SessionTree.test.mjs +++ b/components/SessionTree.test.mjs @@ -106,14 +106,14 @@ function sessionRow(root, { context = "group", status = {}, archivedAt = null, r function groupRow(extra = {}, projectExtra = {}) { const groupProject = { ...project, ...projectExtra }; - return { kind: "group", key: `group:${groupProject.key}`, project: groupProject, expanded: true, running: 0, unread: 0, ...extra }; + return { kind: "group", key: `group:${groupProject.key}`, project: groupProject, expanded: true, running: 0, awaiting: 0, unread: 0, ...extra }; } const footer = [{ kind: "footer-open", key: "footer-open" }]; test("the pinned header toggles the section and shows activity dots only while collapsed", () => { const collapsed = rowMarkup(render({ - rows: [{ kind: "pinned-header", key: "pinned-header", count: 3, collapsed: true, running: 1, unread: 2 }], + rows: [{ kind: "pinned-header", key: "pinned-header", count: 3, collapsed: true, running: 1, awaiting: 0, unread: 2 }], }), "pinned-header"); // A group header's height: both are section headers. assert.match(collapsed, /^
/); @@ -123,16 +123,37 @@ test("the pinned header toggles the section and shows activity dots only while c assert.doesNotMatch(collapsed, /session-tree-group-icon/); assert.match(collapsed, /class="session-tree-pinned-dot is-running" role="img" title="Agent running…" aria-label="Agent running… \(1\)"/); assert.match(collapsed, /class="session-tree-pinned-dot is-unread" role="img" title="New session activity" aria-label="New session activity \(2\)"/); + assert.doesNotMatch(collapsed, /session-tree-pinned-dot is-awaiting/); assert.doesNotMatch(collapsed, /session-tree-chevron is-open/); const expanded = rowMarkup(render({ - rows: [{ kind: "pinned-header", key: "pinned-header", count: 3, collapsed: false, running: 1, unread: 2 }], + rows: [{ kind: "pinned-header", key: "pinned-header", count: 3, collapsed: false, running: 1, awaiting: 1, unread: 2 }], }), "pinned-header"); assert.match(expanded, /aria-expanded="true"/); assert.match(expanded, /session-tree-chevron is-open/); assert.doesNotMatch(expanded, /session-tree-pinned-dot/); }); +test("a collapsed pinned header shows an awaiting dot of its own, without the running one", () => { + const collapsed = rowMarkup(render({ + rows: [{ kind: "pinned-header", key: "pinned-header", count: 2, collapsed: true, running: 0, awaiting: 2, unread: 0 }], + }), "pinned-header"); + assert.match(collapsed, /class="session-tree-pinned-dot is-awaiting" role="img" title="Agent waiting for your input" aria-label="Agent waiting for your input \(2\)"/); + assert.doesNotMatch(collapsed, /session-tree-pinned-dot is-running/); +}); + +test("a collapsed group shows awaiting families apart from running ones", () => { + const mixed = rowMarkup(render({ rows: [groupRow({ expanded: false, running: 1, awaiting: 2, unread: 0 })] }), "group:/work/app"); + assert.match(mixed, /aria-label="Agent running… \(1\)"/); + assert.match(mixed, /<\/span>2<\/span>/); + + // Only awaiting: the amber count, no spinner. + const awaiting = rowMarkup(render({ rows: [groupRow({ expanded: false, running: 0, awaiting: 1, unread: 0 })] }), "group:/work/app"); + assert.match(awaiting, /session-tree-summary-awaiting/); + assert.doesNotMatch(awaiting, /session-tree-summary-running/); + assert.doesNotMatch(awaiting, /sidebar-spin/); +}); + test("a collapsed group shows its running and unread counts and labelled actions", () => { const html = rowMarkup(render({ rows: [groupRow({ expanded: false, running: 2, unread: 1 }, { pinned: true, current: false })], diff --git a/components/SessionTree.tsx b/components/SessionTree.tsx index f96a9245e5..ec1e8a9ab2 100644 --- a/components/SessionTree.tsx +++ b/components/SessionTree.tsx @@ -811,11 +811,12 @@ function DeleteConfirm({ } /** - * Spinner + count and dot + count; the label carries the count for screen - * readers. Also a project's badge in the project menus (ProjectWorktreePicker). + * Spinner + count, amber dot + count (awaiting input) and dot + count; the + * label carries the count for screen readers. Also a project's badge in the + * project menus (ProjectWorktreePicker), which counts no awaiting. */ -export function ActivitySummary({ running, unread, t }: { running: number; unread: number; t: (key: string) => string }) { - if (running === 0 && unread === 0) return null; +export function ActivitySummary({ running, awaiting = 0, unread, t }: { running: number; awaiting?: number; unread: number; t: (key: string) => string }) { + if (running === 0 && awaiting === 0 && unread === 0) return null; return ( {running > 0 && ( @@ -829,6 +830,17 @@ export function ActivitySummary({ running, unread, t }: { running: number; unrea {running} )} + {awaiting > 0 && ( + + + {awaiting} + + )} {unread > 0 && ( {project.name} {project.pinned && } - {!expanded && } + {!expanded && }