@enderfga/claw-orchestrator 3.7.0 → 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.
- package/README.md +19 -3
- package/dist/bin/cli.js +30 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/autoloop/messages.d.ts +0 -2
- package/dist/src/autoloop/messages.js +0 -2
- package/dist/src/autoloop/messages.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +3 -3
- package/dist/src/autoloop/planner-tools.js +3 -3
- package/dist/src/autoloop/runner.d.ts +1 -2
- package/dist/src/autoloop/runner.js +1 -2
- package/dist/src/autoloop/runner.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +0 -2
- package/dist/src/autoloop/types.js +0 -2
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/council.js +1 -0
- package/dist/src/council.js.map +1 -1
- package/dist/src/dashboard/index.html +709 -8
- package/dist/src/embedded-server.js +172 -6
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.js +222 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-manager.d.ts +82 -1
- package/dist/src/session-manager.js +264 -6
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/ultraapp/build-events.d.ts +46 -0
- package/dist/src/ultraapp/build-events.js +6 -0
- package/dist/src/ultraapp/build-events.js.map +1 -0
- package/dist/src/ultraapp/build.d.ts +39 -0
- package/dist/src/ultraapp/build.js +111 -0
- package/dist/src/ultraapp/build.js.map +1 -0
- package/dist/src/ultraapp/conventions.d.ts +8 -0
- package/dist/src/ultraapp/conventions.js +248 -0
- package/dist/src/ultraapp/conventions.js.map +1 -0
- package/dist/src/ultraapp/council-adapter.d.ts +49 -0
- package/dist/src/ultraapp/council-adapter.js +152 -0
- package/dist/src/ultraapp/council-adapter.js.map +1 -0
- package/dist/src/ultraapp/deploy.d.ts +45 -0
- package/dist/src/ultraapp/deploy.js +82 -0
- package/dist/src/ultraapp/deploy.js.map +1 -0
- package/dist/src/ultraapp/diff-apply.d.ts +15 -0
- package/dist/src/ultraapp/diff-apply.js +48 -0
- package/dist/src/ultraapp/diff-apply.js.map +1 -0
- package/dist/src/ultraapp/docker.d.ts +57 -0
- package/dist/src/ultraapp/docker.js +83 -0
- package/dist/src/ultraapp/docker.js.map +1 -0
- package/dist/src/ultraapp/feedback-classifier.d.ts +24 -0
- package/dist/src/ultraapp/feedback-classifier.js +85 -0
- package/dist/src/ultraapp/feedback-classifier.js.map +1 -0
- package/dist/src/ultraapp/files.d.ts +24 -0
- package/dist/src/ultraapp/files.js +79 -0
- package/dist/src/ultraapp/files.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure-session.d.ts +23 -0
- package/dist/src/ultraapp/fix-on-failure-session.js +48 -0
- package/dist/src/ultraapp/fix-on-failure-session.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure.d.ts +39 -0
- package/dist/src/ultraapp/fix-on-failure.js +69 -0
- package/dist/src/ultraapp/fix-on-failure.js.map +1 -0
- package/dist/src/ultraapp/host-strategy.d.ts +57 -0
- package/dist/src/ultraapp/host-strategy.js +205 -0
- package/dist/src/ultraapp/host-strategy.js.map +1 -0
- package/dist/src/ultraapp/interview-parser.d.ts +41 -0
- package/dist/src/ultraapp/interview-parser.js +83 -0
- package/dist/src/ultraapp/interview-parser.js.map +1 -0
- package/dist/src/ultraapp/interview-tools.d.ts +16 -0
- package/dist/src/ultraapp/interview-tools.js +36 -0
- package/dist/src/ultraapp/interview-tools.js.map +1 -0
- package/dist/src/ultraapp/json-patch.d.ts +6 -0
- package/dist/src/ultraapp/json-patch.js +78 -0
- package/dist/src/ultraapp/json-patch.js.map +1 -0
- package/dist/src/ultraapp/lifecycle.d.ts +38 -0
- package/dist/src/ultraapp/lifecycle.js +31 -0
- package/dist/src/ultraapp/lifecycle.js.map +1 -0
- package/dist/src/ultraapp/manager.d.ts +141 -0
- package/dist/src/ultraapp/manager.js +661 -0
- package/dist/src/ultraapp/manager.js.map +1 -0
- package/dist/src/ultraapp/narrator-prompt.d.ts +10 -0
- package/dist/src/ultraapp/narrator-prompt.js +54 -0
- package/dist/src/ultraapp/narrator-prompt.js.map +1 -0
- package/dist/src/ultraapp/narrator.d.ts +49 -0
- package/dist/src/ultraapp/narrator.js +83 -0
- package/dist/src/ultraapp/narrator.js.map +1 -0
- package/dist/src/ultraapp/patcher.d.ts +35 -0
- package/dist/src/ultraapp/patcher.js +159 -0
- package/dist/src/ultraapp/patcher.js.map +1 -0
- package/dist/src/ultraapp/router.d.ts +39 -0
- package/dist/src/ultraapp/router.js +139 -0
- package/dist/src/ultraapp/router.js.map +1 -0
- package/dist/src/ultraapp/spec-delta.d.ts +18 -0
- package/dist/src/ultraapp/spec-delta.js +35 -0
- package/dist/src/ultraapp/spec-delta.js.map +1 -0
- package/dist/src/ultraapp/spec.d.ts +82 -0
- package/dist/src/ultraapp/spec.js +125 -0
- package/dist/src/ultraapp/spec.js.map +1 -0
- package/dist/src/ultraapp/store.d.ts +67 -0
- package/dist/src/ultraapp/store.js +177 -0
- package/dist/src/ultraapp/store.js.map +1 -0
- package/dist/src/ultraapp/versions.d.ts +50 -0
- package/dist/src/ultraapp/versions.js +65 -0
- package/dist/src/ultraapp/versions.js.map +1 -0
- package/dist/src/validation.d.ts +4 -4
- package/dist/src/validation.js +6 -5
- package/dist/src/validation.js.map +1 -1
- package/openclaw.plugin.json +15 -1
- package/package.json +5 -2
- package/skills/SKILL.md +2 -2
- package/skills/references/autoloop.md +1 -1
- package/skills/references/mcp.md +2 -2
- package/skills/references/tools.md +183 -0
- package/skills/references/ultraapp.md +203 -0
- 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 =
|
|
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 =
|
|
460
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 ──────────────────────────────────────────
|