@enderfga/claw-orchestrator 3.7.1 → 4.0.0

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 (107) hide show
  1. package/README.md +19 -3
  2. package/dist/bin/cli.js +30 -7
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/src/autoloop/messages.d.ts +0 -2
  5. package/dist/src/autoloop/messages.js +0 -2
  6. package/dist/src/autoloop/messages.js.map +1 -1
  7. package/dist/src/autoloop/planner-tools.d.ts +3 -3
  8. package/dist/src/autoloop/planner-tools.js +3 -3
  9. package/dist/src/autoloop/runner.d.ts +1 -2
  10. package/dist/src/autoloop/runner.js +1 -2
  11. package/dist/src/autoloop/runner.js.map +1 -1
  12. package/dist/src/autoloop/types.d.ts +0 -2
  13. package/dist/src/autoloop/types.js +0 -2
  14. package/dist/src/autoloop/types.js.map +1 -1
  15. package/dist/src/council.js +1 -0
  16. package/dist/src/council.js.map +1 -1
  17. package/dist/src/dashboard/index.html +709 -8
  18. package/dist/src/embedded-server.js +172 -6
  19. package/dist/src/embedded-server.js.map +1 -1
  20. package/dist/src/index.js +222 -1
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/session-manager.d.ts +82 -1
  23. package/dist/src/session-manager.js +262 -5
  24. package/dist/src/session-manager.js.map +1 -1
  25. package/dist/src/ultraapp/build-events.d.ts +46 -0
  26. package/dist/src/ultraapp/build-events.js +6 -0
  27. package/dist/src/ultraapp/build-events.js.map +1 -0
  28. package/dist/src/ultraapp/build.d.ts +39 -0
  29. package/dist/src/ultraapp/build.js +111 -0
  30. package/dist/src/ultraapp/build.js.map +1 -0
  31. package/dist/src/ultraapp/conventions.d.ts +8 -0
  32. package/dist/src/ultraapp/conventions.js +248 -0
  33. package/dist/src/ultraapp/conventions.js.map +1 -0
  34. package/dist/src/ultraapp/council-adapter.d.ts +49 -0
  35. package/dist/src/ultraapp/council-adapter.js +152 -0
  36. package/dist/src/ultraapp/council-adapter.js.map +1 -0
  37. package/dist/src/ultraapp/deploy.d.ts +45 -0
  38. package/dist/src/ultraapp/deploy.js +82 -0
  39. package/dist/src/ultraapp/deploy.js.map +1 -0
  40. package/dist/src/ultraapp/diff-apply.d.ts +15 -0
  41. package/dist/src/ultraapp/diff-apply.js +48 -0
  42. package/dist/src/ultraapp/diff-apply.js.map +1 -0
  43. package/dist/src/ultraapp/docker.d.ts +57 -0
  44. package/dist/src/ultraapp/docker.js +83 -0
  45. package/dist/src/ultraapp/docker.js.map +1 -0
  46. package/dist/src/ultraapp/feedback-classifier.d.ts +24 -0
  47. package/dist/src/ultraapp/feedback-classifier.js +85 -0
  48. package/dist/src/ultraapp/feedback-classifier.js.map +1 -0
  49. package/dist/src/ultraapp/files.d.ts +24 -0
  50. package/dist/src/ultraapp/files.js +79 -0
  51. package/dist/src/ultraapp/files.js.map +1 -0
  52. package/dist/src/ultraapp/fix-on-failure-session.d.ts +23 -0
  53. package/dist/src/ultraapp/fix-on-failure-session.js +48 -0
  54. package/dist/src/ultraapp/fix-on-failure-session.js.map +1 -0
  55. package/dist/src/ultraapp/fix-on-failure.d.ts +39 -0
  56. package/dist/src/ultraapp/fix-on-failure.js +69 -0
  57. package/dist/src/ultraapp/fix-on-failure.js.map +1 -0
  58. package/dist/src/ultraapp/host-strategy.d.ts +57 -0
  59. package/dist/src/ultraapp/host-strategy.js +205 -0
  60. package/dist/src/ultraapp/host-strategy.js.map +1 -0
  61. package/dist/src/ultraapp/interview-parser.d.ts +41 -0
  62. package/dist/src/ultraapp/interview-parser.js +83 -0
  63. package/dist/src/ultraapp/interview-parser.js.map +1 -0
  64. package/dist/src/ultraapp/interview-tools.d.ts +16 -0
  65. package/dist/src/ultraapp/interview-tools.js +36 -0
  66. package/dist/src/ultraapp/interview-tools.js.map +1 -0
  67. package/dist/src/ultraapp/json-patch.d.ts +6 -0
  68. package/dist/src/ultraapp/json-patch.js +78 -0
  69. package/dist/src/ultraapp/json-patch.js.map +1 -0
  70. package/dist/src/ultraapp/lifecycle.d.ts +38 -0
  71. package/dist/src/ultraapp/lifecycle.js +31 -0
  72. package/dist/src/ultraapp/lifecycle.js.map +1 -0
  73. package/dist/src/ultraapp/manager.d.ts +141 -0
  74. package/dist/src/ultraapp/manager.js +661 -0
  75. package/dist/src/ultraapp/manager.js.map +1 -0
  76. package/dist/src/ultraapp/narrator-prompt.d.ts +10 -0
  77. package/dist/src/ultraapp/narrator-prompt.js +54 -0
  78. package/dist/src/ultraapp/narrator-prompt.js.map +1 -0
  79. package/dist/src/ultraapp/narrator.d.ts +49 -0
  80. package/dist/src/ultraapp/narrator.js +83 -0
  81. package/dist/src/ultraapp/narrator.js.map +1 -0
  82. package/dist/src/ultraapp/patcher.d.ts +35 -0
  83. package/dist/src/ultraapp/patcher.js +159 -0
  84. package/dist/src/ultraapp/patcher.js.map +1 -0
  85. package/dist/src/ultraapp/router.d.ts +39 -0
  86. package/dist/src/ultraapp/router.js +139 -0
  87. package/dist/src/ultraapp/router.js.map +1 -0
  88. package/dist/src/ultraapp/spec-delta.d.ts +18 -0
  89. package/dist/src/ultraapp/spec-delta.js +35 -0
  90. package/dist/src/ultraapp/spec-delta.js.map +1 -0
  91. package/dist/src/ultraapp/spec.d.ts +82 -0
  92. package/dist/src/ultraapp/spec.js +125 -0
  93. package/dist/src/ultraapp/spec.js.map +1 -0
  94. package/dist/src/ultraapp/store.d.ts +67 -0
  95. package/dist/src/ultraapp/store.js +177 -0
  96. package/dist/src/ultraapp/store.js.map +1 -0
  97. package/dist/src/ultraapp/versions.d.ts +50 -0
  98. package/dist/src/ultraapp/versions.js +65 -0
  99. package/dist/src/ultraapp/versions.js.map +1 -0
  100. package/openclaw.plugin.json +15 -1
  101. package/package.json +3 -1
  102. package/skills/SKILL.md +2 -2
  103. package/skills/references/autoloop.md +1 -1
  104. package/skills/references/mcp.md +2 -2
  105. package/skills/references/tools.md +183 -0
  106. package/skills/references/ultraapp.md +203 -0
  107. package/skills/ultraapp/SKILL.md +112 -0
@@ -353,6 +353,237 @@
353
353
  border-top: 1px solid var(--border);
354
354
  background: var(--surface);
355
355
  }
356
+ /* ─── Forge tab ──────────────────────────────────────────── */
357
+ .forge-layout {
358
+ display: grid;
359
+ grid-template-columns: 1fr 380px;
360
+ height: 100%;
361
+ gap: 0;
362
+ }
363
+ .forge-chat {
364
+ display: flex;
365
+ flex-direction: column;
366
+ padding: 16px;
367
+ overflow: hidden;
368
+ height: 100%;
369
+ }
370
+ .forge-mode {
371
+ display: inline-block;
372
+ padding: 4px 10px;
373
+ border-radius: 12px;
374
+ font-size: 11px;
375
+ background: var(--surface-2);
376
+ color: var(--muted);
377
+ margin-bottom: 8px;
378
+ text-transform: uppercase;
379
+ align-self: flex-start;
380
+ }
381
+ .forge-mode[data-mode='build'],
382
+ .forge-mode[data-mode='building'] { background: var(--info); color: white; }
383
+ .forge-mode[data-mode='done'],
384
+ .forge-mode[data-mode='build-complete'] { background: var(--accent); color: white; }
385
+ .forge-mode[data-mode='failed'] { background: var(--error); color: white; }
386
+ .forge-mode[data-mode='queued'] { background: var(--warn); color: white; }
387
+ .forge-empty {
388
+ margin: auto;
389
+ max-width: 440px;
390
+ text-align: center;
391
+ color: var(--muted);
392
+ }
393
+ .forge-messages {
394
+ flex: 1;
395
+ overflow-y: auto;
396
+ padding-right: 8px;
397
+ margin-bottom: 8px;
398
+ }
399
+ .forge-msg {
400
+ padding: 6px 10px;
401
+ margin: 4px 0;
402
+ border-radius: 4px;
403
+ font-size: 13px;
404
+ }
405
+ .forge-msg.user { background: #e0eaff; align-self: flex-end; }
406
+ .forge-msg.assistant { background: var(--surface); border: 1px solid var(--border); }
407
+ .forge-msg.system { background: var(--surface-2); color: var(--muted); font-size: 12px; }
408
+ .forge-share {
409
+ background: var(--surface);
410
+ border: 1px solid var(--accent);
411
+ border-radius: 6px;
412
+ padding: 12px;
413
+ margin: 8px 0;
414
+ }
415
+ .forge-share strong { color: var(--accent); display: block; margin-bottom: 6px; }
416
+ .forge-share a { color: var(--accent); word-break: break-all; }
417
+ .forge-share-actions { display: flex; gap: 8px; margin-top: 10px; }
418
+ .forge-share-actions button {
419
+ padding: 4px 12px;
420
+ font-size: 12px;
421
+ border: 1px solid var(--border);
422
+ background: var(--surface);
423
+ border-radius: 4px;
424
+ cursor: pointer;
425
+ }
426
+ .forge-public-modal {
427
+ max-width: 600px;
428
+ padding: 20px;
429
+ border: 1px solid var(--border);
430
+ border-radius: 8px;
431
+ }
432
+ .forge-public-modal pre {
433
+ background: var(--surface-2);
434
+ padding: 8px;
435
+ border-radius: 4px;
436
+ font-size: 12px;
437
+ overflow-x: auto;
438
+ }
439
+ .forge-public-modal h4 { margin-bottom: 4px; }
440
+ .runItem .lifecycle-actions {
441
+ display: none;
442
+ gap: 4px;
443
+ margin-top: 4px;
444
+ }
445
+ .runItem:hover .lifecycle-actions { display: flex; }
446
+ .lifecycle-actions button {
447
+ font-size: 10px;
448
+ padding: 2px 6px;
449
+ border: 1px solid var(--border);
450
+ background: var(--surface);
451
+ border-radius: 3px;
452
+ cursor: pointer;
453
+ }
454
+ .lifecycle-actions button:hover { background: var(--surface-2); }
455
+ .forge-question {
456
+ background: var(--surface);
457
+ border: 1px solid var(--border);
458
+ border-radius: 6px;
459
+ padding: 12px;
460
+ margin: 8px 0;
461
+ }
462
+ .forge-question .ctx { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
463
+ .forge-question .q { font-weight: 600; margin-bottom: 8px; }
464
+ .forge-question .opts { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
465
+ .forge-question .opt {
466
+ padding: 4px 10px;
467
+ border: 1px solid var(--border);
468
+ border-radius: 16px;
469
+ cursor: pointer;
470
+ font-size: 12px;
471
+ background: var(--surface);
472
+ }
473
+ .forge-question .opt.recommended { border-color: var(--accent); }
474
+ .forge-question .opt.selected { background: var(--accent); color: white; border-color: var(--accent); }
475
+ .forge-input {
476
+ display: grid;
477
+ grid-template-columns: 1fr auto;
478
+ gap: 8px;
479
+ padding-top: 8px;
480
+ border-top: 1px solid var(--border);
481
+ }
482
+ .forge-input textarea {
483
+ resize: vertical;
484
+ font-family: inherit;
485
+ padding: 6px;
486
+ border: 1px solid var(--border);
487
+ border-radius: 4px;
488
+ }
489
+ .forge-input-row {
490
+ display: grid;
491
+ grid-template-columns: 1fr auto;
492
+ gap: 8px;
493
+ margin-top: 4px;
494
+ }
495
+ .forge-input button {
496
+ padding: 6px 14px;
497
+ border-radius: 4px;
498
+ border: 1px solid var(--accent);
499
+ background: var(--accent);
500
+ color: white;
501
+ cursor: pointer;
502
+ }
503
+ .forge-input button:disabled { opacity: 0.4; cursor: not-allowed; }
504
+ .forge-spec {
505
+ border-left: 1px solid var(--border);
506
+ padding: 16px;
507
+ overflow-y: auto;
508
+ background: var(--surface-2);
509
+ display: flex;
510
+ flex-direction: column;
511
+ }
512
+ .forge-spec h3 { margin: 0 0 8px 0; }
513
+ .forge-spec textarea {
514
+ width: 100%;
515
+ font-family: ui-monospace, SFMono-Regular, monospace;
516
+ font-size: 11px;
517
+ padding: 6px;
518
+ border: 1px solid var(--border);
519
+ border-radius: 4px;
520
+ flex: 1;
521
+ }
522
+ .forge-spec-actions {
523
+ display: flex;
524
+ gap: 8px;
525
+ margin-top: 8px;
526
+ align-items: center;
527
+ }
528
+ .forge-spec-actions button {
529
+ padding: 4px 10px;
530
+ border-radius: 4px;
531
+ border: 1px solid var(--border);
532
+ background: var(--surface);
533
+ cursor: pointer;
534
+ }
535
+ .forge-spec-msg { font-size: 11px; color: var(--muted); }
536
+ #forgeNew {
537
+ margin: 8px 12px;
538
+ padding: 6px 12px;
539
+ border-radius: 4px;
540
+ border: 1px solid var(--accent);
541
+ background: var(--accent);
542
+ color: white;
543
+ cursor: pointer;
544
+ width: calc(100% - 24px);
545
+ }
546
+ #forgeStartBuild {
547
+ margin-top: 8px;
548
+ padding: 8px 14px;
549
+ border-radius: 4px;
550
+ border: 1px solid var(--accent);
551
+ background: var(--accent);
552
+ color: white;
553
+ cursor: pointer;
554
+ font-weight: 600;
555
+ }
556
+ #forgeStartBuild:disabled { opacity: 0.4; cursor: not-allowed; }
557
+ .forge-versions { margin-top: 12px; }
558
+ .forge-versions h4 { margin: 0 0 6px 0; font-size: 12px; color: var(--muted); text-transform: uppercase; }
559
+ .version-list { list-style: none; padding: 0; margin: 0; }
560
+ .version-row {
561
+ display: flex;
562
+ gap: 8px;
563
+ align-items: center;
564
+ padding: 4px 0;
565
+ font-size: 12px;
566
+ border-top: 1px solid var(--border);
567
+ }
568
+ .version-tag {
569
+ background: var(--surface-2);
570
+ padding: 1px 6px;
571
+ border-radius: 3px;
572
+ font-family: monospace;
573
+ font-weight: 600;
574
+ }
575
+ .version-meta { color: var(--muted); }
576
+ .version-row a { color: var(--accent); text-decoration: none; word-break: break-all; }
577
+ .version-row button {
578
+ margin-left: auto;
579
+ padding: 2px 8px;
580
+ font-size: 11px;
581
+ border: 1px solid var(--border);
582
+ background: var(--surface);
583
+ border-radius: 3px;
584
+ cursor: pointer;
585
+ }
586
+ .version-row button:disabled { opacity: 0.4; cursor: not-allowed; }
356
587
  </style>
357
588
  </head>
358
589
  <body>
@@ -362,6 +593,7 @@
362
593
  <nav class="tabs" role="tablist">
363
594
  <button class="tab" role="tab" aria-selected="true" data-tab="autoloop">Autoloop</button>
364
595
  <button class="tab" role="tab" aria-selected="false" data-tab="council">Council</button>
596
+ <button class="tab" role="tab" aria-selected="false" data-tab="forge">Forge</button>
365
597
  </nav>
366
598
  <div class="grow"></div>
367
599
  <small id="status">connected</small>
@@ -370,6 +602,7 @@
370
602
  <div class="body">
371
603
  <aside class="sidebar">
372
604
  <h2 id="sidebarTitle">Runs</h2>
605
+ <button id="forgeNew" hidden>+ New</button>
373
606
  <ul class="runlist" id="runlist"></ul>
374
607
  </aside>
375
608
 
@@ -402,6 +635,14 @@
402
635
  events: [],
403
636
  eventSource: null,
404
637
  },
638
+ // forge run detail
639
+ forge: {
640
+ runId: null,
641
+ spec: null,
642
+ mode: 'interview',
643
+ currentQuestion: null,
644
+ eventSource: null,
645
+ },
405
646
  };
406
647
 
407
648
  const $ = (id) => document.getElementById(id);
@@ -423,7 +664,13 @@
423
664
  document.querySelectorAll('.tab').forEach((b) => {
424
665
  b.setAttribute('aria-selected', String(b.dataset.tab === tab));
425
666
  });
426
- $('sidebarTitle').textContent = tab === 'autoloop' ? 'Autoloop runs' : 'Council sessions';
667
+ $('sidebarTitle').textContent =
668
+ tab === 'autoloop'
669
+ ? 'Autoloop runs'
670
+ : tab === 'council'
671
+ ? 'Council sessions'
672
+ : 'Forge apps';
673
+ $('forgeNew').hidden = tab !== 'forge';
427
674
  state.selectedId = null;
428
675
  closeStreams();
429
676
  renderEmptyMain();
@@ -439,6 +686,10 @@
439
686
  state.council.eventSource.close();
440
687
  state.council.eventSource = null;
441
688
  }
689
+ if (state.forge.eventSource) {
690
+ state.forge.eventSource.close();
691
+ state.forge.eventSource = null;
692
+ }
442
693
  // Reset caches.
443
694
  state.autoloop.state = null;
444
695
  state.autoloop.plannerLog = [];
@@ -447,6 +698,10 @@
447
698
  state.autoloop.pushLog = [];
448
699
  state.council.session = null;
449
700
  state.council.events = [];
701
+ state.forge.runId = null;
702
+ state.forge.spec = null;
703
+ state.forge.currentQuestion = null;
704
+ state.forge.mode = 'interview';
450
705
  }
451
706
 
452
707
  function renderEmptyMain() {
@@ -456,10 +711,21 @@
456
711
  // ─── List polling ─────────────────────────────────────────────
457
712
  async function refreshList() {
458
713
  try {
459
- const url = state.tab === 'autoloop' ? '/autoloop/list' : '/council/list';
460
- const r = await fetch(url);
714
+ const url =
715
+ state.tab === 'autoloop'
716
+ ? '/autoloop/list'
717
+ : state.tab === 'council'
718
+ ? '/council/list'
719
+ : '/ultraapp/list';
720
+ const opts = state.tab === 'forge' ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } : {};
721
+ const r = await fetch(url, opts);
461
722
  const j = await r.json();
462
- state.runs = state.tab === 'autoloop' ? j.runs || [] : j.councils || [];
723
+ state.runs =
724
+ state.tab === 'autoloop'
725
+ ? j.runs || []
726
+ : state.tab === 'council'
727
+ ? j.councils || []
728
+ : j.runs || [];
463
729
  renderList();
464
730
  $('status').textContent = 'connected';
465
731
  } catch (err) {
@@ -471,9 +737,11 @@
471
737
  function renderList() {
472
738
  const ul = $('runlist');
473
739
  if (state.runs.length === 0) {
740
+ const noun =
741
+ state.tab === 'autoloop' ? 'runs' : state.tab === 'council' ? 'councils' : 'apps';
474
742
  ul.innerHTML =
475
743
  '<li class="empty" style="padding:20px;list-style:none">No active ' +
476
- (state.tab === 'autoloop' ? 'runs' : 'councils') +
744
+ noun +
477
745
  '.</li>';
478
746
  return;
479
747
  }
@@ -489,7 +757,7 @@
489
757
  <span>↑${r.push_log_count}</span>
490
758
  </div>
491
759
  </li>`;
492
- } else {
760
+ } else if (state.tab === 'council') {
493
761
  return `
494
762
  <li class="runItem" data-id="${escapeHtml(r.id)}" aria-selected="${r.id === state.selectedId}">
495
763
  <div class="id">${escapeHtml(r.id)}</div>
@@ -498,16 +766,46 @@
498
766
  <span>${r.responses?.length ?? 0} resp</span>
499
767
  </div>
500
768
  </li>`;
769
+ } else {
770
+ const showLifecycle = r.mode === 'done' || r.mode === 'failed' || r.mode === 'build-complete';
771
+ const lifecycleHtml = showLifecycle
772
+ ? `<div class="lifecycle-actions">
773
+ ${r.mode === 'done' ? `<button data-act="stop">Stop</button>` : ''}
774
+ ${r.mode !== 'done' ? `<button data-act="start">Start</button>` : ''}
775
+ <button data-act="delete">Delete</button>
776
+ </div>`
777
+ : '';
778
+ return `
779
+ <li class="runItem" data-id="${escapeHtml(r.runId)}" aria-selected="${r.runId === state.selectedId}">
780
+ <div class="id">${escapeHtml(r.title || r.runId)}</div>
781
+ <div class="meta">
782
+ <span class="badge ${r.mode}">${r.mode}</span>
783
+ </div>
784
+ ${lifecycleHtml}
785
+ </li>`;
501
786
  }
502
787
  })
503
788
  .join('');
504
789
  ul.innerHTML = html;
505
790
  ul.querySelectorAll('.runItem').forEach((el) => {
506
- el.addEventListener('click', () => {
791
+ // Click on item itself (not action button) opens it
792
+ el.addEventListener('click', (ev) => {
793
+ const t = ev.target;
794
+ if (t && t.closest('.lifecycle-actions')) return;
507
795
  state.selectedId = el.dataset.id;
508
796
  renderList();
509
797
  openDetail(el.dataset.id);
510
798
  });
799
+ el.querySelectorAll('.lifecycle-actions button').forEach((btn) => {
800
+ btn.addEventListener('click', async (ev) => {
801
+ ev.stopPropagation();
802
+ const action = btn.dataset.act;
803
+ const id = el.dataset.id;
804
+ if (action === 'delete' && !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')) return;
805
+ await forgeLifecycleAction(id, action);
806
+ refreshList();
807
+ });
808
+ });
511
809
  });
512
810
  }
513
811
 
@@ -515,7 +813,410 @@
515
813
  function openDetail(id) {
516
814
  closeStreams();
517
815
  if (state.tab === 'autoloop') openAutoloop(id);
518
- else openCouncil(id);
816
+ else if (state.tab === 'council') openCouncil(id);
817
+ else openForgeRun(id);
818
+ }
819
+
820
+ // ─── Forge ────────────────────────────────────────────────────
821
+ $('forgeNew').addEventListener('click', async () => {
822
+ const r = await fetch('/ultraapp/new', {
823
+ method: 'POST',
824
+ headers: { 'Content-Type': 'application/json' },
825
+ body: '{}',
826
+ });
827
+ const j = await r.json();
828
+ if (!j.ok) {
829
+ alert('Failed to create run: ' + (j.error || 'unknown'));
830
+ return;
831
+ }
832
+ state.selectedId = j.runId;
833
+ await refreshList();
834
+ openForgeRun(j.runId);
835
+ });
836
+
837
+ function renderForgeShell() {
838
+ $('main').innerHTML = `
839
+ <div class="forge-layout">
840
+ <div class="forge-chat">
841
+ <div class="forge-mode" id="forgeMode" data-mode="interview">interview</div>
842
+ <div class="forge-messages" id="forgeMessages"></div>
843
+ <div class="forge-input">
844
+ <textarea id="forgeFreeform" placeholder="Free-form input (or click an option above and Submit)" rows="2"></textarea>
845
+ <button id="forgeSubmit">Submit</button>
846
+ </div>
847
+ <div class="forge-input-row">
848
+ <input type="file" id="forgeFile" multiple />
849
+ <input type="text" id="forgePath" placeholder="…or paste an absolute path" />
850
+ </div>
851
+ </div>
852
+ <div class="forge-spec">
853
+ <h3>AppSpec</h3>
854
+ <textarea id="forgeSpecJson" rows="20" spellcheck="false"></textarea>
855
+ <div class="forge-spec-actions">
856
+ <button id="forgeSpecApply">Apply edit</button>
857
+ <span class="forge-spec-msg" id="forgeSpecMsg"></span>
858
+ </div>
859
+ <button id="forgeStartBuild" disabled>Start Build</button>
860
+ <div id="forgeVersions" class="forge-versions"></div>
861
+ </div>
862
+ </div>
863
+ `;
864
+ $('forgeSubmit').addEventListener('click', forgeOnSubmit);
865
+ $('forgeSpecApply').addEventListener('click', forgeOnSpecApply);
866
+ $('forgeStartBuild').addEventListener('click', forgeOnStartBuild);
867
+ }
868
+
869
+ async function forgeOnStartBuild() {
870
+ const runId = state.forge.runId;
871
+ if (!runId) return;
872
+ const btn = $('forgeStartBuild');
873
+ if (btn) btn.disabled = true;
874
+ try {
875
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/build', {
876
+ method: 'POST',
877
+ headers: { 'Content-Type': 'application/json' },
878
+ body: '{}',
879
+ });
880
+ const j = await r.json();
881
+ if (!j.ok) {
882
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] start build: ' + (j.error || 'unknown') });
883
+ if (btn) btn.disabled = false;
884
+ }
885
+ } catch (err) {
886
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] start build: ' + err.message });
887
+ if (btn) btn.disabled = false;
888
+ }
889
+ }
890
+
891
+ async function openForgeRun(runId) {
892
+ state.forge.runId = runId;
893
+ renderForgeShell();
894
+ try {
895
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId), {
896
+ method: 'POST',
897
+ headers: { 'Content-Type': 'application/json' },
898
+ body: '{}',
899
+ });
900
+ const j = await r.json();
901
+ if (!j.ok) {
902
+ $('forgeMessages').textContent = 'error: ' + (j.error || 'unknown');
903
+ return;
904
+ }
905
+ state.forge.spec = j.spec;
906
+ state.forge.mode = j.state.mode;
907
+ renderSpec(j.spec);
908
+ setForgeMode(j.state.mode);
909
+ for (const e of j.chat || []) renderChatEntry(e);
910
+ } catch (err) {
911
+ $('forgeMessages').textContent = 'fetch failed: ' + err.message;
912
+ return;
913
+ }
914
+ const es = new EventSource('/ultraapp/' + encodeURIComponent(runId) + '/events');
915
+ state.forge.eventSource = es;
916
+ es.addEventListener('ultraapp', (ev) => {
917
+ const data = JSON.parse(ev.data);
918
+ if (data.type === 'question') renderQuestion(data.question);
919
+ else if (data.type === 'spec-updated') {
920
+ state.forge.spec = data.spec;
921
+ renderSpec(data.spec);
922
+ } else if (data.type === 'chat') renderChatEntry(data.entry);
923
+ else if (data.type === 'interview-complete') {
924
+ renderChatEntry({ role: 'system', kind: 'narrator', text: 'Interview complete: ' + data.summary });
925
+ $('forgeStartBuild').disabled = false;
926
+ } else if (data.type === 'build-event') {
927
+ const ev = data.event;
928
+ if (ev.type === 'queued') setForgeMode('queued');
929
+ else if (ev.type === 'build-start') setForgeMode('building');
930
+ else if (ev.type === 'build-complete') setForgeMode('build-complete');
931
+ else if (ev.type === 'build-failed') {
932
+ setForgeMode('failed');
933
+ const btn = $('forgeStartBuild');
934
+ if (btn) btn.disabled = false;
935
+ } else if (ev.type === 'build-cancelled') {
936
+ setForgeMode('failed');
937
+ const btn = $('forgeStartBuild');
938
+ if (btn) btn.disabled = false;
939
+ }
940
+ } else if (data.type === 'app-url') {
941
+ setForgeMode('done');
942
+ renderShareCard(data.url, data.version);
943
+ } else if (data.type === 'error') {
944
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + data.message });
945
+ }
946
+ });
947
+ es.onerror = () => {
948
+ /* allow auto-reconnect */
949
+ };
950
+ }
951
+
952
+ function setForgeMode(m) {
953
+ state.forge.mode = m;
954
+ const el = $('forgeMode');
955
+ if (el) {
956
+ el.dataset.mode = m;
957
+ el.textContent = m;
958
+ }
959
+ const btn = $('forgeStartBuild');
960
+ if (btn) {
961
+ // Disabled while interview, queued, or building. Enabled at any
962
+ // resting state from which a (re)build is sensible.
963
+ const lockedModes = new Set(['interview', 'queued', 'building']);
964
+ btn.disabled = lockedModes.has(m);
965
+ }
966
+ // Hint the input's role per mode (interview = answer; done = feedback).
967
+ const ff = $('forgeFreeform');
968
+ if (ff) {
969
+ ff.placeholder =
970
+ m === 'done'
971
+ ? 'Done — type feedback (e.g. "make button green" / "add a thumbnail step")'
972
+ : 'Free-form input (or click an option above and Submit)';
973
+ }
974
+ // Refresh versions panel when entering a state that may have artifacts.
975
+ if (m === 'done' || m === 'build-complete') {
976
+ void forgeRefreshVersions();
977
+ }
978
+ }
979
+
980
+ function renderSpec(spec) {
981
+ const ta = $('forgeSpecJson');
982
+ if (!ta) return;
983
+ if (document.activeElement !== ta) {
984
+ ta.value = JSON.stringify(spec, null, 2);
985
+ }
986
+ }
987
+
988
+ function renderChatEntry(entry) {
989
+ const m = $('forgeMessages');
990
+ if (!m) return;
991
+ const div = document.createElement('div');
992
+ div.className = 'forge-msg ' + entry.role;
993
+ div.textContent = entry.text;
994
+ m.appendChild(div);
995
+ m.scrollTop = m.scrollHeight;
996
+ }
997
+
998
+ function renderShareCard(url, version) {
999
+ const m = $('forgeMessages');
1000
+ if (!m) return;
1001
+ const card = document.createElement('div');
1002
+ card.className = 'forge-share';
1003
+ card.innerHTML =
1004
+ `<strong>App deployed (${escapeHtml(version)})</strong>` +
1005
+ `<div><a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a></div>` +
1006
+ `<div class="forge-share-actions">` +
1007
+ `<button data-act="copy">Copy</button>` +
1008
+ `<button data-act="open">Open</button>` +
1009
+ `<button data-act="public">Make Public…</button>` +
1010
+ `</div>`;
1011
+ card.querySelector('[data-act="copy"]').addEventListener('click', () => {
1012
+ navigator.clipboard?.writeText(url);
1013
+ });
1014
+ card.querySelector('[data-act="open"]').addEventListener('click', () => {
1015
+ window.open(url, '_blank');
1016
+ });
1017
+ card.querySelector('[data-act="public"]').addEventListener('click', () => openPublicModal(url));
1018
+ m.appendChild(card);
1019
+ m.scrollTop = m.scrollHeight;
1020
+ }
1021
+
1022
+ function openPublicModal(localUrl) {
1023
+ const dlg = document.createElement('dialog');
1024
+ dlg.className = 'forge-public-modal';
1025
+ const u = new URL(localUrl);
1026
+ dlg.innerHTML =
1027
+ `<h3>Make this app public</h3>` +
1028
+ `<p>This app currently only runs on your machine. To share it, expose port ${escapeHtml(String(u.port || '19000'))} with one of these tools:</p>` +
1029
+ `<h4>Cloudflare Tunnel</h4>` +
1030
+ `<pre>cloudflared tunnel --url ${escapeHtml(u.origin)}</pre>` +
1031
+ `<h4>ngrok</h4>` +
1032
+ `<pre>ngrok http ${escapeHtml(u.port || '19000')}</pre>` +
1033
+ `<h4>Tailscale Funnel</h4>` +
1034
+ `<pre>tailscale serve --bg ${escapeHtml(u.origin)}</pre>` +
1035
+ `<h4>Self-hosted (Caddy)</h4>` +
1036
+ `<pre>example.com {\n reverse_proxy ${escapeHtml(u.host)}\n}</pre>` +
1037
+ `<p>Once exposed, your app will be reachable at <code>{your-public-host}${escapeHtml(u.pathname)}</code>.</p>` +
1038
+ `<button data-act="close">Close</button>`;
1039
+ document.body.appendChild(dlg);
1040
+ dlg.showModal();
1041
+ dlg.querySelector('[data-act="close"]').addEventListener('click', () => {
1042
+ dlg.close();
1043
+ dlg.remove();
1044
+ });
1045
+ }
1046
+
1047
+ async function forgeLifecycleAction(runId, action) {
1048
+ try {
1049
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/' + action, {
1050
+ method: 'POST',
1051
+ headers: { 'Content-Type': 'application/json' },
1052
+ body: '{}',
1053
+ });
1054
+ const j = await r.json();
1055
+ if (!j.ok) {
1056
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + action + ': ' + (j.error || 'unknown') });
1057
+ }
1058
+ if (action === 'delete' && j.ok && state.forge.runId === runId) {
1059
+ // Run is gone — close it and refresh sidebar
1060
+ state.forge.runId = null;
1061
+ $('forgeMessages').innerHTML = '';
1062
+ }
1063
+ } catch (err) {
1064
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + action + ': ' + err.message });
1065
+ }
1066
+ }
1067
+
1068
+ function renderQuestion(q) {
1069
+ state.forge.currentQuestion = q;
1070
+ const m = $('forgeMessages');
1071
+ if (!m) return;
1072
+ const card = document.createElement('div');
1073
+ card.className = 'forge-question';
1074
+ card.innerHTML =
1075
+ (q.context ? `<div class="ctx">${escapeHtml(q.context)}</div>` : '') +
1076
+ `<div class="q">${escapeHtml(q.question)}</div>` +
1077
+ `<div class="opts">` +
1078
+ q.options
1079
+ .map(
1080
+ (o) =>
1081
+ `<span class="opt ${o.value === q.recommended ? 'recommended' : ''}" data-val="${escapeHtml(o.value)}">${escapeHtml(o.label)}</span>`,
1082
+ )
1083
+ .join('') +
1084
+ `</div>`;
1085
+ card.querySelectorAll('.opt').forEach((el) => {
1086
+ el.addEventListener('click', () => {
1087
+ card.querySelectorAll('.opt').forEach((x) => x.classList.remove('selected'));
1088
+ el.classList.add('selected');
1089
+ state.forge.currentQuestion._selected = el.dataset.val;
1090
+ });
1091
+ });
1092
+ m.appendChild(card);
1093
+ m.scrollTop = m.scrollHeight;
1094
+ }
1095
+
1096
+ async function forgeOnSubmit() {
1097
+ const runId = state.forge.runId;
1098
+ if (!runId) return;
1099
+ const ff = $('forgeFreeform');
1100
+ const path = $('forgePath');
1101
+ const fileInput = $('forgeFile');
1102
+
1103
+ for (const f of fileInput.files) {
1104
+ const buf = await f.arrayBuffer();
1105
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(buf)));
1106
+ await fetch('/ultraapp/' + encodeURIComponent(runId) + '/files', {
1107
+ method: 'POST',
1108
+ headers: { 'Content-Type': 'application/json' },
1109
+ body: JSON.stringify({ filename: f.name, dataB64: b64 }),
1110
+ });
1111
+ }
1112
+ fileInput.value = '';
1113
+
1114
+ if (path.value.trim()) {
1115
+ await fetch('/ultraapp/' + encodeURIComponent(runId) + '/files', {
1116
+ method: 'POST',
1117
+ headers: { 'Content-Type': 'application/json' },
1118
+ body: JSON.stringify({ absolutePath: path.value.trim() }),
1119
+ });
1120
+ path.value = '';
1121
+ }
1122
+
1123
+ const freeform = ff.value.trim();
1124
+ const selected = state.forge.currentQuestion?._selected;
1125
+ // Mode-aware routing: in done mode, free-form text is feedback,
1126
+ // not an interview answer. /feedback runs through the classifier
1127
+ // and routes to patcher / spec-delta / structural-suggest.
1128
+ if (state.forge.mode === 'done' && freeform) {
1129
+ await fetch('/ultraapp/' + encodeURIComponent(runId) + '/feedback', {
1130
+ method: 'POST',
1131
+ headers: { 'Content-Type': 'application/json' },
1132
+ body: JSON.stringify({ text: freeform }),
1133
+ });
1134
+ ff.value = '';
1135
+ return;
1136
+ }
1137
+ if (freeform || selected) {
1138
+ const body = freeform ? { value: selected ?? '', freeform } : { value: selected };
1139
+ await fetch('/ultraapp/' + encodeURIComponent(runId) + '/answer', {
1140
+ method: 'POST',
1141
+ headers: { 'Content-Type': 'application/json' },
1142
+ body: JSON.stringify(body),
1143
+ });
1144
+ ff.value = '';
1145
+ state.forge.currentQuestion = null;
1146
+ }
1147
+ }
1148
+
1149
+ async function forgeRefreshVersions() {
1150
+ const runId = state.forge.runId;
1151
+ if (!runId) return;
1152
+ const panel = $('forgeVersions');
1153
+ if (!panel) return;
1154
+ try {
1155
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/artifacts', {
1156
+ method: 'POST',
1157
+ headers: { 'Content-Type': 'application/json' },
1158
+ body: '{}',
1159
+ });
1160
+ const j = await r.json();
1161
+ if (!j.ok || !j.artifacts || j.artifacts.length === 0) {
1162
+ panel.innerHTML = '';
1163
+ return;
1164
+ }
1165
+ const html = j.artifacts
1166
+ .map((a) => {
1167
+ const isDeployed = !!a.deploy;
1168
+ const url = a.deploy?.url || '';
1169
+ return `<li class="version-row">
1170
+ <span class="version-tag">${escapeHtml(a.version)}</span>
1171
+ <span class="version-meta">${a.builtAt ? new Date(a.builtAt).toLocaleString() : ''}</span>
1172
+ ${isDeployed ? `<a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a>` : '<span class="version-meta">(not deployed)</span>'}
1173
+ ${isDeployed ? `<button data-promote="${escapeHtml(a.version)}">Promote</button>` : ''}
1174
+ </li>`;
1175
+ })
1176
+ .join('');
1177
+ panel.innerHTML = '<h4>Versions</h4><ul class="version-list">' + html + '</ul>';
1178
+ panel.querySelectorAll('button[data-promote]').forEach((btn) => {
1179
+ btn.addEventListener('click', async () => {
1180
+ const v = btn.dataset.promote;
1181
+ if (!confirm(`Promote ${v} as the deployed version?`)) return;
1182
+ btn.disabled = true;
1183
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/promote-version', {
1184
+ method: 'POST',
1185
+ headers: { 'Content-Type': 'application/json' },
1186
+ body: JSON.stringify({ version: v }),
1187
+ });
1188
+ const j = await r.json();
1189
+ if (!j.ok) {
1190
+ renderChatEntry({ role: 'system', kind: 'error', text: '[error] promote: ' + (j.error || 'unknown') });
1191
+ }
1192
+ btn.disabled = false;
1193
+ await forgeRefreshVersions();
1194
+ });
1195
+ });
1196
+ } catch (err) {
1197
+ /* best-effort */
1198
+ }
1199
+ }
1200
+
1201
+ async function forgeOnSpecApply() {
1202
+ const runId = state.forge.runId;
1203
+ if (!runId) return;
1204
+ const msg = $('forgeSpecMsg');
1205
+ msg.textContent = '';
1206
+ let next;
1207
+ try {
1208
+ next = JSON.parse($('forgeSpecJson').value);
1209
+ } catch (e) {
1210
+ msg.textContent = 'JSON parse error: ' + e.message;
1211
+ return;
1212
+ }
1213
+ const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/spec-edit', {
1214
+ method: 'POST',
1215
+ headers: { 'Content-Type': 'application/json' },
1216
+ body: JSON.stringify({ patch: [{ op: 'replace', path: '', value: next }] }),
1217
+ });
1218
+ const j = await r.json();
1219
+ msg.textContent = j.ok ? 'Saved.' : 'Error: ' + (j.error ?? 'unknown');
519
1220
  }
520
1221
 
521
1222
  // ─── Autoloop detail ──────────────────────────────────────────