Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Install into any protoAgent agent from this git URL — it's not tied to one age
- **protoAgent theming** — the xterm theme (background/foreground/cursor/selection +
the 16 ANSI colours) is built from the console's `--pl-*` tokens on the DS-kit
handshake and re-applied on every live re-theme.
- **Crisp block art** — the canvas renderer with `customGlyphs` draws block/box-drawing
glyphs as exact cell-filling shapes, so contiguous block art (e.g. the Claude Code
splash) renders **flush, no seams** (the default DOM renderer draws them from the
font, which leaves gaps).

## Security — read this before enabling

Expand Down
1 change: 1 addition & 0 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"xterm.css": "text/css",
"addon-fit.js": "application/javascript",
"addon-web-links.js": "application/javascript",
"addon-canvas.js": "application/javascript",
}


Expand Down
2 changes: 1 addition & 1 deletion protoagent.plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: terminal
name: Terminal
version: 0.4.0
version: 0.4.1
description: >-
A full terminal in the protoAgent console — an xterm.js view wired to a real PTY
shell over a WebSocket. The terminal is themed from the protoAgent design system
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "terminal-plugin"
version = "0.4.0" # keep in lockstep with protoagent.plugin.yaml (a test enforces it)
version = "0.4.1" # keep in lockstep with protoagent.plugin.yaml (a test enforces it)
description = "A full terminal (xterm.js + a real PTY over WebSocket) as a protoAgent console plugin."
requires-python = ">=3.11"

Expand Down
1 change: 1 addition & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def test_vendored_assets_served_locally():
assert js.status_code == 200 and "javascript" in js.headers["content-type"]
assert c.get("/plugins/terminal/static/xterm.css").status_code == 200
assert c.get("/plugins/terminal/static/addon-fit.js").status_code == 200
assert c.get("/plugins/terminal/static/addon-canvas.js").status_code == 200
# whitelist only — an unknown name is 404 (no traversal)
assert c.get("/plugins/terminal/static/secret.py").status_code == 404

Expand Down
2 changes: 2 additions & 0 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def test_view_page_pulls_in_the_protoagent_theme_and_four_rules():
# the terminal itself: VENDORED xterm (offline — no CDN) + the WS to the PTY bridge.
assert "/plugins/terminal/static/" in PAGE and "xterm.js" in PAGE and "new Terminal(" in PAGE
assert "cdn.jsdelivr" not in PAGE and "https://" not in PAGE # fully self-served, no CDN
# the CANVAS renderer + customGlyphs is what makes block/box art render flush.
assert "CanvasAddon" in PAGE and "customGlyphs" in PAGE
assert "/plugins/terminal/ws?token=" in PAGE
# THE theme requirement: xterm's theme is built from protoAgent's --pl-* tokens,
# and re-applied live on a re-theme (MutationObserver on :root).
Expand Down
2 changes: 2 additions & 0 deletions vendor/addon-canvas.js

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions view.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,18 @@
});
}
const ST = BASE + "/plugins/terminal/static/";
let Terminal, FitAddon, WebLinksAddon;
let Terminal, FitAddon, WebLinksAddon, CanvasAddon;
try {
await loadScript(ST + "xterm.js");
await Promise.all([loadScript(ST + "addon-fit.js"), loadScript(ST + "addon-web-links.js")]);
await Promise.all([
loadScript(ST + "addon-fit.js"),
loadScript(ST + "addon-web-links.js"),
loadScript(ST + "addon-canvas.js"),
]);
Terminal = window.Terminal;
FitAddon = window.FitAddon.FitAddon;
WebLinksAddon = window.WebLinksAddon.WebLinksAddon;
CanvasAddon = window.CanvasAddon.CanvasAddon;
} catch (e) {
document.getElementById("err").hidden = false;
document.getElementById("err").textContent = "Could not load the terminal assets. " + e;
Expand Down Expand Up @@ -179,14 +184,26 @@
s.ws.onerror = () => { s.status = "error"; s.statusCls = "bad"; if (s.id === activeId) setStatus("error", "bad"); };
}

// Resolve the mono font CONCRETELY: the canvas renderer builds a canvas font string,
// which can't resolve a CSS var() — so read --pl-font-mono now and append fallbacks.
const monoVar = css("--pl-font-mono", "").replace(/['"]/g, "").trim();
const MONO = (monoVar ? monoVar + ", " : "") + "Menlo, Monaco, 'Courier New', monospace";

function newSession(){
const id = "t" + (++counter);
const el = document.createElement("div"); el.className = "termpane"; el.dataset.id = id; $("terms").appendChild(el);
const term = new Terminal({
cursorBlink: true, fontSize: 13, scrollback: 5000, allowProposedApi: true,
fontFamily: "var(--pl-font-mono), Menlo, Monaco, 'Courier New', monospace", theme: xtermTheme(),
fontFamily: MONO,
lineHeight: 1.0, // flush rows
customGlyphs: true, // draw block/box glyphs as exact cell-filling shapes (needs canvas/webgl)
theme: xtermTheme(),
});
const fitA = new FitAddon(); term.loadAddon(fitA); term.loadAddon(new WebLinksAddon()); term.open(el);
// The CANVAS renderer is what makes customGlyphs work — block/box art renders flush
// (the default DOM renderer draws them from the font, which leaves seams). Best-effort:
// a renderer failure just falls back to the DOM renderer.
try { term.loadAddon(new CanvasAddon()); } catch (e) {}
const s = { id, name: "Terminal " + counter, term, fit: fitA, ws: null, el, status: "connecting…", statusCls: "", exited: false, meta: "" };
term.onData((d) => { if (s.ws && s.ws.readyState === 1) s.ws.send(JSON.stringify({ type: "input", data: d })); });
sessions.set(id, s);
Expand Down
Loading