Standalone Terminal Window
Overview
A standalone terminal window with a session sidebar and terminal view — distinct from the project window's embedded terminal. This is the primary window for non-project terminal usage. The window uses an HSplitView with a session list on the left and a terminal view on the right, and creates its own independent SessionManager instance. It shares the same terminal-pane spec for terminal behavior (PTY sessions, session list, terminal rendering, profiles) but operates as a completely independent instance with no session sharing between windows.
Terminology
| Term | Definition |
|---|---|
| Standalone terminal window | A top-level window containing only a session sidebar and terminal view, not embedded in a project window |
| Session sidebar | The left panel listing all terminal sessions owned by this window's session manager |
| Session manager | A per-window controller owning an ordered list of sessions — see ui/Recipes/terminal-pane.md per-window-manager |
| Terminal view | The rendering surface for the selected session — see ui/Recipes/terminal-pane.md nsview-representable through palette-color-structure |
| Active profile | The currently selected color profile applied to terminal rendering — see ui/color-profile.md single-active-profile |
| Window scene | A SwiftUI WindowGroup identified by a string, used to open and manage window instances |
Behavioral Requirements
Window structure
- window-group-terminal: The window MUST use a
WindowGroup(id: "terminal")scene declaration. - hsplit-sidebar-terminal: The window MUST use an HSplitView with two sections: session list sidebar (left) and terminal view (right).
- sidebar-width-range: The session list sidebar MUST have a width between 150pt and 200pt.
- persist-window-frame: The window frame MUST be persisted using the window-frame-persistence component (as defined in
ui/window-frame-persistence.md). The autosave name MUST be"terminal-window". - min-size-constraints: The window MUST enforce minimum size constraints: minWidth 600pt, minHeight 400pt.
Session management
- own-session-manager: The window MUST create its own
SessionManagerinstance as a@StateObject. This instance MUST be independent from any project window's session manager. - auto-create-default: On window appear (
onAppear), if the session manager contains no sessions, a default session MUST be created automatically by callingaddSession(). - terminate-on-close: On window close, all sessions MUST be terminated by calling
terminateAll()on the session manager. - focused-object-dispatch: The session manager MUST be provided as
.focusedObject()so that menu commands (New Session, Close Session, etc.) can dispatch to the correct window's session manager.
Terminal view and profile
- apply-active-profile: The terminal view MUST apply the active color profile (colors, font, cursor style) from
TerminalProfile.activeProfile(). - global-profile-storage: The active profile ID MUST be read from
@AppStorage. This is a global setting shared across all terminal windows. - profile-fallback-default: If the stored active profile ID is invalid (not found among available profiles), the window MUST fall back to the first built-in profile (Solarized Dark), as specified in
ui/color-profile.mdfallback-to-default.
Relationship to project window
- shared-terminal-spec: The standalone terminal window and the project window's embedded terminal pane MUST share the same terminal-pane spec (
ui/Recipes/terminal-pane.md) for all terminal behavior — PTY lifecycle, session management, terminal rendering, OSC handling, and session list display. - independent-sessions: Each standalone terminal window MUST have its own
SessionManagerinstance. Sessions MUST NOT be shared between standalone terminal windows or between standalone terminal windows and project windows.
Delegation to sub-components
- delegate-session-list: The session list sidebar MUST delegate to terminal-pane.md sidebar-session-list through row-context-menu for session list behavior (row display, selection binding, add button, context menu).
- delegate-terminal-view: The terminal view MUST delegate to terminal-pane.md nsview-representable through palette-color-structure for terminal rendering, reparenting, and profile application.
- delegate-empty-state: The empty state MUST delegate to terminal-pane.md empty-state-no-sessions for display when no sessions exist.
Appearance
Window layout
┌──────────────┬─────────────────────────────────────────┐
│ Sessions [+] │ │
├──────────────┤ │
│ │ │
│ ● Session 1 │ user@host ~ % │
│ ~/projects │ ls -la │
│ main │ total 42 │
│ zsh │ drwxr-xr-x 5 user staff 160 ... │
│ │ -rw-r--r-- 1 user staff 230 ... │
│ ○ Session 2 │ │
│ ~/docs │ │
│ bash │ │
│ │ │
│ │ │
│ │ │
└──────────────┴─────────────────────────────────────────┘
- Window minimum size: 600 x 400pt
- Session sidebar width: 150–200pt, resizable within that range
- Terminal view: Fills remaining width
- Terminal background: Determined by active color profile
- Terminal font: Determined by active color profile (monospaced)
- Sidebar appearance: Standard sidebar material, matching the terminal-pane session list style
States
| State | Behavior |
|---|---|
| Window opened, no sessions | Default session created automatically on appear; terminal view shows shell prompt |
| One or more sessions, one selected | Selected session's terminal view reparented into container; sidebar highlights selected row |
| Session added | New session appended, selected, terminal view shown |
| Session removed | PTY terminated, smart selection applied (previous > next > nil per terminal-pane remove-smart-select) |
| All sessions removed | Empty state displayed (per terminal-pane empty-state-no-sessions); next session creation re-populates |
| Profile changed | Colors/font applied to terminal view without reparenting |
| Profile deleted while in use | Falls back to Solarized Dark (per color-profile fallback-to-default) |
| Window closing | terminateAll() called; all PTYs cleaned up |
| Multiple standalone windows open | Each window operates independently with its own session manager |
Accessibility
- inherit-pane-accessibility: The standalone terminal window MUST inherit all accessibility requirements from the terminal-pane spec (keyboard-nav-sessions through terminated-announce), including keyboard-navigable session list, accessible labels, VoiceOver support, and screen reader announcements.
- accessible-window-title: The window MUST have an accessible window title that distinguishes it from project windows (e.g., "Terminal" or "Terminal — Session Name").
Conformance Test Vectors
| ID | Requirements | Input | Expected |
|---|---|---|---|
| stw-001 | window-group-terminal | Inspect SwiftUI scene declaration | WindowGroup(id: "terminal") is registered |
| stw-002 | hsplit-sidebar-terminal | Open a standalone terminal window | HSplitView renders with session sidebar (left) and terminal view (right) |
| stw-003 | sidebar-width-range | Inspect session sidebar width | Width is between 150pt and 200pt |
| stw-004 | persist-window-frame | Open terminal window, move to (300, 200), close, reopen | Window restores at (300, 200); autosave name is "terminal-window" |
| stw-005 | min-size-constraints | Attempt to resize window below 600x400 | Window enforces minimum size constraints |
| stw-006 | own-session-manager | Open a standalone terminal window and a project window | Each has its own SessionManager instance; adding a session in one does not affect the other |
| stw-007 | auto-create-default | Open a standalone terminal window for the first time | A default session ("Session 1") is created automatically; terminal shows shell prompt |
| stw-008 | terminate-on-close | Open window with 3 sessions, close window | All 3 PTYs terminated |
| stw-009 | focused-object-dispatch | Open two standalone terminal windows, focus window 1, invoke "New Session" menu | Session created in window 1's session manager only (via focusedObject dispatch) |
| stw-010 | apply-active-profile, global-profile-storage | Set active profile to Dracula, open terminal window | Terminal renders with Dracula colors (#282a36 background, #f8f8f2 foreground) |
| stw-011 | profile-fallback-default | Set active profile ID in AppStorage to an invalid UUID, open terminal window | Terminal falls back to Solarized Dark (#002b36 background, #839496 foreground) |
| stw-012 | shared-terminal-spec | Compare terminal behavior in standalone window vs. project window terminal pane | Identical PTY lifecycle, OSC handling, session list, and rendering behavior |
| stw-013 | independent-sessions | Open two standalone terminal windows, create sessions in each | Sessions are independent; removing a session in window A does not affect window B |
| stw-014 | auto-create-default | Open window, remove all sessions, no auto-creation on removal | Empty state displayed; auto-creation only happens on initial appear |
| stw-015 | delegate-session-list, delegate-terminal-view | Open window, create multiple sessions, switch between them | Session list displays rows per terminal-pane spec; reparenting preserves scrollback |
| stw-016 | accessible-window-title | Enable VoiceOver, open terminal window | Window title announced as "Terminal" (or similar), distinguishable from project windows |
Edge Cases
- Last session closed by user: When the user closes the last session, the empty state is displayed. A new session is NOT automatically created — auto-creation only occurs on initial
onAppearwhen the session list is empty. The user must click the "+" button or "New Session" to create a new session. - Profile deleted while in use: If the active profile is a custom profile that gets deleted while a standalone terminal window is open, the window MUST fall back to Solarized Dark immediately (per color-profile fallback-to-default). Terminal colors update without reparenting.
- Multiple standalone terminal windows: Each window has its own
SessionManagerinstance. Opening N standalone terminal windows results in N independent session managers. Menu commands dispatch to the focused window's session manager via.focusedObject(). - Standalone window and project window open simultaneously: Both function independently. Changing the active color profile affects all terminal views across both window types (since profile ID is stored in
@AppStorage, a global setting). - Window restored after crash: Session manager MUST NOT attempt to restore PTY sessions from a previous run. Sessions are ephemeral. On relaunch, the window opens with no sessions, and the
onAppearauto-creation logic creates a fresh default session. - Rapid window open/close:
terminateAll()MUST complete cleanly. PTY file descriptors MUST be closed. No zombie processes should remain. - Window opened with no shell available: Falls back to
/bin/zshper terminal-pane edge case (shell not found). The standalone terminal window does not add additional fallback logic beyond what terminal-pane provides. - Very many sessions in one window (50+): Session list MUST remain scrollable and performant (delegated to terminal-pane edge case handling).
- Frame persistence for multiple standalone windows: All standalone terminal windows share the autosave name
"terminal-window". This means only one window's frame is persisted. If multiple standalone windows are needed with independent frame persistence, a future revision MAY introduce per-window identifiers. - visionOS window placement: On visionOS, the system manages window placement. Frame persistence (persist-window-frame) is a no-op on visionOS. Minimum size constraints still apply.
- focusedObject not set: If menu commands fire before any standalone terminal window is focused, the system's
FocusedValueswill not contain a session manager. Menu commands MUST be disabled when no session manager is available in the focused values.
Logging
Subsystem: {{bundle_id}} | Category: StandaloneTerminalWindow
| Event | Level | Message |
|---|---|---|
| Window opened | info | StandaloneTerminalWindow: opened |
| Window closed | info | StandaloneTerminalWindow: closed |
| Default session created | debug | StandaloneTerminalWindow: created default session on appear |
| All sessions terminated | debug | StandaloneTerminalWindow: all sessions terminated (window closing) |
| Profile applied | debug | StandaloneTerminalWindow: applied profile "{{profileName}}" ({{profileId}}) |
| Profile fallback | debug | StandaloneTerminalWindow: invalid active profile ID, falling back to Solarized Dark |
| Focused object set | debug | StandaloneTerminalWindow: session manager set as focusedObject |
| Frame autosave set | debug | StandaloneTerminalWindow: frame autosave name "terminal-window" |
Platform Notes
- SwiftUI (macOS): Declare the window scene as
WindowGroup(id: "terminal") { StandaloneTerminalView() }. InsideStandaloneTerminalView, create a@StateObject var sessionManager = SessionManager(). UseHSplitViewwith the session list sidebar (per terminal-pane sidebar-session-list through row-context-menu) on the left and the terminal view (per terminal-pane nsview-representable through palette-color-structure) on the right. Apply.frame(minWidth: 150, maxWidth: 200)on the sidebar and.frame(maxWidth: .infinity)on the terminal view. Set.frame(minWidth: 600, minHeight: 400)on the window content. Attach.background(WindowAccessor(name: "terminal-window", onClose: { sessionManager.terminateAll() }))for frame persistence and close handling. Publish the session manager via.focusedObject(sessionManager)so menu commands dispatch correctly. Read the active profile ID from@AppStorage("activeProfileId")and resolve the profile viaTerminalProfile.activeProfile()with Solarized Dark fallback. OnonAppear, checksessionManager.sessions.isEmptyand callsessionManager.addSession()if true. - SwiftUI (visionOS): Same scene and view structure as macOS. The window opens as a standard visionOS window volume.
HSplitViewrenders within the window. Frame persistence is not applicable — visionOS manages window placement. Minimum size constraints are respected by the system. Session sidebar may useNavigationSplitViewwith the session list in the sidebar column for better visionOS integration, as noted in terminal-pane platform notes.
Privacy
- Data collected: Terminal output is rendered in-memory by SwiftTerm. No terminal content is stored to disk.
- Storage: Active profile ID stored in
@AppStorage(UserDefaults). Window frame position stored viaNSWindow.setFrameAutosaveNameon macOS. - Transmission: None — terminal content never leaves the device.
- Retention: Session data exists only for the lifetime of the window. Profile preference persists until changed. Frame position persists until changed or app is uninstalled.
Design Decisions
None yet — decisions made during implementation should be recorded here.