The project switcher is gone. One list holds every session across every project, grouped by what it wants from you, filtered and scoped from the header, and parked with settle or snooze when it's done. This page is designed phone-first because conduit is a PWA you check from your pocket; the desktop layout is the same system with hover and keyboard added on top, never a different product.
Status tiers are the outer grouping because that is what the product is for: the list answers what wants me right now before it answers anything else. Scope, filter and group all live in the header. Settle and snooze are the only things that collapse a row, and both are reachable three ways: a swipe, a sheet, and a keyboard shortcut.
Scope chip sits inside the search field, so the two controls that answer "which sessions" share one 38 px row. Filter chips scroll horizontally and scroll away with the list; search stays pinned.
Tap a row to open. Everything else is a swipe, a long press, or the ⋯ in select mode. New session is a 44 px circle, the touch floor and no more: it is a frequent action, not the loudest thing on screen, and it shrinks to a + in the header on desktop where the pointer is precise.
Scroll past Idle and the two parking shelves sit at the bottom, collapsed by default and remembered per device. Settled rows are 40 px, half-ink, glyph desaturated, and their time is when they settled. Rows the server settled say so: Auto · 3d.
A settled shelf costs seven pixels of chrome until you ask for it, however much is parked in there. It is never counted: see §2.
A short swipe right holds a tray: Settle at the outer edge, Unread beside the row. Carry on past halfway and Settle takes the tray and commits on release. On an unread row the tray offers Read instead. A settled row's tray is Un-settle alone, so the gesture never means two things.
Every swipe has a twin in the long-press sheet and a keyboard shortcut. Nothing is gesture-only.
Long press is the discovery surface: every verb the row supports, in one place, with its desktop shortcut shown so the two stay learnable together. The header repeats the row so you know what you are acting on.
The header gives the title its own two lines, then puts project, branch, length and last activity on a second row as separate objects, because a title squashed against conduit · main · 27 messages is unreadable at a glance.
The top group is the paired verbs, in the order every menu uses: Settle, Snooze, Pin, Mark unread, with Auto-settle when idle under Settle. Each flips in place. This row is unread, so it offers Mark read. On a settled row Settle flips to Un-settle, and Snooze and Mark unread disappear.
Presets are computed from now and show the wall-clock time they resolve to, so you never do the arithmetic. This evening only appears while it is meaningfully before 18:00. Anything that outranks the snooze, an approval, a question, a fresh error, or a turn finishing, wakes the session early.
You can snooze a running session. You can't snooze one that is blocked on you.
Settle, snooze, archive and delete all land a toast with a five-second undo, because all four remove a row from where you were looking. Mark unread lands one too, because it moves a row you may not be looking at. The toast sits above the safe area and never covers the last row.
On a phone the toast sits on the screen; on desktop, at the foot of the sidebar. Undo is also ⌘Z on desktop, for the same five seconds.
Long press then Select, or the ⋯ menu, puts the list in selection mode: checkboxes replace status glyphs, and a bottom bar carries the four verbs. This is how you clear a week of finished work without 30 swipes.
Disabled verbs stay visible at 35% rather than disappearing, so the bar never reflows.
Tapping search takes the whole screen, because on a phone a search field with a list under it is just a smaller list. Scope and filters become removable tokens above the results, so the state you're in is always spelled out rather than implied by a highlighted icon.
Results are flat and ranked, not tiered: when you search you already know what you want.
Status, project, activity and lifecycle in one sheet with live counts, and a single Reset. Filters are additive with the scope chip and both write to the URL, so a filtered list is a shareable link and the back button undoes a filter.
Settled and snoozed are lifecycle switches here, not statuses, because they cut across every status.
Status tiers are the default because they are the point of the product, but grouping is one tap away: none, project, or time. Sort is separate and deliberately static by default, so rows never move under your thumb while you read.
Group and sort both persist per device and go in the URL.
Opening a settled session says so in the bar: a state chip after the title. Sending a message un-settles it, which is the path most people take, so the composer says so. The chip is the explicit way out, for when you want the session back without saying anything yet.
There is no banner above the composer. The chip covers Settled, Auto-settled, Snoozed, Woke and Unread. A snoozed session's placeholder reads Message to wake…. Un-settling returns the session to the top of its tier and pauses auto-settle until there is new activity.
When and why it settled, the verb that flips it, and the setting that did it, in one small menu anchored to the chip. The verb follows the chip: Un-settle, Wake now, or Mark read.
Auto-settle when idle is always here on a settled session, not a one-time offer. It is the same checkbox as the long-press sheet.
Mark unread in the session's ⋯, or ⌘⇧U, marks it and pops to the list. The toast lands there, next to the row it changed. The row is Done, unread: the same row fresh output makes.
Desktop keeps the session open instead, because the list is already on screen. See §5.
A woken session comes back in the slot it left, because the sort is static and moving it would be a second surprise. The Woke pill is the entire signal and survives until you visit. If it woke early, the pill says why.
Pills use saturated text on a tinted ground, never a light fill.
The empty state is the product's whole argument: settle exists so this screen is reachable. It names what is still running so "nothing wants you" never reads as "nothing is happening", and it offers the shelf you just filled.
Reached by settling, not by hiding. No auto-collapse, no "smart" truncation.
The icon rail was a desktop habit. On a 340 px screen it costs 13% of the width permanently, carries no legible counts, labels projects with two-letter squares instead of names, and quietly rebuilds the container we just deleted. Seven replacements, judged on the same phone, and the decision is A with C living inside it.
The chip and the token are the same state rendered twice. Tap the chip and you get a picker; type project: and you get the identical token. Whichever you use, the result is a removable chip in the field and ?p=conduit in the URL, so the two can never disagree and nobody has to learn a syntax to use the product.
One 38 px row answers both "which sessions" questions. The chip is a word with a real target, not a glyph, and tapping it opens the picker with counts and live status.
Nothing here hints at syntax, because at rest nobody needs it.
Focus the field and the chip becomes the first token. Tapping a project in the picker writes project:conduit; typing it writes the same chip. Suggestions are grouped so the syntax is taught by use, never required.
Backspace on an empty field removes the last token, which is the same thing as tapping its ✕.
project:conduit
DFdotfiles4
◫All projects18
branch:is:settledmodel:since:Scope and search are the same question asked twice, so they share one row. The chip reads as a word, not a glyph, and expands to a searchable picker with counts and live status dots.
One row of chrome for both controls. The chip is a 34 px target with a readable name, it survives twelve projects because the picker scrolls and searches, and it collapses to the same combobox on desktop with ⌘P. Scope lands in the URL as ?p=conduit, so back undoes it.
The rail rotated 90°. Chips carry names and counts, one tap switches, and a dot marks projects that want you.
Zero-tap switching and honest counts, but it needs a second chip row for status filters and now 78 px of header is chips. Beyond five or six projects the useful ones live off-screen with no hint they exist, which is the rail's flaw with extra steps.
There is no scope control. You type project:conduit, or tap a suggestion, and the scope becomes a removable token in the field.
The most expressive option and the only one that composes freely, but on touch the keyboard eats half the screen and nobody discovers a syntax they were never shown. It also can't answer "which projects exist" at rest.
iOS Mail's pattern. No separate control at all: the screen title states the scope and taps open the picker.
The biggest target of the seven and beautifully quiet, and scoping frees the row's second line to show branch and model instead of the project name. But it pushes search behind an icon, and a 22 px title that changes identity is a strange thing to dock into a desktop sidebar.
Edge-swipe or hamburger reveals projects, saved filters and settings, Gmail-style.
It scales to fifty projects and everyone already knows it, which is exactly the trap: a drawer is a container, and a container is what we set out to remove. Within a week the drawer holds saved views and the list stops being one list.
Horizontally paged tabs, All first, one project per page, with a thin indicator.
Fast and native-feeling right up until it collides head-on with row swipe, which is the primary verb of the whole lifecycle. Tab bars also cap out around five projects and hide the count that matters.
Group by project when you want project structure; otherwise search. Scope is emergent, not a mode.
The least chrome of any option and the collapsed headers keep every project's urgency visible. But "show me only conduit for the next ten minutes" becomes a round trip through the group menu, and status tiers, the thing the product is actually for, get demoted to a second-level grouping.
The old row assumed a pointer: its actions lived on hover and its height was tuned for a 272 px sidebar. This one is designed at 52 px on a phone and subtracts to 52 px on desktop, where hover and keyboard add affordances that touch reaches by gesture and sheet instead. No action exists on one platform only.
<a href> per row, never a div with a click handler, so long-press-to-share, middle-click and drag-to-another-window all work for free.aria-label that reads status first: “Needs approval, Fix auth token refresh, conduit, 2 minutes ago”.aria-live="polite" region for the whole list, announcing only tier-count changes, so a working session doesn't chatter.<button>s to keyboard users.prefers-reduced-motion drops the swipe spring and the spinner's dash rotation.The rule the table enforces: three ways, always. Anything you can do to a session is reachable by a gesture, by a visible control, and by a key. If a row only has an action on hover it does not exist on a phone, and if it only has a swipe it does not exist to a keyboard.
| Action | Touch | Pointer | Key | Also |
|---|---|---|---|---|
| Open session | Tap row | Click row | ↑ ↓ then ↵, or ⌘1–9 | Push notification, search result |
| New session | FAB, bottom right | + in header | ⌘N | Empty state, ⌘K |
| Search titles | Tap the field, goes full screen | Click the field | / or ⌘F | URL ?q= |
| Search conversations | “Search inside conversations” under results | Same, or ⌘K | ⌘K | Full-text across projects |
| Scope to a project | Scope chip → picker sheet | Chip → popover, type to filter | ⌘P | project: token, URL ?p= |
| Filter by status | Chip row, or the filter sheet | Chip row | ⌘⌥1–5 | is: tokens, URL ?f= |
| Group & sort | ⇅ in header → sheet | ⇅ menu | ⌘⌥G | URL ?g=, per-device default |
| Settle | Full swipe right, or short swipe → Settle, or long press → Settle | Hover ✓, replaces the time | s or ⌘⇧S | Bulk bar, drag onto the Settled header, auto after 3 days unless you turn off Auto-settle when idle. Settling keeps unread. |
| Un-settle | Swipe right on a settled row; the tray is Un-settle alone | Hover ↶ | s or ⌘⇧S | State chip in the session bar, or send a message. Pauses auto-settle until new activity. An unread session comes back unread. |
| Snooze | Swipe left → presets; full swipe uses your default | Hover ☾ → presets | z | Long-press sheet, bulk bar |
| Wake now | Swipe a snoozed row | Hover ☾✕ | z | State chip, or send a message. Automatic on approval, question, fresh error, or turn end |
| Pin / unpin | Long press → Pin | Hover ⚑ | p or ⌘⇧P | Drag into the Pinned shelf |
| Mark unread | Short swipe right → Unread, or long press | Hover ●, ⋯ or right-click | u or ⌘⇧U | Session ⋯. A phone goes back to the list; desktop stays and holds it unread. Offered on rows that need you, not on settled or snoozed rows. |
| Mark read | Short swipe right → Read, or long press | Hover ○, ⋯ or right-click | u or ⌘⇧U | The state chip's popover, while a session is held unread. Opening the session does it too. |
| Auto-settle when idle | Long press → checkbox under Settle | ⋯ or right-click → checkbox | None, it is a setting | State chip popover on a settled session. On by default, per session. |
| Rename | Long press → Rename | Double-click the title | r on a focused row | Regenerate title in the same sheet |
| Fork from here | Long press → Fork | ⋯ menu | f | From a message in the transcript |
| Archive | Long press → Archive | ⋯ menu | e | Leaves the list entirely; lives in Settings → Archived |
| Delete | Long press → Delete, in red | ⋯ menu | ⌫ | Confirm, then a 5 s undo |
| Select several | Long press → Select, then tap rows | Click a checkbox, ⇧-click for a range | x toggles, ⇧↑↓ extends | Bulk bar carries settle, snooze, pin, archive |
| Undo | Toast, 5 seconds | Toast, 5 seconds | ⌘Z | Covers settle, snooze, mark unread, archive, delete, bulk |
| Collapse a shelf | Tap the section header | Click the header | ← → on the header | Remembered per device |
| Reorder manually | Long press → drag | Drag the row | ⌥↑ ⌥↓ | Only in manual sort; verb badge names the drop |
| Refresh | Pull down | — | ⌘R | The list is live; this is for reassurance |
| Back out | Edge swipe, or ‹ Sessions | Close in the session header; ‹ All sessions when the list is collapsed | esc or ⌘[ | Browser back too, as a second route: filter, scope and group all write history |
Same rows, same tiers, same shelves. The sidebar is 300 px instead of full-bleed, rows lose 6 px of height because a pointer is precise, and three things switch on: hover actions in the trailing slot, the ⌘-held jump pills, and drag between shelves. Nothing appears here that a phone cannot reach another way, and nothing here is reachable only through the browser chrome: the session has a visible Close, the list has a visible Back.
Four routes, in order of how often they are used: the hover strip, the right-click menu, the keyboard, and drag. Every one of them carries the same verbs as the phone's swipe and long-press sheet, so learning either platform teaches the other.
Five icons replace the timestamp on hover: mark unread, settle, snooze, pin, more. On an unread row the first is ○, Mark read. Each carries its shortcut in the tooltip, which is how the keyboard gets learned: the ● tooltip is where u is taught. The PR badge never moves, because it is information rather than chrome.
Hovering ☾ opens the presets under the cursor with their resolved wall-clock time. Clicking ☾ without waiting applies your default and shows the undo toast, so the fast path never costs a menu.
Identical order and identical wording to the phone's long-press sheet. The paired verbs lead: Settle, Snooze, Pin, Mark unread, with the auto-settle checkbox under Settle. A session that is blocked on you shows Settle and Snooze greyed with the reason on hover, rather than silently doing nothing. It still offers Mark unread.
Click a row to open, ⇧click for a range, ⌘click to add one. j / k move a focus ring without opening anything, then s settles, z snoozes, p pins, u marks unread and x selects. Every bulk action produces one undo toast, and ⌘Z reverses the whole batch.
pointer: fine and hover: hover, never inferred from width.env(safe-area-inset-bottom) on the FAB, bulk bar, toast and composer.The old page had amber text on a near-white pill. That was not a palette choice, it was a bug: the drag badge's .verb rule painted a light background, and a more specific row rule then repainted only the text with a status colour. Two rules, each sensible alone, colliding into 1.6:1 contrast. The fix is a real system, not a one-off patch.
#0c0d10). Identical in both themes, because a fill is not a surface. Only for “this session is waiting on you”. At most two on screen.#0c0d10 automatically via the .loud modifier, in both themes.
#7c7cff to #9b9bff so quiet indigo text clears AA on the dark surface. Light mode darkens the same token to #5344d4 instead: one name, two values, never a second class name.The design-system migration deletes every custom theme. What ships is two: one light, one dark, both built from the same token names so no component ever branches on which one is active. This page has the switch in its header, because the only honest way to review a light mode is to put the whole document in it.
Both panes above are the same markup with one attribute different, rendered live and side by side regardless of which theme the page is in. That is the test: if a component needs a second class name to look right in light mode, the token set is wrong, not the component.
--bg, --surface, --text, the borders, and every status ink token get a value per theme. Amber text has to be dark on white and light on near-black; that is the whole job.--amber-f, --indigo-f, --brand-f and the project colours are the same bytes in both themes, and --onfill stays #0c0d10 on top of them. An Approve pill is the same object in both themes, which is why it stays the loudest thing on the screen in both.--theme-accent-3, no per-user hue, no .dark class to remember to add. Two blocks of custom properties, roughly thirty lines, and the rest of the stylesheet never mentions a theme.var(--amber) is already correct in both.| token | dark | light |
|---|---|---|
| --bg | 0d0e11 | f5f6f9 |
| --surface | 141619 | ffffff |
| --alt | 1b1e24 | eef0f5 |
| --border | 20242b | e2e6ec |
| --border2 | 2d323b | ccd2dc |
| --text | eceef2 | 14171c |
| --text2 | a8afbb | 4b525e |
| --dim | 79818f | 6c7481 |
Light mode is not dark mode inverted. --bg is the slightly grey f5f6f9 and --surface is pure white, which is the opposite ordering to dark mode, where the page is darker than the panels. Cards have to sit above the page in both, and on light that means lighter, not darker.
| token | dark | light |
|---|---|---|
| --amber | f4b740 | 8a5500 |
| --blue | 5aa9f5 | 0f62c0 |
| --green | 3fd69a | 0c7a51 |
| --red | f4607a | c11d40 |
| --indigo | 9b9bff | 5344d4 |
| --violet | c0a8ff | 6a3ed4 |
| --brand | ff2d7b | c9004f |
The brand splits in two. #ff2d7b on white is 3.3:1, so it cannot be text there. --brand is the ink and darkens to #c9004f; --brand-f is the fill and stays #ff2d7b in both. The FAB is the same pink in light mode; the word “Reset” is not.
rgba(0,0,0,.5) under a sheet reads as depth on near-black and as dirt on white. Two tokens, --sh and --shx, drop to about a seventh of the opacity and pick up a blue cast (rgba(19,26,37,.14)) so the shadow belongs to the same grey family as the borders.rgba(24,25,29,.9) over a blur. In light they become rgba(255,255,255,.88). Same blur, same border token, opposite base — hard-coding either one is the bug..34, because a true black wash makes the page under a sheet look switched off rather than backgrounded.--inv / --inv-ink, which swap between themes rather than shifting. Miss this and the toast quietly becomes invisible in one of the two.prefers-color-scheme, and the page tracks OS changes live until someone picks a side explicitly.<head> before first paint, not by a component after hydration. A dark-mode user loading a light flash is a bug report every time.color-scheme is set with the tokens, so scrollbars, form controls and the browser's own UI match without any extra CSS.Today's sidebar is small and works. The first draft above over-corrected: 60 px rows look generous on a design page and waste a third of a phone screen in real use. Here are the same sessions at four heights, with how many rows each fits on a 390 × 844 device once the status bar, header, search field, chip row and home indicator are taken out, leaving 629 px of list.
Meta collapses into the title line as a project glyph, and the trailing slot keeps the verb. Nearest to what conduit does today.
Costs: no branch, no model, no PR. You can see everything and identify nothing.
Two lines at 13.5 / 11 px. Exactly the touch floor, so every row is still a legal target with nothing to spare.
Costs: the meta line is at the edge of comfortable reading, and swipe feels twitchy at this height.
Recommended. Two lines at 14 / 11.5 px, 8 px over the touch floor, and the status glyph gets its own 20 px column without crowding.
Gains: swipe has room to read as a gesture rather than a slip, and the trailing pill never collides with the title.
The first draft. Handsome in isolation, and you lose two sessions a screen for padding you did not ask for.
Costs: on a 5.4″ phone the settled shelf falls below the fold on a list of twelve.
The recommendation, in the shape the earlier exploration landed on, now mobile-first: D5's shelved container, with the rail replaced by a scope chip inside the search field, D1's header search, D9's ⌘K for deep search, and the rebuilt row. Group-by is a header menu. Status tiers stay the outer grouping, because reacting to what wants you is what the product is for.
SessionInfo gains projectSlug, projectTitle, status, branch, model, prNumber, prState.settledOverride, settledAt, unsettledAt, snoozedUntil, snoozedAt, pinnedAt, pinOrderKey, activeOrderKey, archivedAt.listSessions on the daemon, not per-relay, reading the projection across every registered project's store.read_at: opening a session sets it, session.unread clears it. A new MarkSessionRead RPC serves Undo and Mark read by hand. No new column: a hand-marked row is Done, unread. Settling does not touch read_at, so an unread session un-settles unread./ is the list, /s/<id> is the session, back is back.⋯; band two is the title at 14.5 px with its ▾; band three is the view switcher. Desktop is the same bar on one line and never collapses.⌄ and ⋯, with no mode glyphs. The state is atBottom (within ~24 px), not scroll direction, so it cannot jitter or strand you half-collapsed; scrolling up at all restores the bar and ⌄ forces it open until you next return to the bottom. The ⋯ menu leads with the same four views, so modes are never more than one tap away.▾ (bar 8), which is the same menu as desktop right-click. Layout rule: project and branch are placed first, the title is the only string allowed to ellipse (bar 14), and it may not ellipse below a 110 px floor.⊞ after the switcher, or ⌘⇧D, lights a second tab and splits the content area. Each pane gets a 26 px header with its own close, the split is remembered per session, and below roughly 420 px of pane width it folds back to tabs on its own. The bar does not change: side by side is a property of the content, not a second chrome.project: writes the same token, both compile to ?p=.⋯ and tray all follow it. The verb flips with the state: Mark read on an unread row, Un-settle on a settled one.sessionStorage, per tab, so it survives a reload and never reaches the URL./s/<id>?p=conduit&f=needs&g=project&q=auth. Back works, links share, refresh restores.<a href>. Swipe is a pointer-event overlay that yields to the link on a tap and to the scroller on a vertical drag.prefers-color-scheme, set before first paint, persisted per device. Custom themes are deleted, not migrated.aria-live="polite" region for the whole list.