instar 1.3.818 → 1.3.820

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.
Files changed (92) hide show
  1. package/dashboard/glance.js +504 -6
  2. package/dashboard/index.html +158 -652
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +498 -3
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +69 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/core/BackupManager.d.ts +10 -0
  10. package/dist/core/BackupManager.d.ts.map +1 -1
  11. package/dist/core/BackupManager.js +16 -0
  12. package/dist/core/BackupManager.js.map +1 -1
  13. package/dist/core/BoundedJsonlAudit.d.ts +37 -0
  14. package/dist/core/BoundedJsonlAudit.d.ts.map +1 -0
  15. package/dist/core/BoundedJsonlAudit.js +87 -0
  16. package/dist/core/BoundedJsonlAudit.js.map +1 -0
  17. package/dist/core/JudgmentProvenanceLog.d.ts +144 -0
  18. package/dist/core/JudgmentProvenanceLog.d.ts.map +1 -0
  19. package/dist/core/JudgmentProvenanceLog.js +301 -0
  20. package/dist/core/JudgmentProvenanceLog.js.map +1 -0
  21. package/dist/core/MachineIdentity.d.ts.map +1 -1
  22. package/dist/core/MachineIdentity.js +2 -0
  23. package/dist/core/MachineIdentity.js.map +1 -1
  24. package/dist/core/OwnerDarkLadder.d.ts +108 -0
  25. package/dist/core/OwnerDarkLadder.d.ts.map +1 -0
  26. package/dist/core/OwnerDarkLadder.js +229 -0
  27. package/dist/core/OwnerDarkLadder.js.map +1 -0
  28. package/dist/core/PostUpdateMigrator.d.ts +10 -0
  29. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  30. package/dist/core/PostUpdateMigrator.js +106 -0
  31. package/dist/core/PostUpdateMigrator.js.map +1 -1
  32. package/dist/core/SpawnAdmission.d.ts +218 -0
  33. package/dist/core/SpawnAdmission.d.ts.map +1 -0
  34. package/dist/core/SpawnAdmission.js +440 -0
  35. package/dist/core/SpawnAdmission.js.map +1 -0
  36. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  37. package/dist/core/devGatedFeatures.js +24 -0
  38. package/dist/core/devGatedFeatures.js.map +1 -1
  39. package/dist/core/machineCoherenceManifest.d.ts.map +1 -1
  40. package/dist/core/machineCoherenceManifest.js +30 -0
  41. package/dist/core/machineCoherenceManifest.js.map +1 -1
  42. package/dist/core/types.d.ts +87 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/monitoring/DuplicateSessionReconciler.d.ts +225 -0
  46. package/dist/monitoring/DuplicateSessionReconciler.d.ts.map +1 -0
  47. package/dist/monitoring/DuplicateSessionReconciler.js +544 -0
  48. package/dist/monitoring/DuplicateSessionReconciler.js.map +1 -0
  49. package/dist/monitoring/FailureAnalyzer.d.ts.map +1 -1
  50. package/dist/monitoring/FailureAnalyzer.js +40 -1
  51. package/dist/monitoring/FailureAnalyzer.js.map +1 -1
  52. package/dist/monitoring/FailureLedger.d.ts +9 -0
  53. package/dist/monitoring/FailureLedger.d.ts.map +1 -1
  54. package/dist/monitoring/FailureLedger.js +21 -4
  55. package/dist/monitoring/FailureLedger.js.map +1 -1
  56. package/dist/scaffold/templates.d.ts.map +1 -1
  57. package/dist/scaffold/templates.js +2 -1
  58. package/dist/scaffold/templates.js.map +1 -1
  59. package/dist/server/AgentServer.d.ts +5 -0
  60. package/dist/server/AgentServer.d.ts.map +1 -1
  61. package/dist/server/AgentServer.js +18 -3
  62. package/dist/server/AgentServer.js.map +1 -1
  63. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  64. package/dist/server/CapabilityIndex.js +9 -0
  65. package/dist/server/CapabilityIndex.js.map +1 -1
  66. package/dist/server/fileRoutes.d.ts +16 -0
  67. package/dist/server/fileRoutes.d.ts.map +1 -1
  68. package/dist/server/fileRoutes.js +46 -0
  69. package/dist/server/fileRoutes.js.map +1 -1
  70. package/dist/server/middleware.d.ts +32 -0
  71. package/dist/server/middleware.d.ts.map +1 -1
  72. package/dist/server/middleware.js +34 -0
  73. package/dist/server/middleware.js.map +1 -1
  74. package/dist/server/routes.d.ts +7 -0
  75. package/dist/server/routes.d.ts.map +1 -1
  76. package/dist/server/routes.js +99 -0
  77. package/dist/server/routes.js.map +1 -1
  78. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  79. package/dist/testing/selfActionRegistry.js +89 -0
  80. package/dist/testing/selfActionRegistry.js.map +1 -1
  81. package/package.json +1 -1
  82. package/scripts/lib/self-action-detect.mjs +2 -0
  83. package/skills/instar-dev/templates/side-effects-artifact.md +8 -0
  84. package/skills/spec-converge/SKILL.md +1 -0
  85. package/skills/spec-converge/scripts/write-convergence-tag.mjs +71 -0
  86. package/src/data/builtin-manifest.json +65 -65
  87. package/src/scaffold/templates.ts +2 -1
  88. package/upgrades/1.3.819.md +88 -0
  89. package/upgrades/1.3.820.md +25 -0
  90. package/upgrades/side-effects/glance-p4-sweep.md +112 -0
  91. package/upgrades/side-effects/ownership-gated-spawn-increment-1.md +166 -0
  92. package/upgrades/1.3.818.md +0 -25
@@ -657,6 +657,10 @@
657
657
  color: var(--text); cursor: pointer;
658
658
  }
659
659
  .glance-record-action:hover { border-color: var(--accent-dim); }
660
+ .glance-record-action { text-decoration: none; } /* also covers anchor actions */
661
+ /* Phase 4: a row of actions on a Layer-3 record (e.g. a secret's open/copy/cancel). */
662
+ .glance-record-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
663
+ .glance-record-actions .glance-record-action { margin-top: 0; }
660
664
  /* Phase 3: the #1429 editable nickname field (in a machine's Layer-3 record) and
661
665
  the warn-toned Layer-2 rows (a machine or safety check that needs a look). */
662
666
  .glance-record-input {
@@ -3309,11 +3313,7 @@
3309
3313
  </div>
3310
3314
  </div>
3311
3315
  <div class="tab-purpose">A live view of your open pull requests as they move through review and CI checks toward merge.</div>
3312
- <div id="prPipelinePhaseNotice" style="font-size:12px;color:var(--text-dim)"></div>
3313
- <div id="prPipelineEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3314
- No PR activity to display.
3315
- </div>
3316
- <div id="prPipelineList" style="display:flex;flex-direction:column;gap:12px"></div>
3316
+ <div id="prPipelineGlance" class="glance-root"></div>
3317
3317
  </div>
3318
3318
 
3319
3319
  <!-- Projects Tab — PROJECT-SCOPE-SPEC Phase 1.10 read-only view -->
@@ -3350,16 +3350,9 @@
3350
3350
  </div>
3351
3351
  </div>
3352
3352
  <div class="tab-purpose">
3353
- Long-running work that spans multiple phases, with a status flag on each. Signals: <span style="color:#e27d3b">needs-user</span>,
3354
- <span style="color:#d65f5f">next-check-due</span>,
3355
- <span style="color:#4a9a4a">ready-to-advance</span>,
3356
- <span style="color:#888">stale (&gt;7d)</span>.
3357
- </div>
3358
- <div id="initiativesDigest" style="display:none;padding:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-dim)"></div>
3359
- <div id="initiativesEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3360
- No initiatives yet. Ask your agent to create one, and it'll appear here.
3353
+ Long-running work that spans multiple phases — the headline says how many are in flight and how many need you; tap a tile to see which, tap one for the full record.
3361
3354
  </div>
3362
- <div id="initiativesList" style="display:flex;flex-direction:column;gap:12px"></div>
3355
+ <div id="initiativesGlance" class="glance-root"></div>
3363
3356
  </div>
3364
3357
 
3365
3358
  <!-- Commitments Tab (Open Promises) -->
@@ -3374,10 +3367,7 @@
3374
3367
  <div class="tab-purpose">
3375
3368
  Secret Drop — create one-time links so someone can hand you a password or API key safely, never pasted into chat. Each link expires and is destroyed the moment it's used.
3376
3369
  </div>
3377
- <div id="secretsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3378
- No pending secret requests.
3379
- </div>
3380
- <div id="secretsList" style="display:flex;flex-direction:column;gap:12px"></div>
3370
+ <div id="secretsGlance" class="glance-root"></div>
3381
3371
  </div>
3382
3372
 
3383
3373
  <div id="commitmentsPanel" class="tab-panel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
@@ -3402,30 +3392,7 @@
3402
3392
  <div class="tab-purpose">
3403
3393
  Token usage rolled up from Claude Code session transcripts. Last 24 hours unless noted.
3404
3394
  </div>
3405
-
3406
- <!-- Summary card -->
3407
- <div id="tokensSummaryCard" style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3408
- <div style="font-weight:600">Last 24h</div>
3409
- <div id="tokensSummaryBody" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;font-size:13px">
3410
- <div style="color:var(--text-dim)">Loading...</div>
3411
- </div>
3412
- </div>
3413
-
3414
- <!-- Top sessions -->
3415
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3416
- <div style="font-weight:600">Top Sessions</div>
3417
- <div id="tokensSessionsBody" style="font-size:13px">
3418
- <div style="color:var(--text-dim)">Loading...</div>
3419
- </div>
3420
- </div>
3421
-
3422
- <!-- Orphans -->
3423
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3424
- <div style="font-weight:600">Idle Sessions (&gt; 30 min)</div>
3425
- <div id="tokensOrphansBody" style="font-size:13px">
3426
- <div style="color:var(--text-dim)">Loading...</div>
3427
- </div>
3428
- </div>
3395
+ <div id="tokensGlance" class="glance-root"></div>
3429
3396
  </div>
3430
3397
 
3431
3398
  <!-- Resource Usage tab — per-agent CPU + memory (ResourceLedger Phase B).
@@ -3438,30 +3405,7 @@
3438
3405
  <div class="tab-purpose">
3439
3406
  How much CPU and memory this agent is using right now — the server process and each running session — sampled continuously. Last hour unless noted.
3440
3407
  </div>
3441
-
3442
- <!-- Right-now headline (aggregate) -->
3443
- <div id="resourcesHeadlineCard" style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3444
- <div style="font-weight:600">Right now (all processes)</div>
3445
- <div id="resourcesHeadlineBody" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;font-size:13px">
3446
- <div style="color:var(--text-dim)">Loading...</div>
3447
- </div>
3448
- </div>
3449
-
3450
- <!-- Per-source breakdown (server vs each session) -->
3451
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3452
- <div style="font-weight:600">By process</div>
3453
- <div id="resourcesSourcesBody" style="font-size:13px">
3454
- <div style="color:var(--text-dim)">Loading...</div>
3455
- </div>
3456
- </div>
3457
-
3458
- <!-- Recent trend (last samples of the aggregate) -->
3459
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3460
- <div style="font-weight:600">Recent trend</div>
3461
- <div id="resourcesTrendBody" style="font-size:13px">
3462
- <div style="color:var(--text-dim)">Loading...</div>
3463
- </div>
3464
- </div>
3408
+ <div id="resourcesGlance" class="glance-root"></div>
3465
3409
  </div>
3466
3410
 
3467
3411
  <!-- Blockers tab — Blocker Ledger read surface, rebuilt on the glance template
@@ -3510,21 +3454,7 @@
3510
3454
  only ever finds nothing is a quiet smoke detector; a row that's mostly skipped may be starved.
3511
3455
  </div>
3512
3456
 
3513
- <!-- Totals headline -->
3514
- <div id="llmActivityHeadlineCard" style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3515
- <div style="font-weight:600">All components (selected window)</div>
3516
- <div id="llmActivityHeadlineBody" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;font-size:13px">
3517
- <div style="color:var(--text-dim)">Loading...</div>
3518
- </div>
3519
- </div>
3520
-
3521
- <!-- Per-component breakdown -->
3522
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3523
- <div style="font-weight:600">By component</div>
3524
- <div id="llmActivityBody" style="font-size:13px;overflow-x:auto">
3525
- <div style="color:var(--text-dim)">Loading...</div>
3526
- </div>
3527
- </div>
3457
+ <div id="llmActivityGlance" class="glance-root"></div>
3528
3458
  </div>
3529
3459
 
3530
3460
  <!-- Routing Map tab — the read-only nature-axis routing map (FD11 readable canary,
@@ -4543,7 +4473,7 @@
4543
4473
  renderSessionList();
4544
4474
 
4545
4475
  // Persist session in URL for refresh
4546
- updateFileUrl();
4476
+ updateUrlState();
4547
4477
 
4548
4478
  // Focus input
4549
4479
  document.getElementById('termInput').focus();
@@ -4779,7 +4709,7 @@
4779
4709
  document.getElementById('terminalView').style.display = 'none';
4780
4710
  document.getElementById('noSession').style.display = 'flex';
4781
4711
  renderSessionList();
4782
- updateFileUrl(); // Clear session from URL
4712
+ updateUrlState(); // Clear session from URL
4783
4713
  }
4784
4714
 
4785
4715
  // ── Input ────────────────────────────────────────────────
@@ -5380,10 +5310,6 @@
5380
5310
  display: ['flex'],
5381
5311
  onActivate: () => {
5382
5312
  if (typeof loadSecrets === 'function') loadSecrets();
5383
- if (typeof startSecretsTicker === 'function') startSecretsTicker();
5384
- },
5385
- onDeactivate: () => {
5386
- if (typeof stopSecretsTicker === 'function') stopSecretsTicker();
5387
5313
  },
5388
5314
  },
5389
5315
  ];
@@ -5470,7 +5396,7 @@
5470
5396
  if (activeTab.onActivate) activeTab.onActivate();
5471
5397
  }
5472
5398
 
5473
- updateFileUrl();
5399
+ updateUrlState();
5474
5400
  }
5475
5401
 
5476
5402
  // ── File Viewer ────────────────────────────────────────────
@@ -5796,71 +5722,29 @@
5796
5722
  }
5797
5723
 
5798
5724
  async function loadTokens() {
5799
- const summaryEl = document.getElementById('tokensSummaryBody');
5800
- const sessionsEl = document.getElementById('tokensSessionsBody');
5801
- const orphansEl = document.getElementById('tokensOrphansBody');
5725
+ const glanceRoot = document.getElementById('tokensGlance');
5726
+ if (!glanceRoot) return;
5727
+ const glance = await loadGlanceModule();
5728
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
5802
5729
  try {
5803
5730
  const [summaryResp, sessionsResp, orphansResp] = await Promise.all([
5804
5731
  apiFetch('/tokens/summary'),
5805
5732
  apiFetch('/tokens/sessions?limit=10'),
5806
5733
  apiFetch('/tokens/orphans?idleMs=' + (30 * 60 * 1000)),
5807
5734
  ]);
5808
- const s = summaryResp.summary || {};
5809
- summaryEl.innerHTML = `
5810
- <div><div style="color:var(--text-dim)">Total tokens</div><div style="font-size:18px;font-weight:600">${fmtTokens(s.totalTokens)}</div></div>
5811
- <div><div style="color:var(--text-dim)">Input</div><div>${fmtTokens(s.totalInput)}</div></div>
5812
- <div><div style="color:var(--text-dim)">Output</div><div>${fmtTokens(s.totalOutput)}</div></div>
5813
- <div><div style="color:var(--text-dim)">Cache read</div><div>${fmtTokens(s.totalCacheRead)}</div></div>
5814
- <div><div style="color:var(--text-dim)">Cache create</div><div>${fmtTokens(s.totalCacheCreate)}</div></div>
5815
- <div><div style="color:var(--text-dim)">Events</div><div>${fmtTokens(s.eventCount)}</div></div>
5816
- <div><div style="color:var(--text-dim)">Sessions</div><div>${fmtTokens(s.sessionsActive)}</div></div>
5817
- `;
5818
-
5819
- const sessions = sessionsResp.sessions || [];
5820
- if (sessions.length === 0) {
5821
- sessionsEl.innerHTML = '<div style="color:var(--text-dim)">No sessions in window.</div>';
5822
- } else {
5823
- sessionsEl.innerHTML = `
5824
- <table style="width:100%;border-collapse:collapse;font-size:13px">
5825
- <thead><tr style="text-align:left;color:var(--text-dim)">
5826
- <th style="padding:6px 8px">Session</th>
5827
- <th style="padding:6px 8px">Project</th>
5828
- <th style="padding:6px 8px;text-align:right">Tokens</th>
5829
- <th style="padding:6px 8px;text-align:right">Events</th>
5830
- <th style="padding:6px 8px">Last activity</th>
5831
- </tr></thead>
5832
- <tbody>
5833
- ${sessions.map(r => `
5834
- <tr style="border-top:1px solid var(--border)">
5835
- <td style="padding:6px 8px;font-family:monospace">${escapeHtml((r.sessionId || '').slice(0, 8))}</td>
5836
- <td style="padding:6px 8px">${escapeHtml(r.projectPath || '—')}</td>
5837
- <td style="padding:6px 8px;text-align:right">${fmtTokens(r.totalTokens)}</td>
5838
- <td style="padding:6px 8px;text-align:right">${fmtTokens(r.eventCount)}</td>
5839
- <td style="padding:6px 8px">${fmtRelTime(r.lastTs)}</td>
5840
- </tr>
5841
- `).join('')}
5842
- </tbody>
5843
- </table>
5844
- `;
5845
- }
5846
-
5847
- const orphans = orphansResp.orphans || [];
5848
- if (orphans.length === 0) {
5849
- orphansEl.innerHTML = '<div style="color:var(--text-dim)">No idle sessions.</div>';
5850
- } else {
5851
- orphansEl.innerHTML = orphans.map(o => `
5852
- <div style="padding:6px 0;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-family:monospace;font-size:12px">
5853
- <span>${escapeHtml((o.sessionId || '').slice(0, 12))}</span>
5854
- <span style="color:var(--text-dim)">${escapeHtml(o.projectPath || '—')}</span>
5855
- <span>${fmtRelTime(o.lastTs)}</span>
5856
- </div>
5857
- `).join('');
5858
- }
5735
+ const spec = glance.tokensGlanceSpec(
5736
+ document, summaryResp, sessionsResp.sessions || [], orphansResp.orphans || [], { fmtTs: fmtRelTime },
5737
+ );
5738
+ glance.renderGlance(document, glanceRoot, spec);
5859
5739
  } catch (err) {
5860
- const msg = `<div style="color:var(--red)">Error: ${escapeHtml(err.message || String(err))}</div>`;
5861
- summaryEl.innerHTML = msg;
5862
- sessionsEl.innerHTML = '';
5863
- orphansEl.innerHTML = '';
5740
+ const m = err && err.message ? String(err.message) : String(err);
5741
+ glanceRoot.textContent = '';
5742
+ const note = document.createElement('div');
5743
+ note.className = 'glance-empty';
5744
+ note.textContent = /503|unavailable|not initialized/i.test(m)
5745
+ ? 'Token tracking isn’t available on this agent yet.'
5746
+ : 'Could not load token usage right now — refresh to retry.';
5747
+ glanceRoot.appendChild(note);
5864
5748
  }
5865
5749
  }
5866
5750
 
@@ -5887,76 +5771,25 @@
5887
5771
  return String(src || '—');
5888
5772
  }
5889
5773
  async function loadLlmActivity() {
5890
- const headlineEl = document.getElementById('llmActivityHeadlineBody');
5891
- const bodyEl = document.getElementById('llmActivityBody');
5774
+ const glanceRoot = document.getElementById('llmActivityGlance');
5775
+ if (!glanceRoot) return;
5892
5776
  const winSel = document.getElementById('llmActivityWindow');
5893
5777
  const hours = Number((winSel && winSel.value) || 168);
5894
- const card = (label, val) =>
5895
- '<div><div style="color:var(--text-dim)">' + escapeHtml(label) + '</div>' +
5896
- '<div style="font-size:18px;font-weight:600">' + escapeHtml(String(val)) + '</div></div>';
5897
- const fmtNum = (n) => {
5898
- n = Number(n) || 0;
5899
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
5900
- if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
5901
- return String(n);
5902
- };
5903
- const pct = (num, den) => (den > 0 ? Math.round((100 * num) / den) + '%' : '—');
5778
+ const glance = await loadGlanceModule();
5779
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
5904
5780
  try {
5905
5781
  const data = await apiFetch('/metrics/features?sinceHours=' + hours);
5906
- const totals = data.totals || {};
5907
- const features = (data.features || []).slice().sort((a, b) => (b.calls || 0) - (a.calls || 0));
5908
- const realCalls = totals.realCalls || 0;
5909
- headlineEl.innerHTML =
5910
- card('Components', features.length) +
5911
- card('Calls', totals.calls || 0) +
5912
- card('Real calls', realCalls) +
5913
- card('Acted', totals.fired || 0) +
5914
- card('Fire rate', pct(totals.fired || 0, realCalls)) +
5915
- card('Skipped', pct(totals.shed || 0, totals.calls || 0)) +
5916
- card('Errors', totals.errors || 0) +
5917
- card('Tokens', fmtNum((totals.tokensIn || 0) + (totals.tokensOut || 0)));
5918
-
5919
- if (features.length === 0) {
5920
- bodyEl.innerHTML = '<div style="color:var(--text-dim)">No LLM activity recorded in this window yet.</div>';
5921
- return;
5922
- }
5923
- const th = (t, right) => '<th style="padding:6px 8px;' + (right ? 'text-align:right' : 'text-align:left') + '">' + t + '</th>';
5924
- const td = (v, right) => '<td style="padding:6px 8px;' + (right ? 'text-align:right' : '') + '">' + v + '</td>';
5925
- bodyEl.innerHTML =
5926
- '<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:760px">' +
5927
- '<thead><tr style="color:var(--text-dim)">' +
5928
- th('Component') + th('Provider') + th('Model') +
5929
- th('Calls', true) + th('Real', true) + th('Acted', true) + th('Fire rate', true) +
5930
- th('Skipped', true) + th('Errors', true) + th('Tokens', true) + th('Latency p50/p95', true) +
5931
- '</tr></thead><tbody>' +
5932
- features.map((f) => {
5933
- const real = f.realCalls || 0;
5934
- const provider = (f.frameworks && f.frameworks.length) ? f.frameworks.join(', ') : '—';
5935
- const model = (f.models && f.models.length) ? f.models.join(', ') : '—';
5936
- const tokens = (f.tokensIn || 0) + (f.tokensOut || 0);
5937
- return '<tr style="border-top:1px solid var(--border)">' +
5938
- td(escapeHtml(f.feature)) +
5939
- td(escapeHtml(provider)) +
5940
- td(escapeHtml(model)) +
5941
- td(String(f.calls || 0), true) +
5942
- td(String(real), true) +
5943
- td(String(f.fired || 0), true) +
5944
- td(pct(f.fired || 0, real), true) +
5945
- td(pct(f.shed || 0, f.calls || 0), true) +
5946
- td(String(f.errors || 0), true) +
5947
- td(fmtNum(tokens), true) +
5948
- td((f.p50LatencyMs || 0) + '/' + (f.p95LatencyMs || 0) + 'ms', true) +
5949
- '</tr>';
5950
- }).join('') +
5951
- '</tbody></table>';
5782
+ const spec = glance.llmActivityGlanceSpec(document, data);
5783
+ glance.renderGlance(document, glanceRoot, spec);
5952
5784
  } catch (err) {
5953
5785
  const m = err && err.message ? String(err.message) : String(err);
5954
- if (/503|unavailable|not initialized/i.test(m)) {
5955
- headlineEl.innerHTML = '<div style="color:var(--text-dim)">LLM activity tracking isn\'t available on this agent yet.</div>';
5956
- } else {
5957
- headlineEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(m) + '</div>';
5958
- }
5959
- bodyEl.innerHTML = '';
5786
+ glanceRoot.textContent = '';
5787
+ const note = document.createElement('div');
5788
+ note.className = 'glance-empty';
5789
+ note.textContent = /503|unavailable|not initialized/i.test(m)
5790
+ ? 'LLM activity tracking isn’t available on this agent yet.'
5791
+ : 'Could not load LLM activity right now — refresh to retry.';
5792
+ glanceRoot.appendChild(note);
5960
5793
  }
5961
5794
  }
5962
5795
 
@@ -6032,77 +5865,23 @@
6032
5865
  }
6033
5866
 
6034
5867
  async function loadResources() {
6035
- const headlineEl = document.getElementById('resourcesHeadlineBody');
6036
- const sourcesEl = document.getElementById('resourcesSourcesBody');
6037
- const trendEl = document.getElementById('resourcesTrendBody');
5868
+ const glanceRoot = document.getElementById('resourcesGlance');
5869
+ if (!glanceRoot) return;
5870
+ const glance = await loadGlanceModule();
5871
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
6038
5872
  try {
6039
- const [summaryResp, samplesResp] = await Promise.all([
6040
- apiFetch('/resources/summary?sinceHours=1'),
6041
- apiFetch('/resources/samples?sinceHours=1&source=aggregate&limit=20'),
6042
- ]);
6043
- const sources = summaryResp.sources || [];
6044
- const agg = sources.find(s => s.source === 'aggregate');
6045
- if (agg) {
6046
- headlineEl.innerHTML =
6047
- '<div><div style="color:var(--text-dim)">CPU now</div><div style="font-size:18px;font-weight:600">' + escapeHtml(fmtCpu(agg.currentCpuPercent)) + '</div></div>' +
6048
- '<div><div style="color:var(--text-dim)">Memory now (RSS)</div><div style="font-size:18px;font-weight:600">' + escapeHtml(fmtBytes(agg.currentRssBytes)) + '</div></div>' +
6049
- '<div><div style="color:var(--text-dim)">Avg CPU (1h)</div><div>' + escapeHtml(fmtCpu(agg.avgCpuPercent)) + '</div></div>' +
6050
- '<div><div style="color:var(--text-dim)">Peak CPU (1h)</div><div>' + escapeHtml(fmtCpu(agg.peakCpuPercent)) + '</div></div>' +
6051
- '<div><div style="color:var(--text-dim)">Peak memory (1h)</div><div>' + escapeHtml(fmtBytes(agg.peakRssBytes)) + '</div></div>' +
6052
- '<div><div style="color:var(--text-dim)">Samples</div><div>' + escapeHtml(String(summaryResp.sampleCount || 0)) + '</div></div>';
6053
- } else {
6054
- headlineEl.innerHTML = '<div style="color:var(--text-dim)">No samples collected yet — give it a minute.</div>';
6055
- }
6056
-
6057
- // Per-source breakdown (server + each session), aggregate listed last.
6058
- const breakdown = sources.filter(s => s.source !== 'aggregate');
6059
- if (breakdown.length === 0) {
6060
- sourcesEl.innerHTML = '<div style="color:var(--text-dim)">No process samples in the last hour.</div>';
6061
- } else {
6062
- sourcesEl.innerHTML =
6063
- '<table style="width:100%;border-collapse:collapse;font-size:13px">' +
6064
- '<thead><tr style="text-align:left;color:var(--text-dim)">' +
6065
- '<th style="padding:6px 8px">Process</th>' +
6066
- '<th style="padding:6px 8px;text-align:right">CPU now</th>' +
6067
- '<th style="padding:6px 8px;text-align:right">Avg CPU</th>' +
6068
- '<th style="padding:6px 8px;text-align:right">Memory (RSS)</th>' +
6069
- '<th style="padding:6px 8px;text-align:right">Peak CPU</th>' +
6070
- '</tr></thead><tbody>' +
6071
- breakdown.map(s =>
6072
- '<tr style="border-top:1px solid var(--border)">' +
6073
- '<td style="padding:6px 8px">' + escapeHtml(resourceSourceLabel(s.source)) + '</td>' +
6074
- '<td style="padding:6px 8px;text-align:right">' + escapeHtml(fmtCpu(s.currentCpuPercent)) + '</td>' +
6075
- '<td style="padding:6px 8px;text-align:right">' + escapeHtml(fmtCpu(s.avgCpuPercent)) + '</td>' +
6076
- '<td style="padding:6px 8px;text-align:right">' + escapeHtml(fmtBytes(s.currentRssBytes)) + '</td>' +
6077
- '<td style="padding:6px 8px;text-align:right">' + escapeHtml(fmtCpu(s.peakCpuPercent)) + '</td>' +
6078
- '</tr>'
6079
- ).join('') +
6080
- '</tbody></table>';
6081
- }
6082
-
6083
- // Recent trend: the last aggregate samples (newest first).
6084
- const samples = samplesResp.samples || [];
6085
- if (samples.length === 0) {
6086
- trendEl.innerHTML = '<div style="color:var(--text-dim)">No trend yet.</div>';
6087
- } else {
6088
- trendEl.innerHTML = samples.map(s =>
6089
- '<div style="padding:5px 0;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:12px">' +
6090
- '<span style="color:var(--text-dim)">' + escapeHtml(fmtRelTime(s.ts)) + '</span>' +
6091
- '<span>' + escapeHtml(fmtCpu(s.cpuPercent)) + ' CPU</span>' +
6092
- '<span>' + escapeHtml(fmtBytes(s.rssBytes)) + ' RSS</span>' +
6093
- '</div>'
6094
- ).join('');
6095
- }
5873
+ const summaryResp = await apiFetch('/resources/summary?sinceHours=1');
5874
+ const spec = glance.resourcesGlanceSpec(document, summaryResp);
5875
+ glance.renderGlance(document, glanceRoot, spec);
6096
5876
  } catch (err) {
6097
5877
  const m = err && err.message ? String(err.message) : String(err);
6098
- // 503 / "unavailable" → the sampler isn't turned on for this agent.
6099
- if (/503|unavailable|disabled|not initialized/i.test(m)) {
6100
- headlineEl.innerHTML = '<div style="color:var(--text-dim)">Resource tracking isn\'t turned on for this agent yet.</div>';
6101
- } else {
6102
- headlineEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(m) + '</div>';
6103
- }
6104
- sourcesEl.innerHTML = '';
6105
- trendEl.innerHTML = '';
5878
+ glanceRoot.textContent = '';
5879
+ const note = document.createElement('div');
5880
+ note.className = 'glance-empty';
5881
+ note.textContent = /503|unavailable|disabled|not initialized/i.test(m)
5882
+ ? 'Resource tracking isn’t turned on for this agent yet.'
5883
+ : 'Could not load resource usage right now — refresh to retry.';
5884
+ glanceRoot.appendChild(note);
6106
5885
  }
6107
5886
  }
6108
5887
 
@@ -6536,7 +6315,7 @@
6536
6315
  body.innerHTML = `<div class="file-empty"><div class="icon">&#x1F4E6;</div><p>Binary file (${formatFileSize(data.size)})<br>Preview not available</p></div>`;
6537
6316
  updateFileBadges(false);
6538
6317
  editModifiedTimestamp = '';
6539
- updateFileUrl();
6318
+ updateUrlState();
6540
6319
  return;
6541
6320
  }
6542
6321
 
@@ -6553,7 +6332,7 @@
6553
6332
  }
6554
6333
 
6555
6334
  updateFileBadges(data.editable);
6556
- updateFileUrl();
6335
+ updateUrlState();
6557
6336
  } catch (err) {
6558
6337
  body.innerHTML = `<div class="file-error">Failed to load file: ${escapeHtml(err.message)}</div>`;
6559
6338
  }
@@ -6727,7 +6506,7 @@
6727
6506
  document.getElementById('fileContentHeader').style.display = 'none';
6728
6507
  document.getElementById('fileContentBody').innerHTML =
6729
6508
  '<div class="file-empty"><div class="icon">&#x1F4C2;</div><p>Select a file to view its contents</p></div>';
6730
- updateFileUrl();
6509
+ updateUrlState();
6731
6510
  }
6732
6511
 
6733
6512
  function formatFileSize(bytes) {
@@ -7015,25 +6794,34 @@
7015
6794
 
7016
6795
  // ── Deep linking via query params ──────────────────────────
7017
6796
 
7018
- function updateFileUrl() {
6797
+ // #1442 — record the active tab (and its extras) in the URL so a page refresh
6798
+ // restores the same tab. Before this, only Files wrote ?tab=; every other tab
6799
+ // deleted the param, so any refresh dumped the operator back on Sessions.
6800
+ // Named updateUrlState (was updateFileUrl) now that it is not Files-specific.
6801
+ function updateUrlState() {
7019
6802
  const params = new URLSearchParams(window.location.search);
7020
- if (currentTab === 'files') {
7021
- params.set('tab', 'files');
7022
- if (currentFilePath) {
7023
- params.set('path', currentFilePath);
7024
- } else {
7025
- params.delete('path');
7026
- }
7027
- params.delete('session');
6803
+ // The default Sessions tab omits ?tab= to keep bare URLs clean (and so a
6804
+ // fresh visit with no query still lands on Sessions). Every other tab records
6805
+ // its id, so a refresh returns to it.
6806
+ if (currentTab && currentTab !== 'sessions') {
6807
+ params.set('tab', currentTab);
7028
6808
  } else {
7029
6809
  params.delete('tab');
6810
+ }
6811
+ // Files keeps its deep-link path — existing ?tab=files&path=… links must keep
6812
+ // working byte-for-byte.
6813
+ if (currentTab === 'files' && currentFilePath) {
6814
+ params.set('path', currentFilePath);
6815
+ } else {
7030
6816
  params.delete('path');
7031
- // Persist active session in URL
7032
- if (activeSession) {
7033
- params.set('session', activeSession);
7034
- } else {
7035
- params.delete('session');
7036
- }
6817
+ }
6818
+ // Persist the active streamed session in the URL (unchanged behavior — set
6819
+ // whenever a session is active except on Files; meaningful on Sessions,
6820
+ // harmless elsewhere).
6821
+ if (currentTab !== 'files' && activeSession) {
6822
+ params.set('session', activeSession);
6823
+ } else {
6824
+ params.delete('session');
7037
6825
  }
7038
6826
  const qs = params.toString();
7039
6827
  const newUrl = window.location.pathname + (qs ? '?' + qs : '');
@@ -7046,6 +6834,11 @@
7046
6834
  const filePath = params.get('path');
7047
6835
  const sessionParam = params.get('session');
7048
6836
 
6837
+ // #1442 — restore any registered tab on refresh, not just Files. An unknown
6838
+ // or removed ?tab= value falls through to the default tab with no switchTab
6839
+ // and no console error (validated against TAB_REGISTRY). The session restore
6840
+ // below runs independently, so ?tab=X&session=Y both apply.
6841
+ const isKnownTab = !!tab && TAB_REGISTRY.some(t => t.id === tab);
7049
6842
  if (tab === 'files') {
7050
6843
  switchTab('files');
7051
6844
  if (filePath) {
@@ -7066,7 +6859,11 @@
7066
6859
  };
7067
6860
  waitForTree();
7068
6861
  }
7069
- } else if (sessionParam) {
6862
+ } else if (isKnownTab) {
6863
+ switchTab(tab);
6864
+ }
6865
+
6866
+ if (sessionParam) {
7070
6867
  // Restore active session from URL — wait for session list to populate
7071
6868
  let attempts = 0;
7072
6869
  const waitForSessions = () => {
@@ -8100,96 +7897,39 @@
8100
7897
  // - On 404 (prGate.phase='off' — commit 8), render a disabled
8101
7898
  // placeholder rather than erroring.
8102
7899
  async function loadPrPipeline() {
8103
- const list = document.getElementById('prPipelineList');
8104
- const notice = document.getElementById('prPipelinePhaseNotice');
8105
- const empty = document.getElementById('prPipelineEmpty');
8106
- // Reset view via DOM mutation, not innerHTML, to keep the
8107
- // "never inject content as HTML" invariant trivially true.
8108
- while (list.firstChild) list.removeChild(list.firstChild);
8109
- notice.textContent = 'Loading…';
8110
- empty.style.display = 'none';
7900
+ const glanceRoot = document.getElementById('prPipelineGlance');
7901
+ if (!glanceRoot) return;
7902
+ const glance = await loadGlanceModule();
7903
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
8111
7904
 
8112
- // Use raw fetch (not apiFetch) so we can distinguish 404 (gate
8113
- // disabled, expected during phase=off) from other errors without
8114
- // apiFetch's throw-on-non-ok behavior.
8115
- let res = null;
8116
- let httpStatus = 0;
7905
+ // Raw fetch (not apiFetch) so we can distinguish 404 (gate disabled,
7906
+ // expected during phase=off) from other errors without throw-on-non-ok.
7907
+ let res = null, httpStatus = 0;
8117
7908
  try {
8118
- const raw = await fetch('/pr-gate/metrics', {
8119
- headers: { 'Authorization': `Bearer ${token}` },
8120
- });
7909
+ const raw = await fetch('/pr-gate/metrics', { headers: { 'Authorization': `Bearer ${token}` } });
8121
7910
  httpStatus = raw.status;
8122
7911
  if (raw.ok) res = await raw.json();
8123
- } catch {
8124
- res = null;
8125
- }
7912
+ } catch { res = null; }
8126
7913
 
8127
7914
  if (httpStatus === 404 || (res && res.disabled === true)) {
8128
- notice.textContent = 'Gate disabled (phase=off). No pipeline activity recorded yet.';
8129
- empty.style.display = 'block';
7915
+ glanceRoot.textContent = '';
7916
+ const note = document.createElement('div');
7917
+ note.className = 'glance-empty';
7918
+ note.textContent = 'The auto-merge gate isn’t turned on for this agent yet.';
7919
+ glanceRoot.appendChild(note);
8130
7920
  return;
8131
7921
  }
8132
7922
  if (!res) {
8133
- notice.textContent = 'PR Pipeline metrics unavailable. Endpoint did not respond.';
8134
- empty.style.display = 'block';
8135
- return;
8136
- }
8137
-
8138
- const phase = typeof res.phase === 'string' ? res.phase : 'unknown';
8139
- notice.textContent = `Phase: ${phase}`;
8140
-
8141
- const entries = Array.isArray(res.entries) ? res.entries : [];
8142
- if (entries.length === 0) {
8143
- empty.style.display = 'block';
7923
+ glanceRoot.textContent = '';
7924
+ const note = document.createElement('div');
7925
+ note.className = 'glance-empty';
7926
+ note.textContent = 'Could not load the merge gate right now — refresh to retry.';
7927
+ glanceRoot.appendChild(note);
8144
7928
  return;
8145
7929
  }
8146
7930
 
8147
- for (const entry of entries) {
8148
- const card = document.createElement('div');
8149
- card.style.background = 'var(--panel)';
8150
- card.style.padding = '12px';
8151
- card.style.borderRadius = '6px';
8152
- card.style.border = '1px solid var(--border)';
8153
-
8154
- const header = document.createElement('div');
8155
- header.style.display = 'flex';
8156
- header.style.justifyContent = 'space-between';
8157
- header.style.marginBottom = '6px';
8158
-
8159
- const left = document.createElement('div');
8160
- left.style.fontWeight = '600';
8161
- const pr = typeof entry.pr_number === 'number' ? `PR #${entry.pr_number}` : 'PR #—';
8162
- const sha = typeof entry.head_sha === 'string' ? ` @ ${entry.head_sha.slice(0, 8)}` : '';
8163
- left.textContent = pr + sha;
8164
-
8165
- const right = document.createElement('div');
8166
- right.style.fontSize = '12px';
8167
- right.style.color = entry.eligible ? 'var(--accent)' : 'var(--text-dim)';
8168
- right.textContent = entry.eligible ? 'eligible' : 'not eligible';
8169
-
8170
- header.appendChild(left);
8171
- header.appendChild(right);
8172
- card.appendChild(header);
8173
-
8174
- if (typeof entry.reason === 'string' && entry.reason.length > 0) {
8175
- const reason = document.createElement('div');
8176
- reason.style.fontSize = '13px';
8177
- reason.style.color = 'var(--text-dim)';
8178
- reason.textContent = entry.reason;
8179
- card.appendChild(reason);
8180
- }
8181
-
8182
- if (typeof entry.created_at === 'string') {
8183
- const time = document.createElement('div');
8184
- time.style.fontSize = '11px';
8185
- time.style.color = 'var(--text-dim)';
8186
- time.style.marginTop = '4px';
8187
- time.textContent = entry.created_at;
8188
- card.appendChild(time);
8189
- }
8190
-
8191
- list.appendChild(card);
8192
- }
7931
+ const spec = glance.prPipelineGlanceSpec(document, res);
7932
+ glance.renderGlance(document, glanceRoot, spec);
8193
7933
  }
8194
7934
 
8195
7935
  // ── Projects Tab (PROJECT-SCOPE-SPEC Phase 1.10) ─────────────
@@ -8409,178 +8149,44 @@
8409
8149
  // through textContent; no innerHTML. Matches the PR Pipeline tab's
8410
8150
  // XSS-safe pattern.
8411
8151
  async function loadInitiatives() {
8412
- const list = document.getElementById('initiativesList');
8413
- const empty = document.getElementById('initiativesEmpty');
8414
- const digestBox = document.getElementById('initiativesDigest');
8415
- const filter = document.getElementById('initiativesFilter');
8152
+ const glanceRoot = document.getElementById('initiativesGlance');
8416
8153
  const countBadge = document.getElementById('tabInitiativeCount');
8154
+ if (!glanceRoot) return;
8155
+ const glance = await loadGlanceModule();
8156
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
8417
8157
 
8418
- while (list.firstChild) list.removeChild(list.firstChild);
8419
- while (digestBox.firstChild) digestBox.removeChild(digestBox.firstChild);
8420
- digestBox.style.display = 'none';
8421
- empty.style.display = 'none';
8158
+ // Keep the status filter + showAll controls — they re-query and the glance
8159
+ // renders whatever comes back (behavior preserved).
8160
+ const filter = document.getElementById('initiativesFilter');
8161
+ const showAll = document.getElementById('initiativesShowAll');
8162
+ const params = [];
8163
+ if (filter && filter.value) params.push(`status=${encodeURIComponent(filter.value)}`);
8164
+ if (!showAll || !showAll.checked) { params.push('excludeKind=project'); params.push('excludeParented=true'); }
8165
+ const q = params.length ? '?' + params.join('&') : '';
8422
8166
 
8423
- let itemsRes = null;
8424
- let digestRes = null;
8167
+ let itemsRes = null, digestRes = null;
8425
8168
  try {
8426
- // PROJECT-SCOPE-SPEC Phase 1.10 — by default hide project-kind
8427
- // records (shown in the Projects tab) and parented children
8428
- // (shown inside their parent's card). Server-side filter so
8429
- // we don't ship them over the wire.
8430
- const showAll = document.getElementById('initiativesShowAll');
8431
- const params = [];
8432
- if (filter && filter.value) params.push(`status=${encodeURIComponent(filter.value)}`);
8433
- if (!showAll || !showAll.checked) {
8434
- params.push('excludeKind=project');
8435
- params.push('excludeParented=true');
8436
- }
8437
- const q = params.length ? '?' + params.join('&') : '';
8438
8169
  const [a, b] = await Promise.all([
8439
8170
  apiFetch('/initiatives' + q).catch(() => null),
8440
8171
  apiFetch('/initiatives/digest').catch(() => null),
8441
8172
  ]);
8442
- itemsRes = a;
8443
- digestRes = b;
8444
- } catch { /* handled by null check below */ }
8445
-
8446
- if (!itemsRes) {
8447
- empty.style.display = 'block';
8448
- empty.textContent = 'Initiative tracker unavailable.';
8449
- if (countBadge) countBadge.textContent = '0';
8450
- return;
8451
- }
8173
+ itemsRes = a; digestRes = b;
8174
+ } catch { /* handled by the null check below */ }
8452
8175
 
8453
- const items = Array.isArray(itemsRes.items) ? itemsRes.items : [];
8176
+ const items = (itemsRes && Array.isArray(itemsRes.items)) ? itemsRes.items : [];
8454
8177
  if (countBadge) countBadge.textContent = String(items.length);
8455
8178
 
8456
- // Digest summary (actionable signals)
8457
- if (digestRes && Array.isArray(digestRes.items) && digestRes.items.length > 0) {
8458
- const title = document.createElement('div');
8459
- title.style.fontWeight = '600';
8460
- title.style.marginBottom = '6px';
8461
- title.textContent = `${digestRes.items.length} signal${digestRes.items.length === 1 ? '' : 's'}`;
8462
- digestBox.appendChild(title);
8463
- for (const d of digestRes.items) {
8464
- const row = document.createElement('div');
8465
- row.style.fontSize = '13px';
8466
- row.style.padding = '3px 0';
8467
- const colorMap = {
8468
- 'needs-user': '#e27d3b',
8469
- 'next-check-due': '#d65f5f',
8470
- 'ready-to-advance': '#4a9a4a',
8471
- 'stale': '#888',
8472
- };
8473
- const tag = document.createElement('span');
8474
- tag.style.color = colorMap[d.reason] || 'var(--text-dim)';
8475
- tag.style.marginRight = '6px';
8476
- tag.textContent = `[${d.reason}]`;
8477
- const label = document.createElement('span');
8478
- label.textContent = `${d.title} — ${d.detail}`;
8479
- row.appendChild(tag);
8480
- row.appendChild(label);
8481
- digestBox.appendChild(row);
8482
- }
8483
- digestBox.style.display = 'block';
8484
- }
8485
-
8486
- if (items.length === 0) {
8487
- empty.style.display = 'block';
8488
- empty.textContent = 'No initiatives to display. Create one via POST /initiatives.';
8179
+ if (!itemsRes) {
8180
+ glanceRoot.textContent = '';
8181
+ const note = document.createElement('div');
8182
+ note.className = 'glance-empty';
8183
+ note.textContent = 'The initiative tracker isn’t available on this agent yet.';
8184
+ glanceRoot.appendChild(note);
8489
8185
  return;
8490
8186
  }
8491
8187
 
8492
- for (const ini of items) {
8493
- const card = document.createElement('div');
8494
- card.style.background = 'var(--panel)';
8495
- card.style.padding = '12px';
8496
- card.style.borderRadius = '6px';
8497
- card.style.border = '1px solid var(--border)';
8498
-
8499
- const header = document.createElement('div');
8500
- header.style.display = 'flex';
8501
- header.style.justifyContent = 'space-between';
8502
- header.style.alignItems = 'center';
8503
- header.style.marginBottom = '6px';
8504
-
8505
- const titleEl = document.createElement('div');
8506
- titleEl.style.fontWeight = '600';
8507
- titleEl.textContent = ini.title || ini.id || '(untitled)';
8508
- header.appendChild(titleEl);
8509
-
8510
- const status = document.createElement('span');
8511
- status.style.fontSize = '11px';
8512
- status.style.padding = '2px 8px';
8513
- status.style.borderRadius = '999px';
8514
- const statusColors = {
8515
- active: 'var(--accent)',
8516
- completed: '#4a9a4a',
8517
- archived: 'var(--text-dim)',
8518
- abandoned: '#d65f5f',
8519
- };
8520
- status.style.background = statusColors[ini.status] || 'var(--text-dim)';
8521
- status.style.color = '#fff';
8522
- status.textContent = ini.status || 'unknown';
8523
- header.appendChild(status);
8524
- card.appendChild(header);
8525
-
8526
- if (ini.description) {
8527
- const desc = document.createElement('div');
8528
- desc.style.fontSize = '13px';
8529
- desc.style.color = 'var(--text-dim)';
8530
- desc.style.marginBottom = '8px';
8531
- desc.textContent = ini.description;
8532
- card.appendChild(desc);
8533
- }
8534
-
8535
- // Phase pills
8536
- if (Array.isArray(ini.phases) && ini.phases.length > 0) {
8537
- const phaseRow = document.createElement('div');
8538
- phaseRow.style.display = 'flex';
8539
- phaseRow.style.gap = '6px';
8540
- phaseRow.style.flexWrap = 'wrap';
8541
- phaseRow.style.marginBottom = '8px';
8542
- const phaseColors = {
8543
- pending: 'var(--text-dim)',
8544
- 'in-progress': 'var(--accent)',
8545
- done: '#4a9a4a',
8546
- blocked: '#d65f5f',
8547
- };
8548
- for (const p of ini.phases) {
8549
- const pill = document.createElement('span');
8550
- pill.style.fontSize = '11px';
8551
- pill.style.padding = '2px 8px';
8552
- pill.style.borderRadius = '4px';
8553
- pill.style.border = `1px solid ${phaseColors[p.status] || 'var(--border)'}`;
8554
- pill.style.color = phaseColors[p.status] || 'var(--text)';
8555
- pill.textContent = `${p.name} · ${p.status}`;
8556
- phaseRow.appendChild(pill);
8557
- }
8558
- card.appendChild(phaseRow);
8559
- }
8560
-
8561
- // Meta row
8562
- const meta = document.createElement('div');
8563
- meta.style.fontSize = '11px';
8564
- meta.style.color = 'var(--text-dim)';
8565
- const lastTouched = ini.lastTouchedAt ? ini.lastTouchedAt.slice(0, 10) : '';
8566
- const needsUser = ini.needsUser ? ' · needs you' : '';
8567
- const blockerCount = Array.isArray(ini.blockers) && ini.blockers.length > 0
8568
- ? ` · ${ini.blockers.length} blocker${ini.blockers.length === 1 ? '' : 's'}`
8569
- : '';
8570
- meta.textContent = `Last touched: ${lastTouched}${needsUser}${blockerCount}`;
8571
- card.appendChild(meta);
8572
-
8573
- if (ini.needsUser && ini.needsUserReason) {
8574
- const reason = document.createElement('div');
8575
- reason.style.fontSize = '12px';
8576
- reason.style.marginTop = '4px';
8577
- reason.style.color = '#e27d3b';
8578
- reason.textContent = `→ ${ini.needsUserReason}`;
8579
- card.appendChild(reason);
8580
- }
8581
-
8582
- list.appendChild(card);
8583
- }
8188
+ const spec = glance.initiativesGlanceSpec(document, itemsRes, digestRes, { fmtTs: fmtRelTime });
8189
+ glance.renderGlance(document, glanceRoot, spec);
8584
8190
  }
8585
8191
 
8586
8192
  // ── Commitments Tab — glance floors F10/F11 (topic 29836) ────
@@ -8689,139 +8295,39 @@
8689
8295
  }
8690
8296
  }
8691
8297
 
8298
+ async function cancelSecretRequest(token) {
8299
+ try {
8300
+ await apiFetch(`/secrets/pending/${encodeURIComponent(token)}`, { method: 'DELETE' });
8301
+ await loadSecrets();
8302
+ } catch (err) {
8303
+ alert('Cancel failed: ' + (err && err.message ? err.message : String(err)));
8304
+ }
8305
+ }
8306
+
8692
8307
  async function loadSecrets() {
8693
- const list = document.getElementById('secretsList');
8694
- const empty = document.getElementById('secretsEmpty');
8308
+ const glanceRoot = document.getElementById('secretsGlance');
8309
+ if (!glanceRoot) return;
8695
8310
  const countBadge = document.getElementById('tabSecretCount');
8696
- while (list.firstChild) list.removeChild(list.firstChild);
8697
- empty.style.display = 'none';
8311
+ const glance = await loadGlanceModule();
8312
+ if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
8698
8313
 
8699
8314
  let res = null;
8700
8315
  try {
8701
8316
  res = await apiFetch('/secrets/pending');
8702
- } catch { /* fall through */ }
8317
+ } catch { /* fall through — an empty glance is the honest state */ }
8703
8318
 
8704
8319
  const pending = (res && Array.isArray(res.pending)) ? res.pending : [];
8705
8320
  secretsLastPending = pending;
8706
8321
 
8707
- // Badge shows non-expired count.
8322
+ // Badge shows non-expired count (unchanged behavior).
8708
8323
  const now = Date.now();
8709
8324
  const liveCount = pending.filter(p => p.expiresAt > now).length;
8710
8325
  if (countBadge) countBadge.textContent = String(liveCount);
8711
8326
 
8712
- if (pending.length === 0) {
8713
- empty.style.display = 'block';
8714
- empty.textContent = 'No pending secret requests.';
8715
- return;
8716
- }
8717
-
8718
- const fmtTs = (ms) => {
8719
- if (!ms) return '—';
8720
- try { return new Date(ms).toLocaleString(); } catch { return String(ms); }
8721
- };
8722
-
8723
- for (const p of pending) {
8724
- const card = document.createElement('div');
8725
- card.style.cssText = 'padding:14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-dim);display:flex;flex-direction:column;gap:8px';
8726
-
8727
- const header = document.createElement('div');
8728
- header.style.cssText = 'display:flex;justify-content:space-between;gap:12px;align-items:flex-start';
8729
- const label = document.createElement('div');
8730
- label.style.cssText = 'flex:1;font-weight:600;line-height:1.3';
8731
- label.textContent = p.label || '(no label)';
8732
- header.appendChild(label);
8733
-
8734
- const badge = document.createElement('span');
8735
- const expired = !!p.expired;
8736
- badge.textContent = expired ? 'expired' : 'pending';
8737
- badge.style.cssText = `font-size:11px;padding:3px 8px;border-radius:4px;background:${expired ? '#c66' : '#4a9a4a'};color:#fff;white-space:nowrap`;
8738
- header.appendChild(badge);
8739
- card.appendChild(header);
8740
-
8741
- const meta = document.createElement('div');
8742
- meta.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:6px;font-size:12px;color:var(--text-dim)';
8743
-
8744
- const rows = [
8745
- ['token', (p.token || '').slice(0, 12) + '…'],
8746
- ['topic', p.topicId != null ? String(p.topicId) : '—'],
8747
- ['created', fmtTs(p.createdAt)],
8748
- ['expires', fmtTs(p.expiresAt)],
8749
- ];
8750
- for (const [k, v] of rows) {
8751
- const cell = document.createElement('div');
8752
- const kEl = document.createElement('span');
8753
- kEl.textContent = `${k}: `;
8754
- kEl.style.opacity = '0.7';
8755
- const vEl = document.createElement('span');
8756
- vEl.textContent = v;
8757
- vEl.style.color = 'var(--text)';
8758
- cell.appendChild(kEl);
8759
- cell.appendChild(vEl);
8760
- meta.appendChild(cell);
8761
- }
8762
-
8763
- const countdownCell = document.createElement('div');
8764
- const cLabel = document.createElement('span');
8765
- cLabel.textContent = 'expires in: ';
8766
- cLabel.style.opacity = '0.7';
8767
- const cVal = document.createElement('span');
8768
- cVal.id = `secretCountdown-${p.token}`;
8769
- cVal.style.color = 'var(--text)';
8770
- cVal.style.fontVariantNumeric = 'tabular-nums';
8771
- cVal.textContent = formatCountdown(p.expiresAt - now);
8772
- countdownCell.appendChild(cLabel);
8773
- countdownCell.appendChild(cVal);
8774
- meta.appendChild(countdownCell);
8775
-
8776
- card.appendChild(meta);
8777
-
8778
- // Links and actions.
8779
- const actions = document.createElement('div');
8780
- actions.style.cssText = 'display:flex;gap:8px;align-items:center;flex-wrap:wrap';
8781
-
8782
- const url = p.tunnelUrl || p.localUrl;
8783
- if (url) {
8784
- const openBtn = document.createElement('a');
8785
- openBtn.textContent = 'Open drop link';
8786
- openBtn.href = url;
8787
- openBtn.target = '_blank';
8788
- openBtn.rel = 'noopener noreferrer';
8789
- openBtn.style.cssText = 'padding:6px 10px;border:1px solid var(--border);border-radius:4px;text-decoration:none;color:var(--text);cursor:pointer';
8790
- actions.appendChild(openBtn);
8791
-
8792
- const copyBtn = document.createElement('button');
8793
- copyBtn.textContent = 'Copy link';
8794
- copyBtn.style.cssText = 'padding:6px 10px;cursor:pointer';
8795
- copyBtn.addEventListener('click', async () => {
8796
- try {
8797
- await navigator.clipboard.writeText(url);
8798
- copyBtn.textContent = 'Copied';
8799
- setTimeout(() => { copyBtn.textContent = 'Copy link'; }, 1200);
8800
- } catch {
8801
- copyBtn.textContent = 'Copy failed';
8802
- }
8803
- });
8804
- actions.appendChild(copyBtn);
8805
- }
8806
-
8807
- const cancelBtn = document.createElement('button');
8808
- cancelBtn.textContent = 'Cancel';
8809
- cancelBtn.style.cssText = 'padding:6px 10px;cursor:pointer';
8810
- cancelBtn.addEventListener('click', async () => {
8811
- cancelBtn.disabled = true;
8812
- try {
8813
- await apiFetch(`/secrets/pending/${encodeURIComponent(p.token)}`, { method: 'DELETE' });
8814
- await loadSecrets();
8815
- } catch (err) {
8816
- cancelBtn.disabled = false;
8817
- alert('Cancel failed: ' + (err && err.message ? err.message : String(err)));
8818
- }
8819
- });
8820
- actions.appendChild(cancelBtn);
8821
-
8822
- card.appendChild(actions);
8823
- list.appendChild(card);
8824
- }
8327
+ // The open / copy / cancel actions are preserved on each request's Layer-3
8328
+ // record (glance.js secretRecordNode) — behavior untouched, moved one tap down.
8329
+ const spec = glance.secretsGlanceSpec(document, { pending }, { now, onCancel: cancelSecretRequest });
8330
+ glance.renderGlance(document, glanceRoot, spec);
8825
8331
  }
8826
8332
 
8827
8333
  async function createTestSecretRequest() {