Files
downterm/docs/workspace-split-pane-plan.md
2026-03-14 02:58:59 +11:00

9.2 KiB

Workspace Split Pane Plan

Purpose

Capture the agreed design for workspace-owned split terminals so the feature can be implemented later without redoing scope and architecture decisions.

Agreed Scope

  • Split layouts belong to the workspace, not the screen.
  • A workspace may only be actively presented on one screen at a time.
  • Split layouts should be visible the same way when that workspace is opened on another monitor.
  • Nested splits are supported.
  • Any two tabs may be joined into a split.
  • Closing a pane causes its sibling to expand.
  • Split layout must survive relaunch.

Non-Goals

  • Simultaneous live rendering of the same workspace on multiple screens.
  • Full multi-user or collaborative state sync.
  • Arbitrary freeform pane layout. The model is tree-based binary splits.
  • Solving all interaction polish in the first pass.

Current Constraint

The current workspace model is flat:

  • WorkspaceController owns [TerminalSession]
  • one tab is active at a time
  • ContentView renders exactly one active SwiftTermView

This means split panes are not an additive UI feature. They require a workspace model refactor.

Do not model splits as "adjacent tabs with special behavior" internally.

Instead:

  1. A workspace owns ordered top-level tabs.
  2. Each top-level tab owns a pane tree.
  3. Pane tree leaves own TerminalSession instances.
  4. Pane tree internal nodes represent binary splits.
  5. The tab bar can visually present grouped/joined tabs based on the active tab's pane composition.

This keeps the domain model clean while still allowing the desired visual metaphor.

Proposed Domain Types

typealias WorkspaceTabID = UUID
typealias PaneID = UUID

struct WorkspaceTab: Identifiable, Equatable, Codable {
    var id: WorkspaceTabID
    var rootPane: PaneNode
    var selectedPaneID: PaneID?
    var titleMode: WorkspaceTabTitleMode
}

enum PaneNode: Equatable, Codable, Identifiable {
    case leaf(PaneLeaf)
    case split(PaneSplit)

    var id: PaneID { ... }
}

struct PaneLeaf: Equatable, Codable, Identifiable {
    var id: PaneID
    var sessionID: UUID
}

struct PaneSplit: Equatable, Codable, Identifiable {
    var id: PaneID
    var axis: SplitAxis
    var ratio: Double
    var first: PaneNode
    var second: PaneNode
}

enum SplitAxis: String, Codable {
    case horizontal
    case vertical
}

Notes:

  • ratio should be persisted so user-adjusted divider positions survive relaunch.
  • selectedPaneID allows focus-based commands such as split active pane, close active pane, and move focus.
  • The persisted model should store session identity separately from runtime AppKit/SwiftTerm objects.

Runtime Ownership

Global:

  • workspace summaries
  • workspace ordering
  • workspace-to-screen assignment
  • hotkeys

Workspace:

  • ordered top-level tabs
  • active top-level tab
  • per-tab pane tree
  • active/selected pane
  • terminal sessions for pane leaves

Screen:

  • which workspace is assigned
  • notch open/close state
  • geometry and transitions

This remains aligned with the existing workspace architecture.

Persistence Strategy

Split layout must survive relaunch, so workspace persistence needs to grow beyond summary metadata.

Recommended approach:

  1. Keep WorkspaceSummary for list metadata only.
  2. Introduce a persisted WorkspaceDocument or WorkspaceSnapshot per workspace.
  3. Persist:
    • top-level tabs
    • pane tree
    • active tab ID
    • selected pane ID
    • divider ratios
  4. Do not attempt to persist shell process contents.
  5. On relaunch, recreate sessions for pane leaves and restore layout structure only.

Important distinction:

  • layout persistence is required
  • terminal process continuity is not

UI Model

Main Content

The active top-level tab renders as a recursive split tree.

  • leaf node -> one SwiftTermView
  • split node -> HSplitView/VSplitView equivalent SwiftUI container with draggable divider

Tab Bar

Top-level tabs remain the primary navigation unit.

Visual behavior:

  • a normal unsplit tab looks like today
  • a joined/split tab should look grouped
  • grouped tabs should expose child pane titles visually inside the tab item

Recommended first-pass appearance:

  • one outer tab pill per top-level tab
  • inside that pill, show compact child title chips for each leaf pane
  • highlight the selected pane's chip

This gives the "tabs are joined" feel without making pane leaves first-class top-level tabs in the data model.

Command Model

New actions likely needed:

  • split active pane horizontally
  • split active pane vertically
  • focus next pane
  • focus previous pane
  • close active pane
  • resize focused split divider
  • join tab A into tab B
  • detach pane into new top-level tab

Semantics:

  • joining any two tabs merges one tab's root pane into the other's tree
  • the source top-level tab is removed
  • the destination top-level tab remains
  • sibling expansion on close is standard tree collapse

Join Semantics

Joining any two tabs should work as:

  1. Choose destination tab.
  2. Choose source tab.
  3. Replace destination root with a new split node:
    • first child = old destination root
    • second child = source root
  4. Remove source tab from workspace order.
  5. Select a predictable pane, preferably the source pane that was just added.

An explicit split axis should be required for the join action.

Close Semantics

When closing a pane:

  • if the pane has a sibling, the sibling expands into the parent's position
  • if the pane was the only leaf in a top-level tab:
    • close the whole top-level tab if more than one tab exists
    • otherwise create a replacement shell pane, matching current single-tab safety behavior

Focus Semantics

Pane focus must become explicit.

Recommended rules:

  • mouse click focuses that pane
  • splitting focuses the new pane
  • joining focuses the moved-in pane
  • closing a pane focuses the surviving sibling
  • top-level tab switch restores the previously selected pane in that tab

Migration Path

Implement in stages to reduce risk.

Stage 1: Domain Refactor

  • Replace flat workspace tabs with top-level tab objects.
  • Introduce pane tree types.
  • Keep only one leaf per tab initially so behavior is unchanged.

Stage 2: Runtime Layout Rendering

  • Render pane trees recursively in ContentView.
  • Add active pane selection.
  • Keep persistence off until runtime behavior is stable.

Stage 3: Split Actions

  • Split active pane horizontally/vertically.
  • Close pane with sibling expansion.
  • Basic focus movement.

Stage 4: Joined Tab UI

  • Update tab bar to show grouped child pane chips.
  • Surface active pane clearly.

Stage 5: Join / Unjoin Flows

  • Join any two tabs with explicit axis choice.
  • Support promoting a pane back to its own top-level tab if needed.

Stage 6: Persistence

  • Persist pane trees and top-level tab state.
  • Recreate sessions on launch.

Stage 7: Hotkeys and Polish

  • Add pane-focused shortcuts.
  • Add divider dragging polish.
  • Improve visual grouped-tab affordances.

Main Risks

1. Model complexity

The current flat [TerminalSession] model is simple. Tree-based layout introduces more state, more edge cases, and more focus semantics.

Mitigation:

  • refactor the data model before touching complex UI
  • keep top-level tabs and pane leaves distinct

2. SwiftTerm view ownership

TerminalView cannot be mounted in multiple places safely.

Mitigation:

  • preserve current rule: only one active presenting screen per workspace
  • keep one runtime TerminalSession per pane leaf

3. Persistence mismatch

Persisting layout is easy compared with persisting process state.

Mitigation:

  • persist layout and selection only
  • recreate shell sessions on launch

4. Joined-tab UX ambiguity

If grouped tabs are also used as pane chips, the interaction model can get confusing.

Mitigation:

  • preserve top-level tabs as the real navigation unit
  • use internal chips only as secondary indicators/actions

The first deliverable should include:

  • nested binary split trees
  • active pane focus
  • split and close pane actions
  • sibling expansion
  • grouped tab appearance
  • join any two tabs
  • persisted layout across relaunch
  • same workspace layout when moved to another screen

The first deliverable should not include:

  • simultaneous same-workspace rendering on multiple screens
  • drag-and-drop tree editing
  • restoring running shell process contents

Files Likely Impacted

  • CommandNotch/CommandNotch/Models/WorkspaceController.swift
  • CommandNotch/CommandNotch/Models/WorkspaceRegistry.swift
  • CommandNotch/CommandNotch/Models/WorkspaceStore.swift
  • CommandNotch/CommandNotch/ContentView.swift
  • CommandNotch/CommandNotch/Components/TabBar.swift
  • CommandNotch/CommandNotch/Models/TerminalSession.swift
  • workspace-related tests

Likely new files:

  • pane tree domain types
  • persisted workspace document types
  • split-pane rendering view(s)
  • pane-focused command helpers

Decision Summary

The feature is feasible.

The correct architecture is:

  • workspace-owned split tree
  • top-level tabs remain the primary unit
  • grouped tab visuals are a UI layer over the pane tree
  • one presenting screen per workspace
  • persisted layout, not persisted process contents

That is the implementation direction to use when this work is resumed.