@grifhinz/logics-manager 2.6.0 → 2.8.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.
@@ -13,6 +13,7 @@
13
13
  const connectionDetail = () => document.getElementById("viewer-connection-detail");
14
14
  const filterCount = () => document.getElementById("viewer-filter-count");
15
15
  const repoPill = () => document.getElementById("viewer-repo-pill");
16
+ const projectMenu = () => document.getElementById("viewer-project-menu");
16
17
  const repoGithubLink = () => document.getElementById("viewer-repo-github");
17
18
  const repoFolderButton = () => document.getElementById("viewer-repo-folder");
18
19
  const ciButton = () => document.getElementById("viewer-ci");
@@ -20,6 +21,7 @@
20
21
  const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
21
22
  const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
22
23
  const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
24
+ const versionLink = () => document.getElementById("viewer-version-link");
23
25
  const activityClearControl = () => document.getElementById("activity-clear");
24
26
  const activityStorageLimit = 80;
25
27
  const gitHistoryPageSize = 10;
@@ -37,6 +39,8 @@
37
39
  let latestItems = [];
38
40
  let latestRepoRoot = "";
39
41
  let latestRepository = { root: "", githubUrl: "" };
42
+ let latestCapabilities = {};
43
+ let latestProjects = [];
40
44
  let latestMetaText = "Read-only local viewer";
41
45
  let autoRefreshIntervalMs = defaultAutoRefreshIntervalMs;
42
46
  let nextAutoRefreshAt = 0;
@@ -51,8 +55,27 @@
51
55
  let focusApplied = false;
52
56
  let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
53
57
  let latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
58
+ let latestUpdateInfo = {};
59
+ let latestCdxMissionState = {
60
+ missionId: "full-audit",
61
+ sessionId: "",
62
+ strengthId: "standard",
63
+ missionInputs: {},
64
+ catalog: null,
65
+ statusPayload: null,
66
+ planPayload: null,
67
+ runPayload: null,
68
+ applyPayload: null
69
+ };
54
70
  let connectionState = "connected";
55
71
  let lastSuccessfulSyncAt = 0;
72
+ let latestViewerStateSignature = "";
73
+ let latestGitStatusSignature = "";
74
+ let latestCdxStatusSignature = "";
75
+ let latestCiStatusSignature = "";
76
+ let primaryActionBusyKey = "";
77
+ let cdxMissionBusyKey = "";
78
+ let cdxCloseTarget = null;
56
79
 
57
80
  function readStoredState() {
58
81
  try {
@@ -75,6 +98,181 @@
75
98
  return nextState;
76
99
  }
77
100
 
101
+ function stableStringify(value) {
102
+ if (Array.isArray(value)) {
103
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
104
+ }
105
+ if (value && typeof value === "object") {
106
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
107
+ }
108
+ return JSON.stringify(value);
109
+ }
110
+
111
+ function viewerStateSignature(payload) {
112
+ const items = Array.isArray(payload?.items) ? payload.items : [];
113
+ const projects = Array.isArray(payload?.projects) ? payload.projects : [];
114
+ return stableStringify({
115
+ root: payload?.root || "",
116
+ repository: payload?.repository || {},
117
+ capabilities: normalizeCapabilities(payload),
118
+ projects: projects.map((project) => ({
119
+ id: project?.id || "",
120
+ active: Boolean(project?.active),
121
+ available: project?.available !== false,
122
+ hasLogics: project?.hasLogics !== false,
123
+ root: project?.root || ""
124
+ })),
125
+ items: items.map((item) => ({
126
+ id: item?.id || "",
127
+ relPath: item?.relPath || "",
128
+ stage: item?.stage || "",
129
+ status: item?.indicators?.Status || item?.status || "",
130
+ updatedAt: item?.updatedAt || ""
131
+ }))
132
+ });
133
+ }
134
+
135
+ function gitStatusSignature(payload) {
136
+ return stableStringify({
137
+ state: payload?.state || "",
138
+ branch: payload?.branch || "",
139
+ tracking: payload?.tracking || "",
140
+ ahead: Number(payload?.ahead || 0),
141
+ behind: Number(payload?.behind || 0),
142
+ clean: Boolean(payload?.clean),
143
+ counts: payload?.counts || {},
144
+ badgeCounts: payload?.badgeCounts || {},
145
+ latestCommit: payload?.latestCommit || "",
146
+ recentCommitsHasMore: Boolean(payload?.recentCommitsHasMore)
147
+ });
148
+ }
149
+
150
+ function runtimeStatusSignature(payload) {
151
+ return stableStringify(payload || {});
152
+ }
153
+
154
+ function primaryActionControls() {
155
+ return Array.from(document.querySelectorAll([
156
+ "#viewer-insights",
157
+ "#viewer-health",
158
+ "#viewer-git",
159
+ "#viewer-ci",
160
+ "#viewer-cdx",
161
+ "#viewer-repo-folder",
162
+ '[data-action="refresh"]',
163
+ '[data-viewer-action="edit-document"]',
164
+ "[data-viewer-project-id]",
165
+ "[data-viewer-cdx-mode]",
166
+ "[data-viewer-cdx-report]",
167
+ "[data-viewer-cdx-artifact-path]",
168
+ "[data-viewer-cdx-create-request]"
169
+ ].join(","))).filter((node) => node instanceof HTMLElement);
170
+ }
171
+
172
+ function setPrimaryActionBusy(actionKey, label = "") {
173
+ primaryActionBusyKey = actionKey || "";
174
+ document.body?.classList.toggle("viewer-is-busy", Boolean(primaryActionBusyKey));
175
+ document.body?.toggleAttribute("data-viewer-busy", Boolean(primaryActionBusyKey));
176
+ if (primaryActionBusyKey) {
177
+ document.body?.setAttribute("data-viewer-busy-action", primaryActionBusyKey);
178
+ } else {
179
+ document.body?.removeAttribute("data-viewer-busy-action");
180
+ }
181
+ primaryActionControls().forEach((control) => {
182
+ if (!("disabled" in control)) {
183
+ return;
184
+ }
185
+ control.disabled = Boolean(primaryActionBusyKey);
186
+ control.setAttribute("aria-busy", primaryActionBusyKey ? "true" : "false");
187
+ if (primaryActionBusyKey) {
188
+ control.setAttribute("data-viewer-action-busy", control.getAttribute("data-viewer-action-key") === actionKey ? "active" : "blocked");
189
+ } else {
190
+ control.removeAttribute("data-viewer-action-busy");
191
+ }
192
+ });
193
+ if (!primaryActionBusyKey) {
194
+ updateCapabilityControls();
195
+ applyLocalViewerChrome();
196
+ }
197
+ if (primaryActionBusyKey && label) {
198
+ setMeta(`${label}...`);
199
+ }
200
+ }
201
+
202
+ function withPrimaryAction(actionKey, label, action) {
203
+ if (primaryActionBusyKey) {
204
+ setMeta("Another viewer action is still running.");
205
+ return Promise.resolve(false);
206
+ }
207
+ setPrimaryActionBusy(actionKey, label);
208
+ return Promise.resolve()
209
+ .then(action)
210
+ .then(() => true)
211
+ .catch((error) => {
212
+ setMeta(error.message || "Viewer action failed.");
213
+ return false;
214
+ })
215
+ .finally(() => {
216
+ setPrimaryActionBusy("", "");
217
+ });
218
+ }
219
+
220
+ function cdxMissionActionControls() {
221
+ return Array.from(document.querySelectorAll([
222
+ "[data-viewer-cdx-plan]",
223
+ "[data-viewer-cdx-run]",
224
+ "[data-viewer-cdx-apply-plan]",
225
+ "[data-viewer-cdx-mission]"
226
+ ].join(","))).filter((node) => node instanceof HTMLElement);
227
+ }
228
+
229
+ function setCdxMissionBusy(actionKey, label = "") {
230
+ cdxMissionBusyKey = actionKey || "";
231
+ document.body?.toggleAttribute("data-viewer-cdx-mission-busy", Boolean(cdxMissionBusyKey));
232
+ if (cdxMissionBusyKey) {
233
+ document.body?.setAttribute("data-viewer-cdx-mission-busy-action", cdxMissionBusyKey);
234
+ } else {
235
+ document.body?.removeAttribute("data-viewer-cdx-mission-busy-action");
236
+ }
237
+ cdxMissionActionControls().forEach((control) => {
238
+ if (!("disabled" in control)) {
239
+ return;
240
+ }
241
+ control.disabled = Boolean(cdxMissionBusyKey);
242
+ control.setAttribute("aria-busy", cdxMissionBusyKey ? "true" : "false");
243
+ if (cdxMissionBusyKey) {
244
+ control.setAttribute("data-viewer-action-busy", control.getAttribute("data-viewer-action-key") === actionKey ? "active" : "blocked");
245
+ } else {
246
+ control.removeAttribute("data-viewer-action-busy");
247
+ }
248
+ });
249
+ if (!cdxMissionBusyKey) {
250
+ updateCapabilityControls();
251
+ applyLocalViewerChrome();
252
+ }
253
+ if (cdxMissionBusyKey && label) {
254
+ setMeta(`${label}...`);
255
+ }
256
+ }
257
+
258
+ function withCdxMissionAction(actionKey, label, action) {
259
+ if (cdxMissionBusyKey) {
260
+ setMeta("Another CDX mission action is still running.");
261
+ return Promise.resolve(false);
262
+ }
263
+ setCdxMissionBusy(actionKey, label);
264
+ return Promise.resolve()
265
+ .then(action)
266
+ .then(() => true)
267
+ .catch((error) => {
268
+ setMeta(error.message || "CDX mission action failed.");
269
+ return false;
270
+ })
271
+ .finally(() => {
272
+ setCdxMissionBusy("", "");
273
+ });
274
+ }
275
+
78
276
  function hydrateViewerFilterState() {
79
277
  const storedState = readStoredState();
80
278
  viewerFilterState = sanitizeViewerFilterState(storedState?.viewerFilterState);
@@ -273,6 +471,7 @@
273
471
 
274
472
  function updateRepositoryIdentity(payload) {
275
473
  latestRepoRoot = String(payload.root || latestRepoRoot || "");
474
+ latestProjects = Array.isArray(payload.projects) ? payload.projects : latestProjects;
276
475
  const repository = payload.repository && typeof payload.repository === "object" ? payload.repository : {};
277
476
  latestRepository = {
278
477
  root: String(repository.root || latestRepoRoot || ""),
@@ -281,10 +480,178 @@
281
480
  const pill = repoPill();
282
481
  if (pill) {
283
482
  const repoName = String(payload.repoName || latestRepoRoot.split(/[\\/]/).filter(Boolean).pop() || "repository");
284
- pill.textContent = repoName;
483
+ const label = pill.querySelector("[data-viewer-project-label]");
484
+ if (label) {
485
+ label.textContent = repoName;
486
+ } else {
487
+ pill.textContent = repoName;
488
+ }
285
489
  pill.title = latestRepoRoot || repoName;
490
+ if ("disabled" in pill) {
491
+ pill.disabled = latestProjects.length <= 1;
492
+ }
493
+ pill.onclick = () => {
494
+ const menu = projectMenu();
495
+ setProjectMenuOpen(Boolean(menu?.hidden));
496
+ };
286
497
  }
287
498
  updateRepositoryShortcuts();
499
+ renderProjectMenu();
500
+ }
501
+
502
+ function projectStateLabel(project) {
503
+ if (project?.active) {
504
+ return "current";
505
+ }
506
+ if (project?.available === false) {
507
+ return "missing";
508
+ }
509
+ if (project?.hasLogics === false) {
510
+ return "no Logics";
511
+ }
512
+ return "available";
513
+ }
514
+
515
+ function renderProjectMenu() {
516
+ const menu = projectMenu();
517
+ if (!(menu instanceof HTMLElement)) {
518
+ return;
519
+ }
520
+ const projects = latestProjects.filter((project) => project && typeof project === "object");
521
+ menu.innerHTML = projects.map((project) => `
522
+ <button class="viewer-project-switcher__item${project.active ? " is-active" : ""}" type="button" role="menuitem" data-viewer-project-id="${escapeHtml(project.id || "")}" title="${escapeHtml(project.root || project.name || "")}">
523
+ <span class="viewer-project-switcher__item-name">${escapeHtml(project.name || "project")}</span>
524
+ <span class="viewer-project-switcher__item-state">${escapeHtml(projectStateLabel(project))}</span>
525
+ <span class="viewer-project-switcher__item-path">${escapeHtml(project.root || "")}</span>
526
+ </button>
527
+ `).join("");
528
+ }
529
+
530
+ function setProjectMenuOpen(open) {
531
+ const button = repoPill();
532
+ const menu = projectMenu();
533
+ if (!(button instanceof HTMLElement) || !(menu instanceof HTMLElement)) {
534
+ return;
535
+ }
536
+ const nextOpen = Boolean(open) && latestProjects.length > 1;
537
+ menu.hidden = !nextOpen;
538
+ button.setAttribute("aria-expanded", nextOpen ? "true" : "false");
539
+ }
540
+
541
+ async function switchViewerProject(projectId) {
542
+ if (!projectId) {
543
+ return;
544
+ }
545
+ const target = latestProjects.find((project) => project.id === projectId);
546
+ if (!target || target.active) {
547
+ setProjectMenuOpen(false);
548
+ return;
549
+ }
550
+ setProjectMenuOpen(false);
551
+ setMeta(`Switching to ${target.name || "project"}...`);
552
+ const response = await fetch("/api/switch-project", {
553
+ method: "POST",
554
+ headers: { "Content-Type": "application/json" },
555
+ body: JSON.stringify({ projectId })
556
+ });
557
+ const data = await response.json();
558
+ if (!response.ok || !data.ok) {
559
+ throw new Error(data.error || "Unable to switch project.");
560
+ }
561
+ latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
562
+ latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
563
+ updateMainGitBadges();
564
+ updateMainCiBadge(latestCiStatus);
565
+ updateMainCdxBadge(null);
566
+ const panel = documentPanel();
567
+ if (panel) {
568
+ panel.hidden = true;
569
+ }
570
+ postToApp(data.payload);
571
+ }
572
+
573
+ async function bootstrapLogicsProject() {
574
+ setMeta("Bootstrapping Logics...");
575
+ const response = await fetch("/api/bootstrap-logics", { method: "POST" });
576
+ const data = await response.json();
577
+ if (!response.ok || !data.ok) {
578
+ throw new Error(data.error || "Unable to bootstrap Logics.");
579
+ }
580
+ postToApp(data.payload);
581
+ const created = Array.isArray(data.bootstrap?.created_paths) ? data.bootstrap.created_paths.length : 0;
582
+ setMeta(created > 0 ? `Logics bootstrapped · ${created} paths created.` : "Logics bootstrap checked.");
583
+ }
584
+
585
+ function normalizeCapabilities(payload) {
586
+ const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
587
+ return {
588
+ logics: capabilities.logics || { state: "ready", available: true, message: "" },
589
+ git: capabilities.git || { state: "ready", available: true, message: "" },
590
+ ci: capabilities.ci || { state: "ready", available: true, message: "" },
591
+ cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
592
+ cdxRuns: capabilities.cdxRuns || { state: "unsupported", available: false, message: "" }
593
+ };
594
+ }
595
+
596
+ function capability(name) {
597
+ return latestCapabilities?.[name] || { state: "unknown", available: false, message: "" };
598
+ }
599
+
600
+ function isCapabilityAvailable(name) {
601
+ return capability(name).available === true;
602
+ }
603
+
604
+ function capabilityMessage(name, fallback) {
605
+ return String(capability(name).message || fallback || "");
606
+ }
607
+
608
+ function setButtonUnavailable(button, message) {
609
+ if (!(button instanceof HTMLElement) || !("disabled" in button)) {
610
+ return;
611
+ }
612
+ button.disabled = true;
613
+ button.setAttribute("aria-disabled", "true");
614
+ button.title = message;
615
+ }
616
+
617
+ function setButtonAvailable(button, title) {
618
+ if (!(button instanceof HTMLElement) || !("disabled" in button)) {
619
+ return;
620
+ }
621
+ button.disabled = false;
622
+ button.removeAttribute("aria-disabled");
623
+ button.title = title;
624
+ }
625
+
626
+ function updateCapabilityControls() {
627
+ const gitButton = document.getElementById("viewer-git");
628
+ if (gitButton instanceof HTMLElement) {
629
+ gitButton.hidden = !isCapabilityAvailable("git");
630
+ if (isCapabilityAvailable("git")) {
631
+ setButtonAvailable(gitButton, "Show Git status");
632
+ } else {
633
+ setButtonUnavailable(gitButton, capabilityMessage("git", "Git is not available for this project."));
634
+ }
635
+ }
636
+
637
+ const ci = ciButton();
638
+ if (ci instanceof HTMLElement) {
639
+ ci.hidden = !isCapabilityAvailable("ci");
640
+ if (isCapabilityAvailable("ci")) {
641
+ setButtonAvailable(ci, "Show GitHub Actions CI status");
642
+ } else {
643
+ setButtonUnavailable(ci, capabilityMessage("ci", "CI is not available for this project."));
644
+ }
645
+ }
646
+
647
+ const cdx = document.getElementById("viewer-cdx");
648
+ if (cdx instanceof HTMLElement) {
649
+ if (isCapabilityAvailable("cdx")) {
650
+ setButtonAvailable(cdx, "Show CDX status");
651
+ } else {
652
+ setButtonUnavailable(cdx, capabilityMessage("cdx", "CDX is not available for this project."));
653
+ }
654
+ }
288
655
  }
289
656
 
290
657
  function updateRepositoryShortcuts() {
@@ -304,6 +671,18 @@
304
671
  }
305
672
  }
306
673
 
674
+ function updateVersionLink(updateInfo = latestUpdateInfo) {
675
+ latestUpdateInfo = updateInfo && typeof updateInfo === "object" ? updateInfo : {};
676
+ const link = versionLink();
677
+ if (!(link instanceof HTMLAnchorElement)) {
678
+ return;
679
+ }
680
+ const currentVersion = String(latestUpdateInfo.currentVersion || "").trim();
681
+ link.textContent = currentVersion ? `v${currentVersion.replace(/^v/i, "")}` : "v0.0.0";
682
+ link.href = latestRepository.githubUrl || "https://github.com/AlexAgo83/logics-manager";
683
+ link.title = "Open Logics Manager on GitHub";
684
+ }
685
+
307
686
  async function openRepositoryFolder() {
308
687
  if (!latestRepository.root) {
309
688
  setMeta("Repository folder is unavailable.");
@@ -430,6 +809,10 @@
430
809
  }
431
810
 
432
811
  async function refreshCiBadgeCounters() {
812
+ if (!isCapabilityAvailable("ci")) {
813
+ updateMainCiBadge({ visible: false, badgeState: "unknown", message: capabilityMessage("ci", "CI is not available for this project.") });
814
+ return;
815
+ }
433
816
  try {
434
817
  const response = await fetch("/api/ci-status");
435
818
  if (response.status === 404) {
@@ -438,6 +821,7 @@
438
821
  }
439
822
  const data = await response.json();
440
823
  if (response.ok && data.ok) {
824
+ latestCiStatusSignature = runtimeStatusSignature(data.payload);
441
825
  updateMainCiBadge(data.payload);
442
826
  }
443
827
  } catch {
@@ -468,33 +852,65 @@
468
852
  return cdxProviders(status).reduce((total, provider) => total + Math.max(0, Number(provider.active || 0)), 0);
469
853
  }
470
854
 
471
- function updateMainCdxBadge(payload) {
855
+ function activeCdxRunCountFromPayload(payload) {
856
+ if (!payload || payload.state !== "ok" || !Array.isArray(payload.runs)) {
857
+ return 0;
858
+ }
859
+ return payload.runs.filter((run) => ["running", "starting", "pending"].includes(String(cdxField(run, ["status", "state"], "")).toLowerCase())).length;
860
+ }
861
+
862
+ function updateMainCdxBadge(payload, runsPayload = null) {
472
863
  const button = document.getElementById("viewer-cdx");
473
864
  if (!(button instanceof HTMLElement)) {
474
865
  return;
475
866
  }
476
867
  button.querySelector("[data-viewer-cdx-badge]")?.remove();
477
- const activeCount = activeCdxAssistantCountFromPayload(payload);
868
+ const activeSessions = activeCdxAssistantCountFromPayload(payload);
869
+ const activeRuns = activeCdxRunCountFromPayload(runsPayload);
870
+ const activeCount = activeSessions + activeRuns;
478
871
  if (activeCount <= 0) {
479
- button.title = "Show CDX status";
872
+ button.title = isCapabilityAvailable("cdx")
873
+ ? "Show CDX status"
874
+ : capabilityMessage("cdx", "CDX is not available for this project.");
480
875
  return;
481
876
  }
482
877
  const label = activeCount > 9 ? "9+" : String(activeCount);
483
- const title = activeCount === 1 ? "1 active assistant/session" : `${activeCount} active assistants/sessions`;
878
+ const titleParts = [];
879
+ if (activeSessions > 0) {
880
+ titleParts.push(activeSessions === 1 ? "1 active session" : `${activeSessions} active sessions`);
881
+ }
882
+ if (activeRuns > 0) {
883
+ titleParts.push(activeRuns === 1 ? "1 running run" : `${activeRuns} running runs`);
884
+ }
885
+ const title = titleParts.join(" · ");
484
886
  button.title = `Show CDX status · ${title}`;
485
- button.insertAdjacentHTML("beforeend", `<span class="viewer-cdx-button-badge" data-viewer-cdx-badge title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">${escapeHtml(label)}</span>`);
887
+ const tone = activeRuns > 0 ? " viewer-cdx-button-badge--runs" : "";
888
+ button.insertAdjacentHTML("beforeend", `<span class="viewer-cdx-button-badge${tone}" data-viewer-cdx-badge title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">${escapeHtml(label)}</span>`);
486
889
  }
487
890
 
488
891
  async function refreshCdxBadgeCounters() {
892
+ if (!isCapabilityAvailable("cdx")) {
893
+ updateMainCdxBadge(null);
894
+ return;
895
+ }
489
896
  try {
490
- const response = await fetch("/api/cdx-status");
491
- if (response.status === 404) {
897
+ const [statusResponse, runsResponse] = await Promise.all([
898
+ fetch("/api/cdx-status"),
899
+ fetch("/api/cdx-runs").catch(() => null)
900
+ ]);
901
+ if (statusResponse.status === 404) {
492
902
  updateMainCdxBadge(null);
493
903
  return;
494
904
  }
495
- const data = await response.json();
496
- if (response.ok && data.ok) {
497
- updateMainCdxBadge(data.payload);
905
+ const data = await statusResponse.json();
906
+ let runsPayload = null;
907
+ if (runsResponse && runsResponse.ok) {
908
+ const runsData = await runsResponse.json();
909
+ runsPayload = runsData?.ok ? runsData.payload : null;
910
+ }
911
+ if (statusResponse.ok && data.ok) {
912
+ latestCdxStatusSignature = runtimeStatusSignature({ status: data.payload, runs: runsPayload });
913
+ updateMainCdxBadge(data.payload, runsPayload);
498
914
  }
499
915
  } catch {
500
916
  updateMainCdxBadge(null);
@@ -509,10 +925,16 @@
509
925
  }
510
926
 
511
927
  async function refreshGitBadgeCounters() {
928
+ if (!isCapabilityAvailable("git")) {
929
+ latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
930
+ updateMainGitBadges();
931
+ return;
932
+ }
512
933
  try {
513
934
  const response = await fetch("/api/git-status");
514
935
  const data = await response.json();
515
936
  if (response.ok && data.ok && data.payload?.state === "ok") {
937
+ latestGitStatusSignature = gitStatusSignature(data.payload);
516
938
  setGitBadgeCountsFromPayload(data.payload);
517
939
  }
518
940
  } catch {
@@ -586,13 +1008,10 @@
586
1008
  }, 0);
587
1009
  }
588
1010
 
589
- function applyFocusRequest(payload) {
590
- if (focusApplied) {
591
- return payload;
592
- }
1011
+ function applyFocusRequest(payload, options = {}) {
593
1012
  const request = focusRequest();
594
1013
  if (!request.focus) {
595
- if (window.location.search.includes("focus=")) {
1014
+ if (!focusApplied && !options.silent && window.location.search.includes("focus=")) {
596
1015
  window.setTimeout(() => setMeta("Invalid focus target. Loaded corpus without changing selection."), 0);
597
1016
  }
598
1017
  focusApplied = true;
@@ -600,14 +1019,20 @@
600
1019
  }
601
1020
  const item = findFocusItem(request.focus);
602
1021
  if (!item) {
603
- window.setTimeout(() => setMeta(`Focus target not found: ${request.focus}`), 0);
1022
+ if (!focusApplied && !options.silent) {
1023
+ window.setTimeout(() => setMeta(`Focus target not found: ${request.focus}`), 0);
1024
+ }
604
1025
  focusApplied = true;
605
1026
  return payload;
606
1027
  }
1028
+ const nextPayload = { ...payload, selectedId: item.id };
1029
+ if (focusApplied) {
1030
+ persistSelectedItem(item.id);
1031
+ return nextPayload;
1032
+ }
607
1033
  viewerFilterState = { ...viewerFilterState, focus: "all", type: "all", status: "any", relation: "any", activity: "any" };
608
1034
  persistSelectedItem(item.id);
609
1035
  focusApplied = true;
610
- const nextPayload = { ...payload, selectedId: item.id };
611
1036
  window.setTimeout(() => {
612
1037
  revealFocusedCard(item);
613
1038
  if (request.read) {
@@ -635,6 +1060,7 @@
635
1060
  }
636
1061
 
637
1062
  function setDocument(titleText, html) {
1063
+ cdxCloseTarget = null;
638
1064
  const panel = documentPanel();
639
1065
  const title = documentTitle();
640
1066
  const content = documentContent();
@@ -653,6 +1079,35 @@
653
1079
  renderMermaidDiagrams();
654
1080
  }
655
1081
 
1082
+ function currentDocumentSnapshot(fallbackTitle = "Document") {
1083
+ const title = documentTitle();
1084
+ const content = documentContent();
1085
+ return {
1086
+ title: title?.textContent || fallbackTitle,
1087
+ html: content?.innerHTML || ""
1088
+ };
1089
+ }
1090
+
1091
+ async function closeDocumentPanel() {
1092
+ const target = cdxCloseTarget;
1093
+ cdxCloseTarget = null;
1094
+ if (target?.type === "cdx-report") {
1095
+ setDocument(target.title || "CDX run report", target.html || "");
1096
+ cdxCloseTarget = { type: "cdx-runs" };
1097
+ setMeta("Returned to CDX run report.");
1098
+ return;
1099
+ }
1100
+ if (target?.type === "cdx-runs") {
1101
+ await showCdxRuns({ silent: true });
1102
+ setMeta("Returned to CDX runs.");
1103
+ return;
1104
+ }
1105
+ const panel = documentPanel();
1106
+ if (panel) {
1107
+ panel.hidden = true;
1108
+ }
1109
+ }
1110
+
656
1111
  function showMermaidFallback(message) {
657
1112
  document.querySelectorAll(".markdown-preview__mermaid-fallback").forEach((node) => {
658
1113
  if (!(node instanceof HTMLElement)) {
@@ -759,26 +1214,39 @@
759
1214
 
760
1215
  function postToApp(payload, options = {}) {
761
1216
  markConnectionHealthy({ silent: Boolean(options.silent) });
1217
+ const nextSignature = viewerStateSignature(payload);
1218
+ if (!options.force && latestViewerStateSignature && nextSignature === latestViewerStateSignature) {
1219
+ if (!options.silent) {
1220
+ setMeta(`Checked just now · no viewer changes (${new Date().toLocaleTimeString()})`);
1221
+ }
1222
+ scheduleNextAutoRefresh();
1223
+ return false;
1224
+ }
1225
+ latestViewerStateSignature = nextSignature;
762
1226
  latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
763
1227
  if (!autoRefreshIntervalTouched) {
764
1228
  autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
765
1229
  updateRefreshIntervalControl();
766
1230
  }
767
1231
  updateRepositoryIdentity(payload);
1232
+ latestCapabilities = normalizeCapabilities(payload);
1233
+ updateCapabilityControls();
768
1234
  const payloadWithActivity = { ...payload, items: latestItems };
769
- const nextPayload = options.silent ? payloadWithActivity : applyFocusRequest(payloadWithActivity);
1235
+ const nextPayload = applyFocusRequest(payloadWithActivity, { silent: Boolean(options.silent) });
770
1236
  window.dispatchEvent(new MessageEvent("message", { data: { type: "data", payload: nextPayload } }));
771
1237
  const rootName = payload.root ? payload.root.split(/[\\/]/).filter(Boolean).pop() : "repository";
772
1238
  if (!options.silent) {
773
1239
  setMeta(`${rootName} · ${payload.items.length} docs · refreshed ${new Date().toLocaleTimeString()}`);
774
1240
  }
775
1241
  scheduleNextAutoRefresh();
1242
+ updateVersionLink(payload.updateInfo);
776
1243
  renderUpdateNotice(payload.updateInfo);
777
1244
  refreshCiBadgeCounters();
778
1245
  refreshCdxBadgeCounters();
779
1246
  updateFilterSummary();
780
1247
  applyLocalViewerChrome();
781
1248
  bindRefreshMenuControls();
1249
+ return true;
782
1250
  }
783
1251
 
784
1252
  function renderUpdateNotice(updateInfo) {
@@ -815,11 +1283,11 @@
815
1283
  if (!response.ok || !data.ok) {
816
1284
  throw new Error(data.error || "Unable to load viewer data.");
817
1285
  }
818
- postToApp(data.payload, { silent: Boolean(options.silent) });
1286
+ const changed = postToApp(data.payload, { silent: Boolean(options.silent), force: Boolean(options.force) });
819
1287
  if (method !== "POST") {
820
1288
  await refreshGitBadgeCounters();
821
1289
  }
822
- return true;
1290
+ return changed;
823
1291
  } catch (error) {
824
1292
  markConnectionDisconnected(error);
825
1293
  throw error;
@@ -840,6 +1308,18 @@
840
1308
  return Boolean(panel && !panel.hidden && title && title.textContent === "CDX status");
841
1309
  }
842
1310
 
1311
+ function isCdxRunsOpen() {
1312
+ const panel = documentPanel();
1313
+ const title = documentTitle();
1314
+ return Boolean(panel && !panel.hidden && title && title.textContent === "CDX runs");
1315
+ }
1316
+
1317
+ function isCdxMissionsOpen() {
1318
+ const panel = documentPanel();
1319
+ const title = documentTitle();
1320
+ return Boolean(panel && !panel.hidden && title && title.textContent === "CDX missions");
1321
+ }
1322
+
843
1323
  function isCiStatusOpen() {
844
1324
  const panel = documentPanel();
845
1325
  const title = documentTitle();
@@ -847,16 +1327,23 @@
847
1327
  }
848
1328
 
849
1329
  async function refreshViewer(method = "POST", options = {}) {
850
- await loadItems(method, options);
1330
+ const changed = await loadItems(method, options);
851
1331
  if (isGitStatusOpen()) {
852
- await showGitStatus({ preserve: true, silent: Boolean(options.silent) });
1332
+ await showGitStatus({ preserve: true, silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
853
1333
  } else if (isCiStatusOpen()) {
854
- await showCiStatus({ silent: Boolean(options.silent) });
1334
+ await showCiStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
855
1335
  } else if (isCdxStatusOpen()) {
856
- await showCdxStatus({ silent: Boolean(options.silent) });
1336
+ await showCdxStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1337
+ } else if (isCdxRunsOpen()) {
1338
+ if (changed || options.force) {
1339
+ await showCdxRuns({ silent: Boolean(options.silent) });
1340
+ }
857
1341
  } else if (method === "POST") {
858
1342
  await refreshGitBadgeCounters();
859
1343
  }
1344
+ if (!changed && !options.silent && !options.force) {
1345
+ setMeta(`Checked just now · no viewer changes (${new Date().toLocaleTimeString()})`);
1346
+ }
860
1347
  }
861
1348
 
862
1349
  function autoRefreshItems() {
@@ -1147,6 +1634,31 @@
1147
1634
  `).join("");
1148
1635
  }
1149
1636
 
1637
+ function renderGitSummaryCard(label, value) {
1638
+ return `
1639
+ <div class="viewer-insights__card">
1640
+ <div class="viewer-insights__label">${escapeHtml(label)}</div>
1641
+ <div class="viewer-insights__value">${escapeHtml(value)}</div>
1642
+ </div>
1643
+ `;
1644
+ }
1645
+
1646
+ function renderGitSummarySegments(label, segments) {
1647
+ return `
1648
+ <div class="viewer-insights__card viewer-git__summary-card">
1649
+ <div class="viewer-insights__label">${escapeHtml(label)}</div>
1650
+ <div class="viewer-git__summary-segments">
1651
+ ${segments.map(([segmentLabel, value]) => `
1652
+ <span class="viewer-git__summary-segment">
1653
+ <span>${escapeHtml(segmentLabel)}</span>
1654
+ <strong>${escapeHtml(value)}</strong>
1655
+ </span>
1656
+ `).join("")}
1657
+ </div>
1658
+ </div>
1659
+ `;
1660
+ }
1661
+
1150
1662
  function renderInsightBars(entries, total) {
1151
1663
  const denominator = Math.max(1, Number(total) || 0);
1152
1664
  if (!entries.length) {
@@ -1570,15 +2082,41 @@
1570
2082
  return asArray(status?.rows);
1571
2083
  }
1572
2084
 
2085
+ function numericValues(values) {
2086
+ return values.map((value) => Number(value)).filter((value) => Number.isFinite(value));
2087
+ }
2088
+
2089
+ function formatPercentRange(values) {
2090
+ const numbers = numericValues(values).map((value) => Math.max(0, Math.min(100, Math.round(value))));
2091
+ if (!numbers.length) {
2092
+ return "not reported";
2093
+ }
2094
+ const min = Math.min(...numbers);
2095
+ const max = Math.max(...numbers);
2096
+ return min === max ? `${min}%` : `${min}-${max}%`;
2097
+ }
2098
+
1573
2099
  function cdxProviders(status) {
1574
- const explicitProviders = pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1575
- if (explicitProviders.length) {
1576
- return explicitProviders;
2100
+ const rows = cdxRows(status);
2101
+ if (!rows.length) {
2102
+ return pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1577
2103
  }
1578
2104
  const grouped = new Map();
1579
- cdxRows(status).forEach((row) => {
2105
+ rows.forEach((row) => {
1580
2106
  const provider = String(row.provider || "unknown");
1581
- const current = grouped.get(provider) || { name: provider, enabled: 0, active: 0, authenticated: 0, sessions: 0, lowest_available_pct: null };
2107
+ const current = grouped.get(provider) || {
2108
+ name: provider,
2109
+ enabled: 0,
2110
+ active: 0,
2111
+ authenticated: 0,
2112
+ sessions: 0,
2113
+ remaining_5h: "not reported",
2114
+ remaining_week: "not reported",
2115
+ credits: "",
2116
+ _remaining5hValues: [],
2117
+ _remainingWeekValues: [],
2118
+ _creditsValues: []
2119
+ };
1582
2120
  current.sessions += 1;
1583
2121
  if (row.enabled) {
1584
2122
  current.enabled += 1;
@@ -1589,15 +2127,31 @@
1589
2127
  if (String(row.auth_status || "").toLowerCase() === "authenticated") {
1590
2128
  current.authenticated += 1;
1591
2129
  }
1592
- if (typeof row.available_pct === "number") {
1593
- current.lowest_available_pct = current.lowest_available_pct === null
1594
- ? row.available_pct
1595
- : Math.min(current.lowest_available_pct, row.available_pct);
2130
+ const fiveHour = Number(row.remaining_5h_pct ?? row.remaining5hPct);
2131
+ if (Number.isFinite(fiveHour)) {
2132
+ current._remaining5hValues.push(fiveHour);
2133
+ }
2134
+ const week = Number(row.remaining_week_pct ?? row.remainingWeekPct);
2135
+ if (Number.isFinite(week)) {
2136
+ current._remainingWeekValues.push(week);
2137
+ }
2138
+ if (row.credits !== undefined && row.credits !== null && row.credits !== "") {
2139
+ current._creditsValues.push(row.credits);
1596
2140
  }
1597
2141
  current.state = current.active > 0 ? "active" : current.enabled > 0 ? "enabled" : "disabled";
1598
2142
  grouped.set(provider, current);
1599
2143
  });
1600
- return Array.from(grouped.values());
2144
+ return Array.from(grouped.values()).map((provider) => {
2145
+ const creditsNumbers = numericValues(provider._creditsValues);
2146
+ const creditsTotal = creditsNumbers.length ? creditsNumbers.reduce((total, value) => total + value, 0) : null;
2147
+ const { _remaining5hValues, _remainingWeekValues, _creditsValues, ...publicProvider } = provider;
2148
+ return {
2149
+ ...publicProvider,
2150
+ remaining_5h: formatPercentRange(_remaining5hValues),
2151
+ remaining_week: formatPercentRange(_remainingWeekValues),
2152
+ credits: creditsTotal === null ? "" : creditsTotal.toFixed(2)
2153
+ };
2154
+ });
1601
2155
  }
1602
2156
 
1603
2157
  function cdxSessions(status) {
@@ -1637,8 +2191,25 @@
1637
2191
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1638
2192
  }
1639
2193
 
2194
+ function renderCdxArtifactRows(value, emptyText) {
2195
+ const rows = objectEntries(value).slice(0, 12).map(([key, entry]) => {
2196
+ const path = typeof entry === "string" ? entry : "";
2197
+ return `
2198
+ <li class="viewer-cdx__row">
2199
+ <span>${escapeHtml(cdxLabel(key))}</span>
2200
+ <strong>${path
2201
+ ? `<button class="viewer-cdx__path-link" type="button" data-viewer-cdx-artifact-path="${escapeHtml(path)}">${escapeHtml(path)}</button>`
2202
+ : escapeHtml(typeof entry === "object" ? JSON.stringify(entry) : entry)}
2203
+ </strong>
2204
+ </li>
2205
+ `;
2206
+ }).join("");
2207
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
2208
+ }
2209
+
1640
2210
  function cdxLabel(value) {
1641
2211
  return String(value || "")
2212
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
1642
2213
  .replace(/[_-]+/g, " ")
1643
2214
  .replace(/\b\w/g, (letter) => letter.toUpperCase());
1644
2215
  }
@@ -1648,7 +2219,7 @@
1648
2219
  if (["ready", "ok", "active", "enabled", "authenticated"].some((entry) => state.includes(entry))) {
1649
2220
  return "ok";
1650
2221
  }
1651
- if (["starting", "pending", "warning", "low", "limited"].some((entry) => state.includes(entry))) {
2222
+ if (["starting", "pending", "running", "warning", "low", "limited", "stale"].some((entry) => state.includes(entry))) {
1652
2223
  return "warn";
1653
2224
  }
1654
2225
  if (["error", "failed", "disabled", "unavailable", "unauthenticated"].some((entry) => state.includes(entry))) {
@@ -1781,6 +2352,10 @@
1781
2352
  return `<span class="viewer-cdx__badge viewer-cdx__badge--${cdxStateClass(label)}">${escapeHtml(cdxLabel(label))}</span>`;
1782
2353
  }
1783
2354
 
2355
+ function cdxRunStatusDetail(run) {
2356
+ return "";
2357
+ }
2358
+
1784
2359
  function cdxDetailEntries(item, excludedKeys) {
1785
2360
  return objectEntries(item)
1786
2361
  .filter(([key, value]) => !excludedKeys.includes(key) && value !== undefined && value !== null && value !== "")
@@ -1906,36 +2481,264 @@
1906
2481
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1907
2482
  }
1908
2483
 
1909
- function renderCdxStatus(payload) {
1910
- if (!payload || payload.state !== "ok") {
2484
+ function cdxMissionCatalog(payload = {}) {
2485
+ return payload.catalog || {
2486
+ missions: [
2487
+ { id: "full-audit", title: "Full audit", description: "Audit the repository and optionally apply safe, validated fixes.", scope: "repository", requiresPlanConfirmation: false, supportsFileWrites: true, inputFields: [{ id: "directFixes", label: "Fix directly", type: "checkbox" }] },
2488
+ { id: "release-review", title: "Review since latest release", description: "Review changes since the latest release and optionally apply safe fixes.", scope: "latest-release", requiresPlanConfirmation: false, supportsFileWrites: true, inputFields: [{ id: "directFixes", label: "Fix directly", type: "checkbox" }] },
2489
+ { id: "corpus-ready", title: "Prepare dev-ready corpus", description: "Produce a corpus plan for explicit deterministic application.", scope: "open-logics-workflow", requiresPlanConfirmation: true, supportsFileWrites: false },
2490
+ { id: "wish-to-request", title: "Wish to request", description: "Create or draft a structured Logics request from a free-form wish.", scope: "request-draft", requiresPlanConfirmation: false, supportsFileWrites: true, inputFields: [{ id: "wishText", label: "Wish or intent", type: "textarea", required: true }] },
2491
+ { id: "pre-release", title: "Guarded pre-release", description: "Prepare release metadata, changelog, validation, and fixes without tagging or publishing.", scope: "pre-release-report", requiresPlanConfirmation: false, supportsFileWrites: true, inputFields: [{ id: "releaseVersion", label: "Version", type: "text", placeholder: "vX.X.X", required: true }, { id: "runFullValidation", label: "Run full validation and report fixes before pre-release", type: "checkbox" }] }
2492
+ ],
2493
+ strengths: [
2494
+ { id: "standard", label: "Standard" },
2495
+ { id: "deep", label: "Deep" },
2496
+ { id: "max", label: "Max" }
2497
+ ],
2498
+ defaultMissionId: "full-audit",
2499
+ defaultStrengthId: "standard"
2500
+ };
2501
+ }
2502
+
2503
+ function selectedCdxMissionRequest() {
2504
+ const catalog = latestCdxMissionState.catalog || cdxMissionCatalog();
2505
+ const missions = Array.isArray(catalog.missions) ? catalog.missions : [];
2506
+ const missionId = latestCdxMissionState.missionId || "full-audit";
2507
+ const mission = missions.find((entry) => entry.id === missionId) || {};
2508
+ const allowFileWrites = mission.supportsFileWrites === false
2509
+ ? "false"
2510
+ : (latestCdxMissionState.missionInputs.allowFileWrites === "false" ? "false" : "true");
2511
+ return {
2512
+ missionId,
2513
+ sessionId: latestCdxMissionState.sessionId || "",
2514
+ strengthId: latestCdxMissionState.strengthId || "standard",
2515
+ ...latestCdxMissionState.missionInputs,
2516
+ allowFileWrites,
2517
+ commitAtEnd: latestCdxMissionState.missionInputs.commitAtEnd === "true" ? "true" : "false"
2518
+ };
2519
+ }
2520
+
2521
+ function renderCdxMissionInputs(mission) {
2522
+ const fields = Array.isArray(mission?.inputFields) ? mission.inputFields : [];
2523
+ if (!fields.length) {
2524
+ return "";
2525
+ }
2526
+ const rows = fields.map((field) => {
2527
+ const id = field.id || "";
2528
+ const value = latestCdxMissionState.missionInputs[id] || "";
2529
+ if (field.type === "checkbox") {
2530
+ return `
2531
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2532
+ <input data-viewer-cdx-input="${escapeHtml(id)}" type="checkbox"${value === "true" ? " checked" : ""}>
2533
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2534
+ </label>
2535
+ `;
2536
+ }
2537
+ if (field.type === "textarea") {
2538
+ return `
2539
+ <label class="viewer-cdx__field">
2540
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2541
+ <textarea data-viewer-cdx-input="${escapeHtml(id)}" placeholder="${escapeHtml(field.placeholder || "")}" rows="5">${escapeHtml(value)}</textarea>
2542
+ </label>
2543
+ `;
2544
+ }
1911
2545
  return `
1912
- <div class="viewer-cdx">
1913
- <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
1914
- </div>
2546
+ <label class="viewer-cdx__field">
2547
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2548
+ <input data-viewer-cdx-input="${escapeHtml(id)}" type="${escapeHtml(field.type || "text")}" value="${escapeHtml(value)}" placeholder="${escapeHtml(field.placeholder || "")}"${field.pattern ? ` pattern="${escapeHtml(field.pattern)}"` : ""}>
2549
+ </label>
1915
2550
  `;
1916
- }
1917
- const status = payload.status || {};
1918
- const providers = cdxProviders(status);
2551
+ }).join("");
2552
+ return `<div class="viewer-cdx__inputs">${rows}</div>`;
2553
+ }
2554
+
2555
+ function renderCdxMissionSetup(statusPayload, planPayload, runPayload, applyPayload) {
2556
+ const catalog = cdxMissionCatalog(planPayload || {});
2557
+ latestCdxMissionState.catalog = catalog;
2558
+ const missions = Array.isArray(catalog.missions) ? catalog.missions : [];
2559
+ const strengths = Array.isArray(catalog.strengths) ? catalog.strengths : [];
2560
+ const status = statusPayload?.status || {};
1919
2561
  const sessions = cdxSessions(status);
1920
- const readiness = cdxReadiness(status);
1921
- const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
1922
- .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
1923
- .filter(Boolean);
1924
- if (!commands.length) {
1925
- commands.push("cdx status --json");
1926
- }
1927
- const runtimeState = status.state || status.status || status.availability || "ok";
1928
- const readinessCount = objectEntries(readiness).length;
1929
- const cards = [
1930
- ["Runtime", runtimeState],
1931
- ["Providers", providers.length],
1932
- ["Sessions", sessions.length],
1933
- ["Readiness", readinessCount ? `${readinessCount} signals` : "Not reported"]
1934
- ].map(([label, value]) => `
1935
- <div class="viewer-cdx__card">
1936
- <div class="viewer-cdx__label">${escapeHtml(label)}</div>
1937
- <div class="viewer-cdx__value">${label === "Runtime" ? renderCdxBadge(value) : escapeHtml(value)}</div>
1938
- </div>
2562
+ const selectedSession = latestCdxMissionState.sessionId || cdxField(sessions[0] || {}, ["id", "name", "session_name", "value"], "");
2563
+ const missionId = latestCdxMissionState.missionId || catalog.defaultMissionId || "full-audit";
2564
+ const selectedMission = missions.find((mission) => mission.id === missionId) || {};
2565
+ const strengthId = latestCdxMissionState.strengthId || catalog.defaultStrengthId || "standard";
2566
+ const supportsFileWrites = selectedMission.supportsFileWrites !== false;
2567
+ const allowFileWrites = supportsFileWrites && latestCdxMissionState.missionInputs.allowFileWrites !== "false";
2568
+ const fileWriteLabel = ["full-audit", "release-review"].includes(selectedMission.id)
2569
+ ? "Write mission corpus/report"
2570
+ : "Allow CDX to modify files";
2571
+ const fileWriteControl = supportsFileWrites
2572
+ ? `
2573
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2574
+ <input data-viewer-cdx-input="allowFileWrites" type="checkbox"${allowFileWrites ? " checked" : ""}>
2575
+ <span>${escapeHtml(fileWriteLabel)}</span>
2576
+ </label>
2577
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2578
+ <input data-viewer-cdx-input="commitAtEnd" type="checkbox"${latestCdxMissionState.missionInputs.commitAtEnd === "true" ? " checked" : ""}>
2579
+ <span>Commit changes at end</span>
2580
+ </label>
2581
+ `
2582
+ : `
2583
+ <div class="viewer-cdx__meta">Corpus updates are applied after CDX returns allowed actions.</div>
2584
+ `;
2585
+ latestCdxMissionState.sessionId = selectedSession;
2586
+ const missionCards = missions.map((mission) => `
2587
+ <button class="viewer-cdx__mission${mission.id === missionId ? " is-active" : ""}" type="button" data-viewer-cdx-mission="${escapeHtml(mission.id)}" aria-pressed="${mission.id === missionId ? "true" : "false"}">
2588
+ <strong>${escapeHtml(mission.title || mission.id)}</strong>
2589
+ <span>${escapeHtml(mission.description || "")}</span>
2590
+ <em>${escapeHtml(cdxLabel(mission.scope || ""))}</em>
2591
+ </button>
2592
+ `).join("");
2593
+ const sessionOptions = sessions.map((session) => {
2594
+ const item = session && typeof session === "object" ? session : { value: session };
2595
+ const id = cdxField(item, ["id", "name", "session_name", "value"], "");
2596
+ const label = [id, cdxField(item, ["provider"], ""), renderTextRemaining(item)].filter(Boolean).join(" · ");
2597
+ return `<option value="${escapeHtml(id)}"${id === selectedSession ? " selected" : ""}>${escapeHtml(label || id)}</option>`;
2598
+ }).join("");
2599
+ const strengthButtons = strengths.map((strength) => `
2600
+ <button class="viewer-cdx__mode${strength.id === strengthId ? " is-active" : ""}" type="button" data-viewer-cdx-strength="${escapeHtml(strength.id)}" aria-pressed="${strength.id === strengthId ? "true" : "false"}">${escapeHtml(strength.label || cdxLabel(strength.id))}</button>
2601
+ `).join("");
2602
+ const plan = planPayload?.plan;
2603
+ const warnings = Array.isArray(plan?.warnings) ? plan.warnings : [];
2604
+ const command = Array.isArray(plan?.command) ? plan.command.join(" ") : "";
2605
+ const warningRows = warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
2606
+ const canRun = planPayload?.state === "ok" && plan?.canRun;
2607
+ const usage = runPayload?.run?.usage || {};
2608
+ const run = runPayload?.run;
2609
+ const usageText = usage.available
2610
+ ? `${usage.totalTokens ?? "-"} total · ${usage.inputTokens ?? "-"} in · ${usage.outputTokens ?? "-"} out`
2611
+ : (usage.message || "Token usage not reported yet.");
2612
+ const parsedActions = Array.isArray(run?.parsed?.actions) ? run.parsed.actions : [];
2613
+ const applyResults = Array.isArray(applyPayload?.results) ? applyPayload.results : [];
2614
+ const actionRows = parsedActions.map((action) => `
2615
+ <li class="viewer-cdx__row"><span>${escapeHtml(cdxLabel(action.type || "action"))}</span><strong>${escapeHtml(action.target || "-")}</strong></li>
2616
+ `).join("");
2617
+ const applyRows = applyResults.map((result) => `
2618
+ <li class="viewer-cdx__row"><span>${escapeHtml(cdxLabel(result.type || "action"))}</span><strong>${escapeHtml(result.returnCode === 0 ? "applied" : "failed")}</strong></li>
2619
+ `).join("");
2620
+ return `
2621
+ <div class="viewer-cdx__workspace viewer-cdx__workspace--missions">
2622
+ <div class="viewer-cdx__stack">
2623
+ <section class="viewer-cdx__section">
2624
+ <h2 class="viewer-cdx__heading">Mission</h2>
2625
+ <div class="viewer-cdx__missions">${missionCards}</div>
2626
+ </section>
2627
+ <section class="viewer-cdx__section">
2628
+ <h2 class="viewer-cdx__heading">Execution</h2>
2629
+ <label class="viewer-cdx__field">
2630
+ <span>Session</span>
2631
+ <select data-viewer-cdx-session>${sessionOptions || '<option value="">No session reported</option>'}</select>
2632
+ </label>
2633
+ <div class="viewer-cdx__strengths">${strengthButtons}</div>
2634
+ ${fileWriteControl}
2635
+ ${renderCdxMissionInputs(selectedMission)}
2636
+ <div class="viewer-cdx__actions">
2637
+ <button class="btn" type="button" data-viewer-cdx-plan>Preview</button>
2638
+ <button class="btn" type="button" data-viewer-cdx-run${canRun ? "" : " disabled"}>Launch run</button>
2639
+ </div>
2640
+ </section>
2641
+ </div>
2642
+ <div class="viewer-cdx__stack">
2643
+ <section class="viewer-cdx__section">
2644
+ <h2 class="viewer-cdx__heading">Plan preview</h2>
2645
+ ${planPayload && planPayload.state !== "ok" ? `<div class="viewer-cdx__state">${escapeHtml(planPayload.message || "Unable to build mission plan.")}</div>` : ""}
2646
+ ${command ? `<pre class="viewer-cdx__code">${escapeHtml(command)}</pre>` : '<div class="viewer-cdx__empty">Preview a mission to inspect the exact command before launch.</div>'}
2647
+ ${plan?.releaseTag ? `<div class="viewer-cdx__meta">Base tag: ${escapeHtml(plan.releaseTag)}</div>` : ""}
2648
+ ${plan?.commitAtEnd ? '<div class="viewer-cdx__meta">Commit at end: enabled when mission changes files.</div>' : ""}
2649
+ ${plan?.requiresConfirmation ? '<div class="viewer-cdx__meta">Plan-first mission: Logics changes need explicit apply after CDX returns allowed actions.</div>' : ""}
2650
+ ${warningRows ? `<ul class="viewer-cdx__warnings">${warningRows}</ul>` : ""}
2651
+ </section>
2652
+ <section class="viewer-cdx__section">
2653
+ <h2 class="viewer-cdx__heading">Run output</h2>
2654
+ ${runPayload ? `<div class="viewer-cdx__state viewer-cdx__state--${escapeHtml(cdxStateClass(runPayload.state))}">${escapeHtml(runPayload.message || cdxLabel(runPayload.state))}</div>` : '<div class="viewer-cdx__empty">No mission run launched yet.</div>'}
2655
+ ${run ? `<ul class="viewer-cdx__list">
2656
+ <li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.runId || "-")}</strong></li>
2657
+ <li class="viewer-cdx__row"><span>Usage</span><strong>${escapeHtml(usageText)}</strong></li>
2658
+ <li class="viewer-cdx__row"><span>Return code</span><strong>${escapeHtml(run.returnCode ?? "-")}</strong></li>
2659
+ </ul>` : ""}
2660
+ ${run?.stdout ? `<pre class="viewer-cdx__code">${escapeHtml(run.stdout)}</pre>` : ""}
2661
+ ${run?.stderr ? `<pre class="viewer-cdx__code viewer-cdx__code--error">${escapeHtml(run.stderr)}</pre>` : ""}
2662
+ </section>
2663
+ ${plan?.missionId === "corpus-ready" || latestCdxMissionState.missionId === "corpus-ready" ? `
2664
+ <section class="viewer-cdx__section">
2665
+ <h2 class="viewer-cdx__heading">Corpus apply</h2>
2666
+ <ul class="viewer-cdx__list">${actionRows || '<li class="viewer-cdx__empty">CDX has not returned allowed corpus actions yet.</li>'}</ul>
2667
+ <div class="viewer-cdx__actions">
2668
+ <button class="btn" type="button" data-viewer-cdx-apply-plan${parsedActions.length ? "" : " disabled"}>Apply allowed actions</button>
2669
+ </div>
2670
+ ${applyPayload ? `<div class="viewer-cdx__state viewer-cdx__state--${escapeHtml(cdxStateClass(applyPayload.state))}">${escapeHtml(applyPayload.message || cdxLabel(applyPayload.state))}</div>` : ""}
2671
+ ${applyRows ? `<ul class="viewer-cdx__list">${applyRows}</ul>` : ""}
2672
+ </section>
2673
+ ` : ""}
2674
+ </div>
2675
+ </div>
2676
+ `;
2677
+ }
2678
+
2679
+ function renderTextRemaining(item) {
2680
+ const percent = cdxRemainingPct(item);
2681
+ return percent === null ? "" : `${percent}% remaining`;
2682
+ }
2683
+
2684
+ function renderCdxMissions(statusPayload, planPayload = null, runPayload = null, applyPayload = null) {
2685
+ if (!statusPayload || statusPayload.state !== "ok") {
2686
+ return `
2687
+ <div class="viewer-cdx">
2688
+ ${renderCdxModeSwitcher("missions")}
2689
+ <div class="viewer-cdx__state">${escapeHtml(statusPayload?.message || "CDX missions are unavailable.")}</div>
2690
+ </div>
2691
+ `;
2692
+ }
2693
+ return `
2694
+ <div class="viewer-cdx">
2695
+ ${renderCdxModeSwitcher("missions")}
2696
+ ${renderCdxMissionSetup(statusPayload, planPayload, runPayload, applyPayload)}
2697
+ </div>
2698
+ `;
2699
+ }
2700
+
2701
+ function renderCdxModeSwitcher(active) {
2702
+ return `
2703
+ <div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
2704
+ <button class="viewer-cdx__mode${active === "status" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="status" aria-selected="${active === "status" ? "true" : "false"}">Status</button>
2705
+ <button class="viewer-cdx__mode${active === "missions" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="missions" aria-selected="${active === "missions" ? "true" : "false"}">Missions</button>
2706
+ <button class="viewer-cdx__mode${active === "runs" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="runs" aria-selected="${active === "runs" ? "true" : "false"}">Runs</button>
2707
+ </div>
2708
+ `;
2709
+ }
2710
+
2711
+ function renderCdxStatus(payload) {
2712
+ if (!payload || payload.state !== "ok") {
2713
+ return `
2714
+ <div class="viewer-cdx">
2715
+ ${renderCdxModeSwitcher("status")}
2716
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
2717
+ </div>
2718
+ `;
2719
+ }
2720
+ const status = payload.status || {};
2721
+ const providers = cdxProviders(status);
2722
+ const sessions = cdxSessions(status);
2723
+ const readiness = cdxReadiness(status);
2724
+ const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
2725
+ .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
2726
+ .filter(Boolean);
2727
+ if (!commands.length) {
2728
+ commands.push("cdx status --json");
2729
+ }
2730
+ const runtimeState = status.state || status.status || status.availability || "ok";
2731
+ const readinessCount = objectEntries(readiness).length;
2732
+ const cards = [
2733
+ ["Runtime", runtimeState],
2734
+ ["Providers", providers.length],
2735
+ ["Sessions", sessions.length],
2736
+ ["Readiness", readinessCount ? `${readinessCount} signals` : "Not reported"]
2737
+ ].map(([label, value]) => `
2738
+ <div class="viewer-cdx__card">
2739
+ <div class="viewer-cdx__label">${escapeHtml(label)}</div>
2740
+ <div class="viewer-cdx__value">${label === "Runtime" ? renderCdxBadge(value) : escapeHtml(value)}</div>
2741
+ </div>
1939
2742
  `).join("");
1940
2743
  const commandRows = commands.slice(0, 10).map((command, index) => `
1941
2744
  <li>
@@ -1945,6 +2748,7 @@
1945
2748
  `).join("");
1946
2749
  return `
1947
2750
  <div class="viewer-cdx">
2751
+ ${renderCdxModeSwitcher("status")}
1948
2752
  <div class="viewer-cdx__summary">${cards}</div>
1949
2753
  <div class="viewer-cdx__workspace">
1950
2754
  <div class="viewer-cdx__stack">
@@ -1952,10 +2756,6 @@
1952
2756
  <h2 class="viewer-cdx__heading">Sessions</h2>
1953
2757
  ${renderCdxSessionTable(sessions, "No sessions reported.")}
1954
2758
  </section>
1955
- <section class="viewer-cdx__section">
1956
- <h2 class="viewer-cdx__heading">Providers</h2>
1957
- <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
1958
- </section>
1959
2759
  </div>
1960
2760
  <div class="viewer-cdx__stack">
1961
2761
  <section class="viewer-cdx__section">
@@ -1966,13 +2766,316 @@
1966
2766
  <h2 class="viewer-cdx__heading">Safe next commands</h2>
1967
2767
  <ul class="viewer-cdx__commands">${commandRows || '<li class="viewer-cdx__empty">No suggested commands reported.</li>'}</ul>
1968
2768
  </section>
2769
+ <section class="viewer-cdx__section">
2770
+ <h2 class="viewer-cdx__heading">Providers</h2>
2771
+ <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
2772
+ </section>
2773
+ </div>
2774
+ </div>
2775
+ </div>
2776
+ `;
2777
+ }
2778
+
2779
+ function renderCdxRuns(payload) {
2780
+ if (!payload || payload.state !== "ok") {
2781
+ return `
2782
+ <div class="viewer-cdx">
2783
+ ${renderCdxModeSwitcher("runs")}
2784
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX runs are unavailable.")}</div>
2785
+ </div>
2786
+ `;
2787
+ }
2788
+ const runs = Array.isArray(payload.runs) ? payload.runs : [];
2789
+ const staleCount = runs.filter((run) => String(cdxField(run, ["status", "state"], "")).toLowerCase() === "stale").length;
2790
+ const runningCount = runs.filter((run) => ["running", "starting", "pending"].includes(String(cdxField(run, ["status", "state"], "")).toLowerCase())).length;
2791
+ const runsSummary = staleCount
2792
+ ? `${runs.length} reported · ${staleCount} incomplete${runningCount ? ` · ${runningCount} running` : ""}`
2793
+ : runningCount
2794
+ ? `${runs.length} reported · ${runningCount} running`
2795
+ : `${runs.length} reported`;
2796
+ const rows = runs.map((run) => {
2797
+ const runId = cdxField(run, ["run_id", "runId", "id"], "");
2798
+ const status = cdxField(run, ["status", "state"], "unknown");
2799
+ const detail = cdxRunStatusDetail(run);
2800
+ return `
2801
+ <tr>
2802
+ <td><code>${escapeHtml(runId || "-")}</code>${detail ? `<div class="viewer-cdx__meta">${escapeHtml(detail)}</div>` : ""}</td>
2803
+ <td>${renderCdxBadge(status)}</td>
2804
+ <td>${escapeHtml(cdxField(run, ["kind"], "assistant"))}</td>
2805
+ <td>${escapeHtml(cdxField(run, ["session", "session_id", "sessionId"], "-"))}</td>
2806
+ <td>${escapeHtml(cdxField(run, ["cwd", "workspace", "repo"], "-"))}</td>
2807
+ <td>${runId ? `<button class="viewer-cdx__mode" type="button" data-viewer-cdx-report="${escapeHtml(runId)}">Report</button>` : ""}</td>
2808
+ </tr>
2809
+ `;
2810
+ }).join("");
2811
+ return `
2812
+ <div class="viewer-cdx">
2813
+ ${renderCdxModeSwitcher("runs")}
2814
+ <section class="viewer-cdx__section">
2815
+ <div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runsSummary)}</span></div>
2816
+ <div class="viewer-cdx__table-wrap">
2817
+ <table class="viewer-cdx__table">
2818
+ <thead><tr><th>RUN</th><th>STATUS</th><th>KIND</th><th>SESSION</th><th>CWD</th><th>REPORT</th></tr></thead>
2819
+ <tbody>${rows || '<tr><td colspan="6" class="viewer-cdx__empty">No assistant runs reported.</td></tr>'}</tbody>
2820
+ </table>
1969
2821
  </div>
2822
+ </section>
2823
+ </div>
2824
+ `;
2825
+ }
2826
+
2827
+ function cdxReportMissionOutput(report, run, taskReport) {
2828
+ const parsed = report?.parsed && typeof report.parsed === "object" ? report.parsed : {};
2829
+ const candidates = [
2830
+ report?.missionOutput,
2831
+ report?.mission_output,
2832
+ parsed.missionOutput,
2833
+ parsed.mission_output,
2834
+ run?.missionOutput,
2835
+ run?.mission_output,
2836
+ taskReport?.missionOutput,
2837
+ taskReport?.mission_output
2838
+ ];
2839
+ return candidates.find((candidate) => candidate && typeof candidate === "object" && !Array.isArray(candidate)) || null;
2840
+ }
2841
+
2842
+ function cdxCount(value) {
2843
+ if (Array.isArray(value)) {
2844
+ return value.length;
2845
+ }
2846
+ if (value && typeof value === "object") {
2847
+ return objectEntries(value).length;
2848
+ }
2849
+ return value ? 1 : 0;
2850
+ }
2851
+
2852
+ function cdxReportCanCreateRequest(taskReport, missionOutput) {
2853
+ if (taskReport?.kind === "code-review") {
2854
+ return true;
2855
+ }
2856
+ if (cdxCount(taskReport?.findings)) {
2857
+ return true;
2858
+ }
2859
+ return ["findings", "recommendations", "requestFiles", "actionableFixes", "releasePlan"].some((key) => cdxCount(missionOutput?.[key]));
2860
+ }
2861
+
2862
+ function renderCdxReportCards(cards) {
2863
+ return `
2864
+ <div class="viewer-cdx__summary">
2865
+ ${cards.map(([label, value]) => `
2866
+ <div class="viewer-cdx__card">
2867
+ <div class="viewer-cdx__label">${escapeHtml(label)}</div>
2868
+ <div class="viewer-cdx__value">${escapeHtml(value)}</div>
2869
+ </div>
2870
+ `).join("")}
2871
+ </div>
2872
+ `;
2873
+ }
2874
+
2875
+ function renderCdxDetailValue(value) {
2876
+ if (Array.isArray(value)) {
2877
+ return `
2878
+ <ol class="viewer-cdx__detail-list">
2879
+ ${value.map((item) => `
2880
+ <li>${typeof item === "object" && item !== null
2881
+ ? `<pre class="viewer-cdx__detail-code">${escapeHtml(JSON.stringify(item, null, 2))}</pre>`
2882
+ : escapeHtml(String(item))}
2883
+ </li>
2884
+ `).join("")}
2885
+ </ol>
2886
+ `;
2887
+ }
2888
+ if (value && typeof value === "object") {
2889
+ return `<pre class="viewer-cdx__detail-code">${escapeHtml(JSON.stringify(value, null, 2))}</pre>`;
2890
+ }
2891
+ return `<strong>${escapeHtml(String(value))}</strong>`;
2892
+ }
2893
+
2894
+ function renderCdxDetailRow(label, value) {
2895
+ return `
2896
+ <li class="viewer-cdx__row viewer-cdx__row--block">
2897
+ <span>${escapeHtml(label)}</span>
2898
+ <div class="viewer-cdx__detail-value">${renderCdxDetailValue(value)}</div>
2899
+ </li>
2900
+ `;
2901
+ }
2902
+
2903
+ function parseCdxLogJson(content) {
2904
+ const raw = String(content || "").trim();
2905
+ if (!raw) {
2906
+ return null;
2907
+ }
2908
+ try {
2909
+ return { kind: "json", value: JSON.parse(raw) };
2910
+ } catch {
2911
+ // Fall through to JSONL detection.
2912
+ }
2913
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2914
+ if (lines.length < 2) {
2915
+ return null;
2916
+ }
2917
+ const values = [];
2918
+ for (const line of lines) {
2919
+ try {
2920
+ values.push(JSON.parse(line));
2921
+ } catch {
2922
+ return null;
2923
+ }
2924
+ }
2925
+ return { kind: "jsonl", value: values };
2926
+ }
2927
+
2928
+ function renderCdxStructuredLog(parsed) {
2929
+ if (!parsed) {
2930
+ return "";
2931
+ }
2932
+ const label = parsed.kind === "jsonl" ? `${parsed.value.length} JSONL event(s)` : "JSON document";
2933
+ return `
2934
+ <details class="viewer-cdx__log-structured" open>
2935
+ <summary>Structured preview · ${escapeHtml(label)}</summary>
2936
+ <div class="viewer-cdx__detail-value">${renderCdxDetailValue(parsed.value)}</div>
2937
+ </details>
2938
+ `;
2939
+ }
2940
+
2941
+ function renderCdxLogPreview(payload) {
2942
+ const path = payload?.path || "";
2943
+ const content = payload?.content || "";
2944
+ const truncated = Boolean(payload?.truncated);
2945
+ const parsed = parseCdxLogJson(content);
2946
+ return `
2947
+ <div class="viewer-cdx">
2948
+ <section class="viewer-cdx__section">
2949
+ <div class="viewer-ci__heading"><h2>Log preview</h2><span>${truncated ? "latest output" : "complete file"}</span></div>
2950
+ <div class="viewer-cdx__log-preview">
2951
+ <div class="viewer-cdx__meta">${escapeHtml(path)}</div>
2952
+ ${truncated ? '<div class="viewer-cdx__state viewer-cdx__state--warn">Preview truncated to the end of the file. Open the file externally for the full log.</div>' : ""}
2953
+ ${renderCdxStructuredLog(parsed)}
2954
+ <details class="viewer-cdx__log-raw"${parsed ? "" : " open"}>
2955
+ <summary>Raw log</summary>
2956
+ <pre class="viewer-cdx__log-content">${escapeHtml(content || "Log is empty.")}</pre>
2957
+ </details>
2958
+ </div>
2959
+ </section>
2960
+ </div>
2961
+ `;
2962
+ }
2963
+
2964
+ function renderCdxMissionOutput(output) {
2965
+ if (!output) {
2966
+ return "";
2967
+ }
2968
+ const rows = [
2969
+ ["Summary", output.summary],
2970
+ ["Version", output.version],
2971
+ ["Validation", output.validationMode],
2972
+ ["Blocked", typeof output.blocked === "boolean" ? (output.blocked ? "Yes" : "No") : ""],
2973
+ ["Actions", cdxCount(output.actions)],
2974
+ ["Findings", cdxCount(output.findings)],
2975
+ ["Recommendations", cdxCount(output.recommendations)],
2976
+ ["Changed files", cdxCount(output.changedFiles)],
2977
+ ["Corpus files", cdxCount(output.corpusFiles)],
2978
+ ["Generated files", cdxCount(output.generatedFiles)],
2979
+ ["Validation evidence", cdxCount(output.validationEvidence)]
2980
+ ].filter(([_label, value]) => value !== undefined && value !== null && value !== "" && value !== 0);
2981
+ const detailKeys = [
2982
+ "actions",
2983
+ "findings",
2984
+ "recommendations",
2985
+ "directFixes",
2986
+ "requestFiles",
2987
+ "actionableFixes",
2988
+ "changedFiles",
2989
+ "corpusFiles",
2990
+ "generatedFiles",
2991
+ "validationEvidence",
2992
+ "releasePlan"
2993
+ ];
2994
+ const details = detailKeys
2995
+ .filter((key) => cdxCount(output[key]))
2996
+ .map((key) => renderCdxDetailRow(cdxLabel(key), output[key]))
2997
+ .join("");
2998
+ return `
2999
+ <section class="viewer-cdx__section">
3000
+ <div class="viewer-ci__heading"><h2>Mission output</h2><span>${escapeHtml(rows.length)} signals</span></div>
3001
+ <ul class="viewer-cdx__list">
3002
+ ${rows.map(([label, value]) => renderCdxDetailRow(label, value)).join("") || '<li class="viewer-cdx__empty">No structured mission output was reported.</li>'}
3003
+ </ul>
3004
+ ${details ? `<ul class="viewer-cdx__list">${details}</ul>` : ""}
3005
+ </section>
3006
+ `;
3007
+ }
3008
+
3009
+ function renderCdxReport(payload) {
3010
+ if (!payload || payload.state !== "ok" || !payload.report) {
3011
+ return `
3012
+ <div class="viewer-cdx">
3013
+ ${renderCdxModeSwitcher("runs")}
3014
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX run report is unavailable.")}</div>
1970
3015
  </div>
3016
+ `;
3017
+ }
3018
+ const report = payload.report || {};
3019
+ const run = report.run || {};
3020
+ const taskReport = report.task_report || {};
3021
+ const runError = report.error || run.error || {};
3022
+ const artifacts = report.artifacts || run.artifacts || {};
3023
+ const findings = Array.isArray(taskReport.findings) ? taskReport.findings : [];
3024
+ const missionOutput = cdxReportMissionOutput(report, run, taskReport);
3025
+ const findingRows = findings.map((finding, index) => {
3026
+ const location = [finding.path || finding.file || "", finding.line || ""].filter(Boolean).join(":") || "-";
3027
+ return `<li class="viewer-cdx__entity"><div class="viewer-cdx__entity-main"><div><strong>${escapeHtml(finding.message || finding.title || `Finding ${index + 1}`)}</strong><div class="viewer-cdx__meta">${escapeHtml(location)}</div></div>${renderCdxBadge(finding.severity || "unknown")}</div></li>`;
3028
+ }).join("");
3029
+ const canCreate = cdxReportCanCreateRequest(taskReport, missionOutput);
3030
+ return `
3031
+ <div class="viewer-cdx">
3032
+ ${renderCdxModeSwitcher("runs")}
3033
+ <section class="viewer-cdx__section">
3034
+ <div class="viewer-ci__heading viewer-ci__heading--actions">
3035
+ <div><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
3036
+ <button class="viewer-cdx__mode" type="button" data-viewer-cdx-back-runs>Back to runs</button>
3037
+ </div>
3038
+ ${renderCdxReportCards([
3039
+ ["Status", run.status || "unknown"],
3040
+ ["Kind", taskReport.kind || run.kind || "assistant"],
3041
+ ["Findings", String(findings.length)],
3042
+ ["Artifacts", String(objectEntries(artifacts).length)]
3043
+ ])}
3044
+ <ul class="viewer-cdx__list">
3045
+ <li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.run_id || taskReport.run_id || "-")}</strong></li>
3046
+ <li class="viewer-cdx__row"><span>Kind</span><strong>${escapeHtml(taskReport.kind || run.kind || "assistant")}</strong></li>
3047
+ ${renderCdxDetailRow("Summary", taskReport.summary || "No summary reported.")}
3048
+ </ul>
3049
+ ${canCreate ? `<button class="btn" type="button" data-viewer-cdx-create-request="${escapeHtml(run.run_id || taskReport.run_id || "")}">Create Logics request</button>` : ""}
3050
+ </section>
3051
+ ${renderCdxMissionOutput(missionOutput)}
3052
+ ${objectEntries(runError).length ? `
3053
+ <section class="viewer-cdx__section">
3054
+ <div class="viewer-ci__heading"><h2>Run signal</h2><span>${escapeHtml(runError.code || "reported")}</span></div>
3055
+ <ul class="viewer-cdx__list">${renderCdxObjectRows(runError, "No run signal reported.")}</ul>
3056
+ </section>
3057
+ ` : ""}
3058
+ ${objectEntries(artifacts).length ? `
3059
+ <section class="viewer-cdx__section">
3060
+ <div class="viewer-ci__heading"><h2>Artifacts</h2><span>${escapeHtml(objectEntries(artifacts).length)} paths</span></div>
3061
+ <ul class="viewer-cdx__list">${renderCdxArtifactRows(artifacts, "No artifact paths reported.")}</ul>
3062
+ </section>
3063
+ ` : ""}
3064
+ <section class="viewer-cdx__section">
3065
+ <div class="viewer-ci__heading"><h2>Findings</h2><span>${escapeHtml(findings.length)} reported</span></div>
3066
+ <ul class="viewer-cdx__list">${findingRows || '<li class="viewer-cdx__empty">No structured findings reported.</li>'}</ul>
3067
+ </section>
1971
3068
  </div>
1972
3069
  `;
1973
3070
  }
1974
3071
 
1975
3072
  async function showCdxStatus(options = {}) {
3073
+ if (!isCapabilityAvailable("cdx")) {
3074
+ const message = capabilityMessage("cdx", "CDX is not available for this project.");
3075
+ setDocument("CDX status", renderCdxStatus({ state: capability("cdx").state, message }));
3076
+ setMeta(message);
3077
+ return;
3078
+ }
1976
3079
  if (!options.silent) {
1977
3080
  setMeta("Checking CDX status...");
1978
3081
  }
@@ -1994,11 +3097,207 @@
1994
3097
  if (!response.ok || !data.ok) {
1995
3098
  throw new Error(data.error || "Unable to load CDX status.");
1996
3099
  }
3100
+ const nextCdxSignature = runtimeStatusSignature(data.payload);
3101
+ if (options.skipUnchanged && !options.force && latestCdxStatusSignature && nextCdxSignature === latestCdxStatusSignature) {
3102
+ updateMainCdxBadge(data.payload);
3103
+ if (!options.silent) {
3104
+ setMeta(`Checked CDX status just now · no changes (${new Date().toLocaleTimeString()})`);
3105
+ }
3106
+ return;
3107
+ }
3108
+ latestCdxStatusSignature = nextCdxSignature;
1997
3109
  updateMainCdxBadge(data.payload);
1998
3110
  setDocument("CDX status", renderCdxStatus(data.payload));
1999
3111
  setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
2000
3112
  }
2001
3113
 
3114
+ async function showCdxMissions(options = {}) {
3115
+ if (!isCapabilityAvailable("cdx")) {
3116
+ const message = capabilityMessage("cdx", "CDX is not available for this project.");
3117
+ setDocument("CDX missions", renderCdxMissions({ state: capability("cdx").state, message }));
3118
+ setMeta(message);
3119
+ return;
3120
+ }
3121
+ if (!options.silent) {
3122
+ setMeta("Loading CDX missions...");
3123
+ }
3124
+ const response = await fetch("/api/cdx-status");
3125
+ let data = {};
3126
+ try {
3127
+ data = await response.json();
3128
+ } catch {
3129
+ data = {};
3130
+ }
3131
+ if (!response.ok || !data.ok) {
3132
+ throw new Error(data.error || "Unable to load CDX mission status.");
3133
+ }
3134
+ latestCdxMissionState.statusPayload = data.payload;
3135
+ const sessions = cdxSessions(data.payload?.status || {});
3136
+ if (!latestCdxMissionState.sessionId && sessions.length) {
3137
+ latestCdxMissionState.sessionId = cdxField(sessions[0], ["id", "name", "session_name", "value"], "");
3138
+ }
3139
+ updateMainCdxBadge(data.payload);
3140
+ setDocument("CDX missions", renderCdxMissions(data.payload, latestCdxMissionState.planPayload, latestCdxMissionState.runPayload, latestCdxMissionState.applyPayload));
3141
+ setMeta(options.silent ? "CDX missions refreshed." : "CDX missions loaded.");
3142
+ }
3143
+
3144
+ async function previewCdxMission() {
3145
+ setMeta("Preparing CDX mission preview...");
3146
+ const response = await fetch("/api/cdx-mission-plan", {
3147
+ method: "POST",
3148
+ headers: { "Content-Type": "application/json" },
3149
+ body: JSON.stringify(selectedCdxMissionRequest())
3150
+ });
3151
+ const data = await response.json();
3152
+ if (!response.ok || !data.ok) {
3153
+ throw new Error(data.error || "Unable to preview CDX mission.");
3154
+ }
3155
+ latestCdxMissionState.planPayload = data.payload;
3156
+ latestCdxMissionState.runPayload = null;
3157
+ latestCdxMissionState.applyPayload = null;
3158
+ if (data.payload?.plan?.sessionId) {
3159
+ latestCdxMissionState.sessionId = data.payload.plan.sessionId;
3160
+ }
3161
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload || data.payload?.status, data.payload, null, null));
3162
+ setMeta(data.payload?.state === "ok" ? "CDX mission preview ready." : (data.payload?.message || "CDX mission preview failed."));
3163
+ }
3164
+
3165
+ async function launchCdxMission() {
3166
+ setMeta("Launching CDX mission...");
3167
+ const request = selectedCdxMissionRequest();
3168
+ const plan = latestCdxMissionState.planPayload?.plan || null;
3169
+ const pendingPayload = {
3170
+ state: "running",
3171
+ message: "CDX mission is running. You can keep using the viewer; this panel will update when it completes.",
3172
+ plan,
3173
+ run: {
3174
+ runId: "pending",
3175
+ returnCode: "pending",
3176
+ pending: true,
3177
+ usage: { available: false, message: "Still running." },
3178
+ stdout: "",
3179
+ stderr: ""
3180
+ }
3181
+ };
3182
+ latestCdxMissionState.runPayload = pendingPayload;
3183
+ latestCdxMissionState.applyPayload = null;
3184
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, pendingPayload, null));
3185
+ const response = await fetch("/api/cdx-mission-run", {
3186
+ method: "POST",
3187
+ headers: { "Content-Type": "application/json" },
3188
+ body: JSON.stringify(request)
3189
+ });
3190
+ const data = await response.json();
3191
+ if (!response.ok || !data.ok) {
3192
+ throw new Error(data.error || "Unable to launch CDX mission.");
3193
+ }
3194
+ latestCdxMissionState.planPayload = { state: data.payload?.state === "ok" ? "ok" : data.payload?.state, message: data.payload?.message || "", plan: data.payload?.plan };
3195
+ latestCdxMissionState.runPayload = data.payload;
3196
+ latestCdxMissionState.applyPayload = null;
3197
+ if (isCdxMissionsOpen()) {
3198
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, data.payload, null));
3199
+ }
3200
+ setMeta(data.payload?.state === "ok" ? "CDX mission launched." : (data.payload?.message || "CDX mission failed."));
3201
+ }
3202
+
3203
+ async function applyCdxMissionPlan() {
3204
+ const actions = latestCdxMissionState.runPayload?.run?.parsed?.actions;
3205
+ if (!Array.isArray(actions) || !actions.length) {
3206
+ setMeta("No corpus actions to apply.");
3207
+ return;
3208
+ }
3209
+ setMeta("Applying allowed corpus actions...");
3210
+ const response = await fetch("/api/cdx-mission-apply-plan", {
3211
+ method: "POST",
3212
+ headers: { "Content-Type": "application/json" },
3213
+ body: JSON.stringify({ actions })
3214
+ });
3215
+ const data = await response.json();
3216
+ if (!response.ok || !data.ok) {
3217
+ throw new Error(data.error || "Unable to apply corpus plan.");
3218
+ }
3219
+ latestCdxMissionState.applyPayload = data.payload;
3220
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, latestCdxMissionState.runPayload, data.payload));
3221
+ setMeta(data.payload?.state === "ok" ? "Corpus actions applied." : (data.payload?.message || "Corpus apply failed."));
3222
+ }
3223
+
3224
+ async function showCdxRuns(options = {}) {
3225
+ if (!isCapabilityAvailable("cdx")) {
3226
+ const message = capabilityMessage("cdx", "CDX is not available for this project.");
3227
+ setDocument("CDX runs", renderCdxRuns({ state: capability("cdx").state, message }));
3228
+ setMeta(message);
3229
+ return;
3230
+ }
3231
+ if (!options.silent) {
3232
+ setMeta("Checking CDX runs...");
3233
+ }
3234
+ const response = await fetch("/api/cdx-runs");
3235
+ let data = {};
3236
+ try {
3237
+ data = await response.json();
3238
+ } catch {
3239
+ data = {};
3240
+ }
3241
+ if (!response.ok || !data.ok) {
3242
+ throw new Error(data.error || "Unable to load CDX runs.");
3243
+ }
3244
+ setDocument("CDX runs", renderCdxRuns(data.payload));
3245
+ setMeta(options.silent ? "CDX runs refreshed." : "CDX runs loaded.");
3246
+ }
3247
+
3248
+ async function showCdxReport(runId) {
3249
+ if (!runId) {
3250
+ return;
3251
+ }
3252
+ setMeta("Loading CDX report...");
3253
+ const response = await fetch(`/api/cdx-run-report?${new URLSearchParams({ runId }).toString()}`);
3254
+ const data = await response.json();
3255
+ if (!response.ok || !data.ok) {
3256
+ throw new Error(data.error || "Unable to load CDX report.");
3257
+ }
3258
+ setDocument("CDX run report", renderCdxReport(data.payload));
3259
+ cdxCloseTarget = { type: "cdx-runs" };
3260
+ setMeta("CDX report loaded.");
3261
+ }
3262
+
3263
+ async function openCdxArtifact(path) {
3264
+ if (!path) {
3265
+ return;
3266
+ }
3267
+ setMeta("Loading CDX log...");
3268
+ const response = await fetch("/api/file-preview", {
3269
+ method: "POST",
3270
+ headers: { "Content-Type": "application/json" },
3271
+ body: JSON.stringify({ path })
3272
+ });
3273
+ const data = await response.json();
3274
+ if (!response.ok || !data.ok) {
3275
+ throw new Error(data.error || "Unable to load CDX artifact.");
3276
+ }
3277
+ const reportSnapshot = currentDocumentSnapshot("CDX run report");
3278
+ setDocument(data.payload?.name ? `CDX log · ${data.payload.name}` : "CDX log", renderCdxLogPreview(data.payload));
3279
+ cdxCloseTarget = { type: "cdx-report", title: reportSnapshot.title, html: reportSnapshot.html };
3280
+ setMeta(`Loaded ${data.payload?.path || path}.`);
3281
+ }
3282
+
3283
+ async function createRequestFromCdxReport(runId) {
3284
+ if (!runId) {
3285
+ return;
3286
+ }
3287
+ setMeta("Creating Logics request from CDX report...");
3288
+ const response = await fetch("/api/cdx-report-request", {
3289
+ method: "POST",
3290
+ headers: { "Content-Type": "application/json" },
3291
+ body: JSON.stringify({ runId })
3292
+ });
3293
+ const data = await response.json();
3294
+ if (!response.ok || !data.ok) {
3295
+ throw new Error(data.error || "Unable to create Logics request.");
3296
+ }
3297
+ postToApp(data.payload);
3298
+ setMeta(`Created ${data.created?.id || "Logics request"} from CDX report.`);
3299
+ }
3300
+
2002
3301
  function renderCiBadge(value) {
2003
3302
  const tone = ciBadgeTone(value);
2004
3303
  return `<span class="viewer-ci__badge viewer-ci__badge--${escapeHtml(tone)}">${escapeHtml(ciBadgeLabel(value))}</span>`;
@@ -2023,11 +3322,26 @@
2023
3322
  const run = payload.run && typeof payload.run === "object" ? payload.run : null;
2024
3323
  const jobs = Array.isArray(payload.jobs) ? payload.jobs : [];
2025
3324
  const state = payload.badgeState || run?.badgeState || payload.state || "unknown";
3325
+ const matchLabel = run?.matchSource === "head-active"
3326
+ ? "Current HEAD running"
3327
+ : run?.matchSource === "head-failing"
3328
+ ? "Current HEAD failing"
3329
+ : run?.matchSource === "head-cancelled"
3330
+ ? "Current HEAD cancelled"
3331
+ : run?.matchSource === "head-unknown"
3332
+ ? "Current HEAD unknown"
3333
+ : run?.matchSource === "head"
3334
+ ? "Current HEAD"
3335
+ : run?.matchSource === "branch-active"
3336
+ ? "Branch running"
3337
+ : run?.matchSource === "branch-failing"
3338
+ ? "Branch failing"
3339
+ : "Latest branch run";
2026
3340
  const cards = renderMetricCards([
2027
3341
  ["State", ciBadgeLabel(state)],
2028
3342
  ["Branch", run?.branch || payload.branch || "Unknown"],
2029
3343
  ["Commit", (run?.headSha || payload.headSha || "").slice(0, 7) || "Unknown"],
2030
- ["Match", run?.matchSource === "head" ? "Current HEAD" : "Latest branch run"]
3344
+ ["Match", matchLabel]
2031
3345
  ]);
2032
3346
  const runUrl = run?.htmlUrl ? `<a class="viewer-ci__link" href="${escapeHtml(run.htmlUrl)}" target="_blank" rel="noreferrer">Open in GitHub</a>` : "";
2033
3347
  const runRows = run ? [
@@ -2068,6 +3382,12 @@
2068
3382
  }
2069
3383
 
2070
3384
  async function showCiStatus(options = {}) {
3385
+ if (!isCapabilityAvailable("ci")) {
3386
+ const message = capabilityMessage("ci", "CI is not available for this project.");
3387
+ setDocument("CI status", renderCiStatus({ visible: false, state: capability("ci").state, message }));
3388
+ setMeta(message);
3389
+ return;
3390
+ }
2071
3391
  if (!options.silent) {
2072
3392
  setMeta("Checking CI status...");
2073
3393
  }
@@ -2091,6 +3411,15 @@
2091
3411
  if (!response.ok || !data.ok) {
2092
3412
  throw new Error(data.error || "Unable to load CI status.");
2093
3413
  }
3414
+ const nextCiSignature = runtimeStatusSignature(data.payload);
3415
+ if (options.skipUnchanged && !options.force && latestCiStatusSignature && nextCiSignature === latestCiStatusSignature) {
3416
+ updateMainCiBadge(data.payload);
3417
+ if (!options.silent) {
3418
+ setMeta(`Checked CI status just now · no changes (${new Date().toLocaleTimeString()})`);
3419
+ }
3420
+ return;
3421
+ }
3422
+ latestCiStatusSignature = nextCiSignature;
2094
3423
  updateMainCiBadge(data.payload);
2095
3424
  setDocument("CI status", renderCiStatus(data.payload));
2096
3425
  setMeta(options.silent ? "CI status refreshed." : "CI status loaded.");
@@ -2110,17 +3439,20 @@
2110
3439
  const deletedCount = Number(counts.deleted || 0);
2111
3440
  const renamedCount = Number(counts.renamed || 0);
2112
3441
  const untrackedCount = Number(counts.untracked || 0);
2113
- const summary = [
2114
- ["Branch", payload.branch || "HEAD"],
2115
- ["Tracking", payload.tracking || "None"],
2116
- ["Ahead", payload.ahead || 0],
2117
- ["Behind", payload.behind || 0],
2118
- ["State", payload.clean ? "Clean" : "Dirty"],
2119
- ["Staged", stagedCount],
2120
- ["Worktree", modifiedCount + deletedCount + renamedCount],
2121
- ["Untracked", untrackedCount]
2122
- ];
2123
- const cards = renderMetricCards(summary);
3442
+ const cards = [
3443
+ renderGitSummaryCard("Branch", payload.branch || "HEAD"),
3444
+ renderGitSummaryCard("Tracking", payload.tracking || "None"),
3445
+ renderGitSummarySegments("Ahead / Behind", [
3446
+ ["Ahead", payload.ahead || 0],
3447
+ ["Behind", payload.behind || 0]
3448
+ ]),
3449
+ renderGitSummaryCard("State", payload.clean ? "Clean" : "Dirty"),
3450
+ renderGitSummarySegments("Files", [
3451
+ ["Staged", stagedCount],
3452
+ ["Worktree", modifiedCount + deletedCount + renamedCount],
3453
+ ["Untracked", untrackedCount]
3454
+ ])
3455
+ ].join("");
2124
3456
  const groupDefs = [
2125
3457
  ["staged", "Staged", "staged"],
2126
3458
  ["modified", "Modified", "worktree"],
@@ -2133,7 +3465,7 @@
2133
3465
  ["staged", "Staged", stagedCount],
2134
3466
  ["worktree", "Worktree", modifiedCount + deletedCount + renamedCount],
2135
3467
  ["untracked", "Untracked", untrackedCount],
2136
- ["history", "History", Array.isArray(payload.recentCommits) ? payload.recentCommits.length : (payload.latestCommit ? 1 : 0)],
3468
+ ["history", "History", formatGitHistoryCount(payload)],
2137
3469
  ["remote", "Remote", payload.tracking ? 1 : 0]
2138
3470
  ];
2139
3471
  const domains = domainDefs.map(([key, label, count], index) => `
@@ -2175,6 +3507,7 @@
2175
3507
  const untrackedSections = renderFileSections(["untracked"]);
2176
3508
  const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
2177
3509
  const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
3510
+ const historyCount = formatGitHistoryCount(payload);
2178
3511
  const renderGitHistoryReveal = (hiddenCount) => {
2179
3512
  if (hiddenCount <= 0) {
2180
3513
  return "";
@@ -2212,7 +3545,7 @@
2212
3545
  return `
2213
3546
  <div class="viewer-git">
2214
3547
  <div class="viewer-git__summary">${cards}</div>
2215
- <div class="viewer-git__workspace">
3548
+ <div class="viewer-git__workspace has-diff-detail">
2216
3549
  <nav class="viewer-git__domains" aria-label="Git domains">${domains}</nav>
2217
3550
  <div class="viewer-git__content" aria-label="Git domain content">
2218
3551
  <section class="viewer-git__panel" data-viewer-git-panel="changes">
@@ -2233,7 +3566,7 @@
2233
3566
  ${untrackedSections || '<p class="viewer-git__state">No untracked files.</p>'}
2234
3567
  </section>
2235
3568
  <section class="viewer-git__panel" data-viewer-git-panel="history" hidden>
2236
- <header class="viewer-git__panel-header"><span>History</span><strong>${escapeHtml(recentCommits.length || (payload.latestCommit ? 1 : 0))} commits</strong></header>
3569
+ <header class="viewer-git__panel-header"><span>History</span><strong>${escapeHtml(historyCount)} commits</strong></header>
2237
3570
  ${history}
2238
3571
  </section>
2239
3572
  <section class="viewer-git__panel" data-viewer-git-panel="remote" hidden>
@@ -2241,7 +3574,7 @@
2241
3574
  ${remote}
2242
3575
  </section>
2243
3576
  </div>
2244
- <section class="viewer-git__detail" aria-label="Git diff">
3577
+ <section class="viewer-git__detail" aria-label="Git diff" data-viewer-git-detail>
2245
3578
  <div class="viewer-git__detail-title">Diff preview</div>
2246
3579
  <div class="viewer-git__diff" data-viewer-git-diff>Select a changed file to preview its diff.</div>
2247
3580
  </section>
@@ -2250,6 +3583,11 @@
2250
3583
  `;
2251
3584
  }
2252
3585
 
3586
+ function formatGitHistoryCount(payload) {
3587
+ const count = Array.isArray(payload?.recentCommits) ? payload.recentCommits.length : (payload?.latestCommit ? 1 : 0);
3588
+ return `${count}${payload?.recentCommitsHasMore ? "+" : ""}`;
3589
+ }
3590
+
2253
3591
  function setActiveGitFile(button) {
2254
3592
  document.querySelectorAll("[data-viewer-git-file]").forEach((node) => {
2255
3593
  if (node instanceof HTMLElement) {
@@ -2283,12 +3621,16 @@
2283
3621
 
2284
3622
  async function loadGitDiff(path, cached, button = null) {
2285
3623
  const diffPanel = document.querySelector("[data-viewer-git-diff]");
3624
+ const detailTitle = document.querySelector("[data-viewer-git-detail] .viewer-git__detail-title");
2286
3625
  if (!(diffPanel instanceof HTMLElement) || !path) {
2287
3626
  return;
2288
3627
  }
2289
3628
  if (button instanceof HTMLElement) {
2290
3629
  setActiveGitFile(button);
2291
3630
  }
3631
+ if (detailTitle instanceof HTMLElement) {
3632
+ detailTitle.textContent = "Diff preview";
3633
+ }
2292
3634
  diffPanel.textContent = "Loading diff...";
2293
3635
  const params = new URLSearchParams({ path });
2294
3636
  if (cached) {
@@ -2301,12 +3643,38 @@
2301
3643
  diffPanel.textContent = payload.message || data.error || "Unable to load diff.";
2302
3644
  return;
2303
3645
  }
2304
- const content = payload.diff || payload.message || "No diff is available for this file.";
3646
+ const content = payload.diff || "";
3647
+ if (!content.trim()) {
3648
+ await loadGitFilePreview(path, diffPanel, detailTitle);
3649
+ return;
3650
+ }
2305
3651
  diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · ${escapeHtml(payload.mode || "worktree")}${payload.truncated ? " · truncated" : ""}</div><pre><code>${renderGitDiffPreview(content)}</code></pre>`;
2306
3652
  }
2307
3653
 
3654
+ async function loadGitFilePreview(path, diffPanel, detailTitle = null) {
3655
+ if (detailTitle instanceof HTMLElement) {
3656
+ detailTitle.textContent = "File preview";
3657
+ }
3658
+ diffPanel.textContent = "Loading file preview...";
3659
+ const response = await fetch(`/api/git-file-preview?${new URLSearchParams({ path }).toString()}`);
3660
+ const data = await response.json();
3661
+ const payload = data.payload || {};
3662
+ if (!response.ok || !data.ok) {
3663
+ diffPanel.textContent = data.error || "Unable to load file preview.";
3664
+ return;
3665
+ }
3666
+ if (payload.state !== "ok") {
3667
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · file preview unavailable</div><p class="viewer-git__state">${escapeHtml(payload.message || "File preview is unavailable.")}</p>`;
3668
+ return;
3669
+ }
3670
+ const content = payload.content || "";
3671
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · file preview${payload.truncated ? " · truncated" : ""}</div><pre><code>${renderGitDiffPreview(content)}</code></pre>`;
3672
+ }
3673
+
2308
3674
  function applyGitDomain(domain) {
2309
3675
  const selected = domain || "changes";
3676
+ const diffDomains = new Set(["changes", "staged", "worktree", "untracked"]);
3677
+ const showDiffDetail = diffDomains.has(selected);
2310
3678
  document.querySelectorAll(".viewer-git__domain[data-viewer-git-domain]").forEach((node) => {
2311
3679
  if (node instanceof HTMLElement) {
2312
3680
  const active = node.getAttribute("data-viewer-git-domain") === selected;
@@ -2319,6 +3687,16 @@
2319
3687
  node.hidden = node.getAttribute("data-viewer-git-panel") !== selected;
2320
3688
  }
2321
3689
  });
3690
+ document.querySelectorAll(".viewer-git__workspace").forEach((node) => {
3691
+ if (node instanceof HTMLElement) {
3692
+ node.classList.toggle("has-diff-detail", showDiffDetail);
3693
+ }
3694
+ });
3695
+ document.querySelectorAll("[data-viewer-git-detail]").forEach((node) => {
3696
+ if (node instanceof HTMLElement) {
3697
+ node.hidden = !showDiffDetail;
3698
+ }
3699
+ });
2322
3700
  }
2323
3701
 
2324
3702
  function currentGitViewState() {
@@ -2341,6 +3719,12 @@
2341
3719
 
2342
3720
  async function showGitStatus(options = {}) {
2343
3721
  const previous = options.preserve ? currentGitViewState() : { domain: "changes", path: "", cached: false };
3722
+ if (!isCapabilityAvailable("git")) {
3723
+ const message = capabilityMessage("git", "Git is not available for this project.");
3724
+ setDocument("Git status", renderGitStatus({ state: capability("git").state, message }));
3725
+ setMeta(message);
3726
+ return;
3727
+ }
2344
3728
  if (!options.silent) {
2345
3729
  setMeta("Checking Git status...");
2346
3730
  }
@@ -2362,6 +3746,16 @@
2362
3746
  if (!response.ok || !data.ok) {
2363
3747
  throw new Error(data.error || "Unable to load Git status.");
2364
3748
  }
3749
+ const nextGitSignature = gitStatusSignature(data.payload);
3750
+ if (options.skipUnchanged && !options.force && latestGitStatusSignature && nextGitSignature === latestGitStatusSignature) {
3751
+ setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
3752
+ updateMainGitBadges();
3753
+ if (!options.silent) {
3754
+ setMeta(`Checked Git status just now · no changes (${new Date().toLocaleTimeString()})`);
3755
+ }
3756
+ return;
3757
+ }
3758
+ latestGitStatusSignature = nextGitSignature;
2365
3759
  setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
2366
3760
  updateMainGitBadges();
2367
3761
  setDocument("Git status", renderGitStatus(data.payload));
@@ -2385,7 +3779,11 @@
2385
3779
  return;
2386
3780
  }
2387
3781
  if (message.type === "refresh") {
2388
- refreshViewer("POST").catch((error) => setMeta(error.message));
3782
+ refreshViewer("POST", { force: Boolean(message.force) }).catch((error) => setMeta(error.message));
3783
+ return;
3784
+ }
3785
+ if (message.type === "bootstrap-logics") {
3786
+ bootstrapLogicsProject().catch((error) => setMeta(error.message));
2389
3787
  return;
2390
3788
  }
2391
3789
  if (message.type === "open" || message.type === "read") {
@@ -2419,7 +3817,7 @@
2419
3817
  [document.getElementById("viewer-insights")].forEach((button) => {
2420
3818
  button?.addEventListener("click", () => {
2421
3819
  setRefreshMenuOpen(false);
2422
- showCorpusInsights().catch((error) => setMeta(error.message));
3820
+ withPrimaryAction("insights", "Loading insights", showCorpusInsights);
2423
3821
  });
2424
3822
  });
2425
3823
  const autoControl = autoRefreshControl();
@@ -2462,26 +3860,30 @@
2462
3860
  if (!(element instanceof HTMLElement)) {
2463
3861
  return;
2464
3862
  }
2465
- element.addEventListener("click", () => {
3863
+ element.addEventListener("click", (event) => {
2466
3864
  setRefreshMenuOpen(false);
2467
- refreshViewer("POST").catch((error) => setMeta(error.message));
3865
+ withPrimaryAction("refresh", "Refreshing", () => refreshViewer("POST", { force: Boolean(event.shiftKey) }));
2468
3866
  });
2469
3867
  });
2470
3868
  document.getElementById("viewer-health")?.addEventListener("click", () => {
2471
3869
  setRefreshMenuOpen(false);
2472
- showHealth().catch((error) => setMeta(error.message));
3870
+ withPrimaryAction("health", "Checking health", showHealth);
2473
3871
  });
2474
3872
  document.getElementById("viewer-git")?.addEventListener("click", () => {
2475
- showGitStatus().catch((error) => setMeta(error.message));
3873
+ withPrimaryAction("git", "Checking Git status", showGitStatus);
2476
3874
  });
2477
3875
  ciButton()?.addEventListener("click", () => {
2478
- showCiStatus().catch((error) => setMeta(error.message));
3876
+ withPrimaryAction("ci", "Checking CI status", showCiStatus);
2479
3877
  });
2480
3878
  document.getElementById("viewer-cdx")?.addEventListener("click", () => {
2481
- showCdxStatus().catch((error) => setMeta(error.message));
3879
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
3880
+ });
3881
+ repoPill()?.addEventListener("click", () => {
3882
+ const menu = projectMenu();
3883
+ setProjectMenuOpen(Boolean(menu?.hidden));
2482
3884
  });
2483
3885
  repoFolderButton()?.addEventListener("click", () => {
2484
- openRepositoryFolder().catch((error) => setMeta(error.message));
3886
+ withPrimaryAction("open-repo-folder", "Opening repository folder", openRepositoryFolder);
2485
3887
  });
2486
3888
  activityClearControl()?.addEventListener("click", () => {
2487
3889
  clearActivityHistory();
@@ -2506,9 +3908,29 @@
2506
3908
  const editButton = editDocumentButton();
2507
3909
  if (editButton instanceof HTMLElement) {
2508
3910
  editButton.addEventListener("click", () => {
2509
- editDocument(selectedItem()).catch((error) => setMeta(error.message));
3911
+ withPrimaryAction("edit-document", "Opening document", () => editDocument(selectedItem()));
2510
3912
  });
2511
3913
  }
3914
+ document.addEventListener("change", (event) => {
3915
+ const sessionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-session]") : null;
3916
+ const cdxInputTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-input]") : null;
3917
+ if (sessionTarget instanceof HTMLSelectElement) {
3918
+ latestCdxMissionState.sessionId = sessionTarget.value || "";
3919
+ latestCdxMissionState.planPayload = null;
3920
+ latestCdxMissionState.runPayload = null;
3921
+ latestCdxMissionState.applyPayload = null;
3922
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
3923
+ }
3924
+ if (cdxInputTarget instanceof HTMLInputElement || cdxInputTarget instanceof HTMLTextAreaElement) {
3925
+ const key = cdxInputTarget.getAttribute("data-viewer-cdx-input") || "";
3926
+ if (key) {
3927
+ latestCdxMissionState.missionInputs[key] = cdxInputTarget instanceof HTMLInputElement && cdxInputTarget.type === "checkbox" ? (cdxInputTarget.checked ? "true" : "false") : (cdxInputTarget.value || "");
3928
+ latestCdxMissionState.planPayload = null;
3929
+ latestCdxMissionState.runPayload = null;
3930
+ latestCdxMissionState.applyPayload = null;
3931
+ }
3932
+ }
3933
+ });
2512
3934
  document.addEventListener("click", (event) => {
2513
3935
  window.setTimeout(() => applyLocalViewerChrome(), 0);
2514
3936
  const target = event.target instanceof Element ? event.target.closest("[data-viewer-doc-path]") : null;
@@ -2518,6 +3940,84 @@
2518
3940
  const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
2519
3941
  const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
2520
3942
  const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
3943
+ const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
3944
+ const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
3945
+ const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
3946
+ const cdxBackRunsTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-back-runs]") : null;
3947
+ const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
3948
+ const cdxArtifactTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-artifact-path]") : null;
3949
+ const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
3950
+ const cdxMissionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mission]") : null;
3951
+ const cdxStrengthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-strength]") : null;
3952
+ const cdxPlanTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-plan]") : null;
3953
+ const cdxRunTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-run]") : null;
3954
+ const cdxApplyPlanTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-apply-plan]") : null;
3955
+ if (cdxMissionTarget instanceof HTMLElement) {
3956
+ latestCdxMissionState.missionId = cdxMissionTarget.getAttribute("data-viewer-cdx-mission") || "full-audit";
3957
+ latestCdxMissionState.planPayload = null;
3958
+ latestCdxMissionState.runPayload = null;
3959
+ latestCdxMissionState.applyPayload = null;
3960
+ latestCdxMissionState.missionInputs = {};
3961
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
3962
+ return;
3963
+ }
3964
+ if (cdxStrengthTarget instanceof HTMLElement) {
3965
+ latestCdxMissionState.strengthId = cdxStrengthTarget.getAttribute("data-viewer-cdx-strength") || "standard";
3966
+ latestCdxMissionState.planPayload = null;
3967
+ latestCdxMissionState.runPayload = null;
3968
+ latestCdxMissionState.applyPayload = null;
3969
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
3970
+ return;
3971
+ }
3972
+ if (cdxPlanTarget instanceof HTMLElement) {
3973
+ withCdxMissionAction("cdx-plan", "Building CDX mission plan", previewCdxMission);
3974
+ return;
3975
+ }
3976
+ if (cdxRunTarget instanceof HTMLElement) {
3977
+ withCdxMissionAction("cdx-run", "Launching CDX mission", launchCdxMission);
3978
+ return;
3979
+ }
3980
+ if (cdxApplyPlanTarget instanceof HTMLElement) {
3981
+ withCdxMissionAction("cdx-apply-plan", "Applying CDX mission plan", applyCdxMissionPlan);
3982
+ return;
3983
+ }
3984
+ if (cdxBackRunsTarget instanceof HTMLElement) {
3985
+ withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
3986
+ return;
3987
+ }
3988
+ if (cdxReportTarget instanceof HTMLElement) {
3989
+ withPrimaryAction("cdx-report", "Loading CDX report", () => showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || ""));
3990
+ return;
3991
+ }
3992
+ if (cdxArtifactTarget instanceof HTMLElement) {
3993
+ withPrimaryAction("cdx-artifact", "Opening CDX artifact", () => openCdxArtifact(cdxArtifactTarget.getAttribute("data-viewer-cdx-artifact-path") || ""));
3994
+ return;
3995
+ }
3996
+ if (cdxCreateRequestTarget instanceof HTMLElement) {
3997
+ withPrimaryAction("cdx-create-request", "Creating Logics request", () => createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || ""));
3998
+ return;
3999
+ }
4000
+ if (cdxModeTarget instanceof HTMLElement) {
4001
+ const mode = cdxModeTarget.getAttribute("data-viewer-cdx-mode") || "status";
4002
+ if (mode === "runs") {
4003
+ withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
4004
+ } else if (mode === "missions") {
4005
+ withPrimaryAction("cdx-missions", "Loading CDX missions", showCdxMissions);
4006
+ } else {
4007
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
4008
+ }
4009
+ return;
4010
+ }
4011
+ if (projectSwitcherTarget instanceof HTMLElement) {
4012
+ const menu = projectMenu();
4013
+ setProjectMenuOpen(Boolean(menu?.hidden));
4014
+ return;
4015
+ }
4016
+ if (projectTarget instanceof HTMLElement) {
4017
+ event.preventDefault();
4018
+ withPrimaryAction("switch-project", "Switching project", () => switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || ""));
4019
+ return;
4020
+ }
2521
4021
  if (gitHistoryRevealTarget instanceof HTMLElement) {
2522
4022
  event.preventDefault();
2523
4023
  event.stopImmediatePropagation();
@@ -2567,7 +4067,7 @@
2567
4067
  return;
2568
4068
  }
2569
4069
  if (healthTarget instanceof HTMLElement) {
2570
- showHealth().catch((error) => setMeta(error.message));
4070
+ withPrimaryAction("health", "Checking health", showHealth);
2571
4071
  return;
2572
4072
  }
2573
4073
  if (filterTarget instanceof HTMLElement) {
@@ -2577,14 +4077,11 @@
2577
4077
  }
2578
4078
  const path = target instanceof HTMLElement ? target.getAttribute("data-viewer-doc-path") : "";
2579
4079
  if (path) {
2580
- showDocumentByPath(path).catch((error) => setMeta(error.message));
4080
+ withPrimaryAction("read-document", "Loading document", () => showDocumentByPath(path));
2581
4081
  }
2582
4082
  });
2583
4083
  document.getElementById("viewer-document-close")?.addEventListener("click", () => {
2584
- const panel = documentPanel();
2585
- if (panel) {
2586
- panel.hidden = true;
2587
- }
4084
+ withPrimaryAction("close-document", "Closing preview", closeDocumentPanel);
2588
4085
  });
2589
4086
  startAutoRefresh();
2590
4087
  });