Skip to content

feat(desktop): terminal persistence via daemon process#619

Merged
AviPeltz merged 64 commits into
mainfrom
terminal-persistence-v2
Jan 15, 2026
Merged

feat(desktop): terminal persistence via daemon process#619
AviPeltz merged 64 commits into
mainfrom
terminal-persistence-v2

Conversation

@andreasasprou
Copy link
Copy Markdown
Contributor

@andreasasprou andreasasprou commented Jan 6, 2026

Links

Summary

This PR implements terminal session persistence via a background daemon process that survives app restarts, and hardens the feature for real-world usage (many accumulated terminal panes).

Happy path: https://www.loom.com/share/d84c42fdb4c24952ad112f9e6be4c82e?from_recorder=1&focus_title=1

User-visible behavior

  • No startup freeze: when persistence is enabled, the app no longer mounts/attaches every terminal tab on startup. Only the active tab plus a per-run MRU warm set (max 8 terminal tabs) stays mounted (hidden via CSS visibility: hidden) to keep common switching smooth.
  • Progressive attach: terminals attach progressively (max 3 concurrent attaches), prioritizing the focused pane to keep heavy split tabs responsive.
  • Cold restore: when the daemon does not have a session but on-disk scrollback exists from an unclean shutdown, the UI shows restored scrollback without spawning a new shell until the user clicks Start Shell.
  • Correctness-only exit signals: if a terminal exits while its pane isn't mounted (no stream subscription), we still clear stuck agent lifecycle indicators (working/permissionidle). No new background "terminal output" notifications/indicators are introduced in this PR.

Recovery / management

Settings → Terminal → Manage sessions:

  • View daemon session count + list sessions
  • Kill a session / kill sessions for active workspace / kill all sessions
  • Clear terminal history (cold-restore scrollback)

Dev-only:

  • Dev → Reset Terminal State menu action for fast local reproduction/iteration.

Architecture

Runtime Abstraction

┌─────────────────────┐
│   tRPC Routers      │
└─────────┬───────────┘
          │ registry.getForWorkspaceId(id)
          ▼
┌─────────────────────┐
│ WorkspaceRuntime    │
│ Registry            │ (process-scoped, capability-based selection)
└─────────┬───────────┘
          │
          ▼
┌─────────────────────┐
│ LocalTerminalRuntime│ (adapts backend to TerminalRuntime interface)
└─────────┬───────────┘
          │ delegates to
          ▼
┌─────────────────────┐
│ DaemonTerminalMgr   │ ◄── daemon mode (persistent)
│ or TerminalManager  │ ◄── in-process (non-persistent)
└─────────────────────┘

This abstraction enables future cloud workspace providers without spreading backend-specific branching throughout the codebase. Backend selection uses capability checks (terminal.management !== null) rather than instanceof checks.

Key modules:

  • apps/desktop/src/main/lib/workspace-runtime/ - Runtime abstraction layer (types, registry, local adapter)
  • apps/desktop/src/main/lib/terminal/daemon-manager.ts - Daemon-backed terminal manager
  • apps/desktop/src/main/lib/terminal/manager.ts - In-process terminal manager

Daemon Mode

┌─────────────────────┐
│   Electron Main     │
│  (DaemonManager)    │◄──── tRPC calls from renderer
└─────────┬───────────┘
          │ Unix socket (NDJSON; control + stream)
          ▼
┌─────────────────────┐
│  Terminal Host      │
│  Daemon Process     │ (detached, survives app restart)
└─────────┬───────────┘
          │ Binary framing (optimized for escape sequences)
          ▼
┌─────────────────────┐
│  PTY Subprocess 1   │ (one per terminal)
├─────────────────────┤
│  PTY Subprocess 2   │
└─────────────────────┘

Daemon components:

  • apps/desktop/src/main/terminal-host/ - Daemon process (runs as ELECTRON_RUN_AS_NODE)
  • apps/desktop/src/main/lib/terminal-host/client.ts - Client for Electron main → daemon

Key DX/Performance Changes (Hardening)

  • Renderer mount policy: bounded warm set (WARM_TERMINAL_TAB_LIMIT = 8) in apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • Attach scheduling (renderer): concurrency cap (MAX_CONCURRENT_ATTACHES = 3) in apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/attach-scheduler.ts
  • Main-process limiter: priority-based createOrAttach limiter (max 3) to prevent accidental attach fan-out
  • Daemon spawn limiter: limit concurrent new PTY spawns (max 3, permit held until ready/timeout)
  • Cold restore semantics: daemon session existence check (via listSessions) + skipColdRestore support to avoid spawning during cold restore
  • tRPC stream lifecycle: terminal.stream does not complete on exit so stable pane subscriptions survive session loss/restart (fixes the listeners=0 cold-restore regression)
  • Cold restore Start Shell guard: drop any queued pre-restore stream events and ignore terminal input while overlays are visible (prevents stale exit from clearing the terminal UI)
  • History writer init fix: avoid a buffer replay loop that could hang new terminal creation (RangeError: Invalid array length).
  • Byte-accurate scrollback caps: enforce caps using UTF-8 byte counts + safe suffix truncation (prevents multi-byte output from exceeding limits)
  • Worktree cwd guard: worktree terminals require a usable worktree path (no fallback to $HOME)
  • History file permissions: create terminal-history files with 0o600 (defense in depth)
  • Unattached exit signal: daemon broadcasts exit lifecycle event even when no client is attached; main forwards via notifications subscription (terminal-exit)

Cold Restore Regression Fix (listeners=0 / blank terminal after Start Shell)

Symptom

  • After daemon restart/session loss: cold restore overlay shows, clicking Start Shell creates a new session but terminal stays blank and input goes to the tab title.
  • Main-process logs show daemon output arriving but listeners=0 on data:${paneId}.

Root cause

  • Server-side terminal.stream completed the observable on exit (emit.complete()).
  • The renderer subscribes with a stable paneId key; @trpc/react-query does not auto-resubscribe after completion unless the input changes.
  • We reuse the same paneId across restarts/cold restore → the pane becomes permanently detached from output.

Fix

  • Keep the terminal.stream subscription open across exit (treat exit as a state transition).
  • In cold restore UI, drop queued pre-restore stream events before starting a new shell and ignore terminal input while overlays are visible (prevents stale queued exit triggering an unintended restart/clear).
  • Documented in apps/desktop/docs/TERMINAL_HOST_EVENTS.md and covered by regression test apps/desktop/src/lib/trpc/routers/terminal/terminal.stream.test.ts.

Verification Matrix

Optional debug signals:

  • Main/Electron: SUPERSET_TERMINAL_DEBUG=1
  • Renderer DevTools: localStorage.setItem('SUPERSET_TERMINAL_DEBUG', '1')

Non-daemon (default / main parity)

  • Tab switching: terminal persists across tab/workspace switches (no remount flicker)
  • Resize: correct cols/rows; no render glitches
  • Exit + restart: exit shows restart prompt; any key restarts and output resumes
  • Clear scrollback: clears visible buffer; subsequent output renders normally
  • TUI / alt-screen (vim/codex): no keyboard-driven auto-title while in alt screen; escape-based titles still work
  • Large paste: bracketed paste envelope preserved; no dropped chars

Daemon mode (warm attach)

  • Many panes + restart app: UI interactive quickly (no startup freeze)
  • Active pane attaches first; warm set attaches progressively; others stay detached
  • Heavy split tab: panes attach progressively; app remains responsive (max 3 concurrent attaches)
  • Status correctness: exit while pane not mounted clears working/permissionidle
  • Settings → Terminal → Manage sessions: list sessions, kill session(s), clear history

Daemon mode (cold restore)

  • Daemon absent + history on disk: cold restore scrollback shows without spawning a shell
  • Start Shell: output resumes; input goes to terminal (not tab title); no listeners=0 state
  • Overlay typing safety: typing while overlay is visible does not clear the terminal after Start Shell

Failure & recovery

  • Kill daemon while attached: connection error shown; app stays stable
  • Retry/reconnect: session reattaches or cold restore flow works; no stuck isExited state

Backpressure / correctness

  • Flood output (e.g. yes | head -n 50000): UI stays responsive; no crash/OOM
  • Workspace deletion with active terminals: no error toast flood; sessions exit/cleanup cleanly

Validation

  • bun run typecheck
  • bun run lint
  • NODE_ENV=test bun test

Known Limitations / Follow-ups

  • No "notify me when pnpm test finishes" or general background terminal output indicators (out of scope; evaluate in follow-up PR based on feedback).
  • Warm set sizing is a tradeoff; currently fixed at 8 (per-run, not persisted).

Summary by CodeRabbit

  • New Features

    • Terminal persistence with cold-restore (scrollback/snapshots) and a Terminal Settings page to manage persistence, sessions, clear history, and kill sessions
    • Background terminal host for persistent sessions and improved session listing/management
  • Improvements

    • More reliable reconnect, backpressure handling, and smoother terminal restoration UX
    • Session lifecycle overlays (restored, killed, connection errors) and better observability
  • Documentation

    • Terminal host and runtime docs added
  • Tests

    • Extensive integration/unit tests for host, emulator, and streaming behavior

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Adds terminal persistence and a daemon-backed terminal host: new IPC protocol and daemon, headless emulator, PTY subprocess, history persistence, workspace runtime abstraction, renderer/UX changes for cold-restore and persistent terminal tabs, TRPC endpoints and settings, and many tests and migrations.

Changes

Cohort / File(s) Summary
Daemon & IPC
apps/desktop/src/main/terminal-host/index.ts, .../client.ts, .../types.ts, .../pty-subprocess.ts, .../pty-subprocess-ipc.ts, .../terminal-host.ts, .../session.ts, .../session-lifecycle.test.ts, .../daemon.test.ts
New Terminal Host daemon, two-socket control/stream protocol, NDJSON framing + auth, PTY subprocess framing, request/response and event contracts, daemon lifecycle and tests.
Headless Emulator & Session
apps/desktop/src/main/lib/terminal-host/headless-emulator.ts, .../headless-emulator.test.ts, apps/desktop/src/main/terminal-host/session.ts, apps/desktop/src/main/terminal-host/session-lifecycle.test.ts
Headless xterm wrapper with snapshot/rehydration, OSC-7 CWD tracking, buffering, query support, and session lifecycle integrating PTY subprocess and emulator; tests included.
History & Write Queue
apps/desktop/src/main/lib/terminal-history.ts, apps/desktop/src/main/lib/terminal/pty-write-queue.ts
Disk-backed terminal history (HistoryWriter/HistoryReader) and PtyWriteQueue for backpressure-aware PTY writes.
Daemon Manager & Host Integration
apps/desktop/src/main/lib/terminal/daemon-manager.ts, apps/desktop/src/main/lib/terminal/terminal-host/*
DaemonTerminalManager delegating to daemon, reconciliation, session lifecycle, history wiring, public manager API and singleton helpers.
Workspace Runtime Abstraction
apps/desktop/src/main/lib/workspace-runtime/{types,registry,local,index}.ts, apps/desktop/src/main/lib/terminal/index.ts
New workspace runtime registry and LocalWorkspaceRuntime adapting in-process vs daemon backends; isDaemonModeEnabled and runtime selection APIs.
TRPC Router & Terminal API
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts, .../settings/index.ts, .../notifications.ts
createOrAttach enhanced (skipColdRestore, allowKilled), ackColdRestore, stream emits disconnect/error, new daemon management endpoints, and settings endpoints for terminalPersistence.
Renderer: Terminal & Helpers
apps/desktop/src/renderer/.../Terminal/Terminal.tsx, helpers.ts, types.ts, attach-scheduler.ts, hooks/useTerminalConnection.ts
Terminal refactor: isTabVisible prop, attach scheduler, cold-restore overlays and flows, renderer selection, chunked paste, and stable useTerminalConnection hook.
Tabs / Persistence UI
apps/desktop/src/renderer/.../TabsContent/index.tsx, TabView/{index.tsx,TabPane.tsx}, routes/_authenticated/settings/terminal/page.tsx, .../GeneralSettings.tsx
Bounded multi-mount terminal persistence rendering, settings page to toggle persistence and manage daemon sessions, and settings sidebar entry.
Port Manager & Integration
apps/desktop/src/main/lib/terminal/port-manager.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/{branch,delete}.ts, apps/desktop/src/lib/trpc/routers/projects/projects.ts
Port hint detection, daemon session tracking in PortManager, and wiring of workspace-scoped runtime for workspace/project flows.
App Startup, Env & Dev Utilities
apps/desktop/src/main/index.ts, .../app-environment.ts, .../local-db/index.ts, .../terminal/dev-reset.ts, .../menu.ts, .../windows/main.ts
Ensure superset home dir permissions, pre-auth reconcile/shutdown orphaned daemon on startup, DB permission handling, dev reset utility, and notifications wiring for terminal-exit.
Renderer State & Tracking
apps/desktop/src/renderer/lib/terminal-kill-tracking.ts, apps/desktop/src/renderer/stores/tabs/..., apps/desktop/src/renderer/stores/hotkeys/store.ts, apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts, apps/desktop/src/renderer/screens/.../TabView/*
In-memory kill-tracking, minor store imports/guards, and agent handling for TERMINAL_EXIT.
Schema & Migrations
packages/local-db/drizzle/0011_add_terminal_persistence.sql, packages/local-db/src/schema/schema.ts, packages/local-db/drizzle/meta/_journal.json, packages/local-db/drizzle/meta/0011_snapshot.json
Adds terminal_persistence boolean column and migration/meta updates.
Build & Constants
apps/desktop/electron.vite.config.ts, apps/desktop/src/shared/constants.ts
Rollup entries for terminal-host and pty-subprocess, new DEFAULT_TERMINAL_PERSISTENCE and TERMINAL_EXIT notification constant.
Docs & Plans
apps/desktop/docs/*, apps/desktop/plans/*
New documentation and planning artifacts for terminal host events, runtime architecture, and DX hardening.
Tests
many new/updated tests under apps/desktop/src/main/... and apps/desktop/src/lib/trpc/routers/terminal/terminal.stream.test.ts
Integration and unit tests covering daemon protocol, headless emulator, session lifecycle, stream behaviors, and router streaming semantics.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer (Terminal UI)
    participant TRPC as TRPC Router
    participant Registry as Workspace Runtime Registry
    participant Manager as Active Terminal Manager
    participant Daemon as Terminal Host Daemon
    participant PTYSub as PTY Subprocess
    participant History as History Persistence

    Renderer->>TRPC: createOrAttach(paneId, ...)
    TRPC->>Registry: getForWorkspaceId(workspaceId)
    Registry->>Manager: resolve terminal runtime
    Manager->>Daemon: createOrAttach (IPC) / or spawn in-process
    alt daemon mode
        Daemon->>PTYSub: spawn or attach
        PTYSub-->>Daemon: Ready / Spawned / Data / Exit
        Daemon->>History: write metadata/scrollback
        Daemon-->>TRPC: response (snapshot, isColdRestore)
    else in-process
        Manager->>History: init/write
        Manager-->>TRPC: response (scrollback, snapshot)
    end
    TRPC-->>Renderer: snapshot + metadata
    Renderer->>Renderer: rehydrate terminal UI
    loop runtime
        PTYSub->>Daemon: output
        Daemon-->>TRPC: emit data event
        TRPC-->>Renderer: stream data event
    end
Loading
sequenceDiagram
    participant App as Electron App
    participant Registry as Workspace Runtime Registry
    participant LocalRT as LocalWorkspaceRuntime
    participant InProc as In-Process Manager
    participant DaemonMgr as DaemonTerminalManager

    App->>Registry: getWorkspaceRuntimeRegistry()
    Registry->>Registry: getDefault()
    Registry->>LocalRT: instantiate LocalWorkspaceRuntime
    LocalRT->>InProc: use TerminalManager (if daemon off)
    LocalRT->>DaemonMgr: use DaemonTerminalManager (if daemon on)
    LocalRT-->>App: return runtime with capabilities
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

Possibly related PRs

Suggested reviewers

  • AviPeltz

Poem

🐰 I dug a socket in the ground, a cozy, secret lair,

A token, frames, and headless blooms—terminals sleep there.
Scrollback tucked like carrots, snapshots snug and bright,
Daemon hums a lullaby through every quiet night.
Hop, reboot, restore — the shells wake up with cheer.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(desktop): terminal persistence via daemon process' clearly and concisely describes the main change: implementing terminal persistence using a background daemon process.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a clear summary, user-visible behavior, architecture diagrams, key DX/performance changes, cold-restore regression fix explanation, verification matrix, and validation steps. All major sections from the template are covered with detailed content.
Linked Issues check ✅ Passed The PR addresses the core objectives from linked issue #518: terminal session persistence with CWD restore, scrollback restoration, safe lifecycle management, cold-restore semantics, and many hardening improvements. While #518 specified a tmux-based approach, this PR implements a Superset-owned daemon alternative that achieves the same user-visible goals and satisfies the stated requirements.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing terminal persistence via daemon process and hardening the feature. File additions span the daemon implementation (terminal-host/), runtime abstraction (workspace-runtime/), enhanced managers, renderer scheduling/persistence logic, database schema for settings, documentation, tests, and supporting infrastructure—all in scope for the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI Agents
In @apps/desktop/src/main/lib/terminal-host/headless-emulator.ts:
- Around line 93-97: The anonymous callback passed to terminal.onData is missing
an explicit parameter type; change the handler to declare the data parameter
type (e.g., update this.terminal.onData((data: string) => { ... })) and ensure
related members (pendingOutput array and onDataCallback) are typed accordingly
(e.g., pendingOutput: string[] and onDataCallback?: (data: string) => void) so
TypeScript no longer reports TS7006.
- Around line 10-11: The build is failing because @xterm/headless is imported
(Terminal in headless-emulator.ts) but not listed in apps/desktop/package.json;
add "@xterm/headless" with a matching version to the other @xterm packages in
the apps/desktop/package.json dependencies (or update workspace manifest if
using workspaces), run yarn/npm install to update lockfile, and ensure imports
like `Terminal` continue to resolve.

In @apps/desktop/src/main/lib/terminal/daemon-manager.ts:
- Around line 159-163: The session cleanup timer created with setTimeout (using
SESSION_CLEANUP_DELAY_MS) isn't tracked, so its callback may run after the
manager is disposed and mutate this.sessions; to fix, store the timeout ID when
scheduling the cleanup (e.g., a new map like sessionCleanupTimers keyed by
paneId) and replace the anonymous setTimeout with one that saves the ID; then
update cleanup() to iterate and clearTimeout for any pending timers and remove
their entries before disposing so sessions.delete(paneId) cannot run on a
disposed manager.

In @apps/desktop/src/main/terminal-host/daemon.test.ts:
- Around line 89-141: The startDaemon() helper can race between the
DAEMON_TIMEOUT timer and other resolve/reject paths; fix it by tracking the
timeoutId returned from setTimeout and a local settled flag, clearing the
timeout (clearTimeout(timeoutId)) and marking settled=true when resolving or
rejecting, and early-return from subsequent handlers if settled; also remove or
cleanup listeners on daemonProcess (stdout/stderr/error/exit) when settling to
avoid multiple callbacks. Ensure this logic is applied inside startDaemon(),
referencing the DAEMON_TIMEOUT, daemonProcess, output, and the resolve/reject
handlers so the timeout is cleared on success and duplicate settles are
prevented.

In @apps/desktop/src/main/terminal-host/index.ts:
- Around line 218-240: The createOrAttach handler is async but handleRequest
currently invokes it without awaiting, so post-await errors escape the
try/catch; update handleRequest to await the handler invocation (e.g., await
handler(socket, id, payload, clientState)) so thrown/rejected errors are caught,
and ensure any caller of handleRequest (the socket data event handler) is either
made async and awaits handleRequest or properly handles the returned promise to
propagate rejections into the existing try/catch; apply the same awaiting
pattern for other async handlers invoked by handleRequest.

In @apps/desktop/src/main/terminal-host/session-lifecycle.test.ts:
- Around line 91-139: The startDaemon() promise can call resolve/reject multiple
times because the DAEMON_TIMEOUT timer and process event listeners remain active
after settlement; update startDaemon to store the timeout ID,
clearTimeout(timeoutId) immediately after calling resolve() or reject(), and
remove or noop the process listeners (daemonProcess.stdout?.off/on or use once)
to prevent further callbacks; ensure the timeout is set to a variable (e.g.,
const timeoutId = setTimeout(...)) and is cleared inside the stdout 'Daemon
started' handler, the 'error' handler, and the 'exit' handler so the promise
cannot attempt to settle after it has already resolved via
startDaemon/daemonProcess/DAEMON_TIMEOUT.
- Around line 189-219: The sendRequest function creates a 5s timeout but never
clears it on successful response; modify sendRequest to store the timeout id
(from setTimeout) in a variable and call clearTimeout(timeoutId) immediately
before resolve(JSON.parse(line)) and also before rejecting on parse error, and
ensure the timeout handler removes the data listener (socket.off("data",
onData)) as it already does so when expiring.

In @apps/desktop/src/main/terminal-host/session.ts:
- Around line 611-644: The timeout cleanup in flushToSnapshotBoundary
incorrectly clears the entire snapshotBoundaryWaiters array; instead remove only
the specific waiter added for this call so concurrent flushToSnapshotBoundary
callers aren't affected. Change the boundaryPromise creation to capture the
resolve function (e.g. const resolveFn = resolve), push resolveFn into
snapshotBoundaryWaiters, and on timeout/filter cleanup remove only that
resolveFn from snapshotBoundaryWaiters (e.g. this.snapshotBoundaryWaiters =
this.snapshotBoundaryWaiters.filter(r => r !== resolveFn)); also update the
misleading comment to state we remove only our waiter; ensure this logic works
with processEmulatorWriteQueue, scheduleEmulatorWrite, and snapshotBoundaryIndex
as currently used.
🧹 Nitpick comments (26)
apps/desktop/src/renderer/stores/hotkeys/store.ts (1)

301-307: Refine the null check and resolve type inconsistency.

The guard works but has two issues:

  1. Imprecise check: if (!state) uses truthiness, catching null, undefined, 0, false, and empty string. For an object type, prefer an explicit null check:

    if (state == null) { // catches both null and undefined
  2. Type mismatch: The callback is typed (state: HotkeysState) but the guard implies the query can return null/undefined. Either:

    • Update the type: .then((state: HotkeysState | null | undefined) => {
    • Or fix the upstream query/storage to guarantee non-null returns

The defensive guard suggests data quality concerns in the storage layer that may warrant investigation.

🔎 Proposed refinement
 trpcClient.uiState.hotkeys.get
   .query()
-  .then((state: HotkeysState) => {
+  .then((state: HotkeysState | null | undefined) => {
     // Guard against null/undefined state from storage
-    if (!state) {
+    if (state == null) {
       console.warn(
         "[hotkeys] Storage returned null/undefined state, skipping sync",
       );
       return;
     }
apps/desktop/src/main/lib/terminal/port-manager.ts (3)

333-336: Inconsistent handling of standalone \r characters.

The newline index calculation treats \r as a line terminator (via Math.max), but the actual line splitting on line 371 uses /\r?\n/ which only handles \r\n or \n, not standalone \r (classic Mac line endings).

This could cause issues:

  • If output contains "foo\rbar\n", the lastNewlineIndex would be 7 (the \n), which is correct.
  • But if output is "foo\rbar" with no \n, lastNewlineIndex would be 3 (the \r), buffering "bar" as incomplete, and "foo" would be processed as a complete line—which is likely fine.
  • However, if the pattern spans across the \r boundary, it could be missed.

Consider using a consistent regex for both operations, or simplifying to only track \n:

🔎 Suggested simplification
-		const lastNewlineIndex = Math.max(
-			combined.lastIndexOf("\n"),
-			combined.lastIndexOf("\r"),
-		);
+		const lastNewlineIndex = combined.lastIndexOf("\n");

358-368: Heuristic may miss some port patterns.

The relevance check doesn't cover all keywords from PORT_PATTERNS. Patterns like "bound to port", "binding to", "Serving HTTP on", and "Tomcat" might be missed if they don't co-occur with the sampled keywords.

Consider aligning the heuristic with PORT_PATTERNS for completeness:

🔎 Suggested enhancement
 		const looksRelevant =
-			/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|https?:\/\/|listening|started|ready|running|\bon port\b)/i.test(
+			/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|https?:\/\/|listening|started|ready|running|bound|binding|serving|\bon port\b)/i.test(
 				sample,
 			);

360-363: Extract magic numbers to named constants.

The values 4096 and 2048 for sampling thresholds should be extracted to named constants for clarity and consistency with MAX_LINE_BUFFER. Per coding guidelines, magic numbers should be extracted to module-level constants.

🔎 Suggested refactor
 // Max buffer size for incomplete lines (bytes) - prevents memory issues with pathological input
 const MAX_LINE_BUFFER = 4096;
+
+// Threshold for sampling output during relevance check (chars)
+const RELEVANCE_SAMPLE_THRESHOLD = 4096;
+const RELEVANCE_SAMPLE_SIZE = 2048;

Then update the usage:

 		const sample =
-			completePart.length > 4096
-				? `${completePart.slice(0, 2048)}${completePart.slice(-2048)}`
+			completePart.length > RELEVANCE_SAMPLE_THRESHOLD
+				? `${completePart.slice(0, RELEVANCE_SAMPLE_SIZE)}${completePart.slice(-RELEVANCE_SAMPLE_SIZE)}`
 				: completePart;
apps/desktop/src/renderer/screens/main/components/SettingsView/SettingsContent.tsx (1)

16-28: Consider: Refactor conditionals to a lookup object.

Per coding guidelines, lookup objects are preferred over repeated if (type === ...) conditionals. This is optional since the current pattern is established across the file.

🔎 Example refactor
const SETTINGS_COMPONENTS: Record<SettingsSection, React.ComponentType> = {
	account: AccountSettings,
	project: ProjectSettings,
	workspace: WorkspaceSettings,
	appearance: AppearanceSettings,
	ringtones: RingtonesSettings,
	keyboard: KeyboardShortcutsSettings,
	presets: PresetsSettings,
	behavior: BehaviorSettings,
	terminal: TerminalSettings,
};

export function SettingsContent({ activeSection }: SettingsContentProps) {
	const Component = SETTINGS_COMPONENTS[activeSection];
	return (
		<div className="h-full overflow-y-auto flex justify-center">
			{Component && <Component />}
		</div>
	);
}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (2)

52-71: Consider scoping mounted terminals to current workspace.

The current implementation renders all tabs across all workspaces, keeping even inactive workspace terminals mounted. While this prevents remount issues, it's more aggressive than necessary and compounds the "increased memory with many terminals" limitation noted in the PR.

Suggestion: Filter to only render tabs from the current workspace:

-{allTabs.map((tab) => {
+{currentWorkspaceTabs.map((tab) => {

This would:

  • Still prevent TUI white screen issues when switching tabs within a workspace
  • Reduce memory footprint by unmounting terminals from inactive workspaces
  • Make the visible/hidden logic simpler (only need to check tab.id === activeTabId)

If keeping all workspaces mounted is intentional, please add a comment explaining why tabs from inactive workspaces must remain mounted.


10-11: Consider handling loading state explicitly to prevent potential flicker.

The terminalPersistence query has no loading state check. During initial load, terminalPersistence will be undefined, causing the component to render non-persistence mode (line 78-90), then potentially switch to persistence mode once loaded.

If the setting loads quickly this may not be noticeable, but with slower loads it could cause a visual flicker or re-mount of terminals.

🔍 Optional: Add explicit loading check
 const { data: terminalPersistence } =
   trpc.settings.getTerminalPersistence.useQuery();
+const { isLoading: isLoadingPersistence } =
+  trpc.settings.getTerminalPersistence.useQuery();
 
 // ... 

-if (terminalPersistence) {
+if (isLoadingPersistence) {
+  return null; // or loading skeleton
+}
+
+if (terminalPersistence) {

Alternatively, if the setting is expected to load synchronously from local SQLite, this may not be necessary.

Also applies to: 36-36

apps/desktop/src/main/terminal-host/pty-subprocess.ts (2)

281-291: Sensitive environment variables may be logged.

The spawn debug logging includes ZDOTDIR, SUPERSET_ORIG_ZDOTDIR, and a prefix of PATH. While useful for debugging, this could inadvertently log sensitive environment variables in production. Consider gating this behind DEBUG_OUTPUT_BATCHING or a separate debug flag.

🔎 Proposed fix
-	// Debug: Log spawn parameters
-	console.error("[pty-subprocess] Spawning PTY:", {
-		shell: msg.shell,
-		args: msg.args,
-		cwd: msg.cwd,
-		cols: msg.cols,
-		rows: msg.rows,
-		ZDOTDIR: msg.env.ZDOTDIR,
-		SUPERSET_ORIG_ZDOTDIR: msg.env.SUPERSET_ORIG_ZDOTDIR,
-		PATH_start: msg.env.PATH?.substring(0, 100),
-	});
+	if (DEBUG_OUTPUT_BATCHING) {
+		console.error("[pty-subprocess] Spawning PTY:", {
+			shell: msg.shell,
+			args: msg.args,
+			cwd: msg.cwd,
+			cols: msg.cols,
+			rows: msg.rows,
+		});
+	}

410-431: handleDispose skips graceful shutdown.

Unlike handleKill, handleDispose immediately sends SIGKILL without first attempting SIGTERM. This is acceptable for disposal semantics (immediate cleanup), but consider if a brief graceful window would be beneficial for processes that can save state.

apps/desktop/src/main/terminal-host/daemon.test.ts (1)

45-222: Significant code duplication with session-lifecycle.test.ts.

The cleanup(), startDaemon(), stopDaemon(), connectToDaemon(), and sendRequest() functions are nearly identical between this file and session-lifecycle.test.ts. Consider extracting these into a shared test utilities module.

🔎 Proposed approach

Create a shared test utilities file:

// apps/desktop/src/main/terminal-host/__tests__/daemon-test-utils.ts
export const SUPERSET_DIR_NAME = ".superset-dev";
export const SUPERSET_HOME_DIR = join(homedir(), SUPERSET_DIR_NAME);
// ... other constants

export function cleanup(): void { /* ... */ }
export function startDaemon(): Promise<void> { /* ... */ }
export function stopDaemon(daemonProcess: ChildProcess | null): Promise<void> { /* ... */ }
export function connectToDaemon(): Promise<Socket> { /* ... */ }
export function sendRequest(socket: Socket, request: IpcRequest): Promise<IpcResponse> { /* ... */ }

Then import in both test files:

import { cleanup, startDaemon, stopDaemon, connectToDaemon, sendRequest } from "./__tests__/daemon-test-utils";
apps/desktop/src/main/lib/terminal/manager.ts (1)

413-426: Write return value ignored in refreshPromptsForWorkspace.

writeQueue.write() returns false when the queue is full, but this return value is ignored. While unlikely for a single newline to exceed the queue, the inconsistency with the strict error-throwing in write() at line 176 is worth noting.

🔎 Proposed fix for consistency
 refreshPromptsForWorkspace(workspaceId: string): void {
   for (const [paneId, session] of this.sessions.entries()) {
     if (session.workspaceId === workspaceId && session.isAlive) {
       try {
-        session.writeQueue.write("\n");
+        if (!session.writeQueue.write("\n")) {
+          console.warn(
+            `[TerminalManager] Queue full when refreshing prompt for pane ${paneId}`,
+          );
+        }
       } catch (error) {
         console.warn(
           `[TerminalManager] Failed to refresh prompt for pane ${paneId}:`,
           error,
         );
       }
     }
   }
 }
apps/desktop/src/main/lib/terminal/daemon-manager.ts (2)

263-287: Async operation in writeToHistory initiated without awaiting or error boundary.

Lines 273-280 call initHistoryWriter without awaiting and only attach a .catch(() => {}) which silently swallows errors. While this is fire-and-forget, consider at minimum logging the error for debuggability.

🔎 Suggested improvement
 			this.initHistoryWriter(
 				paneId,
 				session.workspaceId,
 				session.cwd,
 				80,
 				24,
 				contentAfterClear || undefined,
-			).catch(() => {});
+			).catch((error) => {
+				console.error(
+					`[DaemonTerminalManager] Failed to reinit history after clear for ${paneId}:`,
+					error,
+				);
+			});

685-693: Hardcoded dimensions (80x24) when reinitializing history after clearScrollback.

These magic numbers should ideally come from the current session dimensions rather than hardcoded defaults.

🔎 Suggested improvement

Consider storing current dimensions in SessionInfo or querying the daemon for current terminal size:

+	// Get current dimensions from daemon if available
+	// Fallback to standard defaults
+	const currentCols = 80; // TODO: track actual dimensions
+	const currentRows = 24;
 	await this.initHistoryWriter(
 		paneId,
 		session.workspaceId,
 		session.cwd,
-		80,
-		24,
+		currentCols,
+		currentRows,
 		undefined,
 	);
apps/desktop/src/main/terminal-host/index.ts (4)

51-61: Duplicate SUPERSET_DIR_NAME/SUPERSET_HOME_DIR definitions.

These constants are defined in both this file and app-environment.ts. Consider importing from a shared location to avoid drift.

Based on the relevant code snippets, SUPERSET_HOME_DIR is already exported from apps/desktop/src/main/lib/app-environment.ts. Consider importing it:

-const SUPERSET_DIR_NAME =
-	process.env.NODE_ENV === "development" ? ".superset-dev" : ".superset";
-const SUPERSET_HOME_DIR = join(homedir(), SUPERSET_DIR_NAME);
+import { SUPERSET_HOME_DIR } from "../lib/app-environment";

Note: The daemon runs as a separate process, so verify the import path works in the bundled daemon script.


85-102: Auth token generation and validation are secure.

  • 32 bytes (256 bits) of randomness is cryptographically strong
  • Token file permissions (0o600) restrict access to owner only
  • Timing-safe comparison should be used for token validation to prevent timing attacks
🔎 Use timing-safe comparison for token validation
+import { timingSafeEqual } from "node:crypto";

 function validateToken(token: string): boolean {
-	return token === authToken;
+	if (token.length !== authToken.length) {
+		return false;
+	}
+	try {
+		return timingSafeEqual(Buffer.from(token), Buffer.from(authToken));
+	} catch {
+		return false;
+	}
 }

353-376: Shutdown handler sends response before exit but uses fixed delay.

The 100ms delay is arbitrary. If the response write is slow, the process may exit before the client receives it.

Consider using a callback on the socket write to ensure the response is flushed:

 	// Send success response before shutting down
-	sendSuccess(socket, id, { success: true });
+	socket.write(`${JSON.stringify({ id, ok: true, payload: { success: true } })}\n`, () => {
+		// Kill sessions if requested
+		if (request.killSessions) {
+			terminalHost.killAll({ deleteHistory: false });
+		}
+		stopServer().then(() => process.exit(0));
+	});

-	// Kill sessions if requested
-	if (request.killSessions) {
-		terminalHost.killAll({ deleteHistory: false });
-	}
-
-	// Schedule shutdown after a brief delay to allow response to be sent
-	setTimeout(() => {
-		stopServer().then(() => process.exit(0));
-	}, 100);

445-471: isSocketLive uses require instead of import.

Line 452 uses dynamic require("node:net") when Socket is already imported at the top of the file.

🔎 Proposed fix
-		const testSocket = new (require("node:net").Socket)();
+		const { Socket: NetSocket } = await import("node:net");
+		const testSocket = new NetSocket();

Or since Socket is already imported, use it directly (though you'd need the Socket class, not the type):

+import { createServer, type Server, Socket } from "node:net";
 // ...
-		const testSocket = new (require("node:net").Socket)();
+		const testSocket = new Socket();
apps/desktop/src/main/lib/terminal/index.ts (1)

74-83: Console log on every getActiveTerminalManager call may be noisy.

Line 78 logs every time this function is called, which could happen frequently. Consider moving the log to only fire when the cached value is first determined.

🔎 Reduce logging noise
 export function getActiveTerminalManager():
 	| TerminalManager
 	| DaemonTerminalManager {
 	const daemonEnabled = isDaemonModeEnabled();
-	console.log("[getActiveTerminalManager] Daemon mode enabled:", daemonEnabled);
 	if (daemonEnabled) {
 		return getDaemonTerminalManager();
 	}
 	return terminalManager;
 }

The mode is already logged in isDaemonModeEnabled() when the cached value is set.

apps/desktop/src/main/lib/terminal-host/client.ts (2)

63-79: Duplicate SUPERSET path constants across files.

Same issue as in the daemon index.ts - these paths are defined in multiple places. As per coding guidelines, consider extracting to a shared constants module.


591-627: Daemon log file helps debugging but file descriptor may leak.

Line 595 opens a file descriptor for logging but it's never explicitly closed. The spawn with detached: true should handle this, but consider using a helper that manages the fd lifecycle.

The fd is passed to the child and the child is unref'd, so the parent doesn't need to manage it. However, explicitly closing after spawn would be cleaner:

 		child.unref();
+
+		// Close log fd - child process has its own reference
+		if (logFd >= 0) {
+			try {
+				require("node:fs").closeSync(logFd);
+			} catch {
+				// Best effort
+			}
+		}
apps/desktop/src/main/terminal-host/terminal-host.ts (1)

321-343: Recursive cleanup scheduling could run indefinitely.

If clients never detach from a dead session, scheduleSessionCleanup will reschedule itself forever every 5 seconds. Consider adding a maximum retry count.

🔎 Add cleanup retry limit
-	private scheduleSessionCleanup(sessionId: string): void {
+	private scheduleSessionCleanup(sessionId: string, retryCount = 0): void {
+		const MAX_CLEANUP_RETRIES = 12; // 1 minute max
+
 		setTimeout(() => {
 			const session = this.sessions.get(sessionId);
 			if (!session || session.isAlive) {
 				return;
 			}

 			if (session.clientCount === 0) {
 				session.dispose();
 				this.sessions.delete(sessionId);
+			} else if (retryCount >= MAX_CLEANUP_RETRIES) {
+				console.warn(
+					`[TerminalHost] Force disposing session ${sessionId} after max retries (clients still attached: ${session.clientCount})`,
+				);
+				session.dispose();
+				this.sessions.delete(sessionId);
 			} else {
-				this.scheduleSessionCleanup(sessionId);
+				this.scheduleSessionCleanup(sessionId, retryCount + 1);
 			}
 		}, 5000);
 	}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (2)

48-77: Consider extracting the CreateOrAttachResult type to a shared location.

This inline type duplicates structure that likely exists in the backend types (apps/desktop/src/main/lib/terminal-host/types.ts). Consider importing from there or from a shared types file to maintain a single source of truth.


657-660: Verify !xtermRef.current condition doesn't cause silent failures.

In handleStreamData, when !xtermRef.current or !isStreamReadyRef.current, events are queued. However, if xtermRef.current becomes null permanently (e.g., after cleanup), events will accumulate in pendingEventsRef indefinitely without being processed or cleared.

Consider adding a guard in the cleanup to clear pending events, or add a size limit to prevent unbounded growth.

🔎 Suggested addition to cleanup
 return () => {
   isUnmounted = true;
+  // Clear pending events to prevent memory leak if component never remounts
+  pendingEventsRef.current = [];
   // ... rest of cleanup
apps/desktop/src/main/terminal-host/session.ts (2)

161-252: Subprocess spawn logic has a potential issue with env filtering.

The processEnv is built by filtering process.env, but then spawn() uses { ...process.env, ELECTRON_RUN_AS_NODE: "1" } directly (line 195-198), ignoring the carefully filtered processEnv. The filtered env is only used in pendingSpawn which is sent to the subprocess later.

This appears intentional (subprocess needs ELECTRON_RUN_AS_NODE to run as Node, while the PTY process spawned inside gets the filtered env), but the code flow is confusing. Consider adding a clarifying comment.

🔎 Suggested clarifying comment
+		// Note: We use process.env + ELECTRON_RUN_AS_NODE for the subprocess itself
+		// (it needs to run as Node.js). The filtered processEnv is passed via IPC
+		// to the PTY spawn inside the subprocess.
 		this.subprocess = spawn(electronPath, [subprocessPath], {
 			stdio: ["pipe", "pipe", "inherit"], // pipe stdin/stdout, inherit stderr
 			env: {
 				...process.env,
 				ELECTRON_RUN_AS_NODE: "1",
 			},
 		});

808-848: Dispose method is thorough but has a minor issue.

The killTimer.unref() prevents the daemon from staying alive just for this timer, which is good. However, after this.subprocess = null on line 825, the subprocess reference captured in the closure (line 814) is still valid, so the SIGKILL fallback works correctly.

One issue: if sendDisposeToSubprocess() returns false (queue full), the subprocess might not receive the dispose command, but we still set this.subprocess = null. The fallback SIGKILL after 1s should handle this, but consider logging a warning.

🔎 Suggested warning
 		if (this.subprocess) {
 			// Capture reference before nullifying - the timeout needs it
 			const subprocess = this.subprocess;
-			this.sendDisposeToSubprocess();
+			const disposeSent = this.sendDisposeToSubprocess();
+			if (!disposeSent) {
+				console.warn(`[Session ${this.sessionId}] Failed to send dispose, will force kill`);
+			}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (1)

318-431: Paste handler with chunking and bracketed paste support is comprehensive.

The implementation handles:

  • Small pastes synchronously for performance
  • Large pastes chunked with delays to prevent PTY overwhelm
  • Bracketed paste mode wrapping per-chunk (correct for TUIs that defer repainting)
  • Cancellation of in-flight pastes
  • Newline normalization (\r?\n\r)

One concern: the CHUNK_DELAY_MS = 0 in the direct write path (line 395) means chunks are processed via setTimeout(..., 0) which still yields to the event loop but may be very fast. This might still overwhelm slow PTYs. Consider using a small non-zero delay like the fallback path's 5ms.

🔎 Consider non-zero delay for direct write path
 		let offset = 0;
 		const CHUNK_CHARS = 16_384;
-		const CHUNK_DELAY_MS = 0;
+		const CHUNK_DELAY_MS = 1; // Small delay to allow PTY to process

Comment thread apps/desktop/src/main/lib/terminal-host/headless-emulator.test.ts
Comment thread apps/desktop/src/main/lib/terminal-host/headless-emulator.ts
Comment thread apps/desktop/src/main/lib/terminal-host/headless-emulator.ts
Comment thread apps/desktop/src/main/lib/terminal/daemon-manager.ts Outdated
Comment thread apps/desktop/src/main/terminal-host/daemon.test.ts
Comment thread apps/desktop/src/main/terminal-host/index.ts
Comment thread apps/desktop/src/main/terminal-host/session-lifecycle.test.ts
Comment thread apps/desktop/src/main/terminal-host/session-lifecycle.test.ts
Comment thread apps/desktop/src/main/terminal-host/session.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 6, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor Author

@andreasasprou andreasasprou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please search for all logs introduced in this pr and make sure they are appropriately retained

Comment thread apps/desktop/src/lib/trpc/routers/settings/index.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/terminal/terminal.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/terminal/terminal.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/terminal/terminal.ts Outdated
Comment thread apps/desktop/src/main/lib/terminal-host/client.ts
Comment thread apps/desktop/src/main/lib/terminal-host/client.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docs/CLOUD_WORKSPACE_PLAN.md`:
- Around line 642-690: The examples for CloudTerminal and the POST
/api/cloud-workspace/command lack validation, error handling, timeouts and
cleanup: validate workspaceId as a UUID before using it in CloudTerminal, build
the WebSocket URL with URLSearchParams, add ws.onerror and reconnection/close
handling and ensure term.dispose() runs on unmount; for the command POST use an
AbortController to enforce a timeout, wrap fetch in try/catch, check
response.ok, handle AbortError vs other errors, and document/implement
server-side command allowlisting/rate-limiting; reference CloudTerminal,
workspaceId, ws, AttachAddon, and the POST endpoint
(/api/cloud-workspace/command) when making these changes.
- Around line 692-739: The freestyleService wrapper needs production hardening:
wrap createVM, getSSHCredentials, and exec in try/catch (use logger.error on
failures), validate that tmux exists (run 'which tmux' and check exitCode) and
verify the tmux new-session result in createVM (freestyleService.createVM),
parameterize or namespace the tmux session name instead of a single "main" to
support multiple windows, implement caching and expiration for identities/tokens
(use a credentialCache keyed by vmId and ensure tokens are cleaned up or reused)
in getSSHCredentials (avoid creating identities every call), and add execution
guards in exec (freestyleService.exec) such as a Promise timeout, output size
limits, and proper error handling and logging; also consider adding simple rate
limiting or a request pool around freestyle.* API calls to prevent throttling.
- Around line 43-189: The architecture doc misses operational/reliability
details and has an outdated note about "terminal persistence" via tmux; add a
dedicated Operations & Reliability subsection covering failure modes (VM
unavailability, network failures, startup failures) and recovery behaviors,
cost/billing model (usage tracking, org limits, idle timeouts), data residency &
compliance (region choices, GDPR/CCPA handling), security posture (secret
management, multi-tenant threat model, SSH/access controls), capacity planning
(concurrent workspace limits, resource quotas, autoscaling strategy) and
degraded/local modes (how users can work offline or sync locally), and update
the terminal persistence decision to explicitly state whether tmux remains the
plan or the daemon-based solution from PR `#619` supersedes tmux, referencing the
"terminal persistence" line and PR `#619` so reviewers know which approach is
authoritative.
- Around line 620-640: The current connectCloudTerminal implementation passes a
connectionString containing the token as a process argument (visible to ps), so
update connectCloudTerminal (and trpc.cloudWorkspace.getSSHCredentials usage) to
return host, vmId and token separately, write the token to a temporary file with
restrictive permissions (0600), spawn ssh with the -i <tempKeyPath> flag and use
`${vmId}@${host}` instead of embedding the token in args, avoid logging the
token anywhere, and ensure the temp file is securely unlinked when the pty exits
(handle pty.onExit / process cleanup); also consider adding ssh options like
StrictHostKeyChecking=no and ensure tokens are short-lived or rotated by the
backend.
- Around line 398-430: The GET WebSocket handler lacks input validation,
authorization, error handling and operational controls; update the GET function
to validate workspaceId (e.g., ensure searchParams.get('workspaceId') is present
and isValidUUID), wrap freestyleService.getSSHCredentials and DB lookups
(db.query.cloudWorkspaces.findFirst) in try/catch, enforce authorization by
checking session.user.organizationId against workspace.organizationId after
auth(), return proper 400/401/404/500 responses, and add operational protections
(rate limiting/connection limits per user/workspace, input/output sanitization
for the SSH proxy, and graceful shutdown/cleanup for long-lived WebSocket
connections) while logging failures with logger.error including error and
workspaceId for observability.
- Around line 1-10: docs/CLOUD_WORKSPACE_PLAN.md is a large future-planning
document that does not belong in this terminal daemon fixes PR; move the entire
CLOUD_WORKSPACE_PLAN.md out of this branch and either (a) create a separate
documentation PR containing it, or (b) relocate the file into a dedicated
docs/planning/ or docs/rfcs/ directory and commit that separately, then update
this PR to remove the file so only the terminal kill-all reliability and UI
feedback changes remain.
- Around line 743-777: Replace the unscoped xterm addon packages listed under
"Dependencies to Add" (xterm-addon-fit, xterm-addon-attach) with their scoped
equivalents (`@xterm/addon-fit`, `@xterm/addon-attach`) and ensure you pin or verify
versions in package.json so they match the installed xterm version (xterm) per
xterm.js release notes; leave ws, ssh2, and freestyle-sandboxes unchanged and
add a brief note in the docs to verify addon/xterm version compatibility after
installation.
♻️ Duplicate comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (1)

30-31: Handle the loading state for terminalPersistence to prevent tab remounting.

When terminalPersistence is undefined (during initial load), the component renders the non-persistence path (lines 165-188). Once the query resolves to true, it switches to the persistence path (lines 87-162), causing terminal tabs to unmount and remount.

Provide a stable default using initialData or placeholderData:

-	const { data: terminalPersistence } =
-		trpc.settings.getTerminalPersistence.useQuery();
+	const { data: terminalPersistence } =
+		trpc.settings.getTerminalPersistence.useQuery(undefined, {
+			initialData: false, // or import DEFAULT_TERMINAL_PERSISTENCE from shared/constants
+		});

Alternatively, gate rendering on isLoading to prevent committing to either strategy until the setting is known.

🧹 Nitpick comments (15)
apps/desktop/src/main/lib/terminal/types.ts (1)

64-86: Consider extracting the snapshot type to a named interface.

The inline snapshot type definition is quite large and deeply nested. Extracting it to a separate named interface (e.g., DaemonSnapshot and DaemonSnapshotDebug) would improve readability and enable reuse if this structure appears elsewhere.

♻️ Suggested refactor
// At module level, before SessionResult:

export interface DaemonSnapshotDebug {
	xtermBufferType: string;
	hasAltScreenEntry: boolean;
	altBuffer?: {
		lines: number;
		nonEmptyLines: number;
		totalChars: number;
		cursorX: number;
		cursorY: number;
		sampleLines: string[];
	};
	normalBufferLines: number;
}

export interface DaemonSnapshot {
	snapshotAnsi: string;
	rehydrateSequences: string;
	cwd: string | null;
	modes: Record<string, boolean>;
	cols: number;
	rows: number;
	scrollbackLines: number;
	debug?: DaemonSnapshotDebug;
}

// Then in SessionResult:
export interface SessionResult {
	// ... other fields ...
	snapshot?: DaemonSnapshot;
}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (1)

147-159: Consider extracting the sidebar to reduce duplication.

The sidebar JSX (lines 147-159 and 174-186) is identical in both rendering branches. Extracting it to a shared fragment or restructuring the component to render the sidebar outside the conditional would reduce maintenance burden.

♻️ Optional refactor to extract sidebar
const sidebarElement = isSidebarOpen && (
  <ResizablePanel
    width={sidebarWidth}
    onWidthChange={setSidebarWidth}
    isResizing={isResizing}
    onResizingChange={setIsResizing}
    minWidth={MIN_SIDEBAR_WIDTH}
    maxWidth={MAX_SIDEBAR_WIDTH}
    handleSide="left"
  >
    <Sidebar />
  </ResizablePanel>
);

// Then use {sidebarElement} in both branches
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (4)

27-40: Potential unnecessary re-computation of sessionsSorted.

The sessions variable at line 29 creates a new array reference on every render when daemonSessions?.sessions is nullish (due to ?? []). This causes useMemo to re-run even when the underlying data hasn't changed.

♻️ Suggested improvement
+const EMPTY_SESSIONS: typeof sessions = [];
+
 function TerminalSettingsPage() {
   const utils = trpc.useUtils();
   const { data: terminalPersistence, isLoading } =
     trpc.settings.getTerminalPersistence.useQuery();

   const { data: daemonSessions } = trpc.terminal.listDaemonSessions.useQuery();
   const daemonModeEnabled = daemonSessions?.daemonModeEnabled ?? false;
-  const sessions = daemonSessions?.sessions ?? [];
+  const sessions = daemonSessions?.sessions ?? EMPTY_SESSIONS;

125-127: Extract magic number to a named constant.

The 300ms delay should be extracted to a descriptive constant at module level for clarity and maintainability.

♻️ Suggested fix

Add at top of file:

const DAEMON_CLEANUP_DELAY_MS = 300;

Then update:

-			setTimeout(() => {
-				utils.terminal.listDaemonSessions.invalidate();
-			}, 300);
+			setTimeout(() => {
+				utils.terminal.listDaemonSessions.invalidate();
+			}, DAEMON_CLEANUP_DELAY_MS);

137-141: Consider adding console logging for mutation errors.

Per coding guidelines, errors should not be swallowed silently—at minimum log them with context. While the toast shows the error to users, adding a console log with the [terminal/settings] prefix aids debugging.

♻️ Suggested improvement
 onError: (error) => {
+  console.error("[terminal/settings] Failed to clear terminal history:", error);
   toast.error("Failed to clear terminal history", {
     description: error.message,
   });
 },

Apply similar pattern to killDaemonSession.onError (line 149-153).


217-223: Extract session count threshold to a named constant.

The threshold of 20 sessions for showing the performance warning should be a named constant at module level.

♻️ Suggested fix

Add at top of file:

const SESSION_WARNING_THRESHOLD = 20;

Then update:

-							{sessions.length >= 20 && (
+							{sessions.length >= SESSION_WARNING_THRESHOLD && (
apps/desktop/docs/HANDOFF-terminal-cold-restore-bug.md (1)

18-25: Add language specifier to fenced code block.

This code block and the one at line 67 (architecture diagram) are missing language identifiers. Use text or log for log output and text for ASCII diagrams to satisfy markdown linting.

Suggested fix
-```
+```log
 [DaemonTerminalManager] Received data from daemon: paneId=..., bytes=211, listeners=1  # Working!

And for line 67:

-```
+```text
 Renderer (Terminal.tsx)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (3)

142-153: Minor: Duplicate error logging when DEBUG_TERMINAL is enabled.

When DEBUG_TERMINAL is true, errors are logged twice - once via console.warn (line 144) and once via console.error (line 151). Consider consolidating to a single log or removing the unconditional console.error since the debug log already captures the error.

Suggested consolidation
 			} catch (error) {
-				if (DEBUG_TERMINAL) {
-					console.warn("[Terminal Router] createOrAttach failed:", {
-						callId,
-						paneId,
-						durationMs: Date.now() - startedAt,
-						error: error instanceof Error ? error.message : String(error),
-					});
-				}
-				console.error("[Terminal Router] createOrAttach ERROR:", error);
+				console.error("[Terminal Router] createOrAttach failed:", {
+					callId,
+					paneId,
+					durationMs: Date.now() - startedAt,
+					error,
+				});
 				throw error;
 			}

288-307: Extract magic numbers to module-level constants.

Per coding guidelines, extract magic numbers to named constants. The retry configuration values (10, 100) are used only here, but naming them improves readability.

Suggested refactor

At module top (around line 14):

const KILL_SESSION_MAX_RETRIES = 10;
const KILL_SESSION_RETRY_DELAY_MS = 100;

Then in the function:

-			const MAX_RETRIES = 10;
-			const RETRY_DELAY_MS = 100;
+			const MAX_RETRIES = KILL_SESSION_MAX_RETRIES;
+			const RETRY_DELAY_MS = KILL_SESSION_RETRY_DELAY_MS;

335-339: Consider parallel session kills with error resilience.

Sequential kills could be slow with many sessions, and a single failure would abort remaining kills. Using Promise.allSettled would be more resilient.

Suggested improvement
-			for (const session of toKill) {
-				await terminal.kill({ paneId: session.sessionId });
-			}
+			const results = await Promise.allSettled(
+				toKill.map((session) => terminal.kill({ paneId: session.sessionId })),
+			);
+			const killedCount = results.filter((r) => r.status === "fulfilled").length;
 
-			return { daemonModeEnabled: true, killedCount: toKill.length };
+			return { daemonModeEnabled: true, killedCount };
apps/desktop/src/main/lib/terminal/manager.ts (3)

112-167: Enhanced exit handler looks good, but consider extracting magic number.

The diagnostic logging (shell, exitCode, signal, sessionDuration, cwd) is valuable for debugging. The writeQueue.dispose() is correctly placed after marking the session as not alive.

Per coding guidelines, consider extracting the cleanup delay to a named constant for clarity.

Suggested improvement
+const SESSION_CLEANUP_DELAY_MS = 5000;
+
 // Near top of file with other constants

Then at line 163:

-			const timeout = setTimeout(() => {
-				this.sessions.delete(paneId);
-			}, 5000);
+			const timeout = setTimeout(() => {
+				this.sessions.delete(paneId);
+			}, SESSION_CLEANUP_DELAY_MS);

262-272: Consider prefixing unused parameter with underscore for consistency.

The viewportY parameter is added for API compatibility with the daemon manager but is unused in non-daemon mode. For consistency with ackColdRestore (line 188), consider either prefixing it with an underscore or destructuring and ignoring it explicitly.

Suggested improvement
-	detach(params: { paneId: string; viewportY?: number }): void {
-		const { paneId } = params;
+	detach(params: { paneId: string; viewportY?: number }): void {
+		const { paneId, viewportY: _viewportY } = params;

Or alternatively, add a comment explaining the parameter is for API compatibility.


332-397: WriteQueue disposal is correctly placed; consider extracting timeout constants.

The writeQueue.dispose() calls are correctly positioned to prevent further writes before killing sessions. The escalation strategy (SIGTERM → SIGKILL → force cleanup) is sound.

Per coding guidelines, consider extracting the timeout magic numbers to named constants for clarity and maintainability.

Suggested improvement
+const SIGTERM_TIMEOUT_MS = 2000;
+const SIGKILL_TIMEOUT_MS = 500;
+
 // Near top of file with other constants
docs/CLOUD_WORKSPACE_PLAN.md (2)

501-600: Enhance implementation phases with rollback strategy and observability.

The phased approach is logical, but several production concerns are missing:

Missing phases:

  • Observability: No phase for metrics, logging, alerting, or dashboards to monitor VM health, costs, and usage.
  • Cost tracking: Phase 5 adds command API but nowhere do you implement cost tracking/billing mentioned in open questions.

Rollback strategy:

  • Line 517 (Phase 1.7): "Run migration" with bun run db:push—no mention of backup, rollback plan, or migration validation.
  • Each phase should define rollback procedures if verification fails.

Feature flag strategy:

  • No mention of feature flags to enable gradual rollout per organization or user group.
  • Launching cloud workspaces globally could be risky without controlled rollout.

Phase dependencies:

  • Phase 2 (Web Terminal) and Phase 3 (Desktop Integration) appear independent—clarify if they can run in parallel or have dependencies.

Consider adding:

  • Phase 0: Observability foundation (metrics, logs, alerts)
  • Phase 6: Monitoring dashboards and cost tracking UI
  • Phase 7: Load testing and performance optimization

45-45: Add language specifiers to fenced code blocks.

Multiple fenced code blocks are missing language specifiers, which affects syntax highlighting and rendering in some markdown viewers. Based on the static analysis hints, add specifiers to improve documentation quality:

  • Line 45: ASCII diagram → ```text
  • Lines 104, 115, 125, 135, 156: User flow descriptions → ```text or ```markdown
  • Line 436: File tree structure → ```text

Example fix for line 45:

-```
+```text
                          ┌─────────────────────────────────────┐

Also applies to: 104-104, 115-115, 125-125, 135-135, 156-156, 436-436

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 421d381 and 8999412.

📒 Files selected for processing (12)
  • apps/desktop/docs/HANDOFF-terminal-cold-restore-bug.md
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts
  • docs/CLOUD_WORKSPACE_PLAN.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
🧰 Additional context used
📓 Path-based instructions (7)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
apps/desktop/src/main/index.ts

📄 CodeRabbit inference engine (AGENTS.md)

Load environment variables from monorepo root .env in desktop app with override: true before any imports in src/main/index.ts and electron.vite.config.ts

Files:

  • apps/desktop/src/main/index.ts
🧠 Learnings (4)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/lib/trpc/routers/projects/projects.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from `trpc/server/observable` instead of async generators, as the library explicitly checks `isObservable(result)` and throws an error otherwise

Applied to files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
🧬 Code graph analysis (3)
apps/desktop/src/main/lib/terminal/types.ts (1)
apps/desktop/src/main/lib/terminal/pty-write-queue.ts (1)
  • PtyWriteQueue (29-159)
apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)
apps/desktop/src/main/lib/workspace-runtime/index.ts (1)
  • getWorkspaceRuntimeRegistry (18-18)
apps/desktop/src/main/lib/workspace-runtime/registry.ts (1)
  • getWorkspaceRuntimeRegistry (75-80)
apps/desktop/src/main/index.ts (3)
apps/desktop/src/main/lib/workspace-runtime/index.ts (1)
  • getWorkspaceRuntimeRegistry (18-18)
apps/desktop/src/main/lib/workspace-runtime/registry.ts (1)
  • getWorkspaceRuntimeRegistry (75-80)
apps/desktop/src/main/lib/terminal/index.ts (2)
  • reconcileDaemonSessions (101-116)
  • shutdownOrphanedDaemon (125-152)
🪛 LanguageTool
apps/desktop/docs/HANDOFF-terminal-cold-restore-bug.md

[style] ~123-~123: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... its own subscription management. 5. **Is React Query tearing down the subscripti...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~180-~180: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...ns 5. React component unmounting - Maybe the Terminal component is being unmount...

(REP_MAYBE)

🪛 markdownlint-cli2 (0.18.1)
docs/CLOUD_WORKSPACE_PLAN.md

45-45: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


125-125: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


135-135: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


156-156: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


436-436: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

apps/desktop/docs/HANDOFF-terminal-cold-restore-bug.md

18-18: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


67-67: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Web
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy API
  • GitHub Check: Build
  • GitHub Check: Typecheck
🔇 Additional comments (35)
apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx (1)

8-8: LGTM!

The new Terminal settings entry is cleanly integrated following the existing patterns. The import, route type extension, and section configuration are all consistent with the other settings entries.

Also applies to: 21-22, 64-68

apps/desktop/src/main/lib/terminal/types.ts (3)

1-5: LGTM!

The imports are well-organized with proper type qualifiers for type-only imports, following TypeScript best practices.


20-21: LGTM!

The writeQueue field is properly typed and documented. Making it required ensures all terminal sessions have proper backpressure handling for large writes.


100-101: LGTM!

The skipColdRestore flag is well-documented and serves a clear purpose in preventing re-triggering of cold restore detection when resuming after a cold restore.

apps/desktop/src/lib/trpc/routers/projects/projects.ts (2)

16-16: LGTM!

Import correctly uses the path alias and aligns with the workspace runtime registry pattern introduced in this PR.


781-785: Code is type-safe; no defensive handling needed.

The registry returns a WorkspaceRuntime with a guaranteed terminal property. The type system ensures getForWorkspaceId() never returns an object without a terminal member—it's a required, non-null property defined in the WorkspaceRuntime interface. The pattern is correct as written.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (5)

17-26: Well-structured constant and helper function.

Good extraction of WARM_TERMINAL_TAB_LIMIT as a named constant and clear JSDoc on the helper. The hasTerminalPane function is pure and straightforward.


44-54: Good fallback logic for hydration edge case.

The fallback chain (activeTabIds[workspaceId] → first workspace tab → null) prevents blank renders during store hydration.


66-79: MRU tracking implementation looks correct.

The effect properly maintains bounded MRU ordering. Stale tab IDs from deleted tabs are filtered out during render (lines 92-94), so no cleanup effect is strictly necessary.


117-133: Visibility toggle approach is appropriate for xterm persistence.

Using visibility: hidden instead of display: none correctly preserves xterm's measured dimensions, avoiding resize/redraw issues on tab switch. The isTabVisible prop enables components to optimize behavior when hidden.


164-172: Original behavior preserved correctly.

The fallback path maintains the existing single-active-tab rendering when persistence is disabled, ensuring backward compatibility.

apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (8)

1-16: LGTM!

Imports are well-organized and follow the coding guidelines—using tRPC from the renderer lib path, alias imports, and no Node.js modules in the renderer process.


18-20: LGTM!

Route definition follows the file-based routing convention correctly.


42-48: LGTM!

Local state management is appropriate here—zustand would be overkill for these ephemeral dialog states.


156-159: LGTM!

Simple timestamp formatting that handles edge cases appropriately for this settings UI context.


161-198: LGTM on the persistence toggle section.

Good accessibility with proper label-switch association via htmlFor/id. The disabled state correctly accounts for both loading and pending mutation states.


326-369: LGTM on kill-all confirmation dialog.

Proper confirmation flow with clear messaging about the destructive action. Dialog closes before mutation executes to prevent double-click issues.


371-414: LGTM on clear history confirmation dialog.

Clear explanation of what will be cleared and its implications. Good UX to inform users that running processes continue.


453-458: No issues found. The parameter naming is correct.

The tRPC kill mutation schema confirms the parameter should be named paneId (not sessionId). The code correctly calls killDaemonSession.mutate({ paneId: sessionId }), where sessionId is a local variable holding the value that gets passed as the paneId parameter. This is an appropriate pattern—extracting the value before clearing state and passing it with the correct parameter name.

apps/desktop/docs/HANDOFF-terminal-cold-restore-bug.md (1)

1-192: Excellent debugging documentation.

This handoff document is well-structured with clear problem description, root cause analysis, architecture overview, and reproduction steps. It will be valuable for future debugging of similar issues.

apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (5)

163-183: Good error handling for write failures.

The error handling properly distinguishes between session-not-found (emits synthetic exit) and other errors (emits structured error event). This prevents toast floods when workspaces are deleted and terminals are orphaned.


370-384: Consistent null returns for not-found cases.

Good change to return null consistently instead of mixing undefined and null. This improves type predictability for callers.


460-467: Critical fix: Stream subscription stays open across exit events.

This is the key fix documented in the handoff. By not calling emit.complete() on exit, the subscription remains active for the stable paneId, allowing data to flow after session recovery. The comment clearly explains the rationale.


481-495: Proper event listener cleanup.

The cleanup function correctly removes all four event listeners that were registered, preventing memory leaks. Good symmetry between on() and off() calls.


31-40: This pattern is intentional and correct — no changes needed.

The terminal instance is captured once at router creation time because both getWorkspaceRuntimeRegistry() and getDefault() return cached singletons for the lifetime of the Electron process. The registry explicitly caches localRuntime and only initializes it once. A daemon restart would terminate the entire process (creating a new singleton), and mode toggles don't occur dynamically at runtime. This stable reference pattern is consistent throughout the codebase (main.ts, index.ts) and is the intended design.

apps/desktop/src/main/index.ts (3)

18-22: LGTM!

The new imports correctly align with the terminal persistence architecture, bringing in the daemon session management functions and the workspace-runtime registry for terminal lifecycle management.


177-180: LGTM!

The parallel cleanup via Promise.all is appropriate since terminal and analytics shutdown are independent operations. The workspace-runtime registry abstraction correctly decouples the cleanup path from the specific terminal backend implementation.


220-227: LGTM!

The sequential calls are correctly ordered - reconcileDaemonSessions() early-returns if daemon mode is disabled, while shutdownOrphanedDaemon() early-returns if daemon mode is enabled. These are mutually exclusive cleanup paths that appropriately handle both persistence-enabled and persistence-disabled scenarios before the renderer restore runs.

apps/desktop/src/main/lib/terminal/manager.ts (6)

170-182: LGTM!

The write method correctly uses the write queue with proper error handling when the queue is full. Throwing an exception provides a clear signal to callers about backpressure, which is appropriate for preventing write backlogs.


184-190: LGTM!

The ackColdRestore method correctly serves as a no-op in non-daemon mode while maintaining interface compatibility with the daemon manager. The underscore prefix on _paneId properly indicates the unused parameter, and the JSDoc clearly explains the design rationale.


399-403: LGTM!

The async signature maintains interface compatibility with the daemon manager while the synchronous implementation remains performant. This allows seamless switching between daemon and non-daemon modes without caller changes.


409-422: LGTM!

The method correctly uses the write queue with proper error handling via try-catch. Since writeQueue.write() can throw when the queue is full, the existing error handling prevents prompt refresh failures from affecting other terminals in the workspace.


424-437: LGTM!

The detachAllListeners method correctly includes the new terminalExit event in the cleanup list, ensuring proper cleanup of the broadcast event listeners added at line 160.


439-473: LGTM!

The cleanup method correctly disposes write queues for both alive and non-alive sessions before termination. The exit promise pattern with timeout ensures cleanup doesn't hang indefinitely.

docs/CLOUD_WORKSPACE_PLAN.md (1)

433-499: LGTM: File structure is well-organized.

The proposed file structure follows good separation of concerns and aligns with the architecture. The modular approach with separate procedure files and clear client/server boundaries will aid maintainability.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment on lines +43 to +189
## Architecture

```
┌─────────────────────────────────────┐
│ GitHub │
│ (Persistent Code Storage) │
└────────────────┬────────────────────┘
git clone
git push
┌─────────────────────────────────────────────────────────────────────────┐
│ CLOUD WORKSPACE │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Freestyle VM │ │
│ │ │ │
│ │ /workspace tmux session │ │
│ │ ├── .git ├── window 0: shell │ │
│ │ ├── src/ └── window 1: dev server │ │
│ │ └── ... │ │
│ │ │ │
│ │ SOURCE OF TRUTH for active development │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ SSH Access │
│ │ │
└────────────────────────────────────┼─────────────────────────────────────┘
┌────────────────────────┼────────────────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Desktop │ │ Web │ │ Phone │
│ │ │ │ │ │
│ SSH term│ │ xterm.js│ │ Command │
│ +local │ │ via WS │ │ API │
│ sync? │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘
```

### Cloud is Source of Truth

- **Files live on cloud VM** - Edits happen directly on the VM
- **GitHub is persistent storage** - Code is pushed/pulled via standard git
- **Clients connect to cloud** - No local file copies required (optional for desktop)

### Client Capabilities

| Client | Terminal | File Edit | Sync |
|--------|----------|-----------|------|
| **Desktop** | SSH (node-pty) | Cloud or Local+Sync | Optional |
| **Web** | xterm.js → WebSocket → SSH | Cloud (vim/nano) | N/A |
| **Phone** | View-only | None | N/A |
| **API** | Send commands | None | N/A |

---

## User Flows

### Flow 1: Create Cloud Workspace from Desktop

```
1. User clicks "New Cloud Workspace" in sidebar
2. Selects repository and branch
3. System creates Freestyle VM, clones from GitHub
4. VM initializes (install deps, start tmux)
5. Desktop connects via SSH, shows terminal
6. User works directly on cloud
```

### Flow 2: Access from Web

```
1. User opens web app, sees cloud workspace in list
2. Clicks workspace to open
3. Web terminal (xterm.js) connects via WebSocket to API
4. API proxies to SSH on Freestyle VM
5. User has full terminal access in browser
```

### Flow 3: Send Command from Phone

```
1. User opens mobile app (or uses API/chat)
2. Sees list of running cloud workspaces
3. Sends command: "npm test"
4. API executes on VM, returns output
5. User sees test results
```

### Flow 4: Handoff (Laptop → Phone → Web)

```
[Laptop]
1. Working on cloud workspace via desktop app
2. Close laptop, walk away
3. Cloud workspace continues running (or pauses after timeout)

[Phone]
4. Open app, see workspace status
5. Send command: /run "git status"
6. Chat with AI: "Fix the failing test"
7. AI edits files on cloud, pushes to GitHub

[Web - later]
8. Open web app on another computer
9. Connect to same cloud workspace
10. See all changes made via phone/AI
11. Continue working
```

### Flow 5: Desktop with Local Sync (for IDE users)

```
1. Create cloud workspace
2. Click "Sync to Local" in desktop app
3. System creates local worktree, clones from GitHub
4. Edit files in VS Code/Cursor locally
5. Commit + push to GitHub
6. Cloud VM auto-pulls (or manual trigger)
7. Terminal on cloud sees updated files
```

---

## Key Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| **Cloud Provider** | Freestyle.dev | Sub-second VM startup, built-in Git, SSH support |
| **Source of Truth** | Cloud VM | Simplifies multi-device access, no sync conflicts |
| **Persistent Storage** | GitHub | Standard git workflow, PRs, code review |
| **Sync Mechanism** | Git push/pull | No new tools, familiar workflow |
| **Desktop Local Sync** | Optional | For IDE users who prefer local editing |
| **Multi-device Model** | Shared access | Multiple clients can connect simultaneously |
| **Terminal Persistence** | tmux | Sessions survive disconnections |
| **Web Terminal** | xterm.js + WebSocket | Standard, well-supported |

### Rejected Alternatives

| Alternative | Why Rejected |
|-------------|--------------|
| **Mutagen file sync** | Added complexity, not needed with cloud-centric model |
| **CRDT collaboration** | Overkill for file-based editing, complex integration |
| **Freestyle Git** | GitHub already serves as source of truth |
| **Handoff model (one writer)** | Shared access is more flexible |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add operational and reliability considerations to the architecture.

The architecture section focuses on the happy path but lacks discussion of:

  • Failure modes: What happens when the Freestyle VM is unavailable, experiences network issues, or fails to start?
  • Cost model: How will VM usage be tracked, billed, and limited per organization?
  • Data residency & compliance: Where do VMs run? GDPR/CCPA implications for code and data?
  • Security posture: How are secrets managed? What's the threat model for multi-tenant VMs?
  • Capacity planning: Max concurrent workspaces per org, VM resource limits, scaling strategy?
  • Degraded modes: Can users work locally if cloud is unavailable?

Additionally, Line 178 states tmux will provide "terminal persistence," but PR #619 implements a daemon-based solution instead—clarify if tmux is still planned or if the daemon approach supersedes it.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

45-45: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


125-125: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


135-135: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


156-156: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@docs/CLOUD_WORKSPACE_PLAN.md` around lines 43 - 189, The architecture doc
misses operational/reliability details and has an outdated note about "terminal
persistence" via tmux; add a dedicated Operations & Reliability subsection
covering failure modes (VM unavailability, network failures, startup failures)
and recovery behaviors, cost/billing model (usage tracking, org limits, idle
timeouts), data residency & compliance (region choices, GDPR/CCPA handling),
security posture (secret management, multi-tenant threat model, SSH/access
controls), capacity planning (concurrent workspace limits, resource quotas,
autoscaling strategy) and degraded/local modes (how users can work offline or
sync locally), and update the terminal persistence decision to explicitly state
whether tmux remains the plan or the daemon-based solution from PR `#619`
supersedes tmux, referencing the "terminal persistence" line and PR `#619` so
reviewers know which approach is authoritative.

Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (1)

33-34: **Handle the loading state for terminalPersistence to prevent rendering strategy flip.**When terminalPersistence is undefined (during initial query load), the component takes the non-persistence code path (lines 173-196), then switches to the persistence path (lines 95-170) once data arrives. InitialData works on cache level, while placeholderData works on observer level. This has a couple of implications: First of all, initialData is persisted to the cache.

Since terminalPersistence is a user setting with a known default, use initialData to provide an immediate stable value and prevent the rendering strategy flip:

🐛 Proposed fix
+import { DEFAULT_TERMINAL_PERSISTENCE } from "@superset/shared/constants"; // or wherever the default is defined

 const { data: terminalPersistence } =
-	electronTrpc.settings.getTerminalPersistence.useQuery();
+	electronTrpc.settings.getTerminalPersistence.useQuery(undefined, {
+		initialData: DEFAULT_TERMINAL_PERSISTENCE,
+	});

Alternatively, destructure isLoading and defer the conditional branch until data is available:

-const { data: terminalPersistence } =
+const { data: terminalPersistence, isLoading: isLoadingPersistence } =
 	electronTrpc.settings.getTerminalPersistence.useQuery();

+// Early return while loading to avoid strategy flip
+if (isLoadingPersistence) {
+	return null; // or a minimal loading skeleton
+}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (2)

352-405: Extract paste chunking constants to module level.

Per coding guidelines, magic numbers should be extracted to named constants at module top. The paste chunking values are defined inline with different values for different code paths:

  • MAX_SYNC_PASTE_CHARS = 16_384 (line 352)
  • CHUNK_CHARS = 4096 for xterm.paste path (line 357)
  • CHUNK_CHARS = 16_384 for direct write path (line 404, shadows the first)

The shadowing and different chunk sizes are confusing. Consider extracting with descriptive names like XTERM_PASTE_CHUNK_SIZE and DIRECT_WRITE_CHUNK_SIZE.


87-156: TerminalRenderer.kind becomes stale after WebGL context loss.

The onContextLoss callback updates the local kind variable, but the returned TerminalRenderer object captures kind by value at return time. After context loss, consumers checking renderer.kind still see "webgl" even though the actual renderer switched to canvas.

While clearTextureAtlas safely becomes a no-op (the captured webglAddon is nulled), the stale kind causes incorrect renderer identification in Terminal.tsx (e.g., line 630).

Consider returning a mutable object whose properties update in the onContextLoss handler, as suggested in the previous review.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (2)

53-53: Use DOM-safe timeout typing instead of NodeJS.Timeout.

pendingDetaches uses NodeJS.Timeout, which can cause type mismatches in DOM-typed renderer builds. Use ReturnType<typeof setTimeout> for portability.

Proposed fix
-const pendingDetaches = new Map<string, NodeJS.Timeout>();
+const pendingDetaches = new Map<string, ReturnType<typeof setTimeout>>();

1042-1047: Remove duplicate state initialization.

Lines 1042-1044 and 1045-1047 are identical - the same three assignments appear twice consecutively:

  • isStreamReadyRef.current = false
  • didFirstRenderRef.current = false
  • pendingInitialStateRef.current = null
Proposed fix
 xtermRef.current = xterm;
 fitAddonRef.current = fitAddon;
 rendererRef.current = renderer;
 isExitedRef.current = false;
 setXtermInstance(xterm);
 isStreamReadyRef.current = false;
 didFirstRenderRef.current = false;
 pendingInitialStateRef.current = null;
-isStreamReadyRef.current = false;
-didFirstRenderRef.current = false;
-pendingInitialStateRef.current = null;
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)

36-44: Terminal runtime captured at router creation may become stale.

The terminal reference is captured once when the router is created. If the daemon restarts or terminal persistence mode is toggled at runtime, procedures may operate on a dead or outdated backend.

Consider resolving the terminal runtime lazily inside each procedure, or ensure the registry/runtime handles reconnection transparently.

#!/bin/bash
# Check how the workspace runtime registry handles runtime lifecycle
ast-grep --pattern $'class $_ implements WorkspaceRuntimeRegistry {
  $$$
  getDefault() {
    $$$
  }
  $$$
}'
🧹 Nitpick comments (7)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (2)

22-29: Consider using object parameters for consistency.

Per coding guidelines, functions with 2+ parameters should use a single params object. This is a minor suggestion for this simple helper.

♻️ Optional refactor
-function hasTerminalPane(tab: Tab, panes: Record<string, Pane>): boolean {
+function hasTerminalPane({ tab, panes }: { tab: Tab; panes: Record<string, Pane> }): boolean {
 	const paneIds = extractPaneIdsFromLayout(tab.layout);
 	return paneIds.some((paneId) => panes[paneId]?.type === "terminal");
 }

Then update call sites to use hasTerminalPane({ tab, panes }).


148-153: Simplify the fallback condition.

The condition !activeNonTerminalTab && !tabToRender can be simplified to !tabToRender since activeNonTerminalTab is derived from tabToRender and will always be null when tabToRender is null.

♻️ Optional simplification
-				{/* Fallback: show empty view without unmounting terminal tabs */}
-				{!activeNonTerminalTab && !tabToRender && (
+				{/* Fallback: show empty view when no active tab */}
+				{!tabToRender && (
 					<div className="absolute inset-0 overflow-hidden">
 						<EmptyTabView />
 					</div>
 				)}
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (3)

406-472: Event handling logic is duplicated with handleStreamData.

The event processing logic in flushPendingEvents (lines 416-471) is nearly identical to handleStreamData (lines 894-952). Both implement the same special-case handling for "Session not found", "PTY not spawned", WRITE_QUEUE_FULL, etc.

Consider extracting a shared processTerminalEvent helper to reduce duplication and divergence risk.


586-593: Consider extracting the SIGWINCH delay to a named constant.

The 100ms timeout at line 592 is a magic number used for ensuring SIGWINCH propagation. Per coding guidelines, consider extracting this to a named constant at module level for clarity and maintainability.

Suggested change
// At module level
const SIGWINCH_PROPAGATION_DELAY_MS = 100;

// In the code
setTimeout(() => {
  // ...
}, SIGWINCH_PROPAGATION_DELAY_MS);

1461-1493: Consider extracting StrictMode detach delay to a named constant.

The 50ms timeout at line 1474 is a magic number used to handle React StrictMode's unmount/remount cycle. While the delay is well-documented in comments, extracting it to a constant improves maintainability:

const STRICT_MODE_DETACH_DELAY_MS = 50;

The setTimeout(0) for xterm.dispose() at line 1491-1493 is appropriate and well-documented.

apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)

313-314: Extract retry constants to module level.

Per coding guidelines, magic numbers should be extracted to named constants at module top for clarity and reuse.

Suggested refactor

Add near the top of the file (after line 19):

const KILL_SESSIONS_MAX_RETRIES = 10;
const KILL_SESSIONS_RETRY_DELAY_MS = 100;

Then reference them in the procedure.

apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (1)

125-131: Consider cleanup for delayed invalidation.

The setTimeout in onSettled could fire after unmount. While React Query handles stale invalidations gracefully, a cleanup pattern would be cleaner.

Alternative approach

Use a ref to track mount state, or consider using React Query's built-in refetchInterval on the query itself with a short burst after mutations, avoiding manual setTimeout.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d14275e and 1893425.

📒 Files selected for processing (16)
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalConnection.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/stores/hotkeys/store.ts
  • apps/desktop/src/renderer/stores/tabs/store.ts
  • apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/shared/constants.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts
  • apps/desktop/src/renderer/stores/hotkeys/store.ts
  • apps/desktop/src/shared/constants.ts
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx
  • apps/desktop/src/renderer/stores/tabs/store.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalConnection.ts
🧰 Additional context used
📓 Path-based instructions (7)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/desktop/src/main/index.ts

📄 CodeRabbit inference engine (AGENTS.md)

Load environment variables from monorepo root .env in desktop app with override: true before any imports in src/main/index.ts and electron.vite.config.ts

Files:

  • apps/desktop/src/main/index.ts
🧠 Learnings (10)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/main/windows/main.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Applied to files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/*.ts : Never import Node.js modules in shared code like electron-router-dom.ts - it runs in both main and renderer processes

Applied to files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/main/index.ts : Load environment variables from monorepo root .env in desktop app with override: true before any imports in src/main/index.ts and electron.vite.config.ts

Applied to files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/main/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/shared/ipc-channels.ts : Define IPC channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/main/windows/main.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/main/windows/main.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to **/*.{ts,tsx} : Extract magic numbers and hardcoded values to named constants at module top

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/api/**/*.{ts,tsx} : Extract business logic from tRPC procedures into utility functions when logic exceeds ~50 lines, is used by multiple procedures, or needs independent testing

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from `trpc/server/observable` instead of async generators, as the library explicitly checks `isObservable(result)` and throws an error otherwise

Applied to files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
🧬 Code graph analysis (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (4)
apps/desktop/src/renderer/stores/tabs/types.ts (2)
  • Tab (18-20)
  • Pane (12-12)
apps/desktop/src/renderer/stores/tabs/utils.ts (1)
  • extractPaneIdsFromLayout (97-108)
apps/desktop/src/renderer/lib/electron-trpc.ts (1)
  • electronTrpc (9-11)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx (1)
  • TabView (27-200)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalConnection.ts (1)
  • useTerminalConnection (20-69)
apps/desktop/src/renderer/lib/terminal-kill-tracking.ts (2)
  • isTerminalKilledByUser (7-8)
  • clearTerminalKilledByUser (10-12)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/utils.ts (2)
  • restoreScrollPosition (27-37)
  • getScrollOffsetFromBottom (21-24)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/attach-scheduler.ts (1)
  • scheduleTerminalAttach (129-209)
apps/desktop/src/main/index.ts (3)
apps/desktop/src/main/lib/workspace-runtime/index.ts (1)
  • getWorkspaceRuntimeRegistry (18-18)
apps/desktop/src/main/lib/workspace-runtime/registry.ts (1)
  • getWorkspaceRuntimeRegistry (75-80)
apps/desktop/src/main/lib/terminal/index.ts (2)
  • reconcileDaemonSessions (101-116)
  • shutdownOrphanedDaemon (125-152)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (6)
apps/desktop/src/main/lib/workspace-runtime/index.ts (1)
  • getWorkspaceRuntimeRegistry (18-18)
apps/desktop/src/main/lib/workspace-runtime/registry.ts (1)
  • getWorkspaceRuntimeRegistry (75-80)
apps/desktop/src/main/lib/terminal/manager.ts (1)
  • signal (231-244)
apps/desktop/src/main/lib/terminal/daemon-manager.ts (1)
  • signal (847-865)
apps/desktop/src/main/lib/terminal-host/client.ts (1)
  • signal (1273-1276)
apps/desktop/src/main/terminal-host/terminal-host.ts (1)
  • signal (235-245)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Web
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy API
  • GitHub Check: Build
🔇 Additional comments (20)
apps/desktop/src/main/index.ts (3)

20-24: LGTM!

The new imports for daemon session management (reconcileDaemonSessions, shutdownOrphanedDaemon) and workspace runtime registry align with the terminal persistence feature. The imports are from concrete module paths as recommended.


245-251: LGTM!

The daemon session reconciliation and orphan cleanup are correctly positioned:

  1. After initAppState() (settings/state available)
  2. Before ensureShellEnvVars() and window creation (as required by the comment)
  3. Sequential execution ensures reconciliation completes before orphan shutdown

Both functions internally guard against errors and log warnings without throwing, so startup remains resilient even if daemon cleanup fails.


179-182: No changes needed—the property access chain is safe.

The chained call getWorkspaceRuntimeRegistry().getDefault().terminal.cleanup() is fully type-safe:

  • getWorkspaceRuntimeRegistry() returns a cached singleton WorkspaceRuntimeRegistry (never undefined)
  • .getDefault() lazy-initializes and always returns a WorkspaceRuntime instance (never undefined)
  • .terminal is a required non-optional property on WorkspaceRuntime (always defined in LocalWorkspaceRuntime constructor)

The cleanup pattern is sound: the try/finally ensures app.quit() executes even if cleanup fails.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx (5)

20-20: LGTM!

Good practice extracting WARM_TERMINAL_TAB_LIMIT as a named constant at module top per coding guidelines.


48-72: LGTM!

The memoization chain for deriving activeTabId, tabToRender, and activeTabHasTerminal is well-structured with appropriate dependencies. The separation of concerns makes the logic easy to follow.


74-87: LGTM!

The MRU tracking logic correctly maintains a bounded warm set of terminal tabs with proper guards and functional state updates to avoid stale closures.


125-140: LGTM!

The visibility toggling logic correctly combines workspace and tab ID checks, ensuring terminals from inactive workspaces remain mounted but hidden. The pointerEvents: "none" prevents accidental interaction with hidden terminals.


172-196: LGTM!

The non-persistence fallback correctly preserves the original single-tab rendering behavior.

apps/desktop/src/main/windows/main.ts (3)

25-25: LGTM!

The import correctly references the new workspace runtime registry abstraction, aligning with the PR's architecture for terminal persistence.


235-235: LGTM!

The cleanup correctly uses the workspace runtime registry pattern, maintaining consistency with the event listener registration at lines 176-177.


173-187: LGTM!

The terminal exit event forwarding is well-structured:

  • Inline type annotation correctly matches the actual event shape emitted by terminal runtime
  • Proper cleanup via detachAllListeners() on window close (line 235)
  • Follows the existing event forwarding pattern established for AGENT_LIFECYCLE events
  • Events are correctly exposed to the renderer through tRPC subscription with the observable pattern
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (1)

193-226: Well-designed async renderer loading with proper cleanup guards.

The deferred GPU renderer loading via requestAnimationFrame with isDisposed guard elegantly avoids the race condition described in the comments. The cleanup properly cancels pending RAF and disposes the current renderer via the ref pattern.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (2)

799-803: Good defensive handling of stale events during cold restore.

Dropping queued events before starting a new shell prevents stale exit events from incorrectly marking the new session as exited. The comment clearly explains the rationale.


1554-1610: Well-structured overlay system for terminal states.

The three overlay states (killed, connection error, cold restore) are cleanly separated with appropriate conditions preventing overlap. The UX flow is clear:

  • Session killed → Restart Session
  • Connection error → Retry Connection
  • Cold restore → Start Shell (with preserved CWD display)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (3)

461-523: LGTM!

The stream subscription correctly uses the observable pattern required for trpc-electron, properly handles multiple event types, and includes clear documentation explaining why emit.complete() is intentionally not called on exit to support session reuse.


184-203: LGTM!

Good error handling that gracefully emits an exit event when the session is gone, preventing error floods when workspaces with terminals are deleted. The signal is correctly typed as a number.


76-89: LGTM!

The userKilledSessions tracking with allowKilled bypass provides a clean mechanism to prevent auto-reattach of intentionally killed sessions while allowing explicit recreation.

apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/components/GeneralSettings/GeneralSettings.tsx (1)

64-68: LGTM!

The Terminal settings entry follows the established pattern and integrates cleanly with the existing navigation structure.

apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (2)

164-201: LGTM!

The Terminal persistence toggle with clear explanations about memory usage and restart requirements provides good UX. The optimistic update pattern with rollback on error is well-implemented.


329-375: LGTM!

The Kill All confirmation dialog provides appropriate warnings about the destructive action, correctly marks all sessions as killed by user before triggering the mutation to prevent auto-reattach.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread packages/local-db/src/schema/schema.ts Outdated
Comment on lines 126 to 156
@@ -139,6 +149,9 @@ export const settings = sqliteTable("settings", {
terminalLinkBehavior: text(
"terminal_link_behavior",
).$type<TerminalLinkBehavior>(),
navigationStyle: text("navigation_style").$type<NavigationStyle>(),
groupTabsPosition: text("group_tabs_position").$type<GroupTabsPosition>(),
terminalPersistence: integer("terminal_persistence", { mode: "boolean" }),
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove old variables

Comment thread docs/CLOUD_WORKSPACE_PLAN.md Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx`:
- Around line 252-263: The Clear terminal history button is currently disabled
when there are no live sessions; remove the aliveSessions.length === 0 check so
the button is enabled whenever daemonModeEnabled is true (still respecting
clearTerminalHistory.isPending). Update the disabled expression used on the
Button rendered in page.tsx (the Button component with onClick={() =>
setConfirmClearHistoryOpen(true)}) to only consider !daemonModeEnabled and
clearTerminalHistory.isPending so users can clear disk-based scrollback even
with no live sessions.
- Around line 374-380: The click handler currently iterates over sessions and
marks every session as killed by user; change that loop to iterate over
aliveSessions instead so only sessions that will be terminated are marked via
markTerminalKilledByUser(session.sessionId). Keep the existing calls to
setConfirmKillAllOpen(false) and killAllDaemonSessions.mutate(), but replace the
for (const session of sessions) { ... } loop with for (const session of
aliveSessions) { ... } (or equivalent filtering of sessions to only alive ones)
to ensure only live terminals are flagged.
♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (1)

31-46: Unstable array reference causes unnecessary memo recalculations.

The ?? [] fallback creates a new array reference each render when daemonSessions?.sessions is undefined, defeating the memoization of aliveSessions and sessionsSorted.

♻️ Proposed fix
+const EMPTY_SESSIONS: typeof daemonSessions.sessions = [];
+
 function TerminalSettingsPage() {
   const utils = electronTrpc.useUtils();
   // ...
-  const sessions = daemonSessions?.sessions ?? [];
+  const sessions = daemonSessions?.sessions ?? EMPTY_SESSIONS;

Alternatively, fold the filtering into a single memo:

-  const sessions = daemonSessions?.sessions ?? [];
-  const aliveSessions = useMemo(
-    () => sessions.filter((session) => session.isAlive),
-    [sessions],
-  );
-  const sessionsSorted = useMemo(() => {
-    return [...aliveSessions].sort((a, b) => {
+  const sessionsSorted = useMemo(() => {
+    const alive = (daemonSessions?.sessions ?? []).filter((s) => s.isAlive);
+    return alive.sort((a, b) => {
       // ...
     });
-  }, [aliveSessions]);
+  }, [daemonSessions?.sessions]);

Note: This is a refinement of the prior review comment about the useMemo dependency issue.

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (2)

128-134: Extract magic number and consider race condition.

The 300ms delay is a hardcoded value that may not be sufficient if the daemon cleanup takes longer. Consider extracting to a named constant and documenting the assumption.

♻️ Proposed fix
+// Delay before refetching to allow daemon cleanup to complete
+const DAEMON_CLEANUP_DELAY_MS = 300;
+
 // ... inside mutation config
 onSettled: () => {
   // Always refetch to get actual state after mutation settles
-  // Small delay to allow daemon to finish cleanup
   setTimeout(() => {
     utils.terminal.listDaemonSessions.invalidate();
-  }, 300);
+  }, DAEMON_CLEANUP_DELAY_MS);
 },

162-165: Consider moving pure utility outside the component.

formatTimestamp is recreated each render. Since it has no dependencies on component state, extract it to module scope.

♻️ Proposed fix
+const formatTimestamp = (value?: string) => {
+  if (!value) return "—";
+  return value.replace("T", " ").replace(/\.\d+Z$/, "Z");
+};
+
 function TerminalSettingsPage() {
   // ...
-  const formatTimestamp = (value?: string) => {
-    if (!value) return "—";
-    return value.replace("T", " ").replace(/\.\d+Z$/, "Z");
-  };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2f8de2 and e0a1b02.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy Web
  • GitHub Check: Build
🔇 Additional comments (2)
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx (2)

1-21: LGTM!

Imports and route definition follow project conventions. Proper use of aliases and no Node.js modules in renderer code.


433-482: LGTM!

The per-session kill dialog handles null state defensively with proper early return. Dialog structure follows accessibility patterns.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +252 to +263
<Button
variant="secondary"
size="sm"
disabled={
!daemonModeEnabled ||
aliveSessions.length === 0 ||
clearTerminalHistory.isPending
}
onClick={() => setConfirmClearHistoryOpen(true)}
>
Clear terminal history
</Button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"Clear terminal history" should be available when daemon is enabled, regardless of live sessions.

Clearing history removes disk-based scrollback files used for cold restore. Users may want to clear old history even when no sessions are currently alive (e.g., after all sessions exited but before app restart).

🐛 Proposed fix
 <Button
   variant="secondary"
   size="sm"
   disabled={
     !daemonModeEnabled ||
-    aliveSessions.length === 0 ||
     clearTerminalHistory.isPending
   }
   onClick={() => setConfirmClearHistoryOpen(true)}
 >
   Clear terminal history
 </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
variant="secondary"
size="sm"
disabled={
!daemonModeEnabled ||
aliveSessions.length === 0 ||
clearTerminalHistory.isPending
}
onClick={() => setConfirmClearHistoryOpen(true)}
>
Clear terminal history
</Button>
<Button
variant="secondary"
size="sm"
disabled={
!daemonModeEnabled ||
clearTerminalHistory.isPending
}
onClick={() => setConfirmClearHistoryOpen(true)}
>
Clear terminal history
</Button>
🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx`
around lines 252 - 263, The Clear terminal history button is currently disabled
when there are no live sessions; remove the aliveSessions.length === 0 check so
the button is enabled whenever daemonModeEnabled is true (still respecting
clearTerminalHistory.isPending). Update the disabled expression used on the
Button rendered in page.tsx (the Button component with onClick={() =>
setConfirmClearHistoryOpen(true)}) to only consider !daemonModeEnabled and
clearTerminalHistory.isPending so users can clear disk-based scrollback even
with no live sessions.

Comment on lines +374 to +380
onClick={() => {
setConfirmKillAllOpen(false);
for (const session of sessions) {
markTerminalKilledByUser(session.sessionId);
}
killAllDaemonSessions.mutate();
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Iterate over aliveSessions when marking sessions as killed.

The loop marks all sessions (including already-dead ones) as killed by user, but only alive sessions are actually terminated.

🐛 Proposed fix
 onClick={() => {
   setConfirmKillAllOpen(false);
-  for (const session of sessions) {
+  for (const session of aliveSessions) {
     markTerminalKilledByUser(session.sessionId);
   }
   killAllDaemonSessions.mutate();
 }}
🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/routes/_authenticated/settings/terminal/page.tsx`
around lines 374 - 380, The click handler currently iterates over sessions and
marks every session as killed by user; change that loop to iterate over
aliveSessions instead so only sessions that will be terminated are marked via
markTerminalKilledByUser(session.sessionId). Keep the existing calls to
setConfirmKillAllOpen(false) and killAllDaemonSessions.mutate(), but replace the
for (const session of sessions) { ... } loop with for (const session of
aliveSessions) { ... } (or equivalent filtering of sessions to only alive ones)
to ensure only live terminals are flagged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)

185-191: Inconsistent SAFE_ID validation on paneId across procedures.

createOrAttach validates paneId with SAFE_ID (preventing path traversal), but write, resize, signal, kill, detach, clearScrollback, ackColdRestore, getSession, and stream all accept plain z.string().

If paneId is used to construct file paths (e.g., for history persistence in daemon mode), this inconsistency could expose path traversal vulnerabilities. Apply SAFE_ID consistently to all paneId inputs.

🔒 Example fix for write mutation
 write: publicProcedure
   .input(
     z.object({
-      paneId: z.string(),
+      paneId: SAFE_ID,
       data: z.string(),
     }),
   )
🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/terminal/terminal.ts`:
- Around line 303-317: The code adds session IDs to userKilledSessions before
calling terminal.management.killAllSessions(), causing createOrAttach to treat
sessions as dead if killAll fails; change the logic so you only add IDs to
userKilledSessions after killAllSessions() completes successfully (or in a loop
that confirms each id was killed), or wrap the killAllSessions() call in
try/catch and on failure remove any previously added IDs from
userKilledSessions; reference terminal.management.killAllSessions(),
userKilledSessions, and createOrAttach to locate and update the flow so session
IDs are added only after confirmed kill (or rolled back on error).
♻️ Duplicate comments (1)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)

43-52: Terminal reference captured once may become stale on daemon restart or mode toggle.

The terminal object is resolved once at router creation. If the daemon restarts or terminal persistence mode is toggled, procedures will continue using the stale reference.

Consider resolving the terminal runtime inside each procedure or introducing a getter helper.

🧹 Nitpick comments (2)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (2)

18-19: Unbounded growth of userKilledSessions Set.

This Set accumulates paneIds across the app lifetime and is never pruned. Over extended usage (especially with many workspace/terminal operations), this could grow unboundedly.

Consider periodic cleanup (e.g., removing entries when the corresponding workspace is deleted or on a TTL basis), or using a bounded LRU cache.


368-371: Sequential session kills could be parallelized.

The loop awaits each terminal.kill() sequentially. If multiple sessions need killing, this adds latency. Consider parallel execution.

♻️ Proposed refactor
-for (const session of toKill) {
-  userKilledSessions.add(session.sessionId);
-  await terminal.kill({ paneId: session.sessionId });
-}
+await Promise.all(
+  toKill.map((session) => {
+    userKilledSessions.add(session.sessionId);
+    return terminal.kill({ paneId: session.sessionId });
+  }),
+);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81b435e and c212b86.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/terminal-host/client.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/main/lib/terminal-host/client.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
🧠 Learnings (2)
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from `trpc/server/observable` instead of async generators, as the library explicitly checks `isObservable(result)` and throws an error otherwise

Applied to files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Deploy Marketing
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Web
  • GitHub Check: Build
🔇 Additional comments (2)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (2)

54-183: Well-structured mutation with proper validation and error handling.

Good use of SAFE_ID validation, debug telemetry with callId tracking, and the allowKilled escape hatch for intentional session restarts.


469-531: LGTM!

The observable pattern is correctly used for the tRPC subscription (per trpc-electron requirements). Event handlers are properly registered and cleaned up, and the comment explaining why emit.complete() is avoided on exit is valuable documentation. Based on learnings about trpc-electron observable requirements.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +303 to +317
// Get sessions before kill for accurate count
const before = await terminal.management.listSessions();
const beforeIds = before.sessions.map((s) => s.sessionId);
for (const id of beforeIds) {
userKilledSessions.add(id);
}
console.log(
"[killAllDaemonSessions] Before kill:",
beforeIds.length,
"sessions",
beforeIds,
);

// Request kill of all sessions
await terminal.management.killAllSessions();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sessions marked as killed before killAllSessions() completes—may block createOrAttach incorrectly if kill fails.

If terminal.management.killAllSessions() throws or partially fails, the sessions are already in userKilledSessions, causing createOrAttach to reject them even though they're still alive.

Consider adding sessions to userKilledSessions only after confirming the kill succeeded, or wrapping in a try/catch that removes the entries on failure.

🐛 Proposed fix
-const beforeIds = before.sessions.map((s) => s.sessionId);
-for (const id of beforeIds) {
-  userKilledSessions.add(id);
-}
-console.log(
-  "[killAllDaemonSessions] Before kill:",
-  beforeIds.length,
-  "sessions",
-  beforeIds,
-);
-
-// Request kill of all sessions
-await terminal.management.killAllSessions();
+const beforeIds = before.sessions.map((s) => s.sessionId);
+console.log(
+  "[killAllDaemonSessions] Before kill:",
+  beforeIds.length,
+  "sessions",
+  beforeIds,
+);
+
+// Request kill of all sessions
+await terminal.management.killAllSessions();
+
+// Mark as killed only after successful kill request
+for (const id of beforeIds) {
+  userKilledSessions.add(id);
+}
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/terminal/terminal.ts` around lines 303 -
317, The code adds session IDs to userKilledSessions before calling
terminal.management.killAllSessions(), causing createOrAttach to treat sessions
as dead if killAll fails; change the logic so you only add IDs to
userKilledSessions after killAllSessions() completes successfully (or in a loop
that confirms each id was killed), or wrap the killAllSessions() call in
try/catch and on failure remove any previously added IDs from
userKilledSessions; reference terminal.management.killAllSessions(),
userKilledSessions, and createOrAttach to locate and update the flow so session
IDs are added only after confirmed kill (or rolled back on error).

Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants