Skip to content

feat(desktop): pane context menus + binary tree layout#3196

Merged
saddlepaddle merged 2 commits into
mainfrom
saddlepaddle/pane-context
Apr 6, 2026
Merged

feat(desktop): pane context menus + binary tree layout#3196
saddlepaddle merged 2 commits into
mainfrom
saddlepaddle/pane-context

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 6, 2026

Summary

  • Add data-driven context menu system to @superset/panes — right-click any pane for split/move/close actions, terminal panes get copy/paste/clear/scroll
  • Rewrite layout model from N-ary splits (children[]/weights[]) to strict binary tree (first/second/splitPercentage) matching react-mosaic's proven model
  • Add movePaneToTab/movePaneToNewTab store actions for "Move to Tab" submenu
  • Add terminal hotkeys (CLEAR_TERMINAL, SCROLL_TO_BOTTOM) and scroll-to-bottom button to v2 terminal pane

Key changes

Context menus

  • ContextMenuActionConfig<TData> type with recursive children support for submenus
  • Default actions (split h/v, split with chat/browser, equalize, move to tab, close) passed via contextMenuActions prop on <Workspace>
  • Each pane kind customizes via PaneDefinition.contextMenuActions (same pattern as paneActions for header buttons)

Binary tree layout (matching react-mosaic)

  • LayoutNode split variant: first/second/splitPercentage? instead of id/children[]/weights[]
  • Path-based addressing (SplitPath = ("first" | "second")[]) replaces split node IDs
  • Pane removal = sibling promotion (only neighbor affected, not all siblings)
  • Equalize = leaf-count-weighted splitPercentage recursively (all panes get equal visual space)

Test plan

  • Right-click terminal pane — Copy, Paste, Clear Terminal, Scroll to Bottom, Split ×4, Equalize, Move to Tab, Close Terminal
  • Right-click chat/browser/file panes — appropriate context menu with correct close label
  • Split panes horizontally and vertically — correct directions
  • Close a pane — only the neighbor grows, rest of layout unaffected
  • Equalize pane splits — all panes get equal visual space
  • Move to Tab submenu — lists other tabs + New Tab
  • Resize splits by dragging — sizes persist
  • Terminal hotkeys (clear, scroll to bottom) work
  • Scroll-to-bottom button appears when terminal scrolled up

Summary by cubic

Adds data-driven pane context menus and rewrites @superset/panes layout to a react-mosaic–style binary tree with path-based resizing and equalize. Improves terminal UX with copy/paste/clear/scroll actions, hotkeys, and a scroll-to-bottom button.

  • New Features

    • Right‑click menus on all panes with split/move/equalize/close; terminal adds Copy, Paste, Clear, Scroll to Bottom (Copy is disabled with no selection; shows shortcuts).
    • Workspace-level contextMenuActions and per‑pane PaneDefinition.contextMenuActions to customize menus; new PaneContextMenu component renders items, submenus, separators, and shortcuts via ContextMenuActionConfig.
    • “Move to Tab” submenu with movePaneToTab and movePaneToNewTab store actions.
    • New terminal hotkeys: CLEAR_TERMINAL, SCROLL_TO_BOTTOM; added scroll‑to‑bottom UI.
  • Migration

    • Layout node shape changed from children[]/weights[]/id to first/second/splitPercentage?; split IDs removed in favor of SplitPath.
    • Store API updates:
      • resizeSplit({ tabId, path, splitPercentage }) replaces resizeSplit({ splitId, weights }).
      • equalizeSplit({ tabId, path }) and new equalizeTab({ tabId }) replace split‑ID–based equalize.
      • New moves: movePaneToTab({ paneId, targetTabId }), movePaneToNewTab({ paneId }).
    • Utils renamed: updateSplitInLayoutupdateAtPath; added equalizeAllSplits, getNodeAtPath, positionToDirection.
    • Behavior: pane close promotes its sibling only; splits always nest (no flattening).

Written for commit 6a5c725. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Terminal pane: copy, paste, clear, and scroll-to-bottom actions (menu items, shortcuts, and a scroll-to-bottom button).
    • Workspace-level, customizable context menus with per-pane overrides and nested submenu support.
  • Improvements

    • Pane layout model updated to nested binary splits with path-based resizing and a tab-wide "equalize" action.
    • New actions to move panes between tabs or into a new tab.

- Add data-driven context menu system to @superset/panes with
  ContextMenuActionConfig type and recursive rendering
- Wire up context menus for all v2 pane types (terminal, chat, browser, file)
  with pane-specific actions (copy/paste/clear/scroll for terminal)
- Add terminal hotkeys (CLEAR_TERMINAL, SCROLL_TO_BOTTOM) and scroll-to-bottom
  button to v2 terminal pane
- Add movePaneToTab/movePaneToNewTab store actions for Move to Tab submenu
- Rewrite layout model from N-ary splits (children[]/weights[]) to strict
  binary tree (first/second/splitPercentage) matching react-mosaic's model
- Replace split node IDs with path-based addressing (SplitPath)
- Equalize now uses leaf-count-weighted splitPercentage recursively
- Pane removal uses sibling promotion (only neighbor affected)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds a hierarchical, customizable pane context menu system; refactors split layouts from array-based weights to binary tree (first/second) with path-based APIs; extends terminal runtime registry with selection, clear, scroll-to-bottom, paste, and terminal access methods; and plumbs context-menu actions through Workspace/Tab/Pane components.

Changes

Cohort / File(s) Summary
Terminal Runtime Registry
apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
Added public methods getSelection(terminalId), clear(terminalId), scrollToBottom(terminalId), paste(terminalId, text), and getTerminal(terminalId) to access/control terminal instances by ID with safe null handling.
Default Context Menu Hook
apps/desktop/src/renderer/routes/.../hooks/useDefaultContextMenuActions/index.ts, .../useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
Added/exported useDefaultContextMenuActions hook producing memoized default context menu actions (split, split-with, equalize, move/close) with hotkey-aware shortcuts and dynamic "Move to Tab" children.
Terminal Pane Hotkeys & UI
apps/desktop/src/renderer/routes/.../hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx, .../usePaneRegistry/usePaneRegistry.tsx
Wired CLEAR_TERMINAL and SCROLL_TO_BOTTOM hotkeys to registry calls; obtained memoized terminal via registry; updated pane layout to include ScrollToBottomButton; added terminal-specific context menu actions (Copy, Paste, Clear, Scroll) using registry and clipboard.
Workspace Integration
apps/desktop/src/renderer/routes/.../v2-workspace/$workspaceId/page.tsx, .../useWorkspaceHotkeys/useWorkspaceHotkeys.ts
Passed defaultContextMenuActions into Workspace; simplified EQUALIZE_PANE_SPLITS handler to always call equalizeTab({ tabId }).
Context Menu Types & Exports
packages/panes/src/react/types.ts, packages/panes/src/react/index.ts, packages/panes/src/index.ts
Added ContextMenuActionConfig<TData> type; extended PaneDefinition and WorkspaceProps to accept contextMenuActions; re-exported the new type(s) from package surfaces.
PaneContextMenu Component
packages/panes/src/react/components/.../PaneContextMenu/PaneContextMenu.tsx, .../PaneContextMenu/index.ts
New PaneContextMenu<TData> component renders recursive context menus from ContextMenuActionConfig arrays or resolvers, handles separators, nested submenus, disabled state, shortcuts, icons, and onSelect wiring.
Pane Prop Plumbing & Resolve Logic
packages/panes/src/react/components/.../Tab/Tab.tsx, .../Pane/Pane.tsx
Added optional contextMenuActions prop to Tab and Pane; introduced generic resolveActions helper; merged workspace-provided and pane-defined actions; wrapped pane rendering with PaneContextMenu.
Layout Types Refactor
packages/panes/src/types.ts
Replaced split node shape: removed id, children[], weights[]; added first, second, optional splitPercentage; introduced SplitBranch and SplitPath types.
Store API & Layout Utilities
packages/panes/src/core/store/store.ts, packages/panes/src/core/store/utils/utils.ts, packages/panes/src/core/store/utils/index.ts
Switched store/layout ops to path-based APIs (resizeSplit({ path, splitPercentage }), equalizeSplit({ path })); added equalizeTab, movePaneToTab, movePaneToNewTab; introduced getNodeAtPath, updateAtPath, getOtherBranch, equalizeAllSplits, and balanced-tree build/update helpers; removed weight-based plumbing.
Store & Utils Tests
packages/panes/src/core/store/store.test.ts, packages/panes/src/core/store/utils/utils.test.ts
Updated tests to reflect binary split shape and path-based APIs: removed weight/flattening assertions, added tests for getNodeAtPath, updateAtPath, equalizeAllSplits, and adjusted collapse/resize/equalize expectations.
Workspace Rendering & Resize Plumbing
packages/panes/src/react/components/Workspace/Workspace.tsx, .../Tab/components/.../SplitView/LayoutNodeView (and related)
Workspace accepts contextMenuActions and forwards to Tab; split rendering switched to use first/second with splitPercentage; resize persistence now reports { path, splitPercentage }; added path propagation through split views.
Public API Exports
packages/panes/src/index.ts, packages/panes/src/react/index.ts
Export surface expanded to include ContextMenuActionConfig, SplitBranch, and SplitPath.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Pane (Tab/Workspace)
    participant Registry as TerminalRuntimeRegistry
    participant Terminal
    participant Clipboard
    participant Store

    User->>UI: Right-click pane (open context menu)
    UI->>UI: resolve workspace + pane contextMenuActions
    User->>UI: Select "Paste"
    UI->>Clipboard: readText()
    Clipboard-->>UI: text
    UI->>Registry: paste(terminalId, text)
    Registry->>Terminal: paste text / insertText()

    User->>UI: Select "Copy"
    UI->>Registry: getSelection(terminalId)
    Registry-->>UI: selectedText
    UI->>Clipboard: writeText(selectedText)

    User->>UI: Select "Clear Terminal"
    UI->>Registry: clear(terminalId)
    Registry->>Terminal: clear()

    User->>UI: Press hotkey EQUALIZE_PANE_SPLITS
    UI->>Store: equalizeTab({ tabId })
    Store->>Store: equalizeAllSplits()
    Store-->>UI: updated layout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nudge the panes and split them fair,
First and second dance in tidy pairs,
Menus sprout branches, shortcuts take flight,
Terminals paste, clear, and scroll at night,
Hopping through paths — oh what delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(desktop): pane context menus + binary tree layout' clearly and concisely summarizes the main changes: adding context menus to panes and refactoring the layout system to use binary trees.
Description check ✅ Passed The PR description comprehensively covers the key changes, includes a detailed test plan with checkboxes, explains the migration path, and provides clear context for reviewers and future reference.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch saddlepaddle/pane-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 6, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx:89">
P2: `terminal` is memoized with `[terminalId]`, so it can stay `null` after attach and break the scroll-to-bottom button wiring. Recompute it each render (or include a dependency that changes after attach).</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:126">
P2: Handle clipboard write failures instead of firing `writeText()` without rejection handling.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:139">
P2: Avoid empty `catch` here; log or otherwise surface clipboard read failures so paste issues are diagnosable.

(Based on your team's feedback about handling async failures explicitly and not silently swallowing errors.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

try {
const text = await navigator.clipboard.readText();
if (text) terminalRuntimeRegistry.paste(terminalId, text);
} catch {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid empty catch here; log or otherwise surface clipboard read failures so paste issues are diagnosable.

(Based on your team's feedback about handling async failures explicitly and not silently swallowing errors.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx, line 139:

<comment>Avoid empty `catch` here; log or otherwise surface clipboard read failures so paste issues are diagnosable.

(Based on your team's feedback about handling async failures explicitly and not silently swallowing errors.) </comment>

<file context>
@@ -78,13 +98,84 @@ export function usePaneRegistry(
+								try {
+									const text = await navigator.clipboard.readText();
+									if (text) terminalRuntimeRegistry.paste(terminalId, text);
+								} catch {
+									// Clipboard access denied
+								}
</file context>
Fix with Cubic

onSelect: (ctx) => {
const { terminalId } = ctx.pane.data as TerminalPaneData;
const text = terminalRuntimeRegistry.getSelection(terminalId);
if (text) navigator.clipboard.writeText(text);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Handle clipboard write failures instead of firing writeText() without rejection handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx, line 126:

<comment>Handle clipboard write failures instead of firing `writeText()` without rejection handling.</comment>

<file context>
@@ -78,13 +98,84 @@ export function usePaneRegistry(
+							onSelect: (ctx) => {
+								const { terminalId } = ctx.pane.data as TerminalPaneData;
+								const text = terminalRuntimeRegistry.getSelection(terminalId);
+								if (text) navigator.clipboard.writeText(text);
+							},
+						},
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts (1)

235-239: Add a focused regression test for tab-wide equalization.

This hotkey now drives equalizeTab, so it’s worth locking in the nested-split behavior and the single-pane no-op path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
around lines 235 - 239, Add a focused regression test that exercises the
tab-wide equalization hotkey behavior: simulate invoking the useHotkey handler
for "EQUALIZE_PANE_SPLITS" (or invoke the exported function that triggers it)
and assert two scenarios—(1) when the active tab contains nested splits,
equalizeTab is called with the active tab id and panes are redistributed as
expected, and (2) when the active tab has a single pane, equalizeTab is a no-op
(no state change). Locate the hotkey wiring in useWorkspaceHotkeys (useHotkey
call) and the equalizeTab action on the store (store.getState().equalizeTab) to
invoke/mock the store and active tab (getActiveTab) accordingly; use mocks/stubs
for store.getState and equalizeTab to assert the call and state-change/no-op
behavior.
packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/PaneContextMenu.tsx (1)

13-16: Consider using a package-level import instead of deep relative path.

The import path "../../../../../../../../types" is fragile and hard to maintain. Since this is within the @superset/panes package, consider adding a shorter alias or re-exporting from a more accessible location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/PaneContextMenu.tsx`
around lines 13 - 16, Replace the fragile deep-relative import of
ContextMenuActionConfig and RendererContext in PaneContextMenu.tsx with a
package-level import by re-exporting those types from the `@superset/panes`
package (or the package's top-level types entry) and then importing them like
import type { ContextMenuActionConfig, RendererContext } from "@superset/panes";
— update the import statement to reference the package-level module and ensure
the package index exports those types.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx (1)

34-36: navigator.platform is deprecated.

While still widely supported, navigator.platform is deprecated. Consider using navigator.userAgentData?.platform with a fallback, or a utility that already handles platform detection elsewhere in the codebase.

#!/bin/bash
# Check if there's an existing platform detection utility in the codebase
rg -n "userAgentData|platform.*mac|isMac" --type=ts apps/desktop/src/renderer/ -l | head -10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
around lines 34 - 36, Replace the deprecated navigator.platform usage in the
MOD_KEY constant with a safe detection that prefers
navigator.userAgentData?.platform and falls back to navigator.platform (or a
shared utility if one exists), e.g., read platform =
navigator.userAgentData?.platform ?? navigator.platform, normalize with
.toLowerCase(), then check includes("mac") to choose "⌘" vs "Ctrl+"; update the
MOD_KEY definition in usePaneRegistry.tsx accordingly and reuse any existing
isMac/isPlatform utility if present.
packages/panes/src/core/store/store.test.ts (1)

474-549: Add tests for the two new tab-move mutations.

This suite covers movePaneToSplit, but movePaneToTab and movePaneToNewTab — the new state transitions behind the submenu work in this PR — still have no direct assertions here. A nested-layout case would be especially useful to lock down source-tab collapse and focus behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/core/store/store.test.ts` around lines 474 - 549, Add unit
tests in store.test.ts for the two new mutations movePaneToTab and
movePaneToNewTab: create scenarios that mirror the existing movePaneToSplit
tests (within-tab, across-tab, and no-op on self) and include at least one
nested-layout case to verify the source tab collapses correctly and
focus/activeTabId is updated as expected; specifically exercise the
store.getState().movePaneToTab and store.getState().movePaneToNewTab calls, then
assert tabs[].panes, tabs[].layout, activePaneId, and
store.getState().activeTabId reflect the intended post-move state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx:
- Line 29: Replace the terminal-specific hotkey with the generic pane hotkey:
change the useHotkeyDisplay call that sets closePaneShortcut from
useHotkeyDisplay("CLOSE_TERMINAL") to useHotkeyDisplay("CLOSE_PANE") so the
closePaneShortcut variable uses the registry's "Close Pane" binding; update any
references to closePaneShortcut in the same hook/component to ensure they now
reflect the generic CLOSE_PANE hotkey.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx:
- Around line 89-93: The memoized terminal is only keyed by terminalId so it
never re-computes when connectionState changes; update the useMemo dependency
array to include connectionState (i.e., useMemo(() =>
terminalRuntimeRegistry.getTerminal(terminalId), [terminalId, connectionState]))
so terminal is re-derived after attach, and remove or update the biome-ignore
comment accordingly; reference useMemo, terminalRuntimeRegistry.getTerminal,
terminalId, and connectionState to locate the code.

In `@packages/panes/src/core/store/store.ts`:
- Around line 32-47: buildBalancedTree constructs the binary split shape but
doesn't normalize splitPercentage so three panes become 25/25/50; after creating
the split tree call equalizeAllSplits(node) before returning to distribute
splitPercentage by leaf counts. Update buildBalancedTree to compute the tree as
it currently does and then run equalizeAllSplits on the resulting LayoutNode
(and make the same change in the analogous function at the other location
referenced by lines 55-69) so new multi-pane tabs get evenly sized
splitPercentage values.
- Around line 738-758: The active-pane fallback uses
findFirstPaneId(nextSourceLayout) which picks the leftmost pane instead of the
sibling that was promoted when removePaneFromLayout removed the moved pane;
update removePaneFromLayout (or its caller) to return the promotedPaneId (or
expose it via nextSourcePanes result) and change the activePaneId fallback in
the block that builds nextTabs (the branch handling t.id === sourceTab.id where
it currently does activePaneId: t.activePaneId === args.paneId ?
findFirstPaneId(nextSourceLayout) : t.activePaneId) to use the promotedPaneId
when available, falling back to findFirstPaneId only if no promotedPaneId is
provided; apply the same change to the analogous code around the other
occurrence (the similar activePaneId logic mentioned for the 792-803 region).

In `@packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx`:
- Around line 52-53: The mounted panel sizes aren’t updated when
node.splitPercentage changes because defaultSize is only used on mount; add a
useEffect that watches node.splitPercentage, recomputes firstSize =
node.splitPercentage ?? 50 and secondSize = 100 - firstSize, and calls
groupRef.current?.setLayout([firstSize, secondSize]) to sync the live layout;
also check the double-click handler that calls
groupRef.current?.setLayout([50,50]) (and the equalizeTab() flow) and if that
setLayout does not trigger the onLayout callback, follow it with a call to
resizeSplit() (or otherwise invoke the same onLayout handler) so the equalized
sizes persist to the store.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:
- Around line 34-36: Replace the deprecated navigator.platform usage in the
MOD_KEY constant with a safe detection that prefers
navigator.userAgentData?.platform and falls back to navigator.platform (or a
shared utility if one exists), e.g., read platform =
navigator.userAgentData?.platform ?? navigator.platform, normalize with
.toLowerCase(), then check includes("mac") to choose "⌘" vs "Ctrl+"; update the
MOD_KEY definition in usePaneRegistry.tsx accordingly and reuse any existing
isMac/isPlatform utility if present.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts:
- Around line 235-239: Add a focused regression test that exercises the tab-wide
equalization hotkey behavior: simulate invoking the useHotkey handler for
"EQUALIZE_PANE_SPLITS" (or invoke the exported function that triggers it) and
assert two scenarios—(1) when the active tab contains nested splits, equalizeTab
is called with the active tab id and panes are redistributed as expected, and
(2) when the active tab has a single pane, equalizeTab is a no-op (no state
change). Locate the hotkey wiring in useWorkspaceHotkeys (useHotkey call) and
the equalizeTab action on the store (store.getState().equalizeTab) to
invoke/mock the store and active tab (getActiveTab) accordingly; use mocks/stubs
for store.getState and equalizeTab to assert the call and state-change/no-op
behavior.

In `@packages/panes/src/core/store/store.test.ts`:
- Around line 474-549: Add unit tests in store.test.ts for the two new mutations
movePaneToTab and movePaneToNewTab: create scenarios that mirror the existing
movePaneToSplit tests (within-tab, across-tab, and no-op on self) and include at
least one nested-layout case to verify the source tab collapses correctly and
focus/activeTabId is updated as expected; specifically exercise the
store.getState().movePaneToTab and store.getState().movePaneToNewTab calls, then
assert tabs[].panes, tabs[].layout, activePaneId, and
store.getState().activeTabId reflect the intended post-move state.

In
`@packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/PaneContextMenu.tsx`:
- Around line 13-16: Replace the fragile deep-relative import of
ContextMenuActionConfig and RendererContext in PaneContextMenu.tsx with a
package-level import by re-exporting those types from the `@superset/panes`
package (or the package's top-level types entry) and then importing them like
import type { ContextMenuActionConfig, RendererContext } from "@superset/panes";
— update the import statement to reference the package-level module and ensure
the package index exports those types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 142e5eae-14a5-44cd-af15-db444f1bd8bd

📥 Commits

Reviewing files that changed from the base of the PR and between 11ed4f8 and 0610b51.

📒 Files selected for processing (21)
  • apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • packages/panes/src/core/store/store.test.ts
  • packages/panes/src/core/store/store.ts
  • packages/panes/src/core/store/utils/index.ts
  • packages/panes/src/core/store/utils/utils.test.ts
  • packages/panes/src/core/store/utils/utils.ts
  • packages/panes/src/index.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx
  • packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/PaneContextMenu.tsx
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/index.ts
  • packages/panes/src/react/index.ts
  • packages/panes/src/react/types.ts
  • packages/panes/src/types.ts

const equalizePaneSplitsShortcut = useHotkeyDisplay(
"EQUALIZE_PANE_SPLITS",
).text;
const closePaneShortcut = useHotkeyDisplay("CLOSE_TERMINAL").text;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a separate CLOSE_PANE hotkey defined
rg -n "CLOSE_PANE|CLOSE_TERMINAL" --type=ts -C2 apps/desktop/src/renderer/hotkeys/

Repository: superset-sh/superset

Length of output: 874


🏁 Script executed:

sed -n '25,35p' apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/\$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx

Repository: superset-sh/superset

Length of output: 437


Use CLOSE_PANE hotkey instead of CLOSE_TERMINAL for the generic close pane action.

Line 29 uses useHotkeyDisplay("CLOSE_TERMINAL") for closePaneShortcut, but a dedicated CLOSE_PANE hotkey is defined in the registry (with label "Close Pane"). The "Close Pane" action is generic and applies to all pane types, not just terminals, so it should reference CLOSE_PANE to match the semantic intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
at line 29, Replace the terminal-specific hotkey with the generic pane hotkey:
change the useHotkeyDisplay call that sets closePaneShortcut from
useHotkeyDisplay("CLOSE_TERMINAL") to useHotkeyDisplay("CLOSE_PANE") so the
closePaneShortcut variable uses the registry's "Close Pane" binding; update any
references to closePaneShortcut in the same hook/component to ensure they now
reflect the generic CLOSE_PANE hotkey.

Comment on lines +32 to +47
function buildBalancedTree(
panes: LayoutNode[],
direction: "horizontal" | "vertical" = "vertical",
): LayoutNode {
if (panes.length === 1) return panes[0]!;

const mid = Math.ceil(panes.length / 2);
const nextDirection = direction === "vertical" ? "horizontal" : "vertical";

return {
type: "split",
direction,
first: buildBalancedTree(panes.slice(0, mid), nextDirection),
second: buildBalancedTree(panes.slice(mid), nextDirection),
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Equalize multi-pane tabs when constructing them.

buildBalancedTree() only gives you the binary shape. Because the renderer falls back missing splitPercentage values to 50/50 at every branch, addTab({ panes: [a, b, c] }) now comes out as 25/25/50 instead of evenly sized panes. Running equalizeAllSplits() here keeps new multi-pane tabs consistent with the leaf-count-based layout model used everywhere else in this PR.

♻️ Proposed fix
-		layout: buildBalancedTree(leaves),
+		layout: equalizeAllSplits(buildBalancedTree(leaves)),

Also applies to: 55-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/core/store/store.ts` around lines 32 - 47,
buildBalancedTree constructs the binary split shape but doesn't normalize
splitPercentage so three panes become 25/25/50; after creating the split tree
call equalizeAllSplits(node) before returning to distribute splitPercentage by
leaf counts. Update buildBalancedTree to compute the tree as it currently does
and then run equalizeAllSplits on the resulting LayoutNode (and make the same
change in the analogous function at the other location referenced by lines
55-69) so new multi-pane tabs get evenly sized splitPercentage values.

Comment on lines +738 to +758
const nextTabs = s.tabs
.map((t) => {
if (t.id === sourceTab.id) {
if (!nextSourceLayout) return null;
return {
...t,
layout: nextSourceLayout,
panes: nextSourcePanes,
activePaneId:
t.activePaneId === args.paneId
? findFirstPaneId(nextSourceLayout)
: t.activePaneId,
};
}
if (t.id === targetTab.id) {
return {
...t,
layout: nextTargetLayout,
panes: { ...t.panes, [pane.id]: pane },
activePaneId: pane.id,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep source-tab focus on the promoted sibling after a move.

If the source layout is [p1, [p2, p3]] and active p3 is moved away, removePaneFromLayout() promotes p2, but findFirstPaneId(nextSourceLayout) still picks p1. That makes focus jump to the leftmost pane instead of the pane that inherited the moved pane’s slot. Please carry the promoted pane id out of the removal step and use that fallback here.

Also applies to: 792-803

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/core/store/store.ts` around lines 738 - 758, The
active-pane fallback uses findFirstPaneId(nextSourceLayout) which picks the
leftmost pane instead of the sibling that was promoted when removePaneFromLayout
removed the moved pane; update removePaneFromLayout (or its caller) to return
the promotedPaneId (or expose it via nextSourcePanes result) and change the
activePaneId fallback in the block that builds nextTabs (the branch handling
t.id === sourceTab.id where it currently does activePaneId: t.activePaneId ===
args.paneId ? findFirstPaneId(nextSourceLayout) : t.activePaneId) to use the
promotedPaneId when available, falling back to findFirstPaneId only if no
promotedPaneId is provided; apply the same change to the analogous code around
the other occurrence (the similar activePaneId logic mentioned for the 792-803
region).

Comment on lines +52 to +53
const firstSize = node.splitPercentage ?? 50;
const secondSize = 100 - firstSize;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In react-resizable-panels (the library behind @superset/ui/resizable), does changing a Panel's defaultSizeafter mount update the live layout, and doesPanelGroup.setLayout()fireonLayout so external state stays in sync?

💡 Result:

No, changing a Panel's defaultSize after mount does not update the live layout. The defaultSize prop is only used for the initial size on first render/mount; subsequent changes to it are ignored. This is confirmed in GitHub issue #125 where the maintainer states: "The defaultSize param is only used once– the first time the Panel renders, to set the initial size. After that, any changes to that property will not affect anything." Yes, PanelGroup.setLayout (via groupRef) updates the live layout and fires onLayoutChanged (the recommended callback for layout changes, including for external state sync like persistence), keeping external state in sync if using that prop. The changelog confirms setLayout persists to the in-memory cache (4.5.9), and docs position onLayoutChanged for post-change notifications suitable for saving state. onLayoutChange (continuous during drag) may or may not fire depending on implementation, but onLayoutChanged is the one for final sync.

Citations:


🏁 Script executed:

cd /dev/null && find . -type f -name "Tab.tsx" | grep -i workspace | head -5

Repository: superset-sh/superset

Length of output: 113


🏁 Script executed:

rg -l "SplitView|groupRef" --type tsx | head -10

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

find . -path "*/Workspace/components/Tab/Tab.tsx" -o -path "*/panes/src/react/components/Workspace/components/Tab/Tab.tsx"

Repository: superset-sh/superset

Length of output: 134


🏁 Script executed:

find . -name "Tab.tsx" | xargs wc -l 2>/dev/null | sort -rn | head

Repository: superset-sh/superset

Length of output: 138


🏁 Script executed:

rg "packages/panes" --files --type tsx | head -20

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

fd "Tab.tsx" --type f

Repository: superset-sh/superset

Length of output: 766


🏁 Script executed:

fd "packages/panes" --type d

Repository: superset-sh/superset

Length of output: 425


🏁 Script executed:

find . -type f -path "*packages/panes/src/react/components/Workspace/components/Tab*"

Repository: superset-sh/superset

Length of output: 2374


🏁 Script executed:

git ls-files | grep -i "tab.tsx" | grep -i workspace

Repository: superset-sh/superset

Length of output: 429


🏁 Script executed:

git ls-files | grep "panes/src"

Repository: superset-sh/superset

Length of output: 2970


🏁 Script executed:

cat -n ./packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx | head -100

Repository: superset-sh/superset

Length of output: 3238


🏁 Script executed:

rg "ResizablePanelGroup" --type tsx -A 5 | grep -E "(onLayout|onLayoutChanged)" | head -10

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

find . -path "*superset/ui/resizable*" -o -path "*@superset/ui*resizable*" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

git ls-files | grep -i resizable | head -20

Repository: superset-sh/superset

Length of output: 265


🏁 Script executed:

cat -n packages/ui/src/components/ui/resizable.tsx | head -50

Repository: superset-sh/superset

Length of output: 2261


Sync store-driven split percentage changes to the panel group layout.

The defaultSize prop is only evaluated on initial render. When node.splitPercentage updates in the store (e.g., from equalizeTab()), the mounted panels will not reflect the new values because defaultSize changes after mount are ignored. Add a useEffect to sync updates: when node.splitPercentage changes, call groupRef.current?.setLayout([firstSize, secondSize]) to apply the new layout to the live component. Also verify that the double-click handler calling setLayout([50, 50]) on line 70 correctly triggers the onLayout callback so the equalization persists to the store; if the layout change does not trigger onLayout, explicitly call resizeSplit() after setLayout().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx` around
lines 52 - 53, The mounted panel sizes aren’t updated when node.splitPercentage
changes because defaultSize is only used on mount; add a useEffect that watches
node.splitPercentage, recomputes firstSize = node.splitPercentage ?? 50 and
secondSize = 100 - firstSize, and calls groupRef.current?.setLayout([firstSize,
secondSize]) to sync the live layout; also check the double-click handler that
calls groupRef.current?.setLayout([50,50]) (and the equalizeTab() flow) and if
that setLayout does not trigger the onLayout callback, follow it with a call to
resizeSplit() (or otherwise invoke the same onLayout handler) so the equalized
sizes persist to the store.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 6, 2026

Greptile Summary

This PR rewrites the pane layout model from N-ary flat splits to a strict binary tree and adds a comprehensive context menu system. The binary tree utilities are well-implemented and tested. Two P1 bugs need fixing: terminal useMemo returns null permanently, and equalizeTab doesn't visually move panels.

Confidence Score: 3/5

Not safe to merge — two P1 bugs break features explicitly in the test plan (ScrollToBottomButton and equalize).

The binary tree layout model, context menu architecture, and store actions are well-designed and thoroughly tested. However two P1 bugs break primary user paths: terminal useMemo always returns null so ScrollToBottomButton never shows, and equalizeTab has no visual effect because defaultSize is uncontrolled. Once fixed the PR should reach 4-5.

TerminalPane.tsx (terminal useMemo missing connectionState dep) and Tab.tsx (defaultSize uncontrolled so equalizeTab has no visual effect)

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts Adds getSelection, clear, scrollToBottom, paste, getTerminal helpers — clean delegations to xterm
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/index.ts Barrel export for the new hook
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx Correct structure; CLOSE_TERMINAL shortcut shown for all pane types' generic close action
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx P1 bug: terminal useMemo missing connectionState dep — terminal always null, ScrollToBottomButton never works
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx Per-pane context menu customization correct; navigator.platform is deprecated
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts Correctly switches EQUALIZE_PANE_SPLITS from equalizeSplit to equalizeTab
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Clean wiring of useDefaultContextMenuActions into Workspace via contextMenuActions prop
packages/panes/src/core/store/store.test.ts Tests updated for binary tree; some reorderTab and movePaneToSplit regression tests removed without replacement
packages/panes/src/core/store/store.ts Binary tree store well-implemented; movePaneToTab/movePaneToNewTab handle layout removal and tab lifecycle correctly
packages/panes/src/core/store/utils/index.ts Barrel export for binary tree utilities
packages/panes/src/core/store/utils/utils.test.ts Comprehensive tests for all binary tree utilities including path traversal, equalize, and sibling promotion
packages/panes/src/core/store/utils/utils.ts Clean, correct binary tree utilities; sibling promotion and leaf-count-weighted equalization properly implemented
packages/panes/src/index.ts Correct public exports including ContextMenuActionConfig, SplitBranch, SplitPath
packages/panes/src/react/components/Workspace/Workspace.tsx contextMenuActions prop threaded through to Tab correctly
packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx P1 bug: ResizablePanel uses uncontrolled defaultSize so equalizeTab store updates don't visually move panels
packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx Context menu resolution mirrors paneActions pattern cleanly
packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/PaneContextMenu.tsx Recursive context menu rendering correctly handles separators, dynamic submenus, and disabled states
packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneContextMenu/index.ts Barrel export
packages/panes/src/react/index.ts Exports ContextMenuActionConfig and related types correctly
packages/panes/src/react/types.ts ContextMenuActionConfig with recursive children and per-pane-kind override pattern are well-typed
packages/panes/src/types.ts Clean binary tree LayoutNode union type with optional splitPercentage

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Right-click Pane] --> B[PaneContextMenu]
    B --> C[resolve actions]
    C --> D[workspace contextMenuActions]
    D -->|defaults| E[PaneDefinition.contextMenuActions]
    E -->|final list| F[ContextMenuItems]
    F --> G{action type?}
    G -->|separator| H[ContextMenuSeparator]
    G -->|has children| I[ContextMenuSub recursive]
    G -->|item| J[ContextMenuItem + shortcut]

    K[equalizeTab] --> L[splitPercentage updated in store]
    L --> M[SplitView re-renders with new defaultSize]
    M --> N[react-resizable-panels ignores defaultSize after mount]
    N --> O[Panels stay in place - visual bug]

    P[LayoutNode] --> Q{type}
    Q -->|pane| R[Pane component]
    Q -->|split| S[SplitView]
    S --> T[ResizablePanel defaultSize=first%]
    S --> U[ResizablePanel defaultSize=second%]
    T --> V[onLayout fires resizeSplit to store]

    W[closePane] --> X[removePaneFromLayout]
    X --> Y{sibling?}
    Y -->|yes| Z[promote sibling]
    Y -->|no| AA[remove tab]
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx, line 303-305 (link)

    P2 navigator.platform is deprecated

    navigator.platform is deprecated per MDN. Prefer navigator.userAgentData?.platform with a fallback:

    This is also a module-level constant evaluated at load time — if the module ever loads in a test environment without navigator it will throw.

Reviews (1): Last reviewed commit: "feat(desktop): add pane context menus + ..." | Re-trigger Greptile

</>
);
})}
<ResizablePanel defaultSize={firstSize}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Equalize does not visually update panels

ResizablePanel uses the uncontrolled defaultSize prop, which react-resizable-panels only applies on initial mount. When equalizeTab updates splitPercentage in the store, re-renders pass new defaultSize values, but mounted panels ignore them and stay put. The test plan item “Equalize pane splits — all panes get equal visual space” will fail. The existing double-click handler uses groupRef.current?.setLayout([50, 50]) specifically because defaultSize cannot move an already-mounted panel; the same imperative approach is needed after equalizeTab.

Comment on lines 95 to +99
return (
<div className="flex h-full w-full flex-col p-2">
<div
ref={containerRef}
className="min-h-0 flex-1 overflow-hidden"
style={{ backgroundColor: appearance.background }}
/>
<div className="relative min-h-0 flex-1 overflow-hidden">
<div
ref={containerRef}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 terminal is permanently nullScrollToBottomButton never works

The useMemo only lists [terminalId] as dependencies. On the first render the terminal isn’t yet in the registry (attachment happens inside a useEffect that runs after render), so getTerminal(terminalId) returns null and that result is cached. When connectionState later changes and triggers a re-render, useMemo finds terminalId unchanged and returns the cached null — it does not re-run the factory.

The biome-ignore comment claims “connectionState triggers re-derive after attach”, but this is incorrect: useMemo only re-runs when a listed dependency changes, not on every re-render.

Suggested change
return (
<div className="flex h-full w-full flex-col p-2">
<div
ref={containerRef}
className="min-h-0 flex-1 overflow-hidden"
style={{ backgroundColor: appearance.background }}
/>
<div className="relative min-h-0 flex-1 overflow-hidden">
<div
ref={containerRef}
const terminal = useMemo(
() => terminalRuntimeRegistry.getTerminal(terminalId),
[terminalId, connectionState],
);

kind: "chat",
data: { sessionId: null } as ChatPaneData,
});
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CLOSE_TERMINAL shortcut displayed for all pane types

closePaneShortcut is derived from useHotkeyDisplay("CLOSE_TERMINAL") and used for the generic “Close Pane” action shown in every pane’s context menu. If CLOSE_TERMINAL is terminal-specific, right-clicking a browser or chat pane will show a misleading shortcut hint. Consider a generic CLOSE_PANE hotkey ID or omit the shortcut from the default and let each pane-kind override add the correct binding.

Suggested change
},
const closePaneShortcut = useHotkeyDisplay("CLOSE_PANE").text;

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/panes/src/core/store/store.ts (1)

733-739: Consider equalizing the target tab after inserting the moved pane.

The new split wrapper is created without a splitPercentage, defaulting to 50/50. If the target tab has multiple existing panes, the moved pane will take 50% of the entire tab space regardless of existing pane count. This may not match user expectations for "Move to Tab."

Consider calling equalizeAllSplits(nextTargetLayout) to distribute space proportionally by leaf count, consistent with the equalization model elsewhere.

♻️ Proposed fix
 				const paneLeaf: LayoutNode = { type: "pane", paneId: pane.id };
-				const nextTargetLayout: LayoutNode = {
+				const nextTargetLayout: LayoutNode = equalizeAllSplits({
 					type: "split",
 					direction: "horizontal",
 					first: targetTab.layout,
 					second: paneLeaf,
-				};
+				});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/core/store/store.ts` around lines 733 - 739, The split
wrapper for moving a pane is created as nextTargetLayout (with paneLeaf
referencing pane.id and using targetTab.layout as first/second) but no
splitPercentage is set, so the moved pane always gets a 50/50 size; call
equalizeAllSplits(nextTargetLayout) after constructing nextTargetLayout and
before applying it to the store so the split percentages are distributed by leaf
count consistent with the equalization model used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/panes/src/core/store/store.ts`:
- Around line 733-739: The split wrapper for moving a pane is created as
nextTargetLayout (with paneLeaf referencing pane.id and using targetTab.layout
as first/second) but no splitPercentage is set, so the moved pane always gets a
50/50 size; call equalizeAllSplits(nextTargetLayout) after constructing
nextTargetLayout and before applying it to the store so the split percentages
are distributed by leaf count consistent with the equalization model used
elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4493b213-b419-4b6d-825e-16e80dbb0d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 0610b51 and 6a5c725.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • packages/panes/src/core/store/store.ts
  • packages/panes/src/core/store/utils/utils.ts

@saddlepaddle saddlepaddle merged commit 0c52ecc into main Apr 6, 2026
15 checks passed
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 7, 2026
)

* feat(desktop): add pane context menus + rewrite layout to binary tree

- Add data-driven context menu system to @superset/panes with
  ContextMenuActionConfig type and recursive rendering
- Wire up context menus for all v2 pane types (terminal, chat, browser, file)
  with pane-specific actions (copy/paste/clear/scroll for terminal)
- Add terminal hotkeys (CLEAR_TERMINAL, SCROLL_TO_BOTTOM) and scroll-to-bottom
  button to v2 terminal pane
- Add movePaneToTab/movePaneToNewTab store actions for Move to Tab submenu
- Rewrite layout model from N-ary splits (children[]/weights[]) to strict
  binary tree (first/second/splitPercentage) matching react-mosaic's model
- Replace split node IDs with path-based addressing (SplitPath)
- Equalize now uses leaf-count-weighted splitPercentage recursively
- Pane removal uses sibling promotion (only neighbor affected)

* fix: resolve lint warnings (non-null assertions, unused suppression)
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 7, 2026
cherry-pick済み:
- e728ebd feat(desktop): wire up missing hotkeys for v2 workspace (superset-sh#3190)
- 1eddeb3 feat(desktop): git changes sidebar with resource-oriented API (superset-sh#3177)
- 11ed4f8 V2 terminal env (superset-sh#3184)
- 0c52ecc feat(desktop): pane context menus + binary tree layout (superset-sh#3196)
- 5578746 fix(desktop): resolve file icons from origin instead of href (superset-sh#3199)
- 5a1e5d1 feat(panes): prefer sibling pane when closing active pane (superset-sh#3198)
- d670c4a V2 top bar: right sidebar toggle, org dropdown in sidebar, unified open-in button (superset-sh#3197)
- 2573fa2 fix(desktop): remove macOS background-to-tray quit interception (superset-sh#3205)
- 4a29342 feat: Superset CLI + CLI framework + Better Auth 1.5.6 (superset-sh#3194)
- 700cd65 fix(desktop): revert broken file icon origin fix + bundle all icon sources (superset-sh#3218)

フォーク独自対応:
- cleanupMainWindowResources()をexit pathに移動維持 (#3205対応)
- BROWSER_HARD_RELOAD/SEARCH_IN_FILESをv2 workspaceに配線
- BROWSER_RELOAD/HARD_RELOADのuseHotkey配線修正(リマップ対応)
- ansi_up依存維持
@Kitenite Kitenite deleted the saddlepaddle/pane-context branch April 13, 2026 16:36
Sign up for free to 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