Skip to content

Add split view to the Explorer (two panes side by side) - #517

Open
jaysuk wants to merge 2 commits into
Duet3D:v3.7-devfrom
jaysuk:explorer-split-view
Open

Add split view to the Explorer (two panes side by side)#517
jaysuk wants to merge 2 commits into
Duet3D:v3.7-devfrom
jaysuk:explorer-split-view

Conversation

@jaysuk

@jaysuk jaysuk commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a split view to the Explorer page: a pane can be split into two independent tab strips with a draggable divider between them, so two files (or a file and a directory listing) can be open side by side - e.g. reference one macro while editing another, or compare two config files.

  • Split right button (in the toolbar, next to "+") creates a second pane and moves the active tab into it. Enabled once there are 2+ tabs to split from.
  • Close split merges the second pane's tabs back into the first and removes the split.
  • Each pane behaves exactly like the pre-split single tab strip: its own tabs, its own drag-and-drop reordering, its own "+" button.
  • Tabs can be dragged from one pane's strip onto the other to move them across.
  • The divider is draggable to resize the panes live; the ratio persists across sessions (cacheStore.explorerSplitRatio), mirroring the existing nav-drawer resize handle.
  • Below the lg breakpoint the non-focused pane is hidden (not unmounted) and the split controls are hidden, so it degrades to the current single-pane behaviour on narrower viewports without losing state. (Originally gated on md; raised to lg since two side-by-side tab strips need more room than a typical tablet-width window gives them.)

Implementation notes

  • Kept the existing flat tabs array and its URL-sync/drag/close logic largely intact by annotating each tab with a groupId (which pane it's in), rather than restructuring into per-pane arrays. groups is capped at exactly two fixed ids, so there's nothing to grow or clean up.
  • setActiveTab() replaces direct activeTab.value writes and tracks which pane last had focus (focusedGroupId); the URL sync, scroll-restore watcher, and new-tab placement all key off it.
  • moveTabToGroup() / splitRight() / closeSplit() handle pane transitions, reusing the existing closeTab fallback-tab-selection logic so closing/moving the active tab in a pane always lands on a sensible sibling.
  • useGcodeThumbnails() is single-flight internally, so it's now instantiated once per pane rather than once for the whole page - otherwise two panes browsing different /gcodes directories would cancel each other's fetch and share one progress bar.
  • The non-focused pane collapses via a CSS class on narrow viewports rather than v-if, since MonacoEditor's unmount has no unsaved-content flush path - unmounting a dirty editor would silently lose the edits.
  • Browser back/forward ordinal resolution is scoped to the focused pane's own tabs, so a stray ordinal can never resolve into - and steal focus from - the other, non-focused pane.

Testing

  • vue-tsc --noEmit and npm run build both pass, including after the md -> lg breakpoint change.
  • Manually tested against a real Duet board: opened two macros side by side and dragged the divider to resize the panes.
  • Also manually driven end-to-end against the dev server with Playwright (no live board): opened tabs, split, dragged the divider, dragged a tab across panes, closed the split, and shrank the viewport below the breakpoint - all confirmed via screenshots and DOM assertions, zero console/page errors throughout. That pass predates the md -> lg change; the collapse behaviour itself is unchanged, just the threshold it fires at, but it hasn't been re-run specifically against lg.
  • Not yet exercised on real hardware: the narrow-viewport collapse, thumbnails against actual /gcodes directories in split panes, and the dirty-editor-survives-a-resize case.

🤖 Generated with Claude Code

jaysuk and others added 2 commits August 17, 2026 15:03
Lets a pane be split into two independent tab strips with a draggable
divider, so two files (or a file and a directory listing) can be open
side by side - e.g. reference one macro while editing another.

Implementation keeps the existing flat `tabs` array and URL/drag/close
logic largely intact by annotating each tab with which pane (group) it
belongs to, rather than restructuring tabs into per-pane arrays:

- groups: [{ id, activeTabId }], capped at exactly two fixed ids so
  there's nothing to grow or clean up
- setActiveTab() replaces direct activeTab.value writes and tracks
  which pane last had focus (focusedGroupId), which the URL sync,
  scroll-restore watcher, and new-tab placement all key off
- moveTabToGroup()/splitRight()/closeSplit() handle pane transitions,
  reusing closeTab's fallback-tab selection logic
- Per-pane thumbnail fetchers (useGcodeThumbnails is single-flight)
  so two panes browsing different /gcodes directories don't cancel
  each other's fetch
- The non-focused pane collapses via CSS on narrow viewports rather
  than unmounting, since MonacoEditor.dispose() has no unsaved-content
  flush path
- Browser back/forward ordinal resolution is scoped to the focused
  pane's own tabs so it can never reveal into or steal focus from the
  other, non-focused pane

Divider drag mirrors the existing nav-drawer resize handle in
layouts/builtin.vue; the split ratio persists via a new
cacheStore.explorerSplitRatio field the same way menuWidth does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two side-by-side tab strips need more room than the md threshold
(840px) gives them on a typical tablet-width window; lg leaves the
split controls and pane collapse gated on a genuinely wide viewport.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jaysuk

jaysuk commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

recheck

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