Skip to content

Commit de84bba

Browse files
feat(web): vendor xterm.js 6.0.0 + fit/webgl addons (§1.6)
Drops the latest-stable @xterm/xterm UMD bundle, @xterm/addon-fit, @xterm/addon-webgl, and xterm.css into web/static/vendor/ and loads them from index.html. No Alpine component yet — that's §1.7 and ships in its own PR so the frontend iteration can proceed alone. Why: unblocks §1.7's agentTerminal() component. With the UMD globals (Terminal, FitAddon, WebglAddon) available at DOMContentLoaded time, the component can mount into a <div> and pipe the /api/session/ws stream directly — no ESM, no build step, no service worker rewrites. Files vendored (versions pinned in index.html): - xterm-6.0.0.js (477 KB, UMD) - xterm-6.0.0.css (7 KB) - xterm-addon-fit-0.11.0.js (1.5 KB) - xterm-addon-webgl-0.19.0.js (242 KB) Load order matters: xterm-6.0.0.js defers first, then the two addons defer after it. test_xterm_umd_bundles_load_in_correct_order locks this so a future reorder can't silently break addon init. Plus Amendment #6 to the refactor plan documenting §1.5b + §1.6 landing and the §1.7 frontend contract (ws_url from /session/start is the exact string to feed new WebSocket). Tests: +7 smoke tests (test_web_vendor_xterm.py) — file presence, size sanity (>1KB JS, >500B CSS), index.html references, and script load order. 1992 passed total (1985 → 1992). Ruff + format clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 17e9964 commit de84bba

6 files changed

Lines changed: 374 additions & 0 deletions

File tree

packages/studyloop/src/studyloop/web/static/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@
1010
<link rel="stylesheet" href="/style.css">
1111
<link rel="manifest" href="/manifest.json">
1212
<link rel="icon" href="/icon-192.svg" type="image/svg+xml">
13+
<!-- xterm.js base stylesheet (loaded but not wired until §1.7 Alpine component) -->
14+
<link rel="stylesheet" href="/vendor/css/xterm-6.0.0.css">
1315
<style>[x-cloak] { display: none !important; }</style>
1416
<!-- HTMX + SSE extension (vendored for offline PWA) -->
1517
<script src="/vendor/js/htmx-2.0.4.min.js"></script>
1618
<script src="/vendor/js/htmx-ext-sse-2.2.2.js"></script>
1719
<!-- Alpine.js (vendored for offline PWA) -->
1820
<script defer src="/vendor/js/alpine-3.14.8.min.js"></script>
21+
<!-- xterm.js + fit + webgl addons (vendored; wired in §1.7). UMD globals:
22+
Terminal on window.Terminal, FitAddon on window.FitAddon, WebglAddon
23+
on window.WebglAddon.addons.webgl (UMD exports are namespaced). -->
24+
<script defer src="/vendor/js/xterm-6.0.0.js"></script>
25+
<script defer src="/vendor/js/xterm-addon-fit-0.11.0.js"></script>
26+
<script defer src="/vendor/js/xterm-addon-webgl-0.19.0.js"></script>
1927
</head>
2028
<body x-data x-init="$store.nav.init()">
2129
<header>
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
3+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4+
* https://github.com/chjj/term.js
5+
* @license MIT
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*
25+
* Originally forked from (with the author's permission):
26+
* Fabrice Bellard's javascript vt100 for jslinux:
27+
* http://bellard.org/jslinux/
28+
* Copyright (c) 2011 Fabrice Bellard
29+
* The original design remains. The terminal itself
30+
* has been extended to include xterm CSI codes, among
31+
* other features.
32+
*/
33+
34+
/**
35+
* Default styles for xterm.js
36+
*/
37+
38+
.xterm {
39+
cursor: text;
40+
position: relative;
41+
user-select: none;
42+
-ms-user-select: none;
43+
-webkit-user-select: none;
44+
}
45+
46+
.xterm.focus,
47+
.xterm:focus {
48+
outline: none;
49+
}
50+
51+
.xterm .xterm-helpers {
52+
position: absolute;
53+
top: 0;
54+
/**
55+
* The z-index of the helpers must be higher than the canvases in order for
56+
* IMEs to appear on top.
57+
*/
58+
z-index: 5;
59+
}
60+
61+
.xterm .xterm-helper-textarea {
62+
padding: 0;
63+
border: 0;
64+
margin: 0;
65+
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
66+
position: absolute;
67+
opacity: 0;
68+
left: -9999em;
69+
top: 0;
70+
width: 0;
71+
height: 0;
72+
z-index: -5;
73+
/** Prevent wrapping so the IME appears against the textarea at the correct position */
74+
white-space: nowrap;
75+
overflow: hidden;
76+
resize: none;
77+
}
78+
79+
.xterm .composition-view {
80+
/* TODO: Composition position got messed up somewhere */
81+
background: #000;
82+
color: #FFF;
83+
display: none;
84+
position: absolute;
85+
white-space: nowrap;
86+
z-index: 1;
87+
}
88+
89+
.xterm .composition-view.active {
90+
display: block;
91+
}
92+
93+
.xterm .xterm-viewport {
94+
/* On OS X this is required in order for the scroll bar to appear fully opaque */
95+
background-color: #000;
96+
overflow-y: scroll;
97+
cursor: default;
98+
position: absolute;
99+
right: 0;
100+
left: 0;
101+
top: 0;
102+
bottom: 0;
103+
}
104+
105+
.xterm .xterm-screen {
106+
position: relative;
107+
}
108+
109+
.xterm .xterm-screen canvas {
110+
position: absolute;
111+
left: 0;
112+
top: 0;
113+
}
114+
115+
.xterm-char-measure-element {
116+
display: inline-block;
117+
visibility: hidden;
118+
position: absolute;
119+
top: 0;
120+
left: -9999em;
121+
line-height: normal;
122+
}
123+
124+
.xterm.enable-mouse-events {
125+
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
126+
cursor: default;
127+
}
128+
129+
.xterm.xterm-cursor-pointer,
130+
.xterm .xterm-cursor-pointer {
131+
cursor: pointer;
132+
}
133+
134+
.xterm.column-select.focus {
135+
/* Column selection mode */
136+
cursor: crosshair;
137+
}
138+
139+
.xterm .xterm-accessibility:not(.debug),
140+
.xterm .xterm-message {
141+
position: absolute;
142+
left: 0;
143+
top: 0;
144+
bottom: 0;
145+
right: 0;
146+
z-index: 10;
147+
color: transparent;
148+
pointer-events: none;
149+
}
150+
151+
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
152+
color: transparent;
153+
}
154+
155+
.xterm .xterm-accessibility-tree {
156+
font-family: monospace;
157+
user-select: text;
158+
white-space: pre;
159+
}
160+
161+
.xterm .xterm-accessibility-tree > div {
162+
transform-origin: left;
163+
width: fit-content;
164+
}
165+
166+
.xterm .live-region {
167+
position: absolute;
168+
left: -9999px;
169+
width: 1px;
170+
height: 1px;
171+
overflow: hidden;
172+
}
173+
174+
.xterm-dim {
175+
/* Dim should not apply to background, so the opacity of the foreground color is applied
176+
* explicitly in the generated class and reset to 1 here */
177+
opacity: 1 !important;
178+
}
179+
180+
.xterm-underline-1 { text-decoration: underline; }
181+
.xterm-underline-2 { text-decoration: double underline; }
182+
.xterm-underline-3 { text-decoration: wavy underline; }
183+
.xterm-underline-4 { text-decoration: dotted underline; }
184+
.xterm-underline-5 { text-decoration: dashed underline; }
185+
186+
.xterm-overline {
187+
text-decoration: overline;
188+
}
189+
190+
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
191+
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
192+
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
193+
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
194+
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
195+
196+
.xterm-strikethrough {
197+
text-decoration: line-through;
198+
}
199+
200+
.xterm-screen .xterm-decoration-container .xterm-decoration {
201+
z-index: 6;
202+
position: absolute;
203+
}
204+
205+
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
206+
z-index: 7;
207+
}
208+
209+
.xterm-decoration-overview-ruler {
210+
z-index: 8;
211+
position: absolute;
212+
top: 0;
213+
right: 0;
214+
pointer-events: none;
215+
}
216+
217+
.xterm-decoration-top {
218+
z-index: 2;
219+
position: relative;
220+
}
221+
222+
223+
224+
/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
225+
226+
/* xterm.js customization: Override xterm's cursor style */
227+
.xterm .xterm-scrollable-element > .scrollbar {
228+
cursor: default;
229+
}
230+
231+
/* Arrows */
232+
.xterm .xterm-scrollable-element > .scrollbar > .scra {
233+
cursor: pointer;
234+
font-size: 11px !important;
235+
}
236+
237+
.xterm .xterm-scrollable-element > .visible {
238+
opacity: 1;
239+
240+
/* Background rule added for IE9 - to allow clicks on dom node */
241+
background:rgba(0,0,0,0);
242+
243+
transition: opacity 100ms linear;
244+
/* In front of peek view */
245+
z-index: 11;
246+
}
247+
.xterm .xterm-scrollable-element > .invisible {
248+
opacity: 0;
249+
pointer-events: none;
250+
}
251+
.xterm .xterm-scrollable-element > .invisible.fade {
252+
transition: opacity 800ms linear;
253+
}
254+
255+
/* Scrollable Content Inset Shadow */
256+
.xterm .xterm-scrollable-element > .shadow {
257+
position: absolute;
258+
display: none;
259+
}
260+
.xterm .xterm-scrollable-element > .shadow.top {
261+
display: block;
262+
top: 0;
263+
left: 3px;
264+
height: 3px;
265+
width: 100%;
266+
box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
267+
}
268+
.xterm .xterm-scrollable-element > .shadow.left {
269+
display: block;
270+
top: 3px;
271+
left: 0;
272+
height: 100%;
273+
width: 3px;
274+
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
275+
}
276+
.xterm .xterm-scrollable-element > .shadow.top-left-corner {
277+
display: block;
278+
top: 0;
279+
left: 0;
280+
height: 3px;
281+
width: 3px;
282+
}
283+
.xterm .xterm-scrollable-element > .shadow.top.left {
284+
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
285+
}

packages/studyloop/src/studyloop/web/static/vendor/js/xterm-6.0.0.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/studyloop/src/studyloop/web/static/vendor/js/xterm-addon-fit-0.11.0.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/studyloop/src/studyloop/web/static/vendor/js/xterm-addon-webgl-0.19.0.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Smoke tests for the vendored xterm.js assets (§1.6).
2+
3+
Asserts that xterm.js + fit/webgl addons + xterm.css exist on disk and
4+
are referenced from ``web/static/index.html``. Catches the regression
5+
where one of the vendor files is deleted or the index tag is dropped
6+
during a refactor — cheap to assert, would otherwise only surface as
7+
a blank terminal pane in the browser.
8+
9+
This test does NOT exercise the terminal itself (that's §1.7 with the
10+
Alpine component). It's a pure file-existence + reference check.
11+
12+
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md §1.6
13+
"""
14+
15+
from __future__ import annotations
16+
17+
from pathlib import Path
18+
19+
import pytest
20+
21+
STATIC_DIR = Path(__file__).resolve().parents[1] / "src" / "studyloop" / "web" / "static"
22+
23+
VENDOR_JS = STATIC_DIR / "vendor" / "js"
24+
VENDOR_CSS = STATIC_DIR / "vendor" / "css"
25+
INDEX_HTML = STATIC_DIR / "index.html"
26+
27+
# Keep these in sync with the versions pinned in index.html. Bumping a
28+
# vendored file is a two-line change: drop the new asset, update the
29+
# version here.
30+
EXPECTED_JS = {
31+
"xterm-6.0.0.js",
32+
"xterm-addon-fit-0.11.0.js",
33+
"xterm-addon-webgl-0.19.0.js",
34+
}
35+
EXPECTED_CSS = {"xterm-6.0.0.css"}
36+
37+
38+
class TestVendorFilesExist:
39+
@pytest.mark.parametrize("filename", sorted(EXPECTED_JS))
40+
def test_js_file_exists(self, filename: str) -> None:
41+
path = VENDOR_JS / filename
42+
assert path.exists(), f"Missing vendored JS asset: {path}"
43+
# UMD bundles should be non-trivial in size. If one shrinks to
44+
# zero bytes it's usually a broken download that slipped through.
45+
assert path.stat().st_size > 1000, f"Suspiciously small asset: {path}"
46+
47+
@pytest.mark.parametrize("filename", sorted(EXPECTED_CSS))
48+
def test_css_file_exists(self, filename: str) -> None:
49+
path = VENDOR_CSS / filename
50+
assert path.exists(), f"Missing vendored CSS asset: {path}"
51+
assert path.stat().st_size > 500, f"Suspiciously small asset: {path}"
52+
53+
54+
class TestIndexReferencesVendor:
55+
def test_index_references_all_vendor_assets(self) -> None:
56+
"""Every expected vendor filename must appear verbatim in index.html."""
57+
html = INDEX_HTML.read_text(encoding="utf-8")
58+
for name in EXPECTED_JS | EXPECTED_CSS:
59+
assert name in html, f"index.html does not reference {name}"
60+
61+
def test_index_has_xterm_css_link_tag(self) -> None:
62+
html = INDEX_HTML.read_text(encoding="utf-8")
63+
assert "/vendor/css/xterm-6.0.0.css" in html
64+
assert "<link" in html.split("/vendor/css/xterm-6.0.0.css")[0].splitlines()[-1]
65+
66+
def test_xterm_umd_bundles_load_in_correct_order(self) -> None:
67+
"""fit and webgl addons must load AFTER xterm.js — they reference
68+
the xterm UMD globals. A transposed order would leave FitAddon
69+
unable to find the Terminal class."""
70+
html = INDEX_HTML.read_text(encoding="utf-8")
71+
xterm_pos = html.index("xterm-6.0.0.js")
72+
fit_pos = html.index("xterm-addon-fit-0.11.0.js")
73+
webgl_pos = html.index("xterm-addon-webgl-0.19.0.js")
74+
assert xterm_pos < fit_pos
75+
assert xterm_pos < webgl_pos

0 commit comments

Comments
 (0)