Skip to content

PRD: Terminal Abstraction & Frontend-Owned UX #1

@DanielGGordon

Description

@DanielGGordon

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions