@enderfga/claw-orchestrator 4.0.0 → 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 +27 -1
- 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/src/autoloop/dispatcher.js +16 -2
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +8 -3
- package/dist/src/autoloop/planner-tools.js +21 -4
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- package/dist/src/dashboard/index.html +359 -6
- package/dist/src/embedded-server.d.ts +7 -0
- package/dist/src/embedded-server.js +188 -12
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/session-manager.d.ts +16 -0
- package/dist/src/session-manager.js +79 -1
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/ultraapp/manager.d.ts +9 -0
- package/dist/src/ultraapp/manager.js +31 -8
- package/dist/src/ultraapp/manager.js.map +1 -1
- package/package.json +1 -1
- package/skills/references/autoloop.md +6 -3
- package/skills/references/dashboard.md +153 -0
- package/skills/references/ultraapp.md +2 -2
|
@@ -533,7 +533,9 @@
|
|
|
533
533
|
cursor: pointer;
|
|
534
534
|
}
|
|
535
535
|
.forge-spec-msg { font-size: 11px; color: var(--muted); }
|
|
536
|
-
#forgeNew
|
|
536
|
+
#forgeNew,
|
|
537
|
+
#councilNew,
|
|
538
|
+
#autoloopNew {
|
|
537
539
|
margin: 8px 12px;
|
|
538
540
|
padding: 6px 12px;
|
|
539
541
|
border-radius: 4px;
|
|
@@ -543,6 +545,109 @@
|
|
|
543
545
|
cursor: pointer;
|
|
544
546
|
width: calc(100% - 24px);
|
|
545
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
|
+
}
|
|
546
651
|
#forgeStartBuild {
|
|
547
652
|
margin-top: 8px;
|
|
548
653
|
padding: 8px 14px;
|
|
@@ -602,7 +707,9 @@
|
|
|
602
707
|
<div class="body">
|
|
603
708
|
<aside class="sidebar">
|
|
604
709
|
<h2 id="sidebarTitle">Runs</h2>
|
|
605
|
-
<button id="
|
|
710
|
+
<button id="autoloopNew" hidden>+ New autoloop</button>
|
|
711
|
+
<button id="councilNew" hidden>+ New council</button>
|
|
712
|
+
<button id="forgeNew" hidden>+ New forge</button>
|
|
606
713
|
<ul class="runlist" id="runlist"></ul>
|
|
607
714
|
</aside>
|
|
608
715
|
|
|
@@ -614,6 +721,60 @@
|
|
|
614
721
|
<div class="footer">SSE-driven dashboard • served by claw-orchestrator embedded server</div>
|
|
615
722
|
</div>
|
|
616
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
|
+
|
|
617
778
|
<script>
|
|
618
779
|
// ─── State ────────────────────────────────────────────────────
|
|
619
780
|
const state = {
|
|
@@ -671,6 +832,8 @@
|
|
|
671
832
|
? 'Council sessions'
|
|
672
833
|
: 'Forge apps';
|
|
673
834
|
$('forgeNew').hidden = tab !== 'forge';
|
|
835
|
+
$('councilNew').hidden = tab !== 'council';
|
|
836
|
+
$('autoloopNew').hidden = tab !== 'autoloop';
|
|
674
837
|
state.selectedId = null;
|
|
675
838
|
closeStreams();
|
|
676
839
|
renderEmptyMain();
|
|
@@ -705,7 +868,33 @@
|
|
|
705
868
|
}
|
|
706
869
|
|
|
707
870
|
function renderEmptyMain() {
|
|
708
|
-
|
|
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
|
+
}
|
|
709
898
|
}
|
|
710
899
|
|
|
711
900
|
// ─── List polling ─────────────────────────────────────────────
|
|
@@ -727,6 +916,9 @@
|
|
|
727
916
|
? j.councils || []
|
|
728
917
|
: j.runs || [];
|
|
729
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();
|
|
730
922
|
$('status').textContent = 'connected';
|
|
731
923
|
} catch (err) {
|
|
732
924
|
$('status').textContent = 'disconnected';
|
|
@@ -756,6 +948,9 @@
|
|
|
756
948
|
<span>iter ${r.iter}</span>
|
|
757
949
|
<span>↑${r.push_log_count}</span>
|
|
758
950
|
</div>
|
|
951
|
+
<div class="lifecycle-actions">
|
|
952
|
+
<button data-act="delete">Delete</button>
|
|
953
|
+
</div>
|
|
759
954
|
</li>`;
|
|
760
955
|
} else if (state.tab === 'council') {
|
|
761
956
|
return `
|
|
@@ -801,6 +996,30 @@
|
|
|
801
996
|
ev.stopPropagation();
|
|
802
997
|
const action = btn.dataset.act;
|
|
803
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
|
+
}
|
|
804
1023
|
if (action === 'delete' && !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')) return;
|
|
805
1024
|
await forgeLifecycleAction(id, action);
|
|
806
1025
|
refreshList();
|
|
@@ -1291,9 +1510,21 @@
|
|
|
1291
1510
|
});
|
|
1292
1511
|
es.addEventListener('planner_reply', (e) => {
|
|
1293
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');
|
|
1294
1515
|
state.autoloop.plannerLog.push({ who: 'planner', text, ts: new Date().toISOString() });
|
|
1295
1516
|
paintPlanner();
|
|
1296
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
|
+
});
|
|
1297
1528
|
es.addEventListener('coder_reply', (e) => {
|
|
1298
1529
|
const { text } = JSON.parse(e.data);
|
|
1299
1530
|
state.autoloop.coderLog.push({ who: 'coder', text, ts: new Date().toISOString() });
|
|
@@ -1442,16 +1673,47 @@
|
|
|
1442
1673
|
const btn = $('auto-send');
|
|
1443
1674
|
btn.disabled = true;
|
|
1444
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 = '';
|
|
1445
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
|
+
});
|
|
1446
1687
|
paintPlanner();
|
|
1447
1688
|
try {
|
|
1448
|
-
await fetch(
|
|
1689
|
+
const r = await fetch(`/autoloop/${encodeURIComponent(id)}/chat`, {
|
|
1449
1690
|
method: 'POST',
|
|
1450
1691
|
headers: { 'content-type': 'application/json' },
|
|
1451
|
-
body: JSON.stringify({
|
|
1692
|
+
body: JSON.stringify({ text }),
|
|
1452
1693
|
});
|
|
1453
|
-
|
|
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).
|
|
1454
1715
|
} catch (err) {
|
|
1716
|
+
state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
|
|
1455
1717
|
state.autoloop.plannerLog.push({
|
|
1456
1718
|
who: 'system',
|
|
1457
1719
|
text: `[error] ${(err && err.message) || 'send failed'}`,
|
|
@@ -1604,7 +1866,98 @@
|
|
|
1604
1866
|
el.scrollTop = el.scrollHeight;
|
|
1605
1867
|
}
|
|
1606
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
|
+
|
|
1607
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';
|
|
1608
1961
|
refreshList();
|
|
1609
1962
|
</script>
|
|
1610
1963
|
</body>
|
|
@@ -18,6 +18,13 @@ export declare class EmbeddedServer {
|
|
|
18
18
|
private host;
|
|
19
19
|
constructor(manager: SessionManager, port?: number, host?: string);
|
|
20
20
|
private _checkRateLimit;
|
|
21
|
+
/**
|
|
22
|
+
* Try to reuse the token persisted by a previous server. Returns null if
|
|
23
|
+
* the file is absent, unreadable, the wrong shape, or the wrong perms.
|
|
24
|
+
* We accept any 32+ hex-char string (the format crypto.randomBytes(32)
|
|
25
|
+
* produces) — anything else is treated as garbage and rotated out.
|
|
26
|
+
*/
|
|
27
|
+
private _readPersistedToken;
|
|
21
28
|
private _writeTokenFile;
|
|
22
29
|
start(): Promise<number>;
|
|
23
30
|
stop(): Promise<void>;
|