Fix command modified keys. Add plan for splitscreen
This commit is contained in:
351
docs/workspace-split-pane-plan.md
Normal file
351
docs/workspace-split-pane-plan.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# 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
|
||||
|
||||
- `Downterm/CommandNotch/Models/WorkspaceController.swift`
|
||||
- `Downterm/CommandNotch/Models/WorkspaceRegistry.swift`
|
||||
- `Downterm/CommandNotch/Models/WorkspaceStore.swift`
|
||||
- `Downterm/CommandNotch/ContentView.swift`
|
||||
- `Downterm/CommandNotch/Components/TabBar.swift`
|
||||
- `Downterm/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.
|
||||
Reference in New Issue
Block a user