clay-server 2.31.0 → 2.32.0-beta.10

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 (82) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/codex-defaults.js +18 -0
  3. package/lib/debate-mcp-server.js +14 -31
  4. package/lib/mcp-local.js +31 -1
  5. package/lib/project-connection.js +9 -6
  6. package/lib/project-debate.js +8 -0
  7. package/lib/project-filesystem.js +47 -1
  8. package/lib/project-http.js +75 -8
  9. package/lib/project-mate-interaction.js +102 -16
  10. package/lib/project-mcp.js +4 -0
  11. package/lib/project-notifications.js +9 -0
  12. package/lib/project-sessions.js +94 -51
  13. package/lib/project-user-message.js +12 -7
  14. package/lib/project.js +234 -99
  15. package/lib/public/app.js +135 -454
  16. package/lib/public/codex-avatar.png +0 -0
  17. package/lib/public/css/debate.css +3 -2
  18. package/lib/public/css/filebrowser.css +91 -1
  19. package/lib/public/css/icon-strip.css +21 -5
  20. package/lib/public/css/input.css +338 -104
  21. package/lib/public/css/mates.css +43 -0
  22. package/lib/public/css/mention.css +48 -4
  23. package/lib/public/css/menus.css +1 -1
  24. package/lib/public/css/messages.css +2 -0
  25. package/lib/public/css/notifications-center.css +26 -0
  26. package/lib/public/css/tooltip.css +47 -0
  27. package/lib/public/index.html +78 -26
  28. package/lib/public/modules/app-connection.js +138 -37
  29. package/lib/public/modules/app-cursors.js +18 -17
  30. package/lib/public/modules/app-debate-ui.js +9 -9
  31. package/lib/public/modules/app-dm.js +175 -131
  32. package/lib/public/modules/app-favicon.js +28 -26
  33. package/lib/public/modules/app-header.js +79 -68
  34. package/lib/public/modules/app-home-hub.js +55 -47
  35. package/lib/public/modules/app-loop-ui.js +34 -18
  36. package/lib/public/modules/app-loop-wizard.js +6 -6
  37. package/lib/public/modules/app-messages.js +199 -153
  38. package/lib/public/modules/app-misc.js +23 -12
  39. package/lib/public/modules/app-notifications.js +119 -9
  40. package/lib/public/modules/app-panels.js +203 -49
  41. package/lib/public/modules/app-projects.js +161 -150
  42. package/lib/public/modules/app-rate-limit.js +5 -4
  43. package/lib/public/modules/app-rendering.js +149 -101
  44. package/lib/public/modules/app-skills-install.js +4 -4
  45. package/lib/public/modules/context-sources.js +102 -66
  46. package/lib/public/modules/dom-refs.js +21 -0
  47. package/lib/public/modules/filebrowser.js +173 -2
  48. package/lib/public/modules/input.js +122 -0
  49. package/lib/public/modules/markdown.js +5 -1
  50. package/lib/public/modules/mate-sidebar.js +38 -0
  51. package/lib/public/modules/mention.js +24 -6
  52. package/lib/public/modules/scheduler.js +1 -1
  53. package/lib/public/modules/sidebar-mates.js +79 -35
  54. package/lib/public/modules/sidebar-mobile.js +34 -30
  55. package/lib/public/modules/sidebar-projects.js +60 -57
  56. package/lib/public/modules/sidebar-sessions.js +75 -69
  57. package/lib/public/modules/sidebar.js +12 -20
  58. package/lib/public/modules/skills.js +8 -9
  59. package/lib/public/modules/sticky-notes.js +1 -2
  60. package/lib/public/modules/store.js +9 -2
  61. package/lib/public/modules/stt.js +4 -1
  62. package/lib/public/modules/terminal.js +12 -0
  63. package/lib/public/modules/tools.js +18 -13
  64. package/lib/public/modules/tooltip.js +32 -5
  65. package/lib/sdk-bridge.js +562 -1114
  66. package/lib/sdk-message-processor.js +150 -135
  67. package/lib/sdk-worker.js +4 -0
  68. package/lib/server-dm.js +1 -0
  69. package/lib/server.js +86 -1
  70. package/lib/sessions.js +81 -37
  71. package/lib/ws-schema.js +2 -0
  72. package/lib/yoke/adapters/claude-worker.js +559 -0
  73. package/lib/yoke/adapters/claude.js +1483 -0
  74. package/lib/yoke/adapters/codex.js +1121 -0
  75. package/lib/yoke/adapters/gemini.js +709 -0
  76. package/lib/yoke/codex-app-server.js +307 -0
  77. package/lib/yoke/index.js +199 -0
  78. package/lib/yoke/instructions.js +62 -0
  79. package/lib/yoke/interface.js +98 -0
  80. package/lib/yoke/mcp-bridge-server.js +294 -0
  81. package/lib/yoke/package.json +7 -0
  82. 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,50 @@ 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();
213
+ var termBtn = document.getElementById("terminal-toggle-btn");
214
+ if (termBtn) termBtn.style.display = "none";
184
215
  // Apply mate color to chat title bar and panels
185
216
  var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#7c3aed";
186
217
  document.body.style.setProperty("--mate-color", mateColor);
@@ -188,12 +219,12 @@ export function enterDmMode(key, targetUser, messages) {
188
219
  document.body.classList.add("mate-dm-active");
189
220
  // Build mate avatar URL for DM bubble injection
190
221
  var mp = targetUser.profile || {};
191
- var mateAvUrlDm = _ctx.mateAvatarUrl(targetUser, 36);
192
- var myUser = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
222
+ var mateAvUrlDm = mateAvatarUrl(targetUser, 36);
223
+ var myUser = store.get('cachedAllUsers').find(function (u) { return u.id === store.get('myUserId'); });
193
224
  if (!myUser) {
194
225
  try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
195
226
  }
196
- var myAvatarUrl = _ctx.userAvatarUrl(myUser || { id: _ctx.myUserId }, 36);
227
+ var myAvatarUrl = userAvatarUrl(myUser || { id: store.get('myUserId') }, 36);
197
228
  var myDisplayName = (myUser && myUser.displayName) || "";
198
229
  document.body.dataset.mateAvatarUrl = mateAvUrlDm;
199
230
  document.body.dataset.mateName = mp.displayName || targetUser.displayName || targetUser.name || "";
@@ -219,7 +250,7 @@ export function enterDmMode(key, targetUser, messages) {
219
250
  if (mateMobileStatus) mateMobileStatus.textContent = "online";
220
251
  mateMobileTitle.classList.remove("hidden");
221
252
  // Store mate data for profile sheet
222
- _ctx.setMobileSheetMateData({
253
+ setMobileSheetMateData({
223
254
  id: targetUser.id,
224
255
  displayName: mp.displayName || targetUser.displayName || targetUser.name || "",
225
256
  description: mp.description || targetUser.description || "",
@@ -238,20 +269,22 @@ export function enterDmMode(key, targetUser, messages) {
238
269
  if (userIsland && !isMate) userIsland.classList.add("dm-hidden");
239
270
 
240
271
  // Render DM messages
241
- _ctx.dmMessageCache = messages ? messages.slice() : [];
242
- _ctx.messagesEl.innerHTML = "";
272
+ store.set({ dmMessageCache: messages ? messages.slice() : [] });
273
+ var messagesEl = getMessagesEl();
274
+ messagesEl.innerHTML = "";
243
275
  if (messages && messages.length > 0) {
244
276
  for (var i = 0; i < messages.length; i++) {
245
277
  appendDmMessage(messages[i]);
246
278
  }
247
279
  }
248
- _ctx.scrollToBottom();
280
+ scrollToBottom();
249
281
 
250
282
  // Focus input
251
- if (_ctx.inputEl) {
283
+ var inputEl = getInputEl();
284
+ if (inputEl) {
252
285
  var targetName = targetUser ? ((targetUser.profile && targetUser.profile.displayName) || targetUser.displayName || targetUser.name || "") : "";
253
- _ctx.inputEl.placeholder = "Message " + targetName;
254
- _ctx.inputEl.focus();
286
+ inputEl.placeholder = "Message " + targetName;
287
+ inputEl.focus();
255
288
  }
256
289
 
257
290
  // Populate DM header bar with user avatar, name, and personal color
@@ -265,7 +298,7 @@ export function enterDmMode(key, targetUser, messages) {
265
298
  } else {
266
299
  if (dmHeaderBar) dmHeaderBar.style.display = "";
267
300
  if (dmAvatar) {
268
- dmAvatar.src = _ctx.userAvatarUrl(targetUser, 28);
301
+ dmAvatar.src = userAvatarUrl(targetUser, 28);
269
302
  }
270
303
  if (dmName) dmName.textContent = targetUser.displayName;
271
304
  if (dmHeaderBar && targetUser.avatarColor) {
@@ -279,26 +312,22 @@ export function enterDmMode(key, targetUser, messages) {
279
312
  }
280
313
 
281
314
  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;
315
+ if (!store.get('dmMode')) return;
316
+ var wasMate = store.get('dmTargetUser') && store.get('dmTargetUser').isMate;
317
+ store.set({ dmMode: false, dmKey: null, dmTargetUser: null });
287
318
  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");
319
+ setCurrentDmUser(null);
320
+
321
+ // dm-mode CSS classes are handled by the store subscriber.
296
322
  // 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();
323
+ setTimeout(function () { syncResizeHandles(); }, 100);
324
+ hideMateSidebar();
325
+ hideKnowledge();
326
+ hideMemory();
327
+ if (isSchedulerOpen()) closeScheduler();
328
+ // Restore terminal button
329
+ var termBtn = document.getElementById("terminal-toggle-btn");
330
+ if (termBtn) termBtn.style.display = "";
302
331
  // Reset DM header
303
332
  var dmHeaderBar = document.getElementById("dm-header-bar");
304
333
  if (dmHeaderBar) {
@@ -315,7 +344,8 @@ export function exitDmMode(skipProjectSwitch) {
315
344
  delete document.body.dataset.mateName;
316
345
  delete document.body.dataset.myAvatarUrl;
317
346
  // Remove injected DM bubble avatars
318
- var bubbleAvatars = _ctx.messagesEl.querySelectorAll(".dm-bubble-avatar");
347
+ var messagesEl = getMessagesEl();
348
+ var bubbleAvatars = messagesEl.querySelectorAll(".dm-bubble-avatar");
319
349
  for (var ba = 0; ba < bubbleAvatars.length; ba++) bubbleAvatars[ba].remove();
320
350
  var titleBarContent = document.querySelector(".title-bar-content");
321
351
  if (titleBarContent) {
@@ -330,38 +360,40 @@ export function exitDmMode(skipProjectSwitch) {
330
360
  var userIsland = document.getElementById("user-island");
331
361
  if (userIsland) userIsland.classList.remove("dm-hidden");
332
362
 
333
- if (_ctx.inputEl) _ctx.inputEl.placeholder = "";
363
+ var inputEl = getInputEl();
364
+ if (inputEl) inputEl.placeholder = "";
334
365
 
335
366
  // Switch back to main project (same as project switching)
336
367
  if (wasMate && !skipProjectSwitch) {
337
368
  disconnectMateProject();
338
369
  } else if (wasMate && skipProjectSwitch) {
339
370
  // 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);
371
+ store.set({ returningFromMateDm: true, mateProjectSlug: null, savedMainSlug: null });
372
+ showDebateSticky("hide", null);
344
373
  var debateFloat = document.getElementById("debate-info-float");
345
374
  if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
346
375
  } else {
347
376
  // 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" }));
377
+ var ws = getWs();
378
+ if (ws && ws.readyState === 1) {
379
+ ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
380
+ ws.send(JSON.stringify({ type: "note_list_request" }));
351
381
  }
352
382
  }
353
- _ctx.renderProjectList();
383
+ renderProjectList();
354
384
  }
355
385
 
356
386
  export function handleMateCreatedInApp(mate, msg) {
357
387
  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);
388
+ var newMates = store.get('cachedMatesList').concat([mate]);
389
+ var updates = { cachedMatesList: newMates };
390
+ if (msg && msg.availableBuiltins) updates.cachedAvailableBuiltins = msg.availableBuiltins;
391
+ if (msg && msg.dmFavorites) updates.cachedDmFavorites = msg.dmFavorites;
392
+ store.set(updates);
393
+ // renderUserStrip is handled by the store subscriber
362
394
  // Built-in mates handle their own onboarding via CLAUDE.md, skip auto-interview
363
395
  if (!mate.builtinKey) {
364
- _ctx.pendingMateInterview = mate;
396
+ store.set({ pendingMateInterview: mate });
365
397
  }
366
398
  openDm(mate.id);
367
399
  }
@@ -402,18 +434,20 @@ export function renderAvailableBuiltins(builtins) {
402
434
  addBtn.title = "Re-add " + b.displayName;
403
435
  addBtn.addEventListener("click", function (e) {
404
436
  e.stopPropagation();
405
- if (_ctx.ws && _ctx.ws.readyState === 1) {
406
- _ctx.ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
437
+ var ws = getWs();
438
+ if (ws && ws.readyState === 1) {
439
+ ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
407
440
  }
408
- _ctx.closeDmUserPicker();
441
+ closeDmUserPicker();
409
442
  });
410
443
  item.appendChild(addBtn);
411
444
 
412
445
  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 }));
446
+ var ws = getWs();
447
+ if (ws && ws.readyState === 1) {
448
+ ws.send(JSON.stringify({ type: "mate_readd_builtin", builtinKey: b.key }));
415
449
  }
416
- _ctx.closeDmUserPicker();
450
+ closeDmUserPicker();
417
451
  });
418
452
 
419
453
  matesList.appendChild(item);
@@ -424,7 +458,7 @@ export function renderAvailableBuiltins(builtins) {
424
458
  export function buildMateInterviewPrompt(mate) {
425
459
  var sd = mate.seedData || {};
426
460
  var parts = [];
427
- var spokenLang = _ctx.getProfileLang() || "en-US";
461
+ var spokenLang = getProfileLang() || "en-US";
428
462
  parts.push("Spoken Language: " + spokenLang);
429
463
  if (sd.relationship) parts.push("Relationship: " + sd.relationship);
430
464
  if (sd.activity && sd.activity.length > 0) parts.push("Activities: " + sd.activity.join(", "));
@@ -449,8 +483,8 @@ export function buildMateInterviewPrompt(mate) {
449
483
  }
450
484
 
451
485
  export function updateMateIconStatus(msg) {
452
- if (!_ctx.mateProjectSlug) return;
453
- var slug = _ctx.mateProjectSlug;
486
+ if (!store.get('mateProjectSlug')) return;
487
+ var slug = store.get('mateProjectSlug');
454
488
  if (msg.type === "content" || msg.type === "tool" || msg.type === "tool_use" || msg.type === "thinking") {
455
489
  var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
456
490
  if (ioDot) {
@@ -474,42 +508,47 @@ export function updateMateIconStatus(msg) {
474
508
  }
475
509
 
476
510
  export function connectMateProject(slug) {
477
- _ctx.mateProjectSlug = slug;
511
+ var s = store.snap();
512
+ store.set({ mateProjectSlug: slug });
478
513
  // 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();
514
+ if (!s.savedMainSlug) store.set({ savedMainSlug: s.currentSlug });
515
+ store.set({ currentSlug: slug, wsPath: "/p/" + slug + "/ws" });
516
+ resetClientState();
517
+ connect();
484
518
  }
485
519
 
486
520
  export function disconnectMateProject() {
487
- _ctx.mateProjectSlug = null;
521
+ store.set({ mateProjectSlug: null });
488
522
  // Hide debate sticky when leaving mate DM
489
- _ctx.showDebateSticky("hide", null);
523
+ showDebateSticky("hide", null);
490
524
  // Hide debate info float
491
525
  var debateFloat = document.getElementById("debate-info-float");
492
526
  if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
493
527
  // 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();
528
+ var savedMainSlug = store.get('savedMainSlug');
529
+ if (savedMainSlug) {
530
+ store.set({
531
+ returningFromMateDm: true,
532
+ currentSlug: savedMainSlug,
533
+ basePath: "/p/" + savedMainSlug + "/",
534
+ wsPath: "/p/" + savedMainSlug + "/ws",
535
+ savedMainSlug: null
536
+ });
537
+ resetClientState();
538
+ connect();
502
539
  }
503
540
  }
504
541
 
505
542
  export function appendDmMessage(msg) {
506
- if (_ctx.dmMode) _ctx.dmMessageCache.push(msg);
507
- var isMe = msg.from === _ctx.myUserId;
543
+ var s = store.snap();
544
+ if (s.dmMode) s.dmMessageCache.push(msg);
545
+ var isMe = msg.from === s.myUserId;
508
546
  var d = new Date(msg.ts);
509
547
  var timeStr = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0");
510
548
 
549
+ var messagesEl = getMessagesEl();
511
550
  // Check if we can compact (same sender as previous, within 5 min)
512
- var prev = _ctx.messagesEl.lastElementChild;
551
+ var prev = messagesEl.lastElementChild;
513
552
  var compact = false;
514
553
  if (prev && prev.dataset.from === msg.from) {
515
554
  var prevTs = parseInt(prev.dataset.ts || "0", 10);
@@ -537,10 +576,10 @@ export function appendDmMessage(msg) {
537
576
  var avatar = document.createElement("img");
538
577
  avatar.className = "dm-msg-avatar";
539
578
  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);
579
+ var myUser = s.cachedAllUsers.find(function (u) { return u.id === s.myUserId; });
580
+ avatar.src = userAvatarUrl(myUser || { id: s.myUserId }, 36);
581
+ } else if (s.dmTargetUser) {
582
+ avatar.src = userAvatarUrl(s.dmTargetUser, 36);
544
583
  }
545
584
  div.appendChild(avatar);
546
585
 
@@ -553,10 +592,10 @@ export function appendDmMessage(msg) {
553
592
  var name = document.createElement("span");
554
593
  name.className = "dm-msg-name";
555
594
  if (isMe) {
556
- var mu = _ctx.cachedAllUsers.find(function (u) { return u.id === _ctx.myUserId; });
595
+ var mu = s.cachedAllUsers.find(function (u) { return u.id === s.myUserId; });
557
596
  name.textContent = mu ? mu.displayName : "Me";
558
597
  } else {
559
- name.textContent = _ctx.dmTargetUser ? _ctx.dmTargetUser.displayName : "User";
598
+ name.textContent = s.dmTargetUser ? s.dmTargetUser.displayName : "User";
560
599
  }
561
600
  header.appendChild(name);
562
601
 
@@ -575,7 +614,7 @@ export function appendDmMessage(msg) {
575
614
  div.appendChild(content);
576
615
  }
577
616
 
578
- _ctx.messagesEl.appendChild(div);
617
+ messagesEl.appendChild(div);
579
618
  }
580
619
 
581
620
  export function showDmTypingIndicator(typing) {
@@ -585,7 +624,8 @@ export function showDmTypingIndicator(typing) {
585
624
  return;
586
625
  }
587
626
  if (existing) return; // already showing
588
- if (!_ctx.dmTargetUser) return;
627
+ var dmTargetUser = store.get('dmTargetUser');
628
+ if (!dmTargetUser) return;
589
629
 
590
630
  var div = document.createElement("div");
591
631
  div.id = "dm-typing-indicator";
@@ -593,7 +633,7 @@ export function showDmTypingIndicator(typing) {
593
633
 
594
634
  var avatar = document.createElement("img");
595
635
  avatar.className = "dm-msg-avatar";
596
- avatar.src = _ctx.userAvatarUrl(_ctx.dmTargetUser, 36);
636
+ avatar.src = userAvatarUrl(dmTargetUser, 36);
597
637
  div.appendChild(avatar);
598
638
 
599
639
  var dots = document.createElement("div");
@@ -601,8 +641,9 @@ export function showDmTypingIndicator(typing) {
601
641
  dots.innerHTML = "<span></span><span></span><span></span>";
602
642
  div.appendChild(dots);
603
643
 
604
- _ctx.messagesEl.appendChild(div);
605
- _ctx.scrollToBottom();
644
+ var messagesEl = getMessagesEl();
645
+ messagesEl.appendChild(div);
646
+ scrollToBottom();
606
647
 
607
648
  // Auto-hide after 5s in case stop signal is missed
608
649
  clearTimeout(dmTypingTimer);
@@ -612,11 +653,14 @@ export function showDmTypingIndicator(typing) {
612
653
  }
613
654
 
614
655
  export function handleDmSend() {
615
- if (!_ctx.dmMode || !_ctx.dmKey || !_ctx.inputEl) return false;
616
- var text = _ctx.inputEl.value.trim();
656
+ var s = store.snap();
657
+ var inputEl = getInputEl();
658
+ if (!s.dmMode || !s.dmKey || !inputEl) return false;
659
+ var text = inputEl.value.trim();
617
660
  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();
661
+ var ws = getWs();
662
+ ws.send(JSON.stringify({ type: "dm_send", dmKey: s.dmKey, text: text }));
663
+ inputEl.value = "";
664
+ autoResize();
621
665
  return true;
622
666
  }