From 45ad9de89e65f3cf5a30d754683656d933a2cfe2 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 10 Aug 2026 03:59:43 +0200 Subject: [PATCH] feat(settings): rebuild App Settings as a rail over one scrolling document The modal had grown to 8 tabs that wrapped onto two rows on desktop and became a horizontal scroller on phones, with a "Display" mega-tab holding 11 sections and ~35 controls. Local Echo sat 60% down it, and the model settings were split across two tabs whose three controls fought each other (the 1M Opus toggle's own hint said it was "ignored when a Claude Model is selected above"). Replaced with a left rail that is a TABLE OF CONTENTS over one scrolling document: every section stays mounted, the rail follows the scroll, and find-in-page works across the whole thing. Nine sections: Terminal & Input (Local Echo is the first row of the first section) Appearance, Header & Panels, Models, Agents & CLIs, Notifications, Voice, Shortcuts, System Models are now one page. The picker is a card grid of BASE models with a single "1M context window" switch; context becomes a property of the chosen model and composes back into `claudeModel` as `base + [1m]`, which retires the precedence trap. Thinking effort is a segmented control on the same page, and the old Models tab (task routing) becomes a collapsed Advanced block under it. The 12 header-button toggles and the 8 panel toggles become chip grids, which is most of the old Display tab reclaimed. Rows now say whether a setting is per-device or synced, stated once per group. Phones drop the rail for a sticky jump pill that names the current section and opens a jump list, move Save into the header (the bottom action bar cost 60px), and render groups as one inset rounded list with hairline dividers instead of a stack of bordered cards. Load and save are untouched: every control keeps its id, so openAppSettings()/saveAppSettings() work as before. Model cards and the effort segment are views over hidden `s** that remain the source of truth; the cards hold the BASE model and the "1M context window" switch composes `base + [1m]` back into `claudeModel`, which is what retires the old "takes precedence over the toggle below" trap. ⚠️ `.modal-tabs`/`.modal-tab-btn`/`.modal-tab-content` are still used by `#sessionOptionsModal` and `#createCaseModal`; the settings rail uses its own `set-*` classes and must not restyle them. ⚠️ `admin-ui.js` injects the multi-user Users entry into `.set-rail-items` + `.set-doc`, so those hooks must survive any restructure. + **Header button visibility**: most header controls are opt-in and hidden by a marker class (`btn-multimonitor--hidden`, `btn-response-viewer-header--hidden`, `btn-file-viewer--hidden`, `btn-cron--hidden`) that `applyHeaderVisibilitySettings()` (settings-ui.js) toggles after settings load; the multi-monitor button is instead stripped at render by `renderIndexHtml`. ⚠️ Hiding must go through the marker class: the base rules are `display:inline-flex !important`, so an inline style cannot override them. Current desktop default is WS/CPU/MEM + File Viewer + gear, with the token chip and lifecycle-log button OFF. ⚠️ New header controls must not leak onto phones; `test/mobile-header-buttons-policy.test.ts` is the static guard. → [architecture-invariants#header-button-visibility-multi-monitor-response-viewer-file-viewer-cron](docs/architecture-invariants.md#header-button-visibility-multi-monitor-response-viewer-file-viewer-cron) **Gesture control** (camera hand-tracking overlay, opt-in, default OFF): `CODEMAN_GESTURE=1` makes the feature *available*; `gestureControlEnabled` turns it on. The bundle is injected by `renderIndexHtml` only when enabled, which is why that method is `async` and reads settings with `readSettings(true)` (a fresh read: a post-save reload lands inside the 2s cache TTL and would otherwise render the pre-toggle state). **Source lives in `packages/gesture-control/`; edit there, run `npm run build:gesture`, and commit the regenerated bundle** because dev serves the committed bundle with no runtime bundler. The MediaPipe wasm + model are fetched separately and gitignored. ⚠️ Keep `MP_VERSION` in `fetch-gesture-assets.mjs` in sync with `@mediapipe/tasks-vision`. → [architecture-invariants#gesture-control-the-source-package](docs/architecture-invariants.md#gesture-control-the-source-package) diff --git a/src/web/public/admin-ui.js b/src/web/public/admin-ui.js index 51929803..90c0901d 100644 --- a/src/web/public/admin-ui.js +++ b/src/web/public/admin-ui.js @@ -110,34 +110,49 @@ } // ── Admin Users panel (injected into the App Settings modal) ────────────── + // The settings modal is a rail (table of contents) over ONE scrolling + // document, so this appends a rail entry plus a real section rather than a + // tab button plus a hidden panel. function injectUsersTab() { const modal = document.getElementById('appSettingsModal'); - if (!modal || modal.querySelector('[data-tab="settings-users"]')) return; - const tabs = modal.querySelector('.modal-tabs'); - const body = modal.querySelector('.modal-body'); - if (!tabs || !body) return; + if (!modal || modal.querySelector('[data-section="settings-users"]')) return; + const rail = modal.querySelector('.set-rail-items'); + const body = modal.querySelector('.set-doc'); + if (!rail || !body) return; const btn = document.createElement('button'); - btn.className = 'modal-tab-btn'; - btn.dataset.tab = 'settings-users'; - btn.textContent = 'Users'; - tabs.appendChild(btn); - const content = document.createElement('div'); - content.className = 'modal-tab-content hidden'; + btn.type = 'button'; + btn.className = 'set-rail-item'; + btn.dataset.section = 'settings-users'; + btn.innerHTML = + 'Users'; + rail.appendChild(btn); + const content = document.createElement('section'); + content.className = 'set-section'; content.id = 'settings-users'; + content.dataset.label = 'Users'; content.innerHTML = ` -
- Users - - - - +
+ +

Users

-

Users share the host account; this separates workspaces, it does not sandbox +

Users share the host account; this separates workspaces, it does not sandbox users from each other. Pair with Docker cases for isolation.

-
-

`; +
+

Accounts

+
+
+
Manage users
+
+ + +
+
+
+

+
+
`; body.appendChild(content); - // Render whenever the tab is shown (the shared switchSettingsTab toggles it). + // Render whenever the entry is used (the shared switchSettingsTab scrolls to it). btn.addEventListener('click', renderUsers); content.querySelector('#adminAddUser').onclick = addUserFlow; content.querySelector('#adminOpenPanel').onclick = openAdminPanel; diff --git a/src/web/public/index.html b/src/web/public/index.html index 6ec66057..07b372ee 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -1273,778 +1273,837 @@