Skip to content

Conversation

@andreasasprou
Copy link
Collaborator

@andreasasprou andreasasprou commented Jan 4, 2026

⚠️ Stacked PR: This PR is stacked on top of #559 (workspace-sidebar). Please merge #559 first.

[Internal] Bugs to fix pre-merge

  • Very slow start with lots of active terminals. And cases where attachOrCreate session fails and needs retrying via the error empty state. Retrying works.
image
  • shortcuts (like CMD+W) don't work when focusing on the terminal content

    Root cause & proposed fix (click to expand)

    Root cause: Event propagation failure between xterm.js and react-hotkeys-hook.

    When xterm's attachCustomKeyEventHandler returns false:

    • xterm doesn't process the keystroke
    • But xterm still blocks the event from bubbling to document
    • useHotkeys("CLOSE_TERMINAL", ...) in WorkspaceView never receives the event

    Proposed fix (follow-up PR): Blended approach with two tiers:

    1. Priority shortcuts (e.g., CLOSE_TERMINAL): Always dispatch to document - "owned" by Superset
    2. Other app shortcuts: Only dispatch if NOT in alternate screen mode (respects TUI apps like vim, Claude Code)
    // helpers.ts
    const PRIORITY_HOTKEYS = ["CLOSE_TERMINAL"] as const;
    
    if (isAppHotkeyEvent(event)) {
      const isPriority = PRIORITY_HOTKEYS.some(id => 
        matchesHotkeyEvent(event, getHotkeyKeys(id))
      );
      
      // Priority shortcuts always dispatch, others respect alternate screen
      if (isPriority || !options.isInAlternateScreen?.()) {
        document.dispatchEvent(new KeyboardEvent(event.type, event));
      }
      return false;
    }
    Scenario CMD+W Other shortcuts
    Normal shell Closes pane ✅ Work ✅
    vim/less/htop Closes pane ✅ Pass to TUI
    Claude Code Closes pane ✅ Pass to CC

    Files to modify: helpers.ts (add priority hotkeys logic), Terminal.tsx (pass isInAlternateScreen callback)

Summary

This PR adds terminal session persistence via a background daemon process that survives app restarts.

Closes #518 (supersedes the tmux-based approach with a Superset-owned daemon)

Details

Architecture

  • Terminal host daemon: Long-lived detached orchestrator that manages sessions and maintains headless emulation state
  • Per-session PTY subprocess: Each terminal PTY lives in its own subprocess so one terminal can't freeze the system
  • IPC: Main ↔ daemon uses authenticated Unix socket protocol; daemon ↔ subprocess uses lightweight binary framing optimized for escape-heavy streams
  • DaemonTerminalManager: Drop-in replacement that delegates to daemon while preserving TRPC API surface

How it works

  1. Enable via Settings → Terminal → "Terminal persistence" toggle (or SUPERSET_TERMINAL_DAEMON=1 env var)
  2. On first terminal creation, daemon is spawned as detached process
  3. Daemon + per-session subprocesses maintain PTY sessions and terminal state while app is closed
  4. On app restart, existing sessions are recovered with full scrollback
  5. TUI apps (like Claude Code) resume correctly via SIGWINCH-triggered redraw
  6. Workspace/tab switching keeps terminals mounted — no unmount/remount cycle, eliminating white screen issues

Why daemon instead of tmux

  • No external dependency (tmux not installed by default on macOS)
  • Full control over terminal emulation for best-effort resume
  • Self-contained while still isolating terminals at the OS-process level
  • Cross-platform potential (daemon approach works on Windows too)

New Features

Settings UI Toggle

  • New "Terminal" section in Settings sidebar
  • Toggle to enable/disable terminal persistence
  • Requires app restart to take effect
  • Env var SUPERSET_TERMINAL_DAEMON=1 still works as override for development
  • Warning copy explains potential memory impact and suggests disabling if issues occur

TUI Mode Rehydration

  • Terminal modes (alternate screen, cursor visibility, etc.) are captured and restored
  • TUI apps like Claude Code, opencode, codex resume correctly via SIGWINCH redraw
  • See Technical Notes section for how this works

Smooth Workspace/Tab Switching

  • When persistence is enabled, terminals stay mounted across workspace and tab switches
  • Uses CSS visibility: hidden to hide inactive terminals while preserving xterm state
  • Eliminates the white screen issue that occurred with unmount/remount cycles

Large Paste Reliability (vi)

  • PTY runs in a per-session subprocess (one terminal can't freeze others)
  • Subprocess uses binary framing to avoid JSON overhead on escape-heavy output
  • Daemon time-slices headless emulator processing to prevent event-loop starvation during vi repaints
  • PTY input backpressure (EAGAIN/EWOULDBLOCK) is retried with backoff (no dropped chunks)
  • Renderer wraps pastes with bracketed paste + chunks large payloads

Manual Test Checklist

Core Functionality

Daemon Lifecycle

  • Daemon Restart (Settings) - Removed for v1 (see Known Limitations)
  • Daemon Kill Recovery: Open terminals → Run pkill -f terminal-host → Daemon respawns on next terminal operation
  • Daemon Survives Quit: Open terminals → Quit app → Run ps aux | grep terminal-host → Daemon still running

Error Handling

  • Connection Timeout: If daemon is unresponsive, error UI appears within ~10 seconds (not infinite hang)

  • Retry Button: Error overlay "Retry Connection" button successfully reconnects

image

Workspace Management

  • Workspace Deletion: Create workspace with terminals → Delete workspace → Daemon sessions cleaned up
  • Multiple Workspaces: Open terminals in 2+ workspaces → Quit → Reopen → Each workspace restores its own terminals
  • Workspace Switching: Run TUI in workspace A → Switch to workspace B → Switch back to A → TUI displays correctly (no white screen)

Edge Cases

  • Empty Terminal: New terminal (no commands run) → Quit → Reopen → Restores empty prompt correctly
  • Long Scrollback: Run command with lots of output (find /) → Quit → Reopen → Full scrollback preserved
    https://www.loom.com/share/9822c91f288549ba8311da960144bdac
  • Active Process: Start long-running process (sleep 1000) → Quit → Reopen → Process still running in restored terminal
  • Large Paste: Paste 4+ pages of text into vi → No freezes and paste completes (tested with ~3k lines)

Production Build

  • Packaged App: Build app (bun run build) → Test all above in .app bundle
  • Clean Install: Delete ~/.superset/terminal-host.* files → Launch app → Daemon spawns correctly

Basic Test Plan (Quick Validation)

  1. Open Settings → Terminal → Enable "Terminal persistence"
  2. Restart app
  3. Open workspace, create terminal, run commands (or start a TUI like claude or opencode)
  4. Quit app (Cmd+Q)
  5. Verify daemon still running: ps aux | grep terminal-host
  6. Restart app
  7. Terminal shows previous content / TUI resumes ✅
  8. Switch between workspaces with TUIs running — should be instant with no white screen

Known Limitations

  1. Protocol version strictness — Strict equality check on protocol version may break "survive app updates" promise if version is ever bumped. Consider semantic versioning or graceful degradation.

  2. No daemon restart UI — For v1, if the daemon becomes unresponsive, users can manually kill it: pkill -f terminal-host. The daemon will respawn automatically on next terminal operation. If users report frequent daemon issues, implement auto-recovery (health check + automatic restart) in v2.

  3. Memory usage with many terminals — When persistence is enabled, all terminals stay mounted across workspace switches. This may increase memory usage for users with many terminals/workspaces. Settings page includes warning copy. Users can disable persistence if they notice performance issues.


Future Improvements

These are documented in apps/desktop/docs/2026-01-02-terminal-persistence-technical-notes.md:

  1. Buffer PTY output for hidden terminals — Pause xterm.write() when a terminal's tab is hidden; replay buffer on reactivation. Reduces CPU for inactive terminals.

  2. LRU terminal hibernation — If memory pressure detected, dispose oldest inactive xterm instances and rely on SIGWINCH restoration when user returns. Balances memory vs smooth switching.

  3. Reduce scrollback for hidden terminals — Temporarily shrink scrollback buffer for hidden terminals; restore on activation.

  4. Memory usage telemetry — Add metrics to understand real-world memory patterns (terminals per workspace, scrollback sizes, etc.) to inform future optimizations.


Technical Notes

TUI Restoration via SIGWINCH

TUI apps (alternate screen mode) are restored using a SIGWINCH-based redraw approach instead of snapshot serialization:

  1. On reattach to a TUI session, we skip the serialized snapshot (which renders incorrectly due to styled spaces and positioning issues)
  2. Enter alt-screen mode directly so TUI output goes to the correct buffer
  3. Enable streaming so live PTY output comes through
  4. Trigger SIGWINCH via resize down/up — the TUI redraws itself from scratch

Trade-off: Brief visual flash as TUI redraws, but the result is always correct. The TUI is the authority on its own display—we just trigger a refresh.

Why snapshots don't work for TUIs: TUIs use styled spaces (spaces with background colors) to create panels and borders. SerializeAddon captures buffer cell content, but serialization of styled empty cells is inconsistent. When restored, the snapshot renders sparsely—missing panels, borders, and UI chrome.

Note: With "keep terminals mounted" enabled, the SIGWINCH path is primarily a fallback for app restart recovery. During normal workspace/tab switching, terminals stay mounted and no reattach is needed.

Keep Terminals Mounted

To eliminate white screen issues during workspace/tab switching, we keep all terminal xterm.js instances mounted when persistence is enabled:

  • TabsContent renders all tabs from all workspaces
  • Inactive tabs use visibility: hidden; pointer-events: none
  • Active tab uses visibility: visible; pointer-events: auto

Why visibility: hidden: Unlike display: none, it preserves element dimensions. xterm.js and FitAddon require non-zero dimensions to function correctly.

Trade-off: Higher memory usage with many terminals. The SIGWINCH restoration logic remains as a fallback for app restart recovery.

See apps/desktop/docs/2026-01-02-terminal-persistence-technical-notes.md for detailed analysis.


Bug Fixes in This PR

TUI White Screen on Workspace Switch (FIXED)

Problem: Switching between workspaces with TUI apps running (vim, opencode, claude) caused white/blank screens. Manual window resize would fix it.

Root Cause: React unmounts the Terminal component on workspace switch, destroying the xterm.js instance. On return, a new xterm instance must be created and reattached to the PTY session. Despite correct SIGWINCH timing, race conditions between xterm initialization and PTY output caused blank screens. The fundamental issue is that xterm.js emulator state is lost on unmount.

Fix: Keep all terminal xterm.js instances mounted when persistence is enabled:

  • Render all tabs from all workspaces in TabsContent
  • Use CSS visibility: hidden for inactive terminals
  • No unmount/remount cycle = no reattach timing issues = no white screen

Trade-off: Higher memory usage with many terminals. Settings page includes warning copy.

TUI Corruption on Tab Switch (e.g., opencode, vim)

Problem: Switching away from a terminal running a TUI and switching back resulted in visual corruption—missing ASCII art, input boxes, and UI elements.

Root Cause: Serialized snapshots don't capture TUI state correctly. TUIs use styled spaces (spaces with background colors) for panels and borders, and these serialize inconsistently.

Fix: For alt-screen (TUI) sessions, skip the broken snapshot and trigger SIGWINCH instead:

  • Enter alt-screen mode directly
  • Enable streaming so PTY output flows
  • Resize down/up to send SIGWINCH
  • TUI redraws itself from scratch

Trade-off: Brief flash as TUI redraws, but always renders correctly.

xterm "dimensions" Error (Tab Switching)

Problem: Switching between terminal tabs caused Cannot read properties of undefined (reading 'dimensions') error, breaking keyboard input.

Root Cause: xterm.open() schedules internal timeouts. If xterm.dispose() runs before those timeouts fire, it crashes.

Fix:

  • Delayed xterm.dispose() with setTimeout(0) to let internal xterm.js timeouts complete
  • Added pendingDetaches Map for React StrictMode debouncing

React StrictMode Double-Mount

Problem: In dev mode, terminals couldn't accept input after tab switch.

Root Cause: StrictMode runs effects twice (mount→unmount→mount). The detach on first unmount corrupted state.

Fix: Module-level pendingDetaches Map that cancels pending detaches when component remounts.

Large Paste into vi (hangs / dropped chunks)

Problem: Pasting large blocks of text into vi could freeze the daemon (all terminals) or stop mid-paste with missing chunks.

Root Cause: Combination of CPU-bound output processing during vi repaints and missing backpressure handling for PTY input (non-blocking fd writes returning EAGAIN/EWOULDBLOCK).

Fix:

  • Move PTY ownership to per-session subprocesses and use a binary framing protocol (no NDJSON on hot paths)
  • Batch PTY output + time-slice daemon emulator work so output bursts can't monopolize the daemon
  • Implement input flow control: retry EAGAIN/EWOULDBLOCK with backoff; pause/resume on backlog
  • Update renderer paste path (bracketed paste + chunking) and surface terminal errors

WebGL Render Corruption on macOS (Tab Switching)

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

Root Cause: xterm-webgl has rendering issues on macOS when terminals are hidden/shown or switched between panes.

Fix: Default to Canvas renderer on macOS for stability. WebGL is still used on other platforms.

To force a renderer for testing: localStorage.setItem('terminal-renderer', 'webgl' | 'canvas' | 'dom'), then reload. --disable-gpu remains a useful diagnostic if WebGL is forced.

Example of the corruption (before fix):

image

Status

  • Daemon infrastructure (socket, auth, IPC protocol)
  • Session management (create, attach, detach, kill)
  • Headless terminal emulator with snapshot/restore
  • Client integration in Electron main process
  • Sessions survive app restart
  • Settings UI toggle
  • TUI mode rehydration (alternate screen, cursor modes)
  • TUI restoration via SIGWINCH redraw
  • Connection timeout fix (prevents terminal hang)
  • Error UI with retry button
  • Fix xterm "dimensions" error on tab switch
  • Fix React StrictMode double-mount issue
  • Large paste reliability (subprocess + backpressure)
  • Fix WebGL render corruption on macOS
  • Fix TUI white screen on workspace switch (keep terminals mounted)
  • Optimize IPC payload (~50% reduction by removing duplicate scrollback data)

Files Changed

New Files

  • apps/desktop/src/main/terminal-host/ — Daemon process (7 files: index.ts, terminal-host.ts, session.ts, pty-subprocess.ts, pty-subprocess-ipc.ts, + tests)
  • apps/desktop/src/main/lib/terminal-host/ — Client library (client.ts, headless-emulator.ts, types.ts, + tests)
  • apps/desktop/src/main/lib/terminal/daemon-manager.ts — DaemonTerminalManager
  • apps/desktop/src/main/lib/terminal/pty-write-queue.ts — Input backpressure handling
  • apps/desktop/src/renderer/.../SettingsView/TerminalSettings.tsx — Settings UI
  • apps/desktop/src/renderer/.../Terminal/hooks/useTerminalConnection.ts — Connection hook
  • packages/local-db/drizzle/0004_add_terminal_persistence_setting.sql — Migration
  • apps/desktop/docs/2026-01-02-terminal-persistence-technical-notes.md — Technical notes (consolidated)

Modified Files

  • apps/desktop/src/main/lib/terminal/index.tsisDaemonModeEnabled reads from settings
  • apps/desktop/src/main/lib/terminal/session.ts — PTY session management
  • apps/desktop/src/main/lib/terminal/manager.ts — Terminal manager with daemon support
  • apps/desktop/src/main/lib/terminal/types.ts — Terminal type definitions
  • apps/desktop/src/main/lib/terminal/port-manager.ts — Port detection
  • apps/desktop/src/renderer/.../Terminal/Terminal.tsx — SIGWINCH TUI restore, error UI, StrictMode fix, IPC payload optimization
  • apps/desktop/src/renderer/.../Terminal/helpers.ts — Deferred GPU renderer, Canvas default on macOS
  • apps/desktop/src/renderer/.../Terminal/types.ts — Stream event types
  • apps/desktop/src/renderer/.../TabsContent/index.tsx — Keep terminals mounted when persistence enabled
  • apps/desktop/src/renderer/stores/app-state.ts — App state updates
  • apps/desktop/src/lib/trpc/routers/settings/index.ts — Terminal persistence setting endpoints
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts — Terminal router updates
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts — Workspace cleanup integration
  • apps/desktop/src/lib/trpc/routers/projects/projects.ts — Project cleanup integration
  • apps/desktop/src/main/index.ts — Daemon lifecycle management
  • apps/desktop/src/main/windows/main.ts — Window integration
  • apps/desktop/electron.vite.config.ts — Build config updates
  • apps/desktop/package.json — Dependencies
  • packages/local-db/src/schema/schema.ts — Settings schema
  • Settings sidebar (SettingsContent.tsx, GeneralSettings.tsx) — Terminal section in sidebar

@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

@github-actions
Copy link

github-actions bot commented Jan 4, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

andreasasprou and others added 22 commits January 4, 2026 22:12
Introduces a workspace-level view mode toggle allowing users to switch between:
- **Workbench mode**: Mosaic panes layout with terminals + file viewers for in-flow work
- **Review mode**: Dedicated Changes page for focused code review

Key changes:
- Add ViewModeToggle component in workspace header (prominent segmented control)
- Add FileViewerPane with Raw/Rendered/Diff modes, lock/unlock, and split support
- Add GroupStrip for group switching above Mosaic content
- Unify sidebar to use full ChangesView in both modes (with onFileOpen callback)
- Add workspace-view-mode store with per-workspace persistence
- Add readWorkingFile tRPC procedure for safe file reads (size/binary checks)
- Wire file clicks to open/reuse FileViewer panes (MRU unlocked policy)
- Cmd+T in Review mode switches to Workbench first, then creates terminal
- Add !!worktreePath checks to FileViewerPane query enabled conditions
- Add !!worktreePath checks to save handler guards (handleSaveRaw, handleSaveDiff)
- Fix stale state reference after addTab() in addFileViewerPane action

Addresses CodeRabbit review feedback.
… and UX improvements

- Add validatePathForWrite() to prevent path traversal attacks in saveFile
- Add aria-pressed attribute to ViewModeToggle buttons for accessibility
- Increase close button touch target in GroupStrip for better UX
- Add .mdx file support for rendered view mode
P0 Security:
- Add path validation to getUnstagedVersions before readFile (prevents traversal)
- Key Editor/DiffViewer by filePath to force remount (fixes stale Cmd+S closure)

P1 Fixes:
- Update originalContentRef when raw content loads (dirty tracking fix)
- Add .mdx to isMarkdown check for toolbar consistency
- Gate diff editable to staged/unstaged only (against-main/committed now read-only)

P2 Performance:
- Add safeGitShow helper with 2MB size limit on all git show calls
- Add size check before reading working tree files in getUnstagedVersions
- P0-1: Add file-viewer type and fileViewer object to paneSchema for tab persistence
- P0-2: Fix symlink escape vulnerability in validatePathForWrite by checking
  if target is symlink and resolving parent directory paths
- P1-1: Pass defaultBranch to FileViewerPane for against-main diffs
- P1-2: Switch staged diff to unstaged after save (matches Review mode behavior)
- P2: Use Buffer.byteLength instead of string.length in safeGitShow for
  accurate UTF-8 byte counting
P0 (critical):
- Remove duplicate saveFile from git-operations.ts that was overwriting
  the hardened version in file-contents.ts (security vulnerability)

P1 (must fix):
- Use basename()/dirname() from node:path instead of split('/') for
  cross-platform Windows compatibility in validatePathForWrite
- Add preflight size check with git cat-file -s in safeGitShow to
  prevent memory spikes from large blobs before materializing content

P2 (UX):
- Fix split pane to clone file-viewer state instead of creating terminal
  when splitting a file-viewer pane (locked by default)

Question fix:
- Show GroupStrip with add button even when tabs.length === 0 so users
  have visible UI to create new terminal (not just hotkey)
…ocalDb

P0 (CRITICAL SECURITY):
- Add validateWorktreePathInDb() that verifies worktreePath exists in
  localDb.worktrees before any filesystem operations
- Without this, a compromised renderer could read/write arbitrary files
  by passing worktreePath='/' and filePath='.ssh/id_rsa'
- Applied to getFileContents, saveFile, and readWorkingFile procedures

P1 (correctness):
- Replace startsWith('..') checks with segment-aware containsPathTraversal()
  and isPathOutsideBase() helpers that use path.sep
- Fixes false positives on valid paths like '..foo/bar' (directories
  starting with '..')
- Cross-platform compatible (handles both / and \ separators)

P2 (performance):
- Guard killTerminalForPane() calls on pane.type === 'terminal'
- Prevents unnecessary IPC and warning logs when closing file-viewer panes
…ve editor drafts

P0 (security):
- Extract assertWorktreePathInDb to shared security.ts
- Returns worktree record to avoid duplicate queries
- Apply validation to ALL routes: git-operations, status, staging, branches

P1 (data loss):
- Preserve unsaved editor content across view mode switches
- Store draft in ref before switching away from raw mode
- Restore draft when returning to raw mode
- Clear draft on save and file change
- Update dirty state on editor mount with draft content
…otection

P0 Security (BLOCK):
- Add validatePathInWorktree() check before rm() in deleteUntracked
- Prevents path traversal (../) and symlink escape attacks
- Consolidated path validation utilities in security.ts

P1 Data Loss:
- Track save source (Raw vs Diff) with savingFromRawRef
- Only clear draft when saving from Raw mode
- Disable Diff editing when Raw draft exists (forces user to save/discard)

P2:
- Use ['--', filePath] in git.add() to handle paths starting with -
…th validation

- Reduce security.ts from 213 to 65 lines
- Remove async symlink/realpath checking (users own their repos)
- Remove segment-aware path traversal (..foo edge case not worth complexity)
- Keep worktreePath DB validation (the real security boundary)
- Keep simple .. and absolute path checks (sufficient for 99.99% of cases)
P0 (blocking):
- Add path validation (rejects .. and absolute) to applyUntrackedLineCount
- Add 1MB size cap to prevent OOM on large untracked files during polling

P2:
- Add type guard in updateTabLayout - only call killTerminalForPane for terminal panes
- Use ['--', branch] in switchBranch to ensure branch treated as refname
…lidation

- Add path-validation.ts with industry-standard containment check (path.relative)
- Add secure-fs.ts with self-validating FS wrappers (symlink escape protection)
- Add git-commands.ts with semantic helpers (gitSwitchBranch vs gitCheckoutFile)
- Fix switchBranch: use 'git switch' instead of 'git checkout --' (was file checkout)
- Fix path traversal check: segment-aware (allows ..foo, rejects ..)
- Fix symlink bypass: check parent dirs for new files, use stat not lstat
- Fix worktree root deletion: explicit allowRoot check
- All FS operations now go through secureFs with validation built-in
Add a setting to control whether Cmd+clicking file paths in the terminal
opens them in an external editor (default) or in the in-app FileViewerPane.

- Add terminalLinkBehavior setting to local-db schema with migration
- Add getTerminalLinkBehavior/setTerminalLinkBehavior tRPC procedures
- Refactor createTerminalInstance to accept onFileLinkClick callback
- Wire up setting in Terminal.tsx with ref pattern (avoids terminal recreation)
- Add Select UI in BehaviorSettings for choosing link behavior
Remove async symlink escape detection from path validation since the
threat model doesn't justify it: a compromised renderer already has
terminal access for arbitrary command execution.

Changes:
- Replace resolveSecurePath (async) with resolvePathInWorktree (sync)
- Remove assertNoSymlinkEscape and checkSymlinks option
- Add validateRelativePath for simple path safety checks
- Update secure-fs.ts to use new sync functions
- Update threat model documentation in path-validation.ts
…s sidebar)

Add a setting to let users choose between displaying workspaces as
horizontal tabs in the TopBar (current behavior) or in a dedicated left
sidebar (new feature, similar to Linear/GitHub Desktop).

Key changes:
- Add navigationStyle column to settings table (migration 0005)
- Add navigation style dropdown in Behavior Settings
- Create WorkspaceSidebar component with collapsible project sections
- Create shared useWorkspaceShortcuts hook (⌘1-9 shortcuts, auto-create)
- Update TopBar to conditionally render based on navigation style
- Add ⌘⇧B hotkey to toggle workspace sidebar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ition

P0: Add CreateWorkspaceButton to TopBar when in sidebar mode
- Button was previously only rendered via WorkspaceTabs
- Now renders in top-right section when navigationStyle is sidebar

P1: Fix race condition in createBranchWorkspace
- Add unique partial index on (projectId) WHERE type='branch'
- Use INSERT ON CONFLICT DO NOTHING to handle concurrent calls
- If conflict, fetch the existing workspace instead of failing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
WorkspaceListItem now has full feature parity:
- Close/delete button with confirmation dialog
- Context menu (Rename, Open in Finder)
- Hover card with PR details, checks, reviews
- Needs attention indicator (red pulse)
- Inline rename (double-click)
- Drag & drop reordering
- BranchSwitcher for branch workspaces

Other changes:
- Remove CreateWorkspaceButton from TopBar in sidebar mode
- Add per-project "Add workspace" dropdown (New Workspace, Quick Create)
- Add preSelectedProjectId to modal store for pre-selecting project
- Simplify GroupStrip to use consistent browser-tab style

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Extract useWorkspaceDeleteHandler hook for shared delete logic
- Use existing extractPaneIdsFromLayout from tabs/utils instead of inline collectPaneIds
- Add named constants for magic numbers (staleTime, delays, shortcut index)
- Reduces code duplication between WorkspaceItem and WorkspaceListItem

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ar mode

Move SidebarControl to shared location (screens/main/components/) and
conditionally render based on navigation style:
- top-bar mode: toggle in TopBar (unchanged)
- sidebar mode: toggle in WorkspaceActionBar (left side, before branch info)

This improves UX by placing the sidebar toggle closer to the content
it controls when in sidebar navigation mode.
Default to Mac layout (80px padding) while platform query is loading,
since undefined === 'darwin' evaluates to false, causing 16px padding
to be used initially on Mac before the query resolves.
…minal links

P0: Fix branch workspace support in assertRegisteredWorktree
- Extended validation to check both worktrees.path AND projects.mainRepoPath
- Branch workspaces use mainRepoPath which wasn't being validated

P1: Fix terminal file-viewer links for absolute paths
- Normalize absolute paths to worktree-relative before opening file viewer
- File viewer expects relative paths but terminal links can be absolute

P2: Fix misleading security comments
- Removed claims about symlink checks that aren't implemented
- Comments now accurately describe worktree registration + path traversal validation
Merge 3 separate documentation files into a single comprehensive reference:
- TERMINAL_RENDERING_REATTACH_RESEARCH.md (research log)
- 20251229-terminal-host-daemon-terminal-persistence.md (exec plan)
- LARGE_PASTE_HANG_ANALYSIS.md (bug analysis)

New file uses date prefix for chronological sorting.
- Add escalation watchdog in handleKill: SIGTERM → SIGKILL → force exit
  node-pty's onExit callback doesn't fire reliably after pty.kill(SIGTERM)
- Fix dispose() async bug: capture subprocess ref before nullifying
- Add diagnostic logging throughout kill flow for debugging
- Fix Terminal.tsx hook dependency warnings with targeted biome-ignore
- Add TERMINAL_HOST_RUNBOOK.md for daemon debugging/testing
Keep essential warnings (force exit, attach timeout, force dispose stuck session).
Remove step-by-step debugging logs that are too noisy for production.
…kspace switch

When terminal persistence is enabled, render all tabs from all workspaces
and use visibility:hidden for inactive ones. This eliminates the
unmount/remount cycle that caused race conditions during TUI reattach.

Changes:
- TabsContent: query terminalPersistence setting, render all tabs when enabled
- TerminalSettings: add memory warning copy
- Terminal.tsx: remove debug logging, add comments clarifying SIGWINCH as fallback
- Technical notes: document the approach and trade-offs
In daemon mode, both scrollback and snapshot.snapshotAnsi contained
identical ANSI content, doubling IPC payload size (~500KB → 1MB).

Changes:
- daemon-manager: set scrollback to empty string in daemon mode
- Terminal.tsx: use initialAnsi variable preferring snapshot.snapshotAnsi
- Terminal.tsx: use snapshot.cwd directly instead of parsing ANSI
- Terminal.tsx: only run escape scanning when snapshot.modes unavailable
- types.ts: add JSDoc clarifying daemon mode behavior

~50% reduction in IPC payload for terminal sessions.
- Remove unused ptyPid property from Session
- Remove unused flushEmulatorWrites method (superseded by flushEmulatorWritesUpTo)
- Remove unused getSession method (superseded by getActiveSession)
- Fix template literal style in Terminal.tsx
- Fix export ordering in hooks/index.ts
Preserves terminal scrollback across daemon restarts/reboots. When app
restarts without a running daemon, previous session output is restored
with a full-screen overlay prompting user to start a new shell.
The rebase incorrectly kept an old version that used a non-existent
.display property on HOTKEYS instead of the formatHotkeyDisplay function.
The rebase incorrectly applied older versions of workspace sidebar
files, removing context menus, add workspace button, drag-and-drop,
hover cards, and other functionality.
- Restore migrations 0004-0007 from workspace-sidebar branch
- Delete incorrectly consolidated 0004_settings_workspace_improvements.sql
- Add new 0009_add_terminal_persistence migration
- Remove deprecated navigationStyle/groupTabsPosition settings router code
- Keep only terminal persistence procedures in settings router
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