From 99c92f46f7305968b6963accce5312cc6c9f854c Mon Sep 17 00:00:00 2001 From: Dean Stratakos <29683763+dastratakos@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:25:57 +0000 Subject: [PATCH 1/2] feat(ui,host,desktop): mark Inbox items read and unread An unread Inbox item, one changed since the reader last read it or marked unread, wears the blue dot in a gutter before its tile, its title in the semibold weight and its preview in the body's ink. Opening an item marks it read, once each time it is shown. A row offers Mark as read / Mark as unread (MailOpen / Mail) over its time while hovered or focused, and as the first item of its right-click menu, beside U; a plain U toggles the chosen item; the view's menu gains Mark all as read, which clears the dots alone. A mark flips the dot at once and is let go where the service refuses it. The Factory's rows and summary wear the attention mark for what waits, so blue means unread everywhere. The host gains two acts over the service's read state: codingAgents.inboxReadState ({ itemId, unread }) and codingAgents.inboxReadAll ({ workspaceId? }). ListRow gains LIST_ROW_ACTIONS.HOVER, and its unread look is the semibold title and the description in the body's ink. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com> --- .../renderer/desktop/desktop-factory.test.ts | 23 +- .../src/renderer/desktop/desktop-factory.tsx | 14 +- .../src/renderer/desktop/desktop-home.test.ts | 4 +- .../renderer/desktop/desktop-inbox.test.ts | 211 +++++++++++++++++- .../src/renderer/desktop/desktop-inbox.tsx | 19 +- .../renderer/desktop/desktop-tasks.test.ts | 40 ++++ .../src/renderer/desktop/desktop-tasks.tsx | 16 +- apps/desktop/src/renderer/styles/desktop.css | 46 ++-- .../desktop/src/renderer/tasks/inbox-list.tsx | 122 ++++++++-- .../src/renderer/tasks/inbox-model.test.ts | 27 +++ .../desktop/src/renderer/tasks/inbox-model.ts | 42 ++++ .../renderer/tasks/use-inbox-read-marks.ts | 134 +++++++++++ ...codingAgentInboxReadStateParamsSchema.json | 17 ++ packages/features/src/coding-agent-view.ts | 8 + .../features/src/hosted-wire-schemas.test.ts | 1 + packages/gateway/src/protocol.ts | 4 + .../src/act-router/coding-agent-acts.test.ts | 61 +++++ .../host/src/act-router/coding-agent-acts.ts | 20 ++ packages/host/src/acts.ts | 22 ++ .../host/src/compose-coding-agents.test.ts | 33 +++ packages/host/src/compose-coding-agents.ts | 29 +++ packages/host/src/host-operator.ts | 28 +++ packages/settings/src/shortcuts.ts | 6 + .../ui/src/components/list-row/list-row.css | 46 +++- .../ui/src/components/list-row/list-row.tsx | 23 +- 25 files changed, 927 insertions(+), 69 deletions(-) create mode 100644 apps/desktop/src/renderer/tasks/use-inbox-read-marks.ts create mode 100644 packages/features/fixtures/json-schema/coding-agent-view-codingAgentInboxReadStateParamsSchema.json diff --git a/apps/desktop/src/renderer/desktop/desktop-factory.test.ts b/apps/desktop/src/renderer/desktop/desktop-factory.test.ts index a51bc296d2..cc935e0eed 100644 --- a/apps/desktop/src/renderer/desktop/desktop-factory.test.ts +++ b/apps/desktop/src/renderer/desktop/desktop-factory.test.ts @@ -173,6 +173,23 @@ function mount(listed: readonly TaskSummary[], overrides: Partial return { page: container, opened, redraw, rerender }; } +/** + * Whether a row waits on the developer, as it reads: ` (waits)` where its + * name stands in the unread weight and the attention mark before its time + * says "Needs you", none where neither, and ` (?)` where only one stands + * or it wears the blue dot that now means unread, so a half-drawn mark fails + * every listing. + */ +function waitsOf(row: Element): string { + const weight = row.getAttribute("data-unread") === "true"; + const mark = + row.querySelector(".sidebar-task-time .attention-mark") !== null && + row.querySelector(".sidebar-task-time .visually-hidden")?.textContent === "Needs you"; + if (row.querySelector(".inbox-row-unread, .inbox-row-dot") !== null) return " (?)"; + if (weight && mark) return " (waits)"; + return weight || mark ? " (?)" : ""; +} + /** A station's rows as a reader scans them: each row's words and task, and whether it waits on the developer. */ function rows(page: HTMLElement, step: string): string[] { const region = page.querySelector(`section[data-step="${step}"]`); @@ -180,7 +197,7 @@ function rows(page: HTMLElement, step: string): string[] { return [...region.querySelectorAll(".sidebar-task")].map((row) => { const words = row.querySelector(".sidebar-task-name")?.textContent; const context = row.querySelector(".home-row-context")?.textContent; - const waits = row.getAttribute("data-unread") === "true" ? " (waits)" : ""; + const waits = waitsOf(row); return `${words}${context === undefined ? "" : ` · ${context}`}${waits}`; }); } @@ -213,6 +230,10 @@ test("each plan stands in its station, and what waits on the developer leads the page.querySelector(".factory-summary")?.textContent, "2 need you·1 agent working·0 shipped today", ); + assert.ok( + page.querySelector(".factory-needs .attention-mark"), + "what needs you is counted beside the attention mark", + ); // A draft with no agents stands nowhere; Talk holds only the task on a call. assert.deepEqual(rows(page, "talk"), []); assert.equal( diff --git a/apps/desktop/src/renderer/desktop/desktop-factory.tsx b/apps/desktop/src/renderer/desktop/desktop-factory.tsx index 33d39b834d..dfc3efc999 100644 --- a/apps/desktop/src/renderer/desktop/desktop-factory.tsx +++ b/apps/desktop/src/renderer/desktop/desktop-factory.tsx @@ -36,6 +36,7 @@ import { import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { ACTOR_SIZE, ActorMark, CreatorHeard } from "../actor-ref"; import { useAppCommand } from "../app-commands"; +import { AttentionMark } from "../attention-mark"; import { LINK_COPY, useCopyToast } from "../copy-feedback"; import { agentPlace, LinkWorkspaceProvider, useCopyLinkItem } from "../copy-link-item"; import { ExperimentTag, SendFeedbackButton } from "../experiment-marks"; @@ -345,13 +346,14 @@ function FactoryRow({ {actor === undefined ? null : } {end} + {waiting ? ( + + + {NEEDS_YOU.LABEL} + + ) : null} - {waiting ? ( - - {NEEDS_YOU.LABEL} - - ) : null} @@ -680,7 +682,7 @@ function Summary({ board }: { board: FactoryBoard }): React.JSX.Element { ) : ( -