castle-web-cli 0.4.11 → 0.4.13

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.
Files changed (49) hide show
  1. package/dist/agent-prompts.d.ts +31 -0
  2. package/dist/agent-prompts.js +104 -0
  3. package/dist/agent.d.ts +17 -0
  4. package/dist/agent.js +952 -0
  5. package/dist/chat-client.d.ts +1 -0
  6. package/dist/chat-client.js +425 -0
  7. package/dist/commonInstructions.d.ts +1 -0
  8. package/dist/commonInstructions.js +8 -0
  9. package/dist/ide-client.js +46 -14
  10. package/dist/ide.d.ts +2 -0
  11. package/dist/ide.js +348 -36
  12. package/dist/init.js +12 -1
  13. package/dist/serve.js +18 -3
  14. package/kits/basic-2d/CLAUDE.md +3 -1
  15. package/kits/basic-3d/.prettierrc +8 -0
  16. package/kits/basic-3d/CLAUDE.md +162 -0
  17. package/kits/basic-3d/behaviors/Camera.jsx +56 -0
  18. package/kits/basic-3d/behaviors/Collider.jsx +78 -0
  19. package/kits/basic-3d/behaviors/Mesh.jsx +82 -0
  20. package/kits/basic-3d/behaviors/Model.jsx +61 -0
  21. package/kits/basic-3d/behaviors/Transform.jsx +35 -0
  22. package/kits/basic-3d/editors/App.jsx +147 -0
  23. package/kits/basic-3d/editors/CodeEditor.jsx +112 -0
  24. package/kits/basic-3d/editors/FileBrowser.jsx +143 -0
  25. package/kits/basic-3d/editors/ModelEditor.jsx +400 -0
  26. package/kits/basic-3d/editors/PlayOnly.jsx +14 -0
  27. package/kits/basic-3d/editors/SceneEditor.jsx +1087 -0
  28. package/kits/basic-3d/editors/behaviorRegistry.js +24 -0
  29. package/kits/basic-3d/editors/editorHistory.js +52 -0
  30. package/kits/basic-3d/editors/viewportRig.js +90 -0
  31. package/kits/basic-3d/engine/ScenePlayer.jsx +55 -0
  32. package/kits/basic-3d/engine/SceneUI.jsx +67 -0
  33. package/kits/basic-3d/engine/SceneViewport.jsx +102 -0
  34. package/kits/basic-3d/engine/TouchControls.jsx +136 -0
  35. package/kits/basic-3d/engine/autoInspector.jsx +51 -0
  36. package/kits/basic-3d/engine/files.js +73 -0
  37. package/kits/basic-3d/engine/scene.js +502 -0
  38. package/kits/basic-3d/engine/threeUtil.js +260 -0
  39. package/kits/basic-3d/engine/ui.jsx +352 -0
  40. package/kits/basic-3d/engine/ui.module.css +944 -0
  41. package/kits/basic-3d/eslint.config.js +51 -0
  42. package/kits/basic-3d/index.html +11 -0
  43. package/kits/basic-3d/main.jsx +10 -0
  44. package/kits/basic-3d/models/block.model +14 -0
  45. package/kits/basic-3d/package-lock.json +2713 -0
  46. package/kits/basic-3d/package.json +41 -0
  47. package/kits/basic-3d/scenes/main.scene +76 -0
  48. package/kits/basic-3d/vite.config.js +1 -0
  49. 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 the
31
- // IDE shell iframe, Ctrl+T moves keyboard focus out to the terminal -- but
32
- // while the iframe holds focus the parent sees no keydowns, so the deck page
33
- // has to forward Ctrl+T to the parent itself. No-op for a standalone deck.
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
- if (asset === 'ide-client.js') {
291
- const filePath = path.join(DIST_DIR, 'ide-client.js');
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
- const TERMINAL_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></svg>`;
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
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
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
- /* xterm host: takes whatever height is left after the header. min-height:0
340
- lets it shrink within the flex column instead of overflowing. */
341
- #term-host {
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
- width: 100%;
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: 190px;
445
- padding: 8px 10px;
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: 12px;
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,256 @@ const IDE_STYLES = `
457
491
  justify-content: space-between;
458
492
  gap: 10px;
459
493
  }
460
- #term-settings-panel .seg {
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
- #term-settings-panel .seg button {
467
- padding: 3px 9px;
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
- #term-settings-panel .seg button + button { border-left: 1px solid #000000; }
475
- #term-settings-panel .seg button.active { background: #222222; color: #ffffff; }
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
+ /* Live stream of a running task agent (text lines + [tool labels]) shown
630
+ on expand -- capped height, auto-scrolled, fading at both scroll edges. */
631
+ .task-feed {
632
+ margin-top: 6px;
633
+ max-height: 150px;
634
+ overflow-y: auto;
635
+ font-size: 12px;
636
+ line-height: 1.5;
637
+ color: #57606a;
638
+ cursor: default;
639
+ word-break: break-word;
640
+ -webkit-mask-image: linear-gradient(
641
+ to bottom,
642
+ transparent,
643
+ #000000 14px,
644
+ #000000 calc(100% - 14px),
645
+ transparent
646
+ );
647
+ mask-image: linear-gradient(
648
+ to bottom,
649
+ transparent,
650
+ #000000 14px,
651
+ #000000 calc(100% - 14px),
652
+ transparent
653
+ );
654
+ }
655
+ .task-row { display: flex; align-items: center; gap: 8px; }
656
+ .task-title {
657
+ flex: 1 1 auto;
658
+ min-width: 0;
659
+ overflow: hidden;
660
+ text-overflow: ellipsis;
661
+ white-space: nowrap;
662
+ }
663
+ .task-title::first-letter { text-transform: uppercase; }
664
+ .task-status { color: #6e7781; font-size: 12px; flex: 0 0 auto; }
665
+ .task-status.failed, .task-status.interrupted { color: #a40e26; }
666
+ .task-pie {
667
+ width: 15px;
668
+ height: 15px;
669
+ border-radius: 50%;
670
+ flex: 0 0 15px;
671
+ border: 1px solid #d0d7de;
672
+ }
673
+ .task-bar { display: none; margin-top: 6px; height: 3px; border-radius: 2px; background: #eaeef2; overflow: hidden; }
674
+ .task-bar > div { height: 100%; background: #0969da; }
675
+ body.progress-bars .task-bar { display: block; }
676
+ body.progress-bars .task-pie { display: none; }
677
+ /* The task board: pinned above the conversation, one row per task until
678
+ the user checks it off. Grows freely -- hiding tasks behind a scroll
679
+ made them too easy to forget. Same text size as the chat. */
680
+ #chat-strip {
681
+ flex: 0 0 auto;
682
+ border-bottom: 1px solid #cccccc;
683
+ padding: 8px 14px;
684
+ display: flex;
685
+ flex-direction: column;
686
+ gap: 4px;
687
+ font-size: 15px;
688
+ background: #f2f4f7;
689
+ }
690
+ .task-card { background: #ffffff; }
691
+ /* Input row styled like the kit editor's inspector controls: 1px black
692
+ borders, 4px radius, same text size; the button matches the kit .button
693
+ (subtle drop shadow). */
694
+ #chat-input-row {
695
+ flex: 0 0 auto;
696
+ display: flex;
697
+ align-items: stretch;
698
+ gap: 8px;
699
+ padding: 10px 14px;
700
+ border-top: 1px solid #cccccc;
701
+ }
702
+ #chat-input {
703
+ flex: 1 1 auto;
704
+ resize: none;
705
+ font: inherit;
706
+ font-size: 15px;
707
+ line-height: 1.4;
708
+ border: 1px solid #000000;
709
+ border-radius: 4px;
710
+ padding: 5px 8px;
711
+ outline: none;
712
+ max-height: 120px;
713
+ background: #ffffff;
714
+ color: #222222;
715
+ }
716
+ #chat-send {
717
+ font: inherit;
718
+ font-size: 15px;
719
+ padding: 6px 10px;
720
+ border: 1px solid #000000;
721
+ border-radius: 4px;
722
+ background: #ffffff;
723
+ color: #222222;
724
+ cursor: pointer;
725
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
726
+ }
727
+ #chat-send:hover { background: #eeeeee; }
728
+ /* Pending image attachments (pasted or picked) shown as chips above the
729
+ input; click a chip to remove it. */
730
+ #chat-pending {
731
+ flex: 0 0 auto;
732
+ display: flex;
733
+ gap: 6px;
734
+ padding: 8px 14px 0;
735
+ border-top: 1px solid #cccccc;
736
+ }
737
+ #chat-pending img {
738
+ height: 44px;
739
+ border: 1px solid #cccccc;
740
+ border-radius: 4px;
741
+ cursor: pointer;
742
+ }
743
+ #chat-pending + #chat-input-row { border-top: 0; }
476
744
 
477
745
  /* Let the xterm light theme show through, and hide every scrollbar xterm
478
746
  can draw -- the native viewport one AND the renderer overlay one. */
@@ -509,21 +777,65 @@ const IDE_STYLES = `
509
777
  .xterm .xterm-scrollable-element > .shadow { display: none !important; }
510
778
  `;
511
779
  const IDE_BODY = `
512
- <button id="term-settings" type="button" tabindex="-1" title="terminal settings" aria-label="terminal settings">${COG_ICON_SVG}</button>
513
- <button id="term-toggle" type="button" tabindex="-1" title="toggle terminal" aria-label="toggle terminal">${TERMINAL_ICON_SVG}</button>
780
+ <button id="term-settings" type="button" tabindex="-1" title="panel settings" aria-label="panel settings">${COG_ICON_SVG}</button>
781
+ <button id="term-toggle" type="button" tabindex="-1" title="toggle panel" aria-label="toggle panel">${HAMMER_ICON_SVG}</button>
514
782
  <div id="term-settings-panel">
515
783
  <div class="row">
516
- <span>theme</span>
784
+ <span>Router agent</span>
785
+ <div class="seg" id="agent-router-seg">
786
+ <button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
787
+ <button type="button" tabindex="-1" data-backend="claude">Claude</button>
788
+ </div>
789
+ </div>
790
+ <div class="row">
791
+ <span>Task agents</span>
792
+ <div class="seg" id="agent-tasks-seg">
793
+ <button type="button" tabindex="-1" data-backend="cursor">Cursor</button>
794
+ <button type="button" tabindex="-1" data-backend="claude">Claude</button>
795
+ </div>
796
+ </div>
797
+ <div class="row">
798
+ <span>Claude model</span>
799
+ <div class="seg" id="agent-model-seg">
800
+ <button type="button" tabindex="-1" data-model="sonnet">Sonnet</button>
801
+ <button type="button" tabindex="-1" data-model="opus">Opus</button>
802
+ <button type="button" tabindex="-1" data-model="fable">Fable</button>
803
+ </div>
804
+ </div>
805
+ <div class="row">
806
+ <span>Progress</span>
807
+ <div class="seg" id="chat-progress-seg">
808
+ <button type="button" tabindex="-1" data-progress="pie">Pie</button>
809
+ <button type="button" tabindex="-1" data-progress="bar">Bar</button>
810
+ </div>
811
+ </div>
812
+ <div class="row">
813
+ <span>Terminal theme</span>
517
814
  <div class="seg" id="term-theme-seg">
518
- <button type="button" tabindex="-1" data-theme="light">light</button>
519
- <button type="button" tabindex="-1" data-theme="dark">dark</button>
815
+ <button type="button" tabindex="-1" data-theme="light">Light</button>
816
+ <button type="button" tabindex="-1" data-theme="dark">Dark</button>
520
817
  </div>
521
818
  </div>
522
819
  </div>
523
820
  <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"><div id="term-header"><span class="term-header-title">Terminal</span></div><div id="term-host"></div></div>
821
+ <div id="term">
822
+ <div id="term-header">
823
+ <div class="seg" id="panel-view-seg">
824
+ <button type="button" tabindex="-1" data-view="chat">Chat</button>
825
+ <button type="button" tabindex="-1" data-view="terminal">Terminal</button>
826
+ </div>
827
+ </div>
828
+ <div id="panel-body">
829
+ <div id="chat-host"></div>
830
+ <div id="term-host"></div>
831
+ </div>
832
+ </div>
525
833
  <script src="${IDE_ASSET_PREFIX}xterm.js"></script>
526
834
  <script src="${IDE_ASSET_PREFIX}addon-fit.js"></script>
835
+ <script src="${IDE_ASSET_PREFIX}react.js"></script>
836
+ <script src="${IDE_ASSET_PREFIX}react-dom.js"></script>
837
+ <script src="${IDE_ASSET_PREFIX}marked.js"></script>
838
+ <script type="module" src="${IDE_ASSET_PREFIX}chat-client.js"></script>
527
839
  <script type="module" src="${IDE_ASSET_PREFIX}ide-client.js"></script>
528
840
  `;
529
841
  function renderIdePage(deckLabel) {
@@ -537,7 +849,7 @@ function renderIdePage(deckLabel) {
537
849
  <link rel="stylesheet" href="${IDE_ASSET_PREFIX}xterm.css" />
538
850
  <style>${IDE_STYLES}</style>
539
851
  </head>
540
- <body>${IDE_BODY}</body>
852
+ <body class="term-open">${IDE_BODY}</body>
541
853
  </html>
542
854
  `;
543
855
  }
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.2';
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);
@@ -96,6 +105,7 @@ function scaffoldBare(projectDir) {
96
105
  fs.writeFileSync(path.join(projectDir, 'index.html'), INDEX_HTML);
97
106
  fs.writeFileSync(path.join(projectDir, 'game.js'), GAME_JS);
98
107
  fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeClaudeMd());
108
+ appendCommonInstructions(projectDir);
99
109
  ensureAgentsSymlink(projectDir);
100
110
  fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(makePackageJson(projectDir), null, 2) + '\n');
101
111
  }
@@ -186,6 +196,7 @@ function scaffoldFromKit(kit, projectDir) {
186
196
  if (!fs.existsSync(claudePath)) {
187
197
  fs.writeFileSync(claudePath, makeClaudeMd());
188
198
  }
199
+ appendCommonInstructions(projectDir);
189
200
  ensureAgentsSymlink(projectDir);
190
201
  }
191
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
  });
@@ -179,13 +182,23 @@ export async function serve(dir, options = {}) {
179
182
  deckLabel: path.basename(projectDir),
180
183
  });
181
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());
182
192
  const vite = await createServer({
183
193
  root: projectDir,
184
- plugins: [castlePlugin(wsPort, ideServer)],
194
+ plugins: [castlePlugin(wsPort, ideServer, agentServer)],
185
195
  server: {
186
196
  port,
187
197
  strictPort: true,
188
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,
189
202
  open: options.open ? true : undefined,
190
203
  hmr: false,
191
204
  proxy: {
@@ -204,7 +217,9 @@ export async function serve(dir, options = {}) {
204
217
  // than through Vite's proxy (Vite's ws proxy didn't forward this path).
205
218
  if (vite.httpServer) {
206
219
  vite.httpServer.on('upgrade', (req, socket, head) => {
207
- ideServer.handleUpgrade(req, socket, head);
220
+ if (!ideServer.handleUpgrade(req, socket, head)) {
221
+ agentServer.handleUpgrade(req, socket, head);
222
+ }
208
223
  });
209
224
  }
210
225
  vite.printUrls();
@@ -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('Space')) /* launch ball */ ;
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)
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "arrowParens": "always"
8
+ }