The wide hub: a 140-column breakpoint that splits list and preview (kxxph)
At 140 terminal columns and above, the cyberine agents hub stops being one column. The runs list becomes the left pane, the run preview becomes a full-height right pane, and a vertical rule sits between them. Below 140 the layout is exactly what it was: the list at full width with the preview stacked under it.
The hub has carried a focusedPane: 'list' | 'preview' model and a Tab binding since long before it had any geometry to express them — the preview sat under the list at every width, and "focus" only changed the colour of its rule. This is that geometry. Tab now moves focus between two real panes, and the seam takes the accent while the preview holds it.
Why the breakpoint is 140 and not a rounder number: it is exactly MAX_TABLE_W. Past that width k09p3 already stops the list rows growing (it clamps a table's measure so a run summary does not end up 150 cells from the column that qualifies it), so every column beyond 140 was being spent as right margin. The second pane is made of that surplus — which is also why the breakpoint cannot be lower: below it the surplus does not exist and a split would have to take width from the list.
Pane widths: the preview takes a third of the content, floored at 40 columns (below that the wrapped task shreds into two-word lines) and capped at 56 (past that it is a column of whitespace worth more to the list); the rule plus one column of breathing space on each side costs 3; the list takes the rest. The geometry is a pure function, hubSplit() in src/tui/hubLayout.ts, and the row itself is HubPanes (src/tui/components/HubPanes.tsx) — both outside app.tsx, which cannot be rendered headlessly, so that the breakpoint is something a smoke can actually measure.
The rule is VerticalRule (src/tui/components/VerticalRule.tsx), Divider's vertical twin, and it follows the same two conventions: the LIGHT SYM.vbar (U+2502), matching SYM.hbarLight and chromeBorder's edge weight, never the heavy U+2503; and dim opt-in, defaulting to false (k6shr). Its tone is RULE at rest and ACCENT only while the preview holds focus, which is the "focused pane's frame" claim ACCENT_CLAIM_BUDGET allows — the split hub spends at most two claims in either focus state, and the smoke measures both. The preview drops its own leading horizontal rule in the split: the vertical rule is already the seam, and a horizontal one meeting it at a corner would close the same seam twice, the doubled-rule shape k6shr removed from under the header.
Run recipe:
npm run tui # resize the terminal past 140 columns to see the split
npm run selftest:wide-breakpoint # offline; sweeps hubSplit() and renders the panes through InkThe guard has five parts. hubSplit() is swept over every column count from 20 to 139 and must report no split AND hand the list the full content width — if the list width is untouched at every narrow width, no narrow layout can have moved — then over 140 to 260, where the three pane widths must sum exactly to the content width. The breakpoint is pinned as a literal EXPECTED_WIDE = 140 in the smoke rather than read from WIDE_COLS, because a sweep bounded by the constant moves with it and can never catch the move (measured: lowering WIDE_COLS to 130 left the self-referential version green). The app.tsx source is checked for the early return that hands back the identical element tree when the split is off, and for both call sites reading one previewVisible predicate instead of two condition lists that can drift. The composition is then rendered at 140/180/240 and asserted to draw a straight, light, single-column rule with content on both sides of it; and at 80/100/120/139 to draw no vertical rule at all while the stacked preview keeps its own leading horizontal one.
Extend recipe. Moving the breakpoint means changing WIDE_COLS in src/tui/theme.ts and EXPECTED_WIDE in scripts/kxxph-wide-breakpoint-smoke.tsx in the same commit — the smoke fails otherwise, deliberately, because the breakpoint is a contract with every terminal narrower than it. Pane bounds live beside the function as HUB_PREVIEW_MIN_W / HUB_PREVIEW_MAX_W / HUB_RULE_W. A new pane arrangement should extend hubSplit() and HubPanes rather than adding arithmetic to app.tsx, or it will be geometry nothing can measure again.
Known Limitations and Deferred Work. The split applies to the runs list only. Every condition that already hid the stacked preview still applies: the unified agents view (chats mode), the session browser, the hub welcome, an open overlay (help, folder picker, fuzzy filter), the compose panel, and the quick-reply / steer bars all render the single-column layout at any width — a split with nothing in the right pane is just a narrower list. The short breakpoint (fewer than 24 rows) also suppresses it, since the preview is hidden there. The rule's height is the list pane's height rather than the terminal's, so with a very short list the rule stops where the list stops instead of running to the footer. The run detail, peek, diff, workflow and agent-chat screens are unchanged at every width — the breakpoint is about the hub. And the rendered LOOK past 140 columns has not been verified on a real TTY by the change's author; run npm run tui in a wide terminal and press Tab.
One dot separator, no raw colour literal, no doubled hue (k1rse)
A census (not an estimate) found the "field · field · field" dot separator (SYM.bullet, padded) rendered at THREE different widths across 31 pure separator sites in 11 files: two spaces before /...
The streaming label pulses, and its copy escalates (kk92a)
While a chat response is in flight the waiting label pulses between the muted tone and a slightly brighter one on a 2-second sine, after a 3-second warm-up. The warm-up is the load-bearing part: a...