clay-server 4.0.0-beta.9 → 4.0.0

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 (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -7
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -14,44 +14,13 @@
14
14
  // An in-flight save is debounced so rapid drag reorders don't spam.
15
15
 
16
16
  import { refreshIcons } from './icons.js';
17
-
18
- // Registry order = default order for users who haven't customized. Users
19
- // with saved preferences keep their own order (applyPreferences uses
20
- // saved order first, then appends any registry entries the user's saved
21
- // list doesn't mention). So changes here only affect fresh palettes.
22
- var SESSION_TOOLS = [
23
- { id: "file-browser-btn", icon: "folder-tree", label: "File browser" },
24
- { id: "terminal-sidebar-btn", icon: "square-terminal", label: "Terminal", countId: "terminal-sidebar-count" },
25
- { id: "sticky-notes-sidebar-btn", icon: "sticky-note", label: "Sticky Notes", countId: "sticky-notes-sidebar-count" },
26
- { id: "scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
27
- { id: "loop-tool-btn", icon: "repeat", label: "Loop" },
28
- { id: "git-sidebar-btn", icon: "git-branch", label: "Git", countId: "git-sidebar-count" },
29
- { id: "mcp-btn", icon: "cable", label: "MCP Servers", countId: "mcp-sidebar-count" },
30
- { id: "skills-btn", icon: "puzzle", label: "Skills" },
31
- ];
32
-
33
- var MATE_TOOLS = [
34
- { id: "mate-memory-btn", icon: "brain", label: "Memory", countId: "mate-memory-count" },
35
- { id: "mate-knowledge-btn", icon: "book-open", label: "Knowledge", countId: "mate-knowledge-count" },
36
- { id: "mate-sticky-notes-btn", icon: "sticky-note", label: "Sticky Notes" },
37
- { id: "mate-scheduler-btn", icon: "calendar-clock", label: "Scheduled Tasks" },
38
- { id: "mate-debate-btn", icon: "mic", label: "Debate" },
39
- { id: "mate-mcp-btn", icon: "cable", label: "MCP Servers", countId: "mate-mcp-sidebar-count" },
40
- { id: "mate-skills-btn", icon: "puzzle", label: "Skills" },
41
- ];
42
-
43
- var PALETTES = {
44
- session: {
45
- tools: SESSION_TOOLS,
46
- activeContainerId: "session-actions",
47
- hiddenSectionId: "session-actions-hidden",
48
- },
49
- mate: {
50
- tools: MATE_TOOLS,
51
- activeContainerId: "mate-sidebar-tools",
52
- hiddenSectionId: "mate-sidebar-tools-hidden",
53
- },
54
- };
17
+ import { PALETTES, normalizeToolPreferences } from './tool-palette-order.js';
18
+ import {
19
+ enterToolPickMode,
20
+ exitToolPickMode,
21
+ isToolPickActive,
22
+ openPaletteContextMenu
23
+ } from './tool-palette-overlays.js';
55
24
 
56
25
  var _saveTimers = {};
57
26
  var _draggingEl = null;
@@ -86,7 +55,7 @@ export function initToolPalettes() {
86
55
  // handler below, which would immediately tear down the pick-mode
87
56
  // we just entered.
88
57
  e.stopPropagation();
89
- if (_pickActive) exitToolPickMode();
58
+ if (isToolPickActive()) exitToolPickMode();
90
59
  else enterToolPickMode();
91
60
  });
92
61
  }
@@ -303,152 +272,6 @@ function moveToActive(name, toolId) {
303
272
  // attached in buildToolButton before any external module attaches
304
273
  // handlers by ID. See the stopImmediatePropagation logic there.)
305
274
 
306
- // --- Right-click context menu ---
307
-
308
- var _openMenu = null;
309
-
310
- function openPaletteContextMenu(x, y, items) {
311
- closePaletteContextMenu();
312
- var menu = document.createElement('div');
313
- menu.className = 'tool-palette-ctx-menu';
314
- for (var i = 0; i < items.length; i++) {
315
- (function (item) {
316
- var btn = document.createElement('button');
317
- btn.type = 'button';
318
- btn.className = 'tool-palette-ctx-item';
319
- btn.textContent = item.label;
320
- btn.addEventListener('click', function (e) {
321
- e.stopPropagation();
322
- closePaletteContextMenu();
323
- item.action();
324
- });
325
- menu.appendChild(btn);
326
- })(items[i]);
327
- }
328
- document.body.appendChild(menu);
329
- // Clamp to viewport.
330
- var rect = menu.getBoundingClientRect();
331
- var px = x, py = y;
332
- if (px + rect.width > window.innerWidth - 4) px = window.innerWidth - rect.width - 4;
333
- if (py + rect.height > window.innerHeight - 4) py = window.innerHeight - rect.height - 4;
334
- menu.style.left = px + 'px';
335
- menu.style.top = py + 'px';
336
- _openMenu = menu;
337
- }
338
-
339
- function closePaletteContextMenu() {
340
- if (_openMenu && _openMenu.parentNode) _openMenu.parentNode.removeChild(_openMenu);
341
- _openMenu = null;
342
- }
343
-
344
- document.addEventListener('click', function () { closePaletteContextMenu(); });
345
- document.addEventListener('keydown', function (e) {
346
- if (e.key === 'Escape') closePaletteContextMenu();
347
- });
348
- window.addEventListener('blur', function () { closePaletteContextMenu(); });
349
- window.addEventListener('resize', function () { closePaletteContextMenu(); });
350
-
351
- // --- Hotkey overlay (Cmd/Ctrl + O) ---
352
- //
353
- // Press Cmd/Ctrl + O to reveal a hotkey badge on each visible palette
354
- // tile. Then press the shown digit (1..9, 0) or letter (a..z) to
355
- // activate that tool. Escape or clicking away dismisses. Inspired by
356
- // Vimium / Superhuman-style "pick a tile with one keystroke" flows.
357
-
358
- var _pickActive = false;
359
- var HOTKEYS = (function () {
360
- var k = [];
361
- for (var i = 1; i <= 9; i++) k.push(String(i));
362
- k.push('0');
363
- for (var c = 97; c <= 122; c++) k.push(String.fromCharCode(c));
364
- return k;
365
- })();
366
-
367
- function getVisiblePaletteName() {
368
- // offsetParent is null when the element (or an ancestor) is hidden.
369
- var mateActive = document.getElementById('mate-sidebar-tools');
370
- if (mateActive && mateActive.offsetParent !== null) return 'mate';
371
- var sessionActive = document.getElementById('session-actions');
372
- if (sessionActive && sessionActive.offsetParent !== null) return 'session';
373
- return null;
374
- }
375
-
376
- function enterToolPickMode() {
377
- if (_pickActive) return;
378
- var name = getVisiblePaletteName();
379
- if (!name) return;
380
- var active = document.getElementById(PALETTES[name].activeContainerId);
381
- if (!active) return;
382
- var tiles = active.querySelectorAll('[data-tool-id]');
383
- if (tiles.length === 0) return;
384
- for (var i = 0; i < tiles.length && i < HOTKEYS.length; i++) {
385
- var key = HOTKEYS[i];
386
- tiles[i].dataset.hotkey = key;
387
- var badge = document.createElement('span');
388
- badge.className = 'tool-palette-hotkey-badge';
389
- badge.textContent = key.toUpperCase();
390
- tiles[i].appendChild(badge);
391
- }
392
- _pickActive = true;
393
- }
394
-
395
- function exitToolPickMode() {
396
- if (!_pickActive) return;
397
- _pickActive = false;
398
- var badges = document.querySelectorAll('.tool-palette-hotkey-badge');
399
- for (var i = 0; i < badges.length; i++) {
400
- if (badges[i].parentNode) badges[i].parentNode.removeChild(badges[i]);
401
- }
402
- var marked = document.querySelectorAll('[data-hotkey]');
403
- for (var j = 0; j < marked.length; j++) {
404
- delete marked[j].dataset.hotkey;
405
- }
406
- }
407
-
408
- // Capture phase so we reliably preempt typing and browser shortcuts
409
- // when focus is in an input.
410
- document.addEventListener('keydown', function (e) {
411
- // Pick mode: consume the next printable key.
412
- if (_pickActive) {
413
- if (e.key === 'Escape') {
414
- e.preventDefault();
415
- exitToolPickMode();
416
- return;
417
- }
418
- // Ignore modifier-only keys; react to single-character keys.
419
- if (e.key.length !== 1) return;
420
- if (e.ctrlKey || e.metaKey || e.altKey) return;
421
- var key = e.key.toLowerCase();
422
- var tile = document.querySelector('[data-hotkey="' + key + '"]');
423
- if (tile) {
424
- e.preventDefault();
425
- exitToolPickMode();
426
- tile.click();
427
- } else {
428
- // Unknown key exits pick mode without triggering a tile.
429
- exitToolPickMode();
430
- }
431
- return;
432
- }
433
-
434
- // Enter pick mode on Cmd/Ctrl + O (letter O). Intercepted globally
435
- // including inside text inputs — the browser's native Cmd+O is a
436
- // file-picker dialog Clay never wants, so taking it over costs the
437
- // user nothing and is especially useful when jumping to a tool from
438
- // the session search box.
439
- if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key && e.key.toLowerCase() === 'o') {
440
- if (!getVisiblePaletteName()) return;
441
- e.preventDefault();
442
- enterToolPickMode();
443
- }
444
- }, true);
445
-
446
- // Dismiss the overlay on click-away, blur, or viewport changes so it
447
- // never lingers invisibly.
448
- document.addEventListener('click', function () { if (_pickActive) exitToolPickMode(); });
449
- window.addEventListener('blur', function () { if (_pickActive) exitToolPickMode(); });
450
- window.addEventListener('resize', function () { if (_pickActive) exitToolPickMode(); });
451
-
452
275
  function getDragAfterElement(container, x, y) {
453
276
  var tiles = Array.prototype.slice.call(
454
277
  container.querySelectorAll('[data-tool-id]:not(.dragging)')
@@ -495,13 +318,17 @@ function applyPreferences(name, prefs) {
495
318
  var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
496
319
  if (!active || !hiddenGrid) return;
497
320
 
321
+ var normalized = normalizeToolPreferences(name, prefs);
322
+
498
323
  var hiddenSet = {};
499
- var hiddenList = prefs.hidden || [];
324
+ var hiddenList = normalized.hidden;
500
325
  for (var i = 0; i < hiddenList.length; i++) hiddenSet[hiddenList[i]] = true;
501
326
 
502
- var orderList = prefs.order || [];
327
+ var orderList = normalized.order;
503
328
  // Any tools not in the stored order are appended in registry order so
504
- // newly-added tools surface automatically for existing users.
329
+ // newly-added tools surface automatically for existing users. This is how
330
+ // a restored tool reaches a saved palette that predates it: no rewrite, no
331
+ // server state, just the normal new-registry append.
505
332
  var placed = {};
506
333
  for (var j = 0; j < orderList.length; j++) {
507
334
  var id = orderList[j];
@@ -525,6 +352,12 @@ function applyPreferences(name, prefs) {
525
352
  hiddenGrid.appendChild(hbtn);
526
353
  }
527
354
  }
355
+
356
+ // Write the rewritten preference back once, through the normal save path, so
357
+ // the migration survives reloads and reaches the user's other devices. The
358
+ // save reads the DOM this function just arranged, so it records exactly what
359
+ // is on screen.
360
+ if (normalized.migrated) queueSave(name);
528
361
  }
529
362
 
530
363
  function queueSave(name) {
@@ -7,6 +7,7 @@ import { mateAvatarUrl } from './avatar.js';
7
7
  import { getChatLayout } from './theme.js';
8
8
  import { store } from './store.js';
9
9
  import { VENDOR_NAMES } from './app-rendering.js';
10
+ import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
10
11
 
11
12
  var ctx;
12
13
 
@@ -46,8 +47,6 @@ var todoMeta = {
46
47
 
47
48
  // --- Tool tracking ---
48
49
  var tools = {};
49
- var currentThinking = null;
50
- var thinkingGroup = null; // { el, count, totalDuration }
51
50
  var pendingPermissions = {};
52
51
 
53
52
  // --- Tool group tracking ---
@@ -203,6 +202,23 @@ export function toolActivityText(name, input) {
203
202
  return "Running " + name + "...";
204
203
  }
205
204
 
205
+ // The sublabel for a call that has finished. `toolActivityText` is written in
206
+ // the present progressive for a call in flight, so re-asserting it after a
207
+ // result has arrived leaves a finished card claiming to still be running —
208
+ // which is how a failed custom tool ended up reading "Running
209
+ // send_to_partner..." beside its own error output.
210
+ //
211
+ // Only ever used where a result exists, so a genuinely in-flight call is never
212
+ // relabelled. A failure states so plainly; a success keeps whatever specific
213
+ // description the activity text produced ("Reading foo.js"), because that still
214
+ // describes the call accurately, and only the generic present-progressive
215
+ // fallback is replaced.
216
+ export function toolTerminalText(name, input, isError) {
217
+ if (isError) return name + " failed";
218
+ var activity = toolActivityText(name, input);
219
+ return activity === "Running " + name + "..." ? name : activity;
220
+ }
221
+
206
222
  function shortPath(p) {
207
223
  if (!p) return "";
208
224
  var parts = p.split("/");
@@ -1337,12 +1353,7 @@ export function markUserDialogResolved(requestId, behavior) {
1337
1353
  // Annotates the active thinking block's label with a running token estimate.
1338
1354
  // No-op when no thinking block is active.
1339
1355
  export function updateThinkingTokens(estimatedTokens) {
1340
- if (!currentThinking || !currentThinking.el) return;
1341
- var label = currentThinking.el.querySelector(".thinking-label");
1342
- if (!label) return;
1343
- var n = estimatedTokens || 0;
1344
- var disp = n >= 1000 ? ("~" + (Math.round(n / 100) / 10) + "k") : ("~" + n);
1345
- label.textContent = "Thinking " + disp + " tokens";
1356
+ updateThinkingTokenEstimate(estimatedTokens);
1346
1357
  }
1347
1358
 
1348
1359
  // --- Plan mode rendering ---
@@ -1712,128 +1723,23 @@ function updateTodoSticky() {
1712
1723
  // --- Thinking ---
1713
1724
  export function startThinking() {
1714
1725
  ctx.finalizeAssistantBlock();
1715
-
1716
- // Reuse existing thinking group if consecutive
1717
- if (thinkingGroup && thinkingGroup.el.classList.contains("done")) {
1718
- var el = thinkingGroup.el;
1719
- el.classList.remove("done");
1720
- el.querySelector(".thinking-content").textContent = "";
1721
- var reuseLabel = el.querySelector(".thinking-label");
1722
- if (reuseLabel) reuseLabel.textContent = "Thinking";
1723
- // Mate mode: restore dots activity row, hide thinking header
1724
- if (el.classList.contains("mate-thinking")) {
1725
- var actRow = el.querySelector(".mate-thinking-activity");
1726
- if (actRow) {
1727
- actRow.style.display = "";
1728
- }
1729
- var header = el.querySelector(".thinking-header");
1730
- if (header) header.style.display = "none";
1731
- }
1732
- currentThinking = { el: el, fullText: "", startTime: Date.now() };
1733
- refreshIcons();
1734
- maybeScrollToBottom();
1735
- if (!el.classList.contains("mate-thinking")) {
1736
- ctx.setActivity("thinking");
1737
- }
1738
- return;
1739
- }
1740
-
1741
- var el = document.createElement("div");
1742
- el.className = "thinking-item";
1743
-
1744
- if (ctx.isMateDm()) {
1745
- var mateName = ctx.getMateName();
1746
- var mateAvatar = ctx.getMateAvatarUrl();
1747
- el.classList.add("mate-thinking");
1748
- el.innerHTML =
1749
- '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
1750
- '<div class="dm-bubble-content">' +
1751
- '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
1752
- '<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
1753
- '<div class="thinking-header" style="display:none">' +
1754
- '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1755
- '<span class="thinking-label">Thinking</span>' +
1756
- '<span class="thinking-duration"></span>' +
1757
- '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
1758
- '</div>' +
1759
- '<div class="thinking-content"></div>' +
1760
- '</div>';
1761
- } else {
1762
- el.innerHTML =
1763
- '<div class="thinking-header">' +
1764
- '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1765
- '<span class="thinking-label">Thinking</span>' +
1766
- '<span class="thinking-duration"></span>' +
1767
- '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
1768
- '</div>' +
1769
- '<div class="thinking-content"></div>';
1770
- }
1771
-
1772
- el.querySelector(".thinking-header").addEventListener("click", function () {
1773
- el.classList.toggle("expanded");
1774
- });
1775
-
1776
- ctx.addToMessages(el);
1777
- refreshIcons();
1778
- maybeScrollToBottom();
1779
- thinkingGroup = { el: el, count: 0, totalDuration: 0 };
1780
- currentThinking = { el: el, fullText: "", startTime: Date.now() };
1726
+ var el = startThinkingSegment();
1781
1727
  if (!ctx.isMateDm()) {
1782
1728
  ctx.setActivity("thinking");
1783
1729
  }
1730
+ return el;
1784
1731
  }
1785
1732
 
1786
1733
  export function appendThinking(text) {
1787
- if (!currentThinking) return;
1788
- currentThinking.fullText += text;
1789
- currentThinking.el.querySelector(".thinking-content").textContent = currentThinking.fullText;
1790
- maybeScrollToBottom();
1734
+ appendThinkingText(text);
1791
1735
  }
1792
1736
 
1793
1737
  export function stopThinking(duration) {
1794
- if (!currentThinking) return;
1795
- var secs = typeof duration === "number" ? duration : (Date.now() - currentThinking.startTime) / 1000;
1796
- currentThinking.el.classList.add("done");
1797
- if (thinkingGroup && thinkingGroup.el === currentThinking.el) {
1798
- thinkingGroup.count++;
1799
- thinkingGroup.totalDuration += secs;
1800
- currentThinking.el.querySelector(".thinking-duration").textContent = " " + thinkingGroup.totalDuration.toFixed(1) + "s";
1801
- } else {
1802
- currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs.toFixed(1) + "s";
1803
- }
1804
- // If no thinking text was streamed (e.g. Codex reasoning items arrive
1805
- // with encrypted/hidden content, or Claude without extended-thinking),
1806
- // the expand affordance is misleading because there's nothing inside.
1807
- // Strip the chevron and the click handler so the header reads as a
1808
- // plain label.
1809
- var hasContent = !!(currentThinking.fullText && currentThinking.fullText.length > 0);
1810
- if (!hasContent) {
1811
- currentThinking.el.classList.add("empty");
1812
- var chev = currentThinking.el.querySelector(".thinking-chevron");
1813
- if (chev) chev.style.display = "none";
1814
- var hdr = currentThinking.el.querySelector(".thinking-header");
1815
- if (hdr) {
1816
- hdr.style.cursor = "default";
1817
- // Replace click listener by cloning the node (cheapest way to strip listeners).
1818
- var clone = hdr.cloneNode(true);
1819
- hdr.parentNode.replaceChild(clone, hdr);
1820
- }
1821
- }
1822
- // In mate mode: hide sparkle activity, show compact thinking header.
1823
- if (currentThinking.el.classList.contains("mate-thinking")) {
1824
- var actRow = currentThinking.el.querySelector(".mate-thinking-activity");
1825
- if (actRow) actRow.style.display = "none";
1826
- var header = currentThinking.el.querySelector(".thinking-header");
1827
- if (header) {
1828
- header.style.display = "";
1829
- header.style.cursor = hasContent ? "pointer" : "default";
1830
- }
1831
- }
1832
- currentThinking = null;
1738
+ stopThinkingSegment(duration);
1833
1739
  }
1834
1740
 
1835
1741
  export function resetThinkingGroup() {
1836
- thinkingGroup = null;
1742
+ resetThinkingTurn();
1837
1743
  }
1838
1744
 
1839
1745
  // --- Tool items ---
@@ -2136,9 +2042,12 @@ export function updateToolResult(id, content, isError, images) {
2136
2042
  var tool = tools[id];
2137
2043
  if (!tool) return;
2138
2044
 
2045
+ // Re-render the sublabel from the now-complete input (streaming can have
2046
+ // built it from a partial one), but in its terminal form: a result has
2047
+ // arrived, so this call is no longer running.
2139
2048
  var subtitleText = tool.el.querySelector(".tool-subtitle-text");
2140
2049
  if (subtitleText && tool.input) {
2141
- subtitleText.textContent = toolActivityText(tool.name, tool.input);
2050
+ subtitleText.textContent = toolTerminalText(tool.name, tool.input, isError);
2142
2051
  }
2143
2052
 
2144
2053
  var resultBlock = document.createElement("div");
@@ -2501,7 +2410,7 @@ export function getHiddenResultTools() { return HIDDEN_RESULT_TOOLS; }
2501
2410
  export function saveToolState() {
2502
2411
  return {
2503
2412
  tools: tools,
2504
- currentThinking: currentThinking,
2413
+ thinkingState: saveThinkingState(),
2505
2414
  todoWidgetEl: todoWidgetEl,
2506
2415
  todoMeta: todoMeta,
2507
2416
  inPlanMode: inPlanMode,
@@ -2516,7 +2425,7 @@ export function saveToolState() {
2516
2425
 
2517
2426
  export function restoreToolState(saved) {
2518
2427
  tools = saved.tools;
2519
- currentThinking = saved.currentThinking;
2428
+ restoreThinkingState(saved.thinkingState);
2520
2429
  todoWidgetEl = saved.todoWidgetEl;
2521
2430
  todoMeta = saved.todoMeta || normalizeTodoMeta();
2522
2431
  inPlanMode = saved.inPlanMode;
@@ -2533,8 +2442,7 @@ export function restoreToolState(saved) {
2533
2442
 
2534
2443
  export function resetToolState() {
2535
2444
  tools = {};
2536
- currentThinking = null;
2537
- thinkingGroup = null;
2445
+ clearThinkingState();
2538
2446
  inPlanMode = false;
2539
2447
  planContent = null;
2540
2448
  currentPlanCardEl = null;
@@ -0,0 +1,169 @@
1
+ // Snooze control for the update-available banner.
2
+ //
3
+ // Lives outside app-notifications.js so the banner module does not grow
4
+ // further, and because this is a self-contained concern: one compact menu, one
5
+ // message out, one message in.
6
+ //
7
+ // The client decides nothing. It sends an allowlisted duration key and, as a
8
+ // hint only, the browser's UTC offset so "Tomorrow" lands on the user's
9
+ // morning rather than the server's. The deadline, the target version and the
10
+ // user identity are all resolved server-side, so a tampered payload can only
11
+ // ever produce a refusal or a shorter snooze.
12
+
13
+ import { getWs } from './ws-ref.js';
14
+ import { iconHtml, refreshIcons } from './icons.js';
15
+
16
+ // Must match SNOOZE_OPTIONS in lib/update-snooze.js. The server re-validates
17
+ // every key, so this list is presentation, not policy.
18
+ var SNOOZE_CHOICES = [
19
+ { id: "3h", label: "3 hours" },
20
+ { id: "8h", label: "8 hours" },
21
+ { id: "tomorrow", label: "Tomorrow" },
22
+ ];
23
+
24
+ var openMenu = null;
25
+ var bannerRemover = null;
26
+
27
+ // app-notifications.js owns banner teardown (it runs the exit animation and
28
+ // keeps the clear-all button in sync). Registering the remover here keeps the
29
+ // dependency one-way, so the two modules never form a cycle.
30
+ export function setSnoozeBannerRemover(fn) {
31
+ bannerRemover = typeof fn === "function" ? fn : null;
32
+ }
33
+
34
+ export function isSnoozeChoice(id) {
35
+ for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
36
+ if (SNOOZE_CHOICES[i].id === id) return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function closeMenu() {
42
+ if (!openMenu) return;
43
+ var trigger = openMenu.trigger;
44
+ if (openMenu.menu && openMenu.menu.parentNode) {
45
+ openMenu.menu.parentNode.removeChild(openMenu.menu);
46
+ }
47
+ openMenu = null;
48
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
49
+ }
50
+
51
+ function sendSnooze(duration) {
52
+ if (!isSnoozeChoice(duration)) return;
53
+ var ws = getWs();
54
+ if (!ws || ws.readyState !== 1) return;
55
+ // A hint, not an instruction: the server validates the range, computes the
56
+ // instant itself, and clamps the result to its own maximum.
57
+ var offset = -new Date().getTimezoneOffset();
58
+ ws.send(JSON.stringify({ type: "update_snooze", duration: duration, tzOffsetMinutes: offset }));
59
+ }
60
+
61
+ function buildMenu(trigger) {
62
+ var menu = document.createElement("div");
63
+ menu.className = "notif-banner-snooze-menu";
64
+ menu.setAttribute("role", "menu");
65
+ menu.setAttribute("aria-label", "Snooze this update");
66
+
67
+ for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
68
+ (function (choice) {
69
+ var item = document.createElement("button");
70
+ item.type = "button";
71
+ item.className = "notif-banner-snooze-item";
72
+ item.setAttribute("role", "menuitem");
73
+ item.textContent = choice.label;
74
+ item.addEventListener("click", function (e) {
75
+ e.stopPropagation();
76
+ closeMenu();
77
+ sendSnooze(choice.id);
78
+ });
79
+ menu.appendChild(item);
80
+ })(SNOOZE_CHOICES[i]);
81
+ }
82
+
83
+ // Roving focus with the arrow keys, Escape back to the trigger. Enough for a
84
+ // three-item menu to be usable without a pointer.
85
+ menu.addEventListener("keydown", function (e) {
86
+ var items = menu.querySelectorAll(".notif-banner-snooze-item");
87
+ var index = -1;
88
+ for (var i = 0; i < items.length; i++) {
89
+ if (items[i] === document.activeElement) { index = i; break; }
90
+ }
91
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
92
+ e.preventDefault();
93
+ var next = e.key === "ArrowDown" ? index + 1 : index - 1;
94
+ if (next < 0) next = items.length - 1;
95
+ if (next >= items.length) next = 0;
96
+ if (items[next]) items[next].focus();
97
+ return;
98
+ }
99
+ if (e.key === "Escape") {
100
+ e.preventDefault();
101
+ e.stopPropagation();
102
+ closeMenu();
103
+ if (trigger) trigger.focus();
104
+ }
105
+ });
106
+
107
+ return menu;
108
+ }
109
+
110
+ // Append a compact Snooze control to an existing update banner's action row.
111
+ // Reuses the banner's own button shape rather than introducing a new one.
112
+ export function attachSnoozeControl(actionsEl) {
113
+ if (!actionsEl) return null;
114
+
115
+ var wrap = document.createElement("div");
116
+ wrap.className = "notif-banner-snooze";
117
+
118
+ var trigger = document.createElement("button");
119
+ trigger.type = "button";
120
+ trigger.className = "notif-banner-snooze-btn";
121
+ trigger.setAttribute("aria-haspopup", "menu");
122
+ trigger.setAttribute("aria-expanded", "false");
123
+ trigger.setAttribute("aria-label", "Snooze this update notification");
124
+ trigger.innerHTML = iconHtml("clock") + "<span>Snooze</span>";
125
+
126
+ trigger.addEventListener("click", function (e) {
127
+ e.stopPropagation();
128
+ if (openMenu && openMenu.trigger === trigger) {
129
+ closeMenu();
130
+ return;
131
+ }
132
+ closeMenu();
133
+ var menu = buildMenu(trigger);
134
+ wrap.appendChild(menu);
135
+ openMenu = { menu: menu, trigger: trigger };
136
+ trigger.setAttribute("aria-expanded", "true");
137
+ var first = menu.querySelector(".notif-banner-snooze-item");
138
+ if (first) first.focus();
139
+ });
140
+
141
+ wrap.appendChild(trigger);
142
+ actionsEl.appendChild(wrap);
143
+ refreshIcons();
144
+ return wrap;
145
+ }
146
+
147
+ // The server confirms a snooze to every device of that user, so each one drops
148
+ // the banner rather than only the device the user acted on.
149
+ export function handleUpdateSnoozed(msg) {
150
+ closeMenu();
151
+ if (!msg || !msg.ok) return;
152
+ var banner = document.querySelector('.notif-banner[data-notif-id="_update"]');
153
+ if (!banner) return;
154
+ if (msg.version && banner.getAttribute("data-update-version") !== msg.version) return;
155
+ if (bannerRemover) bannerRemover(banner);
156
+ else if (banner.parentNode) banner.parentNode.removeChild(banner);
157
+ }
158
+
159
+ // Dismiss on click-away, blur, or a viewport change so the menu never lingers
160
+ // invisibly. Registered defensively because importing this module must stay
161
+ // side-effect-safe: several suites load the client graph against a partial
162
+ // document/window stub, and a dismiss convenience must not be what breaks them.
163
+ function onGlobal(target, name, fn) {
164
+ if (target && typeof target.addEventListener === "function") target.addEventListener(name, fn);
165
+ }
166
+
167
+ onGlobal(typeof document !== "undefined" ? document : null, "click", function () { closeMenu(); });
168
+ onGlobal(typeof window !== "undefined" ? window : null, "blur", function () { closeMenu(); });
169
+ onGlobal(typeof window !== "undefined" ? window : null, "resize", function () { closeMenu(); });
@@ -10,6 +10,7 @@ import { setSTTLang } from './stt.js';
10
10
  import { userAvatarUrl } from './avatar.js';
11
11
  import { store } from './store.js';
12
12
  import { getWs } from './ws-ref.js';
13
+ import { initCapsulePreference, syncCapsulePreference } from './capsule-preference.js';
13
14
 
14
15
  var ctx = null;
15
16
  var settingsEl = null;
@@ -34,6 +35,8 @@ export function initUserSettings(appCtx) {
34
35
 
35
36
  if (!settingsEl || !openBtn) return;
36
37
 
38
+ initCapsulePreference();
39
+
37
40
  navItems = settingsEl.querySelectorAll('.us-nav-item');
38
41
  sections = settingsEl.querySelectorAll('.us-section');
39
42
  pinForm = document.getElementById('us-pin-form');
@@ -466,6 +469,7 @@ function populateAccount() {
466
469
  if (matesToggle) matesToggle.checked = matesOn;
467
470
  store.set({ projectMateDmsEnabled: matesOn });
468
471
  applyProjectMateDmsClass(matesOn);
472
+ syncCapsulePreference(data.capsulesEnabled === true);
469
473
  if (data.chatLayout) {
470
474
  setChatLayout(data.chatLayout); // update local cache + CSS
471
475
  }