@runcore-sh/runcore 0.5.6 → 0.5.7

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 (174) hide show
  1. package/dictionary.json +2 -2
  2. package/dist/.extensions/ext-byok.json +1 -0
  3. package/dist/.extensions/ext-hosted.json +1 -0
  4. package/dist/.extensions/ext-spawn.json +1 -0
  5. package/dist/agents/autonomous.d.ts.map +1 -1
  6. package/dist/agents/runtime/bus.d.ts +1 -0
  7. package/dist/agents/runtime/bus.d.ts.map +1 -1
  8. package/dist/agents/spawn.d.ts.map +1 -1
  9. package/dist/auth/middleware.d.ts.map +1 -1
  10. package/dist/auth/middleware.js +3 -2
  11. package/dist/auth/middleware.js.map +1 -1
  12. package/dist/calendar/routes.d.ts.map +1 -1
  13. package/dist/calendar/routes.js +8 -7
  14. package/dist/calendar/routes.js.map +1 -1
  15. package/dist/files/registry.d.ts +4 -5
  16. package/dist/files/registry.d.ts.map +1 -1
  17. package/dist/files/registry.js +4 -5
  18. package/dist/files/registry.js.map +1 -1
  19. package/dist/files/store.d.ts +2 -0
  20. package/dist/files/store.d.ts.map +1 -1
  21. package/dist/files/store.js +5 -1
  22. package/dist/files/store.js.map +1 -1
  23. package/dist/instance.d.ts +2 -0
  24. package/dist/instance.d.ts.map +1 -1
  25. package/dist/instance.js +2 -0
  26. package/dist/instance.js.map +1 -1
  27. package/dist/lib/paths.d.ts.map +1 -1
  28. package/dist/lib/paths.js +15 -1
  29. package/dist/lib/paths.js.map +1 -1
  30. package/dist/library/brain-shadow.d.ts.map +1 -1
  31. package/dist/library/brain-shadow.js +5 -4
  32. package/dist/library/brain-shadow.js.map +1 -1
  33. package/dist/library/routes.d.ts.map +1 -1
  34. package/dist/library/routes.js +10 -9
  35. package/dist/library/routes.js.map +1 -1
  36. package/dist/llm/cache.d.ts.map +1 -1
  37. package/dist/llm/cache.js +7 -5
  38. package/dist/llm/cache.js.map +1 -1
  39. package/dist/llm/complete.js +2 -0
  40. package/dist/llm/complete.js.map +1 -1
  41. package/dist/llm/providers/ollama.d.ts.map +1 -1
  42. package/dist/llm/providers/ollama.js +32 -7
  43. package/dist/llm/providers/ollama.js.map +1 -1
  44. package/dist/llm/providers/openrouter.d.ts.map +1 -1
  45. package/dist/llm/providers/openrouter.js +105 -12
  46. package/dist/llm/providers/openrouter.js.map +1 -1
  47. package/dist/llm/providers/types.d.ts +18 -0
  48. package/dist/llm/providers/types.d.ts.map +1 -1
  49. package/dist/llm/retry.d.ts.map +1 -1
  50. package/dist/llm/retry.js +6 -0
  51. package/dist/llm/retry.js.map +1 -1
  52. package/dist/llm/tools/handlers.d.ts +27 -0
  53. package/dist/llm/tools/handlers.d.ts.map +1 -0
  54. package/dist/llm/tools/handlers.js +842 -0
  55. package/dist/llm/tools/handlers.js.map +1 -0
  56. package/dist/llm/tools/index.d.ts +12 -0
  57. package/dist/llm/tools/index.d.ts.map +1 -0
  58. package/dist/llm/tools/index.js +10 -0
  59. package/dist/llm/tools/index.js.map +1 -0
  60. package/dist/llm/tools/loop.d.ts +47 -0
  61. package/dist/llm/tools/loop.d.ts.map +1 -0
  62. package/dist/llm/tools/loop.js +126 -0
  63. package/dist/llm/tools/loop.js.map +1 -0
  64. package/dist/llm/tools/registry.d.ts +27 -0
  65. package/dist/llm/tools/registry.d.ts.map +1 -0
  66. package/dist/llm/tools/registry.js +60 -0
  67. package/dist/llm/tools/registry.js.map +1 -0
  68. package/dist/llm/tools/schemas.d.ts +92 -0
  69. package/dist/llm/tools/schemas.d.ts.map +1 -0
  70. package/dist/llm/tools/schemas.js +154 -0
  71. package/dist/llm/tools/schemas.js.map +1 -0
  72. package/dist/llm/tools/types.d.ts +44 -0
  73. package/dist/llm/tools/types.d.ts.map +1 -0
  74. package/dist/llm/tools/types.js +9 -0
  75. package/dist/llm/tools/types.js.map +1 -0
  76. package/dist/mcp-server.d.ts +1 -1
  77. package/dist/mcp-server.js +249 -5
  78. package/dist/mcp-server.js.map +1 -1
  79. package/dist/memory/visual.d.ts.map +1 -1
  80. package/dist/memory/visual.js +3 -6
  81. package/dist/memory/visual.js.map +1 -1
  82. package/dist/openloop/foldback.js +1 -1
  83. package/dist/openloop/foldback.js.map +1 -1
  84. package/dist/openloop/resolution-scanner.d.ts.map +1 -1
  85. package/dist/openloop/resolution-scanner.js +76 -63
  86. package/dist/openloop/resolution-scanner.js.map +1 -1
  87. package/dist/plugins/github/index.d.ts +49 -0
  88. package/dist/plugins/github/index.d.ts.map +1 -0
  89. package/dist/plugins/github/index.js +153 -0
  90. package/dist/plugins/github/index.js.map +1 -0
  91. package/dist/plugins/index.d.ts +1 -2
  92. package/dist/plugins/index.d.ts.map +1 -1
  93. package/dist/plugins/index.js +79 -2
  94. package/dist/plugins/index.js.map +1 -1
  95. package/dist/plugins/slack/index.d.ts +43 -0
  96. package/dist/plugins/slack/index.d.ts.map +1 -0
  97. package/dist/plugins/slack/index.js +158 -0
  98. package/dist/plugins/slack/index.js.map +1 -0
  99. package/dist/plugins/twilio/index.d.ts +41 -0
  100. package/dist/plugins/twilio/index.d.ts.map +1 -0
  101. package/dist/plugins/twilio/index.js +102 -0
  102. package/dist/plugins/twilio/index.js.map +1 -0
  103. package/dist/pulse/tier.d.ts +1 -1
  104. package/dist/pulse/tier.js +2 -2
  105. package/dist/pulse/tier.js.map +1 -1
  106. package/dist/search/gemini.d.ts +27 -0
  107. package/dist/search/gemini.d.ts.map +1 -0
  108. package/dist/search/gemini.js +103 -0
  109. package/dist/search/gemini.js.map +1 -0
  110. package/dist/server.d.ts.map +1 -1
  111. package/dist/server.js +850 -536
  112. package/dist/server.js.map +1 -1
  113. package/dist/services/routine-patterns.d.ts.map +1 -1
  114. package/dist/services/routine-patterns.js +6 -0
  115. package/dist/services/routine-patterns.js.map +1 -1
  116. package/dist/services/traceInsights.d.ts +5 -0
  117. package/dist/services/traceInsights.d.ts.map +1 -1
  118. package/dist/services/traceInsights.js +18 -1
  119. package/dist/services/traceInsights.js.map +1 -1
  120. package/dist/settings.d.ts +1 -1
  121. package/dist/settings.d.ts.map +1 -1
  122. package/dist/types.d.ts +26 -2
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/vault/store.d.ts +1 -1
  125. package/dist/vault/store.d.ts.map +1 -1
  126. package/dist/webhooks/mount.d.ts.map +1 -1
  127. package/dist/whiteboard/store.d.ts +40 -0
  128. package/dist/whiteboard/store.d.ts.map +1 -0
  129. package/dist/whiteboard/store.js +280 -0
  130. package/dist/whiteboard/store.js.map +1 -0
  131. package/dist/whiteboard/types.d.ts +55 -0
  132. package/dist/whiteboard/types.d.ts.map +1 -0
  133. package/dist/whiteboard/types.js +9 -0
  134. package/dist/whiteboard/types.js.map +1 -0
  135. package/dist/whiteboard/weight.d.ts +23 -0
  136. package/dist/whiteboard/weight.d.ts.map +1 -0
  137. package/dist/whiteboard/weight.js +126 -0
  138. package/dist/whiteboard/weight.js.map +1 -0
  139. package/package.json +2 -2
  140. package/public/avatar/cache/0cdaf9c41eff4347.mp4 +0 -0
  141. package/public/avatar/cache/3dacc4ea1082ae36.mp4 +0 -0
  142. package/public/avatar/cache/44f5db0bfdde93c6.mp4 +0 -0
  143. package/public/avatar/cache/5628fd10fe55e529.mp4 +0 -0
  144. package/public/avatar/cache/7ee2ab1577690c8a.mp4 +0 -0
  145. package/public/avatar/cache/8c470929e814b6b0.mp4 +0 -0
  146. package/public/avatar/cache/8c908421ce52bf91.mp4 +0 -0
  147. package/public/avatar/cache/9532f8782a42a89c.mp4 +0 -0
  148. package/public/avatar/cache/9ce0dddd0cc4d7a1.mp4 +0 -0
  149. package/public/avatar/cache/a6508e00b6711143.mp4 +0 -0
  150. package/public/avatar/cache/ba61810a8915e0c7.mp4 +0 -0
  151. package/public/avatar/cache/c07bee3a10c917cf.mp4 +0 -0
  152. package/public/avatar/cache/d69175900ea4ea2a.mp4 +0 -0
  153. package/public/avatar/cache/e61039bc8d39cb93.mp4 +0 -0
  154. package/public/avatar/cache/e61c6b7047e2cbdb.mp4 +0 -0
  155. package/public/avatar/cache/efd93c9b18930cf6.mp4 +0 -0
  156. package/public/avatar/cache/f052d74f5c4abab7.mp4 +0 -0
  157. package/public/avatar/cache/f7c0be3429a4ef97.mp4 +0 -0
  158. package/public/avatar/cache/fc8e480f63fe4e35.mp4 +0 -0
  159. package/public/index.html +225 -51
  160. package/public/search-flyout.js +324 -0
  161. package/public/whiteboard.html +915 -0
  162. package/public/avatar/cache/06fa55aececcc478.mp4 +0 -0
  163. package/public/avatar/cache/07a65738ba170827.mp4 +0 -0
  164. package/public/avatar/cache/1185fd491f413406.mp4 +0 -0
  165. package/public/avatar/cache/272c004a41087de5.mp4 +0 -0
  166. package/public/avatar/cache/332384e088ca214b.mp4 +0 -0
  167. package/public/avatar/cache/5d9a960bbf71732c.mp4 +0 -0
  168. package/public/avatar/cache/5e0954401e15af89.mp4 +0 -0
  169. package/public/avatar/cache/b35f7a3d558f22cb.mp4 +0 -0
  170. package/public/avatar/cache/be89f49970672374.mp4 +0 -0
  171. package/public/avatar/cache/c900811e3382ac6d.mp4 +0 -0
  172. package/public/avatar/cache/d42a73667acf5716.mp4 +0 -0
  173. package/public/avatar/cache/e539f247a8908603.mp4 +0 -0
  174. package/public/avatar/cache/ec95af57d33b3f07.mp4 +0 -0
package/public/index.html CHANGED
@@ -1646,6 +1646,33 @@
1646
1646
 
1647
1647
  /* Agent card wrapper — sits in chat flow like a message */
1648
1648
  /* Agent block — inline in assistant message, Claude Code style */
1649
+ /* --- Tool call indicators --- */
1650
+ .tool-indicator {
1651
+ display: inline-flex;
1652
+ align-items: center;
1653
+ gap: 6px;
1654
+ padding: 4px 10px;
1655
+ margin: 4px 0;
1656
+ font-size: 12px;
1657
+ color: var(--text-dim);
1658
+ background: rgba(109,93,252,0.08);
1659
+ border-radius: 6px;
1660
+ font-family: var(--mono);
1661
+ }
1662
+ .tool-indicator strong { color: var(--accent); font-weight: 500; }
1663
+ .tool-indicator.tool-done { color: var(--green); background: rgba(34,197,94,0.08); }
1664
+ .tool-indicator.tool-error { color: #e57373; background: rgba(229,115,115,0.08); }
1665
+ .tool-spinner {
1666
+ display: inline-block;
1667
+ width: 12px;
1668
+ height: 12px;
1669
+ border: 2px solid rgba(109,93,252,0.3);
1670
+ border-top-color: var(--accent);
1671
+ border-radius: 50%;
1672
+ animation: tool-spin 0.6s linear infinite;
1673
+ }
1674
+ @keyframes tool-spin { to { transform: rotate(360deg); } }
1675
+
1649
1676
  .agent-block {
1650
1677
  margin: 8px 0;
1651
1678
  border: 1px solid rgba(168,85,247,0.15);
@@ -2675,10 +2702,12 @@
2675
2702
  <a href="/" style="color:inherit;text-decoration:none;font-weight:600;" id="agent-name-header">Core</a>
2676
2703
  <nav class="header-nav" id="header-nav">
2677
2704
  <a href="/" class="active">Chat</a>
2705
+ <a href="/whiteboard">Whiteboard</a>
2678
2706
  </nav>
2679
2707
  <button class="thread-toggle-btn" id="thread-toggle-btn" title="Toggle chats">&#9776;</button>
2680
2708
  <span class="thread-current-label" id="thread-current-label" title="Click to toggle chats"></span>
2681
2709
  <span class="spacer"></span>
2710
+ <button class="gear-btn" id="search-flyout-btn" onclick="openSearchFlyout()" title="Search (Ctrl+K)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>
2682
2711
  <button class="gear-btn" id="mobile-pair-btn" onclick="openMobileModal()" title="Go Mobile"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg></button>
2683
2712
  <button class="gear-btn" id="share-btn" onclick="nativeShare()" title="Share"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg></button>
2684
2713
  <button class="gear-btn" id="theme-toggle-btn" title="Toggle theme"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button>
@@ -2923,7 +2952,7 @@
2923
2952
  </div>
2924
2953
  <!-- Model selection (always visible — local tier needs this) -->
2925
2954
  <div class="settings-section" id="llm-settings-section">
2926
- <h3 class="settings-section-title">Model <a href="https://ollama.com/library" target="_blank" rel="noopener" style="font-size:11px;font-weight:400;color:var(--accent);text-decoration:none;margin-left:6px;">Browse models &rarr;</a></h3>
2955
+ <h3 class="settings-section-title">Model <a href="https://ollama.com/library" target="_blank" rel="noopener" style="font-size:11px;font-weight:400;color:var(--accent);text-decoration:none;margin-left:6px;">Ollama &rarr;</a> <a href="https://openrouter.ai/models" target="_blank" rel="noopener" style="font-size:11px;font-weight:400;color:var(--accent);text-decoration:none;margin-left:6px;">OpenRouter &rarr;</a></h3>
2927
2956
  <div class="model-field">
2928
2957
  <label>Chat model</label>
2929
2958
  <select id="model-chat" style="flex:1;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px;font-family:inherit;">
@@ -3274,12 +3303,6 @@
3274
3303
 
3275
3304
  function renderThreadList() {
3276
3305
  threadListEl.innerHTML = "";
3277
- // Add "Main chat" as the default option
3278
- const mainItem = document.createElement("div");
3279
- mainItem.className = "thread-item" + (currentThreadId === null ? " active" : "");
3280
- mainItem.innerHTML = '<div class="thread-item-title">Main</div><div class="thread-item-meta">Always here</div>';
3281
- mainItem.addEventListener("click", () => switchThread(null));
3282
- threadListEl.appendChild(mainItem);
3283
3306
 
3284
3307
  for (const t of threads) {
3285
3308
  const item = document.createElement("div");
@@ -3301,7 +3324,7 @@
3301
3324
  await api("/api/threads/" + encodeURIComponent(tid) + "?sessionId=" + encodeURIComponent(sessionId), {
3302
3325
  method: "DELETE",
3303
3326
  });
3304
- if (currentThreadId === tid) await switchThread(null);
3327
+ if (currentThreadId === tid) await createAndSwitchNewThread();
3305
3328
  loadThreads();
3306
3329
  } catch (err) { console.log("Delete thread failed:", err.message); }
3307
3330
  });
@@ -3346,7 +3369,7 @@
3346
3369
  updateThreadLabel();
3347
3370
 
3348
3371
  if (threadId) {
3349
- // Load thread-specific history
3372
+ // Load thread history
3350
3373
  try {
3351
3374
  const data = await api("/api/threads/" + encodeURIComponent(threadId) + "/history?sessionId=" + encodeURIComponent(sessionId));
3352
3375
  if (data.messages) {
@@ -3355,29 +3378,25 @@
3355
3378
  if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
3356
3379
  content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
3357
3380
  }
3358
- addMessage(msg.role === "user" ? "user" : "assistant", content);
3359
- }
3360
- }
3361
- } catch (err) {
3362
- console.log("Failed to load thread history:", err.message);
3363
- }
3364
- } else {
3365
- // Load main session history
3366
- try {
3367
- const data = await api("/api/history?sessionId=" + encodeURIComponent(sessionId));
3368
- if (data.messages) {
3369
- for (const msg of data.messages) {
3370
- let content = msg.content;
3371
- if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
3372
- content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
3381
+ var el = addMessage(msg.role === "user" ? "user" : "assistant", content);
3382
+ if (msg.toolsUsed && msg.toolsUsed.length > 0 && el) {
3383
+ var th = msg.toolsUsed.map(function(t) {
3384
+ var c2 = t.isError ? "tool-indicator tool-error" : "tool-indicator tool-done";
3385
+ return '<div class="' + c2 + '">' + (t.isError ? "&#10007; " : "&#10003; ") + '<strong>' + t.name.replace(/</g,"&lt;") + '</strong></div>';
3386
+ }).join("");
3387
+ var td = document.createElement("div");
3388
+ td.innerHTML = th;
3389
+ var cd = el.querySelector(".content");
3390
+ if (cd && cd.parentElement) cd.parentElement.insertBefore(td, cd);
3373
3391
  }
3374
- addMessage(msg.role === "user" ? "user" : "assistant", content);
3392
+ renderPersistedAgents(el, msg.agentsUsed);
3375
3393
  }
3376
3394
  }
3377
3395
  } catch (err) {
3378
- console.log("Failed to load history:", err.message);
3396
+ console.log("Failed to load thread history:", err.message);
3379
3397
  }
3380
3398
  }
3399
+ // If no threadId, just show empty — user will get a thread on first message
3381
3400
  }
3382
3401
 
3383
3402
  function updateThreadLabel() {
@@ -3385,11 +3404,11 @@
3385
3404
  const t = threads.find(t => t.id === currentThreadId);
3386
3405
  threadCurrentLabel.textContent = t ? t.title : "Chat";
3387
3406
  } else {
3388
- threadCurrentLabel.textContent = "";
3407
+ threadCurrentLabel.textContent = "New chat";
3389
3408
  }
3390
3409
  }
3391
3410
 
3392
- threadNewBtn.addEventListener("click", async () => {
3411
+ async function createAndSwitchNewThread() {
3393
3412
  try {
3394
3413
  const data = await fetch("/api/threads", {
3395
3414
  method: "POST",
@@ -3400,15 +3419,19 @@
3400
3419
  if (data.thread) {
3401
3420
  await loadThreads();
3402
3421
  await switchThread(data.thread.id);
3422
+ return data.thread.id;
3403
3423
  }
3404
3424
  } catch (err) {
3405
3425
  console.error("Failed to create thread:", err.message);
3406
3426
  }
3407
- });
3427
+ return null;
3428
+ }
3429
+
3430
+ threadNewBtn.addEventListener("click", () => createAndSwitchNewThread());
3408
3431
 
3409
3432
  // --- Auto-title chats after first exchange ---
3410
3433
  var chatAutoTitled = {}; // threadId → true if already titled
3411
- function autoTitleChat(threadId) {
3434
+ async function autoTitleChat(threadId) {
3412
3435
  if (!threadId || chatAutoTitled[threadId]) return;
3413
3436
  // Skip if already has a real title
3414
3437
  var existing = threads.find(function(x) { return x.id === threadId; });
@@ -3443,18 +3466,19 @@
3443
3466
 
3444
3467
  chatAutoTitled[threadId] = true;
3445
3468
 
3446
- // Update server
3447
- fetch("/api/threads/" + encodeURIComponent(threadId), {
3448
- method: "PATCH",
3449
- headers: { "Content-Type": "application/json", ...authHeaders() },
3450
- body: JSON.stringify({ sessionId: sessionId, title: title }),
3451
- }).then(function() {
3469
+ // Update server — await so loadThreads() doesn't race
3470
+ try {
3471
+ await fetch("/api/threads/" + encodeURIComponent(threadId), {
3472
+ method: "PATCH",
3473
+ headers: { "Content-Type": "application/json", ...authHeaders() },
3474
+ body: JSON.stringify({ sessionId: sessionId, title: title }),
3475
+ });
3452
3476
  // Update local state and sidebar
3453
3477
  var t = threads.find(function(x) { return x.id === threadId; });
3454
3478
  if (t) t.title = title;
3455
3479
  renderThreadList();
3456
3480
  updateThreadLabel();
3457
- }).catch(function() {});
3481
+ } catch (e) { /* ignore title failures */ }
3458
3482
  }
3459
3483
 
3460
3484
  // --- Attached files ---
@@ -4169,7 +4193,18 @@
4169
4193
  if (content.includes("[AGENT_REQUEST]")) {
4170
4194
  content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
4171
4195
  }
4172
- addMessage(msg.role === "user" ? "user" : "assistant", content);
4196
+ var hEl = addMessage(msg.role === "user" ? "user" : "assistant", content);
4197
+ if (msg.toolsUsed && msg.toolsUsed.length > 0 && hEl) {
4198
+ var htHtml = msg.toolsUsed.map(function(t) {
4199
+ var hcls = t.isError ? "tool-indicator tool-error" : "tool-indicator tool-done";
4200
+ return '<div class="' + hcls + '">' + (t.isError ? "&#10007; " : "&#10003; ") + '<strong>' + t.name.replace(/</g,"&lt;") + '</strong></div>';
4201
+ }).join("");
4202
+ var htDiv = document.createElement("div");
4203
+ htDiv.innerHTML = htHtml;
4204
+ var hcDiv = hEl.querySelector(".content");
4205
+ if (hcDiv && hcDiv.parentElement) hcDiv.parentElement.insertBefore(htDiv, hcDiv);
4206
+ }
4207
+ renderPersistedAgents(hEl, msg.agentsUsed);
4173
4208
  }
4174
4209
  }
4175
4210
  } catch (err) {
@@ -4232,6 +4267,10 @@
4232
4267
  if (thread) await switchThread(thread);
4233
4268
  if (scroll && messagesEl) requestAnimationFrame(() => { messagesEl.scrollTop = scroll; });
4234
4269
  }
4270
+ // Ensure we always have a thread — auto-select the most recent or create one
4271
+ if (!currentThreadId && threads.length > 0) {
4272
+ await switchThread(threads[0].id);
4273
+ }
4235
4274
  } catch (e) { console.log("Restore failed:", e); }
4236
4275
 
4237
4276
  // Initialize voice features (non-blocking)
@@ -4857,7 +4896,20 @@
4857
4896
  if (m.role === "user" && m.source === "phone") {
4858
4897
  label = (userName || "You") + " (phone)";
4859
4898
  }
4860
- addMessage(m.role === "user" ? "user" : "assistant", m.content, label);
4899
+ var msgEl = addMessage(m.role === "user" ? "user" : "assistant", m.content, label);
4900
+ // Render persisted tool usage indicators
4901
+ if (m.toolsUsed && m.toolsUsed.length > 0 && msgEl) {
4902
+ var toolsHtml = m.toolsUsed.map(function(t) {
4903
+ var cls = t.isError ? "tool-indicator tool-error" : "tool-indicator tool-done";
4904
+ var icon = t.isError ? "&#10007; " : "&#10003; ";
4905
+ return '<div class="' + cls + '">' + icon + '<strong>' + t.name.replace(/</g,"&lt;") + '</strong></div>';
4906
+ }).join("");
4907
+ var toolsDiv = document.createElement("div");
4908
+ toolsDiv.innerHTML = toolsHtml;
4909
+ var contentDiv = msgEl.querySelector(".content");
4910
+ if (contentDiv && contentDiv.parentElement) contentDiv.parentElement.insertBefore(toolsDiv, contentDiv);
4911
+ }
4912
+ renderPersistedAgents(msgEl, m.agentsUsed);
4861
4913
  }
4862
4914
  chatPollIndex = data.total;
4863
4915
  messagesEl.scrollTop = messagesEl.scrollHeight;
@@ -4993,9 +5045,23 @@
4993
5045
  }
4994
5046
 
4995
5047
  try {
4996
- const payload = { sessionId, message: msgText };
5048
+ // Ensure we always have a thread — auto-create silently if needed
5049
+ if (!currentThreadId) {
5050
+ try {
5051
+ const td = await fetch("/api/threads", {
5052
+ method: "POST",
5053
+ headers: { "Content-Type": "application/json", ...authHeaders() },
5054
+ body: JSON.stringify({ sessionId }),
5055
+ }).then(r => r.json());
5056
+ if (td.thread) {
5057
+ currentThreadId = td.thread.id;
5058
+ loadThreads(); // refresh sidebar (fire-and-forget)
5059
+ }
5060
+ } catch {}
5061
+ if (!currentThreadId) { addMessage("system", "Failed to create chat thread."); return; }
5062
+ }
5063
+ const payload = { sessionId, message: msgText, threadId: currentThreadId };
4997
5064
  if (imgPayload) payload.images = imgPayload;
4998
- if (currentThreadId) payload.threadId = currentThreadId;
4999
5065
 
5000
5066
  const res = await fetch("/api/chat", {
5001
5067
  method: "POST",
@@ -5065,6 +5131,25 @@
5065
5131
  contentEl.innerHTML = renderStreamingMarkdown(streamingText);
5066
5132
  messagesEl.scrollTop = messagesEl.scrollHeight;
5067
5133
  }
5134
+ if (data.toolCall) {
5135
+ // Model is calling a tool — show indicator
5136
+ var toolEl = document.createElement("div");
5137
+ toolEl.className = "tool-indicator";
5138
+ toolEl.innerHTML = '<span class="tool-spinner"></span> Using <strong>' + (data.toolCall.name || "tool").replace(/</g,"&lt;") + '</strong>...';
5139
+ if (contentEl && contentEl.parentElement) {
5140
+ contentEl.parentElement.appendChild(toolEl);
5141
+ messagesEl.scrollTop = messagesEl.scrollHeight;
5142
+ }
5143
+ }
5144
+ if (data.toolResult) {
5145
+ // Tool finished — update indicator
5146
+ var indicators = document.querySelectorAll(".tool-indicator");
5147
+ if (indicators.length > 0) {
5148
+ var last = indicators[indicators.length - 1];
5149
+ last.innerHTML = (data.toolResult.isError ? "&#10007; " : "&#10003; ") + '<strong>' + (data.toolResult.name || "tool").replace(/</g,"&lt;") + '</strong>';
5150
+ last.className = "tool-indicator " + (data.toolResult.isError ? "tool-error" : "tool-done");
5151
+ }
5152
+ }
5068
5153
  if (data.agentSpawned) {
5069
5154
  // Server spawned an agent — render block immediately with real task ID
5070
5155
  console.log("[agent] SSE agentSpawned:", data.agentSpawned);
@@ -5203,9 +5288,9 @@
5203
5288
  abortController = null;
5204
5289
  setButtonSend();
5205
5290
  chatInput.focus();
5206
- // Auto-title chat after first assistant response
5291
+ // Auto-title chat after first assistant response, then refresh sidebar
5207
5292
  if (currentThreadId) {
5208
- autoTitleChat(currentThreadId);
5293
+ await autoTitleChat(currentThreadId);
5209
5294
  loadThreads();
5210
5295
  }
5211
5296
  }
@@ -5265,6 +5350,69 @@
5265
5350
  return s < 60 ? s + "s" : Math.floor(s/60) + "m " + (s%60) + "s";
5266
5351
  }
5267
5352
 
5353
+ /** Create a static agent block for history rendering (already completed/failed). */
5354
+ function createPersistedAgentBlock(agent) {
5355
+ var isDone = agent.status === "completed";
5356
+ var isFailed = agent.status === "failed" || agent.status === "cancelled";
5357
+ var block = document.createElement("div");
5358
+ block.className = "agent-block " + (isDone ? "done" : isFailed ? "failed" : "");
5359
+ block.dataset.taskId = agent.taskId || "";
5360
+ var icon = isDone
5361
+ ? '<span class="agent-block-check">\u2713</span>'
5362
+ : isFailed
5363
+ ? '<span class="agent-block-fail">\u2717</span>'
5364
+ : '<div class="agent-block-spinner"></div>';
5365
+ var timerText = agent.elapsed || agent.status || "";
5366
+ block.innerHTML =
5367
+ '<div class="agent-block-header">'
5368
+ + icon
5369
+ + '<span class="agent-block-label">' + (agent.label || "Agent").replace(/</g, "&lt;") + '</span>'
5370
+ + '<span class="agent-block-timer">' + timerText + '</span>'
5371
+ + '</div>'
5372
+ + '<div class="agent-block-body">' + (agent.resultSummary || "").replace(/</g, "&lt;") + '</div>';
5373
+ var header = block.querySelector(".agent-block-header");
5374
+ header.addEventListener("click", function() { block.classList.toggle("expanded"); });
5375
+ return block;
5376
+ }
5377
+
5378
+ /** Render agentsUsed blocks inside a message element. */
5379
+ function renderPersistedAgents(msgEl, agents) {
5380
+ if (!agents || agents.length === 0 || !msgEl) return;
5381
+ for (var i = 0; i < agents.length; i++) {
5382
+ var block = createPersistedAgentBlock(agents[i]);
5383
+ msgEl.appendChild(block);
5384
+ }
5385
+ // If any agents have taskIds and status is "running" or "completed" placeholder, poll once for real status
5386
+ var taskIds = agents.filter(function(a) { return a.taskId; }).map(function(a) { return a.taskId; });
5387
+ if (taskIds.length > 0) {
5388
+ api("/api/agents/tasks").then(function(data) {
5389
+ if (!data.tasks) return;
5390
+ for (var j = 0; j < data.tasks.length; j++) {
5391
+ var task = data.tasks[j];
5392
+ var blockEl = msgEl.querySelector('.agent-block[data-task-id="' + task.id + '"]');
5393
+ if (!blockEl) continue;
5394
+ var isDone = task.status === "completed";
5395
+ var isFailed = task.status === "failed" || task.status === "cancelled";
5396
+ var isRunning = task.status === "running" || task.status === "pending";
5397
+ blockEl.className = "agent-block " + (isDone ? "done" : isFailed ? "failed" : "");
5398
+ var spinnerEl = blockEl.querySelector(".agent-block-spinner, .agent-block-check, .agent-block-fail");
5399
+ if (spinnerEl) {
5400
+ spinnerEl.outerHTML = isDone
5401
+ ? '<span class="agent-block-check">\u2713</span>'
5402
+ : isFailed
5403
+ ? '<span class="agent-block-fail">\u2717</span>'
5404
+ : '<div class="agent-block-spinner"></div>';
5405
+ }
5406
+ if (isRunning) { blockEl.className = "agent-block"; startAgentPoll(); }
5407
+ var timerEl = blockEl.querySelector(".agent-block-timer");
5408
+ if (timerEl && !isRunning) timerEl.textContent = task.status;
5409
+ var bodyEl = blockEl.querySelector(".agent-block-body");
5410
+ if (bodyEl && task.resultSummary) bodyEl.textContent = task.resultSummary;
5411
+ }
5412
+ }).catch(function() {});
5413
+ }
5414
+ }
5415
+
5268
5416
  async function pollAgentTasks() {
5269
5417
  try {
5270
5418
  var data = await api("/api/agents/tasks");
@@ -5595,6 +5743,9 @@
5595
5743
  try {
5596
5744
  const data = await api("/api/models");
5597
5745
  const models = data.models || [];
5746
+ const ollamaModels = models.filter(function(m) { return m.source === "ollama"; });
5747
+ const openrouterModels = models.filter(function(m) { return m.source === "openrouter"; });
5748
+
5598
5749
  var dropdowns = [
5599
5750
  { el: modelChatSelect, current: selectedChat, autoLabel: "Auto (best available)" },
5600
5751
  { el: modelUtilitySelect, current: selectedUtility, autoLabel: "Auto (best available)" },
@@ -5602,16 +5753,38 @@
5602
5753
  ];
5603
5754
  for (var dd of dropdowns) {
5604
5755
  dd.el.innerHTML = '<option value="auto">' + dd.autoLabel + '</option>';
5605
- for (const m of models) {
5606
- const opt = document.createElement("option");
5607
- opt.value = m.name;
5608
- const sizeMB = m.size ? Math.round(m.size / 1024 / 1024) : 0;
5609
- opt.textContent = m.name + (sizeMB ? ` (${sizeMB >= 1024 ? (sizeMB / 1024).toFixed(1) + "GB" : sizeMB + "MB"})` : "");
5610
- if (m.name === dd.current) opt.selected = true;
5611
- dd.el.appendChild(opt);
5756
+
5757
+ // Ollama models
5758
+ if (ollamaModels.length > 0) {
5759
+ var ollamaGroup = document.createElement("optgroup");
5760
+ ollamaGroup.label = "Local (Ollama)";
5761
+ for (const m of ollamaModels) {
5762
+ const opt = document.createElement("option");
5763
+ opt.value = m.name;
5764
+ const sizeMB = m.size ? Math.round(m.size / 1024 / 1024) : 0;
5765
+ opt.textContent = m.name + (sizeMB ? " (" + (sizeMB >= 1024 ? (sizeMB / 1024).toFixed(1) + "GB" : sizeMB + "MB") + ")" : "");
5766
+ if (m.name === dd.current) opt.selected = true;
5767
+ ollamaGroup.appendChild(opt);
5768
+ }
5769
+ dd.el.appendChild(ollamaGroup);
5612
5770
  }
5771
+
5772
+ // OpenRouter models
5773
+ if (openrouterModels.length > 0) {
5774
+ var orGroup = document.createElement("optgroup");
5775
+ orGroup.label = "Cloud (OpenRouter)";
5776
+ for (const m of openrouterModels) {
5777
+ const opt = document.createElement("option");
5778
+ opt.value = m.name;
5779
+ opt.textContent = m.name;
5780
+ if (m.name === dd.current) opt.selected = true;
5781
+ orGroup.appendChild(opt);
5782
+ }
5783
+ dd.el.appendChild(orGroup);
5784
+ }
5785
+
5613
5786
  // If the saved model isn't in the list, add it as an option anyway
5614
- if (dd.current && dd.current !== "auto" && !models.some(m => m.name === dd.current)) {
5787
+ if (dd.current && dd.current !== "auto" && !models.some(function(m) { return m.name === dd.current; })) {
5615
5788
  const opt = document.createElement("option");
5616
5789
  opt.value = dd.current;
5617
5790
  opt.textContent = dd.current + " (not found)";
@@ -6816,6 +6989,7 @@
6816
6989
  </script>
6817
6990
 
6818
6991
  <script src="/public/share-modal.js"></script>
6992
+ <script src="/public/search-flyout.js"></script>
6819
6993
 
6820
6994
  <!-- UI update toast -->
6821
6995
  <div class="ui-update-toast" id="ui-update-toast">