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