@grifhinz/logics-manager 2.7.0 → 2.8.1

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.
@@ -1,5 +1,7 @@
1
1
  (() => {
2
2
  const stateKey = "logics.localViewer.state";
3
+ const preferenceKey = "logics.localViewer.preferences.v1";
4
+ const preferenceVersion = 1;
3
5
  const meta = () => document.getElementById("viewer-meta");
4
6
  const documentPanel = () => document.getElementById("viewer-document");
5
7
  const documentTitle = () => document.getElementById("viewer-document-title");
@@ -16,11 +18,13 @@
16
18
  const projectMenu = () => document.getElementById("viewer-project-menu");
17
19
  const repoGithubLink = () => document.getElementById("viewer-repo-github");
18
20
  const repoFolderButton = () => document.getElementById("viewer-repo-folder");
21
+ const workspaceButton = () => document.getElementById("viewer-workspace");
19
22
  const ciButton = () => document.getElementById("viewer-ci");
20
23
  const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
21
24
  const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
22
25
  const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
23
26
  const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
27
+ const versionLink = () => document.getElementById("viewer-version-link");
24
28
  const activityClearControl = () => document.getElementById("activity-clear");
25
29
  const activityStorageLimit = 80;
26
30
  const gitHistoryPageSize = 10;
@@ -54,8 +58,44 @@
54
58
  let focusApplied = false;
55
59
  let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
56
60
  let latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
61
+ let latestUpdateInfo = {};
62
+ let latestCdxMissionState = {
63
+ missionId: "full-audit",
64
+ sessionId: "",
65
+ strengthId: "standard",
66
+ missionInputs: {},
67
+ catalog: null,
68
+ statusPayload: null,
69
+ planPayload: null,
70
+ runPayload: null,
71
+ applyPayload: null
72
+ };
57
73
  let connectionState = "connected";
58
74
  let lastSuccessfulSyncAt = 0;
75
+ let latestViewerStateSignature = "";
76
+ let latestGitStatusSignature = "";
77
+ let latestCdxStatusSignature = "";
78
+ let latestCdxStatusPayload = null;
79
+ let latestCiStatusSignature = "";
80
+ let primaryActionBusyKey = "";
81
+ let cdxMissionBusyKey = "";
82
+ let cdxCloseTarget = null;
83
+ let viewerPreferences = readViewerPreferences();
84
+ let autoRefreshIntervalForcedByLaunch = false;
85
+ const cdxStatusColumns = [
86
+ { id: "session", label: "SESSION" },
87
+ { id: "provider", label: "PROV." },
88
+ { id: "status", label: "STATUS" },
89
+ { id: "auth", label: "AUTH" },
90
+ { id: "ok", label: "OK" },
91
+ { id: "remaining5h", label: "5H" },
92
+ { id: "remainingWeek", label: "WEEK" },
93
+ { id: "block", label: "BLOCK", defaultVisible: false },
94
+ { id: "credits", label: "CR", defaultVisible: false },
95
+ { id: "reset5h", label: "RESET 5H" },
96
+ { id: "resetWeek", label: "RESET WEEK" },
97
+ { id: "updated", label: "UPDATED" }
98
+ ];
59
99
 
60
100
  function readStoredState() {
61
101
  try {
@@ -65,6 +105,83 @@
65
105
  }
66
106
  }
67
107
 
108
+ function readViewerPreferences() {
109
+ try {
110
+ const value = JSON.parse(window.localStorage.getItem(preferenceKey) || "null");
111
+ if (!value || typeof value !== "object" || value.version !== preferenceVersion) {
112
+ return { version: preferenceVersion };
113
+ }
114
+ return { ...value, version: preferenceVersion };
115
+ } catch {
116
+ return { version: preferenceVersion };
117
+ }
118
+ }
119
+
120
+ function writeViewerPreferences(nextPreferences) {
121
+ viewerPreferences = { ...nextPreferences, version: preferenceVersion };
122
+ try {
123
+ window.localStorage.setItem(preferenceKey, JSON.stringify(viewerPreferences));
124
+ } catch {
125
+ // Keep the in-memory preference for this session when browser storage is unavailable.
126
+ }
127
+ }
128
+
129
+ function updateViewerPreferences(patch) {
130
+ writeViewerPreferences({ ...viewerPreferences, ...patch });
131
+ }
132
+
133
+ function preferredAutoRefreshIntervalSeconds() {
134
+ const seconds = Number(viewerPreferences.autoRefreshIntervalSeconds);
135
+ return Number.isFinite(seconds) && seconds > 0 ? normalizeAutoRefreshIntervalSeconds(seconds) : null;
136
+ }
137
+
138
+ function cdxColumnVisibilityPreference() {
139
+ const stored = viewerPreferences.cdxStatusColumns;
140
+ const storedVisibility = stored && typeof stored === "object" ? stored.visibility : null;
141
+ const visibility = {};
142
+ cdxStatusColumns.forEach((column) => {
143
+ visibility[column.id] = column.defaultVisible !== false;
144
+ if (storedVisibility && typeof storedVisibility[column.id] === "boolean") {
145
+ visibility[column.id] = storedVisibility[column.id];
146
+ }
147
+ });
148
+ return visibility;
149
+ }
150
+
151
+ function persistCdxColumnVisibility(columnId, visible) {
152
+ const current = cdxColumnVisibilityPreference();
153
+ if (!cdxStatusColumns.some((column) => column.id === columnId)) {
154
+ return;
155
+ }
156
+ updateViewerPreferences({
157
+ cdxStatusColumns: {
158
+ visibility: { ...current, [columnId]: Boolean(visible) }
159
+ }
160
+ });
161
+ }
162
+
163
+ function cdxProviderFilterPreference() {
164
+ const stored = viewerPreferences.cdxStatusProviders;
165
+ if (!stored || typeof stored !== "object" || stored.mode !== "subset") {
166
+ return { mode: "all", selected: [] };
167
+ }
168
+ const selected = Array.isArray(stored.selected)
169
+ ? stored.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
170
+ : [];
171
+ return selected.length ? { mode: "subset", selected: Array.from(new Set(selected)) } : { mode: "all", selected: [] };
172
+ }
173
+
174
+ function persistCdxProviderFilter(nextFilter) {
175
+ const selected = Array.isArray(nextFilter?.selected)
176
+ ? nextFilter.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
177
+ : [];
178
+ updateViewerPreferences({
179
+ cdxStatusProviders: selected.length
180
+ ? { mode: "subset", selected: Array.from(new Set(selected)).sort() }
181
+ : { mode: "all", selected: [] }
182
+ });
183
+ }
184
+
68
185
  function sanitizeViewerFilterState(value) {
69
186
  const nextState = { ...defaultFilterState };
70
187
  if (!value || typeof value !== "object") {
@@ -78,6 +195,182 @@
78
195
  return nextState;
79
196
  }
80
197
 
198
+ function stableStringify(value) {
199
+ if (Array.isArray(value)) {
200
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
201
+ }
202
+ if (value && typeof value === "object") {
203
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
204
+ }
205
+ return JSON.stringify(value);
206
+ }
207
+
208
+ function viewerStateSignature(payload) {
209
+ const items = Array.isArray(payload?.items) ? payload.items : [];
210
+ const projects = Array.isArray(payload?.projects) ? payload.projects : [];
211
+ return stableStringify({
212
+ root: payload?.root || "",
213
+ repository: payload?.repository || {},
214
+ capabilities: normalizeCapabilities(payload),
215
+ projects: projects.map((project) => ({
216
+ id: project?.id || "",
217
+ active: Boolean(project?.active),
218
+ available: project?.available !== false,
219
+ hasLogics: project?.hasLogics !== false,
220
+ root: project?.root || ""
221
+ })),
222
+ items: items.map((item) => ({
223
+ id: item?.id || "",
224
+ relPath: item?.relPath || "",
225
+ stage: item?.stage || "",
226
+ status: item?.indicators?.Status || item?.status || "",
227
+ updatedAt: item?.updatedAt || ""
228
+ }))
229
+ });
230
+ }
231
+
232
+ function gitStatusSignature(payload) {
233
+ return stableStringify({
234
+ state: payload?.state || "",
235
+ branch: payload?.branch || "",
236
+ tracking: payload?.tracking || "",
237
+ ahead: Number(payload?.ahead || 0),
238
+ behind: Number(payload?.behind || 0),
239
+ clean: Boolean(payload?.clean),
240
+ counts: payload?.counts || {},
241
+ badgeCounts: payload?.badgeCounts || {},
242
+ latestCommit: payload?.latestCommit || "",
243
+ recentCommitsHasMore: Boolean(payload?.recentCommitsHasMore)
244
+ });
245
+ }
246
+
247
+ function runtimeStatusSignature(payload) {
248
+ return stableStringify(payload || {});
249
+ }
250
+
251
+ function primaryActionControls() {
252
+ return Array.from(document.querySelectorAll([
253
+ "#viewer-insights",
254
+ "#viewer-health",
255
+ "#viewer-workspace",
256
+ "#viewer-git",
257
+ "#viewer-ci",
258
+ "#viewer-cdx",
259
+ "#viewer-repo-folder",
260
+ '[data-action="refresh"]',
261
+ '[data-viewer-action="edit-document"]',
262
+ "[data-viewer-project-id]",
263
+ "[data-viewer-cdx-mode]",
264
+ "[data-viewer-cdx-report]",
265
+ "[data-viewer-cdx-artifact-path]",
266
+ "[data-viewer-cdx-create-request]"
267
+ ].join(","))).filter((node) => node instanceof HTMLElement);
268
+ }
269
+
270
+ function setPrimaryActionBusy(actionKey, label = "") {
271
+ primaryActionBusyKey = actionKey || "";
272
+ document.body?.classList.toggle("viewer-is-busy", Boolean(primaryActionBusyKey));
273
+ document.body?.toggleAttribute("data-viewer-busy", Boolean(primaryActionBusyKey));
274
+ if (primaryActionBusyKey) {
275
+ document.body?.setAttribute("data-viewer-busy-action", primaryActionBusyKey);
276
+ } else {
277
+ document.body?.removeAttribute("data-viewer-busy-action");
278
+ }
279
+ primaryActionControls().forEach((control) => {
280
+ if (!("disabled" in control)) {
281
+ return;
282
+ }
283
+ control.disabled = Boolean(primaryActionBusyKey);
284
+ control.setAttribute("aria-busy", primaryActionBusyKey ? "true" : "false");
285
+ if (primaryActionBusyKey) {
286
+ control.setAttribute("data-viewer-action-busy", control.getAttribute("data-viewer-action-key") === actionKey ? "active" : "blocked");
287
+ } else {
288
+ control.removeAttribute("data-viewer-action-busy");
289
+ }
290
+ });
291
+ if (!primaryActionBusyKey) {
292
+ updateCapabilityControls();
293
+ applyLocalViewerChrome();
294
+ }
295
+ if (primaryActionBusyKey && label) {
296
+ setMeta(`${label}...`);
297
+ }
298
+ }
299
+
300
+ function withPrimaryAction(actionKey, label, action) {
301
+ if (primaryActionBusyKey) {
302
+ setMeta("Another viewer action is still running.");
303
+ return Promise.resolve(false);
304
+ }
305
+ setPrimaryActionBusy(actionKey, label);
306
+ return Promise.resolve()
307
+ .then(action)
308
+ .then(() => true)
309
+ .catch((error) => {
310
+ setMeta(error.message || "Viewer action failed.");
311
+ return false;
312
+ })
313
+ .finally(() => {
314
+ setPrimaryActionBusy("", "");
315
+ });
316
+ }
317
+
318
+ function cdxMissionActionControls() {
319
+ return Array.from(document.querySelectorAll([
320
+ "[data-viewer-cdx-plan]",
321
+ "[data-viewer-cdx-run]",
322
+ "[data-viewer-cdx-apply-plan]",
323
+ "[data-viewer-cdx-mission]"
324
+ ].join(","))).filter((node) => node instanceof HTMLElement);
325
+ }
326
+
327
+ function setCdxMissionBusy(actionKey, label = "") {
328
+ cdxMissionBusyKey = actionKey || "";
329
+ document.body?.toggleAttribute("data-viewer-cdx-mission-busy", Boolean(cdxMissionBusyKey));
330
+ if (cdxMissionBusyKey) {
331
+ document.body?.setAttribute("data-viewer-cdx-mission-busy-action", cdxMissionBusyKey);
332
+ } else {
333
+ document.body?.removeAttribute("data-viewer-cdx-mission-busy-action");
334
+ }
335
+ cdxMissionActionControls().forEach((control) => {
336
+ if (!("disabled" in control)) {
337
+ return;
338
+ }
339
+ control.disabled = Boolean(cdxMissionBusyKey);
340
+ control.setAttribute("aria-busy", cdxMissionBusyKey ? "true" : "false");
341
+ if (cdxMissionBusyKey) {
342
+ control.setAttribute("data-viewer-action-busy", control.getAttribute("data-viewer-action-key") === actionKey ? "active" : "blocked");
343
+ } else {
344
+ control.removeAttribute("data-viewer-action-busy");
345
+ }
346
+ });
347
+ if (!cdxMissionBusyKey) {
348
+ updateCapabilityControls();
349
+ applyLocalViewerChrome();
350
+ }
351
+ if (cdxMissionBusyKey && label) {
352
+ setMeta(`${label}...`);
353
+ }
354
+ }
355
+
356
+ function withCdxMissionAction(actionKey, label, action) {
357
+ if (cdxMissionBusyKey) {
358
+ setMeta("Another CDX mission action is still running.");
359
+ return Promise.resolve(false);
360
+ }
361
+ setCdxMissionBusy(actionKey, label);
362
+ return Promise.resolve()
363
+ .then(action)
364
+ .then(() => true)
365
+ .catch((error) => {
366
+ setMeta(error.message || "CDX mission action failed.");
367
+ return false;
368
+ })
369
+ .finally(() => {
370
+ setCdxMissionBusy("", "");
371
+ });
372
+ }
373
+
81
374
  function hydrateViewerFilterState() {
82
375
  const storedState = readStoredState();
83
376
  viewerFilterState = sanitizeViewerFilterState(storedState?.viewerFilterState);
@@ -257,6 +550,7 @@
257
550
  autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(value) * 1000;
258
551
  if (options.user) {
259
552
  autoRefreshIntervalTouched = true;
553
+ updateViewerPreferences({ autoRefreshIntervalSeconds: Math.round(autoRefreshIntervalMs / 1000) });
260
554
  }
261
555
  updateRefreshIntervalControl();
262
556
  scheduleNextAutoRefresh();
@@ -391,6 +685,7 @@
391
685
  const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
392
686
  return {
393
687
  logics: capabilities.logics || { state: "ready", available: true, message: "" },
688
+ workspace: capabilities.workspace || { state: "ready", available: true, message: "" },
394
689
  git: capabilities.git || { state: "ready", available: true, message: "" },
395
690
  ci: capabilities.ci || { state: "ready", available: true, message: "" },
396
691
  cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
@@ -429,6 +724,16 @@
429
724
  }
430
725
 
431
726
  function updateCapabilityControls() {
727
+ const workspace = workspaceButton();
728
+ if (workspace instanceof HTMLElement) {
729
+ workspace.hidden = !isCapabilityAvailable("workspace");
730
+ if (isCapabilityAvailable("workspace")) {
731
+ setButtonAvailable(workspace, "Show file explorer");
732
+ } else {
733
+ setButtonUnavailable(workspace, capabilityMessage("workspace", "Explorer is not available for this project."));
734
+ }
735
+ }
736
+
432
737
  const gitButton = document.getElementById("viewer-git");
433
738
  if (gitButton instanceof HTMLElement) {
434
739
  gitButton.hidden = !isCapabilityAvailable("git");
@@ -476,6 +781,18 @@
476
781
  }
477
782
  }
478
783
 
784
+ function updateVersionLink(updateInfo = latestUpdateInfo) {
785
+ latestUpdateInfo = updateInfo && typeof updateInfo === "object" ? updateInfo : {};
786
+ const link = versionLink();
787
+ if (!(link instanceof HTMLAnchorElement)) {
788
+ return;
789
+ }
790
+ const currentVersion = String(latestUpdateInfo.currentVersion || "").trim();
791
+ link.textContent = currentVersion ? `v${currentVersion.replace(/^v/i, "")}` : "v0.0.0";
792
+ link.href = latestRepository.githubUrl || "https://github.com/AlexAgo83/logics-manager";
793
+ link.title = "Open Logics Manager on GitHub";
794
+ }
795
+
479
796
  async function openRepositoryFolder() {
480
797
  if (!latestRepository.root) {
481
798
  setMeta("Repository folder is unavailable.");
@@ -614,6 +931,7 @@
614
931
  }
615
932
  const data = await response.json();
616
933
  if (response.ok && data.ok) {
934
+ latestCiStatusSignature = runtimeStatusSignature(data.payload);
617
935
  updateMainCiBadge(data.payload);
618
936
  }
619
937
  } catch {
@@ -644,13 +962,22 @@
644
962
  return cdxProviders(status).reduce((total, provider) => total + Math.max(0, Number(provider.active || 0)), 0);
645
963
  }
646
964
 
647
- function updateMainCdxBadge(payload) {
965
+ function activeCdxRunCountFromPayload(payload) {
966
+ if (!payload || payload.state !== "ok" || !Array.isArray(payload.runs)) {
967
+ return 0;
968
+ }
969
+ return payload.runs.filter((run) => ["running", "starting", "pending"].includes(String(cdxField(run, ["status", "state"], "")).toLowerCase())).length;
970
+ }
971
+
972
+ function updateMainCdxBadge(payload, runsPayload = null) {
648
973
  const button = document.getElementById("viewer-cdx");
649
974
  if (!(button instanceof HTMLElement)) {
650
975
  return;
651
976
  }
652
977
  button.querySelector("[data-viewer-cdx-badge]")?.remove();
653
- const activeCount = activeCdxAssistantCountFromPayload(payload);
978
+ const activeSessions = activeCdxAssistantCountFromPayload(payload);
979
+ const activeRuns = activeCdxRunCountFromPayload(runsPayload);
980
+ const activeCount = activeSessions + activeRuns;
654
981
  if (activeCount <= 0) {
655
982
  button.title = isCapabilityAvailable("cdx")
656
983
  ? "Show CDX status"
@@ -658,9 +985,17 @@
658
985
  return;
659
986
  }
660
987
  const label = activeCount > 9 ? "9+" : String(activeCount);
661
- const title = activeCount === 1 ? "1 active assistant/session" : `${activeCount} active assistants/sessions`;
988
+ const titleParts = [];
989
+ if (activeSessions > 0) {
990
+ titleParts.push(activeSessions === 1 ? "1 active session" : `${activeSessions} active sessions`);
991
+ }
992
+ if (activeRuns > 0) {
993
+ titleParts.push(activeRuns === 1 ? "1 running run" : `${activeRuns} running runs`);
994
+ }
995
+ const title = titleParts.join(" · ");
662
996
  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>`);
997
+ const tone = activeRuns > 0 ? " viewer-cdx-button-badge--runs" : "";
998
+ 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
999
  }
665
1000
 
666
1001
  async function refreshCdxBadgeCounters() {
@@ -669,14 +1004,23 @@
669
1004
  return;
670
1005
  }
671
1006
  try {
672
- const response = await fetch("/api/cdx-status");
673
- if (response.status === 404) {
1007
+ const [statusResponse, runsResponse] = await Promise.all([
1008
+ fetch("/api/cdx-status"),
1009
+ fetch("/api/cdx-runs").catch(() => null)
1010
+ ]);
1011
+ if (statusResponse.status === 404) {
674
1012
  updateMainCdxBadge(null);
675
1013
  return;
676
1014
  }
677
- const data = await response.json();
678
- if (response.ok && data.ok) {
679
- updateMainCdxBadge(data.payload);
1015
+ const data = await statusResponse.json();
1016
+ let runsPayload = null;
1017
+ if (runsResponse && runsResponse.ok) {
1018
+ const runsData = await runsResponse.json();
1019
+ runsPayload = runsData?.ok ? runsData.payload : null;
1020
+ }
1021
+ if (statusResponse.ok && data.ok) {
1022
+ latestCdxStatusSignature = runtimeStatusSignature({ status: data.payload, runs: runsPayload });
1023
+ updateMainCdxBadge(data.payload, runsPayload);
680
1024
  }
681
1025
  } catch {
682
1026
  updateMainCdxBadge(null);
@@ -700,6 +1044,7 @@
700
1044
  const response = await fetch("/api/git-status");
701
1045
  const data = await response.json();
702
1046
  if (response.ok && data.ok && data.payload?.state === "ok") {
1047
+ latestGitStatusSignature = gitStatusSignature(data.payload);
703
1048
  setGitBadgeCountsFromPayload(data.payload);
704
1049
  }
705
1050
  } catch {
@@ -825,6 +1170,7 @@
825
1170
  }
826
1171
 
827
1172
  function setDocument(titleText, html) {
1173
+ cdxCloseTarget = null;
828
1174
  const panel = documentPanel();
829
1175
  const title = documentTitle();
830
1176
  const content = documentContent();
@@ -843,6 +1189,35 @@
843
1189
  renderMermaidDiagrams();
844
1190
  }
845
1191
 
1192
+ function currentDocumentSnapshot(fallbackTitle = "Document") {
1193
+ const title = documentTitle();
1194
+ const content = documentContent();
1195
+ return {
1196
+ title: title?.textContent || fallbackTitle,
1197
+ html: content?.innerHTML || ""
1198
+ };
1199
+ }
1200
+
1201
+ async function closeDocumentPanel() {
1202
+ const target = cdxCloseTarget;
1203
+ cdxCloseTarget = null;
1204
+ if (target?.type === "cdx-report") {
1205
+ setDocument(target.title || "CDX run report", target.html || "");
1206
+ cdxCloseTarget = { type: "cdx-runs" };
1207
+ setMeta("Returned to CDX run report.");
1208
+ return;
1209
+ }
1210
+ if (target?.type === "cdx-runs") {
1211
+ await showCdxRuns({ silent: true });
1212
+ setMeta("Returned to CDX runs.");
1213
+ return;
1214
+ }
1215
+ const panel = documentPanel();
1216
+ if (panel) {
1217
+ panel.hidden = true;
1218
+ }
1219
+ }
1220
+
846
1221
  function showMermaidFallback(message) {
847
1222
  document.querySelectorAll(".markdown-preview__mermaid-fallback").forEach((node) => {
848
1223
  if (!(node instanceof HTMLElement)) {
@@ -949,9 +1324,24 @@
949
1324
 
950
1325
  function postToApp(payload, options = {}) {
951
1326
  markConnectionHealthy({ silent: Boolean(options.silent) });
1327
+ const nextSignature = viewerStateSignature(payload);
1328
+ if (!options.force && latestViewerStateSignature && nextSignature === latestViewerStateSignature) {
1329
+ if (!options.silent) {
1330
+ setMeta(`Checked just now · no viewer changes (${new Date().toLocaleTimeString()})`);
1331
+ }
1332
+ scheduleNextAutoRefresh();
1333
+ return false;
1334
+ }
1335
+ latestViewerStateSignature = nextSignature;
952
1336
  latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
953
1337
  if (!autoRefreshIntervalTouched) {
954
- autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
1338
+ const launchSeconds = Number(payload.autoRefreshIntervalSeconds);
1339
+ const preferredSeconds = preferredAutoRefreshIntervalSeconds();
1340
+ autoRefreshIntervalForcedByLaunch = Boolean(payload.autoRefreshIntervalForced);
1341
+ const nextSeconds = autoRefreshIntervalForcedByLaunch || preferredSeconds === null
1342
+ ? launchSeconds
1343
+ : preferredSeconds;
1344
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(nextSeconds) * 1000;
955
1345
  updateRefreshIntervalControl();
956
1346
  }
957
1347
  updateRepositoryIdentity(payload);
@@ -965,12 +1355,14 @@
965
1355
  setMeta(`${rootName} · ${payload.items.length} docs · refreshed ${new Date().toLocaleTimeString()}`);
966
1356
  }
967
1357
  scheduleNextAutoRefresh();
1358
+ updateVersionLink(payload.updateInfo);
968
1359
  renderUpdateNotice(payload.updateInfo);
969
1360
  refreshCiBadgeCounters();
970
1361
  refreshCdxBadgeCounters();
971
1362
  updateFilterSummary();
972
1363
  applyLocalViewerChrome();
973
1364
  bindRefreshMenuControls();
1365
+ return true;
974
1366
  }
975
1367
 
976
1368
  function renderUpdateNotice(updateInfo) {
@@ -1007,11 +1399,11 @@
1007
1399
  if (!response.ok || !data.ok) {
1008
1400
  throw new Error(data.error || "Unable to load viewer data.");
1009
1401
  }
1010
- postToApp(data.payload, { silent: Boolean(options.silent) });
1402
+ const changed = postToApp(data.payload, { silent: Boolean(options.silent), force: Boolean(options.force) });
1011
1403
  if (method !== "POST") {
1012
1404
  await refreshGitBadgeCounters();
1013
1405
  }
1014
- return true;
1406
+ return changed;
1015
1407
  } catch (error) {
1016
1408
  markConnectionDisconnected(error);
1017
1409
  throw error;
@@ -1026,6 +1418,12 @@
1026
1418
  return Boolean(panel && !panel.hidden && title && title.textContent === "Git status");
1027
1419
  }
1028
1420
 
1421
+ function isWorkspaceOpen() {
1422
+ const panel = documentPanel();
1423
+ const title = documentTitle();
1424
+ return Boolean(panel && !panel.hidden && title && title.textContent === "Explorer");
1425
+ }
1426
+
1029
1427
  function isCdxStatusOpen() {
1030
1428
  const panel = documentPanel();
1031
1429
  const title = documentTitle();
@@ -1038,6 +1436,12 @@
1038
1436
  return Boolean(panel && !panel.hidden && title && title.textContent === "CDX runs");
1039
1437
  }
1040
1438
 
1439
+ function isCdxMissionsOpen() {
1440
+ const panel = documentPanel();
1441
+ const title = documentTitle();
1442
+ return Boolean(panel && !panel.hidden && title && title.textContent === "CDX missions");
1443
+ }
1444
+
1041
1445
  function isCiStatusOpen() {
1042
1446
  const panel = documentPanel();
1043
1447
  const title = documentTitle();
@@ -1045,18 +1449,27 @@
1045
1449
  }
1046
1450
 
1047
1451
  async function refreshViewer(method = "POST", options = {}) {
1048
- await loadItems(method, options);
1049
- if (isGitStatusOpen()) {
1050
- await showGitStatus({ preserve: true, silent: Boolean(options.silent) });
1452
+ const changed = await loadItems(method, options);
1453
+ if (isWorkspaceOpen()) {
1454
+ if (changed || options.force) {
1455
+ await showWorkspace({ silent: Boolean(options.silent) });
1456
+ }
1457
+ } else if (isGitStatusOpen()) {
1458
+ await showGitStatus({ preserve: true, silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1051
1459
  } else if (isCiStatusOpen()) {
1052
- await showCiStatus({ silent: Boolean(options.silent) });
1460
+ await showCiStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1053
1461
  } else if (isCdxStatusOpen()) {
1054
- await showCdxStatus({ silent: Boolean(options.silent) });
1462
+ await showCdxStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1055
1463
  } else if (isCdxRunsOpen()) {
1056
- await showCdxRuns({ silent: Boolean(options.silent) });
1464
+ if (changed || options.force) {
1465
+ await showCdxRuns({ silent: Boolean(options.silent) });
1466
+ }
1057
1467
  } else if (method === "POST") {
1058
1468
  await refreshGitBadgeCounters();
1059
1469
  }
1470
+ if (!changed && !options.silent && !options.force) {
1471
+ setMeta(`Checked just now · no viewer changes (${new Date().toLocaleTimeString()})`);
1472
+ }
1060
1473
  }
1061
1474
 
1062
1475
  function autoRefreshItems() {
@@ -1347,6 +1760,31 @@
1347
1760
  `).join("");
1348
1761
  }
1349
1762
 
1763
+ function renderGitSummaryCard(label, value) {
1764
+ return `
1765
+ <div class="viewer-insights__card">
1766
+ <div class="viewer-insights__label">${escapeHtml(label)}</div>
1767
+ <div class="viewer-insights__value">${escapeHtml(value)}</div>
1768
+ </div>
1769
+ `;
1770
+ }
1771
+
1772
+ function renderGitSummarySegments(label, segments) {
1773
+ return `
1774
+ <div class="viewer-insights__card viewer-git__summary-card">
1775
+ <div class="viewer-insights__label">${escapeHtml(label)}</div>
1776
+ <div class="viewer-git__summary-segments">
1777
+ ${segments.map(([segmentLabel, value]) => `
1778
+ <span class="viewer-git__summary-segment">
1779
+ <span>${escapeHtml(segmentLabel)}</span>
1780
+ <strong>${escapeHtml(value)}</strong>
1781
+ </span>
1782
+ `).join("")}
1783
+ </div>
1784
+ </div>
1785
+ `;
1786
+ }
1787
+
1350
1788
  function renderInsightBars(entries, total) {
1351
1789
  const denominator = Math.max(1, Number(total) || 0);
1352
1790
  if (!entries.length) {
@@ -1733,6 +2171,145 @@
1733
2171
  setMeta("Health loaded.");
1734
2172
  }
1735
2173
 
2174
+ function workspaceParentPath(path) {
2175
+ const parts = String(path || "").split("/").filter(Boolean);
2176
+ parts.pop();
2177
+ return parts.join("/");
2178
+ }
2179
+
2180
+ function renderWorkspaceTree(treePayload, selectedPath = "") {
2181
+ if (!treePayload || treePayload.state !== "ok") {
2182
+ return `<div class="viewer-workspace__empty">${escapeHtml(treePayload?.message || "Workspace tree is unavailable.")}</div>`;
2183
+ }
2184
+ const currentPath = String(treePayload.path || "");
2185
+ const parentPath = workspaceParentPath(currentPath);
2186
+ const upButton = currentPath
2187
+ ? `<button class="viewer-workspace__item viewer-workspace__item--up" type="button" data-viewer-workspace-tree="${escapeHtml(parentPath)}">..</button>`
2188
+ : "";
2189
+ const rows = (Array.isArray(treePayload.entries) ? treePayload.entries : []).map((entry) => {
2190
+ const path = String(entry.path || "");
2191
+ const kind = String(entry.kind || "file");
2192
+ const ignored = Boolean(entry.ignored);
2193
+ const selected = path === selectedPath;
2194
+ const actionAttr = kind === "directory" && !ignored
2195
+ ? `data-viewer-workspace-tree="${escapeHtml(path)}"`
2196
+ : `data-viewer-workspace-preview="${escapeHtml(path)}"`;
2197
+ const icon = kind === "directory" ? (ignored ? "x" : ">") : "-";
2198
+ return `
2199
+ <button class="viewer-workspace__item${selected ? " is-selected" : ""}${ignored ? " is-muted" : ""}" type="button" ${actionAttr} title="${escapeHtml(path)}">
2200
+ <span class="viewer-workspace__item-icon" aria-hidden="true">${escapeHtml(icon)}</span>
2201
+ <span class="viewer-workspace__item-name">${escapeHtml(entry.name || path || "/")}</span>
2202
+ </button>
2203
+ `;
2204
+ }).join("");
2205
+ return `
2206
+ <div class="viewer-workspace__tree-header">
2207
+ <span>${escapeHtml(currentPath || "/")}</span>
2208
+ </div>
2209
+ <div class="viewer-workspace__tree-list">
2210
+ ${upButton}
2211
+ ${rows || '<div class="viewer-workspace__empty">Directory is empty.</div>'}
2212
+ </div>
2213
+ ${treePayload.truncated ? '<div class="viewer-workspace__empty">Directory listing truncated.</div>' : ""}
2214
+ `;
2215
+ }
2216
+
2217
+ function renderWorkspacePreview(previewPayload) {
2218
+ if (!previewPayload) {
2219
+ return '<div class="viewer-workspace__empty">Select a file or directory.</div>';
2220
+ }
2221
+ const path = previewPayload.path || "/";
2222
+ const name = previewPayload.name || path || "/";
2223
+ const state = previewPayload.state || "unknown";
2224
+ if (state === "ok") {
2225
+ return `
2226
+ <div class="viewer-workspace__preview-header">
2227
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2228
+ <em>${escapeHtml(previewPayload.truncated ? "truncated" : `${previewPayload.size || 0} bytes`)}</em>
2229
+ </div>
2230
+ ${previewPayload.truncated ? '<div class="viewer-cdx__state viewer-cdx__state--warn">Preview truncated.</div>' : ""}
2231
+ <pre class="viewer-workspace__code">${escapeHtml(previewPayload.content || "")}</pre>
2232
+ `;
2233
+ }
2234
+ if (state === "image") {
2235
+ return `
2236
+ <div class="viewer-workspace__preview-header">
2237
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2238
+ <em>${escapeHtml(previewPayload.contentType || "image")}</em>
2239
+ </div>
2240
+ <img class="viewer-workspace__image" src="/api/workspace-file?path=${encodeURIComponent(path)}" alt="${escapeHtml(name)}">
2241
+ `;
2242
+ }
2243
+ return `
2244
+ <div class="viewer-workspace__preview-header">
2245
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2246
+ <em>${escapeHtml(state)}</em>
2247
+ </div>
2248
+ <div class="viewer-workspace__empty">${escapeHtml(previewPayload.message || "No preview is available.")}</div>
2249
+ `;
2250
+ }
2251
+
2252
+ function renderWorkspace(treePayload, previewPayload) {
2253
+ const selectedPath = previewPayload?.path || "";
2254
+ return `
2255
+ <div class="viewer-workspace">
2256
+ <aside class="viewer-workspace__tree" aria-label="Workspace files">
2257
+ ${renderWorkspaceTree(treePayload, selectedPath)}
2258
+ </aside>
2259
+ <section class="viewer-workspace__preview" aria-label="Workspace preview">
2260
+ ${renderWorkspacePreview(previewPayload)}
2261
+ </section>
2262
+ </div>
2263
+ `;
2264
+ }
2265
+
2266
+ async function fetchWorkspaceTree(path = "") {
2267
+ const response = await fetch(`/api/workspace-tree?path=${encodeURIComponent(path)}`);
2268
+ const data = await response.json();
2269
+ if (!response.ok || !data.ok) {
2270
+ throw new Error(data.error || "Unable to load workspace tree.");
2271
+ }
2272
+ return data.payload;
2273
+ }
2274
+
2275
+ async function fetchWorkspacePreview(path = "") {
2276
+ const response = await fetch(`/api/workspace-preview?path=${encodeURIComponent(path)}`);
2277
+ const data = await response.json();
2278
+ if (!response.ok || !data.ok) {
2279
+ throw new Error(data.error || "Unable to load workspace preview.");
2280
+ }
2281
+ return data.payload;
2282
+ }
2283
+
2284
+ async function showWorkspace(options = {}) {
2285
+ if (!isCapabilityAvailable("workspace")) {
2286
+ const message = capabilityMessage("workspace", "Explorer is not available for this project.");
2287
+ setDocument("Explorer", renderWorkspace({ state: "unavailable", message }, { state: "unavailable", message }));
2288
+ setMeta(message);
2289
+ return;
2290
+ }
2291
+ if (!options.silent) {
2292
+ setMeta("Loading workspace...");
2293
+ }
2294
+ const tree = await fetchWorkspaceTree("");
2295
+ const preview = await fetchWorkspacePreview("");
2296
+ setDocument("Explorer", renderWorkspace(tree, preview));
2297
+ setMeta(options.silent ? "Explorer refreshed." : "Explorer loaded.");
2298
+ }
2299
+
2300
+ async function openWorkspaceTree(path) {
2301
+ const [tree, preview] = await Promise.all([fetchWorkspaceTree(path), fetchWorkspacePreview(path)]);
2302
+ setDocument("Explorer", renderWorkspace(tree, preview));
2303
+ setMeta(path ? `Explorer folder ${path}` : "Explorer root.");
2304
+ }
2305
+
2306
+ async function openWorkspacePreview(path) {
2307
+ const treePath = workspaceParentPath(path);
2308
+ const [tree, preview] = await Promise.all([fetchWorkspaceTree(treePath), fetchWorkspacePreview(path)]);
2309
+ setDocument("Explorer", renderWorkspace(tree, preview));
2310
+ setMeta(`Previewing ${path || "workspace root"}.`);
2311
+ }
2312
+
1736
2313
  function objectEntries(value) {
1737
2314
  return value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
1738
2315
  }
@@ -1770,15 +2347,41 @@
1770
2347
  return asArray(status?.rows);
1771
2348
  }
1772
2349
 
2350
+ function numericValues(values) {
2351
+ return values.map((value) => Number(value)).filter((value) => Number.isFinite(value));
2352
+ }
2353
+
2354
+ function formatPercentRange(values) {
2355
+ const numbers = numericValues(values).map((value) => Math.max(0, Math.min(100, Math.round(value))));
2356
+ if (!numbers.length) {
2357
+ return "not reported";
2358
+ }
2359
+ const min = Math.min(...numbers);
2360
+ const max = Math.max(...numbers);
2361
+ return min === max ? `${min}%` : `${min}-${max}%`;
2362
+ }
2363
+
1773
2364
  function cdxProviders(status) {
1774
- const explicitProviders = pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1775
- if (explicitProviders.length) {
1776
- return explicitProviders;
2365
+ const rows = cdxRows(status);
2366
+ if (!rows.length) {
2367
+ return pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1777
2368
  }
1778
2369
  const grouped = new Map();
1779
- cdxRows(status).forEach((row) => {
2370
+ rows.forEach((row) => {
1780
2371
  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 };
2372
+ const current = grouped.get(provider) || {
2373
+ name: provider,
2374
+ enabled: 0,
2375
+ active: 0,
2376
+ authenticated: 0,
2377
+ sessions: 0,
2378
+ remaining_5h: "not reported",
2379
+ remaining_week: "not reported",
2380
+ credits: "",
2381
+ _remaining5hValues: [],
2382
+ _remainingWeekValues: [],
2383
+ _creditsValues: []
2384
+ };
1782
2385
  current.sessions += 1;
1783
2386
  if (row.enabled) {
1784
2387
  current.enabled += 1;
@@ -1789,15 +2392,31 @@
1789
2392
  if (String(row.auth_status || "").toLowerCase() === "authenticated") {
1790
2393
  current.authenticated += 1;
1791
2394
  }
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);
2395
+ const fiveHour = Number(row.remaining_5h_pct ?? row.remaining5hPct);
2396
+ if (Number.isFinite(fiveHour)) {
2397
+ current._remaining5hValues.push(fiveHour);
2398
+ }
2399
+ const week = Number(row.remaining_week_pct ?? row.remainingWeekPct);
2400
+ if (Number.isFinite(week)) {
2401
+ current._remainingWeekValues.push(week);
2402
+ }
2403
+ if (row.credits !== undefined && row.credits !== null && row.credits !== "") {
2404
+ current._creditsValues.push(row.credits);
1796
2405
  }
1797
2406
  current.state = current.active > 0 ? "active" : current.enabled > 0 ? "enabled" : "disabled";
1798
2407
  grouped.set(provider, current);
1799
2408
  });
1800
- return Array.from(grouped.values());
2409
+ return Array.from(grouped.values()).map((provider) => {
2410
+ const creditsNumbers = numericValues(provider._creditsValues);
2411
+ const creditsTotal = creditsNumbers.length ? creditsNumbers.reduce((total, value) => total + value, 0) : null;
2412
+ const { _remaining5hValues, _remainingWeekValues, _creditsValues, ...publicProvider } = provider;
2413
+ return {
2414
+ ...publicProvider,
2415
+ remaining_5h: formatPercentRange(_remaining5hValues),
2416
+ remaining_week: formatPercentRange(_remainingWeekValues),
2417
+ credits: creditsTotal === null ? "" : creditsTotal.toFixed(2)
2418
+ };
2419
+ });
1801
2420
  }
1802
2421
 
1803
2422
  function cdxSessions(status) {
@@ -1837,8 +2456,25 @@
1837
2456
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1838
2457
  }
1839
2458
 
2459
+ function renderCdxArtifactRows(value, emptyText) {
2460
+ const rows = objectEntries(value).slice(0, 12).map(([key, entry]) => {
2461
+ const path = typeof entry === "string" ? entry : "";
2462
+ return `
2463
+ <li class="viewer-cdx__row">
2464
+ <span>${escapeHtml(cdxLabel(key))}</span>
2465
+ <strong>${path
2466
+ ? `<button class="viewer-cdx__path-link" type="button" data-viewer-cdx-artifact-path="${escapeHtml(path)}">${escapeHtml(path)}</button>`
2467
+ : escapeHtml(typeof entry === "object" ? JSON.stringify(entry) : entry)}
2468
+ </strong>
2469
+ </li>
2470
+ `;
2471
+ }).join("");
2472
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
2473
+ }
2474
+
1840
2475
  function cdxLabel(value) {
1841
2476
  return String(value || "")
2477
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
1842
2478
  .replace(/[_-]+/g, " ")
1843
2479
  .replace(/\b\w/g, (letter) => letter.toUpperCase());
1844
2480
  }
@@ -1848,7 +2484,7 @@
1848
2484
  if (["ready", "ok", "active", "enabled", "authenticated"].some((entry) => state.includes(entry))) {
1849
2485
  return "ok";
1850
2486
  }
1851
- if (["starting", "pending", "warning", "low", "limited"].some((entry) => state.includes(entry))) {
2487
+ if (["starting", "pending", "running", "warning", "low", "limited", "stale"].some((entry) => state.includes(entry))) {
1852
2488
  return "warn";
1853
2489
  }
1854
2490
  if (["error", "failed", "disabled", "unavailable", "unauthenticated"].some((entry) => state.includes(entry))) {
@@ -1981,6 +2617,10 @@
1981
2617
  return `<span class="viewer-cdx__badge viewer-cdx__badge--${cdxStateClass(label)}">${escapeHtml(cdxLabel(label))}</span>`;
1982
2618
  }
1983
2619
 
2620
+ function cdxRunStatusDetail(run) {
2621
+ return "";
2622
+ }
2623
+
1984
2624
  function cdxDetailEntries(item, excludedKeys) {
1985
2625
  return objectEntries(item)
1986
2626
  .filter(([key, value]) => !excludedKeys.includes(key) && value !== undefined && value !== null && value !== "")
@@ -2023,31 +2663,108 @@
2023
2663
  return explicit === true ? "YES" : "-";
2024
2664
  }
2025
2665
 
2666
+ function cdxProviderName(item) {
2667
+ return String(cdxField(item, ["provider", "name"], "unknown") || "unknown");
2668
+ }
2669
+
2670
+ function cdxKnownProviders(status, providers, sessions) {
2671
+ const names = new Set();
2672
+ providers.forEach((provider) => {
2673
+ const name = cdxProviderName(provider);
2674
+ if (name) {
2675
+ names.add(name);
2676
+ }
2677
+ });
2678
+ sessions.forEach((session) => {
2679
+ const name = cdxProviderName(session);
2680
+ if (name) {
2681
+ names.add(name);
2682
+ }
2683
+ });
2684
+ pickFirstArray(status, ["providers", "providerStatus", "provider_status"]).forEach((provider) => {
2685
+ const name = cdxProviderName(provider);
2686
+ if (name) {
2687
+ names.add(name);
2688
+ }
2689
+ });
2690
+ return Array.from(names).sort((left, right) => left.localeCompare(right));
2691
+ }
2692
+
2693
+ function filterCdxEntriesByProvider(entries, providerFilter) {
2694
+ if (providerFilter.mode !== "subset" || !providerFilter.selected.length) {
2695
+ return entries;
2696
+ }
2697
+ const selected = new Set(providerFilter.selected);
2698
+ return entries.filter((entry) => selected.has(cdxProviderName(entry)));
2699
+ }
2700
+
2701
+ function renderCdxStatusControls(knownProviders, visibleColumns, providerFilter) {
2702
+ const columnRows = cdxStatusColumns.map((column) => `
2703
+ <label class="viewer-cdx__menu-check">
2704
+ <input type="checkbox" data-viewer-cdx-column="${escapeHtml(column.id)}"${visibleColumns[column.id] ? " checked" : ""}>
2705
+ <span>${escapeHtml(column.label)}</span>
2706
+ </label>
2707
+ `).join("");
2708
+ const selected = new Set(providerFilter.mode === "subset" ? providerFilter.selected : knownProviders);
2709
+ const providerRows = knownProviders.map((provider) => `
2710
+ <label class="viewer-cdx__menu-check">
2711
+ <input type="checkbox" data-viewer-cdx-provider="${escapeHtml(provider)}"${selected.has(provider) ? " checked" : ""}>
2712
+ <span>${escapeHtml(provider)}</span>
2713
+ </label>
2714
+ `).join("");
2715
+ const providerSummary = providerFilter.mode === "subset" && providerFilter.selected.length
2716
+ ? `${providerFilter.selected.length}/${knownProviders.length || providerFilter.selected.length}`
2717
+ : "All";
2718
+ return `
2719
+ <div class="viewer-cdx__controls" aria-label="CDX status table controls">
2720
+ <details class="viewer-cdx__menu">
2721
+ <summary class="viewer-cdx__icon-button" title="Configure status columns" aria-label="Configure status columns">
2722
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2 3.4-.2-.1a1.7 1.7 0 0 0-2 .1 1.7 1.7 0 0 0-.8 1.7v.2H9.2v-.2a1.7 1.7 0 0 0-.8-1.7 1.7 1.7 0 0 0-2-.1l-.2.1-2-3.4.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1.1H3v-3.8h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.9l-.1-.1 2-3.4.2.1a1.7 1.7 0 0 0 2-.1 1.7 1.7 0 0 0 .8-1.7v-.2h5.6v.2a1.7 1.7 0 0 0 .8 1.7 1.7 1.7 0 0 0 2 .1l.2-.1 2 3.4-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1.1h.1v3.8h-.1a1.7 1.7 0 0 0-1.5 1.1Z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
2723
+ </summary>
2724
+ <div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX status columns">${columnRows}</div>
2725
+ </details>
2726
+ <details class="viewer-cdx__menu">
2727
+ <summary class="viewer-cdx__icon-button" title="Filter status providers" aria-label="Filter status providers">
2728
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16l-6.5 7.2V19l-3 1.5v-7.3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>
2729
+ <span class="viewer-cdx__icon-count">${escapeHtml(providerSummary)}</span>
2730
+ </summary>
2731
+ <div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX provider filter">
2732
+ <button class="viewer-cdx__menu-action" type="button" data-viewer-cdx-provider-all>All providers</button>
2733
+ ${providerRows || '<div class="viewer-cdx__empty">No providers reported.</div>'}
2734
+ </div>
2735
+ </details>
2736
+ </div>
2737
+ `;
2738
+ }
2739
+
2026
2740
  function renderCdxSessionTable(sessions, emptyText) {
2027
2741
  if (!sessions.length) {
2028
2742
  return `<div class="viewer-cdx__empty">${escapeHtml(emptyText)}</div>`;
2029
2743
  }
2744
+ const visibleColumns = cdxColumnVisibilityPreference();
2745
+ const cellRenderers = {
2746
+ session: (item) => {
2747
+ const name = cdxField(item, ["session_name", "name", "id", "value"]);
2748
+ return `<td class="viewer-cdx__session-name">${escapeHtml(`${name}${item.active ? "*" : ""}`)}</td>`;
2749
+ },
2750
+ provider: (item) => `<td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>`,
2751
+ status: (item) => `<td>${renderCdxBadge(cdxField(item, ["status", "state"]))}</td>`,
2752
+ auth: (item) => `<td>${escapeHtml(String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged"))}</td>`,
2753
+ ok: (item) => `<td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>`,
2754
+ remaining5h: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>`,
2755
+ remainingWeek: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>`,
2756
+ block: (item) => `<td>${escapeHtml(cdxSessionBlock(item))}</td>`,
2757
+ credits: (item) => `<td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>`,
2758
+ reset5h: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>`,
2759
+ resetWeek: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>`,
2760
+ updated: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>`
2761
+ };
2762
+ const activeColumns = cdxStatusColumns.filter((column) => visibleColumns[column.id]);
2030
2763
  const rows = sessions.slice(0, 24).map((entry) => {
2031
2764
  const item = entry && typeof entry === "object" ? entry : { value: entry };
2032
- const name = cdxField(item, ["session_name", "name", "id", "value"]);
2033
- const sessionName = `${name}${item.active ? "*" : ""}`;
2034
- const status = cdxField(item, ["status", "state"]);
2035
- const auth = String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged");
2036
- const block = cdxSessionBlock(item);
2037
2765
  return `
2038
2766
  <tr>
2039
- <td class="viewer-cdx__session-name">${escapeHtml(sessionName)}</td>
2040
- <td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>
2041
- <td>${renderCdxBadge(status)}</td>
2042
- <td>${escapeHtml(auth)}</td>
2043
- <td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>
2044
- <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>
2045
- <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>
2046
- <td>${escapeHtml(block)}</td>
2047
- <td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>
2048
- <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>
2049
- <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>
2050
- <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>
2767
+ ${activeColumns.map((column) => cellRenderers[column.id](item)).join("")}
2051
2768
  </tr>
2052
2769
  `;
2053
2770
  }).join("");
@@ -2056,18 +2773,7 @@
2056
2773
  <table class="viewer-cdx__table">
2057
2774
  <thead>
2058
2775
  <tr>
2059
- <th>SESSION</th>
2060
- <th>PROV.</th>
2061
- <th>STATUS</th>
2062
- <th>AUTH</th>
2063
- <th>OK</th>
2064
- <th>5H</th>
2065
- <th>WEEK</th>
2066
- <th>BLOCK</th>
2067
- <th>CR</th>
2068
- <th>RESET 5H</th>
2069
- <th>RESET WEEK</th>
2070
- <th>UPDATED</th>
2776
+ ${activeColumns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join("")}
2071
2777
  </tr>
2072
2778
  </thead>
2073
2779
  <tbody>${rows}</tbody>
@@ -2106,28 +2812,250 @@
2106
2812
  return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
2107
2813
  }
2108
2814
 
2109
- function renderCdxModeSwitcher(active) {
2110
- return `
2111
- <div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
2112
- <button class="viewer-cdx__mode${active === "status" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="status" aria-selected="${active === "status" ? "true" : "false"}">Status</button>
2113
- <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
- </div>
2115
- `;
2815
+ function cdxMissionCatalog(payload = {}) {
2816
+ return payload.catalog || {
2817
+ missions: [
2818
+ { 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" }] },
2819
+ { 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" }] },
2820
+ { 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 },
2821
+ { 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 }] },
2822
+ { 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" }] }
2823
+ ],
2824
+ strengths: [
2825
+ { id: "standard", label: "Standard" },
2826
+ { id: "deep", label: "Deep" },
2827
+ { id: "max", label: "Max" }
2828
+ ],
2829
+ defaultMissionId: "full-audit",
2830
+ defaultStrengthId: "standard"
2831
+ };
2116
2832
  }
2117
2833
 
2118
- function renderCdxStatus(payload) {
2119
- if (!payload || payload.state !== "ok") {
2120
- return `
2121
- <div class="viewer-cdx">
2122
- ${renderCdxModeSwitcher("status")}
2123
- <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
2124
- </div>
2125
- `;
2126
- }
2127
- const status = payload.status || {};
2128
- const providers = cdxProviders(status);
2129
- const sessions = cdxSessions(status);
2130
- const readiness = cdxReadiness(status);
2834
+ function selectedCdxMissionRequest() {
2835
+ const catalog = latestCdxMissionState.catalog || cdxMissionCatalog();
2836
+ const missions = Array.isArray(catalog.missions) ? catalog.missions : [];
2837
+ const missionId = latestCdxMissionState.missionId || "full-audit";
2838
+ const mission = missions.find((entry) => entry.id === missionId) || {};
2839
+ const allowFileWrites = mission.supportsFileWrites === false
2840
+ ? "false"
2841
+ : (latestCdxMissionState.missionInputs.allowFileWrites === "false" ? "false" : "true");
2842
+ return {
2843
+ missionId,
2844
+ sessionId: latestCdxMissionState.sessionId || "",
2845
+ strengthId: latestCdxMissionState.strengthId || "standard",
2846
+ ...latestCdxMissionState.missionInputs,
2847
+ allowFileWrites,
2848
+ commitAtEnd: latestCdxMissionState.missionInputs.commitAtEnd === "true" ? "true" : "false"
2849
+ };
2850
+ }
2851
+
2852
+ function renderCdxMissionInputs(mission) {
2853
+ const fields = Array.isArray(mission?.inputFields) ? mission.inputFields : [];
2854
+ if (!fields.length) {
2855
+ return "";
2856
+ }
2857
+ const rows = fields.map((field) => {
2858
+ const id = field.id || "";
2859
+ const value = latestCdxMissionState.missionInputs[id] || "";
2860
+ if (field.type === "checkbox") {
2861
+ return `
2862
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2863
+ <input data-viewer-cdx-input="${escapeHtml(id)}" type="checkbox"${value === "true" ? " checked" : ""}>
2864
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2865
+ </label>
2866
+ `;
2867
+ }
2868
+ if (field.type === "textarea") {
2869
+ return `
2870
+ <label class="viewer-cdx__field">
2871
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2872
+ <textarea data-viewer-cdx-input="${escapeHtml(id)}" placeholder="${escapeHtml(field.placeholder || "")}" rows="5">${escapeHtml(value)}</textarea>
2873
+ </label>
2874
+ `;
2875
+ }
2876
+ return `
2877
+ <label class="viewer-cdx__field">
2878
+ <span>${escapeHtml(field.label || cdxLabel(id))}</span>
2879
+ <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)}"` : ""}>
2880
+ </label>
2881
+ `;
2882
+ }).join("");
2883
+ return `<div class="viewer-cdx__inputs">${rows}</div>`;
2884
+ }
2885
+
2886
+ function renderCdxMissionSetup(statusPayload, planPayload, runPayload, applyPayload) {
2887
+ const catalog = cdxMissionCatalog(planPayload || {});
2888
+ latestCdxMissionState.catalog = catalog;
2889
+ const missions = Array.isArray(catalog.missions) ? catalog.missions : [];
2890
+ const strengths = Array.isArray(catalog.strengths) ? catalog.strengths : [];
2891
+ const status = statusPayload?.status || {};
2892
+ const sessions = cdxSessions(status);
2893
+ const selectedSession = latestCdxMissionState.sessionId || cdxField(sessions[0] || {}, ["id", "name", "session_name", "value"], "");
2894
+ const missionId = latestCdxMissionState.missionId || catalog.defaultMissionId || "full-audit";
2895
+ const selectedMission = missions.find((mission) => mission.id === missionId) || {};
2896
+ const strengthId = latestCdxMissionState.strengthId || catalog.defaultStrengthId || "standard";
2897
+ const supportsFileWrites = selectedMission.supportsFileWrites !== false;
2898
+ const allowFileWrites = supportsFileWrites && latestCdxMissionState.missionInputs.allowFileWrites !== "false";
2899
+ const fileWriteLabel = ["full-audit", "release-review"].includes(selectedMission.id)
2900
+ ? "Write mission corpus/report"
2901
+ : "Allow CDX to modify files";
2902
+ const fileWriteControl = supportsFileWrites
2903
+ ? `
2904
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2905
+ <input data-viewer-cdx-input="allowFileWrites" type="checkbox"${allowFileWrites ? " checked" : ""}>
2906
+ <span>${escapeHtml(fileWriteLabel)}</span>
2907
+ </label>
2908
+ <label class="viewer-cdx__field viewer-cdx__field--check">
2909
+ <input data-viewer-cdx-input="commitAtEnd" type="checkbox"${latestCdxMissionState.missionInputs.commitAtEnd === "true" ? " checked" : ""}>
2910
+ <span>Commit changes at end</span>
2911
+ </label>
2912
+ `
2913
+ : `
2914
+ <div class="viewer-cdx__meta">Corpus updates are applied after CDX returns allowed actions.</div>
2915
+ `;
2916
+ latestCdxMissionState.sessionId = selectedSession;
2917
+ const missionCards = missions.map((mission) => `
2918
+ <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"}">
2919
+ <strong>${escapeHtml(mission.title || mission.id)}</strong>
2920
+ <span>${escapeHtml(mission.description || "")}</span>
2921
+ <em>${escapeHtml(cdxLabel(mission.scope || ""))}</em>
2922
+ </button>
2923
+ `).join("");
2924
+ const sessionOptions = sessions.map((session) => {
2925
+ const item = session && typeof session === "object" ? session : { value: session };
2926
+ const id = cdxField(item, ["id", "name", "session_name", "value"], "");
2927
+ const label = [id, cdxField(item, ["provider"], ""), renderTextRemaining(item)].filter(Boolean).join(" · ");
2928
+ return `<option value="${escapeHtml(id)}"${id === selectedSession ? " selected" : ""}>${escapeHtml(label || id)}</option>`;
2929
+ }).join("");
2930
+ const strengthButtons = strengths.map((strength) => `
2931
+ <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>
2932
+ `).join("");
2933
+ const plan = planPayload?.plan;
2934
+ const warnings = Array.isArray(plan?.warnings) ? plan.warnings : [];
2935
+ const command = Array.isArray(plan?.command) ? plan.command.join(" ") : "";
2936
+ const warningRows = warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
2937
+ const canRun = planPayload?.state === "ok" && plan?.canRun;
2938
+ const usage = runPayload?.run?.usage || {};
2939
+ const run = runPayload?.run;
2940
+ const usageText = usage.available
2941
+ ? `${usage.totalTokens ?? "-"} total · ${usage.inputTokens ?? "-"} in · ${usage.outputTokens ?? "-"} out`
2942
+ : (usage.message || "Token usage not reported yet.");
2943
+ const parsedActions = Array.isArray(run?.parsed?.actions) ? run.parsed.actions : [];
2944
+ const applyResults = Array.isArray(applyPayload?.results) ? applyPayload.results : [];
2945
+ const actionRows = parsedActions.map((action) => `
2946
+ <li class="viewer-cdx__row"><span>${escapeHtml(cdxLabel(action.type || "action"))}</span><strong>${escapeHtml(action.target || "-")}</strong></li>
2947
+ `).join("");
2948
+ const applyRows = applyResults.map((result) => `
2949
+ <li class="viewer-cdx__row"><span>${escapeHtml(cdxLabel(result.type || "action"))}</span><strong>${escapeHtml(result.returnCode === 0 ? "applied" : "failed")}</strong></li>
2950
+ `).join("");
2951
+ return `
2952
+ <div class="viewer-cdx__workspace viewer-cdx__workspace--missions">
2953
+ <div class="viewer-cdx__stack">
2954
+ <section class="viewer-cdx__section">
2955
+ <h2 class="viewer-cdx__heading">Mission</h2>
2956
+ <div class="viewer-cdx__missions">${missionCards}</div>
2957
+ </section>
2958
+ <section class="viewer-cdx__section">
2959
+ <h2 class="viewer-cdx__heading">Execution</h2>
2960
+ <label class="viewer-cdx__field">
2961
+ <span>Session</span>
2962
+ <select data-viewer-cdx-session>${sessionOptions || '<option value="">No session reported</option>'}</select>
2963
+ </label>
2964
+ <div class="viewer-cdx__strengths">${strengthButtons}</div>
2965
+ ${fileWriteControl}
2966
+ ${renderCdxMissionInputs(selectedMission)}
2967
+ <div class="viewer-cdx__actions">
2968
+ <button class="btn" type="button" data-viewer-cdx-plan>Preview</button>
2969
+ <button class="btn" type="button" data-viewer-cdx-run${canRun ? "" : " disabled"}>Launch run</button>
2970
+ </div>
2971
+ </section>
2972
+ </div>
2973
+ <div class="viewer-cdx__stack">
2974
+ <section class="viewer-cdx__section">
2975
+ <h2 class="viewer-cdx__heading">Plan preview</h2>
2976
+ ${planPayload && planPayload.state !== "ok" ? `<div class="viewer-cdx__state">${escapeHtml(planPayload.message || "Unable to build mission plan.")}</div>` : ""}
2977
+ ${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>'}
2978
+ ${plan?.releaseTag ? `<div class="viewer-cdx__meta">Base tag: ${escapeHtml(plan.releaseTag)}</div>` : ""}
2979
+ ${plan?.commitAtEnd ? '<div class="viewer-cdx__meta">Commit at end: enabled when mission changes files.</div>' : ""}
2980
+ ${plan?.requiresConfirmation ? '<div class="viewer-cdx__meta">Plan-first mission: Logics changes need explicit apply after CDX returns allowed actions.</div>' : ""}
2981
+ ${warningRows ? `<ul class="viewer-cdx__warnings">${warningRows}</ul>` : ""}
2982
+ </section>
2983
+ <section class="viewer-cdx__section">
2984
+ <h2 class="viewer-cdx__heading">Run output</h2>
2985
+ ${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>'}
2986
+ ${run ? `<ul class="viewer-cdx__list">
2987
+ <li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.runId || "-")}</strong></li>
2988
+ <li class="viewer-cdx__row"><span>Usage</span><strong>${escapeHtml(usageText)}</strong></li>
2989
+ <li class="viewer-cdx__row"><span>Return code</span><strong>${escapeHtml(run.returnCode ?? "-")}</strong></li>
2990
+ </ul>` : ""}
2991
+ ${run?.stdout ? `<pre class="viewer-cdx__code">${escapeHtml(run.stdout)}</pre>` : ""}
2992
+ ${run?.stderr ? `<pre class="viewer-cdx__code viewer-cdx__code--error">${escapeHtml(run.stderr)}</pre>` : ""}
2993
+ </section>
2994
+ ${plan?.missionId === "corpus-ready" || latestCdxMissionState.missionId === "corpus-ready" ? `
2995
+ <section class="viewer-cdx__section">
2996
+ <h2 class="viewer-cdx__heading">Corpus apply</h2>
2997
+ <ul class="viewer-cdx__list">${actionRows || '<li class="viewer-cdx__empty">CDX has not returned allowed corpus actions yet.</li>'}</ul>
2998
+ <div class="viewer-cdx__actions">
2999
+ <button class="btn" type="button" data-viewer-cdx-apply-plan${parsedActions.length ? "" : " disabled"}>Apply allowed actions</button>
3000
+ </div>
3001
+ ${applyPayload ? `<div class="viewer-cdx__state viewer-cdx__state--${escapeHtml(cdxStateClass(applyPayload.state))}">${escapeHtml(applyPayload.message || cdxLabel(applyPayload.state))}</div>` : ""}
3002
+ ${applyRows ? `<ul class="viewer-cdx__list">${applyRows}</ul>` : ""}
3003
+ </section>
3004
+ ` : ""}
3005
+ </div>
3006
+ </div>
3007
+ `;
3008
+ }
3009
+
3010
+ function renderTextRemaining(item) {
3011
+ const percent = cdxRemainingPct(item);
3012
+ return percent === null ? "" : `${percent}% remaining`;
3013
+ }
3014
+
3015
+ function renderCdxMissions(statusPayload, planPayload = null, runPayload = null, applyPayload = null) {
3016
+ if (!statusPayload || statusPayload.state !== "ok") {
3017
+ return `
3018
+ <div class="viewer-cdx">
3019
+ ${renderCdxModeSwitcher("missions")}
3020
+ <div class="viewer-cdx__state">${escapeHtml(statusPayload?.message || "CDX missions are unavailable.")}</div>
3021
+ </div>
3022
+ `;
3023
+ }
3024
+ return `
3025
+ <div class="viewer-cdx">
3026
+ ${renderCdxModeSwitcher("missions")}
3027
+ ${renderCdxMissionSetup(statusPayload, planPayload, runPayload, applyPayload)}
3028
+ </div>
3029
+ `;
3030
+ }
3031
+
3032
+ function renderCdxModeSwitcher(active) {
3033
+ return `
3034
+ <div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
3035
+ <button class="viewer-cdx__mode${active === "status" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="status" aria-selected="${active === "status" ? "true" : "false"}">Status</button>
3036
+ <button class="viewer-cdx__mode${active === "missions" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="missions" aria-selected="${active === "missions" ? "true" : "false"}">Missions</button>
3037
+ <button class="viewer-cdx__mode${active === "runs" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="runs" aria-selected="${active === "runs" ? "true" : "false"}">Runs</button>
3038
+ </div>
3039
+ `;
3040
+ }
3041
+
3042
+ function renderCdxStatus(payload) {
3043
+ if (!payload || payload.state !== "ok") {
3044
+ return `
3045
+ <div class="viewer-cdx">
3046
+ ${renderCdxModeSwitcher("status")}
3047
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
3048
+ </div>
3049
+ `;
3050
+ }
3051
+ const status = payload.status || {};
3052
+ const allProviders = cdxProviders(status);
3053
+ const allSessions = cdxSessions(status);
3054
+ const providerFilter = cdxProviderFilterPreference();
3055
+ const knownProviders = cdxKnownProviders(status, allProviders, allSessions);
3056
+ const providers = filterCdxEntriesByProvider(allProviders, providerFilter);
3057
+ const sessions = filterCdxEntriesByProvider(allSessions, providerFilter);
3058
+ const readiness = cdxReadiness(status);
2131
3059
  const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
2132
3060
  .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
2133
3061
  .filter(Boolean);
@@ -2157,16 +3085,13 @@
2157
3085
  <div class="viewer-cdx">
2158
3086
  ${renderCdxModeSwitcher("status")}
2159
3087
  <div class="viewer-cdx__summary">${cards}</div>
3088
+ ${renderCdxStatusControls(knownProviders, cdxColumnVisibilityPreference(), providerFilter)}
2160
3089
  <div class="viewer-cdx__workspace">
2161
3090
  <div class="viewer-cdx__stack">
2162
3091
  <section class="viewer-cdx__section">
2163
3092
  <h2 class="viewer-cdx__heading">Sessions</h2>
2164
3093
  ${renderCdxSessionTable(sessions, "No sessions reported.")}
2165
3094
  </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
3095
  </div>
2171
3096
  <div class="viewer-cdx__stack">
2172
3097
  <section class="viewer-cdx__section">
@@ -2177,12 +3102,22 @@
2177
3102
  <h2 class="viewer-cdx__heading">Safe next commands</h2>
2178
3103
  <ul class="viewer-cdx__commands">${commandRows || '<li class="viewer-cdx__empty">No suggested commands reported.</li>'}</ul>
2179
3104
  </section>
3105
+ <section class="viewer-cdx__section">
3106
+ <h2 class="viewer-cdx__heading">Providers</h2>
3107
+ <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
3108
+ </section>
2180
3109
  </div>
2181
3110
  </div>
2182
3111
  </div>
2183
3112
  `;
2184
3113
  }
2185
3114
 
3115
+ function rerenderCdxStatusFromPreferences() {
3116
+ if (isCdxStatusOpen() && latestCdxStatusPayload) {
3117
+ setDocument("CDX status", renderCdxStatus(latestCdxStatusPayload));
3118
+ }
3119
+ }
3120
+
2186
3121
  function renderCdxRuns(payload) {
2187
3122
  if (!payload || payload.state !== "ok") {
2188
3123
  return `
@@ -2193,21 +3128,33 @@
2193
3128
  `;
2194
3129
  }
2195
3130
  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("");
3131
+ const staleCount = runs.filter((run) => String(cdxField(run, ["status", "state"], "")).toLowerCase() === "stale").length;
3132
+ const runningCount = runs.filter((run) => ["running", "starting", "pending"].includes(String(cdxField(run, ["status", "state"], "")).toLowerCase())).length;
3133
+ const runsSummary = staleCount
3134
+ ? `${runs.length} reported · ${staleCount} incomplete${runningCount ? ` · ${runningCount} running` : ""}`
3135
+ : runningCount
3136
+ ? `${runs.length} reported · ${runningCount} running`
3137
+ : `${runs.length} reported`;
3138
+ const rows = runs.map((run) => {
3139
+ const runId = cdxField(run, ["run_id", "runId", "id"], "");
3140
+ const status = cdxField(run, ["status", "state"], "unknown");
3141
+ const detail = cdxRunStatusDetail(run);
3142
+ return `
3143
+ <tr>
3144
+ <td><code>${escapeHtml(runId || "-")}</code>${detail ? `<div class="viewer-cdx__meta">${escapeHtml(detail)}</div>` : ""}</td>
3145
+ <td>${renderCdxBadge(status)}</td>
3146
+ <td>${escapeHtml(cdxField(run, ["kind"], "assistant"))}</td>
3147
+ <td>${escapeHtml(cdxField(run, ["session", "session_id", "sessionId"], "-"))}</td>
3148
+ <td>${escapeHtml(cdxField(run, ["cwd", "workspace", "repo"], "-"))}</td>
3149
+ <td>${runId ? `<button class="viewer-cdx__mode" type="button" data-viewer-cdx-report="${escapeHtml(runId)}">Report</button>` : ""}</td>
3150
+ </tr>
3151
+ `;
3152
+ }).join("");
2206
3153
  return `
2207
3154
  <div class="viewer-cdx">
2208
3155
  ${renderCdxModeSwitcher("runs")}
2209
3156
  <section class="viewer-cdx__section">
2210
- <div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runs.length)} reported</span></div>
3157
+ <div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runsSummary)}</span></div>
2211
3158
  <div class="viewer-cdx__table-wrap">
2212
3159
  <table class="viewer-cdx__table">
2213
3160
  <thead><tr><th>RUN</th><th>STATUS</th><th>KIND</th><th>SESSION</th><th>CWD</th><th>REPORT</th></tr></thead>
@@ -2219,6 +3166,188 @@
2219
3166
  `;
2220
3167
  }
2221
3168
 
3169
+ function cdxReportMissionOutput(report, run, taskReport) {
3170
+ const parsed = report?.parsed && typeof report.parsed === "object" ? report.parsed : {};
3171
+ const candidates = [
3172
+ report?.missionOutput,
3173
+ report?.mission_output,
3174
+ parsed.missionOutput,
3175
+ parsed.mission_output,
3176
+ run?.missionOutput,
3177
+ run?.mission_output,
3178
+ taskReport?.missionOutput,
3179
+ taskReport?.mission_output
3180
+ ];
3181
+ return candidates.find((candidate) => candidate && typeof candidate === "object" && !Array.isArray(candidate)) || null;
3182
+ }
3183
+
3184
+ function cdxCount(value) {
3185
+ if (Array.isArray(value)) {
3186
+ return value.length;
3187
+ }
3188
+ if (value && typeof value === "object") {
3189
+ return objectEntries(value).length;
3190
+ }
3191
+ return value ? 1 : 0;
3192
+ }
3193
+
3194
+ function cdxReportCanCreateRequest(taskReport, missionOutput) {
3195
+ if (taskReport?.kind === "code-review") {
3196
+ return true;
3197
+ }
3198
+ if (cdxCount(taskReport?.findings)) {
3199
+ return true;
3200
+ }
3201
+ return ["findings", "recommendations", "requestFiles", "actionableFixes", "releasePlan"].some((key) => cdxCount(missionOutput?.[key]));
3202
+ }
3203
+
3204
+ function renderCdxReportCards(cards) {
3205
+ return `
3206
+ <div class="viewer-cdx__summary">
3207
+ ${cards.map(([label, value]) => `
3208
+ <div class="viewer-cdx__card">
3209
+ <div class="viewer-cdx__label">${escapeHtml(label)}</div>
3210
+ <div class="viewer-cdx__value">${escapeHtml(value)}</div>
3211
+ </div>
3212
+ `).join("")}
3213
+ </div>
3214
+ `;
3215
+ }
3216
+
3217
+ function renderCdxDetailValue(value) {
3218
+ if (Array.isArray(value)) {
3219
+ return `
3220
+ <ol class="viewer-cdx__detail-list">
3221
+ ${value.map((item) => `
3222
+ <li>${typeof item === "object" && item !== null
3223
+ ? `<pre class="viewer-cdx__detail-code">${escapeHtml(JSON.stringify(item, null, 2))}</pre>`
3224
+ : escapeHtml(String(item))}
3225
+ </li>
3226
+ `).join("")}
3227
+ </ol>
3228
+ `;
3229
+ }
3230
+ if (value && typeof value === "object") {
3231
+ return `<pre class="viewer-cdx__detail-code">${escapeHtml(JSON.stringify(value, null, 2))}</pre>`;
3232
+ }
3233
+ return `<strong>${escapeHtml(String(value))}</strong>`;
3234
+ }
3235
+
3236
+ function renderCdxDetailRow(label, value) {
3237
+ return `
3238
+ <li class="viewer-cdx__row viewer-cdx__row--block">
3239
+ <span>${escapeHtml(label)}</span>
3240
+ <div class="viewer-cdx__detail-value">${renderCdxDetailValue(value)}</div>
3241
+ </li>
3242
+ `;
3243
+ }
3244
+
3245
+ function parseCdxLogJson(content) {
3246
+ const raw = String(content || "").trim();
3247
+ if (!raw) {
3248
+ return null;
3249
+ }
3250
+ try {
3251
+ return { kind: "json", value: JSON.parse(raw) };
3252
+ } catch {
3253
+ // Fall through to JSONL detection.
3254
+ }
3255
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3256
+ if (lines.length < 2) {
3257
+ return null;
3258
+ }
3259
+ const values = [];
3260
+ for (const line of lines) {
3261
+ try {
3262
+ values.push(JSON.parse(line));
3263
+ } catch {
3264
+ return null;
3265
+ }
3266
+ }
3267
+ return { kind: "jsonl", value: values };
3268
+ }
3269
+
3270
+ function renderCdxStructuredLog(parsed) {
3271
+ if (!parsed) {
3272
+ return "";
3273
+ }
3274
+ const label = parsed.kind === "jsonl" ? `${parsed.value.length} JSONL event(s)` : "JSON document";
3275
+ return `
3276
+ <details class="viewer-cdx__log-structured" open>
3277
+ <summary>Structured preview · ${escapeHtml(label)}</summary>
3278
+ <div class="viewer-cdx__detail-value">${renderCdxDetailValue(parsed.value)}</div>
3279
+ </details>
3280
+ `;
3281
+ }
3282
+
3283
+ function renderCdxLogPreview(payload) {
3284
+ const path = payload?.path || "";
3285
+ const content = payload?.content || "";
3286
+ const truncated = Boolean(payload?.truncated);
3287
+ const parsed = parseCdxLogJson(content);
3288
+ return `
3289
+ <div class="viewer-cdx">
3290
+ <section class="viewer-cdx__section">
3291
+ <div class="viewer-ci__heading"><h2>Log preview</h2><span>${truncated ? "latest output" : "complete file"}</span></div>
3292
+ <div class="viewer-cdx__log-preview">
3293
+ <div class="viewer-cdx__meta">${escapeHtml(path)}</div>
3294
+ ${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>' : ""}
3295
+ ${renderCdxStructuredLog(parsed)}
3296
+ <details class="viewer-cdx__log-raw"${parsed ? "" : " open"}>
3297
+ <summary>Raw log</summary>
3298
+ <pre class="viewer-cdx__log-content">${escapeHtml(content || "Log is empty.")}</pre>
3299
+ </details>
3300
+ </div>
3301
+ </section>
3302
+ </div>
3303
+ `;
3304
+ }
3305
+
3306
+ function renderCdxMissionOutput(output) {
3307
+ if (!output) {
3308
+ return "";
3309
+ }
3310
+ const rows = [
3311
+ ["Summary", output.summary],
3312
+ ["Version", output.version],
3313
+ ["Validation", output.validationMode],
3314
+ ["Blocked", typeof output.blocked === "boolean" ? (output.blocked ? "Yes" : "No") : ""],
3315
+ ["Actions", cdxCount(output.actions)],
3316
+ ["Findings", cdxCount(output.findings)],
3317
+ ["Recommendations", cdxCount(output.recommendations)],
3318
+ ["Changed files", cdxCount(output.changedFiles)],
3319
+ ["Corpus files", cdxCount(output.corpusFiles)],
3320
+ ["Generated files", cdxCount(output.generatedFiles)],
3321
+ ["Validation evidence", cdxCount(output.validationEvidence)]
3322
+ ].filter(([_label, value]) => value !== undefined && value !== null && value !== "" && value !== 0);
3323
+ const detailKeys = [
3324
+ "actions",
3325
+ "findings",
3326
+ "recommendations",
3327
+ "directFixes",
3328
+ "requestFiles",
3329
+ "actionableFixes",
3330
+ "changedFiles",
3331
+ "corpusFiles",
3332
+ "generatedFiles",
3333
+ "validationEvidence",
3334
+ "releasePlan"
3335
+ ];
3336
+ const details = detailKeys
3337
+ .filter((key) => cdxCount(output[key]))
3338
+ .map((key) => renderCdxDetailRow(cdxLabel(key), output[key]))
3339
+ .join("");
3340
+ return `
3341
+ <section class="viewer-cdx__section">
3342
+ <div class="viewer-ci__heading"><h2>Mission output</h2><span>${escapeHtml(rows.length)} signals</span></div>
3343
+ <ul class="viewer-cdx__list">
3344
+ ${rows.map(([label, value]) => renderCdxDetailRow(label, value)).join("") || '<li class="viewer-cdx__empty">No structured mission output was reported.</li>'}
3345
+ </ul>
3346
+ ${details ? `<ul class="viewer-cdx__list">${details}</ul>` : ""}
3347
+ </section>
3348
+ `;
3349
+ }
3350
+
2222
3351
  function renderCdxReport(payload) {
2223
3352
  if (!payload || payload.state !== "ok" || !payload.report) {
2224
3353
  return `
@@ -2231,24 +3360,49 @@
2231
3360
  const report = payload.report || {};
2232
3361
  const run = report.run || {};
2233
3362
  const taskReport = report.task_report || {};
3363
+ const runError = report.error || run.error || {};
3364
+ const artifacts = report.artifacts || run.artifacts || {};
2234
3365
  const findings = Array.isArray(taskReport.findings) ? taskReport.findings : [];
3366
+ const missionOutput = cdxReportMissionOutput(report, run, taskReport);
2235
3367
  const findingRows = findings.map((finding, index) => {
2236
3368
  const location = [finding.path || finding.file || "", finding.line || ""].filter(Boolean).join(":") || "-";
2237
3369
  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
3370
  }).join("");
2239
- const canCreate = taskReport.kind === "code-review";
3371
+ const canCreate = cdxReportCanCreateRequest(taskReport, missionOutput);
2240
3372
  return `
2241
3373
  <div class="viewer-cdx">
2242
3374
  ${renderCdxModeSwitcher("runs")}
2243
3375
  <section class="viewer-cdx__section">
2244
- <div class="viewer-ci__heading"><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
3376
+ <div class="viewer-ci__heading viewer-ci__heading--actions">
3377
+ <div><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
3378
+ <button class="viewer-cdx__mode" type="button" data-viewer-cdx-back-runs>Back to runs</button>
3379
+ </div>
3380
+ ${renderCdxReportCards([
3381
+ ["Status", run.status || "unknown"],
3382
+ ["Kind", taskReport.kind || run.kind || "assistant"],
3383
+ ["Findings", String(findings.length)],
3384
+ ["Artifacts", String(objectEntries(artifacts).length)]
3385
+ ])}
2245
3386
  <ul class="viewer-cdx__list">
2246
3387
  <li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.run_id || taskReport.run_id || "-")}</strong></li>
2247
3388
  <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>
3389
+ ${renderCdxDetailRow("Summary", taskReport.summary || "No summary reported.")}
2249
3390
  </ul>
2250
3391
  ${canCreate ? `<button class="btn" type="button" data-viewer-cdx-create-request="${escapeHtml(run.run_id || taskReport.run_id || "")}">Create Logics request</button>` : ""}
2251
3392
  </section>
3393
+ ${renderCdxMissionOutput(missionOutput)}
3394
+ ${objectEntries(runError).length ? `
3395
+ <section class="viewer-cdx__section">
3396
+ <div class="viewer-ci__heading"><h2>Run signal</h2><span>${escapeHtml(runError.code || "reported")}</span></div>
3397
+ <ul class="viewer-cdx__list">${renderCdxObjectRows(runError, "No run signal reported.")}</ul>
3398
+ </section>
3399
+ ` : ""}
3400
+ ${objectEntries(artifacts).length ? `
3401
+ <section class="viewer-cdx__section">
3402
+ <div class="viewer-ci__heading"><h2>Artifacts</h2><span>${escapeHtml(objectEntries(artifacts).length)} paths</span></div>
3403
+ <ul class="viewer-cdx__list">${renderCdxArtifactRows(artifacts, "No artifact paths reported.")}</ul>
3404
+ </section>
3405
+ ` : ""}
2252
3406
  <section class="viewer-cdx__section">
2253
3407
  <div class="viewer-ci__heading"><h2>Findings</h2><span>${escapeHtml(findings.length)} reported</span></div>
2254
3408
  <ul class="viewer-cdx__list">${findingRows || '<li class="viewer-cdx__empty">No structured findings reported.</li>'}</ul>
@@ -2285,11 +3439,131 @@
2285
3439
  if (!response.ok || !data.ok) {
2286
3440
  throw new Error(data.error || "Unable to load CDX status.");
2287
3441
  }
3442
+ const nextCdxSignature = runtimeStatusSignature(data.payload);
3443
+ if (options.skipUnchanged && !options.force && latestCdxStatusSignature && nextCdxSignature === latestCdxStatusSignature) {
3444
+ updateMainCdxBadge(data.payload);
3445
+ if (!options.silent) {
3446
+ setMeta(`Checked CDX status just now · no changes (${new Date().toLocaleTimeString()})`);
3447
+ }
3448
+ return;
3449
+ }
3450
+ latestCdxStatusSignature = nextCdxSignature;
3451
+ latestCdxStatusPayload = data.payload;
2288
3452
  updateMainCdxBadge(data.payload);
2289
3453
  setDocument("CDX status", renderCdxStatus(data.payload));
2290
3454
  setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
2291
3455
  }
2292
3456
 
3457
+ async function showCdxMissions(options = {}) {
3458
+ if (!isCapabilityAvailable("cdx")) {
3459
+ const message = capabilityMessage("cdx", "CDX is not available for this project.");
3460
+ setDocument("CDX missions", renderCdxMissions({ state: capability("cdx").state, message }));
3461
+ setMeta(message);
3462
+ return;
3463
+ }
3464
+ if (!options.silent) {
3465
+ setMeta("Loading CDX missions...");
3466
+ }
3467
+ const response = await fetch("/api/cdx-status");
3468
+ let data = {};
3469
+ try {
3470
+ data = await response.json();
3471
+ } catch {
3472
+ data = {};
3473
+ }
3474
+ if (!response.ok || !data.ok) {
3475
+ throw new Error(data.error || "Unable to load CDX mission status.");
3476
+ }
3477
+ latestCdxMissionState.statusPayload = data.payload;
3478
+ const sessions = cdxSessions(data.payload?.status || {});
3479
+ if (!latestCdxMissionState.sessionId && sessions.length) {
3480
+ latestCdxMissionState.sessionId = cdxField(sessions[0], ["id", "name", "session_name", "value"], "");
3481
+ }
3482
+ updateMainCdxBadge(data.payload);
3483
+ setDocument("CDX missions", renderCdxMissions(data.payload, latestCdxMissionState.planPayload, latestCdxMissionState.runPayload, latestCdxMissionState.applyPayload));
3484
+ setMeta(options.silent ? "CDX missions refreshed." : "CDX missions loaded.");
3485
+ }
3486
+
3487
+ async function previewCdxMission() {
3488
+ setMeta("Preparing CDX mission preview...");
3489
+ const response = await fetch("/api/cdx-mission-plan", {
3490
+ method: "POST",
3491
+ headers: { "Content-Type": "application/json" },
3492
+ body: JSON.stringify(selectedCdxMissionRequest())
3493
+ });
3494
+ const data = await response.json();
3495
+ if (!response.ok || !data.ok) {
3496
+ throw new Error(data.error || "Unable to preview CDX mission.");
3497
+ }
3498
+ latestCdxMissionState.planPayload = data.payload;
3499
+ latestCdxMissionState.runPayload = null;
3500
+ latestCdxMissionState.applyPayload = null;
3501
+ if (data.payload?.plan?.sessionId) {
3502
+ latestCdxMissionState.sessionId = data.payload.plan.sessionId;
3503
+ }
3504
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload || data.payload?.status, data.payload, null, null));
3505
+ setMeta(data.payload?.state === "ok" ? "CDX mission preview ready." : (data.payload?.message || "CDX mission preview failed."));
3506
+ }
3507
+
3508
+ async function launchCdxMission() {
3509
+ setMeta("Launching CDX mission...");
3510
+ const request = selectedCdxMissionRequest();
3511
+ const plan = latestCdxMissionState.planPayload?.plan || null;
3512
+ const pendingPayload = {
3513
+ state: "running",
3514
+ message: "CDX mission is running. You can keep using the viewer; this panel will update when it completes.",
3515
+ plan,
3516
+ run: {
3517
+ runId: "pending",
3518
+ returnCode: "pending",
3519
+ pending: true,
3520
+ usage: { available: false, message: "Still running." },
3521
+ stdout: "",
3522
+ stderr: ""
3523
+ }
3524
+ };
3525
+ latestCdxMissionState.runPayload = pendingPayload;
3526
+ latestCdxMissionState.applyPayload = null;
3527
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, pendingPayload, null));
3528
+ const response = await fetch("/api/cdx-mission-run", {
3529
+ method: "POST",
3530
+ headers: { "Content-Type": "application/json" },
3531
+ body: JSON.stringify(request)
3532
+ });
3533
+ const data = await response.json();
3534
+ if (!response.ok || !data.ok) {
3535
+ throw new Error(data.error || "Unable to launch CDX mission.");
3536
+ }
3537
+ latestCdxMissionState.planPayload = { state: data.payload?.state === "ok" ? "ok" : data.payload?.state, message: data.payload?.message || "", plan: data.payload?.plan };
3538
+ latestCdxMissionState.runPayload = data.payload;
3539
+ latestCdxMissionState.applyPayload = null;
3540
+ if (isCdxMissionsOpen()) {
3541
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, data.payload, null));
3542
+ }
3543
+ setMeta(data.payload?.state === "ok" ? "CDX mission launched." : (data.payload?.message || "CDX mission failed."));
3544
+ }
3545
+
3546
+ async function applyCdxMissionPlan() {
3547
+ const actions = latestCdxMissionState.runPayload?.run?.parsed?.actions;
3548
+ if (!Array.isArray(actions) || !actions.length) {
3549
+ setMeta("No corpus actions to apply.");
3550
+ return;
3551
+ }
3552
+ setMeta("Applying allowed corpus actions...");
3553
+ const response = await fetch("/api/cdx-mission-apply-plan", {
3554
+ method: "POST",
3555
+ headers: { "Content-Type": "application/json" },
3556
+ body: JSON.stringify({ actions })
3557
+ });
3558
+ const data = await response.json();
3559
+ if (!response.ok || !data.ok) {
3560
+ throw new Error(data.error || "Unable to apply corpus plan.");
3561
+ }
3562
+ latestCdxMissionState.applyPayload = data.payload;
3563
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload, latestCdxMissionState.planPayload, latestCdxMissionState.runPayload, data.payload));
3564
+ setMeta(data.payload?.state === "ok" ? "Corpus actions applied." : (data.payload?.message || "Corpus apply failed."));
3565
+ }
3566
+
2293
3567
  async function showCdxRuns(options = {}) {
2294
3568
  if (!isCapabilityAvailable("cdx")) {
2295
3569
  const message = capabilityMessage("cdx", "CDX is not available for this project.");
@@ -2325,9 +3599,30 @@
2325
3599
  throw new Error(data.error || "Unable to load CDX report.");
2326
3600
  }
2327
3601
  setDocument("CDX run report", renderCdxReport(data.payload));
3602
+ cdxCloseTarget = { type: "cdx-runs" };
2328
3603
  setMeta("CDX report loaded.");
2329
3604
  }
2330
3605
 
3606
+ async function openCdxArtifact(path) {
3607
+ if (!path) {
3608
+ return;
3609
+ }
3610
+ setMeta("Loading CDX log...");
3611
+ const response = await fetch("/api/file-preview", {
3612
+ method: "POST",
3613
+ headers: { "Content-Type": "application/json" },
3614
+ body: JSON.stringify({ path })
3615
+ });
3616
+ const data = await response.json();
3617
+ if (!response.ok || !data.ok) {
3618
+ throw new Error(data.error || "Unable to load CDX artifact.");
3619
+ }
3620
+ const reportSnapshot = currentDocumentSnapshot("CDX run report");
3621
+ setDocument(data.payload?.name ? `CDX log · ${data.payload.name}` : "CDX log", renderCdxLogPreview(data.payload));
3622
+ cdxCloseTarget = { type: "cdx-report", title: reportSnapshot.title, html: reportSnapshot.html };
3623
+ setMeta(`Loaded ${data.payload?.path || path}.`);
3624
+ }
3625
+
2331
3626
  async function createRequestFromCdxReport(runId) {
2332
3627
  if (!runId) {
2333
3628
  return;
@@ -2370,11 +3665,26 @@
2370
3665
  const run = payload.run && typeof payload.run === "object" ? payload.run : null;
2371
3666
  const jobs = Array.isArray(payload.jobs) ? payload.jobs : [];
2372
3667
  const state = payload.badgeState || run?.badgeState || payload.state || "unknown";
3668
+ const matchLabel = run?.matchSource === "head-active"
3669
+ ? "Current HEAD running"
3670
+ : run?.matchSource === "head-failing"
3671
+ ? "Current HEAD failing"
3672
+ : run?.matchSource === "head-cancelled"
3673
+ ? "Current HEAD cancelled"
3674
+ : run?.matchSource === "head-unknown"
3675
+ ? "Current HEAD unknown"
3676
+ : run?.matchSource === "head"
3677
+ ? "Current HEAD"
3678
+ : run?.matchSource === "branch-active"
3679
+ ? "Branch running"
3680
+ : run?.matchSource === "branch-failing"
3681
+ ? "Branch failing"
3682
+ : "Latest branch run";
2373
3683
  const cards = renderMetricCards([
2374
3684
  ["State", ciBadgeLabel(state)],
2375
3685
  ["Branch", run?.branch || payload.branch || "Unknown"],
2376
3686
  ["Commit", (run?.headSha || payload.headSha || "").slice(0, 7) || "Unknown"],
2377
- ["Match", run?.matchSource === "head" ? "Current HEAD" : "Latest branch run"]
3687
+ ["Match", matchLabel]
2378
3688
  ]);
2379
3689
  const runUrl = run?.htmlUrl ? `<a class="viewer-ci__link" href="${escapeHtml(run.htmlUrl)}" target="_blank" rel="noreferrer">Open in GitHub</a>` : "";
2380
3690
  const runRows = run ? [
@@ -2444,6 +3754,15 @@
2444
3754
  if (!response.ok || !data.ok) {
2445
3755
  throw new Error(data.error || "Unable to load CI status.");
2446
3756
  }
3757
+ const nextCiSignature = runtimeStatusSignature(data.payload);
3758
+ if (options.skipUnchanged && !options.force && latestCiStatusSignature && nextCiSignature === latestCiStatusSignature) {
3759
+ updateMainCiBadge(data.payload);
3760
+ if (!options.silent) {
3761
+ setMeta(`Checked CI status just now · no changes (${new Date().toLocaleTimeString()})`);
3762
+ }
3763
+ return;
3764
+ }
3765
+ latestCiStatusSignature = nextCiSignature;
2447
3766
  updateMainCiBadge(data.payload);
2448
3767
  setDocument("CI status", renderCiStatus(data.payload));
2449
3768
  setMeta(options.silent ? "CI status refreshed." : "CI status loaded.");
@@ -2463,17 +3782,20 @@
2463
3782
  const deletedCount = Number(counts.deleted || 0);
2464
3783
  const renamedCount = Number(counts.renamed || 0);
2465
3784
  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);
3785
+ const cards = [
3786
+ renderGitSummaryCard("Branch", payload.branch || "HEAD"),
3787
+ renderGitSummaryCard("Tracking", payload.tracking || "None"),
3788
+ renderGitSummarySegments("Ahead / Behind", [
3789
+ ["Ahead", payload.ahead || 0],
3790
+ ["Behind", payload.behind || 0]
3791
+ ]),
3792
+ renderGitSummaryCard("State", payload.clean ? "Clean" : "Dirty"),
3793
+ renderGitSummarySegments("Files", [
3794
+ ["Staged", stagedCount],
3795
+ ["Worktree", modifiedCount + deletedCount + renamedCount],
3796
+ ["Untracked", untrackedCount]
3797
+ ])
3798
+ ].join("");
2477
3799
  const groupDefs = [
2478
3800
  ["staged", "Staged", "staged"],
2479
3801
  ["modified", "Modified", "worktree"],
@@ -2486,7 +3808,7 @@
2486
3808
  ["staged", "Staged", stagedCount],
2487
3809
  ["worktree", "Worktree", modifiedCount + deletedCount + renamedCount],
2488
3810
  ["untracked", "Untracked", untrackedCount],
2489
- ["history", "History", Array.isArray(payload.recentCommits) ? payload.recentCommits.length : (payload.latestCommit ? 1 : 0)],
3811
+ ["history", "History", formatGitHistoryCount(payload)],
2490
3812
  ["remote", "Remote", payload.tracking ? 1 : 0]
2491
3813
  ];
2492
3814
  const domains = domainDefs.map(([key, label, count], index) => `
@@ -2528,6 +3850,7 @@
2528
3850
  const untrackedSections = renderFileSections(["untracked"]);
2529
3851
  const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
2530
3852
  const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
3853
+ const historyCount = formatGitHistoryCount(payload);
2531
3854
  const renderGitHistoryReveal = (hiddenCount) => {
2532
3855
  if (hiddenCount <= 0) {
2533
3856
  return "";
@@ -2565,7 +3888,7 @@
2565
3888
  return `
2566
3889
  <div class="viewer-git">
2567
3890
  <div class="viewer-git__summary">${cards}</div>
2568
- <div class="viewer-git__workspace">
3891
+ <div class="viewer-git__workspace has-diff-detail">
2569
3892
  <nav class="viewer-git__domains" aria-label="Git domains">${domains}</nav>
2570
3893
  <div class="viewer-git__content" aria-label="Git domain content">
2571
3894
  <section class="viewer-git__panel" data-viewer-git-panel="changes">
@@ -2586,7 +3909,7 @@
2586
3909
  ${untrackedSections || '<p class="viewer-git__state">No untracked files.</p>'}
2587
3910
  </section>
2588
3911
  <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>
3912
+ <header class="viewer-git__panel-header"><span>History</span><strong>${escapeHtml(historyCount)} commits</strong></header>
2590
3913
  ${history}
2591
3914
  </section>
2592
3915
  <section class="viewer-git__panel" data-viewer-git-panel="remote" hidden>
@@ -2594,7 +3917,7 @@
2594
3917
  ${remote}
2595
3918
  </section>
2596
3919
  </div>
2597
- <section class="viewer-git__detail" aria-label="Git diff">
3920
+ <section class="viewer-git__detail" aria-label="Git diff" data-viewer-git-detail>
2598
3921
  <div class="viewer-git__detail-title">Diff preview</div>
2599
3922
  <div class="viewer-git__diff" data-viewer-git-diff>Select a changed file to preview its diff.</div>
2600
3923
  </section>
@@ -2603,6 +3926,11 @@
2603
3926
  `;
2604
3927
  }
2605
3928
 
3929
+ function formatGitHistoryCount(payload) {
3930
+ const count = Array.isArray(payload?.recentCommits) ? payload.recentCommits.length : (payload?.latestCommit ? 1 : 0);
3931
+ return `${count}${payload?.recentCommitsHasMore ? "+" : ""}`;
3932
+ }
3933
+
2606
3934
  function setActiveGitFile(button) {
2607
3935
  document.querySelectorAll("[data-viewer-git-file]").forEach((node) => {
2608
3936
  if (node instanceof HTMLElement) {
@@ -2636,12 +3964,16 @@
2636
3964
 
2637
3965
  async function loadGitDiff(path, cached, button = null) {
2638
3966
  const diffPanel = document.querySelector("[data-viewer-git-diff]");
3967
+ const detailTitle = document.querySelector("[data-viewer-git-detail] .viewer-git__detail-title");
2639
3968
  if (!(diffPanel instanceof HTMLElement) || !path) {
2640
3969
  return;
2641
3970
  }
2642
3971
  if (button instanceof HTMLElement) {
2643
3972
  setActiveGitFile(button);
2644
3973
  }
3974
+ if (detailTitle instanceof HTMLElement) {
3975
+ detailTitle.textContent = "Diff preview";
3976
+ }
2645
3977
  diffPanel.textContent = "Loading diff...";
2646
3978
  const params = new URLSearchParams({ path });
2647
3979
  if (cached) {
@@ -2654,12 +3986,38 @@
2654
3986
  diffPanel.textContent = payload.message || data.error || "Unable to load diff.";
2655
3987
  return;
2656
3988
  }
2657
- const content = payload.diff || payload.message || "No diff is available for this file.";
3989
+ const content = payload.diff || "";
3990
+ if (!content.trim()) {
3991
+ await loadGitFilePreview(path, diffPanel, detailTitle);
3992
+ return;
3993
+ }
2658
3994
  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
3995
  }
2660
3996
 
3997
+ async function loadGitFilePreview(path, diffPanel, detailTitle = null) {
3998
+ if (detailTitle instanceof HTMLElement) {
3999
+ detailTitle.textContent = "File preview";
4000
+ }
4001
+ diffPanel.textContent = "Loading file preview...";
4002
+ const response = await fetch(`/api/git-file-preview?${new URLSearchParams({ path }).toString()}`);
4003
+ const data = await response.json();
4004
+ const payload = data.payload || {};
4005
+ if (!response.ok || !data.ok) {
4006
+ diffPanel.textContent = data.error || "Unable to load file preview.";
4007
+ return;
4008
+ }
4009
+ if (payload.state !== "ok") {
4010
+ 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>`;
4011
+ return;
4012
+ }
4013
+ const content = payload.content || "";
4014
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · file preview${payload.truncated ? " · truncated" : ""}</div><pre><code>${renderGitDiffPreview(content)}</code></pre>`;
4015
+ }
4016
+
2661
4017
  function applyGitDomain(domain) {
2662
4018
  const selected = domain || "changes";
4019
+ const diffDomains = new Set(["changes", "staged", "worktree", "untracked"]);
4020
+ const showDiffDetail = diffDomains.has(selected);
2663
4021
  document.querySelectorAll(".viewer-git__domain[data-viewer-git-domain]").forEach((node) => {
2664
4022
  if (node instanceof HTMLElement) {
2665
4023
  const active = node.getAttribute("data-viewer-git-domain") === selected;
@@ -2672,6 +4030,16 @@
2672
4030
  node.hidden = node.getAttribute("data-viewer-git-panel") !== selected;
2673
4031
  }
2674
4032
  });
4033
+ document.querySelectorAll(".viewer-git__workspace").forEach((node) => {
4034
+ if (node instanceof HTMLElement) {
4035
+ node.classList.toggle("has-diff-detail", showDiffDetail);
4036
+ }
4037
+ });
4038
+ document.querySelectorAll("[data-viewer-git-detail]").forEach((node) => {
4039
+ if (node instanceof HTMLElement) {
4040
+ node.hidden = !showDiffDetail;
4041
+ }
4042
+ });
2675
4043
  }
2676
4044
 
2677
4045
  function currentGitViewState() {
@@ -2721,6 +4089,16 @@
2721
4089
  if (!response.ok || !data.ok) {
2722
4090
  throw new Error(data.error || "Unable to load Git status.");
2723
4091
  }
4092
+ const nextGitSignature = gitStatusSignature(data.payload);
4093
+ if (options.skipUnchanged && !options.force && latestGitStatusSignature && nextGitSignature === latestGitStatusSignature) {
4094
+ setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
4095
+ updateMainGitBadges();
4096
+ if (!options.silent) {
4097
+ setMeta(`Checked Git status just now · no changes (${new Date().toLocaleTimeString()})`);
4098
+ }
4099
+ return;
4100
+ }
4101
+ latestGitStatusSignature = nextGitSignature;
2724
4102
  setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
2725
4103
  updateMainGitBadges();
2726
4104
  setDocument("Git status", renderGitStatus(data.payload));
@@ -2744,7 +4122,7 @@
2744
4122
  return;
2745
4123
  }
2746
4124
  if (message.type === "refresh") {
2747
- refreshViewer("POST").catch((error) => setMeta(error.message));
4125
+ refreshViewer("POST", { force: Boolean(message.force) }).catch((error) => setMeta(error.message));
2748
4126
  return;
2749
4127
  }
2750
4128
  if (message.type === "bootstrap-logics") {
@@ -2782,7 +4160,13 @@
2782
4160
  [document.getElementById("viewer-insights")].forEach((button) => {
2783
4161
  button?.addEventListener("click", () => {
2784
4162
  setRefreshMenuOpen(false);
2785
- showCorpusInsights().catch((error) => setMeta(error.message));
4163
+ withPrimaryAction("insights", "Loading insights", showCorpusInsights);
4164
+ });
4165
+ });
4166
+ [workspaceButton()].forEach((button) => {
4167
+ button?.addEventListener("click", () => {
4168
+ setRefreshMenuOpen(false);
4169
+ withPrimaryAction("workspace", "Loading Explorer", showWorkspace);
2786
4170
  });
2787
4171
  });
2788
4172
  const autoControl = autoRefreshControl();
@@ -2825,30 +4209,30 @@
2825
4209
  if (!(element instanceof HTMLElement)) {
2826
4210
  return;
2827
4211
  }
2828
- element.addEventListener("click", () => {
4212
+ element.addEventListener("click", (event) => {
2829
4213
  setRefreshMenuOpen(false);
2830
- refreshViewer("POST").catch((error) => setMeta(error.message));
4214
+ withPrimaryAction("refresh", "Refreshing", () => refreshViewer("POST", { force: Boolean(event.shiftKey) }));
2831
4215
  });
2832
4216
  });
2833
4217
  document.getElementById("viewer-health")?.addEventListener("click", () => {
2834
4218
  setRefreshMenuOpen(false);
2835
- showHealth().catch((error) => setMeta(error.message));
4219
+ withPrimaryAction("health", "Checking health", showHealth);
2836
4220
  });
2837
4221
  document.getElementById("viewer-git")?.addEventListener("click", () => {
2838
- showGitStatus().catch((error) => setMeta(error.message));
4222
+ withPrimaryAction("git", "Checking Git status", showGitStatus);
2839
4223
  });
2840
4224
  ciButton()?.addEventListener("click", () => {
2841
- showCiStatus().catch((error) => setMeta(error.message));
4225
+ withPrimaryAction("ci", "Checking CI status", showCiStatus);
2842
4226
  });
2843
4227
  document.getElementById("viewer-cdx")?.addEventListener("click", () => {
2844
- showCdxStatus().catch((error) => setMeta(error.message));
4228
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
2845
4229
  });
2846
4230
  repoPill()?.addEventListener("click", () => {
2847
4231
  const menu = projectMenu();
2848
4232
  setProjectMenuOpen(Boolean(menu?.hidden));
2849
4233
  });
2850
4234
  repoFolderButton()?.addEventListener("click", () => {
2851
- openRepositoryFolder().catch((error) => setMeta(error.message));
4235
+ withPrimaryAction("open-repo-folder", "Opening repository folder", openRepositoryFolder);
2852
4236
  });
2853
4237
  activityClearControl()?.addEventListener("click", () => {
2854
4238
  clearActivityHistory();
@@ -2873,9 +4257,50 @@
2873
4257
  const editButton = editDocumentButton();
2874
4258
  if (editButton instanceof HTMLElement) {
2875
4259
  editButton.addEventListener("click", () => {
2876
- editDocument(selectedItem()).catch((error) => setMeta(error.message));
4260
+ withPrimaryAction("edit-document", "Opening document", () => editDocument(selectedItem()));
2877
4261
  });
2878
4262
  }
4263
+ document.addEventListener("change", (event) => {
4264
+ const sessionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-session]") : null;
4265
+ const cdxInputTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-input]") : null;
4266
+ const cdxColumnTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-column]") : null;
4267
+ const cdxProviderTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider]") : null;
4268
+ if (sessionTarget instanceof HTMLSelectElement) {
4269
+ latestCdxMissionState.sessionId = sessionTarget.value || "";
4270
+ latestCdxMissionState.planPayload = null;
4271
+ latestCdxMissionState.runPayload = null;
4272
+ latestCdxMissionState.applyPayload = null;
4273
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
4274
+ }
4275
+ if (cdxInputTarget instanceof HTMLInputElement || cdxInputTarget instanceof HTMLTextAreaElement) {
4276
+ const key = cdxInputTarget.getAttribute("data-viewer-cdx-input") || "";
4277
+ if (key) {
4278
+ latestCdxMissionState.missionInputs[key] = cdxInputTarget instanceof HTMLInputElement && cdxInputTarget.type === "checkbox" ? (cdxInputTarget.checked ? "true" : "false") : (cdxInputTarget.value || "");
4279
+ latestCdxMissionState.planPayload = null;
4280
+ latestCdxMissionState.runPayload = null;
4281
+ latestCdxMissionState.applyPayload = null;
4282
+ }
4283
+ }
4284
+ if (cdxColumnTarget instanceof HTMLInputElement) {
4285
+ persistCdxColumnVisibility(cdxColumnTarget.getAttribute("data-viewer-cdx-column") || "", cdxColumnTarget.checked);
4286
+ rerenderCdxStatusFromPreferences();
4287
+ }
4288
+ if (cdxProviderTarget instanceof HTMLInputElement) {
4289
+ const provider = cdxProviderTarget.getAttribute("data-viewer-cdx-provider") || "";
4290
+ const status = latestCdxStatusPayload?.status || {};
4291
+ const allProviders = cdxKnownProviders(status, cdxProviders(status), cdxSessions(status));
4292
+ const current = cdxProviderFilterPreference();
4293
+ const selected = new Set(current.mode === "subset" ? current.selected : allProviders);
4294
+ if (cdxProviderTarget.checked) {
4295
+ selected.add(provider);
4296
+ } else {
4297
+ selected.delete(provider);
4298
+ }
4299
+ const nextSelected = Array.from(selected).filter((entry) => allProviders.includes(entry));
4300
+ persistCdxProviderFilter(nextSelected.length === allProviders.length ? { mode: "all", selected: [] } : { mode: "subset", selected: nextSelected });
4301
+ rerenderCdxStatusFromPreferences();
4302
+ }
4303
+ });
2879
4304
  document.addEventListener("click", (event) => {
2880
4305
  window.setTimeout(() => applyLocalViewerChrome(), 0);
2881
4306
  const target = event.target instanceof Element ? event.target.closest("[data-viewer-doc-path]") : null;
@@ -2885,28 +4310,92 @@
2885
4310
  const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
2886
4311
  const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
2887
4312
  const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
4313
+ const workspaceTreeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-tree]") : null;
4314
+ const workspacePreviewTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-preview]") : null;
2888
4315
  const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
2889
4316
  const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
2890
4317
  const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
4318
+ const cdxBackRunsTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-back-runs]") : null;
2891
4319
  const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
4320
+ const cdxArtifactTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-artifact-path]") : null;
4321
+ const cdxProviderAllTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider-all]") : null;
2892
4322
  const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
4323
+ const cdxMissionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mission]") : null;
4324
+ const cdxStrengthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-strength]") : null;
4325
+ const cdxPlanTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-plan]") : null;
4326
+ const cdxRunTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-run]") : null;
4327
+ const cdxApplyPlanTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-apply-plan]") : null;
4328
+ if (cdxMissionTarget instanceof HTMLElement) {
4329
+ latestCdxMissionState.missionId = cdxMissionTarget.getAttribute("data-viewer-cdx-mission") || "full-audit";
4330
+ latestCdxMissionState.planPayload = null;
4331
+ latestCdxMissionState.runPayload = null;
4332
+ latestCdxMissionState.applyPayload = null;
4333
+ latestCdxMissionState.missionInputs = {};
4334
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
4335
+ return;
4336
+ }
4337
+ if (cdxStrengthTarget instanceof HTMLElement) {
4338
+ latestCdxMissionState.strengthId = cdxStrengthTarget.getAttribute("data-viewer-cdx-strength") || "standard";
4339
+ latestCdxMissionState.planPayload = null;
4340
+ latestCdxMissionState.runPayload = null;
4341
+ latestCdxMissionState.applyPayload = null;
4342
+ setDocument("CDX missions", renderCdxMissions(latestCdxMissionState.statusPayload));
4343
+ return;
4344
+ }
4345
+ if (cdxPlanTarget instanceof HTMLElement) {
4346
+ withCdxMissionAction("cdx-plan", "Building CDX mission plan", previewCdxMission);
4347
+ return;
4348
+ }
4349
+ if (cdxRunTarget instanceof HTMLElement) {
4350
+ withCdxMissionAction("cdx-run", "Launching CDX mission", launchCdxMission);
4351
+ return;
4352
+ }
4353
+ if (cdxApplyPlanTarget instanceof HTMLElement) {
4354
+ withCdxMissionAction("cdx-apply-plan", "Applying CDX mission plan", applyCdxMissionPlan);
4355
+ return;
4356
+ }
4357
+ if (cdxProviderAllTarget instanceof HTMLElement) {
4358
+ persistCdxProviderFilter({ mode: "all", selected: [] });
4359
+ rerenderCdxStatusFromPreferences();
4360
+ return;
4361
+ }
4362
+ if (cdxBackRunsTarget instanceof HTMLElement) {
4363
+ withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
4364
+ return;
4365
+ }
2893
4366
  if (cdxReportTarget instanceof HTMLElement) {
2894
- showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || "").catch((error) => setMeta(error.message));
4367
+ withPrimaryAction("cdx-report", "Loading CDX report", () => showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || ""));
4368
+ return;
4369
+ }
4370
+ if (cdxArtifactTarget instanceof HTMLElement) {
4371
+ withPrimaryAction("cdx-artifact", "Opening CDX artifact", () => openCdxArtifact(cdxArtifactTarget.getAttribute("data-viewer-cdx-artifact-path") || ""));
2895
4372
  return;
2896
4373
  }
2897
4374
  if (cdxCreateRequestTarget instanceof HTMLElement) {
2898
- createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || "").catch((error) => setMeta(error.message));
4375
+ withPrimaryAction("cdx-create-request", "Creating Logics request", () => createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || ""));
2899
4376
  return;
2900
4377
  }
2901
4378
  if (cdxModeTarget instanceof HTMLElement) {
2902
4379
  const mode = cdxModeTarget.getAttribute("data-viewer-cdx-mode") || "status";
2903
4380
  if (mode === "runs") {
2904
- showCdxRuns().catch((error) => setMeta(error.message));
4381
+ withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
4382
+ } else if (mode === "missions") {
4383
+ withPrimaryAction("cdx-missions", "Loading CDX missions", showCdxMissions);
2905
4384
  } else {
2906
- showCdxStatus().catch((error) => setMeta(error.message));
4385
+ withPrimaryAction("cdx", "Checking CDX status", showCdxStatus);
2907
4386
  }
2908
4387
  return;
2909
4388
  }
4389
+ if (workspaceTreeTarget instanceof HTMLElement) {
4390
+ event.preventDefault();
4391
+ withPrimaryAction("workspace-tree", "Loading Explorer folder", () => openWorkspaceTree(workspaceTreeTarget.getAttribute("data-viewer-workspace-tree") || ""));
4392
+ return;
4393
+ }
4394
+ if (workspacePreviewTarget instanceof HTMLElement) {
4395
+ event.preventDefault();
4396
+ withPrimaryAction("workspace-preview", "Loading Explorer preview", () => openWorkspacePreview(workspacePreviewTarget.getAttribute("data-viewer-workspace-preview") || ""));
4397
+ return;
4398
+ }
2910
4399
  if (projectSwitcherTarget instanceof HTMLElement) {
2911
4400
  const menu = projectMenu();
2912
4401
  setProjectMenuOpen(Boolean(menu?.hidden));
@@ -2914,7 +4403,7 @@
2914
4403
  }
2915
4404
  if (projectTarget instanceof HTMLElement) {
2916
4405
  event.preventDefault();
2917
- switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || "").catch((error) => setMeta(error.message));
4406
+ withPrimaryAction("switch-project", "Switching project", () => switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || ""));
2918
4407
  return;
2919
4408
  }
2920
4409
  if (gitHistoryRevealTarget instanceof HTMLElement) {
@@ -2966,7 +4455,7 @@
2966
4455
  return;
2967
4456
  }
2968
4457
  if (healthTarget instanceof HTMLElement) {
2969
- showHealth().catch((error) => setMeta(error.message));
4458
+ withPrimaryAction("health", "Checking health", showHealth);
2970
4459
  return;
2971
4460
  }
2972
4461
  if (filterTarget instanceof HTMLElement) {
@@ -2976,14 +4465,11 @@
2976
4465
  }
2977
4466
  const path = target instanceof HTMLElement ? target.getAttribute("data-viewer-doc-path") : "";
2978
4467
  if (path) {
2979
- showDocumentByPath(path).catch((error) => setMeta(error.message));
4468
+ withPrimaryAction("read-document", "Loading document", () => showDocumentByPath(path));
2980
4469
  }
2981
4470
  });
2982
4471
  document.getElementById("viewer-document-close")?.addEventListener("click", () => {
2983
- const panel = documentPanel();
2984
- if (panel) {
2985
- panel.hidden = true;
2986
- }
4472
+ withPrimaryAction("close-document", "Closing preview", closeDocumentPanel);
2987
4473
  });
2988
4474
  startAutoRefresh();
2989
4475
  });