clay-server 3.8.1 → 4.0.0-beta.2

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 (189) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/builtin-mates.js +6 -6
  3. package/lib/claude-hook-installer.js +15 -0
  4. package/lib/clay-history-mcp-server.js +62 -297
  5. package/lib/daemon.js +1 -1
  6. package/lib/debate-model-selection.js +119 -0
  7. package/lib/home-debate-tool-policy.js +164 -0
  8. package/lib/home-response-notifications.js +26 -0
  9. package/lib/mate-creation-mcp-server.js +55 -0
  10. package/lib/mate-ready-creation.js +95 -0
  11. package/lib/mates.js +2 -0
  12. package/lib/notes.js +2 -1
  13. package/lib/project-ask-user.js +64 -0
  14. package/lib/project-capsule-catalog.js +101 -0
  15. package/lib/project-connection.js +4 -0
  16. package/lib/project-debate-proposal.js +133 -17
  17. package/lib/project-debate.js +214 -110
  18. package/lib/project-delegated-follow-up.js +84 -0
  19. package/lib/project-delegated-session.js +53 -0
  20. package/lib/project-filesystem.js +7 -9
  21. package/lib/project-knowledge.js +1 -0
  22. package/lib/project-mate-creation-proposal.js +120 -0
  23. package/lib/project-memory.js +14 -7
  24. package/lib/project-models.js +157 -32
  25. package/lib/project-session-notes.js +2 -2
  26. package/lib/project-session-pair.js +8 -8
  27. package/lib/project-sessions.js +66 -51
  28. package/lib/project-vendor-login.js +397 -0
  29. package/lib/project-worker-proposal.js +25 -25
  30. package/lib/project-workspace-query.js +144 -0
  31. package/lib/project.js +349 -102
  32. package/lib/public/app.js +85 -42
  33. package/lib/public/css/avatar-imprints.css +0 -1
  34. package/lib/public/css/base.css +4 -0
  35. package/lib/public/css/capsule-ui.css +389 -0
  36. package/lib/public/css/command-palette.css +306 -6
  37. package/lib/public/css/home-capsule-library.css +243 -0
  38. package/lib/public/css/home-capsule-source.css +192 -0
  39. package/lib/public/css/home-debate-live.css +146 -0
  40. package/lib/public/css/home-debate-planning.css +125 -0
  41. package/lib/public/css/home-debates-archive.css +281 -0
  42. package/lib/public/css/home-hub.css +896 -405
  43. package/lib/public/css/home-mate-creation.css +34 -0
  44. package/lib/public/css/home-mate-model-picker.css +131 -0
  45. package/lib/public/css/home-mate-settings.css +344 -0
  46. package/lib/public/css/home-session-actions.css +265 -0
  47. package/lib/public/css/home-sidebar.css +667 -0
  48. package/lib/public/css/icon-strip.css +1 -2
  49. package/lib/public/css/mates.css +7 -34
  50. package/lib/public/css/pane.css +1 -1
  51. package/lib/public/css/playbook.css +0 -80
  52. package/lib/public/css/sticky-notes.css +53 -117
  53. package/lib/public/css/tui-attention.css +0 -44
  54. package/lib/public/css/workspace-assignment.css +45 -0
  55. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  56. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  57. package/lib/public/index.html +93 -74
  58. package/lib/public/modules/app-connection.js +10 -1
  59. package/lib/public/modules/app-dm.js +43 -160
  60. package/lib/public/modules/app-home-hub.js +328 -585
  61. package/lib/public/modules/app-message-cards.js +242 -0
  62. package/lib/public/modules/app-message-router.js +84 -0
  63. package/lib/public/modules/app-messages.js +102 -37
  64. package/lib/public/modules/app-notifications.js +8 -119
  65. package/lib/public/modules/app-projects.js +9 -1
  66. package/lib/public/modules/app-rendering.js +21 -602
  67. package/lib/public/modules/avatar-imprint.js +11 -8
  68. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  69. package/lib/public/modules/chat-render-runtime.js +198 -0
  70. package/lib/public/modules/command-palette.js +225 -436
  71. package/lib/public/modules/dm-render.js +82 -0
  72. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  73. package/lib/public/modules/home-capsule-library.js +146 -0
  74. package/lib/public/modules/home-capsule-source.js +383 -0
  75. package/lib/public/modules/home-chat-identity.js +35 -0
  76. package/lib/public/modules/home-chat-scroll.js +77 -0
  77. package/lib/public/modules/home-chat-stream-state.js +34 -0
  78. package/lib/public/modules/home-composer-focus.js +25 -0
  79. package/lib/public/modules/home-conversations-sheet.js +179 -0
  80. package/lib/public/modules/home-debate-activity.js +48 -0
  81. package/lib/public/modules/home-debate-controls.js +295 -0
  82. package/lib/public/modules/home-debate-launch.js +41 -0
  83. package/lib/public/modules/home-debate-live.js +284 -0
  84. package/lib/public/modules/home-debate-models.js +73 -0
  85. package/lib/public/modules/home-debate-planning.js +335 -0
  86. package/lib/public/modules/home-debates-archive.js +279 -0
  87. package/lib/public/modules/home-dock-resize.js +74 -0
  88. package/lib/public/modules/home-dock.js +446 -0
  89. package/lib/public/modules/home-mate-chat.js +496 -0
  90. package/lib/public/modules/home-mate-creation.js +109 -0
  91. package/lib/public/modules/home-mate-model-picker.js +281 -0
  92. package/lib/public/modules/home-mate-selection.js +38 -0
  93. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  94. package/lib/public/modules/home-mate-settings.js +383 -0
  95. package/lib/public/modules/home-session-actions.js +373 -0
  96. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  97. package/lib/public/modules/home-shell.js +9 -0
  98. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  99. package/lib/public/modules/home-sidebar.js +158 -0
  100. package/lib/public/modules/home-sub-surface.js +19 -0
  101. package/lib/public/modules/home-surface-boot.js +68 -0
  102. package/lib/public/modules/home-surface.js +141 -0
  103. package/lib/public/modules/home-tools.js +271 -0
  104. package/lib/public/modules/markdown.js +50 -2
  105. package/lib/public/modules/mate-management.js +86 -0
  106. package/lib/public/modules/mate-wizard.js +0 -7
  107. package/lib/public/modules/mention.js +17 -19
  108. package/lib/public/modules/pane-bridge.js +9 -0
  109. package/lib/public/modules/profile.js +4 -5
  110. package/lib/public/modules/project-settings.js +34 -28
  111. package/lib/public/modules/project-switcher.js +11 -5
  112. package/lib/public/modules/project-workspace-assignment.js +23 -0
  113. package/lib/public/modules/search-clay-chat.js +402 -0
  114. package/lib/public/modules/server-settings.js +14 -12
  115. package/lib/public/modules/session-actions.js +1 -1
  116. package/lib/public/modules/sidebar-mates.js +56 -552
  117. package/lib/public/modules/sidebar-mobile.js +7 -64
  118. package/lib/public/modules/sidebar-presence.js +37 -0
  119. package/lib/public/modules/split-pair-ui.js +13 -13
  120. package/lib/public/modules/split-view.js +7 -0
  121. package/lib/public/modules/sticky-notes.js +7 -6
  122. package/lib/public/modules/tool-input-composition.js +48 -0
  123. package/lib/public/modules/tool-llm-status.js +167 -0
  124. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  125. package/lib/public/modules/tool-renderer-chart.js +158 -0
  126. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  127. package/lib/public/modules/tool-renderer.js +464 -0
  128. package/lib/public/modules/tool-runtime.js +309 -0
  129. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  130. package/lib/public/modules/tool-ui-tree.js +21 -0
  131. package/lib/public/modules/tui-attention.js +10 -0
  132. package/lib/public/modules/user-settings.js +16 -23
  133. package/lib/public/modules/vendor-login.js +287 -0
  134. package/lib/public/modules/worker-proposal.js +2 -2
  135. package/lib/public/modules/workspace-assignment-card.js +149 -0
  136. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  137. package/lib/public/style.css +13 -2
  138. package/lib/runtime-env.js +65 -0
  139. package/lib/sdk-bridge.js +120 -32
  140. package/lib/sdk-message-processor.js +10 -6
  141. package/lib/server-home-capsule-creation.js +36 -0
  142. package/lib/server-home-chat-events.js +287 -0
  143. package/lib/server-home-chat.js +518 -0
  144. package/lib/server-home-clay-entry.js +65 -0
  145. package/lib/server-home-clay-session-links.js +26 -0
  146. package/lib/server-home-debate-planning.js +214 -0
  147. package/lib/server-home-debates.js +137 -0
  148. package/lib/server-home-mate-creation.js +261 -0
  149. package/lib/server-home-models.js +241 -0
  150. package/lib/server-home-preferences.js +61 -0
  151. package/lib/server-mates.js +37 -0
  152. package/lib/server-tools.js +377 -0
  153. package/lib/server.js +111 -59
  154. package/lib/session-pair-mcp-server.js +6 -6
  155. package/lib/sessions.js +18 -3
  156. package/lib/tool-capsule-source.js +142 -0
  157. package/lib/tool-control-mcp-server.js +126 -0
  158. package/lib/tool-llm.js +48 -0
  159. package/lib/tool-storage.js +77 -0
  160. package/lib/tool-ui-spec-advanced.js +82 -0
  161. package/lib/tool-ui-spec.js +261 -0
  162. package/lib/tools-registry.js +286 -0
  163. package/lib/users-home-dock-preferences.js +77 -0
  164. package/lib/users-home-surface-preferences.js +99 -0
  165. package/lib/users-preferences.js +4 -7
  166. package/lib/users.js +12 -0
  167. package/lib/whats-new-content.js +3 -3
  168. package/lib/workspace-assignment-service.js +420 -0
  169. package/lib/workspace-query-mcp-server.js +154 -0
  170. package/lib/workspace-query-service.js +492 -0
  171. package/lib/ws-schema.js +93 -12
  172. package/lib/yoke/adapters/acp.js +2 -1
  173. package/lib/yoke/adapters/antigravity.js +2 -1
  174. package/lib/yoke/adapters/claude-worker.js +11 -4
  175. package/lib/yoke/adapters/claude.js +73 -43
  176. package/lib/yoke/adapters/codex.js +40 -10
  177. package/lib/yoke/adapters/kiro.js +2 -1
  178. package/lib/yoke/claude-user-input.js +38 -0
  179. package/lib/yoke/codex-app-server.js +20 -2
  180. package/lib/yoke/codex-user-input.js +64 -0
  181. package/lib/yoke/complete-once.js +164 -0
  182. package/lib/yoke/index.js +5 -1
  183. package/lib/yoke/interface.js +19 -0
  184. package/lib/yoke/mcp-bridge-server.js +2 -6
  185. package/lib/yoke/user-input.js +254 -0
  186. package/package.json +3 -2
  187. package/lib/public/css/home-chat.css +0 -380
  188. package/lib/public/modules/home-chat.js +0 -438
  189. package/lib/server-clay-home.js +0 -245
@@ -2,317 +2,36 @@
2
2
  // Extracted from app.js (PR-28)
3
3
 
4
4
  import { store } from './store.js';
5
- import { getWs } from './ws-ref.js';
6
- import { getMessagesEl, getInputEl, getSendBtn } from './dom-refs.js';
7
- import { escapeHtml, copyToClipboard } from './utils.js';
8
- import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
9
- import { iconHtml, refreshIcons } from './icons.js';
10
- import { userAvatarUrl } from './avatar.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { createAssistantBubble, renderAssistantBubbleText, enhanceAssistantBubble, addAssistantCopyHandler, finalizeAssistantBubble } from './chat-bubble-renderer.js';
7
+ import { refreshIcons } from './icons.js';
11
8
  import { closeToolGroup } from './tools.js';
12
- import { showImageModal, showPasteModal } from './app-misc.js';
13
9
  import { sendMessage, hasSendableContent } from './input.js';
14
10
  import { getChatLayout } from './theme.js';
15
- import { getScheduledMsgEl } from './app-rate-limit.js';
11
+ import { addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl, VENDOR_AVATARS, VENDOR_NAMES } from './chat-render-runtime.js';
16
12
  export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
17
-
18
- // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
19
- // browser cannot import the server's CommonJS registry directly.
20
- export var VENDOR_AVATARS = {
21
- claude: "/claude-code-avatar.png",
22
- codex: "/codex-avatar.png",
23
- antigravity: "/antigravity-avatar.png",
24
- opencode: "/opencode-avatar.svg",
25
- kimi: "/kimi-avatar.svg",
26
- grok: "/grok-avatar.svg",
27
- copilot: "/copilot-avatar.svg",
28
- qwen: "/qwen-avatar.svg",
29
- junie: "/junie-avatar.svg",
30
- kiro: "/kiro-avatar.svg",
31
- };
32
- export var VENDOR_NAMES = {
33
- claude: "Claude Code",
34
- codex: "Codex",
35
- antigravity: "Antigravity CLI",
36
- opencode: "OpenCode",
37
- kimi: "Kimi Code",
38
- grok: "Grok Build",
39
- copilot: "GitHub Copilot CLI",
40
- qwen: "Qwen Code",
41
- junie: "Junie CLI",
42
- kiro: "Kiro CLI",
43
- };
44
- // Display order for every vendor Clay knows about, installed or not. Pickers
45
- // render the full list so a missing CLI reads as "not installed yet" rather
46
- // than "Clay doesn't support it".
47
- // Where to send the user when they pick a vendor whose CLI isn't installed.
48
- export var VENDOR_HOMEPAGES = {
49
- claude: "https://claude.com/product/claude-code",
50
- codex: "https://openai.com/codex/",
51
- antigravity: "https://antigravity.google/product/antigravity-cli",
52
- opencode: "https://opencode.ai/",
53
- kimi: "https://www.kimi.com/code/",
54
- grok: "https://docs.x.ai/build/overview",
55
- copilot: "https://github.com/features/copilot/cli",
56
- qwen: "https://qwenlm.github.io/qwen-code-docs/",
57
- junie: "https://junie.jetbrains.com/",
58
- kiro: "https://kiro.dev/",
59
- };
60
- var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
61
- var NEW_MSG_BTN_ACTIVITY = "\u2193 New activity";
13
+ export { initRendering, addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
14
+ export { addUserMessage, addSystemMessage, addConflictMessage, addContextOverflowMessage } from './app-message-cards.js';
62
15
 
63
16
  // --- Module-owned state (not in store) ---
64
- var turnCounter = 0;
65
- var prependAnchor = null;
66
- var activityEl = null;
67
17
  var matePreThinkingTimer = null;
68
18
  var highlightTimer = null;
69
19
  var streamBuffer = "";
70
20
  var streamDrainTimer = null;
71
- var isUserScrolledUp = false;
72
- var scrollThreshold = 150;
73
-
74
- // --- Sticky-bottom mode ---
75
- // While armed, a ResizeObserver re-pins #messages to scrollHeight on every
76
- // height change so deferred content (tools, syntax highlighting, images,
77
- // IntersectionObserver-driven reflows) doesn't strand the user mid-page.
78
- // The scroll listener in app.js consults getStickyBottom() and ignores
79
- // growth-induced scroll events while armed.
80
- //
81
- // Disarm rules:
82
- // - Real user input (wheel / touchmove / PageUp / Home / ArrowUp): immediate.
83
- // - Quiet detector: armStickyBottom(durationMs) treats durationMs as the
84
- // QUIET WINDOW, not a hard timer. Each ResizeObserver callback resets
85
- // a debounce timer; sticky-bottom only disarms after no resize for
86
- // durationMs. Long-settling sessions (large todo widgets, slow code
87
- // highlighting) keep extending the window naturally.
88
- // - Hard ceiling: a separate cap prevents pathological lock-in.
89
- var stickyBottom = false;
90
- var stickyBottomQuietTimer = null;
91
- var stickyBottomCeilingTimer = null;
92
- var stickyBottomQuietMs = 750;
93
- var stickyBottomCeilingMs = 8000;
94
- var stickyBottomResizeObs = null;
95
- var stickyBottomInputBound = false;
96
-
97
- export function getStickyBottom() { return stickyBottom; }
98
-
99
- function pinToBottomNow() {
100
- var messagesEl = getMessagesEl();
101
- if (!messagesEl) return;
102
- messagesEl.scrollTop = messagesEl.scrollHeight;
103
- }
104
-
105
- function ensureStickyInfrastructure() {
106
- var messagesEl = getMessagesEl();
107
- if (!messagesEl) return;
108
- if (!stickyBottomResizeObs && typeof ResizeObserver !== "undefined") {
109
- stickyBottomResizeObs = new ResizeObserver(function () {
110
- if (!stickyBottom) return;
111
- // Re-pin on every layout change while armed.
112
- pinToBottomNow();
113
- // Reset the quiet timer — settling has not finished yet.
114
- if (stickyBottomQuietTimer) clearTimeout(stickyBottomQuietTimer);
115
- stickyBottomQuietTimer = setTimeout(disarmStickyBottom, stickyBottomQuietMs);
116
- });
117
- stickyBottomResizeObs.observe(messagesEl);
118
- // Also observe direct children so child-size changes (image loads, code
119
- // block highlighting, expanding tool groups) trigger a re-pin even when
120
- // they don't change the scroller's own size.
121
- var kids = messagesEl.children;
122
- for (var i = 0; i < kids.length; i++) stickyBottomResizeObs.observe(kids[i]);
123
- }
124
- if (!stickyBottomInputBound) {
125
- stickyBottomInputBound = true;
126
- var disarmOnUserScroll = function () { disarmStickyBottom(); };
127
- messagesEl.addEventListener("wheel", disarmOnUserScroll, { passive: true });
128
- messagesEl.addEventListener("touchmove", disarmOnUserScroll, { passive: true });
129
- document.addEventListener("keydown", function (e) {
130
- if (!stickyBottom) return;
131
- if (e.key === "PageUp" || e.key === "Home" || e.key === "ArrowUp") {
132
- disarmStickyBottom();
133
- }
134
- });
135
- }
136
- }
137
-
138
- export function armStickyBottom(durationMs) {
139
- if (prependAnchor) return; // never fight pagination
140
- ensureStickyInfrastructure();
141
- stickyBottom = true;
142
- isUserScrolledUp = false;
143
- var newMsgBtn = document.getElementById("new-msg-btn");
144
- if (newMsgBtn) {
145
- newMsgBtn.classList.add("hidden");
146
- newMsgBtn.textContent = NEW_MSG_BTN_DEFAULT;
147
- }
148
- pinToBottomNow();
149
- // After children may have been replaced since last arm, re-observe.
150
- if (stickyBottomResizeObs) {
151
- var messagesEl = getMessagesEl();
152
- if (messagesEl) {
153
- var kids = messagesEl.children;
154
- for (var i = 0; i < kids.length; i++) {
155
- try { stickyBottomResizeObs.observe(kids[i]); } catch (e) {}
156
- }
157
- }
158
- }
159
- // Quiet window: callers pass intended quiet duration; ResizeObserver
160
- // resets this each time layout changes, so the actual armed duration
161
- // stretches to "no resize for durationMs".
162
- stickyBottomQuietMs = durationMs || 750;
163
- if (stickyBottomQuietTimer) clearTimeout(stickyBottomQuietTimer);
164
- stickyBottomQuietTimer = setTimeout(disarmStickyBottom, stickyBottomQuietMs);
165
- // Hard ceiling so we never lock the scroller indefinitely if some
166
- // animation/observer keeps firing forever.
167
- if (stickyBottomCeilingTimer) clearTimeout(stickyBottomCeilingTimer);
168
- stickyBottomCeilingTimer = setTimeout(disarmStickyBottom, stickyBottomCeilingMs);
169
- }
170
-
171
- export function disarmStickyBottom() {
172
- stickyBottom = false;
173
- if (stickyBottomQuietTimer) { clearTimeout(stickyBottomQuietTimer); stickyBottomQuietTimer = null; }
174
- if (stickyBottomCeilingTimer) { clearTimeout(stickyBottomCeilingTimer); stickyBottomCeilingTimer = null; }
175
- }
176
-
177
- export function initRendering() {
178
- // Update input placeholder when vendor changes
179
- store.subscribe(function (state, prev) {
180
- if (state.currentVendor !== prev.currentVendor) {
181
- var inputEl = document.getElementById("input");
182
- if (inputEl) {
183
- inputEl.placeholder = "Message " + (VENDOR_NAMES[state.currentVendor] || VENDOR_NAMES.claude) + "...";
184
- }
185
- }
186
- });
187
- }
188
-
189
- // --- State accessors (module-local, not in store) ---
190
- export function getTurnCounter() { return turnCounter; }
191
- export function setTurnCounter(v) { turnCounter = v; }
192
- export function getPrependAnchor() { return prependAnchor; }
193
- export function setPrependAnchor(v) { prependAnchor = v; }
194
- export function getActivityEl() { return activityEl; }
195
- export function setActivityEl(v) { activityEl = v; }
196
- export function getIsUserScrolledUp() { return isUserScrolledUp; }
197
- export function setIsUserScrolledUp(v) { isUserScrolledUp = v; }
198
-
199
- // --- Rendering functions ---
200
-
201
- export function addToMessages(el) {
202
- var messagesEl = getMessagesEl();
203
- if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
204
- else messagesEl.appendChild(el);
205
- var _sme = getScheduledMsgEl();
206
- if (_sme && el !== _sme && _sme.parentNode === messagesEl) {
207
- messagesEl.appendChild(_sme);
208
- }
209
- }
210
-
211
- export function scrollToBottom() {
212
- if (prependAnchor) return;
213
- var messagesEl = getMessagesEl();
214
- if (!messagesEl) return;
215
- // Compute the user's current scroll position from the DOM rather than
216
- // the cached isUserScrolledUp flag. The flag is updated by an async
217
- // scroll-event listener; if a delta arrives between the user's wheel
218
- // input and that listener firing, the cached flag is still false and
219
- // we'd snap them back to the bottom against their intent. Reading
220
- // scrollTop/scrollHeight here is synchronous and race-free.
221
- var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
222
- if (distFromBottom > 150 || isUserScrolledUp) {
223
- // Keep the flag and the "New activity" button in sync even when the
224
- // scroll-listener hasn't fired yet, so other call sites observing
225
- // isUserScrolledUp see the truth too.
226
- if (distFromBottom > 150) isUserScrolledUp = true;
227
- var newMsgBtn = document.getElementById("new-msg-btn");
228
- if (newMsgBtn) {
229
- newMsgBtn.textContent = NEW_MSG_BTN_ACTIVITY;
230
- newMsgBtn.classList.remove("hidden");
231
- }
232
- return;
233
- }
234
- requestAnimationFrame(function () {
235
- // Re-check just before the actual write — the user may have scrolled
236
- // up during the frame between the synchronous gate above and this rAF
237
- // firing. Without this re-check the snap-back race re-emerges at a
238
- // single-frame granularity.
239
- var dist = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
240
- if (dist > 150) {
241
- isUserScrolledUp = true;
242
- return;
243
- }
244
- messagesEl.scrollTop = messagesEl.scrollHeight;
245
- });
246
- }
247
-
248
- export function forceScrollToBottom() {
249
- if (prependAnchor) return;
250
- // Arm sticky-bottom mode so deferred layout (tool widgets, code highlighting,
251
- // image loads) can't strand the user partway down — single-rAF pin captures
252
- // a stale scrollHeight, then growth below pushes the bottom further away.
253
- // The quiet detector extends the window automatically while layout shifts.
254
- armStickyBottom(750);
255
- }
256
-
257
- export function getMsgTime() {
258
- var _ts = store.get('currentMsgTs');
259
- var d = _ts ? new Date(_ts) : new Date();
260
- var time = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
261
- var now = new Date();
262
- if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) {
263
- return time;
264
- }
265
- return (d.getMonth() + 1) + "/" + d.getDate() + " " + time;
266
- }
267
-
268
- export function shouldGroupMessage(senderClass) {
269
- var _s = store.snap();
270
- if (_s.replayingHistory && !_s.currentMsgTs) return false;
271
- var prev = getMessagesEl().lastElementChild;
272
- if (!prev || !prev.classList.contains(senderClass)) return false;
273
- var prevTime = prev.querySelector(".dm-bubble-time");
274
- if (!prevTime) return false;
275
- return prevTime.textContent === getMsgTime();
276
- }
277
21
 
278
22
  export function ensureAssistantBlock() {
279
23
  var _el = store.get('currentMsgEl');
280
24
  if (!_el) {
281
- _el = document.createElement("div");
282
- _el.className = "msg-assistant";
283
- _el.dataset.turn = turnCounter;
284
-
285
- var grouped = shouldGroupMessage("msg-assistant");
286
- if (grouped) _el.classList.add("grouped");
287
-
288
25
  var _isDm2 = document.body.classList.contains("mate-dm-active") && document.body.dataset.mateAvatarUrl;
289
26
  var vendor = store.get('currentVendor') || "claude";
290
- var avi = document.createElement("img");
291
- avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
292
- avi.src = _isDm2 ? document.body.dataset.mateAvatarUrl : (VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude);
293
- _el.appendChild(avi);
294
-
295
- var contentWrap = document.createElement("div");
296
- contentWrap.className = "dm-bubble-content";
297
-
298
- var header = document.createElement("div");
299
- header.className = "dm-bubble-header";
300
- var nameSpan = document.createElement("span");
301
- nameSpan.className = "dm-bubble-name";
302
27
  var dmTarget = store.get('dmTargetUser');
303
- nameSpan.textContent = _isDm2 ? ((dmTarget && dmTarget.displayName) || "Mate") : (VENDOR_NAMES[vendor] || VENDOR_NAMES.claude);
304
- header.appendChild(nameSpan);
305
- var timeSpan = document.createElement("span");
306
- timeSpan.className = "dm-bubble-time";
307
- timeSpan.textContent = getMsgTime();
308
- header.appendChild(timeSpan);
309
- contentWrap.appendChild(header);
310
-
311
- var mdDiv = document.createElement("div");
312
- mdDiv.className = "md-content";
313
- mdDiv.dir = "auto";
314
- contentWrap.appendChild(mdDiv);
315
- _el.appendChild(contentWrap);
28
+ _el = createAssistantBubble({
29
+ turn: getTurnCounter(),
30
+ avatarUrl: _isDm2 ? document.body.dataset.mateAvatarUrl : (VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude),
31
+ name: _isDm2 ? ((dmTarget && dmTarget.displayName) || "Mate") : (VENDOR_NAMES[vendor] || VENDOR_NAMES.claude),
32
+ time: getMsgTime(),
33
+ });
34
+ if (shouldGroupMessage("msg-assistant")) _el.classList.add("grouped");
316
35
  addToMessages(_el);
317
36
  store.set({ currentMsgEl: _el, currentFullText: "" });
318
37
  }
@@ -320,47 +39,7 @@ export function ensureAssistantBlock() {
320
39
  }
321
40
 
322
41
  export function addCopyHandler(msgEl, rawText) {
323
- var primed = false;
324
- var resetTimer = null;
325
-
326
- var isTouchDevice = "ontouchstart" in window;
327
-
328
- var hint = document.createElement("div");
329
- hint.className = "msg-copy-hint";
330
- hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
331
- msgEl.appendChild(hint);
332
-
333
- function reset() {
334
- primed = false;
335
- msgEl.classList.remove("copy-primed", "copy-done");
336
- hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
337
- }
338
-
339
- msgEl.addEventListener("click", function (e) {
340
- if (e.target.closest("a, pre, code")) return;
341
- var sel = window.getSelection();
342
- if (sel && sel.toString().length > 0) return;
343
-
344
- if (!primed) {
345
- primed = true;
346
- msgEl.classList.add("copy-primed");
347
- hint.textContent = isTouchDevice ? "Tap again to grab" : "Click again to grab";
348
- clearTimeout(resetTimer);
349
- resetTimer = setTimeout(reset, 3000);
350
- } else {
351
- clearTimeout(resetTimer);
352
- copyToClipboard(rawText).then(function () {
353
- msgEl.classList.remove("copy-primed");
354
- msgEl.classList.add("copy-done");
355
- hint.textContent = "Grabbed!";
356
- resetTimer = setTimeout(reset, 1500);
357
- });
358
- }
359
- });
360
-
361
- document.addEventListener("click", function (e) {
362
- if (primed && !msgEl.contains(e.target)) reset();
363
- });
42
+ addAssistantCopyHandler(msgEl, rawText);
364
43
  }
365
44
 
366
45
  export function appendDelta(text) {
@@ -389,12 +68,11 @@ function drainStreamTick() {
389
68
  var newText = _s.currentFullText + chunk;
390
69
  store.set({ currentFullText: newText });
391
70
 
392
- var contentEl = _s.currentMsgEl.querySelector(".md-content");
393
- contentEl.innerHTML = renderMarkdown(newText);
71
+ renderAssistantBubbleText(_s.currentMsgEl, newText, false);
394
72
 
395
73
  if (highlightTimer) clearTimeout(highlightTimer);
396
74
  highlightTimer = setTimeout(function () {
397
- highlightCodeBlocks(contentEl);
75
+ enhanceAssistantBubble(_s.currentMsgEl, false);
398
76
  }, 150);
399
77
 
400
78
  scrollToBottom();
@@ -414,8 +92,7 @@ export function flushStreamBuffer() {
414
92
  if (_s.currentMsgEl) {
415
93
  var contentEl = _s.currentMsgEl.querySelector(".md-content");
416
94
  if (contentEl) {
417
- contentEl.innerHTML = renderMarkdown(_s.currentFullText);
418
- highlightCodeBlocks(contentEl);
95
+ renderAssistantBubbleText(_s.currentMsgEl, _s.currentFullText, true);
419
96
  }
420
97
  }
421
98
  }
@@ -424,271 +101,12 @@ export function finalizeAssistantBlock() {
424
101
  flushStreamBuffer();
425
102
  var _s = store.snap();
426
103
  if (_s.currentMsgEl) {
427
- var contentEl = _s.currentMsgEl.querySelector(".md-content");
428
- if (contentEl) {
429
- highlightCodeBlocks(contentEl);
430
- renderMermaidBlocks(contentEl);
431
- }
432
- if (_s.currentFullText) {
433
- addCopyHandler(_s.currentMsgEl, _s.currentFullText);
434
- }
104
+ finalizeAssistantBubble(_s.currentMsgEl, _s.currentFullText, !!_s.currentFullText);
435
105
  closeToolGroup();
436
106
  }
437
107
  store.set({ currentMsgEl: null, currentFullText: "" });
438
108
  }
439
109
 
440
- export function addUserMessage(text, images, pastes, fromUserId, fromUserName, delegatedMeta) {
441
- if (!text && (!images || images.length === 0) && (!pastes || pastes.length === 0)) return;
442
- var myUserId = store.get('myUserId');
443
- var isDelegated = !!delegatedMeta;
444
- var isOtherUser = isDelegated || (fromUserId && fromUserId !== myUserId);
445
- var div = document.createElement("div");
446
- div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
447
- if (isDelegated) div.classList.add("msg-user-delegated");
448
- div.dataset.turn = ++turnCounter;
449
- if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
450
- var bubble = document.createElement("div");
451
- bubble.className = "bubble";
452
- bubble.dir = "auto";
453
-
454
- // Delegated work orders render as a self-contained card: identity lives
455
- // in an in-card header strip (the generic bubble avatar/header are hidden
456
- // via CSS) and the body is clean markdown on a neutral background.
457
- if (isDelegated) {
458
- var cardHead = document.createElement("div");
459
- cardHead.className = "delegated-card-head";
460
- var cardAvi = document.createElement("img");
461
- cardAvi.className = "delegated-card-avatar";
462
- cardAvi.src = VENDOR_AVATARS[delegatedMeta.delegatedByVendor] || VENDOR_AVATARS.claude;
463
- cardAvi.alt = "";
464
- cardHead.appendChild(cardAvi);
465
- var cardHeadText = document.createElement("div");
466
- cardHeadText.className = "delegated-card-headtext";
467
- var eyebrow = document.createElement("span");
468
- eyebrow.className = "delegated-card-eyebrow";
469
- eyebrow.textContent = "Delegated task";
470
- cardHeadText.appendChild(eyebrow);
471
- var cardFrom = document.createElement("span");
472
- cardFrom.className = "delegated-card-from";
473
- cardFrom.textContent = delegatedMeta.delegatedByTitle || "Driver";
474
- cardHeadText.appendChild(cardFrom);
475
- cardHead.appendChild(cardHeadText);
476
- var cardTime = document.createElement("span");
477
- cardTime.className = "delegated-card-time";
478
- cardTime.textContent = getMsgTime();
479
- cardHead.appendChild(cardTime);
480
- bubble.appendChild(cardHead);
481
- }
482
-
483
- if (images && images.length > 0) {
484
- var imgRow = document.createElement("div");
485
- imgRow.className = "bubble-images";
486
- for (var i = 0; i < images.length; i++) {
487
- var img = document.createElement("img");
488
- if (images[i].url) {
489
- img.src = images[i].url;
490
- } else if (images[i].data) {
491
- img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
492
- }
493
- img.loading = "lazy";
494
- img.className = "bubble-img";
495
- img.addEventListener("click", function () { showImageModal(this.src); });
496
- img.addEventListener("error", function () {
497
- var placeholder = document.createElement("div");
498
- placeholder.className = "bubble-img-expired";
499
- placeholder.textContent = "Image deleted";
500
- this.parentNode.replaceChild(placeholder, this);
501
- });
502
- imgRow.appendChild(img);
503
- }
504
- bubble.appendChild(imgRow);
505
- }
506
-
507
- if (pastes && pastes.length > 0) {
508
- var pasteRow = document.createElement("div");
509
- pasteRow.className = "bubble-pastes";
510
- for (var p = 0; p < pastes.length; p++) {
511
- (function (pasteText) {
512
- var chip = document.createElement("div");
513
- chip.className = "bubble-paste";
514
- var preview = pasteText.substring(0, 60).replace(/\n/g, " ");
515
- if (pasteText.length > 60) preview += "...";
516
- chip.innerHTML = '<span class="bubble-paste-preview">' + escapeHtml(preview) + '</span><span class="bubble-paste-label">PASTED</span>';
517
- chip.addEventListener("click", function (e) {
518
- e.stopPropagation();
519
- showPasteModal(pasteText);
520
- });
521
- pasteRow.appendChild(chip);
522
- })(pastes[p]);
523
- }
524
- bubble.appendChild(pasteRow);
525
- }
526
-
527
- if (text) {
528
- if (isDelegated) {
529
- // Delegated instructions are agent-authored work orders: render them
530
- // as markdown (headings, lists, code) instead of a plain text blob.
531
- var mdEl = document.createElement("div");
532
- mdEl.className = "md-content delegated-brief";
533
- mdEl.innerHTML = renderMarkdown(text);
534
- highlightCodeBlocks(mdEl);
535
- bubble.appendChild(mdEl);
536
- } else {
537
- var textEl = document.createElement("span");
538
- textEl.textContent = text;
539
- bubble.appendChild(textEl);
540
- }
541
- }
542
-
543
-
544
- var cachedAllUsers = store.get('cachedAllUsers');
545
- var _targetUser;
546
- var _displayName;
547
- if (isDelegated) {
548
- _targetUser = null;
549
- _displayName = "from " + (delegatedMeta.delegatedByTitle || "Driver");
550
- } else if (isOtherUser) {
551
- _targetUser = cachedAllUsers.find(function (u) { return u.id === fromUserId; });
552
- _displayName = fromUserName || (_targetUser && (_targetUser.displayName || _targetUser.username)) || "User";
553
- } else {
554
- _targetUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
555
- _displayName = document.body.dataset.myDisplayName || "";
556
- if (!_displayName) {
557
- _displayName = (_targetUser && (_targetUser.displayName || _targetUser.username)) || "Me";
558
- }
559
- }
560
-
561
- var avi = document.createElement("img");
562
- avi.className = "dm-bubble-avatar" + (isOtherUser ? " dm-bubble-avatar-other" : " dm-bubble-avatar-me");
563
- avi.src = isDelegated
564
- ? (VENDOR_AVATARS[delegatedMeta.delegatedByVendor] || VENDOR_AVATARS.claude)
565
- : isOtherUser
566
- ? userAvatarUrl(_targetUser || { id: fromUserId }, 36)
567
- : (document.body.dataset.myAvatarUrl || userAvatarUrl(_targetUser || { id: myUserId }, 36));
568
- div.appendChild(avi);
569
-
570
- var contentWrap = document.createElement("div");
571
- contentWrap.className = "dm-bubble-content";
572
-
573
- var header = document.createElement("div");
574
- header.className = "dm-bubble-header";
575
- var nameSpan = document.createElement("span");
576
- nameSpan.className = "dm-bubble-name";
577
- nameSpan.textContent = _displayName;
578
- header.appendChild(nameSpan);
579
- var timeSpan = document.createElement("span");
580
- timeSpan.className = "dm-bubble-time";
581
- timeSpan.textContent = getMsgTime();
582
- header.appendChild(timeSpan);
583
- contentWrap.appendChild(header);
584
- contentWrap.appendChild(bubble);
585
- div.appendChild(contentWrap);
586
-
587
- var actions = document.createElement("div");
588
- actions.className = "msg-actions";
589
- actions.innerHTML =
590
- '<span class="msg-action-time">' + getMsgTime() + '</span>' +
591
- '<button class="msg-action-btn msg-action-copy" type="button" title="Copy">' + iconHtml("copy") + '</button>' +
592
- '<button class="msg-action-btn msg-action-fork" type="button" title="Fork">' + iconHtml("git-branch") + '</button>' +
593
- (((store.get('vendorCapabilities') || {}).rewind !== false) ? '<button class="msg-action-btn msg-action-rewind msg-user-rewind-btn" type="button" title="Rewind">' + iconHtml("rotate-ccw") + '</button>' : '') +
594
- '<button class="msg-action-btn msg-action-hidden msg-action-edit" type="button" title="Edit">' + iconHtml("pencil") + '</button>';
595
- div.appendChild(actions);
596
-
597
- actions.querySelector(".msg-action-copy").addEventListener("click", function () {
598
- var self = this;
599
- copyToClipboard(text || "");
600
- self.innerHTML = iconHtml("check");
601
- refreshIcons();
602
- setTimeout(function () { self.innerHTML = iconHtml("copy"); refreshIcons(); }, 1200);
603
- });
604
-
605
- addToMessages(div);
606
- refreshIcons();
607
- forceScrollToBottom();
608
- }
609
-
610
- export function addSystemMessage(text, isError) {
611
- var div = document.createElement("div");
612
- div.className = "sys-msg" + (isError ? " error" : "");
613
- div.innerHTML = '<span class="sys-text"></span>';
614
- div.querySelector(".sys-text").textContent = text;
615
- addToMessages(div);
616
- scrollToBottom();
617
- }
618
-
619
- export function addConflictMessage(msg) {
620
- var div = document.createElement("div");
621
- div.className = "conflict-msg";
622
- var header = document.createElement("div");
623
- header.className = "conflict-header";
624
- header.textContent = msg.text || "Another Claude Code process is already running.";
625
- div.appendChild(header);
626
-
627
- var hint = document.createElement("div");
628
- hint.className = "conflict-hint";
629
- hint.textContent = "Kill the conflicting process to continue, or use the existing Claude Code session.";
630
- div.appendChild(hint);
631
-
632
- for (var i = 0; i < msg.processes.length; i++) {
633
- var p = msg.processes[i];
634
- var row = document.createElement("div");
635
- row.className = "conflict-process";
636
-
637
- var info = document.createElement("span");
638
- info.className = "conflict-pid";
639
- info.textContent = "PID " + p.pid;
640
- row.appendChild(info);
641
-
642
- var cmd = document.createElement("code");
643
- cmd.className = "conflict-cmd";
644
- cmd.textContent = p.command.length > 80 ? p.command.substring(0, 80) + "..." : p.command;
645
- cmd.title = p.command;
646
- row.appendChild(cmd);
647
-
648
- var killBtn = document.createElement("button");
649
- killBtn.className = "conflict-kill-btn";
650
- killBtn.textContent = "Kill Process";
651
- killBtn.setAttribute("data-pid", p.pid);
652
- killBtn.addEventListener("click", function() {
653
- var pid = parseInt(this.getAttribute("data-pid"), 10);
654
- getWs().send(JSON.stringify({ type: "kill_process", pid: pid }));
655
- this.disabled = true;
656
- this.textContent = "Killing...";
657
- });
658
- row.appendChild(killBtn);
659
- div.appendChild(row);
660
- }
661
-
662
- addToMessages(div);
663
- scrollToBottom();
664
- }
665
-
666
- export function addContextOverflowMessage(msg) {
667
- var div = document.createElement("div");
668
- div.className = "context-overflow-msg";
669
-
670
- var header = document.createElement("div");
671
- header.className = "context-overflow-header";
672
- header.textContent = msg.text || "Conversation too long to continue.";
673
- div.appendChild(header);
674
-
675
- var hint = document.createElement("div");
676
- hint.className = "context-overflow-hint";
677
- hint.textContent = "The conversation has exceeded the model's context limit. Please start a new conversation to continue.";
678
- div.appendChild(hint);
679
-
680
- var btn = document.createElement("button");
681
- btn.className = "context-overflow-btn";
682
- btn.textContent = "New Conversation";
683
- btn.addEventListener("click", function() {
684
- getWs().send(JSON.stringify({ type: "new_session" }));
685
- });
686
- div.appendChild(btn);
687
-
688
- addToMessages(div);
689
- scrollToBottom();
690
- }
691
-
692
110
  // --- Pre-thinking (instant dots before server responds) ---
693
111
 
694
112
  export function showClaudePreThinking() {
@@ -717,8 +135,9 @@ function doShowMatePreThinking(mateName, mateAvatar) {
717
135
  '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>' +
718
136
  '</div>';
719
137
  store.set({ matePreThinkingEl: _el });
720
- if (activityEl && activityEl.parentNode) {
721
- activityEl.parentNode.insertBefore(_el, activityEl);
138
+ var activity = getActivityEl();
139
+ if (activity && activity.parentNode) {
140
+ activity.parentNode.insertBefore(_el, activity);
722
141
  } else {
723
142
  addToMessages(_el);
724
143
  }