instar 0.28.77 → 0.28.79

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 (102) hide show
  1. package/dashboard/index.html +354 -11
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +6 -4
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/playbook.d.ts.map +1 -1
  6. package/dist/commands/playbook.js +2 -1
  7. package/dist/commands/playbook.js.map +1 -1
  8. package/dist/commands/server.d.ts.map +1 -1
  9. package/dist/commands/server.js +136 -9
  10. package/dist/commands/server.js.map +1 -1
  11. package/dist/commands/setup.d.ts.map +1 -1
  12. package/dist/commands/setup.js +5 -3
  13. package/dist/commands/setup.js.map +1 -1
  14. package/dist/core/Config.d.ts.map +1 -1
  15. package/dist/core/Config.js +2 -1
  16. package/dist/core/Config.js.map +1 -1
  17. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  18. package/dist/core/PostUpdateMigrator.js +4 -5
  19. package/dist/core/PostUpdateMigrator.js.map +1 -1
  20. package/dist/core/SessionManager.d.ts +38 -0
  21. package/dist/core/SessionManager.d.ts.map +1 -1
  22. package/dist/core/SessionManager.js +157 -23
  23. package/dist/core/SessionManager.js.map +1 -1
  24. package/dist/core/UpdateChecker.d.ts.map +1 -1
  25. package/dist/core/UpdateChecker.js +3 -1
  26. package/dist/core/UpdateChecker.js.map +1 -1
  27. package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -1
  28. package/dist/core/UpgradeGuideProcessor.js +3 -1
  29. package/dist/core/UpgradeGuideProcessor.js.map +1 -1
  30. package/dist/core/types.d.ts +18 -0
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/core/types.js.map +1 -1
  33. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  34. package/dist/lifeline/ServerSupervisor.js +3 -1
  35. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  36. package/dist/memory/SemanticMemory.d.ts +9 -0
  37. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  38. package/dist/memory/SemanticMemory.js +131 -0
  39. package/dist/memory/SemanticMemory.js.map +1 -1
  40. package/dist/monitoring/TokenLedger.d.ts +39 -0
  41. package/dist/monitoring/TokenLedger.d.ts.map +1 -1
  42. package/dist/monitoring/TokenLedger.js +110 -13
  43. package/dist/monitoring/TokenLedger.js.map +1 -1
  44. package/dist/monitoring/TokenLedgerPoller.d.ts.map +1 -1
  45. package/dist/monitoring/TokenLedgerPoller.js +8 -8
  46. package/dist/monitoring/TokenLedgerPoller.js.map +1 -1
  47. package/dist/scheduler/JobRunHistory.d.ts +6 -0
  48. package/dist/scheduler/JobRunHistory.d.ts.map +1 -1
  49. package/dist/scheduler/JobRunHistory.js +11 -0
  50. package/dist/scheduler/JobRunHistory.js.map +1 -1
  51. package/dist/scheduler/JobScheduler.d.ts +23 -0
  52. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  53. package/dist/scheduler/JobScheduler.js +84 -0
  54. package/dist/scheduler/JobScheduler.js.map +1 -1
  55. package/dist/server/AgentServer.d.ts +4 -0
  56. package/dist/server/AgentServer.d.ts.map +1 -1
  57. package/dist/server/AgentServer.js +14 -1
  58. package/dist/server/AgentServer.js.map +1 -1
  59. package/dist/server/routes.d.ts +8 -1
  60. package/dist/server/routes.d.ts.map +1 -1
  61. package/dist/server/routes.js +154 -0
  62. package/dist/server/routes.js.map +1 -1
  63. package/dist/threadline/BackfillCore.d.ts +70 -0
  64. package/dist/threadline/BackfillCore.d.ts.map +1 -0
  65. package/dist/threadline/BackfillCore.js +117 -0
  66. package/dist/threadline/BackfillCore.js.map +1 -0
  67. package/dist/threadline/ListenerSessionManager.d.ts +35 -0
  68. package/dist/threadline/ListenerSessionManager.d.ts.map +1 -1
  69. package/dist/threadline/ListenerSessionManager.js +41 -0
  70. package/dist/threadline/ListenerSessionManager.js.map +1 -1
  71. package/dist/threadline/TelegramBridge.d.ts +140 -0
  72. package/dist/threadline/TelegramBridge.d.ts.map +1 -0
  73. package/dist/threadline/TelegramBridge.js +224 -0
  74. package/dist/threadline/TelegramBridge.js.map +1 -0
  75. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  76. package/dist/threadline/ThreadlineBootstrap.js +3 -2
  77. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  78. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  79. package/dist/threadline/ThreadlineMCPServer.js +5 -0
  80. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  81. package/dist/threadline/ThreadlineObservability.d.ts +95 -0
  82. package/dist/threadline/ThreadlineObservability.d.ts.map +1 -0
  83. package/dist/threadline/ThreadlineObservability.js +310 -0
  84. package/dist/threadline/ThreadlineObservability.js.map +1 -0
  85. package/dist/threadline/relay/ConnectionManager.d.ts.map +1 -1
  86. package/dist/threadline/relay/ConnectionManager.js +34 -7
  87. package/dist/threadline/relay/ConnectionManager.js.map +1 -1
  88. package/package.json +1 -1
  89. package/scripts/pre-push-gate.js +26 -0
  90. package/scripts/threadline-bridge-backfill.mjs +379 -0
  91. package/src/data/builtin-manifest.json +65 -65
  92. package/upgrades/0.28.78.md +90 -0
  93. package/upgrades/0.28.79.md +67 -0
  94. package/upgrades/side-effects/0.28.79.md +310 -0
  95. package/upgrades/side-effects/assembler-context-endpoint.md +67 -0
  96. package/upgrades/side-effects/post-update-migrator-path-fix.md +52 -0
  97. package/upgrades/side-effects/semantic-memory-corruption-recovery.md +98 -0
  98. package/upgrades/side-effects/threadline-bridge-backfill.md +203 -0
  99. package/upgrades/side-effects/threadline-observability-tab.md +206 -0
  100. package/upgrades/side-effects/threadline-tg-bridge-module.md +196 -0
  101. package/upgrades/side-effects/token-ledger-bounded-scan.md +230 -0
  102. package/upgrades/side-effects/url-pathname-path-encoding-fix.md +45 -0
@@ -52,6 +52,12 @@
52
52
  padding: 12px 20px;
53
53
  border-bottom: 1px solid var(--border);
54
54
  background: var(--bg-panel);
55
+ position: relative;
56
+ }
57
+
58
+ /* Hamburger toggle — visible only on mobile, sits in the header row */
59
+ .nav-toggle {
60
+ display: none;
55
61
  }
56
62
 
57
63
  .header-left {
@@ -2206,9 +2212,26 @@
2206
2212
  font-size: 14px;
2207
2213
  }
2208
2214
 
2215
+ /* Compact terminal action row — only essential keys inline,
2216
+ rest collapse into an overflow scroll strip */
2209
2217
  .terminal-actions {
2210
2218
  gap: 4px;
2211
- flex-wrap: wrap;
2219
+ flex-wrap: nowrap;
2220
+ overflow-x: auto;
2221
+ -webkit-overflow-scrolling: touch;
2222
+ scrollbar-width: none;
2223
+ }
2224
+ .terminal-actions::-webkit-scrollbar { display: none; }
2225
+
2226
+ .terminal-actions .action-btn {
2227
+ flex-shrink: 0;
2228
+ min-height: 36px; /* slightly smaller than 44 since these are arrow keys */
2229
+ }
2230
+
2231
+ /* The arrow-key cluster compresses tighter to leave room */
2232
+ .terminal-actions .action-btn.action-arrow {
2233
+ padding: 6px 10px;
2234
+ min-width: 36px;
2212
2235
  }
2213
2236
 
2214
2237
  .terminal-header {
@@ -2285,14 +2308,62 @@
2285
2308
  padding: 12px 14px;
2286
2309
  }
2287
2310
 
2288
- /* Tab bar compact */
2289
- .tab-bar {
2311
+ /* Hamburger toggle visible in header row on mobile */
2312
+ .nav-toggle {
2313
+ display: flex;
2314
+ align-items: center;
2315
+ gap: 8px;
2290
2316
  margin-left: 12px;
2317
+ padding: 8px 12px;
2318
+ min-height: 40px;
2319
+ background: var(--bg);
2320
+ border: 1px solid var(--border);
2321
+ border-radius: 6px;
2322
+ color: var(--text);
2323
+ font-size: 14px;
2324
+ font-weight: 500;
2325
+ cursor: pointer;
2326
+ }
2327
+ .nav-toggle:hover { background: var(--bg-hover); }
2328
+ .nav-toggle[aria-expanded="true"] { background: var(--bg-active); border-color: var(--accent); }
2329
+
2330
+ /* On mobile the tab-bar becomes a vertical dropdown panel,
2331
+ hidden by default and toggled via the .nav-open class on .app */
2332
+ .tab-bar {
2333
+ position: absolute;
2334
+ top: 100%; /* below the header row */
2335
+ left: 0;
2336
+ right: 0;
2337
+ flex-direction: column;
2338
+ margin-left: 0;
2339
+ padding: 8px;
2340
+ gap: 4px;
2341
+ background: var(--bg-panel);
2342
+ border-bottom: 1px solid var(--border);
2343
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
2344
+ z-index: 100;
2345
+ overflow-x: visible;
2346
+ max-height: calc(100vh - 60px);
2347
+ overflow-y: auto;
2348
+ display: none;
2349
+ }
2350
+ .app.nav-open .tab-bar {
2351
+ display: flex;
2291
2352
  }
2292
2353
 
2293
2354
  .tab-bar .tab {
2294
- padding: 5px 12px;
2295
- font-size: 12px;
2355
+ width: 100%;
2356
+ text-align: left;
2357
+ border-radius: 6px;
2358
+ padding: 12px 14px;
2359
+ font-size: 15px;
2360
+ min-height: 44px;
2361
+ border-bottom: 1px solid var(--border);
2362
+ margin-bottom: 0;
2363
+ }
2364
+ .tab-bar .tab.active {
2365
+ background: var(--bg-active);
2366
+ border-color: var(--accent);
2296
2367
  }
2297
2368
 
2298
2369
  /* File viewer mobile */
@@ -2385,6 +2456,41 @@
2385
2456
  .jobs-detail-content {
2386
2457
  padding: 14px;
2387
2458
  }
2459
+
2460
+ /* Filter chips and sort select were below 32px — bump to comfortable tap size */
2461
+ .jobs-filter-chip {
2462
+ padding: 8px 12px;
2463
+ font-size: 12px;
2464
+ min-height: 36px;
2465
+ }
2466
+ .jobs-sort {
2467
+ font-size: 13px;
2468
+ padding: 6px 8px;
2469
+ min-height: 36px;
2470
+ }
2471
+
2472
+ /* Modal — explicit mobile rule instead of relying on max-width: 90vw fallback */
2473
+ .modal-dialog, .modal-content {
2474
+ width: calc(100vw - 24px);
2475
+ max-width: 100%;
2476
+ margin: 12px;
2477
+ }
2478
+ }
2479
+
2480
+ @media (max-width: 480px) {
2481
+ /* Even tighter — phone-only refinements */
2482
+ .header h1 { font-size: 14px; }
2483
+ .nav-toggle { padding: 6px 10px; font-size: 13px; }
2484
+
2485
+ /* Terminal action row — drop arrow keys to icon-only at very narrow widths */
2486
+ .terminal-actions .action-btn.action-arrow {
2487
+ padding: 6px 8px;
2488
+ min-width: 30px;
2489
+ }
2490
+
2491
+ /* Jobs summary cards stack tighter */
2492
+ .jobs-summary-card { padding: 4px 2px; min-width: 40px; }
2493
+ .jobs-summary-val { font-size: 14px; }
2388
2494
  }
2389
2495
  </style>
2390
2496
  </head>
@@ -2406,7 +2512,8 @@
2406
2512
  <div class="header-left">
2407
2513
  <img class="logo" src="/dashboard/logo.png" alt="Instar">
2408
2514
  <h1>Instar Dashboard</h1>
2409
- <nav class="tab-bar">
2515
+ <button class="nav-toggle" id="navToggle" aria-label="Open navigation menu" aria-expanded="false" onclick="toggleNavMenu()">&#9776; <span id="navToggleLabel">Sessions</span></button>
2516
+ <nav class="tab-bar" id="tabBar">
2410
2517
  <button class="tab active" data-tab="sessions" onclick="switchTab('sessions')">Sessions <span class="tab-count" id="tabSessionCount">0</span></button>
2411
2518
  <button class="tab" data-tab="files" onclick="switchTab('files')">Files</button>
2412
2519
  <button class="tab" data-tab="dropzone" onclick="switchTab('dropzone')">Send Content</button>
@@ -2946,9 +3053,34 @@
2946
3053
  <div id="tlBridgeError" style="color:var(--err, #c44);font-size:12px;display:none"></div>
2947
3054
  </div>
2948
3055
 
2949
- <!-- Future home of the conversation observability view (deliverable 4) -->
2950
- <div style="border:1px dashed var(--border);border-radius:8px;padding:16px;font-size:12px;color:var(--text-dim);line-height:1.5">
2951
- The conversation observability view — threads list, color-coded message stream, latency metrics, FTS search — lands here in a follow-up. The settings above ship now so the bridge stays quiet by default once it goes live.
3056
+ <!-- Conversation observability threads list + message stream + search -->
3057
+ <div style="border:1px solid var(--border);border-radius:8px;padding:0;display:flex;flex-direction:column;min-height:480px">
3058
+ <!-- Toolbar -->
3059
+ <div style="display:flex;gap:8px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border)">
3060
+ <div style="font-weight:600">Conversations</div>
3061
+ <div style="flex:1"></div>
3062
+ <input type="search" id="tlObsSearchInput" placeholder="search messages…" style="padding:6px 8px;font-size:12px;width:220px" oninput="tlObsSearchDebounced(this.value)" />
3063
+ <select id="tlObsHasTopicFilter" onchange="tlObsLoadThreads()" style="padding:6px 8px;font-size:12px">
3064
+ <option value="">all threads</option>
3065
+ <option value="yes">with Telegram topic</option>
3066
+ <option value="no">without Telegram topic</option>
3067
+ </select>
3068
+ <input type="text" id="tlObsRemoteFilter" placeholder="agent filter" style="padding:6px 8px;font-size:12px;width:140px" oninput="tlObsLoadThreadsDebounced()" />
3069
+ <button onclick="tlObsLoadThreads()" style="padding:6px 10px;font-size:12px">Refresh</button>
3070
+ </div>
3071
+
3072
+ <!-- Two-pane: threads list + active thread -->
3073
+ <div style="display:grid;grid-template-columns:280px 1fr;flex:1;min-height:420px">
3074
+ <ul id="tlObsThreadsList" style="list-style:none;padding:0;margin:0;border-right:1px solid var(--border);overflow-y:auto;max-height:520px">
3075
+ <li style="padding:16px;color:var(--text-dim);font-size:12px">Loading…</li>
3076
+ </ul>
3077
+ <div id="tlObsConversation" style="padding:16px;overflow-y:auto;max-height:520px;display:flex;flex-direction:column;gap:10px">
3078
+ <div style="color:var(--text-dim);font-size:12px;font-style:italic">Select a thread on the left to view the conversation.</div>
3079
+ </div>
3080
+ </div>
3081
+
3082
+ <!-- Search results -->
3083
+ <div id="tlObsSearchResults" style="display:none;border-top:1px solid var(--border);padding:12px 16px;max-height:280px;overflow-y:auto"></div>
2952
3084
  </div>
2953
3085
  </div>
2954
3086
 
@@ -3445,7 +3577,7 @@
3445
3577
  brightCyan: '#99f6e4',
3446
3578
  brightWhite: '#eee',
3447
3579
  },
3448
- fontSize: window.innerWidth <= 768 ? 11 : 13,
3580
+ fontSize: window.innerWidth <= 480 ? 10 : window.innerWidth <= 768 ? 11 : 13,
3449
3581
  fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace",
3450
3582
  cursorBlink: false,
3451
3583
  cursorStyle: 'underline',
@@ -3463,6 +3595,12 @@
3463
3595
  });
3464
3596
  resizeObserver.observe(container);
3465
3597
 
3598
+ // Refit on window resize / orientation change — phone rotation may not
3599
+ // trigger ResizeObserver if the parent grid keeps its size.
3600
+ window.addEventListener('resize', () => {
3601
+ try { fitAddon.fit(); } catch {}
3602
+ });
3603
+
3466
3604
  // Initial fit after a tick (container needs to be visible)
3467
3605
  requestAnimationFrame(() => {
3468
3606
  try { fitAddon.fit(); } catch {}
@@ -4008,7 +4146,10 @@
4008
4146
  id: 'threadline',
4009
4147
  panels: ['threadlineTab'],
4010
4148
  display: ['flex'],
4011
- onActivate: () => { if (typeof loadThreadlineBridgeConfig === 'function') loadThreadlineBridgeConfig(); },
4149
+ onActivate: () => {
4150
+ if (typeof loadThreadlineBridgeConfig === 'function') loadThreadlineBridgeConfig();
4151
+ if (typeof tlObsLoadThreads === 'function') tlObsLoadThreads();
4152
+ },
4012
4153
  },
4013
4154
  {
4014
4155
  id: 'secrets',
@@ -4024,6 +4165,51 @@
4024
4165
  },
4025
4166
  ];
4026
4167
 
4168
+ function toggleNavMenu() {
4169
+ const app = document.querySelector('.app');
4170
+ const toggle = document.getElementById('navToggle');
4171
+ const open = app.classList.toggle('nav-open');
4172
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
4173
+ }
4174
+
4175
+ function updateNavToggleLabel(tabName) {
4176
+ // Read the tab name from the actual button so we don't drift when
4177
+ // tabs get renamed in the registry. Strip the trailing count badge
4178
+ // ("Jobs 3" → "Jobs") by ignoring child element text.
4179
+ const labelEl = document.getElementById('navToggleLabel');
4180
+ if (!labelEl) return;
4181
+ const tabBtn = document.querySelector(`.tab-bar .tab[data-tab="${tabName}"]`);
4182
+ let text = tabName;
4183
+ if (tabBtn) {
4184
+ // Clone the button, strip count badges, then read trimmed text.
4185
+ const clone = tabBtn.cloneNode(true);
4186
+ clone.querySelectorAll('.tab-count').forEach(el => el.remove());
4187
+ text = clone.textContent.trim() || tabName;
4188
+ }
4189
+ labelEl.textContent = text;
4190
+ }
4191
+
4192
+ function closeNavMenu() {
4193
+ const app = document.querySelector('.app');
4194
+ if (!app || !app.classList.contains('nav-open')) return;
4195
+ app.classList.remove('nav-open');
4196
+ const toggle = document.getElementById('navToggle');
4197
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
4198
+ }
4199
+
4200
+ // Close the mobile menu on Escape or on tap outside the tab bar.
4201
+ document.addEventListener('keydown', (e) => {
4202
+ if (e.key === 'Escape') closeNavMenu();
4203
+ });
4204
+ document.addEventListener('click', (e) => {
4205
+ const app = document.querySelector('.app');
4206
+ if (!app || !app.classList.contains('nav-open')) return;
4207
+ const tabBar = document.querySelector('.tab-bar');
4208
+ const toggle = document.getElementById('navToggle');
4209
+ if (tabBar?.contains(e.target) || toggle?.contains(e.target)) return;
4210
+ closeNavMenu();
4211
+ });
4212
+
4027
4213
  function switchTab(tabName) {
4028
4214
  if (tabName === currentTab) return;
4029
4215
 
@@ -4038,6 +4224,11 @@
4038
4224
  btn.classList.toggle('active', btn.dataset.tab === tabName);
4039
4225
  });
4040
4226
 
4227
+ updateNavToggleLabel(tabName);
4228
+ document.querySelector('.app').classList.remove('nav-open');
4229
+ const navToggle = document.getElementById('navToggle');
4230
+ if (navToggle) navToggle.setAttribute('aria-expanded', 'false');
4231
+
4041
4232
  // Hide all panels
4042
4233
  for (const tab of TAB_REGISTRY) {
4043
4234
  for (const panelId of tab.panels) {
@@ -4303,6 +4494,158 @@
4303
4494
  wire('tlBridgeMirrorExisting', 'mirrorExisting');
4304
4495
  });
4305
4496
 
4497
+ // ── Threadline observability — threads list + conversation view ──
4498
+ let tlObsActiveThreadId = null;
4499
+ let tlObsLoadThreadsTimer = null;
4500
+ let tlObsSearchTimer = null;
4501
+
4502
+ function tlObsLoadThreadsDebounced() {
4503
+ if (tlObsLoadThreadsTimer) clearTimeout(tlObsLoadThreadsTimer);
4504
+ tlObsLoadThreadsTimer = setTimeout(tlObsLoadThreads, 300);
4505
+ }
4506
+
4507
+ function tlObsSearchDebounced(value) {
4508
+ if (tlObsSearchTimer) clearTimeout(tlObsSearchTimer);
4509
+ tlObsSearchTimer = setTimeout(() => tlObsRunSearch(value), 300);
4510
+ }
4511
+
4512
+ async function tlObsLoadThreads() {
4513
+ const list = document.getElementById('tlObsThreadsList');
4514
+ if (!list) return;
4515
+ const remoteAgent = document.getElementById('tlObsRemoteFilter')?.value?.trim() || '';
4516
+ const hasTopic = document.getElementById('tlObsHasTopicFilter')?.value || '';
4517
+ const params = new URLSearchParams();
4518
+ if (remoteAgent) params.set('remoteAgent', remoteAgent);
4519
+ if (hasTopic) params.set('hasTopic', hasTopic);
4520
+ try {
4521
+ const resp = await apiFetch('/threadline/observability/threads?' + params.toString());
4522
+ const threads = resp.threads || [];
4523
+ if (threads.length === 0) {
4524
+ list.innerHTML = '<li style="padding:16px;color:var(--text-dim);font-size:12px;font-style:italic">No threads yet.</li>';
4525
+ return;
4526
+ }
4527
+ list.innerHTML = threads.map(t => tlObsRenderThreadRow(t)).join('');
4528
+ } catch (err) {
4529
+ list.innerHTML = `<li style="padding:16px;color:var(--red)">Error: ${escapeHtml(err.message || String(err))}</li>`;
4530
+ }
4531
+ }
4532
+
4533
+ function tlObsRenderThreadRow(t) {
4534
+ const lastSeen = t.lastSeen ? fmtRelTime(t.lastSeen) : '—';
4535
+ const bridgeBadge = t.bridge
4536
+ ? '<span style="display:inline-block;padding:1px 6px;border-radius:8px;background:#3b82f6;color:#fff;font-size:10px;margin-left:6px">TG</span>'
4537
+ : '';
4538
+ const spawnBadge = t.hasSpawnedSession
4539
+ ? '<span style="display:inline-block;padding:1px 6px;border-radius:8px;background:#16a34a;color:#fff;font-size:10px;margin-left:4px">S</span>'
4540
+ : '';
4541
+ const isActive = tlObsActiveThreadId === t.threadId ? 'background:rgba(59,130,246,0.08);' : '';
4542
+ return `
4543
+ <li onclick="tlObsLoadThread('${t.threadId.replace(/'/g, "\\'")}')"
4544
+ style="padding:10px 12px;border-bottom:1px solid var(--border);cursor:pointer;${isActive}">
4545
+ <div style="display:flex;justify-content:space-between;align-items:center">
4546
+ <div style="font-weight:600;font-size:13px">${escapeHtml(t.remoteAgentName)}${bridgeBadge}${spawnBadge}</div>
4547
+ <div style="font-size:11px;color:var(--text-dim)">${escapeHtml(lastSeen)}</div>
4548
+ </div>
4549
+ <div style="font-size:11px;color:var(--text-dim);margin-top:2px;font-family:monospace">
4550
+ ${escapeHtml(t.threadId.slice(0, 16))}…
4551
+ </div>
4552
+ <div style="font-size:11px;color:var(--text-dim);margin-top:2px">
4553
+ ${t.messageCount} msgs · ${t.inboundCount} in / ${t.outboundCount} out
4554
+ ${typeof t.avgResponseLatencyMs === 'number' ? ' · ~' + Math.round(t.avgResponseLatencyMs / 1000) + 's reply' : ''}
4555
+ </div>
4556
+ </li>
4557
+ `;
4558
+ }
4559
+
4560
+ async function tlObsLoadThread(threadId) {
4561
+ tlObsActiveThreadId = threadId;
4562
+ tlObsLoadThreads(); // Re-render threads list to highlight active row
4563
+ const conv = document.getElementById('tlObsConversation');
4564
+ if (!conv) return;
4565
+ conv.innerHTML = '<div style="color:var(--text-dim);font-size:12px">Loading…</div>';
4566
+ try {
4567
+ const detail = await apiFetch(`/threadline/observability/threads/${encodeURIComponent(threadId)}`);
4568
+ conv.innerHTML = tlObsRenderConversation(detail);
4569
+ } catch (err) {
4570
+ conv.innerHTML = `<div style="color:var(--red);font-size:12px">Error: ${escapeHtml(err.message || String(err))}</div>`;
4571
+ }
4572
+ }
4573
+
4574
+ function tlObsRenderConversation(detail) {
4575
+ const header = `
4576
+ <div style="border-bottom:1px solid var(--border);padding-bottom:10px;margin-bottom:10px">
4577
+ <div style="font-weight:600;font-size:14px">${escapeHtml(detail.remoteAgentName)}</div>
4578
+ <div style="font-family:monospace;font-size:11px;color:var(--text-dim)">${escapeHtml(detail.threadId)}</div>
4579
+ <div style="display:flex;gap:14px;font-size:11px;color:var(--text-dim);margin-top:6px;flex-wrap:wrap">
4580
+ <span>${detail.messageCount} messages</span>
4581
+ <span>${detail.inboundCount} in / ${detail.outboundCount} out</span>
4582
+ <span>first: ${escapeHtml(fmtRelTime(detail.firstSeen))}</span>
4583
+ <span>last: ${escapeHtml(fmtRelTime(detail.lastSeen))}</span>
4584
+ ${typeof detail.avgResponseLatencyMs === 'number' ? '<span>avg reply: ' + Math.round(detail.avgResponseLatencyMs/1000) + 's</span>' : ''}
4585
+ ${detail.bridge ? '<span>📨 topic ' + detail.bridge.topicId + '</span>' : '<span style="color:var(--text-dim);font-style:italic">no Telegram topic</span>'}
4586
+ ${detail.hasSpawnedSession ? '<span>🧵 spawn-session</span>' : ''}
4587
+ </div>
4588
+ </div>
4589
+ `;
4590
+ const messages = (detail.messages || []).map(m => tlObsRenderMessage(m)).join('');
4591
+ return header + messages;
4592
+ }
4593
+
4594
+ function tlObsRenderMessage(m) {
4595
+ const isIn = m.direction === 'in';
4596
+ const bg = isIn ? 'rgba(99,102,241,0.10)' : 'rgba(34,197,94,0.10)';
4597
+ const border = isIn ? '#6366f1' : '#22c55e';
4598
+ const arrow = isIn ? '←' : '→';
4599
+ const meta = `${escapeHtml(m.timestamp)} · ${escapeHtml(m.id.slice(0, 8))}${m.outcome ? ' · ' + escapeHtml(m.outcome) : ''} · trust:${escapeHtml(m.trustLevel)}`;
4600
+ return `
4601
+ <div style="border-left:3px solid ${border};background:${bg};padding:8px 10px;border-radius:4px">
4602
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">
4603
+ <div style="font-weight:600;font-size:12px">
4604
+ ${arrow} ${escapeHtml(m.remoteAgentName)}
4605
+ </div>
4606
+ <div style="font-size:11px;color:var(--text-dim)">${escapeHtml(fmtRelTime(m.timestamp))}</div>
4607
+ </div>
4608
+ <div style="font-size:13px;white-space:pre-wrap;word-break:break-word;line-height:1.4">${escapeHtml(m.text)}</div>
4609
+ <div style="font-family:monospace;font-size:10px;color:var(--text-dim);margin-top:4px">${meta}</div>
4610
+ </div>
4611
+ `;
4612
+ }
4613
+
4614
+ async function tlObsRunSearch(query) {
4615
+ const panel = document.getElementById('tlObsSearchResults');
4616
+ if (!panel) return;
4617
+ const q = (query || '').trim();
4618
+ if (!q) { panel.style.display = 'none'; return; }
4619
+ panel.style.display = 'block';
4620
+ panel.innerHTML = '<div style="color:var(--text-dim);font-size:12px">Searching…</div>';
4621
+ try {
4622
+ const resp = await apiFetch('/threadline/observability/search?q=' + encodeURIComponent(q) + '&limit=30');
4623
+ const hits = resp.hits || [];
4624
+ if (hits.length === 0) {
4625
+ panel.innerHTML = '<div style="color:var(--text-dim);font-size:12px">No matches.</div>';
4626
+ return;
4627
+ }
4628
+ panel.innerHTML = `<div style="font-size:12px;color:var(--text-dim);margin-bottom:8px">${hits.length} match${hits.length === 1 ? '' : 'es'} (click to open thread)</div>` +
4629
+ hits.map(h => `
4630
+ <div onclick="tlObsLoadThread('${h.message.threadId.replace(/'/g, "\\'")}')"
4631
+ style="padding:6px 8px;border-bottom:1px solid var(--border);cursor:pointer;font-size:12px">
4632
+ <div style="display:flex;justify-content:space-between">
4633
+ <span style="font-weight:600">${h.message.direction === 'in' ? '←' : '→'} ${escapeHtml(h.message.remoteAgentName)}</span>
4634
+ <span style="font-size:11px;color:var(--text-dim)">${escapeHtml(fmtRelTime(h.message.timestamp))}</span>
4635
+ </div>
4636
+ <div style="margin-top:2px;line-height:1.4">${tlObsRenderSnippet(h.snippet)}</div>
4637
+ </div>
4638
+ `).join('');
4639
+ } catch (err) {
4640
+ panel.innerHTML = `<div style="color:var(--red);font-size:12px">Error: ${escapeHtml(err.message || String(err))}</div>`;
4641
+ }
4642
+ }
4643
+
4644
+ function tlObsRenderSnippet(s) {
4645
+ // Server returns matches wrapped in «...» — render as <mark>
4646
+ return escapeHtml(s).replace(/«/g, '<mark style="background:#fef08a;color:#000">').replace(/»/g, '</mark>');
4647
+ }
4648
+
4306
4649
  async function loadFileTree() {
4307
4650
  fileTreeLoaded = true;
4308
4651
  const list = document.getElementById('fileTreeList');
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAgDH,UAAU,WAAW;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE;AAkzCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAkqC1E;AA0mBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAuBlF"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAmDH,UAAU,WAAW;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE;AAkzCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAkqC1E;AA0mBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAuBlF"}
@@ -29,6 +29,8 @@
29
29
  import fs from 'node:fs';
30
30
  import os from 'node:os';
31
31
  import path from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
34
  import pc from 'picocolors';
33
35
  import { randomUUID } from 'node:crypto';
34
36
  import { execFileSync, execSync } from 'node:child_process';
@@ -2534,7 +2536,7 @@ function installBuildSkill(skillsDir) {
2534
2536
  if (fs.existsSync(skillFile) || fs.existsSync(path.join(buildDir, 'skill.md')))
2535
2537
  return;
2536
2538
  // Try to copy from bundled .claude/skills/build/ first
2537
- const modDir = path.dirname(new URL(import.meta.url).pathname);
2539
+ const modDir = __dirname;
2538
2540
  const bundledSkill = path.join(modDir, '..', '..', '.claude', 'skills', 'build', 'SKILL.md');
2539
2541
  if (fs.existsSync(bundledSkill)) {
2540
2542
  fs.mkdirSync(buildDir, { recursive: true });
@@ -2573,7 +2575,7 @@ function installAutonomousSkill(skillsDir) {
2573
2575
  const hooksDir = path.join(autonomousDir, 'hooks');
2574
2576
  const scriptsDir = path.join(autonomousDir, 'scripts');
2575
2577
  // Copy from instar's bundled skill files if they exist
2576
- const modDir = path.dirname(new URL(import.meta.url).pathname);
2578
+ const modDir = __dirname;
2577
2579
  const bundledDir = path.join(path.dirname(path.dirname(modDir)), '.claude', 'skills', 'autonomous');
2578
2580
  if (fs.existsSync(bundledDir)) {
2579
2581
  // Copy from bundled source
@@ -3304,7 +3306,7 @@ function refreshScripts(projectDir, stateDir) {
3304
3306
  * PostUpdateMigrator.getTelegramReplyScript() uses the same loading pattern.
3305
3307
  */
3306
3308
  function loadRelayTemplate(filename, port) {
3307
- const modDir = path.dirname(new URL(import.meta.url).pathname);
3309
+ const modDir = __dirname;
3308
3310
  const candidates = [
3309
3311
  // dev: src/commands → ../templates/scripts
3310
3312
  path.resolve(modDir, '..', 'templates', 'scripts', filename),
@@ -4010,7 +4012,7 @@ function installSerendipityCapture(projectDir) {
4010
4012
  // Resolve template from package directory
4011
4013
  // In dev: src/commands/ → ../../src/templates/scripts/serendipity-capture.sh
4012
4014
  // In dist: dist/commands/ → ../templates/scripts/serendipity-capture.sh
4013
- const modDir = path.dirname(new URL(import.meta.url).pathname);
4015
+ const modDir = __dirname;
4014
4016
  const candidates = [
4015
4017
  path.resolve(modDir, '..', 'templates', 'scripts', 'serendipity-capture.sh'),
4016
4018
  path.resolve(modDir, '..', '..', 'src', 'templates', 'scripts', 'serendipity-capture.sh'),