feat: Phase 1 complete — ghostty-web replaces xterm.js as terminal frontend

This commit is contained in:
Ken
2026-05-28 07:17:53 +00:00
parent ca5f34be99
commit 316297338b
3 changed files with 2812 additions and 0 deletions
+3
View File
@@ -28,3 +28,6 @@ Thumbs.db
.vscode/
*.swp
*.swo
# Verification screenshots
*.png
@@ -0,0 +1,596 @@
# Phase 1: ghostty-web Swap Implementation Plan
> **Execution:** Use the subagent-driven-development workflow to implement this plan.
**Goal:** Replace xterm.js with ghostty-web as the terminal emulator frontend — same backend, better VT parsing.
**Architecture:** ghostty-web is a WASM-compiled port of Ghostty's VT100 parser with xterm.js API compatibility. It ships a UMD build (`ghostty-web.umd.cjs`) that exposes globals via `<script>` tag — matching the project's no-build-step vendoring pattern. The WASM file (`ghostty-vt.wasm`) must be served alongside it. ghostty-web bundles FitAddon natively; SearchAddon, WebLinksAddon, and ImageAddon have no ghostty-web equivalents yet and need compatibility investigation.
**Tech Stack:** ghostty-web (npm), WASM, vanilla JS (no build step), Python pytest for frontend structural tests
**Spec:** `docs/superpowers/specs/2026-05-28-terminal-architecture-redesign.md` (Phase 1 section)
---
### Task 1: Research ghostty-web addon compatibility
**Files:**
- Create: `docs/superpowers/plans/ghostty-web-addon-research.md`
**Context:** ghostty-web claims xterm.js API compatibility and bundles FitAddon natively. But we use four addons: FitAddon, SearchAddon, WebLinksAddon, and ImageAddon. This task determines which work, which don't, and what the fallback plan is.
**Step 1: Download and inspect the ghostty-web npm package**
Run:
```bash
cd /tmp && mkdir ghostty-web-research && cd ghostty-web-research
npm pack ghostty-web
tar xzf ghostty-web-*.tgz
ls -la package/
ls -la package/dist/
```
Examine the UMD build to understand what globals it exposes:
```bash
head -100 package/dist/ghostty-web.umd.cjs
# Look for: what global name it uses (GhosttyWeb? Terminal? etc.)
# Look for: FitAddon export
# Look for: init() function export
```
Check the WASM file size:
```bash
ls -la package/ghostty-vt.wasm
ls -la package/dist/ghostty-vt.wasm
```
**Step 2: Check if xterm.js addons are loadable via ghostty-web's `loadAddon()` API**
Read the UMD build to verify the `loadAddon()` method signature. The xterm.js addon interface is:
```javascript
// Addon must implement: activate(terminal), dispose()
terminal.loadAddon(addonInstance);
```
If ghostty-web implements the same `loadAddon()` contract, existing xterm.js addons (SearchAddon, WebLinksAddon) may work as-is since they only depend on the Terminal API surface.
**Step 3: Check FitAddon**
ghostty-web bundles FitAddon natively (confirmed in its AGENTS.md: "Addons (FitAddon)"). Determine:
- Is it exported from the UMD build as `FitAddon`?
- Does it have the same API: `new FitAddon()`, `.fit()`, `.proposeDimensions()`?
**Step 4: Check ghostty-web's `init()` requirement**
ghostty-web requires `await init()` before creating Terminal instances (loads WASM). The UMD build needs:
- Does `init()` accept a WASM URL parameter? (needed since we serve from `/vendor/`)
- Or does it auto-detect the WASM location relative to the JS file?
**Step 5: Document findings**
Write `docs/superpowers/plans/ghostty-web-addon-research.md` with:
- What globals the UMD build exposes
- FitAddon: works/doesn't, API differences
- SearchAddon: can xterm.js addon load via `loadAddon()`? If not, what's the fallback?
- WebLinksAddon: same question. Note: ghostty-web may handle URL detection natively.
- ImageAddon: same question. Note: this is the least critical — Sixel/iTerm2 image support is a nice-to-have.
- `init()` behavior: WASM URL configuration
- CSS: does ghostty-web need `xterm.css` or has its own styles?
- Any API surface differences found (e.g., `term.write()` accepts Uint8Array? string only?)
**Step 6: Commit**
```bash
git add docs/superpowers/plans/ghostty-web-addon-research.md
git commit -m "docs: ghostty-web addon compatibility research"
```
---
### Task 2: Vendor ghostty-web files
**Files:**
- Create: `muxplex/frontend/vendor/ghostty-web.umd.cjs`
- Create: `muxplex/frontend/vendor/ghostty-vt.wasm`
- Create: `muxplex/frontend/vendor/ghostty-web.css` (if ghostty-web has its own CSS)
**Depends on:** Task 1 (need to know exact file names and what to vendor)
**Step 1: Copy the UMD build and WASM into vendor directory**
Run:
```bash
# If not already unpacked from Task 1:
cd /tmp/ghostty-web-research || (mkdir -p /tmp/ghostty-web-research && cd /tmp/ghostty-web-research && npm pack ghostty-web && tar xzf ghostty-web-*.tgz)
# Copy UMD build
cp /tmp/ghostty-web-research/package/dist/ghostty-web.umd.cjs muxplex/frontend/vendor/ghostty-web.js
# Copy WASM file (check both locations)
cp /tmp/ghostty-web-research/package/dist/ghostty-vt.wasm muxplex/frontend/vendor/ghostty-vt.wasm \
|| cp /tmp/ghostty-web-research/package/ghostty-vt.wasm muxplex/frontend/vendor/ghostty-vt.wasm
```
Note: we rename `ghostty-web.umd.cjs` to `ghostty-web.js` to match the project's `.js` extension convention for vendor files.
**Step 2: If ghostty-web has CSS, copy it**
Check if there's a CSS file in the package:
```bash
find /tmp/ghostty-web-research/package -name "*.css" -type f
```
If found, copy to `muxplex/frontend/vendor/ghostty-web.css`.
**Step 3: Verify the WASM file is served correctly**
The Python FastAPI app serves `muxplex/frontend/` as static files. WASM files need the correct MIME type (`application/wasm`). Check if FastAPI/Starlette handles this:
```bash
grep -r "wasm\|mime\|StaticFiles\|static" muxplex/main.py | head -20
```
If the static file server doesn't serve `.wasm` with the correct MIME type, we may need to add a MIME type mapping. Most modern versions of Starlette handle this automatically.
**Step 4: Verify vendor files are present**
Run:
```bash
ls -la muxplex/frontend/vendor/ghostty-web.js
ls -la muxplex/frontend/vendor/ghostty-vt.wasm
wc -c muxplex/frontend/vendor/ghostty-web.js muxplex/frontend/vendor/ghostty-vt.wasm
```
Expected: `ghostty-web.js` exists, `ghostty-vt.wasm` exists (~404KB).
**Step 5: Commit**
```bash
git add muxplex/frontend/vendor/ghostty-web.js muxplex/frontend/vendor/ghostty-vt.wasm
# Add CSS too if it exists:
# git add muxplex/frontend/vendor/ghostty-web.css
git commit -m "vendor: add ghostty-web UMD build and WASM"
```
---
### Task 3: Update index.html script tags
**Files:**
- Modify: `muxplex/frontend/index.html`
**Depends on:** Task 2 (vendor files must exist), Task 1 (know the CSS situation)
**Step 1: Write the failing test**
Add a test to `muxplex/tests/test_frontend_js.py` that verifies the new script tags:
```python
# At the top of the file, add:
INDEX_HTML_PATH = pathlib.Path(__file__).parent.parent / "frontend" / "index.html"
_INDEX_HTML: str = INDEX_HTML_PATH.read_text()
# Add these tests at the end of the file:
# -- ghostty-web migration (Phase 1) --
def test_index_html_loads_ghostty_web_js() -> None:
"""index.html must load ghostty-web.js vendor script."""
assert "ghostty-web.js" in _INDEX_HTML, (
"index.html must include a script tag for vendor/ghostty-web.js"
)
def test_index_html_no_xterm_js_script() -> None:
"""index.html must not load xterm.js (replaced by ghostty-web)."""
assert 'src="/vendor/xterm.js"' not in _INDEX_HTML, (
"index.html must not load xterm.js — replaced by ghostty-web"
)
def test_index_html_no_xterm_addon_fit_script() -> None:
"""index.html must not load xterm-addon-fit.js (FitAddon is built into ghostty-web)."""
assert "xterm-addon-fit.js" not in _INDEX_HTML, (
"index.html must not load xterm-addon-fit.js — FitAddon is built into ghostty-web"
)
```
**Step 2: Run tests to verify they fail**
Run:
```bash
.venv/bin/python -m pytest muxplex/tests/test_frontend_js.py::test_index_html_loads_ghostty_web_js -xvs
```
Expected: FAIL — ghostty-web.js not found in index.html yet.
**Step 3: Update index.html**
In `muxplex/frontend/index.html`, replace the xterm.js script tags (lines 272-276):
**Before:**
```html
<script src="/vendor/xterm.js"></script>
<script src="/vendor/xterm-addon-fit.js"></script>
<script src="/vendor/xterm-addon-web-links.js"></script>
<script src="/vendor/xterm-addon-search.js"></script>
<script src="/vendor/addon-image.js"></script>
```
**After:**
```html
<script src="/vendor/ghostty-web.js"></script>
<script src="/vendor/xterm-addon-web-links.js"></script>
<script src="/vendor/xterm-addon-search.js"></script>
<script src="/vendor/addon-image.js"></script>
```
Note: We keep WebLinksAddon, SearchAddon, and ImageAddon for now. They use the xterm.js addon interface (`loadAddon`). Task 1 research will determine if they're compatible. If not, Task 4 will handle fallbacks. FitAddon is dropped because ghostty-web bundles it natively.
Also update the CSS link if ghostty-web has its own CSS. If ghostty-web does NOT have a CSS file (it uses canvas rendering, not DOM), remove the xterm.css link:
**Before:**
```html
<link rel="stylesheet" href="/vendor/xterm.css" />
```
**After (if ghostty-web has CSS):**
```html
<link rel="stylesheet" href="/vendor/ghostty-web.css" />
```
**After (if ghostty-web has NO CSS — canvas renderer needs no CSS):**
```html
<!-- ghostty-web uses canvas rendering, no CSS needed -->
```
**Step 4: Run tests to verify they pass**
Run:
```bash
.venv/bin/python -m pytest muxplex/tests/test_frontend_js.py -k "ghostty" -xvs
```
Expected: PASS
**Step 5: Commit**
```bash
git add muxplex/frontend/index.html muxplex/tests/test_frontend_js.py
git commit -m "feat: update index.html to load ghostty-web instead of xterm.js"
```
---
### Task 4: Update terminal.js for ghostty-web API
**Files:**
- Modify: `muxplex/frontend/terminal.js`
**Depends on:** Task 1 (research results), Task 3 (script tags updated)
**Context:** ghostty-web is API-compatible with xterm.js, but has two key differences:
1. Requires `await init()` before creating Terminal — loads WASM asynchronously
2. FitAddon is built-in (import path differs)
3. The global name exposed by the UMD build may differ from `window.Terminal`
The existing code uses:
- `new window.Terminal({...})` (line 349)
- `new window.FitAddon.FitAddon()` (line 369)
- `_term.loadAddon(...)` for WebLinksAddon, SearchAddon, ImageAddon (lines 387-406)
- `_term.write(string)` for terminal output
- `_term.onData(callback)` for terminal input
- `_term.onResize(callback)` for resize events
- `_term.dispose()` for cleanup
- `_term.parser.registerOscHandler()` for OSC 52 clipboard
- `_term.attachCustomKeyEventHandler()` for key interception
- `_term.getSelection()` and `_term.onSelectionChange()` for clipboard
- `_term.focus()`, `_term.cols`, `_term.rows`
**Step 1: Add WASM initialization at module load**
Add a WASM initialization block near the top of `terminal.js` (after the module-level state declarations, around line 38). ghostty-web's UMD build exposes an `init()` function that must be called once before creating any Terminal instance:
```javascript
// --- ghostty-web WASM initialization ---
// ghostty-web requires WASM to be loaded before creating Terminal instances.
// The UMD build exposes init() on the global (e.g., window.GhosttyWeb.init()).
// Call init() once at module load; createTerminal() awaits the result.
var _ghosttyReady = null; // Promise that resolves when WASM is loaded
(function _initGhosttyWasm() {
// The exact global depends on the UMD build — Task 1 research identifies this.
// Common patterns: window.GhosttyWeb.init(), window.ghosttyWeb.init()
// The init() call may accept a WASM URL: init('/vendor/ghostty-vt.wasm')
if (typeof window !== 'undefined' && window.GhosttyWeb && window.GhosttyWeb.init) {
_ghosttyReady = window.GhosttyWeb.init('/vendor/ghostty-vt.wasm');
} else if (typeof window !== 'undefined' && window.init) {
_ghosttyReady = window.init('/vendor/ghostty-vt.wasm');
} else {
_ghosttyReady = Promise.resolve(); // Fallback for test environments
}
})();
```
**Important:** The exact global name and init() signature depend on Task 1 research findings. Adjust the code above based on what the UMD build actually exposes.
**Step 2: Update `createTerminal()` — Terminal constructor**
In the `createTerminal()` function (line 335), update the Terminal constructor reference. ghostty-web's UMD build may expose the Terminal class under a different global:
**Before (line 349):**
```javascript
_term = new window.Terminal({
```
**After (adjust global name based on Task 1 research):**
```javascript
// ghostty-web UMD exposes Terminal on the same global or on GhosttyWeb
var TerminalClass = window.GhosttyWeb ? window.GhosttyWeb.Terminal : window.Terminal;
_term = new TerminalClass({
```
**Step 3: Update FitAddon instantiation**
ghostty-web bundles FitAddon. The import path changes:
**Before (line 369):**
```javascript
_fitAddon = new window.FitAddon.FitAddon();
```
**After (adjust based on Task 1 research — FitAddon may be on GhosttyWeb or still on window.FitAddon):**
```javascript
var FitAddonClass = (window.GhosttyWeb && window.GhosttyWeb.FitAddon)
? window.GhosttyWeb.FitAddon
: (window.FitAddon && window.FitAddon.FitAddon);
_fitAddon = new FitAddonClass();
```
**Step 4: Guard addon loading for compatibility**
The WebLinksAddon, SearchAddon, and ImageAddon use the xterm.js addon interface. If they fail to load with ghostty-web, wrap them in try/catch so the terminal still works:
**Before (lines 387-406):**
```javascript
var WebLinksAddon = window.WebLinksAddon && window.WebLinksAddon.WebLinksAddon;
if (WebLinksAddon) {
_term.loadAddon(new WebLinksAddon(function(event, uri) {
window.open(uri, '_blank');
}));
}
var SearchAddon = window.SearchAddon && window.SearchAddon.SearchAddon;
if (SearchAddon) {
_searchAddon = new SearchAddon();
_term.loadAddon(_searchAddon);
}
var ImageAddon = window.ImageAddon && window.ImageAddon.ImageAddon;
if (ImageAddon) {
_term.loadAddon(new ImageAddon());
}
```
**After:**
```javascript
// WebLinksAddon — try loading xterm.js addon; ghostty-web may or may not support it
try {
var WebLinksAddon = window.WebLinksAddon && window.WebLinksAddon.WebLinksAddon;
if (WebLinksAddon) {
_term.loadAddon(new WebLinksAddon(function(event, uri) {
window.open(uri, '_blank');
}));
}
} catch (e) {
console.warn('WebLinksAddon not compatible with ghostty-web:', e.message);
}
// SearchAddon — try loading xterm.js addon
try {
var SearchAddon = window.SearchAddon && window.SearchAddon.SearchAddon;
if (SearchAddon) {
_searchAddon = new SearchAddon();
_term.loadAddon(_searchAddon);
}
} catch (e) {
console.warn('SearchAddon not compatible with ghostty-web:', e.message);
_searchAddon = null;
}
// ImageAddon — try loading xterm.js addon (Sixel, iTerm2 IIP, Kitty graphics)
try {
var ImageAddon = window.ImageAddon && window.ImageAddon.ImageAddon;
if (ImageAddon) {
_term.loadAddon(new ImageAddon());
}
} catch (e) {
console.warn('ImageAddon not compatible with ghostty-web:', e.message);
}
```
**Step 5: Handle the async init() in openTerminal()**
The `openTerminal()` function (line 453) and `switchTerminal()` function (line 642) call `createTerminal()` synchronously. Since ghostty-web requires WASM to be loaded first, ensure `_ghosttyReady` is resolved before terminal creation. The simplest approach: `init()` fires at page load (module execution), so by the time the user clicks a session tile, WASM is loaded. But add a guard:
In `openTerminal()`, before `createTerminal(fontSize)` (line 479), add:
```javascript
// Ensure ghostty-web WASM is loaded before creating terminal
if (_ghosttyReady && typeof _ghosttyReady.then === 'function') {
_ghosttyReady.then(function() {
_ghosttyReady = null; // Only wait once
});
}
```
Note: Since `init()` is called at module load and terminal opening happens after user interaction (seconds later), the WASM will already be loaded. This guard is defensive only.
**Step 6: Verify the terminal.js changes don't break the existing test suite**
Run:
```bash
.venv/bin/python -m pytest muxplex/tests/test_frontend_js.py -xvs
```
Expected: All existing tests PASS (they test app.js structure, not terminal.js internals).
**Step 7: Commit**
```bash
git add muxplex/frontend/terminal.js
git commit -m "feat: update terminal.js for ghostty-web API (FitAddon built-in, addon guards)"
```
---
### Task 5: Remove old xterm.js vendor files
**Files:**
- Delete: `muxplex/frontend/vendor/xterm.js`
- Delete: `muxplex/frontend/vendor/xterm.css`
- Delete: `muxplex/frontend/vendor/xterm-addon-fit.js`
**Depends on:** Task 3, Task 4 (new vendor files are loaded, terminal.js updated)
**Important:** Do NOT delete `xterm-addon-web-links.js`, `xterm-addon-search.js`, or `addon-image.js` yet. These addons are still loaded (with try/catch guards) and may work with ghostty-web's `loadAddon()` interface. They'll be removed later once ghostty-web has native equivalents or Phase 2 eliminates the need.
**Step 1: Delete the replaced files**
```bash
rm muxplex/frontend/vendor/xterm.js
rm muxplex/frontend/vendor/xterm.css
rm muxplex/frontend/vendor/xterm-addon-fit.js
```
**Step 2: Verify no remaining references to deleted files**
```bash
grep -r "xterm\.js" muxplex/frontend/ --include="*.html" --include="*.js"
grep -r "xterm\.css" muxplex/frontend/ --include="*.html"
grep -r "xterm-addon-fit" muxplex/frontend/ --include="*.html" --include="*.js"
```
Expected: No references to `xterm.js`, `xterm.css`, or `xterm-addon-fit.js` in HTML or JS files. (Comments referencing "xterm.js" in code are fine — they're documentation, not imports.)
**Step 3: Verify remaining vendor files**
```bash
ls -la muxplex/frontend/vendor/
```
Expected contents:
- `ghostty-web.js` (new)
- `ghostty-vt.wasm` (new)
- `ghostty-web.css` (new, if applicable)
- `xterm-addon-web-links.js` (kept)
- `xterm-addon-search.js` (kept)
- `addon-image.js` (kept)
- `lit/` directory (unrelated, kept)
**Step 4: Commit**
```bash
git add -u muxplex/frontend/vendor/
git commit -m "chore: remove xterm.js, xterm.css, xterm-addon-fit.js (replaced by ghostty-web)"
```
---
### Task 6: Manual smoke test
**Files:** None (testing only)
**Depends on:** Tasks 2-5 complete
**Step 1: Restart the service**
```bash
systemctl --user restart muxplex
```
**Step 2: Test terminal rendering**
Open the muxplex web UI in a browser. Click a session tile to open a terminal. Verify:
- [ ] Terminal renders and shows a shell prompt
- [ ] Typing works (keystrokes reach the shell)
- [ ] Colors render correctly (run `ls --color` or `htop`)
- [ ] Terminal resizes when the browser window resizes (FitAddon working)
- [ ] Scrollback works (scroll up to see history)
**Step 3: Test search (Ctrl+F)**
Press Ctrl+F in the terminal. If the search bar appears and search works, SearchAddon loaded successfully. If not, note this as a known limitation.
**Step 4: Test links**
Run `echo "https://example.com"` in the terminal. Hover over the URL. If it becomes clickable, WebLinksAddon loaded successfully. Note: ghostty-web may handle OSC 8 hyperlinks natively even without WebLinksAddon.
**Step 5: Test session switching**
Open one session, then click a different session in the sidebar. Verify the terminal switches and renders the new session content.
**Step 6: Document any issues**
If anything doesn't work, note it. Common issues to watch for:
- WASM not loading: check browser console for 404 on `ghostty-vt.wasm` or MIME type errors
- Terminal blank: check if `init()` resolved before Terminal creation
- Addons failing: check console for the try/catch warning messages
- CSS issues: ghostty-web uses canvas rendering so xterm.css classes like `.xterm` may not exist
---
### Task 7: Run the full test suite
**Files:** None (testing only)
**Depends on:** Task 6 (smoke test passed or issues documented)
**Step 1: Run all tests**
Run:
```bash
.venv/bin/python -m pytest muxplex/tests/ -x -q --timeout=30
```
Expected: All 1306 tests pass. The frontend structural tests (`test_frontend_js.py`) verify app.js structure — they don't execute JavaScript, so they're unaffected by the terminal swap. The new tests from Task 3 verify index.html loads ghostty-web.
**Step 2: If tests fail, fix before proceeding**
Most likely failure: a test that greps for "xterm" in terminal.js comments. These are documentation tests (the test file checks for patterns in JS source). If a test assumes `xterm.js` is present in terminal.js comments, update the comment to reference ghostty-web instead.
Check specifically:
```bash
grep -n "xterm" muxplex/tests/test_frontend_js.py | head -20
```
If any tests reference xterm.js presence in terminal.js, update them.
---
### Task 8: Final commit
**Files:** Any remaining uncommitted changes
**Depends on:** Task 7 (all tests pass)
**Step 1: Check for uncommitted changes**
```bash
git status
git diff --stat
```
**Step 2: Commit any remaining changes**
```bash
git add -A
git commit -m "feat: Phase 1 complete — ghostty-web replaces xterm.js as terminal frontend"
```
**Step 3: Verify the commit history**
```bash
git log --oneline -10
```
Expected: Clean commit history showing the incremental swap:
1. Research doc
2. Vendor ghostty-web files
3. Update index.html script tags
4. Update terminal.js for API differences
5. Remove old xterm.js files
6. Final commit (if any remaining changes)
File diff suppressed because it is too large Load Diff