Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
b35f263
feat(desktop): add Workbench/Review mode with FileViewer panes
andreasasprou Dec 29, 2025
0d91d55
fix(desktop): add worktreePath guards and fix stale state in tabs store
andreasasprou Dec 29, 2025
530a423
fix(desktop): address CodeRabbit review - path traversal security fix…
andreasasprou Dec 29, 2025
676363d
fix(desktop): address PR review - security fixes and UX improvements
andreasasprou Dec 29, 2025
e8e2a19
fix(desktop): address review - persistence schema and security fixes
andreasasprou Dec 29, 2025
e06731e
fix(desktop): address critical review feedback - security and UX fixes
andreasasprou Dec 29, 2025
e9f4f2e
fix(desktop): critical security fix - validate worktreePath against l…
andreasasprou Dec 29, 2025
927d040
fix(desktop): security - validate worktreePath in all routes + preser…
andreasasprou Dec 29, 2025
31eb9cf
fix(desktop): security - path traversal in deleteUntracked + draft pr…
andreasasprou Dec 30, 2025
b2931b6
refactor(desktop): simplify security code - remove overcomplicated pa…
andreasasprou Dec 30, 2025
bc7dd58
fix(desktop): P0 untracked linecount + P2 terminal guard + branch safety
andreasasprou Dec 30, 2025
6d86a00
refactor(desktop): security architecture overhaul with proper path va…
andreasasprou Dec 30, 2025
d70d6b3
feat(desktop): add configurable terminal file link behavior
andreasasprou Dec 31, 2025
0bfdbe6
refactor(desktop): simplify security - remove symlink escape detection
andreasasprou Dec 31, 2025
3d0234b
feat(desktop): add configurable workspace navigation style (top-bar v…
andreasasprou Dec 31, 2025
dcd6125
fix(desktop): P0 CreateWorkspaceButton in sidebar mode + P1 race cond…
andreasasprou Dec 31, 2025
0627835
feat(desktop): workspace sidebar 1-1 feature parity with top-bar tabs
andreasasprou Dec 31, 2025
c721e57
refactor(desktop): extract shared utilities for workspace components
andreasasprou Dec 31, 2025
4e1711d
refactor(desktop): move sidebar toggle to WorkspaceActionBar in sideb…
andreasasprou Dec 31, 2025
36aff25
fix(desktop): prevent TopBar overlap with Mac traffic lights during load
andreasasprou Dec 31, 2025
aa12043
fix(desktop): increase Mac traffic light padding for better spacing
andreasasprou Dec 31, 2025
484be79
fix(desktop): address PR review feedback for changes security and ter…
andreasasprou Dec 31, 2025
e6ab981
feat(desktop): add 'Mark as Unread' context menu option for workspaces
andreasasprou Dec 31, 2025
14294ec
fix(desktop): clear workspace attention when clicking workspace
andreasasprou Dec 31, 2025
2dcad38
fix(desktop): address second round of PR review feedback
andreasasprou Dec 31, 2025
d4e1456
fix(desktop): address third round of PR review feedback
andreasasprou Dec 31, 2025
94f6116
Merge branch 'superset-sh:main' into workspace-sidebar
andreasasprou Jan 2, 2026
e1c6cca
feat(desktop): merge workspace action bar into top bar
andreasasprou Jan 2, 2026
e3111f2
fix(desktop): always show close/delete dialog for workspaces
andreasasprou Jan 2, 2026
303a158
fix(desktop): resolve TypeScript errors in workspace-sidebar branch
andreasasprou Jan 2, 2026
45f4741
style(desktop): use border instead of bg for active tab in GroupStrip
andreasasprou Jan 2, 2026
9d168c9
style(desktop): use top/side borders for active tab in GroupStrip
andreasasprou Jan 2, 2026
96fa309
fix(desktop): update sidebar toggle tooltip to 'Toggle Changes Sidebar'
andreasasprou Jan 2, 2026
ed7a9d0
feat(desktop): add sidebar toggle to TabsContent in sidebar navigatio…
andreasasprou Jan 2, 2026
166be33
fix(desktop): lift SidebarControl to ContentView to fix review mode r…
andreasasprou Jan 2, 2026
936dd58
refactor(desktop): rename NEW_TERMINAL hotkey to NEW_GROUP for clarity
andreasasprou Jan 2, 2026
6ced48d
feat(desktop): move workspace controls to content header in sidebar mode
andreasasprou Jan 3, 2026
1f988d8
fix(desktop): harden file viewer security and improve terminal link h…
andreasasprou Jan 3, 2026
ab1d0f6
fix(desktop): address PR review security and UX issues
andreasasprou Jan 3, 2026
c791f54
fix(desktop): use sep-aware check in assertParentInWorktree ENOENT path
andreasasprou Jan 3, 2026
1a2988e
fix(desktop): address P0/P1 security issues from review
andreasasprou Jan 3, 2026
930c122
fix(desktop): guard against undefined panes in attention check
andreasasprou Jan 3, 2026
edc4dae
fix(desktop): use strict allowlist for SafeImage - only allow data: URLs
andreasasprou Jan 3, 2026
9599bdc
feat(desktop): terminal persistence via daemon process
andreasasprou Dec 29, 2025
56e2ae0
feat(desktop): add settings UI toggle and TUI mode rehydration
andreasasprou Dec 29, 2025
87422c3
fix(desktop): address PR review feedback for terminal persistence
andreasasprou Dec 29, 2025
3bb707e
fix(desktop): address second round of PR review feedback
andreasasprou Dec 29, 2025
15a5192
fix(desktop): address third round of PR review feedback
andreasasprou Dec 30, 2025
f714ec0
fix(desktop): query daemon as source of truth for workspace sessions
andreasasprou Dec 30, 2025
68ff9ab
feat(desktop): add daemon restart button to Terminal settings
andreasasprou Dec 30, 2025
c9bd412
fix(desktop): externalize @xterm/headless in vite config
andreasasprou Dec 30, 2025
47fcd35
revert: remove @xterm/headless from externals
andreasasprou Dec 30, 2025
ce280f9
fix(desktop): add timeout to daemon connection polling to prevent ter…
andreasasprou Dec 30, 2025
0694e7a
fix(desktop): force terminal refresh after scrollback restore
andreasasprou Dec 30, 2025
0c4f826
fix(desktop): use fitAddon.fit() with requestAnimationFrame for termi…
andreasasprou Dec 30, 2025
025a8d1
fix(desktop): dispose DaemonTerminalManager when restarting daemon
andreasasprou Dec 30, 2025
b5e3fb7
fix(desktop): reload window after daemon restart to clear stale termi…
andreasasprou Dec 30, 2025
7cbc0ab
refactor(desktop): remove restart daemon button for v1 simplicity
andreasasprou Dec 30, 2025
90e00ce
fix(desktop): detect daemon disconnect and show error UI in existing …
andreasasprou Dec 30, 2025
f2a27dd
fix(desktop): gate TUI restoration until xterm renderer is ready
andreasasprou Dec 30, 2025
0ce3830
fix(desktop): don't auto-title from keyboard input when in TUI apps
andreasasprou Dec 30, 2025
ddfc11f
fix(desktop): enter alternate screen mode before writing TUI restorat…
andreasasprou Dec 30, 2025
982fc2d
chore(desktop): remove noisy 'Already connected' log from ensureConne…
andreasasprou Dec 30, 2025
e1e2016
fix(desktop): track alternate screen mode ourselves instead of relyin…
andreasasprou Dec 30, 2025
d65da34
fix(desktop): detect alternate screen mode from queued events and scr…
andreasasprou Dec 30, 2025
9c665be
fix(desktop): force redraw after terminal restore
andreasasprou Dec 30, 2025
a92026b
fix(desktop): nudge resize to redraw TUIs after restore
andreasasprou Dec 30, 2025
9ee7f55
fix(desktop): force repaint on reattached terminals
andreasasprou Dec 30, 2025
22d8a7f
fix(desktop): retry redraw after terminal restore
andreasasprou Dec 30, 2025
ff2edfa
chore(desktop): fix terminal restore indentation
andreasasprou Dec 30, 2025
99f6a36
fix(desktop): force repaint on restored TUIs
andreasasprou Dec 30, 2025
3d26516
fix(desktop): smooth terminal restore repaint
andreasasprou Dec 30, 2025
297682d
fix(desktop): fix WebGL corruption on terminal restore
andreasasprou Dec 31, 2025
6cc7643
fix(desktop): make large pastes reliable
andreasasprou Dec 31, 2025
3caadf7
fix(desktop): force terminal redraw on tab switch
andreasasprou Dec 31, 2025
056544a
fix(desktop): reduce terminal redraw side effects
andreasasprou Dec 31, 2025
4c79505
fix(desktop): address terminal persistence PR feedback
andreasasprou Dec 31, 2025
d73c4eb
fix(desktop): restore missing queue byte counters
andreasasprou Dec 31, 2025
67ffd50
fix(desktop): avoid terminal refresh while hidden
andreasasprou Dec 31, 2025
5612ee3
fix(desktop): stabilize terminal rendering on macOS
andreasasprou Dec 31, 2025
9b6f1ac
fix(desktop): address P0/P1 terminal persistence PR feedback
andreasasprou Dec 31, 2025
1095ad0
refactor(desktop): improve code quality and organization
andreasasprou Dec 31, 2025
ca368d4
fix(desktop): avoid spawning daemon during orphan cleanup when persis…
andreasasprou Dec 31, 2025
9349f9d
docs(desktop): add terminal reattach/rendering research log
andreasasprou Jan 1, 2026
0991c79
chore(desktop): improve code hygiene for PR review
andreasasprou Jan 1, 2026
c06b733
fix(desktop): resolve TUI corruption on tab switch via SIGWINCH redraw
andreasasprou Jan 1, 2026
0442aa2
fix(desktop): use position-based alt-screen detection in scrollback f…
andreasasprou Jan 1, 2026
ee901c7
docs(desktop): consolidate terminal persistence technical notes
andreasasprou Jan 2, 2026
da42b6b
fix(desktop): resolve orphan PTY processes on workspace deletion
andreasasprou Jan 2, 2026
b03a096
chore(desktop): remove verbose diagnostic logging from kill flow
andreasasprou Jan 2, 2026
7a535db
fix(desktop): keep terminals mounted to eliminate white screen on wor…
andreasasprou Jan 2, 2026
536d6ea
perf(desktop): remove duplicate scrollback payload in daemon mode
andreasasprou Jan 2, 2026
f83e3e7
chore(desktop): remove unused code and fix style issues
andreasasprou Jan 2, 2026
df93762
feat(desktop): add 3-color workspace status indicators
andreasasprou Jan 3, 2026
2dbb1ce
fix(desktop): address code review - guard store mutations, persist st…
andreasasprou Jan 3, 2026
5458689
fix(desktop): use HOOK_PROTOCOL_VERSION constant instead of hardcoded…
andreasasprou Jan 3, 2026
574432a
chore: remove session handoff artifacts from repo
andreasasprou Jan 3, 2026
20c8561
refactor(desktop): extract StatusIndicator shared component
andreasasprou Jan 3, 2026
127625e
fix(desktop): add missing ptyPid property to Session class
andreasasprou Jan 3, 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
271 changes: 271 additions & 0 deletions .agents/commands/create-plan-file.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

335 changes: 335 additions & 0 deletions apps/desktop/docs/2026-01-02-terminal-persistence-technical-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
# Terminal Persistence — Technical Notes

> **Date**: January 2026
> **Feature**: Terminal session persistence via daemon process
> **PR**: #541

This document captures the technical decisions, debugging investigations, and solutions for the terminal persistence feature. It's intended for engineers who need to understand **why** certain approaches were chosen.

---

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [TUI Restoration: Why SIGWINCH Instead of Snapshots](#tui-restoration-why-sigwinch-instead-of-snapshots)
3. [Keeping Terminals Mounted Across Workspace Switches](#keeping-terminals-mounted-across-workspace-switches)
4. [Large Paste Reliability: Subprocess Isolation + Backpressure](#large-paste-reliability-subprocess-isolation--backpressure)
5. [Renderer Notes: WebGL vs Canvas on macOS](#renderer-notes-webgl-vs-canvas-on-macos)
6. [Design Options Considered](#design-options-considered)
7. [Future Improvements](#future-improvements)
8. [Reference Links](#reference-links)

---

## Architecture Overview

High-level data flow:

```
Renderer (xterm.js in React)
↕ TRPC stream/write calls
Electron main
↕ Unix socket IPC
terminal-host daemon (Node.js)
↕ stdin/stdout IPC (binary framing)
per-session PTY subprocess (Node.js + node-pty)
↕ PTY
shell / TUI (opencode, vim, etc.)
```

Key concepts:

- **Daemon owns sessions** so terminals persist across app restarts.
- **Headless emulator** in daemon maintains a model of the terminal state (screen + modes) and produces a snapshot for reattach.
- **Per-session subprocess** isolates each PTY so one terminal can't freeze others.
- **Renderer is recreated** on React mount; on "switch away" we detach and later reattach to the daemon session.

---

## TUI Restoration: Why SIGWINCH Instead of Snapshots

### The Problem

When switching away from a terminal running a TUI (like opencode, vim, claude) and switching back, we saw visual corruption—missing ASCII art, input boxes, and UI elements.

### Why Snapshots Don't Work for TUIs

1. TUIs use "styled spaces" (spaces with background colors) to create UI elements
2. `SerializeAddon` captures buffer cell content, but serialization of styled empty cells is inconsistent
3. When restored, the serialized snapshot renders sparsely—missing panels, borders, and UI chrome

**Diagnostic data showed the problem:**
```
ALT-BUFFER: lines=52 nonEmpty=14 chars=2156
```
A full TUI screen (91×52 = 4732 cells) should have far more content. The alt buffer was sparse.

### Investigation Timeline

| # | Hypothesis | Test | Result |
|---|------------|------|--------|
| 1 | Live events interleaving with snapshot | Added logging for pending events | ❌ PENDING_EVENTS=0 |
| 2 | Double alt-screen entry | Disabled manual entry | ❌ Still corrupted |
| 3 | WebGL renderer issues | Forced Canvas renderer | ❌ Still corrupted |
| 4 | Dimension mismatch | Logged xterm vs snapshot dims | ❌ MATCH=true |
| 5 | Alt-screen buffer mismatch | Logged transition sequences | ❌ CONSISTENT=true |

### Root Cause: Flush Timeout During Snapshot

**Location:** `Session.attach()` in `apps/desktop/src/main/terminal-host/session.ts`

With continuous TUI output (like OpenCode), the emulator write queue NEVER empties in the timeout window. `Promise.race()` times out, but queued data never made it to xterm before snapshot capture.

### The Solution: Skip Snapshot, Trigger SIGWINCH

Instead of trying to perfectly serialize and restore TUI state:

1. **Skip writing the broken snapshot** for alt-screen (TUI) sessions
2. **Enter alt-screen mode** directly so TUI output goes to the correct buffer
3. **Enable streaming first** so live PTY output comes through
4. **Trigger SIGWINCH** via resize down/up—TUI redraws itself from scratch

```typescript
if (isAltScreenReattach) {
xterm.write("\x1b[?1049h"); // Enter alt-screen
isStreamReadyRef.current = true;
flushPendingEvents();

// Trigger SIGWINCH via resize
resizeRef.current({ paneId, cols, rows: rows - 1 });
setTimeout(() => {
resizeRef.current({ paneId, cols, rows });
}, 100);
}
```

### Trade-offs

| Aspect | Snapshot Approach | SIGWINCH Approach |
|--------|-------------------|-------------------|
| Visual continuity | Broken (sparse/corrupted) | Brief flash as TUI redraws |
| Correctness | Unreliable | Reliable (TUI owns its state) |
| Complexity | High | Low |

**Non-TUI sessions** (normal shells) still use the snapshot approach, which works correctly for scrollback history and shell prompts.

---

## Keeping Terminals Mounted Across Workspace Switches

### The Problem

Even with SIGWINCH-based TUI restoration working correctly, switching between workspaces still caused intermittent white screen issues for TUI apps. Manual window resize would fix it, but the experience was jarring.

**Root cause:** When switching workspaces, React unmounts the `Terminal` component entirely, destroying the xterm.js instance. On return, a new xterm instance must be created and reattached to the existing PTY session. Despite correct SIGWINCH timing, race conditions between xterm initialization and PTY output caused blank/white screens.

### The Solution: Keep All Terminals Mounted

Instead of unmounting Terminal components on workspace/tab switch:

1. **Render all tabs from all workspaces** simultaneously in `TabsContent`
2. **Hide inactive tabs with CSS** (`visibility: hidden; pointer-events: none;`)
3. **Show only the active tab** for the active workspace

**Implementation:** `TabsContent/index.tsx` renders `allTabs` with visibility toggling instead of conditional rendering.

### Why `visibility: hidden` Instead of `display: none`

Using `visibility: hidden` (not `display: none`) is critical:
- `display: none` removes the element from layout, giving it 0×0 dimensions
- xterm.js and FitAddon expect non-zero dimensions to function correctly
- `visibility: hidden` preserves the element's layout dimensions while hiding it visually

### Why This Works

- xterm.js instances persist across navigation—no recreation needed
- No state reconstruction, no reattach timing issues
- The terminal stays exactly as it was when hidden
- The complex SIGWINCH/snapshot restoration code becomes a fallback path only (used for app restart recovery)

### Trade-offs

| Aspect | Impact | Mitigation |
|--------|--------|------------|
| Memory | Each terminal holds scrollback buffer + xterm render state | See Future Improvements: LRU hibernation |
| CPU | Hidden terminals still process PTY output | See Future Improvements: buffer output |
| DOM nodes | Many elements even when hidden | `visibility: hidden` is cheap; browser optimizes |

### When This Applies

This optimization is **only enabled when Terminal Persistence is ON** in Settings. When persistence is disabled, the original behavior (unmount on switch) is used.

### Fallback Path

The SIGWINCH-based restoration logic remains in `Terminal.tsx` as a fallback for:
- **App restart recovery** — fresh xterm must reattach to daemon's PTY session
- **Edge cases** — any scenario where the Terminal component truly remounts

---

## Large Paste Reliability: Subprocess Isolation + Backpressure

### The Problem

Pasting large blocks of text (e.g. 3k+ lines) into `vi` could:
- Hang the terminal daemon / freeze all terminals, or
- Partially paste and then silently stop (missing chunks)

Most visible on macOS (small kernel PTY buffer + very high output volume during `vi` repaints).

### Two Distinct Failure Modes

**1) CPU saturation on output (daemon side)**

Large pastes cause `vi` to repaint aggressively, producing huge volumes of escape-sequence-heavy output. If the daemon tries to parse that output in large unbounded chunks, it monopolizes the event loop.

**2) Backpressure on input (PTY write side)**

PTY writes must respect backpressure. When writing to a PTY fd in non-blocking mode, the kernel can return `EAGAIN`/`EWOULDBLOCK`. If treated as fatal, paste chunks get dropped.

### The Solution

**Process isolation (per terminal)**

Each PTY runs in its own subprocess (`pty-subprocess.ts`). One terminal hitting backpressure can't freeze the daemon or other terminals.

**Binary framing (no JSON on hot paths)**

Subprocess ↔ daemon communication uses length-prefixed binary framing (`pty-subprocess-ipc.ts`) to avoid JSON overhead on escape-heavy output.

**Output batching + stdout backpressure**

Subprocess batches PTY output (32ms cadence, 128KB max) and pauses PTY reads when `process.stdout` is backpressured.

**Input backpressure (retry, don't drop)**

Subprocess treats `EAGAIN`/`EWOULDBLOCK` as expected backpressure:
- Keeps queued buffers
- Retries with exponential backoff (2ms → 50ms)
- Pauses upstream when backlog exceeds high watermark

**Daemon responsiveness (time-sliced emulator)**

The daemon applies PTY output to the headless emulator in time-budgeted slices.

### Debugging

Set these env vars and restart the app:
- `SUPERSET_PTY_SUBPROCESS_DEBUG=1` — subprocess batching + PTY input backpressure logs
- `SUPERSET_TERMINAL_EMULATOR_DEBUG=1` — daemon emulator budget/overrun logs

```bash
ps aux | rg "terminal-host|pty-subprocess"
```

---

## Renderer Notes: WebGL vs Canvas on macOS

### The Problem

Severe corruption/glitching when switching between terminals on macOS with `xterm-webgl`.

### Current Approach

- **Default to Canvas on macOS** for stability
- **WebGL on other platforms** for performance
- Allow override for testing via localStorage:
```javascript
localStorage.setItem('terminal-renderer', 'webgl' | 'canvas' | 'dom')
localStorage.removeItem('terminal-renderer') // revert to default
```

### Why Warp Feels Smoother

Warp's architecture is GPU-first (Metal on macOS) with careful minimization of bottlenecks. That doesn't automatically mean "WebGL fixes it" inside xterm.js—in practice `xterm-webgl` has had regressions on macOS.

---

## Design Options Considered

These were evaluated during the design phase:

### Option A — Don't detach on tab switch (keep renderer alive)

Make tab switching purely a show/hide operation. Removes the core "reattach baseline mismatch" failure mode.

**Pros:** Fastest path to eliminating corruption
**Cons:** More memory if many terminals open; needs hibernation policies

### Option B — tmux-style server authoritative screen diff

Daemon maintains full screen grid; sends diffs to clients.

**Pros:** Robust reattach
**Cons:** Significant engineering effort; essentially building a multiplexer

### Option C — Use tmux/screen as persistence layer

Put tmux behind the scenes.

**Pros:** tmux already solved this
**Cons:** External dependency; platform concerns

### Option D — Per-terminal WebContents/BrowserView

Host each terminal in a persistent Electron view.

**Pros:** Avoid rehydrate for navigation
**Cons:** Complex Electron lifecycle

### What We Chose

For v1, we implemented a daemon with SIGWINCH-based TUI restoration. This balances correctness (TUI redraws itself) with implementation complexity.

**Update (v1.1):** We discovered that keeping xterm instances mounted (Option A) eliminates the reattach timing issues that caused white screen flashes during workspace/tab switches. When terminal persistence is enabled, we now render all tabs and toggle visibility instead of unmounting. The SIGWINCH restoration logic remains as a fallback for app restart recovery when a fresh xterm instance must reattach to an existing PTY session.

---

## Future Improvements

These are documented for future work. They are not blocking for the current implementation.

### 1. Buffer PTY Output for Hidden Terminals

Currently, hidden terminals continue processing PTY output through xterm.js. For users with many terminals producing continuous output, this wastes CPU cycles.

**Proposed solution:**
- When a terminal becomes hidden, pause writes to xterm
- Buffer PTY events in memory (or discard if not in alt-screen mode)
- On show, flush buffered events to xterm

### 2. LRU Terminal Hibernation

For users with many workspaces (10+), keeping all terminals alive may use excessive memory.

**Proposed solution:**
- Track terminal last-active timestamps
- When memory pressure is detected, hibernate oldest inactive terminals
- Hibernation = dispose xterm instance, keep PTY alive in daemon
- On reactivation, create new xterm and run normal restore flow

### 3. Reduce Scrollback for Hidden Terminals

Each terminal's scrollback buffer can be large (default 10,000 lines).

**Proposed solution:**
- Reduce `scrollback` option for inactive terminals
- Restore full scrollback on activation (daemon has full history)

### 4. Memory Usage Metrics

Add observability to understand real-world memory usage patterns:
- Track number of terminals per user session
- Track memory per terminal (xterm buffers + DOM)
- Surface warnings if approaching problematic thresholds

---

## Reference Links

- [xterm.js flow control guide](https://xtermjs.org/docs/guides/flowcontrol/) — buffering, time-sliced processing
- [xterm.js issue #595](https://github.com/xtermjs/xterm.js/issues/595) — "Support saving and restoring of terminal state"
- [xterm.js VT features](https://xtermjs.org/docs/api/vtfeatures/) — supported sequences
- [xterm.js WebGL issues](https://github.com/xtermjs/xterm.js/issues/4665) — regression examples
- [How Warp Works](https://www.warp.dev/blog/how-warp-works) — GPU-first architecture
Loading
Loading