feat(settings): reorder App Settings, tighten the rows, add a live layout preview

The document side of the settings modal was wider than it needed to be: every
row is text on the left and a switch pinned to the right, so a 960px shell plus
a 62ch cap on the description left a dead gap of ~350px between the two. The
shell is now 840px, the rail 196px, and descriptions run to 78ch, which closes
the gap and makes the right side sit proportionally with the rail.

Section order now leads with what you look at first: System (the version this
install runs and whether an update is waiting, with Updates promoted above
Paths/Automation/Remote access), then Terminal & Input, then Header & Panels.
The modal opens scrolled to System instead of Terminal & Input.

Header & Panels gains two things:

- every chip carries the icon of the button it switches on, so the list reads
  as the header itself rather than as a column of names (File Viewer shows the
  folder button, Cron the clock, and so on);
- a live preview above the chips: a scale model of the app with a header bar,
  right-docked panels, a toolbar and floating windows, rebuilt on every chip
  change so "what does this add" is answered in place, before saving.

The preview owns no icons of its own - it CLONES `.set-chip-ico` out of the
chip - so 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 by carrying
`data-preview` (which slot) and `data-preview-order` (where in it); readouts
that are not buttons (plan usage, CPU, font size) use `data-preview-text`
instead. The frame is painted from skin tokens only, since hardcoded black
alphas turned it into a grey slab on the four light skins, and it is marked
`data-i18n-skip`: the mock tab names are decoration, and the labels inside are
copies of chip text i18n has already translated.

Cron moved into its own Scheduling group (it is a toolbar button, not a header
one, and the preview places it accordingly).

test/app-settings-structure.test.ts pins the new contract: the rail and the
document agree on order, System leads with the version above the paths, and
every previewed chip has both an icon to clone and a slot that exists.
This commit is contained in:
Codeman maintainer
2026-08-10 10:58:05 +02:00
parent 752374abc7
commit a6cf4c2b2a
5 changed files with 604 additions and 189 deletions
+214 -173
View File
@@ -1312,18 +1312,22 @@
<input type="search" id="appSettingsSearch" placeholder="Search settings" autocomplete="off" aria-label="Search settings">
</div>
<div class="set-rail-items" id="appSettingsRailItems">
<button type="button" class="set-rail-item active" data-section="settings-terminal">
<button type="button" class="set-rail-item active" data-section="settings-system">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.26.6.86 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.65 0-1.25.4-1.51 1z"/></svg>
<span>System</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-terminal">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
<span>Terminal &amp; Input</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-appearance">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18 4.5 4.5 0 0 0 0-9 4.5 4.5 0 0 1 0-9z"/></svg>
<span>Appearance</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-layout">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
<span>Header &amp; Panels</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-appearance">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18 4.5 4.5 0 0 0 0-9 4.5 4.5 0 0 1 0-9z"/></svg>
<span>Appearance</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-models">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z"/><path d="M12 12l8-4.5M12 12v9M12 12L4 7.5"/></svg>
<span>Models</span>
@@ -1344,10 +1348,6 @@
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 13h.01M18 13h.01M9 13h6"/></svg>
<span>Shortcuts</span>
</button>
<button type="button" class="set-rail-item" data-section="settings-system">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.26.6.86 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.65 0-1.25.4-1.51 1z"/></svg>
<span>System</span>
</button>
</div>
<p class="set-rail-foot">Settings marked <b>device</b> stay on this browser. The rest sync to every device.</p>
</nav>
@@ -1364,6 +1364,96 @@
<p class="set-empty" id="appSettingsSearchEmpty" hidden>No settings match that search.</p>
<!-- ══ System ═══════════════════════════════════════════════════ -->
<section class="set-section" id="settings-system" data-label="System">
<div class="set-section-head">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.26.6.86 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.65 0-1.25.4-1.51 1z"/></svg>
<h2>System</h2>
</div>
<p class="set-section-blurb">What this install is running, plus paths, automation and remote access.</p>
<div class="set-group">
<div class="set-group-head"><h4>Updates</h4></div>
<div class="set-group-body">
<div class="set-row" data-search="version current release">
<div class="set-row-text"><span class="set-row-label">Current version</span></div>
<span class="set-static set-static-strong mono" id="updateCurrentVersion">&mdash;</span>
</div>
<div class="set-row" id="updateCheckRow" data-search="check for updates github release">
<div class="set-row-text">
<span class="set-row-label">Check for updates</span>
<span class="set-row-desc">Looks at GitHub for a newer Codeman release.</span>
</div>
<button class="btn-toolbar btn-sm" id="updateCheckBtn" onclick="app.checkForUpdate()">Check now</button>
</div>
<div id="updateResult" class="set-note" style="display:none"></div>
<div class="set-row" id="updateActionRow" style="display:none">
<div class="set-row-text"><span class="set-row-label" id="updateActionLabel">Update available</span></div>
<button class="btn-toolbar btn-sm btn-primary" id="updateNowBtn" onclick="app.startSelfUpdate()">Update now</button>
</div>
<div id="updateNotes" class="set-notes" style="display:none"></div>
<div id="updateProgress" class="set-progress" style="display:none"></div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Paths</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row has-field" data-search="claude.md template path new cases">
<div class="set-row-text">
<span class="set-row-label">CLAUDE.md template</span>
<span class="set-row-desc">Used when creating new cases. Leave empty for the built-in template.</span>
</div>
<input type="text" id="appSettingsClaudeMdPath" class="set-input" placeholder="/path/to/CLAUDE.md template">
</div>
<div class="set-row has-field" data-search="default working directory">
<div class="set-row-text">
<span class="set-row-label">Default working directory</span>
<span class="set-row-desc">Starting directory for new sessions.</span>
</div>
<input type="text" id="appSettingsDefaultDir" class="set-input" placeholder="~/projects">
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Automation</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row" data-search="image watcher popup screenshots">
<div class="set-row-text">
<span class="set-row-label">Image watcher</span>
<span class="set-row-desc">Detect and pop up new images found in session directories.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsImageWatcherEnabled"><span class="slider"></span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Remote access</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row" data-search="cloudflare tunnel remote access url">
<div class="set-row-text">
<span class="set-row-label">Cloudflare Tunnel</span>
<span class="set-row-desc">Expose Codeman on a public URL. Set CODEMAN_PASSWORD first.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsTunnelEnabled"><span class="slider"></span></label>
</div>
<div class="set-row" id="tunnelUrlRow" style="display:none">
<div class="set-row-text"><span class="set-row-label">Tunnel URL</span></div>
<div class="set-row-actions">
<span id="tunnelUrlDisplay" class="set-copy" title="Click to copy"></span>
<button class="btn-icon-sm" id="tunnelQrBtn" onclick="app.showTunnelQR()" title="Show QR code">&#x229E;</button>
</div>
</div>
<div class="set-row" id="tunnelUploadUrlRow" style="display:none">
<div class="set-row-text"><span class="set-row-label">Upload URL</span></div>
<span id="tunnelUploadUrlDisplay" class="set-copy" title="Click to copy"></span>
</div>
</div>
</div>
</section>
<!-- ══ Terminal &amp; Input ══════════════════════════════════════ -->
<section class="set-section" id="settings-terminal" data-label="Terminal &amp; Input">
<div class="set-section-head">
@@ -1427,6 +1517,121 @@
</div>
</section>
<!-- ══ Header &amp; Panels ═══════════════════════════════════════ -->
<section class="set-section" id="settings-layout" data-label="Header &amp; Panels">
<div class="set-section-head">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
<h2>Header &amp; Panels</h2>
</div>
<p class="set-section-blurb">Pick the buttons and panels you actually use. Everything else stays out of the way.</p>
<!-- Live preview: a scale model of the app that redraws from the
chips below on every change, so "what does this add" is answered
in place instead of after a save. Icons are CLONED from the
chips (settings-ui.js: _syncLayoutPreview), so each icon has
exactly one copy in this file and the two can never drift. -->
<div class="set-preview" id="appSettingsLayoutPreview">
<div class="set-preview-head">
<span class="set-preview-title">Live preview</span>
<span class="set-preview-hint">Updates as you pick, before you save.</span>
</div>
<div class="set-preview-frame" data-i18n-skip>
<div class="set-preview-bar">
<span class="set-preview-logo">C</span>
<span class="set-preview-tab">w1-codeman</span>
<span class="set-preview-tab set-preview-tab-dim">w2-docs</span>
<span class="set-preview-gap"></span>
<span class="set-preview-btns" id="appSettingsPreviewHeader"></span>
<span class="set-preview-btn set-preview-btn-fixed" title="App Settings"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
</div>
<div class="set-preview-stage">
<div class="set-preview-term"><span class="set-preview-caret">&#10095;</span> npm run dev</div>
<div class="set-preview-panels" id="appSettingsPreviewPanels"></div>
<div class="set-preview-floats" id="appSettingsPreviewFloats"></div>
</div>
<div class="set-preview-tray">
<span class="set-preview-run">Run</span>
<span class="set-preview-tools" id="appSettingsPreviewToolbar"></span>
</div>
</div>
<p class="set-preview-empty" id="appSettingsPreviewEmpty" hidden>Nothing extra switched on: header keeps the gear only.</p>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Header buttons</h4><span class="set-scope">device</span></div>
<p class="set-group-hint">Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.</p>
<div class="set-group-body">
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response file viewer attachments monitor session away cron redraw">
<label class="set-chip" data-preview="header" data-preview-order="1" data-preview-text="A+"><input type="checkbox" id="appSettingsShowFontControls"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20 10 5l6 15"/><path d="M6.5 15h7"/><path d="M18 12h4M20 10v4"/></svg><span>Font Size</span></label>
<label class="set-chip" data-preview="header" data-preview-order="2" data-preview-text="CPU 12%"><input type="checkbox" id="appSettingsShowSystemStats" checked><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l2.5-7 4 14L16 12h5"/></svg><span>System Stats</span></label>
<label class="set-chip" data-preview="header" data-preview-order="3"><input type="checkbox" id="appSettingsShowRedrawButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg><span>Redraw Terminal</span></label>
<label class="set-chip" data-preview="header" data-preview-order="4"><input type="checkbox" id="appSettingsShowResponseViewer"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg><span>Response Viewer</span></label>
<label class="set-chip" data-preview="header" data-preview-order="5"><input type="checkbox" id="appSettingsShowAwayDigestButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg><span>Away Digest</span></label>
<label class="set-chip" data-preview="header" data-preview-order="6"><input type="checkbox" id="appSettingsShowSessionButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg><span>Session Manager</span></label>
<label class="set-chip" data-preview="header" data-preview-order="9"><input type="checkbox" id="appSettingsShowAttachmentsButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg><span>Attachments</span></label>
<label class="set-chip" data-preview="header" data-preview-order="10"><input type="checkbox" id="appSettingsShowFileViewerButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span>File Viewer</span></label>
<label class="set-chip" data-preview="header" data-preview-order="11"><input type="checkbox" id="appSettingsShowMultiMonitorButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg><span>Multi-monitor</span></label>
<label class="set-chip" data-preview="header" data-preview-order="13" data-preview-text="42%"><input type="checkbox" id="appSettingsShowPlanUsageLimits"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18a8 8 0 1 1 16 0"/><path d="M12 18l4.5-5"/></svg><span>Plan Usage</span></label>
<label class="set-chip" data-preview="header" data-preview-order="14"><input type="checkbox" id="appSettingsShowLifecycleLog"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg><span>Lifecycle Log</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Panels</h4><span class="set-scope">device</span></div>
<div class="set-group-body">
<div class="set-chips" data-search="panels monitor project insights file browser subagents">
<label class="set-chip" data-preview="panel" data-preview-order="1"><input type="checkbox" id="appSettingsShowMonitor" checked><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l2.5-7 4 14L16 12h5"/></svg><span>Monitor</span></label>
<label class="set-chip" data-preview="panel" data-preview-order="2"><input type="checkbox" id="appSettingsShowProjectInsights"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19V9"/><path d="M10 19V5"/><path d="M16 19v-7"/><path d="M22 19H2"/></svg><span>Project Insights</span></label>
<label class="set-chip" data-preview="panel" data-preview-order="3"><input type="checkbox" id="appSettingsShowFileBrowser"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span>File Browser</span></label>
<label class="set-chip" data-preview="panel" data-preview-order="4"><input type="checkbox" id="appSettingsShowSubagents"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><path d="M8.2 7.2 15.6 11M8.2 16.8 15.6 13"/></svg><span>Subagents</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Cross-session features</h4><span class="set-scope">synced</span></div>
<p class="set-group-hint">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.</p>
<div class="set-group-body">
<div class="set-chips" data-search="approvals inbox read my mind ultracode agents floating windows workflow">
<label class="set-chip" data-preview="header" data-preview-order="7"><input type="checkbox" id="appSettingsApprovalsInbox"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg><span>Approvals Inbox</span></label>
<label class="set-chip" data-preview="header" data-preview-order="8"><input type="checkbox" id="appSettingsReadMyMind"><span class="set-chip-ico set-chip-emoji" aria-hidden="true">🧠</span><span>Read My Mind</span></label>
<label class="set-chip" data-preview="header" data-preview-order="12"><input type="checkbox" id="appSettingsShowUltracodeAgents"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><path d="M8.2 7.2 15.6 11M8.2 16.8 15.6 13"/></svg><span>Ultracode Agents</span></label>
<label class="set-chip" data-preview="float" data-preview-order="1"><input type="checkbox" id="appSettingsUltracodeFloatingWindows"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18"/><circle cx="6" cy="6.5" r="0.6"/></svg><span>Ultracode Windows</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Scheduling</h4><span class="set-scope">device</span></div>
<div class="set-group-body">
<div class="set-chips" data-search="cron scheduled jobs toolbar">
<label class="set-chip" data-preview="toolbar" data-preview-order="1"><input type="checkbox" id="appSettingsShowCronButton"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg><span>Cron</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Subagent windows</h4></div>
<div class="set-group-body">
<div class="set-row" data-search="subagent tracking background agents">
<div class="set-row-text">
<span class="set-row-label">Track background agents <span class="set-scope">synced</span></span>
<span class="set-row-desc">Follow Claude Code subagents in real time.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsSubagentTracking" checked><span class="slider"></span></label>
</div>
<div class="set-row" data-search="subagent active tab only windows">
<div class="set-row-text">
<span class="set-row-label">Active tab only <span class="set-scope">device</span></span>
<span class="set-row-desc">Only show agent windows for the selected session.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsSubagentActiveTabOnly" checked><span class="slider"></span></label>
</div>
</div>
</div>
</section>
<!-- ══ Appearance ═══════════════════════════════════════════════ -->
<section class="set-section" id="settings-appearance" data-label="Appearance">
<div class="set-section-head">
@@ -1525,81 +1730,6 @@
</div>
</section>
<!-- ══ Header &amp; Panels ═══════════════════════════════════════ -->
<section class="set-section" id="settings-layout" data-label="Header &amp; Panels">
<div class="set-section-head">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
<h2>Header &amp; Panels</h2>
</div>
<p class="set-section-blurb">Pick the buttons and panels you actually use. Everything else stays out of the way.</p>
<div class="set-group">
<div class="set-group-head"><h4>Header buttons</h4><span class="set-scope">device</span></div>
<p class="set-group-hint">Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.</p>
<div class="set-group-body">
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response file viewer attachments monitor session away cron redraw">
<label class="set-chip"><input type="checkbox" id="appSettingsShowPlanUsageLimits"><span>Plan Usage</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowFontControls"><span>Font Size</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowSystemStats" checked><span>System Stats</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowLifecycleLog"><span>Lifecycle Log</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowResponseViewer"><span>Response Viewer</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowFileViewerButton"><span>File Viewer</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowAttachmentsButton"><span>Attachments</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowMultiMonitorButton"><span>Multi-monitor</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowSessionButton"><span>Session Manager</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowAwayDigestButton"><span>Away Digest</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowCronButton"><span>Cron</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowRedrawButton"><span>Redraw Terminal</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Panels</h4><span class="set-scope">device</span></div>
<div class="set-group-body">
<div class="set-chips" data-search="panels monitor project insights file browser subagents">
<label class="set-chip"><input type="checkbox" id="appSettingsShowMonitor" checked><span>Monitor</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowProjectInsights"><span>Project Insights</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowFileBrowser"><span>File Browser</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowSubagents"><span>Subagents</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Cross-session features</h4><span class="set-scope">synced</span></div>
<p class="set-group-hint">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.</p>
<div class="set-group-body">
<div class="set-chips" data-search="approvals inbox read my mind ultracode agents floating windows workflow">
<label class="set-chip"><input type="checkbox" id="appSettingsApprovalsInbox"><span>Approvals Inbox</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsReadMyMind"><span>Read My Mind</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsShowUltracodeAgents"><span>Ultracode Agents</span></label>
<label class="set-chip"><input type="checkbox" id="appSettingsUltracodeFloatingWindows"><span>Ultracode Windows</span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Subagent windows</h4></div>
<div class="set-group-body">
<div class="set-row" data-search="subagent tracking background agents">
<div class="set-row-text">
<span class="set-row-label">Track background agents <span class="set-scope">synced</span></span>
<span class="set-row-desc">Follow Claude Code subagents in real time.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsSubagentTracking" checked><span class="slider"></span></label>
</div>
<div class="set-row" data-search="subagent active tab only windows">
<div class="set-row-text">
<span class="set-row-label">Active tab only <span class="set-scope">device</span></span>
<span class="set-row-desc">Only show agent windows for the selected session.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsSubagentActiveTabOnly" checked><span class="slider"></span></label>
</div>
</div>
</div>
</section>
<!-- ══ Models ═══════════════════════════════════════════════════ -->
<section class="set-section" id="settings-models" data-label="Models">
<div class="set-section-head">
@@ -2032,95 +2162,6 @@
</div>
</section>
<!-- ══ System ═══════════════════════════════════════════════════ -->
<section class="set-section" id="settings-system" data-label="System">
<div class="set-section-head">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.26.6.86 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.65 0-1.25.4-1.51 1z"/></svg>
<h2>System</h2>
</div>
<p class="set-section-blurb">Paths, automation, remote access and updates.</p>
<div class="set-group">
<div class="set-group-head"><h4>Paths</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row has-field" data-search="claude.md template path new cases">
<div class="set-row-text">
<span class="set-row-label">CLAUDE.md template</span>
<span class="set-row-desc">Used when creating new cases. Leave empty for the built-in template.</span>
</div>
<input type="text" id="appSettingsClaudeMdPath" class="set-input" placeholder="/path/to/CLAUDE.md template">
</div>
<div class="set-row has-field" data-search="default working directory">
<div class="set-row-text">
<span class="set-row-label">Default working directory</span>
<span class="set-row-desc">Starting directory for new sessions.</span>
</div>
<input type="text" id="appSettingsDefaultDir" class="set-input" placeholder="~/projects">
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Automation</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row" data-search="image watcher popup screenshots">
<div class="set-row-text">
<span class="set-row-label">Image watcher</span>
<span class="set-row-desc">Detect and pop up new images found in session directories.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsImageWatcherEnabled"><span class="slider"></span></label>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Remote access</h4><span class="set-scope">synced</span></div>
<div class="set-group-body">
<div class="set-row" data-search="cloudflare tunnel remote access url">
<div class="set-row-text">
<span class="set-row-label">Cloudflare Tunnel</span>
<span class="set-row-desc">Expose Codeman on a public URL. Set CODEMAN_PASSWORD first.</span>
</div>
<label class="switch switch-sm"><input type="checkbox" id="appSettingsTunnelEnabled"><span class="slider"></span></label>
</div>
<div class="set-row" id="tunnelUrlRow" style="display:none">
<div class="set-row-text"><span class="set-row-label">Tunnel URL</span></div>
<div class="set-row-actions">
<span id="tunnelUrlDisplay" class="set-copy" title="Click to copy"></span>
<button class="btn-icon-sm" id="tunnelQrBtn" onclick="app.showTunnelQR()" title="Show QR code">&#x229E;</button>
</div>
</div>
<div class="set-row" id="tunnelUploadUrlRow" style="display:none">
<div class="set-row-text"><span class="set-row-label">Upload URL</span></div>
<span id="tunnelUploadUrlDisplay" class="set-copy" title="Click to copy"></span>
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-head"><h4>Updates</h4></div>
<div class="set-group-body">
<div class="set-row" data-search="version current release">
<div class="set-row-text"><span class="set-row-label">Current version</span></div>
<span class="set-static mono" id="updateCurrentVersion">&mdash;</span>
</div>
<div class="set-row" id="updateCheckRow" data-search="check for updates github release">
<div class="set-row-text">
<span class="set-row-label">Check for updates</span>
<span class="set-row-desc">Looks at GitHub for a newer Codeman release.</span>
</div>
<button class="btn-toolbar btn-sm" id="updateCheckBtn" onclick="app.checkForUpdate()">Check now</button>
</div>
<div id="updateResult" class="set-note" style="display:none"></div>
<div class="set-row" id="updateActionRow" style="display:none">
<div class="set-row-text"><span class="set-row-label" id="updateActionLabel">Update available</span></div>
<button class="btn-toolbar btn-sm btn-primary" id="updateNowBtn" onclick="app.startSelfUpdate()">Update now</button>
</div>
<div id="updateNotes" class="set-notes" style="display:none"></div>
<div id="updateProgress" class="set-progress" style="display:none"></div>
</div>
</div>
</section>
</div>
</div>
<div class="form-actions set-foot">
+9
View File
@@ -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;
+81 -2
View File
@@ -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'));
+256 -12
View File
@@ -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;
+44 -2
View File
@@ -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(/<section class="set-section" id="([a-z-]+)"/g)].map((m) => 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(/<label class="set-chip"([^>]*)>([\s\S]*?)<\/label>/g)];
const previewed = chips.filter(([, attrs]) => attrs.includes('data-preview='));
expect(previewed.length).toBeGreaterThanOrEqual(15);
for (const [, attrs, body] of previewed) {
const kind = attrs.match(/data-preview="([a-z]+)"/)?.[1];
expect(['header', 'panel', 'toolbar', 'float']).toContain(kind);
expect(attrs, `chip ${body} needs a preview order`).toMatch(/data-preview-order="\d+"/);
// A text token replaces the icon for readouts (plan usage, CPU, font size).
const hasIcon = body.includes('class="set-chip-ico') || attrs.includes('data-preview-text=');
expect(hasIcon, `chip ${body} has nothing to render in the preview`).toBe(true);
}
for (const id of [
'appSettingsPreviewHeader',
'appSettingsPreviewPanels',
'appSettingsPreviewToolbar',
'appSettingsPreviewFloats',
]) {
expect(layout).toContain(`id="${id}"`);
expect(settingsUi).toContain(`'${id}'`);
}
});
it('models: keeps the 1M variants as select options behind the context switch', () => {
const modal = settingsModal();
const select = modal.match(/id="appSettingsClaudeModel"([\s\S]*?)<\/select>/)?.[1] ?? '';