feat(home): dock the desktop tab list as a left rail with age stamps

The open-tabs list on the welcome screen was a fixed 256px card floating
vertically centered in the left gutter, which read as debris rather than
chrome and left 12px type stranded on a wide display.

- Dock it: left/top/bottom 0, full height, hairline right border and a soft
  background fade. The centered welcome content still does not move.
- Scale it off one knob: width clamp(250px, 19vw, 430px) plus a fluid
  font-size on .home-sessions, every child sized in em. Measured 250px/12.2px
  at the 1180px gate, 380px/15px at 2000px, 430px/17px at 2938px; the gap to
  the centered content never goes negative.
- Show when each session was first created and last active, on a full-width
  footer line so it does not fight the status pill, exact dates in the title.
  Both stamps refresh in place on a 20s clock (disarmed when the home screen
  goes away) rather than by re-rendering, which would restart every row's
  blink animation and working ring twice a minute.
- Mute idle green: dot and pill mix toward --text-muted, so idle reads as
  greyed-out next to the vivid green of a working session. Mixed rather than
  hardcoded, so every skin keeps its own green.

Verified in a browser at 1180/2000/2938px and on a light skin, plus
test/home-sessions.test.ts, frontend-syntax, public-assets, prettier and a
PostCSS parse of styles.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-10 02:56:35 +02:00
parent c942bb5dfb
commit c891a8045d
4 changed files with 240 additions and 51 deletions
+1 -1
View File
@@ -254,7 +254,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
**Phone overview home screen** (`mobile-overview.js`, phones only, per-device `mobileOverviewEnabled`, default ON): under 430px the "C" logo shows a session overview (NEEDS YOU / CURRENT SESSIONS / PAST SESSIONS) instead of the welcome overlay; tablet and desktop are unchanged. The branch lives in `showWelcome()`/`hideWelcome()` (terminal-ui.js) behind `shouldUseMobileOverview()`, which is **width-driven** (`getDeviceType() === 'mobile'`) because this is a layout decision, unlike the settings namespace which stays handheld-based. ⚠️ The container ships with the `hidden` attribute and only this module removes it: never give `.mobile-overview` a bare `display` rule, since desktop does not load `mobile.css` (`media="(max-width: 1023px)"`) and would then render it unstyled. Live re-renders ride on the tail of `_renderSessionTabsImmediate()` (every state change it needs already funnels there); PAST rows come from one `_fetchUnifiedSessions(60)` per home-screen visit and resume through the shared `resumeHistorySession()`, so they behave exactly like the welcome screen's Resume list. ⚠️ Two things must stay in lockstep with surfaces outside this module, because divergence reads as a bug rather than a style: the split Run button carries the **toolbar's own classes** (`btn-toolbar btn-run mode-<backend>` / `btn-run-gear`) so the per-backend gradient and the light-skin overrides apply unchanged (mobile.css must therefore set no `background`/`color` on it), and row status uses the **session-tab language** (green dot when fine, `pulse` while working, yellow blinking row when waiting for input, red blinking row when a question is pending, mirroring `tab-alert-idle`/`tab-alert-action`). The picker mirrors the toolbar run-mode menu (`setRunMode()` + `run()`, `openWebviewFromMenu()` for saved dashboards) and deliberately omits its Recent-Sessions block, since PAST SESSIONS is that. Status pills carry `data-i18n-skip` (generic words like "idle" collide with state strings elsewhere).
**Desktop home tab column** (`home-sessions.js`, desktop only): the welcome overlay centers ~560px of content in a ~1400px window, so its left gutter is dead space; it now carries the open tabs as a vertical list. Rows are in **tab order**, not sorted by urgency like the phone overview, because the row badges are the Alt+1..9 indices. State classification is REUSED from mobile-overview.js (`_mobileOverviewState`/`_mobileOverviewCaseFor`), which is why the module loads after it. ⚠️ The column is `position: absolute` so the centered content never moves, which is exactly why it needs a **width gate in two places** — `HOME_SESSIONS_MIN_WIDTH` (1180) in the JS plus a `max-width: 1179px` media query as the backstop for a resize that outruns the matchMedia listener; drift between them means a column overlapping the search panel, and `test/home-sessions.test.ts` pins them equal. ⚠️ `.home-sessions` is `display: flex`, so `[hidden]` must be re-asserted as `display: none` or the module's only visibility lever does nothing. Working state is deliberately byte-identical to the phone's: pulsing green dot + the `tab-load-spin` ring reused from the tab strip + the same green halo (added to `.mobile-overview-dot--working` at the same time), so "working" reads the same on every surface. Live re-renders ride the tail of `_renderSessionTabsImmediate()` alongside the phone overview.
**Desktop home tab rail** (`home-sessions.js`, desktop only): the welcome overlay centers ~560px of content in a ~1400px window, so its left gutter is dead space; it carries the open tabs as a rail **docked flush to the left edge, full height** (a vertically centered card floating mid-gutter read as debris). Rows are in **tab order**, not sorted by urgency like the phone overview, because the row badges are the Alt+1..9 indices, and each carries **created / last-active** stamps. State classification is REUSED from mobile-overview.js (`_mobileOverviewState`/`_mobileOverviewCaseFor`), which is why the module loads after it. ⚠️ The rail is `position: absolute` so the centered content never moves, which is exactly why it needs a **width gate in two places** — `HOME_SESSIONS_MIN_WIDTH` (1180) in the JS plus a `max-width: 1179px` media query as the backstop for a resize that outruns the matchMedia listener; drift between them means a rail overlapping the search panel, and `test/home-sessions.test.ts` pins them equal. ⚠️ `.home-sessions` is `display: flex`, so `[hidden]` must be re-asserted as `display: none` or the module's only visibility lever does nothing. ⚠️ Size scales with the viewport off **one knob**: `width: clamp(250px, 19vw, 430px)` plus a fluid `font-size` on `.home-sessions`, with every child sized in `em` — reintroducing `rem`/px type inside the block silently breaks the scaling, and widening the clamp past the gutter reintroduces the overlap the gate exists to prevent. The age stamps are refreshed **in place** by a 20s clock (`_tickHomeSessionsTimes()`, disarmed in `hideHomeSessions()`), never by re-rendering, which would restart every row's blink and working ring. Working state is deliberately byte-identical to the phone's: pulsing green dot + the `tab-load-spin` ring reused from the tab strip + the same green halo (added to `.mobile-overview-dot--working` at the same time), so "working" reads the same on every surface; **idle** is deliberately NOT that green — dot and pill mix toward `--text-muted` so a glance separates running from sitting. Live re-renders ride the tail of `_renderSessionTabsImmediate()` alongside the phone overview.
**Command palette + shortcut registry**: `Ctrl/Cmd/Alt+K` opens the session palette; shortcuts live in a rebindable registry (`DEFAULT_SHORTCUTS`/`getShortcutRegistry()`/`matchesShortcutEvent()` in app.js, overrides in `settings.shortcutOverrides`). ⚠️ Palette-chord keys must ALSO be swallowed in `attachCustomKeyEventHandler` (terminal-ui.js) or xterm writes the control byte (0x0B) into the PTY. ⚠️ `saveAppSettings()` rebuilds settings from the DOM, so keys edited elsewhere (`shortcutOverrides`, `showTokenCount`, `showCost`) need explicit `_prev` carry-over. ⚠️ **Smart copy (`Ctrl+C`)** lives in that same handler: with a selection it copies, with none it must `return true` **without** `preventDefault()` or the interrupt is lost. `copyTerminalSelection` is deliberately absent from `SHORTCUT_ACTIONS` because the generic capture loop preventDefaults every match it dispatches. → [architecture-invariants#command-palette-and-shortcut-registry](docs/architecture-invariants.md#command-palette-and-shortcut-registry)
+119 -8
View File
@@ -1,6 +1,6 @@
/**
* @fileoverview Desktop home screen session list: the open tabs as a vertical
* column down the left of the welcome overlay.
* @fileoverview Desktop home screen session list: the open tabs as a rail docked
* down the left edge of the welcome overlay.
*
* The welcome screen centers ~560px of content in a window that is usually
* 1400px+, so the two gutters are dead space. The left one now carries the same
@@ -8,11 +8,19 @@
* one row per live tab, in TAB ORDER (not sorted by state) so it reads as the
* tab strip rotated, and so Alt+1..9 still matches what you see.
*
* DESKTOP ONLY, and only in a wide enough window: the column is absolutely
* DESKTOP ONLY, and only in a wide enough window: the rail is absolutely
* positioned so the centered welcome content never moves, which means it can
* only exist where the gutter is genuinely wider than the column. Below
* only exist where the gutter is genuinely wider than the rail. Below
* `HOME_SESSIONS_MIN_WIDTH` nothing renders; on a phone the mobile overview owns
* the home screen entirely and this surface stays out of its way.
* the home screen entirely and this surface stays out of its way. Width and type
* both scale with the viewport (see the `.home-sessions` block in styles.css) —
* a fixed 256px card looks abandoned on a 2560px display.
*
* Each row carries when the session was FIRST CREATED and when it was LAST
* ACTIVE, both relative. Those two stamps go stale on their own (a sitting
* session emits no event), so a slow clock refreshes them IN PLACE from the
* epoch-ms values parked on the elements, rather than re-rendering: a re-render
* would restart every row's blink animation and its working ring.
*
* The working state is deliberately identical to the phone's: a pulsing green
* dot ringed by the spinner a tab shows while it loads (`tab-load-spin`, reused
@@ -27,19 +35,23 @@
* @mixin Extends CodemanApp.prototype via Object.assign
* @dependency app.js (this.sessions, this.cases, this.pendingHooks, selectSession)
* @dependency mobile-overview.js (_mobileOverviewState, _mobileOverviewCaseFor, shouldUseMobileOverview)
* @dependency ralph-panel.js (formatRelativeTime — the app's one relative-time formatter)
* @dependency webview-tabs.js (this.webviews, this.webviewOrder, openWebview)
* @dependency mobile-handlers.js (MobileDetection)
* @loadorder 12.56 of 16, after mobile-overview.js, before entrance-animations.js
*/
/**
* Narrowest window that gets the column. The welcome content is 560px wide and
* centered, so at 1180px each gutter is 310px — enough for the 256px column plus
* its 20px offset and still a visible gap. Anything narrower would overlap the
* Narrowest window that gets the rail. The welcome content is 560px wide and
* centered, so at 1180px each gutter is 310px — enough for the rail at its
* 250px floor and still a visible gap. Anything narrower would overlap the
* search panel, which is why this is a width gate and not a device-type gate.
*/
const HOME_SESSIONS_MIN_WIDTH = 1180;
/** How often the relative stamps are rewritten while the home screen is up. */
const HOME_SESSIONS_CLOCK_MS = 20000;
/** Pill copy per state. Same words as the phone overview, same reasons. */
const HOME_SESSIONS_PILL_LABEL = {
needs: 'needs you',
@@ -87,6 +99,7 @@ Object.assign(CodemanApp.prototype, {
this._wireHomeSessions(el);
if (!this.shouldShowHomeSessions()) {
el.hidden = true;
this._stopHomeSessionsClock();
return;
}
el.hidden = false;
@@ -96,6 +109,7 @@ Object.assign(CodemanApp.prototype, {
hideHomeSessions() {
const el = document.getElementById('homeSessions');
if (el) el.hidden = true;
this._stopHomeSessionsClock();
},
/** Re-render only when showing (called from the tab renderer's tail). */
@@ -173,6 +187,10 @@ Object.assign(CodemanApp.prototype, {
dir: this._shortenHomePath ? this._shortenHomePath(session.workingDir) : session.workingDir || '',
state,
pill: HOME_SESSIONS_PILL_LABEL[state] || state,
// Epoch ms, straight off the session payload; formatting happens at
// render time so the clock below can redo it without a re-render.
createdAt: Number(session.createdAt) || 0,
lastActivityAt: Number(session.lastActivityAt) || 0,
};
});
},
@@ -193,6 +211,7 @@ Object.assign(CodemanApp.prototype, {
if (!rows.length && !webviews.length) {
el.hidden = true;
el.replaceChildren();
this._stopHomeSessionsClock();
return;
}
el.hidden = false;
@@ -205,6 +224,95 @@ Object.assign(CodemanApp.prototype, {
for (const row of rows) list.appendChild(this._buildHomeSessionRow(row));
for (const webview of webviews) list.appendChild(this._buildHomeSessionsWebviewRow(webview));
el.appendChild(list);
this._startHomeSessionsClock();
},
// ═══════════════════════════════════════════════════════════════
// Age stamps: created / last active
// ═══════════════════════════════════════════════════════════════
/**
* The "created 2h ago · active 3m ago" footer line. Both stamps keep their raw
* epoch-ms on the element (`data-hs-ts`) so `_tickHomeSessionsTimes()` can
* rewrite the text without rebuilding the row.
*/
_buildHomeSessionsMeta(row) {
const meta = document.createElement('span');
meta.className = 'home-sessions-row-meta';
// Relative times are generated text, and "created"/"active" here are the
// same generic words that mean something else on other surfaces.
meta.setAttribute('data-i18n-skip', '');
meta.appendChild(this._buildHomeSessionsStamp('created', row.createdAt, 'home-sessions-meta-created'));
const sep = document.createElement('span');
sep.className = 'home-sessions-meta-sep';
sep.setAttribute('aria-hidden', 'true');
sep.textContent = '·';
meta.appendChild(sep);
meta.appendChild(this._buildHomeSessionsStamp('active', row.lastActivityAt, 'home-sessions-meta-active'));
return meta;
},
/** One labelled stamp: a dim key, the relative value, full date in the title. */
_buildHomeSessionsStamp(key, timestamp, className) {
const wrap = document.createElement('span');
wrap.className = `home-sessions-meta-item ${className}`;
const label = document.createElement('span');
label.className = 'home-sessions-meta-key';
label.textContent = key;
wrap.appendChild(label);
const value = document.createElement('span');
value.dataset.hsTs = String(timestamp || 0);
value.textContent = this._homeSessionsAgo(timestamp);
wrap.appendChild(value);
if (timestamp)
wrap.title = `${key === 'created' ? 'First created' : 'Last active'}: ${new Date(timestamp).toLocaleString()}`;
return wrap;
},
/** Relative label for a stamp. `formatRelativeTime` is the app's one formatter. */
_homeSessionsAgo(timestamp) {
if (!timestamp) return '—';
return this.formatRelativeTime(timestamp) || '—';
},
/**
* Rewrites the stamps in place every `HOME_SESSIONS_CLOCK_MS`. In place, not a
* re-render: replacing the rows would restart the blink animation on every
* waiting row and the ring on every working one, twice a minute, for nothing.
*/
_startHomeSessionsClock() {
if (this._homeSessionsClock) return;
this._homeSessionsClock = setInterval(() => {
if (!this.isHomeSessionsVisible()) {
this._stopHomeSessionsClock();
return;
}
this._tickHomeSessionsTimes();
}, HOME_SESSIONS_CLOCK_MS);
},
_stopHomeSessionsClock() {
if (!this._homeSessionsClock) return;
clearInterval(this._homeSessionsClock);
this._homeSessionsClock = null;
},
_tickHomeSessionsTimes() {
const el = document.getElementById('homeSessions');
if (!el) return;
for (const node of el.querySelectorAll('[data-hs-ts]')) {
const ts = Number(node.dataset.hsTs) || 0;
const text = this._homeSessionsAgo(ts);
if (node.textContent !== text) node.textContent = text;
}
},
_buildHomeSessionsHeader(count) {
@@ -287,6 +395,9 @@ Object.assign(CodemanApp.prototype, {
pill.textContent = row.pill;
item.appendChild(pill);
// Wraps onto its own line (the row is flex-wrap) so it gets the full width.
item.appendChild(this._buildHomeSessionsMeta(row));
return item;
},
+2 -2
View File
@@ -323,9 +323,9 @@
<!-- Welcome Overlay (shown when no session active) -->
<div class="welcome-overlay" id="welcomeOverlay">
<!-- Open tabs as a vertical column in the left gutter (home-sessions.js).
<!-- Open tabs as a rail docked to the left edge (home-sessions.js).
Absolutely positioned so the centered content below never moves, and
therefore only rendered where the gutter is wider than the column;
therefore only rendered where the gutter is wider than the rail;
ships `hidden` and only that module reveals it. -->
<aside class="home-sessions" id="homeSessions" hidden></aside>
<div class="welcome-content">
+118 -40
View File
@@ -14014,15 +14014,24 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
}
/* ══════════════════════════════════════════════════════════════════════════
Home screen: open tabs in the left gutter (home-sessions.js)
Home screen: open tabs docked down the left edge (home-sessions.js)
The welcome content is 560px wide and centered, so this column lives in dead
The welcome content is 560px wide and centered, so this rail lives in dead
space. It is `position: absolute` precisely so that stays true: the centered
content does not move by a pixel whether the column renders or not. That in
content does not move by a pixel whether the rail renders or not. That in
turn is why the width gate below has to exist — in a narrow window there is
no gutter to sit in, and an absolute box would simply overlap the search
panel. JS gates on the same 1180px so the two can never disagree.
It is FLUSH to the left edge and FULL HEIGHT on purpose: a floating, vertically
centered card in the middle of the gutter reads as debris, a docked rail with
an edge to hang off reads as part of the chrome.
Everything inside scales off ONE knob — the `font-size` below — because every
child sizes in `em`. Widen the window and the whole rail grows with it instead
of leaving 12px type stranded on a 2560px display. Do not reintroduce `rem`
or fixed px sizes for type/spacing here; that is what breaks the scaling.
The working dot is the phone's, exactly: pulsing green ringed by the very
same `tab-load-spin` a tab shows while it loads (reused from above, never
re-declared), plus a green halo. One signal, one motion, both home screens.
@@ -14030,14 +14039,20 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
left: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
gap: 8px;
width: 256px;
max-height: calc(100% - 3rem);
gap: 0.9em;
/* Ramps with the viewport, floored so it still clears the centered content at
the 1180px gate and capped so it never becomes the main event. */
width: clamp(250px, 19vw, 430px);
padding: 1.5em 1.1em;
background: linear-gradient(90deg, color-mix(in srgb, var(--bg-card) 65%, transparent) 0%, transparent 100%);
border-right: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
/* The single scale knob: ~12px at the gate, ~17px on a 2560px display. */
font-size: clamp(12.2px, calc(8px + 0.35vw), 17px);
text-align: left;
z-index: 1;
}
@@ -14059,12 +14074,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-header {
display: flex;
align-items: center;
gap: 8px;
padding: 0 6px;
gap: 0.66em;
flex-shrink: 0;
padding: 0 0.5em;
}
.home-sessions-title {
font-size: 0.66rem;
font-size: 0.87em;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
@@ -14075,25 +14091,29 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
min-width: 1.5em;
height: 1.35em;
padding: 0 0.42em;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.6rem;
font-size: 0.79em;
font-weight: 700;
font-family: monospace;
}
/* `min-height: 0` is what lets this scroll inside the full-height rail: without
it the flex item's auto minimum keeps growing and the overflow never engages. */
.home-sessions-list {
display: flex;
flex-direction: column;
gap: 4px;
gap: 0.35em;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 2px 6px;
padding: 0.16em 0.16em 0.5em;
}
.home-sessions-list::-webkit-scrollbar {
@@ -14108,15 +14128,16 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
gap: 0.66em;
width: 100%;
padding: 7px 9px;
border-radius: 9px;
padding: 0.6em 0.75em;
border-radius: 0.75em;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-dim);
font-family: inherit;
font-size: 0.76rem;
font-size: 1em;
text-align: left;
cursor: pointer;
transition: background var(--transition-smooth), border-color var(--transition-smooth), color var(--transition-smooth);
@@ -14136,14 +14157,14 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
display: inline-flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
width: 1.3em;
height: 1.3em;
flex-shrink: 0;
border-radius: 3px;
border-radius: 0.25em;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-size: 0.76em;
font-weight: 700;
font-family: monospace;
}
@@ -14151,8 +14172,8 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-dot {
position: relative;
flex-shrink: 0;
width: 9px;
height: 9px;
width: 0.75em;
height: 0.75em;
border-radius: 50%;
background: var(--text-muted);
}
@@ -14166,8 +14187,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
background: var(--yellow);
}
/* Idle is green, but a MUTED green: still unmistakably "fine", visibly greyer
and darker than the vivid green a working session gets, so a glance down the
rail separates "running right now" from "sitting there" without reading a
word. Mixed toward --text-muted rather than hardcoded, so every skin keeps
its own green. */
.home-sessions-dot--idle {
background: var(--green);
background: color-mix(in srgb, var(--green) 42%, var(--text-muted));
}
.home-sessions-dot--done {
@@ -14189,7 +14215,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-dot--working::after {
content: '';
position: absolute;
inset: -4px;
inset: -0.34em;
border: 2px solid color-mix(in srgb, var(--green) 25%, transparent);
border-top-color: var(--green);
border-radius: 50%;
@@ -14199,7 +14225,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row-body {
display: flex;
flex-direction: column;
gap: 1px;
gap: 0.12em;
min-width: 0;
flex: 1;
}
@@ -14207,7 +14233,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-row-title {
display: flex;
align-items: center;
gap: 5px;
gap: 0.42em;
min-width: 0;
color: var(--text);
font-weight: 600;
@@ -14220,21 +14246,66 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
}
.home-sessions-row-sub {
font-size: 0.66rem;
font-size: 0.87em;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Age line: when the session was first created and when it last did anything.
Both are relative and refreshed in place on a timer (home-sessions.js), never
by a re-render — a re-render would restart the row's blink animation. */
.home-sessions-row-meta {
display: flex;
align-items: center;
gap: 0.4em;
/* Wraps to its own full-width line under the row (the row is `flex-wrap`),
so the stamps get the whole width instead of the sliver left beside the
status pill and ellipsize away on the narrow end of the rail. */
flex: 0 0 100%;
min-width: 0;
margin-top: -0.2em;
font-size: 0.78em;
font-family: monospace;
color: var(--text-muted);
opacity: 0.75;
white-space: nowrap;
overflow: hidden;
}
.home-sessions-meta-item {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.home-sessions-meta-key {
margin-right: 0.35em;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.home-sessions-meta-sep {
opacity: 0.45;
}
/* The freshest signal on the row: while a session is actually doing something,
its "active" stamp is the one the eye should land on. */
.home-sessions-row--working .home-sessions-meta-active {
color: var(--green);
opacity: 0.95;
}
.home-sessions-mode {
flex-shrink: 0;
padding: 0 4px;
border-radius: 3px;
padding: 0 0.35em;
border-radius: 0.25em;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.55rem;
font-size: 0.72em;
font-weight: 700;
font-family: monospace;
text-transform: uppercase;
@@ -14242,12 +14313,12 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
.home-sessions-pill {
flex-shrink: 0;
padding: 2px 6px;
padding: 0.2em 0.55em;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-size: 0.76em;
font-weight: 700;
letter-spacing: 0.02em;
white-space: nowrap;
@@ -14266,13 +14337,20 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
color: var(--yellow);
}
.home-sessions-pill--working,
.home-sessions-pill--idle {
.home-sessions-pill--working {
background: color-mix(in srgb, var(--green) 15%, transparent);
border-color: color-mix(in srgb, var(--green) 40%, transparent);
color: var(--green);
}
/* Same muting as the idle dot, one step further: the pill is a block of color,
so it reads louder than a 9px dot at the same mix. */
.home-sessions-pill--idle {
background: color-mix(in srgb, var(--green) 7%, transparent);
border-color: color-mix(in srgb, var(--green) 18%, var(--border));
color: color-mix(in srgb, var(--green) 45%, var(--text-muted));
}
/* Row accents: same language as the session tabs and the phone overview — red
means a question is pending, yellow means it wants input, green means work is
happening. Nothing else on this screen may reuse these colors. */