feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(web): split-view divider controls, active-pane opening, and pane swap - #170

Merged
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls
Aug 23, 2026
Merged

feat(web): split-view divider controls, active-pane opening, and pane swap#170
pandec merged 3 commits into
devfrom
t3code/improve-split-view-controls

Conversation

@pandec

Copy link
Copy Markdown
Owner

Follow-ups to the two-pane split view (#163) after real use.

Problems. The secondary pane's switch/close buttons rendered in ChatView's absolute titlebar overlay, which only reserves 64px, so they covered the git actions. Sidebar clicks and ⌘K picks always retargeted the primary route, so the secondary pane's thread was effectively pinned, and clicking its thread while the primary showed a draft tripped the duplicate-thread guard and folded the whole split. There was also no way to swap the two panes, and the sidebar gave no hint which threads were open in the split.

Fixes.

  • Secondary-pane controls moved to a hover-revealed cluster on the resize divider: swap threads (ArrowLeftRight), switch split thread (Replace), close (X). The cluster is a sibling of the drag surface, stops pointerdown propagation, and hides while dragging, so it can never start or catch a resize.
  • "Open split view" is now a self-gated control at the end of the chat header's action row, between the git actions and the terminal toggle.
  • Sidebar clicks (incl. search results) and ⌘K thread picks route through planThreadOpen: the active pane receives the pick, and a thread already on screen just gets its pane focused instead of navigating, which is what fixes the split-folding bug. The palette snapshots its owner pane at open time because its own focus traffic moves the live value. Mod-click and shift-range selection are untouched.
  • New threadPane.swap command (default mod+shift+, ⌘K "Swap split threads", divider button). Swap sets the secondary to the old primary and navigates to the old secondary under a 5s store latch that the duplicate guard consults; expiry or rejection rolls back and toasts, and the same physical side stays active afterwards. Disabled while the primary shows a draft.
  • Sidebar rows for the two open threads carry PanelLeft/PanelRight glyphs while the split is mounted (one string-stable store subscription at list level; only the two affected rows re-render).

Verification. 35 focused tests across threadSplitStore.test.ts, threadOpenTarget.test.ts, splitPaletteItems.test.ts (the latch guard was mutation-tested); vp run typecheck 15/15; vp check 0 errors.

Implemented by Claude Fable 5 in Claude Code.

… swap
The split-view pane controls lived in ChatView's titlebar overlay and
covered the git actions; sidebar and palette picks always retargeted the
primary route, and clicking the secondary pane's thread while the primary
showed a draft folded the whole split.
Secondary-pane controls now sit in a hover cluster on the resize divider
(swap / switch / close), and Open split view moved into the chat header's
action row. Sidebar clicks and cmd+K picks open into the active pane; a
thread already on screen just gets its pane focused, so a pick can never
fold the split. New threadPane.swap command (default mod+shift+\) trades
the two panes' threads under a store latch that keeps the duplicate-thread
guard quiet mid-swap. Sidebar rows mark the two open threads with pane
glyphs.
… controls
Review round (sol + opus on PR #170):
- The swap latch now expires through a store-owned timer; without one a
stalled navigation left it (and every swap affordance) stuck, since the
TTL was only read on re-renders. Mid-swap secondary picks keep the latch
so the late route arrival is still recognized; abort already yields to
the newer pick.
- mod+shift+\ never matched: Shift+Backslash types "|" and the resolver
had no Backslash code alias. Added, with a regression test; both new
behaviors were mutation-tested.
- Enter/Space row activation and archived-row opens now route through the
active-pane helper like clicks; its primary branch delegates to the
upstream navigateToThread instead of copying it.
- openThreadInActivePane returns the executed plan and the navigation
promise, so palette picks surface rejections again; post-swap focus
reclaim moved into swapThreadPanes so shortcut, palette, and divider
paths behave alike; the overlay focus intent moved out of the store into
the palette call sites so sidebar picks stop leaving stray intents.
- Divider: drag overhang biased off the primary pane's scrollbar lane,
reveal delayed to stop crossing flashes, cluster pinned visible on
coarse pointers, group label added, pane ring lowered below it, dead
no-drag removed, disabled swap button's tooltip made reachable.
- Pane glyphs ride slim rows too; stale-named modules renamed
(PaneControls, SplitPaneMarker).
…icks
Verification round follow-ups: a swap whose latch expired could still see
its navigation settle later and abort, toast over, or steal focus from a
newer swap — abortPaneSwap now takes the owning latch and stale
continuations no-op. Picking the in-flight swap's destination thread from
the sidebar or palette now focuses the primary pane (the route is already
heading there) instead of reopening it in the secondary, which folded the
split on arrival.
@pandec
pandec merged commit 5c90ea9 into devAug 23, 2026
1 check passed
pandec added a commit that referenced this pull request Aug 23, 2026
PR #170 landed on origin/dev mid-sync, touching ChatView.tsx and
Sidebar.tsx. The merge was conflict-free and holds up on inspection: the
slim-row split-pane marker sits after the provider icon and composes with
the pin indicator inserted after the title, and #170 already assumed a
settled thread can occupy a pane, which is exactly what pingdotgg#7969 makes common.
vp check, typecheck, focused split-view/sidebar/keybinding tests, and the
full suite all rerun and pass, except the documented PortScanner pair.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec