mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
feat(settings): put Session Options on the same surface as App Settings
Session Options was the last modal still wearing the old chrome: a strip of top tabs over `.form-row` stacks, sitting next to a settings modal that had just been rebuilt around a rail and grouped row cards. It now uses the same surface. The `set-*` rules move from `#appSettingsModal` to `:is(#appSettingsModal, #sessionOptionsModal)`. An `:is()` list takes the specificity of its most specific argument, and both arguments are ids, so every rule keeps exactly the weight it had - nothing downstream shifts in the cascade. What the two modals do NOT share is what the rail means: - App Settings stays a table of contents over one scrolling document. - Session Options switches: one `.set-section` visible, `.hidden` on the rest. Summary owns its own scroller and Respawn is long, so stacking them into a single document would bury both. `switchOptionsTab` now queries `.set-rail-item` (it read `.modal-tab-btn` before) and resets the document scroll, so a switched-to section starts at its own top. Phones get a horizontal, scrollable rail strip rather than App Settings' sticky jump pill, which Session Options has no equivalent of. That is close to the tab bar it replaces, so the phone gesture is unchanged. Content is regrouped into the row language - label, description, control pinned right - across all four sections: usage limits / respawn loop / cycle steps / loop control, identity / token management / this session, tracker / limits, and the summary timeline. The three cycle-step checkboxes became chips, which is why `_syncSettingsChips` now covers both modals and Session Options registers one delegated change listener per page for them. Every id and handler the JS reads is preserved, and the component classes it queries (`.duration-preset-btn`, `.duration-custom-input`, `.color-swatch`, `.respawn-status-text`, `.run-summary-filters .filter-btn`) are untouched. `data-claude-only` moved onto the rail entries, so external-CLI sessions still lose Respawn and Ralph and land on Context. `.modal-tabs`/`.modal-tab-btn`/`.modal-tab-content` now belong to #createCaseModal alone. test/session-options-structure.test.ts pins the rail to section pairing, the ids openSessionOptions reads, the one-visible-section invariant and the Claude-only entries.
This commit is contained in:
@@ -264,7 +264,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Per-device vs synced settings**: the `displayKeys` set in settings-ui.js is a **client-side merge policy**, not a wire filter. A display key seeds from the server only when localStorage has no value for it, which is what prevents one device overwriting another; `showPlanUsageLimits` is additionally `delete`d from the incoming payload outright. Separately, `SettingsUpdateSchema` is `.strict()` and simply **does not declare** `skin`, `showFileViewerButton`, `showCronButton`, `webglRendererEnabled`, `localEchoEnabled`, `cjkInputEnabled`, or `extendedKeyboardBar`, so sending one of those is a validation error. The rest (`showResponseViewer`, `showPlanUsageLimits`, `language`, and most `show*` keys) ARE in the schema and do persist server-side; they are per-device by client policy only. ⚠️ Adding a new per-device setting means deciding **both** questions: membership in `displayKeys`, and presence in the schema.
|
||||
|
||||
**App Settings modal** (`#appSettingsModal`): a left rail that is a **table of contents over ONE scrolling document**, not a tab switcher. Every section stays mounted (`.set-section`, ids `settings-terminal|appearance|layout|models|clis|notifications|voice|shortcuts|system`), the rail follows the scroll, and `switchSettingsTab(id)` keeps its historical name but SCROLLS instead of hiding. Phones swap the rail for the sticky `#appSettingsJump` pill (compact layout at ≤860px in mobile.css). ⚠️ **The load/save contract is `getElementById` by id**: `openAppSettings()`/`saveAppSettings()` read every control by a fixed id, so moving a control between sections is free but renaming or dropping one silently stops it loading or saving. `test/app-settings-structure.test.ts` is the static guard. ⚠️ Model cards (`#appSettingsModelCards`) and the effort segment are **views over hidden `<select>`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.
|
||||
**Settings surface** (`#appSettingsModal` + `#sessionOptionsModal`): the `set-*` language (left rail, groups of rows, control pinned right) is shared by BOTH modals through one `:is(#appSettingsModal, #sessionOptionsModal)` scope in styles.css — an `:is()` list takes its most specific argument's specificity, so both stay at the old id weight. **App Settings** is a left rail that is a **table of contents over ONE scrolling document**, not a tab switcher: every section stays mounted (`.set-section`, ids `settings-system|terminal|layout|appearance|models|clis|notifications|voice|shortcuts`, in that order — System first so the version and the updater are the first thing seen), the rail follows the scroll, and `switchSettingsTab(id)` keeps its historical name but SCROLLS instead of hiding. **Session Options** uses the same surface with a rail that really SWITCHES (`switchOptionsTab` shows one `.set-section` and puts `.hidden` on the rest, since Summary owns its own scroller and Respawn is long); phones give it a horizontal rail strip instead of the jump pill, which it does not have. `test/session-options-structure.test.ts` guards its rail↔section pairing and the `data-claude-only` entries external CLIs drop. Phones swap the rail for the sticky `#appSettingsJump` pill (compact layout at ≤860px in mobile.css). ⚠️ **The load/save contract is `getElementById` by id**: `openAppSettings()`/`saveAppSettings()` read every control by a fixed id, so moving a control between sections is free but renaming or dropping one silently stops it loading or saving. `test/app-settings-structure.test.ts` is the static guard. ⚠️ Model cards (`#appSettingsModelCards`) and the effort segment are **views over hidden `<select>`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` now belong to `#createCaseModal` alone; the settings surface uses its own `set-*` classes and must not restyle them. ⚠️ The **Header & Panels live preview** is a scale model rebuilt from the chips (`_syncLayoutPreview`); it owns NO icons, it CLONES `.set-chip-ico` out of the chip, so each icon has exactly one copy in index.html. A chip joins it via `data-preview` (slot) + `data-preview-order`, or `data-preview-text` for readouts that are not buttons. Its frame is painted from skin tokens only (hardcoded black alphas turned it into a grey slab on the light skins) and is `data-i18n-skip`. ⚠️ `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)
|
||||
|
||||
|
||||
+354
-258
@@ -983,282 +983,378 @@
|
||||
</div>
|
||||
|
||||
<!-- Session Options Modal (includes Respawn Settings) -->
|
||||
<!-- Session Options: same `set-*` surface as App Settings, but the rail is a
|
||||
real switcher (one .set-section at a time) rather than a table of
|
||||
contents — see the settings-surface block in styles.css. -->
|
||||
<div class="modal" id="sessionOptionsModal">
|
||||
<div class="modal-backdrop" onclick="app.closeSessionOptions()"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-content modal-lg set-shell">
|
||||
<div class="modal-header set-shell-head">
|
||||
<h3>Session Options</h3>
|
||||
<button class="modal-close" onclick="app.closeSessionOptions()" aria-label="Close session options">×</button>
|
||||
</div>
|
||||
<!-- Modal Tabs -->
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab-btn active" data-tab="respawn" data-claude-only onclick="app.switchOptionsTab('respawn')">Respawn</button>
|
||||
<button class="modal-tab-btn" data-tab="context" onclick="app.switchOptionsTab('context')">Context</button>
|
||||
<button class="modal-tab-btn" data-tab="ralph" data-claude-only onclick="app.switchOptionsTab('ralph')">Ralph / Todo</button>
|
||||
<button class="modal-tab-btn" data-tab="summary" onclick="app.switchOptionsTab('summary')">Summary</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Respawn Tab Content -->
|
||||
<div class="modal-tab-content" id="respawn-tab">
|
||||
<!-- Respawn Settings Section -->
|
||||
<div class="session-respawn-section" id="sessionRespawnSection">
|
||||
<div class="auto-resume-box">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoResumeEnabled" onchange="app.autoSaveAutoResume()">
|
||||
<span>Auto-resume when usage limit resets</span>
|
||||
</label>
|
||||
<span class="auto-resume-status" id="autoResumeStatus"></span>
|
||||
<span class="form-hint">If Claude pauses on a usage limit ("limit reached · resets 3pm"), Codeman waits for the reset time and automatically continues the work. Independent of the respawn loop below.</span>
|
||||
</div>
|
||||
|
||||
<div class="respawn-loop-box">
|
||||
<div class="respawn-loop-title">Respawn loop</div>
|
||||
<span class="form-hint respawn-loop-hint">One autonomous work cycle: whenever Claude goes idle, Codeman sends the update prompt, optionally runs /clear + /init, and kickstarts the next round — repeating for the chosen duration. All settings below belong to this loop; configure them, then press Enable.</span>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Duration</label>
|
||||
<div class="duration-presets">
|
||||
<button type="button" class="duration-preset-btn" data-minutes="30" onclick="app.selectDurationPreset('30')">30m</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="60" onclick="app.selectDurationPreset('60')">1h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="120" onclick="app.selectDurationPreset('120')">2h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="240" onclick="app.selectDurationPreset('240')">4h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="480" onclick="app.selectDurationPreset('480')">8h</button>
|
||||
<button type="button" class="duration-preset-btn active" data-minutes="" onclick="app.selectDurationPreset('')">∞</button>
|
||||
<div class="duration-custom">
|
||||
<button type="button" class="duration-preset-btn" data-minutes="custom" onclick="app.selectDurationPreset('custom')">Custom</button>
|
||||
<div class="input-suffix input-suffix-sm duration-custom-input">
|
||||
<input type="number" id="modalRespawnDuration" value="" min="1" placeholder="min">
|
||||
<span>min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">Presets</div>
|
||||
<div class="form-row">
|
||||
<label>Load Preset</label>
|
||||
<div class="preset-selector">
|
||||
<select id="respawnPresetSelect" onchange="app.updatePresetDescription()">
|
||||
<option value="">-- Select preset --</option>
|
||||
<optgroup label="Built-in" id="builtinPresetsGroup">
|
||||
</optgroup>
|
||||
<optgroup label="Custom" id="customPresetsGroup">
|
||||
</optgroup>
|
||||
</select>
|
||||
<button type="button" class="btn btn-sm" onclick="app.loadRespawnPreset()" title="Apply preset">Apply</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="app.saveCurrentAsPreset()" title="Save current config as preset">Save As...</button>
|
||||
</div>
|
||||
<p class="form-hint" id="presetDescriptionHint"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">Cycle Steps</div>
|
||||
<div class="form-row">
|
||||
<label>1. Update Prompt</label>
|
||||
<textarea id="modalRespawnPrompt" rows="1" placeholder="Prompt to send when idle" onchange="app.autoSaveRespawnConfig()" style="resize: vertical; min-height: 30px;">update all the docs and CLAUDE.md</textarea>
|
||||
</div>
|
||||
|
||||
<div class="respawn-options-row" style="margin: 8px 0;">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>2. Send /clear</span>
|
||||
</label>
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>3. Send /init</span>
|
||||
</label>
|
||||
<label class="checkbox-inline" title="Presses Enter for plan approvals and default question options">
|
||||
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Auto-accept prompts</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>4. Kickstart Prompt</label>
|
||||
<textarea id="modalRespawnKickstart" rows="1" placeholder="Optional: prompt if /init doesn't trigger work" onchange="app.autoSaveRespawnConfig()" style="resize: vertical; min-height: 30px;"></textarea>
|
||||
<span class="form-hint">Sent only when /init completes but Claude stays idle · Auto-accept presses Enter for plan approvals and default options</span>
|
||||
</div>
|
||||
|
||||
<div class="respawn-header">
|
||||
<div class="session-respawn-status" id="sessionRespawnStatus">
|
||||
<span class="respawn-status-indicator"></span>
|
||||
<span class="respawn-status-text">Not active</span>
|
||||
</div>
|
||||
<div class="respawn-actions">
|
||||
<button class="btn-toolbar btn-success btn-sm" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable</button>
|
||||
<button class="btn-toolbar btn-danger btn-sm" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End respawn-loop-box -->
|
||||
<div class="set-head-actions">
|
||||
<button class="modal-close" onclick="app.closeSessionOptions()" aria-label="Close session options">×</button>
|
||||
</div>
|
||||
</div><!-- End respawn-tab -->
|
||||
|
||||
<!-- Context Management Tab Content -->
|
||||
<div class="modal-tab-content hidden" id="context-tab">
|
||||
<div class="form-section-header">Appearance</div>
|
||||
<div class="form-row">
|
||||
<label>Session Name</label>
|
||||
<div style="display: flex; align-items: center; gap: 4px;">
|
||||
<span id="modalSessionPrefix" style="color: var(--text-muted); white-space: nowrap; display: none;"></span>
|
||||
<input type="text" id="modalSessionName" maxlength="128" placeholder="Auto (directory name)" onblur="app.saveSessionName()" style="flex: 1;">
|
||||
</div>
|
||||
<span class="form-hint">Custom name shown in the tab (right-click tab to rename inline)</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Session Color</label>
|
||||
<div class="color-picker" id="sessionColorPicker">
|
||||
<button type="button" class="color-swatch" data-color="default" title="Default">
|
||||
<span class="swatch-inner" style="background: var(--bg-hover)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="red" title="Red">
|
||||
<span class="swatch-inner" style="background: var(--session-red)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="orange" title="Orange">
|
||||
<span class="swatch-inner" style="background: var(--session-orange)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="yellow" title="Yellow">
|
||||
<span class="swatch-inner" style="background: var(--session-yellow)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="green" title="Green">
|
||||
<span class="swatch-inner" style="background: var(--session-green)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="blue" title="Blue">
|
||||
<span class="swatch-inner" style="background: var(--session-blue)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="purple" title="Purple">
|
||||
<span class="swatch-inner" style="background: var(--session-purple)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="pink" title="Pink">
|
||||
<span class="swatch-inner" style="background: var(--session-pink)"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-body">
|
||||
<nav class="set-rail" id="sessionOptionsRail" aria-label="Session option sections">
|
||||
<div class="set-rail-items">
|
||||
<button type="button" class="set-rail-item active" data-tab="respawn" data-claude-only onclick="app.switchOptionsTab('respawn')">
|
||||
<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"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||||
<span>Respawn</span>
|
||||
</button>
|
||||
<button type="button" class="set-rail-item" data-tab="context" onclick="app.switchOptionsTab('context')">
|
||||
<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"><ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/></svg>
|
||||
<span>Context</span>
|
||||
</button>
|
||||
<button type="button" class="set-rail-item" data-tab="ralph" data-claude-only onclick="app.switchOptionsTab('ralph')">
|
||||
<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="M9 6h11"/><path d="M9 12h11"/><path d="M9 18h11"/><polyline points="3 6 4 7 6 5"/><polyline points="3 12 4 13 6 11"/><polyline points="3 18 4 19 6 17"/></svg>
|
||||
<span>Ralph / Todo</span>
|
||||
</button>
|
||||
<button type="button" class="set-rail-item" data-tab="summary" onclick="app.switchOptionsTab('summary')">
|
||||
<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="M3 3v18h18"/><polyline points="7 14 11 10 14 13 20 7"/></svg>
|
||||
<span>Summary</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="set-rail-foot">These options belong to the selected session and save as you change them.</p>
|
||||
</nav>
|
||||
|
||||
<div class="form-section-header">Token Management</div>
|
||||
<div class="context-settings-grid">
|
||||
<div class="context-setting">
|
||||
<div class="context-setting-header">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()">
|
||||
<span>Auto-Compact</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoCompactThreshold" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
|
||||
<span>tokens</span>
|
||||
<div class="set-doc" id="sessionOptionsDoc">
|
||||
<!-- ══ Respawn ══════════════════════════════════════════════════ -->
|
||||
<section class="set-section" id="respawn-tab" data-label="Respawn">
|
||||
<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"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||||
<h2>Respawn</h2>
|
||||
</div>
|
||||
<p class="set-section-blurb">One autonomous work cycle, plus what to do when a usage limit pauses the session.</p>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Usage limits</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Auto-resume when the limit resets</span>
|
||||
<span class="set-row-desc">If Claude pauses on a subscription limit ("limit reached · resets 3pm"), Codeman waits for the reset and continues on its own. Independent of the respawn loop below.</span>
|
||||
<span class="auto-resume-status" id="autoResumeStatus"></span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalAutoResumeEnabled" onchange="app.autoSaveAutoResume()"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="modalAutoCompactPrompt" value="" placeholder="Focus prompt (optional)" onchange="app.autoSaveAutoCompact()">
|
||||
<span class="form-hint">Summarizes context while preserving key information</span>
|
||||
</div>
|
||||
|
||||
<div class="context-setting">
|
||||
<div class="context-setting-header">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()">
|
||||
<span>Auto-Clear</span>
|
||||
</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalAutoClearThreshold" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
|
||||
<span>tokens</span>
|
||||
<!-- Hidden wholesale for non-claude sessions and for a session with
|
||||
no live process (session-ui.js: openSessionOptions). -->
|
||||
<div id="sessionRespawnSection">
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Respawn loop</h4></div>
|
||||
<p class="set-group-hint">Whenever Claude goes idle, Codeman sends the update prompt, optionally runs /clear + /init, and kickstarts the next round — repeating for the chosen duration. Configure it here, then press Enable.</p>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row set-row-block">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Duration</span>
|
||||
<span class="set-row-desc">How long the loop keeps cycling before it stops on its own.</span>
|
||||
</div>
|
||||
<div class="duration-presets">
|
||||
<button type="button" class="duration-preset-btn" data-minutes="30" onclick="app.selectDurationPreset('30')">30m</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="60" onclick="app.selectDurationPreset('60')">1h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="120" onclick="app.selectDurationPreset('120')">2h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="240" onclick="app.selectDurationPreset('240')">4h</button>
|
||||
<button type="button" class="duration-preset-btn" data-minutes="480" onclick="app.selectDurationPreset('480')">8h</button>
|
||||
<button type="button" class="duration-preset-btn active" data-minutes="" onclick="app.selectDurationPreset('')">∞</button>
|
||||
<div class="duration-custom">
|
||||
<button type="button" class="duration-preset-btn" data-minutes="custom" onclick="app.selectDurationPreset('custom')">Custom</button>
|
||||
<div class="input-suffix input-suffix-sm duration-custom-input">
|
||||
<input type="number" id="modalRespawnDuration" value="" min="1" placeholder="min">
|
||||
<span>min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-row set-row-block">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Preset</span>
|
||||
<span class="set-row-desc">Load a saved loop configuration, or store the current one under a name.</span>
|
||||
</div>
|
||||
<div class="set-row-actions set-row-actions-wide">
|
||||
<select id="respawnPresetSelect" class="set-select" onchange="app.updatePresetDescription()">
|
||||
<option value="">-- Select preset --</option>
|
||||
<optgroup label="Built-in" id="builtinPresetsGroup">
|
||||
</optgroup>
|
||||
<optgroup label="Custom" id="customPresetsGroup">
|
||||
</optgroup>
|
||||
</select>
|
||||
<button type="button" class="btn-toolbar btn-sm" onclick="app.loadRespawnPreset()" title="Apply preset">Apply</button>
|
||||
<button type="button" class="btn-toolbar btn-sm" onclick="app.saveCurrentAsPreset()" title="Save current config as preset">Save As…</button>
|
||||
</div>
|
||||
<!-- Filled by updatePresetDescription() and CLEARED on every
|
||||
open, so it can never hold the row's own description. -->
|
||||
<span class="set-row-desc set-row-note" id="presetDescriptionHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-hint">Full context reset at threshold (use higher than compact)</span>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Cycle steps</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row set-row-block">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">1. Update prompt</span>
|
||||
<span class="set-row-desc">Sent as soon as the session goes idle.</span>
|
||||
</div>
|
||||
<textarea id="modalRespawnPrompt" class="set-textarea" rows="1" placeholder="Prompt to send when idle" onchange="app.autoSaveRespawnConfig()">update all the docs and CLAUDE.md</textarea>
|
||||
</div>
|
||||
|
||||
<div class="set-chips" id="respawnStepChips">
|
||||
<label class="set-chip"><input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()"><span>2. Send /clear</span></label>
|
||||
<label class="set-chip"><input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()"><span>3. Send /init</span></label>
|
||||
<label class="set-chip" title="Presses Enter for plan approvals and default question options"><input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()"><span>Auto-accept prompts</span></label>
|
||||
</div>
|
||||
|
||||
<div class="set-row set-row-block">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">4. Kickstart prompt</span>
|
||||
<span class="set-row-desc">Sent only when /init finishes but Claude stays idle. Optional.</span>
|
||||
</div>
|
||||
<textarea id="modalRespawnKickstart" class="set-textarea" rows="1" placeholder="Optional: prompt if /init doesn't trigger work" onchange="app.autoSaveRespawnConfig()"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Loop control</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Status</span>
|
||||
<div class="session-respawn-status" id="sessionRespawnStatus">
|
||||
<span class="respawn-status-indicator"></span>
|
||||
<span class="respawn-status-text">Not active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row-actions">
|
||||
<button class="btn-toolbar btn-success btn-sm" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable</button>
|
||||
<button class="btn-toolbar btn-danger btn-sm" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End sessionRespawnSection -->
|
||||
</section>
|
||||
|
||||
<!-- ══ Context ══════════════════════════════════════════════════ -->
|
||||
<section class="set-section hidden" id="context-tab" data-label="Context">
|
||||
<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"><ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/></svg>
|
||||
<h2>Context</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="set-section-blurb">What this tab is called, and how its context window is kept from filling up.</p>
|
||||
|
||||
<div class="form-section-header">File Watching</div>
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Image Watcher</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modalImageWatcherEnabled" checked onchange="app.toggleSessionImageWatcher()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Auto-popup new images (screenshots, generated files) in this session's directory</span>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">Performance</div>
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Flicker Filter</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modalFlickerFilterEnabled" onchange="app.toggleFlickerFilter()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Buffers output after screen clears to reduce Ink UI flickering (adds ~50ms latency)</span>
|
||||
</div>
|
||||
</div><!-- End context-tab -->
|
||||
|
||||
<!-- Ralph Wiggum Tab Content -->
|
||||
<div class="modal-tab-content hidden" id="ralph-tab">
|
||||
<div class="form-row form-row-switch">
|
||||
<label>Enable Tracker</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modalRalphEnabled">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="form-hint">Auto-detects loops and todos in Claude output</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Completion Phrase</label>
|
||||
<input type="text" id="modalRalphPhrase" placeholder="e.g., COMPLETE, DONE, FINISHED">
|
||||
<span class="form-hint">Phrase Claude outputs when loop is complete (without <promise> tags)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-section-header">Limits</div>
|
||||
<div class="ralph-limits-grid">
|
||||
<div class="form-col">
|
||||
<label>Max Iterations</label>
|
||||
<input type="number" id="modalRalphMaxIterations" value="0" min="0" max="1000">
|
||||
<span class="form-hint">0 = unlimited</span>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<label>Max Todos</label>
|
||||
<input type="number" id="modalRalphMaxTodos" value="50" min="1" max="500">
|
||||
<span class="form-hint">Per session</span>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<label>Todo Expiration</label>
|
||||
<div class="input-suffix input-suffix-sm">
|
||||
<input type="number" id="modalRalphTodoExpiration" value="60" min="1" max="1440">
|
||||
<span>min</span>
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Identity</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row has-field">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Session name</span>
|
||||
<span class="set-row-desc">Shown in the tab. Right-click a tab to rename it inline.</span>
|
||||
</div>
|
||||
<div class="set-row-actions">
|
||||
<span id="modalSessionPrefix" class="set-static" style="display: none; white-space: nowrap;"></span>
|
||||
<input type="text" id="modalSessionName" class="set-input" maxlength="128" placeholder="Auto (directory name)" onblur="app.saveSessionName()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row set-row-block">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Session color</span>
|
||||
<span class="set-row-desc">Tints the tab so a session is findable at a glance.</span>
|
||||
</div>
|
||||
<div class="color-picker" id="sessionColorPicker">
|
||||
<button type="button" class="color-swatch" data-color="default" title="Default">
|
||||
<span class="swatch-inner" style="background: var(--bg-hover)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="red" title="Red">
|
||||
<span class="swatch-inner" style="background: var(--session-red)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="orange" title="Orange">
|
||||
<span class="swatch-inner" style="background: var(--session-orange)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="yellow" title="Yellow">
|
||||
<span class="swatch-inner" style="background: var(--session-yellow)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="green" title="Green">
|
||||
<span class="swatch-inner" style="background: var(--session-green)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="blue" title="Blue">
|
||||
<span class="swatch-inner" style="background: var(--session-blue)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="purple" title="Purple">
|
||||
<span class="swatch-inner" style="background: var(--session-purple)"></span>
|
||||
</button>
|
||||
<button type="button" class="color-swatch" data-color="pink" title="Pink">
|
||||
<span class="swatch-inner" style="background: var(--session-pink)"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ralph-config-actions">
|
||||
<button class="btn-toolbar btn-primary" onclick="app.saveRalphConfig()">Save Ralph Config</button>
|
||||
</div>
|
||||
</div><!-- End ralph-tab -->
|
||||
|
||||
<!-- Summary Tab Content -->
|
||||
<div class="modal-tab-content hidden" id="summary-tab">
|
||||
<!-- Filter Buttons -->
|
||||
<div class="run-summary-filters">
|
||||
<button class="filter-btn active" data-filter="all" onclick="app.filterRunSummary('all')">All</button>
|
||||
<button class="filter-btn" data-filter="errors" onclick="app.filterRunSummary('errors')">Errors</button>
|
||||
<button class="filter-btn" data-filter="warnings" onclick="app.filterRunSummary('warnings')">Warnings</button>
|
||||
<button class="filter-btn" data-filter="respawn" onclick="app.filterRunSummary('respawn')">Respawn</button>
|
||||
<button class="filter-btn" data-filter="idle" onclick="app.filterRunSummary('idle')">Idle/Working</button>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="run-summary-timeline" id="runSummaryTimeline">
|
||||
<p class="empty-message">Select a session to view summary</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer with actions -->
|
||||
<div class="run-summary-footer">
|
||||
<span class="run-summary-session-info" id="runSummarySessionInfo"></span>
|
||||
<div class="run-summary-actions">
|
||||
<label class="auto-refresh-label">
|
||||
<input type="checkbox" id="runSummaryAutoRefresh" onchange="app.toggleRunSummaryAutoRefresh()">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshRunSummary()" title="Refresh summary">↻ Refresh</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('json')" title="Export as JSON">Export JSON</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('md')" title="Export as Markdown">Export MD</button>
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Token management</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Auto-compact</span>
|
||||
<span class="set-row-desc">Summarizes the context at the threshold while preserving key information.</span>
|
||||
</div>
|
||||
<div class="set-row-actions">
|
||||
<input type="number" id="modalAutoCompactThreshold" class="set-num set-num-wide" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
|
||||
<span class="set-unit">tokens</span>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row has-field">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Compact focus prompt</span>
|
||||
<span class="set-row-desc">Optional: what the summary should hold on to.</span>
|
||||
</div>
|
||||
<input type="text" id="modalAutoCompactPrompt" class="set-input" value="" placeholder="Focus prompt (optional)" onchange="app.autoSaveAutoCompact()">
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Auto-clear</span>
|
||||
<span class="set-row-desc">Full context reset at the threshold. Keep it higher than auto-compact.</span>
|
||||
</div>
|
||||
<div class="set-row-actions">
|
||||
<input type="number" id="modalAutoClearThreshold" class="set-num set-num-wide" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
|
||||
<span class="set-unit">tokens</span>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End summary-tab -->
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>This session</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Image watcher</span>
|
||||
<span class="set-row-desc">Pops up new images (screenshots, generated files) found in this session's directory.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalImageWatcherEnabled" checked onchange="app.toggleSessionImageWatcher()"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Flicker filter</span>
|
||||
<span class="set-row-desc">Buffers output after screen clears to reduce Ink UI flickering. Adds ~50ms latency.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalFlickerFilterEnabled" onchange="app.toggleFlickerFilter()"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══ Ralph / Todo ═════════════════════════════════════════════ -->
|
||||
<section class="set-section hidden" id="ralph-tab" data-label="Ralph / Todo">
|
||||
<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"><path d="M9 6h11"/><path d="M9 12h11"/><path d="M9 18h11"/><polyline points="3 6 4 7 6 5"/><polyline points="3 12 4 13 6 11"/><polyline points="3 18 4 19 6 17"/></svg>
|
||||
<h2>Ralph / Todo</h2>
|
||||
</div>
|
||||
<p class="set-section-blurb">Loop and todo tracking read out of this session's output.</p>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Tracker</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Enable tracker</span>
|
||||
<span class="set-row-desc">Auto-detects loops and todos in Claude's output.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="modalRalphEnabled"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row has-field">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Completion phrase</span>
|
||||
<span class="set-row-desc">What Claude prints when the loop is done, without <promise> tags.</span>
|
||||
</div>
|
||||
<input type="text" id="modalRalphPhrase" class="set-input" placeholder="e.g. COMPLETE, DONE, FINISHED">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Limits</h4></div>
|
||||
<div class="set-group-body">
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Max iterations</span>
|
||||
<span class="set-row-desc">0 = unlimited.</span>
|
||||
</div>
|
||||
<input type="number" id="modalRalphMaxIterations" class="set-num" value="0" min="0" max="1000">
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Max todos</span>
|
||||
<span class="set-row-desc">Per session; the oldest are evicted first.</span>
|
||||
</div>
|
||||
<input type="number" id="modalRalphMaxTodos" class="set-num" value="50" min="1" max="500">
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Todo expiration</span>
|
||||
<span class="set-row-desc">How long an untouched todo survives.</span>
|
||||
</div>
|
||||
<div class="set-row-actions">
|
||||
<input type="number" id="modalRalphTodoExpiration" class="set-num" value="60" min="1" max="1440">
|
||||
<span class="set-unit">min</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Apply</span>
|
||||
<span class="set-row-desc">Ralph settings are the one group here that does not save on change.</span>
|
||||
</div>
|
||||
<button class="btn-toolbar btn-primary btn-sm" onclick="app.saveRalphConfig()">Save Ralph config</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══ Summary ══════════════════════════════════════════════════ -->
|
||||
<section class="set-section hidden" id="summary-tab" data-label="Summary">
|
||||
<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"><path d="M3 3v18h18"/><polyline points="7 14 11 10 14 13 20 7"/></svg>
|
||||
<h2>Summary</h2>
|
||||
</div>
|
||||
<p class="set-section-blurb">What this session has done: respawns, errors, idle stretches.</p>
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Timeline</h4><span class="set-scope" id="runSummarySessionInfo"></span></div>
|
||||
<div class="set-group-body">
|
||||
<div class="run-summary-filters">
|
||||
<button class="filter-btn active" data-filter="all" onclick="app.filterRunSummary('all')">All</button>
|
||||
<button class="filter-btn" data-filter="errors" onclick="app.filterRunSummary('errors')">Errors</button>
|
||||
<button class="filter-btn" data-filter="warnings" onclick="app.filterRunSummary('warnings')">Warnings</button>
|
||||
<button class="filter-btn" data-filter="respawn" onclick="app.filterRunSummary('respawn')">Respawn</button>
|
||||
<button class="filter-btn" data-filter="idle" onclick="app.filterRunSummary('idle')">Idle/Working</button>
|
||||
</div>
|
||||
<div class="run-summary-timeline" id="runSummaryTimeline">
|
||||
<p class="empty-message">Select a session to view summary</p>
|
||||
</div>
|
||||
<div class="run-summary-footer">
|
||||
<label class="auto-refresh-label">
|
||||
<input type="checkbox" id="runSummaryAutoRefresh" onchange="app.toggleRunSummaryAutoRefresh()">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<div class="run-summary-actions">
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshRunSummary()" title="Refresh summary">↻ Refresh</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('json')" title="Export as JSON">Export JSON</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('md')" title="Export as Markdown">Export MD</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+104
-61
@@ -3170,21 +3170,21 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
stack of separate cards: at 390px the per-card borders were most of the pixels.
|
||||
============================================================================ */
|
||||
@media (max-width: 860px) {
|
||||
#appSettingsModal .modal-content.modal-lg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .modal-content.modal-lg {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-body {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Rail keeps only its search field, laid out as a bar */
|
||||
#appSettingsModal .set-rail {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-rail {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-right: 0;
|
||||
@@ -3195,37 +3195,37 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-rail-items,
|
||||
#appSettingsModal .set-rail-foot {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-rail-items,
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-rail-foot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-search {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-search {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-search input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-search input {
|
||||
padding: 9px 10px 9px 30px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Save moves into the header; the bottom action bar would cost 60px */
|
||||
#appSettingsModal .set-head-save {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-head-save {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-foot {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-foot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-doc {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-doc {
|
||||
padding: 0 14px 34px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── jump control ──────────────────────────────────────────────────── */
|
||||
#appSettingsModal .set-jump {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -3246,30 +3246,30 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-ico {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-ico {
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-label {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-label {
|
||||
font-weight: 580;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-chev {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-chev {
|
||||
margin-left: auto;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump[aria-expanded='true'] .set-jump-chev {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump[aria-expanded='true'] .set-jump-chev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-veil {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-veil {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -3277,7 +3277,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
background: rgba(4, 8, 13, 0.62);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-menu {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
@@ -3299,7 +3299,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
display: block;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
@@ -3314,43 +3314,43 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-row svg {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row .set-jump-count {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-row .set-jump-count {
|
||||
margin-left: auto;
|
||||
font-size: 0.62rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row.active {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-row.active {
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
color: var(--text);
|
||||
font-weight: 570;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-jump-row.active svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-jump-row.active svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── sections step down: the jump pill already names the current one ── */
|
||||
#appSettingsModal .set-section {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section + .set-section {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section + .set-section {
|
||||
border-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section-head {
|
||||
gap: 7px;
|
||||
margin: 18px 0 2px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head svg {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section-head svg {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
@@ -3359,7 +3359,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head h2 {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section-head h2 {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 640;
|
||||
letter-spacing: 0.1em;
|
||||
@@ -3367,45 +3367,45 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-head::after {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section-head::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--border), transparent);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-section-blurb {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-section-blurb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── live layout preview ───────────────────────────────────────────── */
|
||||
#appSettingsModal .set-preview {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-preview {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-preview-stage {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-preview-stage {
|
||||
min-height: 62px;
|
||||
}
|
||||
|
||||
/* ── inset grouped list ────────────────────────────────────────────── */
|
||||
#appSettingsModal .set-group {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group + .set-group {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group + .set-group {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-head {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-head {
|
||||
margin-bottom: 7px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-hint {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-hint {
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body {
|
||||
gap: 0;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
@@ -3413,7 +3413,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .set-row {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@@ -3421,104 +3421,147 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-row + .set-row {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .set-row + .set-row {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .set-chips,
|
||||
#appSettingsModal .set-group-body > .set-modelgrid,
|
||||
#appSettingsModal .set-group-body > .set-minigrid,
|
||||
#appSettingsModal .set-group-body > #appSettingsShortcutsList {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .set-chips,
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .set-modelgrid,
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .set-minigrid,
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > #appSettingsShortcutsList {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-group-body > .event-type-grid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-group-body > .event-type-grid {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-label {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row-label {
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-desc {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row-desc {
|
||||
font-size: 0.69rem;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Fields go full width under their label instead of fighting for the row */
|
||||
#appSettingsModal .set-row.has-field {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row.has-field {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row.has-field .set-select,
|
||||
#appSettingsModal .set-row.has-field .set-input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row.has-field .set-select,
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row.has-field .set-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-actions-wide {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row-actions-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-row-actions-wide .set-input {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-row-actions-wide .set-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-num {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-num {
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
/* Bigger touch targets for the toggles and chips */
|
||||
#appSettingsModal .switch-sm {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .switch-sm {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#appSettingsModal .switch-sm .slider:before {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .switch-sm .slider:before {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
#appSettingsModal .switch-sm input:checked + .slider:before {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .switch-sm input:checked + .slider:before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
#appSettingsModal .set-chip {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-chip {
|
||||
font-size: 0.78rem;
|
||||
padding: 9px 14px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-modelgrid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-modelgrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-minigrid {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-minigrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-mini .set-select {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-mini .set-select {
|
||||
width: 148px;
|
||||
}
|
||||
|
||||
/* One scrollable line beats a ragged two-row wrap for 7 effort levels */
|
||||
#appSettingsModal .set-segment {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-segment {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-segment::-webkit-scrollbar {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-segment::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#appSettingsModal .set-segment button {
|
||||
:is(#appSettingsModal, #sessionOptionsModal) .set-segment button {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Session Options, compact layout (<= 860px)
|
||||
|
||||
App Settings collapses its rail and hands navigation to the sticky
|
||||
#appSettingsJump pill. Session Options has no such pill (and no search), so
|
||||
its rail stays put and becomes a horizontal, scrollable strip — which is
|
||||
what its tab bar was before the two modals started sharing a surface.
|
||||
============================================================================ */
|
||||
@media (max-width: 860px) {
|
||||
#sessionOptionsModal .set-rail {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
#sessionOptionsModal .set-rail-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#sessionOptionsModal .set-rail-items::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sessionOptionsModal .set-rail-item {
|
||||
white-space: nowrap;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* The active marker is a left bar in the vertical rail; horizontally that
|
||||
reads as a stray tick, so the strip uses a filled pill instead. */
|
||||
#sessionOptionsModal .set-rail-item.active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sessionOptionsModal .set-rail-item.active {
|
||||
background: rgba(var(--accent-rgb), 0.13);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,8 +1278,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('presetDescriptionHint').textContent = '';
|
||||
|
||||
// Hide Ralph/Todo tab and Respawn tab for external CLI sessions (not supported)
|
||||
const ralphTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="ralph"]');
|
||||
const respawnTabBtn = document.querySelector('#sessionOptionsModal .modal-tab-btn[data-tab="respawn"]');
|
||||
const ralphTabBtn = document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="ralph"]');
|
||||
const respawnTabBtn = document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="respawn"]');
|
||||
if (isExternalCli) {
|
||||
if (ralphTabBtn) ralphTabBtn.style.display = 'none';
|
||||
if (respawnTabBtn) respawnTabBtn.style.display = 'none';
|
||||
@@ -1303,6 +1303,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
const modal = document.getElementById('sessionOptionsModal');
|
||||
|
||||
// Chips mirror their checkbox onto the label, the same way App Settings does
|
||||
// (settings-ui.js: _syncSettingsChips). Registered once per page, never per
|
||||
// open, or a long-lived tab accumulates one listener per visit.
|
||||
if (modal.dataset.chipsReady !== '1') {
|
||||
modal.dataset.chipsReady = '1';
|
||||
modal.addEventListener('change', e => {
|
||||
if (e.target?.closest?.('.set-chip')) this._syncSettingsChips();
|
||||
});
|
||||
}
|
||||
this._syncSettingsChips();
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
// Activate focus trap
|
||||
@@ -1500,18 +1512,32 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Session Options Modal Tabs
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Show one section of the Session Options modal.
|
||||
*
|
||||
* The chrome is the shared `set-*` settings surface, but unlike App Settings
|
||||
* (whose rail is a table of contents over one scrolling document) this rail
|
||||
* is a real switcher: exactly one `.set-section` is visible and the rest
|
||||
* carry `.hidden`. Summary owns its own scroller and Respawn is long, so
|
||||
* stacking them into a single document would bury both.
|
||||
*/
|
||||
switchOptionsTab(tabName) {
|
||||
// Toggle active class on tab buttons
|
||||
document.querySelectorAll('#sessionOptionsModal .modal-tab-btn').forEach(btn => {
|
||||
// Toggle active class on rail entries
|
||||
document.querySelectorAll('#sessionOptionsModal .set-rail-item').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// Toggle hidden class on tab content
|
||||
// Toggle hidden class on the sections
|
||||
document.getElementById('respawn-tab').classList.toggle('hidden', tabName !== 'respawn');
|
||||
document.getElementById('context-tab').classList.toggle('hidden', tabName !== 'context');
|
||||
document.getElementById('ralph-tab').classList.toggle('hidden', tabName !== 'ralph');
|
||||
document.getElementById('summary-tab').classList.toggle('hidden', tabName !== 'summary');
|
||||
|
||||
// A switched-to section starts at its own top, not at the scroll offset the
|
||||
// previous one was left at.
|
||||
const doc = document.getElementById('sessionOptionsDoc');
|
||||
if (doc) doc.scrollTop = 0;
|
||||
|
||||
// Load run summary data when switching to summary tab
|
||||
if (tabName === 'summary' && this.editingSessionId) {
|
||||
this.loadRunSummary(this.editingSessionId);
|
||||
|
||||
@@ -689,9 +689,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsJump')?.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
},
|
||||
|
||||
/** Mirror checkbox state onto the chip labels (see _initSettingsNav). */
|
||||
/**
|
||||
* Mirror checkbox state onto the chip labels (see _initSettingsNav).
|
||||
*
|
||||
* Covers Session Options too: it shares the `set-*` surface, and its cycle-step
|
||||
* chips would otherwise depend on `:has()` alone for their checked styling.
|
||||
*/
|
||||
_syncSettingsChips() {
|
||||
document.querySelectorAll('#appSettingsModal .set-chip').forEach(chip => {
|
||||
document.querySelectorAll('#appSettingsModal .set-chip, #sessionOptionsModal .set-chip').forEach(chip => {
|
||||
chip.classList.toggle('is-on', !!chip.querySelector('input')?.checked);
|
||||
});
|
||||
this._syncLayoutPreview();
|
||||
|
||||
+234
-145
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Session Options structural guard.
|
||||
*
|
||||
* The modal shares the `set-*` settings surface with App Settings, but its rail
|
||||
* is a real switcher: switchOptionsTab shows one `.set-section` and hides the
|
||||
* rest. Like App Settings, its load/save path is `getElementById` by a fixed set
|
||||
* of ids, so dropping or renaming an element in the markup fails silently — the
|
||||
* option just stops loading, or stops being written back.
|
||||
*
|
||||
* These tests read the REAL session-ui.js and index.html and pin that contract.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const publicDir = resolve(import.meta.dirname, '../src/web/public');
|
||||
const html = readFileSync(resolve(publicDir, 'index.html'), 'utf8');
|
||||
const sessionUi = readFileSync(resolve(publicDir, 'session-ui.js'), 'utf8');
|
||||
|
||||
/** The Session Options markup, so assertions can't be satisfied elsewhere. */
|
||||
function optionsModal(): string {
|
||||
const start = html.indexOf('<div class="modal" id="sessionOptionsModal">');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const end = html.indexOf('<!-- Close Session Confirmation Modal -->', start);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return html.slice(start, end);
|
||||
}
|
||||
|
||||
/** Body of a session-ui.js method, by name. */
|
||||
function methodBody(signature: string): string {
|
||||
const start = sessionUi.indexOf(`\n ${signature} {`);
|
||||
expect(start, `${signature} not found in session-ui.js`).toBeGreaterThan(-1);
|
||||
return sessionUi.slice(start, sessionUi.indexOf('\n },', start));
|
||||
}
|
||||
|
||||
const TABS = ['respawn', 'context', 'ralph', 'summary'];
|
||||
|
||||
describe('Session Options modal structure', () => {
|
||||
it('keeps every element openSessionOptions and switchOptionsTab touch by id', () => {
|
||||
const modal = optionsModal();
|
||||
const ids = new Set<string>();
|
||||
for (const sig of ['openSessionOptions(sessionId)', 'switchOptionsTab(tabName)', 'getRalphConfig()']) {
|
||||
for (const m of methodBody(sig).matchAll(/getElementById\('([A-Za-z0-9_-]+)'\)/g)) ids.add(m[1]);
|
||||
}
|
||||
// openSessionOptions also drives elements outside this modal (tabs, toasts);
|
||||
// only the ones it expects to find in here are this file's contract.
|
||||
const outside = new Set(['sessionOptionsDoc']);
|
||||
const missing = [...ids].filter((id) => !outside.has(id) && !modal.includes(`id="${id}"`));
|
||||
expect(missing).toEqual([]);
|
||||
expect(modal).toContain('id="sessionOptionsDoc"');
|
||||
});
|
||||
|
||||
it('pairs each rail entry with exactly one section, in the same order', () => {
|
||||
const modal = optionsModal();
|
||||
const rail = [...modal.matchAll(/class="set-rail-item[^"]*" data-tab="([a-z]+)"/g)].map((m) => m[1]);
|
||||
expect(rail).toEqual(TABS);
|
||||
for (const tab of TABS) {
|
||||
const hits = modal.split(`id="${tab}-tab"`).length - 1;
|
||||
expect(hits, `section ${tab}-tab should exist exactly once`).toBe(1);
|
||||
}
|
||||
// switchOptionsTab queries the rail by THIS class; `.modal-tab-btn` here
|
||||
// would silently stop the active marker from moving.
|
||||
expect(methodBody('switchOptionsTab(tabName)')).toContain("'#sessionOptionsModal .set-rail-item'");
|
||||
expect(methodBody('openSessionOptions(sessionId)')).toContain('.set-rail-item[data-tab="ralph"]');
|
||||
});
|
||||
|
||||
it('opens with exactly one section visible, the rest hidden', () => {
|
||||
const modal = optionsModal();
|
||||
const visible = TABS.filter((t) => modal.includes(`<section class="set-section" id="${t}-tab"`));
|
||||
expect(visible).toEqual(['respawn']);
|
||||
for (const t of TABS.filter((t) => t !== 'respawn')) {
|
||||
expect(modal).toContain(`<section class="set-section hidden" id="${t}-tab"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the Claude-only rail entries marked, so external CLIs lose them', () => {
|
||||
const modal = optionsModal();
|
||||
for (const tab of ['respawn', 'ralph']) {
|
||||
const entry = modal.match(new RegExp(`<button[^>]*data-tab="${tab}"[^>]*>`))?.[0] ?? '';
|
||||
expect(entry, `${tab} rail entry`).toContain('data-claude-only');
|
||||
}
|
||||
expect(modal.match(/<button[^>]*data-tab="context"[^>]*>/)?.[0]).not.toContain('data-claude-only');
|
||||
});
|
||||
|
||||
it('uses the shared settings surface rather than the modal-tab chrome', () => {
|
||||
const modal = optionsModal();
|
||||
expect(modal).toContain('class="modal-content modal-lg set-shell"');
|
||||
expect(modal).toContain('class="set-body"');
|
||||
expect(modal).not.toContain('class="modal-tabs"');
|
||||
expect(modal).not.toContain('modal-tab-btn');
|
||||
expect(modal).not.toContain('modal-tab-content');
|
||||
// The `set-*` rules are shared by both modals through one :is() scope.
|
||||
const css = readFileSync(resolve(publicDir, 'styles.css'), 'utf8');
|
||||
expect(css).toContain(':is(#appSettingsModal, #sessionOptionsModal) .set-row {');
|
||||
expect(css).toContain('#sessionOptionsModal .set-section.hidden {');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user