6f624b8565
- Add package.json with @xterm/xterm 6.0 and all addons - Create terminal.ts client with WebSocket protocol support - Bundle with Bun (bun run build -> terminal.js) - Remove textual-serve dependency from pyproject.toml - Remove canvas monkey-patch workaround (no longer needed) - Add scrollback support (configurable via data-scrollback) - Update static file routing to serve from /static/ - Add Makefile targets: bundle, bundle-watch, bundle-clean - Update tests for new static path structure Benefits: - Full control over xterm.js configuration - Scrollback history now works (default 1000 lines) - Custom font family without workarounds - Smaller footprint (no unused Roboto Mono fonts) - Latest xterm.js 6.0 features available
11 KiB
11 KiB
Roadmap: Migration to xterm.js 6.0 with Bun
This document outlines the plan for bundling xterm.js 6.0 directly, replacing the dependency on textual-serve's bundled textual.js.
Current State Analysis
What textual-serve Provides
| Asset | Size | What We Use | Required? |
|---|---|---|---|
static/js/textual.js |
502 KB | xterm.js + WebSocket client | Yes |
static/css/xterm.css |
4.6 KB | Terminal styling | Yes |
static/fonts/RobotoMono*.ttf |
381 KB | Roboto Mono font | No (we override font) |
static/images/background.png |
58 KB | Background image | No |
| Total | 948 KB |
What textual.js Bundle Contains
The minified textual.js bundles:
xterm.js (core terminal)
├── @xterm/addon-fit (auto-resize to container)
├── @xterm/addon-webgl (GPU-accelerated rendering)
├── @xterm/addon-canvas (fallback 2D canvas renderer)
├── @xterm/addon-unicode11 (wide character support)
├── @xterm/addon-web-links (clickable URLs)
├── @xterm/addon-clipboard (clipboard integration)
└── WebSocket client wrapper (class w)
Hardcoded Configuration in textual.js
new Terminal({
allowProposedApi: true,
fontSize: /* from data-font-size attribute */,
scrollback: 0, // ❌ No scrollback history
fontFamily: "'Roboto Mono', Monaco, 'Courier New', monospace" // ❌ Hardcoded
})
WebSocket Protocol (Fully Compatible)
The protocol is simple JSON arrays. Our server already implements this:
| Direction | Message | Description |
|---|---|---|
| Client → Server | ["stdin", "data"] |
Terminal input |
| Client → Server | ["resize", {width: N, height: M}] |
Window resize |
| Client → Server | ["ping", data] |
Keep-alive |
| Server → Client | ["stdout", "data"] |
Terminal output (text) |
| Server → Client | Binary frame | Terminal output (binary) |
| Server → Client | ["pong", data] |
Keep-alive response |
Current Workarounds
- Font override: Canvas monkey-patch in HTML to replace hardcoded font family
- No scrollback: Users cannot scroll back through terminal history
Tradeoffs Analysis
Option A: Keep textual-serve Dependency
| Pros | Cons |
|---|---|
| Zero build tooling | Hardcoded font requires workaround |
| Automatic updates via pip | No scrollback (scrollback: 0) |
| Maintained by Textualize | No theme customization |
| Carries unused fonts/images (381 KB) | |
| Tied to textual-serve release cycle | |
| Unknown xterm.js version (likely 5.x) |
Option B: Bundle xterm.js 6.0 Directly
| Pros | Cons |
|---|---|
| Full configuration control | Requires Bun toolchain |
| Scrollback history support | ~150-200 KB bundle to maintain |
| Custom themes/colors | Must track xterm.js updates |
| Latest xterm.js 6.0 features | Initial setup effort (2-3 days) |
| Smaller bundle (no unused fonts) | |
| Can drop textual-serve dependency |
xterm.js 6.0 Features We'd Gain
| Feature | Benefit |
|---|---|
| Synchronized output (DEC 2026) | Smoother rapid output rendering |
| Ligature support | Better programming font rendering |
| Progress addon | Visual progress indicators |
| Shadow DOM support | Better CSS encapsulation |
| ESM support | Modern module loading |
| Performance improvements | Faster search, less memory |
| OSC 52 clipboard | Secure clipboard from terminal |
Implementation Plan
Phase 1: Tooling Setup
Goal: Establish Bun-based build pipeline
src/textual_webterm/
├── static/
│ ├── js/
│ │ └── terminal.ts # New: our xterm wrapper
│ ├── css/
│ │ └── xterm.css # Copied from xterm.js package
│ └── monospace.css # Existing
├── package.json # New: npm dependencies
└── bunfig.toml # New: Bun configuration
Tasks:
- Create
package.jsonwith xterm.js 6.0 dependencies - Create
bunfig.tomlfor build configuration - Add
Makefiletargets:make bundle,make bundle-watch - Add
.gitignoreentries fornode_modules/ - Document Bun installation in README
package.json (draft):
{
"name": "textual-webterm-frontend",
"private": true,
"type": "module",
"dependencies": {
"@xterm/xterm": "^6.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-clipboard": "^0.2.0"
},
"devDependencies": {
"typescript": "^5.3.0"
},
"scripts": {
"build": "bun build src/textual_webterm/static/js/terminal.ts --outdir=src/textual_webterm/static/js --minify --target=browser",
"watch": "bun build src/textual_webterm/static/js/terminal.ts --outdir=src/textual_webterm/static/js --watch --target=browser"
}
}
Phase 2: Terminal Client Implementation
Goal: Create terminal.ts that replicates textual.js functionality
Tasks:
- Implement Terminal wrapper class
- WebSocket connection with reconnection logic
- Message protocol handling (stdin, resize, ping/pong)
- Addon initialization (fit, webgl, canvas, unicode11, web-links, clipboard)
- Configurable options via data attributes or window config
terminal.ts (draft structure):
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebglAddon } from '@xterm/addon-webgl';
import { CanvasAddon } from '@xterm/addon-canvas';
import { Unicode11Addon } from '@xterm/addon-unicode11';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { ClipboardAddon } from '@xterm/addon-clipboard';
interface TerminalConfig {
fontFamily?: string;
fontSize?: number;
scrollback?: number;
theme?: object;
}
class WebTerminal {
private terminal: Terminal;
private socket: WebSocket | null = null;
private fitAddon: FitAddon;
constructor(container: HTMLElement, wsUrl: string, config: TerminalConfig = {}) {
this.terminal = new Terminal({
allowProposedApi: true,
fontFamily: config.fontFamily ?? 'ui-monospace, "Fira Code", monospace',
fontSize: config.fontSize ?? 16,
scrollback: config.scrollback ?? 1000,
theme: config.theme,
});
// Initialize addons
this.fitAddon = new FitAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(new WebglAddon());
this.terminal.loadAddon(new CanvasAddon());
this.terminal.loadAddon(new Unicode11Addon());
this.terminal.loadAddon(new WebLinksAddon());
this.terminal.loadAddon(new ClipboardAddon());
this.terminal.open(container);
this.connect(wsUrl);
}
// ... WebSocket handling, resize, etc.
}
// Auto-initialize on page load
window.addEventListener('load', () => {
document.querySelectorAll('.textual-terminal').forEach(el => {
const wsUrl = el.dataset.sessionWebsocketUrl;
const config = {
fontSize: parseInt(el.dataset.fontSize ?? '16'),
scrollback: parseInt(el.dataset.scrollback ?? '1000'),
fontFamily: el.dataset.fontFamily,
};
new WebTerminal(el as HTMLElement, wsUrl, config);
});
});
Phase 3: Server Integration
Goal: Update local_server.py to use new bundle
Tasks:
- Update HTML template to load our bundle instead of textual.js
- Remove canvas monkey-patch workaround
- Add data attributes for scrollback, theme configuration
- Copy xterm.css to our static folder (or bundle inline)
- Update static file routes
HTML template changes:
<!-- Before -->
<script src="/static/js/textual.js"></script>
<!-- After -->
<link rel="stylesheet" href="/static-webterm/xterm.css">
<script src="/static-webterm/terminal.js"></script>
Phase 4: Configuration Support
Goal: Make terminal appearance configurable
Tasks:
- Add terminal config to CLI (--scrollback, --font-family)
- Add terminal config to TOML manifest files
- Pass config to HTML template via data attributes
- Document configuration options
Config schema addition:
[terminal]
scrollback = 5000
font_family = "ui-monospace, 'Fira Code', monospace"
font_size = 16
theme = "dark" # or custom theme object
Phase 5: Remove textual-serve Dependency
Goal: Eliminate dependency once our bundle is stable
Tasks:
- Remove
textual-servefrom pyproject.toml dependencies - Update ARCHITECTURE.md to document new frontend
- Update README.md with build instructions
- Ensure Docker build includes Bun for bundling
- Add CI step to verify bundle is up-to-date
pyproject.toml change:
# Remove this line:
textual-serve = "^1.1.0"
Phase 6: Testing & Polish
Goal: Ensure reliability across browsers
Tasks:
- Cross-browser testing (Chrome, Firefox, Safari, Edge)
- Mobile browser testing (iOS Safari, Chrome Android)
- WebGL fallback to Canvas testing
- Reconnection logic testing
- Performance comparison vs textual.js
- Bundle size verification (target: <200 KB minified)
Build Integration
Makefile Additions
# Frontend build
.PHONY: bundle bundle-watch bundle-clean
bundle: node_modules
bun run build
bundle-watch: node_modules
bun run watch
bundle-clean:
rm -rf node_modules src/textual_webterm/static/js/terminal.js
node_modules: package.json
bun install
Dockerfile Changes
# Add Bun for frontend build
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
# Build frontend
COPY package.json bunfig.toml ./
RUN bun install
COPY src/textual_webterm/static/js/terminal.ts src/textual_webterm/static/js/
RUN bun run build
CI/CD Considerations
- Pre-commit hook to verify
terminal.jsmatchesterminal.ts - Or: commit built bundle to repo (simpler for users without Bun)
- GitHub Actions step to build and verify bundle
Risk Mitigation
| Risk | Mitigation |
|---|---|
| xterm.js 6.0 breaking changes | Pin exact version, test thoroughly |
| Bun compatibility issues | Fall back to esbuild if needed |
| WebSocket protocol mismatch | Keep protocol identical to textual.js |
| Performance regression | Benchmark before/after, keep WebGL |
| Missing addon features | Test each addon explicitly |
Timeline Estimate
| Phase | Effort | Dependencies |
|---|---|---|
| Phase 1: Tooling Setup | 0.5 days | None |
| Phase 2: Terminal Client | 1-2 days | Phase 1 |
| Phase 3: Server Integration | 0.5 days | Phase 2 |
| Phase 4: Configuration | 0.5 days | Phase 3 |
| Phase 5: Remove Dependency | 0.5 days | Phase 4 |
| Phase 6: Testing | 1 day | Phase 5 |
| Total | 4-5 days |
Decision Checkpoints
- After Phase 2: Verify terminal.ts works in isolation before integrating
- After Phase 3: Side-by-side comparison with textual.js
- After Phase 5: Confirm no regressions before removing dependency
- After Phase 6: Final sign-off for release
Success Criteria
- Terminal renders correctly in Chrome, Firefox, Safari
- Scrollback history works (configurable limit)
- Custom fonts load without workarounds
- WebGL rendering enabled with Canvas fallback
- Bundle size ≤ 200 KB (minified + gzipped)
- No textual-serve dependency in pyproject.toml
- All existing tests pass
- Documentation updated