From 030180803cbbf3a72b465bec1feb63f9dabfcefb Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Fri, 15 May 2026 17:19:12 +0300 Subject: [PATCH 1/3] [fix:global] windows support fix and favicon added --- frontend/index.html | 6 +- frontend/public/favicon.png | Bin 0 -> 2636 bytes frontend/public/icon-192.png | Bin 0 -> 16266 bytes frontend/public/icon-512.png | Bin 0 -> 57531 bytes frontend/public/logo.png | Bin 0 -> 4171 bytes run.ps1 | 88 ++++++++++++++++-- run.sh | 50 ++++++++-- ...ate => claude-code-webui.service.template} | 2 +- scripts/install-service.sh | 4 +- 9 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 frontend/public/favicon.png create mode 100644 frontend/public/icon-192.png create mode 100644 frontend/public/icon-512.png create mode 100644 frontend/public/logo.png rename scripts/{claude-code-dashboard.service.template => claude-code-webui.service.template} (89%) diff --git a/frontend/index.html b/frontend/index.html index 2b04383..03dfa65 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,11 @@ - Claude Code Dashboard + + + + + Claude Code Web UI diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b0b48ae9841d599f1803fe15637f9113490791 GIT binary patch literal 2636 zcmbVOc{o&iA0KP7Ma#XGZjG5}!^~l3%uGhirm-~!k)>Je~0mxS=fx}sgh z6flIpDOv)0M7wzgMTZ6vxQLB*aN8)NLO=jQYXiHiB$HVOg%3V}jN zh;L5$xH91skpzTeEv+qr&;S6(;Vc0R7Ka1O;Z|tC3WZkOI12zn#NvonHt_EkLSaq9 zOjX( z6l@%t3;!{7*r?_BQxlQWdU8Pr^H{74b^$X=`5)kgXy=;>fgaZ z#RaO!4NLF!Q&oA$2gp&H=g=dM)Wh-+^5xz5lxc$rLqs7X?vd6+LhiIBx~ZhiN4I%> zaw+TRWPmcqUGq&n3i0v8m|2+q<&5Ts{?{uVKkt5op$+DS>gr9mzL=K9Avbj_uQf52 z=jl7@X+4iNKi6*f&U)_^sA{c`U?D&4sVdLpBl7dZCY-~r#iz+f$F>;MM0pp#`gpjw zNi&mY+OYJBmU|>ARqCmS6u129BA0D0`^4t*&g7Zd+3w5> z%gHnw`>~EszA=>8uPao@HU>Cvqr$M& z+mF|@uW!DXFc6rN^Dwb%50ar?k_@P-n%ZvKg3`` z#NqMtL2U={gS|U12YgyI$x3FqHGA~XU2Tu9;}JB*;cfb9u3v=oz-zA#a_X9H8a~-l z?5x#r=h>a7%cK#t^}0dBEt9YFkG;Q?8JJ`mT^W!TzmBP5rG3^bulIv@lG0H%y;1)T zjm+6nGo?D8+y1?Nv2h0@Y##r$xqvP!NQCqe0zR}WbgBk>I zU3}=m@CkC-!^UFm?zi31ji8AI)}pUqP7H>>cJ zm$8AWQ1gKfjk0V6Z)#PQE~RkAop;U~H8Wl*`9=o^;Ez>}R6bLgu)gW^ zU@ZOGz;bJSwm_CR8}n?L?Y@`Kea<9K)b=j9cxWW6h_TW=^qSS3tcXVh87H?7(^scU z43=LV8IE=ObkMG0iI+MK22`CfI9t1HiL)gZ!d*9$WU$WWfB2H}A<3p1DPcj_15i>~ zQ&+r)7=`XVOec?Zywwa&D72U6*hF1^yn+~;e&A12WpA`ja!Y!QpP|R<3HeERl>p!6 zkYSNqRMG#-+L%0k_#alD_YGF+lS&ID^#gU1XmdU6rMweEMcJB*~ljWS8 zb#&Q*PtW1{n+){dsN6qwr_jSM3JlRrYFdSC_YKEcnYV`ft!+N-9do7N7fji|F2#E=mn1C>ee+JY>YhseZyW5wx#y8C-4!8!7o7U@VqKe7 zPk7wZvI{qa?&fN)yz!-q8Iq$DLsF+@T?we0Qj* iWChjkUj?yh2C&dw$2B+qMWE09-!W)z)Jk$-{C@yMUQv1g literal 0 HcmV?d00001 diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc82d80eef7aa2ce01d553a18cfbb1ad9f66fe4 GIT binary patch literal 16266 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 t69H&h=gh2002ovPDHLkV1iFhpVt5Y literal 0 HcmV?d00001 diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a2cfb35b128adb3a7744b348f65220a73119e61e GIT binary patch literal 4171 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 | Out-Null + } + } + } +} else { + Write-Host 'Building...' + Push-Location (Join-Path $RootDir 'frontend') + npm run build + Pop-Location -$Port = if ($env:PORT) { $env:PORT } else { '9998' } -Write-Host "Starting Claude Code Dashboard → http://localhost:$Port" -node (Join-Path $RootDir 'backend\dist\server.js') + Push-Location (Join-Path $RootDir 'backend') + npm run build + Pop-Location + + $Port = if ($env:PORT) { $env:PORT } else { '9998' } + Write-Host "Starting Claude Code Web UI → http://localhost:$Port" + node (Join-Path $RootDir 'backend\dist\server.js') +} diff --git a/run.sh b/run.sh index 02a392c..de0e496 100755 --- a/run.sh +++ b/run.sh @@ -1,6 +1,30 @@ #!/usr/bin/env bash set -euo pipefail +DEV_MODE=0 +for arg in "$@"; do + case "$arg" in + --dev) DEV_MODE=1 ;; + -h|--help) + cat <&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; + esac +done + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Bootstrap .env on first run @@ -19,10 +43,24 @@ echo "Installing dependencies..." cd "$ROOT_DIR/frontend" && npm install cd "$ROOT_DIR/backend" && npm install -echo "Building..." -cd "$ROOT_DIR/frontend" && npm run build -cd "$ROOT_DIR/backend" && npm run build +if [[ $DEV_MODE -eq 1 ]]; then + echo "Starting Claude Code Web UI in DEV mode (hot reload)" + echo " Backend → http://localhost:${PORT:-9998} (tsx watch)" + echo " Frontend → http://localhost:9999 (Vite HMR)" + + # Forward SIGINT/SIGTERM/EXIT to the whole process group so both + # children die when the user hits Ctrl+C. + trap 'trap - INT TERM EXIT; kill 0' INT TERM EXIT -PORT="${PORT:-9998}" -echo "Starting Claude Code Dashboard → http://localhost:${PORT}" -exec node "$ROOT_DIR/backend/dist/server.js" + (cd "$ROOT_DIR/backend" && npm run dev) & + (cd "$ROOT_DIR/frontend" && npm run dev) & + wait +else + echo "Building..." + cd "$ROOT_DIR/frontend" && npm run build + cd "$ROOT_DIR/backend" && npm run build + + PORT="${PORT:-9998}" + echo "Starting Claude Code Web UI → http://localhost:${PORT}" + exec node "$ROOT_DIR/backend/dist/server.js" +fi diff --git a/scripts/claude-code-dashboard.service.template b/scripts/claude-code-webui.service.template similarity index 89% rename from scripts/claude-code-dashboard.service.template rename to scripts/claude-code-webui.service.template index 41698e7..5993f1e 100644 --- a/scripts/claude-code-dashboard.service.template +++ b/scripts/claude-code-webui.service.template @@ -1,5 +1,5 @@ [Unit] -Description=Claude Code Dashboard +Description=Claude Code Web UI After=network.target [Service] diff --git a/scripts/install-service.sh b/scripts/install-service.sh index b7f3598..eeba6d1 100755 --- a/scripts/install-service.sh +++ b/scripts/install-service.sh @@ -4,7 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSTALL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" SYSTEMD_USER_DIR="$HOME/.config/systemd/user" -SERVICE_NAME="claude-code-dashboard" +SERVICE_NAME="claude-code-webui" ENV_FILE="$SYSTEMD_USER_DIR/$SERVICE_NAME.env" UNIT_FILE="$SYSTEMD_USER_DIR/$SERVICE_NAME.service" TEMPLATE="$SCRIPT_DIR/$SERVICE_NAME.service.template" @@ -87,6 +87,6 @@ loginctl enable-linger "$USER" || echo "Warning: could not enable linger — you PORT_ACTUAL=$PORT echo "" -echo "Done! Claude Code Dashboard is running at: http://localhost:${PORT_ACTUAL}" +echo "Done! Claude Code Web UI is running at: http://localhost:${PORT_ACTUAL}" echo "Check status : systemctl --user status $SERVICE_NAME" echo "View logs : journalctl --user -u $SERVICE_NAME -f" From 75c7e7de0f3f60b86ee6280bc3a92de11629b049 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Fri, 15 May 2026 17:40:40 +0300 Subject: [PATCH 2/3] [fix:global] windows support fix and favicon added --- CLAUDE.md | 8 +- README.md | 4 +- backend/src/db/schema.ts | 4 +- backend/src/server.ts | 2 +- docker-compose.yml | 2 +- docs/plans/2026-05-12-terminal-mode.md | 1029 ----------------- docs/specs/2026-05-12-terminal-mode-design.md | 160 --- frontend/src/App.tsx | 4 +- frontend/src/components/SessionList.tsx | 2 +- .../hooks/{useDashboard.ts => useHomeData.ts} | 4 +- .../views/{DashboardView.tsx => HomeView.tsx} | 6 +- frontend/src/views/SessionRoute.tsx | 8 +- scripts/claude-code-webui.service.template | 2 +- scripts/test-service-install.sh | 10 +- scripts/uninstall-service.sh | 2 +- 15 files changed, 29 insertions(+), 1218 deletions(-) delete mode 100644 docs/plans/2026-05-12-terminal-mode.md delete mode 100644 docs/specs/2026-05-12-terminal-mode-design.md rename frontend/src/hooks/{useDashboard.ts => useHomeData.ts} (95%) rename frontend/src/views/{DashboardView.tsx => HomeView.tsx} (98%) diff --git a/CLAUDE.md b/CLAUDE.md index e4ee894..1d64449 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,7 @@ Two modes are supported per session, stored in the `sessions.mode` column: ### Frontend data flow -- `useDashboard.ts` fetches account + usage + sessions in parallel with a 60s auto-refresh +- `useHomeData.ts` fetches account + usage + sessions in parallel with a 60s auto-refresh - `SessionContext.tsx` holds the active session state (including `mode`); `useWebSocket.ts` manages the chat WS connection; `useTerminalSession.ts` manages the terminal WS connection - `TerminalSession.tsx` and `TerminalDrawer.tsx` are **never unmounted** — both are CSS-toggled (`display: none`) to preserve xterm scroll buffer across navigation @@ -103,13 +103,13 @@ Two modes are supported per session, stored in the `sessions.mode` column: ## Docker ```bash -docker build -t claude-code-dashboard . +docker build -t claude-code-webui . docker-compose up -# Dashboard at http://localhost:8080 +# Web UI at http://localhost:8080 ``` `docker-compose.yml` mounts `~/.claude` (read-write — required for session deletion) and `~/projects` (read-write) from the host. ## Systemd service (Linux) -`scripts/install-service.sh` generates a systemd user unit from `scripts/claude-code-dashboard.service.template`, writes an env file to `~/.config/systemd/user/`, builds the project, and enables the service. Run with `--skip-build` to reuse existing `dist/` directories. +`scripts/install-service.sh` generates a systemd user unit from `scripts/claude-code-webui.service.template`, writes an env file to `~/.config/systemd/user/`, builds the project, and enables the service. Run with `--skip-build` to reuse existing `dist/` directories. diff --git a/README.md b/README.md index 4b53596..b7eb275 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ make service-install ARGS=--skip-build ```bash make service-uninstall -systemctl --user status claude-code-dashboard -journalctl --user -u claude-code-dashboard -f +systemctl --user status claude-code-webui +journalctl --user -u claude-code-webui -f ``` ## Commands reference diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 42e67fc..4c2e76f 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -2,8 +2,8 @@ import Database from 'better-sqlite3' import path from 'path' import fs from 'fs' -const DB_DIR = process.env.DATA_DIR ?? path.join(process.env.HOME ?? '/root', '.claude', 'dashboard') -const DB_PATH = path.join(DB_DIR, 'dashboard.db') +const DB_DIR = process.env.DATA_DIR ?? path.join(process.env.HOME ?? '/root', '.claude', 'webui') +const DB_PATH = path.join(DB_DIR, 'webui.db') function initDb(): Database.Database { fs.mkdirSync(DB_DIR, { recursive: true }) diff --git a/backend/src/server.ts b/backend/src/server.ts index b26f16c..c7741c1 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -15,7 +15,7 @@ import { statuslineRoutes } from './routes/statusline' import { setupStatusline } from './lib/setup-statusline' // TODO: add bearer token auth — add @fastify/bearer-auth plugin here -// and set token via DASHBOARD_TOKEN env var +// and set token via WEBUI_TOKEN env var // fastify.addHook('onRequest', async (request, reply) => { ... }) const fastify = Fastify({ logger: true }) diff --git a/docker-compose.yml b/docker-compose.yml index 4bebfed..15c8616 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - dashboard: + webui: image: ghcr.io/adeotek/claude-code-webui:latest build: context: . diff --git a/docs/plans/2026-05-12-terminal-mode.md b/docs/plans/2026-05-12-terminal-mode.md deleted file mode 100644 index 296b8dc..0000000 --- a/docs/plans/2026-05-12-terminal-mode.md +++ /dev/null @@ -1,1029 +0,0 @@ -# Terminal Mode Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a global `session_mode` setting (`chat` | `terminal`) that switches the session content area between the existing chat layout and a full interactive PTY terminal backed by a new `/ws/terminal/:id` WebSocket endpoint. - -**Architecture:** A separate `ws/terminal.ts` module handles interactive PTY sessions (no JSON parsing, raw I/O passthrough) alongside the untouched `ws/session.ts`. Sessions carry their mode in the DB; `SessionContext` holds `mode` for the active session, and `DashboardView` renders either `TerminalSession` (new) or the existing chat layout based on it. - -**Tech Stack:** node-pty, better-sqlite3, Fastify WebSocket, xterm.js + FitAddon, React + TypeScript, Tailwind CSS - ---- - -## File Map - -**Create:** -- `backend/src/ws/terminal.ts` — TerminalManager singleton + `/ws/terminal/:id` WS handler -- `frontend/src/hooks/useTerminalSession.ts` — WS hook for `/ws/terminal/:id` -- `frontend/src/components/TerminalSession.tsx` — full-height xterm.js terminal component - -**Modify:** -- `backend/src/db/schema.ts` — add `mode` column migration guard to sessions table -- `backend/src/routes/settings.ts` — add `session_mode` to DEFAULTS/ALLOWED -- `backend/src/routes/sessions.ts` — POST stores mode; stop+delete kill terminal PTY -- `backend/src/server.ts` — register `terminalWsRoutes` -- `frontend/src/context/SessionContext.tsx` — add `mode` to SessionState + actions -- `frontend/src/hooks/useDashboard.ts` — add `mode` to Session type; fetch settings; return `defaultSessionMode` -- `frontend/src/hooks/useWebSocket.ts` — skip connecting when `state.mode === 'terminal'` -- `frontend/src/views/DashboardView.tsx` — dispatch mode on create/resume; swap content area -- `frontend/src/views/SettingsView.tsx` — add Session Mode toggle - ---- - -## Task 1: DB migration + settings key - -**Files:** -- Modify: `backend/src/db/schema.ts` -- Modify: `backend/src/routes/settings.ts` - -- [ ] **Step 1: Add `mode` column migration guard to `schema.ts`** - - In `backend/src/db/schema.ts`, add this block after the existing `working_time_ms` migration guard (after line 69): - - ```typescript - if (!sessionCols.find((c) => c.name === 'mode')) { - db.prepare("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'chat'").run() - } - ``` - -- [ ] **Step 2: Add `session_mode` to settings DEFAULTS** - - In `backend/src/routes/settings.ts`, replace lines 4-6: - - ```typescript - const DEFAULTS: Record = { - bypass_permissions: 'true', - session_mode: 'chat', - } - ``` - -- [ ] **Step 3: Lint backend** - - ```bash - cd backend && npm run lint - ``` - Expected: no errors. - -- [ ] **Step 4: Commit** - - ```bash - git add backend/src/db/schema.ts backend/src/routes/settings.ts - git commit -m "feat: add session mode DB column and settings key" - ``` - ---- - -## Task 2: Terminal WebSocket handler - -**Files:** -- Create: `backend/src/ws/terminal.ts` - -- [ ] **Step 1: Create `backend/src/ws/terminal.ts`** - - ```typescript - import * as pty from 'node-pty' - import * as os from 'os' - import * as path from 'path' - import type { WebSocket } from 'ws' - import type { FastifyInstance } from 'fastify' - import { db } from '../db/schema' - - const IDLE_TIMEOUT_MS = 30 * 60 * 1000 - - function getBypassPermissions(): boolean { - const row = db - .prepare('SELECT value FROM settings WHERE key = ?') - .get('bypass_permissions') as { value: string } | undefined - return row ? row.value === 'true' : true - } - - class ActiveTerminalSession { - private ptyProc: pty.IPty | null = null - private sockets = new Set() - private idleTimer: NodeJS.Timeout | null = null - - constructor( - readonly id: string, - private readonly workdir: string, - ) { - this.spawnPty() - this.resetIdle() - } - - private spawnPty() { - const claudeBin = process.env.CLAUDE_BIN ?? 'claude' - const bypassPermissions = getBypassPermissions() - const args: string[] = [] - if (bypassPermissions) args.push('--dangerously-skip-permissions') - - const resolvedCwd = this.workdir.startsWith('~') - ? path.join(os.homedir(), this.workdir.slice(1)) - : this.workdir - - try { - this.ptyProc = pty.spawn(claudeBin, args, { - name: 'xterm-256color', - cols: 220, - rows: 50, - cwd: resolvedCwd, - env: { ...process.env } as Record, - }) - } catch (err) { - this.broadcast({ type: 'output', data: `\r\nError starting terminal: ${(err as Error).message}\r\n` }) - this.broadcast({ type: 'status', state: 'disconnected' }) - return - } - - this.ptyProc.onData((data) => { - this.resetIdle() - this.broadcast({ type: 'output', data }) - }) - - this.ptyProc.onExit(() => { - this.ptyProc = null - db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) - this.broadcast({ type: 'status', state: 'disconnected' }) - if (this.idleTimer) clearTimeout(this.idleTimer) - }) - } - - attach(ws: WebSocket) { - this.sockets.add(ws) - ws.send(JSON.stringify({ type: 'status', state: 'connected' })) - ws.on('close', () => this.sockets.delete(ws)) - } - - writeInput(data: string) { - this.resetIdle() - this.ptyProc?.write(data) - } - - resize(cols: number, rows: number) { - this.ptyProc?.resize(cols, rows) - } - - kill() { - db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) - if (this.ptyProc) { - this.ptyProc.kill() - this.ptyProc = null - } - if (this.idleTimer) clearTimeout(this.idleTimer) - this.broadcast({ type: 'status', state: 'disconnected' }) - } - - private broadcast(msg: { type: string; data?: string; state?: string }) { - const payload = JSON.stringify(msg) - for (const ws of this.sockets) { - if (ws.readyState === ws.OPEN) ws.send(payload) - } - } - - private resetIdle() { - if (this.idleTimer) clearTimeout(this.idleTimer) - this.idleTimer = setTimeout(() => this.kill(), IDLE_TIMEOUT_MS) - } - } - - class TerminalManager { - private sessions = new Map() - - getOrCreate(id: string, workdir: string): ActiveTerminalSession { - if (!this.sessions.has(id)) { - this.sessions.set(id, new ActiveTerminalSession(id, workdir)) - } - return this.sessions.get(id)! - } - - kill(id: string) { - this.sessions.get(id)?.kill() - this.sessions.delete(id) - } - } - - export const terminalManager = new TerminalManager() - - export async function terminalWsRoutes(fastify: FastifyInstance) { - fastify.get<{ Params: { id: string } }>( - '/ws/terminal/:id', - { websocket: true }, - (socket, req) => { - const { id } = req.params - - const row = db.prepare('SELECT workdir, ended_at FROM sessions WHERE id = ?').get(id) as - | { workdir: string; ended_at: number | null } - | undefined - - if (!row) { - socket.send(JSON.stringify({ type: 'status', state: 'error', data: 'session not found' })) - socket.close() - return - } - - if (row.ended_at !== null) { - db.prepare('UPDATE sessions SET ended_at = NULL WHERE id = ?').run(id) - } - - const session = terminalManager.getOrCreate(id, row.workdir) - session.attach(socket) - - socket.on('message', (raw: Buffer | string) => { - try { - const msg = JSON.parse(raw.toString()) as { - type: string - data?: string - cols?: number - rows?: number - } - if (msg.type === 'input' && msg.data) { - session.writeInput(msg.data) - } else if (msg.type === 'resize' && msg.cols && msg.rows) { - session.resize(msg.cols, msg.rows) - } - } catch { - // ignore malformed frames - } - }) - }, - ) - } - ``` - -- [ ] **Step 2: Lint backend** - - ```bash - cd backend && npm run lint - ``` - Expected: no errors. - -- [ ] **Step 3: Commit** - - ```bash - git add backend/src/ws/terminal.ts - git commit -m "feat: add terminal WS handler and TerminalManager" - ``` - ---- - -## Task 3: Sessions routes + server registration - -**Files:** -- Modify: `backend/src/routes/sessions.ts` -- Modify: `backend/src/server.ts` - -- [ ] **Step 1: Import `terminalManager` in `sessions.ts`** - - Replace the import block at the top of `backend/src/routes/sessions.ts` (lines 1-8): - - ```typescript - import type { FastifyInstance } from 'fastify' - import { v4 as uuidv4 } from 'uuid' - import fs from 'fs' - import os from 'os' - import path from 'path' - import { db } from '../db/schema' - import { sessionManager } from '../ws/session' - import { terminalManager } from '../ws/terminal' - ``` - -- [ ] **Step 2: Store mode in POST `/api/sessions`** - - Replace the POST handler body (lines 45-57): - - ```typescript - fastify.post<{ Body: { workdir: string; name?: string } }>('/api/sessions', async (req, reply) => { - const { workdir, name } = req.body - if (!workdir || typeof workdir !== 'string') { - return reply.status(400).send({ error: 'workdir is required' }) - } - - const modeRow = db - .prepare("SELECT value FROM settings WHERE key = 'session_mode'") - .get() as { value: string } | undefined - const mode = modeRow?.value === 'terminal' ? 'terminal' : 'chat' - - const id = uuidv4() - db.prepare( - 'INSERT INTO sessions (id, workdir, name, mode, started_at) VALUES (?, ?, ?, ?, ?)', - ).run(id, workdir, name?.trim() || null, mode, Date.now()) - - return reply.status(201).send({ sessionId: id }) - }) - ``` - -- [ ] **Step 3: Kill terminal PTY on stop** - - Replace the stop handler body (lines 59-63): - - ```typescript - fastify.post<{ Params: { id: string } }>('/api/sessions/:id/stop', async (req, reply) => { - const { id } = req.params - sessionManager.kill(id) - terminalManager.kill(id) - db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), id) - return reply.send({ ok: true }) - }) - ``` - -- [ ] **Step 4: Kill terminal PTY on delete** - - Replace the delete handler body (lines 94-105): - - ```typescript - fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (req, reply) => { - const { id } = req.params - const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id) - if (!session) { - return reply.status(404).send({ error: 'Session not found' }) - } - sessionManager.kill(id) - terminalManager.kill(id) - db.prepare('DELETE FROM messages WHERE session_id = ?').run(id) - db.prepare('DELETE FROM sessions WHERE id = ?').run(id) - return reply.send({ ok: true }) - }) - ``` - -- [ ] **Step 5: Register `terminalWsRoutes` in `server.ts`** - - In `backend/src/server.ts`, add the import at line 10 (after the existing `sessionWsRoutes` import): - - ```typescript - import { terminalWsRoutes } from './ws/terminal' - ``` - - And register the route after the existing `sessionWsRoutes` registration (after line 36): - - ```typescript - await fastify.register(terminalWsRoutes) - ``` - -- [ ] **Step 6: Lint backend** - - ```bash - cd backend && npm run lint - ``` - Expected: no errors. - -- [ ] **Step 7: Commit** - - ```bash - git add backend/src/routes/sessions.ts backend/src/server.ts - git commit -m "feat: wire terminal WS route and mode into sessions API" - ``` - ---- - -## Task 4: SessionContext mode field - -**Files:** -- Modify: `frontend/src/context/SessionContext.tsx` - -- [ ] **Step 1: Add `mode` to `SessionState`** - - In `frontend/src/context/SessionContext.tsx`, replace the `SessionState` interface (lines 15-26): - - ```typescript - export interface SessionState { - sessionId: string | null - workdir: string | null - name: string | null - model: string | null - mode: 'chat' | 'terminal' - wsState: 'disconnected' | 'connecting' | 'running' | 'idle' | 'error' - messages: Message[] - workingTimeMs: number - runningStartedAt: number | null - totalTokens: number - pendingPermissions: PermissionRequest[] | null - } - ``` - -- [ ] **Step 2: Add `mode` to `SESSION_CREATED` and `RESUME_SESSION` action types** - - Replace the `Action` type definition (lines 28-41): - - ```typescript - type Action = - | { type: 'SESSION_CREATED'; sessionId: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } - | { type: 'SESSION_CLEARED' } - | { type: 'RESUME_SESSION'; id: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } - | { type: 'WS_STATE'; state: SessionState['wsState']; timestamp: number } - | { type: 'MESSAGE_ADDED'; message: Message } - | { type: 'HISTORY_LOADED'; messages: Message[] } - | { type: 'MODEL_SET'; model: string } - | { type: 'SESSION_RENAMED'; name: string | null } - | { type: 'TOKENS_ADDED'; inputTokens: number; outputTokens: number } - | { type: 'STATS_RESTORED'; totalTokens: number; workingTimeMs: number } - | { type: 'PERMISSION_REQUEST'; permissions: PermissionRequest[] } - | { type: 'PERMISSION_CLEARED' } - ``` - -- [ ] **Step 3: Add `mode` to the initial state** - - Replace the `initial` constant (lines 43-53): - - ```typescript - const initial: SessionState = { - sessionId: null, - workdir: null, - name: null, - model: null, - mode: 'chat', - wsState: 'disconnected', - messages: [], - workingTimeMs: 0, - runningStartedAt: null, - totalTokens: 0, - pendingPermissions: null, - } - ``` - -- [ ] **Step 4: Update reducer cases for `SESSION_CREATED` and `RESUME_SESSION`** - - Replace the two reducer cases (lines 57-62): - - ```typescript - case 'SESSION_CREATED': - return { ...state, sessionId: action.sessionId, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, pendingPermissions: null } - case 'SESSION_CLEARED': - return { ...initial } - case 'RESUME_SESSION': - return { ...state, sessionId: action.id, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, totalTokens: 0, pendingPermissions: null } - ``` - -- [ ] **Step 5: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: errors about callers of `SESSION_CREATED`/`RESUME_SESSION` missing `mode` — these will be fixed in Task 9. The important thing is no errors in `SessionContext.tsx` itself. - - > Note: TypeScript errors in `DashboardView.tsx` about missing `mode` property are expected at this stage. They confirm the type change propagated correctly and will be resolved in Task 9. - -- [ ] **Step 6: Commit** - - ```bash - git add frontend/src/context/SessionContext.tsx - git commit -m "feat: add mode field to SessionContext state and actions" - ``` - ---- - -## Task 5: useDashboard — Session type + settings fetch - -**Files:** -- Modify: `frontend/src/hooks/useDashboard.ts` - -- [ ] **Step 1: Add `mode` to the `Session` interface and add settings to the fetch** - - Replace the entire `useDashboard.ts` file: - - ```typescript - import { useState, useEffect, useCallback } from 'react' - import type { AccountInfo } from './useAccount' - import type { UsageData } from './useUsage' - - export interface Session { - id: string - workdir: string - name: string | null - model: string | null - started_at: number - ended_at: number | null - is_active: boolean - message_count: number - mode: 'chat' | 'terminal' - } - - export interface DashboardData { - account: AccountInfo | null - usage: UsageData | null - sessions: Session[] - activeSessions: number - defaultSessionMode: 'chat' | 'terminal' - loading: boolean - error: string | null - } - - export function useDashboard(month?: string): DashboardData & { refresh: () => void } { - const [account, setAccount] = useState(null) - const [usage, setUsage] = useState(null) - const [sessions, setSessions] = useState([]) - const [defaultSessionMode, setDefaultSessionMode] = useState<'chat' | 'terminal'>('chat') - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - const target = month ?? new Date().toISOString().slice(0, 7) - - const fetchAll = useCallback(() => { - setLoading(true) - setError(null) - - Promise.all([ - fetch('/api/account').then((r) => r.json() as Promise), - fetch(`/api/usage?month=${target}`).then((r) => r.json() as Promise), - fetch('/api/sessions').then((r) => r.json() as Promise), - fetch('/api/settings').then((r) => r.json() as Promise>), - ]) - .then(([acc, usg, sess, settings]) => { - setAccount(acc) - setUsage(usg) - setSessions(Array.isArray(sess) ? sess : []) - setDefaultSessionMode(settings.session_mode === 'terminal' ? 'terminal' : 'chat') - setLoading(false) - }) - .catch((e: Error) => { - setError(e.message) - setLoading(false) - }) - }, [target]) - - useEffect(() => { - fetchAll() - const timer = setInterval(fetchAll, 60_000) - return () => clearInterval(timer) - }, [fetchAll]) - - const activeSessions = sessions.filter((s) => s.is_active).length - - return { account, usage, sessions, activeSessions, defaultSessionMode, loading, error, refresh: fetchAll } - } - ``` - -- [ ] **Step 2: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: same `DashboardView.tsx` errors from Task 4 (still missing `mode` in dispatches). No new errors. - -- [ ] **Step 3: Commit** - - ```bash - git add frontend/src/hooks/useDashboard.ts - git commit -m "feat: add mode to Session type and fetch settings in useDashboard" - ``` - ---- - -## Task 6: useWebSocket mode guard - -**Files:** -- Modify: `frontend/src/hooks/useWebSocket.ts` - -- [ ] **Step 1: Add `state.mode` guard to prevent chat WS connecting in terminal mode** - - In `frontend/src/hooks/useWebSocket.ts`, replace lines 12-14 (the start of the useEffect): - - ```typescript - useEffect(() => { - if (!state.sessionId || state.mode === 'terminal') return - let closed = false - ``` - - Also update the dependency array at line 95 to include `state.mode`: - - ```typescript - }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps - ``` - -- [ ] **Step 2: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: same existing `DashboardView.tsx` errors only. No new errors. - -- [ ] **Step 3: Commit** - - ```bash - git add frontend/src/hooks/useWebSocket.ts - git commit -m "feat: skip chat WS connection when session mode is terminal" - ``` - ---- - -## Task 7: useTerminalSession hook - -**Files:** -- Create: `frontend/src/hooks/useTerminalSession.ts` - -- [ ] **Step 1: Create `frontend/src/hooks/useTerminalSession.ts`** - - ```typescript - import { useEffect, useRef, useCallback } from 'react' - import { useSession } from '../context/SessionContext' - - const MAX_RECONNECT_ATTEMPTS = 5 - const BASE_DELAY_MS = 500 - - export function useTerminalSession(onOutput: (data: string) => void) { - const { state, dispatch } = useSession() - const wsRef = useRef(null) - const attemptsRef = useRef(0) - - useEffect(() => { - if (!state.sessionId || state.mode !== 'terminal') return - let closed = false - - function connect() { - const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' - const host = window.location.host - const ws = new WebSocket(`${protocol}://${host}/ws/terminal/${state.sessionId}`) - wsRef.current = ws - dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'connecting' }) - - ws.onopen = () => { - attemptsRef.current = 0 - } - - ws.onmessage = (event) => { - try { - const msg = JSON.parse(event.data as string) as { - type: string - data?: string - state?: string - } - if (msg.type === 'output' && msg.data) { - onOutput(msg.data) - } else if (msg.type === 'status') { - if (msg.state === 'connected') { - dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'idle' }) - } else if (msg.state === 'disconnected' || msg.state === 'error') { - dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) - } - } - } catch { - // ignore malformed frames - } - } - - ws.onerror = () => dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'error' }) - - ws.onclose = () => { - if (closed) return - if (attemptsRef.current < MAX_RECONNECT_ATTEMPTS) { - const delay = BASE_DELAY_MS * 2 ** attemptsRef.current - attemptsRef.current++ - setTimeout(connect, delay) - } else { - dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) - setTimeout(() => { - if (!closed) { attemptsRef.current = 0; connect() } - }, 30_000) - } - } - } - - connect() - return () => { - closed = true - attemptsRef.current = 0 - wsRef.current?.close() - wsRef.current = null - } - }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps - - const send = useCallback((payload: object) => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify(payload)) - } - }, []) - - return { send } - } - ``` - -- [ ] **Step 2: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: same existing `DashboardView.tsx` errors only. - -- [ ] **Step 3: Commit** - - ```bash - git add frontend/src/hooks/useTerminalSession.ts - git commit -m "feat: add useTerminalSession hook for /ws/terminal/:id" - ``` - ---- - -## Task 8: TerminalSession component - -**Files:** -- Create: `frontend/src/components/TerminalSession.tsx` - -- [ ] **Step 1: Create `frontend/src/components/TerminalSession.tsx`** - - ```typescript - import { useEffect, useRef, useCallback } from 'react' - import { Terminal } from '@xterm/xterm' - import { FitAddon } from '@xterm/addon-fit' - import '@xterm/xterm/css/xterm.css' - import { useTerminalSession } from '../hooks/useTerminalSession' - - export default function TerminalSession() { - const containerRef = useRef(null) - const termRef = useRef(null) - const fitRef = useRef(null) - - const onOutput = useCallback((data: string) => { - termRef.current?.write(data) - }, []) - - const { send } = useTerminalSession(onOutput) - - useEffect(() => { - const term = new Terminal({ - theme: { - background: '#050505', - foreground: '#e2e2e2', - cursor: '#d97706', - selectionBackground: '#d9770640', - }, - fontFamily: 'JetBrains Mono, Fira Code, monospace', - fontSize: 12, - lineHeight: 1.4, - cursorBlink: true, - }) - const fit = new FitAddon() - term.loadAddon(fit) - termRef.current = term - fitRef.current = fit - - if (containerRef.current) { - term.open(containerRef.current) - requestAnimationFrame(() => fit.fit()) - term.onResize(({ cols, rows }) => send({ type: 'resize', cols, rows })) - term.onData((data) => send({ type: 'input', data })) - } - - return () => term.dispose() - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - // Re-fit terminal when container dimensions change (window resize, panel resize) - useEffect(() => { - if (!containerRef.current) return - const observer = new ResizeObserver(() => { - requestAnimationFrame(() => fitRef.current?.fit()) - }) - observer.observe(containerRef.current) - return () => observer.disconnect() - }, []) - - return ( -
-
-
- ) - } - ``` - -- [ ] **Step 2: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: same existing `DashboardView.tsx` errors only. No errors in `TerminalSession.tsx`. - -- [ ] **Step 3: Commit** - - ```bash - git add frontend/src/components/TerminalSession.tsx - git commit -m "feat: add full-height TerminalSession component" - ``` - ---- - -## Task 9: DashboardView wiring - -**Files:** -- Modify: `frontend/src/views/DashboardView.tsx` - -- [ ] **Step 1: Import `TerminalSession` and destructure `defaultSessionMode`** - - Add the import at the top of `DashboardView.tsx` (after the existing imports, before line 17): - - ```typescript - import TerminalSession from '../components/TerminalSession' - ``` - - Update the `useDashboard` destructure (line 23) to include `defaultSessionMode`: - - ```typescript - const { account, usage, sessions, activeSessions, loading, refresh, defaultSessionMode } = useDashboard() - ``` - -- [ ] **Step 2: Add `mode` to `handleSessionStart` dispatch** - - Replace `handleSessionStart` (lines 54-59): - - ```typescript - function handleSessionStart(sessionId: string, workdir: string, name: string | null) { - dispatch({ type: 'SESSION_CREATED', sessionId, workdir, ...(name ? { name } : {}), mode: defaultSessionMode }) - if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) - setShowModal(false) - refresh() - } - ``` - -- [ ] **Step 3: Add `mode` to `handleResume` dispatch** - - Replace `handleResume` (lines 97-100): - - ```typescript - function handleResume(session: Session) { - dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, ...(session.name ? { name: session.name } : {}), mode: session.mode ?? 'chat' }) - if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) - } - ``` - -- [ ] **Step 4: Guard the PTY resize registration for chat mode only** - - Replace the resize `useEffect` (lines 47-51): - - ```typescript - useEffect(() => { - if (state.mode === 'terminal') return - terminalRef.current?.sendResize((cols, rows) => { - send({ type: 'resize', cols, rows }) - }) - }, [send, state.mode]) - ``` - -- [ ] **Step 5: Swap session content area based on `state.mode`** - - Replace the inner session layout (lines 162-185) — the block between `
` and its closing tag: - - ```tsx - {state.sessionId ? ( -
- {state.pendingPermissions && ( - - )} - - {state.mode === 'terminal' ? ( - - ) : ( - <> - - - - - )} -
- ) : showModal ? ( - - ) : ( - setShowModal(true)} - /> - )} - ``` - -- [ ] **Step 6: Lint frontend — should now be clean** - - ```bash - cd frontend && npm run lint - ``` - Expected: **no errors**. This is the step where all previously expected TypeScript errors resolve. - -- [ ] **Step 7: Commit** - - ```bash - git add frontend/src/views/DashboardView.tsx - git commit -m "feat: wire terminal mode into DashboardView session layout" - ``` - ---- - -## Task 10: SettingsView session mode toggle - -**Files:** -- Modify: `frontend/src/views/SettingsView.tsx` - -- [ ] **Step 1: Add `sessionMode` state and load it from settings** - - In `frontend/src/views/SettingsView.tsx`, add the new state variable after the `bypassPermissions` state (line 9): - - ```typescript - const [sessionMode, setSessionMode] = useState<'chat' | 'terminal'>('chat') - ``` - - In the `useEffect` fetch callback, add parsing for `session_mode` (after the `setBypassPermissions` line): - - ```typescript - .then((data) => { - setBypassPermissions(data.bypass_permissions !== 'false') - setSessionMode(data.session_mode === 'terminal' ? 'terminal' : 'chat') - setSettingsLoaded(true) - }) - ``` - -- [ ] **Step 2: Include `session_mode` in `handleSave`** - - Replace the `await fetch('/api/settings', ...)` call in `handleSave`: - - ```typescript - await fetch('/api/settings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - bypass_permissions: String(bypassPermissions), - session_mode: sessionMode, - }), - }).catch(() => {}) - ``` - -- [ ] **Step 3: Add the Session Mode toggle UI** - - Add this block in the JSX, after the "Tool Permissions" section and before the Save button: - - ```tsx - {/* Session mode toggle */} -
- -

- Chat mode shows a structured conversation with a collapsible terminal drawer. Terminal mode replaces the chat view with a full interactive terminal — interact with Claude Code exactly as you would in your local terminal. -

- -
- ``` - -- [ ] **Step 4: Lint frontend** - - ```bash - cd frontend && npm run lint - ``` - Expected: no errors. - -- [ ] **Step 5: Full lint check (both packages)** - - ```bash - make lint - ``` - Expected: no errors in either package. - -- [ ] **Step 6: Commit** - - ```bash - git add frontend/src/views/SettingsView.tsx - git commit -m "feat: add session mode toggle to SettingsView" - ``` - ---- - -## Verification - -Run `make dev` (backend on :9998, frontend on :9999) and verify: - -1. **Settings toggle**: Navigate to `/settings` — confirm "Session Mode" toggle appears alongside Tool Permissions, can be switched between chat/terminal, and saves correctly. - -2. **Chat mode (default)**: With mode=chat, create a new session — confirm existing chat layout (MessageList + TerminalDrawer + ChatInput) renders unchanged. - -3. **Terminal mode**: Switch to terminal mode, create a new session — confirm the session view shows only a full-height xterm.js terminal (no chat input, no message list, no terminal drawer header). - -4. **I/O passthrough**: In terminal mode, type a prompt into the terminal — confirm keystrokes appear in the terminal and Claude Code responds with ANSI-formatted output. - -5. **Resize**: Resize the browser window — confirm the terminal re-fits and PTY cols/rows update (visible when claude CLI redraws its prompt). - -6. **Stop session**: Click "Stop session" in the header while a terminal session is active — confirm PTY is killed, session shows as ended in the sessions list. - -7. **Session list + resume**: Both chat and terminal sessions appear in the sessions list. Resuming a terminal session reconnects to `/ws/terminal/:id`; resuming a chat session reconnects to `/ws/session/:id`. - -8. **Chat mode unaffected**: Switch back to chat mode, confirm the existing chat experience is identical to before. diff --git a/docs/specs/2026-05-12-terminal-mode-design.md b/docs/specs/2026-05-12-terminal-mode-design.md deleted file mode 100644 index 7c9824a..0000000 --- a/docs/specs/2026-05-12-terminal-mode-design.md +++ /dev/null @@ -1,160 +0,0 @@ -# Terminal Mode Design - -**Date:** 2026-05-12 -**Branch:** feat/terminal -**Status:** Approved - -## Context - -The session view currently shows only a chat interface (MessageList + TerminalDrawer drawer + ChatInput). The TerminalDrawer renders PTY output from the claude CLI spawned in non-interactive, structured-JSON mode (`--print --output-format stream-json`). There is no way to interact with the Claude Code CLI as a real terminal. - -This feature adds a global `session_mode` setting (`chat` | `terminal`) that switches the session view between the existing chat layout and a full terminal experience backed by a proper interactive PTY. - -## Approach - -Separate WebSocket endpoint (`/ws/terminal/:id`) alongside the existing `/ws/session/:id`. The two paths are fully independent — no changes to the existing chat session handler. Sessions carry their mode in the database so resuming always uses the correct endpoint. - -## Architecture - -``` -CHAT MODE TERMINAL MODE -───────────────────────────── ───────────────────────────── -POST /api/sessions POST /api/sessions - → mode: 'chat' stored in DB → mode: 'terminal' stored in DB - ↓ ↓ -WS /ws/session/:id WS /ws/terminal/:id (NEW) - → claude --print → claude (interactive PTY) - --output-format stream-json → raw I/O passthrough, no JSON - → structured message parsing → no parsing - ↓ ↓ -DashboardView DashboardView - → SessionHeader (shared) → SessionHeader (shared) - → MessageList → TerminalSession.tsx (NEW) - → TerminalDrawer (full-height xterm.js) - → ChatInput -``` - -## Backend Changes - -### `backend/src/routes/settings.ts` -Add `session_mode` to DEFAULTS and ALLOWED set: -```typescript -const DEFAULTS = { bypass_permissions: 'true', session_mode: 'chat' } -``` - -### `backend/src/db/schema.ts` -Add `mode` column to sessions table in CREATE TABLE statement: -```sql -mode TEXT NOT NULL DEFAULT 'chat' -``` -Add a migration guard at DB init time: -```sql -ALTER TABLE sessions ADD COLUMN mode TEXT DEFAULT 'chat' --- guarded by checking existing columns first -``` - -### `backend/src/routes/sessions.ts` -- **POST** `/api/sessions`: read `session_mode` from settings at creation time, store as `mode` on the new session row -- **GET** `/api/sessions`: include `mode` field in each session object returned -- **POST** `/api/sessions/:id/stop`: after existing SessionManager check, also call `terminalManager.stop(id)` to kill a running terminal PTY - -### `backend/src/ws/terminal.ts` (NEW, ~120 lines) - -`TerminalManager` singleton — tracks active terminal PTYs by session ID (mirrors the existing `SessionManager` pattern). - -WebSocket handler for `/ws/terminal/:id`: -- On connect: look up session from DB; spawn `claude` as persistent interactive PTY - - Args: `--dangerously-skip-permissions` when bypass_permissions=true, nothing else - - PTY config: `xterm-256color`, cols=220, rows=50, cwd=session workdir, inherits env -- Messages **in**: - - `{ type: 'input', data: string }` → write to PTY stdin - - `{ type: 'resize', cols: number, rows: number }` → resize PTY -- Messages **out**: - - `{ type: 'output', data: string }` → raw PTY bytes (ANSI preserved) - - `{ type: 'status', state: 'connected' | 'disconnected' }` -- On PTY exit: broadcast `status: disconnected`, mark session `ended_at` in DB -- Idle timeout: 30 minutes (same as chat sessions) - -### `backend/src/server.ts` -Register new WS route: `server.register(terminalWsPlugin)` alongside existing session WS. - -## Frontend Changes - -### `frontend/src/views/SettingsView.tsx` -Add a "Session Mode" toggle row using the same pattern as the `bypass_permissions` toggle. Two options: **Chat** (default) | **Terminal**. POSTs `{ session_mode: 'chat' | 'terminal' }` to `/api/settings` on change. Applies to all new sessions. - -### `frontend/src/context/SessionContext.tsx` -Add `mode: 'chat' | 'terminal'` to `SessionState`. Populate it via: -- `SESSION_CREATED` action (includes mode from global setting at creation time) -- `RESUME_SESSION` action (includes mode from the session's DB record) - -This is the source of truth for which UI to render — not the global setting directly. - -### `frontend/src/hooks/useDashboard.ts` -Extend the existing parallel fetch to also load `GET /api/settings`. Return `defaultSessionMode: 'chat' | 'terminal'` (used only when creating new sessions to pre-populate the mode). - -### `frontend/src/views/DashboardView.tsx` -Two targeted changes: -1. Pass `null` to `useWebSocket` when `state.mode === 'terminal'` to prevent the chat WS from connecting -2. Swap session content based on `state.mode` — SessionHeader is always rendered: -```tsx -{state.mode === 'terminal' - ? - : <> - - - - -} -``` - -### `frontend/src/hooks/useTerminalSession.ts` (NEW) -Mirrors `useWebSocket` structure but targets `/ws/terminal/:id`: -- Connects on mount when `sessionId` is set -- Sends `{ type: 'input', data }` and `{ type: 'resize', cols, rows }` -- On `status: connected` → dispatch `WS_STATE('idle')` to SessionContext -- On `status: disconnected` → dispatch `WS_STATE('disconnected')` -- On `output` → call `onOutput(data)` callback for xterm write -- Reconnect: exponential backoff up to 5 attempts, then 30s polling (same as useWebSocket) - -### `frontend/src/components/TerminalSession.tsx` (NEW) -Full-height xterm.js terminal filling the space between SessionHeader and viewport bottom: -- Same xterm theme and FitAddon config as existing `TerminalDrawer` -- All keystrokes → `type: 'input'` via useTerminalSession -- ResizeObserver on container → `type: 'resize'` -- `onOutput` callback writes raw data to xterm instance -- No MessageList, no ChatInput — the terminal IS the interface - -### `SessionHeader` (unchanged) -Works for both modes. In terminal mode naturally shows fewer stats (no tokens counter, no running spinner — status is connected/idle). Session name, workdir, stop button, new session button, rename all function identically. - -## Session Resumption - -Sessions carry `mode` in the DB. The session list (`GET /api/sessions`) returns `mode` per session. When `DashboardView.handleResume()` is called, it dispatches `RESUME_SESSION` with the session's stored `mode` — this updates `SessionContext.state.mode` and the correct UI renders automatically. The global setting (`defaultSessionMode`) only determines the mode stored when a **new** session is created; it does not affect resumed sessions. - -## Files to Create -- `backend/src/ws/terminal.ts` -- `frontend/src/hooks/useTerminalSession.ts` -- `frontend/src/components/TerminalSession.tsx` - -## Files to Modify -- `backend/src/routes/settings.ts` -- `backend/src/db/schema.ts` (or wherever migrations run) -- `backend/src/routes/sessions.ts` -- `backend/src/server.ts` -- `frontend/src/context/SessionContext.tsx` -- `frontend/src/hooks/useDashboard.ts` -- `frontend/src/views/DashboardView.tsx` -- `frontend/src/views/SettingsView.tsx` - -## Verification - -1. **Settings**: Open `/settings`, confirm new "Session Mode" toggle appears and saves (Chat ↔ Terminal) -2. **Terminal session creation**: Switch to Terminal mode, create a new session — confirm PTY spawns `claude` interactively, full terminal renders in session view -3. **Chat session creation**: Switch back to Chat mode, create a session — confirm existing chat layout is untouched -4. **I/O passthrough**: In terminal mode, type a prompt directly in the terminal, confirm response renders with ANSI formatting -5. **Resize**: Resize browser window, confirm PTY cols/rows update -6. **Stop**: Click Stop in session header while terminal is active — confirm PTY is killed, session marked ended -7. **Session list**: Confirm both chat and terminal sessions appear in the list and resume correctly -8. **Idle timeout**: Verify terminal PTY is killed after 30 min of inactivity (same as chat) -9. **Lint**: `make lint` passes in both packages diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae5d095..236f72d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' -import DashboardView from './views/DashboardView' +import HomeView from './views/HomeView' import SessionRoute from './views/SessionRoute' import SettingsView from './views/SettingsView' import SessionsTableView from './views/SessionsTableView' @@ -12,7 +12,7 @@ export default function App() {
- } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/SessionList.tsx b/frontend/src/components/SessionList.tsx index 1d78762..3c0a11f 100644 --- a/frontend/src/components/SessionList.tsx +++ b/frontend/src/components/SessionList.tsx @@ -1,6 +1,6 @@ import { Plus, Play, Square, Trash2 } from 'lucide-react' import { Link } from 'react-router-dom' -import type { Session } from '../hooks/useDashboard' +import type { Session } from '../hooks/useHomeData' import { formatRelativeTime, lastSegment } from '../utils/format' interface SessionListProps { diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useHomeData.ts similarity index 95% rename from frontend/src/hooks/useDashboard.ts rename to frontend/src/hooks/useHomeData.ts index 7e571bb..ea95827 100644 --- a/frontend/src/hooks/useDashboard.ts +++ b/frontend/src/hooks/useHomeData.ts @@ -29,7 +29,7 @@ export interface Session { last_used: number | null } -export interface DashboardData { +export interface HomeData { account: AccountInfo | null usage: UsageData | null sessions: Session[] @@ -39,7 +39,7 @@ export interface DashboardData { error: string | null } -export function useDashboard(month?: string): DashboardData & { refresh: () => void } { +export function useHomeData(month?: string): HomeData & { refresh: () => void } { const [account, setAccount] = useState(null) const [usage, setUsage] = useState(null) const [sessions, setSessions] = useState([]) diff --git a/frontend/src/views/DashboardView.tsx b/frontend/src/views/HomeView.tsx similarity index 98% rename from frontend/src/views/DashboardView.tsx rename to frontend/src/views/HomeView.tsx index 3044818..9ab4973 100644 --- a/frontend/src/views/DashboardView.tsx +++ b/frontend/src/views/HomeView.tsx @@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom' import { ChevronDown, ChevronUp } from 'lucide-react' import { useSession } from '../context/SessionContext' import { useWebSocket } from '../hooks/useWebSocket' -import { useDashboard, type Session } from '../hooks/useDashboard' +import { useHomeData, type Session } from '../hooks/useHomeData' import StatsStrip from '../components/StatsStrip' import SessionList from '../components/SessionList' import SessionHeader from '../components/SessionHeader' @@ -15,7 +15,7 @@ import UsageChart from '../components/UsageChart' import PermissionDialog from '../components/PermissionDialog' import TerminalSession from '../components/TerminalSession' -export default function DashboardView() { +export default function HomeView() { const { state, dispatch } = useSession() const location = useLocation() const navigate = useNavigate() @@ -23,7 +23,7 @@ export default function DashboardView() { const [showModal, setShowModal] = useState(location.state?.openModal === true) const [chartOpen, setChartOpen] = useState(false) - const { account, usage, sessions, activeSessions, defaultSessionMode, loading, refresh } = useDashboard() + const { account, usage, sessions, activeSessions, defaultSessionMode, loading, refresh } = useHomeData() // Local sessions state for optimistic deletion const [localSessions, setLocalSessions] = useState(null) const displaySessions = localSessions ?? sessions diff --git a/frontend/src/views/SessionRoute.tsx b/frontend/src/views/SessionRoute.tsx index 26502ec..5ef6ad6 100644 --- a/frontend/src/views/SessionRoute.tsx +++ b/frontend/src/views/SessionRoute.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import { useParams, Navigate, useLocation, useNavigate } from 'react-router-dom' import { useSession } from '../context/SessionContext' -import DashboardView from './DashboardView' -import type { Session } from '../hooks/useDashboard' +import HomeView from './HomeView' +import type { Session } from '../hooks/useHomeData' export default function SessionRoute() { const { sessionId } = useParams<{ sessionId: string }>() @@ -47,7 +47,7 @@ export default function SessionRoute() { // Keep URL and context in sync: clear session state whenever this route unmounts. // This handles browser Back/Forward navigation, which bypasses the explicit navigate() // calls in the session handlers and would otherwise leave a stale sessionId in context, - // causing DashboardView at "/" to render the session view instead of the session list. + // causing HomeView at "/" to render the session view instead of the session list. useEffect(() => { return () => { dispatch({ type: 'SESSION_CLEARED' }) } }, [dispatch]) @@ -75,5 +75,5 @@ export default function SessionRoute() { ) } - return + return } diff --git a/scripts/claude-code-webui.service.template b/scripts/claude-code-webui.service.template index 5993f1e..3c10f89 100644 --- a/scripts/claude-code-webui.service.template +++ b/scripts/claude-code-webui.service.template @@ -6,7 +6,7 @@ After=network.target Type=simple WorkingDirectory=__INSTALL_DIR__ ExecStart=__NODE_BIN__ __INSTALL_DIR__/backend/dist/server.js -EnvironmentFile=%h/.config/systemd/user/claude-code-dashboard.env +EnvironmentFile=%h/.config/systemd/user/claude-code-webui.env Restart=on-failure RestartSec=5 diff --git a/scripts/test-service-install.sh b/scripts/test-service-install.sh index 1deacab..f5e8aaa 100755 --- a/scripts/test-service-install.sh +++ b/scripts/test-service-install.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TEMPLATE="$SCRIPT_DIR/claude-code-dashboard.service.template" +TEMPLATE="$SCRIPT_DIR/claude-code-webui.service.template" PASS=0 FAIL=0 @@ -40,13 +40,13 @@ trap 'rm -rf "$TMPDIR"' EXIT echo "=== Unit file template substitution ===" sed \ - "s|__INSTALL_DIR__|/opt/claude-dashboard|g; s|__NODE_BIN__|/usr/bin/node|g" \ + "s|__INSTALL_DIR__|/opt/claude-code-webui|g; s|__NODE_BIN__|/usr/bin/node|g" \ "$TEMPLATE" > "$TMPDIR/test.service" UNIT=$(cat "$TMPDIR/test.service") -assert_contains "WorkingDirectory set" "WorkingDirectory=/opt/claude-dashboard" "$UNIT" -assert_contains "ExecStart node path" "ExecStart=/usr/bin/node /opt/claude-dashboard/backend/dist/server.js" "$UNIT" -assert_contains "EnvironmentFile uses %h" "EnvironmentFile=%h/.config/systemd/user/claude-code-dashboard.env" "$UNIT" +assert_contains "WorkingDirectory set" "WorkingDirectory=/opt/claude-code-webui" "$UNIT" +assert_contains "ExecStart node path" "ExecStart=/usr/bin/node /opt/claude-code-webui/backend/dist/server.js" "$UNIT" +assert_contains "EnvironmentFile uses %h" "EnvironmentFile=%h/.config/systemd/user/claude-code-webui.env" "$UNIT" assert_contains "Restart=on-failure" "Restart=on-failure" "$UNIT" assert_contains "WantedBy=default.target" "WantedBy=default.target" "$UNIT" assert_not_contains "no __INSTALL_DIR__ remaining" "__INSTALL_DIR__" "$UNIT" diff --git a/scripts/uninstall-service.sh b/scripts/uninstall-service.sh index 4700630..2d8ed2d 100755 --- a/scripts/uninstall-service.sh +++ b/scripts/uninstall-service.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -SERVICE_NAME="claude-code-dashboard" +SERVICE_NAME="claude-code-webui" SYSTEMD_USER_DIR="$HOME/.config/systemd/user" ENV_FILE="$SYSTEMD_USER_DIR/$SERVICE_NAME.env" UNIT_FILE="$SYSTEMD_USER_DIR/$SERVICE_NAME.service" From c1d9cce5da0f2d60953eaf18d066f35ff55d3699 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Fri, 15 May 2026 18:06:23 +0300 Subject: [PATCH 3/3] [fix:global] windows support fix and favicon added --- CLAUDE.md | 1 + backend/src/db/schema.ts | 5 +++-- backend/src/routes/system.ts | 8 ++++++++ backend/src/server.ts | 2 ++ frontend/src/views/SettingsView.tsx | 21 ++++++++++++++++++++- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 backend/src/routes/system.ts diff --git a/CLAUDE.md b/CLAUDE.md index 1d64449..8ca437a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,7 @@ The Makefile's `dev-backend` target and `run.sh` both conditionally set `NODE_EX | GET/POST | `/api/sessions` | `routes/sessions.ts` | List sessions, create session | | POST | `/api/sessions/:id/stop` | `routes/sessions.ts` | Stop active session | | GET/POST | `/api/settings` | `routes/settings.ts` | Persistent key-value settings (SQLite-backed) | +| GET | `/api/system` | `routes/system.ts` | Read-only runtime info (SQLite DB path) | | GET | `/health` | `server.ts` | Health check | | WS | `/ws/session/:id` | `ws/session.ts` | Chat PTY I/O over WebSocket | | WS | `/ws/terminal/:id` | `ws/terminal.ts` | Terminal PTY I/O over WebSocket | diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 4c2e76f..86b64b7 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,9 +1,10 @@ import Database from 'better-sqlite3' import path from 'path' import fs from 'fs' +import os from 'os' -const DB_DIR = process.env.DATA_DIR ?? path.join(process.env.HOME ?? '/root', '.claude', 'webui') -const DB_PATH = path.join(DB_DIR, 'webui.db') +const DB_DIR = process.env.DATA_DIR ?? path.join(os.homedir(), '.claude', 'webui') +export const DB_PATH = path.join(DB_DIR, 'webui.db') function initDb(): Database.Database { fs.mkdirSync(DB_DIR, { recursive: true }) diff --git a/backend/src/routes/system.ts b/backend/src/routes/system.ts new file mode 100644 index 0000000..a9d7e87 --- /dev/null +++ b/backend/src/routes/system.ts @@ -0,0 +1,8 @@ +import type { FastifyInstance } from 'fastify' +import { DB_PATH } from '../db/schema' + +export async function systemRoutes(fastify: FastifyInstance) { + fastify.get('/api/system', async (_req, reply) => { + return reply.send({ db_path: DB_PATH }) + }) +} diff --git a/backend/src/server.ts b/backend/src/server.ts index c7741c1..4b35463 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -10,6 +10,7 @@ import { sessionRoutes } from './routes/sessions' import { sessionWsRoutes } from './ws/session' import { terminalWsRoutes } from './ws/terminal' import { settingsRoutes } from './routes/settings' +import { systemRoutes } from './routes/system' import { initAccountCache } from './services/accountCache' import { statuslineRoutes } from './routes/statusline' import { setupStatusline } from './lib/setup-statusline' @@ -42,6 +43,7 @@ async function start() { await fastify.register(sessionWsRoutes) await fastify.register(terminalWsRoutes) await fastify.register(settingsRoutes) + await fastify.register(systemRoutes) await fastify.register(statuslineRoutes) fastify.get('/health', async () => ({ status: 'ok' })) diff --git a/frontend/src/views/SettingsView.tsx b/frontend/src/views/SettingsView.tsx index 1947bc1..36a0f93 100644 --- a/frontend/src/views/SettingsView.tsx +++ b/frontend/src/views/SettingsView.tsx @@ -9,6 +9,7 @@ export default function SettingsView() { const savedTimerRef = useRef | null>(null) const [statuslineUnmatched, setStatuslineUnmatched] = useState<'ignore' | 'create'>('ignore') const [settingsLoaded, setSettingsLoaded] = useState(false) + const [dbPath, setDbPath] = useState(null) useEffect(() => { return () => { if (savedTimerRef.current) clearTimeout(savedTimerRef.current) } @@ -24,6 +25,11 @@ export default function SettingsView() { setSettingsLoaded(true) }) .catch(() => setSettingsLoaded(true)) + + fetch('/api/system') + .then((r) => r.json() as Promise<{ db_path: string }>) + .then((data) => setDbPath(data.db_path)) + .catch(() => {}) }, []) async function handleSave() { @@ -56,7 +62,7 @@ export default function SettingsView() {
-
+
{/* Session mode toggle */}
)