-
Notifications
You must be signed in to change notification settings - Fork 0
feat: bootstrap — working web terminal (ghostty-web port) #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
6d57808
docs: add SPEC and ADR for wtty bootstrap
jesse23 8d6ff71
docs: update hardcoded config and theme details in ADR 001
jesse23 c224892
chore: add runtime deps and simplify build for app
jesse23 0d0ae1c
chore: update build script and CI for server-only target
jesse23 e4890f9
feat: implement bootstrap server (ghostty-web port)
jesse23 4e6b86d
chore: change default port to 2346
jesse23 650fbcb
chore: replace start with dev and preview scripts
jesse23 0411e10
fix: use named spawn import from node-pty
jesse23 35d27f2
fix: correct FitAddon import and await term.open
jesse23 0f14944
fix: switch to default import for node-pty CJS compat
jesse23 19ef903
fix: defer fitAddon.fit and connect to requestAnimationFrame
jesse23 0a96ae9
fix: rewrite server as faithful TypeScript port of ghostty-web demo
jesse23 70e6915
fix: upgrade ghostty-web to next (0.4.0-next.14) to fix blank render
jesse23 7ec38b5
fix: match demo exact deps (ghostty-web@0.2.1, node-pty@1.1.0, ws@8.1…
jesse23 1180a14
fix: use ghostty-web@next which has init() export
jesse23 705ec6f
chore: change default port to 2346
jesse23 95378ef
fix: register term.onData before connect so DA responses reach PTY
jesse23 e36f89b
fix: faithful 1:1 TypeScript port of ghostty-web demo server
jesse23 c3d5647
fix: send PTY data as text frames (binary: false) to browser
jesse23 d8eaed2
fix: run server with node+tsx instead of bun (pty compat)
jesse23 00004df
feat: dual PTY adapter — Bun.Terminal on bun, node-pty on node
jesse23 be8fde2
chore: log PTY backend on startup
jesse23 f6d5403
chore: print pty backend at module load, not per-request
jesse23 2230ce8
refactor: split PTY into src/pty/bun.ts and src/pty/node.ts, dynamic …
jesse23 b833fc0
refactor: drop allowImportingTsExtensions, use extensionless imports
jesse23 525582a
refactor: move PtyProcess interface to src/pty/types.ts
jesse23 c8ee6ce
docs: update ADR 001 with dual PTY adapter and mark Accepted
jesse23 c83a30a
feat: remove title bar, full-screen terminal only
jesse23 7d41b49
chore: simplify dev:node to tsx src/server.ts
jesse23 31eca0e
fix: update preview script to use bun for running the server
jesse23 5b7a5ee
chore: update deps to latest
jesse23 eb474b6
chore: mark runtime deps as external in build, clean up scripts
jesse23 82d9b50
chore: remove PR approval requirement, CI checks sufficient
jesse23 c71aa5d
ci: add Copilot review workflow, restore 1 required approval
jesse23 629183b
ci: allow self-approval (disable require_code_owner_reviews)
jesse23 191dbdc
ci: restore require_code_owner_reviews
jesse23 bb80f9e
ci: remove unnecessary checkout from copilot-review workflow
jesse23 fe92eac
test: add unit tests for mimeType and ghosttyWebRootFromMain
jesse23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,17 @@ | ||
| # GitHub repository settings reference. | ||
| # These rules must be applied manually in GitHub → Settings → Branches. | ||
| # These rules must be applied manually via: gh api repos/jesse23/wtty/branches/main/protection | ||
| # They are documented here so the intended configuration is version-controlled. | ||
| # | ||
| # To apply: Settings → Branches → Add branch ruleset → target "main" | ||
| # | ||
| # Branch protection for: main | ||
| # | ||
| # required_status_checks: | ||
| # strict: true # branch must be up to date before merge | ||
| # contexts: | ||
| # - lint | ||
| # - build | ||
| # - test | ||
| # strict: true | ||
| # contexts: [lint, build, test] | ||
| # | ||
| # required_pull_request_reviews: | ||
| # required_approving_review_count: 1 | ||
| # dismiss_stale_reviews: true # re-review required after new push | ||
| # require_code_owner_reviews: true # enforces CODEOWNERS (jesse23 only) | ||
| # dismiss_stale_reviews: true | ||
| # require_code_owner_reviews: true | ||
| # | ||
| # allow_force_pushes: false | ||
| # allow_deletions: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,4 +34,4 @@ jobs: | |
| with: | ||
| bun-version: 1.3.10 | ||
| - run: bun install --frozen-lockfile | ||
| - run: bun test | ||
| - run: bun run test | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| name: Copilot Review | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - main | ||
|
|
||
| jobs: | ||
| copilot-review: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write | ||
| steps: | ||
| - name: Request Copilot review | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh pr edit ${{ github.event.pull_request.number }} --add-reviewer "Copilot" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # ADR 001: Bootstrap — Port ghostty-web demo | ||
|
|
||
| **SPEC:** wtty | ||
| **Status:** Accepted | ||
| **Date:** 2026-03-21 | ||
|
|
||
| --- | ||
|
|
||
| ## Context | ||
|
|
||
| The first slice of wtty is a direct port of the `ghostty-web` demo. The demo already proves the full round-trip — browser renders a terminal, user types, a real PTY responds — so rather than building from scratch, we take that working implementation and make it the foundation of wtty. Everything else (config, multi-session, CLI) is deferred. | ||
|
|
||
| ## Decision | ||
|
|
||
| Port `ghostty-web/demo` into a single entry point (`src/server.ts`). The server serves everything on one port — HTML, assets, and WebSocket — with all config hardcoded inline. | ||
|
|
||
| **Server (`src/server.ts`):** | ||
| - HTTP server on port `2346` | ||
| - Serves the terminal HTML inline as a template string at `/` | ||
| - Serves `/dist/` assets (ghostty-web JS + WASM) from the installed `ghostty-web` package via `require.resolve` | ||
| - WebSocket endpoint at `/ws?cols=&rows=` — spawns one PTY per connection | ||
| - PTY: auto-detects shell (`$SHELL` on macOS/Linux, `cmd.exe` on Windows), cwd `$HOME` | ||
| - WebSocket message framing: raw string for PTY input; JSON `{ type: "resize", cols, rows }` for resize | ||
| - On PTY exit: sends exit message and closes WebSocket | ||
|
|
||
| **PTY layer (`src/pty/`):** | ||
| - `types.ts` — `PtyProcess` interface shared by both adapters | ||
| - `bun.ts` — `Bun.Terminal` adapter (Bun v1.3.5+, native, no native addon) | ||
| - `node.ts` — `@lydell/node-pty` adapter (Node.js) | ||
| - `index.ts` — detects runtime via `process.versions.bun`, dynamically imports the correct adapter | ||
|
|
||
| **Browser (inline HTML template in `src/server.ts`):** | ||
| - Full-viewport terminal with macOS-style title bar, no surrounding chrome | ||
| - `ghostty-web` (`init` + `Terminal` + `FitAddon`) as the terminal renderer | ||
| - Hardcoded config: `fontSize: 14`, `fontFamily: 'FiraMono Nerd Font, Menlo, Monaco, Courier New, monospace'` | ||
| - Theme: Dracula (official) — `background: #282A36`, `foreground: #F8F8F2`, `cursor: #F8F8F2`, `selection: #44475A`, `black: #21222C`, `red: #FF5555`, `green: #50FA7B`, `yellow: #F1FA8C`, `blue: #BD93F9`, `purple: #FF79C6`, `cyan: #8BE9FD`, `white: #F8F8F2`, `brightBlack: #6272A4`, `brightRed: #FF6E6E`, `brightGreen: #69FF94`, `brightYellow: #FFFFA5`, `brightBlue: #D6ACFF`, `brightPurple: #FF92DF`, `brightCyan: #A4FFFF`, `brightWhite: #FFFFFF` | ||
| - `FitAddon.fit()` + `observeResize()` for auto-resize | ||
| - WebSocket connects to same origin at `/ws` | ||
| - On resize: sends JSON resize message | ||
| - On close: reconnects after 2s | ||
|
|
||
| **Dependencies:** | ||
| - `@lydell/node-pty@1.1.0` — cross-platform PTY for Node.js (fork with prebuilt binaries, no node-gyp) | ||
| - `ws@8.18.3` — WebSocket server | ||
| - `ghostty-web@0.4.0-next` — WASM terminal emulator, ported directly from the demo | ||
|
|
||
| ## Considered Options | ||
|
|
||
| **Option A: xterm.js instead of ghostty-web** | ||
| xterm.js is the industry standard (used by ttyd, VS Code, wetty). More documentation and addons. Rejected for this slice — ghostty-web is already the working reference, uses the same `Terminal`/`FitAddon` API, and porting it is the fastest path to a running terminal. | ||
|
|
||
| **Option B: Separate frontend build (Vite/esbuild)** | ||
| Would enable TypeScript in the browser and hot reload. Deferred — the demo uses plain `<script type="module">` and so does this port. A build step can be introduced when the frontend grows beyond a single file. | ||
|
|
||
| **Option C: Serve `index.html` as a file on disk** | ||
| Keeps HTML separate from server code. Rejected — inline template keeps everything in one deployable file, consistent with the ghostty-web/demo pattern. | ||
|
|
||
| **Option D: `Bun.Terminal` only (drop node-pty)** | ||
| `Bun.Terminal` is native to Bun, zero-dependency, and faster. Rejected as the sole option — would make Bun a hard runtime requirement for users. The dual-adapter pattern keeps Node.js as a valid runtime. | ||
|
|
||
| **Option E: `node-pty` (original microsoft/node-pty)** | ||
| The original package requires `node-gyp` compilation on install. `@lydell/node-pty` is a maintained fork with prebuilt binaries that eliminates this friction while keeping full API compatibility. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Full round-trip working on both Bun and Node.js runtimes | ||
| - `bun run dev` uses `Bun.Terminal` (native); `npm run dev:node` uses `node-pty` | ||
| - No config file yet — all values hardcoded; acceptable for this slice | ||
| - No auth — localhost-only, same security posture as ghostty-web/demo | ||
| - Frontend is plain HTML/JS — no TypeScript in browser until a build step is added | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # SPEC: CLI | ||
|
|
||
| **Author:** jesse23 | ||
| **Last Updated:** 2026-03-21 | ||
|
|
||
| --- | ||
|
|
||
| ## Description | ||
|
|
||
| The `wtty` CLI is a thin client that controls a running wtty server over HTTP. It handles two concerns: server lifecycle (start/stop/restart the daemon) and session management (create/list/kill sessions via the REST API). | ||
|
|
||
| The CLI communicates with the server exclusively over HTTP to localhost — no Unix sockets, no direct process management beyond the startup fork. A PID file (`~/.wtty/server.pid`) is the source of truth for whether a daemon is running; a `/health` HTTP check confirms it is actually responsive. | ||
|
|
||
| **Why HTTP over Unix socket?** wtty already speaks HTTP — reusing the same interface keeps the surface area minimal and makes the CLI trivially debuggable with `curl`. Unix sockets offer lower latency but the difference is imperceptible for CLI interactions. | ||
|
|
||
| **Why Commander.js?** Zero dependencies, 18ms startup, TypeScript-native, used by Vue CLI and Vite. Yargs and oclif are heavier and provide features (plugin systems, config files) that are unnecessary here. | ||
|
|
||
| **Why PID file + health check vs port probe alone?** A port probe can give false positives (another process on the same port). PID file + signal 0 check + HTTP health check gives reliable daemon detection with graceful handling of stale PID files. | ||
|
|
||
| ## Features | ||
|
|
||
| | Feature | Description | ADR | Done? | | ||
| |---------|-------------|-----|-------| | ||
| | Server lifecycle | `wtty start`, `wtty stop`, `wtty restart`, `wtty status` — daemon control via PID file + HTTP | — | ⬜ | | ||
| | Session management | `wtty session create/list/kill` — thin wrappers over the session REST API | — | ⬜ | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # SPEC: UI | ||
|
|
||
| **Author:** jesse23 | ||
| **Last Updated:** 2026-03-21 | ||
|
|
||
| --- | ||
|
|
||
| ## Description | ||
|
|
||
| The wtty web UI is served by the wtty server and runs entirely in the browser. It has two surfaces: a terminal view (full-viewport, one session per tab) and a session manager (list, create, open, kill sessions, and control the server). | ||
|
|
||
| The UI has no build step in the initial slices — plain HTML + `<script type="module">` importing `ghostty-web` assets served by the server itself. A bundler can be introduced later if the UI grows beyond a handful of files. | ||
|
|
||
| **Why ghostty-web over xterm.js?** ghostty-web is the reference implementation for this project, already available locally, and shares the same `Terminal` / `FitAddon` API shape as xterm.js. xterm.js is the safer long-term bet (wider ecosystem, VS Code backing) but ghostty-web is sufficient for the initial slices and avoids an early dependency decision. | ||
|
|
||
| **Why no framework (React/Vue) yet?** The session manager UI is simple enough (a table + buttons) that a framework adds more complexity than it removes. Revisit when the UI grows. | ||
|
|
||
| ## Features | ||
|
|
||
| | Feature | Description | ADR | Done? | | ||
| |---------|-------------|-----|-------| | ||
| | Terminal view | Full-viewport terminal using `ghostty-web`, auto-fit, reconnect on disconnect | [001](../adrs/001.wtty.bootstrap.md) | ⬜ | | ||
| | Session manager | List sessions, open in new tab, create new session, kill session | — | ⬜ | | ||
| | Server control | Restart and stop server from the UI (calls server control API) | — | ⬜ | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # SPEC: wtty | ||
|
|
||
| **Author:** jesse23 | ||
| **Last Updated:** 2026-03-21 | ||
|
|
||
| --- | ||
|
|
||
| ## Description | ||
|
|
||
| wtty is a web TTY that lets you run CLI/TUI applications in a browser tab, on any platform. Point a browser at a running wtty server and you get a full terminal — colors, cursor, resize, keyboard input — backed by a real PTY on the host machine. | ||
|
|
||
| The goal is the same as ttyd and GoTTY: zero client-side installation, full TUI support, cross-platform. wtty uses `ghostty-web` as the terminal renderer (WASM-backed, same API as xterm.js) and `@lydell/node-pty` for cross-platform PTY support. | ||
|
|
||
| **Why a single-port design?** Simplifies reverse proxy setup (nginx, ngrok, Cloudflare Tunnel) — one upstream, no separate port for assets vs API vs WebSocket. Same pattern used by ttyd and ghostty-web/demo. | ||
|
|
||
| **Why `ws` over Socket.IO?** Minimal footprint, no client-side library requirement, sufficient for raw PTY streaming. Socket.IO's rooms/namespaces are unnecessary overhead for this use case. | ||
|
|
||
| **Persona:** Developers who want shell or TUI app access from any browser tab. | ||
|
|
||
| ## Features | ||
|
|
||
| | Feature | Description | ADR | Done? | | ||
| |---------|-------------|-----|-------| | ||
| | Bootstrap | Port `ghostty-web` demo into wtty — full-screen terminal in a browser tab, single server, hardcoded config | [001](../adrs/001.wtty.bootstrap.md) | ⬜ | | ||
| | Config file | Load shell, port, font, theme from a config file (`~/.wtty/config.json`) | — | ⬜ | | ||
| | Named sessions | Session registry keyed by ID; create/locate via `/ws?session=<id>`; PTY survives WebSocket disconnect | — | ⬜ | | ||
| | Session REST API | `GET /api/sessions`, `POST /api/sessions`, `DELETE /api/sessions/:id` | — | ⬜ | | ||
| | Server control API | `POST /api/server/restart`, `POST /api/server/stop` | — | ⬜ | | ||
| | Health endpoint | `GET /health` — returns 200 + uptime/version; used by CLI for daemon lifecycle checks | — | ⬜ | | ||
| | UI | Browser interface for terminal, session management, and server control | — | ⬜ | | ||
| | CLI | `wtty` binary for server lifecycle and session management | — | ⬜ | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { homedir } from 'node:os'; | ||
| import type { PtyProcess } from './types'; | ||
|
|
||
| export function spawn(shell: string, cols: number, rows: number): PtyProcess { | ||
| let onDataCb: ((data: string) => void) | undefined; | ||
| let onExitCb: ((e: { exitCode: number }) => void) | undefined; | ||
|
|
||
| const proc = Bun.spawn([shell], { | ||
| terminal: { | ||
| cols, | ||
| rows, | ||
| data(_term: unknown, data: Uint8Array) { | ||
| onDataCb?.(Buffer.from(data).toString('utf8')); | ||
| }, | ||
| }, | ||
| cwd: homedir(), | ||
| env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, | ||
| }); | ||
|
|
||
| proc.exited.then((exitCode) => { | ||
| onExitCb?.({ exitCode: exitCode ?? 0 }); | ||
| }); | ||
|
|
||
| return { | ||
| onData(cb) { | ||
| onDataCb = cb; | ||
| }, | ||
| onExit(cb) { | ||
| onExitCb = cb; | ||
| }, | ||
| write(data) { | ||
| proc.terminal?.write(data); | ||
| }, | ||
| resize(cols, rows) { | ||
| proc.terminal?.resize(cols, rows); | ||
| }, | ||
| kill() { | ||
| proc.kill(); | ||
| }, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export type { PtyProcess } from './types'; | ||
|
|
||
| const isBun = !!process.versions.bun; | ||
| console.log(`pty: ${isBun ? 'Bun.Terminal' : 'node-pty'}`); | ||
|
|
||
| const { spawn: _spawn } = await (isBun ? import('./bun') : import('./node')); | ||
|
|
||
| export const spawn = _spawn; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.