feat(desktop): add 3-color workspace status indicators#3
Closed
andreasasprou wants to merge 51 commits intoworkspace-sidebarfrom
Closed
feat(desktop): add 3-color workspace status indicators#3andreasasprou wants to merge 51 commits intoworkspace-sidebarfrom
andreasasprou wants to merge 51 commits intoworkspace-sidebarfrom
Conversation
Implement terminal session persistence using a background daemon process that survives app restarts. Key features: - Terminal host daemon: Long-lived process that owns PTYs and maintains terminal emulation state while Electron app is closed - Headless terminal emulator: Captures full terminal state (screen, scrollback, modes) for perfect resume - NDJSON-over-Unix-socket IPC: Secure communication with token authentication - DaemonTerminalManager: Drop-in replacement that delegates to daemon while preserving existing TRPC API When enabled (SUPERSET_TERMINAL_DAEMON=1), terminals persist across app quit/restart with exact screen state and interactive input working immediately.
- Add 'Terminal' settings section with persistence toggle - Settings UI allows enabling terminal persistence without env var - Add TRPC endpoints for terminalPersistence setting (get/set) - Add local-db migration for terminal_persistence column - isDaemonModeEnabled now reads from settings (cached at startup) - Apply rehydrateSequences in Terminal.tsx for TUI app restore - Env var SUPERSET_TERMINAL_DAEMON=1 still works as override
- P0-1: Use getActiveTerminalManager() instead of hardcoded terminalManager - P0-2: Make attach() async with getSnapshotAsync() to flush pending writes - P1-1: Implement chunk-safe escape sequence parsing with carry buffer - P1-2: Add socket liveness check and spawn lock to prevent daemon races - P2-1: Truncate/redact sensitive data in NDJSON parse error logs - Q1: Query daemon listSessions for workspace cleanup after app restart
- P0: Fix escape sequence buffering to only buffer DECSET/DECRST and OSC-7 (prevents memory leak from buffering color codes like ESC[31m) - P1: Use getActiveTerminalManager() in main.ts detachAllListeners - P1: Ensure ~/.superset* directory exists before spawn lock write - P1: Resize session to requested dimensions on attach (with try-catch) - P2: Make getSessionCountByWorkspaceId async and query daemon after restart - Fix flaky session-lifecycle test by waiting for session ready state and handling PTY EBADF errors during resize
- P0: Fix dead sessions - dispose and recreate when session exists but !isAlive Also improved cleanup: reschedule if clients attached, cleanup on detach - P1: Fix snapshot restore order - rehydrateSequences BEFORE snapshotAnsi (matches headless emulator's applySnapshot order for correct TUI restoration) - P1: Fix stale PID reuse - check socket liveness first, then clean up stale PID (prevents daemon startup failure when PID is reused by another process) - P2: Centralize socket disconnect handling in daemon handleConnection (avoids per-session socket listeners that could cause MaxListenersExceeded)
- killByWorkspaceId now always queries daemon for authoritative session list - getSessionCountByWorkspaceId now always queries daemon first - Fixes orphan sessions when users partially reattach after app restart
- Add shutdown IPC request type to daemon protocol - Add shutdown handler to daemon (graceful shutdown after response) - Add shutdown() method to terminal host client - Add restartDaemon tRPC endpoint in settings router - Add 'Restart Daemon' button in Terminal settings (visible when persistence enabled) This allows users to restart the daemon to pick up new code after app updates.
@xterm/headless is a pure JS package that should be bundled, not externalized. Only native modules (better-sqlite3, node-pty) need to be external. Externalizing @xterm/headless caused the daemon to crash on startup because it couldn't find the module.
…minal hang - Add ConnectionState enum to replace boolean flag for clearer state management - Add 10-second timeout to ensureConnected() polling loop that was hanging forever - Properly set state to DISCONNECTED on connection failure for error recovery - Add error UI with retry button in Terminal component when connection fails Fixes terminal hang on initial app load when multiple terminals connect simultaneously.
xterm.write() is asynchronous - escape sequences may not be fully processed when the terminal first renders, causing garbled display. Force a re-render after write completes to ensure correct display. Symptom: restored terminals showed corrupted text until panel was resized.
…nal refresh The previous xterm.refresh() approach wasn't working because the write callback fires when data is parsed, not when it's rendered. Using fitAddon.fit() inside requestAnimationFrame ensures we're after the render cycle, triggering a full re-layout that fixes the garbled display. Also fixed handleRetryConnection which was missing the refresh fix.
The DaemonTerminalManager singleton caches a reference to the TerminalHostClient. When restartDaemon disposes the client, the manager still held the old disposed client reference, causing all terminal operations to hang. Fix: - Add disposeDaemonManager() function to reset the manager singleton - Call it from restartDaemon alongside disposeTerminalHostClient() - Also fix disconnect() to set connectionState to DISCONNECTED
…nal state Existing mounted terminals don't detect daemon restart - their tRPC subscriptions just stop receiving data silently. Reloading the window ensures all terminal components get fresh state and can reconnect to the new daemon.
Manual recovery via 'pkill -f terminal-host' if daemon becomes unresponsive. Daemon will respawn automatically on next terminal operation. Auto-recovery can be implemented in v2 if users report frequent issues.
…terminals When the daemon is killed or crashes, existing terminals now: - Receive disconnect events through tRPC subscription - Show error overlay with 'Retry Connection' button - Can reconnect when daemon respawns Changes: - DaemonTerminalManager emits disconnect events for all sessions on client disconnect - Terminal router forwards disconnect events through stream subscription - Terminal.tsx handles disconnect event to show error UI
When restoring TUI applications (claude, vim, etc.) after app restart, keyboard input would fail with xterm crash: 'Cannot read properties of undefined (reading dimensions)'. Root cause: xterm's internal viewport/render service wasn't fully initialized when rehydrateSequences (alternate screen mode escapes) were written immediately after open(). Fix: Gate restoration until xterm fires its first onRender event, then apply pending restoration data. This ensures the renderer is ready to handle escape sequences that modify terminal state. - Add didFirstRenderRef to track when xterm has rendered once - Add pendingInitialStateRef to store restoration data - Add maybeApplyInitialState() that runs only when both conditions met - Apply same pattern to all three restore paths: createOrAttach, restartTerminal, handleRetryConnection
When typing in TUI apps (codex, vim, etc.) that use alternate screen mode, the auto-title feature was incorrectly capturing keyboard input (e.g. 'hello') and setting it as the tab name. Fix: Check xterm.buffer.active.type before setting auto-title from keyboard input. TUI apps use 'alternate' screen buffer, so we skip keyboard-based auto-titling for them. TUI apps can still set their own title via escape sequences (handled by onTitleChange).
…ion content After restoration, xterm.js didn't know it was in alternate screen mode because rehydrateSequences intentionally excludes the 1049 escape sequence (sending it after content would clear the screen). Fix: Check snapshot.modes.alternateScreen and send the alternate screen escape sequence BEFORE writing any content. This makes xterm.js properly track that it's in alternate buffer mode, which is needed for: - Correct auto-title behavior (don't capture keyboard in TUI apps) - Proper xterm buffer state for applications expecting alternate screen
…cted fast path This log fired on every single daemon API call (write, resize, etc.) which spammed the console with hundreds of messages. The fast path doesn't need logging since it's the normal successful case.
…g on xterm When the Terminal component remounts (HMR, recovery), a new xterm instance is created that doesn't know about escape sequences sent before it existed. Codex may have sent the alternate screen escape sequence, but the new xterm never saw it, so xterm.buffer.active.type incorrectly returns 'normal'. Fix: Track alternate screen mode ourselves via isAlternateScreenRef: - Set from snapshot.modes.alternateScreen on restore - Update when receiving escape sequences in stream data (1049h/l, 47h/l) - Use this ref instead of xterm.buffer.active.type for auto-title decisions - Reset on cleanup and terminal restart
…ollback The previous fix tracked isAlternateScreenRef but only detected escape sequences in handleStreamData, which runs when isStreamReadyRef is true. Events arriving before stream was ready were queued to pendingEventsRef and flushed without parsing for escape sequences. This fix adds escape sequence detection to: 1. flushPendingEvents - for events queued during initial load 2. maybeApplyInitialState - parses scrollback for enter/exit sequences Fixes TUI apps (Codex, vim) incorrectly triggering auto-title from keyboard input.
- Clear xterm-webgl texture atlas after rehydration to force a clean repaint - Add a first-render fallback so restored sessions can't get stuck not-ready - Surface terminal stream error events
- Run PTYs in per-session subprocesses and frame IO as binary - Time-slice headless emulator output processing to keep daemon responsive - Handle PTY input backpressure (EAGAIN/EWOULDBLOCK) without dropping chunks - Improve renderer paste handling (bracketed paste + chunking) and surface errors
- Track async GPU renderer via ref to reliably clear WebGL atlas - Schedule fit+refresh on focus/resize to avoid partial renders when switching panes
Avoid fit/PTY resizes and WebGL atlas clears on focus; do a lightweight refresh instead.
- Remove duplicate terminalError handler in daemon-manager.ts (was causing duplicate event emissions) - Fix failing write test in manager.test.ts (add async wait for PtyWriteQueue flush before assertion) - Fix attach() hang with continuous output in session.ts (add 500ms timeout to flushEmulatorWrites to prevent indefinite hang when processes like tail -f produce continuous output) - Apply biome formatting fixes
Retry focus redraw until container has non-zero size to avoid WebGL glitches on tab switches.
- Default xterm renderer to Canvas on macOS to avoid WebGL corruption on tab switches - Remove focus redraw hacks that were amplifying WebGL glitches
- Fix initialCommands race: use waitForReady() instead of setTimeout(100) - Add PTY ready promise in session to signal when subprocess spawned - Add queue limits for both subprocess stdin and client notify queues - Emit terminalError when writeNoAck drops input due to full queue - Complete detachAllListeners: add disconnect: and error: prefixes - Shutdown orphaned daemon when persistence disabled on app startup
- Extract magic number to SESSION_CLEANUP_DELAY_MS constant in daemon-manager - Move planning docs to docs/ directory - Extract useTerminalConnection hook to encapsulate tRPC mutations and refs - Refactor Terminal.tsx to use the new hook, reducing component complexity
…tence disabled Previously, shutdownOrphanedDaemon() would call client.shutdown() which internally calls ensureConnected(), spawning a new daemon just to immediately shut it down. This happened on every app startup when terminal persistence was disabled. Added tryConnectAndAuthenticate() and shutdownIfRunning() methods to TerminalHostClient that only attempt cleanup if a daemon is already running, avoiding wasteful spawn+kill cycles.
- Reframe PtyWriteQueue docstring to accurately describe limitations (reduces event loop starvation, does not prevent blocking) - Rename prototype/ to __tests__/ for conventional test organization
For TUI sessions (alternate screen mode), serialized snapshots render incorrectly due to styled spaces and positioning issues. Instead of trying to perfectly serialize and restore TUI state, we now: 1. Skip writing the broken snapshot for alt-screen sessions 2. Enter alt-screen mode directly 3. Enable streaming first so live PTY output comes through 4. Trigger SIGWINCH via resize down/up - TUI redraws itself Trade-off: Brief visual flash as TUI redraws, but the result is correct. Normal shell sessions still use the snapshot approach which works well. - Add SIGWINCH-based redraw for TUI (alt-screen) session reattach - Remove dead resize nudge code (now handled by SIGWINCH approach) - Clean up verbose debug logging from investigation - Update research doc with final fix documentation - Add snapshot boundary tracking for consistent daemon-side captures
…allback The fallback logic for detecting alternate screen mode was using presence/absence checks (includes) instead of position comparison (lastIndexOf). This caused incorrect detection when a user entered and exited alternate screen multiple times (e.g., opened vim, closed it, opened it again). Changed to use lastIndexOf comparison, matching the pattern already used in updateModesFromData and for bracketed paste detection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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
Implements workspace status indicators showing agent lifecycle states: - Amber (pulsing): Agent actively processing - Red (pulsing): Agent blocked, needs user input - Green (static): Agent completed, ready for review Key features: - Status aggregation: workspace shows highest-priority status across all panes - Click behavior: review → idle, permission → working, working unchanged - App restart: stale 'working' status cleared on startup - Migration: old needsAttention boolean migrated to status enum Dev/prod separation hardening: - Removed global OpenCode plugin write (was causing cross-talk) - Added startup cleanup for stale global plugins - Server ignores unknown event types (forward compatibility) - notify.sh no longer defaults to 'Stop' on parse failure - Added SUPERSET_ENV and SUPERSET_HOOK_VERSION to terminal environment - Server validates environment and logs mismatches
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 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. Comment |
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements workspace status indicators showing agent lifecycle states:
Stack
Changes
Workspace Status Indicators
PaneStatusenum:"idle" | "working" | "permission" | "review"review→idle(acknowledged)permission→working(assumes permission granted)working→ unchanged (persists until Stop event)needsAttentionboolean migrated tostatus: "review"Agent Hook Integration
UserPromptSubmit→ Start,Stop→ Stop,PermissionRequest→ Permissionsession.status.busy→ Start,session.status.idle→ Stop,permission.ask→ PermissionDev/Prod Separation (Hardening)
notify.shno longer defaults to "Stop" on parse failureSUPERSET_ENVandSUPERSET_HOOK_VERSIONto terminal environmentUI Locations Updated
WorkspaceItem.tsx)WorkspaceListItem.tsx)GroupStrip.tsx)TabItem/index.tsx)Testing
mapEventType()and terminal env varsBreaking Changes
None - backwards compatible with existing persisted state (migration handled automatically)