Problem
DanCode's terminal experience is built around tmux as both the backend process manager and the user-facing UX layer. Tmux concepts — grouped sessions, window indices, status bars, attach commands — leak directly into the browser experience. Layout management is split between React and tmux, causing quirks with mouse handling, resize behavior, and multiplexing. Creating a "grouped connection session" per browser tab just to achieve independent window views is a workaround, not an architecture. The result is a functional but hacky experience that fights tmux's design rather than building on top of it cleanly.
Solution
Rearchitect the terminal layer so the frontend fully owns the terminal UX — layout, multiplexing, pane management, labels, and resize — while the server provides a clean terminal abstraction backed by direct PTY connections. Tmux operates invisibly as a persistence layer: each server-managed PTY is backed by a hidden tmux session so processes survive browser disconnects and server restarts, but no tmux concept is ever exposed to the client. The server maintains an output ring buffer per terminal for seamless reconnection replay. A file explorer is added as a new panel type alongside terminals. A mobile-first PWA experience provides monitoring and light interaction from a phone — swipe navigation, a system-wide shortcut bar, and a status dashboard designed for checking on running agents. As a stretch goal, sessions remain directly accessible from the host environment for power users who want to drop into a shell.
Requirements
Phase 1: Direct PTY & Frontend-Owned Layout
Server — TerminalManager
- Server exposes a terminal CRUD API: create, list, get, destroy terminals
- Each terminal is identified by a unique ID (not a tmux window index)
- Creating a terminal spawns a real PTY process (
bash or user's default shell) via node-pty
- Terminals are associated with a project (by slug) and have a user-defined label
- Terminal creation accepts optional
command param (e.g., claude --dangerously-skip-permissions) run inside the shell
- Terminal creation accepts optional
cwd param (defaults to project path)
- Server maintains an output ring buffer (~50KB) per terminal for reconnection replay
- When a browser disconnects, the PTY stays alive and continues buffering output
- When a browser reconnects to a terminal ID, the buffered output is replayed before resuming live streaming
resize events from the client resize the PTY directly (no tmux intermediary)
- Terminal metadata (id, project, label, state) is persisted in
~/.dancode/terminals/ so the server can rediscover terminals after restart
Server — API
POST /api/terminals — create terminal (params: projectSlug, label, command?, cwd?) → returns terminal object with id
GET /api/terminals?project=<slug> — list terminals for a project
GET /api/terminals/:id — get terminal metadata
PATCH /api/terminals/:id — update label or other metadata
DELETE /api/terminals/:id — kill PTY process and remove terminal
- WebSocket
/terminal/:id — bidirectional I/O (replaces current /terminal namespace with query params)
Client — TerminalLayout (replaces PaneLayout)
- Layout is 100% frontend-owned: no fetching tmux windows from server
- Supports split view (side-by-side, resizable dividers) and tabbed view
- Mobile (<768px) always uses tabbed view
- Users can create new terminals from the UI ("+" button) — opens in project directory by default
- Users can close/kill terminals from the UI (with confirmation)
- Users can rename terminal labels inline
- Users can reorder tabs via drag-and-drop
- Users can toggle terminals visible/hidden without killing them
- Layout state (which terminals are visible, order, split sizes, active tab) persisted in project config
- Each terminal pane renders an xterm.js instance connected via WebSocket to
/terminal/:id
- Click-to-focus with visual indicator (accent bar) on focused pane
Ctrl+Scroll font sizing, Ctrl+C copy, Ctrl+V paste all preserved
Client — Project Creation (updated)
- Remove "Adopt existing tmux session" flow entirely
- New project creates 2 default terminals: "CLI" (shell) and "Claude" (runs claude command)
- No hardcoded pane types — terminals are generic, labels are just strings
Migration / Cleanup
- Remove
tmux.js module (or gut it for Phase 2 reuse)
- Remove grouped session logic from
terminal.js
- Remove tmux status dots, attach command bar, and tmux context menu items from Sidebar
- Remove
tmuxSession, showTmuxCommands from project config schema
- Remove
/api/tmux-status and /api/tmux/sessions endpoints
Phase 2: Invisible Tmux Persistence
Server — Tmux as Hidden Backend
- Each PTY is spawned inside a hidden tmux session rather than as a bare process
- Tmux session naming is deterministic and internal:
dancode-{projectSlug}-{terminalId}
- The server interacts with the PTY via node-pty spawning a shell inside the tmux session (NOT
tmux attach)
- Tmux is never referenced in any client-facing API response or WebSocket message
- On server startup, the TerminalManager scans for existing
dancode-* tmux sessions and reconciles with terminal metadata files — orphaned sessions are reclaimed, metadata without sessions is cleaned up
- If a PTY handle is lost (server crash) but the tmux session survives, the server reattaches transparently on restart
- Output ring buffer is repopulated from tmux scrollback on reattach (
tmux capture-pane)
Server — Resilience
- Terminal metadata files in
~/.dancode/terminals/ include the backing tmux session name
- Server restart triggers full reconciliation: match metadata ↔ tmux sessions ↔ PTY handles
- Terminals that were running before a crash appear as "reconnecting" in the UI, then resume automatically
- Stale metadata (tmux session gone, no PTY) is cleaned up with a warning log
Stretch Goal: Direct Host Access
- Tmux sessions use clean, human-readable names (
dancode-{projectSlug}-{label})
- A user can
tmux ls on the host and see all DanCode-managed sessions
- A user can
tmux attach -t dancode-myproject-cli to directly access a terminal
- Optional: a
dancode CLI helper script that lists projects and attaches to sessions (dancode attach myproject cli)
- Input from direct tmux attach and browser are interleaved — both see the same terminal
- This is explicitly a power-user feature; the web UI remains the primary interface
Phase 3: File Explorer
Server — File System API
GET /api/files?path=<dir> — list directory contents (name, type, size, modified date), scoped to project path
GET /api/files/read?path=<file> — read file contents (with size limit, e.g., 1MB)
PUT /api/files/write — write file contents (path + content in body)
POST /api/files/mkdir — create directory
POST /api/files/rename — rename/move file or directory
DELETE /api/files — delete file or directory (with confirmation)
- All paths are validated to be within the project directory (no path traversal)
- Symlinks are followed but validated to stay within project bounds
- Hidden files (dotfiles) shown with a toggle
Client — File Explorer Panel
- Tree view panel that can appear alongside terminal panes (left side or as a tab)
- Lazy-loaded: directories expand on click, fetching contents on demand
- File icons by extension (simple icon set — folders, code files, config, images, etc.)
- Single-click to select, double-click to... (future: open in editor; for now: copy path to clipboard or insert into focused terminal)
- Right-click context menu: rename, delete, copy path, new file, new folder
- Drag-and-drop files to terminal panes to insert the file path
- Search/filter within the current directory tree
- Respects
.gitignore patterns by default (toggle to show ignored files)
- Collapsible — can be hidden to maximize terminal space
- File explorer state (expanded directories, scroll position) persisted per project
Integration with Terminals
- "Open terminal here" context menu option on directories — creates a new terminal with
cwd set to that directory
- Dragging a file onto a terminal inserts the relative path
Phase 4: Mobile Experience & PWA
PWA Foundation
- Add web app manifest (
manifest.json) with app name, icons, theme color (Solarized Dark), display: standalone
- Add service worker for offline shell (cache app assets, show "offline" state when server unreachable)
- "Add to Home Screen" works on Android Chrome — launches full-screen, no browser chrome
- Viewport and touch meta tags for native-feeling mobile experience
- App icon and splash screen in Solarized Dark branding
Mobile Layout — Monitoring-First Design
- Default mobile view is a status dashboard, not a terminal
- Dashboard shows all projects in a card list: project name, terminal labels, activity indicator (last output timestamp, active/idle/stopped), and a preview of the last few lines of output per terminal
- Tap a project card to expand and see its terminals
- Tap a terminal to enter full-screen terminal view
- Back gesture or button returns to dashboard
Terminal View — Mobile Optimized
- Terminal takes full screen (no sidebar, no header — just the terminal + a thin top bar with back button and terminal label)
- Read-first: keyboard is dismissed by default, terminal output is scrollable
- Tap the terminal area or a "keyboard" button to enter input mode (soft keyboard appears)
- Auto-dismiss keyboard after pressing Enter (configurable — some users may want it to stay)
Shortcut Bar
- Persistent bottom bar when keyboard is active, sitting above the soft keyboard
- System-wide shortcuts (not per-project):
Ctrl+C, Ctrl+V, Ctrl+D, Tab, Up Arrow, Down Arrow, Esc
- Scrollable horizontally if more shortcuts than screen width
- Buttons are touch-friendly (min 44px tap targets)
- Shortcut bar hides when keyboard is dismissed (read mode)
Swipe Navigation
- Swipe left/right between terminals within the same project
- Dot indicators at top show which terminal is active (like iOS page dots)
- Swipe from left edge opens project list (drawer-style)
- Swipe down from top of terminal to return to dashboard
Mobile-Specific Features
- Pull-to-refresh on dashboard to update activity status
- Long-press a project card for quick actions (kill all terminals, open CLI, open Claude)
- Haptic feedback on shortcut button taps (where supported)
- Font size respects system accessibility settings, with pinch-to-zoom override
- Landscape mode: terminal uses full width, shortcut bar along the bottom
Responsive Breakpoints
< 480px (phone portrait): dashboard cards stack, single terminal full-screen, shortcut bar active
480–768px (phone landscape / small tablet): same as above but wider terminal
768–1024px (tablet): optional split view (2 terminals), shortcut bar available but optional
> 1024px (desktop): full desktop layout, no shortcut bar, no swipe navigation
UX / Interface (all phases)
- Solarized Dark theme maintained throughout
- No tmux concepts visible anywhere in the UI — no session names, attach commands, or status bars referencing tmux
- All terminal management is presented as "terminals" not "panes" or "windows"
- Smooth reconnection UX: terminal shows "Reconnecting..." overlay, then replays buffer and resumes
- Connection state indicators per terminal (connected / reconnecting / disconnected)
- Responsive: splits on desktop, tabs on mobile, file explorer collapses on narrow viewports
Data / Persistence
- Terminal metadata:
~/.dancode/terminals/{terminalId}.json — id, projectSlug, label, tmuxSessionName, createdAt, lastActivity
- Project config:
~/.dancode/projects/{slug}.json — updated schema drops tmux fields, adds layout object for frontend state
- Output ring buffer: in-memory only (not persisted to disk) — repopulated from tmux scrollback on restart
- File explorer state: persisted in project config (expanded paths, panel visibility)
Technical Decisions
- Stack: No changes — Node.js, Express, Socket.io, React, Vite, xterm.js, Tailwind. Proven and working.
- Architecture: New
TerminalManager class server-side replaces current tmux.js + terminal.js. Owns PTY lifecycle, output buffering, and reconnection. Single responsibility: manage terminal processes.
- Data model: Terminals become first-class entities with their own ID and metadata file, decoupled from tmux window indices. Projects reference terminals by ID, not by pane index.
- API surface: Clean REST for terminal CRUD + WebSocket per terminal for I/O. No tmux vocabulary in any API contract. File system API for explorer with strict path validation.
- Persistence strategy: Phase 1 uses bare PTYs (sessions lost on server restart). Phase 2 adds invisible tmux backing for full persistence. This phasing means Phase 1 is shippable without tmux complexity.
- Output buffer: ~50KB circular buffer per terminal, in-memory. Enough to replay a screenful+ on reconnect. Not persisted — tmux scrollback is the durable copy (Phase 2).
- Mobile: PWA (manifest + service worker), not a native app. Touch gestures via standard DOM events or a lightweight library (e.g., Hammer.js). Shortcut bar is a React component, not a native keyboard extension. Responsive breakpoints handled with Tailwind's existing breakpoint system.
- Testing: Existing Vitest + Playwright stack. New tests for TerminalManager lifecycle, reconnection replay, file system API path validation. Playwright mobile emulation for PWA/mobile layout tests. Visual tests updated to assert on new layout.
Out of Scope
- Code editor / Monaco integration (future phase, after file explorer proves useful)
- Ralph UI controls (separate PRD)
- Multi-server management
- Multi-user / role-based access
- Light mode / theming beyond Solarized Dark
- Session recording / playback
- Git integration in the UI
- Native Android/iOS app (PWA covers mobile; native wrapper via Capacitor is a future option if push notifications or app store presence become important)
- Offline terminal interaction (PWA caches the app shell, but terminals require server connectivity)
Open Questions
- Shell selection: Should terminal creation auto-detect the user's default shell (
$SHELL) or always use bash? Leaning toward $SHELL with bash fallback.
- Buffer size: 50KB per terminal is a starting point. Should this be configurable per-project? Probably not worth the complexity initially.
- File explorer size limits: What's the right max file size to read via the API? 1MB seems reasonable for text files. Binary files should be blocked or download-only.
- Terminal limits: Should there be a max number of terminals per project? Probably not enforced, but the UI should handle 10+ terminals gracefully (tabs scroll or overflow).
🤖 Generated with Claude Code
Problem
DanCode's terminal experience is built around tmux as both the backend process manager and the user-facing UX layer. Tmux concepts — grouped sessions, window indices, status bars, attach commands — leak directly into the browser experience. Layout management is split between React and tmux, causing quirks with mouse handling, resize behavior, and multiplexing. Creating a "grouped connection session" per browser tab just to achieve independent window views is a workaround, not an architecture. The result is a functional but hacky experience that fights tmux's design rather than building on top of it cleanly.
Solution
Rearchitect the terminal layer so the frontend fully owns the terminal UX — layout, multiplexing, pane management, labels, and resize — while the server provides a clean terminal abstraction backed by direct PTY connections. Tmux operates invisibly as a persistence layer: each server-managed PTY is backed by a hidden tmux session so processes survive browser disconnects and server restarts, but no tmux concept is ever exposed to the client. The server maintains an output ring buffer per terminal for seamless reconnection replay. A file explorer is added as a new panel type alongside terminals. A mobile-first PWA experience provides monitoring and light interaction from a phone — swipe navigation, a system-wide shortcut bar, and a status dashboard designed for checking on running agents. As a stretch goal, sessions remain directly accessible from the host environment for power users who want to drop into a shell.
Requirements
Phase 1: Direct PTY & Frontend-Owned Layout
Server — TerminalManager
bashor user's default shell) via node-ptycommandparam (e.g.,claude --dangerously-skip-permissions) run inside the shellcwdparam (defaults to project path)resizeevents from the client resize the PTY directly (no tmux intermediary)~/.dancode/terminals/so the server can rediscover terminals after restartServer — API
POST /api/terminals— create terminal (params:projectSlug,label,command?,cwd?) → returns terminal object withidGET /api/terminals?project=<slug>— list terminals for a projectGET /api/terminals/:id— get terminal metadataPATCH /api/terminals/:id— update label or other metadataDELETE /api/terminals/:id— kill PTY process and remove terminal/terminal/:id— bidirectional I/O (replaces current/terminalnamespace with query params)Client — TerminalLayout (replaces PaneLayout)
/terminal/:idCtrl+Scrollfont sizing,Ctrl+Ccopy,Ctrl+Vpaste all preservedClient — Project Creation (updated)
Migration / Cleanup
tmux.jsmodule (or gut it for Phase 2 reuse)terminal.jstmuxSession,showTmuxCommandsfrom project config schema/api/tmux-statusand/api/tmux/sessionsendpointsPhase 2: Invisible Tmux Persistence
Server — Tmux as Hidden Backend
dancode-{projectSlug}-{terminalId}tmux attach)dancode-*tmux sessions and reconciles with terminal metadata files — orphaned sessions are reclaimed, metadata without sessions is cleaned uptmux capture-pane)Server — Resilience
~/.dancode/terminals/include the backing tmux session nameStretch Goal: Direct Host Access
dancode-{projectSlug}-{label})tmux lson the host and see all DanCode-managed sessionstmux attach -t dancode-myproject-clito directly access a terminaldancodeCLI helper script that lists projects and attaches to sessions (dancode attach myproject cli)Phase 3: File Explorer
Server — File System API
GET /api/files?path=<dir>— list directory contents (name, type, size, modified date), scoped to project pathGET /api/files/read?path=<file>— read file contents (with size limit, e.g., 1MB)PUT /api/files/write— write file contents (path + content in body)POST /api/files/mkdir— create directoryPOST /api/files/rename— rename/move file or directoryDELETE /api/files— delete file or directory (with confirmation)Client — File Explorer Panel
.gitignorepatterns by default (toggle to show ignored files)Integration with Terminals
cwdset to that directoryPhase 4: Mobile Experience & PWA
PWA Foundation
manifest.json) with app name, icons, theme color (Solarized Dark), display:standaloneMobile Layout — Monitoring-First Design
Terminal View — Mobile Optimized
Shortcut Bar
Ctrl+C,Ctrl+V,Ctrl+D,Tab,Up Arrow,Down Arrow,EscSwipe Navigation
Mobile-Specific Features
Responsive Breakpoints
< 480px(phone portrait): dashboard cards stack, single terminal full-screen, shortcut bar active480–768px(phone landscape / small tablet): same as above but wider terminal768–1024px(tablet): optional split view (2 terminals), shortcut bar available but optional> 1024px(desktop): full desktop layout, no shortcut bar, no swipe navigationUX / Interface (all phases)
Data / Persistence
~/.dancode/terminals/{terminalId}.json— id, projectSlug, label, tmuxSessionName, createdAt, lastActivity~/.dancode/projects/{slug}.json— updated schema drops tmux fields, addslayoutobject for frontend stateTechnical Decisions
TerminalManagerclass server-side replaces current tmux.js + terminal.js. Owns PTY lifecycle, output buffering, and reconnection. Single responsibility: manage terminal processes.Out of Scope
Open Questions
$SHELL) or always usebash? Leaning toward$SHELLwithbashfallback.🤖 Generated with Claude Code