mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
feat(welcome): one primary launcher plus a calm chip row
The welcome overview drew every CLI as its own gradient pill (one colour, gradient and glow per CLI, plus skin overrides that re-tinted three of them), all at the same size with the same generic play icon, wrapping into an unaligned cluster with Cloudflare Tunnel styled as one more launcher. Nothing said which action mattered. Now the screen has one obvious thing to press: - A single primary button for the first AGENT in the registry catalog (Claude Code on a stock install), in the accent fill the toolbar's Run button uses, with the CLI's real logo on a small light disc (a brand mark straight on the accent turns muddy) and the same translatable "Run <label>" text. - Every other enabled CLI as a slim, uniform pill under it, in the Compact header's language: control-bg surface, control-border, small type, the logo from the shared run-mode-dot slot, the bare name, and "Run <label>" as tooltip and accessible name. Catalog order, centred, wrapping when needed. - Cloudflare Tunnel as a quiet text link under the launchers (same id, handler and cloudflared gate; text colour carries the active and connecting states), still directly above the QR it reveals. The primary is chosen by catalog order and kind, never by an id, so with Claude disabled the next agent takes the slot and a custom CLI listed first gets it like any other. The buttons carry no per-CLI class any more: the id travels only as data-mode and the logo slot, and all the per-CLI welcome gradients and their skin overrides are gone. Everything is token-driven, so it follows every skin; OG gets its own toolbar Run blue with light text because its --accent-ink is an unused placeholder. Focus rings sit offset from the fills, hover motion is off under prefers-reduced-motion, and the phone rules (reached only with the phone overview off) span the primary and keep the chips wrapping. Tests: run-mode-ui pins catalog order across primary and chips, the kind-based primary choice (Claude off, shell listed first, custom agent first, shell only), the logo slot and absence of id classes, the token-only CSS with no .welcome-btn rule left, and the tunnel link's place between the launchers and the QR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,7 @@
|
|||||||
'Run DeepSeek': '运行 DeepSeek',
|
'Run DeepSeek': '运行 DeepSeek',
|
||||||
'Run OMP': '运行 OMP',
|
'Run OMP': '运行 OMP',
|
||||||
'Run Shell': '运行 Shell',
|
'Run Shell': '运行 Shell',
|
||||||
|
'More tools': '更多工具',
|
||||||
'Select AI backend': '选择 AI 后端',
|
'Select AI backend': '选择 AI 后端',
|
||||||
'Create New Case': '新建案例',
|
'Create New Case': '新建案例',
|
||||||
'Create new case': '新建案例',
|
'Create new case': '新建案例',
|
||||||
|
|||||||
@@ -460,9 +460,14 @@
|
|||||||
<div class="welcome-content">
|
<div class="welcome-content">
|
||||||
<h1 class="welcome-title">Codeman</h1>
|
<h1 class="welcome-title">Codeman</h1>
|
||||||
<p class="welcome-desc">Manage AI Coding tools in persistent tmux sessions.</p>
|
<p class="welcome-desc">Manage AI Coding tools in persistent tmux sessions.</p>
|
||||||
|
<!-- Launchers (settings-ui.js renderWelcomeCliActions): one primary
|
||||||
|
button for the first agent in the registry catalog, then a chip
|
||||||
|
row for every other enabled CLI. The tunnel is not a launcher, so
|
||||||
|
it sits under them as a quiet link; settings-ui.js rewrites its
|
||||||
|
contents on every state change and owns its inline display. -->
|
||||||
<div class="welcome-actions">
|
<div class="welcome-actions">
|
||||||
<div class="welcome-cli-actions" id="welcomeCliActions"></div>
|
<div class="welcome-cli-actions" id="welcomeCliActions"></div>
|
||||||
<button class="welcome-btn welcome-btn-tunnel" id="welcomeTunnelBtn" style="display: none;" onclick="app.toggleTunnelFromWelcome()">
|
<button type="button" class="welcome-tunnel-link" id="welcomeTunnelBtn" style="display: none;" onclick="app.toggleTunnelFromWelcome()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||||
Cloudflare Tunnel
|
Cloudflare Tunnel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1705,17 +1705,21 @@ html.mobile-init .file-browser-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-actions {
|
.welcome-actions {
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-btn {
|
/* Only reached with the phone overview switched off. The primary spans the
|
||||||
|
column; the chips keep wrapping, a little taller for a thumb. */
|
||||||
|
.welcome-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
padding: 0.75rem 1rem;
|
font-size: 0.9rem;
|
||||||
font-size: 0.85rem;
|
}
|
||||||
|
|
||||||
|
.welcome-chip {
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-show-more {
|
.history-show-more {
|
||||||
|
|||||||
@@ -1645,40 +1645,81 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return flags[tool] !== false;
|
return flags[tool] !== false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Render the registry's enabled, available CLIs as welcome-screen actions. */
|
/**
|
||||||
|
* Render the registry's enabled, available CLIs as welcome-screen actions:
|
||||||
|
* ONE primary button, then every other entry as a slim chip in a row under it.
|
||||||
|
*
|
||||||
|
* The primary is the first AGENT in catalog order (the first entry whose kind
|
||||||
|
* is not 'shell'), so on a stock install it is Claude Code, and with Claude
|
||||||
|
* disabled or missing it is simply the next agent; only a catalog with no agent
|
||||||
|
* at all promotes the shell. Chosen from the catalog's order and kind, never by
|
||||||
|
* an id: the registry decides what comes first. Everything else keeps catalog
|
||||||
|
* order inside the chip row, so the DOM order across both is the catalog's.
|
||||||
|
*
|
||||||
|
* The CLI id travels only as DATA: `data-mode` for the click, and the
|
||||||
|
* `run-mode-dot <id>` logo slot every Run menu shares (styles.css draws the
|
||||||
|
* brand mark, or a plain dot for an id it has no logo for). No per-id class on
|
||||||
|
* the buttons themselves, so no rule anywhere can give one CLI its own look.
|
||||||
|
*/
|
||||||
renderWelcomeCliActions() {
|
renderWelcomeCliActions() {
|
||||||
const container = document.getElementById('welcomeCliActions');
|
const container = document.getElementById('welcomeCliActions');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const catalog = Array.isArray(window.__codemanCliCatalog) ? window.__codemanCliCatalog : [];
|
const catalog = Array.isArray(window.__codemanCliCatalog) ? window.__codemanCliCatalog : [];
|
||||||
|
const offered = catalog.filter((cli) => cli.enabled && this.isCliAvailable(cli.id));
|
||||||
|
const primary = offered.find((cli) => cli.kind !== 'shell') || offered[0];
|
||||||
|
// "Run <label>", the strings i18n.js translates ("Run Claude Code", "Run Shell");
|
||||||
|
// a custom CLI's label simply has no dictionary entry, so it renders as typed.
|
||||||
|
const runLabel = (cli) => `Run ${cli.kind === 'shell' ? 'Shell' : cli.label}`;
|
||||||
|
const logo = (cli) => {
|
||||||
|
const dot = document.createElement('span');
|
||||||
|
dot.className = `run-mode-dot ${cli.id}`;
|
||||||
|
dot.setAttribute('aria-hidden', 'true');
|
||||||
|
return dot;
|
||||||
|
};
|
||||||
|
const launch = (cli) => () => {
|
||||||
|
this.setRunMode(cli.id);
|
||||||
|
void this.run();
|
||||||
|
};
|
||||||
container.replaceChildren();
|
container.replaceChildren();
|
||||||
for (const cli of catalog) {
|
if (!primary) return;
|
||||||
if (!cli.enabled || !this.isCliAvailable(cli.id)) continue;
|
|
||||||
const btn = document.createElement('button');
|
const main = document.createElement('button');
|
||||||
btn.type = 'button';
|
main.type = 'button';
|
||||||
btn.className = `welcome-btn welcome-btn-cli welcome-btn-${cli.id}`;
|
main.className = 'welcome-primary';
|
||||||
btn.dataset.mode = cli.id;
|
main.dataset.mode = primary.id;
|
||||||
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
// The mark sits on a small light disc: brand marks (Claude's is orange) turn
|
||||||
icon.setAttribute('width', '20');
|
// muddy straight on the accent fill, and the disc reads on every skin.
|
||||||
icon.setAttribute('height', '20');
|
const disc = document.createElement('span');
|
||||||
icon.setAttribute('viewBox', '0 0 24 24');
|
disc.className = 'welcome-primary-logo';
|
||||||
icon.setAttribute('fill', 'none');
|
disc.appendChild(logo(primary));
|
||||||
icon.setAttribute('stroke', 'currentColor');
|
main.appendChild(disc);
|
||||||
icon.setAttribute('stroke-width', '2');
|
// One raw string, kept whole: i18n.js matches the exact text node.
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
main.append(runLabel(primary));
|
||||||
const play = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
main.onclick = launch(primary);
|
||||||
play.setAttribute('points', '5 3 19 12 5 21 5 3');
|
container.appendChild(main);
|
||||||
icon.appendChild(play);
|
|
||||||
btn.appendChild(icon);
|
const rest = offered.filter((cli) => cli !== primary);
|
||||||
// Same "Run <label>" text the static buttons had ("Run Claude Code", "Run Shell"),
|
if (!rest.length) return;
|
||||||
// left translatable on purpose: i18n.js carries these strings, and a custom CLI's
|
const chips = document.createElement('div');
|
||||||
// label simply has no dictionary entry, so it renders as typed.
|
chips.className = 'welcome-chips';
|
||||||
btn.append(`Run ${cli.kind === 'shell' ? 'Shell' : cli.label}`);
|
chips.setAttribute('role', 'group');
|
||||||
btn.onclick = () => {
|
chips.setAttribute('aria-label', 'More tools');
|
||||||
this.setRunMode(cli.id);
|
for (const cli of rest) {
|
||||||
void this.run();
|
const chip = document.createElement('button');
|
||||||
};
|
chip.type = 'button';
|
||||||
container.appendChild(btn);
|
chip.className = 'welcome-chip';
|
||||||
|
chip.dataset.mode = cli.id;
|
||||||
|
// The chip shows the bare name (the row under "Run …" already says what it
|
||||||
|
// does); the full "Run <label>" is its accessible name and tooltip, both of
|
||||||
|
// which i18n.js translates.
|
||||||
|
chip.title = runLabel(cli);
|
||||||
|
chip.setAttribute('aria-label', runLabel(cli));
|
||||||
|
chip.appendChild(logo(cli));
|
||||||
|
chip.append(cli.kind === 'shell' ? 'Shell' : cli.label);
|
||||||
|
chip.onclick = launch(cli);
|
||||||
|
chips.appendChild(chip);
|
||||||
}
|
}
|
||||||
|
container.appendChild(chips);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+188
-267
@@ -4360,231 +4360,209 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Launchers: ONE primary action, then every other CLI as a slim chip.
|
||||||
|
The primary is the first agent in the registry catalog (settings-ui.js
|
||||||
|
renderWelcomeCliActions), drawn in the accent fill the toolbar's Run button
|
||||||
|
uses, so the screen has one obvious thing to press. The chips are the
|
||||||
|
Compact header's pill language (control-bg surface, control-border, small
|
||||||
|
type): uniform, quiet, and the same on every skin. No rule here is keyed on
|
||||||
|
a CLI id; the only per-CLI look is the logo in the shared `run-mode-dot`
|
||||||
|
slot, which also falls back to a plain dot for an id it has no mark for. */
|
||||||
.welcome-actions {
|
.welcome-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
flex-direction: column;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-cli-actions {
|
.welcome-cli-actions {
|
||||||
display: contents;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.1rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-btn-cli {
|
.welcome-primary {
|
||||||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
|
display: inline-flex;
|
||||||
border-color: rgba(148, 163, 184, 0.35);
|
align-items: center;
|
||||||
color: #e2e8f0;
|
gap: 0.65rem;
|
||||||
|
height: 46px;
|
||||||
|
padding: 0 1.4rem 0 0.45rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 23px;
|
||||||
|
background: linear-gradient(135deg, var(--accent-grad-a), var(--accent-grad-b));
|
||||||
|
color: var(--accent-ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.005em;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 8px 24px -12px rgba(var(--accent-rgb), 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||||
|
transition: background var(--transition-smooth), box-shadow var(--transition-smooth), transform 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-btn-cli:hover {
|
.welcome-primary:hover {
|
||||||
background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
|
background: linear-gradient(135deg, var(--run-hover-a), var(--run-hover-b));
|
||||||
border-color: rgba(203, 213, 225, 0.5);
|
box-shadow: 0 10px 30px -10px rgba(var(--accent-rgb), 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
color: #f8fafc;
|
}
|
||||||
transform: translateY(-1px);
|
|
||||||
|
.welcome-primary:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OG has no designed --accent-ink (its Daylight extras are placeholders), and a
|
||||||
|
dark ink on its brighter blue reads poorly: it gets the deeper blue and light
|
||||||
|
text of its own toolbar Run button instead. */
|
||||||
|
html[data-skin="og"] .welcome-primary {
|
||||||
|
background: linear-gradient(135deg, #1e4fb8 0%, #2563eb 100%);
|
||||||
|
border-color: rgba(96, 165, 250, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-skin="og"] .welcome-primary:hover {
|
||||||
|
background: linear-gradient(135deg, #2454a8 0%, #2d6cf0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The global accent outline would vanish against the accent fill; the offset
|
||||||
|
puts a ring of page background between the two. */
|
||||||
|
.welcome-primary:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The logo sits on a small light disc: a brand mark straight on the accent fill
|
||||||
|
turns muddy (Claude's orange on blue), and a disc reads on dark and light
|
||||||
|
skins alike. Monochrome marks are masks over currentColor, so the disc sets
|
||||||
|
a dark ink; --text-dim is redeclared because the plain fallback dot of an id
|
||||||
|
with no logo paints with it. */
|
||||||
|
.welcome-primary-logo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
|
||||||
|
color: #1f2328;
|
||||||
|
--text-dim: #59636e;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 0.85rem 0 0.7rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--control-border);
|
||||||
|
border-radius: 15px;
|
||||||
|
background: var(--control-bg);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-smooth), border-color var(--transition-smooth), color var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-chip:hover {
|
||||||
|
background: var(--control-bg-hover);
|
||||||
|
border-color: var(--control-border-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-chip:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cloudflare Tunnel: not a launcher, so it stays out of the launcher row as a
|
||||||
|
quiet text link under it. settings-ui.js owns its contents (it rewrites the
|
||||||
|
icon, label and spinner on every state change) and its inline display, so
|
||||||
|
everything here is styled from the outside: the 20px svg it writes is sized
|
||||||
|
down, and the states are text colour only. Ahead of #welcomeQr on purpose,
|
||||||
|
so the QR opens right under the control that toggled it. */
|
||||||
|
.welcome-tunnel-link {
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 1.4rem;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-smooth), color var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link svg {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link .tunnel-spinner {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link:hover {
|
||||||
|
background: var(--control-bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link.active {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tunnel-link.connecting,
|
||||||
|
.welcome-tunnel-link:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.welcome-primary,
|
||||||
|
.welcome-chip,
|
||||||
|
.welcome-tunnel-link {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-primary:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-mode-cli-options {
|
.run-mode-cli-options {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.7rem 1.3rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-smooth);
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-claude {
|
|
||||||
background: linear-gradient(135deg, #1a3358 0%, #1e4fb8 50%, #2563eb 100%);
|
|
||||||
border-color: rgba(59, 130, 246, 0.4);
|
|
||||||
color: #e0e7ff;
|
|
||||||
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-claude:hover {
|
|
||||||
background: linear-gradient(135deg, #2454a8 0%, #2d6cf0 50%, #4b8af7 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.35), 0 0 40px rgba(59, 130, 246, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(96, 165, 250, 0.5);
|
|
||||||
color: #fff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-opencode {
|
|
||||||
background: linear-gradient(135deg, #0a2e2a 0%, #0b3d35 50%, #0d4a40 100%);
|
|
||||||
border-color: rgba(16, 185, 129, 0.4);
|
|
||||||
color: #6ee7b7;
|
|
||||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-opencode:hover {
|
|
||||||
background: linear-gradient(135deg, #0d4a40 0%, #116a58 50%, #15856e 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.3), 0 0 40px rgba(16, 185, 129, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(52, 211, 153, 0.5);
|
|
||||||
color: #a7f3d0;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-codex {
|
|
||||||
background: linear-gradient(135deg, #2a0a3e 0%, #350b4d 50%, #400d5e 100%);
|
|
||||||
border-color: rgba(168, 85, 247, 0.4);
|
|
||||||
color: #d8b4fe;
|
|
||||||
box-shadow: 0 2px 8px rgba(168, 85, 247, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-codex:hover {
|
|
||||||
background: linear-gradient(135deg, #400d5e 0%, #581c87 50%, #6b21a8 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(168, 85, 247, 0.3), 0 0 40px rgba(88, 28, 135, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(192, 132, 252, 0.5);
|
|
||||||
color: #e9d5ff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Antigravity: cyan identity, matching .btn-toolbar.btn-run.mode-antigravity so
|
|
||||||
the welcome action reads as the same backend. */
|
|
||||||
.welcome-btn-antigravity {
|
|
||||||
background: linear-gradient(135deg, #0b2b33 0%, #0e7490 55%, #0891b2 100%);
|
|
||||||
border-color: rgba(34, 211, 238, 0.4);
|
|
||||||
color: #cffafe;
|
|
||||||
box-shadow: 0 2px 8px rgba(34, 211, 238, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-antigravity:hover {
|
|
||||||
background: linear-gradient(135deg, #124450 0%, #0891b2 55%, #06b6d4 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(34, 211, 238, 0.3), 0 0 40px rgba(8, 145, 178, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(103, 232, 249, 0.5);
|
|
||||||
color: #ecfeff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pi (pi.dev): rose identity, matching .btn-toolbar.btn-run.mode-pi so the
|
|
||||||
welcome action reads as the same backend. */
|
|
||||||
.welcome-btn-pi {
|
|
||||||
background: linear-gradient(135deg, #33121f 0%, #9d174d 55%, #be185d 100%);
|
|
||||||
border-color: rgba(244, 114, 182, 0.4);
|
|
||||||
color: #fce7f3;
|
|
||||||
box-shadow: 0 2px 8px rgba(244, 114, 182, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-pi:hover {
|
|
||||||
background: linear-gradient(135deg, #4a1a2c 0%, #be185d 55%, #db2777 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(244, 114, 182, 0.3), 0 0 40px rgba(190, 24, 93, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(249, 168, 212, 0.5);
|
|
||||||
color: #fff1f7;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
/* OMP: indigo identity, matching .btn-toolbar.btn-run.mode-omp so the welcome
|
|
||||||
action reads as the same backend. */
|
|
||||||
.welcome-btn-omp {
|
|
||||||
background: linear-gradient(135deg, #1e1b4b 0%, #4f46e5 55%, #6366f1 100%);
|
|
||||||
border-color: rgba(129, 140, 248, 0.4);
|
|
||||||
color: #e0e7ff;
|
|
||||||
box-shadow: 0 2px 8px rgba(129, 140, 248, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-omp:hover {
|
|
||||||
background: linear-gradient(135deg, #312e81 0%, #6366f1 55%, #818cf8 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(129, 140, 248, 0.3), 0 0 40px rgba(79, 70, 229, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(165, 180, 252, 0.5);
|
|
||||||
color: #eef2ff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grok (xAI): monochrome charcoal identity, matching .btn-toolbar.btn-run.mode-grok
|
|
||||||
so the welcome action reads as the same backend. */
|
|
||||||
.welcome-btn-grok {
|
|
||||||
background: linear-gradient(135deg, #131316 0%, #27272a 55%, #3f3f46 100%);
|
|
||||||
border-color: rgba(212, 212, 216, 0.4);
|
|
||||||
color: #f4f4f5;
|
|
||||||
box-shadow: 0 2px 8px rgba(212, 212, 216, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-grok:hover {
|
|
||||||
background: linear-gradient(135deg, #1f1f23 0%, #3f3f46 55%, #52525b 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(212, 212, 216, 0.22), 0 0 40px rgba(161, 161, 170, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(228, 228, 231, 0.5);
|
|
||||||
color: #fafafa;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DeepSeek Harness: the #4d6bfe blue identity, matching
|
|
||||||
.btn-toolbar.btn-run.mode-deepseek so the welcome action reads as the same
|
|
||||||
backend. */
|
|
||||||
.welcome-btn-deepseek {
|
|
||||||
background: linear-gradient(135deg, #101a4d 0%, #2740c4 55%, #4d6bfe 100%);
|
|
||||||
border-color: rgba(124, 147, 255, 0.4);
|
|
||||||
color: #eef2ff;
|
|
||||||
box-shadow: 0 2px 8px rgba(77, 107, 254, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-deepseek:hover {
|
|
||||||
background: linear-gradient(135deg, #16225f 0%, #3350e6 55%, #6b83ff 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(77, 107, 254, 0.3), 0 0 40px rgba(39, 64, 196, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(150, 170, 255, 0.5);
|
|
||||||
color: #f8faff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-gemini {
|
|
||||||
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
|
|
||||||
border-color: rgba(96, 165, 250, 0.4);
|
|
||||||
color: #dbeafe;
|
|
||||||
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-gemini:hover {
|
|
||||||
background: linear-gradient(135deg, #17345f 0%, #2563eb 55%, #6366f1 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(96, 165, 250, 0.3), 0 0 40px rgba(99, 102, 241, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: rgba(147, 197, 253, 0.5);
|
|
||||||
color: #eff6ff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-tunnel {
|
|
||||||
background: linear-gradient(135deg, #221538 0%, #3b1a7a 50%, #6d28d9 100%);
|
|
||||||
border-color: rgba(124, 58, 237, 0.4);
|
|
||||||
color: #ddd6fe;
|
|
||||||
box-shadow: 0 2px 8px rgba(109, 40, 217, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-tunnel:hover {
|
|
||||||
background: linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(124, 58, 237, 0.35), 0 0 40px rgba(124, 58, 237, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(139, 92, 246, 0.5);
|
|
||||||
color: #fff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-tunnel.active {
|
|
||||||
background: linear-gradient(135deg, #047857 0%, #059669 50%, #10b981 100%);
|
|
||||||
border-color: rgba(52, 211, 153, 0.5);
|
|
||||||
color: #ecfdf5;
|
|
||||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-tunnel.active:hover {
|
|
||||||
background: linear-gradient(135deg, #059669 0%, #10b981 50%, #34d399 100%);
|
|
||||||
box-shadow: 0 4px 20px rgba(52, 211, 153, 0.35), 0 0 40px rgba(52, 211, 153, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-btn-tunnel.connecting {
|
|
||||||
background: linear-gradient(135deg, #1e1e3a 0%, #312e81 50%, #4338ca 100%);
|
|
||||||
border-color: rgba(99, 102, 241, 0.4);
|
|
||||||
color: #c7d2fe;
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tunnel-spinner {
|
.tunnel-spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -15903,76 +15881,19 @@ html:not([data-skin="og"]) {
|
|||||||
/* ---- PROGRESS RING (Ralph large): the one place a soft glow lives ---- */
|
/* ---- PROGRESS RING (Ralph large): the one place a soft glow lives ---- */
|
||||||
.ralph-ring-fill { filter: drop-shadow(0 0 6px rgba(var(--accent-rgb), 0.45)); }
|
.ralph-ring-fill { filter: drop-shadow(0 0 6px rgba(var(--accent-rgb), 0.45)); }
|
||||||
|
|
||||||
/* ---- Welcome overlay: emerald title + Daylight buttons ---- */
|
/* ---- Welcome overlay: emerald title (the launchers are token-driven
|
||||||
|
already, see `.welcome-primary` / `.welcome-chip`) ---- */
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
background: linear-gradient(135deg, var(--accent-grad-a) 0%, var(--accent-grad-b) 50%, var(--accent-grad-a) 100%);
|
background: linear-gradient(135deg, var(--accent-grad-a) 0%, var(--accent-grad-b) 50%, var(--accent-grad-a) 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
.welcome-btn-claude {
|
|
||||||
background: linear-gradient(135deg, var(--accent-grad-a), var(--accent-grad-b));
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--accent-ink);
|
|
||||||
}
|
|
||||||
.welcome-btn-claude:hover { box-shadow: 0 0 28px -4px rgba(var(--accent-rgb), 0.3); }
|
|
||||||
.welcome-btn-opencode {
|
|
||||||
background: linear-gradient(135deg, var(--accent-d), var(--accent-grad-b));
|
|
||||||
border-color: rgba(var(--accent-rgb), 0.4);
|
|
||||||
color: var(--accent-ink);
|
|
||||||
}
|
|
||||||
.welcome-btn-opencode:hover { box-shadow: 0 0 28px -4px rgba(var(--accent-rgb), 0.25); }
|
|
||||||
.welcome-btn-tunnel {
|
|
||||||
background: linear-gradient(135deg, var(--bg-input), var(--bg-hover));
|
|
||||||
border-color: var(--border-light);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
.welcome-btn-tunnel:hover { box-shadow: none; }
|
|
||||||
.welcome-btn-tunnel.active {
|
|
||||||
background: linear-gradient(135deg, var(--accent-d), var(--accent-grad-a));
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--accent-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Focus rings (driven by --accent, now emerald) stay accessible ---- */
|
/* ---- Focus rings (driven by --accent, now emerald) stay accessible ---- */
|
||||||
:focus-visible { outline-color: var(--accent); }
|
:focus-visible { outline-color: var(--accent); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Daylight Blue: distinct identity per welcome action button ----
|
|
||||||
Run Claude Code keeps the blue accent; Cloudflare Tunnel is purple; Run
|
|
||||||
OpenCode is emerald green — so the three are clearly different colors instead
|
|
||||||
of all reading blue. Scoped to daylight-blue only; placed after the shared
|
|
||||||
daylight block to win on equal specificity. ---- */
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-opencode {
|
|
||||||
background: linear-gradient(135deg, #0d9f6e, #2fbf85);
|
|
||||||
border-color: rgba(16, 185, 129, 0.5);
|
|
||||||
color: #04291b;
|
|
||||||
}
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-opencode:hover {
|
|
||||||
background: linear-gradient(135deg, #10b981, #34d399);
|
|
||||||
border-color: rgba(52, 211, 153, 0.6);
|
|
||||||
box-shadow: 0 0 28px -4px rgba(16, 185, 129, 0.4);
|
|
||||||
}
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-tunnel {
|
|
||||||
background: linear-gradient(135deg, #7c3aed, #a855f7);
|
|
||||||
border-color: rgba(168, 85, 247, 0.5);
|
|
||||||
color: #f5f3ff;
|
|
||||||
}
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-tunnel:hover {
|
|
||||||
background: linear-gradient(135deg, #8b5cf6, #c084fc);
|
|
||||||
border-color: rgba(192, 132, 252, 0.6);
|
|
||||||
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.45);
|
|
||||||
}
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-tunnel.active {
|
|
||||||
background: linear-gradient(135deg, #6d28d9, #7c3aed);
|
|
||||||
border-color: rgba(124, 58, 237, 0.6);
|
|
||||||
color: #f5f3ff;
|
|
||||||
}
|
|
||||||
html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
|
||||||
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
|
|
||||||
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Cron Jobs ───────────────────────────────── */
|
/* ── Cron Jobs ───────────────────────────────── */
|
||||||
/* There is no global `.hidden{display:none}` utility in this stylesheet — each
|
/* There is no global `.hidden{display:none}` utility in this stylesheet — each
|
||||||
component scopes its own. The cron modal shipped without one, so its
|
component scopes its own. The cron modal shipped without one, so its
|
||||||
|
|||||||
+105
-8
@@ -470,11 +470,21 @@ describe('Codex quick start settings', () => {
|
|||||||
cloudflared: false,
|
cloudflared: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Every welcome launcher in DOM order: the primary button, then the chips
|
||||||
|
// inside their group. A launcher is anything carrying data-mode; the walk
|
||||||
|
// stops there, so a button's own logo spans are never mistaken for one.
|
||||||
|
function launchers(container: any): any[] {
|
||||||
|
return container.children.flatMap((c: any) =>
|
||||||
|
typeof c === 'string' ? [] : c.dataset?.mode ? [c] : launchers(c)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const offeredModes = (container: any) => launchers(container).map((btn: any) => btn.dataset.mode);
|
||||||
|
const textOf = (btn: any) => btn.children.filter((c: unknown) => typeof c === 'string').join('');
|
||||||
|
|
||||||
it('renders only enabled and available registry entries on the welcome screen', () => {
|
it('renders only enabled and available registry entries on the welcome screen', () => {
|
||||||
const { app, welcomeCliActions, tunnelBtn } = loadUi({ ...ALL_OFF, claude: true, 'custom-agent': true });
|
const { app, welcomeCliActions, tunnelBtn } = loadUi({ ...ALL_OFF, claude: true, 'custom-agent': true });
|
||||||
app.applyWelcomeCliVisibility();
|
app.applyWelcomeCliVisibility();
|
||||||
const offered = welcomeCliActions.children.map((btn: any) => btn.dataset.mode);
|
expect(offeredModes(welcomeCliActions)).toEqual(['claude', 'custom-agent', 'shell']);
|
||||||
expect(offered).toEqual(['claude', 'custom-agent', 'shell']);
|
|
||||||
// #200 originally DELETED the tunnel button and its QR outright; it is gated
|
// #200 originally DELETED the tunnel button and its QR outright; it is gated
|
||||||
// on cloudflared instead, so a box that has cloudflared keeps the feature.
|
// on cloudflared instead, so a box that has cloudflared keeps the feature.
|
||||||
expect(tunnelBtn.style.display).toBe('none');
|
expect(tunnelBtn.style.display).toBe('none');
|
||||||
@@ -485,7 +495,7 @@ describe('Codex quick start settings', () => {
|
|||||||
|
|
||||||
const withPi = loadUi({ ...ALL_OFF, pi: true });
|
const withPi = loadUi({ ...ALL_OFF, pi: true });
|
||||||
withPi.app.applyWelcomeCliVisibility();
|
withPi.app.applyWelcomeCliVisibility();
|
||||||
expect(withPi.welcomeCliActions.children.map((btn: any) => btn.dataset.mode)).toEqual(['pi', 'shell']);
|
expect(offeredModes(withPi.welcomeCliActions)).toEqual(['pi', 'shell']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gates every run mode in the dropdown, antigravity included, and never shell', () => {
|
it('gates every run mode in the dropdown, antigravity included, and never shell', () => {
|
||||||
@@ -509,13 +519,100 @@ describe('Codex quick start settings', () => {
|
|||||||
expect(runModeCliOptions.children.map((btn: any) => btn.dataset.mode)).not.toContain('shell');
|
expect(runModeCliOptions.children.map((btn: any) => btn.dataset.mode)).not.toContain('shell');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('labels welcome buttons "Run <label>", the strings i18n.js translates ("Run Claude Code")', () => {
|
it('labels the primary "Run <label>" and each chip by name, with "Run <label>" as its tooltip', () => {
|
||||||
|
// "Run Claude Code" / "Run Shell" are the strings i18n.js translates: the
|
||||||
|
// primary carries one as ONE text node (i18n matches whole nodes), and a
|
||||||
|
// chip carries one as its title and accessible name, both translated
|
||||||
|
// attributes, beside the bare CLI name it shows.
|
||||||
const { app, welcomeCliActions } = loadUi({ ...ALL_OFF, claude: true });
|
const { app, welcomeCliActions } = loadUi({ ...ALL_OFF, claude: true });
|
||||||
app.applyWelcomeCliVisibility();
|
app.applyWelcomeCliVisibility();
|
||||||
const texts = welcomeCliActions.children.map((btn: any) =>
|
const [primary, ...chips] = launchers(welcomeCliActions);
|
||||||
btn.children.filter((c: unknown) => typeof c === 'string').join('')
|
expect(textOf(primary)).toBe('Run Claude Code');
|
||||||
|
expect(chips.map(textOf)).toEqual(['Shell']);
|
||||||
|
expect(chips.map((chip: any) => chip.title)).toEqual(['Run Shell']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promotes the first AGENT in catalog order to the primary, chosen by kind and order, never by id', () => {
|
||||||
|
const primaryOf = (flags: Record<string, boolean> | undefined, catalog = CATALOG) => {
|
||||||
|
const { app, welcomeCliActions } = loadUi(flags, catalog);
|
||||||
|
app.applyWelcomeCliVisibility();
|
||||||
|
const first = welcomeCliActions.children[0];
|
||||||
|
return first ? { mode: first.dataset.mode, className: first.className } : null;
|
||||||
|
};
|
||||||
|
expect(primaryOf(undefined)).toEqual({ mode: 'claude', className: 'welcome-primary' });
|
||||||
|
// Claude missing or disabled: the next agent takes the slot, not a hardcoded fallback.
|
||||||
|
expect(primaryOf({ ...ALL_OFF, opencode: true, codex: true })?.mode).toBe('opencode');
|
||||||
|
const noClaude = CATALOG.map((cli) => (cli.id === 'claude' ? { ...cli, enabled: false } : cli));
|
||||||
|
expect(primaryOf(undefined, noClaude)?.mode).toBe('opencode');
|
||||||
|
// A shell listed first never outranks an agent after it...
|
||||||
|
const shellFirst = [CATALOG[CATALOG.length - 1], ...CATALOG.slice(0, -1)];
|
||||||
|
expect(primaryOf({ ...ALL_OFF, codex: true }, shellFirst)?.mode).toBe('codex');
|
||||||
|
// ...and a custom agent at the head of the catalog is the primary like any other.
|
||||||
|
const customFirst = [
|
||||||
|
{ id: 'my-agent', label: 'My Agent', shortBadge: 'MA', kind: 'agent', enabled: true },
|
||||||
|
...CATALOG,
|
||||||
|
];
|
||||||
|
expect(primaryOf({ ...ALL_OFF, 'my-agent': true, claude: true }, customFirst)?.mode).toBe('my-agent');
|
||||||
|
// Only the shell left: it is the primary rather than an empty screen.
|
||||||
|
expect(primaryOf({ ...ALL_OFF })?.mode).toBe('shell');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the rest as chips in catalog order, carrying the CLI id only as data', () => {
|
||||||
|
const { app, welcomeCliActions } = loadUi(undefined);
|
||||||
|
app.applyWelcomeCliVisibility();
|
||||||
|
expect(welcomeCliActions.children).toHaveLength(2);
|
||||||
|
const [primary, chipRow] = welcomeCliActions.children;
|
||||||
|
expect(chipRow.className).toBe('welcome-chips');
|
||||||
|
expect(chipRow.children.map((chip: any) => chip.dataset.mode)).toEqual(
|
||||||
|
CATALOG.map((cli) => cli.id).filter((id) => id !== primary.dataset.mode)
|
||||||
);
|
);
|
||||||
expect(texts).toEqual(['Run Claude Code', 'Run Shell']);
|
for (const btn of launchers(welcomeCliActions)) {
|
||||||
|
const id = btn.dataset.mode;
|
||||||
|
// The logo is the shared Run-menu slot, so styles.css draws the brand mark
|
||||||
|
// (or the plain fallback dot of an id it has no mark for).
|
||||||
|
const logos: any[] = [];
|
||||||
|
const collect = (el: any) =>
|
||||||
|
el.children.forEach((c: any) => {
|
||||||
|
if (typeof c === 'string') return;
|
||||||
|
if (String(c.className).startsWith('run-mode-dot')) logos.push(c);
|
||||||
|
collect(c);
|
||||||
|
});
|
||||||
|
collect(btn);
|
||||||
|
expect(logos.map((l) => l.className)).toEqual([`run-mode-dot ${id}`]);
|
||||||
|
// No per-id class on the button itself, so no rule can restyle one CLI.
|
||||||
|
expect(btn.className).not.toContain(id);
|
||||||
|
expect(typeof btn.onclick).toBe('function');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('styles the launchers from skin tokens only, with no per-CLI welcome rule left', () => {
|
||||||
|
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
|
||||||
|
const mobileCss = readFileSync(resolve(import.meta.dirname, '../src/web/public/mobile.css'), 'utf8');
|
||||||
|
const html = readFileSync(resolve(import.meta.dirname, '../src/web/public/index.html'), 'utf8');
|
||||||
|
// The old look was one gradient per CLI (.welcome-btn-codex, …) plus skin
|
||||||
|
// overrides of the same classes; any survivor would re-tint one launcher.
|
||||||
|
expect(css).not.toMatch(/\.welcome-btn\b/);
|
||||||
|
expect(mobileCss).not.toMatch(/\.welcome-btn\b/);
|
||||||
|
const rule = (selector: string) => {
|
||||||
|
const at = css.indexOf(`\n${selector} {`);
|
||||||
|
expect(at, `${selector} rule`).toBeGreaterThan(-1);
|
||||||
|
return css.slice(at, css.indexOf('}', at));
|
||||||
|
};
|
||||||
|
expect(rule('.welcome-primary')).toContain('var(--accent-grad-a)');
|
||||||
|
expect(rule('.welcome-chip')).toContain('var(--control-bg)');
|
||||||
|
expect(rule('.welcome-chip')).toContain('var(--control-border)');
|
||||||
|
// Hover motion stays off for anyone who asked for less of it.
|
||||||
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\) \{\s*\.welcome-primary,/);
|
||||||
|
// The tunnel keeps its id and handler, is no longer a launcher, and sits
|
||||||
|
// between the launchers and the QR it reveals.
|
||||||
|
expect(html).toMatch(
|
||||||
|
/class="welcome-tunnel-link" id="welcomeTunnelBtn"[^>]*onclick="app\.toggleTunnelFromWelcome\(\)"/
|
||||||
|
);
|
||||||
|
const actions = html.indexOf('id="welcomeCliActions"');
|
||||||
|
const tunnel = html.indexOf('id="welcomeTunnelBtn"');
|
||||||
|
expect(actions).toBeGreaterThan(-1);
|
||||||
|
expect(tunnel).toBeGreaterThan(actions);
|
||||||
|
expect(html.indexOf('id="welcomeQr"')).toBeGreaterThan(tunnel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the first ENABLED agent when the chosen run mode is disabled, never a hardcoded claude', () => {
|
it('falls back to the first ENABLED agent when the chosen run mode is disabled, never a hardcoded claude', () => {
|
||||||
@@ -545,7 +642,7 @@ describe('Codex quick start settings', () => {
|
|||||||
const { app, welcomeCliActions, modeBtns, menu } = loadUi(undefined);
|
const { app, welcomeCliActions, modeBtns, menu } = loadUi(undefined);
|
||||||
app.applyWelcomeCliVisibility();
|
app.applyWelcomeCliVisibility();
|
||||||
app._refreshRunModeAvailability(menu);
|
app._refreshRunModeAvailability(menu);
|
||||||
expect(welcomeCliActions.children.map((btn: any) => btn.dataset.mode)).toContain('claude');
|
expect(offeredModes(welcomeCliActions)).toContain('claude');
|
||||||
expect(modeBtns.gemini.style.display).toBe('flex');
|
expect(modeBtns.gemini.style.display).toBe('flex');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user