@enderfga/claw-orchestrator 3.7.1 → 4.0.3
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 +45 -3
- package/configs/autoloop-coder-prompt.md +65 -28
- package/configs/autoloop-planner-prompt.md +103 -42
- package/configs/autoloop-reviewer-prompt.md +58 -30
- package/dist/bin/cli.js +30 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/autoloop/dispatcher.js +16 -2
- package/dist/src/autoloop/dispatcher.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 +11 -6
- package/dist/src/autoloop/planner-tools.js +24 -7
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- 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 +1066 -12
- package/dist/src/embedded-server.d.ts +7 -0
- package/dist/src/embedded-server.js +359 -17
- 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 +98 -1
- package/dist/src/session-manager.js +340 -5
- 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 +150 -0
- package/dist/src/ultraapp/manager.js +684 -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/openclaw.plugin.json +15 -1
- package/package.json +3 -1
- package/skills/SKILL.md +2 -2
- package/skills/references/autoloop.md +7 -4
- package/skills/references/dashboard.md +153 -0
- 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,342 @@
|
|
|
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
|
+
#councilNew,
|
|
538
|
+
#autoloopNew {
|
|
539
|
+
margin: 8px 12px;
|
|
540
|
+
padding: 6px 12px;
|
|
541
|
+
border-radius: 4px;
|
|
542
|
+
border: 1px solid var(--accent);
|
|
543
|
+
background: var(--accent);
|
|
544
|
+
color: white;
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
width: calc(100% - 24px);
|
|
547
|
+
}
|
|
548
|
+
#forgeNew:hover,
|
|
549
|
+
#councilNew:hover,
|
|
550
|
+
#autoloopNew:hover {
|
|
551
|
+
filter: brightness(0.95);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ─── Launcher modal (Council / Autoloop forms) ─────────────── */
|
|
555
|
+
.modal-root {
|
|
556
|
+
position: fixed;
|
|
557
|
+
inset: 0;
|
|
558
|
+
background: rgba(0, 0, 0, 0.45);
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
justify-content: center;
|
|
562
|
+
z-index: 100;
|
|
563
|
+
}
|
|
564
|
+
.modal-root[hidden] { display: none; }
|
|
565
|
+
.modal {
|
|
566
|
+
background: var(--surface);
|
|
567
|
+
border-radius: 8px;
|
|
568
|
+
min-width: 440px;
|
|
569
|
+
max-width: 600px;
|
|
570
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
|
|
571
|
+
}
|
|
572
|
+
.modal-header {
|
|
573
|
+
display: flex;
|
|
574
|
+
align-items: center;
|
|
575
|
+
justify-content: space-between;
|
|
576
|
+
padding: 14px 20px;
|
|
577
|
+
border-bottom: 1px solid var(--border);
|
|
578
|
+
}
|
|
579
|
+
.modal-header h2 { margin: 0; font-size: 15px; }
|
|
580
|
+
.modal-header button {
|
|
581
|
+
background: transparent;
|
|
582
|
+
border: none;
|
|
583
|
+
font-size: 22px;
|
|
584
|
+
cursor: pointer;
|
|
585
|
+
color: var(--muted);
|
|
586
|
+
padding: 0 6px;
|
|
587
|
+
}
|
|
588
|
+
.modal-body {
|
|
589
|
+
padding: 16px 20px;
|
|
590
|
+
display: flex;
|
|
591
|
+
flex-direction: column;
|
|
592
|
+
gap: 12px;
|
|
593
|
+
}
|
|
594
|
+
.modal-body label {
|
|
595
|
+
display: flex;
|
|
596
|
+
flex-direction: column;
|
|
597
|
+
gap: 4px;
|
|
598
|
+
font-size: 12px;
|
|
599
|
+
color: var(--muted);
|
|
600
|
+
}
|
|
601
|
+
.modal-body input,
|
|
602
|
+
.modal-body textarea {
|
|
603
|
+
padding: 6px 8px;
|
|
604
|
+
border: 1px solid var(--border);
|
|
605
|
+
border-radius: 4px;
|
|
606
|
+
font: inherit;
|
|
607
|
+
color: var(--text);
|
|
608
|
+
background: var(--bg);
|
|
609
|
+
}
|
|
610
|
+
.modal-footer {
|
|
611
|
+
display: flex;
|
|
612
|
+
justify-content: flex-end;
|
|
613
|
+
gap: 8px;
|
|
614
|
+
padding: 12px 20px 18px;
|
|
615
|
+
}
|
|
616
|
+
.modal-footer button {
|
|
617
|
+
padding: 6px 14px;
|
|
618
|
+
border-radius: 4px;
|
|
619
|
+
border: 1px solid var(--border);
|
|
620
|
+
background: var(--surface);
|
|
621
|
+
cursor: pointer;
|
|
622
|
+
font: inherit;
|
|
623
|
+
}
|
|
624
|
+
.modal-footer button.primary {
|
|
625
|
+
background: var(--accent);
|
|
626
|
+
color: white;
|
|
627
|
+
border-color: var(--accent);
|
|
628
|
+
}
|
|
629
|
+
.modal-footer button:disabled {
|
|
630
|
+
opacity: 0.6;
|
|
631
|
+
cursor: not-allowed;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* ─── Empty-state CTA ──────────────────────────────────────── */
|
|
635
|
+
.empty-cta {
|
|
636
|
+
text-align: center;
|
|
637
|
+
margin: 80px auto 0;
|
|
638
|
+
max-width: 420px;
|
|
639
|
+
}
|
|
640
|
+
.empty-cta h2 { margin: 0 0 8px; font-size: 16px; }
|
|
641
|
+
.empty-cta p { margin: 0 0 16px; color: var(--muted); }
|
|
642
|
+
.empty-cta button {
|
|
643
|
+
padding: 8px 16px;
|
|
644
|
+
border-radius: 4px;
|
|
645
|
+
border: 1px solid var(--accent);
|
|
646
|
+
background: var(--accent);
|
|
647
|
+
color: white;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
font: inherit;
|
|
650
|
+
}
|
|
651
|
+
#forgeStartBuild {
|
|
652
|
+
margin-top: 8px;
|
|
653
|
+
padding: 8px 14px;
|
|
654
|
+
border-radius: 4px;
|
|
655
|
+
border: 1px solid var(--accent);
|
|
656
|
+
background: var(--accent);
|
|
657
|
+
color: white;
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
font-weight: 600;
|
|
660
|
+
}
|
|
661
|
+
#forgeStartBuild:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
662
|
+
.forge-versions { margin-top: 12px; }
|
|
663
|
+
.forge-versions h4 { margin: 0 0 6px 0; font-size: 12px; color: var(--muted); text-transform: uppercase; }
|
|
664
|
+
.version-list { list-style: none; padding: 0; margin: 0; }
|
|
665
|
+
.version-row {
|
|
666
|
+
display: flex;
|
|
667
|
+
gap: 8px;
|
|
668
|
+
align-items: center;
|
|
669
|
+
padding: 4px 0;
|
|
670
|
+
font-size: 12px;
|
|
671
|
+
border-top: 1px solid var(--border);
|
|
672
|
+
}
|
|
673
|
+
.version-tag {
|
|
674
|
+
background: var(--surface-2);
|
|
675
|
+
padding: 1px 6px;
|
|
676
|
+
border-radius: 3px;
|
|
677
|
+
font-family: monospace;
|
|
678
|
+
font-weight: 600;
|
|
679
|
+
}
|
|
680
|
+
.version-meta { color: var(--muted); }
|
|
681
|
+
.version-row a { color: var(--accent); text-decoration: none; word-break: break-all; }
|
|
682
|
+
.version-row button {
|
|
683
|
+
margin-left: auto;
|
|
684
|
+
padding: 2px 8px;
|
|
685
|
+
font-size: 11px;
|
|
686
|
+
border: 1px solid var(--border);
|
|
687
|
+
background: var(--surface);
|
|
688
|
+
border-radius: 3px;
|
|
689
|
+
cursor: pointer;
|
|
690
|
+
}
|
|
691
|
+
.version-row button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
356
692
|
</style>
|
|
357
693
|
</head>
|
|
358
694
|
<body>
|
|
@@ -362,6 +698,7 @@
|
|
|
362
698
|
<nav class="tabs" role="tablist">
|
|
363
699
|
<button class="tab" role="tab" aria-selected="true" data-tab="autoloop">Autoloop</button>
|
|
364
700
|
<button class="tab" role="tab" aria-selected="false" data-tab="council">Council</button>
|
|
701
|
+
<button class="tab" role="tab" aria-selected="false" data-tab="forge">Forge</button>
|
|
365
702
|
</nav>
|
|
366
703
|
<div class="grow"></div>
|
|
367
704
|
<small id="status">connected</small>
|
|
@@ -370,6 +707,9 @@
|
|
|
370
707
|
<div class="body">
|
|
371
708
|
<aside class="sidebar">
|
|
372
709
|
<h2 id="sidebarTitle">Runs</h2>
|
|
710
|
+
<button id="autoloopNew" hidden>+ New autoloop</button>
|
|
711
|
+
<button id="councilNew" hidden>+ New council</button>
|
|
712
|
+
<button id="forgeNew" hidden>+ New forge</button>
|
|
373
713
|
<ul class="runlist" id="runlist"></ul>
|
|
374
714
|
</aside>
|
|
375
715
|
|
|
@@ -381,6 +721,60 @@
|
|
|
381
721
|
<div class="footer">SSE-driven dashboard • served by claw-orchestrator embedded server</div>
|
|
382
722
|
</div>
|
|
383
723
|
|
|
724
|
+
<!-- ─── Launcher modal (tab-aware) ───────────────────────────────── -->
|
|
725
|
+
<div id="modalRoot" class="modal-root" hidden>
|
|
726
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
727
|
+
<div class="modal-header">
|
|
728
|
+
<h2 id="modalTitle">New run</h2>
|
|
729
|
+
<button id="modalClose" type="button" aria-label="Close">×</button>
|
|
730
|
+
</div>
|
|
731
|
+
<form id="modalForm" class="modal-body" autocomplete="off">
|
|
732
|
+
<!-- Council fields -->
|
|
733
|
+
<div data-for-tab="council" style="display: contents">
|
|
734
|
+
<label>
|
|
735
|
+
Task
|
|
736
|
+
<textarea
|
|
737
|
+
name="task"
|
|
738
|
+
rows="3"
|
|
739
|
+
placeholder="What should the council achieve?"
|
|
740
|
+
></textarea>
|
|
741
|
+
</label>
|
|
742
|
+
<label>
|
|
743
|
+
Project directory
|
|
744
|
+
<input
|
|
745
|
+
name="projectDir"
|
|
746
|
+
type="text"
|
|
747
|
+
placeholder="/abs/path/to/workspace"
|
|
748
|
+
/>
|
|
749
|
+
</label>
|
|
750
|
+
<label>
|
|
751
|
+
Max rounds
|
|
752
|
+
<input name="maxRounds" type="number" min="1" max="50" value="15" />
|
|
753
|
+
</label>
|
|
754
|
+
</div>
|
|
755
|
+
<!-- Autoloop fields -->
|
|
756
|
+
<div data-for-tab="autoloop" style="display: contents">
|
|
757
|
+
<label>
|
|
758
|
+
Workspace
|
|
759
|
+
<input
|
|
760
|
+
name="workspace"
|
|
761
|
+
type="text"
|
|
762
|
+
placeholder="/abs/path/to/git/workspace"
|
|
763
|
+
/>
|
|
764
|
+
</label>
|
|
765
|
+
<label>
|
|
766
|
+
Planner model (optional)
|
|
767
|
+
<input name="planner_model" type="text" placeholder="claude-opus-4-7" />
|
|
768
|
+
</label>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="modal-footer">
|
|
771
|
+
<button type="button" id="modalCancel">Cancel</button>
|
|
772
|
+
<button type="submit" id="modalSubmit" class="primary">Start</button>
|
|
773
|
+
</div>
|
|
774
|
+
</form>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
|
|
384
778
|
<script>
|
|
385
779
|
// ─── State ────────────────────────────────────────────────────
|
|
386
780
|
const state = {
|
|
@@ -402,6 +796,14 @@
|
|
|
402
796
|
events: [],
|
|
403
797
|
eventSource: null,
|
|
404
798
|
},
|
|
799
|
+
// forge run detail
|
|
800
|
+
forge: {
|
|
801
|
+
runId: null,
|
|
802
|
+
spec: null,
|
|
803
|
+
mode: 'interview',
|
|
804
|
+
currentQuestion: null,
|
|
805
|
+
eventSource: null,
|
|
806
|
+
},
|
|
405
807
|
};
|
|
406
808
|
|
|
407
809
|
const $ = (id) => document.getElementById(id);
|
|
@@ -423,7 +825,15 @@
|
|
|
423
825
|
document.querySelectorAll('.tab').forEach((b) => {
|
|
424
826
|
b.setAttribute('aria-selected', String(b.dataset.tab === tab));
|
|
425
827
|
});
|
|
426
|
-
$('sidebarTitle').textContent =
|
|
828
|
+
$('sidebarTitle').textContent =
|
|
829
|
+
tab === 'autoloop'
|
|
830
|
+
? 'Autoloop runs'
|
|
831
|
+
: tab === 'council'
|
|
832
|
+
? 'Council sessions'
|
|
833
|
+
: 'Forge apps';
|
|
834
|
+
$('forgeNew').hidden = tab !== 'forge';
|
|
835
|
+
$('councilNew').hidden = tab !== 'council';
|
|
836
|
+
$('autoloopNew').hidden = tab !== 'autoloop';
|
|
427
837
|
state.selectedId = null;
|
|
428
838
|
closeStreams();
|
|
429
839
|
renderEmptyMain();
|
|
@@ -439,6 +849,10 @@
|
|
|
439
849
|
state.council.eventSource.close();
|
|
440
850
|
state.council.eventSource = null;
|
|
441
851
|
}
|
|
852
|
+
if (state.forge.eventSource) {
|
|
853
|
+
state.forge.eventSource.close();
|
|
854
|
+
state.forge.eventSource = null;
|
|
855
|
+
}
|
|
442
856
|
// Reset caches.
|
|
443
857
|
state.autoloop.state = null;
|
|
444
858
|
state.autoloop.plannerLog = [];
|
|
@@ -447,20 +861,64 @@
|
|
|
447
861
|
state.autoloop.pushLog = [];
|
|
448
862
|
state.council.session = null;
|
|
449
863
|
state.council.events = [];
|
|
864
|
+
state.forge.runId = null;
|
|
865
|
+
state.forge.spec = null;
|
|
866
|
+
state.forge.currentQuestion = null;
|
|
867
|
+
state.forge.mode = 'interview';
|
|
450
868
|
}
|
|
451
869
|
|
|
452
870
|
function renderEmptyMain() {
|
|
453
|
-
|
|
871
|
+
// When the sidebar list is empty, invite the user to start their
|
|
872
|
+
// first run. Otherwise just say "pick one". The button delegates
|
|
873
|
+
// to the same launcher trigger as the sidebar "+ New" button.
|
|
874
|
+
const labels = {
|
|
875
|
+
autoloop: 'autoloop run',
|
|
876
|
+
council: 'council session',
|
|
877
|
+
forge: 'forge app',
|
|
878
|
+
};
|
|
879
|
+
const noun = labels[state.tab] || 'run';
|
|
880
|
+
if (state.runs.length === 0) {
|
|
881
|
+
const triggerId =
|
|
882
|
+
state.tab === 'autoloop'
|
|
883
|
+
? 'autoloopNew'
|
|
884
|
+
: state.tab === 'council'
|
|
885
|
+
? 'councilNew'
|
|
886
|
+
: 'forgeNew';
|
|
887
|
+
$('main').innerHTML = `
|
|
888
|
+
<div class="empty empty-cta">
|
|
889
|
+
<h2>No ${escapeHtml(noun)}s yet</h2>
|
|
890
|
+
<p>Click <strong>+ New</strong> in the sidebar to start your first one.</p>
|
|
891
|
+
<button type="button" onclick="document.getElementById('${triggerId}').click()">+ New ${escapeHtml(noun)}</button>
|
|
892
|
+
</div>
|
|
893
|
+
`;
|
|
894
|
+
} else {
|
|
895
|
+
$('main').innerHTML =
|
|
896
|
+
'<div class="empty" style="margin-top:80px">Select a run to view.</div>';
|
|
897
|
+
}
|
|
454
898
|
}
|
|
455
899
|
|
|
456
900
|
// ─── List polling ─────────────────────────────────────────────
|
|
457
901
|
async function refreshList() {
|
|
458
902
|
try {
|
|
459
|
-
const url =
|
|
460
|
-
|
|
903
|
+
const url =
|
|
904
|
+
state.tab === 'autoloop'
|
|
905
|
+
? '/autoloop/list'
|
|
906
|
+
: state.tab === 'council'
|
|
907
|
+
? '/council/list'
|
|
908
|
+
: '/ultraapp/list';
|
|
909
|
+
const opts = state.tab === 'forge' ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } : {};
|
|
910
|
+
const r = await fetch(url, opts);
|
|
461
911
|
const j = await r.json();
|
|
462
|
-
state.runs =
|
|
912
|
+
state.runs =
|
|
913
|
+
state.tab === 'autoloop'
|
|
914
|
+
? j.runs || []
|
|
915
|
+
: state.tab === 'council'
|
|
916
|
+
? j.councils || []
|
|
917
|
+
: j.runs || [];
|
|
463
918
|
renderList();
|
|
919
|
+
// Refresh the empty-state CTA — runs may have just appeared or
|
|
920
|
+
// disappeared, and renderEmptyMain's text depends on state.runs.length.
|
|
921
|
+
if (!state.selectedId) renderEmptyMain();
|
|
464
922
|
$('status').textContent = 'connected';
|
|
465
923
|
} catch (err) {
|
|
466
924
|
$('status').textContent = 'disconnected';
|
|
@@ -471,9 +929,11 @@
|
|
|
471
929
|
function renderList() {
|
|
472
930
|
const ul = $('runlist');
|
|
473
931
|
if (state.runs.length === 0) {
|
|
932
|
+
const noun =
|
|
933
|
+
state.tab === 'autoloop' ? 'runs' : state.tab === 'council' ? 'councils' : 'apps';
|
|
474
934
|
ul.innerHTML =
|
|
475
935
|
'<li class="empty" style="padding:20px;list-style:none">No active ' +
|
|
476
|
-
|
|
936
|
+
noun +
|
|
477
937
|
'.</li>';
|
|
478
938
|
return;
|
|
479
939
|
}
|
|
@@ -488,8 +948,11 @@
|
|
|
488
948
|
<span>iter ${r.iter}</span>
|
|
489
949
|
<span>↑${r.push_log_count}</span>
|
|
490
950
|
</div>
|
|
951
|
+
<div class="lifecycle-actions">
|
|
952
|
+
<button data-act="delete">Delete</button>
|
|
953
|
+
</div>
|
|
491
954
|
</li>`;
|
|
492
|
-
} else {
|
|
955
|
+
} else if (state.tab === 'council') {
|
|
493
956
|
return `
|
|
494
957
|
<li class="runItem" data-id="${escapeHtml(r.id)}" aria-selected="${r.id === state.selectedId}">
|
|
495
958
|
<div class="id">${escapeHtml(r.id)}</div>
|
|
@@ -498,16 +961,70 @@
|
|
|
498
961
|
<span>${r.responses?.length ?? 0} resp</span>
|
|
499
962
|
</div>
|
|
500
963
|
</li>`;
|
|
964
|
+
} else {
|
|
965
|
+
const showLifecycle = r.mode === 'done' || r.mode === 'failed' || r.mode === 'build-complete';
|
|
966
|
+
const lifecycleHtml = showLifecycle
|
|
967
|
+
? `<div class="lifecycle-actions">
|
|
968
|
+
${r.mode === 'done' ? `<button data-act="stop">Stop</button>` : ''}
|
|
969
|
+
${r.mode !== 'done' ? `<button data-act="start">Start</button>` : ''}
|
|
970
|
+
<button data-act="delete">Delete</button>
|
|
971
|
+
</div>`
|
|
972
|
+
: '';
|
|
973
|
+
return `
|
|
974
|
+
<li class="runItem" data-id="${escapeHtml(r.runId)}" aria-selected="${r.runId === state.selectedId}">
|
|
975
|
+
<div class="id">${escapeHtml(r.title || r.runId)}</div>
|
|
976
|
+
<div class="meta">
|
|
977
|
+
<span class="badge ${r.mode}">${r.mode}</span>
|
|
978
|
+
</div>
|
|
979
|
+
${lifecycleHtml}
|
|
980
|
+
</li>`;
|
|
501
981
|
}
|
|
502
982
|
})
|
|
503
983
|
.join('');
|
|
504
984
|
ul.innerHTML = html;
|
|
505
985
|
ul.querySelectorAll('.runItem').forEach((el) => {
|
|
506
|
-
|
|
986
|
+
// Click on item itself (not action button) opens it
|
|
987
|
+
el.addEventListener('click', (ev) => {
|
|
988
|
+
const t = ev.target;
|
|
989
|
+
if (t && t.closest('.lifecycle-actions')) return;
|
|
507
990
|
state.selectedId = el.dataset.id;
|
|
508
991
|
renderList();
|
|
509
992
|
openDetail(el.dataset.id);
|
|
510
993
|
});
|
|
994
|
+
el.querySelectorAll('.lifecycle-actions button').forEach((btn) => {
|
|
995
|
+
btn.addEventListener('click', async (ev) => {
|
|
996
|
+
ev.stopPropagation();
|
|
997
|
+
const action = btn.dataset.act;
|
|
998
|
+
const id = el.dataset.id;
|
|
999
|
+
if (state.tab === 'autoloop') {
|
|
1000
|
+
if (action !== 'delete') return;
|
|
1001
|
+
if (!confirm(`Delete autoloop run "${id}"? The registry entry is removed; the ledger directory under tasks/${id}/ is kept on disk.`)) return;
|
|
1002
|
+
try {
|
|
1003
|
+
const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
1004
|
+
if (!r.ok) {
|
|
1005
|
+
let detail = `HTTP ${r.status}`;
|
|
1006
|
+
try { const j = await r.json(); if (j && j.error) detail = j.error; } catch { /* ignore */ }
|
|
1007
|
+
alert(`Delete failed: ${detail}`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
// If we were viewing this run, close the detail pane and pick another.
|
|
1011
|
+
if (state.selectedId === id) {
|
|
1012
|
+
closeStreams();
|
|
1013
|
+
state.selectedId = null;
|
|
1014
|
+
$('main').innerHTML = '<div class="empty">Select a run from the left.</div>';
|
|
1015
|
+
}
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
alert(`Delete failed: ${(err && err.message) || err}`);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
refreshList();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (action === 'delete' && !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')) return;
|
|
1024
|
+
await forgeLifecycleAction(id, action);
|
|
1025
|
+
refreshList();
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
511
1028
|
});
|
|
512
1029
|
}
|
|
513
1030
|
|
|
@@ -515,7 +1032,410 @@
|
|
|
515
1032
|
function openDetail(id) {
|
|
516
1033
|
closeStreams();
|
|
517
1034
|
if (state.tab === 'autoloop') openAutoloop(id);
|
|
518
|
-
else openCouncil(id);
|
|
1035
|
+
else if (state.tab === 'council') openCouncil(id);
|
|
1036
|
+
else openForgeRun(id);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ─── Forge ────────────────────────────────────────────────────
|
|
1040
|
+
$('forgeNew').addEventListener('click', async () => {
|
|
1041
|
+
const r = await fetch('/ultraapp/new', {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1044
|
+
body: '{}',
|
|
1045
|
+
});
|
|
1046
|
+
const j = await r.json();
|
|
1047
|
+
if (!j.ok) {
|
|
1048
|
+
alert('Failed to create run: ' + (j.error || 'unknown'));
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
state.selectedId = j.runId;
|
|
1052
|
+
await refreshList();
|
|
1053
|
+
openForgeRun(j.runId);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
function renderForgeShell() {
|
|
1057
|
+
$('main').innerHTML = `
|
|
1058
|
+
<div class="forge-layout">
|
|
1059
|
+
<div class="forge-chat">
|
|
1060
|
+
<div class="forge-mode" id="forgeMode" data-mode="interview">interview</div>
|
|
1061
|
+
<div class="forge-messages" id="forgeMessages"></div>
|
|
1062
|
+
<div class="forge-input">
|
|
1063
|
+
<textarea id="forgeFreeform" placeholder="Free-form input (or click an option above and Submit)" rows="2"></textarea>
|
|
1064
|
+
<button id="forgeSubmit">Submit</button>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div class="forge-input-row">
|
|
1067
|
+
<input type="file" id="forgeFile" multiple />
|
|
1068
|
+
<input type="text" id="forgePath" placeholder="…or paste an absolute path" />
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="forge-spec">
|
|
1072
|
+
<h3>AppSpec</h3>
|
|
1073
|
+
<textarea id="forgeSpecJson" rows="20" spellcheck="false"></textarea>
|
|
1074
|
+
<div class="forge-spec-actions">
|
|
1075
|
+
<button id="forgeSpecApply">Apply edit</button>
|
|
1076
|
+
<span class="forge-spec-msg" id="forgeSpecMsg"></span>
|
|
1077
|
+
</div>
|
|
1078
|
+
<button id="forgeStartBuild" disabled>Start Build</button>
|
|
1079
|
+
<div id="forgeVersions" class="forge-versions"></div>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
`;
|
|
1083
|
+
$('forgeSubmit').addEventListener('click', forgeOnSubmit);
|
|
1084
|
+
$('forgeSpecApply').addEventListener('click', forgeOnSpecApply);
|
|
1085
|
+
$('forgeStartBuild').addEventListener('click', forgeOnStartBuild);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function forgeOnStartBuild() {
|
|
1089
|
+
const runId = state.forge.runId;
|
|
1090
|
+
if (!runId) return;
|
|
1091
|
+
const btn = $('forgeStartBuild');
|
|
1092
|
+
if (btn) btn.disabled = true;
|
|
1093
|
+
try {
|
|
1094
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/build', {
|
|
1095
|
+
method: 'POST',
|
|
1096
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1097
|
+
body: '{}',
|
|
1098
|
+
});
|
|
1099
|
+
const j = await r.json();
|
|
1100
|
+
if (!j.ok) {
|
|
1101
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] start build: ' + (j.error || 'unknown') });
|
|
1102
|
+
if (btn) btn.disabled = false;
|
|
1103
|
+
}
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] start build: ' + err.message });
|
|
1106
|
+
if (btn) btn.disabled = false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function openForgeRun(runId) {
|
|
1111
|
+
state.forge.runId = runId;
|
|
1112
|
+
renderForgeShell();
|
|
1113
|
+
try {
|
|
1114
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId), {
|
|
1115
|
+
method: 'POST',
|
|
1116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1117
|
+
body: '{}',
|
|
1118
|
+
});
|
|
1119
|
+
const j = await r.json();
|
|
1120
|
+
if (!j.ok) {
|
|
1121
|
+
$('forgeMessages').textContent = 'error: ' + (j.error || 'unknown');
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
state.forge.spec = j.spec;
|
|
1125
|
+
state.forge.mode = j.state.mode;
|
|
1126
|
+
renderSpec(j.spec);
|
|
1127
|
+
setForgeMode(j.state.mode);
|
|
1128
|
+
for (const e of j.chat || []) renderChatEntry(e);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
$('forgeMessages').textContent = 'fetch failed: ' + err.message;
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const es = new EventSource('/ultraapp/' + encodeURIComponent(runId) + '/events');
|
|
1134
|
+
state.forge.eventSource = es;
|
|
1135
|
+
es.addEventListener('ultraapp', (ev) => {
|
|
1136
|
+
const data = JSON.parse(ev.data);
|
|
1137
|
+
if (data.type === 'question') renderQuestion(data.question);
|
|
1138
|
+
else if (data.type === 'spec-updated') {
|
|
1139
|
+
state.forge.spec = data.spec;
|
|
1140
|
+
renderSpec(data.spec);
|
|
1141
|
+
} else if (data.type === 'chat') renderChatEntry(data.entry);
|
|
1142
|
+
else if (data.type === 'interview-complete') {
|
|
1143
|
+
renderChatEntry({ role: 'system', kind: 'narrator', text: 'Interview complete: ' + data.summary });
|
|
1144
|
+
$('forgeStartBuild').disabled = false;
|
|
1145
|
+
} else if (data.type === 'build-event') {
|
|
1146
|
+
const ev = data.event;
|
|
1147
|
+
if (ev.type === 'queued') setForgeMode('queued');
|
|
1148
|
+
else if (ev.type === 'build-start') setForgeMode('building');
|
|
1149
|
+
else if (ev.type === 'build-complete') setForgeMode('build-complete');
|
|
1150
|
+
else if (ev.type === 'build-failed') {
|
|
1151
|
+
setForgeMode('failed');
|
|
1152
|
+
const btn = $('forgeStartBuild');
|
|
1153
|
+
if (btn) btn.disabled = false;
|
|
1154
|
+
} else if (ev.type === 'build-cancelled') {
|
|
1155
|
+
setForgeMode('failed');
|
|
1156
|
+
const btn = $('forgeStartBuild');
|
|
1157
|
+
if (btn) btn.disabled = false;
|
|
1158
|
+
}
|
|
1159
|
+
} else if (data.type === 'app-url') {
|
|
1160
|
+
setForgeMode('done');
|
|
1161
|
+
renderShareCard(data.url, data.version);
|
|
1162
|
+
} else if (data.type === 'error') {
|
|
1163
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + data.message });
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
es.onerror = () => {
|
|
1167
|
+
/* allow auto-reconnect */
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function setForgeMode(m) {
|
|
1172
|
+
state.forge.mode = m;
|
|
1173
|
+
const el = $('forgeMode');
|
|
1174
|
+
if (el) {
|
|
1175
|
+
el.dataset.mode = m;
|
|
1176
|
+
el.textContent = m;
|
|
1177
|
+
}
|
|
1178
|
+
const btn = $('forgeStartBuild');
|
|
1179
|
+
if (btn) {
|
|
1180
|
+
// Disabled while interview, queued, or building. Enabled at any
|
|
1181
|
+
// resting state from which a (re)build is sensible.
|
|
1182
|
+
const lockedModes = new Set(['interview', 'queued', 'building']);
|
|
1183
|
+
btn.disabled = lockedModes.has(m);
|
|
1184
|
+
}
|
|
1185
|
+
// Hint the input's role per mode (interview = answer; done = feedback).
|
|
1186
|
+
const ff = $('forgeFreeform');
|
|
1187
|
+
if (ff) {
|
|
1188
|
+
ff.placeholder =
|
|
1189
|
+
m === 'done'
|
|
1190
|
+
? 'Done — type feedback (e.g. "make button green" / "add a thumbnail step")'
|
|
1191
|
+
: 'Free-form input (or click an option above and Submit)';
|
|
1192
|
+
}
|
|
1193
|
+
// Refresh versions panel when entering a state that may have artifacts.
|
|
1194
|
+
if (m === 'done' || m === 'build-complete') {
|
|
1195
|
+
void forgeRefreshVersions();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function renderSpec(spec) {
|
|
1200
|
+
const ta = $('forgeSpecJson');
|
|
1201
|
+
if (!ta) return;
|
|
1202
|
+
if (document.activeElement !== ta) {
|
|
1203
|
+
ta.value = JSON.stringify(spec, null, 2);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function renderChatEntry(entry) {
|
|
1208
|
+
const m = $('forgeMessages');
|
|
1209
|
+
if (!m) return;
|
|
1210
|
+
const div = document.createElement('div');
|
|
1211
|
+
div.className = 'forge-msg ' + entry.role;
|
|
1212
|
+
div.textContent = entry.text;
|
|
1213
|
+
m.appendChild(div);
|
|
1214
|
+
m.scrollTop = m.scrollHeight;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function renderShareCard(url, version) {
|
|
1218
|
+
const m = $('forgeMessages');
|
|
1219
|
+
if (!m) return;
|
|
1220
|
+
const card = document.createElement('div');
|
|
1221
|
+
card.className = 'forge-share';
|
|
1222
|
+
card.innerHTML =
|
|
1223
|
+
`<strong>App deployed (${escapeHtml(version)})</strong>` +
|
|
1224
|
+
`<div><a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a></div>` +
|
|
1225
|
+
`<div class="forge-share-actions">` +
|
|
1226
|
+
`<button data-act="copy">Copy</button>` +
|
|
1227
|
+
`<button data-act="open">Open</button>` +
|
|
1228
|
+
`<button data-act="public">Make Public…</button>` +
|
|
1229
|
+
`</div>`;
|
|
1230
|
+
card.querySelector('[data-act="copy"]').addEventListener('click', () => {
|
|
1231
|
+
navigator.clipboard?.writeText(url);
|
|
1232
|
+
});
|
|
1233
|
+
card.querySelector('[data-act="open"]').addEventListener('click', () => {
|
|
1234
|
+
window.open(url, '_blank');
|
|
1235
|
+
});
|
|
1236
|
+
card.querySelector('[data-act="public"]').addEventListener('click', () => openPublicModal(url));
|
|
1237
|
+
m.appendChild(card);
|
|
1238
|
+
m.scrollTop = m.scrollHeight;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function openPublicModal(localUrl) {
|
|
1242
|
+
const dlg = document.createElement('dialog');
|
|
1243
|
+
dlg.className = 'forge-public-modal';
|
|
1244
|
+
const u = new URL(localUrl);
|
|
1245
|
+
dlg.innerHTML =
|
|
1246
|
+
`<h3>Make this app public</h3>` +
|
|
1247
|
+
`<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>` +
|
|
1248
|
+
`<h4>Cloudflare Tunnel</h4>` +
|
|
1249
|
+
`<pre>cloudflared tunnel --url ${escapeHtml(u.origin)}</pre>` +
|
|
1250
|
+
`<h4>ngrok</h4>` +
|
|
1251
|
+
`<pre>ngrok http ${escapeHtml(u.port || '19000')}</pre>` +
|
|
1252
|
+
`<h4>Tailscale Funnel</h4>` +
|
|
1253
|
+
`<pre>tailscale serve --bg ${escapeHtml(u.origin)}</pre>` +
|
|
1254
|
+
`<h4>Self-hosted (Caddy)</h4>` +
|
|
1255
|
+
`<pre>example.com {\n reverse_proxy ${escapeHtml(u.host)}\n}</pre>` +
|
|
1256
|
+
`<p>Once exposed, your app will be reachable at <code>{your-public-host}${escapeHtml(u.pathname)}</code>.</p>` +
|
|
1257
|
+
`<button data-act="close">Close</button>`;
|
|
1258
|
+
document.body.appendChild(dlg);
|
|
1259
|
+
dlg.showModal();
|
|
1260
|
+
dlg.querySelector('[data-act="close"]').addEventListener('click', () => {
|
|
1261
|
+
dlg.close();
|
|
1262
|
+
dlg.remove();
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function forgeLifecycleAction(runId, action) {
|
|
1267
|
+
try {
|
|
1268
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/' + action, {
|
|
1269
|
+
method: 'POST',
|
|
1270
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1271
|
+
body: '{}',
|
|
1272
|
+
});
|
|
1273
|
+
const j = await r.json();
|
|
1274
|
+
if (!j.ok) {
|
|
1275
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + action + ': ' + (j.error || 'unknown') });
|
|
1276
|
+
}
|
|
1277
|
+
if (action === 'delete' && j.ok && state.forge.runId === runId) {
|
|
1278
|
+
// Run is gone — close it and refresh sidebar
|
|
1279
|
+
state.forge.runId = null;
|
|
1280
|
+
$('forgeMessages').innerHTML = '';
|
|
1281
|
+
}
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + action + ': ' + err.message });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function renderQuestion(q) {
|
|
1288
|
+
state.forge.currentQuestion = q;
|
|
1289
|
+
const m = $('forgeMessages');
|
|
1290
|
+
if (!m) return;
|
|
1291
|
+
const card = document.createElement('div');
|
|
1292
|
+
card.className = 'forge-question';
|
|
1293
|
+
card.innerHTML =
|
|
1294
|
+
(q.context ? `<div class="ctx">${escapeHtml(q.context)}</div>` : '') +
|
|
1295
|
+
`<div class="q">${escapeHtml(q.question)}</div>` +
|
|
1296
|
+
`<div class="opts">` +
|
|
1297
|
+
q.options
|
|
1298
|
+
.map(
|
|
1299
|
+
(o) =>
|
|
1300
|
+
`<span class="opt ${o.value === q.recommended ? 'recommended' : ''}" data-val="${escapeHtml(o.value)}">${escapeHtml(o.label)}</span>`,
|
|
1301
|
+
)
|
|
1302
|
+
.join('') +
|
|
1303
|
+
`</div>`;
|
|
1304
|
+
card.querySelectorAll('.opt').forEach((el) => {
|
|
1305
|
+
el.addEventListener('click', () => {
|
|
1306
|
+
card.querySelectorAll('.opt').forEach((x) => x.classList.remove('selected'));
|
|
1307
|
+
el.classList.add('selected');
|
|
1308
|
+
state.forge.currentQuestion._selected = el.dataset.val;
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
m.appendChild(card);
|
|
1312
|
+
m.scrollTop = m.scrollHeight;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function forgeOnSubmit() {
|
|
1316
|
+
const runId = state.forge.runId;
|
|
1317
|
+
if (!runId) return;
|
|
1318
|
+
const ff = $('forgeFreeform');
|
|
1319
|
+
const path = $('forgePath');
|
|
1320
|
+
const fileInput = $('forgeFile');
|
|
1321
|
+
|
|
1322
|
+
for (const f of fileInput.files) {
|
|
1323
|
+
const buf = await f.arrayBuffer();
|
|
1324
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
1325
|
+
await fetch('/ultraapp/' + encodeURIComponent(runId) + '/files', {
|
|
1326
|
+
method: 'POST',
|
|
1327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1328
|
+
body: JSON.stringify({ filename: f.name, dataB64: b64 }),
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
fileInput.value = '';
|
|
1332
|
+
|
|
1333
|
+
if (path.value.trim()) {
|
|
1334
|
+
await fetch('/ultraapp/' + encodeURIComponent(runId) + '/files', {
|
|
1335
|
+
method: 'POST',
|
|
1336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1337
|
+
body: JSON.stringify({ absolutePath: path.value.trim() }),
|
|
1338
|
+
});
|
|
1339
|
+
path.value = '';
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const freeform = ff.value.trim();
|
|
1343
|
+
const selected = state.forge.currentQuestion?._selected;
|
|
1344
|
+
// Mode-aware routing: in done mode, free-form text is feedback,
|
|
1345
|
+
// not an interview answer. /feedback runs through the classifier
|
|
1346
|
+
// and routes to patcher / spec-delta / structural-suggest.
|
|
1347
|
+
if (state.forge.mode === 'done' && freeform) {
|
|
1348
|
+
await fetch('/ultraapp/' + encodeURIComponent(runId) + '/feedback', {
|
|
1349
|
+
method: 'POST',
|
|
1350
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1351
|
+
body: JSON.stringify({ text: freeform }),
|
|
1352
|
+
});
|
|
1353
|
+
ff.value = '';
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (freeform || selected) {
|
|
1357
|
+
const body = freeform ? { value: selected ?? '', freeform } : { value: selected };
|
|
1358
|
+
await fetch('/ultraapp/' + encodeURIComponent(runId) + '/answer', {
|
|
1359
|
+
method: 'POST',
|
|
1360
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1361
|
+
body: JSON.stringify(body),
|
|
1362
|
+
});
|
|
1363
|
+
ff.value = '';
|
|
1364
|
+
state.forge.currentQuestion = null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function forgeRefreshVersions() {
|
|
1369
|
+
const runId = state.forge.runId;
|
|
1370
|
+
if (!runId) return;
|
|
1371
|
+
const panel = $('forgeVersions');
|
|
1372
|
+
if (!panel) return;
|
|
1373
|
+
try {
|
|
1374
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/artifacts', {
|
|
1375
|
+
method: 'POST',
|
|
1376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1377
|
+
body: '{}',
|
|
1378
|
+
});
|
|
1379
|
+
const j = await r.json();
|
|
1380
|
+
if (!j.ok || !j.artifacts || j.artifacts.length === 0) {
|
|
1381
|
+
panel.innerHTML = '';
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
const html = j.artifacts
|
|
1385
|
+
.map((a) => {
|
|
1386
|
+
const isDeployed = !!a.deploy;
|
|
1387
|
+
const url = a.deploy?.url || '';
|
|
1388
|
+
return `<li class="version-row">
|
|
1389
|
+
<span class="version-tag">${escapeHtml(a.version)}</span>
|
|
1390
|
+
<span class="version-meta">${a.builtAt ? new Date(a.builtAt).toLocaleString() : ''}</span>
|
|
1391
|
+
${isDeployed ? `<a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a>` : '<span class="version-meta">(not deployed)</span>'}
|
|
1392
|
+
${isDeployed ? `<button data-promote="${escapeHtml(a.version)}">Promote</button>` : ''}
|
|
1393
|
+
</li>`;
|
|
1394
|
+
})
|
|
1395
|
+
.join('');
|
|
1396
|
+
panel.innerHTML = '<h4>Versions</h4><ul class="version-list">' + html + '</ul>';
|
|
1397
|
+
panel.querySelectorAll('button[data-promote]').forEach((btn) => {
|
|
1398
|
+
btn.addEventListener('click', async () => {
|
|
1399
|
+
const v = btn.dataset.promote;
|
|
1400
|
+
if (!confirm(`Promote ${v} as the deployed version?`)) return;
|
|
1401
|
+
btn.disabled = true;
|
|
1402
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/promote-version', {
|
|
1403
|
+
method: 'POST',
|
|
1404
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1405
|
+
body: JSON.stringify({ version: v }),
|
|
1406
|
+
});
|
|
1407
|
+
const j = await r.json();
|
|
1408
|
+
if (!j.ok) {
|
|
1409
|
+
renderChatEntry({ role: 'system', kind: 'error', text: '[error] promote: ' + (j.error || 'unknown') });
|
|
1410
|
+
}
|
|
1411
|
+
btn.disabled = false;
|
|
1412
|
+
await forgeRefreshVersions();
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
/* best-effort */
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
async function forgeOnSpecApply() {
|
|
1421
|
+
const runId = state.forge.runId;
|
|
1422
|
+
if (!runId) return;
|
|
1423
|
+
const msg = $('forgeSpecMsg');
|
|
1424
|
+
msg.textContent = '';
|
|
1425
|
+
let next;
|
|
1426
|
+
try {
|
|
1427
|
+
next = JSON.parse($('forgeSpecJson').value);
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
msg.textContent = 'JSON parse error: ' + e.message;
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const r = await fetch('/ultraapp/' + encodeURIComponent(runId) + '/spec-edit', {
|
|
1433
|
+
method: 'POST',
|
|
1434
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1435
|
+
body: JSON.stringify({ patch: [{ op: 'replace', path: '', value: next }] }),
|
|
1436
|
+
});
|
|
1437
|
+
const j = await r.json();
|
|
1438
|
+
msg.textContent = j.ok ? 'Saved.' : 'Error: ' + (j.error ?? 'unknown');
|
|
519
1439
|
}
|
|
520
1440
|
|
|
521
1441
|
// ─── Autoloop detail ──────────────────────────────────────────
|
|
@@ -590,9 +1510,21 @@
|
|
|
590
1510
|
});
|
|
591
1511
|
es.addEventListener('planner_reply', (e) => {
|
|
592
1512
|
const { text } = JSON.parse(e.data);
|
|
1513
|
+
// Clear the "waiting…" placeholder, if any, before appending the reply.
|
|
1514
|
+
state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
|
|
593
1515
|
state.autoloop.plannerLog.push({ who: 'planner', text, ts: new Date().toISOString() });
|
|
594
1516
|
paintPlanner();
|
|
595
1517
|
});
|
|
1518
|
+
es.addEventListener('planner_error', (e) => {
|
|
1519
|
+
const { message } = JSON.parse(e.data);
|
|
1520
|
+
state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
|
|
1521
|
+
state.autoloop.plannerLog.push({
|
|
1522
|
+
who: 'system',
|
|
1523
|
+
text: `[planner error] ${message}`,
|
|
1524
|
+
ts: new Date().toISOString(),
|
|
1525
|
+
});
|
|
1526
|
+
paintPlanner();
|
|
1527
|
+
});
|
|
596
1528
|
es.addEventListener('coder_reply', (e) => {
|
|
597
1529
|
const { text } = JSON.parse(e.data);
|
|
598
1530
|
state.autoloop.coderLog.push({ who: 'coder', text, ts: new Date().toISOString() });
|
|
@@ -741,16 +1673,47 @@
|
|
|
741
1673
|
const btn = $('auto-send');
|
|
742
1674
|
btn.disabled = true;
|
|
743
1675
|
ta.disabled = true;
|
|
1676
|
+
// Clear input optimistically — the message is already echoed in the
|
|
1677
|
+
// planner log below, and the new server contract is fire-and-forget
|
|
1678
|
+
// (returns 202 immediately; reply streams via SSE). If the POST fails
|
|
1679
|
+
// synchronously we surface the error in the log.
|
|
1680
|
+
ta.value = '';
|
|
744
1681
|
state.autoloop.plannerLog.push({ who: 'user', text, ts: new Date().toISOString() });
|
|
1682
|
+
state.autoloop.plannerLog.push({
|
|
1683
|
+
who: 'pending',
|
|
1684
|
+
text: 'Planner is thinking…',
|
|
1685
|
+
ts: new Date().toISOString(),
|
|
1686
|
+
});
|
|
745
1687
|
paintPlanner();
|
|
746
1688
|
try {
|
|
747
|
-
await fetch(
|
|
1689
|
+
const r = await fetch(`/autoloop/${encodeURIComponent(id)}/chat`, {
|
|
748
1690
|
method: 'POST',
|
|
749
1691
|
headers: { 'content-type': 'application/json' },
|
|
750
|
-
body: JSON.stringify({
|
|
1692
|
+
body: JSON.stringify({ text }),
|
|
751
1693
|
});
|
|
752
|
-
|
|
1694
|
+
// Server returns 202 on accepted (queued); 200 is also fine for
|
|
1695
|
+
// legacy clients. Anything else is a real failure.
|
|
1696
|
+
if (!r.ok) {
|
|
1697
|
+
let detail = `HTTP ${r.status}`;
|
|
1698
|
+
try {
|
|
1699
|
+
const j = await r.json();
|
|
1700
|
+
if (j && typeof j.error === 'string') detail = j.error;
|
|
1701
|
+
} catch {
|
|
1702
|
+
/* non-JSON body — keep the status code */
|
|
1703
|
+
}
|
|
1704
|
+
state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
|
|
1705
|
+
state.autoloop.plannerLog.push({
|
|
1706
|
+
who: 'system',
|
|
1707
|
+
text: `[error] ${detail}`,
|
|
1708
|
+
ts: new Date().toISOString(),
|
|
1709
|
+
});
|
|
1710
|
+
paintPlanner();
|
|
1711
|
+
}
|
|
1712
|
+
// On success, do nothing — the planner_reply / planner_error SSE
|
|
1713
|
+
// handler upstairs clears the "thinking…" placeholder when the
|
|
1714
|
+
// Planner actually replies (or errors).
|
|
753
1715
|
} catch (err) {
|
|
1716
|
+
state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
|
|
754
1717
|
state.autoloop.plannerLog.push({
|
|
755
1718
|
who: 'system',
|
|
756
1719
|
text: `[error] ${(err && err.message) || 'send failed'}`,
|
|
@@ -903,7 +1866,98 @@
|
|
|
903
1866
|
el.scrollTop = el.scrollHeight;
|
|
904
1867
|
}
|
|
905
1868
|
|
|
1869
|
+
// ─── Modal launcher (Council / Autoloop) ─────────────────────
|
|
1870
|
+
//
|
|
1871
|
+
// forgeNew uses a different shape (no form: ultraapp is interview-
|
|
1872
|
+
// driven, so POST /ultraapp/new with an empty body is enough). For
|
|
1873
|
+
// Council / Autoloop we need real form input; one modal handles
|
|
1874
|
+
// both, switching visible fields by state.tab.
|
|
1875
|
+
function openLauncherModal() {
|
|
1876
|
+
const root = $('modalRoot');
|
|
1877
|
+
$('modalTitle').textContent =
|
|
1878
|
+
state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
|
|
1879
|
+
root.querySelectorAll('[data-for-tab]').forEach((el) => {
|
|
1880
|
+
const visible = el.dataset.forTab === state.tab;
|
|
1881
|
+
el.style.display = visible ? 'contents' : 'none';
|
|
1882
|
+
// Only the visible group's required fields actually count.
|
|
1883
|
+
el.querySelectorAll('input,textarea').forEach((f) => {
|
|
1884
|
+
if (visible && (f.name === 'task' || f.name === 'projectDir' || f.name === 'workspace')) {
|
|
1885
|
+
f.required = true;
|
|
1886
|
+
} else {
|
|
1887
|
+
f.required = false;
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
root.hidden = false;
|
|
1892
|
+
setTimeout(() => {
|
|
1893
|
+
const firstVisible = root.querySelector(
|
|
1894
|
+
`[data-for-tab="${state.tab}"] input, [data-for-tab="${state.tab}"] textarea`,
|
|
1895
|
+
);
|
|
1896
|
+
firstVisible?.focus();
|
|
1897
|
+
}, 0);
|
|
1898
|
+
}
|
|
1899
|
+
function closeLauncherModal() {
|
|
1900
|
+
$('modalRoot').hidden = true;
|
|
1901
|
+
$('modalForm').reset();
|
|
1902
|
+
}
|
|
1903
|
+
$('councilNew').addEventListener('click', openLauncherModal);
|
|
1904
|
+
$('autoloopNew').addEventListener('click', openLauncherModal);
|
|
1905
|
+
$('modalClose').addEventListener('click', closeLauncherModal);
|
|
1906
|
+
$('modalCancel').addEventListener('click', closeLauncherModal);
|
|
1907
|
+
$('modalRoot').addEventListener('click', (e) => {
|
|
1908
|
+
if (e.target.id === 'modalRoot') closeLauncherModal();
|
|
1909
|
+
});
|
|
1910
|
+
document.addEventListener('keydown', (e) => {
|
|
1911
|
+
if (e.key === 'Escape' && !$('modalRoot').hidden) closeLauncherModal();
|
|
1912
|
+
});
|
|
1913
|
+
$('modalForm').addEventListener('submit', async (e) => {
|
|
1914
|
+
e.preventDefault();
|
|
1915
|
+
const fd = new FormData(e.target);
|
|
1916
|
+
const body = {};
|
|
1917
|
+
if (state.tab === 'council') {
|
|
1918
|
+
body.task = (fd.get('task') || '').toString().trim();
|
|
1919
|
+
body.projectDir = (fd.get('projectDir') || '').toString().trim();
|
|
1920
|
+
const mr = (fd.get('maxRounds') || '').toString().trim();
|
|
1921
|
+
if (mr) body.maxRounds = Number(mr);
|
|
1922
|
+
} else if (state.tab === 'autoloop') {
|
|
1923
|
+
body.workspace = (fd.get('workspace') || '').toString().trim();
|
|
1924
|
+
const pm = (fd.get('planner_model') || '').toString().trim();
|
|
1925
|
+
if (pm) body.planner_model = pm;
|
|
1926
|
+
}
|
|
1927
|
+
const url = state.tab === 'council' ? '/council/new' : '/autoloop/new';
|
|
1928
|
+
const submitBtn = $('modalSubmit');
|
|
1929
|
+
submitBtn.disabled = true;
|
|
1930
|
+
submitBtn.textContent = 'Starting...';
|
|
1931
|
+
try {
|
|
1932
|
+
const r = await fetch(url, {
|
|
1933
|
+
method: 'POST',
|
|
1934
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1935
|
+
body: JSON.stringify(body),
|
|
1936
|
+
});
|
|
1937
|
+
const j = await r.json();
|
|
1938
|
+
if (!j.ok) {
|
|
1939
|
+
alert('Failed: ' + (j.error || `HTTP ${r.status}`));
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const id = j.id ?? j.run_id;
|
|
1943
|
+
closeLauncherModal();
|
|
1944
|
+
await refreshList();
|
|
1945
|
+
state.selectedId = id;
|
|
1946
|
+
renderList();
|
|
1947
|
+
openDetail(id);
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
alert('Failed: ' + err.message);
|
|
1950
|
+
} finally {
|
|
1951
|
+
submitBtn.disabled = false;
|
|
1952
|
+
submitBtn.textContent = 'Start';
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
|
|
906
1956
|
// ─── Boot ────────────────────────────────────────────────────
|
|
1957
|
+
// Make the right "+ New" button visible for the default tab.
|
|
1958
|
+
$('autoloopNew').hidden = state.tab !== 'autoloop';
|
|
1959
|
+
$('councilNew').hidden = state.tab !== 'council';
|
|
1960
|
+
$('forgeNew').hidden = state.tab !== 'forge';
|
|
907
1961
|
refreshList();
|
|
908
1962
|
</script>
|
|
909
1963
|
</body>
|