diff --git a/src/web/public/index.html b/src/web/public/index.html index 6bc54df3..97069654 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -1312,18 +1312,22 @@
- + - + -

Settings marked device stay on this browser. The rest sync to every device.

@@ -1364,6 +1364,96 @@ + +
+
+ +

System

+
+

What this install is running, plus paths, automation and remote access.

+ +
+

Updates

+
+
+
Current version
+ — +
+
+
+ Check for updates + Looks at GitHub for a newer Codeman release. +
+ +
+ + + + +
+
+ +
+

Paths

synced
+
+
+
+ CLAUDE.md template + Used when creating new cases. Leave empty for the built-in template. +
+ +
+
+
+ Default working directory + Starting directory for new sessions. +
+ +
+
+
+ +
+

Automation

synced
+
+
+
+ Image watcher + Detect and pop up new images found in session directories. +
+ +
+
+
+ +
+

Remote access

synced
+
+
+
+ Cloudflare Tunnel + Expose Codeman on a public URL. Set CODEMAN_PASSWORD first. +
+ +
+ + +
+
+ +
@@ -1427,6 +1517,121 @@
+ +
+
+ +

Header & Panels

+
+

Pick the buttons and panels you actually use. Everything else stays out of the way.

+ + +
+
+ Live preview + Updates as you pick, before you save. +
+
+
+ + w1-codeman + w2-docs + + + +
+
+
❯ npm run dev
+
+
+
+
+ Run + +
+
+ +
+ +
+

Header buttons

device
+

Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.

+
+
+ + + + + + + + + + + +
+
+
+ +
+

Panels

device
+
+
+ + + + +
+
+
+ +
+

Cross-session features

synced
+

Approvals answers prompts from any session in one place. Read My Mind captures your prompts and predicts the next one on demand; both cost tokens and never auto-send.

+
+
+ + + + +
+
+
+ +
+

Scheduling

device
+
+
+ +
+
+
+ +
+

Subagent windows

+
+
+
+ Track background agents synced + Follow Claude Code subagents in real time. +
+ +
+
+
+ Active tab only device + Only show agent windows for the selected session. +
+ +
+
+
+
+
@@ -1525,81 +1730,6 @@
- -
-
- -

Header & Panels

-
-

Pick the buttons and panels you actually use. Everything else stays out of the way.

- -
-

Header buttons

device
-

Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.

-
-
- - - - - - - - - - - - -
-
-
- -
-

Panels

device
-
-
- - - - -
-
-
- -
-

Cross-session features

synced
-

Approvals answers prompts from any session in one place. Read My Mind captures your prompts and predicts the next one on demand; both cost tokens and never auto-send.

-
-
- - - - -
-
-
- -
-

Subagent windows

-
-
-
- Track background agents synced - Follow Claude Code subagents in real time. -
- -
-
-
- Active tab only device - Only show agent windows for the selected session. -
- -
-
-
-
-
@@ -2032,95 +2162,6 @@
- -
-
- -

System

-
-

Paths, automation, remote access and updates.

- -
-

Paths

synced
-
-
-
- CLAUDE.md template - Used when creating new cases. Leave empty for the built-in template. -
- -
-
-
- Default working directory - Starting directory for new sessions. -
- -
-
-
- -
-

Automation

synced
-
-
-
- Image watcher - Detect and pop up new images found in session directories. -
- -
-
-
- -
-

Remote access

synced
-
-
-
- Cloudflare Tunnel - Expose Codeman on a public URL. Set CODEMAN_PASSWORD first. -
- -
- - -
-
- -
-

Updates

-
-
-
Current version
- — -
-
-
- Check for updates - Looks at GitHub for a newer Codeman release. -
- -
- - - - -
-
-
diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 222b02ac..ae1a7e20 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -3378,6 +3378,15 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat display: none; } + /* ── live layout preview ───────────────────────────────────────────── */ + #appSettingsModal .set-preview { + margin-bottom: 12px; + } + + #appSettingsModal .set-preview-stage { + min-height: 62px; + } + /* ── inset grouped list ────────────────────────────────────────────── */ #appSettingsModal .set-group { margin-top: 14px; diff --git a/src/web/public/settings-ui.js b/src/web/public/settings-ui.js index 2c5f1357..24331c11 100644 --- a/src/web/public/settings-ui.js +++ b/src/web/public/settings-ui.js @@ -503,8 +503,10 @@ Object.assign(CodemanApp.prototype, { this._syncSettingsChips(); this._syncModelCards(); this._syncEffortSegment(); - // Back to the top of the document (one scroll, not a tab reset). - this.switchSettingsTab('settings-terminal'); + // Back to the top of the document (one scroll, not a tab reset). System is + // first now: the version this install is running, and whether an update is + // waiting, are the two things worth seeing before anything is configured. + this.switchSettingsTab('settings-system'); const modal = document.getElementById('appSettingsModal'); modal.classList.add('active'); @@ -692,6 +694,79 @@ Object.assign(CodemanApp.prototype, { document.querySelectorAll('#appSettingsModal .set-chip').forEach(chip => { chip.classList.toggle('is-on', !!chip.querySelector('input')?.checked); }); + this._syncLayoutPreview(); + }, + + /** + * Redraw the Header & Panels live preview from the chips above it. + * + * The preview is a scale model of the app, not a second list of settings, so + * every icon is CLONED from the chip that owns it (`.set-chip-ico`): each icon + * has exactly ONE copy in index.html and a chip can never drift from the button + * it previews. A chip joins the preview purely by carrying `data-preview` + * (which slot) and `data-preview-order` (where in that slot); nothing here + * needs to know the setting's name. + * + * `data-preview-text` replaces the icon with a text token for the header + * entries that are readouts rather than buttons (plan usage, CPU, font size). + */ + _syncLayoutPreview() { + const modal = document.getElementById('appSettingsModal'); + if (!modal || typeof modal.querySelectorAll !== 'function') return; + const slots = { + header: document.getElementById('appSettingsPreviewHeader'), + panel: document.getElementById('appSettingsPreviewPanels'), + toolbar: document.getElementById('appSettingsPreviewToolbar'), + float: document.getElementById('appSettingsPreviewFloats'), + }; + if (!slots.header) return; + Object.values(slots).forEach(el => { + if (el) el.innerHTML = ''; + }); + + const chips = [...modal.querySelectorAll('.set-chip[data-preview]')] + .filter(chip => chip.querySelector('input')?.checked) + .sort((a, b) => (Number(a.dataset.previewOrder) || 0) - (Number(b.dataset.previewOrder) || 0)); + + let shown = 0; + for (const chip of chips) { + const kind = chip.dataset.preview; + const slot = slots[kind]; + if (!slot) continue; + // The label is the chip's own text; the icon span (if any) is skipped by + // taking the LAST span, which is always the label. + const spans = chip.querySelectorAll('span'); + const label = (spans[spans.length - 1]?.textContent || '').trim(); + const el = document.createElement('span'); + el.title = label; + if (kind === 'header') { + const text = chip.dataset.previewText; + el.className = text ? 'set-preview-chip' : 'set-preview-btn'; + if (text) el.textContent = text; + else this._appendPreviewIcon(el, chip); + } else { + el.className = `set-preview-${kind}`; + this._appendPreviewIcon(el, chip); + const name = document.createElement('span'); + name.textContent = label; + el.appendChild(name); + } + slot.appendChild(el); + shown++; + } + + const empty = document.getElementById('appSettingsPreviewEmpty'); + if (empty) empty.hidden = shown > 0; + }, + + /** Clone a chip's icon into a preview element (see _syncLayoutPreview). */ + _appendPreviewIcon(target, chip) { + const icon = chip.querySelector('.set-chip-ico'); + if (!icon) return; + const clone = icon.cloneNode(true); + clone.classList.remove('set-chip-ico'); + clone.classList.add('set-preview-ico'); + target.appendChild(clone); }, /** @@ -860,6 +935,10 @@ Object.assign(CodemanApp.prototype, { section.classList.toggle('set-hit-hidden', !!q && !hasVisible); }); + // The live preview sits outside any group, so it survives the sweep above; + // a search is asking for one row, not for the scale model around it. + doc.querySelectorAll('.set-preview').forEach(pv => pv.classList.toggle('set-hit-hidden', !!q)); + const empty = document.getElementById('appSettingsSearchEmpty'); if (empty) empty.hidden = !q || anyVisible; if (!q) doc.querySelectorAll('.set-group-advanced').forEach(g => g.classList.remove('open')); diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 47c199ad..5dbbfdeb 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -14572,9 +14572,12 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { introduces its own `set-*` classes rather than restyling those. ═══════════════════════════════════════════════════════════════════════════ */ +/* Deliberately narrower than the other .modal-lg users: the document is a + single column of rows whose control sits at the far right, so extra width + buys nothing but a gap between a row's text and its switch. */ #appSettingsModal .modal-content.modal-lg { - max-width: min(960px, 96vw); - width: min(960px, 96vw); + max-width: min(840px, 96vw); + width: min(840px, 96vw); height: min(700px, 88vh); max-height: 88vh; } @@ -14609,7 +14612,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { #appSettingsModal .set-body { display: grid; - grid-template-columns: 212px minmax(0, 1fr); + grid-template-columns: 196px minmax(0, 1fr); flex: 1; min-height: 0; } @@ -14729,19 +14732,19 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { /* ── document ─────────────────────────────────────────────────────────── */ #appSettingsModal .set-doc { overflow-y: auto; - padding: 0 22px 40px; + padding: 0 18px 32px; min-width: 0; min-height: 0; position: relative; } #appSettingsModal .set-section { - padding-top: 20px; + padding-top: 16px; } #appSettingsModal .set-section + .set-section { border-top: 1px solid var(--border); - margin-top: 26px; + margin-top: 20px; } #appSettingsModal .set-section-head { @@ -14770,7 +14773,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { } #appSettingsModal .set-section-blurb { - margin: 0 0 14px 36px; + margin: 0 0 11px 34px; font-size: 0.69rem; color: var(--text-muted); } @@ -14784,7 +14787,7 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { /* ── groups ───────────────────────────────────────────────────────────── */ #appSettingsModal .set-group + .set-group { - margin-top: 18px; + margin-top: 14px; } #appSettingsModal .set-group-head { @@ -14849,15 +14852,15 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { #appSettingsModal .set-group-body { display: flex; flex-direction: column; - gap: 5px; + gap: 4px; } /* ── rows ─────────────────────────────────────────────────────────────── */ #appSettingsModal .set-row { display: flex; align-items: center; - gap: 14px; - padding: 9px 12px; + gap: 10px; + padding: 7px 11px; border-radius: 9px; background: rgba(255, 255, 255, 0.028); border: 1px solid rgba(255, 255, 255, 0.055); @@ -14893,11 +14896,13 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { letter-spacing: -0.005em; } +/* Wide enough that a description runs most of the way to its switch: the + complaint about this panel was the dead gap a 62ch cap left behind. */ #appSettingsModal .set-row-desc { font-size: 0.66rem; line-height: 1.45; color: var(--text-muted); - max-width: 62ch; + max-width: 78ch; } #appSettingsModal .set-row-actions { @@ -14980,6 +14985,14 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { color: var(--text-dim); } +/* The version is the first value in the first section now, so it reads as an + answer to "what am I running", not as one more right-aligned field. */ +#appSettingsModal .set-static-strong { + font-size: 0.82rem; + font-weight: 620; + color: var(--text); +} + #appSettingsModal .mono { font-family: var(--font-mono); } @@ -15054,6 +15067,24 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { pointer-events: none; } +/* The chip carries the icon of the button it switches on, so the list reads as + the header rather than as a list of names. It is also the ONLY copy of that + icon: the live preview clones it (settings-ui.js: _syncLayoutPreview). */ +#appSettingsModal .set-chip-ico { + flex-shrink: 0; + opacity: 0.75; +} + +#appSettingsModal .set-chip-emoji { + font-size: 0.85em; + line-height: 1; +} + +#appSettingsModal .set-chip.is-on .set-chip-ico, +#appSettingsModal .set-chip:has(input:checked) .set-chip-ico { + opacity: 1; +} + #appSettingsModal .set-chip::before { content: '\2713'; font-size: 0.8em; @@ -15089,6 +15120,219 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover { outline-offset: 1px; } +/* ── live layout preview ────────────────────────────────────────────────── + A scale model of the app at the top of Header & Panels, rebuilt from the + chips on every change (settings-ui.js: _syncLayoutPreview). Everything here + is decoration around cloned chip icons, so it carries no icon of its own. + ─────────────────────────────────────────────────────────────────────────── */ +#appSettingsModal .set-preview { + margin: 0 0 14px; +} + +#appSettingsModal .set-preview-head { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 6px; +} + +#appSettingsModal .set-preview-title { + font-size: 0.63rem; + font-weight: 640; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--text-muted); +} + +#appSettingsModal .set-preview-hint { + font-size: 0.62rem; + color: var(--text-muted); + opacity: 0.72; +} + +/* Skin tokens only, no black/white alphas: on the four light skins a + hardcoded rgba(0,0,0,…) turned the whole model into a grey slab. */ +#appSettingsModal .set-preview-frame { + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + background: var(--bg-dark); +} + +#appSettingsModal .set-preview-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + background: var(--bg-card); +} + +#appSettingsModal .set-preview-logo { + display: grid; + place-items: center; + width: 16px; + height: 16px; + border-radius: 5px; + font-size: 0.55rem; + font-weight: 700; + color: var(--accent-ink); + background: linear-gradient(180deg, var(--accent-grad-a), var(--accent-grad-b)); + flex-shrink: 0; +} + +#appSettingsModal .set-preview-tab { + font-size: 0.56rem; + padding: 2px 7px; + border-radius: 5px; + background: var(--control-bg); + color: var(--text-dim); + white-space: nowrap; +} + +#appSettingsModal .set-preview-tab-dim { + opacity: 0.5; +} + +#appSettingsModal .set-preview-gap { + flex: 1; + min-width: 6px; +} + +/* Wraps rather than overflows: every button switched on has to stay visible, + and the frame is narrower than the real header it stands in for. */ +#appSettingsModal .set-preview-btns { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 3px; +} + +#appSettingsModal .set-preview-btn { + display: inline-grid; + place-items: center; + width: 19px; + height: 19px; + border-radius: 5px; + color: var(--text-dim); + background: var(--control-bg); +} + +#appSettingsModal .set-preview-btn-fixed { + color: var(--text-muted); + background: transparent; +} + +#appSettingsModal .set-preview-chip { + font-size: 0.55rem; + padding: 3px 6px; + border-radius: 5px; + color: var(--text-dim); + background: var(--control-bg); + white-space: nowrap; +} + +#appSettingsModal svg.set-preview-ico { + width: 11px; + height: 11px; + flex-shrink: 0; +} + +#appSettingsModal span.set-preview-ico { + font-size: 9px; + line-height: 1; +} + +#appSettingsModal .set-preview-stage { + position: relative; + display: flex; + min-height: 76px; + padding: 9px 10px; +} + +#appSettingsModal .set-preview-term { + flex: 1; + font-family: var(--font-mono, monospace); + font-size: 0.58rem; + color: var(--text-muted); +} + +#appSettingsModal .set-preview-caret { + color: var(--accent); +} + +/* Right-docked, like every real panel in the app (monitor, subagents, file + browser and project insights are all `position: fixed; right: …`). */ +#appSettingsModal .set-preview-panels { + position: absolute; + right: 9px; + top: 9px; + bottom: 9px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} + +#appSettingsModal .set-preview-panel, +#appSettingsModal .set-preview-float, +#appSettingsModal .set-preview-toolbar { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.55rem; + padding: 3px 7px; + border-radius: 6px; + color: var(--text-dim); + background: var(--control-bg); + border: 1px solid var(--border); + white-space: nowrap; +} + +#appSettingsModal .set-preview-floats { + position: absolute; + left: 12px; + bottom: 10px; + display: flex; + gap: 5px; +} + +#appSettingsModal .set-preview-float { + border-style: dashed; + color: var(--text-muted); +} + +#appSettingsModal .set-preview-tray { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 8px; + border-top: 1px solid var(--border); + background: var(--bg-card); +} + +#appSettingsModal .set-preview-run { + font-size: 0.55rem; + font-weight: 640; + padding: 3px 9px; + border-radius: 5px; + color: var(--accent-ink); + background: linear-gradient(180deg, var(--accent-grad-a), var(--accent-grad-b)); +} + +#appSettingsModal .set-preview-tools { + display: flex; + align-items: center; + gap: 4px; +} + +#appSettingsModal .set-preview-empty { + margin: 6px 0 0; + font-size: 0.62rem; + color: var(--text-muted); +} + /* ── model cards ──────────────────────────────────────────────────────── */ #appSettingsModal .set-modelgrid { display: grid; diff --git a/test/app-settings-structure.test.ts b/test/app-settings-structure.test.ts index b71c8096..5a69a25e 100644 --- a/test/app-settings-structure.test.ts +++ b/test/app-settings-structure.test.ts @@ -59,8 +59,23 @@ describe('App Settings modal structure', () => { } }); - it('opens on Terminal & Input, so Local Echo is the first thing in reach', () => { - expect(settingsUi).toContain("this.switchSettingsTab('settings-terminal')"); + it('opens on System, with the version and the updater above everything else', () => { + expect(settingsUi).toContain("this.switchSettingsTab('settings-system')"); + const modal = settingsModal(); + const order = [...modal.matchAll(/
m[1]); + // Rail and document must agree, or scroll-spy paints the wrong entry. + const rail = [...modal.matchAll(/data-section="([a-z-]+)"/g)].map((m) => m[1]); + expect(rail.slice(0, 3)).toEqual(['settings-system', 'settings-terminal', 'settings-layout']); + expect(order.slice(0, 3)).toEqual(['settings-system', 'settings-terminal', 'settings-layout']); + // Updates leads the System section: version first, then the update action. + const system = modal.match(/id="settings-system"([\s\S]*?)<\/section>/)?.[1] ?? ''; + const version = system.indexOf('id="updateCurrentVersion"'); + expect(version).toBeGreaterThan(-1); + expect(version).toBeLessThan(system.indexOf('id="appSettingsClaudeMdPath"')); + expect(system.indexOf('id="updateCheckBtn"')).toBeLessThan(system.indexOf('id="appSettingsClaudeMdPath"')); + }); + + it('keeps Local Echo the first row of the second section', () => { const terminal = settingsModal().match(/id="settings-terminal"([\s\S]*?)<\/section>/); const localEcho = terminal?.[1].indexOf('appSettingsLocalEcho') ?? -1; const cjk = terminal?.[1].indexOf('appSettingsCjkInput') ?? -1; @@ -68,6 +83,33 @@ describe('App Settings modal structure', () => { expect(localEcho).toBeLessThan(cjk); }); + it('gives every previewed chip an icon to clone, and a slot that exists', () => { + // _syncLayoutPreview clones `.set-chip-ico` out of the chip, so a chip that + // opts into the preview without an icon renders as an empty button, and one + // pointing at a slot id that does not exist renders as nothing at all. + const layout = settingsModal().match(/id="settings-layout"([\s\S]*?)<\/section>/)?.[1] ?? ''; + const chips = [...layout.matchAll(/