Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
a2a5262
feat(desktop): terminal persistence via daemon process
andreasasprou Jan 6, 2026
f047963
fix(desktop): add @xterm/headless dependency and fix type error
andreasasprou Jan 6, 2026
527c1d4
fix(desktop): address CodeRabbit review feedback
andreasasprou Jan 6, 2026
52767f0
fix(desktop): fix CI errors after test file move
andreasasprou Jan 6, 2026
fc3fcd8
refactor(desktop): centralize DEFAULT_TERMINAL_PERSISTENCE constant
andreasasprou Jan 6, 2026
794e80c
refactor(desktop): address PR review comments
andreasasprou Jan 6, 2026
234b2d7
fix(desktop): align @xterm/headless version with @xterm/xterm
andreasasprou Jan 6, 2026
af855ff
fix(desktop): address code review feedback for terminal persistence
andreasasprou Jan 6, 2026
129aac3
fix(desktop): guard pane updates against deleted panes
andreasasprou Jan 6, 2026
8e567aa
fix(desktop): split terminal host control/stream sockets
andreasasprou Jan 6, 2026
d77a7ef
chore(desktop): biome format + archive execplan
andreasasprou Jan 6, 2026
9ea4801
fix(desktop): spawn daemon when token missing
andreasasprou Jan 6, 2026
8427264
fix(desktop): harden terminal persistence data perms
andreasasprou Jan 6, 2026
07cae93
fix(desktop): address terminal persistence review feedback
andreasasprou Jan 6, 2026
e484eab
fix(desktop): bundle @xterm packages for terminal-host daemon
andreasasprou Jan 7, 2026
4fc7d26
wip: dx hardening plan
andreasasprou Jan 7, 2026
17029f3
fix(desktop): remove obsolete setNeedsAttention after rebase
andreasasprou Jan 7, 2026
965239d
docs(desktop): update terminal persistence exec plan
andreasasprou Jan 7, 2026
2dd48ac
fix(desktop): harden terminal persistence DX
andreasasprou Jan 8, 2026
d357bc4
fix(desktop): prevent history init buffer loop
andreasasprou Jan 8, 2026
eaf4805
fix(desktop): enable terminal stream when snapshot empty
andreasasprou Jan 8, 2026
8add9e0
fix(desktop): prevent scheduler deadlock on React StrictMode unmount
andreasasprou Jan 8, 2026
e113902
fix(desktop): address PR review blocking issues
andreasasprou Jan 8, 2026
1e2fc0b
fix(desktop): address oracle feedback on PR fixes
andreasasprou Jan 8, 2026
bb63819
docs(desktop): document ordering assumption in sendRequestOnStream
andreasasprou Jan 8, 2026
15b299d
fix(desktop): resolve type errors after rebase onto main
andreasasprou Jan 9, 2026
9bf9a59
fix(desktop): implement daemon signal() support for SIGINT/SIGTERM
andreasasprou Jan 9, 2026
cb993c7
docs(desktop): add terminal host event semantics documentation
andreasasprou Jan 9, 2026
c54ed74
feat(desktop): implement cold restore terminal history persistence
andreasasprou Jan 9, 2026
87bfe80
feat(desktop): add telemetry for terminal persistence events
andreasasprou Jan 9, 2026
49516fb
fix(desktop): trigger cold restore on daemon session loss
andreasasprou Jan 9, 2026
ff354e2
fix(desktop): suppress toast when showing retry UI for session loss
andreasasprou Jan 9, 2026
edcbec1
fix(desktop): suppress toast for transient PTY not spawned errors
andreasasprou Jan 9, 2026
08ad420
fix(desktop): clear connection error on successful initial attach
andreasasprou Jan 9, 2026
4f80897
fix(desktop): trigger cold restore on daemon session loss
andreasasprou Jan 9, 2026
6a5ebc8
fix(desktop): re-focus terminal after successful retry connection
andreasasprou Jan 9, 2026
1ec2279
fix(desktop): cold restore for TUI apps with empty scrollback
andreasasprou Jan 9, 2026
eadbe81
fix(desktop): focus terminal after clicking Start Shell
andreasasprou Jan 9, 2026
a569fe4
fix(desktop): keep terminal stream alive on exit
andreasasprou Jan 9, 2026
c37c3d2
chore(desktop): fix biome check
andreasasprou Jan 9, 2026
e7b2183
chore(desktop): fix lint warnings
andreasasprou Jan 9, 2026
b74b76e
fix(desktop): address persistence review blockers
andreasasprou Jan 9, 2026
8333e40
fix(desktop): harden terminal history caps
andreasasprou Jan 9, 2026
1068f6c
docs(desktop): add terminal runtime abstraction plan
andreasasprou Jan 9, 2026
d6e6319
docs(desktop): expand plan for Terminal.tsx decomposition
andreasasprou Jan 10, 2026
fef4ce9
docs(desktop): add target architecture snippets to plan
andreasasprou Jan 10, 2026
0f0b385
docs(desktop): refine terminal runtime abstraction plan
andreasasprou Jan 10, 2026
c1fd9a2
docs(desktop): add remote runner notes to plan
andreasasprou Jan 10, 2026
0abac66
docs(desktop): align terminal runtime plan with cloud provider direction
andreasasprou Jan 11, 2026
7f7bb1e
docs(desktop): add terminal runtime architecture review packet
andreasasprou Jan 12, 2026
4b746d9
docs(desktop): narrow changes router reference list
andreasasprou Jan 12, 2026
016a120
docs(desktop): incorporate architecture feedback into runtime rewrite…
andreasasprou Jan 12, 2026
421d381
refactor(desktop): introduce WorkspaceRuntime abstraction
andreasasprou Jan 14, 2026
5fdff27
WIP: route-based settings/dashboard structure alignment with upstream
andreasasprou Jan 14, 2026
a664dfb
Merge origin/main into terminal-persistence-v2
andreasasprou Jan 14, 2026
8999412
fix(desktop): improve terminal kill-all reliability and UI feedback
andreasasprou Jan 14, 2026
d14275e
fix(desktop): keep killed terminals dead
andreasasprou Jan 15, 2026
1893425
Merge remote-tracking branch 'origin/main' into terminal-persistence-v2
andreasasprou Jan 15, 2026
b2f8de2
fix(desktop): disable terminal session actions when none
andreasasprou Jan 15, 2026
e0a1b02
fix(desktop): memoize terminal session lists
andreasasprou Jan 15, 2026
586ee9f
remove old code
andreasasprou Jan 15, 2026
2b36456
fix(desktop): log best-effort failures in terminal utils
andreasasprou Jan 15, 2026
81b435e
fix: harden terminal persistence paths and logs
andreasasprou Jan 15, 2026
c212b86
chore: format terminal router and client
andreasasprou Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions apps/desktop/docs/TERMINAL_HOST_EVENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Terminal Host Event Semantics

This document describes the event delivery model for the Terminal Host daemon protocol.

## Event Types

The daemon emits three event types to attached clients:

| Event | Payload | Description |
|---------|-------------------------------------|------------------------------------------|
| `data` | `{ type: "data", data: string }` | PTY output (terminal content) |
| `exit` | `{ type: "exit", exitCode, signal?}` | PTY process terminated |
| `error` | `{ type: "error", error, code? }` | Error condition (e.g., write queue full) |

## Socket Model

Clients connect with two sockets sharing a `clientId`:

- **Control socket** (`role: "control"`): RPC request/response (write, resize, kill, etc.)
- **Stream socket** (`role: "stream"`): Receives unsolicited events

Events are broadcast only to stream sockets. This separation prevents event floods from blocking RPC responses.

## Delivery Semantics

### At-Most-Once Delivery

Events are delivered **at-most-once** per attached client:
- No acknowledgment or retry mechanism
- If a client socket buffer is full, data is queued but may be lost on disconnect
- Clients must be prepared to miss events (especially `data` during reconnection)

### No Durability

Events are not persisted. If no clients are attached, events are emitted but not stored.
For cold restore, use `createOrAttach` which returns a `TerminalSnapshot` containing the current screen state.

## Ordering Guarantees

### Within a Session

Events for a single session are delivered **in-order** relative to each other:
1. PTY output order is preserved (data events arrive in the order produced)
2. Exit event is always delivered after all data events for that session
3. Error events may interleave with data events

### Across Sessions

No ordering guarantees across different sessions. Events from session A and session B may interleave arbitrarily.

## Backpressure Handling

The system implements multi-level backpressure to prevent memory exhaustion:

### Level 1: Client Socket Backpressure
```
Client socket buffer full
→ Session pauses subprocess stdout reads
→ Subprocess backpressures PTY reads
→ PTY write buffer fills → kernel blocks PTY writes
```

When the client drains its buffer, the chain resumes.

### Level 2: Subprocess stdin Backpressure
```
Write requests exceed MAX_SUBPROCESS_STDIN_QUEUE_BYTES (2MB)
→ Frame dropped
→ Error event emitted: { code: "WRITE_QUEUE_FULL" }
```

### Level 3: PTY Write Backpressure (in subprocess)
```
PTY kernel buffer full (EAGAIN/EWOULDBLOCK)
→ Exponential backoff retry (2ms → 50ms)
→ Write queue accumulates up to 64MB hard limit
→ Beyond limit: frames dropped, error reported
```

## Error Codes

| Code | Meaning |
|---------------------|----------------------------------------------|
| `WRITE_QUEUE_FULL` | Input queue exceeded limit, data dropped |
| `SUBPROCESS_ERROR` | PTY subprocess reported an error |
| `WRITE_FAILED` | Failed to write to PTY |
| `UNKNOWN` | Unclassified error |

## Race Conditions

### Kill vs Attach Race

Sessions track `terminatingAt` timestamp when `kill()` is called. The `isAttachable` property returns false for terminating sessions, preventing new attachments to sessions about to exit.

### Data vs Exit Race

The subprocess flushes all buffered output before sending the exit frame, so clients receive all terminal output before the exit event.

## Renderer Integration Notes (tRPC)

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.

### `exit` must not complete the subscription

Treat `exit` as a **state transition**, not a terminal end-of-stream:

- The renderer subscribes with a stable `paneId` input (`trpc.terminal.stream.useSubscription(paneId)`).
- `@trpc/react-query` does **not** auto-resubscribe after a subscription completes unless the input/key changes.
- We reuse the same `paneId` across restarts / cold restore (new session, same pane).

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.

### Cold restore overlay: drop stale queued events

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).

## Usage Example

```typescript
// Stream socket receives events as NDJSON
socket.on("data", (chunk) => {
for (const line of chunk.toString().split("\n").filter(Boolean)) {
const event = JSON.parse(line) as IpcEvent;
if (event.type !== "event") continue;

switch (event.event) {
case "data":
terminal.write(event.payload.data);
break;
case "exit":
console.log(`Session ${event.sessionId} exited: ${event.payload.exitCode}`);
break;
case "error":
console.error(`Error in ${event.sessionId}: ${event.payload.error}`);
break;
}
}
});
```

## Related Files

- `types.ts` - Event type definitions
- `session.ts` - Event emission and backpressure logic
- `pty-subprocess.ts` - PTY-level backpressure handling
- `client.ts` - Client-side event handling
163 changes: 163 additions & 0 deletions apps/desktop/docs/TERMINAL_RUNTIME_ARCH_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Architecture Review Packet: Terminal Runtime + Future Remote Runners

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.

**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.

## What we’re trying to build (big picture)

Superset Desktop is an Electron app that provides:

- A multi-pane terminal UI inside workspaces (think “IDE terminal panes”).
- Git worktree-based workspaces (multiple isolated working copies).
- “Changes” UX (diff/status/staging) tied to those workspaces.
- Agent/CLI integrations that surface lifecycle/status in the UI (e.g. completion events, indicators).

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.).

## Why we’re asking for review now

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).

We’re planning a rewrite/refactor to:

- Centralize backend selection (so most code is backend-agnostic).
- Preserve current behavior (especially around session streaming, attach/detach, and restore).
- Create a foundation that won’t fight us when we introduce remote runners/cloud terminals.

## Current state (high-level)

- Electron main process owns terminal backends:
- **In-process backend:** PTYs owned directly in main process.
- **Daemon backend:** PTYs owned by a separate “terminal host” process; main connects via a local socket.
- Renderer talks to main via tRPC (IPC), including a terminal stream subscription.
- Terminals have “attach/detach” semantics and “cold restore” (disk-backed scrollback restore) for daemon persistence.

## Known constraints (technical + product)

These are constraints we currently operate under; if you think any should change, call it out.

- Renderer must not import Node.js modules (browser environment).
- IPC is via tRPC, and subscriptions must use an observable pattern (not async generators).
- The terminal UI must remain responsive under high output (performance/backpressure matters).
- We want to avoid regressions in tricky lifecycle/ordering behavior (attach timing, exit vs tail output, etc.).

## Critical behaviors we believe we must preserve (please challenge if wrong)

- 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).
- Cold restore should be read-only until the user explicitly starts a new shell.
- Detach/reattach should preserve expected scroll position behavior (when supported).
- Workspace-level actions (delete workspace, refresh prompts, etc.) should affect all active terminal sessions regardless of backend choice.

## Future use cases we want to be compatible with

- **Remote runner / cloud terminals:** terminal sessions execute on a server (possibly while the laptop sleeps).
- **Multi-device access:** a backend session may outlive any single client, and multiple clients/panes may view the same session.
- **Provider model:** not just terminals — we likely need a workspace-scoped runtime that can also deliver:
- agent lifecycle events (start/stop/permission requests, etc.)
- git + “changes” functionality (status/diff/staging/commit/push/pull)
- file read/write (or a sync layer)

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).

## What we want from you

1. A critique of our abstraction boundaries: what’s missing, what’s over-coupled, what’s in the wrong place.
2. Alternative architectures that could reduce complexity and improve long-term extensibility.
3. The biggest failure modes/risk areas you see (especially ordering/lifecycle bugs) and how you’d design to prevent them.
4. A suggested “migration plan” that minimizes regressions while moving from today’s implementation to a cleaner architecture.

## Questions (intentionally open-ended)

### 1) Abstraction boundaries / layering

- If you were designing this from scratch, what are the natural layers/modules you would define?
- Where should backend selection happen so it doesn’t leak across the codebase?
- How would you structure the “terminal runtime” so it can support local + daemon + future remote backends without constant branching?
- 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?

### 2) Contracts, identity, and lifecycle

- What should be the stable identities in the system?
- UI pane IDs vs backend session IDs vs workspace IDs vs user IDs
- multi-client / multi-pane viewing the same backend session
- What lifecycle state machine would you define for a session (running/exited/disposed/etc.) and for the output stream?
- How would you make operations idempotent and race-safe (double-create, attach-after-exit, exit-vs-tail-output, detach/reattach ordering)?
- What does a “clean” detach/reattach contract look like across local/daemon/remote backends?

### 3) Event delivery model (streaming)

- What is the right event delivery contract between backend and UI?
- How do you avoid coupling to Node EventEmitter semantics while still supporting local implementations?
- What delivery guarantees matter (at-most-once vs at-least-once, ordering, replay for late subscribers)?
- How would you handle “late subscribers” (UI attaches after output already started)?
- How would you represent backend connectivity issues (disconnects, auth expiration, retries) in a backend-agnostic way?

### 4) Persistence / scrollback / resource management

- What persistence strategy would you choose for scrollback and session restore?
- What’s the “right” unit of persistence (raw PTY log, terminal emulator snapshot, both)?
- What size limits / retention rules should exist to avoid disk fill and memory pressure?
- How should backpressure be handled end-to-end (PTY → persistence writer → IPC → renderer)?
- Where should truncation/compaction happen, and how should it be tested?

### 5) Remote runners: integrating “worktrees”, “changes”, and “agent status”

- If terminal execution moves remote, what should be the source of truth for:
- workspace files
- git operations and “changes” UX
- agent lifecycle/status events
- What architecture patterns have you seen work for this (VSCode-like remote agents, SSH providers, etc.)?
- What’s the minimum viable set of primitives to expose from a remote runner so the desktop UI can remain mostly unchanged?
- How would you approach security/authentication for a remote agent channel?

### 6) Testing + rollout strategy

- What invariants would you codify as tests to prevent regressions?
- How would you structure integration vs unit tests to catch ordering/lifecycle bugs?
- If we expect a large refactor, how would you stage it to keep changes reviewable and safe?

## Reference docs + files to attach (copy/paste)

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.

### Primary

1. `apps/desktop/plans/20260109-2313-terminal-runtime-abstraction-rewrite.md`
- The current refactor plan (milestones, invariants, proposed boundaries).
2. `docs/CLOUD_WORKSPACE_PLAN.md`
- Product direction for cloud workspaces / remote execution (high level).

### Terminal runtime + daemon backend

3. `apps/desktop/src/main/lib/terminal/manager.ts`
- In-process PTY backend (local).
4. `apps/desktop/src/main/lib/terminal/daemon-manager.ts`
- Daemon-backed backend + cold restore logic (local persistence).
5. `apps/desktop/src/main/lib/terminal-host/client.ts`
- Main-process client that talks to the terminal host daemon.
6. `apps/desktop/src/main/terminal-host/index.ts`
- Terminal host daemon entry point.
7. `apps/desktop/docs/TERMINAL_HOST_EVENTS.md`
- Event/protocol notes for terminal host interactions.

### IPC surface (tRPC) + renderer terminal

8. `apps/desktop/src/lib/trpc/routers/terminal/terminal.ts`
- Terminal IPC API and stream subscription shape.
9. `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx`
- Terminal UI component (current complexity hot-spot).

### “Changes” + agent lifecycle (related UX primitives to preserve)

10. `apps/desktop/src/lib/trpc/routers/changes/index.ts`
- Git/status/diff-related IPC endpoints (local worktree-centric today). Key related files:
- `apps/desktop/src/lib/trpc/routers/changes/status.ts`
- `apps/desktop/src/lib/trpc/routers/changes/staging.ts`
- `apps/desktop/src/lib/trpc/routers/changes/git-operations.ts`
- `apps/desktop/src/lib/trpc/routers/changes/file-contents.ts`
- `apps/desktop/src/lib/trpc/routers/changes/security/path-validation.ts`
11. `apps/desktop/src/main/lib/notifications/server.ts`
- Main-process notifications server that feeds agent lifecycle events.
12. `apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts`
- Renderer listener that consumes agent lifecycle notifications to drive UI state.
4 changes: 4 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export default defineConfig({
rollupOptions: {
input: {
index: resolve("src/main/index.ts"),
// Terminal host daemon process - runs separately for terminal persistence
"terminal-host": resolve("src/main/terminal-host/index.ts"),
// PTY subprocess - spawned by terminal-host for each terminal
"pty-subprocess": resolve("src/main/terminal-host/pty-subprocess.ts"),
},
output: {
dir: resolve(devPath, "main"),
Expand Down
Loading