Skip to content

Commit 213cc65

Browse files
feat(desktop): terminal persistence via daemon process (#619)
* feat(desktop): terminal persistence via daemon process Add terminal session persistence using a background daemon process that survives app restarts. Terminals can now be resumed with full scrollback and TUI state (like Claude Code) exactly where they left off. Key changes: - New terminal host daemon with per-session PTY subprocesses - DaemonTerminalManager as drop-in replacement for TerminalManager - Settings toggle for terminal persistence (requires app restart) - Schema migration for terminal_persistence setting - Smooth workspace/tab switching via CSS visibility (avoids remount) * fix(desktop): add @xterm/headless dependency and fix type error - Add missing @xterm/headless package to desktop dependencies - Use @ts-expect-error for known xterm addon type mismatch (SerializeAddon types expect @xterm/xterm but works with @xterm/headless) * fix(desktop): address CodeRabbit review feedback - Await async handler in terminal-host/index.ts to prevent unhandled promise rejections - Track session cleanup timeouts in daemon-manager.ts and clear on dispose to prevent memory leaks - Move headless-emulator.test.ts to co-locate with implementation (from __tests__/ subfolder) - Fix timeout race conditions in test files by tracking settlement state and clearing timeouts * fix(desktop): fix CI errors after test file move - Update relative imports in headless-emulator.test.ts after moving from __tests__/ - Externalize @xterm/* packages in Vite config (incorrect package exports for bundlers) * refactor(desktop): centralize DEFAULT_TERMINAL_PERSISTENCE constant Extract the terminal persistence default value (false) into a shared constant to avoid duplicating magic values across the codebase. * refactor(desktop): address PR review comments - Add SUPERSET_TERMINAL_DEBUG env var for conditional debug logging - Wrap verbose console.log statements in debug checks (terminal router and terminal host client) - Make sendRequest<T> generic to eliminate type casts in public API - Use switch statement for event payload narrowing - Remove unused imports (IpcSuccessResponse, IpcErrorResponse) * fix(desktop): align @xterm/headless version with @xterm/xterm Downgrade @xterm/headless from ^6.0.0 to ^5.5.0 to match @xterm/xterm version. Mismatched major versions can cause runtime failures. Also removes now-unnecessary @ts-expect-error directive since matching versions resolve the type compatibility issue. * fix(desktop): address code review feedback for terminal persistence P0 Fixes: - Use getActiveTerminalManager() instead of direct terminalManager import in workspaces.ts, projects.ts, and main/windows/main.ts - Add missing await for async getSessionCountByWorkspaceId() call - Update reconcileOnStartup() to preserve sessions for true app restart persistence, only killing orphaned sessions (deleted workspaces) P1 Fixes: - Call shutdownOrphanedDaemon() on startup when persistence is disabled - Fix error code mismatch: QUEUE_FULL -> WRITE_QUEUE_FULL in client.ts P2 Fixes: - Only keep terminal-containing tabs mounted when persistence enabled, non-terminal tabs use normal unmount behavior to save memory Additional Fixes (from runtime testing): - Fix resize race condition: forward resize to daemon regardless of local session cache state (handles startup race) - Add safety checks in initHistoryWriter: validate scrollback is string and cap at 512KB to prevent RangeError: Invalid array length * fix(desktop): guard pane updates against deleted panes When async processes like Claude Code still hold pane references after the terminal is closed, calling setNeedsAttention (or similar) would create an undefined entry in the panes record. This caused GroupStrip to crash when iterating Object.values(panes). Now all pane update functions check if the pane exists first and return early (no-op) if it doesn't, preventing undefined entries. * fix(desktop): split terminal host control/stream sockets Prevents createOrAttach timeouts by removing head-of-line blocking when terminal output backpressures. Adds protocol v2 hello with clientId/role, pairs control+stream sockets, and adds a backpressure isolation integration test. * chore(desktop): biome format + archive execplan Formats a few files to satisfy biome format checks and moves the terminal host dual-socket ExecPlan to apps/desktop/plans/done/. * fix(desktop): spawn daemon when token missing Read terminal-host auth token after ensuring a daemon exists; if token is missing with a live socket, restart the daemon to re-create a coherent socket+token pair. * fix(desktop): harden terminal persistence data perms Ensure ~/.superset* is created/repair-chmodded to 0700 and history/log files are written with 0600 where applicable. Also closes the daemon.log fd in the parent after spawning the daemon. * fix(desktop): address terminal persistence review feedback - Keep terminal tabs mounted even when switching to empty workspaces - Gate noisy logs behind SUPERSET_TERMINAL_DEBUG - Use TRPCError for settings validation - Fix history reinit sizing + avoid silent catch blocks - Gate PTY subprocess spawn logging * fix(desktop): bundle @XTerm packages for terminal-host daemon The daemon runs as standalone Node.js outside app.asar and needs @xterm/headless and @xterm/addon-serialize bundled to function. * wip: dx hardening plan * fix(desktop): remove obsolete setNeedsAttention after rebase PR #588 replaced needsAttention with PaneStatus (idle/working/permission/review). Remove the obsolete setNeedsAttention method and update plan doc references. * docs(desktop): update terminal persistence exec plan * fix(desktop): harden terminal persistence DX * fix(desktop): prevent history init buffer loop * fix(desktop): enable terminal stream when snapshot empty * fix(desktop): prevent scheduler deadlock on React StrictMode unmount React StrictMode simulates mount → unmount → mount cycles. The terminal attach scheduler was deadlocking because: 1. First mount starts a task (inFlight++) 2. Unmount cancels the task but it's still executing 3. Second mount queues a new task 4. tRPC callbacks for unmounted components don't fire reliably 5. done() never gets called → inFlight stays stuck at MAX_CONCURRENT Fix: Track running tasks per paneId and immediately decrement inFlight when canceling a running task. Also add optional debug logging (enable via localStorage.setItem('SUPERSET_TERMINAL_DEBUG', '1')). * fix(desktop): address PR review blocking issues - P0: Fix attach-scheduler race condition where inFlight counter could be double-decremented when cancel() and done() both fire for the same task. Added `released` flag to ensure idempotent completion. - P1: Fix sendRequestOnStream NDJSON parsing bug that dropped messages arriving in the same TCP read as the hello response. Now feeds remainder data to streamParser after parsing first response. - P1: Fix maybeApplyInitialState catch block that would wedge terminal on restoration error. Now fail-open by setting isStreamReady and flushing pending events even on error. - P2: Fix coldRestoreState memory leak by cleaning up on unmount. Previously scrollback (potentially MBs per pane) was only cleared on "Start Shell" click, not on component unmount. * fix(desktop): address oracle feedback on PR fixes - Move coldRestoreState cleanup into detachTimeout to preserve StrictMode unmount/remount semantics. The module-level Map is specifically designed to survive quick remounts, so deleting immediately on unmount was wrong. - Harden attach-scheduler cancel path: when a running task is canceled, re-queue any waiting task for the same paneId. This mirrors the done() behavior and protects against the "done never fires" scenario. * docs(desktop): document ordering assumption in sendRequestOnStream Per oracle review suggestion: add JSDoc comment explaining that the daemon's hello handler guarantees response is first frame. Documents when this assumption would need to change (if daemon ever emits events before hello response). * fix(desktop): resolve type errors after rebase onto main - Add stub terminal-history module (real impl in Phase 4) - Fix port-manager: add checkOutputForHint method, await async calls - Fix TabsContent: add panes store selector - Fix TabView: add Pane type import - Fix daemon-manager: handle null scrollback from stub reader * fix(desktop): implement daemon signal() support for SIGINT/SIGTERM The daemon's signal() method was a no-op, which meant Ctrl+C through the daemon pathway would silently fail. This adds full signal support: - Add SignalRequest type and signal to RequestTypeMap - Add Signal IPC frame type (6) distinct from Kill - Implement handleSignal in pty-subprocess without kill escalation - Add sendSignal chain through session → terminal-host → daemon → client - Update daemon-manager to use client.signal() instead of no-op Unlike kill(), signal() does not mark the session as terminating and does not escalate to SIGKILL, allowing the process to continue running. * docs(desktop): add terminal host event semantics documentation Documents the event delivery model for the daemon protocol: - Event types (data, exit, error) - Dual-socket model (control vs stream) - At-most-once delivery semantics (no durability/retries) - In-order guarantees within sessions - Multi-level backpressure handling - Error codes and race condition handling * feat(desktop): implement cold restore terminal history persistence Replaces the stub terminal-history.ts with a working implementation for Phase 4 of terminal persistence. Enables terminal recovery after app/system restarts when the daemon is not running. Storage format: - scrollback.bin: Raw PTY output (append-only) - meta.json: Session metadata (cols, rows, cwd, timestamps) Cold restore detection: - meta.json without endedAt → unclean shutdown → can restore - meta.json with endedAt → clean shutdown → no restore HistoryWriter API: - init(initialScrollback?) - create directory and files - write(data) - append PTY output - flush() - flush pending writes - close(exitCode?) - write endedAt to meta.json - reinitialize() - reset for clear scrollback - deleteHistory() - remove all files HistoryReader API: - exists() - check if history available - readMetadata() - get cols/rows/cwd/endedAt - readScrollback() - get terminal content - cleanup() - delete history files * feat(desktop): add telemetry for terminal persistence events Adds tracking for key terminal persistence lifecycle events: - terminal_cold_restored: Triggered when recovering terminal after reboot with scrollback_bytes to measure restoration payload size - terminal_warm_attached: Triggered when reconnecting to existing daemon session with snapshot_bytes for payload metrics - terminal_daemon_disconnected: Triggered on daemon connection loss with active_session_count for impact assessment * fix(desktop): trigger cold restore on daemon session loss When daemon restarts and loses sessions, "Session not found" errors were treated as non-fatal (just showed toast). This prevented the retry UI from appearing and cold restore from triggering. Changes: - Promote "Session not found" WRITE_FAILED errors to connection error so retry UI appears instead of endless toast spam - Handle isColdRestore in handleRetryConnection so clicking retry can trigger cold restore if disk history is available - Update both event handler locations for consistency Now when daemon dies and user clicks retry, cold restore kicks in if history exists on disk (meta.json without endedAt). * fix(desktop): suppress toast when showing retry UI for session loss When daemon restarts and loses terminal sessions, show only the retry UI without also showing a toast notification. This prevents confusing UX where both a toast and the retry overlay appear simultaneously. * fix(desktop): suppress toast for transient PTY not spawned errors During daemon recovery, writes may arrive before the PTY subprocess is fully initialized. Treat "PTY not spawned" as a transient error that doesn't need a toast notification - just log to terminal. * fix(desktop): clear connection error on successful initial attach When daemon restarts and component remounts, the background createOrAttach may succeed while the error overlay is still visible. Clear connectionError on success to dismiss the overlay automatically. * fix(desktop): trigger cold restore on daemon session loss When "Session not found" error occurs, clear the stale cache entry so the next createOrAttach properly checks disk history and triggers cold restore instead of creating a new session. * fix(desktop): re-focus terminal after successful retry connection After clicking "Retry Connection" and the connection succeeds, re-focus the terminal so keyboard input works immediately. Skip focus for cold restore since user needs to click overlay button. * fix(desktop): cold restore for TUI apps with empty scrollback Two fixes: 1. Check rawScrollback === null instead of !rawScrollback. TUI apps in alternate screen may have empty normal buffer, which is still valid for cold restore (empty string is truthy check fix). 2. Use fresh xterm ref in handleRetryConnection onSuccess callback to handle potential component remount during async operation. * fix(desktop): focus terminal after clicking Start Shell After cold restore, clicking "Start Shell" creates a new session but wasn't focusing the terminal, causing keystrokes to go elsewhere. * fix(desktop): keep terminal stream alive on exit * chore(desktop): fix biome check * chore(desktop): fix lint warnings * fix(desktop): address persistence review blockers * fix(desktop): harden terminal history caps * docs(desktop): add terminal runtime abstraction plan * docs(desktop): expand plan for Terminal.tsx decomposition * docs(desktop): add target architecture snippets to plan * docs(desktop): refine terminal runtime abstraction plan * docs(desktop): add remote runner notes to plan * docs(desktop): align terminal runtime plan with cloud provider direction * docs(desktop): add terminal runtime architecture review packet * docs(desktop): narrow changes router reference list * docs(desktop): incorporate architecture feedback into runtime rewrite plan * refactor(desktop): introduce WorkspaceRuntime abstraction Adds a provider-neutral runtime layer that abstracts terminal backend selection: - WorkspaceRuntimeRegistry: process-scoped registry for runtime selection - LocalTerminalRuntime: adapts TerminalManager/DaemonTerminalManager - Capability-based checks: uses `terminal.management !== null` instead of `instanceof DaemonTerminalManager` Key changes: - New workspace-runtime module with types, registry, and local implementation - tRPC terminal router migrated to use registry pattern - All call sites updated to use getForWorkspaceId when workspaceId is in-hand - Regression tests for capability presence and stream contract This foundation enables future cloud workspace providers without spreading backend-specific branching throughout the codebase. * WIP: route-based settings/dashboard structure alignment with upstream - Add _dashboard route group with workspace/tasks pages - Rebuild settings routing/layout/sidebar under routes - Port settings pages: account, appearance, keyboard, presets, team, ringtones, project, workspace - Add terminal settings page at /settings/terminal - Update navigation components to use router instead of app-state - Remove obsolete route pages that conflict with upstream _dashboard structure Still needs: - Full merge with origin/main - Complete app-state removal - Terminal backend conflict resolution * fix(desktop): improve terminal kill-all reliability and UI feedback Fixes two bugs in the terminal settings page: 1. UI not refreshing after kill: Added optimistic UI clearing - Clear session list immediately on click for instant feedback - Rollback on error, delayed refetch on success 2. Sessions surviving kill: Added wait/verify loop in backend - Poll daemon up to 10x100ms until sessions are actually dead - Return accurate killedCount and remainingCount - Show warning toast if some sessions survive Also adds diagnostic logging for troubleshooting kill operations. * fix(desktop): keep killed terminals dead * fix(desktop): disable terminal session actions when none * fix(desktop): memoize terminal session lists * remove old code * fix(desktop): log best-effort failures in terminal utils * fix: harden terminal persistence paths and logs * chore: format terminal router and client
1 parent b2be236 commit 213cc65

65 files changed

Lines changed: 16173 additions & 249 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Terminal Host Event Semantics
2+
3+
This document describes the event delivery model for the Terminal Host daemon protocol.
4+
5+
## Event Types
6+
7+
The daemon emits three event types to attached clients:
8+
9+
| Event | Payload | Description |
10+
|---------|-------------------------------------|------------------------------------------|
11+
| `data` | `{ type: "data", data: string }` | PTY output (terminal content) |
12+
| `exit` | `{ type: "exit", exitCode, signal?}` | PTY process terminated |
13+
| `error` | `{ type: "error", error, code? }` | Error condition (e.g., write queue full) |
14+
15+
## Socket Model
16+
17+
Clients connect with two sockets sharing a `clientId`:
18+
19+
- **Control socket** (`role: "control"`): RPC request/response (write, resize, kill, etc.)
20+
- **Stream socket** (`role: "stream"`): Receives unsolicited events
21+
22+
Events are broadcast only to stream sockets. This separation prevents event floods from blocking RPC responses.
23+
24+
## Delivery Semantics
25+
26+
### At-Most-Once Delivery
27+
28+
Events are delivered **at-most-once** per attached client:
29+
- No acknowledgment or retry mechanism
30+
- If a client socket buffer is full, data is queued but may be lost on disconnect
31+
- Clients must be prepared to miss events (especially `data` during reconnection)
32+
33+
### No Durability
34+
35+
Events are not persisted. If no clients are attached, events are emitted but not stored.
36+
For cold restore, use `createOrAttach` which returns a `TerminalSnapshot` containing the current screen state.
37+
38+
## Ordering Guarantees
39+
40+
### Within a Session
41+
42+
Events for a single session are delivered **in-order** relative to each other:
43+
1. PTY output order is preserved (data events arrive in the order produced)
44+
2. Exit event is always delivered after all data events for that session
45+
3. Error events may interleave with data events
46+
47+
### Across Sessions
48+
49+
No ordering guarantees across different sessions. Events from session A and session B may interleave arbitrarily.
50+
51+
## Backpressure Handling
52+
53+
The system implements multi-level backpressure to prevent memory exhaustion:
54+
55+
### Level 1: Client Socket Backpressure
56+
```
57+
Client socket buffer full
58+
→ Session pauses subprocess stdout reads
59+
→ Subprocess backpressures PTY reads
60+
→ PTY write buffer fills → kernel blocks PTY writes
61+
```
62+
63+
When the client drains its buffer, the chain resumes.
64+
65+
### Level 2: Subprocess stdin Backpressure
66+
```
67+
Write requests exceed MAX_SUBPROCESS_STDIN_QUEUE_BYTES (2MB)
68+
→ Frame dropped
69+
→ Error event emitted: { code: "WRITE_QUEUE_FULL" }
70+
```
71+
72+
### Level 3: PTY Write Backpressure (in subprocess)
73+
```
74+
PTY kernel buffer full (EAGAIN/EWOULDBLOCK)
75+
→ Exponential backoff retry (2ms → 50ms)
76+
→ Write queue accumulates up to 64MB hard limit
77+
→ Beyond limit: frames dropped, error reported
78+
```
79+
80+
## Error Codes
81+
82+
| Code | Meaning |
83+
|---------------------|----------------------------------------------|
84+
| `WRITE_QUEUE_FULL` | Input queue exceeded limit, data dropped |
85+
| `SUBPROCESS_ERROR` | PTY subprocess reported an error |
86+
| `WRITE_FAILED` | Failed to write to PTY |
87+
| `UNKNOWN` | Unclassified error |
88+
89+
## Race Conditions
90+
91+
### Kill vs Attach Race
92+
93+
Sessions track `terminatingAt` timestamp when `kill()` is called. The `isAttachable` property returns false for terminating sessions, preventing new attachments to sessions about to exit.
94+
95+
### Data vs Exit Race
96+
97+
The subprocess flushes all buffered output before sending the exit frame, so clients receive all terminal output before the exit event.
98+
99+
## Renderer Integration Notes (tRPC)
100+
101+
The renderer does **not** talk to the daemon directly. It consumes terminal output via the `terminal.stream` tRPC subscription (`apps/desktop/src/lib/trpc/routers/terminal/terminal.ts`), which bridges the main-process `TerminalManager`/`DaemonTerminalManager` EventEmitter.
102+
103+
### `exit` must not complete the subscription
104+
105+
Treat `exit` as a **state transition**, not a terminal end-of-stream:
106+
107+
- The renderer subscribes with a stable `paneId` input (`trpc.terminal.stream.useSubscription(paneId)`).
108+
- `@trpc/react-query` does **not** auto-resubscribe after a subscription completes unless the input/key changes.
109+
- We reuse the same `paneId` across restarts / cold restore (new session, same pane).
110+
111+
So the server-side observable must **not** call `emit.complete()` on `exit`, otherwise the pane becomes permanently detached from output (`listeners=0` in `DaemonTerminalManager` logs) even after a new shell is started.
112+
113+
### Cold restore overlay: drop stale queued events
114+
115+
During cold restore, the renderer intentionally pauses streaming (`isStreamReady=false`) while showing a read-only overlay. Stream events can be queued during this period. Before starting a new shell, the renderer should discard any queued events from the pre-restore session (especially stale `exit`) so they can't mark the new session as exited and trigger an unintended `restartTerminal()` (which clears the UI).
116+
117+
## Usage Example
118+
119+
```typescript
120+
// Stream socket receives events as NDJSON
121+
socket.on("data", (chunk) => {
122+
for (const line of chunk.toString().split("\n").filter(Boolean)) {
123+
const event = JSON.parse(line) as IpcEvent;
124+
if (event.type !== "event") continue;
125+
126+
switch (event.event) {
127+
case "data":
128+
terminal.write(event.payload.data);
129+
break;
130+
case "exit":
131+
console.log(`Session ${event.sessionId} exited: ${event.payload.exitCode}`);
132+
break;
133+
case "error":
134+
console.error(`Error in ${event.sessionId}: ${event.payload.error}`);
135+
break;
136+
}
137+
}
138+
});
139+
```
140+
141+
## Related Files
142+
143+
- `types.ts` - Event type definitions
144+
- `session.ts` - Event emission and backpressure logic
145+
- `pty-subprocess.ts` - PTY-level backpressure handling
146+
- `client.ts` - Client-side event handling
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Architecture Review Packet: Terminal Runtime + Future Remote Runners
2+
3+
This doc is intended for an external architecture review. It provides enough context to understand the problem space and asks open-ended questions to help critique our current direction.
4+
5+
**How to use this:** please read the plan first, then use the questions below as prompts. Feel free to ignore our current approach and propose a better one — we’re explicitly trying to avoid narrowing you into our hypotheses.
6+
7+
## What we’re trying to build (big picture)
8+
9+
Superset Desktop is an Electron app that provides:
10+
11+
- A multi-pane terminal UI inside workspaces (think “IDE terminal panes”).
12+
- Git worktree-based workspaces (multiple isolated working copies).
13+
- “Changes” UX (diff/status/staging) tied to those workspaces.
14+
- Agent/CLI integrations that surface lifecycle/status in the UI (e.g. completion events, indicators).
15+
16+
Today, terminals can run locally and (optionally) persist via a background “terminal host” daemon. In the future, we want to support executing terminals in the cloud / on a remote runner while keeping the same “Superset UX primitives” (worktrees, changes/diff, agent status, etc.).
17+
18+
## Why we’re asking for review now
19+
20+
We have a working implementation of terminal persistence, but it adds a lot of complexity and “mode branching” (daemon vs in-process) across layers (main process, tRPC router, renderer).
21+
22+
We’re planning a rewrite/refactor to:
23+
24+
- Centralize backend selection (so most code is backend-agnostic).
25+
- Preserve current behavior (especially around session streaming, attach/detach, and restore).
26+
- Create a foundation that won’t fight us when we introduce remote runners/cloud terminals.
27+
28+
## Current state (high-level)
29+
30+
- Electron main process owns terminal backends:
31+
- **In-process backend:** PTYs owned directly in main process.
32+
- **Daemon backend:** PTYs owned by a separate “terminal host” process; main connects via a local socket.
33+
- Renderer talks to main via tRPC (IPC), including a terminal stream subscription.
34+
- Terminals have “attach/detach” semantics and “cold restore” (disk-backed scrollback restore) for daemon persistence.
35+
36+
## Known constraints (technical + product)
37+
38+
These are constraints we currently operate under; if you think any should change, call it out.
39+
40+
- Renderer must not import Node.js modules (browser environment).
41+
- IPC is via tRPC, and subscriptions must use an observable pattern (not async generators).
42+
- The terminal UI must remain responsive under high output (performance/backpressure matters).
43+
- We want to avoid regressions in tricky lifecycle/ordering behavior (attach timing, exit vs tail output, etc.).
44+
45+
## Critical behaviors we believe we must preserve (please challenge if wrong)
46+
47+
- The “terminal stream” must not permanently stop delivering data due to a session exit transition (exit is a state change, not the end of the subscription).
48+
- Cold restore should be read-only until the user explicitly starts a new shell.
49+
- Detach/reattach should preserve expected scroll position behavior (when supported).
50+
- Workspace-level actions (delete workspace, refresh prompts, etc.) should affect all active terminal sessions regardless of backend choice.
51+
52+
## Future use cases we want to be compatible with
53+
54+
- **Remote runner / cloud terminals:** terminal sessions execute on a server (possibly while the laptop sleeps).
55+
- **Multi-device access:** a backend session may outlive any single client, and multiple clients/panes may view the same session.
56+
- **Provider model:** not just terminals — we likely need a workspace-scoped runtime that can also deliver:
57+
- agent lifecycle events (start/stop/permission requests, etc.)
58+
- git + “changes” functionality (status/diff/staging/commit/push/pull)
59+
- file read/write (or a sync layer)
60+
61+
We have a separate cloud plan doc that describes the intended product direction (cloud as source of truth, SSH terminals, tmux persistence, optional local sync for IDE users).
62+
63+
## What we want from you
64+
65+
1. A critique of our abstraction boundaries: what’s missing, what’s over-coupled, what’s in the wrong place.
66+
2. Alternative architectures that could reduce complexity and improve long-term extensibility.
67+
3. The biggest failure modes/risk areas you see (especially ordering/lifecycle bugs) and how you’d design to prevent them.
68+
4. A suggested “migration plan” that minimizes regressions while moving from today’s implementation to a cleaner architecture.
69+
70+
## Questions (intentionally open-ended)
71+
72+
### 1) Abstraction boundaries / layering
73+
74+
- If you were designing this from scratch, what are the natural layers/modules you would define?
75+
- Where should backend selection happen so it doesn’t leak across the codebase?
76+
- How would you structure the “terminal runtime” so it can support local + daemon + future remote backends without constant branching?
77+
- Should “terminal runtime” be its own concept, or should it be a sub-component of a broader “workspace runtime/provider”? Where should the seam be?
78+
79+
### 2) Contracts, identity, and lifecycle
80+
81+
- What should be the stable identities in the system?
82+
- UI pane IDs vs backend session IDs vs workspace IDs vs user IDs
83+
- multi-client / multi-pane viewing the same backend session
84+
- What lifecycle state machine would you define for a session (running/exited/disposed/etc.) and for the output stream?
85+
- How would you make operations idempotent and race-safe (double-create, attach-after-exit, exit-vs-tail-output, detach/reattach ordering)?
86+
- What does a “clean” detach/reattach contract look like across local/daemon/remote backends?
87+
88+
### 3) Event delivery model (streaming)
89+
90+
- What is the right event delivery contract between backend and UI?
91+
- How do you avoid coupling to Node EventEmitter semantics while still supporting local implementations?
92+
- What delivery guarantees matter (at-most-once vs at-least-once, ordering, replay for late subscribers)?
93+
- How would you handle “late subscribers” (UI attaches after output already started)?
94+
- How would you represent backend connectivity issues (disconnects, auth expiration, retries) in a backend-agnostic way?
95+
96+
### 4) Persistence / scrollback / resource management
97+
98+
- What persistence strategy would you choose for scrollback and session restore?
99+
- What’s the “right” unit of persistence (raw PTY log, terminal emulator snapshot, both)?
100+
- What size limits / retention rules should exist to avoid disk fill and memory pressure?
101+
- How should backpressure be handled end-to-end (PTY → persistence writer → IPC → renderer)?
102+
- Where should truncation/compaction happen, and how should it be tested?
103+
104+
### 5) Remote runners: integrating “worktrees”, “changes”, and “agent status”
105+
106+
- If terminal execution moves remote, what should be the source of truth for:
107+
- workspace files
108+
- git operations and “changes” UX
109+
- agent lifecycle/status events
110+
- What architecture patterns have you seen work for this (VSCode-like remote agents, SSH providers, etc.)?
111+
- What’s the minimum viable set of primitives to expose from a remote runner so the desktop UI can remain mostly unchanged?
112+
- How would you approach security/authentication for a remote agent channel?
113+
114+
### 6) Testing + rollout strategy
115+
116+
- What invariants would you codify as tests to prevent regressions?
117+
- How would you structure integration vs unit tests to catch ordering/lifecycle bugs?
118+
- If we expect a large refactor, how would you stage it to keep changes reviewable and safe?
119+
120+
## Reference docs + files to attach (copy/paste)
121+
122+
Below is a curated set of files you can paste into Slack for context. If you only read a few, start with the plan + the terminal router + the daemon manager.
123+
124+
### Primary
125+
126+
1. `apps/desktop/plans/20260109-2313-terminal-runtime-abstraction-rewrite.md`
127+
- The current refactor plan (milestones, invariants, proposed boundaries).
128+
2. `docs/CLOUD_WORKSPACE_PLAN.md`
129+
- Product direction for cloud workspaces / remote execution (high level).
130+
131+
### Terminal runtime + daemon backend
132+
133+
3. `apps/desktop/src/main/lib/terminal/manager.ts`
134+
- In-process PTY backend (local).
135+
4. `apps/desktop/src/main/lib/terminal/daemon-manager.ts`
136+
- Daemon-backed backend + cold restore logic (local persistence).
137+
5. `apps/desktop/src/main/lib/terminal-host/client.ts`
138+
- Main-process client that talks to the terminal host daemon.
139+
6. `apps/desktop/src/main/terminal-host/index.ts`
140+
- Terminal host daemon entry point.
141+
7. `apps/desktop/docs/TERMINAL_HOST_EVENTS.md`
142+
- Event/protocol notes for terminal host interactions.
143+
144+
### IPC surface (tRPC) + renderer terminal
145+
146+
8. `apps/desktop/src/lib/trpc/routers/terminal/terminal.ts`
147+
- Terminal IPC API and stream subscription shape.
148+
9. `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx`
149+
- Terminal UI component (current complexity hot-spot).
150+
151+
### “Changes” + agent lifecycle (related UX primitives to preserve)
152+
153+
10. `apps/desktop/src/lib/trpc/routers/changes/index.ts`
154+
- Git/status/diff-related IPC endpoints (local worktree-centric today). Key related files:
155+
- `apps/desktop/src/lib/trpc/routers/changes/status.ts`
156+
- `apps/desktop/src/lib/trpc/routers/changes/staging.ts`
157+
- `apps/desktop/src/lib/trpc/routers/changes/git-operations.ts`
158+
- `apps/desktop/src/lib/trpc/routers/changes/file-contents.ts`
159+
- `apps/desktop/src/lib/trpc/routers/changes/security/path-validation.ts`
160+
11. `apps/desktop/src/main/lib/notifications/server.ts`
161+
- Main-process notifications server that feeds agent lifecycle events.
162+
12. `apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts`
163+
- Renderer listener that consumes agent lifecycle notifications to drive UI state.

apps/desktop/electron.vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export default defineConfig({
6464
rollupOptions: {
6565
input: {
6666
index: resolve("src/main/index.ts"),
67+
// Terminal host daemon process - runs separately for terminal persistence
68+
"terminal-host": resolve("src/main/terminal-host/index.ts"),
69+
// PTY subprocess - spawned by terminal-host for each terminal
70+
"pty-subprocess": resolve("src/main/terminal-host/pty-subprocess.ts"),
6771
},
6872
output: {
6973
dir: resolve(devPath, "main"),

0 commit comments

Comments
 (0)