Skip to content

feat(ui,host,desktop): mark Inbox items read and unread - #3633

Merged
luke-agent[bot] merged 2 commits into
mainfrom
luke/feat-inbox-read-unread
Oct 18, 2026
Merged

luke-agent[bot] merged 2 commits into
mainfrom
luke/feat-inbox-read-unread

Conversation

@dastratakos

@dastratakos dastratakos commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR 2 of 3 for the Inbox list rows in docs/proposals/inbox-threads.md §3–§4, approved by Dean. It builds on #3612 (the two-line rows) and #3618 (the service's per-member read state), both merged. PR 3, the two-stage swipe, stacks on this one.

Unread (tasks/inbox-list.tsx). An item is unread when it changed since the reader last read it, or when they marked it unread. An unread item shows:

  • the blue dot in a left gutter before its tile;
  • its title in semibold;
  • its preview in the body's ink instead of muted;
  • "Unread" spoken to a screen reader.

The gutter stands on every row, so the tiles line up whether or not a row has a dot. Needs you stays the orange attention mark, independent of unread.

Ways to mark. Each one sends one act and flips the dot at once (tasks/use-inbox-read-marks.ts).

  • Opening an item marks it read, once each time it's shown. That includes the item that stands chosen when the Inbox opens. If you mark the shown item unread, it stays unread until you open it again, as Mail does.
  • Hover button in the time's slot. It reads Lucide MailOpen "Mark as read" on an unread item and Mail "Mark as unread" on a read one. While the row is hovered or holds the keyboard, the time and the attention mark step aside, as Pin and Archive do on a sidebar row. It reuses the plans' markReadItem.
  • Right-click menu. "Mark as read" or "Mark as unread" with U beside it, above Copy link.
  • Plain U toggles the chosen item. It's a new APP_COMMAND.TOGGLE_INBOX_ITEM_READ with { key: U, plain: true }, so it's skipped while typing, like J/K. ⇧⌘U stays Automations'.
  • View options › Mark all as read, in its own group under the groupings. It marks the listed workspace's items and clears dots only; attention marks stay. It's disabled when nothing is unread.

How the marks hold (withReadMarks in inbox-model.ts, use-inbox-read-marks.ts).

  • A mark lies over the reads until they hold the item the way the mark left it. Then the mark is dropped, so a later read, including another window's mark, is what the row draws.
  • Marks go to the service one at a time, in click order. If an item is marked again before its last mark goes, only the latest is sent. Mark all as read takes its turn in the same queue.
  • If the service refuses a mark, the mark is dropped.
  • Once the service takes Mark all as read, every listed plan and every agent with no plan is read again, because that write changes nothing that main hears about.
  • U, and marking an item read when it opens, both stand down while the side panel holds the keys or fills the window.

A task's own Inbox tab draws the same rows with the same marks. It has no view menu, so it doesn't offer Mark all as read.

The Factory (desktop-factory.tsx). Its rows and summary now show the attention mark for "waits on you" instead of the blue .inbox-row-dot, so blue means unread everywhere. The orchestrator agreed to this. .inbox-row-dot and its CSS are gone.

Host acts (packages/host, packages/gateway). Each has one ACT_KIND entry, a payload schema, a result guard, and one router row, over #3618's client methods:

@luke/ui ListRow

  • New actionsAt: LIST_ROW_ACTIONS.HOVER, which puts the row's actions over the end of its metadata, shown on :hover and :has(:focus-visible).
  • unread now means a semibold title and the description in the body's ink. Nothing else used ListRow's unread.

Screenshots

Rendered from the jsdom fixtures with the built apps/desktop/dist/renderer/styles.css, captured with Playwright.

Dark Light
Unread rows dark light
Hover: the mark in the time's slot dark light
View options › Mark all as read dark light

Evidence

  • pnpm check:affected: lint, knip, and discarded-effect pass.
    • Two failures came from sandbox memory, and both pass when run alone:
      • @luke/desktop typecheck was OOM-killed (exit 137); alone, it reports 0 errors.
      • issue-client.test.ts timed out under load; alone, it passes 4/4.
    • src/main/app-menu.test.ts can't run here because the sandbox has no Electron binary.
  • After rebasing onto main past feat(service,features,database): keep each member's read state on Inbox items #3618: typecheck reports 0 errors in features, host, and desktop.
  • Nine suites, 569 tests, all pass: hosted-wire-schemas, coding-agent-acts, compose-coding-agents, desktop-inbox, desktop-tasks, desktop-factory, desktop-home, inbox-model, and list-row.
  • JSON Schema goldens: one new file, coding-agent-view-codingAgentInboxReadStateParamsSchema.json ({ itemId: non-empty string, unread: boolean }, both required, no extra keys), written under LUKE_UPDATE_FIXTURES=1. No existing golden changed.
  • Test count: +15 (six from the review fixes are at the end of this list).
    • desktop-inbox.test.ts, +5:
      • the unread look, and opening an item marks it read;
      • the hover button, the menu item with U above Copy link, and plain U;
      • marking the shown item unread keeps it unread until it's reopened;
      • a refused mark is dropped;
      • Mark all as read: its placement, workspace, dots only, and disabled when nothing is unread.
    • desktop-tasks.test.ts, +1: a task's own Inbox marks what it opens read and offers the row mark.
    • inbox-model.test.ts, +1: withReadMarks holds until the item changes.
    • coding-agent-acts.test.ts, +1: both acts route, and the voice window is refused.
    • hosted-wire-schemas, +1: the recorded params schema.
    • compose-coding-agents.test.ts: the existing round-trip test now covers both gateway methods.
    • From Devin's review (+6):
      • use-inbox-read-marks.test.ts (+3): a mark stands until the reads catch up, after which another window's mark is drawn; marks go one at a time in order, with an item's latest mark winning and Mark all as read taking its turn; a refused mark, or a refused Mark all as read, is dropped and asks nothing again.
      • use-inbox-items.test.ts (+1): onReread reads every listed plan again.
      • desktop-inbox.test.ts (+1): Mark all as read re-reads every plan and every agent with no plan.
      • inbox-side-panel.test.ts (+1): U is not the Inbox's while the panel holds the keys.
    • desktop-factory.test.ts: rows() now requires the attention mark with "Needs you" and no blue dot on every waiting row, and the summary counts beside the mark.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI) One new golden, explained above; no existing one changed.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root AGENTS.md "Testing")
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives)
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed. (manual: vitest run --reporter=json)
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording)
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module (@effect/platform-node, effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderer node: grep plus a new grep for those specifiers in renderer bundles)

Notes

  • Read titles keep the body's primary ink. The design show draws them a step quieter, but the dot, the semibold weight, and the brighter preview already set unread rows apart clearly.
  • Home's Inbox and a person's page draw the unread dot from the item, but don't offer the hover button or the menu mark.
  • Blockers: none. PR 3, the two-stage swipe, follows on this branch.

Made with Luke


Devin Review


Summary by cubic

Adds read and unread states to Inbox item rows: an item changed since the reader last read it, or marked unread, wears a blue dot, a semibold title, and a body-ink preview until opened. Opening an item marks it read; a hover button, right-click menu item, plain U, and View options › Mark all as read mark items read or unread.

New Features

  • A mark flips the dot at once, is dropped if the service refuses it, and gives way once a read holds the item as the mark left it, so another window's later mark is drawn. Marks reach the service one at a time in the order made; Mark all as read re-reads every listed plan and agent once taken, since the write moves nothing main hears of.
  • A task's own Inbox tab draws the same marks but omits Mark all as read; Home shows the dot without the mark controls.
  • Factory rows and summary use the attention mark for "needs you", so blue means unread everywhere.
  • @luke/ui ListRow gains LIST_ROW_ACTIONS.HOVER, placing row actions over the metadata's end, and a new unread look.

Written for commit da946ed. Summary will update on new commits.

View guided diff Turn on auto-fix

@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
luke-web Ignored Ignored Preview Oct 18, 2026 5:20pm UTC

Request Review

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>
devin-ai-integration[bot]

This comment was marked as resolved.

…r, and Mark all as read reads again

A read mark stands over the reads only until they hold the item as it
left it, so another window's later mark is drawn. Marks reach the service
one at a time in the order they were made, an item's latest standing for
its own still waiting, Mark all as read in its turn. Once Mark all as read
is taken, every listed plan and agent is read again, since the write
moves nothing main hears of. U and opening an item stand down while the
side panel holds the keys or fills the window.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@luke-agent
luke-agent Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 0c624c2 Oct 18, 2026
11 checks passed
@luke-agent
luke-agent Bot deleted the luke/feat-inbox-read-unread branch October 18, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant