Skip to content

feat(desktop): add 3-color workspace status indicators#3

Closed
andreasasprou wants to merge 51 commits intoworkspace-sidebarfrom
working-indicators
Closed

feat(desktop): add 3-color workspace status indicators#3
andreasasprou wants to merge 51 commits intoworkspace-sidebarfrom
working-indicators

Conversation

@andreasasprou
Copy link
Copy Markdown
Owner

Summary

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

Stack

⚠️ This PR is stacked on superset-sh/superset#559 (workspace-sidebar) and should be merged after that PR.

Changes

Workspace Status Indicators

  • PaneStatus enum: "idle" | "working" | "permission" | "review"
  • Status aggregation: workspace shows highest-priority status across all panes (permission > working > review)
  • Click behavior:
    • reviewidle (acknowledged)
    • permissionworking (assumes permission granted)
    • working → unchanged (persists until Stop event)
  • App restart: stale "working" status cleared on startup
  • Migration: old needsAttention boolean migrated to status: "review"

Agent Hook Integration

  • Claude Code: UserPromptSubmit → Start, Stop → Stop, PermissionRequest → Permission
  • OpenCode: session.status.busy → Start, session.status.idle → Stop, permission.ask → Permission
  • Codex: partial support (review only, no working indicator)

Dev/Prod Separation (Hardening)

  • Removed global OpenCode plugin write (was causing cross-talk between dev/prod)
  • 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

UI Locations Updated

  • Top bar workspace tabs (WorkspaceItem.tsx)
  • Sidebar workspace list (WorkspaceListItem.tsx)
  • Group strip tabs (GroupStrip.tsx)
  • Tab item in sidebar (TabItem/index.tsx)

Testing

  • Manual QA completed for Claude Code and OpenCode
  • Unit tests added for mapEventType() and terminal env vars

Breaking Changes

None - backwards compatible with existing persisted state (migration handled automatically)

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
andreasasprou and others added 21 commits January 2, 2026 18:13
- 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
@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
superset-api Error Error Jan 3, 2026 8:32am
superset-web Error Error Jan 3, 2026 8:32am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 3, 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
Copy Markdown

github-actions Bot commented Jan 3, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

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.

1 participant