clay-server 2.31.0 → 2.32.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 (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +97 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -1,15 +1,53 @@
1
1
  // app-dm.js - DM mode, mate project switching, mate onboarding
2
2
  // Extracted from app.js (PR-24)
3
3
 
4
- var _ctx = null;
4
+ import { store } from './store.js';
5
+ import { getWs } from './ws-ref.js';
6
+ import { getMessagesEl, getInputEl } from './dom-refs.js';
7
+ import { userAvatarUrl, mateAvatarUrl } from './avatar.js';
8
+ import { connect } from './app-connection.js';
9
+ import { resetClientState, renderProjectList, getCachedProjects } from './app-projects.js';
10
+ import { scrollToBottom } from './app-rendering.js';
11
+ import { autoResize } from './input.js';
12
+ import { showDebateSticky } from './app-debate-ui.js';
13
+ import { updateDmBadge, setCurrentDmUser, closeDmUserPicker } from './sidebar-mates.js';
14
+ import { hideHomeHub } from './app-home-hub.js';
15
+ import { hideNotes } from './sticky-notes.js';
16
+ import { showMateSidebar, hideMateSidebar } from './mate-sidebar.js';
17
+ import { hideKnowledge } from './mate-knowledge.js';
18
+ import { hideMemory } from './mate-memory.js';
19
+ import { closeFileViewer } from './filebrowser.js';
20
+ import { closeTerminal } from './terminal.js';
21
+ import { openMobileSheet, setMobileSheetMateData } from './sidebar-mobile.js';
22
+ import { getProfileLang } from './profile.js';
23
+ import { isSchedulerOpen, closeScheduler } from './scheduler.js';
24
+ import { requireClayMateInterview } from './app-skills-install.js';
25
+ import { syncResizeHandles } from './sidebar.js';
5
26
 
6
27
  var MATE_ONBOARDING_KEY = "clay-mate-onboarding-shown";
7
28
  var CLAUDE_CODE_AVATAR = "/claude-code-avatar.png";
8
29
  var bgMateIoTimers = {};
9
30
  var dmTypingTimer = null;
10
31
 
11
- export function initDm(ctx) {
12
- _ctx = ctx;
32
+ export function initDm() {
33
+ // --- Reactive UI sync for dmMode ---
34
+ store.subscribe(function (state, prev) {
35
+ if (state.dmMode !== prev.dmMode) {
36
+ var isMate = state.dmTargetUser && state.dmTargetUser.isMate;
37
+ var mainCol = document.getElementById("main-column");
38
+ var sidebarCol = document.getElementById("sidebar-column");
39
+ var resizeHandle = document.getElementById("sidebar-resize-handle");
40
+ if (state.dmMode) {
41
+ if (!isMate && mainCol) mainCol.classList.add("dm-mode");
42
+ if (sidebarCol) sidebarCol.classList.add("dm-mode");
43
+ if (resizeHandle) resizeHandle.classList.add("dm-mode");
44
+ } else {
45
+ if (mainCol) mainCol.classList.remove("dm-mode");
46
+ if (sidebarCol) sidebarCol.classList.remove("dm-mode");
47
+ if (resizeHandle) resizeHandle.classList.remove("dm-mode");
48
+ }
49
+ }
50
+ });
13
51
 
14
52
  // --- Mobile mate title bar click handlers ---
15
53
  var mobileBack = document.getElementById("mate-mobile-back");
@@ -24,30 +62,32 @@ export function initDm(ctx) {
24
62
  if (mobileMore) {
25
63
  mobileMore.addEventListener("click", function (e) {
26
64
  e.stopPropagation();
27
- _ctx.openMobileSheet("mate-profile");
65
+ openMobileSheet("mate-profile");
28
66
  });
29
67
  }
30
68
  if (mobileTitle) {
31
69
  mobileTitle.addEventListener("click", function () {
32
- _ctx.openMobileSheet("mate-profile");
70
+ openMobileSheet("mate-profile");
33
71
  });
34
72
  }
35
73
  }
36
74
 
37
75
  export function openDm(targetUserId) {
38
- if (!_ctx.ws || _ctx.ws.readyState !== 1) return;
76
+ var ws = getWs();
77
+ if (!ws || ws.readyState !== 1) return;
39
78
  // Persist DM state for refresh recovery
40
79
  try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
41
80
  // Check mate skill updates before opening mate DM
42
81
  if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
43
82
  showMateOnboarding(function () {
44
- _ctx.requireClayMateInterview(function () {
45
- _ctx.ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
83
+ requireClayMateInterview(function () {
84
+ var ws2 = getWs();
85
+ if (ws2) ws2.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
46
86
  });
47
87
  });
48
88
  return;
49
89
  }
50
- _ctx.ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
90
+ ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
51
91
  }
52
92
 
53
93
  function showMateOnboarding(callback) {
@@ -96,11 +136,12 @@ function showMateOnboarding(callback) {
96
136
 
97
137
  export function enterDmMode(key, targetUser, messages) {
98
138
  console.log("[DEBUG enterDmMode] key=" + key, "isMate=" + (targetUser && targetUser.isMate), "messages=" + (messages ? messages.length : 0));
139
+ var s = store.snap();
99
140
  // Clean up previous DM/mate state before entering new one
100
- if (_ctx.dmMode) {
101
- _ctx.hideMateSidebar();
102
- _ctx.hideKnowledge();
103
- _ctx.hideMemory();
141
+ if (s.dmMode) {
142
+ hideMateSidebar();
143
+ hideKnowledge();
144
+ hideMemory();
104
145
  // Reset dm-header-bar
105
146
  var prevHeader = document.getElementById("dm-header-bar");
106
147
  if (prevHeader) {
@@ -109,13 +150,7 @@ export function enterDmMode(key, targetUser, messages) {
109
150
  var prevTag = prevHeader.querySelector(".dm-header-mate-tag");
110
151
  if (prevTag) prevTag.remove();
111
152
  }
112
- // Remove dm-mode classes
113
- var prevMain = document.getElementById("main-column");
114
- if (prevMain) prevMain.classList.remove("dm-mode");
115
- var prevSidebar = document.getElementById("sidebar-column");
116
- if (prevSidebar) prevSidebar.classList.remove("dm-mode");
117
- var prevResize = document.getElementById("sidebar-resize-handle");
118
- if (prevResize) prevResize.classList.remove("dm-mode");
153
+ // dm-mode CSS classes stay managed by the store subscriber.
119
154
  // Reset chat title bar
120
155
  var prevTitleBar = document.querySelector(".title-bar-content");
121
156
  if (prevTitleBar) {
@@ -124,9 +159,7 @@ export function enterDmMode(key, targetUser, messages) {
124
159
  }
125
160
  }
126
161
 
127
- _ctx.dmMode = true;
128
- _ctx.dmKey = key;
129
- _ctx.dmTargetUser = targetUser;
162
+ store.set({ dmMode: true, dmKey: key, dmTargetUser: targetUser });
130
163
 
131
164
  // Notify server of active mate DM (server-side presence tracking)
132
165
  // IMPORTANT: set_mate_dm must go to the MAIN project, not a mate project WS.
@@ -135,52 +168,48 @@ export function enterDmMode(key, targetUser, messages) {
135
168
  // The server will also receive it via the mate project's onDmMessage handler,
136
169
  // but the presence should only be stored on the main project slug.
137
170
  if (targetUser && targetUser.isMate) {
171
+ var ws = getWs();
138
172
  // Send to the current WS only if it's the main project (not another mate)
139
- if (!_ctx.mateProjectSlug && _ctx.ws && _ctx.ws.readyState === 1) {
140
- try { _ctx.ws.send(JSON.stringify({ type: "set_mate_dm", mateId: targetUser.id })); } catch(e) {}
173
+ if (!store.get('mateProjectSlug') && ws && ws.readyState === 1) {
174
+ try { ws.send(JSON.stringify({ type: "set_mate_dm", mateId: targetUser.id })); } catch(e) {}
141
175
  }
142
176
  }
143
177
 
144
178
  // Clear unread for this user
145
179
  if (targetUser) {
146
- _ctx.dmUnread[targetUser.id] = 0;
147
- _ctx.updateDmBadge(targetUser.id, 0);
180
+ store.get('dmUnread')[targetUser.id] = 0;
181
+ updateDmBadge(targetUser.id, 0);
148
182
  }
149
183
 
150
184
  // Update icon strip active state
151
- _ctx.setCurrentDmUser(targetUser ? targetUser.id : null);
185
+ setCurrentDmUser(targetUser ? targetUser.id : null);
152
186
  var activeProj = document.querySelector("#icon-strip-projects .icon-strip-item.active");
153
187
  if (activeProj) activeProj.classList.remove("active");
154
188
  var homeIcon = document.querySelector(".icon-strip-home");
155
189
  if (homeIcon) homeIcon.classList.remove("active");
156
190
  // Re-render user strip to show active state
157
- if (_ctx.cachedProjects && _ctx.cachedProjects.length > 0) {
158
- _ctx.renderProjectList();
191
+ var cp = getCachedProjects();
192
+ if (cp && cp.length > 0) {
193
+ renderProjectList();
159
194
  }
160
195
 
161
196
  // Hide home hub if visible
162
- _ctx.hideHomeHub();
197
+ hideHomeHub();
163
198
 
164
199
  // Hide sticky notes if visible
165
- _ctx.hideNotes();
200
+ hideNotes();
166
201
 
167
202
  var isMate = targetUser && targetUser.isMate;
168
203
 
169
- // Hide project UI + sidebar, show DM UI
170
- var mainCol = document.getElementById("main-column");
171
- if (mainCol && !isMate) mainCol.classList.add("dm-mode");
172
- var sidebarCol = document.getElementById("sidebar-column");
173
- if (sidebarCol) sidebarCol.classList.add("dm-mode");
174
- var resizeHandle = document.getElementById("sidebar-resize-handle");
175
- if (resizeHandle) resizeHandle.classList.add("dm-mode");
204
+ // dm-mode CSS classes are handled by the store subscriber above.
176
205
  // Sync resize handles after DM sidebar appears
177
- setTimeout(function () { if (_ctx.syncResizeHandles) _ctx.syncResizeHandles(); }, 50);
206
+ setTimeout(function () { syncResizeHandles(); }, 50);
178
207
  if (isMate && targetUser.projectSlug) {
179
208
  // Mate DM: switch to mate's project (same as project switching)
180
- _ctx.showMateSidebar(targetUser.id, targetUser);
209
+ showMateSidebar(targetUser.id, targetUser);
181
210
  // Close file viewer and terminal panel BEFORE switching WS (needs old WS still open)
182
- try { _ctx.closeFileViewer(); } catch(e) {}
183
- _ctx.closeTerminal();
211
+ try { closeFileViewer(); } catch(e) {}
212
+ closeTerminal();
184
213
  // Apply mate color to chat title bar and panels
185
214
  var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#7c3aed";
186
215
  document.body.style.setProperty("--mate-color", mateColor);
@@ -188,12 +217,12 @@ export function enterDmMode(key, targetUser, messages) {
188
217
  document.body.classList.add("mate-dm-active");
189
218
  // Build mate avatar URL for DM bubble injection
190
219
  var mp = targetUser.profile || {};
191
- var mateAvUrlDm = _ctx.mateAvatarUrl(targetUser, 36);
192
- var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
220
+ var mateAvUrlDm = mateAvatarUrl(targetUser, 36);
221
+ var myUser = store.get('cachedAllUsers').find(function (u) { return u.id === store.get('myUserId'); });
193
222
  if (!myUser) {
194
223
  try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
195
224
  }
196
- var myAvatarUrl = _ctx.userAvatarUrl(myUser || { id: _ctx.myUserId }, 36);
225
+ var myAvatarUrl = userAvatarUrl(myUser || { id: store.get('myUserId') }, 36);
197
226
  var myDisplayName = (myUser && myUser.displayName) || "";
198
227
  document.body.dataset.mateAvatarUrl = mateAvUrlDm;
199
228
  document.body.dataset.mateName = mp.displayName || targetUser.displayName || targetUser.name || "";
@@ -219,7 +248,7 @@ export function enterDmMode(key, targetUser, messages) {
219
248
  if (mateMobileStatus) mateMobileStatus.textContent = "online";
220
249
  mateMobileTitle.classList.remove("hidden");
221
250
  // Store mate data for profile sheet
222
- _ctx.setMobileSheetMateData({
251
+ setMobileSheetMateData({
223
252
  id: targetUser.id,
224
253
  displayName: mp.displayName || targetUser.displayName || targetUser.name || "",
225
254
  description: mp.description || targetUser.description || "",
@@ -238,20 +267,22 @@ export function enterDmMode(key, targetUser, messages) {
238
267
  if (userIsland && !isMate) userIsland.classList.add("dm-hidden");
239
268
 
240
269
  // Render DM messages
241
- _ctx.dmMessageCache = messages ? messages.slice() : [];
242
- _ctx.messagesEl.innerHTML = "";
270
+ store.set({ dmMessageCache: messages ? messages.slice() : [] });
271
+ var messagesEl = getMessagesEl();
272
+ messagesEl.innerHTML = "";
243
273
  if (messages && messages.length > 0) {
244
274
  for (var i = 0; i < messages.length; i++) {
245
275
  appendDmMessage(messages[i]);
246
276
  }
247
277
  }
248
- _ctx.scrollToBottom();
278
+ scrollToBottom();
249
279
 
250
280
  // Focus input
251
- if (_ctx.inputEl) {
281
+ var inputEl = getInputEl();
282
+ if (inputEl) {
252
283
  var targetName = targetUser ? ((targetUser.profile && targetUser.profile.displayName) || targetUser.displayName || targetUser.name || "") : "";
253
- _ctx.inputEl.placeholder = "Message " + targetName;
254
- _ctx.inputEl.focus();
284
+ inputEl.placeholder = "Message " + targetName;
285
+ inputEl.focus();
255
286
  }
256
287
 
257
288
  // Populate DM header bar with user avatar, name, and personal color
@@ -265,7 +296,7 @@ export function enterDmMode(key, targetUser, messages) {
265
296
  } else {
266
297
  if (dmHeaderBar) dmHeaderBar.style.display = "";
267
298
  if (dmAvatar) {
268
- dmAvatar.src = _ctx.userAvatarUrl(targetUser, 28);
299
+ dmAvatar.src = userAvatarUrl(targetUser, 28);
269
300
  }
270
301
  if (dmName) dmName.textContent = targetUser.displayName;
271
302
  if (dmHeaderBar && targetUser.avatarColor) {
@@ -279,26 +310,19 @@ export function enterDmMode(key, targetUser, messages) {
279
310
  }
280
311
 
281
312
  export function exitDmMode(skipProjectSwitch) {
282
- if (!_ctx.dmMode) return;
283
- var wasMate = _ctx.dmTargetUser && _ctx.dmTargetUser.isMate;
284
- _ctx.dmMode = false;
285
- _ctx.dmKey = null;
286
- _ctx.dmTargetUser = null;
313
+ if (!store.get('dmMode')) return;
314
+ var wasMate = store.get('dmTargetUser') && store.get('dmTargetUser').isMate;
315
+ store.set({ dmMode: false, dmKey: null, dmTargetUser: null });
287
316
  try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
288
- _ctx.setCurrentDmUser(null);
289
-
290
- var mainCol = document.getElementById("main-column");
291
- if (mainCol) mainCol.classList.remove("dm-mode");
292
- var sidebarCol = document.getElementById("sidebar-column");
293
- if (sidebarCol) sidebarCol.classList.remove("dm-mode");
294
- var resizeHandle = document.getElementById("sidebar-resize-handle");
295
- if (resizeHandle) resizeHandle.classList.remove("dm-mode");
317
+ setCurrentDmUser(null);
318
+
319
+ // dm-mode CSS classes are handled by the store subscriber.
296
320
  // Re-sync resize handle positions after DM width changes (defer to let layout settle)
297
- setTimeout(function () { if (_ctx.syncResizeHandles) _ctx.syncResizeHandles(); }, 100);
298
- _ctx.hideMateSidebar();
299
- _ctx.hideKnowledge();
300
- _ctx.hideMemory();
301
- if (_ctx.isSchedulerOpen()) _ctx.closeScheduler();
321
+ setTimeout(function () { syncResizeHandles(); }, 100);
322
+ hideMateSidebar();
323
+ hideKnowledge();
324
+ hideMemory();
325
+ if (isSchedulerOpen()) closeScheduler();
302
326
  // Reset DM header
303
327
  var dmHeaderBar = document.getElementById("dm-header-bar");
304
328
  if (dmHeaderBar) {
@@ -315,7 +339,8 @@ export function exitDmMode(skipProjectSwitch) {
315
339
  delete document.body.dataset.mateName;
316
340
  delete document.body.dataset.myAvatarUrl;
317
341
  // Remove injected DM bubble avatars
318
- var bubbleAvatars = _ctx.messagesEl.querySelectorAll(".dm-bubble-avatar");
342
+ var messagesEl = getMessagesEl();
343
+ var bubbleAvatars = messagesEl.querySelectorAll(".dm-bubble-avatar");
319
344
  for (var ba = 0; ba < bubbleAvatars.length; ba++) bubbleAvatars[ba].remove();
320
345
  var titleBarContent = document.querySelector(".title-bar-content");
321
346
  if (titleBarContent) {
@@ -330,38 +355,40 @@ export function exitDmMode(skipProjectSwitch) {
330
355
  var userIsland = document.getElementById("user-island");
331
356
  if (userIsland) userIsland.classList.remove("dm-hidden");
332
357
 
333
- if (_ctx.inputEl) _ctx.inputEl.placeholder = "";
358
+ var inputEl = getInputEl();
359
+ if (inputEl) inputEl.placeholder = "";
334
360
 
335
361
  // Switch back to main project (same as project switching)
336
362
  if (wasMate && !skipProjectSwitch) {
337
363
  disconnectMateProject();
338
364
  } else if (wasMate && skipProjectSwitch) {
339
365
  // Just clean up mate state, caller will handle project switch
340
- _ctx.returningFromMateDm = true;
341
- _ctx.mateProjectSlug = null;
342
- _ctx.savedMainSlug = null;
343
- _ctx.showDebateSticky("hide", null);
366
+ store.set({ returningFromMateDm: true, mateProjectSlug: null, savedMainSlug: null });
367
+ showDebateSticky("hide", null);
344
368
  var debateFloat = document.getElementById("debate-info-float");
345
369
  if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
346
370
  } else {
347
371
  // Human DM: just re-request state from main project
348
- if (_ctx.ws && _ctx.ws.readyState === 1) {
349
- _ctx.ws.send(JSON.stringify({ type: "switch_session", id: _ctx.activeSessionId }));
350
- _ctx.ws.send(JSON.stringify({ type: "note_list_request" }));
372
+ var ws = getWs();
373
+ if (ws && ws.readyState === 1) {
374
+ ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
375
+ ws.send(JSON.stringify({ type: "note_list_request" }));
351
376
  }
352
377
  }
353
- _ctx.renderProjectList();
378
+ renderProjectList();
354
379
  }
355
380
 
356
381
  export function handleMateCreatedInApp(mate, msg) {
357
382
  if (!mate) return;
358
- _ctx.cachedMatesList.push(mate);
359
- if (msg && msg.availableBuiltins) _ctx.cachedAvailableBuiltins = msg.availableBuiltins;
360
- if (msg && msg.dmFavorites) _ctx.cachedDmFavorites = msg.dmFavorites;
361
- _ctx.renderUserStrip(_ctx.cachedAllUsers, _ctx.cachedOnlineIds, _ctx.myUserId, _ctx.cachedDmFavorites, _ctx.cachedDmConversations, _ctx.dmUnread, _ctx.dmRemovedUsers, _ctx.cachedMatesList);
383
+ var newMates = store.get('cachedMatesList').concat([mate]);
384
+ var updates = { cachedMatesList: newMates };
385
+ if (msg && msg.availableBuiltins) updates.cachedAvailableBuiltins = msg.availableBuiltins;
386
+ if (msg && msg.dmFavorites) updates.cachedDmFavorites = msg.dmFavorites;
387
+ store.set(updates);
388
+ // renderUserStrip is handled by the store subscriber
362
389
  // Built-in mates handle their own onboarding via CLAUDE.md, skip auto-interview
363
390
  if (!mate.builtinKey) {
364
- _ctx.pendingMateInterview = mate;
391
+ store.set({ pendingMateInterview: mate });
365
392
  }
366
393
  openDm(mate.id);
367
394
  }
@@ -402,18 +429,20 @@ export function renderAvailableBuiltins(builtins) {
402
429
  addBtn.title = "Re-add " + b.displayName;
403
430
  addBtn.addEventListener("click", function (e) {
404
431
  e.stopPropagation();
405
- if (_ctx.ws && _ctx.ws.readyState === 1) {
406
- _ctx.ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
432
+ var ws = getWs();
433
+ if (ws && ws.readyState === 1) {
434
+ ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
407
435
  }
408
- _ctx.closeDmUserPicker();
436
+ closeDmUserPicker();
409
437
  });
410
438
  item.appendChild(addBtn);
411
439
 
412
440
  item.addEventListener("click", function () {
413
- if (_ctx.ws && _ctx.ws.readyState === 1) {
414
- _ctx.ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
441
+ var ws = getWs();
442
+ if (ws && ws.readyState === 1) {
443
+ ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
415
444
  }
416
- _ctx.closeDmUserPicker();
445
+ closeDmUserPicker();
417
446
  });
418
447
 
419
448
  matesList.appendChild(item);
@@ -424,7 +453,7 @@ export function renderAvailableBuiltins(builtins) {
424
453
  export function buildMateInterviewPrompt(mate) {
425
454
  var sd = mate.seedData || {};
426
455
  var parts = [];
427
- var spokenLang = _ctx.getProfileLang() || "en-US";
456
+ var spokenLang = getProfileLang() || "en-US";
428
457
  parts.push("Spoken Language: " + spokenLang);
429
458
  if (sd.relationship) parts.push("Relationship: " + sd.relationship);
430
459
  if (sd.activity && sd.activity.length > 0) parts.push("Activities: " + sd.activity.join(", "));
@@ -449,8 +478,8 @@ export function buildMateInterviewPrompt(mate) {
449
478
  }
450
479
 
451
480
  export function updateMateIconStatus(msg) {
452
- if (!_ctx.mateProjectSlug) return;
453
- var slug = _ctx.mateProjectSlug;
481
+ if (!store.get('mateProjectSlug')) return;
482
+ var slug = store.get('mateProjectSlug');
454
483
  if (msg.type === "content" || msg.type === "tool" || msg.type === "tool_use" || msg.type === "thinking") {
455
484
  var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
456
485
  if (ioDot) {
@@ -474,42 +503,47 @@ export function updateMateIconStatus(msg) {
474
503
  }
475
504
 
476
505
  export function connectMateProject(slug) {
477
- _ctx.mateProjectSlug = slug;
506
+ var s = store.snap();
507
+ store.set({ mateProjectSlug: slug });
478
508
  // Only save the main slug on the FIRST mate switch (preserve original main project)
479
- if (!_ctx.savedMainSlug) _ctx.savedMainSlug = _ctx.currentSlug;
480
- _ctx.currentSlug = slug;
481
- _ctx.wsPath = "/p/" + slug + "/ws";
482
- _ctx.resetClientState();
483
- _ctx.connect();
509
+ if (!s.savedMainSlug) store.set({ savedMainSlug: s.currentSlug });
510
+ store.set({ currentSlug: slug, wsPath: "/p/" + slug + "/ws" });
511
+ resetClientState();
512
+ connect();
484
513
  }
485
514
 
486
515
  export function disconnectMateProject() {
487
- _ctx.mateProjectSlug = null;
516
+ store.set({ mateProjectSlug: null });
488
517
  // Hide debate sticky when leaving mate DM
489
- _ctx.showDebateSticky("hide", null);
518
+ showDebateSticky("hide", null);
490
519
  // Hide debate info float
491
520
  var debateFloat = document.getElementById("debate-info-float");
492
521
  if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
493
522
  // Switch back to main project
494
- if (_ctx.savedMainSlug) {
495
- _ctx.returningFromMateDm = true;
496
- _ctx.currentSlug = _ctx.savedMainSlug;
497
- _ctx.basePath = "/p/" + _ctx.savedMainSlug + "/";
498
- _ctx.wsPath = "/p/" + _ctx.savedMainSlug + "/ws";
499
- _ctx.savedMainSlug = null;
500
- _ctx.resetClientState();
501
- _ctx.connect();
523
+ var savedMainSlug = store.get('savedMainSlug');
524
+ if (savedMainSlug) {
525
+ store.set({
526
+ returningFromMateDm: true,
527
+ currentSlug: savedMainSlug,
528
+ basePath: "/p/" + savedMainSlug + "/",
529
+ wsPath: "/p/" + savedMainSlug + "/ws",
530
+ savedMainSlug: null
531
+ });
532
+ resetClientState();
533
+ connect();
502
534
  }
503
535
  }
504
536
 
505
537
  export function appendDmMessage(msg) {
506
- if (_ctx.dmMode) _ctx.dmMessageCache.push(msg);
507
- var isMe = msg.from === _ctx.myUserId;
538
+ var s = store.snap();
539
+ if (s.dmMode) s.dmMessageCache.push(msg);
540
+ var isMe = msg.from === s.myUserId;
508
541
  var d = new Date(msg.ts);
509
542
  var timeStr = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0");
510
543
 
544
+ var messagesEl = getMessagesEl();
511
545
  // Check if we can compact (same sender as previous, within 5 min)
512
- var prev = _ctx.messagesEl.lastElementChild;
546
+ var prev = messagesEl.lastElementChild;
513
547
  var compact = false;
514
548
  if (prev && prev.dataset.from === msg.from) {
515
549
  var prevTs = parseInt(prev.dataset.ts || "0", 10);
@@ -537,10 +571,10 @@ export function appendDmMessage(msg) {
537
571
  var avatar = document.createElement("img");
538
572
  avatar.className = "dm-msg-avatar";
539
573
  if (isMe) {
540
- var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
541
- avatar.src = _ctx.userAvatarUrl(myUser || { id: _ctx.myUserId }, 36);
542
- } else if (_ctx.dmTargetUser) {
543
- avatar.src = _ctx.userAvatarUrl(_ctx.dmTargetUser, 36);
574
+ var myUser = s.cachedAllUsers.find(function (u) { return u.id === s.myUserId; });
575
+ avatar.src = userAvatarUrl(myUser || { id: s.myUserId }, 36);
576
+ } else if (s.dmTargetUser) {
577
+ avatar.src = userAvatarUrl(s.dmTargetUser, 36);
544
578
  }
545
579
  div.appendChild(avatar);
546
580
 
@@ -553,10 +587,10 @@ export function appendDmMessage(msg) {
553
587
  var name = document.createElement("span");
554
588
  name.className = "dm-msg-name";
555
589
  if (isMe) {
556
- var mu = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
590
+ var mu = s.cachedAllUsers.find(function (u) { return u.id === s.myUserId; });
557
591
  name.textContent = mu ? mu.displayName : "Me";
558
592
  } else {
559
- name.textContent = _ctx.dmTargetUser ? _ctx.dmTargetUser.displayName : "User";
593
+ name.textContent = s.dmTargetUser ? s.dmTargetUser.displayName : "User";
560
594
  }
561
595
  header.appendChild(name);
562
596
 
@@ -575,7 +609,7 @@ export function appendDmMessage(msg) {
575
609
  div.appendChild(content);
576
610
  }
577
611
 
578
- _ctx.messagesEl.appendChild(div);
612
+ messagesEl.appendChild(div);
579
613
  }
580
614
 
581
615
  export function showDmTypingIndicator(typing) {
@@ -585,7 +619,8 @@ export function showDmTypingIndicator(typing) {
585
619
  return;
586
620
  }
587
621
  if (existing) return; // already showing
588
- if (!_ctx.dmTargetUser) return;
622
+ var dmTargetUser = store.get('dmTargetUser');
623
+ if (!dmTargetUser) return;
589
624
 
590
625
  var div = document.createElement("div");
591
626
  div.id = "dm-typing-indicator";
@@ -593,7 +628,7 @@ export function showDmTypingIndicator(typing) {
593
628
 
594
629
  var avatar = document.createElement("img");
595
630
  avatar.className = "dm-msg-avatar";
596
- avatar.src = _ctx.userAvatarUrl(_ctx.dmTargetUser, 36);
631
+ avatar.src = userAvatarUrl(dmTargetUser, 36);
597
632
  div.appendChild(avatar);
598
633
 
599
634
  var dots = document.createElement("div");
@@ -601,8 +636,9 @@ export function showDmTypingIndicator(typing) {
601
636
  dots.innerHTML = "<span></span><span></span><span></span>";
602
637
  div.appendChild(dots);
603
638
 
604
- _ctx.messagesEl.appendChild(div);
605
- _ctx.scrollToBottom();
639
+ var messagesEl = getMessagesEl();
640
+ messagesEl.appendChild(div);
641
+ scrollToBottom();
606
642
 
607
643
  // Auto-hide after 5s in case stop signal is missed
608
644
  clearTimeout(dmTypingTimer);
@@ -612,11 +648,14 @@ export function showDmTypingIndicator(typing) {
612
648
  }
613
649
 
614
650
  export function handleDmSend() {
615
- if (!_ctx.dmMode || !_ctx.dmKey || !_ctx.inputEl) return false;
616
- var text = _ctx.inputEl.value.trim();
651
+ var s = store.snap();
652
+ var inputEl = getInputEl();
653
+ if (!s.dmMode || !s.dmKey || !inputEl) return false;
654
+ var text = inputEl.value.trim();
617
655
  if (!text) return false;
618
- _ctx.ws.send(JSON.stringify({ type: "dm_send", dmKey: _ctx.dmKey, text: text }));
619
- _ctx.inputEl.value = "";
620
- _ctx.autoResize();
656
+ var ws = getWs();
657
+ ws.send(JSON.stringify({ type: "dm_send", dmKey: s.dmKey, text: text }));
658
+ inputEl.value = "";
659
+ autoResize();
621
660
  return true;
622
661
  }