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.js
CHANGED
|
@@ -1,26 +1,61 @@
|
|
|
1
|
-
// IDE
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// IDE backend for `castle-web serve`: serves the shell UI (a dockview workspace
|
|
2
|
+
// bundled from `src/shell/` into `dist/shell/`) and runs the terminal panel's
|
|
3
|
+
// PTY. The deck loads in an iframe inside the shell; an agent CLI can be driven
|
|
4
|
+
// in the terminal panel right next to the live deck.
|
|
5
5
|
//
|
|
6
6
|
// Backend pattern ported from castle-cli's `ide` branch: @lydell/node-pty for
|
|
7
7
|
// the PTY, an @xterm/headless screen + @xterm/addon-serialize so a reconnecting
|
|
8
8
|
// client gets a full replay of the current screen + scrollback.
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
-
import { createRequire } from "module";
|
|
12
11
|
import { fileURLToPath } from "url";
|
|
13
12
|
import { spawn as spawnPty } from "@lydell/node-pty";
|
|
14
13
|
import headlessPkg from "@xterm/headless";
|
|
15
14
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
16
15
|
import { WebSocketServer } from "ws";
|
|
17
16
|
const HeadlessTerminal = headlessPkg.Terminal;
|
|
18
|
-
const require_ = createRequire(import.meta.url);
|
|
19
17
|
const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
// The bundled shell app (vite build output). `/` serves its index.html and
|
|
19
|
+
// `/__castle/ide/<asset>` serves its hashed bundle assets (the vite `base`).
|
|
20
|
+
const SHELL_DIR = path.join(DIST_DIR, "shell");
|
|
20
21
|
const PTY_TERM = "xterm-256color";
|
|
21
22
|
const INITIAL_COLS = 80;
|
|
22
23
|
const INITIAL_ROWS = 24;
|
|
23
24
|
const SCROLLBACK = 4000;
|
|
25
|
+
const SHELL_MIME = {
|
|
26
|
+
".html": "text/html; charset=utf-8",
|
|
27
|
+
".js": "text/javascript; charset=utf-8",
|
|
28
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
29
|
+
".css": "text/css; charset=utf-8",
|
|
30
|
+
".json": "application/json; charset=utf-8",
|
|
31
|
+
".svg": "image/svg+xml",
|
|
32
|
+
".png": "image/png",
|
|
33
|
+
".jpg": "image/jpeg",
|
|
34
|
+
".woff": "font/woff",
|
|
35
|
+
".woff2": "font/woff2",
|
|
36
|
+
".ttf": "font/ttf",
|
|
37
|
+
".map": "application/json; charset=utf-8",
|
|
38
|
+
};
|
|
39
|
+
// Serve a file from the bundled shell dir, guarding against path traversal.
|
|
40
|
+
function serveShellFile(res, asset) {
|
|
41
|
+
const rel = path.normalize(asset).replace(/^(\.\.[/\\])+/, "");
|
|
42
|
+
const filePath = path.join(SHELL_DIR, rel);
|
|
43
|
+
if (!filePath.startsWith(SHELL_DIR + path.sep) &&
|
|
44
|
+
filePath !== path.join(SHELL_DIR, "index.html")) {
|
|
45
|
+
res.writeHead(404).end();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
49
|
+
res.writeHead(404).end();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
res.writeHead(200, {
|
|
53
|
+
"content-type": SHELL_MIME[path.extname(filePath)] ?? "application/octet-stream",
|
|
54
|
+
"cache-control": "no-store",
|
|
55
|
+
});
|
|
56
|
+
fs.createReadStream(filePath).pipe(res);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
24
59
|
// The split-view shell is served at the deck root `/` (the deck itself moves
|
|
25
60
|
// into the iframe at `/index.html`); `/__castle/ide/<asset>` are the shell's
|
|
26
61
|
// static assets; `/__castle/pty` is the PTY WebSocket (handled via a direct
|
|
@@ -97,7 +132,7 @@ function broadcast(session, body) {
|
|
|
97
132
|
// Returns a handler for the IDE page + assets + the PTY WebSocket upgrade. The
|
|
98
133
|
// PTY runs a login shell in `deckDir`; it spawns lazily on first connection.
|
|
99
134
|
export function createIdeServer(opts) {
|
|
100
|
-
const { deckDir
|
|
135
|
+
const { deckDir } = opts;
|
|
101
136
|
let session = null;
|
|
102
137
|
function spawnSession() {
|
|
103
138
|
const { command, args } = defaultShell();
|
|
@@ -228,28 +263,11 @@ export function createIdeServer(opts) {
|
|
|
228
263
|
return true;
|
|
229
264
|
}
|
|
230
265
|
function handleHttpRequest(_req, res, reqPath) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
res
|
|
234
|
-
"content-type": "text/html; charset=utf-8",
|
|
235
|
-
"cache-control": "no-store",
|
|
236
|
-
});
|
|
237
|
-
res.end(html);
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
266
|
+
// `/` -> the shell's index.html; `/__castle/ide/<asset>` -> bundle assets.
|
|
267
|
+
if (reqPath === "/")
|
|
268
|
+
return serveShellFile(res, "index.html");
|
|
240
269
|
if (reqPath.startsWith(IDE_ASSET_PREFIX)) {
|
|
241
|
-
|
|
242
|
-
const resolved = resolveIdeAsset(asset);
|
|
243
|
-
if (!resolved) {
|
|
244
|
-
res.writeHead(404).end();
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
res.writeHead(200, {
|
|
248
|
-
"content-type": resolved.contentType,
|
|
249
|
-
"cache-control": "no-store",
|
|
250
|
-
});
|
|
251
|
-
fs.createReadStream(resolved.filePath).pipe(res);
|
|
252
|
-
return true;
|
|
270
|
+
return serveShellFile(res, reqPath.slice(IDE_ASSET_PREFIX.length) || "index.html");
|
|
253
271
|
}
|
|
254
272
|
return false;
|
|
255
273
|
}
|
|
@@ -274,625 +292,3 @@ export function createIdeServer(opts) {
|
|
|
274
292
|
}
|
|
275
293
|
return { handleHttpRequest, handleUpgrade, shutdown };
|
|
276
294
|
}
|
|
277
|
-
// Map an IDE asset name to a file on disk. xterm + addon-fit come straight from
|
|
278
|
-
// node_modules (UMD builds); the terminal client is compiled alongside this
|
|
279
|
-
// file by tsc into the same dist directory.
|
|
280
|
-
function resolveIdeAsset(asset) {
|
|
281
|
-
const assets = {
|
|
282
|
-
"xterm.js": {
|
|
283
|
-
spec: "@xterm/xterm/lib/xterm.js",
|
|
284
|
-
contentType: "text/javascript; charset=utf-8",
|
|
285
|
-
},
|
|
286
|
-
"xterm.css": {
|
|
287
|
-
spec: "@xterm/xterm/css/xterm.css",
|
|
288
|
-
contentType: "text/css; charset=utf-8",
|
|
289
|
-
},
|
|
290
|
-
"addon-fit.js": {
|
|
291
|
-
spec: "@xterm/addon-fit/lib/addon-fit.js",
|
|
292
|
-
contentType: "text/javascript; charset=utf-8",
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
const fromModule = assets[asset];
|
|
296
|
-
if (fromModule) {
|
|
297
|
-
try {
|
|
298
|
-
return {
|
|
299
|
-
filePath: require_.resolve(fromModule.spec),
|
|
300
|
-
contentType: fromModule.contentType,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// React's package "exports" hides umd/ from require.resolve; resolve the
|
|
308
|
-
// package root via its package.json (always exported) and join from there.
|
|
309
|
-
const umdAssets = {
|
|
310
|
-
"react.js": { pkg: "react", rel: "umd/react.production.min.js" },
|
|
311
|
-
"react-dom.js": {
|
|
312
|
-
pkg: "react-dom",
|
|
313
|
-
rel: "umd/react-dom.production.min.js",
|
|
314
|
-
},
|
|
315
|
-
"marked.js": { pkg: "marked", rel: "lib/marked.umd.js" },
|
|
316
|
-
};
|
|
317
|
-
const fromUmd = umdAssets[asset];
|
|
318
|
-
if (fromUmd) {
|
|
319
|
-
try {
|
|
320
|
-
const pkgRoot = path.dirname(require_.resolve(`${fromUmd.pkg}/package.json`));
|
|
321
|
-
const filePath = path.join(pkgRoot, fromUmd.rel);
|
|
322
|
-
return fs.existsSync(filePath)
|
|
323
|
-
? { filePath, contentType: "text/javascript; charset=utf-8" }
|
|
324
|
-
: null;
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (asset === "ide-client.js" || asset === "chat-client.js") {
|
|
331
|
-
const filePath = path.join(DIST_DIR, asset);
|
|
332
|
-
return fs.existsSync(filePath)
|
|
333
|
-
? { filePath, contentType: "text/javascript; charset=utf-8" }
|
|
334
|
-
: null;
|
|
335
|
-
}
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
// Lucide glyphs; currentColor so the button :hover styles drive them. The
|
|
339
|
-
// panel toggle reads as "building" (hammer), not terminal -- the terminal is
|
|
340
|
-
// just one view inside the panel.
|
|
341
|
-
const HAMMER_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9" /><path d="m18 15 4-4" /><path d="m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5" /></svg>`;
|
|
342
|
-
const COG_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>`;
|
|
343
|
-
const IDE_STYLES = `
|
|
344
|
-
:root { color-scheme: light; }
|
|
345
|
-
* { box-sizing: border-box; }
|
|
346
|
-
html, body { height: 100%; margin: 0; }
|
|
347
|
-
body {
|
|
348
|
-
/* Header-strip height. The deck's editor header is 48px, but drops to the
|
|
349
|
-
52px responsive variant once the deck iframe is below 860px wide -- which
|
|
350
|
-
it is whenever the terminal is open at a viewport under 1360px. Track
|
|
351
|
-
that so the terminal header strip and its buttons line up with the deck's
|
|
352
|
-
editor header beside it. */
|
|
353
|
-
--term-header-h: 48px;
|
|
354
|
-
background: #ffffff;
|
|
355
|
-
color: #1f2328;
|
|
356
|
-
/* Same stack as the kit editor (engine/ui.module.css --castle-font-body)
|
|
357
|
-
so the shell + chat read as one app with the deck's editor. */
|
|
358
|
-
font-family: baltomobile, Balto, ui-sans-serif, system-ui, -apple-system,
|
|
359
|
-
BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
360
|
-
display: flex;
|
|
361
|
-
overflow: hidden;
|
|
362
|
-
}
|
|
363
|
-
@media (max-width: 1360px) {
|
|
364
|
-
body.term-open { --term-header-h: 52px; }
|
|
365
|
-
}
|
|
366
|
-
#deck {
|
|
367
|
-
flex: 1 1 0;
|
|
368
|
-
min-width: 0;
|
|
369
|
-
background: #f5f5f5;
|
|
370
|
-
border-right: 1px solid #e1e4e8;
|
|
371
|
-
}
|
|
372
|
-
#deck iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
373
|
-
/* The terminal panel is a flex column: a fixed-height header strip on top,
|
|
374
|
-
and the xterm host taking exactly the leftover height below it. */
|
|
375
|
-
#term {
|
|
376
|
-
flex: 1 1 500px;
|
|
377
|
-
max-width: 500px;
|
|
378
|
-
min-width: 320px;
|
|
379
|
-
background: #ffffff;
|
|
380
|
-
display: flex;
|
|
381
|
-
flex-direction: column;
|
|
382
|
-
}
|
|
383
|
-
body.term-dark #term { background: #1a1b26; }
|
|
384
|
-
/* Panel content area below the header. The chat view and the terminal view
|
|
385
|
-
are both mounted, absolutely filling it; the inactive one is lifted to
|
|
386
|
-
visibility:hidden (not display:none) so xterm keeps its measured layout
|
|
387
|
-
-- same trick as the closed-panel state below. */
|
|
388
|
-
#panel-body {
|
|
389
|
-
flex: 1 1 0;
|
|
390
|
-
min-height: 0;
|
|
391
|
-
position: relative;
|
|
392
|
-
}
|
|
393
|
-
#term-host {
|
|
394
|
-
position: absolute;
|
|
395
|
-
inset: 0;
|
|
396
|
-
padding: 6px;
|
|
397
|
-
overscroll-behavior: contain;
|
|
398
|
-
}
|
|
399
|
-
body:not(.term-view) #term-host { visibility: hidden; }
|
|
400
|
-
#chat-host {
|
|
401
|
-
position: absolute;
|
|
402
|
-
inset: 0;
|
|
403
|
-
display: flex;
|
|
404
|
-
flex-direction: column;
|
|
405
|
-
}
|
|
406
|
-
body.term-view #chat-host { visibility: hidden; }
|
|
407
|
-
|
|
408
|
-
/* Header strip for the terminal panel -- mirrors the deck's editor header
|
|
409
|
-
(same height, 1px bottom border, white background, 16px title). A real
|
|
410
|
-
flex item at the top of the panel; only shown while the terminal is open
|
|
411
|
-
(when closed the panel is hidden and the deck is full width). */
|
|
412
|
-
#term-header {
|
|
413
|
-
flex: 0 0 var(--term-header-h);
|
|
414
|
-
height: var(--term-header-h);
|
|
415
|
-
display: none;
|
|
416
|
-
align-items: center;
|
|
417
|
-
gap: 8px;
|
|
418
|
-
padding: 10px 12px;
|
|
419
|
-
background: #ffffff;
|
|
420
|
-
border-bottom: 1px solid #cccccc;
|
|
421
|
-
}
|
|
422
|
-
body.term-open #term-header { display: flex; }
|
|
423
|
-
body.term-dark #term-header { background: #1a1b26; border-bottom-color: #2a2c3d; }
|
|
424
|
-
|
|
425
|
-
/* Terminal hidden: the panel stays mounted at its full 500px size (so xterm
|
|
426
|
-
keeps its measured layout) -- just lifted out of flow and made invisible,
|
|
427
|
-
and the deck takes the whole width. */
|
|
428
|
-
body:not(.term-open) #deck { border-right: 0; }
|
|
429
|
-
body:not(.term-open) #term {
|
|
430
|
-
position: absolute;
|
|
431
|
-
top: 0;
|
|
432
|
-
right: 0;
|
|
433
|
-
width: 500px;
|
|
434
|
-
height: 100%;
|
|
435
|
-
visibility: hidden;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/* Toggle button: always pinned top-right above both the deck and the
|
|
439
|
-
terminal. Styling is copied from the editor-demo header icon buttons
|
|
440
|
-
(34px square, 1px solid #000, 4px radius, drop shadow) so it reads as a
|
|
441
|
-
header button when it overlays a header bar. Vertically centered in the
|
|
442
|
-
header strip -- (header-h - 34) / 2 -- which tracks the deck's editor
|
|
443
|
-
header height, so it sits centered whether the terminal is open or not. */
|
|
444
|
-
#term-toggle {
|
|
445
|
-
position: fixed;
|
|
446
|
-
top: calc((var(--term-header-h) - 34px) / 2);
|
|
447
|
-
right: 7px;
|
|
448
|
-
z-index: 2147483000;
|
|
449
|
-
width: 34px;
|
|
450
|
-
height: 34px;
|
|
451
|
-
padding: 0;
|
|
452
|
-
display: flex;
|
|
453
|
-
align-items: center;
|
|
454
|
-
justify-content: center;
|
|
455
|
-
background: #ffffff;
|
|
456
|
-
color: #222222;
|
|
457
|
-
border: 1px solid #000000;
|
|
458
|
-
border-radius: 4px;
|
|
459
|
-
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
460
|
-
cursor: pointer;
|
|
461
|
-
}
|
|
462
|
-
#term-toggle:hover { background: #eeeeee; }
|
|
463
|
-
#term-toggle svg { width: 16px; height: 16px; display: block; }
|
|
464
|
-
|
|
465
|
-
/* Settings cog: same button style, sits just left of the toggle. Only
|
|
466
|
-
present while the terminal is open. */
|
|
467
|
-
#term-settings {
|
|
468
|
-
position: fixed;
|
|
469
|
-
top: calc((var(--term-header-h) - 34px) / 2);
|
|
470
|
-
right: 47px;
|
|
471
|
-
z-index: 2147483000;
|
|
472
|
-
width: 34px;
|
|
473
|
-
height: 34px;
|
|
474
|
-
padding: 0;
|
|
475
|
-
display: none;
|
|
476
|
-
align-items: center;
|
|
477
|
-
justify-content: center;
|
|
478
|
-
background: #ffffff;
|
|
479
|
-
color: #222222;
|
|
480
|
-
border: 1px solid #000000;
|
|
481
|
-
border-radius: 4px;
|
|
482
|
-
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
483
|
-
cursor: pointer;
|
|
484
|
-
}
|
|
485
|
-
body.term-open #term-settings { display: flex; }
|
|
486
|
-
#term-settings:hover { background: #eeeeee; }
|
|
487
|
-
#term-settings svg { width: 16px; height: 16px; display: block; }
|
|
488
|
-
|
|
489
|
-
/* Settings popover: flat, opaque, anchored under the buttons. */
|
|
490
|
-
#term-settings-panel {
|
|
491
|
-
position: fixed;
|
|
492
|
-
top: var(--term-header-h);
|
|
493
|
-
right: 7px;
|
|
494
|
-
z-index: 2147483000;
|
|
495
|
-
display: none;
|
|
496
|
-
width: 300px;
|
|
497
|
-
padding: 10px 12px;
|
|
498
|
-
background: #ffffff;
|
|
499
|
-
color: #222222;
|
|
500
|
-
border: 1px solid #000000;
|
|
501
|
-
border-radius: 4px;
|
|
502
|
-
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
503
|
-
font-size: 13px;
|
|
504
|
-
}
|
|
505
|
-
#term-settings-panel .row > span { white-space: nowrap; }
|
|
506
|
-
body.term-settings-open #term-settings-panel { display: block; }
|
|
507
|
-
#term-settings-panel .row {
|
|
508
|
-
display: flex;
|
|
509
|
-
align-items: center;
|
|
510
|
-
justify-content: space-between;
|
|
511
|
-
gap: 10px;
|
|
512
|
-
}
|
|
513
|
-
#term-settings-panel .row + .row { margin-top: 8px; }
|
|
514
|
-
/* Shared segmented control -- the settings popover rows and the panel
|
|
515
|
-
header's chat | terminal switch. */
|
|
516
|
-
.seg {
|
|
517
|
-
display: flex;
|
|
518
|
-
border: 1px solid #000000;
|
|
519
|
-
border-radius: 4px;
|
|
520
|
-
overflow: hidden;
|
|
521
|
-
}
|
|
522
|
-
.seg button {
|
|
523
|
-
padding: 5px 12px;
|
|
524
|
-
font: inherit;
|
|
525
|
-
font-size: 14px;
|
|
526
|
-
background: #ffffff;
|
|
527
|
-
color: #222222;
|
|
528
|
-
border: 0;
|
|
529
|
-
cursor: pointer;
|
|
530
|
-
}
|
|
531
|
-
.seg button + button { border-left: 1px solid #000000; }
|
|
532
|
-
.seg button.active { background: #222222; color: #ffffff; }
|
|
533
|
-
body.term-dark #term-header .seg { border-color: #2a2c3d; }
|
|
534
|
-
body.term-dark #term-header .seg button { background: #1a1b26; color: #c0caf5; }
|
|
535
|
-
body.term-dark #term-header .seg button + button { border-left-color: #2a2c3d; }
|
|
536
|
-
body.term-dark #term-header .seg button.active { background: #c0caf5; color: #1a1b26; }
|
|
537
|
-
|
|
538
|
-
/* -- Agent chat view ----------------------------------------------------
|
|
539
|
-
Matches the editor chrome: Inter, 13px, 1px #e1e4e8 borders, small radii,
|
|
540
|
-
no shadows. */
|
|
541
|
-
#chat-messages {
|
|
542
|
-
flex: 1 1 0;
|
|
543
|
-
min-height: 0;
|
|
544
|
-
overflow-y: auto;
|
|
545
|
-
padding: 14px 14px 8px;
|
|
546
|
-
display: flex;
|
|
547
|
-
flex-direction: column;
|
|
548
|
-
gap: 14px;
|
|
549
|
-
/* Sits between the deck editor's desktop (16px) and narrow (13-14px)
|
|
550
|
-
text sizes -- the iframe crosses that breakpoint as panes resize. */
|
|
551
|
-
font-size: 15px;
|
|
552
|
-
line-height: 1.45;
|
|
553
|
-
/* Hide the scrollbar (Windows draws a persistent one; macOS auto-hides). */
|
|
554
|
-
scrollbar-width: none;
|
|
555
|
-
-ms-overflow-style: none;
|
|
556
|
-
}
|
|
557
|
-
#chat-messages::-webkit-scrollbar { width: 0; height: 0; }
|
|
558
|
-
#chat-empty { color: #6e7781; }
|
|
559
|
-
.msg-activity {
|
|
560
|
-
color: #6e7781;
|
|
561
|
-
font-size: 12px;
|
|
562
|
-
margin-top: 6px;
|
|
563
|
-
animation: chat-pulse 1.4s ease-in-out infinite;
|
|
564
|
-
}
|
|
565
|
-
@keyframes chat-pulse { 50% { opacity: 0.35; } }
|
|
566
|
-
.msg-interrupted { color: #6e7781; font-size: 12px; margin-top: 6px; }
|
|
567
|
-
.msg-log {
|
|
568
|
-
color: #6e7781;
|
|
569
|
-
font-size: 13px;
|
|
570
|
-
padding: 0 2px;
|
|
571
|
-
}
|
|
572
|
-
.msg-text { word-break: break-word; }
|
|
573
|
-
.msg-user .msg-text { white-space: pre-wrap; }
|
|
574
|
-
/* Markdown content (assistant replies + task notes). */
|
|
575
|
-
.msg-text > :first-child, .task-notes > :first-child { margin-top: 0; }
|
|
576
|
-
.msg-text > :last-child, .task-notes > :last-child { margin-bottom: 0; }
|
|
577
|
-
.msg-text p, .task-notes p { margin: 0.55em 0; }
|
|
578
|
-
.msg-text ul, .msg-text ol, .task-notes ul, .task-notes ol {
|
|
579
|
-
margin: 0.55em 0;
|
|
580
|
-
padding-left: 1.4em;
|
|
581
|
-
}
|
|
582
|
-
.msg-text li { margin: 0.15em 0; }
|
|
583
|
-
.msg-text h1, .msg-text h2, .msg-text h3, .msg-text h4 {
|
|
584
|
-
font-size: 1em;
|
|
585
|
-
margin: 0.7em 0 0.35em;
|
|
586
|
-
}
|
|
587
|
-
.msg-text code, .task-notes code {
|
|
588
|
-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
589
|
-
font-size: 0.85em;
|
|
590
|
-
background: #f2f4f7;
|
|
591
|
-
border-radius: 3px;
|
|
592
|
-
padding: 1px 4px;
|
|
593
|
-
}
|
|
594
|
-
.msg-text pre {
|
|
595
|
-
background: #f6f8fa;
|
|
596
|
-
border: 1px solid #e1e4e8;
|
|
597
|
-
border-radius: 4px;
|
|
598
|
-
padding: 8px 10px;
|
|
599
|
-
overflow-x: auto;
|
|
600
|
-
margin: 0.55em 0;
|
|
601
|
-
}
|
|
602
|
-
.msg-text pre code { background: none; padding: 0; }
|
|
603
|
-
.msg-image {
|
|
604
|
-
display: block;
|
|
605
|
-
max-width: 220px;
|
|
606
|
-
max-height: 150px;
|
|
607
|
-
border: 1px solid #cccccc;
|
|
608
|
-
border-radius: 4px;
|
|
609
|
-
margin-top: 6px;
|
|
610
|
-
}
|
|
611
|
-
.msg-user { align-self: flex-end; max-width: 85%; }
|
|
612
|
-
.msg-user .msg-text {
|
|
613
|
-
background: #f2f4f7;
|
|
614
|
-
border: 1px solid #cccccc;
|
|
615
|
-
border-radius: 4px;
|
|
616
|
-
padding: 8px 10px;
|
|
617
|
-
}
|
|
618
|
-
/* Each agent turn is its own bordered card -- turn boundaries read at a
|
|
619
|
-
glance instead of running together as one wall of text. */
|
|
620
|
-
.msg-assistant {
|
|
621
|
-
align-self: stretch;
|
|
622
|
-
border: 1px solid #cccccc;
|
|
623
|
-
border-radius: 4px;
|
|
624
|
-
background: #ffffff;
|
|
625
|
-
padding: 8px 10px;
|
|
626
|
-
}
|
|
627
|
-
.msg-assistant.streaming .msg-text::after {
|
|
628
|
-
content: '\\258c';
|
|
629
|
-
opacity: 0.5;
|
|
630
|
-
animation: chat-blink 1s steps(1) infinite;
|
|
631
|
-
}
|
|
632
|
-
@keyframes chat-blink { 50% { opacity: 0; } }
|
|
633
|
-
.msg-error .msg-text { color: #a40e26; }
|
|
634
|
-
.task-card {
|
|
635
|
-
border: 1px solid #e1e4e8;
|
|
636
|
-
border-radius: 6px;
|
|
637
|
-
padding: 7px 10px;
|
|
638
|
-
cursor: pointer;
|
|
639
|
-
}
|
|
640
|
-
.task-card.waiting { opacity: 0.6; }
|
|
641
|
-
/* A finished task's filled pie IS the check-off control -- click it to
|
|
642
|
-
clear the row. Visible even in bar mode (the bar has no icon to click). */
|
|
643
|
-
.task-ack-pie { cursor: pointer; }
|
|
644
|
-
.task-ack-pie:hover { box-shadow: 0 0 0 2px #cfe0ff; }
|
|
645
|
-
body.progress-bars .task-pie.task-ack-pie { display: block; }
|
|
646
|
-
.task-notes {
|
|
647
|
-
margin-top: 6px;
|
|
648
|
-
color: #57606a;
|
|
649
|
-
word-break: break-word;
|
|
650
|
-
font-size: 14px;
|
|
651
|
-
}
|
|
652
|
-
/* Live stream of a running task agent (text lines + [tool labels]) shown
|
|
653
|
-
on expand -- capped height, auto-scrolled, fading at both scroll edges. */
|
|
654
|
-
.task-feed-tool {
|
|
655
|
-
padding-left: 14px;
|
|
656
|
-
color: #8c959f;
|
|
657
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
658
|
-
font-size: 11px;
|
|
659
|
-
}
|
|
660
|
-
.task-feed-msg > :first-child { margin-top: 0; }
|
|
661
|
-
.task-feed-msg > :last-child { margin-bottom: 0; }
|
|
662
|
-
.task-feed-msg p { margin: 0.25em 0; }
|
|
663
|
-
.task-feed-msg code {
|
|
664
|
-
background: rgba(175, 184, 193, 0.2);
|
|
665
|
-
border-radius: 3px;
|
|
666
|
-
padding: 0 3px;
|
|
667
|
-
font-size: 11px;
|
|
668
|
-
}
|
|
669
|
-
.task-feed {
|
|
670
|
-
margin-top: 6px;
|
|
671
|
-
max-height: 65px;
|
|
672
|
-
overflow-y: auto;
|
|
673
|
-
/* Hide the scrollbar (Windows draws a persistent one; macOS auto-hides). */
|
|
674
|
-
scrollbar-width: none;
|
|
675
|
-
-ms-overflow-style: none;
|
|
676
|
-
font-size: 12px;
|
|
677
|
-
line-height: 1.5;
|
|
678
|
-
color: #57606a;
|
|
679
|
-
cursor: default;
|
|
680
|
-
word-break: break-word;
|
|
681
|
-
-webkit-mask-image: linear-gradient(
|
|
682
|
-
to bottom,
|
|
683
|
-
transparent,
|
|
684
|
-
#000000 5px,
|
|
685
|
-
#000000 100%
|
|
686
|
-
);
|
|
687
|
-
mask-image: linear-gradient(to bottom, transparent, #000000 5px, #000000 100%);
|
|
688
|
-
}
|
|
689
|
-
.task-feed::-webkit-scrollbar { width: 0; height: 0; }
|
|
690
|
-
.task-row { display: flex; align-items: center; gap: 8px; }
|
|
691
|
-
.task-title {
|
|
692
|
-
flex: 1 1 auto;
|
|
693
|
-
min-width: 0;
|
|
694
|
-
overflow: hidden;
|
|
695
|
-
text-overflow: ellipsis;
|
|
696
|
-
white-space: nowrap;
|
|
697
|
-
}
|
|
698
|
-
.task-title::first-letter { text-transform: uppercase; }
|
|
699
|
-
.task-status { color: #6e7781; font-size: 12px; flex: 0 0 auto; }
|
|
700
|
-
.task-status.failed, .task-status.interrupted { color: #a40e26; }
|
|
701
|
-
.task-time { color: #6e7781; font-size: 12px; flex: 0 0 auto; font-variant-numeric: tabular-nums; }
|
|
702
|
-
.task-pie {
|
|
703
|
-
width: 15px;
|
|
704
|
-
height: 15px;
|
|
705
|
-
border-radius: 50%;
|
|
706
|
-
flex: 0 0 15px;
|
|
707
|
-
border: 1px solid #d0d7de;
|
|
708
|
-
}
|
|
709
|
-
.task-bar { display: none; margin-top: 6px; height: 3px; border-radius: 2px; background: #eaeef2; overflow: hidden; }
|
|
710
|
-
.task-bar > div { height: 100%; background: #0969da; }
|
|
711
|
-
body.progress-bars .task-bar { display: block; }
|
|
712
|
-
body.progress-bars .task-pie { display: none; }
|
|
713
|
-
/* The task board: pinned above the conversation, one row per task until
|
|
714
|
-
the user checks it off. Grows freely -- hiding tasks behind a scroll
|
|
715
|
-
made them too easy to forget. Same text size as the chat. */
|
|
716
|
-
#chat-strip {
|
|
717
|
-
flex: 0 0 auto;
|
|
718
|
-
border-bottom: 1px solid #cccccc;
|
|
719
|
-
padding: 8px 14px;
|
|
720
|
-
display: flex;
|
|
721
|
-
flex-direction: column;
|
|
722
|
-
gap: 4px;
|
|
723
|
-
font-size: 15px;
|
|
724
|
-
background: #f2f4f7;
|
|
725
|
-
}
|
|
726
|
-
.task-card { background: #ffffff; }
|
|
727
|
-
/* Input row styled like the kit editor's inspector controls: 1px black
|
|
728
|
-
borders, 4px radius, same text size; the button matches the kit .button
|
|
729
|
-
(subtle drop shadow). */
|
|
730
|
-
#chat-input-row {
|
|
731
|
-
flex: 0 0 auto;
|
|
732
|
-
display: flex;
|
|
733
|
-
align-items: stretch;
|
|
734
|
-
gap: 8px;
|
|
735
|
-
padding: 10px 14px;
|
|
736
|
-
border-top: 1px solid #cccccc;
|
|
737
|
-
}
|
|
738
|
-
#chat-input {
|
|
739
|
-
flex: 1 1 auto;
|
|
740
|
-
resize: none;
|
|
741
|
-
font: inherit;
|
|
742
|
-
font-size: 15px;
|
|
743
|
-
line-height: 1.4;
|
|
744
|
-
border: 1px solid #000000;
|
|
745
|
-
border-radius: 4px;
|
|
746
|
-
padding: 5px 8px;
|
|
747
|
-
outline: none;
|
|
748
|
-
max-height: 120px;
|
|
749
|
-
background: #ffffff;
|
|
750
|
-
color: #222222;
|
|
751
|
-
/* Hide the scrollbar (Windows draws a persistent one; macOS auto-hides). */
|
|
752
|
-
scrollbar-width: none;
|
|
753
|
-
-ms-overflow-style: none;
|
|
754
|
-
}
|
|
755
|
-
#chat-input::-webkit-scrollbar { width: 0; height: 0; }
|
|
756
|
-
#chat-send {
|
|
757
|
-
font: inherit;
|
|
758
|
-
font-size: 15px;
|
|
759
|
-
padding: 6px 10px;
|
|
760
|
-
border: 1px solid #000000;
|
|
761
|
-
border-radius: 4px;
|
|
762
|
-
background: #ffffff;
|
|
763
|
-
color: #222222;
|
|
764
|
-
cursor: pointer;
|
|
765
|
-
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
766
|
-
}
|
|
767
|
-
#chat-send:hover { background: #eeeeee; }
|
|
768
|
-
/* Pending image attachments (pasted or picked) shown as chips above the
|
|
769
|
-
input; click a chip to remove it. */
|
|
770
|
-
#chat-pending {
|
|
771
|
-
flex: 0 0 auto;
|
|
772
|
-
display: flex;
|
|
773
|
-
gap: 6px;
|
|
774
|
-
padding: 8px 14px 0;
|
|
775
|
-
border-top: 1px solid #cccccc;
|
|
776
|
-
}
|
|
777
|
-
#chat-pending img {
|
|
778
|
-
height: 44px;
|
|
779
|
-
border: 1px solid #cccccc;
|
|
780
|
-
border-radius: 4px;
|
|
781
|
-
cursor: pointer;
|
|
782
|
-
}
|
|
783
|
-
#chat-pending + #chat-input-row { border-top: 0; }
|
|
784
|
-
|
|
785
|
-
/* Let the xterm light theme show through, and hide every scrollbar xterm
|
|
786
|
-
can draw -- the native viewport one AND the renderer overlay one. */
|
|
787
|
-
.xterm, .xterm-viewport, .xterm-screen, .xterm-helpers {
|
|
788
|
-
background-color: transparent !important;
|
|
789
|
-
}
|
|
790
|
-
.xterm { overscroll-behavior: contain; }
|
|
791
|
-
.xterm-viewport {
|
|
792
|
-
overscroll-behavior: contain;
|
|
793
|
-
scrollbar-width: none;
|
|
794
|
-
-ms-overflow-style: none;
|
|
795
|
-
}
|
|
796
|
-
.xterm-viewport::-webkit-scrollbar { width: 0; height: 0; }
|
|
797
|
-
.xterm .xterm-scrollable-element > .scrollbar,
|
|
798
|
-
.xterm .xterm-scrollable-element > .scrollbar.visible,
|
|
799
|
-
.xterm .xterm-scrollable-element > .visible.scrollbar,
|
|
800
|
-
.xterm .xterm-scrollable-element > .invisible.scrollbar,
|
|
801
|
-
.xterm .xterm-scrollable-element > .xterm-scrollbar {
|
|
802
|
-
display: none !important;
|
|
803
|
-
width: 0 !important;
|
|
804
|
-
height: 0 !important;
|
|
805
|
-
min-width: 0 !important;
|
|
806
|
-
min-height: 0 !important;
|
|
807
|
-
opacity: 0 !important;
|
|
808
|
-
pointer-events: none !important;
|
|
809
|
-
}
|
|
810
|
-
.xterm .xterm-scrollable-element > .scrollbar > .slider,
|
|
811
|
-
.xterm .xterm-scrollable-element > .scrollbar > .scra,
|
|
812
|
-
.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider {
|
|
813
|
-
display: none !important;
|
|
814
|
-
width: 0 !important;
|
|
815
|
-
height: 0 !important;
|
|
816
|
-
}
|
|
817
|
-
.xterm .xterm-scrollable-element > .shadow { display: none !important; }
|
|
818
|
-
`;
|
|
819
|
-
const IDE_BODY = `
|
|
820
|
-
<button id="term-settings" type="button" tabindex="-1" title="panel settings" aria-label="panel settings">${COG_ICON_SVG}</button>
|
|
821
|
-
<button id="term-toggle" type="button" tabindex="-1" title="toggle panel" aria-label="toggle panel">${HAMMER_ICON_SVG}</button>
|
|
822
|
-
<div id="term-settings-panel">
|
|
823
|
-
<div class="row">
|
|
824
|
-
<span>Router agent</span>
|
|
825
|
-
<div class="seg" id="agent-router-seg">
|
|
826
|
-
<button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
|
|
827
|
-
<button type="button" tabindex="-1" data-backend="claude">Claude</button>
|
|
828
|
-
</div>
|
|
829
|
-
</div>
|
|
830
|
-
<div class="row">
|
|
831
|
-
<span>Task agents</span>
|
|
832
|
-
<div class="seg" id="agent-tasks-seg">
|
|
833
|
-
<button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
|
|
834
|
-
<button type="button" tabindex="-1" data-backend="claude">Claude</button>
|
|
835
|
-
</div>
|
|
836
|
-
</div>
|
|
837
|
-
<div class="row">
|
|
838
|
-
<span>Claude model</span>
|
|
839
|
-
<div class="seg" id="agent-model-seg">
|
|
840
|
-
<button type="button" tabindex="-1" data-model="sonnet">Sonnet</button>
|
|
841
|
-
<button type="button" tabindex="-1" data-model="opus">Opus</button>
|
|
842
|
-
<button type="button" tabindex="-1" data-model="fable">Fable</button>
|
|
843
|
-
</div>
|
|
844
|
-
</div>
|
|
845
|
-
<div class="row">
|
|
846
|
-
<span>Progress</span>
|
|
847
|
-
<div class="seg" id="chat-progress-seg">
|
|
848
|
-
<button type="button" tabindex="-1" data-progress="pie">Pie</button>
|
|
849
|
-
<button type="button" tabindex="-1" data-progress="bar">Bar</button>
|
|
850
|
-
</div>
|
|
851
|
-
</div>
|
|
852
|
-
<div class="row">
|
|
853
|
-
<span>Terminal theme</span>
|
|
854
|
-
<div class="seg" id="term-theme-seg">
|
|
855
|
-
<button type="button" tabindex="-1" data-theme="light">Light</button>
|
|
856
|
-
<button type="button" tabindex="-1" data-theme="dark">Dark</button>
|
|
857
|
-
</div>
|
|
858
|
-
</div>
|
|
859
|
-
</div>
|
|
860
|
-
<div id="deck"><iframe id="deck-frame" src="/index.html" title="deck" allow="autoplay; clipboard-read; clipboard-write; fullscreen; gamepad"></iframe></div>
|
|
861
|
-
<div id="term">
|
|
862
|
-
<div id="term-header">
|
|
863
|
-
<div class="seg" id="panel-view-seg">
|
|
864
|
-
<button type="button" tabindex="-1" data-view="chat">Chat</button>
|
|
865
|
-
<button type="button" tabindex="-1" data-view="terminal">Terminal</button>
|
|
866
|
-
</div>
|
|
867
|
-
</div>
|
|
868
|
-
<div id="panel-body">
|
|
869
|
-
<div id="chat-host"></div>
|
|
870
|
-
<div id="term-host"></div>
|
|
871
|
-
</div>
|
|
872
|
-
</div>
|
|
873
|
-
<script src="${IDE_ASSET_PREFIX}xterm.js"></script>
|
|
874
|
-
<script src="${IDE_ASSET_PREFIX}addon-fit.js"></script>
|
|
875
|
-
<script src="${IDE_ASSET_PREFIX}react.js"></script>
|
|
876
|
-
<script src="${IDE_ASSET_PREFIX}react-dom.js"></script>
|
|
877
|
-
<script src="${IDE_ASSET_PREFIX}marked.js"></script>
|
|
878
|
-
<script type="module" src="${IDE_ASSET_PREFIX}chat-client.js"></script>
|
|
879
|
-
<script type="module" src="${IDE_ASSET_PREFIX}ide-client.js"></script>
|
|
880
|
-
`;
|
|
881
|
-
function renderIdePage(deckLabel) {
|
|
882
|
-
const title = `${deckLabel} -- Castle Editor`;
|
|
883
|
-
return `<!doctype html>
|
|
884
|
-
<html lang="en">
|
|
885
|
-
<head>
|
|
886
|
-
<meta charset="utf-8" />
|
|
887
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
888
|
-
<title>${escapeHtml(title)}</title>
|
|
889
|
-
<link rel="stylesheet" href="${IDE_ASSET_PREFIX}xterm.css" />
|
|
890
|
-
<style>${IDE_STYLES}</style>
|
|
891
|
-
</head>
|
|
892
|
-
<body class="term-open">${IDE_BODY}</body>
|
|
893
|
-
</html>
|
|
894
|
-
`;
|
|
895
|
-
}
|
|
896
|
-
function escapeHtml(value) {
|
|
897
|
-
return value.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c] ?? c);
|
|
898
|
-
}
|