@grifhinz/logics-manager 2.4.0 → 2.5.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.
@@ -11,9 +11,14 @@
11
11
  const filterCount = () => document.getElementById("viewer-filter-count");
12
12
  const repoPill = () => document.getElementById("viewer-repo-pill");
13
13
  const autoRefreshControl = () => document.getElementById("viewer-auto-refresh");
14
+ const refreshIntervalControl = () => document.getElementById("viewer-refresh-interval");
15
+ const refreshMenuButton = () => document.getElementById("viewer-refresh-menu-button");
16
+ const refreshMenuPanel = () => document.getElementById("viewer-refresh-menu");
14
17
  const activityClearControl = () => document.getElementById("activity-clear");
15
18
  const activityStorageLimit = 80;
16
- const defaultAutoRefreshIntervalMs = 60 * 1000;
19
+ const minAutoRefreshIntervalSeconds = 5;
20
+ const maxAutoRefreshIntervalSeconds = 60;
21
+ const defaultAutoRefreshIntervalMs = 15 * 1000;
17
22
  const defaultFilterState = {
18
23
  focus: "active",
19
24
  type: "all",
@@ -29,12 +34,14 @@
29
34
  let nextAutoRefreshAt = 0;
30
35
  let autoRefreshEnabled = true;
31
36
  let autoRefreshTimeoutId = 0;
37
+ let autoRefreshIntervalTouched = false;
32
38
  let applyingLocalChrome = false;
33
39
  let autoRefreshStarted = false;
34
40
  let itemsLoadInFlight = false;
35
41
  let refreshAfterVisible = false;
36
42
  let mermaidInitialized = false;
37
43
  let focusApplied = false;
44
+ let latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
38
45
 
39
46
  function readStoredState() {
40
47
  try {
@@ -162,6 +169,38 @@
162
169
  }
163
170
  }
164
171
 
172
+ function normalizeAutoRefreshIntervalSeconds(value) {
173
+ const seconds = Math.round(Number(value));
174
+ if (!Number.isFinite(seconds) || seconds <= 0) {
175
+ return defaultAutoRefreshIntervalMs / 1000;
176
+ }
177
+ return Math.min(maxAutoRefreshIntervalSeconds, Math.max(minAutoRefreshIntervalSeconds, seconds));
178
+ }
179
+
180
+ function updateRefreshIntervalControl() {
181
+ const control = refreshIntervalControl();
182
+ if (!(control instanceof HTMLSelectElement)) {
183
+ return;
184
+ }
185
+ const seconds = String(Math.round(autoRefreshIntervalMs / 1000));
186
+ if (![...control.options].some((option) => option.value === seconds)) {
187
+ const option = document.createElement("option");
188
+ option.value = seconds;
189
+ option.textContent = `${seconds} sec`;
190
+ control.appendChild(option);
191
+ }
192
+ control.value = seconds;
193
+ }
194
+
195
+ function setAutoRefreshIntervalSeconds(value, options = {}) {
196
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(value) * 1000;
197
+ if (options.user) {
198
+ autoRefreshIntervalTouched = true;
199
+ }
200
+ updateRefreshIntervalControl();
201
+ scheduleNextAutoRefresh();
202
+ }
203
+
165
204
  function scheduleNextAutoRefresh() {
166
205
  if (autoRefreshTimeoutId) {
167
206
  window.clearTimeout(autoRefreshTimeoutId);
@@ -185,6 +224,71 @@
185
224
  pill.title = latestRepoRoot || repoName;
186
225
  }
187
226
 
227
+ function normalizeGitBadgeCounts(payload) {
228
+ const counts = payload && typeof payload === "object" ? payload.badgeCounts || {} : {};
229
+ return {
230
+ unpushedCommits: Math.max(0, Number(counts.unpushedCommits || payload?.ahead || 0)),
231
+ uncommittedFiles: Math.max(0, Number(counts.uncommittedFiles || 0))
232
+ };
233
+ }
234
+
235
+ function renderGitBadge(kind, count) {
236
+ const value = Number(count || 0);
237
+ if (value <= 0) {
238
+ return "";
239
+ }
240
+ const label = kind === "commits"
241
+ ? `${value} commits locaux non pushés`
242
+ : `${value} fichiers modifiés non commités`;
243
+ return `<span class="viewer-git-badge viewer-git-badge--${kind}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${escapeHtml(value)}</span>`;
244
+ }
245
+
246
+ function gitBadgeHtml(scope) {
247
+ const commitsVisible = latestGitBadgeCounts.unpushedCommits > 0 && (
248
+ scope === "main" || scope === "history"
249
+ );
250
+ const filesVisible = latestGitBadgeCounts.uncommittedFiles > 0 && (
251
+ scope === "main" || scope === "changes"
252
+ );
253
+ const html = [
254
+ commitsVisible ? renderGitBadge("commits", latestGitBadgeCounts.unpushedCommits) : "",
255
+ filesVisible ? renderGitBadge("files", latestGitBadgeCounts.uncommittedFiles) : ""
256
+ ].filter(Boolean).join("");
257
+ return html ? `<span class="viewer-git-badges" data-viewer-git-badges="${escapeHtml(scope)}">${html}</span>` : "";
258
+ }
259
+
260
+ function updateMainGitBadges() {
261
+ const button = document.getElementById("viewer-git");
262
+ if (!(button instanceof HTMLElement)) {
263
+ return;
264
+ }
265
+ button.querySelector('[data-viewer-git-badges="main"]')?.remove();
266
+ const html = gitBadgeHtml("main");
267
+ if (html) {
268
+ button.insertAdjacentHTML("beforeend", html);
269
+ }
270
+ }
271
+
272
+ function setGitBadgeCountsFromPayload(payload, options = {}) {
273
+ latestGitBadgeCounts = normalizeGitBadgeCounts(payload);
274
+ if (options.updateMain !== false) {
275
+ updateMainGitBadges();
276
+ }
277
+ }
278
+
279
+ async function refreshGitBadgeCounters() {
280
+ try {
281
+ const response = await fetch("/api/git-status");
282
+ const data = await response.json();
283
+ if (response.ok && data.ok && data.payload?.state === "ok") {
284
+ setGitBadgeCountsFromPayload(data.payload);
285
+ }
286
+ } catch {
287
+ latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
288
+ updateMainGitBadges();
289
+ }
290
+ }
291
+
188
292
  function findItemByPath(relPath) {
189
293
  const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\//, "");
190
294
  return latestItems.find((entry) => entry.relPath === normalized || entry.path === normalized) || null;
@@ -423,10 +527,10 @@
423
527
 
424
528
  function postToApp(payload, options = {}) {
425
529
  latestItems = updateStoredActivity(Array.isArray(payload.items) ? payload.items : []);
426
- const intervalSeconds = Number(payload.autoRefreshIntervalSeconds);
427
- autoRefreshIntervalMs = Number.isFinite(intervalSeconds) && intervalSeconds > 0
428
- ? intervalSeconds * 1000
429
- : defaultAutoRefreshIntervalMs;
530
+ if (!autoRefreshIntervalTouched) {
531
+ autoRefreshIntervalMs = normalizeAutoRefreshIntervalSeconds(payload.autoRefreshIntervalSeconds) * 1000;
532
+ updateRefreshIntervalControl();
533
+ }
430
534
  updateRepositoryIdentity(payload);
431
535
  const payloadWithActivity = { ...payload, items: latestItems };
432
536
  const nextPayload = options.silent ? payloadWithActivity : applyFocusRequest(payloadWithActivity);
@@ -439,6 +543,7 @@
439
543
  renderUpdateNotice(payload.updateInfo);
440
544
  updateFilterSummary();
441
545
  applyLocalViewerChrome();
546
+ bindRefreshMenuControls();
442
547
  }
443
548
 
444
549
  function renderUpdateNotice(updateInfo) {
@@ -476,12 +581,38 @@
476
581
  throw new Error(data.error || "Unable to load viewer data.");
477
582
  }
478
583
  postToApp(data.payload, { silent: Boolean(options.silent) });
584
+ if (method !== "POST") {
585
+ await refreshGitBadgeCounters();
586
+ }
479
587
  return true;
480
588
  } finally {
481
589
  itemsLoadInFlight = false;
482
590
  }
483
591
  }
484
592
 
593
+ function isGitStatusOpen() {
594
+ const panel = documentPanel();
595
+ const title = documentTitle();
596
+ return Boolean(panel && !panel.hidden && title && title.textContent === "Git status");
597
+ }
598
+
599
+ function isCdxStatusOpen() {
600
+ const panel = documentPanel();
601
+ const title = documentTitle();
602
+ return Boolean(panel && !panel.hidden && title && title.textContent === "CDX status");
603
+ }
604
+
605
+ async function refreshViewer(method = "POST", options = {}) {
606
+ await loadItems(method, options);
607
+ if (isGitStatusOpen()) {
608
+ await showGitStatus({ preserve: true, silent: Boolean(options.silent) });
609
+ } else if (isCdxStatusOpen()) {
610
+ await showCdxStatus({ silent: Boolean(options.silent) });
611
+ } else if (method === "POST") {
612
+ await refreshGitBadgeCounters();
613
+ }
614
+ }
615
+
485
616
  function autoRefreshItems() {
486
617
  if (!autoRefreshEnabled) {
487
618
  return;
@@ -490,7 +621,7 @@
490
621
  refreshAfterVisible = true;
491
622
  return;
492
623
  }
493
- loadItems("POST", { silent: true }).catch((error) => setMeta(error.message));
624
+ refreshViewer("POST", { silent: true }).catch((error) => setMeta(error.message));
494
625
  }
495
626
 
496
627
  function startAutoRefresh() {
@@ -518,13 +649,48 @@
518
649
  scheduleNextAutoRefresh();
519
650
  }
520
651
 
652
+ function setRefreshMenuOpen(open) {
653
+ const panel = refreshMenuPanel();
654
+ const button = refreshMenuButton();
655
+ if (!panel) {
656
+ return;
657
+ }
658
+ panel.hidden = !open;
659
+ if (button instanceof HTMLElement) {
660
+ button.setAttribute("aria-expanded", open ? "true" : "false");
661
+ }
662
+ }
663
+
664
+ function bindRefreshMenuControls() {
665
+ const button = refreshMenuButton();
666
+ if (button) {
667
+ button.onclick = (event) => {
668
+ event.stopPropagation();
669
+ const panel = refreshMenuPanel();
670
+ setRefreshMenuOpen(Boolean(panel?.hidden));
671
+ };
672
+ }
673
+ const panel = refreshMenuPanel();
674
+ if (panel) {
675
+ panel.onclick = (event) => {
676
+ event.stopPropagation();
677
+ };
678
+ }
679
+ }
680
+
521
681
  function statusValue(item) {
522
682
  return String(item?.indicators?.Status || "").toLowerCase();
523
683
  }
524
684
 
525
685
  function isClosed(item) {
526
686
  const status = statusValue(item);
527
- return status.includes("done") || status.includes("archived") || status.includes("obsolete");
687
+ return (
688
+ status.includes("done") ||
689
+ status.includes("archived") ||
690
+ status.includes("obsolete") ||
691
+ status.includes("superseded") ||
692
+ status.includes("settled")
693
+ );
528
694
  }
529
695
 
530
696
  function hasLinks(item) {
@@ -555,7 +721,22 @@
555
721
  return true;
556
722
  }
557
723
  const normalized = rawStatus.toLowerCase();
558
- return !["draft", "ready", "in progress", "blocked", "done", "archived", "obsolete"].includes(normalized);
724
+ return ![
725
+ "draft",
726
+ "ready",
727
+ "in progress",
728
+ "blocked",
729
+ "done",
730
+ "active",
731
+ "proposed",
732
+ "accepted",
733
+ "validated",
734
+ "rejected",
735
+ "superseded",
736
+ "settled",
737
+ "archived",
738
+ "obsolete"
739
+ ].includes(normalized);
559
740
  }
560
741
 
561
742
  function isSafeLogicsDocPath(value) {
@@ -1060,6 +1241,471 @@
1060
1241
  setMeta("Health loaded.");
1061
1242
  }
1062
1243
 
1244
+ function objectEntries(value) {
1245
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
1246
+ }
1247
+
1248
+ function asArray(value) {
1249
+ if (Array.isArray(value)) {
1250
+ return value;
1251
+ }
1252
+ if (value && typeof value === "object") {
1253
+ return Object.entries(value).map(([key, entry]) => ({ name: key, ...(entry && typeof entry === "object" ? entry : { value: entry }) }));
1254
+ }
1255
+ return [];
1256
+ }
1257
+
1258
+ function pickFirstObject(status, keys) {
1259
+ for (const key of keys) {
1260
+ if (status?.[key] && typeof status[key] === "object" && !Array.isArray(status[key])) {
1261
+ return status[key];
1262
+ }
1263
+ }
1264
+ return {};
1265
+ }
1266
+
1267
+ function pickFirstArray(status, keys) {
1268
+ for (const key of keys) {
1269
+ const entries = asArray(status?.[key]);
1270
+ if (entries.length) {
1271
+ return entries;
1272
+ }
1273
+ }
1274
+ return [];
1275
+ }
1276
+
1277
+ function cdxRows(status) {
1278
+ return asArray(status?.rows);
1279
+ }
1280
+
1281
+ function cdxProviders(status) {
1282
+ const explicitProviders = pickFirstArray(status, ["providers", "providerStatus", "provider_status"]);
1283
+ if (explicitProviders.length) {
1284
+ return explicitProviders;
1285
+ }
1286
+ const grouped = new Map();
1287
+ cdxRows(status).forEach((row) => {
1288
+ const provider = String(row.provider || "unknown");
1289
+ const current = grouped.get(provider) || { name: provider, enabled: 0, active: 0, authenticated: 0, sessions: 0, lowest_available_pct: null };
1290
+ current.sessions += 1;
1291
+ if (row.enabled) {
1292
+ current.enabled += 1;
1293
+ }
1294
+ if (row.active) {
1295
+ current.active += 1;
1296
+ }
1297
+ if (String(row.auth_status || "").toLowerCase() === "authenticated") {
1298
+ current.authenticated += 1;
1299
+ }
1300
+ if (typeof row.available_pct === "number") {
1301
+ current.lowest_available_pct = current.lowest_available_pct === null
1302
+ ? row.available_pct
1303
+ : Math.min(current.lowest_available_pct, row.available_pct);
1304
+ }
1305
+ current.state = current.active > 0 ? "active" : current.enabled > 0 ? "enabled" : "disabled";
1306
+ grouped.set(provider, current);
1307
+ });
1308
+ return Array.from(grouped.values());
1309
+ }
1310
+
1311
+ function cdxSessions(status) {
1312
+ const explicitSessions = pickFirstArray(status, ["sessions", "activeSessions", "active_sessions"]);
1313
+ return sortCdxSessionsByRemaining(explicitSessions.length ? explicitSessions : cdxRows(status));
1314
+ }
1315
+
1316
+ function cdxReadiness(status) {
1317
+ const explicitReadiness = pickFirstObject(status, ["readiness", "quota", "quotas", "limits"]);
1318
+ if (objectEntries(explicitReadiness).length) {
1319
+ return explicitReadiness;
1320
+ }
1321
+ const rows = cdxRows(status);
1322
+ if (!rows.length) {
1323
+ return {};
1324
+ }
1325
+ const enabled = rows.filter((row) => row.enabled).length;
1326
+ const active = rows.filter((row) => row.active).length;
1327
+ const authenticated = rows.filter((row) => String(row.auth_status || "").toLowerCase() === "authenticated").length;
1328
+ const availableValues = rows.map((row) => row.available_pct).filter((value) => typeof value === "number");
1329
+ const lowestAvailable = availableValues.length ? Math.min(...availableValues) : null;
1330
+ return {
1331
+ enabled_sessions: enabled,
1332
+ active_sessions: active,
1333
+ authenticated_sessions: authenticated,
1334
+ lowest_remaining: lowestAvailable === null ? "not reported" : `${lowestAvailable}%`
1335
+ };
1336
+ }
1337
+
1338
+ function renderCdxObjectRows(value, emptyText) {
1339
+ const rows = objectEntries(value).slice(0, 12).map(([key, entry]) => `
1340
+ <li class="viewer-cdx__row">
1341
+ <span>${escapeHtml(cdxLabel(key))}</span>
1342
+ <strong>${escapeHtml(typeof entry === "object" ? JSON.stringify(entry) : entry)}</strong>
1343
+ </li>
1344
+ `).join("");
1345
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1346
+ }
1347
+
1348
+ function cdxLabel(value) {
1349
+ return String(value || "")
1350
+ .replace(/[_-]+/g, " ")
1351
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
1352
+ }
1353
+
1354
+ function cdxStateClass(value) {
1355
+ const state = String(value || "").toLowerCase();
1356
+ if (["ready", "ok", "active", "enabled", "authenticated"].some((entry) => state.includes(entry))) {
1357
+ return "ok";
1358
+ }
1359
+ if (["starting", "pending", "warning", "low", "limited"].some((entry) => state.includes(entry))) {
1360
+ return "warn";
1361
+ }
1362
+ if (["error", "failed", "disabled", "unavailable", "unauthenticated"].some((entry) => state.includes(entry))) {
1363
+ return "bad";
1364
+ }
1365
+ return "neutral";
1366
+ }
1367
+
1368
+ function cdxRemainingPct(item) {
1369
+ const value = item?.remaining_pct ?? item?.remainingPct ?? item?.available_pct ?? item?.availablePct ?? item?.lowest_available_pct ?? item?.lowestAvailablePct;
1370
+ const percent = Number(value);
1371
+ return Number.isFinite(percent) ? Math.max(0, Math.min(100, Math.round(percent))) : null;
1372
+ }
1373
+
1374
+ function cdxPct(value) {
1375
+ const percent = Number(value);
1376
+ return Number.isFinite(percent) ? `${Math.max(0, Math.min(100, Math.round(percent)))}%` : "-";
1377
+ }
1378
+
1379
+ function cdxField(item, keys, fallback = "-") {
1380
+ for (const key of keys) {
1381
+ const value = item?.[key];
1382
+ if (value !== undefined && value !== null && value !== "") {
1383
+ return value;
1384
+ }
1385
+ }
1386
+ return fallback;
1387
+ }
1388
+
1389
+ function cdxRemainingClass(percent) {
1390
+ if (percent === null) {
1391
+ return "neutral";
1392
+ }
1393
+ if (percent <= 10) {
1394
+ return "bad";
1395
+ }
1396
+ if (percent <= 30) {
1397
+ return "warn";
1398
+ }
1399
+ return "ok";
1400
+ }
1401
+
1402
+ function sortCdxSessionsByRemaining(entries) {
1403
+ return [...entries].sort((left, right) => {
1404
+ const leftRemaining = cdxRemainingPct(left);
1405
+ const rightRemaining = cdxRemainingPct(right);
1406
+ if (leftRemaining === null && rightRemaining === null) {
1407
+ return 0;
1408
+ }
1409
+ if (leftRemaining === null) {
1410
+ return 1;
1411
+ }
1412
+ if (rightRemaining === null) {
1413
+ return -1;
1414
+ }
1415
+ return rightRemaining - leftRemaining;
1416
+ });
1417
+ }
1418
+
1419
+ function formatCdxValue(key, value) {
1420
+ if (["reset_at", "resetAt", "resets_at", "resetsAt", "reset_5h_at", "reset5hAt", "reset_week_at", "resetWeekAt", "updated_at", "updatedAt"].includes(key)) {
1421
+ return formatCdxResetAt(value);
1422
+ }
1423
+ if (typeof value === "object") {
1424
+ return JSON.stringify(value);
1425
+ }
1426
+ return value;
1427
+ }
1428
+
1429
+ function parseCdxDate(value) {
1430
+ const raw = String(value || "").trim();
1431
+ if (!raw) {
1432
+ return null;
1433
+ }
1434
+ const shortDate = raw.match(/^([A-Za-z]{3,})\s+(\d{1,2})\s+(\d{1,2}:\d{2})$/);
1435
+ if (shortDate) {
1436
+ const year = new Date().getFullYear();
1437
+ const timestamp = Date.parse(`${shortDate[1]} ${shortDate[2]} ${year} ${shortDate[3]}`);
1438
+ return Number.isFinite(timestamp) ? timestamp : null;
1439
+ }
1440
+ const timestamp = Date.parse(raw);
1441
+ if (Number.isFinite(timestamp)) {
1442
+ return timestamp;
1443
+ }
1444
+ return null;
1445
+ }
1446
+
1447
+ function formatRelativeTime(timestamp) {
1448
+ const diffMs = timestamp - Date.now();
1449
+ const absMs = Math.abs(diffMs);
1450
+ const minutes = Math.round(absMs / 60000);
1451
+ if (minutes < 1) {
1452
+ return diffMs >= 0 ? "now" : "just now";
1453
+ }
1454
+ const hours = Math.floor(minutes / 60);
1455
+ const days = Math.floor(hours / 24);
1456
+ const remainingHours = hours % 24;
1457
+ const remainingMinutes = minutes % 60;
1458
+ let body = "";
1459
+ if (days > 0) {
1460
+ body = `${days}d${remainingHours > 0 ? ` ${remainingHours}h` : ""}`;
1461
+ } else if (hours > 0) {
1462
+ body = `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ""}`;
1463
+ } else {
1464
+ body = `${minutes}m`;
1465
+ }
1466
+ return diffMs >= 0 ? `in ${body}` : `${body} ago`;
1467
+ }
1468
+
1469
+ function formatCdxResetAt(value) {
1470
+ const raw = String(value || "").trim();
1471
+ if (!raw) {
1472
+ return "-";
1473
+ }
1474
+ const timestamp = parseCdxDate(raw);
1475
+ return timestamp === null ? raw : formatRelativeTime(timestamp);
1476
+ }
1477
+
1478
+ function formatCdxCredits(value) {
1479
+ const text = String(value ?? "").trim();
1480
+ if (!text || text === "-") {
1481
+ return "-";
1482
+ }
1483
+ const number = Number(text);
1484
+ return Number.isFinite(number) ? number.toFixed(2) : text;
1485
+ }
1486
+
1487
+ function renderCdxBadge(value, fallback = "reported") {
1488
+ const label = String(value || fallback || "reported");
1489
+ return `<span class="viewer-cdx__badge viewer-cdx__badge--${cdxStateClass(label)}">${escapeHtml(cdxLabel(label))}</span>`;
1490
+ }
1491
+
1492
+ function cdxDetailEntries(item, excludedKeys) {
1493
+ return objectEntries(item)
1494
+ .filter(([key, value]) => !excludedKeys.includes(key) && value !== undefined && value !== null && value !== "")
1495
+ .slice(0, 6);
1496
+ }
1497
+
1498
+ function renderCdxDetailPills(item, excludedKeys) {
1499
+ const details = cdxDetailEntries(item, excludedKeys).map(([key, value]) => `
1500
+ <span class="viewer-cdx__pill"><span>${escapeHtml(cdxLabel(key))}</span><strong>${escapeHtml(formatCdxValue(key, value))}</strong></span>
1501
+ `).join("");
1502
+ return details ? `<div class="viewer-cdx__pills">${details}</div>` : "";
1503
+ }
1504
+
1505
+ function renderCdxRemainingPill(item) {
1506
+ const percent = cdxRemainingPct(item);
1507
+ if (percent === null) {
1508
+ return "";
1509
+ }
1510
+ return `
1511
+ <span class="viewer-cdx__remaining viewer-cdx__remaining--${cdxRemainingClass(percent)}" title="${escapeHtml(percent)}% usage remaining">
1512
+ <span>Remaining</span>
1513
+ <strong>${escapeHtml(percent)}%</strong>
1514
+ </span>
1515
+ `;
1516
+ }
1517
+
1518
+ function cdxSessionBlock(item) {
1519
+ const explicit = cdxField(item, ["block", "blocked", "blocking"], "");
1520
+ if (explicit && explicit !== true) {
1521
+ return explicit;
1522
+ }
1523
+ const fiveHour = Number(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN));
1524
+ const week = Number(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN));
1525
+ if (Number.isFinite(fiveHour) && fiveHour <= 0) {
1526
+ return "5H";
1527
+ }
1528
+ if (Number.isFinite(week) && week <= 1) {
1529
+ return "WEEK";
1530
+ }
1531
+ return explicit === true ? "YES" : "-";
1532
+ }
1533
+
1534
+ function renderCdxSessionTable(sessions, emptyText) {
1535
+ if (!sessions.length) {
1536
+ return `<div class="viewer-cdx__empty">${escapeHtml(emptyText)}</div>`;
1537
+ }
1538
+ const rows = sessions.slice(0, 24).map((entry) => {
1539
+ const item = entry && typeof entry === "object" ? entry : { value: entry };
1540
+ const name = cdxField(item, ["session_name", "name", "id", "value"]);
1541
+ const sessionName = `${name}${item.active ? "*" : ""}`;
1542
+ const status = cdxField(item, ["status", "state"]);
1543
+ const auth = String(cdxField(item, ["auth_status", "authStatus"], "-")).replace("authenticated", "logged");
1544
+ const block = cdxSessionBlock(item);
1545
+ return `
1546
+ <tr>
1547
+ <td class="viewer-cdx__session-name">${escapeHtml(sessionName)}</td>
1548
+ <td>${escapeHtml(cdxField(item, ["provider"], "-"))}</td>
1549
+ <td>${renderCdxBadge(status)}</td>
1550
+ <td>${escapeHtml(auth)}</td>
1551
+ <td>${renderCdxRemainingPill(item) || escapeHtml(cdxPct(cdxField(item, ["available_pct", "availablePct"], NaN)))}</td>
1552
+ <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_5h_pct", "remaining5hPct"], NaN)))}</td>
1553
+ <td>${escapeHtml(cdxPct(cdxField(item, ["remaining_week_pct", "remainingWeekPct"], NaN)))}</td>
1554
+ <td>${escapeHtml(block)}</td>
1555
+ <td>${escapeHtml(formatCdxCredits(cdxField(item, ["credits", "cr"], "-")))}</td>
1556
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_5h_at", "reset5hAt", "reset_at", "resetAt"], "")))}</td>
1557
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["reset_week_at", "resetWeekAt", "reset_at", "resetAt"], "")))}</td>
1558
+ <td>${escapeHtml(formatCdxResetAt(cdxField(item, ["updated_at", "updatedAt"], "")))}</td>
1559
+ </tr>
1560
+ `;
1561
+ }).join("");
1562
+ return `
1563
+ <div class="viewer-cdx__table-wrap">
1564
+ <table class="viewer-cdx__table">
1565
+ <thead>
1566
+ <tr>
1567
+ <th>SESSION</th>
1568
+ <th>PROV.</th>
1569
+ <th>STATUS</th>
1570
+ <th>AUTH</th>
1571
+ <th>OK</th>
1572
+ <th>5H</th>
1573
+ <th>WEEK</th>
1574
+ <th>BLOCK</th>
1575
+ <th>CR</th>
1576
+ <th>RESET 5H</th>
1577
+ <th>RESET WEEK</th>
1578
+ <th>UPDATED</th>
1579
+ </tr>
1580
+ </thead>
1581
+ <tbody>${rows}</tbody>
1582
+ </table>
1583
+ </div>
1584
+ `;
1585
+ }
1586
+
1587
+ function renderCdxEntityRows(entries, emptyText, options = {}) {
1588
+ const titleKeys = options.titleKeys || ["name", "session_name", "id", "provider", "model", "value"];
1589
+ const stateKeys = options.stateKeys || ["state", "status", "readiness", "available", "auth_status"];
1590
+ const excludedKeys = [...titleKeys, ...stateKeys, "available_pct", "availablePct", "remaining_pct", "remainingPct", "lowest_available_pct", "lowestAvailablePct"];
1591
+ const rows = entries.slice(0, 16).map((entry) => {
1592
+ const item = entry && typeof entry === "object" ? entry : { value: entry };
1593
+ const name = titleKeys.map((key) => item[key]).find(Boolean) || "entry";
1594
+ const state = stateKeys.map((key) => item[key]).find((value) => value !== undefined && value !== null && value !== "") || "";
1595
+ const subtitle = options.subtitleKeys
1596
+ ? options.subtitleKeys.map((key) => item[key]).filter(Boolean).join(" · ")
1597
+ : "";
1598
+ return `
1599
+ <li class="viewer-cdx__entity">
1600
+ <div class="viewer-cdx__entity-main">
1601
+ <div>
1602
+ <strong>${escapeHtml(name)}</strong>
1603
+ ${subtitle ? `<div class="viewer-cdx__meta">${escapeHtml(subtitle)}</div>` : ""}
1604
+ </div>
1605
+ <div class="viewer-cdx__entity-status">
1606
+ ${renderCdxRemainingPill(item)}
1607
+ ${renderCdxBadge(state)}
1608
+ </div>
1609
+ </div>
1610
+ ${renderCdxDetailPills(item, excludedKeys)}
1611
+ </li>
1612
+ `;
1613
+ }).join("");
1614
+ return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
1615
+ }
1616
+
1617
+ function renderCdxStatus(payload) {
1618
+ if (!payload || payload.state !== "ok") {
1619
+ return `
1620
+ <div class="viewer-cdx">
1621
+ <div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
1622
+ </div>
1623
+ `;
1624
+ }
1625
+ const status = payload.status || {};
1626
+ const providers = cdxProviders(status);
1627
+ const sessions = cdxSessions(status);
1628
+ const readiness = cdxReadiness(status);
1629
+ const commands = pickFirstArray(status, ["nextCommands", "next_commands", "safeCommands", "safe_commands", "commands"])
1630
+ .map((entry) => typeof entry === "string" ? entry : (entry.command || entry.value || entry.name || ""))
1631
+ .filter(Boolean);
1632
+ if (!commands.length) {
1633
+ commands.push("cdx status --json");
1634
+ }
1635
+ const runtimeState = status.state || status.status || status.availability || "ok";
1636
+ const readinessCount = objectEntries(readiness).length;
1637
+ const cards = [
1638
+ ["Runtime", runtimeState],
1639
+ ["Providers", providers.length],
1640
+ ["Sessions", sessions.length],
1641
+ ["Readiness", readinessCount ? `${readinessCount} signals` : "Not reported"]
1642
+ ].map(([label, value]) => `
1643
+ <div class="viewer-cdx__card">
1644
+ <div class="viewer-cdx__label">${escapeHtml(label)}</div>
1645
+ <div class="viewer-cdx__value">${label === "Runtime" ? renderCdxBadge(value) : escapeHtml(value)}</div>
1646
+ </div>
1647
+ `).join("");
1648
+ const commandRows = commands.slice(0, 10).map((command, index) => `
1649
+ <li>
1650
+ <span>${escapeHtml(index + 1)}</span>
1651
+ <code>${escapeHtml(command)}</code>
1652
+ </li>
1653
+ `).join("");
1654
+ return `
1655
+ <div class="viewer-cdx">
1656
+ <div class="viewer-cdx__summary">${cards}</div>
1657
+ <div class="viewer-cdx__workspace">
1658
+ <div class="viewer-cdx__stack">
1659
+ <section class="viewer-cdx__section">
1660
+ <h2 class="viewer-cdx__heading">Sessions</h2>
1661
+ ${renderCdxSessionTable(sessions, "No sessions reported.")}
1662
+ </section>
1663
+ <section class="viewer-cdx__section">
1664
+ <h2 class="viewer-cdx__heading">Providers</h2>
1665
+ <ul class="viewer-cdx__list">${renderCdxEntityRows(providers, "No provider status reported.", { subtitleKeys: ["model"] })}</ul>
1666
+ </section>
1667
+ </div>
1668
+ <div class="viewer-cdx__stack">
1669
+ <section class="viewer-cdx__section">
1670
+ <h2 class="viewer-cdx__heading">Readiness and quota</h2>
1671
+ <ul class="viewer-cdx__list">${renderCdxObjectRows(readiness, "No readiness or quota details reported.")}</ul>
1672
+ </section>
1673
+ <section class="viewer-cdx__section">
1674
+ <h2 class="viewer-cdx__heading">Safe next commands</h2>
1675
+ <ul class="viewer-cdx__commands">${commandRows || '<li class="viewer-cdx__empty">No suggested commands reported.</li>'}</ul>
1676
+ </section>
1677
+ </div>
1678
+ </div>
1679
+ </div>
1680
+ `;
1681
+ }
1682
+
1683
+ async function showCdxStatus(options = {}) {
1684
+ if (!options.silent) {
1685
+ setMeta("Checking CDX status...");
1686
+ }
1687
+ const response = await fetch("/api/cdx-status");
1688
+ let data = {};
1689
+ try {
1690
+ data = await response.json();
1691
+ } catch {
1692
+ data = {};
1693
+ }
1694
+ if (response.status === 404) {
1695
+ setDocument("CDX status", renderCdxStatus({
1696
+ state: "unavailable",
1697
+ message: "CDX status endpoint unavailable. Restart the local viewer so it loads the current logics-manager backend."
1698
+ }));
1699
+ setMeta("Restart the local viewer to enable CDX status.");
1700
+ return;
1701
+ }
1702
+ if (!response.ok || !data.ok) {
1703
+ throw new Error(data.error || "Unable to load CDX status.");
1704
+ }
1705
+ setDocument("CDX status", renderCdxStatus(data.payload));
1706
+ setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
1707
+ }
1708
+
1063
1709
  function renderGitStatus(payload) {
1064
1710
  if (!payload || payload.state !== "ok") {
1065
1711
  return `
@@ -1069,19 +1715,43 @@
1069
1715
  `;
1070
1716
  }
1071
1717
  const counts = payload.counts || {};
1718
+ const stagedCount = Number(counts.staged || 0);
1719
+ const modifiedCount = Number(counts.modified || 0);
1720
+ const deletedCount = Number(counts.deleted || 0);
1721
+ const renamedCount = Number(counts.renamed || 0);
1722
+ const untrackedCount = Number(counts.untracked || 0);
1072
1723
  const summary = [
1073
1724
  ["Branch", payload.branch || "HEAD"],
1074
1725
  ["Tracking", payload.tracking || "None"],
1075
1726
  ["Ahead", payload.ahead || 0],
1076
1727
  ["Behind", payload.behind || 0],
1077
1728
  ["State", payload.clean ? "Clean" : "Dirty"],
1078
- ["Staged", counts.staged || 0],
1079
- ["Modified/deleted", Number(counts.modified || 0) + Number(counts.deleted || 0)],
1080
- ["Untracked", counts.untracked || 0]
1729
+ ["Staged", stagedCount],
1730
+ ["Worktree", modifiedCount + deletedCount + renamedCount],
1731
+ ["Untracked", untrackedCount]
1081
1732
  ];
1082
1733
  const cards = renderMetricCards(summary);
1083
- const groupLabels = { staged: "Staged", modified: "Modified", deleted: "Deleted", renamed: "Renamed", untracked: "Untracked" };
1084
- const groups = Object.entries(groupLabels).map(([key, label]) => {
1734
+ const groupDefs = [
1735
+ ["staged", "Staged", "staged"],
1736
+ ["modified", "Modified", "worktree"],
1737
+ ["deleted", "Deleted", "worktree"],
1738
+ ["renamed", "Renamed", "worktree"],
1739
+ ["untracked", "Untracked", "untracked"]
1740
+ ];
1741
+ const domainDefs = [
1742
+ ["changes", "Changes", stagedCount + modifiedCount + deletedCount + renamedCount + untrackedCount],
1743
+ ["staged", "Staged", stagedCount],
1744
+ ["worktree", "Worktree", modifiedCount + deletedCount + renamedCount],
1745
+ ["untracked", "Untracked", untrackedCount],
1746
+ ["history", "History", Array.isArray(payload.recentCommits) ? payload.recentCommits.length : (payload.latestCommit ? 1 : 0)],
1747
+ ["remote", "Remote", payload.tracking ? 1 : 0]
1748
+ ];
1749
+ const domains = domainDefs.map(([key, label, count], index) => `
1750
+ <button class="viewer-git__domain${index === 0 ? " is-active" : ""}" type="button" data-viewer-git-domain="${escapeHtml(key)}" aria-pressed="${index === 0 ? "true" : "false"}">
1751
+ <span class="viewer-git__domain-label">${escapeHtml(label)}${key === "changes" ? gitBadgeHtml("changes") : ""}${key === "history" ? gitBadgeHtml("history") : ""}</span><strong>${escapeHtml(count)}</strong>
1752
+ </button>
1753
+ `).join("");
1754
+ const renderFileSections = (allowedKeys) => groupDefs.filter(([key]) => allowedKeys.includes(key)).map(([key, label]) => {
1085
1755
  const entries = Array.isArray(payload.groups?.[key]) ? payload.groups[key] : [];
1086
1756
  if (!entries.length) {
1087
1757
  return "";
@@ -1090,24 +1760,161 @@
1090
1760
  <section class="viewer-git__section">
1091
1761
  <h2>${escapeHtml(label)}</h2>
1092
1762
  <ul class="viewer-git__files">${entries.map((entry) => `
1093
- <li><code>${escapeHtml(entry.from ? `${entry.from} -> ${entry.path}` : entry.path)}</code></li>
1763
+ <li>
1764
+ <button class="viewer-git__file" type="button" data-viewer-git-file="${escapeHtml(entry.path)}" data-viewer-git-cached="${key === "staged" ? "1" : "0"}">
1765
+ <span class="viewer-git__file-path">${escapeHtml(entry.from ? `${entry.from} -> ${entry.path}` : entry.path)}</span>
1766
+ ${entry.logicsType ? `<span class="viewer-git__file-kind">${escapeHtml(entry.logicsType)}</span>` : ""}
1767
+ </button>
1768
+ </li>
1094
1769
  `).join("")}</ul>
1095
1770
  </section>
1096
1771
  `;
1097
1772
  }).join("");
1773
+ const changesSections = renderFileSections(["staged", "modified", "deleted", "renamed", "untracked"]);
1774
+ const stagedSections = renderFileSections(["staged"]);
1775
+ const worktreeSections = renderFileSections(["modified", "deleted", "renamed"]);
1776
+ const untrackedSections = renderFileSections(["untracked"]);
1098
1777
  const clean = payload.clean ? '<p class="viewer-git__state">Working tree clean.</p>' : "";
1778
+ const recentCommits = Array.isArray(payload.recentCommits) ? payload.recentCommits : [];
1779
+ const historyRows = recentCommits.length
1780
+ ? recentCommits.map((commit) => `
1781
+ <li class="viewer-git__commit-row">
1782
+ <div class="viewer-git__commit-main">
1783
+ <code>${escapeHtml(commit.hash || "")}</code>
1784
+ <strong>${escapeHtml(commit.subject || "Untitled commit")}</strong>
1785
+ </div>
1786
+ <div class="viewer-git__commit-meta">
1787
+ <span>${escapeHtml([commit.author, commit.date].filter(Boolean).join(" · ") || "Unknown")}</span>
1788
+ ${commit.refs ? `<span class="viewer-git__commit-refs">${escapeHtml(commit.refs)}</span>` : ""}
1789
+ </div>
1790
+ </li>
1791
+ `).join("")
1792
+ : `<li class="viewer-git__commit-row">${escapeHtml(payload.latestCommit || "No commit history available.")}</li>`;
1793
+ const history = `
1794
+ <section class="viewer-git__section">
1795
+ <h2>History</h2>
1796
+ <ul class="viewer-git__commits">${historyRows}</ul>
1797
+ </section>
1798
+ `;
1799
+ const remote = `
1800
+ <section class="viewer-git__section">
1801
+ <h2>Remote</h2>
1802
+ <p class="viewer-git__state">${escapeHtml(payload.tracking ? `Tracking ${payload.tracking}` : "No upstream branch detected.")}</p>
1803
+ <p class="viewer-git__state">${escapeHtml(`Ahead ${payload.ahead || 0}, behind ${payload.behind || 0}`)}</p>
1804
+ </section>
1805
+ `;
1099
1806
  return `
1100
1807
  <div class="viewer-git">
1101
1808
  <div class="viewer-git__summary">${cards}</div>
1102
- ${payload.latestCommit ? `<p class="viewer-git__commit">Latest commit: <code>${escapeHtml(payload.latestCommit)}</code></p>` : ""}
1103
- ${clean}
1104
- ${groups}
1809
+ <div class="viewer-git__workspace">
1810
+ <nav class="viewer-git__domains" aria-label="Git domains">${domains}</nav>
1811
+ <div class="viewer-git__content" aria-label="Git domain content">
1812
+ <section class="viewer-git__panel" data-viewer-git-panel="changes">
1813
+ <header class="viewer-git__panel-header"><span>Changes</span><strong>${escapeHtml(stagedCount + modifiedCount + deletedCount + renamedCount + untrackedCount)} files</strong></header>
1814
+ ${clean}
1815
+ ${changesSections || '<p class="viewer-git__state">No file changes detected.</p>'}
1816
+ </section>
1817
+ <section class="viewer-git__panel" data-viewer-git-panel="staged" hidden>
1818
+ <header class="viewer-git__panel-header"><span>Staged</span><strong>${escapeHtml(stagedCount)} files</strong></header>
1819
+ ${stagedSections || '<p class="viewer-git__state">No staged files.</p>'}
1820
+ </section>
1821
+ <section class="viewer-git__panel" data-viewer-git-panel="worktree" hidden>
1822
+ <header class="viewer-git__panel-header"><span>Worktree</span><strong>${escapeHtml(modifiedCount + deletedCount + renamedCount)} files</strong></header>
1823
+ ${worktreeSections || '<p class="viewer-git__state">No modified, deleted, or renamed files.</p>'}
1824
+ </section>
1825
+ <section class="viewer-git__panel" data-viewer-git-panel="untracked" hidden>
1826
+ <header class="viewer-git__panel-header"><span>Untracked</span><strong>${escapeHtml(untrackedCount)} files</strong></header>
1827
+ ${untrackedSections || '<p class="viewer-git__state">No untracked files.</p>'}
1828
+ </section>
1829
+ <section class="viewer-git__panel" data-viewer-git-panel="history" hidden>
1830
+ <header class="viewer-git__panel-header"><span>History</span><strong>${escapeHtml(recentCommits.length || (payload.latestCommit ? 1 : 0))} commits</strong></header>
1831
+ ${history}
1832
+ </section>
1833
+ <section class="viewer-git__panel" data-viewer-git-panel="remote" hidden>
1834
+ <header class="viewer-git__panel-header"><span>Remote</span><strong>${escapeHtml(payload.tracking || "none")}</strong></header>
1835
+ ${remote}
1836
+ </section>
1837
+ </div>
1838
+ <section class="viewer-git__detail" aria-label="Git diff">
1839
+ <div class="viewer-git__detail-title">Diff preview</div>
1840
+ <div class="viewer-git__diff" data-viewer-git-diff>Select a changed file to preview its diff.</div>
1841
+ </section>
1842
+ </div>
1105
1843
  </div>
1106
1844
  `;
1107
1845
  }
1108
1846
 
1109
- async function showGitStatus() {
1110
- setMeta("Checking Git status...");
1847
+ function setActiveGitFile(button) {
1848
+ document.querySelectorAll("[data-viewer-git-file]").forEach((node) => {
1849
+ if (node instanceof HTMLElement) {
1850
+ node.classList.toggle("is-active", node === button);
1851
+ }
1852
+ });
1853
+ }
1854
+
1855
+ async function loadGitDiff(path, cached, button = null) {
1856
+ const diffPanel = document.querySelector("[data-viewer-git-diff]");
1857
+ if (!(diffPanel instanceof HTMLElement) || !path) {
1858
+ return;
1859
+ }
1860
+ if (button instanceof HTMLElement) {
1861
+ setActiveGitFile(button);
1862
+ }
1863
+ diffPanel.textContent = "Loading diff...";
1864
+ const params = new URLSearchParams({ path });
1865
+ if (cached) {
1866
+ params.set("cached", "1");
1867
+ }
1868
+ const response = await fetch(`/api/git-diff?${params.toString()}`);
1869
+ const data = await response.json();
1870
+ const payload = data.payload || {};
1871
+ if (!response.ok || !data.ok || payload.state !== "ok") {
1872
+ diffPanel.textContent = payload.message || data.error || "Unable to load diff.";
1873
+ return;
1874
+ }
1875
+ const content = payload.diff || payload.message || "No diff is available for this file.";
1876
+ diffPanel.innerHTML = `<div class="viewer-git__diff-meta">${escapeHtml(payload.path || path)} · ${escapeHtml(payload.mode || "worktree")}${payload.truncated ? " · truncated" : ""}</div><pre><code>${escapeHtml(content)}</code></pre>`;
1877
+ }
1878
+
1879
+ function applyGitDomain(domain) {
1880
+ const selected = domain || "changes";
1881
+ document.querySelectorAll(".viewer-git__domain[data-viewer-git-domain]").forEach((node) => {
1882
+ if (node instanceof HTMLElement) {
1883
+ const active = node.getAttribute("data-viewer-git-domain") === selected;
1884
+ node.classList.toggle("is-active", active);
1885
+ node.setAttribute("aria-pressed", active ? "true" : "false");
1886
+ }
1887
+ });
1888
+ document.querySelectorAll("[data-viewer-git-panel]").forEach((node) => {
1889
+ if (node instanceof HTMLElement) {
1890
+ node.hidden = node.getAttribute("data-viewer-git-panel") !== selected;
1891
+ }
1892
+ });
1893
+ }
1894
+
1895
+ function currentGitViewState() {
1896
+ const activeDomain = document.querySelector(".viewer-git__domain.is-active[data-viewer-git-domain]");
1897
+ const activeFile = document.querySelector(".viewer-git__file.is-active[data-viewer-git-file]");
1898
+ return {
1899
+ domain: activeDomain instanceof HTMLElement ? activeDomain.getAttribute("data-viewer-git-domain") || "changes" : "changes",
1900
+ path: activeFile instanceof HTMLElement ? activeFile.getAttribute("data-viewer-git-file") || "" : "",
1901
+ cached: activeFile instanceof HTMLElement && activeFile.getAttribute("data-viewer-git-cached") === "1",
1902
+ };
1903
+ }
1904
+
1905
+ function findGitFileButton(path, cached) {
1906
+ return Array.from(document.querySelectorAll("[data-viewer-git-file]")).find((node) => (
1907
+ node instanceof HTMLElement &&
1908
+ node.getAttribute("data-viewer-git-file") === path &&
1909
+ (node.getAttribute("data-viewer-git-cached") === "1") === Boolean(cached)
1910
+ )) || null;
1911
+ }
1912
+
1913
+ async function showGitStatus(options = {}) {
1914
+ const previous = options.preserve ? currentGitViewState() : { domain: "changes", path: "", cached: false };
1915
+ if (!options.silent) {
1916
+ setMeta("Checking Git status...");
1917
+ }
1111
1918
  const response = await fetch("/api/git-status");
1112
1919
  let data = {};
1113
1920
  try {
@@ -1126,8 +1933,16 @@
1126
1933
  if (!response.ok || !data.ok) {
1127
1934
  throw new Error(data.error || "Unable to load Git status.");
1128
1935
  }
1936
+ setGitBadgeCountsFromPayload(data.payload, { updateMain: false });
1937
+ updateMainGitBadges();
1129
1938
  setDocument("Git status", renderGitStatus(data.payload));
1130
- setMeta("Git status loaded.");
1939
+ applyGitDomain(previous.domain || "changes");
1940
+ const restoredFile = previous.path ? findGitFileButton(previous.path, previous.cached) : null;
1941
+ const firstFile = restoredFile || document.querySelector("[data-viewer-git-file]");
1942
+ if (firstFile instanceof HTMLElement) {
1943
+ await loadGitDiff(firstFile.getAttribute("data-viewer-git-file") || "", firstFile.getAttribute("data-viewer-git-cached") === "1", firstFile);
1944
+ }
1945
+ setMeta(options.silent ? "Git status refreshed." : "Git status loaded.");
1131
1946
  }
1132
1947
 
1133
1948
  window.acquireVsCodeApi = function acquireVsCodeApi() {
@@ -1141,7 +1956,7 @@
1141
1956
  return;
1142
1957
  }
1143
1958
  if (message.type === "refresh") {
1144
- loadItems("POST").catch((error) => setMeta(error.message));
1959
+ refreshViewer("POST").catch((error) => setMeta(error.message));
1145
1960
  return;
1146
1961
  }
1147
1962
  if (message.type === "open" || message.type === "read") {
@@ -1184,12 +1999,42 @@
1184
1999
  });
1185
2000
  setAutoRefreshEnabled(autoControl.checked);
1186
2001
  }
2002
+ const intervalControl = refreshIntervalControl();
2003
+ if (intervalControl instanceof HTMLSelectElement) {
2004
+ updateRefreshIntervalControl();
2005
+ intervalControl.addEventListener("change", () => {
2006
+ setAutoRefreshIntervalSeconds(intervalControl.value, { user: true });
2007
+ });
2008
+ }
2009
+ bindRefreshMenuControls();
2010
+ document.addEventListener("click", (event) => {
2011
+ const target = event.target;
2012
+ const button = refreshMenuButton();
2013
+ const panel = refreshMenuPanel();
2014
+ try {
2015
+ if (target && (
2016
+ button?.contains(target) ||
2017
+ panel?.contains(target)
2018
+ )) {
2019
+ return;
2020
+ }
2021
+ } catch {
2022
+ // Ignore non-node event targets and close the menu below.
2023
+ }
2024
+ setRefreshMenuOpen(false);
2025
+ });
2026
+ document.addEventListener("keydown", (event) => {
2027
+ if (event.key === "Escape") {
2028
+ setRefreshMenuOpen(false);
2029
+ }
2030
+ });
1187
2031
  document.querySelectorAll('[data-action="refresh"]').forEach((element) => {
1188
2032
  if (!(element instanceof HTMLElement)) {
1189
2033
  return;
1190
2034
  }
1191
2035
  element.addEventListener("click", () => {
1192
- loadItems("POST").catch((error) => setMeta(error.message));
2036
+ setRefreshMenuOpen(false);
2037
+ refreshViewer("POST").catch((error) => setMeta(error.message));
1193
2038
  });
1194
2039
  });
1195
2040
  document.getElementById("viewer-health")?.addEventListener("click", () => {
@@ -1198,6 +2043,9 @@
1198
2043
  document.getElementById("viewer-git")?.addEventListener("click", () => {
1199
2044
  showGitStatus().catch((error) => setMeta(error.message));
1200
2045
  });
2046
+ document.getElementById("viewer-cdx")?.addEventListener("click", () => {
2047
+ showCdxStatus().catch((error) => setMeta(error.message));
2048
+ });
1201
2049
  activityClearControl()?.addEventListener("click", () => {
1202
2050
  clearActivityHistory();
1203
2051
  });
@@ -1230,6 +2078,8 @@
1230
2078
  const healthTarget = event.target instanceof Element ? event.target.closest("[data-viewer-open-health]") : null;
1231
2079
  const filterTarget = event.target instanceof Element ? event.target.closest("[data-viewer-filter-group][data-viewer-filter-value]") : null;
1232
2080
  const revealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-reveal]") : null;
2081
+ const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
2082
+ const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
1233
2083
  if (revealTarget instanceof HTMLElement) {
1234
2084
  const list = revealTarget.closest("ul");
1235
2085
  list?.querySelectorAll("[data-viewer-hidden-row]").forEach((row) => {
@@ -1241,6 +2091,18 @@
1241
2091
  revealTarget.closest("li")?.remove();
1242
2092
  return;
1243
2093
  }
2094
+ if (gitDomainTarget instanceof HTMLElement) {
2095
+ applyGitDomain(gitDomainTarget.getAttribute("data-viewer-git-domain") || "changes");
2096
+ return;
2097
+ }
2098
+ if (gitFileTarget instanceof HTMLElement) {
2099
+ loadGitDiff(
2100
+ gitFileTarget.getAttribute("data-viewer-git-file") || "",
2101
+ gitFileTarget.getAttribute("data-viewer-git-cached") === "1",
2102
+ gitFileTarget
2103
+ ).catch((error) => setMeta(error.message));
2104
+ return;
2105
+ }
1244
2106
  if (healthTarget instanceof HTMLElement) {
1245
2107
  showHealth().catch((error) => setMeta(error.message));
1246
2108
  return;