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:
Codeman maintainer
2026-08-10 11:19:56 +02:00
parent a6cf4c2b2a
commit 6ccab925b1
7 changed files with 828 additions and 472 deletions
+354 -258
View File
@@ -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">&times;</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 &middot; 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 &mdash; 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 &middot; 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">&times;</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 &middot; 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 &mdash; 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('')">&#8734;</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&hellip;</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 &lt;promise&gt; 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">&#x21BB; 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 &lt;promise&gt; 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">&#x21BB; 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
View File
@@ -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);
}
}
+31 -5
View File
@@ -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);
+7 -2
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff