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
+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);
}
}