Web-based project terminal manager. Provides browser-based terminal access with direct PTY spawning, letting you manage multiple coding projects from any device on your network.
Built for Raspberry Pi 5, accessed via Tailscale.
- Backend: Node.js + Express + Socket.io + standalone
dancode-shellhost(UNIX-socket PTY owner) - Frontend: React + Vite + Tailwind CSS
- Terminal: xterm.js + node-pty owned by
dancode-shellhost - Editor: CodeMirror 6 with per-language packs (JS/TS + JSX/TSX, Python, JSON, Markdown, YAML, Bash via legacy-modes, HTML, CSS)
- Theme: Solarized Dark (#002b36)
- Testing: Vitest + Playwright + Midscene.js
See PROJECT_STRUCTURE.md for the full file tree.
- Node.js
^20.19.0 || >=22.12.0(required by Vite 8; usenvm installto pick up the.nvmrc)
npm install # Install all workspace dependencies
npm run check:setup # Verify Node version, build deps, socket-dir writability
npm run dev # Start shellhost + server + client concurrentlynpm run dev listens on /tmp/dancode-shellhost-dev.sock so it doesn't collide with a production shellhost on ~/.dancode/shellhost.sock. Override either with the DANCODE_SHELLHOST_SOCKET env var.
- Multi-terminal layout — Split (side-by-side) or tabbed view with dynamic terminal creation, close, and rename
- Shellhost-backed terminals —
dancode-shellhostowns every PTY and writes per-terminal scrollback (~/.dancode/terminals/<id>/scrollback.log, 1MB rotating) and metadata (meta.json) to disk. The web server is stateless w.r.t. PTYs; restarting it does not disturb running shells - Server restart recovery (Phase 3) — When the web server is killed mid-session, PTYs keep running in shellhost. On restart the server calls shellhost's
listop, rebuilds its in-memory map, and replays each terminal's on-disk scrollback to reconnecting browsers - Shellhost restart recovery (Phase 5) — When the standalone
dancode-shellhostis killed (Pi reboot,systemctl --user restart, SIGKILL), every terminal'smeta.jsonand on-disk scrollback survive. On the next project open the server calls shellhost'srespawnop, which spawns a fresh PTY at the saved cwd/command and prepends a yellow ANSI banner (--- prior session ended at <ISO> ---) to the samescrollback.log(appended, not truncated) so history survives across respawns - Claude-aware resume (Phase 7) — Shellhost periodically (5s) inspects each PTY's foreground process via
ps -t <tty>. When the foreground isclaude(ornode …/claude.js), it scans~/.claude/projects/<slug>/*.jsonlfor the newest session id and persists it tometa.claudeSessionId. On respawn after a reboot, if the command is a Claude invocation the spawn rewrites toclaude --resume <id>, so the conversation continues. A "Resume Claude" button appears on the terminal pane when the recorded session id is present and Claude isn't currently foreground; clicking it typesclaude --resume <id>and presses Enter (dismissible per-terminal). The detection loop runs under a < 1% sustained CPU budget — verified by a 60s integration test - Reconnection UX — Auto-reconnects on disconnect with "Reconnecting..." overlay, 30-second timeout to "Disconnected" with manual button; per-terminal connection state indicator dots (green/yellow/red)
- Project creation — Automatically creates 2 terminals (CLI + Claude) per project
- Drag-and-drop image upload — Drop images onto a terminal to upload and inject the file path
- Clipboard image paste — Ctrl+V a screenshot into a terminal to upload and inject the file path (for sending images to Claude)
- Clipboard support over plain HTTP — Ctrl+C copies selected text, Ctrl+V pastes (uses
execCommandfallback for non-HTTPS) - Focused pane indicator — 8px blue accent bar + dimmed unfocused panes
- Right-click context menu on sidebar projects — Rename, Delete
- Keyboard shortcuts — Ctrl+K command palette, Alt+arrows project switching, Ctrl+wheel font sizing
- PWA installable — manifest.json with DanCode branding, Solarized Dark theme color (#002b36), standalone display; service worker caches app shell for offline-capable fast loading; installable on Android home screen
- Mobile terminal — Full-screen read-first terminal on mobile (<1024px) with thin top bar, keyboard toggle, and horizontal shortcut bar (Ctrl+C/V/D, Tab, arrows, Esc) with 44px tap targets
- Mobile dashboard — Project card grid with activity indicators (active/idle), terminal labels, last activity timestamps, pull-to-refresh, long-press quick actions (open CLI/Claude terminal), and visibility-aware polling that pauses when the browser tab is hidden to save battery
- Mobile navigation — Three-level flow: dashboard → terminal list → full-screen terminal, with back button at each level
- Swipe gestures — Swipe left/right between terminals with dot pagination indicators; swipe from left edge opens project drawer
- Pinch-to-zoom — Touch gesture for terminal font size on mobile
- Tablet support — Optional side-by-side terminals (768-1024px) with shortcut bar toggle
- File explorer — Collapsible tree-view panel with lazy-loaded directories, file type icons, right-click context menu (rename, delete, copy path, new file, new folder, open terminal here, open in viewer), drag files onto terminals to insert paths, .gitignore filtering with toggle, hidden file toggle
- File editor (Phase 6) — Click a file in the explorer to open it as a pane alongside terminals. CodeMirror 6 with per-extension language packs (JavaScript/TypeScript with JSX/TSX, Python, JSON, Markdown, YAML, Bash, HTML, CSS; unknown extensions fall back to plain text). Built-in find (Ctrl+F) / replace (Ctrl+H), undo/redo (Ctrl+Z / Ctrl+Y), multi-cursor (Alt+Click, Ctrl+D), always-visible line numbers. Saves explicitly on Ctrl+S and automatically when the editor loses focus, both through
PUT /api/projects/:slug/files/*. Server-side path safety rejects any write that would escape the project root with 403 - TOTP authentication — Username/password + TOTP-based login with QR code setup; sessions persist across server restarts with 30-day TTL, automatic expiry cleanup on startup and hourly, async debounced disk writes
- Response optimization — Gzip compression on all HTTP responses; Vite-hashed static assets cached immutably for 1 year,
index.htmlserved withno-cachefor instant updates; file read API returns ETag headers (computed from file mtime + size) with304 Not Modifiedsupport for conditional requests - Server I/O optimization — Gitignore rules cached per project root with 30-second TTL
If you previously ran DanCode against the legacy tmux backend (sessions named
dancode-*), run the one-shot migration script before starting the new
shellhost-based stack:
node bin/dancode-migrate-from-tmuxIt captures each tmux session's scrollback and cwd, writes them into
~/.dancode/terminals/<id>/, appends the new terminal id to the matching
project's layout.json, and kills the source tmux sessions. The script is
idempotent — re-running it after a successful migration prints a trivial
"Migrated 0 terminals" summary and changes nothing.
npm run dev # Server (watch) + Client (HMR) concurrently
npm run build # Production build (client)
npm test # Run unit tests (Vitest)
npm run test:e2e # Run E2E tests (Playwright)Long-running deployments run dancode-shellhost (and optionally
dancode-server) as systemd --user units. Shellhost owns every PTY, so
having systemd supervise it means a Pi reboot brings every terminal back
via Phase 5 respawn semantics — no manual restart, no lost meta.
-
Install Node 20+ and build tools (one-time per machine):
sudo apt install build-essential python3 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - sudo apt install -y nodejs -
Clone the repo + install JS deps:
git clone https://github.com/DanielGGordon/DanCode.git /opt/dancode cd /opt/dancode npm install npm run check:setup # verifies Node, build deps, ~/.dancode is writable npm run build # builds the client into client/dist/
-
Install the systemd --user units:
bash systemd/install.sh
The installer copies
systemd/dancode-shellhost.service(and, by default,systemd/dancode-server.service) into~/.config/systemd/user/, rewrites theExecStart=to your actual repo path, runssystemctl --user daemon-reload, thensystemctl --user enable --now. Override the repo path withDANCODE_REPO=/srv/dancode bash systemd/install.sh, or skip the server unit withDANCODE_INSTALL_SERVER=0 bash systemd/install.shif you'd rather run the web server vianpm run startunder another supervisor. -
Enable linger so the units survive logout and a Pi reboot:
sudo loginctl enable-linger "$USER" -
Verify with the health-check:
node bin/dancode-healthcheck.mjs
The script checks: the shellhost socket is reachable, the
listop responds, a throwaway PTY round-tripsecho healthcheck-<uuid>, and the web server's/api/auth/setup/statusendpoint answers. Non-zero exit fails the install.
systemctl --user status dancode-shellhost shows live unit state.
journalctl --user -u dancode-shellhost -f follows its logs.
Type=simple,Restart=on-failure— a SIGSEGV or panicked exit auto-restarts shellhost in ~1s.Environment=DANCODE_SHELLHOST_SOCKET=%h/.dancode/shellhost.sock— production socket. The dev workflow uses/tmp/dancode-shellhost-dev.sockinstead sonpm run devdoesn't fight the systemd-managed prod socket.- Phase 5 respawn means meta.json + scrollback.log under
~/.dancode/terminals/survive every restart, so the next project open re-spawns each terminal at its saved cwd/command (plus a yellow "prior session ended at …" banner). Phase 7 rewrites Claude terminals toclaude --resume <session>on respawn.
systemd/integration-test/run.sh builds a privileged debian:12-slim
container with systemd as PID 1, performs the README install steps,
SIGKILLs shellhost (asserting auto-restart in < 5s), then drives
stop/start with a pre-spawned terminal to confirm respawn semantics
work end-to-end. CI runs this in lieu of touching a real Pi.
