castle-web-cli 0.4.64 → 0.4.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/castle-host/host.d.ts +1 -0
- package/dist/castle-host/host.js +9 -4
- package/dist/config.js +5 -0
- package/dist/ide.js +46 -650
- package/dist/index.js +20 -5
- package/dist/save-deck.d.ts +7 -1
- package/dist/save-deck.js +19 -7
- package/dist/shell/assets/index-BN8WsJWO.js +106 -0
- package/dist/shell/assets/index-CwmQcHeX.css +1 -0
- package/dist/shell/index.html +13 -0
- package/kits/basic-2d/editors/CodeEditor.jsx +2 -1
- package/package.json +3 -2
- package/dist/buildArchive.d.ts +0 -1
- package/dist/buildArchive.js +0 -108
- package/dist/chat-client.d.ts +0 -1
- package/dist/chat-client.js +0 -538
- package/dist/ide-client.d.ts +0 -1
- package/dist/ide-client.js +0 -569
package/dist/ide-client.js
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
// Browser-side terminal for the `castle-web serve --ide` panel. Plain DOM (no
|
|
2
|
-
// framework) so it can be served straight from dist with no bundler. xterm.js
|
|
3
|
-
// and addon-fit are loaded as UMD <script> tags before this file, so they
|
|
4
|
-
// arrive as the `Terminal` and `FitAddon` globals.
|
|
5
|
-
//
|
|
6
|
-
// Workarounds ported from castle-cli's `ide` branch and lemo threads' xterm:
|
|
7
|
-
// backoff reconnect + wake handlers, screen replay with a multi-stage repaint
|
|
8
|
-
// repair, FitAddon resize with send-dedup, wheel scroll-chain containment, and
|
|
9
|
-
// the text-presentation fix for symbols browsers like to emoji-render.
|
|
10
|
-
(function () {
|
|
11
|
-
const host = document.getElementById('term-host');
|
|
12
|
-
if (!host)
|
|
13
|
-
return;
|
|
14
|
-
const toggleButton = document.getElementById('term-toggle');
|
|
15
|
-
const reconnectDelaysMs = [250, 500, 1000, 2000, 4000, 8000, 12000, 15000];
|
|
16
|
-
// After a replay, xterm's grid can render at a stale size until a few
|
|
17
|
-
// repaints land; threads found this cadence reliable.
|
|
18
|
-
const replayRepairDelaysMs = [50, 160];
|
|
19
|
-
// Force text presentation for symbols browsers render as wide colour emoji;
|
|
20
|
-
// without it bullets/arrows in CLI output break the terminal grid.
|
|
21
|
-
const textPresentationPattern = /[•‣⁃∙■-◿☀-➿⬀-⯿]/g;
|
|
22
|
-
const variationSelectorText = '︎';
|
|
23
|
-
function forceTextSymbols(value) {
|
|
24
|
-
return value.replace(textPresentationPattern, (c) => `${c}${variationSelectorText}`);
|
|
25
|
-
}
|
|
26
|
-
// Terminal themes. The dark palette is Tokyo Night (from the cli `ide`
|
|
27
|
-
// branch); the choice persists in localStorage and is set from the cog.
|
|
28
|
-
const lightTheme = {
|
|
29
|
-
background: '#ffffff',
|
|
30
|
-
foreground: '#1f2328',
|
|
31
|
-
cursor: '#1f2328',
|
|
32
|
-
cursorAccent: '#ffffff',
|
|
33
|
-
selectionBackground: '#cfe0ff',
|
|
34
|
-
selectionForeground: '#1f2328',
|
|
35
|
-
black: '#24292e',
|
|
36
|
-
red: '#cf222e',
|
|
37
|
-
green: '#116329',
|
|
38
|
-
yellow: '#7d4e00',
|
|
39
|
-
blue: '#0969da',
|
|
40
|
-
magenta: '#8250df',
|
|
41
|
-
cyan: '#1b7c83',
|
|
42
|
-
white: '#6e7781',
|
|
43
|
-
brightBlack: '#57606a',
|
|
44
|
-
brightRed: '#a40e26',
|
|
45
|
-
brightGreen: '#1a7f37',
|
|
46
|
-
brightYellow: '#9a6700',
|
|
47
|
-
brightBlue: '#218bff',
|
|
48
|
-
brightMagenta: '#a475f9',
|
|
49
|
-
brightCyan: '#3192aa',
|
|
50
|
-
brightWhite: '#8c959f',
|
|
51
|
-
};
|
|
52
|
-
const darkTheme = {
|
|
53
|
-
background: '#1a1b26',
|
|
54
|
-
foreground: '#c0caf5',
|
|
55
|
-
cursor: '#c0caf5',
|
|
56
|
-
cursorAccent: '#1a1b26',
|
|
57
|
-
selectionBackground: '#283457',
|
|
58
|
-
selectionForeground: '#c0caf5',
|
|
59
|
-
black: '#15161e',
|
|
60
|
-
red: '#f7768e',
|
|
61
|
-
green: '#9ece6a',
|
|
62
|
-
yellow: '#e0af68',
|
|
63
|
-
blue: '#7aa2f7',
|
|
64
|
-
magenta: '#bb9af7',
|
|
65
|
-
cyan: '#7dcfff',
|
|
66
|
-
white: '#a9b1d6',
|
|
67
|
-
brightBlack: '#414868',
|
|
68
|
-
brightRed: '#f7768e',
|
|
69
|
-
brightGreen: '#9ece6a',
|
|
70
|
-
brightYellow: '#e0af68',
|
|
71
|
-
brightBlue: '#7aa2f7',
|
|
72
|
-
brightMagenta: '#bb9af7',
|
|
73
|
-
brightCyan: '#7dcfff',
|
|
74
|
-
brightWhite: '#c0caf5',
|
|
75
|
-
};
|
|
76
|
-
const themeStorageKey = 'castle-terminal-theme';
|
|
77
|
-
function storedThemeName() {
|
|
78
|
-
try {
|
|
79
|
-
return localStorage.getItem(themeStorageKey) === 'dark' ? 'dark' : 'light';
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return 'light';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
let themeName = storedThemeName();
|
|
86
|
-
const term = new Terminal({
|
|
87
|
-
convertEol: false,
|
|
88
|
-
cursorBlink: true,
|
|
89
|
-
macOptionIsMeta: true,
|
|
90
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
91
|
-
fontSize: 12,
|
|
92
|
-
fontWeight: 400,
|
|
93
|
-
fontWeightBold: 700,
|
|
94
|
-
letterSpacing: 0,
|
|
95
|
-
lineHeight: 1.25,
|
|
96
|
-
scrollback: 4000,
|
|
97
|
-
theme: themeName === 'dark' ? darkTheme : lightTheme,
|
|
98
|
-
});
|
|
99
|
-
const fit = new FitAddon.FitAddon();
|
|
100
|
-
term.loadAddon(fit);
|
|
101
|
-
term.open(host);
|
|
102
|
-
fit.fit();
|
|
103
|
-
term.focus();
|
|
104
|
-
// The PTY is lazy: the WebSocket isn't opened (and the server doesn't spawn
|
|
105
|
-
// a shell) until the user first reveals the terminal. If they never click
|
|
106
|
-
// the toggle, no PTY ever runs.
|
|
107
|
-
let activated = false;
|
|
108
|
-
let socket = null;
|
|
109
|
-
let socketToken = 0;
|
|
110
|
-
let reconnectAttempt = 0;
|
|
111
|
-
let reconnectTimer = null;
|
|
112
|
-
let reconnectEnabled = true;
|
|
113
|
-
let needsWakeReconnect = document.visibilityState === 'hidden';
|
|
114
|
-
let lastSentCols = 0;
|
|
115
|
-
let lastSentRows = 0;
|
|
116
|
-
const intentionallyClosed = new WeakSet();
|
|
117
|
-
const replayRepairFrames = [];
|
|
118
|
-
const replayRepairTimers = [];
|
|
119
|
-
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
120
|
-
const wsUrl = `${wsProtocol}//${window.location.host}/__castle/pty`;
|
|
121
|
-
//
|
|
122
|
-
// Toggle: hide the panel without unmounting / resizing xterm, so the text
|
|
123
|
-
// layout survives a hide/show cycle untouched (CSS keeps #term at 500px).
|
|
124
|
-
//
|
|
125
|
-
const openStorageKey = 'castle-terminal-open';
|
|
126
|
-
function storedOpen() {
|
|
127
|
-
// The panel (chat by default) is open unless the user closed it.
|
|
128
|
-
try {
|
|
129
|
-
const value = localStorage.getItem(openStorageKey);
|
|
130
|
-
return value === null ? true : value === '1';
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function persistOpen(open) {
|
|
137
|
-
try {
|
|
138
|
-
localStorage.setItem(openStorageKey, open ? '1' : '0');
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
/* storage disabled -- state just won't persist */
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// First reveal spawns the PTY; later reveals just re-focus a live one.
|
|
145
|
-
function activateTerminal() {
|
|
146
|
-
if (activated)
|
|
147
|
-
return;
|
|
148
|
-
activated = true;
|
|
149
|
-
connect();
|
|
150
|
-
}
|
|
151
|
-
if (toggleButton) {
|
|
152
|
-
toggleButton.addEventListener('click', () => {
|
|
153
|
-
const open = document.body.classList.toggle('term-open');
|
|
154
|
-
persistOpen(open);
|
|
155
|
-
// The header strip is a flex item that appears/disappears with
|
|
156
|
-
// `term-open`, so the xterm host's leftover height changes -- re-fit so
|
|
157
|
-
// the grid matches the new content height (the ResizeObserver also
|
|
158
|
-
// catches this, but fit here makes the relayout immediate).
|
|
159
|
-
fitAndSendResize();
|
|
160
|
-
if (!open) {
|
|
161
|
-
document.body.classList.remove('term-settings-open');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
// Only the terminal view needs the PTY; the chat client handles its own
|
|
165
|
-
// focus when the panel opens onto the chat view.
|
|
166
|
-
if (document.body.classList.contains('term-view')) {
|
|
167
|
-
activateTerminal();
|
|
168
|
-
term.focus();
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
// The chat client owns the chat | terminal view switch and dispatches
|
|
173
|
-
// `castle-panel-view`; the terminal side reacts here (lazy PTY spawn + fit).
|
|
174
|
-
document.addEventListener('castle-panel-view', (event) => {
|
|
175
|
-
const detail = event.detail;
|
|
176
|
-
if (!detail || detail.view !== 'terminal')
|
|
177
|
-
return;
|
|
178
|
-
if (!document.body.classList.contains('term-open'))
|
|
179
|
-
return;
|
|
180
|
-
activateTerminal();
|
|
181
|
-
fitAndSendResize();
|
|
182
|
-
if (detail.focus)
|
|
183
|
-
term.focus();
|
|
184
|
-
});
|
|
185
|
-
//
|
|
186
|
-
// Settings cog: a small popover with a light/dark theme switch for the
|
|
187
|
-
// terminal. The choice persists in localStorage.
|
|
188
|
-
//
|
|
189
|
-
const settingsButton = document.getElementById('term-settings');
|
|
190
|
-
const settingsPanel = document.getElementById('term-settings-panel');
|
|
191
|
-
const segButtons = Array.from(document.querySelectorAll('#term-theme-seg button'));
|
|
192
|
-
function applyTheme(name) {
|
|
193
|
-
themeName = name;
|
|
194
|
-
term.options.theme = name === 'dark' ? darkTheme : lightTheme;
|
|
195
|
-
document.body.classList.toggle('term-dark', name === 'dark');
|
|
196
|
-
for (const button of segButtons) {
|
|
197
|
-
button.classList.toggle('active', button.dataset.theme === name);
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
localStorage.setItem(themeStorageKey, name);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
/* storage disabled -- theme just won't persist */
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
applyTheme(themeName);
|
|
207
|
-
if (settingsButton) {
|
|
208
|
-
settingsButton.addEventListener('click', (event) => {
|
|
209
|
-
event.stopPropagation();
|
|
210
|
-
document.body.classList.toggle('term-settings-open');
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
for (const button of segButtons) {
|
|
214
|
-
button.addEventListener('click', () => {
|
|
215
|
-
applyTheme(button.dataset.theme === 'dark' ? 'dark' : 'light');
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
// A click anywhere outside the popover (or cog) closes it.
|
|
219
|
-
document.addEventListener('click', (event) => {
|
|
220
|
-
if (!document.body.classList.contains('term-settings-open'))
|
|
221
|
-
return;
|
|
222
|
-
const target = event.target;
|
|
223
|
-
if (settingsPanel && settingsPanel.contains(target))
|
|
224
|
-
return;
|
|
225
|
-
if (settingsButton && settingsButton.contains(target))
|
|
226
|
-
return;
|
|
227
|
-
document.body.classList.remove('term-settings-open');
|
|
228
|
-
});
|
|
229
|
-
//
|
|
230
|
-
// Ctrl+T moves keyboard focus between the terminal and the deck iframe.
|
|
231
|
-
// While the iframe holds focus the parent sees no keydowns, so the deck
|
|
232
|
-
// page forwards Ctrl+T via postMessage (see DECK_FOCUS_SCRIPT in ide.ts).
|
|
233
|
-
// The focused side persists in localStorage and is restored on load.
|
|
234
|
-
//
|
|
235
|
-
const deckFrame = document.getElementById('deck-frame');
|
|
236
|
-
const focusStorageKey = 'castle-terminal-focus';
|
|
237
|
-
function storedFocus() {
|
|
238
|
-
try {
|
|
239
|
-
return localStorage.getItem(focusStorageKey) === 'terminal' ? 'terminal' : 'deck';
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return 'deck';
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
let focusTarget = storedFocus();
|
|
246
|
-
function setFocus(target) {
|
|
247
|
-
focusTarget = target;
|
|
248
|
-
try {
|
|
249
|
-
localStorage.setItem(focusStorageKey, target);
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
/* storage disabled -- focus side just won't persist */
|
|
253
|
-
}
|
|
254
|
-
if (target === 'terminal') {
|
|
255
|
-
// Focusing the panel implies revealing it. Focus lands on whichever
|
|
256
|
-
// view the panel is showing -- the chat input or the terminal. The
|
|
257
|
-
// chat client flips classes / focuses its input on this event; the
|
|
258
|
-
// listener above does the PTY activation + focus for the terminal.
|
|
259
|
-
if (!document.body.classList.contains('term-open')) {
|
|
260
|
-
document.body.classList.add('term-open');
|
|
261
|
-
persistOpen(true);
|
|
262
|
-
}
|
|
263
|
-
const view = document.body.classList.contains('term-view') ? 'terminal' : 'chat';
|
|
264
|
-
document.dispatchEvent(new CustomEvent('castle-panel-view', { detail: { view, focus: true } }));
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
deckFrame?.contentWindow?.focus();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function toggleFocus() {
|
|
271
|
-
setFocus(focusTarget === 'terminal' ? 'deck' : 'terminal');
|
|
272
|
-
}
|
|
273
|
-
// Ctrl+T while the parent (panel or shell chrome) holds focus. Capture
|
|
274
|
-
// phase + stopImmediatePropagation so xterm never receives the keystroke.
|
|
275
|
-
document.addEventListener('keydown', (event) => {
|
|
276
|
-
if (event.ctrlKey &&
|
|
277
|
-
!event.metaKey &&
|
|
278
|
-
!event.altKey &&
|
|
279
|
-
!event.shiftKey &&
|
|
280
|
-
(event.key === 't' || event.key === 'T')) {
|
|
281
|
-
event.preventDefault();
|
|
282
|
-
event.stopImmediatePropagation();
|
|
283
|
-
toggleFocus();
|
|
284
|
-
}
|
|
285
|
-
}, true);
|
|
286
|
-
// Ctrl+T forwarded from inside the deck iframe.
|
|
287
|
-
window.addEventListener('message', (event) => {
|
|
288
|
-
if (!event.data || event.data.type !== 'castle-ide-toggle-focus')
|
|
289
|
-
return;
|
|
290
|
-
if (deckFrame && event.source !== deckFrame.contentWindow)
|
|
291
|
-
return;
|
|
292
|
-
toggleFocus();
|
|
293
|
-
});
|
|
294
|
-
// Keep focusTarget honest when focus moves by click rather than Tab/Ctrl+T.
|
|
295
|
-
if (term.textarea) {
|
|
296
|
-
term.textarea.addEventListener('focus', () => {
|
|
297
|
-
focusTarget = 'terminal';
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
document.addEventListener('focusin', (event) => {
|
|
301
|
-
if (event.target?.id === 'chat-input')
|
|
302
|
-
focusTarget = 'terminal';
|
|
303
|
-
});
|
|
304
|
-
window.addEventListener('blur', () => {
|
|
305
|
-
if (deckFrame && document.activeElement === deckFrame)
|
|
306
|
-
focusTarget = 'deck';
|
|
307
|
-
});
|
|
308
|
-
function clearReconnectTimer() {
|
|
309
|
-
if (reconnectTimer === null)
|
|
310
|
-
return;
|
|
311
|
-
window.clearTimeout(reconnectTimer);
|
|
312
|
-
reconnectTimer = null;
|
|
313
|
-
}
|
|
314
|
-
// xterm sometimes leaves stale rows after a resize/replay; force a redraw.
|
|
315
|
-
function refreshRows() {
|
|
316
|
-
if (term.rows > 0)
|
|
317
|
-
term.refresh(0, term.rows - 1);
|
|
318
|
-
}
|
|
319
|
-
function sendResize() {
|
|
320
|
-
if (!socket || socket.readyState !== WebSocket.OPEN)
|
|
321
|
-
return;
|
|
322
|
-
if (term.cols === lastSentCols && term.rows === lastSentRows)
|
|
323
|
-
return;
|
|
324
|
-
lastSentCols = term.cols;
|
|
325
|
-
lastSentRows = term.rows;
|
|
326
|
-
socket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
327
|
-
}
|
|
328
|
-
function fitNow() {
|
|
329
|
-
fit.fit();
|
|
330
|
-
sendResize();
|
|
331
|
-
}
|
|
332
|
-
function fitAndSendResize() {
|
|
333
|
-
window.requestAnimationFrame(() => {
|
|
334
|
-
fitNow();
|
|
335
|
-
refreshRows();
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
//
|
|
339
|
-
// Replay repaint repair (ported from threads): a replayed screen can render
|
|
340
|
-
// at a stale size; repaint across a microtask, two animation frames, and a
|
|
341
|
-
// couple of timers so the grid settles whatever the layout timing.
|
|
342
|
-
//
|
|
343
|
-
function clearReplayRepairWork() {
|
|
344
|
-
while (replayRepairFrames.length > 0) {
|
|
345
|
-
const frame = replayRepairFrames.pop();
|
|
346
|
-
if (frame !== undefined)
|
|
347
|
-
window.cancelAnimationFrame(frame);
|
|
348
|
-
}
|
|
349
|
-
while (replayRepairTimers.length > 0) {
|
|
350
|
-
const timer = replayRepairTimers.pop();
|
|
351
|
-
if (timer !== undefined)
|
|
352
|
-
window.clearTimeout(timer);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function repairReplayLayout() {
|
|
356
|
-
fitNow();
|
|
357
|
-
refreshRows();
|
|
358
|
-
}
|
|
359
|
-
function scheduleReplayRepair() {
|
|
360
|
-
clearReplayRepairWork();
|
|
361
|
-
window.queueMicrotask(repairReplayLayout);
|
|
362
|
-
replayRepairFrames.push(window.requestAnimationFrame(() => {
|
|
363
|
-
repairReplayLayout();
|
|
364
|
-
replayRepairFrames.push(window.requestAnimationFrame(repairReplayLayout));
|
|
365
|
-
}));
|
|
366
|
-
for (const delay of replayRepairDelaysMs) {
|
|
367
|
-
replayRepairTimers.push(window.setTimeout(repairReplayLayout, delay));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
function handleMessage(msg) {
|
|
371
|
-
if (msg.type === 'replay') {
|
|
372
|
-
// Replay must land on a blank terminal, then repaint once layout settles.
|
|
373
|
-
term.reset();
|
|
374
|
-
term.write(forceTextSymbols(String(msg.data ?? '')), scheduleReplayRepair);
|
|
375
|
-
}
|
|
376
|
-
else if (msg.type === 'output') {
|
|
377
|
-
term.write(forceTextSymbols(String(msg.data ?? '')));
|
|
378
|
-
}
|
|
379
|
-
else if (msg.type === 'exit') {
|
|
380
|
-
reconnectEnabled = false;
|
|
381
|
-
clearReconnectTimer();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function scheduleReconnect() {
|
|
385
|
-
if (!reconnectEnabled)
|
|
386
|
-
return;
|
|
387
|
-
clearReconnectTimer();
|
|
388
|
-
if (document.visibilityState === 'hidden' || navigator.onLine === false)
|
|
389
|
-
return;
|
|
390
|
-
if (reconnectAttempt >= reconnectDelaysMs.length)
|
|
391
|
-
return;
|
|
392
|
-
const delay = reconnectDelaysMs[reconnectAttempt];
|
|
393
|
-
reconnectAttempt += 1;
|
|
394
|
-
reconnectTimer = window.setTimeout(connect, delay);
|
|
395
|
-
}
|
|
396
|
-
function connect() {
|
|
397
|
-
if (!reconnectEnabled)
|
|
398
|
-
return;
|
|
399
|
-
clearReconnectTimer();
|
|
400
|
-
if (document.visibilityState === 'hidden' || navigator.onLine === false)
|
|
401
|
-
return;
|
|
402
|
-
if (socket) {
|
|
403
|
-
intentionallyClosed.add(socket);
|
|
404
|
-
try {
|
|
405
|
-
socket.close(1000, 'reconnect');
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
/* ignore */
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const ws = new WebSocket(wsUrl);
|
|
412
|
-
const token = ++socketToken;
|
|
413
|
-
socket = ws;
|
|
414
|
-
lastSentCols = 0;
|
|
415
|
-
lastSentRows = 0;
|
|
416
|
-
ws.addEventListener('open', () => {
|
|
417
|
-
if (socketToken !== token)
|
|
418
|
-
return;
|
|
419
|
-
reconnectAttempt = 0;
|
|
420
|
-
fitAndSendResize();
|
|
421
|
-
});
|
|
422
|
-
ws.addEventListener('message', (event) => {
|
|
423
|
-
if (socketToken !== token)
|
|
424
|
-
return;
|
|
425
|
-
try {
|
|
426
|
-
handleMessage(JSON.parse(String(event.data)));
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
/* ignore malformed frame */
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
ws.addEventListener('close', () => {
|
|
433
|
-
if (socketToken !== token)
|
|
434
|
-
return;
|
|
435
|
-
socket = null;
|
|
436
|
-
if (!reconnectEnabled || intentionallyClosed.has(ws))
|
|
437
|
-
return;
|
|
438
|
-
scheduleReconnect();
|
|
439
|
-
});
|
|
440
|
-
ws.addEventListener('error', () => {
|
|
441
|
-
/* the close handler drives reconnect */
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
//
|
|
445
|
-
// Wake handling: a backgrounded / offline tab drops its socket; reconnect as
|
|
446
|
-
// soon as it becomes visible, focused, or back online (ported from `ide`).
|
|
447
|
-
//
|
|
448
|
-
function wake(force) {
|
|
449
|
-
if (!activated || !reconnectEnabled || document.visibilityState === 'hidden')
|
|
450
|
-
return;
|
|
451
|
-
reconnectAttempt = 0;
|
|
452
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
453
|
-
fitAndSendResize();
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
if (socket && socket.readyState === WebSocket.CONNECTING)
|
|
457
|
-
return;
|
|
458
|
-
if (force || !socket || socket.readyState >= WebSocket.CLOSING)
|
|
459
|
-
connect();
|
|
460
|
-
}
|
|
461
|
-
document.addEventListener('visibilitychange', () => {
|
|
462
|
-
if (document.visibilityState === 'hidden') {
|
|
463
|
-
needsWakeReconnect = true;
|
|
464
|
-
clearReconnectTimer();
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
const force = needsWakeReconnect;
|
|
468
|
-
needsWakeReconnect = false;
|
|
469
|
-
wake(force);
|
|
470
|
-
});
|
|
471
|
-
window.addEventListener('pagehide', () => {
|
|
472
|
-
needsWakeReconnect = true;
|
|
473
|
-
if (socket) {
|
|
474
|
-
intentionallyClosed.add(socket);
|
|
475
|
-
try {
|
|
476
|
-
socket.close(1000, 'page hidden');
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
/* ignore */
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
clearReconnectTimer();
|
|
483
|
-
});
|
|
484
|
-
window.addEventListener('pageshow', (event) => {
|
|
485
|
-
const force = needsWakeReconnect || event.persisted;
|
|
486
|
-
needsWakeReconnect = false;
|
|
487
|
-
wake(force);
|
|
488
|
-
});
|
|
489
|
-
window.addEventListener('focus', () => {
|
|
490
|
-
const force = needsWakeReconnect;
|
|
491
|
-
needsWakeReconnect = false;
|
|
492
|
-
wake(force);
|
|
493
|
-
});
|
|
494
|
-
window.addEventListener('offline', () => {
|
|
495
|
-
needsWakeReconnect = true;
|
|
496
|
-
clearReconnectTimer();
|
|
497
|
-
});
|
|
498
|
-
window.addEventListener('online', () => {
|
|
499
|
-
const force = needsWakeReconnect;
|
|
500
|
-
needsWakeReconnect = false;
|
|
501
|
-
wake(force);
|
|
502
|
-
});
|
|
503
|
-
// Send Ctrl+Enter as CSI 13;5u so claude/codex CLIs receive newline+modifier.
|
|
504
|
-
term.attachCustomKeyEventHandler((ev) => {
|
|
505
|
-
if (ev.type !== 'keydown')
|
|
506
|
-
return true;
|
|
507
|
-
if (ev.key === 'Enter' && ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
|
508
|
-
ev.preventDefault();
|
|
509
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
510
|
-
socket.send(JSON.stringify({ type: 'input', data: '\x1b[13;5u' }));
|
|
511
|
-
}
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
return true;
|
|
515
|
-
});
|
|
516
|
-
term.onData((data) => {
|
|
517
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
518
|
-
socket.send(JSON.stringify({ type: 'input', data }));
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
// Keep wheel scrolling inside the terminal -- don't chain to the host page
|
|
522
|
-
// when the viewport is at its top/bottom (or has nothing to scroll).
|
|
523
|
-
host.addEventListener('wheel', (event) => {
|
|
524
|
-
const viewport = host.querySelector('.xterm-viewport');
|
|
525
|
-
if (!viewport)
|
|
526
|
-
return;
|
|
527
|
-
const maxScrollTop = viewport.scrollHeight - viewport.clientHeight;
|
|
528
|
-
if (maxScrollTop <= 0) {
|
|
529
|
-
event.preventDefault();
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const pastTop = viewport.scrollTop <= 0 && event.deltaY < 0;
|
|
533
|
-
const pastBottom = viewport.scrollTop >= maxScrollTop - 1 && event.deltaY > 0;
|
|
534
|
-
if (pastTop || pastBottom)
|
|
535
|
-
event.preventDefault();
|
|
536
|
-
}, { passive: false });
|
|
537
|
-
const observer = new ResizeObserver(() => fitAndSendResize());
|
|
538
|
-
observer.observe(host);
|
|
539
|
-
// Restore the persisted open state. The server renders the panel open (the
|
|
540
|
-
// body ships with `term-open`); drop it if the user had closed it. The chat
|
|
541
|
-
// client runs first and restores the view class, so a terminal-view restore
|
|
542
|
-
// can spawn its PTY right away.
|
|
543
|
-
if (!storedOpen()) {
|
|
544
|
-
document.body.classList.remove('term-open');
|
|
545
|
-
}
|
|
546
|
-
else if (document.body.classList.contains('term-view')) {
|
|
547
|
-
activateTerminal();
|
|
548
|
-
}
|
|
549
|
-
// Restore the persisted focus side. xterm needs a layout pass after
|
|
550
|
-
// term.open() before focus reliably lands on its helper textarea -- a
|
|
551
|
-
// synchronous focus here runs before xterm finishes mounting and silently
|
|
552
|
-
// fails. Defer the terminal-focus restore behind a fit + two animation
|
|
553
|
-
// frames so the grid has settled first (matching threads' post-layout
|
|
554
|
-
// focus sequencing).
|
|
555
|
-
if (storedFocus() === 'terminal' &&
|
|
556
|
-
document.body.classList.contains('term-open') &&
|
|
557
|
-
document.body.classList.contains('term-view')) {
|
|
558
|
-
window.requestAnimationFrame(() => {
|
|
559
|
-
fitNow();
|
|
560
|
-
window.requestAnimationFrame(() => {
|
|
561
|
-
term.focus();
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
else if (deckFrame) {
|
|
566
|
-
deckFrame.contentWindow?.focus();
|
|
567
|
-
}
|
|
568
|
-
})();
|
|
569
|
-
export {};
|