feat(home): open-tab column on the desktop home screen; bigger phone home button

The welcome overlay centers ~560px of content in a ~1400px window, so both
gutters are dead space. The left one now carries the open tabs as a vertical
list (home-sessions.js): one row per live session plus saved web tabs, in TAB
order rather than by urgency, because the row badges are the Alt+1..9 indices.
Clicking a row enters that session.

Working state is deliberately the phone's, exactly: a pulsing green dot ringed
by the same tab-load-spin the tab strip uses while a tab loads, now with a green
halo added on both surfaces so "working" reads identically wherever you see it.

The column is position:absolute so the centered content never moves, which is
why it needs a width gate in two places (HOME_SESSIONS_MIN_WIDTH = 1180 in JS,
a max-width: 1179px media query as the backstop for a resize that outruns the
matchMedia listener). A test pins the two equal. State classification is reused
from mobile-overview.js rather than re-derived, so the two home screens cannot
disagree about what counts as needing you.

Phones keep the mobile overview, and their brand "C" was a 0.85rem inline span,
roughly a 12x13px target on the one control that gets you back to that screen.
It is now a 44px-wide button filling the full header height, with the glyph
scaled to match. 44 is horizontal only: the phone header is pinned to 36px and
clips overflow, so a true 44x44 would mean taking height off the terminal.

Verified end to end against a real isolated instance (own tmux socket + data
dir): 18 browser checks covering render, live update through the tab renderer,
the working dot's animation/glow/ring, row click, the narrow-window gate, the
phone fallback, and a real touch tap on the far corner of the new hit box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-09 18:34:35 +02:00
parent e6df0c4094
commit 4add38c4b1
10 changed files with 946 additions and 6 deletions
+2
View File
@@ -3686,6 +3686,8 @@ class CodemanApp {
// (create, delete, idle, working, exit, hook alerts via updateTabAlertFromHooks)
// already funnels through here. No-ops unless that surface is showing.
this._refreshMobileOverviewIfVisible?.();
// Same deal for the desktop home screen's tab column.
this._refreshHomeSessionsIfVisible?.();
}
// Auto-wrap desktop session tabs to a second row when they overflow one row,
+335
View File
@@ -0,0 +1,335 @@
/**
* @fileoverview Desktop home screen session list: the open tabs as a vertical
* column down the left 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
* list a phone gets on its home screen (mobile-overview.js), turned vertical:
* 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
* positioned so the centered welcome content never moves, which means it can
* only exist where the gutter is genuinely wider than the column. 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 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
* from styles.css), plus a green halo. Same signal, same motion, both surfaces.
*
* Everything renders from state the page already holds (`this.sessions`,
* `this.cases`, `this.pendingHooks`, `this.webviews`) — no endpoint, no SSE
* event, no schema. State classification and case matching are reused from
* mobile-overview.js rather than re-derived, so the two home screens can never
* disagree about what "working" means.
*
* @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 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
* search panel, which is why this is a width gate and not a device-type gate.
*/
const HOME_SESSIONS_MIN_WIDTH = 1180;
/** Pill copy per state. Same words as the phone overview, same reasons. */
const HOME_SESSIONS_PILL_LABEL = {
needs: 'needs you',
error: 'error',
waiting: 'waiting',
working: 'working',
idle: 'idle',
done: 'done',
};
/** Short backend badge, mirroring `.tab-mode` in the tab strip. */
const HOME_SESSIONS_MODE_BADGE = {
shell: 'sh',
opencode: 'oc',
codex: 'cx',
gemini: 'gm',
antigravity: 'ag',
};
Object.assign(CodemanApp.prototype, {
// ═══════════════════════════════════════════════════════════════
// Gate + visibility
// ═══════════════════════════════════════════════════════════════
/**
* Width-driven, like every other layout decision in the app. Explicitly yields
* to the phone overview: that surface already lists the same sessions, and two
* lists of the same thing on one screen is worse than none.
*/
shouldShowHomeSessions() {
if (this.isSoloWindow) return false;
if (this.shouldUseMobileOverview?.()) return false;
return window.innerWidth >= HOME_SESSIONS_MIN_WIDTH;
},
/** True while the column is the visible home surface. */
isHomeSessionsVisible() {
const el = document.getElementById('homeSessions');
return !!el && !el.hidden;
},
showHomeSessions() {
const el = document.getElementById('homeSessions');
if (!el) return;
this._wireHomeSessions(el);
if (!this.shouldShowHomeSessions()) {
el.hidden = true;
return;
}
el.hidden = false;
this.renderHomeSessions();
},
hideHomeSessions() {
const el = document.getElementById('homeSessions');
if (el) el.hidden = true;
},
/** Re-render only when showing (called from the tab renderer's tail). */
_refreshHomeSessionsIfVisible() {
if (!this.isHomeSessionsVisible()) return;
this._debouncedCall('homeSessions', () => this.renderHomeSessions(), 150);
},
/**
* One delegated click listener for every row, plus a width listener so
* resizing the window while on the home screen adds or drops the column
* instead of leaving it overlapping the content it was sized to clear.
*/
_wireHomeSessions(el) {
if (this._homeSessionsWired) return;
this._homeSessionsWired = true;
el.addEventListener('click', (event) => {
const target = event.target?.closest?.('[data-hs-action]');
if (!target) return;
if (target.dataset.hsAction === 'session') {
void this.selectSession(target.dataset.hsSession);
} else if (target.dataset.hsAction === 'webview') {
void this.openWebview?.(target.dataset.hsWebview);
}
});
if (window.matchMedia) {
const mq = window.matchMedia(`(min-width: ${HOME_SESSIONS_MIN_WIDTH}px)`);
const onChange = () => {
// Only relevant while the welcome screen is up; entering a session
// re-decides through hideWelcome()/showWelcome() anyway.
if (this.activeSessionId) return;
const overlay = document.getElementById('welcomeOverlay');
if (!overlay || !overlay.classList.contains('visible')) return;
this.showHomeSessions();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
}
},
// ═══════════════════════════════════════════════════════════════
// Model
// ═══════════════════════════════════════════════════════════════
/**
* One row per live session, in the user's tab order. State classification is
* `_mobileOverviewState()` (mobile-overview.js) so both home screens agree on
* what counts as needing you; the ORDER differs on purpose — the phone sorts
* by urgency because it shows one screenful at a time, this column mirrors the
* tab strip so the number badges line up with Alt+1..9.
* @returns {Array<object>} row descriptors, ready to render
*/
buildHomeSessionRows() {
const cases = Array.isArray(this.cases) ? this.cases : [];
const order = Array.isArray(this.sessionOrder) ? this.sessionOrder : [];
const ids = order.filter((id) => this.sessions?.has(id));
// A session created before the order list caught up would otherwise be
// invisible here while its tab already exists.
for (const id of this.sessions?.keys() || []) if (!ids.includes(id)) ids.push(id);
return ids.map((id, index) => {
const session = this.sessions.get(id);
const matched = this._mobileOverviewCaseFor(session.workingDir, cases);
const state = this._mobileOverviewState(session, this.pendingHooks?.get(id));
const mode = session.mode || 'claude';
return {
id,
index,
name: this.getSessionName ? this.getSessionName(session) : session.name || id.slice(0, 8),
mode,
modeBadge: HOME_SESSIONS_MODE_BADGE[mode] || '',
caseName: matched ? matched.name : '',
dir: this._shortenHomePath ? this._shortenHomePath(session.workingDir) : session.workingDir || '',
state,
pill: HOME_SESSIONS_PILL_LABEL[state] || state,
};
});
},
// ═══════════════════════════════════════════════════════════════
// Render
// ═══════════════════════════════════════════════════════════════
renderHomeSessions() {
const el = document.getElementById('homeSessions');
if (!el) return;
const rows = this.buildHomeSessionRows();
const webviews = (this.webviewOrder || []).map((id) => this.webviews?.get(id)).filter(Boolean);
// Nothing open means nothing to list: an empty framed box next to a
// first-run welcome screen is noise, not information.
if (!rows.length && !webviews.length) {
el.hidden = true;
el.replaceChildren();
return;
}
el.hidden = false;
el.replaceChildren();
el.appendChild(this._buildHomeSessionsHeader(rows.length + webviews.length));
const list = document.createElement('div');
list.className = 'home-sessions-list';
for (const row of rows) list.appendChild(this._buildHomeSessionRow(row));
for (const webview of webviews) list.appendChild(this._buildHomeSessionsWebviewRow(webview));
el.appendChild(list);
},
_buildHomeSessionsHeader(count) {
const header = document.createElement('div');
header.className = 'home-sessions-header';
const label = document.createElement('span');
label.className = 'home-sessions-title';
label.textContent = 'Open tabs';
header.appendChild(label);
const badge = document.createElement('span');
badge.className = 'home-sessions-count';
badge.setAttribute('data-i18n-skip', '');
badge.textContent = String(count);
header.appendChild(badge);
return header;
},
/**
* A session row. The state class drives the same visual language as the
* session tabs and the phone overview: green dot when it is fine (pulsing and
* ringed by the load spinner while working), a yellow row when it wants input,
* a red row when it asked a question.
*/
_buildHomeSessionRow(row) {
const item = document.createElement('button');
item.type = 'button';
item.className = 'home-sessions-row home-sessions-row--' + row.state;
item.dataset.hsAction = 'session';
item.dataset.hsSession = row.id;
item.title = row.dir ? `${row.name} (${row.dir})` : row.name;
if (row.index < 9) {
const number = document.createElement('span');
number.className = 'home-sessions-number';
number.setAttribute('data-i18n-skip', '');
number.textContent = String(row.index + 1);
item.appendChild(number);
}
const dot = document.createElement('span');
dot.className = 'home-sessions-dot home-sessions-dot--' + row.state;
dot.setAttribute('aria-hidden', 'true');
item.appendChild(dot);
const body = document.createElement('span');
body.className = 'home-sessions-row-body';
const line1 = document.createElement('span');
line1.className = 'home-sessions-row-title';
if (row.modeBadge) {
const badge = document.createElement('span');
badge.className = `home-sessions-mode ${row.mode}`;
badge.setAttribute('data-i18n-skip', '');
badge.textContent = row.modeBadge;
line1.appendChild(badge);
}
const name = document.createElement('span');
// .session-name is in the i18n skip list: a session name is user content.
name.className = 'session-name';
name.textContent = row.name;
line1.appendChild(name);
body.appendChild(line1);
const line2 = document.createElement('span');
line2.className = 'home-sessions-row-sub';
line2.setAttribute('data-i18n-skip', '');
line2.textContent = row.caseName || row.dir || row.mode;
body.appendChild(line2);
item.appendChild(body);
const pill = document.createElement('span');
pill.className = 'home-sessions-pill home-sessions-pill--' + row.state;
// Skipped by i18n on purpose: generic single words ("idle", "done", "error")
// that collide with state strings on other surfaces.
pill.setAttribute('data-i18n-skip', '');
pill.textContent = row.pill;
item.appendChild(pill);
return item;
},
/** A saved dashboard, listed after the sessions exactly as in the tab strip. */
_buildHomeSessionsWebviewRow(webview) {
const item = document.createElement('button');
item.type = 'button';
item.className = 'home-sessions-row home-sessions-row--web';
item.dataset.hsAction = 'webview';
item.dataset.hsWebview = webview.id;
item.title = webview.url || webview.name;
const dot = document.createElement('span');
dot.className = 'home-sessions-dot home-sessions-dot--web';
dot.setAttribute('aria-hidden', 'true');
item.appendChild(dot);
const body = document.createElement('span');
body.className = 'home-sessions-row-body';
const title = document.createElement('span');
title.className = 'home-sessions-row-title';
const name = document.createElement('span');
// A dashboard name is user content.
name.className = 'case-name';
name.textContent = webview.name;
title.appendChild(name);
body.appendChild(title);
const sub = document.createElement('span');
sub.className = 'home-sessions-row-sub';
sub.setAttribute('data-i18n-skip', '');
sub.textContent = webview.url || '';
body.appendChild(sub);
item.appendChild(body);
const pill = document.createElement('span');
pill.className = 'home-sessions-pill home-sessions-pill--web';
pill.setAttribute('data-i18n-skip', '');
pill.textContent = 'web';
item.appendChild(pill);
return item;
},
});
+3
View File
@@ -387,6 +387,9 @@
'在手机上,点击 C 图标打开会话概览(需要你 / 空间 / 空闲),而不是欢迎页',
Phone: '手机',
// Desktop home screen tab column (home-sessions.js)
'Open tabs': '打开的标签',
// Session/case dialogs
'Session Options': '会话选项',
'Session Name': '会话名称',
+6
View File
@@ -322,6 +322,11 @@
<!-- 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).
Absolutely positioned so the centered content below never moves, and
therefore only rendered where the gutter is wider than the column;
ships `hidden` and only that module reveals it. -->
<aside class="home-sessions" id="homeSessions" hidden></aside>
<div class="welcome-content">
<h1 class="welcome-title">Codeman</h1>
<p class="welcome-desc">Manage AI Coding tools in persistent tmux sessions.</p>
@@ -2769,6 +2774,7 @@
<script defer src="session-ui.js"></script>
<script defer src="webview-tabs.js"></script>
<script defer src="mobile-overview.js"></script>
<script defer src="home-sessions.js"></script>
<script defer src="entrance-animations.js"></script>
<script defer src="ralph-wizard.js"></script>
<script defer src="api-client.js"></script>
+40 -4
View File
@@ -353,13 +353,46 @@ html.mobile-init .file-browser-panel {
/* Phone brand collapses to a single "C" home button: hide the wordmark,
keep the tap target */
.header-brand {
padding-right: 0.25rem;
margin-right: 0.2rem;
padding-right: 0;
margin-right: 0.1rem;
border-right: none;
/* styles.css sizes this to the FULL header height, so inside the header's
0.15rem vertical padding it overflows and the last 2.4px are clipped
(`overflow: hidden` on the phone header). Harmless while the brand was
bare text; it would cut the bottom off the tap target's pressed state.
Cancelling the padding for this one item is what makes the button fill
the header edge to edge. `height: 100%` cannot do it: the header sets
min/max-height rather than height, so the percentage has no definite
containing block to resolve against and silently falls back to auto. */
margin-block: -0.15rem;
}
/* The "C" was a 0.85rem inline span — about a 12x13px hit area, far under the
44px minimum, on the one control that gets you back to the home screen.
It is now a real 44px-wide button spanning the full header height, with the
glyph scaled to match. 44 is reachable on the horizontal axis only: the
phone header is pinned to 36px tall (min-height/max-height below) and
`overflow: hidden` clips anything taller, so a true 44x44 would mean growing
the header and taking that height off the terminal. The negative margin
spends the header's OWN left padding on the target instead of pushing the
tab strip right. */
.header-brand .logo {
font-size: 0.85rem;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
/* Taller than its parent on purpose: centred in the padded brand box, this
makes the button fill all 36 header pixels edge to edge. */
height: var(--header-height);
margin-left: -0.3rem;
font-size: 1.15rem;
line-height: 1;
border-radius: 8px;
-webkit-tap-highlight-color: transparent;
}
.header-brand .logo:active {
background: rgba(96, 165, 250, 0.16);
}
.header-brand .logo .logo-text {
@@ -2690,10 +2723,13 @@ html.mobile-init .file-browser-panel {
}
/* Same as .session-tab .tab-status: green when the session is fine, and the
shared `pulse` keyframes while it is working. */
shared `pulse` keyframes while it is working. The halo matches the busy tab
dot and the desktop home column (.home-sessions-dot--working, styles.css):
working reads identically on every surface or it reads as three features. */
.mobile-overview-dot--working {
background: var(--green);
animation: pulse 1.5s infinite;
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--green) 55%, transparent);
will-change: opacity;
}
+296
View File
@@ -13922,3 +13922,299 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
color: #8b93a1;
min-height: 1em;
}
/* ══════════════════════════════════════════════════════════════════════════
Home screen: open tabs in the left gutter (home-sessions.js)
The welcome content is 560px wide and centered, so this column 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
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.
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.
══════════════════════════════════════════════════════════════════════════ */
.home-sessions {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 8px;
width: 256px;
max-height: calc(100% - 3rem);
text-align: left;
z-index: 1;
}
/* `hidden` has to be re-asserted over the display above, or the module's only
lever (el.hidden) does nothing. */
.home-sessions[hidden] {
display: none;
}
/* Belt and braces with shouldShowHomeSessions(): a resize that outruns the
matchMedia listener must never leave the column overlapping the content. */
@media (max-width: 1179px) {
.home-sessions {
display: none !important;
}
}
.home-sessions-header {
display: flex;
align-items: center;
gap: 8px;
padding: 0 6px;
}
.home-sessions-title {
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
}
.home-sessions-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.6rem;
font-weight: 700;
font-family: monospace;
}
.home-sessions-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 2px 6px;
}
.home-sessions-list::-webkit-scrollbar {
width: 4px;
}
.home-sessions-list::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.home-sessions-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 9px;
border-radius: 9px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-dim);
font-family: inherit;
font-size: 0.76rem;
text-align: left;
cursor: pointer;
transition: background var(--transition-smooth), border-color var(--transition-smooth), color var(--transition-smooth);
}
.home-sessions-row:hover {
background: var(--bg-hover);
border-color: rgba(34, 197, 94, 0.35);
color: var(--text);
}
.home-sessions-row:active {
background: rgba(34, 197, 94, 0.12);
}
.home-sessions-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
flex-shrink: 0;
border-radius: 3px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-weight: 700;
font-family: monospace;
}
.home-sessions-dot {
position: relative;
flex-shrink: 0;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--text-muted);
}
.home-sessions-dot--needs,
.home-sessions-dot--error {
background: var(--red);
}
.home-sessions-dot--waiting {
background: var(--yellow);
}
.home-sessions-dot--idle {
background: var(--green);
}
.home-sessions-dot--done {
background: var(--text-muted);
opacity: 0.5;
}
.home-sessions-dot--web {
background: #60a5fa;
}
.home-sessions-dot--working {
background: var(--green);
animation: pulse 1.5s infinite;
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--green) 55%, transparent);
will-change: opacity;
}
.home-sessions-dot--working::after {
content: '';
position: absolute;
inset: -4px;
border: 2px solid color-mix(in srgb, var(--green) 25%, transparent);
border-top-color: var(--green);
border-radius: 50%;
animation: tab-load-spin 0.7s linear infinite;
}
.home-sessions-row-body {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
flex: 1;
}
.home-sessions-row-title {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
color: var(--text);
font-weight: 600;
}
.home-sessions-row-title .session-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-sessions-row-sub {
font-size: 0.66rem;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-sessions-mode {
flex-shrink: 0;
padding: 0 4px;
border-radius: 3px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.55rem;
font-weight: 700;
font-family: monospace;
text-transform: uppercase;
}
.home-sessions-pill {
flex-shrink: 0;
padding: 2px 6px;
border-radius: 999px;
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.58rem;
font-weight: 700;
letter-spacing: 0.02em;
white-space: nowrap;
}
.home-sessions-pill--needs,
.home-sessions-pill--error {
background: color-mix(in srgb, var(--red) 18%, transparent);
border-color: color-mix(in srgb, var(--red) 45%, transparent);
color: var(--red);
}
.home-sessions-pill--waiting {
background: color-mix(in srgb, var(--yellow) 18%, transparent);
border-color: color-mix(in srgb, var(--yellow) 45%, transparent);
color: var(--yellow);
}
.home-sessions-pill--working,
.home-sessions-pill--idle {
background: color-mix(in srgb, var(--green) 15%, transparent);
border-color: color-mix(in srgb, var(--green) 40%, transparent);
color: var(--green);
}
/* 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. */
.home-sessions-row--needs,
.home-sessions-row--error {
border-color: color-mix(in srgb, var(--red) 50%, transparent);
animation: home-sessions-blink-red 2.5s ease-in-out infinite;
}
.home-sessions-row--waiting {
border-color: color-mix(in srgb, var(--yellow) 50%, transparent);
animation: home-sessions-blink-yellow 3.5s ease-in-out infinite;
}
.home-sessions-row--working {
border-color: color-mix(in srgb, var(--green) 35%, transparent);
}
@keyframes home-sessions-blink-red {
0%, 100% { border-color: color-mix(in srgb, var(--red) 50%, transparent); }
50% { border-color: color-mix(in srgb, var(--red) 95%, transparent); }
}
@keyframes home-sessions-blink-yellow {
0%, 100% { border-color: color-mix(in srgb, var(--yellow) 45%, transparent); }
50% { border-color: color-mix(in srgb, var(--yellow) 90%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
.home-sessions-row,
.home-sessions-dot,
.home-sessions-dot::after {
animation: none !important;
}
}
+5
View File
@@ -1427,6 +1427,7 @@ Object.assign(CodemanApp.prototype, {
if (this.shouldUseMobileOverview?.()) {
const overlay = document.getElementById('welcomeOverlay');
if (overlay) overlay.classList.remove('visible');
this.hideHomeSessions?.();
this.showMobileOverview();
this._updateCjkInputState?.();
return;
@@ -1439,6 +1440,9 @@ Object.assign(CodemanApp.prototype, {
this.applyWelcomeCliVisibility();
this.loadHistorySessions();
this.initSearchPanel();
// Open tabs down the left gutter. Self-gating: a window too narrow to hold
// the column without overlapping the content leaves it hidden.
this.showHomeSessions?.();
}
// Home screen has no input target — hide the CJK textarea (activeSessionId
// is null by the time we get here). Guarded: defined on the app object.
@@ -1447,6 +1451,7 @@ Object.assign(CodemanApp.prototype, {
hideWelcome() {
this.hideMobileOverview?.();
this.hideHomeSessions?.();
const overlay = document.getElementById('welcomeOverlay');
if (overlay) {
overlay.classList.remove('visible');