diff --git a/docs/architecture-invariants.md b/docs/architecture-invariants.md index 1e89730c..2e4e7039 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -404,7 +404,7 @@ Anatomy: `.set-shell` → `.set-shell-head` (title + `.set-head-actions`) + `.se ### Split-pane sessions -**Split-pane sessions** (`showSplitButton`, header button, default OFF): shows two live sessions side-by-side in one Codeman window. Pane A is the untouched, existing singleton terminal (`this.terminal`/`this._ws` in terminal-ui.js); Pane B is a new, independent `SplitTerminalPane` (terminal-split.js) with its own xterm instance and its own `/ws/sessions/:id/terminal` WebSocket. ⚠️ **Pane B is deliberately plainer than Pane A** — no local-echo overlay, no CJK IME, no touch/mobile handlers, no keyboard accessory bar — since this is a desktop-only feature (a split view needs a wide viewport) and those features exist for mobile/touch input. No persistence: closing the browser tab or reloading always returns to the normal single-pane view; there is no localStorage key for split state. ⚠️ Splitting a session against itself is disallowed (the picker excludes the active session), and `MAX_WS_PER_SESSION` needs no change since Pane A/B are always two different sessions. ⚠️ Either pane's session ending (deleted locally or from another client) auto-collapses the split — Pane A's session ending promotes Pane B to the new single pane via `selectSession()`, never by trying to hot-swap the lightweight `SplitTerminalPane` object into the primary singleton state. Related but distinct: `detachSession()` already opens one session in a separate OS-level browser window (`isSoloWindow`) — that is prior art for "two sessions visible at once" but not for one window with a draggable in-page divider, which is what this feature adds. Design: `docs/split-pane-sessions-plan.md`. +**Split-pane sessions** (`showSplitButton`, header button, default OFF, per-device like `showFileViewerButton` — not in `SettingsUpdateSchema`, `displayKeys` in settings-ui.js): shows two live sessions side-by-side in one Codeman window. Pane A is the untouched, existing singleton terminal (`this.terminal`/`this._ws` in terminal-ui.js); Pane B is a new, independent `SplitTerminalPane` (terminal-split.js) with its own xterm instance and its own `/ws/sessions/:id/terminal` WebSocket. ⚠️ **Pane B is deliberately plainer than Pane A** — no local-echo overlay, no CJK IME, no touch/mobile handlers, no keyboard accessory bar — since this is a desktop-only feature (a split view needs a wide viewport) and those features exist for mobile/touch input; `.btn-split` is hard-hidden on phones in mobile.css regardless of the setting, and the per-device setting means turning it on at a desk can never sync it onto a phone in the first place. No persistence: closing the browser tab or reloading always returns to the normal single-pane view; there is no localStorage key for split state. ⚠️ Splitting a session against itself is disallowed (the picker excludes the active session), **as is splitting against a popped-out (detached) session** — `buildSplitPickerSessions()` excludes `detachedSessions` because a detached session's own window is already claiming its PTY size, and `MAX_WS_PER_SESSION` needs no change since Pane A/B are always two different sessions. ⚠️ Either pane's session ending (deleted locally or from another client) auto-collapses the split — Pane A's session ending promotes Pane B to the new single pane via `selectSession(id, { auto: true })` (an app-driven selection, so it must not spend the session's idle alert — see the Approvals Inbox note above), never by trying to hot-swap the lightweight `SplitTerminalPane` object into the primary singleton state. ⚠️ Pane B refits on every window/sidebar/tab-rail resize via the SAME trailing-edge `ResizeObserver` callback that resizes Pane A (`throttledResize` in terminal-ui.js) — it only ever measured Pane A's own container, so without an explicit `this._splitPane?.fit()` call there Pane B silently kept its stale PTY size through every resize that did not happen to be a divider drag. ⚠️ A dropped WebSocket leaves Pane B visibly dead (a message written into its own xterm buffer) rather than silently swallowing keystrokes with nothing on screen to explain why — there is no reconnect logic for v1, matching the "deliberately plainer than Pane A" design. Related but distinct: `detachSession()` already opens one session in a separate OS-level browser window (`isSoloWindow`) — that is prior art for "two sessions visible at once" but not for one window with a draggable in-page divider, which is what this feature adds. Design: `docs/split-pane-sessions-plan.md`. ### Gesture control: the setting diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index f364c701..bf4d7985 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -526,7 +526,13 @@ html.mobile-init .file-browser-panel { /* Approvals bell: phones answer from the overview's NEEDS YOU rows instead (inline approve/deny in mobile-overview.js); the bell would only crowd the header it was designed to stay out of. */ - .btn-icon-header.btn-approvals { + .btn-icon-header.btn-approvals, + /* Split-pane: desktop-only by design (two 240px min-width panes plus the + divider need ~486px, which a 393px phone cannot give them, and the + divider has no touch handlers). This is a hard gate independent of the + opt-in showSplitButton setting/marker class, so turning the setting on + at a desk can never also put the button in the phone header. */ + .btn-icon-header.btn-split { display: none !important; } diff --git a/src/web/public/settings-ui.js b/src/web/public/settings-ui.js index 6518a6a5..0d33e187 100644 --- a/src/web/public/settings-ui.js +++ b/src/web/public/settings-ui.js @@ -3373,6 +3373,7 @@ Object.assign(CodemanApp.prototype, { 'showTabDetachButton', 'mobileOverviewEnabled', 'sessionLineageLines', + 'showSplitButton', ]); // The plan-usage chip is a PER-DEVICE display setting (desktop default ON, // handheld default OFF): desktop can show it while mobile stays hidden. Drop diff --git a/src/web/schemas.ts b/src/web/schemas.ts index 485c046b..3541d977 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -1376,7 +1376,10 @@ export const SettingsUpdateSchema = z showFileBrowser: z.boolean().optional(), showSubagents: z.boolean().optional(), showMultiMonitorButton: z.boolean().optional(), - showSplitButton: z.boolean().optional(), + // showSplitButton is per-device (displayKeys in settings-ui.js) and + // deliberately NOT declared here, matching showFileViewerButton/skin/etc: + // a desktop opt-in must never sync onto a phone that never asked for it, + // and the phone header hard-hides .btn-split regardless (mobile.css). // Doubles as the plan-usage telemetry COLLECTION switch, read fresh from // disk by readPlanUsageTelemetryEnabled() (hooks-config.ts) at every claude // session create/respawn — not just the chip's DISPLAY preference. See that