@grifhinz/logics-manager 2.8.0 → 2.9.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 +22 -7
- package/VERSION +1 -1
- package/clients/shared-web/media/vendor/xterm/PROVENANCE.md +31 -0
- package/clients/shared-web/media/vendor/xterm/xterm-addon-fit.js +2 -0
- package/clients/shared-web/media/vendor/xterm/xterm-addon-web-links.js +2 -0
- package/clients/shared-web/media/vendor/xterm/xterm.css +209 -0
- package/clients/shared-web/media/vendor/xterm/xterm.js +2 -0
- package/clients/viewer/browser-host.js +1302 -33
- package/clients/viewer/index.html +15 -0
- package/clients/viewer/viewer.css +938 -70
- package/logics_manager/cli.py +17 -17
- package/logics_manager/viewer.py +1370 -30
- package/package.json +2 -1
- package/pyproject.toml +1 -1
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const stateKey = "logics.localViewer.state";
|
|
3
|
+
const preferenceKey = "logics.localViewer.preferences.v1";
|
|
4
|
+
const lanTokenKey = "logics.lan.token";
|
|
5
|
+
|
|
6
|
+
function captureLanTokenFromUrl() {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(window.location.href);
|
|
9
|
+
const queryToken = url.searchParams.get("t");
|
|
10
|
+
if (queryToken) {
|
|
11
|
+
window.sessionStorage.setItem(lanTokenKey, queryToken);
|
|
12
|
+
url.searchParams.delete("t");
|
|
13
|
+
const cleaned = `${url.pathname}${url.search}${url.hash}`;
|
|
14
|
+
window.history.replaceState(null, "", cleaned || "/");
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// sessionStorage / history may be unavailable in some embed contexts.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getLanToken() {
|
|
22
|
+
try {
|
|
23
|
+
return window.sessionStorage.getItem(lanTokenKey) || "";
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
captureLanTokenFromUrl();
|
|
30
|
+
|
|
31
|
+
const originalFetch = window.fetch.bind(window);
|
|
32
|
+
window.fetch = (input, init) => {
|
|
33
|
+
const token = getLanToken();
|
|
34
|
+
if (!token) return originalFetch(input, init);
|
|
35
|
+
const next = init ? { ...init } : {};
|
|
36
|
+
const headers = new Headers(next.headers || (input instanceof Request ? input.headers : undefined));
|
|
37
|
+
if (!headers.has("Authorization")) headers.set("Authorization", `Bearer ${token}`);
|
|
38
|
+
next.headers = headers;
|
|
39
|
+
return originalFetch(input, next);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (typeof window.EventSource === "function") {
|
|
43
|
+
const NativeEventSource = window.EventSource;
|
|
44
|
+
window.EventSource = function PatchedEventSource(url, init) {
|
|
45
|
+
const token = getLanToken();
|
|
46
|
+
if (!token || typeof url !== "string") {
|
|
47
|
+
return new NativeEventSource(url, init);
|
|
48
|
+
}
|
|
49
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
50
|
+
const tokenized = `${url}${separator}t=${encodeURIComponent(token)}`;
|
|
51
|
+
return new NativeEventSource(tokenized, init);
|
|
52
|
+
};
|
|
53
|
+
window.EventSource.prototype = NativeEventSource.prototype;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const preferenceVersion = 1;
|
|
3
57
|
const meta = () => document.getElementById("viewer-meta");
|
|
4
58
|
const documentPanel = () => document.getElementById("viewer-document");
|
|
5
59
|
const documentTitle = () => document.getElementById("viewer-document-title");
|
|
@@ -16,6 +70,8 @@
|
|
|
16
70
|
const projectMenu = () => document.getElementById("viewer-project-menu");
|
|
17
71
|
const repoGithubLink = () => document.getElementById("viewer-repo-github");
|
|
18
72
|
const repoFolderButton = () => document.getElementById("viewer-repo-folder");
|
|
73
|
+
const workspaceButton = () => document.getElementById("viewer-workspace");
|
|
74
|
+
const workshopButton = () => document.getElementById("viewer-workshop");
|
|
19
75
|
const ciButton = () => document.getElementById("viewer-ci");
|
|
20
76
|
const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
|
|
21
77
|
const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
|
|
@@ -72,10 +128,27 @@
|
|
|
72
128
|
let latestViewerStateSignature = "";
|
|
73
129
|
let latestGitStatusSignature = "";
|
|
74
130
|
let latestCdxStatusSignature = "";
|
|
131
|
+
let latestCdxStatusPayload = null;
|
|
75
132
|
let latestCiStatusSignature = "";
|
|
76
133
|
let primaryActionBusyKey = "";
|
|
77
134
|
let cdxMissionBusyKey = "";
|
|
78
135
|
let cdxCloseTarget = null;
|
|
136
|
+
let viewerPreferences = readViewerPreferences();
|
|
137
|
+
let autoRefreshIntervalForcedByLaunch = false;
|
|
138
|
+
const cdxStatusColumns = [
|
|
139
|
+
{ id: "session", label: "SESSION" },
|
|
140
|
+
{ id: "provider", label: "PROV." },
|
|
141
|
+
{ id: "status", label: "STATUS" },
|
|
142
|
+
{ id: "auth", label: "AUTH" },
|
|
143
|
+
{ id: "ok", label: "OK" },
|
|
144
|
+
{ id: "remaining5h", label: "5H" },
|
|
145
|
+
{ id: "remainingWeek", label: "WEEK" },
|
|
146
|
+
{ id: "block", label: "BLOCK", defaultVisible: false },
|
|
147
|
+
{ id: "credits", label: "CR", defaultVisible: false },
|
|
148
|
+
{ id: "reset5h", label: "RESET 5H" },
|
|
149
|
+
{ id: "resetWeek", label: "RESET WEEK" },
|
|
150
|
+
{ id: "updated", label: "UPDATED" }
|
|
151
|
+
];
|
|
79
152
|
|
|
80
153
|
function readStoredState() {
|
|
81
154
|
try {
|
|
@@ -85,6 +158,83 @@
|
|
|
85
158
|
}
|
|
86
159
|
}
|
|
87
160
|
|
|
161
|
+
function readViewerPreferences() {
|
|
162
|
+
try {
|
|
163
|
+
const value = JSON.parse(window.localStorage.getItem(preferenceKey) || "null");
|
|
164
|
+
if (!value || typeof value !== "object" || value.version !== preferenceVersion) {
|
|
165
|
+
return { version: preferenceVersion };
|
|
166
|
+
}
|
|
167
|
+
return { ...value, version: preferenceVersion };
|
|
168
|
+
} catch {
|
|
169
|
+
return { version: preferenceVersion };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function writeViewerPreferences(nextPreferences) {
|
|
174
|
+
viewerPreferences = { ...nextPreferences, version: preferenceVersion };
|
|
175
|
+
try {
|
|
176
|
+
window.localStorage.setItem(preferenceKey, JSON.stringify(viewerPreferences));
|
|
177
|
+
} catch {
|
|
178
|
+
// Keep the in-memory preference for this session when browser storage is unavailable.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function updateViewerPreferences(patch) {
|
|
183
|
+
writeViewerPreferences({ ...viewerPreferences, ...patch });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function preferredAutoRefreshIntervalSeconds() {
|
|
187
|
+
const seconds = Number(viewerPreferences.autoRefreshIntervalSeconds);
|
|
188
|
+
return Number.isFinite(seconds) && seconds > 0 ? normalizeAutoRefreshIntervalSeconds(seconds) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cdxColumnVisibilityPreference() {
|
|
192
|
+
const stored = viewerPreferences.cdxStatusColumns;
|
|
193
|
+
const storedVisibility = stored && typeof stored === "object" ? stored.visibility : null;
|
|
194
|
+
const visibility = {};
|
|
195
|
+
cdxStatusColumns.forEach((column) => {
|
|
196
|
+
visibility[column.id] = column.defaultVisible !== false;
|
|
197
|
+
if (storedVisibility && typeof storedVisibility[column.id] === "boolean") {
|
|
198
|
+
visibility[column.id] = storedVisibility[column.id];
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return visibility;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function persistCdxColumnVisibility(columnId, visible) {
|
|
205
|
+
const current = cdxColumnVisibilityPreference();
|
|
206
|
+
if (!cdxStatusColumns.some((column) => column.id === columnId)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
updateViewerPreferences({
|
|
210
|
+
cdxStatusColumns: {
|
|
211
|
+
visibility: { ...current, [columnId]: Boolean(visible) }
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function cdxProviderFilterPreference() {
|
|
217
|
+
const stored = viewerPreferences.cdxStatusProviders;
|
|
218
|
+
if (!stored || typeof stored !== "object" || stored.mode !== "subset") {
|
|
219
|
+
return { mode: "all", selected: [] };
|
|
220
|
+
}
|
|
221
|
+
const selected = Array.isArray(stored.selected)
|
|
222
|
+
? stored.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
223
|
+
: [];
|
|
224
|
+
return selected.length ? { mode: "subset", selected: Array.from(new Set(selected)) } : { mode: "all", selected: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function persistCdxProviderFilter(nextFilter) {
|
|
228
|
+
const selected = Array.isArray(nextFilter?.selected)
|
|
229
|
+
? nextFilter.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
230
|
+
: [];
|
|
231
|
+
updateViewerPreferences({
|
|
232
|
+
cdxStatusProviders: selected.length
|
|
233
|
+
? { mode: "subset", selected: Array.from(new Set(selected)).sort() }
|
|
234
|
+
: { mode: "all", selected: [] }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
88
238
|
function sanitizeViewerFilterState(value) {
|
|
89
239
|
const nextState = { ...defaultFilterState };
|
|
90
240
|
if (!value || typeof value !== "object") {
|
|
@@ -155,6 +305,7 @@
|
|
|
155
305
|
return Array.from(document.querySelectorAll([
|
|
156
306
|
"#viewer-insights",
|
|
157
307
|
"#viewer-health",
|
|
308
|
+
"#viewer-workspace",
|
|
158
309
|
"#viewer-git",
|
|
159
310
|
"#viewer-ci",
|
|
160
311
|
"#viewer-cdx",
|
|
@@ -452,6 +603,7 @@
|
|
|
452
603
|
autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(value) * 1000;
|
|
453
604
|
if (options.user) {
|
|
454
605
|
autoRefreshIntervalTouched = true;
|
|
606
|
+
updateViewerPreferences({ autoRefreshIntervalSeconds: Math.round(autoRefreshIntervalMs / 1000) });
|
|
455
607
|
}
|
|
456
608
|
updateRefreshIntervalControl();
|
|
457
609
|
scheduleNextAutoRefresh();
|
|
@@ -582,10 +734,64 @@
|
|
|
582
734
|
setMeta(created > 0 ? `Logics bootstrapped · ${created} paths created.` : "Logics bootstrap checked.");
|
|
583
735
|
}
|
|
584
736
|
|
|
737
|
+
let latestLanShareUrl = "";
|
|
738
|
+
|
|
739
|
+
async function copyTextToClipboard(text) {
|
|
740
|
+
if (!text) return false;
|
|
741
|
+
try {
|
|
742
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
743
|
+
await navigator.clipboard.writeText(text);
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
} catch { /* fall through to legacy path */ }
|
|
747
|
+
try {
|
|
748
|
+
const textarea = document.createElement("textarea");
|
|
749
|
+
textarea.value = text;
|
|
750
|
+
textarea.setAttribute("readonly", "");
|
|
751
|
+
textarea.style.position = "fixed";
|
|
752
|
+
textarea.style.top = "0";
|
|
753
|
+
textarea.style.left = "0";
|
|
754
|
+
textarea.style.opacity = "0";
|
|
755
|
+
textarea.style.pointerEvents = "none";
|
|
756
|
+
document.body.appendChild(textarea);
|
|
757
|
+
textarea.focus();
|
|
758
|
+
textarea.select();
|
|
759
|
+
textarea.setSelectionRange(0, text.length);
|
|
760
|
+
const ok = document.execCommand("copy");
|
|
761
|
+
document.body.removeChild(textarea);
|
|
762
|
+
return ok;
|
|
763
|
+
} catch {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function applyLanBanner(active, shareUrl) {
|
|
769
|
+
const banner = document.getElementById("viewer-lan-banner");
|
|
770
|
+
if (!(banner instanceof HTMLElement)) return;
|
|
771
|
+
banner.hidden = !active;
|
|
772
|
+
latestLanShareUrl = active ? String(shareUrl || "") : "";
|
|
773
|
+
const urlNode = document.getElementById("viewer-lan-banner-url");
|
|
774
|
+
const copyButton = document.getElementById("viewer-lan-banner-copy");
|
|
775
|
+
if (urlNode instanceof HTMLElement) {
|
|
776
|
+
if (latestLanShareUrl) {
|
|
777
|
+
urlNode.hidden = false;
|
|
778
|
+
urlNode.textContent = latestLanShareUrl;
|
|
779
|
+
} else {
|
|
780
|
+
urlNode.hidden = true;
|
|
781
|
+
urlNode.textContent = "";
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (copyButton instanceof HTMLButtonElement) {
|
|
785
|
+
copyButton.hidden = !latestLanShareUrl;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
585
789
|
function normalizeCapabilities(payload) {
|
|
586
790
|
const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
|
|
587
791
|
return {
|
|
588
792
|
logics: capabilities.logics || { state: "ready", available: true, message: "" },
|
|
793
|
+
workspace: capabilities.workspace || { state: "ready", available: true, message: "" },
|
|
794
|
+
workshop: capabilities.workshop || { state: "missing", available: false, message: "" },
|
|
589
795
|
git: capabilities.git || { state: "ready", available: true, message: "" },
|
|
590
796
|
ci: capabilities.ci || { state: "ready", available: true, message: "" },
|
|
591
797
|
cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
|
|
@@ -624,6 +830,29 @@
|
|
|
624
830
|
}
|
|
625
831
|
|
|
626
832
|
function updateCapabilityControls() {
|
|
833
|
+
const workspace = workspaceButton();
|
|
834
|
+
if (workspace instanceof HTMLElement) {
|
|
835
|
+
workspace.hidden = !isCapabilityAvailable("workspace");
|
|
836
|
+
if (isCapabilityAvailable("workspace")) {
|
|
837
|
+
setButtonAvailable(workspace, "Show file explorer");
|
|
838
|
+
} else {
|
|
839
|
+
setButtonUnavailable(workspace, capabilityMessage("workspace", "Explorer is not available for this project."));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const workshop = workshopButton();
|
|
844
|
+
if (workshop instanceof HTMLElement) {
|
|
845
|
+
const workshopAvailable = isCapabilityAvailable("workshop");
|
|
846
|
+
workshop.hidden = !workshopAvailable;
|
|
847
|
+
if (workshopAvailable) {
|
|
848
|
+
setButtonAvailable(workshop, "Show Workshop (terminals and commands)");
|
|
849
|
+
} else {
|
|
850
|
+
setButtonUnavailable(workshop, capabilityMessage("workshop", "Workshop is not available for this project."));
|
|
851
|
+
}
|
|
852
|
+
updateWorkshopBadges();
|
|
853
|
+
hydrateWorkshopTerminals();
|
|
854
|
+
}
|
|
855
|
+
|
|
627
856
|
const gitButton = document.getElementById("viewer-git");
|
|
628
857
|
if (gitButton instanceof HTMLElement) {
|
|
629
858
|
gitButton.hidden = !isCapabilityAvailable("git");
|
|
@@ -733,6 +962,42 @@
|
|
|
733
962
|
return html ? `<span class="viewer-git-badges" data-viewer-git-badges="${escapeHtml(scope)}">${html}</span>` : "";
|
|
734
963
|
}
|
|
735
964
|
|
|
965
|
+
let workshopBadgeCounts = { terminals: 0, commands: 0 };
|
|
966
|
+
|
|
967
|
+
function updateWorkshopBadges() {
|
|
968
|
+
const button = document.getElementById("viewer-workshop");
|
|
969
|
+
if (!(button instanceof HTMLElement)) return;
|
|
970
|
+
button.querySelector('[data-viewer-workshop-badges]')?.remove();
|
|
971
|
+
const { terminals, commands } = workshopBadgeCounts;
|
|
972
|
+
if (terminals <= 0 && commands <= 0) return;
|
|
973
|
+
const html = [
|
|
974
|
+
terminals > 0
|
|
975
|
+
? `<span class="viewer-git-badge viewer-git-badge--commits" title="${escapeHtml(terminals + " terminal session(s) running")}" aria-label="${escapeHtml(terminals + " terminal session(s) running")}">${escapeHtml(String(terminals))}</span>`
|
|
976
|
+
: "",
|
|
977
|
+
commands > 0
|
|
978
|
+
? `<span class="viewer-git-badge viewer-git-badge--files" title="${escapeHtml(commands + " command(s) running")}" aria-label="${escapeHtml(commands + " command(s) running")}">${escapeHtml(String(commands))}</span>`
|
|
979
|
+
: "",
|
|
980
|
+
].filter(Boolean).join("");
|
|
981
|
+
if (html) {
|
|
982
|
+
button.insertAdjacentHTML("beforeend", `<span class="viewer-git-badges" data-viewer-workshop-badges>${html}</span>`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function recomputeWorkshopBadges() {
|
|
987
|
+
const isRunning = (state) => state === "running" || state === "starting";
|
|
988
|
+
let terminals = 0;
|
|
989
|
+
for (const entry of workshopTerminalState.sessions.values()) {
|
|
990
|
+
if (isRunning(entry.state)) terminals += 1;
|
|
991
|
+
}
|
|
992
|
+
let commands = 0;
|
|
993
|
+
for (const entry of workshopCommandState.sessions.values()) {
|
|
994
|
+
if (isRunning(entry.state)) commands += 1;
|
|
995
|
+
}
|
|
996
|
+
if (workshopBadgeCounts.terminals === terminals && workshopBadgeCounts.commands === commands) return;
|
|
997
|
+
workshopBadgeCounts = { terminals, commands };
|
|
998
|
+
updateWorkshopBadges();
|
|
999
|
+
}
|
|
1000
|
+
|
|
736
1001
|
function updateMainGitBadges() {
|
|
737
1002
|
const button = document.getElementById("viewer-git");
|
|
738
1003
|
if (!(button instanceof HTMLElement)) {
|
|
@@ -1225,11 +1490,18 @@
|
|
|
1225
1490
|
latestViewerStateSignature = nextSignature;
|
|
1226
1491
|
latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
|
|
1227
1492
|
if (!autoRefreshIntervalTouched) {
|
|
1228
|
-
|
|
1493
|
+
const launchSeconds = Number(payload.autoRefreshIntervalSeconds);
|
|
1494
|
+
const preferredSeconds = preferredAutoRefreshIntervalSeconds();
|
|
1495
|
+
autoRefreshIntervalForcedByLaunch = Boolean(payload.autoRefreshIntervalForced);
|
|
1496
|
+
const nextSeconds = autoRefreshIntervalForcedByLaunch || preferredSeconds === null
|
|
1497
|
+
? launchSeconds
|
|
1498
|
+
: preferredSeconds;
|
|
1499
|
+
autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(nextSeconds) * 1000;
|
|
1229
1500
|
updateRefreshIntervalControl();
|
|
1230
1501
|
}
|
|
1231
1502
|
updateRepositoryIdentity(payload);
|
|
1232
1503
|
latestCapabilities = normalizeCapabilities(payload);
|
|
1504
|
+
applyLanBanner(Boolean(payload?.lanMode), String(payload?.lanShareUrl || ""));
|
|
1233
1505
|
updateCapabilityControls();
|
|
1234
1506
|
const payloadWithActivity = { ...payload, items: latestItems };
|
|
1235
1507
|
const nextPayload = applyFocusRequest(payloadWithActivity, { silent: Boolean(options.silent) });
|
|
@@ -1302,6 +1574,12 @@
|
|
|
1302
1574
|
return Boolean(panel && !panel.hidden && title && title.textContent === "Git status");
|
|
1303
1575
|
}
|
|
1304
1576
|
|
|
1577
|
+
function isWorkspaceOpen() {
|
|
1578
|
+
const panel = documentPanel();
|
|
1579
|
+
const title = documentTitle();
|
|
1580
|
+
return Boolean(panel && !panel.hidden && title && title.textContent === "Explorer");
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1305
1583
|
function isCdxStatusOpen() {
|
|
1306
1584
|
const panel = documentPanel();
|
|
1307
1585
|
const title = documentTitle();
|
|
@@ -1328,7 +1606,11 @@
|
|
|
1328
1606
|
|
|
1329
1607
|
async function refreshViewer(method = "POST", options = {}) {
|
|
1330
1608
|
const changed = await loadItems(method, options);
|
|
1331
|
-
if (
|
|
1609
|
+
if (isWorkspaceOpen()) {
|
|
1610
|
+
if (changed || options.force) {
|
|
1611
|
+
await showWorkspace({ silent: Boolean(options.silent) });
|
|
1612
|
+
}
|
|
1613
|
+
} else if (isGitStatusOpen()) {
|
|
1332
1614
|
await showGitStatus({ preserve: true, silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
|
|
1333
1615
|
} else if (isCiStatusOpen()) {
|
|
1334
1616
|
await showCiStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
|
|
@@ -2045,6 +2327,804 @@
|
|
|
2045
2327
|
setMeta("Health loaded.");
|
|
2046
2328
|
}
|
|
2047
2329
|
|
|
2330
|
+
function workspaceParentPath(path) {
|
|
2331
|
+
const parts = String(path || "").split("/").filter(Boolean);
|
|
2332
|
+
parts.pop();
|
|
2333
|
+
return parts.join("/");
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function renderWorkspaceBreadcrumb(currentPath) {
|
|
2337
|
+
const segments = String(currentPath || "").split("/").filter(Boolean);
|
|
2338
|
+
const crumbs = [
|
|
2339
|
+
`<button class="viewer-workspace__crumb" type="button" data-viewer-workspace-tree="" title="Workspace root">/</button>`,
|
|
2340
|
+
];
|
|
2341
|
+
let accum = "";
|
|
2342
|
+
segments.forEach((segment, idx) => {
|
|
2343
|
+
accum = accum ? `${accum}/${segment}` : segment;
|
|
2344
|
+
const isLast = idx === segments.length - 1;
|
|
2345
|
+
crumbs.push(`<span class="viewer-workspace__crumb-sep" aria-hidden="true">/</span>`);
|
|
2346
|
+
crumbs.push(
|
|
2347
|
+
`<button class="viewer-workspace__crumb${isLast ? " is-current" : ""}" type="button" data-viewer-workspace-tree="${escapeHtml(accum)}" title="${escapeHtml(accum)}"${isLast ? ' aria-current="location"' : ""}>${escapeHtml(segment)}</button>`,
|
|
2348
|
+
);
|
|
2349
|
+
});
|
|
2350
|
+
return `<nav class="viewer-workspace__breadcrumb" aria-label="Workspace breadcrumb">${crumbs.join("")}</nav>`;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function workspaceEntryIcon(kind, ignored) {
|
|
2354
|
+
if (kind === "directory") {
|
|
2355
|
+
return ignored
|
|
2356
|
+
? '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M2 4h4l1 1h7v8H2V4Zm9.5 3.2L9.7 9l1.8 1.8-.7.7L9 9.7l-1.8 1.8-.7-.7L8.3 9 6.5 7.2l.7-.7L9 8.3l1.8-1.8.7.7Z"/></svg>'
|
|
2357
|
+
: '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M2 4h4l1 1h7v8H2V4Z"/></svg>';
|
|
2358
|
+
}
|
|
2359
|
+
return '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M4 2h6l3 3v9H4V2Zm6 0v3h3"/></svg>';
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function renderWorkspaceTree(treePayload, selectedPath = "") {
|
|
2363
|
+
if (!treePayload || treePayload.state !== "ok") {
|
|
2364
|
+
const message = treePayload?.message || "Workspace tree is unavailable.";
|
|
2365
|
+
const state = treePayload?.state === "unavailable" ? "unavailable" : "empty";
|
|
2366
|
+
return `<div class="viewer-workspace__placeholder viewer-workspace__placeholder--${state}"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">${state === "unavailable" ? "!" : "·"}</span><span>${escapeHtml(message)}</span></div>`;
|
|
2367
|
+
}
|
|
2368
|
+
const currentPath = String(treePayload.path || "");
|
|
2369
|
+
const parentPath = workspaceParentPath(currentPath);
|
|
2370
|
+
const upButton = currentPath
|
|
2371
|
+
? `<button class="viewer-workspace__item viewer-workspace__item--up" type="button" data-viewer-workspace-tree="${escapeHtml(parentPath)}" title="Parent directory"><span class="viewer-workspace__item-icon" aria-hidden="true"><svg viewBox="0 0 16 16" focusable="false"><path fill="currentColor" d="M8 3 3 8h3v5h4V8h3L8 3Z"/></svg></span><span class="viewer-workspace__item-name">..</span></button>`
|
|
2372
|
+
: "";
|
|
2373
|
+
const rows = (Array.isArray(treePayload.entries) ? treePayload.entries : []).map((entry) => {
|
|
2374
|
+
const path = String(entry.path || "");
|
|
2375
|
+
const kind = String(entry.kind || "file");
|
|
2376
|
+
const ignored = Boolean(entry.ignored);
|
|
2377
|
+
const selected = path === selectedPath;
|
|
2378
|
+
const actionAttr = kind === "directory" && !ignored
|
|
2379
|
+
? `data-viewer-workspace-tree="${escapeHtml(path)}"`
|
|
2380
|
+
: `data-viewer-workspace-preview="${escapeHtml(path)}"`;
|
|
2381
|
+
const classes = [
|
|
2382
|
+
"viewer-workspace__item",
|
|
2383
|
+
`viewer-workspace__item--${kind === "directory" ? "directory" : "file"}`,
|
|
2384
|
+
];
|
|
2385
|
+
if (selected) classes.push("is-selected");
|
|
2386
|
+
if (ignored) classes.push("is-muted");
|
|
2387
|
+
return `
|
|
2388
|
+
<button class="${classes.join(" ")}" type="button" ${actionAttr} title="${escapeHtml(path)}"${selected ? ' aria-current="true"' : ""}>
|
|
2389
|
+
<span class="viewer-workspace__item-icon" aria-hidden="true">${workspaceEntryIcon(kind, ignored)}</span>
|
|
2390
|
+
<span class="viewer-workspace__item-name">${escapeHtml(entry.name || path || "/")}</span>
|
|
2391
|
+
</button>
|
|
2392
|
+
`;
|
|
2393
|
+
}).join("");
|
|
2394
|
+
return `
|
|
2395
|
+
<div class="viewer-workspace__tree-header">
|
|
2396
|
+
${renderWorkspaceBreadcrumb(currentPath)}
|
|
2397
|
+
</div>
|
|
2398
|
+
<div class="viewer-workspace__tree-list" role="list">
|
|
2399
|
+
${upButton}
|
|
2400
|
+
${rows || '<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span><span>Directory is empty.</span></div>'}
|
|
2401
|
+
</div>
|
|
2402
|
+
${treePayload.truncated ? '<div class="viewer-workspace__placeholder viewer-workspace__placeholder--warn"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">!</span><span>Directory listing truncated.</span></div>' : ""}
|
|
2403
|
+
`;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function renderWorkspacePreview(previewPayload) {
|
|
2407
|
+
if (!previewPayload) {
|
|
2408
|
+
return '<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span><span>Select a file or directory.</span></div>';
|
|
2409
|
+
}
|
|
2410
|
+
const path = previewPayload.path || "/";
|
|
2411
|
+
const name = previewPayload.name || path || "/";
|
|
2412
|
+
const state = previewPayload.state || "unknown";
|
|
2413
|
+
if (state === "ok") {
|
|
2414
|
+
return `
|
|
2415
|
+
<div class="viewer-workspace__preview-header">
|
|
2416
|
+
<div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
|
|
2417
|
+
<em>${escapeHtml(previewPayload.truncated ? "truncated" : `${previewPayload.size || 0} bytes`)}</em>
|
|
2418
|
+
</div>
|
|
2419
|
+
${previewPayload.truncated ? '<div class="viewer-workspace__placeholder viewer-workspace__placeholder--warn"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">!</span><span>Preview truncated.</span></div>' : ""}
|
|
2420
|
+
<pre class="viewer-workspace__code">${escapeHtml(previewPayload.content || "")}</pre>
|
|
2421
|
+
`;
|
|
2422
|
+
}
|
|
2423
|
+
if (state === "image") {
|
|
2424
|
+
return `
|
|
2425
|
+
<div class="viewer-workspace__preview-header">
|
|
2426
|
+
<div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
|
|
2427
|
+
<em>${escapeHtml(previewPayload.contentType || "image")}</em>
|
|
2428
|
+
</div>
|
|
2429
|
+
<img class="viewer-workspace__image" src="/api/workspace-file?path=${encodeURIComponent(path)}" alt="${escapeHtml(name)}">
|
|
2430
|
+
`;
|
|
2431
|
+
}
|
|
2432
|
+
const placeholderState = state === "unavailable" ? "unavailable" : "empty";
|
|
2433
|
+
const placeholderIcon = placeholderState === "unavailable" ? "!" : "·";
|
|
2434
|
+
return `
|
|
2435
|
+
<div class="viewer-workspace__preview-header">
|
|
2436
|
+
<div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
|
|
2437
|
+
<em>${escapeHtml(state)}</em>
|
|
2438
|
+
</div>
|
|
2439
|
+
<div class="viewer-workspace__placeholder viewer-workspace__placeholder--${placeholderState}"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">${placeholderIcon}</span><span>${escapeHtml(previewPayload.message || "No preview is available.")}</span></div>
|
|
2440
|
+
`;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function renderWorkspace(treePayload, previewPayload) {
|
|
2444
|
+
const selectedPath = previewPayload?.path || "";
|
|
2445
|
+
return `
|
|
2446
|
+
<div class="viewer-workspace">
|
|
2447
|
+
<aside class="viewer-workspace__tree" aria-label="Workspace files">
|
|
2448
|
+
${renderWorkspaceTree(treePayload, selectedPath)}
|
|
2449
|
+
</aside>
|
|
2450
|
+
<section class="viewer-workspace__preview" aria-label="Workspace preview">
|
|
2451
|
+
${renderWorkspacePreview(previewPayload)}
|
|
2452
|
+
</section>
|
|
2453
|
+
</div>
|
|
2454
|
+
`;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
async function fetchWorkspaceTree(path = "") {
|
|
2458
|
+
const response = await fetch(`/api/workspace-tree?path=${encodeURIComponent(path)}`);
|
|
2459
|
+
const data = await response.json();
|
|
2460
|
+
if (!response.ok || !data.ok) {
|
|
2461
|
+
throw new Error(data.error || "Unable to load workspace tree.");
|
|
2462
|
+
}
|
|
2463
|
+
return data.payload;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
async function fetchWorkspacePreview(path = "") {
|
|
2467
|
+
const response = await fetch(`/api/workspace-preview?path=${encodeURIComponent(path)}`);
|
|
2468
|
+
const data = await response.json();
|
|
2469
|
+
if (!response.ok || !data.ok) {
|
|
2470
|
+
throw new Error(data.error || "Unable to load workspace preview.");
|
|
2471
|
+
}
|
|
2472
|
+
return data.payload;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
async function showWorkspace(options = {}) {
|
|
2476
|
+
if (!isCapabilityAvailable("workspace")) {
|
|
2477
|
+
const message = capabilityMessage("workspace", "Explorer is not available for this project.");
|
|
2478
|
+
setDocument("Explorer", renderWorkspace({ state: "unavailable", message }, { state: "unavailable", message }));
|
|
2479
|
+
setMeta(message);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
if (!options.silent) {
|
|
2483
|
+
setMeta("Loading workspace...");
|
|
2484
|
+
}
|
|
2485
|
+
const tree = await fetchWorkspaceTree("");
|
|
2486
|
+
const preview = await fetchWorkspacePreview("");
|
|
2487
|
+
setDocument("Explorer", renderWorkspace(tree, preview));
|
|
2488
|
+
setMeta(options.silent ? "Explorer refreshed." : "Explorer loaded.");
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
const workshopTabs = [
|
|
2492
|
+
{ id: "terminals", label: "Terminals", title: "In-app PTY terminals" },
|
|
2493
|
+
{ id: "commands", label: "Commands", title: "Discovered package and project scripts" },
|
|
2494
|
+
];
|
|
2495
|
+
|
|
2496
|
+
function preferredWorkshopTab() {
|
|
2497
|
+
const stored = String(viewerPreferences.workshopActiveTab || "");
|
|
2498
|
+
return workshopTabs.some((tab) => tab.id === stored) ? stored : "terminals";
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function setWorkshopActiveTab(tabId) {
|
|
2502
|
+
const next = workshopTabs.some((tab) => tab.id === tabId) ? tabId : "terminals";
|
|
2503
|
+
if (next === viewerPreferences.workshopActiveTab) return;
|
|
2504
|
+
updateViewerPreferences({ workshopActiveTab: next });
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function renderWorkshopTabs(activeTab) {
|
|
2508
|
+
const buttons = workshopTabs.map((tab) => {
|
|
2509
|
+
const isActive = tab.id === activeTab;
|
|
2510
|
+
return `<button class="viewer-cdx__mode${isActive ? " is-active" : ""}" type="button" role="tab" aria-selected="${isActive ? "true" : "false"}" data-viewer-workshop-tab="${escapeHtml(tab.id)}" title="${escapeHtml(tab.title)}">${escapeHtml(tab.label)}</button>`;
|
|
2511
|
+
}).join("");
|
|
2512
|
+
return `<div class="viewer-cdx__modes" role="tablist" aria-label="Workshop sub-screens">${buttons}</div>`;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function renderWorkshopPanel(tabId) {
|
|
2516
|
+
if (tabId === "commands") {
|
|
2517
|
+
return `
|
|
2518
|
+
<div class="viewer-workshop__panel" role="tabpanel" data-viewer-workshop-panel="commands" data-viewer-workshop-commands>
|
|
2519
|
+
<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty">
|
|
2520
|
+
<span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span>
|
|
2521
|
+
<span>Discovering commands...</span>
|
|
2522
|
+
</div>
|
|
2523
|
+
</div>
|
|
2524
|
+
`;
|
|
2525
|
+
}
|
|
2526
|
+
const terminalsAvailable = Boolean(capability("workshop").detail?.terminalsAvailable);
|
|
2527
|
+
if (!terminalsAvailable) {
|
|
2528
|
+
return `
|
|
2529
|
+
<div class="viewer-workshop__panel viewer-workshop__panel--terminals" role="tabpanel" data-viewer-workshop-panel="terminals">
|
|
2530
|
+
<div class="viewer-workspace__placeholder viewer-workspace__placeholder--unavailable">
|
|
2531
|
+
<span class="viewer-workspace__placeholder-icon" aria-hidden="true">!</span>
|
|
2532
|
+
<span>In-app terminals require a Unix host with stdlib pty support (macOS or Linux). Use the Commands tab to run discovered scripts in the meantime.</span>
|
|
2533
|
+
</div>
|
|
2534
|
+
</div>
|
|
2535
|
+
`;
|
|
2536
|
+
}
|
|
2537
|
+
return `
|
|
2538
|
+
<div class="viewer-workshop__panel viewer-workshop__panel--terminals-active" role="tabpanel" data-viewer-workshop-panel="terminals">
|
|
2539
|
+
<aside class="viewer-workshop__terminal-list" data-viewer-workshop-terminal-list aria-label="Terminal sessions"></aside>
|
|
2540
|
+
<section class="viewer-workshop__terminal-stage" data-viewer-workshop-terminal-stage>
|
|
2541
|
+
<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty" data-viewer-workshop-terminal-empty>
|
|
2542
|
+
<span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span>
|
|
2543
|
+
<span>No terminal session yet. Click "New terminal" to spawn one.</span>
|
|
2544
|
+
</div>
|
|
2545
|
+
</section>
|
|
2546
|
+
</div>
|
|
2547
|
+
`;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const workshopCommandState = {
|
|
2551
|
+
catalog: null,
|
|
2552
|
+
sessions: new Map(),
|
|
2553
|
+
streams: new Map(),
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
function renderWorkshopCommandRow(entry) {
|
|
2557
|
+
const session = workshopCommandState.sessions.get(entry.id) || null;
|
|
2558
|
+
const state = session?.state || "idle";
|
|
2559
|
+
const running = state === "running" || state === "starting";
|
|
2560
|
+
const exitBadge = session && session.exitCode !== null && session.exitCode !== undefined
|
|
2561
|
+
? `<span class="viewer-workshop__exit viewer-workshop__exit--${session.exitCode === 0 ? "ok" : "fail"}">exit ${escapeHtml(String(session.exitCode))}</span>`
|
|
2562
|
+
: "";
|
|
2563
|
+
return `
|
|
2564
|
+
<li class="viewer-workshop__command" data-viewer-workshop-command="${escapeHtml(entry.id)}">
|
|
2565
|
+
<div class="viewer-workshop__command-header">
|
|
2566
|
+
<div class="viewer-workshop__command-name">
|
|
2567
|
+
<strong>${escapeHtml(entry.name)}</strong>
|
|
2568
|
+
<span class="viewer-workshop__command-source">${escapeHtml(entry.source)}</span>
|
|
2569
|
+
</div>
|
|
2570
|
+
<div class="viewer-workshop__command-actions">
|
|
2571
|
+
<span class="viewer-workshop__state viewer-workshop__state--${escapeHtml(state)}">${escapeHtml(state)}</span>
|
|
2572
|
+
${exitBadge}
|
|
2573
|
+
${running
|
|
2574
|
+
? `<button class="btn" type="button" data-viewer-workshop-command-stop="${escapeHtml(entry.id)}">Stop</button>`
|
|
2575
|
+
: `<button class="btn" type="button" data-viewer-workshop-command-run="${escapeHtml(entry.id)}">Run</button>`}
|
|
2576
|
+
</div>
|
|
2577
|
+
</div>
|
|
2578
|
+
<div class="viewer-workshop__command-meta"><code>${escapeHtml(entry.command)}</code></div>
|
|
2579
|
+
<pre class="viewer-workshop__log" data-viewer-workshop-command-log="${escapeHtml(entry.id)}" aria-live="polite">${escapeHtml(session?.logText || "")}</pre>
|
|
2580
|
+
</li>
|
|
2581
|
+
`;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
function renderWorkshopCommandList(catalog) {
|
|
2585
|
+
if (!catalog || catalog.state === "unavailable") {
|
|
2586
|
+
return `<div class="viewer-workspace__placeholder viewer-workspace__placeholder--unavailable"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">!</span><span>${escapeHtml(catalog?.message || "Commands are unavailable.")}</span></div>`;
|
|
2587
|
+
}
|
|
2588
|
+
const commands = Array.isArray(catalog.commands) ? catalog.commands : [];
|
|
2589
|
+
if (commands.length === 0) {
|
|
2590
|
+
return `<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span><span>${escapeHtml(catalog.message || "No commands discovered.")}</span></div>`;
|
|
2591
|
+
}
|
|
2592
|
+
const groups = new Map();
|
|
2593
|
+
commands.forEach((entry) => {
|
|
2594
|
+
const group = entry.group || "Commands";
|
|
2595
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
2596
|
+
groups.get(group).push(entry);
|
|
2597
|
+
});
|
|
2598
|
+
const sections = [...groups.entries()].map(([group, entries]) => `
|
|
2599
|
+
<section class="viewer-workshop__group">
|
|
2600
|
+
<h3 class="viewer-workshop__group-title">${escapeHtml(group)}</h3>
|
|
2601
|
+
<ul class="viewer-workshop__commands">
|
|
2602
|
+
${entries.map(renderWorkshopCommandRow).join("")}
|
|
2603
|
+
</ul>
|
|
2604
|
+
</section>
|
|
2605
|
+
`).join("");
|
|
2606
|
+
return sections;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
function renderWorkshopCommands() {
|
|
2610
|
+
const container = document.querySelector("[data-viewer-workshop-commands]");
|
|
2611
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2612
|
+
container.innerHTML = renderWorkshopCommandList(workshopCommandState.catalog);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
async function loadWorkshopCommands() {
|
|
2616
|
+
try {
|
|
2617
|
+
const response = await fetch("/api/workshop-commands");
|
|
2618
|
+
const data = await response.json();
|
|
2619
|
+
workshopCommandState.catalog = data?.payload || null;
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
workshopCommandState.catalog = { state: "unavailable", commands: [], message: String(error?.message || error) };
|
|
2622
|
+
}
|
|
2623
|
+
renderWorkshopCommands();
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function updateWorkshopCommandSession(commandId, patch) {
|
|
2627
|
+
const previous = workshopCommandState.sessions.get(commandId) || { logText: "" };
|
|
2628
|
+
workshopCommandState.sessions.set(commandId, { ...previous, ...patch });
|
|
2629
|
+
renderWorkshopCommands();
|
|
2630
|
+
recomputeWorkshopBadges();
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
function appendWorkshopCommandLog(commandId, line) {
|
|
2634
|
+
const previous = workshopCommandState.sessions.get(commandId) || { logText: "" };
|
|
2635
|
+
const next = previous.logText ? `${previous.logText}\n${line}` : line;
|
|
2636
|
+
workshopCommandState.sessions.set(commandId, { ...previous, logText: next });
|
|
2637
|
+
const node = document.querySelector(`[data-viewer-workshop-command-log="${commandId}"]`);
|
|
2638
|
+
if (node instanceof HTMLElement) {
|
|
2639
|
+
node.textContent = next;
|
|
2640
|
+
node.scrollTop = node.scrollHeight;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
function closeWorkshopCommandStream(commandId) {
|
|
2645
|
+
const stream = workshopCommandState.streams.get(commandId);
|
|
2646
|
+
if (stream) {
|
|
2647
|
+
try { stream.close(); } catch { /* noop */ }
|
|
2648
|
+
workshopCommandState.streams.delete(commandId);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
async function startWorkshopCommand(commandId) {
|
|
2653
|
+
try {
|
|
2654
|
+
const response = await fetch("/api/workshop-command-start", {
|
|
2655
|
+
method: "POST",
|
|
2656
|
+
headers: { "Content-Type": "application/json" },
|
|
2657
|
+
body: JSON.stringify({ commandId }),
|
|
2658
|
+
});
|
|
2659
|
+
const data = await response.json();
|
|
2660
|
+
if (!response.ok || !data.ok) {
|
|
2661
|
+
throw new Error(data.error || "Unable to start command.");
|
|
2662
|
+
}
|
|
2663
|
+
const session = data.payload;
|
|
2664
|
+
updateWorkshopCommandSession(commandId, {
|
|
2665
|
+
sessionId: session.id,
|
|
2666
|
+
state: session.state,
|
|
2667
|
+
exitCode: session.exitCode,
|
|
2668
|
+
logText: "",
|
|
2669
|
+
});
|
|
2670
|
+
openWorkshopCommandStream(commandId, session.id);
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
updateWorkshopCommandSession(commandId, { state: "error", logText: `! ${error?.message || error}` });
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function openWorkshopCommandStream(commandId, sessionId) {
|
|
2677
|
+
closeWorkshopCommandStream(commandId);
|
|
2678
|
+
const source = new EventSource(`/api/workshop-session/${encodeURIComponent(sessionId)}/stream`);
|
|
2679
|
+
workshopCommandState.streams.set(commandId, source);
|
|
2680
|
+
source.addEventListener("line", (event) => {
|
|
2681
|
+
try {
|
|
2682
|
+
const payload = JSON.parse(event.data || "{}");
|
|
2683
|
+
appendWorkshopCommandLog(commandId, String(payload.line || ""));
|
|
2684
|
+
} catch { /* noop */ }
|
|
2685
|
+
});
|
|
2686
|
+
source.addEventListener("end", (event) => {
|
|
2687
|
+
try {
|
|
2688
|
+
const payload = JSON.parse(event.data || "{}");
|
|
2689
|
+
updateWorkshopCommandSession(commandId, {
|
|
2690
|
+
state: payload.state,
|
|
2691
|
+
exitCode: payload.exitCode,
|
|
2692
|
+
});
|
|
2693
|
+
} catch { /* noop */ }
|
|
2694
|
+
closeWorkshopCommandStream(commandId);
|
|
2695
|
+
});
|
|
2696
|
+
source.addEventListener("error", () => {
|
|
2697
|
+
closeWorkshopCommandStream(commandId);
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
async function stopWorkshopCommand(commandId) {
|
|
2702
|
+
const session = workshopCommandState.sessions.get(commandId);
|
|
2703
|
+
if (!session?.sessionId) return;
|
|
2704
|
+
try {
|
|
2705
|
+
await fetch("/api/workshop-command-stop", {
|
|
2706
|
+
method: "POST",
|
|
2707
|
+
headers: { "Content-Type": "application/json" },
|
|
2708
|
+
body: JSON.stringify({ sessionId: session.sessionId }),
|
|
2709
|
+
});
|
|
2710
|
+
} catch { /* noop */ }
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
const workshopTerminalState = {
|
|
2714
|
+
sessions: new Map(),
|
|
2715
|
+
activeId: "",
|
|
2716
|
+
streams: new Map(),
|
|
2717
|
+
hydrated: false,
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
async function hydrateWorkshopTerminals() {
|
|
2721
|
+
if (workshopTerminalState.hydrated) return;
|
|
2722
|
+
if (!isCapabilityAvailable("workshop")) return;
|
|
2723
|
+
if (!capability("workshop").detail?.terminalsAvailable) return;
|
|
2724
|
+
workshopTerminalState.hydrated = true;
|
|
2725
|
+
try {
|
|
2726
|
+
const response = await fetch("/api/workshop-terminals");
|
|
2727
|
+
const data = await response.json();
|
|
2728
|
+
const sessions = Array.isArray(data?.payload?.sessions) ? data.payload.sessions : [];
|
|
2729
|
+
for (const remote of sessions) {
|
|
2730
|
+
const id = String(remote?.id || "");
|
|
2731
|
+
if (!id) continue;
|
|
2732
|
+
if (workshopTerminalState.sessions.has(id)) continue;
|
|
2733
|
+
const state = String(remote?.state || "");
|
|
2734
|
+
// Only restore live sessions; the server reaps stopped/failed ones via TTL.
|
|
2735
|
+
if (state !== "running" && state !== "starting") continue;
|
|
2736
|
+
workshopTerminalState.sessions.set(id, {
|
|
2737
|
+
id,
|
|
2738
|
+
label: String(remote?.label || "shell"),
|
|
2739
|
+
state,
|
|
2740
|
+
bufferedOutput: "",
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
if (!workshopTerminalState.activeId) {
|
|
2744
|
+
const next = workshopTerminalState.sessions.keys().next();
|
|
2745
|
+
workshopTerminalState.activeId = next.done ? "" : next.value;
|
|
2746
|
+
}
|
|
2747
|
+
recomputeWorkshopBadges();
|
|
2748
|
+
} catch {
|
|
2749
|
+
workshopTerminalState.hydrated = false;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function workshopTerminalListNode() {
|
|
2754
|
+
return document.querySelector("[data-viewer-workshop-terminal-list]");
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
function workshopTerminalStageNode() {
|
|
2758
|
+
return document.querySelector("[data-viewer-workshop-terminal-stage]");
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function renderWorkshopTerminalList() {
|
|
2762
|
+
const node = workshopTerminalListNode();
|
|
2763
|
+
if (!(node instanceof HTMLElement)) return;
|
|
2764
|
+
const entries = [...workshopTerminalState.sessions.values()];
|
|
2765
|
+
const header = `<div class="viewer-workshop__terminal-list-header">
|
|
2766
|
+
<span>Terminals</span>
|
|
2767
|
+
<span class="viewer-workshop__terminal-actions">
|
|
2768
|
+
<button class="btn viewer-workshop__terminal-new" type="button" data-viewer-workshop-terminal-new title="Spawn a shell session">+ Shell</button>
|
|
2769
|
+
<button class="btn viewer-workshop__terminal-new" type="button" data-viewer-workshop-terminal-custom title="Spawn a session with a custom command">+ Custom</button>
|
|
2770
|
+
</span>
|
|
2771
|
+
</div>`;
|
|
2772
|
+
if (entries.length === 0) {
|
|
2773
|
+
node.innerHTML = `${header}<div class="viewer-workspace__placeholder viewer-workspace__placeholder--empty"><span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span><span>No sessions yet.</span></div>`;
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
const rows = entries.map((entry) => {
|
|
2777
|
+
const isActive = entry.id === workshopTerminalState.activeId;
|
|
2778
|
+
const stateBadge = entry.state ? `<span class="viewer-workshop__state viewer-workshop__state--${escapeHtml(entry.state)}">${escapeHtml(entry.state)}</span>` : "";
|
|
2779
|
+
const closing = Boolean(entry.closing);
|
|
2780
|
+
const closeAttrs = closing
|
|
2781
|
+
? `aria-busy="true" aria-label="Closing session" title="Closing session..."`
|
|
2782
|
+
: `data-viewer-workshop-terminal-close="${escapeHtml(entry.id)}" role="button" tabindex="0" title="Close session" aria-label="Close session"`;
|
|
2783
|
+
const closeGlyph = closing
|
|
2784
|
+
? `<span class="viewer-workshop__spinner" aria-hidden="true"></span>`
|
|
2785
|
+
: `×`;
|
|
2786
|
+
return `<button class="viewer-workshop__terminal-row${isActive ? " is-active" : ""}${closing ? " is-closing" : ""}" type="button" data-viewer-workshop-terminal-select="${escapeHtml(entry.id)}" title="${escapeHtml(entry.label || entry.id)}">
|
|
2787
|
+
<span class="viewer-workshop__terminal-row-label">${escapeHtml(entry.label || entry.id)}</span>
|
|
2788
|
+
${stateBadge}
|
|
2789
|
+
<span class="viewer-workshop__terminal-row-close${closing ? " is-closing" : ""}" ${closeAttrs}>${closeGlyph}</span>
|
|
2790
|
+
</button>`;
|
|
2791
|
+
}).join("");
|
|
2792
|
+
node.innerHTML = `${header}<div class="viewer-workshop__terminal-rows">${rows}</div>`;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function ensureWorkshopTerminalStage() {
|
|
2796
|
+
const stage = workshopTerminalStageNode();
|
|
2797
|
+
if (!(stage instanceof HTMLElement)) return null;
|
|
2798
|
+
const active = workshopTerminalState.activeId
|
|
2799
|
+
? workshopTerminalState.sessions.get(workshopTerminalState.activeId)
|
|
2800
|
+
: null;
|
|
2801
|
+
// Clean up placeholder and host elements for sessions that no longer exist.
|
|
2802
|
+
const placeholder = stage.querySelector("[data-viewer-workshop-terminal-empty]");
|
|
2803
|
+
if (placeholder) placeholder.remove();
|
|
2804
|
+
stage.querySelectorAll("[data-viewer-workshop-terminal-host]").forEach((node) => {
|
|
2805
|
+
if (!(node instanceof HTMLElement)) return;
|
|
2806
|
+
const id = node.getAttribute("data-viewer-workshop-terminal-host") || "";
|
|
2807
|
+
if (!workshopTerminalState.sessions.has(id)) {
|
|
2808
|
+
node.remove();
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
if (!active) {
|
|
2812
|
+
if (!stage.querySelector("[data-viewer-workshop-terminal-empty]")) {
|
|
2813
|
+
const empty = document.createElement("div");
|
|
2814
|
+
empty.className = "viewer-workspace__placeholder viewer-workspace__placeholder--empty";
|
|
2815
|
+
empty.setAttribute("data-viewer-workshop-terminal-empty", "");
|
|
2816
|
+
empty.innerHTML = '<span class="viewer-workspace__placeholder-icon" aria-hidden="true">·</span><span>Select or create a terminal session to start.</span>';
|
|
2817
|
+
stage.appendChild(empty);
|
|
2818
|
+
}
|
|
2819
|
+
// Hide every existing host while no session is active.
|
|
2820
|
+
stage.querySelectorAll("[data-viewer-workshop-terminal-host]").forEach((node) => {
|
|
2821
|
+
if (node instanceof HTMLElement) node.style.display = "none";
|
|
2822
|
+
});
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
// Toggle visibility: only the active host shows, every other host stays
|
|
2826
|
+
// mounted in the DOM so its xterm.js instance and scrollback survive.
|
|
2827
|
+
let host = stage.querySelector(`[data-viewer-workshop-terminal-host="${active.id}"]`);
|
|
2828
|
+
stage.querySelectorAll("[data-viewer-workshop-terminal-host]").forEach((node) => {
|
|
2829
|
+
if (!(node instanceof HTMLElement)) return;
|
|
2830
|
+
const id = node.getAttribute("data-viewer-workshop-terminal-host") || "";
|
|
2831
|
+
node.style.display = id === active.id ? "" : "none";
|
|
2832
|
+
});
|
|
2833
|
+
if (!(host instanceof HTMLElement)) {
|
|
2834
|
+
host = document.createElement("div");
|
|
2835
|
+
host.className = "viewer-workshop__terminal-host";
|
|
2836
|
+
host.setAttribute("data-viewer-workshop-terminal-host", active.id);
|
|
2837
|
+
stage.appendChild(host);
|
|
2838
|
+
}
|
|
2839
|
+
return host instanceof HTMLElement ? host : null;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
function ensureWorkshopTerminalHostFor(sessionId) {
|
|
2843
|
+
const stage = workshopTerminalStageNode();
|
|
2844
|
+
if (!(stage instanceof HTMLElement)) return null;
|
|
2845
|
+
const placeholder = stage.querySelector("[data-viewer-workshop-terminal-empty]");
|
|
2846
|
+
if (placeholder) placeholder.remove();
|
|
2847
|
+
let host = stage.querySelector(`[data-viewer-workshop-terminal-host="${sessionId}"]`);
|
|
2848
|
+
if (!(host instanceof HTMLElement)) {
|
|
2849
|
+
host = document.createElement("div");
|
|
2850
|
+
host.className = "viewer-workshop__terminal-host";
|
|
2851
|
+
host.setAttribute("data-viewer-workshop-terminal-host", sessionId);
|
|
2852
|
+
stage.appendChild(host);
|
|
2853
|
+
}
|
|
2854
|
+
return host;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
function mountWorkshopTerminalEmulator(entry) {
|
|
2858
|
+
if (typeof window.Terminal !== "function") return;
|
|
2859
|
+
if (entry.terminal) return;
|
|
2860
|
+
const host = ensureWorkshopTerminalHostFor(entry.id);
|
|
2861
|
+
if (!host) return;
|
|
2862
|
+
const term = new window.Terminal({
|
|
2863
|
+
fontSize: 12,
|
|
2864
|
+
fontFamily: 'var(--vscode-editor-font-family, "Menlo", "Consolas", monospace)',
|
|
2865
|
+
theme: { background: "#0a0a0a", foreground: "#d4d4d4" },
|
|
2866
|
+
cursorBlink: true,
|
|
2867
|
+
scrollback: 5000,
|
|
2868
|
+
convertEol: true,
|
|
2869
|
+
});
|
|
2870
|
+
const fitAddon = typeof window.FitAddon === "function"
|
|
2871
|
+
? new window.FitAddon()
|
|
2872
|
+
: (window.FitAddon && typeof window.FitAddon.FitAddon === "function" ? new window.FitAddon.FitAddon() : null);
|
|
2873
|
+
const linksAddon = window.WebLinksAddon && typeof window.WebLinksAddon.WebLinksAddon === "function"
|
|
2874
|
+
? new window.WebLinksAddon.WebLinksAddon()
|
|
2875
|
+
: null;
|
|
2876
|
+
if (fitAddon) term.loadAddon(fitAddon);
|
|
2877
|
+
if (linksAddon) term.loadAddon(linksAddon);
|
|
2878
|
+
term.open(host);
|
|
2879
|
+
if (fitAddon) {
|
|
2880
|
+
try { fitAddon.fit(); } catch { /* noop */ }
|
|
2881
|
+
}
|
|
2882
|
+
term.onData((data) => {
|
|
2883
|
+
writeWorkshopTerminalInput(entry.id, data);
|
|
2884
|
+
});
|
|
2885
|
+
term.onResize((size) => {
|
|
2886
|
+
resizeWorkshopTerminal(entry.id, size.rows, size.cols);
|
|
2887
|
+
});
|
|
2888
|
+
entry.terminal = term;
|
|
2889
|
+
entry.fitAddon = fitAddon;
|
|
2890
|
+
if (fitAddon) {
|
|
2891
|
+
try {
|
|
2892
|
+
const dim = fitAddon.proposeDimensions();
|
|
2893
|
+
if (dim) resizeWorkshopTerminal(entry.id, dim.rows, dim.cols);
|
|
2894
|
+
} catch { /* noop */ }
|
|
2895
|
+
}
|
|
2896
|
+
openWorkshopTerminalStream(entry.id);
|
|
2897
|
+
if (entry.bufferedOutput) {
|
|
2898
|
+
term.write(entry.bufferedOutput);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function setActiveWorkshopTerminal(sessionId) {
|
|
2903
|
+
workshopTerminalState.activeId = sessionId || "";
|
|
2904
|
+
renderWorkshopTerminalList();
|
|
2905
|
+
const entry = sessionId ? workshopTerminalState.sessions.get(sessionId) : null;
|
|
2906
|
+
ensureWorkshopTerminalStage();
|
|
2907
|
+
if (entry) {
|
|
2908
|
+
mountWorkshopTerminalEmulator(entry);
|
|
2909
|
+
try { entry.terminal?.focus(); } catch { /* noop */ }
|
|
2910
|
+
try { entry.fitAddon?.fit(); } catch { /* noop */ }
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
async function spawnWorkshopTerminal(options = {}) {
|
|
2915
|
+
try {
|
|
2916
|
+
const body = {};
|
|
2917
|
+
if (Array.isArray(options.command) && options.command.length) body.command = options.command;
|
|
2918
|
+
if (options.label) body.label = String(options.label);
|
|
2919
|
+
const response = await fetch("/api/workshop-terminal-start", {
|
|
2920
|
+
method: "POST",
|
|
2921
|
+
headers: { "Content-Type": "application/json" },
|
|
2922
|
+
body: JSON.stringify(body),
|
|
2923
|
+
});
|
|
2924
|
+
const data = await response.json();
|
|
2925
|
+
if (!response.ok || !data.ok) throw new Error(data.error || "Unable to start terminal.");
|
|
2926
|
+
const session = data.payload;
|
|
2927
|
+
workshopTerminalState.sessions.set(session.id, {
|
|
2928
|
+
id: session.id,
|
|
2929
|
+
label: session.label || "shell",
|
|
2930
|
+
state: session.state,
|
|
2931
|
+
bufferedOutput: "",
|
|
2932
|
+
});
|
|
2933
|
+
recomputeWorkshopBadges();
|
|
2934
|
+
// Ensure the Workshop view is mounted before activating.
|
|
2935
|
+
if (preferredWorkshopTab() !== "terminals") {
|
|
2936
|
+
await showWorkshop({ tab: "terminals" });
|
|
2937
|
+
} else {
|
|
2938
|
+
await showWorkshop({ tab: "terminals" });
|
|
2939
|
+
}
|
|
2940
|
+
setActiveWorkshopTerminal(session.id);
|
|
2941
|
+
return session.id;
|
|
2942
|
+
} catch (error) {
|
|
2943
|
+
setMeta(`Terminal: ${error?.message || error}`);
|
|
2944
|
+
return "";
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
function spawnCustomWorkshopTerminal() {
|
|
2949
|
+
const raw = window.prompt("Command to run (space-separated, e.g. 'node --version'):", "");
|
|
2950
|
+
if (!raw) return;
|
|
2951
|
+
const command = raw.trim().split(/\s+/).filter(Boolean);
|
|
2952
|
+
if (!command.length) return;
|
|
2953
|
+
const label = command.slice(0, 2).join(" ").slice(0, 32) || "custom";
|
|
2954
|
+
spawnWorkshopTerminal({ command, label });
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
// Public API for CDX / handoff launchers and other callers that want to
|
|
2958
|
+
// open a Workshop terminal pre-running a canonical command.
|
|
2959
|
+
window.logicsViewer = window.logicsViewer || {};
|
|
2960
|
+
window.logicsViewer.launchTerminal = (command, label) => spawnWorkshopTerminal({ command, label });
|
|
2961
|
+
|
|
2962
|
+
function writeWorkshopTerminalInput(sessionId, data) {
|
|
2963
|
+
if (!sessionId || !data) return;
|
|
2964
|
+
fetch("/api/workshop-terminal-input", {
|
|
2965
|
+
method: "POST",
|
|
2966
|
+
headers: { "Content-Type": "application/json" },
|
|
2967
|
+
body: JSON.stringify({ sessionId, data }),
|
|
2968
|
+
}).catch(() => { /* noop */ });
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function resizeWorkshopTerminal(sessionId, rows, cols) {
|
|
2972
|
+
if (!sessionId || rows <= 0 || cols <= 0) return;
|
|
2973
|
+
fetch("/api/workshop-terminal-resize", {
|
|
2974
|
+
method: "POST",
|
|
2975
|
+
headers: { "Content-Type": "application/json" },
|
|
2976
|
+
body: JSON.stringify({ sessionId, rows, cols }),
|
|
2977
|
+
}).catch(() => { /* noop */ });
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async function stopWorkshopTerminal(sessionId) {
|
|
2981
|
+
if (!sessionId) return;
|
|
2982
|
+
const pending = workshopTerminalState.sessions.get(sessionId);
|
|
2983
|
+
if (pending?.closing) return;
|
|
2984
|
+
if (pending) {
|
|
2985
|
+
pending.closing = true;
|
|
2986
|
+
renderWorkshopTerminalList();
|
|
2987
|
+
}
|
|
2988
|
+
try {
|
|
2989
|
+
await fetch("/api/workshop-terminal-stop", {
|
|
2990
|
+
method: "POST",
|
|
2991
|
+
headers: { "Content-Type": "application/json" },
|
|
2992
|
+
body: JSON.stringify({ sessionId }),
|
|
2993
|
+
});
|
|
2994
|
+
} catch { /* noop */ }
|
|
2995
|
+
closeWorkshopTerminalStream(sessionId);
|
|
2996
|
+
const entry = workshopTerminalState.sessions.get(sessionId);
|
|
2997
|
+
if (entry?.terminal) {
|
|
2998
|
+
try { entry.terminal.dispose(); } catch { /* noop */ }
|
|
2999
|
+
}
|
|
3000
|
+
workshopTerminalState.sessions.delete(sessionId);
|
|
3001
|
+
if (workshopTerminalState.activeId === sessionId) {
|
|
3002
|
+
const next = workshopTerminalState.sessions.keys().next();
|
|
3003
|
+
setActiveWorkshopTerminal(next.done ? "" : next.value);
|
|
3004
|
+
} else {
|
|
3005
|
+
renderWorkshopTerminalList();
|
|
3006
|
+
}
|
|
3007
|
+
recomputeWorkshopBadges();
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function closeWorkshopTerminalStream(sessionId) {
|
|
3011
|
+
const stream = workshopTerminalState.streams.get(sessionId);
|
|
3012
|
+
if (stream) {
|
|
3013
|
+
try { stream.close(); } catch { /* noop */ }
|
|
3014
|
+
workshopTerminalState.streams.delete(sessionId);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
function openWorkshopTerminalStream(sessionId) {
|
|
3019
|
+
closeWorkshopTerminalStream(sessionId);
|
|
3020
|
+
const source = new EventSource(`/api/workshop-terminal/${encodeURIComponent(sessionId)}/stream`);
|
|
3021
|
+
workshopTerminalState.streams.set(sessionId, source);
|
|
3022
|
+
source.addEventListener("data", (event) => {
|
|
3023
|
+
try {
|
|
3024
|
+
const payload = JSON.parse(event.data || "{}");
|
|
3025
|
+
const chunk = String(payload.data || "");
|
|
3026
|
+
const entry = workshopTerminalState.sessions.get(sessionId);
|
|
3027
|
+
if (!entry) return;
|
|
3028
|
+
if (entry.terminal) {
|
|
3029
|
+
entry.terminal.write(chunk);
|
|
3030
|
+
} else {
|
|
3031
|
+
entry.bufferedOutput = (entry.bufferedOutput || "") + chunk;
|
|
3032
|
+
}
|
|
3033
|
+
} catch { /* noop */ }
|
|
3034
|
+
});
|
|
3035
|
+
source.addEventListener("end", (event) => {
|
|
3036
|
+
try {
|
|
3037
|
+
const payload = JSON.parse(event.data || "{}");
|
|
3038
|
+
const entry = workshopTerminalState.sessions.get(sessionId);
|
|
3039
|
+
if (entry) entry.state = payload.state;
|
|
3040
|
+
renderWorkshopTerminalList();
|
|
3041
|
+
recomputeWorkshopBadges();
|
|
3042
|
+
} catch { /* noop */ }
|
|
3043
|
+
closeWorkshopTerminalStream(sessionId);
|
|
3044
|
+
});
|
|
3045
|
+
source.addEventListener("error", () => {
|
|
3046
|
+
closeWorkshopTerminalStream(sessionId);
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
function renderWorkshop(activeTab, options = {}) {
|
|
3051
|
+
if (options.unavailable) {
|
|
3052
|
+
return `
|
|
3053
|
+
<div class="viewer-workshop">
|
|
3054
|
+
<div class="viewer-workspace__placeholder viewer-workspace__placeholder--unavailable">
|
|
3055
|
+
<span class="viewer-workspace__placeholder-icon" aria-hidden="true">!</span>
|
|
3056
|
+
<span>${escapeHtml(options.message || "Workshop is not available for this project.")}</span>
|
|
3057
|
+
</div>
|
|
3058
|
+
</div>
|
|
3059
|
+
`;
|
|
3060
|
+
}
|
|
3061
|
+
return `
|
|
3062
|
+
<div class="viewer-workshop">
|
|
3063
|
+
<div class="viewer-workshop__tabs" role="tablist" aria-label="Workshop sub-screens">
|
|
3064
|
+
${renderWorkshopTabs(activeTab)}
|
|
3065
|
+
</div>
|
|
3066
|
+
${renderWorkshopPanel(activeTab)}
|
|
3067
|
+
</div>
|
|
3068
|
+
`;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
async function showWorkshop(options = {}) {
|
|
3072
|
+
if (!isCapabilityAvailable("workshop")) {
|
|
3073
|
+
const message = capabilityMessage("workshop", "Workshop is not available for this project.");
|
|
3074
|
+
setDocument("Workshop", renderWorkshop("terminals", { unavailable: true, message }));
|
|
3075
|
+
setMeta(message);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
const activeTab = options.tab && workshopTabs.some((tab) => tab.id === options.tab)
|
|
3079
|
+
? options.tab
|
|
3080
|
+
: preferredWorkshopTab();
|
|
3081
|
+
setWorkshopActiveTab(activeTab);
|
|
3082
|
+
setDocument("Workshop", renderWorkshop(activeTab));
|
|
3083
|
+
setMeta(`Workshop / ${activeTab}`);
|
|
3084
|
+
if (activeTab === "commands") {
|
|
3085
|
+
await loadWorkshopCommands();
|
|
3086
|
+
} else if (activeTab === "terminals") {
|
|
3087
|
+
// The Workshop DOM was just re-rendered, so every prior xterm host /
|
|
3088
|
+
// EventSource is gone. Drop them from the in-memory state too so the
|
|
3089
|
+
// remount path recreates fresh ones and the SSE stream replays the
|
|
3090
|
+
// session buffer.
|
|
3091
|
+
for (const entry of workshopTerminalState.sessions.values()) {
|
|
3092
|
+
if (entry.terminal) {
|
|
3093
|
+
try { entry.terminal.dispose(); } catch { /* noop */ }
|
|
3094
|
+
}
|
|
3095
|
+
entry.terminal = null;
|
|
3096
|
+
entry.fitAddon = null;
|
|
3097
|
+
closeWorkshopTerminalStream(entry.id);
|
|
3098
|
+
}
|
|
3099
|
+
renderWorkshopTerminalList();
|
|
3100
|
+
// Remount every session so switching between rows is instant and
|
|
3101
|
+
// none of the terminals show a black/empty stage.
|
|
3102
|
+
for (const entry of workshopTerminalState.sessions.values()) {
|
|
3103
|
+
mountWorkshopTerminalEmulator(entry);
|
|
3104
|
+
if (entry.id !== workshopTerminalState.activeId) {
|
|
3105
|
+
const host = workshopTerminalStageNode()?.querySelector(`[data-viewer-workshop-terminal-host="${entry.id}"]`);
|
|
3106
|
+
if (host instanceof HTMLElement) host.style.display = "none";
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (workshopTerminalState.activeId) {
|
|
3110
|
+
setActiveWorkshopTerminal(workshopTerminalState.activeId);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
async function openWorkspaceTree(path) {
|
|
3116
|
+
const [tree, preview] = await Promise.all([fetchWorkspaceTree(path), fetchWorkspacePreview(path)]);
|
|
3117
|
+
setDocument("Explorer", renderWorkspace(tree, preview));
|
|
3118
|
+
setMeta(path ? `Explorer folder ${path}` : "Explorer root.");
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
async function openWorkspacePreview(path) {
|
|
3122
|
+
const treePath = workspaceParentPath(path);
|
|
3123
|
+
const [tree, preview] = await Promise.all([fetchWorkspaceTree(treePath), fetchWorkspacePreview(path)]);
|
|
3124
|
+
setDocument("Explorer", renderWorkspace(tree, preview));
|
|
3125
|
+
setMeta(`Previewing ${path || "workspace root"}.`);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2048
3128
|
function objectEntries(value) {
|
|
2049
3129
|
return value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
|
|
2050
3130
|
}
|
|
@@ -2398,31 +3478,108 @@
|
|
|
2398
3478
|
return explicit === true ? "YES" : "-";
|
|
2399
3479
|
}
|
|
2400
3480
|
|
|
3481
|
+
function cdxProviderName(item) {
|
|
3482
|
+
return String(cdxField(item, ["provider", "name"], "unknown") || "unknown");
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function cdxKnownProviders(status, providers, sessions) {
|
|
3486
|
+
const names = new Set();
|
|
3487
|
+
providers.forEach((provider) => {
|
|
3488
|
+
const name = cdxProviderName(provider);
|
|
3489
|
+
if (name) {
|
|
3490
|
+
names.add(name);
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
sessions.forEach((session) => {
|
|
3494
|
+
const name = cdxProviderName(session);
|
|
3495
|
+
if (name) {
|
|
3496
|
+
names.add(name);
|
|
3497
|
+
}
|
|
3498
|
+
});
|
|
3499
|
+
pickFirstArray(status, ["providers", "providerStatus", "provider_status"]).forEach((provider) => {
|
|
3500
|
+
const name = cdxProviderName(provider);
|
|
3501
|
+
if (name) {
|
|
3502
|
+
names.add(name);
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
function filterCdxEntriesByProvider(entries, providerFilter) {
|
|
3509
|
+
if (providerFilter.mode !== "subset" || !providerFilter.selected.length) {
|
|
3510
|
+
return entries;
|
|
3511
|
+
}
|
|
3512
|
+
const selected = new Set(providerFilter.selected);
|
|
3513
|
+
return entries.filter((entry) => selected.has(cdxProviderName(entry)));
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function renderCdxStatusControls(knownProviders, visibleColumns, providerFilter) {
|
|
3517
|
+
const columnRows = cdxStatusColumns.map((column) => `
|
|
3518
|
+
<label class="viewer-cdx__menu-check">
|
|
3519
|
+
<input type="checkbox" data-viewer-cdx-column="${escapeHtml(column.id)}"${visibleColumns[column.id] ? " checked" : ""}>
|
|
3520
|
+
<span>${escapeHtml(column.label)}</span>
|
|
3521
|
+
</label>
|
|
3522
|
+
`).join("");
|
|
3523
|
+
const selected = new Set(providerFilter.mode === "subset" ? providerFilter.selected : knownProviders);
|
|
3524
|
+
const providerRows = knownProviders.map((provider) => `
|
|
3525
|
+
<label class="viewer-cdx__menu-check">
|
|
3526
|
+
<input type="checkbox" data-viewer-cdx-provider="${escapeHtml(provider)}"${selected.has(provider) ? " checked" : ""}>
|
|
3527
|
+
<span>${escapeHtml(provider)}</span>
|
|
3528
|
+
</label>
|
|
3529
|
+
`).join("");
|
|
3530
|
+
const providerSummary = providerFilter.mode === "subset" && providerFilter.selected.length
|
|
3531
|
+
? `${providerFilter.selected.length}/${knownProviders.length || providerFilter.selected.length}`
|
|
3532
|
+
: "All";
|
|
3533
|
+
return `
|
|
3534
|
+
<div class="viewer-cdx__controls" aria-label="CDX status table controls">
|
|
3535
|
+
<details class="viewer-cdx__menu">
|
|
3536
|
+
<summary class="viewer-cdx__icon-button" title="Configure status columns" aria-label="Configure status columns">
|
|
3537
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2 3.4-.2-.1a1.7 1.7 0 0 0-2 .1 1.7 1.7 0 0 0-.8 1.7v.2H9.2v-.2a1.7 1.7 0 0 0-.8-1.7 1.7 1.7 0 0 0-2-.1l-.2.1-2-3.4.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1.1H3v-3.8h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.9l-.1-.1 2-3.4.2.1a1.7 1.7 0 0 0 2-.1 1.7 1.7 0 0 0 .8-1.7v-.2h5.6v.2a1.7 1.7 0 0 0 .8 1.7 1.7 1.7 0 0 0 2 .1l.2-.1 2 3.4-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1.1h.1v3.8h-.1a1.7 1.7 0 0 0-1.5 1.1Z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
|
|
3538
|
+
</summary>
|
|
3539
|
+
<div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX status columns">${columnRows}</div>
|
|
3540
|
+
</details>
|
|
3541
|
+
<details class="viewer-cdx__menu">
|
|
3542
|
+
<summary class="viewer-cdx__icon-button" title="Filter status providers" aria-label="Filter status providers">
|
|
3543
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16l-6.5 7.2V19l-3 1.5v-7.3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>
|
|
3544
|
+
<span class="viewer-cdx__icon-count">${escapeHtml(providerSummary)}</span>
|
|
3545
|
+
</summary>
|
|
3546
|
+
<div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX provider filter">
|
|
3547
|
+
<button class="viewer-cdx__menu-action" type="button" data-viewer-cdx-provider-all>All providers</button>
|
|
3548
|
+
${providerRows || '<div class="viewer-cdx__empty">No providers reported.</div>'}
|
|
3549
|
+
</div>
|
|
3550
|
+
</details>
|
|
3551
|
+
</div>
|
|
3552
|
+
`;
|
|
3553
|
+
}
|
|
3554
|
+
|
|
2401
3555
|
function renderCdxSessionTable(sessions, emptyText) {
|
|
2402
3556
|
if (!sessions.length) {
|
|
2403
3557
|
return `<div class="viewer-cdx__empty">${escapeHtml(emptyText)}</div>`;
|
|
2404
3558
|
}
|
|
3559
|
+
const visibleColumns = cdxColumnVisibilityPreference();
|
|
3560
|
+
const cellRenderers = {
|
|
3561
|
+
session: (item) => {
|
|
3562
|
+
const name = cdxField(item, ["session_name", "name", "id", "value"]);
|
|
3563
|
+
return `<td class="viewer-cdx__session-name">${escapeHtml(`${name}${item.active ? "*" : ""}`)}</td>`;
|
|
3564
|
+
},
|
|
3565
|
+
provider: (item) => `<td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>`,
|
|
3566
|
+
status: (item) => `<td>${renderCdxBadge(cdxField(item, ["status", "state"]))}</td>`,
|
|
3567
|
+
auth: (item) => `<td>${escapeHtml(String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged"))}</td>`,
|
|
3568
|
+
ok: (item) => `<td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>`,
|
|
3569
|
+
remaining5h: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>`,
|
|
3570
|
+
remainingWeek: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>`,
|
|
3571
|
+
block: (item) => `<td>${escapeHtml(cdxSessionBlock(item))}</td>`,
|
|
3572
|
+
credits: (item) => `<td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>`,
|
|
3573
|
+
reset5h: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>`,
|
|
3574
|
+
resetWeek: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>`,
|
|
3575
|
+
updated: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>`
|
|
3576
|
+
};
|
|
3577
|
+
const activeColumns = cdxStatusColumns.filter((column) => visibleColumns[column.id]);
|
|
2405
3578
|
const rows = sessions.slice(0, 24).map((entry) => {
|
|
2406
3579
|
const item = entry && typeof entry === "object" ? entry : { value: entry };
|
|
2407
|
-
const name = cdxField(item, ["session_name", "name", "id", "value"]);
|
|
2408
|
-
const sessionName = `${name}${item.active ? "*" : ""}`;
|
|
2409
|
-
const status = cdxField(item, ["status", "state"]);
|
|
2410
|
-
const auth = String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged");
|
|
2411
|
-
const block = cdxSessionBlock(item);
|
|
2412
3580
|
return `
|
|
2413
3581
|
<tr>
|
|
2414
|
-
|
|
2415
|
-
<td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>
|
|
2416
|
-
<td>${renderCdxBadge(status)}</td>
|
|
2417
|
-
<td>${escapeHtml(auth)}</td>
|
|
2418
|
-
<td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>
|
|
2419
|
-
<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>
|
|
2420
|
-
<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>
|
|
2421
|
-
<td>${escapeHtml(block)}</td>
|
|
2422
|
-
<td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>
|
|
2423
|
-
<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>
|
|
2424
|
-
<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>
|
|
2425
|
-
<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>
|
|
3582
|
+
${activeColumns.map((column) => cellRenderers[column.id](item)).join("")}
|
|
2426
3583
|
</tr>
|
|
2427
3584
|
`;
|
|
2428
3585
|
}).join("");
|
|
@@ -2431,18 +3588,7 @@
|
|
|
2431
3588
|
<table class="viewer-cdx__table">
|
|
2432
3589
|
<thead>
|
|
2433
3590
|
<tr>
|
|
2434
|
-
|
|
2435
|
-
<th>PROV.</th>
|
|
2436
|
-
<th>STATUS</th>
|
|
2437
|
-
<th>AUTH</th>
|
|
2438
|
-
<th>OK</th>
|
|
2439
|
-
<th>5H</th>
|
|
2440
|
-
<th>WEEK</th>
|
|
2441
|
-
<th>BLOCK</th>
|
|
2442
|
-
<th>CR</th>
|
|
2443
|
-
<th>RESET 5H</th>
|
|
2444
|
-
<th>RESET WEEK</th>
|
|
2445
|
-
<th>UPDATED</th>
|
|
3591
|
+
${activeColumns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join("")}
|
|
2446
3592
|
</tr>
|
|
2447
3593
|
</thead>
|
|
2448
3594
|
<tbody>${rows}</tbody>
|
|
@@ -2718,8 +3864,12 @@
|
|
|
2718
3864
|
`;
|
|
2719
3865
|
}
|
|
2720
3866
|
const status = payload.status || {};
|
|
2721
|
-
const
|
|
2722
|
-
const
|
|
3867
|
+
const allProviders = cdxProviders(status);
|
|
3868
|
+
const allSessions = cdxSessions(status);
|
|
3869
|
+
const providerFilter = cdxProviderFilterPreference();
|
|
3870
|
+
const knownProviders = cdxKnownProviders(status, allProviders, allSessions);
|
|
3871
|
+
const providers = filterCdxEntriesByProvider(allProviders, providerFilter);
|
|
3872
|
+
const sessions = filterCdxEntriesByProvider(allSessions, providerFilter);
|
|
2723
3873
|
const readiness = cdxReadiness(status);
|
|
2724
3874
|
const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
|
|
2725
3875
|
.map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
|
|
@@ -2750,6 +3900,7 @@
|
|
|
2750
3900
|
<div class="viewer-cdx">
|
|
2751
3901
|
${renderCdxModeSwitcher("status")}
|
|
2752
3902
|
<div class="viewer-cdx__summary">${cards}</div>
|
|
3903
|
+
${renderCdxStatusControls(knownProviders, cdxColumnVisibilityPreference(), providerFilter)}
|
|
2753
3904
|
<div class="viewer-cdx__workspace">
|
|
2754
3905
|
<div class="viewer-cdx__stack">
|
|
2755
3906
|
<section class="viewer-cdx__section">
|
|
@@ -2776,6 +3927,12 @@
|
|
|
2776
3927
|
`;
|
|
2777
3928
|
}
|
|
2778
3929
|
|
|
3930
|
+
function rerenderCdxStatusFromPreferences() {
|
|
3931
|
+
if (isCdxStatusOpen() && latestCdxStatusPayload) {
|
|
3932
|
+
setDocument("CDX status", renderCdxStatus(latestCdxStatusPayload));
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
|
|
2779
3936
|
function renderCdxRuns(payload) {
|
|
2780
3937
|
if (!payload || payload.state !== "ok") {
|
|
2781
3938
|
return `
|
|
@@ -3106,6 +4263,7 @@
|
|
|
3106
4263
|
return;
|
|
3107
4264
|
}
|
|
3108
4265
|
latestCdxStatusSignature = nextCdxSignature;
|
|
4266
|
+
latestCdxStatusPayload = data.payload;
|
|
3109
4267
|
updateMainCdxBadge(data.payload);
|
|
3110
4268
|
setDocument("CDX status", renderCdxStatus(data.payload));
|
|
3111
4269
|
setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
|
|
@@ -3820,6 +4978,12 @@
|
|
|
3820
4978
|
withPrimaryAction("insights", "Loading insights", showCorpusInsights);
|
|
3821
4979
|
});
|
|
3822
4980
|
});
|
|
4981
|
+
[workspaceButton()].forEach((button) => {
|
|
4982
|
+
button?.addEventListener("click", () => {
|
|
4983
|
+
setRefreshMenuOpen(false);
|
|
4984
|
+
withPrimaryAction("workspace", "Loading Explorer", showWorkspace);
|
|
4985
|
+
});
|
|
4986
|
+
});
|
|
3823
4987
|
const autoControl = autoRefreshControl();
|
|
3824
4988
|
if (autoControl instanceof HTMLInputElement) {
|
|
3825
4989
|
autoControl.addEventListener("change", () => {
|
|
@@ -3869,6 +5033,19 @@
|
|
|
3869
5033
|
setRefreshMenuOpen(false);
|
|
3870
5034
|
withPrimaryAction("health", "Checking health", showHealth);
|
|
3871
5035
|
});
|
|
5036
|
+
document.getElementById("viewer-lan-banner-copy")?.addEventListener("click", async () => {
|
|
5037
|
+
const share = latestLanShareUrl;
|
|
5038
|
+
if (!share) return;
|
|
5039
|
+
const ok = await copyTextToClipboard(share);
|
|
5040
|
+
if (ok) {
|
|
5041
|
+
setMeta("LAN share URL copied to the clipboard.");
|
|
5042
|
+
} else {
|
|
5043
|
+
setMeta(`Copy failed — long-press to select: ${share}`);
|
|
5044
|
+
}
|
|
5045
|
+
});
|
|
5046
|
+
document.getElementById("viewer-workshop")?.addEventListener("click", () => {
|
|
5047
|
+
withPrimaryAction("workshop", "Opening Workshop", () => showWorkshop());
|
|
5048
|
+
});
|
|
3872
5049
|
document.getElementById("viewer-git")?.addEventListener("click", () => {
|
|
3873
5050
|
withPrimaryAction("git", "Checking Git status", showGitStatus);
|
|
3874
5051
|
});
|
|
@@ -3914,6 +5091,8 @@
|
|
|
3914
5091
|
document.addEventListener("change", (event) => {
|
|
3915
5092
|
const sessionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-session]") : null;
|
|
3916
5093
|
const cdxInputTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-input]") : null;
|
|
5094
|
+
const cdxColumnTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-column]") : null;
|
|
5095
|
+
const cdxProviderTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider]") : null;
|
|
3917
5096
|
if (sessionTarget instanceof HTMLSelectElement) {
|
|
3918
5097
|
latestCdxMissionState.sessionId = sessionTarget.value || "";
|
|
3919
5098
|
latestCdxMissionState.planPayload = null;
|
|
@@ -3930,6 +5109,25 @@
|
|
|
3930
5109
|
latestCdxMissionState.applyPayload = null;
|
|
3931
5110
|
}
|
|
3932
5111
|
}
|
|
5112
|
+
if (cdxColumnTarget instanceof HTMLInputElement) {
|
|
5113
|
+
persistCdxColumnVisibility(cdxColumnTarget.getAttribute("data-viewer-cdx-column") || "", cdxColumnTarget.checked);
|
|
5114
|
+
rerenderCdxStatusFromPreferences();
|
|
5115
|
+
}
|
|
5116
|
+
if (cdxProviderTarget instanceof HTMLInputElement) {
|
|
5117
|
+
const provider = cdxProviderTarget.getAttribute("data-viewer-cdx-provider") || "";
|
|
5118
|
+
const status = latestCdxStatusPayload?.status || {};
|
|
5119
|
+
const allProviders = cdxKnownProviders(status, cdxProviders(status), cdxSessions(status));
|
|
5120
|
+
const current = cdxProviderFilterPreference();
|
|
5121
|
+
const selected = new Set(current.mode === "subset" ? current.selected : allProviders);
|
|
5122
|
+
if (cdxProviderTarget.checked) {
|
|
5123
|
+
selected.add(provider);
|
|
5124
|
+
} else {
|
|
5125
|
+
selected.delete(provider);
|
|
5126
|
+
}
|
|
5127
|
+
const nextSelected = Array.from(selected).filter((entry) => allProviders.includes(entry));
|
|
5128
|
+
persistCdxProviderFilter(nextSelected.length === allProviders.length ? { mode: "all", selected: [] } : { mode: "subset", selected: nextSelected });
|
|
5129
|
+
rerenderCdxStatusFromPreferences();
|
|
5130
|
+
}
|
|
3933
5131
|
});
|
|
3934
5132
|
document.addEventListener("click", (event) => {
|
|
3935
5133
|
window.setTimeout(() => applyLocalViewerChrome(), 0);
|
|
@@ -3940,12 +5138,22 @@
|
|
|
3940
5138
|
const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
|
|
3941
5139
|
const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
|
|
3942
5140
|
const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
|
|
5141
|
+
const workspaceTreeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-tree]") : null;
|
|
5142
|
+
const workspacePreviewTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-preview]") : null;
|
|
5143
|
+
const workshopTabTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-tab]") : null;
|
|
5144
|
+
const workshopRunTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-command-run]") : null;
|
|
5145
|
+
const workshopStopTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-command-stop]") : null;
|
|
5146
|
+
const workshopTerminalNewTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-terminal-new]") : null;
|
|
5147
|
+
const workshopTerminalCustomTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-terminal-custom]") : null;
|
|
5148
|
+
const workshopTerminalSelectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-terminal-select]") : null;
|
|
5149
|
+
const workshopTerminalCloseTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workshop-terminal-close]") : null;
|
|
3943
5150
|
const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
|
|
3944
5151
|
const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
|
|
3945
5152
|
const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
|
|
3946
5153
|
const cdxBackRunsTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-back-runs]") : null;
|
|
3947
5154
|
const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
|
|
3948
5155
|
const cdxArtifactTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-artifact-path]") : null;
|
|
5156
|
+
const cdxProviderAllTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider-all]") : null;
|
|
3949
5157
|
const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
|
|
3950
5158
|
const cdxMissionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mission]") : null;
|
|
3951
5159
|
const cdxStrengthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-strength]") : null;
|
|
@@ -3981,6 +5189,11 @@
|
|
|
3981
5189
|
withCdxMissionAction("cdx-apply-plan", "Applying CDX mission plan", applyCdxMissionPlan);
|
|
3982
5190
|
return;
|
|
3983
5191
|
}
|
|
5192
|
+
if (cdxProviderAllTarget instanceof HTMLElement) {
|
|
5193
|
+
persistCdxProviderFilter({ mode: "all", selected: [] });
|
|
5194
|
+
rerenderCdxStatusFromPreferences();
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
3984
5197
|
if (cdxBackRunsTarget instanceof HTMLElement) {
|
|
3985
5198
|
withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
|
|
3986
5199
|
return;
|
|
@@ -4008,6 +5221,62 @@
|
|
|
4008
5221
|
}
|
|
4009
5222
|
return;
|
|
4010
5223
|
}
|
|
5224
|
+
if (workshopTabTarget instanceof HTMLElement) {
|
|
5225
|
+
event.preventDefault();
|
|
5226
|
+
const tab = workshopTabTarget.getAttribute("data-viewer-workshop-tab") || "terminals";
|
|
5227
|
+
withPrimaryAction("workshop-tab", `Switching to ${tab}`, () => showWorkshop({ tab }));
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
5230
|
+
if (workshopTerminalCloseTarget instanceof HTMLElement) {
|
|
5231
|
+
event.preventDefault();
|
|
5232
|
+
event.stopPropagation();
|
|
5233
|
+
const id = workshopTerminalCloseTarget.getAttribute("data-viewer-workshop-terminal-close") || "";
|
|
5234
|
+
if (id) stopWorkshopTerminal(id);
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
if (workshopTerminalNewTarget instanceof HTMLElement) {
|
|
5238
|
+
event.preventDefault();
|
|
5239
|
+
spawnWorkshopTerminal();
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5242
|
+
if (workshopTerminalCustomTarget instanceof HTMLElement) {
|
|
5243
|
+
event.preventDefault();
|
|
5244
|
+
spawnCustomWorkshopTerminal();
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
if (workshopTerminalSelectTarget instanceof HTMLElement) {
|
|
5248
|
+
event.preventDefault();
|
|
5249
|
+
const id = workshopTerminalSelectTarget.getAttribute("data-viewer-workshop-terminal-select") || "";
|
|
5250
|
+
if (id) setActiveWorkshopTerminal(id);
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
if (workshopRunTarget instanceof HTMLElement) {
|
|
5254
|
+
event.preventDefault();
|
|
5255
|
+
const commandId = workshopRunTarget.getAttribute("data-viewer-workshop-command-run") || "";
|
|
5256
|
+
if (commandId) {
|
|
5257
|
+
updateWorkshopCommandSession(commandId, { state: "starting", logText: "" });
|
|
5258
|
+
startWorkshopCommand(commandId);
|
|
5259
|
+
}
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5262
|
+
if (workshopStopTarget instanceof HTMLElement) {
|
|
5263
|
+
event.preventDefault();
|
|
5264
|
+
const commandId = workshopStopTarget.getAttribute("data-viewer-workshop-command-stop") || "";
|
|
5265
|
+
if (commandId) {
|
|
5266
|
+
stopWorkshopCommand(commandId);
|
|
5267
|
+
}
|
|
5268
|
+
return;
|
|
5269
|
+
}
|
|
5270
|
+
if (workspaceTreeTarget instanceof HTMLElement) {
|
|
5271
|
+
event.preventDefault();
|
|
5272
|
+
withPrimaryAction("workspace-tree", "Loading Explorer folder", () => openWorkspaceTree(workspaceTreeTarget.getAttribute("data-viewer-workspace-tree") || ""));
|
|
5273
|
+
return;
|
|
5274
|
+
}
|
|
5275
|
+
if (workspacePreviewTarget instanceof HTMLElement) {
|
|
5276
|
+
event.preventDefault();
|
|
5277
|
+
withPrimaryAction("workspace-preview", "Loading Explorer preview", () => openWorkspacePreview(workspacePreviewTarget.getAttribute("data-viewer-workspace-preview") || ""));
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
4011
5280
|
if (projectSwitcherTarget instanceof HTMLElement) {
|
|
4012
5281
|
const menu = projectMenu();
|
|
4013
5282
|
setProjectMenuOpen(Boolean(menu?.hidden));
|