@appsforgood/next-supabase-kit 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/BEST_PRACTICE_EVIDENCE.md +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/DOGFOOD.md +12 -1
  4. package/README.md +33 -4
  5. package/REPOSITORY_SETTINGS.md +12 -6
  6. package/SECURITY.md +14 -0
  7. package/SUPPLY_CHAIN.md +15 -14
  8. package/UPGRADE.md +14 -4
  9. package/antigravity/plugin.json +1 -1
  10. package/assistant-adapters/README.md +3 -0
  11. package/assistant-adapters/antigravity.md +2 -0
  12. package/assistant-adapters/claude-code-subagents.md +1 -0
  13. package/assistant-adapters/codex-agents.md +2 -0
  14. package/assistant-adapters/cursor-agent-kit.mdc +2 -0
  15. package/assistant-adapters/cursor-planner.mdc +1 -1
  16. package/assistant-adapters/orchestrator-runtime.md +39 -0
  17. package/checklists/frontend-quality.md +1 -0
  18. package/checklists/ui-detectors.md +6 -0
  19. package/dist/index.js +1813 -713
  20. package/dist/index.js.map +1 -1
  21. package/dist/studio/office/assets/office.css +167 -6
  22. package/dist/studio/office/assets/office.js +298 -4
  23. package/dist/studio/wizard/assets/wizard.js +7 -1
  24. package/examples/next-supabase-installed/.agent-kit/manifest.json +187 -14
  25. package/examples/next-supabase-installed/audit-output.json +405 -380
  26. package/package.json +27 -4
  27. package/prompts/frontend-design-review.md +6 -0
  28. package/prompts/screenshot-review.md +2 -1
  29. package/schemas/audit-report-v2.schema.json +71 -0
  30. package/schemas/audit-report.schema.json +16 -16
  31. package/schemas/orchestrator.schema.json +167 -0
  32. package/schemas/runtime-event.schema.json +66 -0
  33. package/schemas/runtime-run.schema.json +58 -0
  34. package/schemas/session-event.schema.json +2 -0
  35. package/templates/next-supabase/.agent-kit/orchestrator.json +48 -0
  36. package/templates/next-supabase/.agent-kit/runtime/gitignore.template +2 -0
  37. package/templates/next-supabase/AGENTS.md +2 -0
  38. package/templates/next-supabase/ASSISTANT_ADAPTERS.md +20 -0
  39. package/templates/next-supabase/DECISIONS.md +14 -0
  40. package/templates/next-supabase/DEPLOYMENT.md +4 -0
  41. package/templates/next-supabase/DESIGN.md +6 -0
  42. package/templates/next-supabase/DOCS.md +8 -0
  43. package/templates/next-supabase/MODEL_ROUTING.md +9 -0
  44. package/templates/next-supabase/QUALITY_GATES.md +5 -1
  45. package/templates/next-supabase/SECURITY.md +10 -0
  46. package/templates/next-supabase/SPEC.md +3 -1
  47. package/templates/next-supabase/STYLE_GUIDE.md +15 -0
  48. package/templates/next-supabase/TESTING.md +6 -0
  49. package/templates/next-supabase/UPGRADE.md +1 -0
@@ -566,6 +566,10 @@ body {
566
566
  display: none !important;
567
567
  }
568
568
 
569
+ [hidden] {
570
+ display: none !important;
571
+ }
572
+
569
573
  code {
570
574
  font-family: ui-monospace, Menlo, Consolas, monospace;
571
575
  font-size: 12px;
@@ -600,13 +604,160 @@ code {
600
604
  max-height: calc(100vh - 64px);
601
605
  }
602
606
 
607
+ .studio-tabs {
608
+ display: grid;
609
+ grid-template-columns: 1fr 1fr;
610
+ margin-bottom: 12px;
611
+ border: 1px solid rgba(255, 255, 255, 0.12);
612
+ border-radius: 6px;
613
+ overflow: hidden;
614
+ }
615
+
616
+ .studio-tab {
617
+ min-height: 34px;
618
+ border: 0;
619
+ border-right: 1px solid rgba(255, 255, 255, 0.12);
620
+ background: #0f172a;
621
+ color: #94a3b8;
622
+ font: inherit;
623
+ cursor: pointer;
624
+ }
625
+
626
+ .studio-tab:last-child {
627
+ border-right: 0;
628
+ }
629
+
630
+ .studio-tab.active {
631
+ background: #334155;
632
+ color: #f8fafc;
633
+ }
634
+
635
+ .studio-tab:focus-visible {
636
+ outline: 2px solid #fbbf24;
637
+ outline-offset: -2px;
638
+ }
639
+
640
+ .studio-controls {
641
+ display: grid;
642
+ gap: 10px;
643
+ margin-bottom: 12px;
644
+ padding-bottom: 12px;
645
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
646
+ }
647
+
648
+ .studio-label {
649
+ font-size: 12px;
650
+ color: #94a3b8;
651
+ }
652
+
653
+ .studio-note-form {
654
+ display: grid;
655
+ gap: 8px;
656
+ }
657
+
658
+ .studio-note-form input,
659
+ .studio-note-form select,
660
+ #session-picker,
661
+ #runtime-picker,
662
+ #runtime-goal {
663
+ width: 100%;
664
+ padding: 8px 10px;
665
+ border-radius: 8px;
666
+ border: 1px solid rgba(255, 255, 255, 0.12);
667
+ background: #0f172a;
668
+ color: #e2e8f0;
669
+ font-size: 13px;
670
+ }
671
+
672
+ #runtime-goal {
673
+ min-height: 82px;
674
+ resize: vertical;
675
+ line-height: 1.4;
676
+ }
677
+
678
+ .runtime-start-form,
679
+ .runtime-toolbar {
680
+ display: grid;
681
+ gap: 8px;
682
+ padding-bottom: 12px;
683
+ margin-bottom: 12px;
684
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
685
+ }
686
+
687
+ .runtime-check {
688
+ display: flex;
689
+ align-items: center;
690
+ gap: 8px;
691
+ font-size: 12px;
692
+ color: #cbd5e1;
693
+ }
694
+
695
+ .runtime-check input {
696
+ width: 16px;
697
+ height: 16px;
698
+ }
699
+
700
+ .runtime-summary {
701
+ margin: 0 0 12px;
702
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
703
+ }
704
+
705
+ .runtime-summary div {
706
+ display: grid;
707
+ grid-template-columns: 72px minmax(0, 1fr);
708
+ gap: 8px;
709
+ padding: 7px 0;
710
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
711
+ font-size: 12px;
712
+ }
713
+
714
+ .runtime-summary dt {
715
+ color: #94a3b8;
716
+ }
717
+
718
+ .runtime-summary dd {
719
+ margin: 0;
720
+ color: #f8fafc;
721
+ overflow-wrap: anywhere;
722
+ }
723
+
724
+ .runtime-approval {
725
+ padding: 10px;
726
+ margin-bottom: 10px;
727
+ border: 1px solid rgba(251, 191, 36, 0.32);
728
+ border-radius: 6px;
729
+ background: rgba(251, 191, 36, 0.08);
730
+ color: #f8fafc;
731
+ }
732
+
733
+ .runtime-approval p {
734
+ margin: 6px 0 10px;
735
+ color: #cbd5e1;
736
+ font-size: 12px;
737
+ line-height: 1.4;
738
+ white-space: pre-wrap;
739
+ overflow-wrap: anywhere;
740
+ }
741
+
742
+ .runtime-actions {
743
+ display: grid;
744
+ grid-template-columns: 1fr 1fr;
745
+ gap: 8px;
746
+ }
747
+
748
+ .runtime-cancel {
749
+ width: 100%;
750
+ margin-bottom: 12px;
751
+ }
752
+
603
753
  .transcript-panel h2 {
604
754
  margin: 0 0 8px;
605
755
  font-size: 14px;
606
756
  color: #f8fafc;
607
757
  }
608
758
 
609
- #transcript-list {
759
+ #transcript-list,
760
+ #runtime-event-list {
610
761
  list-style: none;
611
762
  margin: 0;
612
763
  padding: 0;
@@ -615,7 +766,8 @@ code {
615
766
  font-size: 12px;
616
767
  }
617
768
 
618
- #transcript-list li {
769
+ #transcript-list li,
770
+ #runtime-event-list li {
619
771
  padding: 8px;
620
772
  border-radius: 8px;
621
773
  background: rgba(255, 255, 255, 0.04);
@@ -662,11 +814,20 @@ code {
662
814
  @media (max-width: 900px) {
663
815
  .office-main.studio-layout {
664
816
  grid-template-columns: 1fr;
817
+ grid-template-areas:
818
+ "activity"
819
+ "office";
820
+ }
821
+ .office-main.studio-layout > .canvas-wrap {
822
+ grid-area: office;
665
823
  }
666
824
  .transcript-panel {
667
- max-height: 200px;
825
+ grid-area: activity;
826
+ height: min(640px, 72vh);
827
+ max-height: none;
668
828
  border-left: 0;
669
- border-top: 1px solid rgba(255, 255, 255, 0.08);
829
+ border-top: 0;
830
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
670
831
  }
671
832
  }
672
833
 
@@ -702,8 +863,8 @@ code {
702
863
  padding: 6px 10px;
703
864
  background: #fff;
704
865
  color: var(--ink);
705
- border: 2px solid var(--accent);
706
- border-radius: 10px;
866
+ border: 1px solid #cbd5e1;
867
+ border-radius: 8px;
707
868
  font-size: 11px;
708
869
  line-height: 1.3;
709
870
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
@@ -35,6 +35,10 @@
35
35
  handoffPulse: null,
36
36
  studioSessionId: boot.activeSessionId || "",
37
37
  studioEvents: [],
38
+ studioEventSource: null,
39
+ runtimeRuns: [],
40
+ runtimeRunId: "",
41
+ runtimeEvents: [],
38
42
  speechBubbles: [],
39
43
  agenticLevel: null
40
44
  };
@@ -71,7 +75,30 @@
71
75
  bubbleLayer: document.getElementById("bubble-layer"),
72
76
  officeHint: document.getElementById("office-hint"),
73
77
  canvasWrap: document.querySelector(".canvas-wrap"),
74
- transcriptList: document.getElementById("transcript-list")
78
+ transcriptList: document.getElementById("transcript-list"),
79
+ sessionPicker: document.getElementById("session-picker"),
80
+ studioNoteForm: document.getElementById("studio-note-form"),
81
+ studioNoteAgent: document.getElementById("studio-note-agent"),
82
+ studioNoteText: document.getElementById("studio-note-text"),
83
+ studioRenderBtn: document.getElementById("studio-render-btn"),
84
+ studioCouncilTab: document.getElementById("studio-council-tab"),
85
+ studioRuntimeTab: document.getElementById("studio-runtime-tab"),
86
+ studioCouncilView: document.getElementById("studio-council-view"),
87
+ studioRuntimeView: document.getElementById("studio-runtime-view"),
88
+ runtimeStartForm: document.getElementById("runtime-start-form"),
89
+ runtimeStartBtn: document.getElementById("runtime-start-btn"),
90
+ runtimeGoal: document.getElementById("runtime-goal"),
91
+ runtimeDirtyBase: document.getElementById("runtime-dirty-base"),
92
+ runtimePicker: document.getElementById("runtime-picker"),
93
+ runtimeRefreshBtn: document.getElementById("runtime-refresh-btn"),
94
+ runtimeSummary: document.getElementById("runtime-summary"),
95
+ runtimeApproval: document.getElementById("runtime-approval"),
96
+ runtimeApprovalTitle: document.getElementById("runtime-approval-title"),
97
+ runtimeApprovalDetail: document.getElementById("runtime-approval-detail"),
98
+ runtimeApproveBtn: document.getElementById("runtime-approve-btn"),
99
+ runtimeRejectBtn: document.getElementById("runtime-reject-btn"),
100
+ runtimeCancelBtn: document.getElementById("runtime-cancel-btn"),
101
+ runtimeEventList: document.getElementById("runtime-event-list")
75
102
  };
76
103
 
77
104
  const ctx = els.canvas?.getContext("2d");
@@ -169,7 +196,13 @@
169
196
  }
170
197
 
171
198
  async function api(path, options) {
172
- const response = await fetch(path, options);
199
+ const requestOptions = options ? { ...options } : {};
200
+ const method = String(requestOptions.method || "GET").toUpperCase();
201
+ if (!["GET", "HEAD", "OPTIONS"].includes(method)) {
202
+ const token = document.querySelector('meta[name="agent-kit-csrf-token"]')?.getAttribute("content") || "";
203
+ requestOptions.headers = { ...(requestOptions.headers || {}), "Content-Type": "application/json", "X-Agent-Kit-CSRF": token };
204
+ }
205
+ const response = await fetch(path, requestOptions);
173
206
  const data = await response.json();
174
207
  if (!response.ok) throw new Error(data.error || "Request failed");
175
208
  return data;
@@ -210,22 +243,281 @@
210
243
 
211
244
  async function loadStudioState() {
212
245
  const sessionsRes = await api("/api/sessions");
213
- state.studioSessionId = sessionsRes.activeSessionId || boot.activeSessionId || "";
246
+ const sessions = sessionsRes.sessions || [];
247
+ state.studioSessionId = sessionsRes.activeSessionId || boot.activeSessionId || sessions[0]?.sessionId || "";
248
+ populateSessionPicker(sessions, state.studioSessionId);
249
+ populateStudioAgents();
214
250
  if (els.sessionPill) {
215
251
  els.sessionPill.textContent = state.studioSessionId ? state.studioSessionId.slice(0, 24) : "No session";
216
252
  }
217
- if (els.projectName) els.projectName.textContent = sessionsRes.sessions?.[0]?.title || "Council session";
253
+ const activeSession = sessions.find((s) => s.sessionId === state.studioSessionId);
254
+ if (els.projectName) els.projectName.textContent = activeSession?.title || "Council session";
218
255
  if (els.officeHint) {
219
256
  els.officeHint.classList.remove("hidden");
220
257
  window.setTimeout(() => els.officeHint?.classList.add("hidden"), 6000);
221
258
  }
222
259
  connectStudioStream();
260
+ await loadRuntimeRuns().catch((error) => renderRuntimeUnavailable(error.message));
261
+ }
262
+
263
+ function populateSessionPicker(sessions, selectedId) {
264
+ if (!els.sessionPicker) return;
265
+ els.sessionPicker.innerHTML = sessions.length
266
+ ? sessions
267
+ .map(
268
+ (s) =>
269
+ '<option value="' +
270
+ escapeHtml(s.sessionId) +
271
+ '"' +
272
+ (s.sessionId === selectedId ? " selected" : "") +
273
+ ">" +
274
+ escapeHtml((s.title || s.sessionId).slice(0, 48)) +
275
+ "</option>"
276
+ )
277
+ .join("")
278
+ : '<option value="">No sessions</option>';
279
+ }
280
+
281
+ function populateStudioAgents() {
282
+ if (!els.studioNoteAgent) return;
283
+ els.studioNoteAgent.innerHTML = state.agents.length
284
+ ? state.agents.map((a) => '<option value="' + escapeHtml(a.id) + '">' + escapeHtml(a.name || a.id) + "</option>").join("")
285
+ : '<option value="planner">Planner</option>';
286
+ }
287
+
288
+ function switchStudioView(view) {
289
+ const runtime = view === "runtime";
290
+ if (els.studioCouncilView) els.studioCouncilView.hidden = runtime;
291
+ if (els.studioRuntimeView) els.studioRuntimeView.hidden = !runtime;
292
+ els.studioCouncilTab?.classList.toggle("active", !runtime);
293
+ els.studioRuntimeTab?.classList.toggle("active", runtime);
294
+ els.studioCouncilTab?.setAttribute("aria-selected", String(!runtime));
295
+ els.studioRuntimeTab?.setAttribute("aria-selected", String(runtime));
296
+ if (runtime) void loadRuntimeRuns().catch((error) => renderRuntimeUnavailable(error.message));
297
+ }
298
+
299
+ function populateRuntimePicker() {
300
+ if (!els.runtimePicker) return;
301
+ els.runtimePicker.innerHTML = state.runtimeRuns.length
302
+ ? state.runtimeRuns
303
+ .map(
304
+ (run) =>
305
+ '<option value="' +
306
+ escapeHtml(run.runId) +
307
+ '"' +
308
+ (run.runId === state.runtimeRunId ? " selected" : "") +
309
+ ">" +
310
+ escapeHtml(run.runId.slice(0, 30) + " · " + run.status) +
311
+ "</option>"
312
+ )
313
+ .join("")
314
+ : '<option value="">No runs</option>';
315
+ }
316
+
317
+ async function loadRuntimeRuns() {
318
+ const response = await api("/api/runtime/runs");
319
+ const runtimeEnabled = response.enabled === true;
320
+ if (els.runtimeStartBtn) els.runtimeStartBtn.disabled = !runtimeEnabled;
321
+ state.runtimeRuns = Array.isArray(response.runs) ? response.runs : [];
322
+ if (!state.runtimeRunId || !state.runtimeRuns.some((run) => run.runId === state.runtimeRunId)) {
323
+ state.runtimeRunId = state.runtimeRuns[0]?.runId || "";
324
+ }
325
+ populateRuntimePicker();
326
+ if (state.runtimeRunId) await loadRuntimeRun(state.runtimeRunId);
327
+ else if (runtimeEnabled) renderRuntimeRun(null, []);
328
+ else renderRuntimeUnavailable("Runtime is disabled in .agent-kit/orchestrator.json.");
329
+ }
330
+
331
+ async function loadRuntimeRun(runId) {
332
+ if (!runId) {
333
+ renderRuntimeRun(null, []);
334
+ return;
335
+ }
336
+ const response = await api("/api/runtime/runs/" + encodeURIComponent(runId));
337
+ state.runtimeRunId = runId;
338
+ state.runtimeEvents = response.events || [];
339
+ const index = state.runtimeRuns.findIndex((run) => run.runId === runId);
340
+ if (index >= 0) state.runtimeRuns[index] = response.run;
341
+ else state.runtimeRuns.unshift(response.run);
342
+ populateRuntimePicker();
343
+ renderRuntimeRun(response.run, state.runtimeEvents);
344
+ }
345
+
346
+ function renderRuntimeUnavailable(message) {
347
+ if (els.runtimeStartBtn) els.runtimeStartBtn.disabled = true;
348
+ if (els.runtimeSummary) els.runtimeSummary.innerHTML = "<div><dt>Status</dt><dd>" + escapeHtml(message) + "</dd></div>";
349
+ if (els.runtimeEventList) els.runtimeEventList.innerHTML = "";
350
+ if (els.runtimeApproval) els.runtimeApproval.hidden = true;
351
+ if (els.runtimeCancelBtn) els.runtimeCancelBtn.hidden = true;
352
+ }
353
+
354
+ function renderRuntimeRun(run, events) {
355
+ if (els.runtimeSummary) {
356
+ const items = run
357
+ ? [
358
+ ["Status", run.status],
359
+ ["Workflow", run.workflowId],
360
+ ["Agent", run.activeAgentId || "—"],
361
+ ["Commit", run.commit ? run.commit.slice(0, 12) : "—"]
362
+ ]
363
+ : [["Status", "No runs"]];
364
+ els.runtimeSummary.innerHTML = items.map(([label, value]) => "<div><dt>" + escapeHtml(label) + "</dt><dd>" + escapeHtml(value) + "</dd></div>").join("");
365
+ }
366
+ const pending = run?.pendingApproval;
367
+ if (els.runtimeApproval) els.runtimeApproval.hidden = !pending;
368
+ if (els.runtimeApprovalTitle) els.runtimeApprovalTitle.textContent = pending?.title || "";
369
+ if (els.runtimeApprovalDetail) els.runtimeApprovalDetail.textContent = pending?.detail || "";
370
+ const terminal = !run || ["complete", "failed", "cancelled"].includes(run.status);
371
+ if (els.runtimeCancelBtn) els.runtimeCancelBtn.hidden = terminal;
372
+ if (els.runtimeEventList) {
373
+ els.runtimeEventList.innerHTML = (events || [])
374
+ .slice(-100)
375
+ .map(
376
+ (event) =>
377
+ '<li><span class="tx-time">' +
378
+ escapeHtml((event.createdAt || "").slice(11, 19)) +
379
+ "</span> <strong>" +
380
+ escapeHtml(event.agentId || "run") +
381
+ "</strong> " +
382
+ escapeHtml(eventLabel(event)) +
383
+ "</li>"
384
+ )
385
+ .join("");
386
+ els.runtimeEventList.scrollTop = els.runtimeEventList.scrollHeight;
387
+ }
388
+ if (run && els.sessionPill && !els.studioRuntimeView?.hidden) els.sessionPill.textContent = run.runId.slice(0, 24);
389
+ }
390
+
391
+ async function decideRuntime(decision) {
392
+ if (!state.runtimeRunId) return;
393
+ const response = await api("/api/runtime/runs/" + encodeURIComponent(state.runtimeRunId) + "/decision", {
394
+ method: "POST",
395
+ body: JSON.stringify({ decision, actor: "studio-operator" })
396
+ });
397
+ state.runtimeEvents = response.events || [];
398
+ renderRuntimeRun(response.run, state.runtimeEvents);
399
+ await loadRuntimeRuns();
400
+ }
401
+
402
+ function bindStudioControls() {
403
+ els.studioCouncilTab?.addEventListener("click", () => switchStudioView("council"));
404
+ els.studioRuntimeTab?.addEventListener("click", () => switchStudioView("runtime"));
405
+ if (els.sessionPicker) {
406
+ els.sessionPicker.addEventListener("change", () => {
407
+ state.studioSessionId = els.sessionPicker.value;
408
+ if (els.sessionPill) {
409
+ els.sessionPill.textContent = state.studioSessionId ? state.studioSessionId.slice(0, 24) : "No session";
410
+ }
411
+ connectStudioStream();
412
+ });
413
+ }
414
+ if (els.studioNoteForm) {
415
+ els.studioNoteForm.addEventListener("submit", async (event) => {
416
+ event.preventDefault();
417
+ if (!state.studioSessionId) {
418
+ setStatus("error", "Select a session first.");
419
+ return;
420
+ }
421
+ const agent = els.studioNoteAgent?.value || "planner";
422
+ const text = (els.studioNoteText?.value || "").trim();
423
+ if (!text) {
424
+ setStatus("error", "Enter note text.");
425
+ return;
426
+ }
427
+ try {
428
+ await api("/api/sessions/" + encodeURIComponent(state.studioSessionId) + "/note", {
429
+ method: "POST",
430
+ headers: { "Content-Type": "application/json" },
431
+ body: JSON.stringify({ agent, text })
432
+ });
433
+ if (els.studioNoteText) els.studioNoteText.value = "";
434
+ setStatus("ok", "Note recorded.");
435
+ } catch (error) {
436
+ setStatus("error", error.message);
437
+ }
438
+ });
439
+ }
440
+ if (els.studioRenderBtn) {
441
+ els.studioRenderBtn.addEventListener("click", async () => {
442
+ if (!state.studioSessionId) {
443
+ setStatus("error", "Select a session first.");
444
+ return;
445
+ }
446
+ try {
447
+ const result = await api("/api/sessions/" + encodeURIComponent(state.studioSessionId) + "/render", {
448
+ method: "POST"
449
+ });
450
+ setStatus("ok", "Rendered " + (result.files?.length || 0) + " markdown files.");
451
+ } catch (error) {
452
+ setStatus("error", error.message);
453
+ }
454
+ });
455
+ }
456
+ if (els.runtimePicker) {
457
+ els.runtimePicker.addEventListener("change", () => {
458
+ state.runtimeRunId = els.runtimePicker.value;
459
+ void loadRuntimeRun(state.runtimeRunId).catch((error) => setStatus("error", error.message));
460
+ });
461
+ }
462
+ els.runtimeRefreshBtn?.addEventListener("click", () => {
463
+ void loadRuntimeRuns().catch((error) => setStatus("error", error.message));
464
+ });
465
+ if (els.runtimeStartForm) {
466
+ els.runtimeStartForm.addEventListener("submit", async (event) => {
467
+ event.preventDefault();
468
+ const goal = (els.runtimeGoal?.value || "").trim();
469
+ if (!goal) {
470
+ setStatus("error", "Enter a run goal.");
471
+ return;
472
+ }
473
+ try {
474
+ const response = await api("/api/runtime/runs", {
475
+ method: "POST",
476
+ body: JSON.stringify({ goal, acknowledgeDirtyBase: Boolean(els.runtimeDirtyBase?.checked) })
477
+ });
478
+ state.runtimeRunId = response.run.runId;
479
+ state.runtimeEvents = response.events || [];
480
+ if (els.runtimeGoal) els.runtimeGoal.value = "";
481
+ await loadRuntimeRuns();
482
+ setStatus("ok", "Run started.");
483
+ } catch (error) {
484
+ setStatus("error", error.message);
485
+ }
486
+ });
487
+ }
488
+ els.runtimeApproveBtn?.addEventListener("click", () => {
489
+ void decideRuntime("approve").catch((error) => setStatus("error", error.message));
490
+ });
491
+ els.runtimeRejectBtn?.addEventListener("click", () => {
492
+ void decideRuntime("reject").catch((error) => setStatus("error", error.message));
493
+ });
494
+ els.runtimeCancelBtn?.addEventListener("click", async () => {
495
+ if (!state.runtimeRunId) return;
496
+ try {
497
+ const response = await api("/api/runtime/runs/" + encodeURIComponent(state.runtimeRunId) + "/cancel", { method: "POST" });
498
+ state.runtimeEvents = response.events || [];
499
+ renderRuntimeRun(response.run, state.runtimeEvents);
500
+ await loadRuntimeRuns();
501
+ } catch (error) {
502
+ setStatus("error", error.message);
503
+ }
504
+ });
505
+ window.setInterval(() => {
506
+ if (els.studioRuntimeView && !els.studioRuntimeView.hidden) {
507
+ void loadRuntimeRuns().catch(() => undefined);
508
+ }
509
+ }, 2500);
223
510
  }
224
511
 
225
512
  function connectStudioStream() {
226
513
  if (!state.studioSessionId) return;
514
+ if (state.studioEventSource) {
515
+ state.studioEventSource.close();
516
+ state.studioEventSource = null;
517
+ }
227
518
  const url = "/api/events/stream?sessionId=" + encodeURIComponent(state.studioSessionId);
228
519
  const source = new EventSource(url);
520
+ state.studioEventSource = source;
229
521
  source.addEventListener("snapshot", (ev) => {
230
522
  try {
231
523
  const payload = JSON.parse(ev.data);
@@ -249,6 +541,7 @@
249
541
  });
250
542
  source.onerror = () => {
251
543
  source.close();
544
+ if (state.studioEventSource === source) state.studioEventSource = null;
252
545
  };
253
546
  }
254
547
 
@@ -1119,6 +1412,7 @@
1119
1412
  }
1120
1413
 
1121
1414
  loop();
1415
+ if (isStudio) bindStudioControls();
1122
1416
  loadState().catch((error) => setStatus("error", error.message));
1123
1417
  window.addEventListener("resize", () => {
1124
1418
  renderNameplates();
@@ -59,7 +59,13 @@
59
59
  }
60
60
 
61
61
  async function api(path, options) {
62
- const response = await fetch(path, options);
62
+ const requestOptions = options ? { ...options } : {};
63
+ const method = String(requestOptions.method || "GET").toUpperCase();
64
+ if (!["GET", "HEAD", "OPTIONS"].includes(method)) {
65
+ const token = document.querySelector('meta[name="agent-kit-csrf-token"]')?.getAttribute("content") || "";
66
+ requestOptions.headers = { ...(requestOptions.headers || {}), "Content-Type": "application/json", "X-Agent-Kit-CSRF": token };
67
+ }
68
+ const response = await fetch(path, requestOptions);
63
69
  const data = await response.json();
64
70
  if (!response.ok) throw new Error(data.error || "Request failed");
65
71
  return data;