# 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. ## Recommended Model 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 ```swift 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 ## Recommended First Implementation Boundary 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.