Files
muxplex/docs/superpowers/plans/2026-05-28-phase1-ghostty-web-swap.md
T

596 lines
21 KiB
Markdown

# 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)