From 37a989af442a2f3788669d3e767e0d17f1d76768 Mon Sep 17 00:00:00 2001 From: Valerii Kovalskii Date: Sun, 19 Jul 2026 22:21:43 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Workspace=20browser=20terminal=20?= =?UTF-8?q?=E2=80=94=20B-0=20walking=20skeleton=20(v7.7.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a real terminal in the browser (new "Workspace" view) as the first step of the TermDeck merge. Single pane for now; splits, tmux-less reattach, saved commands and per-project workspaces come next. Architecture (optional feature — core stays dependency-free): - src/terminal.js: lazily loads @lydell/node-pty (prebuilt-only, no node-gyp) and hand-rolls the WebSocket on the stdlib http.Server 'upgrade' event, so `ws` is NOT a dependency. Binary frames carry raw terminal bytes; text frames carry JSON control (resize/ready/exit). - Security: a per-process token + same-origin Origin check gate the WS (it grants shell access), and cwd must pass isSafeLaunchPath. Safe to run in server mode. - server.js: server.on('upgrade') wiring, GET /api/terminal/status (reports availability + serves the token same-origin), a strict allowlisted GET /vendor/* static route (xterm.js served lazily so the base dashboard never carries the ~490KB bundle), and ws:/wss: added to connect-src CSP. - Frontend: src/frontend/workspace.js lazy-loads the vendored xterm.js + fit addon, opens the WS, wires stdin/stdout/resize; nav item, render() dispatch, NAV_HELP entry, teardown on leaving the view, themed CSS. - @lydell/node-pty in optionalDependencies; CLAUDE.md updated to reflect the one optional dep. Vendored @xterm/xterm 6.0.0 + addon-fit (MIT). Tests: 9 new unit tests for the WS frame codec + token compare. Verified end-to-end in a browser (real zsh round-trip, bad-token rejection, teardown on navigate). Full suite 166 pass / 2 skipped / 0 fail. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 +- CLAUDE.md | 2 +- package.json | 6 +- src/changelog.js | 11 ++ src/frontend/app.js | 11 ++ src/frontend/index.html | 4 + src/frontend/sidebar-config.js | 3 +- src/frontend/styles.css | 24 +++ src/frontend/vendor/addon-fit.js | 2 + src/frontend/vendor/xterm.css | 285 ++++++++++++++++++++++++++++ src/frontend/vendor/xterm.js | 2 + src/frontend/workspace.js | 171 +++++++++++++++++ src/html.js | 1 + src/server.js | 57 +++++- src/terminal.js | 315 +++++++++++++++++++++++++++++++ test/terminal.test.js | 93 +++++++++ 16 files changed, 985 insertions(+), 5 deletions(-) create mode 100644 src/frontend/vendor/addon-fit.js create mode 100644 src/frontend/vendor/xterm.css create mode 100644 src/frontend/vendor/xterm.js create mode 100644 src/frontend/workspace.js create mode 100644 src/terminal.js create mode 100644 test/terminal.test.js diff --git a/.gitignore b/.gitignore index 106c9ab..db0fb87 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules/ .hooks/ .agent-memory/ hooks/ -.tmp/ \ No newline at end of file +.tmp/ +package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md index d780846..572c217 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ docs/ ## Key architecture decisions -- **Zero dependencies** — only Node.js stdlib + system `sqlite3` CLI for SQLite agents +- **Zero dependencies for the core** — dashboard/CLI use only Node.js stdlib + system `sqlite3` CLI for SQLite agents. The optional browser terminal (Workspace) is the sole exception: it lazily loads `@lydell/node-pty` (declared in `optionalDependencies`, prebuilt-only, never runs node-gyp). If it is absent the dashboard still runs and the terminal reports itself disabled. Do NOT add core dependencies. - **Node >= 18** — minimum supported version - **Single process** — server + static HTML in one process - **Template injection** — `html.js` reads CSS/JS files and injects via `split/join` (not `String.replace` which breaks on `$` characters in JS code) diff --git a/package.json b/package.json index b123970..ccb1b13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codbash-app", - "version": "7.6.0", + "version": "7.7.0", "description": "Dashboard + CLI for AI coding agents — Claude Code, Codex, Cursor, OpenCode, Kiro. View, search, resume, convert, sync sessions.", "bin": { "codbash": "./bin/cli.js", @@ -38,5 +38,9 @@ "license": "MIT", "engines": { "node": ">=18" + }, + "comment:optionalDependencies": "Browser terminal (Workspace) only. @lydell/node-pty ships prebuilt binaries and never invokes node-gyp; if no prebuilt exists for the platform it is simply skipped and the dashboard still runs — the terminal feature stays disabled with a friendly notice. The core dashboard remains dependency-free.", + "optionalDependencies": { + "@lydell/node-pty": "^1.2.0-beta.12" } } diff --git a/src/changelog.js b/src/changelog.js index e080de9..be92d5e 100644 --- a/src/changelog.js +++ b/src/changelog.js @@ -1,6 +1,17 @@ 'use strict'; const CHANGELOG = [ + { + version: '7.7.0', + date: '2026-07-19', + title: 'Workspace — browser terminal (preview)', + changes: [ + 'New "Workspace" view: a real terminal right in the browser (xterm.js) — run shells and agents without leaving codbash', + 'Optional feature: the core dashboard stays dependency-free; the terminal lazily loads @lydell/node-pty, which ships prebuilt binaries (no compiler / node-gyp needed)', + 'Secured by a per-process token + same-origin check on the WebSocket, so it is safe to run the dashboard in server mode', + 'This is the first step (single pane); splits, reattach across reload, saved commands and per-project workspaces are coming next', + ], + }, { version: '7.6.0', date: '2026-07-19', diff --git a/src/frontend/app.js b/src/frontend/app.js index 77b3a7f..577f0e3 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1594,6 +1594,12 @@ function render() { var stats = document.getElementById('stats'); if (!content) return; + // Tear down the live terminal when navigating away from the Workspace view + // so the pty + WebSocket don't leak in the background. + if (currentView !== 'workspace' && typeof teardownWorkspaceIfActive === 'function') { + teardownWorkspaceIfActive(); + } + // Preserve scroll + collapsed state across re-renders var scrollTop = content.scrollTop; var collapsedGroups = new Set(); @@ -1665,6 +1671,11 @@ function render() { return; } + if (currentView === 'workspace') { + renderWorkspace(content); + return; + } + if (currentView === 'settings') { renderSettings(content); return; diff --git a/src/frontend/index.html b/src/frontend/index.html index d2899ea..7d74930 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -61,6 +61,10 @@ All Sessions +