castle-web-cli 0.4.10 → 0.4.12
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/agent-prompts.d.ts +31 -0
- package/dist/agent-prompts.js +100 -0
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +894 -0
- package/dist/chat-client.d.ts +1 -0
- package/dist/chat-client.js +398 -0
- package/dist/commonInstructions.d.ts +1 -0
- package/dist/commonInstructions.js +8 -0
- package/dist/ide-client.js +46 -14
- package/dist/ide.d.ts +2 -0
- package/dist/ide.js +321 -36
- package/dist/init.js +12 -2
- package/dist/serve.js +62 -3
- package/kits/basic-2d/CLAUDE.md +3 -1
- package/kits/basic-2d/package.json +0 -1
- package/kits/basic-3d/.prettierrc +8 -0
- package/kits/basic-3d/CLAUDE.md +162 -0
- package/kits/basic-3d/behaviors/Camera.jsx +56 -0
- package/kits/basic-3d/behaviors/Collider.jsx +78 -0
- package/kits/basic-3d/behaviors/Mesh.jsx +82 -0
- package/kits/basic-3d/behaviors/Model.jsx +61 -0
- package/kits/basic-3d/behaviors/Transform.jsx +35 -0
- package/kits/basic-3d/editors/App.jsx +147 -0
- package/kits/basic-3d/editors/CodeEditor.jsx +112 -0
- package/kits/basic-3d/editors/FileBrowser.jsx +143 -0
- package/kits/basic-3d/editors/ModelEditor.jsx +400 -0
- package/kits/basic-3d/editors/PlayOnly.jsx +14 -0
- package/kits/basic-3d/editors/SceneEditor.jsx +1087 -0
- package/kits/basic-3d/editors/behaviorRegistry.js +24 -0
- package/kits/basic-3d/editors/editorHistory.js +52 -0
- package/kits/basic-3d/editors/viewportRig.js +90 -0
- package/kits/basic-3d/engine/ScenePlayer.jsx +55 -0
- package/kits/basic-3d/engine/SceneUI.jsx +67 -0
- package/kits/basic-3d/engine/SceneViewport.jsx +102 -0
- package/kits/basic-3d/engine/TouchControls.jsx +136 -0
- package/kits/basic-3d/engine/autoInspector.jsx +51 -0
- package/kits/basic-3d/engine/files.js +73 -0
- package/kits/basic-3d/engine/scene.js +502 -0
- package/kits/basic-3d/engine/threeUtil.js +260 -0
- package/kits/basic-3d/engine/ui.jsx +352 -0
- package/kits/basic-3d/engine/ui.module.css +944 -0
- package/kits/basic-3d/eslint.config.js +51 -0
- package/kits/basic-3d/index.html +11 -0
- package/kits/basic-3d/main.jsx +10 -0
- package/kits/basic-3d/models/block.model +14 -0
- package/kits/basic-3d/package-lock.json +2713 -0
- package/kits/basic-3d/package.json +41 -0
- package/kits/basic-3d/scenes/main.scene +76 -0
- package/kits/basic-3d/vite.config.js +1 -0
- package/package.json +6 -1
package/dist/ide.js
CHANGED
|
@@ -27,10 +27,11 @@ const SCROLLBACK = 4000;
|
|
|
27
27
|
// upgrade handler on Vite's HTTP server).
|
|
28
28
|
export const IDE_ASSET_PREFIX = '/__castle/ide/';
|
|
29
29
|
export const PTY_WS_PATH = '/__castle/pty';
|
|
30
|
-
// Injected into every served deck's <head>. When the deck runs embedded in
|
|
31
|
-
// IDE shell iframe, Ctrl+T moves keyboard focus out to the
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// Injected into every served deck's <head>. When the deck runs embedded in
|
|
31
|
+
// the IDE shell iframe, Ctrl+T moves keyboard focus out to the panel (the
|
|
32
|
+
// chat input or the terminal, whichever view is active) -- but while the
|
|
33
|
+
// iframe holds focus the parent sees no keydowns, so the deck page has to
|
|
34
|
+
// forward Ctrl+T itself. No-op for a standalone deck.
|
|
34
35
|
export const DECK_FOCUS_SCRIPT = `<script>(function(){if(window.parent===window)return;` +
|
|
35
36
|
`document.addEventListener('keydown',function(e){` +
|
|
36
37
|
`if(e.ctrlKey&&!e.metaKey&&!e.altKey&&!e.shiftKey&&(e.key==='t'||e.key==='T')){` +
|
|
@@ -62,7 +63,7 @@ function clampSize(value, fallback) {
|
|
|
62
63
|
return fallback;
|
|
63
64
|
return Math.max(2, Math.min(1000, Math.floor(n)));
|
|
64
65
|
}
|
|
65
|
-
function rawDataToString(data) {
|
|
66
|
+
export function rawDataToString(data) {
|
|
66
67
|
if (Array.isArray(data))
|
|
67
68
|
return Buffer.concat(data).toString('utf8');
|
|
68
69
|
if (Buffer.isBuffer(data))
|
|
@@ -287,16 +288,38 @@ function resolveIdeAsset(asset) {
|
|
|
287
288
|
return null;
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
// React's package "exports" hides umd/ from require.resolve; resolve the
|
|
292
|
+
// package root via its package.json (always exported) and join from there.
|
|
293
|
+
const umdAssets = {
|
|
294
|
+
'react.js': { pkg: 'react', rel: 'umd/react.production.min.js' },
|
|
295
|
+
'react-dom.js': { pkg: 'react-dom', rel: 'umd/react-dom.production.min.js' },
|
|
296
|
+
'marked.js': { pkg: 'marked', rel: 'lib/marked.umd.js' },
|
|
297
|
+
};
|
|
298
|
+
const fromUmd = umdAssets[asset];
|
|
299
|
+
if (fromUmd) {
|
|
300
|
+
try {
|
|
301
|
+
const pkgRoot = path.dirname(require_.resolve(`${fromUmd.pkg}/package.json`));
|
|
302
|
+
const filePath = path.join(pkgRoot, fromUmd.rel);
|
|
303
|
+
return fs.existsSync(filePath)
|
|
304
|
+
? { filePath, contentType: 'text/javascript; charset=utf-8' }
|
|
305
|
+
: null;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (asset === 'ide-client.js' || asset === 'chat-client.js') {
|
|
312
|
+
const filePath = path.join(DIST_DIR, asset);
|
|
292
313
|
return fs.existsSync(filePath)
|
|
293
314
|
? { filePath, contentType: 'text/javascript; charset=utf-8' }
|
|
294
315
|
: null;
|
|
295
316
|
}
|
|
296
317
|
return null;
|
|
297
318
|
}
|
|
298
|
-
// Lucide glyphs; currentColor so the button :hover styles drive them.
|
|
299
|
-
|
|
319
|
+
// Lucide glyphs; currentColor so the button :hover styles drive them. The
|
|
320
|
+
// panel toggle reads as "building" (hammer), not terminal -- the terminal is
|
|
321
|
+
// just one view inside the panel.
|
|
322
|
+
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>`;
|
|
300
323
|
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>`;
|
|
301
324
|
const IDE_STYLES = `
|
|
302
325
|
:root { color-scheme: light; }
|
|
@@ -311,7 +334,10 @@ const IDE_STYLES = `
|
|
|
311
334
|
--term-header-h: 48px;
|
|
312
335
|
background: #ffffff;
|
|
313
336
|
color: #1f2328;
|
|
314
|
-
|
|
337
|
+
/* Same stack as the kit editor (engine/ui.module.css --castle-font-body)
|
|
338
|
+
so the shell + chat read as one app with the deck's editor. */
|
|
339
|
+
font-family: baltomobile, Balto, ui-sans-serif, system-ui, -apple-system,
|
|
340
|
+
BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
315
341
|
display: flex;
|
|
316
342
|
overflow: hidden;
|
|
317
343
|
}
|
|
@@ -336,15 +362,29 @@ const IDE_STYLES = `
|
|
|
336
362
|
flex-direction: column;
|
|
337
363
|
}
|
|
338
364
|
body.term-dark #term { background: #1a1b26; }
|
|
339
|
-
/*
|
|
340
|
-
|
|
341
|
-
|
|
365
|
+
/* Panel content area below the header. The chat view and the terminal view
|
|
366
|
+
are both mounted, absolutely filling it; the inactive one is lifted to
|
|
367
|
+
visibility:hidden (not display:none) so xterm keeps its measured layout
|
|
368
|
+
-- same trick as the closed-panel state below. */
|
|
369
|
+
#panel-body {
|
|
342
370
|
flex: 1 1 0;
|
|
343
371
|
min-height: 0;
|
|
344
|
-
|
|
372
|
+
position: relative;
|
|
373
|
+
}
|
|
374
|
+
#term-host {
|
|
375
|
+
position: absolute;
|
|
376
|
+
inset: 0;
|
|
345
377
|
padding: 6px;
|
|
346
378
|
overscroll-behavior: contain;
|
|
347
379
|
}
|
|
380
|
+
body:not(.term-view) #term-host { visibility: hidden; }
|
|
381
|
+
#chat-host {
|
|
382
|
+
position: absolute;
|
|
383
|
+
inset: 0;
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
}
|
|
387
|
+
body.term-view #chat-host { visibility: hidden; }
|
|
348
388
|
|
|
349
389
|
/* Header strip for the terminal panel -- mirrors the deck's editor header
|
|
350
390
|
(same height, 1px bottom border, white background, 16px title). A real
|
|
@@ -362,13 +402,6 @@ const IDE_STYLES = `
|
|
|
362
402
|
}
|
|
363
403
|
body.term-open #term-header { display: flex; }
|
|
364
404
|
body.term-dark #term-header { background: #1a1b26; border-bottom-color: #2a2c3d; }
|
|
365
|
-
#term-header .term-header-title {
|
|
366
|
-
font-size: 16px;
|
|
367
|
-
line-height: 1;
|
|
368
|
-
letter-spacing: 0.01em;
|
|
369
|
-
color: #222222;
|
|
370
|
-
}
|
|
371
|
-
body.term-dark #term-header .term-header-title { color: #c0caf5; }
|
|
372
405
|
|
|
373
406
|
/* Terminal hidden: the panel stays mounted at its full 500px size (so xterm
|
|
374
407
|
keeps its measured layout) -- just lifted out of flow and made invisible,
|
|
@@ -441,15 +474,16 @@ const IDE_STYLES = `
|
|
|
441
474
|
right: 7px;
|
|
442
475
|
z-index: 2147483000;
|
|
443
476
|
display: none;
|
|
444
|
-
width:
|
|
445
|
-
padding:
|
|
477
|
+
width: 300px;
|
|
478
|
+
padding: 10px 12px;
|
|
446
479
|
background: #ffffff;
|
|
447
480
|
color: #222222;
|
|
448
481
|
border: 1px solid #000000;
|
|
449
482
|
border-radius: 4px;
|
|
450
483
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
451
|
-
font-size:
|
|
484
|
+
font-size: 13px;
|
|
452
485
|
}
|
|
486
|
+
#term-settings-panel .row > span { white-space: nowrap; }
|
|
453
487
|
body.term-settings-open #term-settings-panel { display: block; }
|
|
454
488
|
#term-settings-panel .row {
|
|
455
489
|
display: flex;
|
|
@@ -457,22 +491,229 @@ const IDE_STYLES = `
|
|
|
457
491
|
justify-content: space-between;
|
|
458
492
|
gap: 10px;
|
|
459
493
|
}
|
|
460
|
-
#term-settings-panel .
|
|
494
|
+
#term-settings-panel .row + .row { margin-top: 8px; }
|
|
495
|
+
/* Shared segmented control -- the settings popover rows and the panel
|
|
496
|
+
header's chat | terminal switch. */
|
|
497
|
+
.seg {
|
|
461
498
|
display: flex;
|
|
462
499
|
border: 1px solid #000000;
|
|
463
500
|
border-radius: 4px;
|
|
464
501
|
overflow: hidden;
|
|
465
502
|
}
|
|
466
|
-
|
|
467
|
-
padding:
|
|
503
|
+
.seg button {
|
|
504
|
+
padding: 5px 12px;
|
|
468
505
|
font: inherit;
|
|
506
|
+
font-size: 14px;
|
|
469
507
|
background: #ffffff;
|
|
470
508
|
color: #222222;
|
|
471
509
|
border: 0;
|
|
472
510
|
cursor: pointer;
|
|
473
511
|
}
|
|
474
|
-
|
|
475
|
-
|
|
512
|
+
.seg button + button { border-left: 1px solid #000000; }
|
|
513
|
+
.seg button.active { background: #222222; color: #ffffff; }
|
|
514
|
+
body.term-dark #term-header .seg { border-color: #2a2c3d; }
|
|
515
|
+
body.term-dark #term-header .seg button { background: #1a1b26; color: #c0caf5; }
|
|
516
|
+
body.term-dark #term-header .seg button + button { border-left-color: #2a2c3d; }
|
|
517
|
+
body.term-dark #term-header .seg button.active { background: #c0caf5; color: #1a1b26; }
|
|
518
|
+
|
|
519
|
+
/* -- Agent chat view ----------------------------------------------------
|
|
520
|
+
Matches the editor chrome: Inter, 13px, 1px #e1e4e8 borders, small radii,
|
|
521
|
+
no shadows. */
|
|
522
|
+
#chat-messages {
|
|
523
|
+
flex: 1 1 0;
|
|
524
|
+
min-height: 0;
|
|
525
|
+
overflow-y: auto;
|
|
526
|
+
padding: 14px 14px 8px;
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
gap: 14px;
|
|
530
|
+
/* Sits between the deck editor's desktop (16px) and narrow (13-14px)
|
|
531
|
+
text sizes -- the iframe crosses that breakpoint as panes resize. */
|
|
532
|
+
font-size: 15px;
|
|
533
|
+
line-height: 1.45;
|
|
534
|
+
}
|
|
535
|
+
#chat-empty { color: #6e7781; }
|
|
536
|
+
.msg-activity {
|
|
537
|
+
color: #6e7781;
|
|
538
|
+
font-size: 12px;
|
|
539
|
+
margin-top: 6px;
|
|
540
|
+
animation: chat-pulse 1.4s ease-in-out infinite;
|
|
541
|
+
}
|
|
542
|
+
@keyframes chat-pulse { 50% { opacity: 0.35; } }
|
|
543
|
+
.msg-interrupted { color: #6e7781; font-size: 12px; margin-top: 6px; }
|
|
544
|
+
.msg-log {
|
|
545
|
+
color: #6e7781;
|
|
546
|
+
font-size: 13px;
|
|
547
|
+
padding: 0 2px;
|
|
548
|
+
}
|
|
549
|
+
.msg-text { word-break: break-word; }
|
|
550
|
+
.msg-user .msg-text { white-space: pre-wrap; }
|
|
551
|
+
/* Markdown content (assistant replies + task notes). */
|
|
552
|
+
.msg-text > :first-child, .task-notes > :first-child { margin-top: 0; }
|
|
553
|
+
.msg-text > :last-child, .task-notes > :last-child { margin-bottom: 0; }
|
|
554
|
+
.msg-text p, .task-notes p { margin: 0.55em 0; }
|
|
555
|
+
.msg-text ul, .msg-text ol, .task-notes ul, .task-notes ol {
|
|
556
|
+
margin: 0.55em 0;
|
|
557
|
+
padding-left: 1.4em;
|
|
558
|
+
}
|
|
559
|
+
.msg-text li { margin: 0.15em 0; }
|
|
560
|
+
.msg-text h1, .msg-text h2, .msg-text h3, .msg-text h4 {
|
|
561
|
+
font-size: 1em;
|
|
562
|
+
margin: 0.7em 0 0.35em;
|
|
563
|
+
}
|
|
564
|
+
.msg-text code, .task-notes code {
|
|
565
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
566
|
+
font-size: 0.85em;
|
|
567
|
+
background: #f2f4f7;
|
|
568
|
+
border-radius: 3px;
|
|
569
|
+
padding: 1px 4px;
|
|
570
|
+
}
|
|
571
|
+
.msg-text pre {
|
|
572
|
+
background: #f6f8fa;
|
|
573
|
+
border: 1px solid #e1e4e8;
|
|
574
|
+
border-radius: 4px;
|
|
575
|
+
padding: 8px 10px;
|
|
576
|
+
overflow-x: auto;
|
|
577
|
+
margin: 0.55em 0;
|
|
578
|
+
}
|
|
579
|
+
.msg-text pre code { background: none; padding: 0; }
|
|
580
|
+
.msg-image {
|
|
581
|
+
display: block;
|
|
582
|
+
max-width: 220px;
|
|
583
|
+
max-height: 150px;
|
|
584
|
+
border: 1px solid #cccccc;
|
|
585
|
+
border-radius: 4px;
|
|
586
|
+
margin-top: 6px;
|
|
587
|
+
}
|
|
588
|
+
.msg-user { align-self: flex-end; max-width: 85%; }
|
|
589
|
+
.msg-user .msg-text {
|
|
590
|
+
background: #f2f4f7;
|
|
591
|
+
border: 1px solid #cccccc;
|
|
592
|
+
border-radius: 4px;
|
|
593
|
+
padding: 8px 10px;
|
|
594
|
+
}
|
|
595
|
+
/* Each agent turn is its own bordered card -- turn boundaries read at a
|
|
596
|
+
glance instead of running together as one wall of text. */
|
|
597
|
+
.msg-assistant {
|
|
598
|
+
align-self: stretch;
|
|
599
|
+
border: 1px solid #cccccc;
|
|
600
|
+
border-radius: 4px;
|
|
601
|
+
background: #ffffff;
|
|
602
|
+
padding: 8px 10px;
|
|
603
|
+
}
|
|
604
|
+
.msg-assistant.streaming .msg-text::after {
|
|
605
|
+
content: '\\258c';
|
|
606
|
+
opacity: 0.5;
|
|
607
|
+
animation: chat-blink 1s steps(1) infinite;
|
|
608
|
+
}
|
|
609
|
+
@keyframes chat-blink { 50% { opacity: 0; } }
|
|
610
|
+
.msg-error .msg-text { color: #a40e26; }
|
|
611
|
+
.task-card {
|
|
612
|
+
border: 1px solid #e1e4e8;
|
|
613
|
+
border-radius: 6px;
|
|
614
|
+
padding: 7px 10px;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
}
|
|
617
|
+
.task-card.waiting { opacity: 0.6; }
|
|
618
|
+
/* A finished task's filled pie IS the check-off control -- click it to
|
|
619
|
+
clear the row. Visible even in bar mode (the bar has no icon to click). */
|
|
620
|
+
.task-ack-pie { cursor: pointer; }
|
|
621
|
+
.task-ack-pie:hover { box-shadow: 0 0 0 2px #cfe0ff; }
|
|
622
|
+
body.progress-bars .task-pie.task-ack-pie { display: block; }
|
|
623
|
+
.task-notes {
|
|
624
|
+
margin-top: 6px;
|
|
625
|
+
color: #57606a;
|
|
626
|
+
word-break: break-word;
|
|
627
|
+
font-size: 14px;
|
|
628
|
+
}
|
|
629
|
+
.task-row { display: flex; align-items: center; gap: 8px; }
|
|
630
|
+
.task-title {
|
|
631
|
+
flex: 1 1 auto;
|
|
632
|
+
min-width: 0;
|
|
633
|
+
overflow: hidden;
|
|
634
|
+
text-overflow: ellipsis;
|
|
635
|
+
white-space: nowrap;
|
|
636
|
+
}
|
|
637
|
+
.task-status { color: #6e7781; font-size: 12px; flex: 0 0 auto; }
|
|
638
|
+
.task-status.failed, .task-status.interrupted { color: #a40e26; }
|
|
639
|
+
.task-pie {
|
|
640
|
+
width: 15px;
|
|
641
|
+
height: 15px;
|
|
642
|
+
border-radius: 50%;
|
|
643
|
+
flex: 0 0 15px;
|
|
644
|
+
border: 1px solid #d0d7de;
|
|
645
|
+
}
|
|
646
|
+
.task-bar { display: none; margin-top: 6px; height: 3px; border-radius: 2px; background: #eaeef2; overflow: hidden; }
|
|
647
|
+
.task-bar > div { height: 100%; background: #0969da; }
|
|
648
|
+
body.progress-bars .task-bar { display: block; }
|
|
649
|
+
body.progress-bars .task-pie { display: none; }
|
|
650
|
+
/* The task board: pinned above the conversation, one row per task until
|
|
651
|
+
the user checks it off. Grows freely -- hiding tasks behind a scroll
|
|
652
|
+
made them too easy to forget. Same text size as the chat. */
|
|
653
|
+
#chat-strip {
|
|
654
|
+
flex: 0 0 auto;
|
|
655
|
+
border-bottom: 1px solid #cccccc;
|
|
656
|
+
padding: 8px 14px;
|
|
657
|
+
display: flex;
|
|
658
|
+
flex-direction: column;
|
|
659
|
+
gap: 4px;
|
|
660
|
+
font-size: 15px;
|
|
661
|
+
background: #f2f4f7;
|
|
662
|
+
}
|
|
663
|
+
.task-card { background: #ffffff; }
|
|
664
|
+
/* Input row styled like the kit editor's inspector controls: 1px black
|
|
665
|
+
borders, 4px radius, same text size; the button matches the kit .button
|
|
666
|
+
(subtle drop shadow). */
|
|
667
|
+
#chat-input-row {
|
|
668
|
+
flex: 0 0 auto;
|
|
669
|
+
display: flex;
|
|
670
|
+
align-items: stretch;
|
|
671
|
+
gap: 8px;
|
|
672
|
+
padding: 10px 14px;
|
|
673
|
+
border-top: 1px solid #cccccc;
|
|
674
|
+
}
|
|
675
|
+
#chat-input {
|
|
676
|
+
flex: 1 1 auto;
|
|
677
|
+
resize: none;
|
|
678
|
+
font: inherit;
|
|
679
|
+
font-size: 15px;
|
|
680
|
+
line-height: 1.4;
|
|
681
|
+
border: 1px solid #000000;
|
|
682
|
+
border-radius: 4px;
|
|
683
|
+
padding: 5px 8px;
|
|
684
|
+
outline: none;
|
|
685
|
+
max-height: 120px;
|
|
686
|
+
background: #ffffff;
|
|
687
|
+
color: #222222;
|
|
688
|
+
}
|
|
689
|
+
#chat-send {
|
|
690
|
+
font: inherit;
|
|
691
|
+
font-size: 15px;
|
|
692
|
+
padding: 6px 10px;
|
|
693
|
+
border: 1px solid #000000;
|
|
694
|
+
border-radius: 4px;
|
|
695
|
+
background: #ffffff;
|
|
696
|
+
color: #222222;
|
|
697
|
+
cursor: pointer;
|
|
698
|
+
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
|
699
|
+
}
|
|
700
|
+
#chat-send:hover { background: #eeeeee; }
|
|
701
|
+
/* Pending image attachments (pasted or picked) shown as chips above the
|
|
702
|
+
input; click a chip to remove it. */
|
|
703
|
+
#chat-pending {
|
|
704
|
+
flex: 0 0 auto;
|
|
705
|
+
display: flex;
|
|
706
|
+
gap: 6px;
|
|
707
|
+
padding: 8px 14px 0;
|
|
708
|
+
border-top: 1px solid #cccccc;
|
|
709
|
+
}
|
|
710
|
+
#chat-pending img {
|
|
711
|
+
height: 44px;
|
|
712
|
+
border: 1px solid #cccccc;
|
|
713
|
+
border-radius: 4px;
|
|
714
|
+
cursor: pointer;
|
|
715
|
+
}
|
|
716
|
+
#chat-pending + #chat-input-row { border-top: 0; }
|
|
476
717
|
|
|
477
718
|
/* Let the xterm light theme show through, and hide every scrollbar xterm
|
|
478
719
|
can draw -- the native viewport one AND the renderer overlay one. */
|
|
@@ -509,21 +750,65 @@ const IDE_STYLES = `
|
|
|
509
750
|
.xterm .xterm-scrollable-element > .shadow { display: none !important; }
|
|
510
751
|
`;
|
|
511
752
|
const IDE_BODY = `
|
|
512
|
-
<button id="term-settings" type="button" tabindex="-1" title="
|
|
513
|
-
<button id="term-toggle" type="button" tabindex="-1" title="toggle
|
|
753
|
+
<button id="term-settings" type="button" tabindex="-1" title="panel settings" aria-label="panel settings">${COG_ICON_SVG}</button>
|
|
754
|
+
<button id="term-toggle" type="button" tabindex="-1" title="toggle panel" aria-label="toggle panel">${HAMMER_ICON_SVG}</button>
|
|
514
755
|
<div id="term-settings-panel">
|
|
515
756
|
<div class="row">
|
|
516
|
-
<span>
|
|
757
|
+
<span>Router agent</span>
|
|
758
|
+
<div class="seg" id="agent-router-seg">
|
|
759
|
+
<button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
|
|
760
|
+
<button type="button" tabindex="-1" data-backend="claude">Claude</button>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
<div class="row">
|
|
764
|
+
<span>Task agents</span>
|
|
765
|
+
<div class="seg" id="agent-tasks-seg">
|
|
766
|
+
<button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
|
|
767
|
+
<button type="button" tabindex="-1" data-backend="claude">Claude</button>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="row">
|
|
771
|
+
<span>Claude model</span>
|
|
772
|
+
<div class="seg" id="agent-model-seg">
|
|
773
|
+
<button type="button" tabindex="-1" data-model="sonnet">Sonnet</button>
|
|
774
|
+
<button type="button" tabindex="-1" data-model="opus">Opus</button>
|
|
775
|
+
<button type="button" tabindex="-1" data-model="fable">Fable</button>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="row">
|
|
779
|
+
<span>Progress</span>
|
|
780
|
+
<div class="seg" id="chat-progress-seg">
|
|
781
|
+
<button type="button" tabindex="-1" data-progress="pie">Pie</button>
|
|
782
|
+
<button type="button" tabindex="-1" data-progress="bar">Bar</button>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="row">
|
|
786
|
+
<span>Terminal theme</span>
|
|
517
787
|
<div class="seg" id="term-theme-seg">
|
|
518
|
-
<button type="button" tabindex="-1" data-theme="light">
|
|
519
|
-
<button type="button" tabindex="-1" data-theme="dark">
|
|
788
|
+
<button type="button" tabindex="-1" data-theme="light">Light</button>
|
|
789
|
+
<button type="button" tabindex="-1" data-theme="dark">Dark</button>
|
|
520
790
|
</div>
|
|
521
791
|
</div>
|
|
522
792
|
</div>
|
|
523
793
|
<div id="deck"><iframe id="deck-frame" src="/index.html" title="deck" allow="autoplay; clipboard-read; clipboard-write; fullscreen; gamepad"></iframe></div>
|
|
524
|
-
<div id="term"
|
|
794
|
+
<div id="term">
|
|
795
|
+
<div id="term-header">
|
|
796
|
+
<div class="seg" id="panel-view-seg">
|
|
797
|
+
<button type="button" tabindex="-1" data-view="chat">Chat</button>
|
|
798
|
+
<button type="button" tabindex="-1" data-view="terminal">Terminal</button>
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
<div id="panel-body">
|
|
802
|
+
<div id="chat-host"></div>
|
|
803
|
+
<div id="term-host"></div>
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
525
806
|
<script src="${IDE_ASSET_PREFIX}xterm.js"></script>
|
|
526
807
|
<script src="${IDE_ASSET_PREFIX}addon-fit.js"></script>
|
|
808
|
+
<script src="${IDE_ASSET_PREFIX}react.js"></script>
|
|
809
|
+
<script src="${IDE_ASSET_PREFIX}react-dom.js"></script>
|
|
810
|
+
<script src="${IDE_ASSET_PREFIX}marked.js"></script>
|
|
811
|
+
<script type="module" src="${IDE_ASSET_PREFIX}chat-client.js"></script>
|
|
527
812
|
<script type="module" src="${IDE_ASSET_PREFIX}ide-client.js"></script>
|
|
528
813
|
`;
|
|
529
814
|
function renderIdePage(deckLabel) {
|
|
@@ -537,7 +822,7 @@ function renderIdePage(deckLabel) {
|
|
|
537
822
|
<link rel="stylesheet" href="${IDE_ASSET_PREFIX}xterm.css" />
|
|
538
823
|
<style>${IDE_STYLES}</style>
|
|
539
824
|
</head>
|
|
540
|
-
<body>${IDE_BODY}</body>
|
|
825
|
+
<body class="term-open">${IDE_BODY}</body>
|
|
541
826
|
</html>
|
|
542
827
|
`;
|
|
543
828
|
}
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { COMMON_INSTRUCTIONS } from './commonInstructions.js';
|
|
4
5
|
import { getCliEntryPath, getKitsDir, getRepoRoot, getSdkPackagePath, toPosixPath } from './localPaths.js';
|
|
5
6
|
import { serve } from './serve.js';
|
|
6
7
|
const INDEX_HTML = `<!DOCTYPE html>
|
|
@@ -34,7 +35,7 @@ const DEFAULT_KIT = 'basic-2d';
|
|
|
34
35
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
35
36
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
36
37
|
// alongside cli/sdk version bumps.
|
|
37
|
-
const PUBLISHED_SDK_VERSION = '0.4.
|
|
38
|
+
const PUBLISHED_SDK_VERSION = '0.4.3';
|
|
38
39
|
// Never copied into a fresh deck: build/dependency junk, and castle.json (a
|
|
39
40
|
// fresh deck has no deckId until its first save-deck).
|
|
40
41
|
const KIT_COPY_EXCLUDE = new Set(['node_modules', '.castle', 'dist', '.git', 'castle.json']);
|
|
@@ -54,6 +55,14 @@ function makeClaudeMd() {
|
|
|
54
55
|
return `# Castle Experimental Web\n\nSee https://github.com/castle-xyz/castle-experimental-web for the agent guide.\n`;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
// Append the cli-owned common guidance to the deck's CLAUDE.md. Runs after
|
|
59
|
+
// the kit's (or bare) CLAUDE.md is written; the AGENTS.md symlink picks the
|
|
60
|
+
// appended content up for free.
|
|
61
|
+
function appendCommonInstructions(projectDir) {
|
|
62
|
+
const claudePath = path.join(projectDir, 'CLAUDE.md');
|
|
63
|
+
const existing = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf8').trimEnd() + '\n\n' : '';
|
|
64
|
+
fs.writeFileSync(claudePath, existing + COMMON_INSTRUCTIONS);
|
|
65
|
+
}
|
|
57
66
|
function tryMakeAgentsSymlink(agentsPath) {
|
|
58
67
|
try {
|
|
59
68
|
fs.symlinkSync('CLAUDE.md', agentsPath);
|
|
@@ -70,7 +79,6 @@ function makePackageJson(projectDir) {
|
|
|
70
79
|
private: true,
|
|
71
80
|
type: 'module',
|
|
72
81
|
scripts: {
|
|
73
|
-
serve: `node ${cliEntry} serve . --open`,
|
|
74
82
|
restart: `node ${cliEntry} restart .`,
|
|
75
83
|
screenshot: `node ${cliEntry} screenshot .`,
|
|
76
84
|
'save-deck': `node ${cliEntry} save-deck .`,
|
|
@@ -97,6 +105,7 @@ function scaffoldBare(projectDir) {
|
|
|
97
105
|
fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
|
|
98
106
|
fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
|
|
99
107
|
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
|
|
108
|
+
appendCommonInstructions(projectDir);
|
|
100
109
|
ensureAgentsSymlink(projectDir);
|
|
101
110
|
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
|
|
102
111
|
}
|
|
@@ -187,6 +196,7 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
187
196
|
if (!fs.existsSync(claudePath)) {
|
|
188
197
|
fs.writeFileSync(claudePath, makeClaudeMd());
|
|
189
198
|
}
|
|
199
|
+
appendCommonInstructions(projectDir);
|
|
190
200
|
ensureAgentsSymlink(projectDir);
|
|
191
201
|
}
|
|
192
202
|
export async function init(dir, opts = {}) {
|
package/dist/serve.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from 'child_process';
|
|
|
5
5
|
import { createServer } from 'vite';
|
|
6
6
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
7
|
import { createIdeServer, DECK_FOCUS_SCRIPT } from './ide.js';
|
|
8
|
+
import { createAgentServer } from './agent.js';
|
|
8
9
|
import * as config from './config.js';
|
|
9
10
|
function isPortFree(port) {
|
|
10
11
|
return new Promise((resolve) => {
|
|
@@ -21,7 +22,7 @@ async function findFreePorts(startPort) {
|
|
|
21
22
|
}
|
|
22
23
|
throw new Error(`No free port pair found starting from ${startPort}`);
|
|
23
24
|
}
|
|
24
|
-
function castlePlugin(wsPort, ideServer) {
|
|
25
|
+
function castlePlugin(wsPort, ideServer, agentServer) {
|
|
25
26
|
return {
|
|
26
27
|
name: 'castle-dev',
|
|
27
28
|
transformIndexHtml: {
|
|
@@ -54,6 +55,8 @@ function castlePlugin(wsPort, ideServer) {
|
|
|
54
55
|
const reqPath = req.url.split('?')[0];
|
|
55
56
|
if (ideServer.handleHttpRequest(req, res, reqPath))
|
|
56
57
|
return;
|
|
58
|
+
if (agentServer.handleHttpRequest(req, res, reqPath))
|
|
59
|
+
return;
|
|
57
60
|
}
|
|
58
61
|
next();
|
|
59
62
|
});
|
|
@@ -74,6 +77,38 @@ function readDeckContext(projectDir) {
|
|
|
74
77
|
}
|
|
75
78
|
const DEFAULT_PORT = 5757;
|
|
76
79
|
const SCREENSHOT_REQUEST_TTL_MS = 15_000;
|
|
80
|
+
function readServeJson(projectDir) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(path.join(projectDir, '.castle', 'serve.json'), 'utf8');
|
|
83
|
+
const info = JSON.parse(raw);
|
|
84
|
+
if (typeof info.port !== 'number' || typeof info.wsPort !== 'number' || typeof info.pid !== 'number')
|
|
85
|
+
return null;
|
|
86
|
+
return { port: info.port, wsPort: info.wsPort, pid: info.pid };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isPidAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Return the live serve already owning this deck, if any. Lets repeated
|
|
102
|
+
// `castle-web serve`/`npm run serve` calls become idempotent instead of
|
|
103
|
+
// piling up independent vite instances on adjacent ports.
|
|
104
|
+
function existingServe(projectDir) {
|
|
105
|
+
const info = readServeJson(projectDir);
|
|
106
|
+
if (!info)
|
|
107
|
+
return null;
|
|
108
|
+
if (!isPidAlive(info.pid))
|
|
109
|
+
return null;
|
|
110
|
+
return info;
|
|
111
|
+
}
|
|
77
112
|
export async function serve(dir, options = {}) {
|
|
78
113
|
const projectDir = path.resolve(dir);
|
|
79
114
|
if (!fs.existsSync(projectDir)) {
|
|
@@ -84,6 +119,18 @@ export async function serve(dir, options = {}) {
|
|
|
84
119
|
console.error(`No index.html found in ${projectDir}`);
|
|
85
120
|
process.exit(1);
|
|
86
121
|
}
|
|
122
|
+
// If a live serve is already running for this deck, reuse it. Skip the
|
|
123
|
+
// dedup when --port / --host are passed (caller wants those specifics, so
|
|
124
|
+
// start a fresh serve even if another is up).
|
|
125
|
+
if (!options.port && !options.host) {
|
|
126
|
+
const existing = existingServe(projectDir);
|
|
127
|
+
if (existing) {
|
|
128
|
+
console.log(`Serve already running for ${projectDir}`);
|
|
129
|
+
console.log(`URL: http://localhost:${existing.port}`);
|
|
130
|
+
console.log(`PID: ${existing.pid}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
87
134
|
// --detach is kept for back-compat. Agent evals (and any harness running
|
|
88
135
|
// castle-web in a context that can't manage a foreground process) should
|
|
89
136
|
// prefer the CASTLE_WEB_CLI_DETACH=1 env var instead.
|
|
@@ -135,13 +182,23 @@ export async function serve(dir, options = {}) {
|
|
|
135
182
|
deckLabel: path.basename(projectDir),
|
|
136
183
|
});
|
|
137
184
|
process.on('exit', () => ideServer.shutdown());
|
|
185
|
+
// The agent panel backend: router conversation + background task agents,
|
|
186
|
+
// state under .castle/agent/. Its WebSocket shares Vite's HTTP server.
|
|
187
|
+
const agentServer = createAgentServer({
|
|
188
|
+
deckDir: projectDir,
|
|
189
|
+
deckLabel: path.basename(projectDir),
|
|
190
|
+
});
|
|
191
|
+
process.on('exit', () => agentServer.shutdown());
|
|
138
192
|
const vite = await createServer({
|
|
139
193
|
root: projectDir,
|
|
140
|
-
plugins: [castlePlugin(wsPort, ideServer)],
|
|
194
|
+
plugins: [castlePlugin(wsPort, ideServer, agentServer)],
|
|
141
195
|
server: {
|
|
142
196
|
port,
|
|
143
197
|
strictPort: true,
|
|
144
198
|
host: options.host,
|
|
199
|
+
// The serve is reached via tailnet/LAN hostnames (not just localhost);
|
|
200
|
+
// vite's host check would reject those.
|
|
201
|
+
allowedHosts: true,
|
|
145
202
|
open: options.open ? true : undefined,
|
|
146
203
|
hmr: false,
|
|
147
204
|
proxy: {
|
|
@@ -160,7 +217,9 @@ export async function serve(dir, options = {}) {
|
|
|
160
217
|
// than through Vite's proxy (Vite's ws proxy didn't forward this path).
|
|
161
218
|
if (vite.httpServer) {
|
|
162
219
|
vite.httpServer.on('upgrade', (req, socket, head) => {
|
|
163
|
-
ideServer.handleUpgrade(req, socket, head)
|
|
220
|
+
if (!ideServer.handleUpgrade(req, socket, head)) {
|
|
221
|
+
agentServer.handleUpgrade(req, socket, head);
|
|
222
|
+
}
|
|
164
223
|
});
|
|
165
224
|
}
|
|
166
225
|
vite.printUrls();
|
package/kits/basic-2d/CLAUDE.md
CHANGED
|
@@ -111,9 +111,11 @@ Rules: every actor needs a unique `id` (any string) and almost always a `Layout`
|
|
|
111
111
|
```jsx
|
|
112
112
|
if (scene.keys.has('ArrowLeft')) layout.x -= speed * dt;
|
|
113
113
|
if (scene.keys.has('ArrowRight')) layout.x += speed * dt;
|
|
114
|
-
if (scene.keys.has('
|
|
114
|
+
if (scene.keys.has('KeyX')) /* launch ball */ ;
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
**Space is reserved** — the editor binds it to the play/stop toggle, so don't bind Space to a gameplay action (jump / shoot / launch / ...). Use arrows, WASD, letter keys, or on-screen buttons instead.
|
|
118
|
+
|
|
117
119
|
For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys + a button); no setup needed.
|
|
118
120
|
|
|
119
121
|
## Common breakout-shaped recipe (sketch)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"serve": "node ../../cli/dist/index.js serve . --open",
|
|
7
6
|
"restart": "node ../../cli/dist/index.js restart .",
|
|
8
7
|
"screenshot": "node ../../cli/dist/index.js screenshot .",
|
|
9
8
|
"check": "eslint . && jscpd && node --input-type=module -e \"const { bundleProject } = await import('../../cli/dist/bundle.js'); await bundleProject('.');\""
|