From 2724c922ce909f006a209f999d188c89bb88f29b Mon Sep 17 00:00:00 2001 From: timkjr Date: Wed, 16 Sep 2026 20:23:42 -0500 Subject: [PATCH] fix(split-pane): style and dismiss the split-picker menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .split-picker-menu/-item/-empty (created in openSplitPicker()) had zero CSS and could not be dismissed except by picking an item — a default-path defect since the Split button ships enabled to anyone who flips showSplitButton on. Add CSS matching the sibling .run-mode-menu popover's look (floating-bg backdrop blur, border, shadow, z-index 1000 above the header's 100), and dismiss on outside click or Escape via the same one-shot listener pattern session-ui.js already uses for its other transient popovers (toggleCaseSettings(), toggleRunModeMenu()). Picking an item now routes through the same _dismissSplitPicker() method as the outside-click/Escape handlers, so the listeners never outlive the menu. Co-Authored-By: Claude Sonnet 5 --- src/web/public/styles.css | 40 ++++++++++++++++++++++++++++++++ src/web/public/terminal-split.js | 27 ++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/web/public/styles.css b/src/web/public/styles.css index b130c7f7..c24d554b 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -18635,3 +18635,43 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle flex: 1 1 auto; min-height: 0; } + +/* Split-picker menu: a small popover listing sessions to split with, appended + to document.body and positioned `fixed` by openSplitPicker() (JS sets + top/right against the Split header button's own rect). z-index above the + header (100) with headroom to spare, matching the sibling .run-mode-menu + dropdown's 1000. Dismissed by openSplitPane()'s own inline onclick, or by + the click-outside/Escape listeners installed alongside it. */ +.split-picker-menu { + position: fixed; + z-index: 1000; + min-width: 200px; + max-width: 320px; + padding: 4px; + background: var(--floating-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--control-border); + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.split-picker-item { + padding: 8px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.split-picker-item:hover { + background: var(--control-bg-hover, rgba(255, 255, 255, 0.08)); +} + +.split-picker-empty { + padding: 8px 10px; + font-size: 13px; + color: var(--text-muted); +} diff --git a/src/web/public/terminal-split.js b/src/web/public/terminal-split.js index 700a9d5b..96c378ce 100644 --- a/src/web/public/terminal-split.js +++ b/src/web/public/terminal-split.js @@ -146,7 +146,7 @@ Object.assign(CodemanApp.prototype, { menu.innerHTML = candidates .map( (c) => - `
${escapeHtml(c.label)}
` + `
${escapeHtml(c.label)}
` ) .join(''); } @@ -158,6 +158,31 @@ Object.assign(CodemanApp.prototype, { menu.style.top = `${rect.bottom + 4}px`; menu.style.right = `${window.innerWidth - rect.right}px`; } + + // Dismiss on outside click or Escape — same one-shot listener pattern as + // session-ui.js's other transient popovers (toggleCaseSettings(), + // toggleRunModeMenu()). Deferred by a tick so the click that OPENED the + // menu (still bubbling) doesn't immediately close it. Picking an item + // (above) calls the SAME dismiss method, so these listeners never + // outlive the menu either way. + const closeOnOutsideClick = (e) => { + if (!menu.contains(e.target) && e.target !== splitBtn) this._dismissSplitPicker(); + }; + const closeOnEscape = (e) => { + if (e.key === 'Escape') this._dismissSplitPicker(); + }; + this._splitPickerDismissHandlers = { closeOnOutsideClick, closeOnEscape }; + setTimeout(() => document.addEventListener('click', closeOnOutsideClick), 0); + document.addEventListener('keydown', closeOnEscape); + }, + + _dismissSplitPicker() { + document.getElementById('splitPickerMenu')?.remove(); + if (this._splitPickerDismissHandlers) { + document.removeEventListener('click', this._splitPickerDismissHandlers.closeOnOutsideClick); + document.removeEventListener('keydown', this._splitPickerDismissHandlers.closeOnEscape); + this._splitPickerDismissHandlers = null; + } }, openSplitPane(sessionId) {