Layouts
Explore how Tasty arranges navigation and content. Compare list-and-detail layouts with tabs and sections. Turn on Specs to inspect the grid and dimensions.
This page uses a saved copy of the design system, last changed on . It may differ from the current design source.
Sidebar & rail
Full sidebar ↔ collapsed rail
The window's left navigation has two states. Full (212px) shows the wordmark, workspace list, and a footer of Tools / Plugins / Settings. Collapsed (52px) drops to an icon rail — workspaces become single-letter IconButtons, footer actions stack at the bottom. Same actions, same order, both states. Each workspace row is just status dot + name + notification: every row is a workspace, so a per-row folder icon carried no information, and a tab count is low-signal noise — the dot (idle / running / agent) and an unread badge are what actually vary. A workspace that is a local mirror of a remote instance gets a small sky remote-link glyph before its name (a corner chip on the rail) — a separate axis from the dot, so the dot stays execution-only.
Don't give every row the same leading icon — when one kind fills the list, the icon is decoration, not information. Lead with the status dot, which varies per row, and let an unread badge earn the trailing slot instead of a static count.
Active selection now reads through surface fill + a 2px accent left bar (the folder icon used to carry that accent). Footer order is fixed: Tools → Plugins → Settings, bottom-aligned; in the rail they become 28px IconButtons in the same order. Mirror (remote-linked) moved off the status dot onto its own leading glyph — the dot no longer carries a sky "remote" color, only execution (running / idle / agent); the glyph never collides with the attached lavender ring or the notification badge.
Workspace categories (sidebar folders)
An opt-in grouping (Settings › General › Workspace categories (folders), default off). The flat list splits into collapsible category sections: a disclosure header (chevron) over that category's workspace rows. The reserved normal category is always first and keeps the plain "Workspaces" label — it can't be renamed, deleted, or reordered. Empty categories (e.g. Archived) show the header only. In the rail, categories aren't labelled: each boundary becomes a clickable horizontal --- button, with that category's avatars below it. Collapse state is per-category and shared across both. All category management is via right-click (no dedicated buttons) — see Overlays › Popups.
Backend invariants honoured: normal reserved & pinned top; category order = section order; collapsed persisted in layout.json; empty categories allowed (no auto-delete). Toggle off → the flat single "Workspaces" list above, unchanged.
Rail marks — category line and mirror chip (2026-10-12)
Two rail marks were too faint on bg-sidebar. Category --- line: drawn in the idle avatar glyph ink (--tasty-rail-category-line → text-muted) at 2px (--tasty-rail-category-line-width → focus-ring-width), 24 long with round ends. One colour for every category, including the one holding the active workspace: the active avatar already carries the accent, and the line marks a boundary, not a selection. Hover and pressed lift the line to text-secondary on the usual overlay bed. The modifier keycap that replaces it is unchanged. The flat-mode top divider uses the same line (28 long) so both rail modes look the same. Mirror chip: same 12 circle and 8 glyph, plus a full-strength 1px sky edge (--tasty-workspace-mirror-chip-border → workspace-mirror-fg). The bed stays bg-sidebar so the glyph keeps its contrast, and the 2px halo still cuts it out of an active fill.
The 2px line is a mark at avatar-glyph weight, not a box edge, so the 1px border rule still holds for borders. Move and attach-refusal chips keep their current look: they are short-lived states with their own coloured glyph, so no edge is added for now.
Category quick-switch — Ctrl+Shift held
A keyboard axis over the folders: holding Ctrl+Shift (rebindable) previews Ctrl+Shift+1–9 / 0 to jump between categories, the way Alt jumps between workspaces and Ctrl between tabs. Each category gets a switch keycap: in the full sidebar it's right-aligned on the header (the chevron is kept — it carries collapse state and its auto-expand rotation); in the rail it sits on the --- boundary. The reserved normal category is 1. This and the workspace overlay are modifier-exclusive, so rows keep their status dots while headers show keycaps. See Overlays › Popups → Switch-number overlay for the full spec.
Reuses the switch-overlay keycap and all --tasty-switch-overlay-* tokens — no new tokens. Auto-expand: switching to a collapsed category opens it (chevron rotates, rows reveal) and persists collapsed:false. Last-active: the landed workspace uses the ordinary active treatment (--tasty-surface-active + 2px accent bar) — no separate cue.
Pane tab strip
Tab strip — fixed 24px row
Sits at the top of a pane group on --tasty-bg-sidebar. Tabs are 24px × 150px; the active tab adopts the panel color with an accent top bar. A + IconButton follows the tabs; pane actions (split, search) pin to the right.
Tab cell — the right-hand status cluster (2026-09-29)
Everything a tab reports sits in one fixed cluster at the right end of the cell, never glued to the label text. Order, left → right: HTML script marker · move glyph · busy dot · close. Items are --tasty-tab-status-gap (4) apart; the label keeps --tasty-tab-gap (8) to the cluster. The cluster never shrinks: the label ellipsises first, so markers stay visible in a narrow tab. Close keeps its slot (hidden until active / hover) so nothing shifts on hover. The lock marker has a 16 × 16 hit cell (--tasty-html-script-marker-hit) with its own hover fill; hovering it is still hovering the tab, so close shows too. The scriptFile marker is tooltip-only, no hover fill.
Tooltips in the strip open upward — native content below (2026-09-29)
Under an HTML tab the pane body is a native WebView, and the OS always draws it above Tasty's own layer. A tooltip opened down from the strip lands inside it and is never seen. So every tooltip anchored in a pane tab strip or pane head (script marker, close, scroll arrows, head buttons) opens top, over the title area or the pane above, whatever the surface kind, so the strip behaves the same everywhere. Candidates in order: top, then bottom; the first that fits the window and clears every native content rect wins. Revised 2026-10-06 — if neither clears (an HTML pane stacked above another HTML pane), a third candidate is tried: inside the strip — the bubble is centred vertically on the strip row (Tasty paints the strip, so no WebView covers it) and placed beside the anchor cell, right first, then left, --tasty-tooltip-offset from the cell. It may cover neighbouring tabs; that is the price of being seen. Revised 2026-10-06 b2 — the in-strip candidate tolerates 1px (border-width) per edge, and when every candidate fails the bubble is still placed inside the strip, the one place Tasty paints over. Top clamped (--tasty-tooltip-offset as the vertical margin, never flush at y = 0) remains only for tooltips with no anchor cell, i.e. outside a strip. Horizontally the bubble clamps to the window edges with --tasty-tooltip-offset. Hiding the WebView while a tooltip is up is rejected: the page would flash on every hover. Hover delay, copy and click are unchanged.
Scroll arrows — chevron icon, square cell, no own fill (2026-09-29)
The strip's scroll arrows are icons: chevronLeft / chevronRight at --tasty-tab-scroll-arrow-glyph-size (14 since 2026-10-06 — at 12 the 1px stroke lands on a pixel seam and rasters at half alpha). The < > characters go: a text character is not an icon. The cell is square, --tasty-tab-scroll-arrow-width = strip height 24 (was 20; the tab viewport loses 4 per side, accepted). The cell has no fill of its own; it sits on the strip ground (focused pane surface-raised, unfocused bg-sidebar). Hover lays --tasty-tab-scroll-arrow-hover-bg on the enabled side only. The same shape and roles apply to every horizontal tab bar with arrows, including horizontal_tab_bar_with_arrows (its 0.4 tint retires; the end it has reached is disabled). Contrast is judged on flat colour like every other token ratio; rasteriser AA is not part of the target.
Don't draw arrows with a font glyph. A proportional < at 11px renders thinner than the icon stroke and depends on the UI font.
Move source scrolled out of view — the arrow on that side turns pink (2026-09-29)
When the pending move target (a tab, or a surface inside an inactive tab) is in the active workspace but its tab cell is scrolled out of the strip, the scroll arrow on that side takes --tasty-tab-scroll-arrow-move-fg (= move glyph pink). Tab targets and surface targets use the same cue. Exception (2026-10-06): a surface inside the active tab gets no arrow cue even when that tab's cell is scrolled away — its ring is drawn in the content area, so the target is on screen; the arrow only ever points at a target you cannot see. A cell counts as visible only when it is fully inside the viewport; until then the arrow carries the cue. The cue is static and behaves like the arrow: hover is the arrow's hover, a click is the arrow's normal one-step scroll (no jump). It ends when the cell comes fully into view; the ring or glyph in the cell takes over. That side can never be disabled while the target is beyond it. A rect too small for the ring (under 2 × ring width) gets no substitute: it cannot happen at the minimum pane size. Pane maximise does not exist yet; request it separately when it does.
Nothing new appears in the strip. The arrow already means "more tabs this way"; the pink says the move source is one of them.
1-depth — list → detail (Plugins / Tools idiom)
One sidebar, one detail pane
The simplest content layout: a fixed-width list (a TreeRow list of items) feeding a flexible detail pane. Use it for plugins, tools, port lists, file handlers — anything that's "pick one from a list, see its detail." No top tabs.
Drill-down — content swap (list ⇄ detail)
One area, swapped — list → detail → back
When the model is "one full-width list, pick an item, see its detail, go back" — and a side-by-side split would starve both — the content area swaps in place. The list view (a full-width ListCtrl) becomes a detail view the moment you select a row; a pinned back bar (← + title) returns you. This is a generic DrillDown layout — its first home is Settings › Keybindings › Preset (preset list → diff preview), replacing the old cramped 120px-list + right-preview split. Select a preset below, then press ←.
Do put the detail's own action (Apply) in the back-bar actions slot — it stays clear of a surrounding modal footer's Cancel / Save, so the two button rows never collide. Apply is disabled for the already-active item (nothing to change).
Don't reach for drill-down when items are few and detail is small — a plain 1-depth list→detail split shows both at once. Swap only when the detail (or the list) genuinely wants the whole width.
2-depth — tabs → sections → content (Settings idiom)
Top tabs over a filterable section list
When one list isn't enough, add a top tab row (L1) above the list. L1 is a small fixed set; the left list (L2) is the growable, filterable dimension. This is the Settings window's structure — reuse it for any surface whose content tree outgrows a flat list.
Do give plugins their own L1 tab. Don't nest a growing plugin list inside another group's L2.
Multi-tier tab layout
Stacked tab tiers — workspace over panes
When a window holds more than one level of switchable context, tabs stack into tiers: a top workspace tier (which project) over the per-pane tab strip (which surface). Each tier is a full tab row in its own band — the upper tier sits on --tasty-bg-app, the pane strip on --tasty-bg-sidebar. Only the active path down the tiers is highlighted, so the hierarchy reads at a glance.
Don't stack more than two tiers — beyond that, move the broadest dimension into the sidebar. Tiers are for switchable context, not deep trees.
Divider
Pane divider — the resize boundary
The 1px separator between split panes, doubling as the drag-to-resize handle. At rest it's a hairline (--tasty-separator); on hover the hit-band (a ~6px transparent zone around the line) lights to the accent and the cursor becomes a resize arrow. Works on both axes — a vertical divider splits left/right, a horizontal one splits top/bottom.
Hover either divider above — the hairline turns accent. The hit-band is wider than the visible line so the 1px boundary stays easy to grab without thickening the resting visual.
Surface focus states
Focused · unfocused · agent
Split panes make focus explicit through color. The focused terminal surface goes true black (#000) with full-strength text; unfocused surfaces dim to --surface-terminal-unfocused at 92% opacity. An agent surface carries the mauve pulsing dot — you always know who's driving.
This is the soul of the product made visual: user vs. agent and focused vs. not are never ambiguous. Carry these colors into any surface mock.
Mixed split — terminal + markdown in one pane group
A pane group is kind-agnostic: a terminal and a markdown surface can sit side by side in the same tab — the common "run it left, read the doc right" layout. Each surface keeps its own focus bed: the terminal goes #000 when focused, the markdown surface uses the document bed (--tasty-surface-markdown-focused-bg) focused and the sidebar bed unfocused — never black. The markdown surface keeps its 36px address bar inside the split, and the status bar reports the focused surface, not the tab.
Don't paint the markdown pane black to "match" its terminal neighbour — black is a raw-TTY convention. Prose stays on the Catppuccin document bed, which is how the two kinds stay tellable apart at a glance.
Occupancy & completion borders
Three surface states ride one visual channel — the terminal surface's border — and are told apart by color alone, so the palette must stay mutually unmistakable at a thin edge. Occupied · soft (green, 1px): a subject (remote user or AI agent) holds the surface — a cooperative heads-up, no write restriction; it may close or change at any time. Occupied · hard (peach, 1px): the same notice plus readonly (input blocked, mirror-observe) — it carries a force-detach affordance top-right and absorbs the old remote-attach border. Completed (blue, 2px): a surface-highlight drawing attention to a finished task; it clears on focus. Occupancy outranks the completion highlight when both apply (a source-side priority rule, not a token).
Why blue, not sky, for completion. The ADR describes the completion border as “sky”, but the live source draws it blue (--tasty-accent-primary) and the design confirms blue as canonical: against the green (soft) and peach (hard) neighbours on the same channel, blue is the most discriminable — sky sits too close to green at a 1px edge. Hard reuses the peach primitive but through its own role (--tasty-accent-occupied-hard), kept distinct from --tasty-accent-attention (plugin needs-attention) so the two intents can diverge. Completion is one attention kind; the second kind (NeedsInput, yellow) and the full cross-channel priority live in Attention kinds below.
Attention kinds
The scale — kind → color → rank
A background subject (agent, task, remote session) raises attention on a target. Attention is a scale, not a pair of colors: each kind takes one semantic slot (--tasty-attention-<kind>) that borrows an existing accent role, plus a rank. Every channel that renders attention — workspace badge, collapsed rail dot, tab title, surface border — reads the same slot, so adding a kind is one token line per channel and no new logic.
NeedsInput outranks everything because it is the only kind that is a request — work has stopped until the human answers. Completion is an FYI and clears on focus. No attention kind mints a new hue; a kind that can't honestly borrow an existing accent role is a sign the role list is missing something.
Workspace row — one or two count badges
The expanded sidebar row can carry a badge per kind. The trailing slot is the badge slot: with one kind present it sits there regardless of which kind it is, so a row never shifts its badge position by kind. With both, NeedsInput leads and Completion trails — higher rank reads first in a left-to-right scan — separated by --tasty-badge-group-gap. Counts over two digits collapse to 99+ for both kinds.
Don't reserve an empty slot for the missing kind. A fixed two-slot row makes every quiet workspace look like it has holes in it; the group is inline and collapses to the count that exists.
Collapsed rail — one dot, highest rank wins
The rail has one dot slot per workspace (the compact 6px dot, --tasty-status-dot-size-compact — the rail is 24px-class chrome) and no room for a number, so the dot answers “what is the most urgent thing here”, not “how many”. It is always a single dot, colored by the highest-ranked live state: NeedsInput › Completion › busy/running. Two dots were considered and rejected — at 6px on the rail a second dot reads as noise, still carries no count, and breaks the one-slot alignment the rail depends on. The count is one click away in the expanded sidebar.
Attention beats activity. Busy-green says “something is happening”, which is the normal state of this product and the least actionable thing on the rail; both attention kinds are events that want a human. The previous code already preferred highlight over busy — this makes it a rule.
Tab title & surface border — the priority ladder
The pane tab strip tints the title color and the surface tints its border. Both are single channels shared with non-attention states, so both resolve by rank. Tab title: NeedsInput → Completion → active → rest. Surface border: NeedsInput (2px yellow) → occupancy (1px green/peach) → Completion (2px blue) — NeedsInput steps above occupancy, the one change to the earlier occupancy-over-completion order.
Why NeedsInput outranks occupancy. Occupancy reads as “held, working, as expected” — which is exactly the state a blocked prompt would hide behind. A session that has stopped to ask you something must not look like a session that is busy. Completion stays below occupancy, as before. Attention clears only when the surface actually receives focus (focused pane, current workspace, window focused). The visible focused tab therefore never shows a tint; an active tab in an unfocused pane or another workspace can hold attention and keeps its tint by the ladder above (needs-input / completion over active).
Don't stack the two edges (a 1px occupancy line inside a 2px NeedsInput line). One channel, one color — stacked edges read as a rendering bug at these widths.
Move source highlight
The mark — one dashed ring, three scopes
After Move Surface / Move Tab / Move Pane, the armed target carries a dashed 2px ring in --tasty-accent-move (pink) until the move runs, another target is armed, or the target closes. The ring is the same for all three kinds; only the rect changes — surface rect, tab cell rect, or the whole pane (tab strip + content). Dashed is the discriminator: every other edge in the product (notification, occupancy, focus, divider) is solid. Static, never animated, and it does not take input.
Same language, different rect. A pane ring encloses the tab strip, a surface ring does not — that alone tells the three apart, and the menu already names the kind in the toast. Why pink. Blue, yellow, green, peach, mauve, lavender and sky already mean completion, needs-input, occupancy, agent, attach and remote; pink is the one Catppuccin hue with no state attached, and it stays far from all of them at a 2px edge. Inside, not outside: split surfaces share a 1px divider, so an outside ring would paint over the neighbour.
Tab cell — ring over every tab state
The tab ring sits on top of the cell whatever its state. On the active tab the top edge crosses the 2px active indicator: pink dashes cover it, the blue shows through the gaps, so "active" stays readable from the panel fill and title color.
Target not visible — move glyph on the nearest visible container
The target is often out of sight: the user arms a move, then goes to another workspace to pick the destination. The nearest visible container of the target shows the move glyph (12px, pink): the workspace row (and the rail avatar when collapsed) when the target lives in another workspace, or the inactive tab cell that holds a target surface in this workspace. Ring = this is it; glyph = it is in here. Only one cue is on screen at a time; once the target itself is visible, the glyph goes and the ring shows.
Don't ring a container. A ringed tab means "this tab moves"; a ringed workspace row would read as "this workspace moves", which is not an action.
Overlap — dashes on top, gaps show what is underneath
The move ring is painted last on the target rect, above the notification border, occupancy border, occupancy overlay and focus bed. It never replaces them: the dashes are pink, the gaps show the edge beneath. A move source that also needs input reads as a pink/yellow dashed band — both facts at once, no stacked edges, no priority to resolve.
Not decided, deliberately left out. Dimming the source the way Explorer dims a cut file: rejected — the source stays fully usable while armed, and a dim collides with the unfocused-surface dim. Destination candidates while the menu is open: rejected — every other surface/tab/pane is a candidate, so marking them all carries no information, and the menu is native.
Loading overlay
Loading overlay — four placements over existing content (2026-10-16)
While a slow request runs (first: remote attach adding its structure) a LoadingOverlay is drawn on top of what is already there. The part itself adds no tab, pane, slot or "Loading…" title and never touches the stored name. (2026-10-19) This is the part's responsibility only: when the user asks for a new tab or split, the client first lays out a normal tab / split and the part is drawn on it — see Pre-layout below. Each placement has a fixed box (dashed below = the overlay rect): surface = the surface rect, spinner 16 on a 24 bed bottom-right · tab = the 14 kind-icon slot · LNB card = the 16 dot slot · rail = the 36 avatar, a 28 ring around the initial. Ink is one token everywhere. The overlay has no hit-test and no focus: click, drag, hover, close, select and keys behave as if it were not there. The caller passes the rect and visible; the part owns no timers.
Tab — the spinner takes the kind-icon slot, nothing else
The spinner covers only the 14 kind icon at the start of the cell, on a bed of the cell's current fill (inactive / hover / active). The label, its attention colour (needs-input, completion), the active indicator and the whole right cluster (HTML marker · move · busy · close) stay as they are; close still appears on hover. Loading (accent arc, left) and busy (green dot, right) can show together and do not read alike. The Ctrl switch keycap fills the same slot and draws above the spinner while the modifier is held. In a narrow tab the label still ellipsises first. A tab scrolled out of the strip is not drawn, so its spinner needs no repaint.
LNB card — the spinner takes the dot slot
On the expanded sidebar the spinner sits in the card's 16 dot slot (title-line height) over the status dot and, if present, the attached ring, on a bed of the row's current fill. Title, REMOTE pill, subtitle, description, notification badge, active accent bar and hover are untouched; the ring and dot come back as they are when loading ends. Only the loading card changes; no other card and no part of the LNB is covered. The Alt switch keycap draws above it. Works the same with categories on or off. A card inside a collapsed category is not drawn, and its category header (and rail --- button) shows nothing: it follows the existing collapse rule.
Rail avatar — a ring around the initial
The four avatar corners are taken (TL refusal · TR attention dot · BL move · BR mirror) and the initial is the avatar's identity, so the spinner is a 28 ring around the initial with no bed. The initial stays readable inside it. Corner chips keep their 2px bg-sidebar halo and draw above the ring, so they cut it cleanly; the attached ring (square, lavender) sits outside it. Active, hover and the Ctrl+Alt switch keycap behave as today; the keycap draws above.
Surface — corner bed, content stays live
Inside a surface the spinner sits on a 24 bed bottom-right, 8 in from the rect, never outside it: other splits, panes, the tab strip and the LNB are not covered. There is no dim: terminal cells, cursor and selection keep drawing at 0ms and are never faded or moved; output keeps arriving under it. Bottom-right keeps clear of the search bar (top-right) and of a prompt at the bottom-left; if a long line runs under the bed, the bed wins for 24 × 24 only. The move-target ring stays on the rect edge outside the bed, and in-surface overlays (search bar, popups) draw above the spinner. On a native WebView surface nothing is drawn inside: the OS paints the page above Tasty and hiding the WebView for a non-blocking spinner is not acceptable, so the tab and LNB card carry the loading. If the native overlay layer lands later, the same bed applies there.
All three at once · UI scale · reduced motion · timing
One request can light the rail avatar (or LNB card), the tab and the surface together; each is switched independently by its caller. The overlay appears only if the request is still running after --tasty-loading-overlay-delay (200), so fast round trips never flash; it appears and disappears without a fade (the arc is the motion) and goes the instant loading ends, showing the content as it is at that moment. Rotation is Spinner's 900ms period. With reduced motion it is a still arc on its track, not three dots: dots read as a typing or ellipsis mark and do not fit in a 14 slot or around an initial. Sizes are tokens, so they follow --tasty-ui-scale; stroke stays 2px at 1×.
Labels and tooltips
Don't write "Loading…" into the tab title or workspace name, and don't add a slot beside the title for it.
Pre-layout — the client draws a normal tab / split first, the overlay sits on it (2026-10-19)
Two different changes, shown apart. (a) Overlay toggle on a fixed target (the specimens above): geometry never changes. (b) Pre-layout: the user asks for a new tab, a surface split or a pane split on an attached workspace; on the next frame the client adds a normal tab / split — same strip, same split geometry, no loading layout, no new SurfaceKind — and puts the existing LoadingOverlay on the new tab's kind-icon slot, the new surface, and the workspace's LNB card. The spinner's 200 delay applies to the spinner only, never to the pre-layout. When the response arrives the same spot turns into the real content. The tab keeps its own title; "Loading…" is never written into it.
Pre-layout — selection moved · several adds · explicit failure · merged results (2026-10-19)
Selection moved: while waiting, the user picks another real tab; when the response lands, the new tab turns real in the background and the user's selection is kept. Several adds to the same pane: each request gets its own tab with its own overlay. Explicit failure: only that tab / split and its overlay go; everything the user did since stays; the reason goes through the existing error notice. Merged results that cannot be matched to requests: the confirmed structure is shown, pre-layout areas that might duplicate it are folded, the LNB card keeps its wait for unfinished requests, and nothing is auto-focused. This is an exception path, never the default for a single add.
Response wait on an attached workspace — PendingIndicator (2026-10-19)
Requests that make no new area — close, move, restore closed, convert — show a small PendingIndicator in the window's top-right notice column (under the top-right pane's tab strip, 8 in; see Overlays · Popups › Notice column for its neighbours). Text is neutral (“Waiting for response”) plus the elapsed time; with several requests it counts them and shows the oldest wait. It is view-only: no cancel, no retry, no resend. It disappears on completion; failure and disconnect hand over to the existing error notice and reconnect banner. Unrelated output and other completions do not reset the time. It belongs to its workspace: switch away and it is not shown elsewhere; come back and it shows the running time. It never starts because a shell is quiet (password prompt, long command), and on an old server only supported requests are tracked.
Contrast on surface-raised: label text-secondary-raised Mocha ≈ 7.1:1 · Latte n1100 5.17:1 · time text-primary Mocha ≈ 9.5:1 · Latte 5.17:1 · separator text-muted is decorative · spinner arc accent-primary Mocha ≈ 6.9:1 · Latte ≈ 3.6:1 (≥ 3). Chip edge border-strong is the same floating edge as Toast / Banner.
Don't start the wait because a terminal has been silent, and don't use the chip in place of the pre-layout for a new tab or split.
Notifications summary (LNB · rail)
LNB summary — expanded sidebar (2026-10-17)
A small block directly above the Tools · Plugins · Settings block, with its own top rule. The whole block is one button: click (or the existing notifications shortcut) toggles the popup. Line 1: bell · Notifications · one count pill per level with unread items, fixed order error › warning › info. Line 2 (only when something is unread): the newest unread title, one line, ellipsis, with its level glyph and elapsed time. With nothing unread the block is one muted line. Workspace rows get no new badge; their blue count stays the surface-attention count.
Don't add the notification counts to workspace rows or rail avatars, and don't sum them with the surface completion / needs-input counts.
Rail — bell with stacked counts
The collapsed rail keeps the same block above Tools, as a 44-wide button (wider than the 36 avatar, so 99+ fits with its glyph). Bell on top, then the level pills stacked error › warning › info, zeros skipped. With nothing unread it is the bell alone at button height. No line 2; the tooltip carries the full counts.
Level count badge — NotificationBadge
A 16 pill with the level glyph and the count, so the level reads without colour: error red (critical failure) · warning yellow (non-critical failure) · info the completion blue. Each pill has an aria-label / tooltip ("3 errors"). 0 draws nothing; above 99 reads 99+.
Workspace status bar
What the 24px bar carries, and what goes first when it can't
The bar is a read-only summary of the focused surface plus one keyboard reminder. Left cluster, in this order: git branch (glyph + name), surface id (mono s3·p1), shell, grid (120×32). Right cluster: the palette shortcut and the theme glyph. Nothing here is a destination — clicking an item never moves focus, and the data is read from the focused surface at the existing 1Hz cache. Items that have no value disappear rather than showing a dash: outside a repo the branch item is absent, not empty.
The theme indicator used to borrow accent-warning for light and accent-agent for dark. Light-vs-dark is not a warning and not an agent, so it becomes a glyph in dim ink — no colour role is minted for it.
Don't add counts, clocks, CPU meters or notification badges. Anything that changes faster than the bar's 1Hz read, or that is not a property of the focused surface, belongs elsewhere.
Theme cell — one glyph box, both themes (settled)
The theme cell draws sun in Latte and theme in Mocha at one size: --tasty-statusbar-glyph-size = --tasty-icon-size-xs (12), the same box every other inline glyph in the bar uses. A dimension must not depend on which theme is active, so the two glyphs are not given different sizes. They used to read differently anyway — sun's rays ran to the edge of the 24 box (ink 92%) while theme's ring stops short (ink 83%), so Latte's glyph measured +2px taller than the caption beside it at every UI scale. That is a drawing fault, not a sizing one, so it is fixed in the asset: icons/sun.svg ray extent pulled to the same 83% ink box. Both glyphs now land at 10px ink inside the 12 box, matching the 11px caption's ascender.
The two glyphs sit side by side here only to compare them. In the product the cell carries exactly one: sun in Latte, theme in Mocha, with the theme name beside it.
Don't give sun its own size token to cancel the overshoot. A size that changes with theme state turns a dimension into a state, and the same overshoot would follow sun into every other place it is drawn. Optical parity belongs in the glyph.