@grifhinz/logics-manager 2.7.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.
@@ -21,6 +21,7 @@
21
21
  const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
22
22
  const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
23
23
  const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
24
+ const versionLink = () => document.getElementById("viewer-version-link");
24
25
  const activityClearControl = () => document.getElementById("activity-clear");
25
26
  const activityStorageLimit = 80;
26
27
  const gitHistoryPageSize = 10;
@@ -54,8 +55,27 @@
54
55
  let focusApplied = false;
55
56
  let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
56
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
+ };
57
70
  let connectionState = "connected";
58
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;
59
79
 
60
80
  function readStoredState() {
61
81
  try {
@@ -78,6 +98,181 @@
78
98
  return nextState;
79
99
  }
80
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
+
81
276
  function hydrateViewerFilterState() {
82
277
  const storedState = readStoredState();
83
278
  viewerFilterState = sanitizeViewerFilterState(storedState?.viewerFilterState);
@@ -476,6 +671,18 @@
476
671
  }
477
672
  }
478
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
+
479
686
  async function openRepositoryFolder() {
480
687
  if (!latestRepository.root) {
481
688
  setMeta("Repository folder is unavailable.");
@@ -614,6 +821,7 @@
614
821
  }
615
822
  const data = await response.json();
616
823
  if (response.ok && data.ok) {
824
+ latestCiStatusSignature = runtimeStatusSignature(data.payload);
617
825
  updateMainCiBadge(data.payload);
618
826
  }
619
827
  } catch {
@@ -644,13 +852,22 @@
644
852
  return cdxProviders(status).reduce((total, provider) => total + Math.max(0, Number(provider.active || 0)), 0);
645
853
  }
646
854
 
647
- 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) {
648
863
  const button = document.getElementById("viewer-cdx");
649
864
  if (!(button instanceof HTMLElement)) {
650
865
  return;
651
866
  }
652
867
  button.querySelector("[data-viewer-cdx-badge]")?.remove();
653
- const activeCount = activeCdxAssistantCountFromPayload(payload);
868
+ const activeSessions = activeCdxAssistantCountFromPayload(payload);
869
+ const activeRuns = activeCdxRunCountFromPayload(runsPayload);
870
+ const activeCount = activeSessions + activeRuns;
654
871
  if (activeCount <= 0) {
655
872
  button.title = isCapabilityAvailable("cdx")
656
873
  ? "Show CDX status"
@@ -658,9 +875,17 @@
658
875
  return;
659
876
  }
660
877
  const label = activeCount > 9 ? "9+" : String(activeCount);
661
- 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(" · ");
662
886
  button.title = `Show CDX status · ${title}`;
663
- 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>`);
664
889
  }
665
890
 
666
891
  async function refreshCdxBadgeCounters() {
@@ -669,14 +894,23 @@
669
894
  return;
670
895
  }
671
896
  try {
672
- const response = await fetch("/api/cdx-status");
673
- 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) {
674
902
  updateMainCdxBadge(null);
675
903
  return;
676
904
  }
677
- const data = await response.json();
678
- if (response.ok && data.ok) {
679
- 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);
680
914
  }
681
915
  } catch {
682
916
  updateMainCdxBadge(null);
@@ -700,6 +934,7 @@
700
934
  const response = await fetch("/api/git-status");
701
935
  const data = await response.json();
702
936
  if (response.ok && data.ok && data.payload?.state === "ok") {
937
+ latestGitStatusSignature = gitStatusSignature(data.payload);
703
938
  setGitBadgeCountsFromPayload(data.payload);
704
939
  }
705
940
  } catch {
@@ -825,6 +1060,7 @@
825
1060
  }
826
1061
 
827
1062
  function setDocument(titleText, html) {
1063
+ cdxCloseTarget = null;
828
1064
  const panel = documentPanel();
829
1065
  const title = documentTitle();
830
1066
  const content = documentContent();
@@ -843,6 +1079,35 @@
843
1079
  renderMermaidDiagrams();
844
1080
  }
845
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
+
846
1111
  function showMermaidFallback(message) {
847
1112
  document.querySelectorAll(".markdown-preview__mermaid-fallback").forEach((node) => {
848
1113
  if (!(node instanceof HTMLElement)) {
@@ -949,6 +1214,15 @@
949
1214
 
950
1215
  function postToApp(payload, options = {}) {
951
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;
952
1226
  latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
953
1227
  if (!autoRefreshIntervalTouched) {
954
1228
  autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
@@ -965,12 +1239,14 @@
965
1239
  setMeta(`${rootName} · ${payload.items.length} docs · refreshed ${new Date().toLocaleTimeString()}`);
966
1240
  }
967
1241
  scheduleNextAutoRefresh();
1242
+ updateVersionLink(payload.updateInfo);
968
1243
  renderUpdateNotice(payload.updateInfo);
969
1244
  refreshCiBadgeCounters();
970
1245
  refreshCdxBadgeCounters();
971
1246
  updateFilterSummary();
972
1247
  applyLocalViewerChrome();
973
1248
  bindRefreshMenuControls();
1249
+ return true;
974
1250
  }
975
1251
 
976
1252
  function renderUpdateNotice(updateInfo) {
@@ -1007,11 +1283,11 @@
1007
1283
  if (!response.ok || !data.ok) {
1008
1284
  throw new Error(data.error || "Unable to load viewer data.");
1009
1285
  }
1010
- postToApp(data.payload, { silent: Boolean(options.silent) });
1286
+ const changed = postToApp(data.payload, { silent: Boolean(options.silent), force: Boolean(options.force) });
1011
1287
  if (method !== "POST") {
1012
1288
  await refreshGitBadgeCounters();
1013
1289
  }
1014
- return true;
1290
+ return changed;
1015
1291
  } catch (error) {
1016
1292
  markConnectionDisconnected(error);
1017
1293
  throw error;
@@ -1038,6 +1314,12 @@
1038
1314
  return Boolean(panel && !panel.hidden && title && title.textContent === "CDX runs");
1039
1315
  }
1040
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
+
1041
1323
  function isCiStatusOpen() {
1042
1324
  const panel = documentPanel();
1043
1325
  const title = documentTitle();
@@ -1045,18 +1327,23 @@
1045
1327
  }
1046
1328
 
1047
1329
  async function refreshViewer(method = "POST", options = {}) {
1048
- await loadItems(method, options);
1330
+ const changed = await loadItems(method, options);
1049
1331
  if (isGitStatusOpen()) {
1050
- 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) });
1051
1333
  } else if (isCiStatusOpen()) {
1052
- await showCiStatus({ silent: Boolean(options.silent) });
1334
+ await showCiStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1053
1335
  } else if (isCdxStatusOpen()) {
1054
- await showCdxStatus({ silent: Boolean(options.silent) });
1336
+ await showCdxStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1055
1337
  } else if (isCdxRunsOpen()) {
1056
- await showCdxRuns({ silent: Boolean(options.silent) });
1338
+ if (changed || options.force) {
1339
+ await showCdxRuns({ silent: Boolean(options.silent) });
1340
+ }
1057
1341
  } else if (method === "POST") {
1058
1342
  await refreshGitBadgeCounters();
1059
1343
  }
1344
+ if (!changed && !options.silent && !options.force) {
1345
+ setMeta(`Checked just now · no viewer changes (${new Date().toLocaleTimeString()})`);
1346
+ }
1060
1347
  }
1061
1348
 
1062
1349
  function autoRefreshItems() {
@@ -1347,6 +1634,31 @@
1347
1634
  `).join("");
1348
1635
  }
1349
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
+
1350
1662
  function renderInsightBars(entries, total) {
1351
1663
  const denominator = Math.max(1, Number(total) || 0);
1352
1664
  if (!entries.length) {
@@ -1770,15 +2082,41 @@
1770
2082
  return asArray(status?.rows);
1771
2083
  }
1772
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
+
1773
2099
  function cdxProviders(status) {
1774
- const explicitProviders = pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1775
- if (explicitProviders.length) {
1776
- return explicitProviders;
2100
+ const rows = cdxRows(status);
2101
+ if (!rows.length) {
2102
+ return pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1777
2103
  }
1778
2104
  const grouped = new Map();
1779
- cdxRows(status).forEach((row) => {
2105
+ rows.forEach((row) => {
1780
2106
  const provider = String(row.provider || "unknown");
1781
- 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
+ };
1782
2120
  current.sessions += 1;
1783
2121
  if (row.enabled) {
1784
2122
  current.enabled += 1;
@@ -1789,15 +2127,31 @@
1789
2127
  if (String(row.auth_status || "").toLowerCase() === "authenticated") {
1790
2128
  current.authenticated += 1;
1791
2129
  }
1792
- if (typeof row.available_pct === "number") {
1793
- current.lowest_available_pct = current.lowest_available_pct === null
1794
- ? row.available_pct
1795
- : 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);
1796
2140
  }
1797
2141
  current.state = current.active > 0 ? "active" : current.enabled > 0 ? "enabled" : "disabled";
1798
2142
  grouped.set(provider, current);
1799
2143
  });
1800
- 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
+ });
1801
2155
  }
1802
2156
 
1803
2157
  function cdxSessions(status) {
@@ -1837,8 +2191,25 @@
1837
2191
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1838
2192
  }
1839
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
+
1840
2210
  function cdxLabel(value) {
1841
2211
  return String(value || "")
2212
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
1842
2213
  .replace(/[_-]+/g, " ")
1843
2214
  .replace(/\b\w/g, (letter) => letter.toUpperCase());
1844
2215
  }
@@ -1848,7 +2219,7 @@
1848
2219
  if (["ready", "ok", "active", "enabled", "authenticated"].some((entry) => state.includes(entry))) {
1849
2220
  return "ok";
1850
2221
  }
1851
- 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))) {
1852
2223
  return "warn";
1853
2224
  }
1854
2225
  if (["error", "failed", "disabled", "unavailable", "unauthenticated"].some((entry) => state.includes(entry))) {
@@ -1981,6 +2352,10 @@
1981
2352
  return `<span class="viewer-cdx__badge viewer-cdx__badge--${cdxStateClass(label)}">${escapeHtml(cdxLabel(label))}</span>`;
1982
2353
  }
1983
2354
 
2355
+ function cdxRunStatusDetail(run) {
2356
+ return "";
2357
+ }
2358
+
1984
2359
  function cdxDetailEntries(item, excludedKeys) {
1985
2360
  return objectEntries(item)
1986
2361
  .filter(([key, value]) => !excludedKeys.includes(key) && value !== undefined && value !== null && value !== "")
@@ -2106,10 +2481,228 @@
2106
2481
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
2107
2482
  }
2108
2483
 
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
+ }
2545
+ return `
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>
2550
+ `;
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 || {};
2561
+ const sessions = cdxSessions(status);
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
+
2109
2701
  function renderCdxModeSwitcher(active) {
2110
2702
  return `
2111
2703
  <div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
2112
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>
2113
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>
2114
2707
  </div>
2115
2708
  `;
@@ -2163,10 +2756,6 @@
2163
2756
  <h2 class="viewer-cdx__heading">Sessions</h2>
2164
2757
  ${renderCdxSessionTable(sessions, "No sessions reported.")}
2165
2758
  </section>
2166
- <section class="viewer-cdx__section">
2167
- <h2 class="viewer-cdx__heading">Providers</h2>
2168
- <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
2169
- </section>
2170
2759
  </div>
2171
2760
  <div class="viewer-cdx__stack">
2172
2761
  <section class="viewer-cdx__section">
@@ -2177,6 +2766,10 @@
2177
2766
  <h2 class="viewer-cdx__heading">Safe next commands</h2>
2178
2767
  <ul class="viewer-cdx__commands">${commandRows || '<li class="viewer-cdx__empty">No suggested commands reported.</li>'}</ul>
2179
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>
2180
2773
  </div>
2181
2774
  </div>
2182
2775
  </div>
@@ -2193,21 +2786,33 @@
2193
2786
  `;
2194
2787
  }
2195
2788
  const runs = Array.isArray(payload.runs) ? payload.runs : [];
2196
- const rows = runs.map((run) => `
2197
- <tr>
2198
- <td><code>${escapeHtml(run.run_id || "-")}</code></td>
2199
- <td>${renderCdxBadge(run.status || "unknown")}</td>
2200
- <td>${escapeHtml(run.kind || "assistant")}</td>
2201
- <td>${escapeHtml(run.session || "-")}</td>
2202
- <td>${escapeHtml(run.cwd || "-")}</td>
2203
- <td><button class="viewer-cdx__mode" type="button" data-viewer-cdx-report="${escapeHtml(run.run_id || "")}">Report</button></td>
2204
- </tr>
2205
- `).join("");
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("");
2206
2811
  return `
2207
2812
  <div class="viewer-cdx">
2208
2813
  ${renderCdxModeSwitcher("runs")}
2209
2814
  <section class="viewer-cdx__section">
2210
- <div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runs.length)} reported</span></div>
2815
+ <div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runsSummary)}</span></div>
2211
2816
  <div class="viewer-cdx__table-wrap">
2212
2817
  <table class="viewer-cdx__table">
2213
2818
  <thead><tr><th>RUN</th><th>STATUS</th><th>KIND</th><th>SESSION</th><th>CWD</th><th>REPORT</th></tr></thead>
@@ -2219,6 +2824,188 @@
2219
2824
  `;
2220
2825
  }
2221
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
+
2222
3009
  function renderCdxReport(payload) {
2223
3010
  if (!payload || payload.state !== "ok" || !payload.report) {
2224
3011
  return `
@@ -2231,24 +3018,49 @@
2231
3018
  const report = payload.report || {};
2232
3019
  const run = report.run || {};
2233
3020
  const taskReport = report.task_report || {};
3021
+ const runError = report.error || run.error || {};
3022
+ const artifacts = report.artifacts || run.artifacts || {};
2234
3023
  const findings = Array.isArray(taskReport.findings) ? taskReport.findings : [];
3024
+ const missionOutput = cdxReportMissionOutput(report, run, taskReport);
2235
3025
  const findingRows = findings.map((finding, index) => {
2236
3026
  const location = [finding.path || finding.file || "", finding.line || ""].filter(Boolean).join(":") || "-";
2237
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>`;
2238
3028
  }).join("");
2239
- const canCreate = taskReport.kind === "code-review";
3029
+ const canCreate = cdxReportCanCreateRequest(taskReport, missionOutput);
2240
3030
  return `
2241
3031
  <div class="viewer-cdx">
2242
3032
  ${renderCdxModeSwitcher("runs")}
2243
3033
  <section class="viewer-cdx__section">
2244
- <div class="viewer-ci__heading"><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
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
+ ])}
2245
3044
  <ul class="viewer-cdx__list">
2246
3045
  <li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.run_id || taskReport.run_id || "-")}</strong></li>
2247
3046
  <li class="viewer-cdx__row"><span>Kind</span><strong>${escapeHtml(taskReport.kind || run.kind || "assistant")}</strong></li>
2248
- <li class="viewer-cdx__row"><span>Summary</span><strong>${escapeHtml(taskReport.summary || "No summary reported.")}</strong></li>
3047
+ ${renderCdxDetailRow("Summary", taskReport.summary || "No summary reported.")}
2249
3048
  </ul>
2250
3049
  ${canCreate ? `<button class="btn" type="button" data-viewer-cdx-create-request="${escapeHtml(run.run_id || taskReport.run_id || "")}">Create Logics request</button>` : ""}
2251
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
+ ` : ""}
2252
3064
  <section class="viewer-cdx__section">
2253
3065
  <div class="viewer-ci__heading"><h2>Findings</h2><span>${escapeHtml(findings.length)} reported</span></div>
2254
3066
  <ul class="viewer-cdx__list">${findingRows || '<li class="viewer-cdx__empty">No structured findings reported.</li>'}</ul>
@@ -2285,11 +3097,130 @@
2285
3097
  if (!response.ok || !data.ok) {
2286
3098
  throw new Error(data.error || "Unable to load CDX status.");
2287
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;
2288
3109
  updateMainCdxBadge(data.payload);
2289
3110
  setDocument("CDX status", renderCdxStatus(data.payload));
2290
3111
  setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
2291
3112
  }
2292
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
+
2293
3224
  async function showCdxRuns(options = {}) {
2294
3225
  if (!isCapabilityAvailable("cdx")) {
2295
3226
  const message = capabilityMessage("cdx", "CDX is not available for this project.");
@@ -2325,9 +3256,30 @@
2325
3256
  throw new Error(data.error || "Unable to load CDX report.");
2326
3257
  }
2327
3258
  setDocument("CDX run report", renderCdxReport(data.payload));
3259
+ cdxCloseTarget = { type: "cdx-runs" };
2328
3260
  setMeta("CDX report loaded.");
2329
3261
  }
2330
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
+
2331
3283
  async function createRequestFromCdxReport(runId) {
2332
3284
  if (!runId) {
2333
3285
  return;
@@ -2370,11 +3322,26 @@
2370
3322
  const run = payload.run && typeof payload.run === "object" ? payload.run : null;
2371
3323
  const jobs = Array.isArray(payload.jobs) ? payload.jobs : [];
2372
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";
2373
3340
  const cards = renderMetricCards([
2374
3341
  ["State", ciBadgeLabel(state)],
2375
3342
  ["Branch", run?.branch || payload.branch || "Unknown"],
2376
3343
  ["Commit", (run?.headSha || payload.headSha || "").slice(0, 7) || "Unknown"],
2377
- ["Match", run?.matchSource === "head" ? "Current HEAD" : "Latest branch run"]
3344
+ ["Match", matchLabel]
2378
3345
  ]);
2379
3346
  const runUrl = run?.htmlUrl ? `<a class="viewer-ci__link" href="${escapeHtml(run.htmlUrl)}" target="_blank" rel="noreferrer">Open in GitHub</a>` : "";
2380
3347
  const runRows = run ? [
@@ -2444,6 +3411,15 @@
2444
3411
  if (!response.ok || !data.ok) {
2445
3412
  throw new Error(data.error || "Unable to load CI status.");
2446
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;
2447
3423
  updateMainCiBadge(data.payload);
2448
3424
  setDocument("CI status", renderCiStatus(data.payload));
2449
3425
  setMeta(options.silent ? "CI status refreshed." : "CI status loaded.");
@@ -2463,17 +3439,20 @@
2463
3439
  const deletedCount = Number(counts.deleted || 0);
2464
3440
  const renamedCount = Number(counts.renamed || 0);
2465
3441
  const untrackedCount = Number(counts.untracked || 0);
2466
- const summary = [
2467
- ["Branch", payload.branch || "HEAD"],
2468
- ["Tracking", payload.tracking || "None"],
2469
- ["Ahead", payload.ahead || 0],
2470
- ["Behind", payload.behind || 0],
2471
- ["State", payload.clean ? "Clean" : "Dirty"],
2472
- ["Staged", stagedCount],
2473
- ["Worktree", modifiedCount + deletedCount + renamedCount],
2474
- ["Untracked", untrackedCount]
2475
- ];
2476
- 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("");
2477
3456
  const groupDefs = [
2478
3457
  ["staged", "Staged", "staged"],
2479
3458
  ["modified", "Modified", "worktree"],
@@ -2486,7 +3465,7 @@
2486
3465
  ["staged", "Staged", stagedCount],
2487
3466
  ["worktree", "Worktree", modifiedCount + deletedCount + renamedCount],
2488
3467
  ["untracked", "Untracked", untrackedCount],
2489
- ["history", "History", Array.isArray(payload.recentCommits) ? payload.recentCommits.length : (payload.latestCommit ? 1 : 0)],
3468
+ ["history", "History", formatGitHistoryCount(payload)],
2490
3469
  ["remote", "Remote", payload.tracking ? 1 : 0]
2491
3470
  ];
2492
3471
  const domains = domainDefs.map(([key, label, count], index) => `
@@ -2528,6 +3507,7 @@
2528
3507
  const untrackedSections = renderFileSections(["untracked"]);
2529
3508
  const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
2530
3509
  const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
3510
+ const historyCount = formatGitHistoryCount(payload);
2531
3511
  const renderGitHistoryReveal = (hiddenCount) => {
2532
3512
  if (hiddenCount <= 0) {
2533
3513
  return "";
@@ -2565,7 +3545,7 @@
2565
3545
  return `
2566
3546
  <div class="viewer-git">
2567
3547
  <div class="viewer-git__summary">${cards}</div>
2568
- <div class="viewer-git__workspace">
3548
+ <div class="viewer-git__workspace has-diff-detail">
2569
3549
  <nav class="viewer-git__domains" aria-label="Git domains">${domains}</nav>
2570
3550
  <div class="viewer-git__content" aria-label="Git domain content">
2571
3551
  <section class="viewer-git__panel" data-viewer-git-panel="changes">
@@ -2586,7 +3566,7 @@
2586
3566
  ${untrackedSections || '<p class="viewer-git__state">No untracked files.</p>'}
2587
3567
  </section>
2588
3568
  <section class="viewer-git__panel" data-viewer-git-panel="history" hidden>
2589
- <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>
2590
3570
  ${history}
2591
3571
  </section>
2592
3572
  <section class="viewer-git__panel" data-viewer-git-panel="remote" hidden>
@@ -2594,7 +3574,7 @@
2594
3574
  ${remote}
2595
3575
  </section>
2596
3576
  </div>
2597
- <section class="viewer-git__detail" aria-label="Git diff">
3577
+ <section class="viewer-git__detail" aria-label="Git diff" data-viewer-git-detail>
2598
3578
  <div class="viewer-git__detail-title">Diff preview</div>
2599
3579
  <div class="viewer-git__diff" data-viewer-git-diff>Select a changed file to preview its diff.</div>
2600
3580
  </section>
@@ -2603,6 +3583,11 @@
2603
3583
  `;
2604
3584
  }
2605
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
+
2606
3591
  function setActiveGitFile(button) {
2607
3592
  document.querySelectorAll("[data-viewer-git-file]").forEach((node) => {
2608
3593
  if (node instanceof HTMLElement) {
@@ -2636,12 +3621,16 @@
2636
3621
 
2637
3622
  async function loadGitDiff(path, cached, button = null) {
2638
3623
  const diffPanel = document.querySelector("[data-viewer-git-diff]");
3624
+ const detailTitle = document.querySelector("[data-viewer-git-detail] .viewer-git__detail-title");
2639
3625
  if (!(diffPanel instanceof HTMLElement) || !path) {
2640
3626
  return;
2641
3627
  }
2642
3628
  if (button instanceof HTMLElement) {
2643
3629
  setActiveGitFile(button);
2644
3630
  }
3631
+ if (detailTitle instanceof HTMLElement) {
3632
+ detailTitle.textContent = "Diff preview";
3633
+ }
2645
3634
  diffPanel.textContent = "Loading diff...";
2646
3635
  const params = new URLSearchParams({ path });
2647
3636
  if (cached) {
@@ -2654,12 +3643,38 @@
2654
3643
  diffPanel.textContent = payload.message || data.error || "Unable to load diff.";
2655
3644
  return;
2656
3645
  }
2657
- 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
+ }
2658
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>`;
2659
3652
  }
2660
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
+
2661
3674
  function applyGitDomain(domain) {
2662
3675
  const selected = domain || "changes";
3676
+ const diffDomains = new Set(["changes", "staged", "worktree", "untracked"]);
3677
+ const showDiffDetail = diffDomains.has(selected);
2663
3678
  document.querySelectorAll(".viewer-git__domain[data-viewer-git-domain]").forEach((node) => {
2664
3679
  if (node instanceof HTMLElement) {
2665
3680
  const active = node.getAttribute("data-viewer-git-domain") === selected;
@@ -2672,6 +3687,16 @@
2672
3687
  node.hidden = node.getAttribute("data-viewer-git-panel") !== selected;
2673
3688
  }
2674
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
+ });
2675
3700
  }
2676
3701
 
2677
3702
  function currentGitViewState() {
@@ -2721,6 +3746,16 @@
2721
3746
  if (!response.ok || !data.ok) {
2722
3747
  throw new Error(data.error || "Unable to load Git status.");
2723
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;
2724
3759
  setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
2725
3760
  updateMainGitBadges();
2726
3761
  setDocument("Git status", renderGitStatus(data.payload));
@@ -2744,7 +3779,7 @@
2744
3779
  return;
2745
3780
  }
2746
3781
  if (message.type === "refresh") {
2747
- refreshViewer("POST").catch((error) => setMeta(error.message));
3782
+ refreshViewer("POST", { force: Boolean(message.force) }).catch((error) => setMeta(error.message));
2748
3783
  return;
2749
3784
  }
2750
3785
  if (message.type === "bootstrap-logics") {
@@ -2782,7 +3817,7 @@
2782
3817
  [document.getElementById("viewer-insights")].forEach((button) => {
2783
3818
  button?.addEventListener("click", () => {
2784
3819
  setRefreshMenuOpen(false);
2785
- showCorpusInsights().catch((error) => setMeta(error.message));
3820
+ withPrimaryAction("insights", "Loading insights", showCorpusInsights);
2786
3821
  });
2787
3822
  });
2788
3823
  const autoControl = autoRefreshControl();
@@ -2825,30 +3860,30 @@
2825
3860
  if (!(element instanceof HTMLElement)) {
2826
3861
  return;
2827
3862
  }
2828
- element.addEventListener("click", () => {
3863
+ element.addEventListener("click", (event) => {
2829
3864
  setRefreshMenuOpen(false);
2830
- refreshViewer("POST").catch((error) => setMeta(error.message));
3865
+ withPrimaryAction("refresh", "Refreshing", () => refreshViewer("POST", { force: Boolean(event.shiftKey) }));
2831
3866
  });
2832
3867
  });
2833
3868
  document.getElementById("viewer-health")?.addEventListener("click", () => {
2834
3869
  setRefreshMenuOpen(false);
2835
- showHealth().catch((error) => setMeta(error.message));
3870
+ withPrimaryAction("health", "Checking health", showHealth);
2836
3871
  });
2837
3872
  document.getElementById("viewer-git")?.addEventListener("click", () => {
2838
- showGitStatus().catch((error) => setMeta(error.message));
3873
+ withPrimaryAction("git", "Checking Git status", showGitStatus);
2839
3874
  });
2840
3875
  ciButton()?.addEventListener("click", () => {
2841
- showCiStatus().catch((error) => setMeta(error.message));
3876
+ withPrimaryAction("ci", "Checking CI status", showCiStatus);
2842
3877
  });
2843
3878
  document.getElementById("viewer-cdx")?.addEventListener("click", () => {
2844
- showCdxStatus().catch((error) => setMeta(error.message));
3879
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
2845
3880
  });
2846
3881
  repoPill()?.addEventListener("click", () => {
2847
3882
  const menu = projectMenu();
2848
3883
  setProjectMenuOpen(Boolean(menu?.hidden));
2849
3884
  });
2850
3885
  repoFolderButton()?.addEventListener("click", () => {
2851
- openRepositoryFolder().catch((error) => setMeta(error.message));
3886
+ withPrimaryAction("open-repo-folder", "Opening repository folder", openRepositoryFolder);
2852
3887
  });
2853
3888
  activityClearControl()?.addEventListener("click", () => {
2854
3889
  clearActivityHistory();
@@ -2873,9 +3908,29 @@
2873
3908
  const editButton = editDocumentButton();
2874
3909
  if (editButton instanceof HTMLElement) {
2875
3910
  editButton.addEventListener("click", () => {
2876
- editDocument(selectedItem()).catch((error) => setMeta(error.message));
3911
+ withPrimaryAction("edit-document", "Opening document", () => editDocument(selectedItem()));
2877
3912
  });
2878
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
+ });
2879
3934
  document.addEventListener("click", (event) => {
2880
3935
  window.setTimeout(() => applyLocalViewerChrome(), 0);
2881
3936
  const target = event.target instanceof Element ? event.target.closest("[data-viewer-doc-path]") : null;
@@ -2888,22 +3943,68 @@
2888
3943
  const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
2889
3944
  const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
2890
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;
2891
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;
2892
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
+ }
2893
3988
  if (cdxReportTarget instanceof HTMLElement) {
2894
- showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || "").catch((error) => setMeta(error.message));
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") || ""));
2895
3994
  return;
2896
3995
  }
2897
3996
  if (cdxCreateRequestTarget instanceof HTMLElement) {
2898
- createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || "").catch((error) => setMeta(error.message));
3997
+ withPrimaryAction("cdx-create-request", "Creating Logics request", () => createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || ""));
2899
3998
  return;
2900
3999
  }
2901
4000
  if (cdxModeTarget instanceof HTMLElement) {
2902
4001
  const mode = cdxModeTarget.getAttribute("data-viewer-cdx-mode") || "status";
2903
4002
  if (mode === "runs") {
2904
- showCdxRuns().catch((error) => setMeta(error.message));
4003
+ withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
4004
+ } else if (mode === "missions") {
4005
+ withPrimaryAction("cdx-missions", "Loading CDX missions", showCdxMissions);
2905
4006
  } else {
2906
- showCdxStatus().catch((error) => setMeta(error.message));
4007
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
2907
4008
  }
2908
4009
  return;
2909
4010
  }
@@ -2914,7 +4015,7 @@
2914
4015
  }
2915
4016
  if (projectTarget instanceof HTMLElement) {
2916
4017
  event.preventDefault();
2917
- switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || "").catch((error) => setMeta(error.message));
4018
+ withPrimaryAction("switch-project", "Switching project", () => switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || ""));
2918
4019
  return;
2919
4020
  }
2920
4021
  if (gitHistoryRevealTarget instanceof HTMLElement) {
@@ -2966,7 +4067,7 @@
2966
4067
  return;
2967
4068
  }
2968
4069
  if (healthTarget instanceof HTMLElement) {
2969
- showHealth().catch((error) => setMeta(error.message));
4070
+ withPrimaryAction("health", "Checking health", showHealth);
2970
4071
  return;
2971
4072
  }
2972
4073
  if (filterTarget instanceof HTMLElement) {
@@ -2976,14 +4077,11 @@
2976
4077
  }
2977
4078
  const path = target instanceof HTMLElement ? target.getAttribute("data-viewer-doc-path") : "";
2978
4079
  if (path) {
2979
- showDocumentByPath(path).catch((error) => setMeta(error.message));
4080
+ withPrimaryAction("read-document", "Loading document", () => showDocumentByPath(path));
2980
4081
  }
2981
4082
  });
2982
4083
  document.getElementById("viewer-document-close")?.addEventListener("click", () => {
2983
- const panel = documentPanel();
2984
- if (panel) {
2985
- panel.hidden = true;
2986
- }
4084
+ withPrimaryAction("close-document", "Closing preview", closeDocumentPanel);
2987
4085
  });
2988
4086
  startAutoRefresh();
2989
4087
  });