Yep. AI rewrote the whole thing.
This commit is contained in:
885
docs/workspace-architecture-spec.md
Normal file
885
docs/workspace-architecture-spec.md
Normal file
@@ -0,0 +1,885 @@
|
||||
# Workspace Architecture Spec
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the target architecture for refactoring CommandNotch into a maintainable, testable macOS app with:
|
||||
|
||||
- explicit runtime state ownership
|
||||
- virtual workspaces shared across screens
|
||||
- typed settings instead of `UserDefaults` as the event bus
|
||||
- reduced singleton coupling
|
||||
- a realistic testing strategy for logic, integration, and UI
|
||||
|
||||
This is the implementation reference for the refactor.
|
||||
|
||||
## Goals
|
||||
|
||||
- Preserve the current product behavior where possible.
|
||||
- Support multiple virtual workspaces.
|
||||
- Allow multiple screens to point at the same workspace.
|
||||
- Allow screens to switch workspaces independently.
|
||||
- Keep screen-specific UI state local to each screen.
|
||||
- Make business logic unit testable without real `NSWindow`, `NSScreen`, or `UserDefaults`.
|
||||
- Keep AppKit/SwiftUI at the edges.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Multi-window rendering of the same live terminal instance at the same time.
|
||||
- Full state restoration of shell process contents across launches.
|
||||
- Perfect backward compatibility for internal architecture.
|
||||
- Solving all visual polish issues during the refactor.
|
||||
|
||||
## Constraints
|
||||
|
||||
- A `TerminalView` cannot safely belong to more than one window/view hierarchy at once.
|
||||
- A `LocalProcess`-backed session is inherently stateful and UI-coupled through SwiftTerm.
|
||||
- The app is macOS-only and already depends on AppKit + SwiftUI + SwiftTerm.
|
||||
|
||||
## Target Mental Model
|
||||
|
||||
There are three layers of state:
|
||||
|
||||
1. Global app state
|
||||
Global settings, hotkeys, launch behavior, workspace metadata, screen assignments.
|
||||
2. Workspace state
|
||||
Tabs, active tab, terminal sessions, workspace title, workspace-local behavior.
|
||||
3. Screen state
|
||||
Which workspace a screen is viewing, notch geometry, open/closed state, hover state, focus state.
|
||||
|
||||
The current app blurs these boundaries. The refactor makes them explicit.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
### Composition Root
|
||||
|
||||
`AppDelegate` becomes the composition root. It wires concrete implementations together and passes them into coordinators/controllers. It should be the only place that knows about most concrete types.
|
||||
|
||||
### Core Modules
|
||||
|
||||
- `AppSettingsStore`
|
||||
Loads and saves global settings. Publishes typed changes.
|
||||
- `WorkspaceRegistry`
|
||||
Owns all workspaces and workspace metadata.
|
||||
- `ScreenRegistry`
|
||||
Tracks connected screens and their screen-local state.
|
||||
- `NotchOrchestrator`
|
||||
Coordinates notch lifecycle per screen.
|
||||
- `WindowCoordinator`
|
||||
Owns AppKit windows/panels and binds them to screen models.
|
||||
- `HotkeyService`
|
||||
Registers global and local hotkeys and emits typed intents.
|
||||
- `SettingsCoordinator`
|
||||
Presents settings UI.
|
||||
- `PopoutCoordinator`
|
||||
Presents detached terminal windows.
|
||||
|
||||
### UI Boundaries
|
||||
|
||||
- SwiftUI views render state and emit intents only.
|
||||
- AppKit controllers own `NSWindow`, `NSPanel`, `NSHostingView`.
|
||||
- SwiftTerm integration is behind a terminal session abstraction.
|
||||
|
||||
## Ownership Model
|
||||
|
||||
### AppSettings
|
||||
|
||||
Owns:
|
||||
|
||||
- app-wide appearance settings
|
||||
- hotkey bindings
|
||||
- launch-at-login
|
||||
- menu bar visibility
|
||||
- default workspace behavior
|
||||
- workspace assignment persistence
|
||||
|
||||
Does not own:
|
||||
|
||||
- transient window geometry
|
||||
- active terminal sessions
|
||||
- per-screen hover/open state
|
||||
|
||||
### Workspace
|
||||
|
||||
Owns:
|
||||
|
||||
- `workspaceID`
|
||||
- display name
|
||||
- ordered tabs
|
||||
- active tab selection
|
||||
- detached tab metadata if retained
|
||||
- workspace-local terminal state
|
||||
|
||||
Does not own:
|
||||
|
||||
- screen assignment
|
||||
- notch geometry
|
||||
- whether a screen is open or closed
|
||||
|
||||
### ScreenContext
|
||||
|
||||
Owns:
|
||||
|
||||
- `screenID`
|
||||
- assigned `workspaceID`
|
||||
- notch presentation state
|
||||
- current notch frame/size
|
||||
- hover state
|
||||
- local focus state
|
||||
- local transition state
|
||||
|
||||
Does not own:
|
||||
|
||||
- tabs
|
||||
- terminal session collection
|
||||
- app-wide settings
|
||||
|
||||
## Required Domain Types
|
||||
|
||||
### Identifiers
|
||||
|
||||
```swift
|
||||
typealias WorkspaceID = UUID
|
||||
typealias ScreenID = String
|
||||
typealias TabID = UUID
|
||||
typealias SessionID = UUID
|
||||
```
|
||||
|
||||
### App Settings
|
||||
|
||||
```swift
|
||||
struct AppSettings: Equatable, Codable {
|
||||
var showMenuBarIcon: Bool
|
||||
var showOnAllDisplays: Bool
|
||||
var launchAtLogin: Bool
|
||||
|
||||
var appearance: AppearanceSettings
|
||||
var animation: AnimationSettings
|
||||
var terminal: TerminalSettings
|
||||
var hotkeys: HotkeySettings
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Summary
|
||||
|
||||
```swift
|
||||
struct WorkspaceSummary: Equatable, Codable, Identifiable {
|
||||
var id: WorkspaceID
|
||||
var name: String
|
||||
var createdAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Assignment
|
||||
|
||||
```swift
|
||||
struct ScreenWorkspaceAssignment: Equatable, Codable {
|
||||
var screenID: ScreenID
|
||||
var workspaceID: WorkspaceID
|
||||
}
|
||||
```
|
||||
|
||||
### Screen UI State
|
||||
|
||||
```swift
|
||||
struct ScreenUIState: Equatable {
|
||||
var screenID: ScreenID
|
||||
var workspaceID: WorkspaceID
|
||||
var notchState: NotchPresentationState
|
||||
var notchSize: CGSize
|
||||
var closedNotchSize: CGSize
|
||||
var isHovering: Bool
|
||||
var isFocused: Bool
|
||||
var transitionState: NotchTransitionState
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace State
|
||||
|
||||
```swift
|
||||
struct WorkspaceState: Identifiable {
|
||||
var id: WorkspaceID
|
||||
var name: String
|
||||
var tabs: [TerminalTabState]
|
||||
var activeTabID: TabID?
|
||||
}
|
||||
```
|
||||
|
||||
### Tab State
|
||||
|
||||
```swift
|
||||
struct TerminalTabState: Identifiable {
|
||||
var id: TabID
|
||||
var sessionID: SessionID
|
||||
var title: String
|
||||
}
|
||||
```
|
||||
|
||||
### Notch Lifecycle State
|
||||
|
||||
```swift
|
||||
enum NotchPresentationState: Equatable {
|
||||
case closed
|
||||
case open
|
||||
}
|
||||
|
||||
enum NotchTransitionState: Equatable {
|
||||
case idle
|
||||
case opening
|
||||
case closing
|
||||
case resizingUser
|
||||
case resizingPreset
|
||||
}
|
||||
```
|
||||
|
||||
## Target Runtime Objects
|
||||
|
||||
### `AppController`
|
||||
|
||||
Top-level coordinator created by `AppDelegate`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- boot app services
|
||||
- respond to lifecycle events
|
||||
- connect hotkey intents to workspace/screen actions
|
||||
- own references to main long-lived services
|
||||
|
||||
### `WorkspaceRegistry`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- create workspace
|
||||
- delete workspace
|
||||
- rename workspace
|
||||
- fetch workspace by id
|
||||
- publish workspace list changes
|
||||
- ensure at least one workspace exists
|
||||
|
||||
Suggested API:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
protocol WorkspaceRegistryType: AnyObject {
|
||||
var workspaceSummariesPublisher: AnyPublisher<[WorkspaceSummary], Never> { get }
|
||||
|
||||
func allWorkspaceSummaries() -> [WorkspaceSummary]
|
||||
func workspaceController(for id: WorkspaceID) -> WorkspaceControllerType?
|
||||
func createWorkspace(named: String?) -> WorkspaceID
|
||||
func deleteWorkspace(id: WorkspaceID)
|
||||
func renameWorkspace(id: WorkspaceID, name: String)
|
||||
func ensureWorkspaceExists() -> WorkspaceID
|
||||
}
|
||||
```
|
||||
|
||||
### `WorkspaceController`
|
||||
|
||||
Replaces most of `TerminalManager`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- own tabs for a single workspace
|
||||
- create and close tabs
|
||||
- detach tabs
|
||||
- switch active tab
|
||||
- update session appearance when settings change
|
||||
- publish workspace state
|
||||
|
||||
Suggested API:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
protocol WorkspaceControllerType: ObservableObject {
|
||||
var state: WorkspaceState { get }
|
||||
|
||||
func newTab()
|
||||
func closeTab(id: TabID)
|
||||
func closeActiveTab()
|
||||
func switchToTab(id: TabID)
|
||||
func switchToTab(index: Int)
|
||||
func nextTab()
|
||||
func previousTab()
|
||||
func detachActiveTab() -> TerminalSessionType?
|
||||
func updateTheme(_ theme: TerminalTheme)
|
||||
func updateFontSize(_ size: CGFloat)
|
||||
}
|
||||
```
|
||||
|
||||
### `ScreenRegistry`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- discover connected screens
|
||||
- maintain `ScreenContext` for each screen
|
||||
- maintain screen-to-workspace assignment
|
||||
- rebuild state on screen changes
|
||||
|
||||
Suggested API:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
protocol ScreenRegistryType: AnyObject {
|
||||
var screenContextsPublisher: AnyPublisher<[ScreenContext], Never> { get }
|
||||
|
||||
func allScreens() -> [ScreenContext]
|
||||
func screenContext(for id: ScreenID) -> ScreenContext?
|
||||
func assignWorkspace(_ workspaceID: WorkspaceID, to screenID: ScreenID)
|
||||
func activeScreenID() -> ScreenID?
|
||||
func refreshConnectedScreens()
|
||||
}
|
||||
```
|
||||
|
||||
### `ScreenContext`
|
||||
|
||||
Observable object for one physical display.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- store local UI state
|
||||
- expose a current `workspaceID`
|
||||
- emit user intents for local notch behavior
|
||||
|
||||
This should replace today’s overloaded `NotchViewModel`.
|
||||
|
||||
### `NotchOrchestrator`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- open/close notch for a screen
|
||||
- coordinate focus, hover, suppression, and transitions
|
||||
- enforce transition rules
|
||||
- drive frame updates indirectly through window coordination
|
||||
|
||||
The orchestrator should own the state machine, not `ContentView`.
|
||||
|
||||
### `WindowCoordinator`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- create/destroy one `NotchWindow` per screen
|
||||
- bind `ScreenContext` to window content
|
||||
- update window frames
|
||||
- respond to `resignKey`
|
||||
- focus the correct terminal view
|
||||
|
||||
This keeps AppKit-specific code out of registry/controller classes.
|
||||
|
||||
### `HotkeyService`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- register global hotkey bindings
|
||||
- observe only hotkey-related settings changes
|
||||
- emit typed commands
|
||||
|
||||
Suggested intent enum:
|
||||
|
||||
```swift
|
||||
enum AppCommand {
|
||||
case toggleNotch(screenID: ScreenID?)
|
||||
case newTab(workspaceID: WorkspaceID)
|
||||
case closeTab(workspaceID: WorkspaceID)
|
||||
case nextTab(workspaceID: WorkspaceID)
|
||||
case previousTab(workspaceID: WorkspaceID)
|
||||
case switchToTab(workspaceID: WorkspaceID, index: Int)
|
||||
case detachTab(workspaceID: WorkspaceID)
|
||||
case applySizePreset(screenID: ScreenID, presetID: UUID)
|
||||
case switchWorkspace(screenID: ScreenID, workspaceID: WorkspaceID)
|
||||
case createWorkspace(screenID: ScreenID)
|
||||
}
|
||||
```
|
||||
|
||||
## Terminal Session Design
|
||||
|
||||
### Current Problem
|
||||
|
||||
`TerminalSession` mixes:
|
||||
|
||||
- process lifecycle
|
||||
- SwiftTerm view ownership
|
||||
- delegate callbacks
|
||||
- some UI-facing published state
|
||||
|
||||
This makes reuse difficult.
|
||||
|
||||
### Target Split
|
||||
|
||||
Introduce:
|
||||
|
||||
- `TerminalSession`
|
||||
Process + session metadata + delegate event translation
|
||||
- `TerminalViewHost`
|
||||
Owns a `TerminalView` for one active screen/window
|
||||
|
||||
Minimal first-step compromise:
|
||||
|
||||
- Keep `TerminalSession` owning one `TerminalView`
|
||||
- Enforce that only one screen at a time can actively render a given workspace
|
||||
- Make that rule explicit in the architecture
|
||||
|
||||
Recommended v1 rule:
|
||||
|
||||
- many screens may point at the same workspace
|
||||
- only one screen may have that workspace open and focused at a time
|
||||
|
||||
This avoids trying to mirror one live terminal view into multiple windows.
|
||||
|
||||
## Settings Design
|
||||
|
||||
### Current Problem
|
||||
|
||||
`@AppStorage` is scattered across many views and managers, and `UserDefaults` changes trigger broad runtime work.
|
||||
|
||||
### Target Design
|
||||
|
||||
Introduce:
|
||||
|
||||
- `AppSettingsStore`
|
||||
Persistence boundary
|
||||
- `AppSettingsController`
|
||||
In-memory observable runtime settings
|
||||
|
||||
Suggested pattern:
|
||||
|
||||
```swift
|
||||
protocol AppSettingsStoreType {
|
||||
func load() -> AppSettings
|
||||
func save(_ settings: AppSettings)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class AppSettingsController: ObservableObject {
|
||||
@Published private(set) var settings: AppSettings
|
||||
|
||||
func update(_ update: (inout AppSettings) -> Void)
|
||||
}
|
||||
```
|
||||
|
||||
Views bind to `AppSettingsController`.
|
||||
Services subscribe to precise sub-slices of settings.
|
||||
|
||||
## Persistence Design
|
||||
|
||||
Persist only durable data:
|
||||
|
||||
- app settings
|
||||
- workspace summaries
|
||||
- screen-to-workspace assignments
|
||||
- optional workspace-local preferences
|
||||
- size presets
|
||||
|
||||
Do not persist:
|
||||
|
||||
- live `LocalProcess`
|
||||
- active shell buffer contents
|
||||
- transient hover/focus/transition state
|
||||
|
||||
Suggested storage keys:
|
||||
|
||||
- `appSettings`
|
||||
- `workspaceSummaries`
|
||||
- `screenAssignments`
|
||||
- `terminalSizePresets`
|
||||
|
||||
Prefer a single encoded settings object per concern over many independent keys.
|
||||
|
||||
## UI Structure
|
||||
|
||||
### Root Views
|
||||
|
||||
- `NotchRootView`
|
||||
- `ClosedNotchView`
|
||||
- `OpenWorkspaceView`
|
||||
- `TabStripView`
|
||||
- `WorkspacePickerView`
|
||||
- `SettingsRootView`
|
||||
|
||||
### View Rules
|
||||
|
||||
- Views may read state from observable objects.
|
||||
- Views may emit intents via closures or controller methods.
|
||||
- Views do not call global singletons.
|
||||
- Views do not own delayed business logic unless it is purely visual.
|
||||
|
||||
## Workspace UX Rules
|
||||
|
||||
- Every screen always has an assigned workspace.
|
||||
- A new screen defaults to:
|
||||
- the app default workspace strategy, or
|
||||
- the first existing workspace
|
||||
- Users can:
|
||||
- switch a screen to another workspace
|
||||
- create a new workspace from a screen
|
||||
- rename a workspace
|
||||
- Deleting a workspace:
|
||||
- reassigns affected screens to a fallback workspace
|
||||
- is disallowed if it is the last workspace
|
||||
|
||||
## Screen UX Rules
|
||||
|
||||
- A screen can be open/closed independently of other screens.
|
||||
- Two screens may point at the same workspace.
|
||||
- If a workspace is already open on another screen:
|
||||
- opening it on this screen should either transfer focus, or
|
||||
- close it on the other screen first
|
||||
|
||||
Recommended v1 behavior:
|
||||
|
||||
- one active open screen per workspace
|
||||
- opening workspace `W` on screen `B` closes `W` on screen `A`
|
||||
|
||||
That is simple, deterministic, and compatible with single `TerminalView` ownership.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 0: Hygiene
|
||||
|
||||
- remove dead settings and unused properties
|
||||
- fix version drift
|
||||
- fix stale comments
|
||||
- add test targets
|
||||
- isolate root build artifacts from source tree if desired
|
||||
|
||||
### Phase 1: Settings Layer
|
||||
|
||||
- create `AppSettings`, `AppSettingsStore`, `AppSettingsController`
|
||||
- move scattered `@AppStorage` usage into typed settings reads/writes
|
||||
- make `HotkeyService` observe only hotkey settings
|
||||
- make window sizing observe only relevant settings
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- no runtime logic depends on `UserDefaults.didChangeNotification`
|
||||
|
||||
### Phase 2: Workspace Core
|
||||
|
||||
- add `WorkspaceSummary`, `WorkspaceRegistry`, `WorkspaceController`
|
||||
- migrate current `TerminalManager` logic into `WorkspaceController`
|
||||
- ensure one default workspace exists
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- tabs exist inside a workspace object, not globally
|
||||
|
||||
### Phase 3: Screen Core
|
||||
|
||||
- add `ScreenContext`, `ScreenRegistry`
|
||||
- migrate current `NotchViewModel` responsibilities into `ScreenContext`
|
||||
- persist screen-to-workspace assignments
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- screen-local state is independent from workspace state
|
||||
|
||||
### Phase 4: Notch Lifecycle Orchestrator
|
||||
|
||||
- move hover/open/close/transition logic out of `ContentView`
|
||||
- add explicit transition states
|
||||
- centralize focus/open/close sequencing
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `ContentView` becomes mostly rendering + view intents
|
||||
|
||||
### Phase 5: Window Coordination
|
||||
|
||||
- extract AppKit window creation and frame management into `WindowCoordinator`
|
||||
- keep `ScreenManager` as thin glue or replace it entirely
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- no window-specific behavior in workspace logic
|
||||
|
||||
### Phase 6: Workspace Switching UX
|
||||
|
||||
- add workspace picker UI
|
||||
- support create/switch/rename/delete workspaces
|
||||
- enforce one-open-screen-per-workspace behavior
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- a user can assign the same workspace to multiple screens
|
||||
|
||||
### Phase 7: Popout and Session Cleanup
|
||||
|
||||
- formalize detached tab ownership
|
||||
- remove session observer leaks
|
||||
- remove app-global current-directory mutation
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- session lifetime is explicit and testable
|
||||
|
||||
### Phase 8: Test Expansion
|
||||
|
||||
- add unit coverage for all core models/controllers
|
||||
- add integration tests for orchestrators
|
||||
- add XCUITests for key user flows
|
||||
|
||||
## Proposed File Layout
|
||||
|
||||
```text
|
||||
Downterm/CommandNotch/
|
||||
App/
|
||||
CommandNotchApp.swift
|
||||
AppDelegate.swift
|
||||
AppController.swift
|
||||
|
||||
Core/
|
||||
Settings/
|
||||
AppSettings.swift
|
||||
AppSettingsController.swift
|
||||
AppSettingsStore.swift
|
||||
UserDefaultsAppSettingsStore.swift
|
||||
Workspaces/
|
||||
WorkspaceSummary.swift
|
||||
WorkspaceState.swift
|
||||
WorkspaceRegistry.swift
|
||||
WorkspaceController.swift
|
||||
WorkspaceStore.swift
|
||||
Screens/
|
||||
ScreenContext.swift
|
||||
ScreenRegistry.swift
|
||||
ScreenAssignmentStore.swift
|
||||
Notch/
|
||||
NotchOrchestrator.swift
|
||||
NotchStateMachine.swift
|
||||
NotchFrameCalculator.swift
|
||||
Terminal/
|
||||
TerminalSession.swift
|
||||
TerminalSessionFactory.swift
|
||||
TerminalTheme.swift
|
||||
TerminalTabState.swift
|
||||
Hotkeys/
|
||||
HotkeyService.swift
|
||||
HotkeyBinding.swift
|
||||
AppCommand.swift
|
||||
|
||||
UI/
|
||||
Notch/
|
||||
NotchRootView.swift
|
||||
ClosedNotchView.swift
|
||||
OpenWorkspaceView.swift
|
||||
TabStripView.swift
|
||||
WorkspacePickerView.swift
|
||||
Settings/
|
||||
SettingsRootView.swift
|
||||
GeneralSettingsView.swift
|
||||
AppearanceSettingsView.swift
|
||||
AnimationSettingsView.swift
|
||||
TerminalSettingsView.swift
|
||||
HotkeySettingsView.swift
|
||||
WorkspaceSettingsView.swift
|
||||
Components/
|
||||
SwiftTermView.swift
|
||||
HotkeyRecorderView.swift
|
||||
NotchShape.swift
|
||||
|
||||
AppKit/
|
||||
WindowCoordinator.swift
|
||||
NotchWindow.swift
|
||||
SettingsCoordinator.swift
|
||||
PopoutCoordinator.swift
|
||||
ScreenProvider.swift
|
||||
|
||||
Persistence/
|
||||
CodableStore.swift
|
||||
|
||||
Extensions/
|
||||
NSScreen+Extensions.swift
|
||||
```
|
||||
|
||||
## Protocol Boundaries Required For Tests
|
||||
|
||||
Create protocols around:
|
||||
|
||||
- settings store
|
||||
- workspace store
|
||||
- screen provider
|
||||
- window factory
|
||||
- hotkey registrar
|
||||
- launch-at-login service
|
||||
- terminal session factory
|
||||
- clock/scheduler for delayed hover/open/close logic
|
||||
|
||||
Examples:
|
||||
|
||||
```swift
|
||||
protocol ScreenProviderType {
|
||||
var screens: [ScreenDescriptor] { get }
|
||||
}
|
||||
|
||||
protocol WindowCoordinatorType {
|
||||
func showWindow(for screenID: ScreenID)
|
||||
func hideWindow(for screenID: ScreenID)
|
||||
func updateFrame(for screenID: ScreenID, frame: CGRect)
|
||||
func focusTerminal(for screenID: ScreenID)
|
||||
}
|
||||
|
||||
protocol SchedulerType {
|
||||
func schedule(after interval: TimeInterval, _ action: @escaping () -> Void) -> Cancellable
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
## 1. Unit Tests
|
||||
|
||||
Add a `CommandNotchTests` target.
|
||||
|
||||
Focus on pure logic:
|
||||
|
||||
- `WorkspaceRegistryTests`
|
||||
- creates default workspace
|
||||
- cannot delete last workspace
|
||||
- rename/delete/create behavior
|
||||
- `WorkspaceControllerTests`
|
||||
- new tab
|
||||
- close active tab
|
||||
- detach active tab
|
||||
- active tab index/id updates correctly
|
||||
- `ScreenRegistryTests`
|
||||
- new screen gets valid assignment
|
||||
- assignment changes persist
|
||||
- missing workspace reassigns to fallback
|
||||
- `NotchStateMachineTests`
|
||||
- closed -> opening -> open
|
||||
- close suppression while hovering
|
||||
- resizing transitions
|
||||
- `NotchFrameCalculatorTests`
|
||||
- clamping
|
||||
- per-screen max bounds
|
||||
- closed-notch size behavior
|
||||
- `HotkeyBindingTests`
|
||||
- event matching
|
||||
- preset digit mapping
|
||||
- `AppSettingsControllerTests`
|
||||
- only targeted changes publish
|
||||
|
||||
## 2. Integration Tests
|
||||
|
||||
Use fakes for AppKit boundaries.
|
||||
|
||||
- `NotchOrchestratorIntegrationTests`
|
||||
- opening a workspace on one screen closes it on another screen if shared
|
||||
- focus transfer rules
|
||||
- preset resize behavior
|
||||
- `AppControllerIntegrationTests`
|
||||
- hotkey command routes to active screen/workspace
|
||||
- settings changes update workspaces without broad side effects
|
||||
|
||||
## 3. UI Tests
|
||||
|
||||
Add a `CommandNotchUITests` target with XCUITest.
|
||||
|
||||
Recommended initial flows:
|
||||
|
||||
- launch app and open settings
|
||||
- create workspace from settings or notch UI
|
||||
- assign workspace to another screen context if testable
|
||||
- create tab
|
||||
- switch tabs
|
||||
- apply size preset
|
||||
- detach active tab
|
||||
|
||||
UI tests should validate behavior and existence, not exact pixel values.
|
||||
|
||||
## 4. Optional Snapshot Tests
|
||||
|
||||
Only add after state is injectable.
|
||||
|
||||
Snapshot candidates:
|
||||
|
||||
- closed notch
|
||||
- open notch with one tab
|
||||
- workspace picker
|
||||
- settings panes
|
||||
|
||||
Do not start here.
|
||||
|
||||
## Testing SwiftUI/AppKit UI
|
||||
|
||||
### What to test directly
|
||||
|
||||
- view model/controller behavior
|
||||
- user intents emitted by the view
|
||||
- XCUITest end-to-end flows
|
||||
|
||||
### What not to over-invest in early
|
||||
|
||||
- exact animation curves
|
||||
- fragile layout pixel assertions
|
||||
- direct testing of `NSWindow` internals unless wrapped behind protocols
|
||||
|
||||
### Practical answer
|
||||
|
||||
For this app, “testing UI” mostly means testing the logic that drives the UI, plus a thin XCUITest layer that proves the main flows work.
|
||||
|
||||
## Current Issues Mapped To Fixes
|
||||
|
||||
- Global singleton tab state
|
||||
Fix with `WorkspaceRegistry` + `WorkspaceController`
|
||||
- `UserDefaults` as runtime bus
|
||||
Fix with typed settings controller and targeted subscriptions
|
||||
- Dead gesture settings
|
||||
Remove or implement later behind a dedicated feature
|
||||
- Observer leakage in tab creation
|
||||
Store cancellables per session or derive titles from workspace state
|
||||
- App-global cwd mutation
|
||||
Remove from session startup; set process cwd explicitly if supported
|
||||
- Monolithic settings file
|
||||
Split into feature files bound to typed settings
|
||||
- Timing-based UI behavior spread across layers
|
||||
Centralize in `NotchOrchestrator`
|
||||
- No tests
|
||||
Add unit + integration + XCUITest targets
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Add `CommandNotchTests`
|
||||
- [ ] Add `CommandNotchUITests`
|
||||
- [ ] Create `AppSettings` model
|
||||
- [ ] Create `AppSettingsStore`
|
||||
- [ ] Replace broad defaults observation
|
||||
- [ ] Create `WorkspaceRegistry`
|
||||
- [ ] Create `WorkspaceController`
|
||||
- [ ] Move tab logic out of `TerminalManager`
|
||||
- [ ] Create `ScreenContext`
|
||||
- [ ] Create `ScreenRegistry`
|
||||
- [ ] Persist screen assignments
|
||||
- [ ] Create `NotchOrchestrator`
|
||||
- [ ] Create `NotchFrameCalculator`
|
||||
- [ ] Extract `WindowCoordinator`
|
||||
- [ ] Add workspace picker UI
|
||||
- [ ] Add create/switch/rename/delete workspace flows
|
||||
- [ ] Enforce one-open-screen-per-workspace
|
||||
- [ ] Clean up detached session ownership
|
||||
- [ ] Split settings views into separate files
|
||||
- [ ] Remove dead settings and unused code
|
||||
- [ ] Add unit tests for registry/controller/state machine/frame calculator
|
||||
- [ ] Add initial XCUITest flows
|
||||
|
||||
## First Concrete Refactor Slice
|
||||
|
||||
If implementing incrementally, the best first slice is:
|
||||
|
||||
1. add tests target
|
||||
2. add `AppSettings`
|
||||
3. add `WorkspaceRegistry` with a single default workspace
|
||||
4. migrate current `TerminalManager` into a first `WorkspaceController`
|
||||
5. change the app so all current behavior still uses one workspace
|
||||
|
||||
That gives immediate architectural improvement without changing UX yet.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
The refactor is successful when:
|
||||
|
||||
- there is no critical runtime logic driven by broad `UserDefaults` notifications
|
||||
- tabs/sessions live under workspaces, not globally
|
||||
- screens are explicitly assigned to workspaces
|
||||
- the same workspace can be selected on multiple screens
|
||||
- only one open/focused screen owns a workspace’s live terminal at a time
|
||||
- window behavior is separated from domain logic
|
||||
- unit tests cover the core state machines and registries
|
||||
- XCUITests cover main user flows
|
||||
|
||||
Reference in New Issue
Block a user