21 KiB
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:
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:
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:
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:
// 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.cssor has its own styles? - Any API surface differences found (e.g.,
term.write()accepts Uint8Array? string only?)
Step 6: Commit
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:
# 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:
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:
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:
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
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:
# 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:
.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:
<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:
<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:
<link rel="stylesheet" href="/vendor/xterm.css" />
After (if ghostty-web has CSS):
<link rel="stylesheet" href="/vendor/ghostty-web.css" />
After (if ghostty-web has NO CSS — canvas renderer needs no CSS):
<!-- ghostty-web uses canvas rendering, no CSS needed -->
Step 4: Run tests to verify they pass
Run:
.venv/bin/python -m pytest muxplex/tests/test_frontend_js.py -k "ghostty" -xvs
Expected: PASS
Step 5: Commit
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:
- Requires
await init()before creating Terminal — loads WASM asynchronously - FitAddon is built-in (import path differs)
- 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:
// --- 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):
_term = new window.Terminal({
After (adjust global name based on Task 1 research):
// 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):
_fitAddon = new window.FitAddon.FitAddon();
After (adjust based on Task 1 research — FitAddon may be on GhosttyWeb or still on window.FitAddon):
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):
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:
// 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:
// 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:
.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
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
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
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
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
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
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 --colororhtop) - 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.wasmor 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
.xtermmay 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:
.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:
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
git status
git diff --stat
Step 2: Commit any remaining changes
git add -A
git commit -m "feat: Phase 1 complete — ghostty-web replaces xterm.js as terminal frontend"
Step 3: Verify the commit history
git log --oneline -10
Expected: Clean commit history showing the incremental swap:
- Research doc
- Vendor ghostty-web files
- Update index.html script tags
- Update terminal.js for API differences
- Remove old xterm.js files
- Final commit (if any remaining changes)