@grifhinz/logics-manager 2.8.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  (() => {
2
2
  const stateKey = "logics.localViewer.state";
3
+ const preferenceKey = "logics.localViewer.preferences.v1";
4
+ const preferenceVersion = 1;
3
5
  const meta = () => document.getElementById("viewer-meta");
4
6
  const documentPanel = () => document.getElementById("viewer-document");
5
7
  const documentTitle = () => document.getElementById("viewer-document-title");
@@ -16,6 +18,7 @@
16
18
  const projectMenu = () => document.getElementById("viewer-project-menu");
17
19
  const repoGithubLink = () => document.getElementById("viewer-repo-github");
18
20
  const repoFolderButton = () => document.getElementById("viewer-repo-folder");
21
+ const workspaceButton = () => document.getElementById("viewer-workspace");
19
22
  const ciButton = () => document.getElementById("viewer-ci");
20
23
  const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
21
24
  const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
@@ -72,10 +75,27 @@
72
75
  let latestViewerStateSignature = "";
73
76
  let latestGitStatusSignature = "";
74
77
  let latestCdxStatusSignature = "";
78
+ let latestCdxStatusPayload = null;
75
79
  let latestCiStatusSignature = "";
76
80
  let primaryActionBusyKey = "";
77
81
  let cdxMissionBusyKey = "";
78
82
  let cdxCloseTarget = null;
83
+ let viewerPreferences = readViewerPreferences();
84
+ let autoRefreshIntervalForcedByLaunch = false;
85
+ const cdxStatusColumns = [
86
+ { id: "session", label: "SESSION" },
87
+ { id: "provider", label: "PROV." },
88
+ { id: "status", label: "STATUS" },
89
+ { id: "auth", label: "AUTH" },
90
+ { id: "ok", label: "OK" },
91
+ { id: "remaining5h", label: "5H" },
92
+ { id: "remainingWeek", label: "WEEK" },
93
+ { id: "block", label: "BLOCK", defaultVisible: false },
94
+ { id: "credits", label: "CR", defaultVisible: false },
95
+ { id: "reset5h", label: "RESET 5H" },
96
+ { id: "resetWeek", label: "RESET WEEK" },
97
+ { id: "updated", label: "UPDATED" }
98
+ ];
79
99
 
80
100
  function readStoredState() {
81
101
  try {
@@ -85,6 +105,83 @@
85
105
  }
86
106
  }
87
107
 
108
+ function readViewerPreferences() {
109
+ try {
110
+ const value = JSON.parse(window.localStorage.getItem(preferenceKey) || "null");
111
+ if (!value || typeof value !== "object" || value.version !== preferenceVersion) {
112
+ return { version: preferenceVersion };
113
+ }
114
+ return { ...value, version: preferenceVersion };
115
+ } catch {
116
+ return { version: preferenceVersion };
117
+ }
118
+ }
119
+
120
+ function writeViewerPreferences(nextPreferences) {
121
+ viewerPreferences = { ...nextPreferences, version: preferenceVersion };
122
+ try {
123
+ window.localStorage.setItem(preferenceKey, JSON.stringify(viewerPreferences));
124
+ } catch {
125
+ // Keep the in-memory preference for this session when browser storage is unavailable.
126
+ }
127
+ }
128
+
129
+ function updateViewerPreferences(patch) {
130
+ writeViewerPreferences({ ...viewerPreferences, ...patch });
131
+ }
132
+
133
+ function preferredAutoRefreshIntervalSeconds() {
134
+ const seconds = Number(viewerPreferences.autoRefreshIntervalSeconds);
135
+ return Number.isFinite(seconds) && seconds > 0 ? normalizeAutoRefreshIntervalSeconds(seconds) : null;
136
+ }
137
+
138
+ function cdxColumnVisibilityPreference() {
139
+ const stored = viewerPreferences.cdxStatusColumns;
140
+ const storedVisibility = stored && typeof stored === "object" ? stored.visibility : null;
141
+ const visibility = {};
142
+ cdxStatusColumns.forEach((column) => {
143
+ visibility[column.id] = column.defaultVisible !== false;
144
+ if (storedVisibility && typeof storedVisibility[column.id] === "boolean") {
145
+ visibility[column.id] = storedVisibility[column.id];
146
+ }
147
+ });
148
+ return visibility;
149
+ }
150
+
151
+ function persistCdxColumnVisibility(columnId, visible) {
152
+ const current = cdxColumnVisibilityPreference();
153
+ if (!cdxStatusColumns.some((column) => column.id === columnId)) {
154
+ return;
155
+ }
156
+ updateViewerPreferences({
157
+ cdxStatusColumns: {
158
+ visibility: { ...current, [columnId]: Boolean(visible) }
159
+ }
160
+ });
161
+ }
162
+
163
+ function cdxProviderFilterPreference() {
164
+ const stored = viewerPreferences.cdxStatusProviders;
165
+ if (!stored || typeof stored !== "object" || stored.mode !== "subset") {
166
+ return { mode: "all", selected: [] };
167
+ }
168
+ const selected = Array.isArray(stored.selected)
169
+ ? stored.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
170
+ : [];
171
+ return selected.length ? { mode: "subset", selected: Array.from(new Set(selected)) } : { mode: "all", selected: [] };
172
+ }
173
+
174
+ function persistCdxProviderFilter(nextFilter) {
175
+ const selected = Array.isArray(nextFilter?.selected)
176
+ ? nextFilter.selected.map((entry) => String(entry || "").trim()).filter(Boolean)
177
+ : [];
178
+ updateViewerPreferences({
179
+ cdxStatusProviders: selected.length
180
+ ? { mode: "subset", selected: Array.from(new Set(selected)).sort() }
181
+ : { mode: "all", selected: [] }
182
+ });
183
+ }
184
+
88
185
  function sanitizeViewerFilterState(value) {
89
186
  const nextState = { ...defaultFilterState };
90
187
  if (!value || typeof value !== "object") {
@@ -155,6 +252,7 @@
155
252
  return Array.from(document.querySelectorAll([
156
253
  "#viewer-insights",
157
254
  "#viewer-health",
255
+ "#viewer-workspace",
158
256
  "#viewer-git",
159
257
  "#viewer-ci",
160
258
  "#viewer-cdx",
@@ -452,6 +550,7 @@
452
550
  autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(value) * 1000;
453
551
  if (options.user) {
454
552
  autoRefreshIntervalTouched = true;
553
+ updateViewerPreferences({ autoRefreshIntervalSeconds: Math.round(autoRefreshIntervalMs / 1000) });
455
554
  }
456
555
  updateRefreshIntervalControl();
457
556
  scheduleNextAutoRefresh();
@@ -586,6 +685,7 @@
586
685
  const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
587
686
  return {
588
687
  logics: capabilities.logics || { state: "ready", available: true, message: "" },
688
+ workspace: capabilities.workspace || { state: "ready", available: true, message: "" },
589
689
  git: capabilities.git || { state: "ready", available: true, message: "" },
590
690
  ci: capabilities.ci || { state: "ready", available: true, message: "" },
591
691
  cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
@@ -624,6 +724,16 @@
624
724
  }
625
725
 
626
726
  function updateCapabilityControls() {
727
+ const workspace = workspaceButton();
728
+ if (workspace instanceof HTMLElement) {
729
+ workspace.hidden = !isCapabilityAvailable("workspace");
730
+ if (isCapabilityAvailable("workspace")) {
731
+ setButtonAvailable(workspace, "Show file explorer");
732
+ } else {
733
+ setButtonUnavailable(workspace, capabilityMessage("workspace", "Explorer is not available for this project."));
734
+ }
735
+ }
736
+
627
737
  const gitButton = document.getElementById("viewer-git");
628
738
  if (gitButton instanceof HTMLElement) {
629
739
  gitButton.hidden = !isCapabilityAvailable("git");
@@ -1225,7 +1335,13 @@
1225
1335
  latestViewerStateSignature = nextSignature;
1226
1336
  latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
1227
1337
  if (!autoRefreshIntervalTouched) {
1228
- autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
1338
+ const launchSeconds = Number(payload.autoRefreshIntervalSeconds);
1339
+ const preferredSeconds = preferredAutoRefreshIntervalSeconds();
1340
+ autoRefreshIntervalForcedByLaunch = Boolean(payload.autoRefreshIntervalForced);
1341
+ const nextSeconds = autoRefreshIntervalForcedByLaunch || preferredSeconds === null
1342
+ ? launchSeconds
1343
+ : preferredSeconds;
1344
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(nextSeconds) * 1000;
1229
1345
  updateRefreshIntervalControl();
1230
1346
  }
1231
1347
  updateRepositoryIdentity(payload);
@@ -1302,6 +1418,12 @@
1302
1418
  return Boolean(panel && !panel.hidden && title && title.textContent === "Git status");
1303
1419
  }
1304
1420
 
1421
+ function isWorkspaceOpen() {
1422
+ const panel = documentPanel();
1423
+ const title = documentTitle();
1424
+ return Boolean(panel && !panel.hidden && title && title.textContent === "Explorer");
1425
+ }
1426
+
1305
1427
  function isCdxStatusOpen() {
1306
1428
  const panel = documentPanel();
1307
1429
  const title = documentTitle();
@@ -1328,7 +1450,11 @@
1328
1450
 
1329
1451
  async function refreshViewer(method = "POST", options = {}) {
1330
1452
  const changed = await loadItems(method, options);
1331
- if (isGitStatusOpen()) {
1453
+ if (isWorkspaceOpen()) {
1454
+ if (changed || options.force) {
1455
+ await showWorkspace({ silent: Boolean(options.silent) });
1456
+ }
1457
+ } else if (isGitStatusOpen()) {
1332
1458
  await showGitStatus({ preserve: true, silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
1333
1459
  } else if (isCiStatusOpen()) {
1334
1460
  await showCiStatus({ silent: Boolean(options.silent), skipUnchanged: !changed && !options.force, force: Boolean(options.force) });
@@ -2045,6 +2171,145 @@
2045
2171
  setMeta("Health loaded.");
2046
2172
  }
2047
2173
 
2174
+ function workspaceParentPath(path) {
2175
+ const parts = String(path || "").split("/").filter(Boolean);
2176
+ parts.pop();
2177
+ return parts.join("/");
2178
+ }
2179
+
2180
+ function renderWorkspaceTree(treePayload, selectedPath = "") {
2181
+ if (!treePayload || treePayload.state !== "ok") {
2182
+ return `<div class="viewer-workspace__empty">${escapeHtml(treePayload?.message || "Workspace tree is unavailable.")}</div>`;
2183
+ }
2184
+ const currentPath = String(treePayload.path || "");
2185
+ const parentPath = workspaceParentPath(currentPath);
2186
+ const upButton = currentPath
2187
+ ? `<button class="viewer-workspace__item viewer-workspace__item--up" type="button" data-viewer-workspace-tree="${escapeHtml(parentPath)}">..</button>`
2188
+ : "";
2189
+ const rows = (Array.isArray(treePayload.entries) ? treePayload.entries : []).map((entry) => {
2190
+ const path = String(entry.path || "");
2191
+ const kind = String(entry.kind || "file");
2192
+ const ignored = Boolean(entry.ignored);
2193
+ const selected = path === selectedPath;
2194
+ const actionAttr = kind === "directory" && !ignored
2195
+ ? `data-viewer-workspace-tree="${escapeHtml(path)}"`
2196
+ : `data-viewer-workspace-preview="${escapeHtml(path)}"`;
2197
+ const icon = kind === "directory" ? (ignored ? "x" : ">") : "-";
2198
+ return `
2199
+ <button class="viewer-workspace__item${selected ? " is-selected" : ""}${ignored ? " is-muted" : ""}" type="button" ${actionAttr} title="${escapeHtml(path)}">
2200
+ <span class="viewer-workspace__item-icon" aria-hidden="true">${escapeHtml(icon)}</span>
2201
+ <span class="viewer-workspace__item-name">${escapeHtml(entry.name || path || "/")}</span>
2202
+ </button>
2203
+ `;
2204
+ }).join("");
2205
+ return `
2206
+ <div class="viewer-workspace__tree-header">
2207
+ <span>${escapeHtml(currentPath || "/")}</span>
2208
+ </div>
2209
+ <div class="viewer-workspace__tree-list">
2210
+ ${upButton}
2211
+ ${rows || '<div class="viewer-workspace__empty">Directory is empty.</div>'}
2212
+ </div>
2213
+ ${treePayload.truncated ? '<div class="viewer-workspace__empty">Directory listing truncated.</div>' : ""}
2214
+ `;
2215
+ }
2216
+
2217
+ function renderWorkspacePreview(previewPayload) {
2218
+ if (!previewPayload) {
2219
+ return '<div class="viewer-workspace__empty">Select a file or directory.</div>';
2220
+ }
2221
+ const path = previewPayload.path || "/";
2222
+ const name = previewPayload.name || path || "/";
2223
+ const state = previewPayload.state || "unknown";
2224
+ if (state === "ok") {
2225
+ return `
2226
+ <div class="viewer-workspace__preview-header">
2227
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2228
+ <em>${escapeHtml(previewPayload.truncated ? "truncated" : `${previewPayload.size || 0} bytes`)}</em>
2229
+ </div>
2230
+ ${previewPayload.truncated ? '<div class="viewer-cdx__state viewer-cdx__state--warn">Preview truncated.</div>' : ""}
2231
+ <pre class="viewer-workspace__code">${escapeHtml(previewPayload.content || "")}</pre>
2232
+ `;
2233
+ }
2234
+ if (state === "image") {
2235
+ return `
2236
+ <div class="viewer-workspace__preview-header">
2237
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2238
+ <em>${escapeHtml(previewPayload.contentType || "image")}</em>
2239
+ </div>
2240
+ <img class="viewer-workspace__image" src="/api/workspace-file?path=${encodeURIComponent(path)}" alt="${escapeHtml(name)}">
2241
+ `;
2242
+ }
2243
+ return `
2244
+ <div class="viewer-workspace__preview-header">
2245
+ <div><strong>${escapeHtml(name)}</strong><span>${escapeHtml(path)}</span></div>
2246
+ <em>${escapeHtml(state)}</em>
2247
+ </div>
2248
+ <div class="viewer-workspace__empty">${escapeHtml(previewPayload.message || "No preview is available.")}</div>
2249
+ `;
2250
+ }
2251
+
2252
+ function renderWorkspace(treePayload, previewPayload) {
2253
+ const selectedPath = previewPayload?.path || "";
2254
+ return `
2255
+ <div class="viewer-workspace">
2256
+ <aside class="viewer-workspace__tree" aria-label="Workspace files">
2257
+ ${renderWorkspaceTree(treePayload, selectedPath)}
2258
+ </aside>
2259
+ <section class="viewer-workspace__preview" aria-label="Workspace preview">
2260
+ ${renderWorkspacePreview(previewPayload)}
2261
+ </section>
2262
+ </div>
2263
+ `;
2264
+ }
2265
+
2266
+ async function fetchWorkspaceTree(path = "") {
2267
+ const response = await fetch(`/api/workspace-tree?path=${encodeURIComponent(path)}`);
2268
+ const data = await response.json();
2269
+ if (!response.ok || !data.ok) {
2270
+ throw new Error(data.error || "Unable to load workspace tree.");
2271
+ }
2272
+ return data.payload;
2273
+ }
2274
+
2275
+ async function fetchWorkspacePreview(path = "") {
2276
+ const response = await fetch(`/api/workspace-preview?path=${encodeURIComponent(path)}`);
2277
+ const data = await response.json();
2278
+ if (!response.ok || !data.ok) {
2279
+ throw new Error(data.error || "Unable to load workspace preview.");
2280
+ }
2281
+ return data.payload;
2282
+ }
2283
+
2284
+ async function showWorkspace(options = {}) {
2285
+ if (!isCapabilityAvailable("workspace")) {
2286
+ const message = capabilityMessage("workspace", "Explorer is not available for this project.");
2287
+ setDocument("Explorer", renderWorkspace({ state: "unavailable", message }, { state: "unavailable", message }));
2288
+ setMeta(message);
2289
+ return;
2290
+ }
2291
+ if (!options.silent) {
2292
+ setMeta("Loading workspace...");
2293
+ }
2294
+ const tree = await fetchWorkspaceTree("");
2295
+ const preview = await fetchWorkspacePreview("");
2296
+ setDocument("Explorer", renderWorkspace(tree, preview));
2297
+ setMeta(options.silent ? "Explorer refreshed." : "Explorer loaded.");
2298
+ }
2299
+
2300
+ async function openWorkspaceTree(path) {
2301
+ const [tree, preview] = await Promise.all([fetchWorkspaceTree(path), fetchWorkspacePreview(path)]);
2302
+ setDocument("Explorer", renderWorkspace(tree, preview));
2303
+ setMeta(path ? `Explorer folder ${path}` : "Explorer root.");
2304
+ }
2305
+
2306
+ async function openWorkspacePreview(path) {
2307
+ const treePath = workspaceParentPath(path);
2308
+ const [tree, preview] = await Promise.all([fetchWorkspaceTree(treePath), fetchWorkspacePreview(path)]);
2309
+ setDocument("Explorer", renderWorkspace(tree, preview));
2310
+ setMeta(`Previewing ${path || "workspace root"}.`);
2311
+ }
2312
+
2048
2313
  function objectEntries(value) {
2049
2314
  return value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
2050
2315
  }
@@ -2398,31 +2663,108 @@
2398
2663
  return explicit === true ? "YES" : "-";
2399
2664
  }
2400
2665
 
2666
+ function cdxProviderName(item) {
2667
+ return String(cdxField(item, ["provider", "name"], "unknown") || "unknown");
2668
+ }
2669
+
2670
+ function cdxKnownProviders(status, providers, sessions) {
2671
+ const names = new Set();
2672
+ providers.forEach((provider) => {
2673
+ const name = cdxProviderName(provider);
2674
+ if (name) {
2675
+ names.add(name);
2676
+ }
2677
+ });
2678
+ sessions.forEach((session) => {
2679
+ const name = cdxProviderName(session);
2680
+ if (name) {
2681
+ names.add(name);
2682
+ }
2683
+ });
2684
+ pickFirstArray(status, ["providers", "providerStatus", "provider_status"]).forEach((provider) => {
2685
+ const name = cdxProviderName(provider);
2686
+ if (name) {
2687
+ names.add(name);
2688
+ }
2689
+ });
2690
+ return Array.from(names).sort((left, right) => left.localeCompare(right));
2691
+ }
2692
+
2693
+ function filterCdxEntriesByProvider(entries, providerFilter) {
2694
+ if (providerFilter.mode !== "subset" || !providerFilter.selected.length) {
2695
+ return entries;
2696
+ }
2697
+ const selected = new Set(providerFilter.selected);
2698
+ return entries.filter((entry) => selected.has(cdxProviderName(entry)));
2699
+ }
2700
+
2701
+ function renderCdxStatusControls(knownProviders, visibleColumns, providerFilter) {
2702
+ const columnRows = cdxStatusColumns.map((column) => `
2703
+ <label class="viewer-cdx__menu-check">
2704
+ <input type="checkbox" data-viewer-cdx-column="${escapeHtml(column.id)}"${visibleColumns[column.id] ? " checked" : ""}>
2705
+ <span>${escapeHtml(column.label)}</span>
2706
+ </label>
2707
+ `).join("");
2708
+ const selected = new Set(providerFilter.mode === "subset" ? providerFilter.selected : knownProviders);
2709
+ const providerRows = knownProviders.map((provider) => `
2710
+ <label class="viewer-cdx__menu-check">
2711
+ <input type="checkbox" data-viewer-cdx-provider="${escapeHtml(provider)}"${selected.has(provider) ? " checked" : ""}>
2712
+ <span>${escapeHtml(provider)}</span>
2713
+ </label>
2714
+ `).join("");
2715
+ const providerSummary = providerFilter.mode === "subset" && providerFilter.selected.length
2716
+ ? `${providerFilter.selected.length}/${knownProviders.length || providerFilter.selected.length}`
2717
+ : "All";
2718
+ return `
2719
+ <div class="viewer-cdx__controls" aria-label="CDX status table controls">
2720
+ <details class="viewer-cdx__menu">
2721
+ <summary class="viewer-cdx__icon-button" title="Configure status columns" aria-label="Configure status columns">
2722
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2 3.4-.2-.1a1.7 1.7 0 0 0-2 .1 1.7 1.7 0 0 0-.8 1.7v.2H9.2v-.2a1.7 1.7 0 0 0-.8-1.7 1.7 1.7 0 0 0-2-.1l-.2.1-2-3.4.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1.1H3v-3.8h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.9l-.1-.1 2-3.4.2.1a1.7 1.7 0 0 0 2-.1 1.7 1.7 0 0 0 .8-1.7v-.2h5.6v.2a1.7 1.7 0 0 0 .8 1.7 1.7 1.7 0 0 0 2 .1l.2-.1 2 3.4-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1.1h.1v3.8h-.1a1.7 1.7 0 0 0-1.5 1.1Z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
2723
+ </summary>
2724
+ <div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX status columns">${columnRows}</div>
2725
+ </details>
2726
+ <details class="viewer-cdx__menu">
2727
+ <summary class="viewer-cdx__icon-button" title="Filter status providers" aria-label="Filter status providers">
2728
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16l-6.5 7.2V19l-3 1.5v-7.3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>
2729
+ <span class="viewer-cdx__icon-count">${escapeHtml(providerSummary)}</span>
2730
+ </summary>
2731
+ <div class="viewer-cdx__menu-panel" role="menu" aria-label="CDX provider filter">
2732
+ <button class="viewer-cdx__menu-action" type="button" data-viewer-cdx-provider-all>All providers</button>
2733
+ ${providerRows || '<div class="viewer-cdx__empty">No providers reported.</div>'}
2734
+ </div>
2735
+ </details>
2736
+ </div>
2737
+ `;
2738
+ }
2739
+
2401
2740
  function renderCdxSessionTable(sessions, emptyText) {
2402
2741
  if (!sessions.length) {
2403
2742
  return `<div class="viewer-cdx__empty">${escapeHtml(emptyText)}</div>`;
2404
2743
  }
2744
+ const visibleColumns = cdxColumnVisibilityPreference();
2745
+ const cellRenderers = {
2746
+ session: (item) => {
2747
+ const name = cdxField(item, ["session_name", "name", "id", "value"]);
2748
+ return `<td class="viewer-cdx__session-name">${escapeHtml(`${name}${item.active ? "*" : ""}`)}</td>`;
2749
+ },
2750
+ provider: (item) => `<td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>`,
2751
+ status: (item) => `<td>${renderCdxBadge(cdxField(item, ["status", "state"]))}</td>`,
2752
+ auth: (item) => `<td>${escapeHtml(String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged"))}</td>`,
2753
+ ok: (item) => `<td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>`,
2754
+ remaining5h: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>`,
2755
+ remainingWeek: (item) => `<td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>`,
2756
+ block: (item) => `<td>${escapeHtml(cdxSessionBlock(item))}</td>`,
2757
+ credits: (item) => `<td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>`,
2758
+ reset5h: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>`,
2759
+ resetWeek: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>`,
2760
+ updated: (item) => `<td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>`
2761
+ };
2762
+ const activeColumns = cdxStatusColumns.filter((column) => visibleColumns[column.id]);
2405
2763
  const rows = sessions.slice(0, 24).map((entry) => {
2406
2764
  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
2765
  return `
2413
2766
  <tr>
2414
- <td class="viewer-cdx__session-name">${escapeHtml(sessionName)}</td>
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>
2767
+ ${activeColumns.map((column) => cellRenderers[column.id](item)).join("")}
2426
2768
  </tr>
2427
2769
  `;
2428
2770
  }).join("");
@@ -2431,18 +2773,7 @@
2431
2773
  <table class="viewer-cdx__table">
2432
2774
  <thead>
2433
2775
  <tr>
2434
- <th>SESSION</th>
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>
2776
+ ${activeColumns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join("")}
2446
2777
  </tr>
2447
2778
  </thead>
2448
2779
  <tbody>${rows}</tbody>
@@ -2718,8 +3049,12 @@
2718
3049
  `;
2719
3050
  }
2720
3051
  const status = payload.status || {};
2721
- const providers = cdxProviders(status);
2722
- const sessions = cdxSessions(status);
3052
+ const allProviders = cdxProviders(status);
3053
+ const allSessions = cdxSessions(status);
3054
+ const providerFilter = cdxProviderFilterPreference();
3055
+ const knownProviders = cdxKnownProviders(status, allProviders, allSessions);
3056
+ const providers = filterCdxEntriesByProvider(allProviders, providerFilter);
3057
+ const sessions = filterCdxEntriesByProvider(allSessions, providerFilter);
2723
3058
  const readiness = cdxReadiness(status);
2724
3059
  const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
2725
3060
  .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
@@ -2750,6 +3085,7 @@
2750
3085
  <div class="viewer-cdx">
2751
3086
  ${renderCdxModeSwitcher("status")}
2752
3087
  <div class="viewer-cdx__summary">${cards}</div>
3088
+ ${renderCdxStatusControls(knownProviders, cdxColumnVisibilityPreference(), providerFilter)}
2753
3089
  <div class="viewer-cdx__workspace">
2754
3090
  <div class="viewer-cdx__stack">
2755
3091
  <section class="viewer-cdx__section">
@@ -2776,6 +3112,12 @@
2776
3112
  `;
2777
3113
  }
2778
3114
 
3115
+ function rerenderCdxStatusFromPreferences() {
3116
+ if (isCdxStatusOpen() && latestCdxStatusPayload) {
3117
+ setDocument("CDX status", renderCdxStatus(latestCdxStatusPayload));
3118
+ }
3119
+ }
3120
+
2779
3121
  function renderCdxRuns(payload) {
2780
3122
  if (!payload || payload.state !== "ok") {
2781
3123
  return `
@@ -3106,6 +3448,7 @@
3106
3448
  return;
3107
3449
  }
3108
3450
  latestCdxStatusSignature = nextCdxSignature;
3451
+ latestCdxStatusPayload = data.payload;
3109
3452
  updateMainCdxBadge(data.payload);
3110
3453
  setDocument("CDX status", renderCdxStatus(data.payload));
3111
3454
  setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
@@ -3820,6 +4163,12 @@
3820
4163
  withPrimaryAction("insights", "Loading insights", showCorpusInsights);
3821
4164
  });
3822
4165
  });
4166
+ [workspaceButton()].forEach((button) => {
4167
+ button?.addEventListener("click", () => {
4168
+ setRefreshMenuOpen(false);
4169
+ withPrimaryAction("workspace", "Loading Explorer", showWorkspace);
4170
+ });
4171
+ });
3823
4172
  const autoControl = autoRefreshControl();
3824
4173
  if (autoControl instanceof HTMLInputElement) {
3825
4174
  autoControl.addEventListener("change", () => {
@@ -3914,6 +4263,8 @@
3914
4263
  document.addEventListener("change", (event) => {
3915
4264
  const sessionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-session]") : null;
3916
4265
  const cdxInputTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-input]") : null;
4266
+ const cdxColumnTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-column]") : null;
4267
+ const cdxProviderTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider]") : null;
3917
4268
  if (sessionTarget instanceof HTMLSelectElement) {
3918
4269
  latestCdxMissionState.sessionId = sessionTarget.value || "";
3919
4270
  latestCdxMissionState.planPayload = null;
@@ -3930,6 +4281,25 @@
3930
4281
  latestCdxMissionState.applyPayload = null;
3931
4282
  }
3932
4283
  }
4284
+ if (cdxColumnTarget instanceof HTMLInputElement) {
4285
+ persistCdxColumnVisibility(cdxColumnTarget.getAttribute("data-viewer-cdx-column") || "", cdxColumnTarget.checked);
4286
+ rerenderCdxStatusFromPreferences();
4287
+ }
4288
+ if (cdxProviderTarget instanceof HTMLInputElement) {
4289
+ const provider = cdxProviderTarget.getAttribute("data-viewer-cdx-provider") || "";
4290
+ const status = latestCdxStatusPayload?.status || {};
4291
+ const allProviders = cdxKnownProviders(status, cdxProviders(status), cdxSessions(status));
4292
+ const current = cdxProviderFilterPreference();
4293
+ const selected = new Set(current.mode === "subset" ? current.selected : allProviders);
4294
+ if (cdxProviderTarget.checked) {
4295
+ selected.add(provider);
4296
+ } else {
4297
+ selected.delete(provider);
4298
+ }
4299
+ const nextSelected = Array.from(selected).filter((entry) => allProviders.includes(entry));
4300
+ persistCdxProviderFilter(nextSelected.length === allProviders.length ? { mode: "all", selected: [] } : { mode: "subset", selected: nextSelected });
4301
+ rerenderCdxStatusFromPreferences();
4302
+ }
3933
4303
  });
3934
4304
  document.addEventListener("click", (event) => {
3935
4305
  window.setTimeout(() => applyLocalViewerChrome(), 0);
@@ -3940,12 +4310,15 @@
3940
4310
  const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
3941
4311
  const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
3942
4312
  const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
4313
+ const workspaceTreeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-tree]") : null;
4314
+ const workspacePreviewTarget = event.target instanceof Element ? event.target.closest("[data-viewer-workspace-preview]") : null;
3943
4315
  const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
3944
4316
  const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
3945
4317
  const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
3946
4318
  const cdxBackRunsTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-back-runs]") : null;
3947
4319
  const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
3948
4320
  const cdxArtifactTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-artifact-path]") : null;
4321
+ const cdxProviderAllTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-provider-all]") : null;
3949
4322
  const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
3950
4323
  const cdxMissionTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mission]") : null;
3951
4324
  const cdxStrengthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-strength]") : null;
@@ -3981,6 +4354,11 @@
3981
4354
  withCdxMissionAction("cdx-apply-plan", "Applying CDX mission plan", applyCdxMissionPlan);
3982
4355
  return;
3983
4356
  }
4357
+ if (cdxProviderAllTarget instanceof HTMLElement) {
4358
+ persistCdxProviderFilter({ mode: "all", selected: [] });
4359
+ rerenderCdxStatusFromPreferences();
4360
+ return;
4361
+ }
3984
4362
  if (cdxBackRunsTarget instanceof HTMLElement) {
3985
4363
  withPrimaryAction("cdx-runs", "Loading CDX runs", showCdxRuns);
3986
4364
  return;
@@ -4008,6 +4386,16 @@
4008
4386
  }
4009
4387
  return;
4010
4388
  }
4389
+ if (workspaceTreeTarget instanceof HTMLElement) {
4390
+ event.preventDefault();
4391
+ withPrimaryAction("workspace-tree", "Loading Explorer folder", () => openWorkspaceTree(workspaceTreeTarget.getAttribute("data-viewer-workspace-tree") || ""));
4392
+ return;
4393
+ }
4394
+ if (workspacePreviewTarget instanceof HTMLElement) {
4395
+ event.preventDefault();
4396
+ withPrimaryAction("workspace-preview", "Loading Explorer preview", () => openWorkspacePreview(workspacePreviewTarget.getAttribute("data-viewer-workspace-preview") || ""));
4397
+ return;
4398
+ }
4011
4399
  if (projectSwitcherTarget instanceof HTMLElement) {
4012
4400
  const menu = projectMenu();
4013
4401
  setProjectMenuOpen(Boolean(menu?.hidden));
@@ -37,6 +37,7 @@
37
37
  <div class="viewer-topbar__meta" id="viewer-meta">Read-only local viewer</div>
38
38
  </div>
39
39
  <div class="viewer-topbar__actions">
40
+ <button class="btn" id="viewer-workspace" type="button" title="Show file explorer" hidden>Explorer</button>
40
41
  <button class="btn" id="viewer-git" type="button" title="Show Git status">Git</button>
41
42
  <button class="btn" id="viewer-ci" type="button" title="Show GitHub Actions CI status" hidden>CI</button>
42
43
  <button class="btn" id="viewer-cdx" type="button" title="Show CDX status">CDX</button>