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
@@ -11,7 +11,7 @@ import { refreshIcons, iconHtml } from './icons.js';
11
11
  import { renderMarkdown } from './markdown.js';
12
12
  import { updatePageTitle } from './sidebar.js';
13
13
  import { renderSessionList, updateSessionPresence, populateCliSessionList, handleSearchResults, updateSessionBadge } from './sidebar-sessions.js';
14
- import { renderUserStrip, updateDmBadge, renderSidebarPresence } from './sidebar-mates.js';
14
+ import { updateDmBadge, renderSidebarPresence, setMentionActive, renderUserStrip } from './sidebar-mates.js';
15
15
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
16
16
  import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
17
17
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
@@ -21,7 +21,7 @@ import { handleFindInSessionResults } from './session-search.js';
21
21
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
22
22
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
23
23
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
24
- import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate } from './filebrowser.js';
24
+ import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch } from './filebrowser.js';
25
25
  import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
26
26
  import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
27
27
  import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
@@ -38,19 +38,19 @@ import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunF
38
38
 
39
39
  // --- App module imports ---
40
40
  import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, addAuthRequiredMessage, showSuggestionChips } from './app-rendering.js';
41
- import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot } from './app-favicon.js';
41
+ import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
42
42
  import { setStatus } from './app-connection.js';
43
- import { updateConfigChip, getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
43
+ import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
44
44
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
45
45
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
46
46
  import { hideHomeHub, handleHubSchedules } from './app-home-hub.js';
47
47
  import { openDm, enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
48
48
  import { handleRateLimitEvent, updateRateLimitUsage, addScheduledMessageBubble, removeScheduledMessageBubble, handleFastModeState } from './app-rate-limit.js';
49
49
  import { handleRemoteCursorMove, handleRemoteCursorLeave, handleRemoteSelection, clearRemoteCursors } from './app-cursors.js';
50
- import { updateLoopButton, showLoopBanner, updateLoopBanner, updateRalphBars, updateLoopInputVisibility, showRalphApprovalBar, updateRalphApprovalStatus, openRalphPreviewModal, showExecModal, updateExecModalStatus } from './app-loop-ui.js';
50
+ import { showLoopBanner, updateLoopBanner, updateLoopInputVisibility, showRalphApprovalBar, updateRalphApprovalStatus, openRalphPreviewModal, showExecModal, updateExecModalStatus } from './app-loop-ui.js';
51
51
  import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitDebateFloorMode, exitDebateConcludeMode, exitDebateEndedMode, updateDebateRound, renderDebateUserFloorDone } from './app-debate-ui.js';
52
52
  import { handleSkillInstallWs } from './app-skills-install.js';
53
- import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll } from './app-notifications.js';
53
+ import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner } from './app-notifications.js';
54
54
  import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
55
55
  import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse } from './mention.js';
56
56
 
@@ -62,8 +62,8 @@ var connectOverlay = document.getElementById("connect-overlay");
62
62
 
63
63
  export function processMessage(msg) {
64
64
  // Preserve original timestamp from history replay
65
- store.setState({ currentMsgTs: msg._ts || null });
66
- var isMateDm = store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate;
65
+ store.set({ currentMsgTs: msg._ts || null });
66
+ var isMateDm = store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate;
67
67
 
68
68
  // Mate DM: update mate icon status indicators
69
69
  if (isMateDm) updateMateIconStatus(msg);
@@ -74,11 +74,11 @@ export function processMessage(msg) {
74
74
  renderMateSessionList(msg.sessions || []);
75
75
  refreshMobileChatSheet();
76
76
  // Override title bar with mate name and re-apply color
77
- var _mdn = (store.getState().dmTargetUser.displayName || "New Mate");
77
+ var _mdn = (store.get('dmTargetUser').displayName || "New Mate");
78
78
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
79
79
  var _tbpn = document.getElementById("title-bar-project-name");
80
80
  if (_tbpn) _tbpn.textContent = _mdn;
81
- var _mc2 = (store.getState().dmTargetUser.profile && store.getState().dmTargetUser.profile.avatarColor) || store.getState().dmTargetUser.avatarColor || "#7c3aed";
81
+ var _mc2 = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
82
82
  var _tbc2 = document.querySelector(".title-bar-content");
83
83
  if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
84
84
  document.body.classList.add("mate-dm-active");
@@ -115,12 +115,12 @@ export function processMessage(msg) {
115
115
  var readyMatch = fullText.match(/\[\[MATE_READY:\s*(.+?)\]\]/);
116
116
  if (readyMatch) {
117
117
  var newName = readyMatch[1].trim();
118
- store.getState().dmTargetUser.displayName = newName;
119
- updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: store.getState().dmTargetUser.avatarColor, avatarStyle: store.getState().dmTargetUser.avatarStyle, avatarSeed: store.getState().dmTargetUser.avatarSeed } });
118
+ store.get('dmTargetUser').displayName = newName;
119
+ updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: store.get('dmTargetUser').avatarColor, avatarStyle: store.get('dmTargetUser').avatarStyle, avatarSeed: store.get('dmTargetUser').avatarSeed } });
120
120
  if (getWs() && getWs().readyState === 1) {
121
121
  getWs().send(JSON.stringify({
122
122
  type: "mate_update",
123
- mateId: store.getState().dmTargetUser.id,
123
+ mateId: store.get('dmTargetUser').id,
124
124
  updates: { name: newName, status: "ready", profile: { displayName: newName } },
125
125
  }));
126
126
  }
@@ -138,7 +138,7 @@ export function processMessage(msg) {
138
138
 
139
139
  switch (msg.type) {
140
140
  case "history_meta":
141
- store.setState({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
141
+ store.set({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
142
142
  updateHistorySentinel();
143
143
  break;
144
144
 
@@ -147,10 +147,14 @@ export function processMessage(msg) {
147
147
  break;
148
148
 
149
149
  case "history_done":
150
- store.setState({ replayingHistory: false });
150
+ store.set({ replayingHistory: false });
151
+ // Hide vendor toggle if session has history (vendor already locked)
152
+ var _hTotal = store.get('historyTotal') || 0;
153
+ var _vtw2 = document.getElementById("vendor-toggle-wrap");
154
+ if (_vtw2 && _hTotal > 0) { _vtw2.classList.remove("hidden"); _vtw2.classList.add("locked"); }
151
155
  // Restore cached rich context usage BEFORE updateContextPanel runs
152
156
  if (msg.contextUsage) {
153
- store.setState({ richContextUsage: msg.contextUsage });
157
+ store.set({ richContextUsage: msg.contextUsage });
154
158
  }
155
159
  // Restore accurate context data from the last result in full history
156
160
  if (msg.lastUsage || msg.lastModelUsage) {
@@ -159,7 +163,7 @@ export function processMessage(msg) {
159
163
  updateContextPanel();
160
164
  updateUsagePanel();
161
165
  // Render + finalize any incomplete turn from the replayed history
162
- var _hs = store.getState();
166
+ var _hs = store.snap();
163
167
  if (_hs.currentMsgEl && _hs.currentFullText) {
164
168
  var replayContentEl = _hs.currentMsgEl.querySelector(".md-content");
165
169
  if (replayContentEl) {
@@ -178,7 +182,7 @@ export function processMessage(msg) {
178
182
  var dbBadges = document.querySelectorAll(".debate-header-badge");
179
183
  for (var dbi = 0; dbi < dbBadges.length; dbi++) dbBadges[dbi].remove();
180
184
  // Clean up all debate mode banners if debate is not active on this session
181
- var _ds = store.getState();
185
+ var _ds = store.snap();
182
186
  if (_ds.debateFloorMode) exitDebateFloorMode();
183
187
  if (_ds.debateConcludeMode) exitDebateConcludeMode();
184
188
  if (_ds.debateEndedMode) exitDebateEndedMode();
@@ -208,20 +212,20 @@ export function processMessage(msg) {
208
212
  break;
209
213
 
210
214
  case "restore_mate_dm":
211
- if (msg.mateId && !store.getState().returningFromMateDm) {
215
+ if (msg.mateId && !store.get('returningFromMateDm')) {
212
216
  // Server-driven mate DM restore on reconnect
213
217
  // Note: do NOT remove mate-dm-active here; openDm is async (skill check)
214
218
  // and removing the class causes a flash where mate UI is lost.
215
219
  // enterDmMode will properly set/reset the class when DM is entered.
216
- if (store.getState().dmMode) {
217
- store.setState({ dmMode: false });
220
+ if (store.get('dmMode')) {
221
+ store.set({ dmMode: false });
218
222
  }
219
223
  messagesEl.innerHTML = "";
220
224
  openDm(msg.mateId);
221
225
  }
222
226
  // Clear the flag and notify server that mate DM is closed
223
- if (store.getState().returningFromMateDm) {
224
- store.setState({ returningFromMateDm: false });
227
+ if (store.get('returningFromMateDm')) {
228
+ store.set({ returningFromMateDm: false });
225
229
  if (getWs() && getWs().readyState === 1) {
226
230
  try { getWs().send(JSON.stringify({ type: "set_mate_dm", mateId: null })); } catch(e) {}
227
231
  }
@@ -233,25 +237,25 @@ export function processMessage(msg) {
233
237
  addSystemMessage(msg.text, false);
234
238
  break;
235
239
  }
236
- store.setState({ projectName: msg.project || msg.cwd });
237
- if (msg.cwd) store.setState({ cwd: msg.cwd });
238
- if (msg.slug) store.setState({ currentSlug: msg.slug });
239
- try { var _is = store.getState(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
240
+ store.set({ projectName: msg.project || msg.cwd });
241
+ if (msg.cwd) store.set({ cwd: msg.cwd });
242
+ if (msg.slug) store.set({ currentSlug: msg.slug });
243
+ try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
240
244
  // In mate DM, keep title as mate name and re-apply mate color
241
- if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate) {
242
- var _mateDN = store.getState().dmTargetUser.displayName || "New Mate";
245
+ if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
246
+ var _mateDN = store.get('dmTargetUser').displayName || "New Mate";
243
247
  headerTitleEl.textContent = _mateDN;
244
248
  var tbProjectName = document.getElementById("title-bar-project-name");
245
249
  if (tbProjectName) tbProjectName.textContent = _mateDN;
246
250
  // Re-apply mate title bar styling (may be lost during project switch)
247
- var _mc = (store.getState().dmTargetUser.profile && store.getState().dmTargetUser.profile.avatarColor) || store.getState().dmTargetUser.avatarColor || "#7c3aed";
251
+ var _mc = (store.get('dmTargetUser').profile && store.get('dmTargetUser').profile.avatarColor) || store.get('dmTargetUser').avatarColor || "#7c3aed";
248
252
  var _tbc = document.querySelector(".title-bar-content");
249
253
  if (_tbc) { _tbc.style.background = _mc; _tbc.classList.add("mate-dm-active"); }
250
254
  document.body.classList.add("mate-dm-active");
251
255
  } else {
252
- headerTitleEl.textContent = store.getState().projectName;
256
+ headerTitleEl.textContent = store.get('projectName');
253
257
  var tbProjectName = document.getElementById("title-bar-project-name");
254
- if (tbProjectName) tbProjectName.textContent = msg.title || store.getState().projectName;
258
+ if (tbProjectName) tbProjectName.textContent = msg.title || store.get('projectName');
255
259
  }
256
260
  updatePageTitle();
257
261
  if (msg.version) {
@@ -259,12 +263,12 @@ export function processMessage(msg) {
259
263
  var serverVersionEl = document.getElementById("settings-server-version");
260
264
  if (serverVersionEl) serverVersionEl.textContent = msg.version;
261
265
  }
262
- if (msg.projectOwnerId !== undefined) store.setState({ currentProjectOwnerId: msg.projectOwnerId });
263
- if (msg.ownerLocked !== undefined) store.setState({ ownerLocked: !!msg.ownerLocked });
264
- if (msg.osUsers !== undefined) store.setState({ isOsUsers: !!msg.osUsers });
266
+ if (msg.projectOwnerId !== undefined) store.set({ currentProjectOwnerId: msg.projectOwnerId });
267
+ if (msg.ownerLocked !== undefined) store.set({ ownerLocked: !!msg.ownerLocked });
268
+ if (msg.osUsers !== undefined) store.set({ isOsUsers: !!msg.osUsers });
265
269
  if (msg.lanHost) window.__lanHost = msg.lanHost;
266
270
  if (msg.dangerouslySkipPermissions) {
267
- store.setState({ skipPermsEnabled: true });
271
+ store.set({ skipPermsEnabled: true });
268
272
  var spBanner = document.getElementById("skip-perms-pill");
269
273
  if (spBanner) spBanner.classList.remove("hidden");
270
274
  }
@@ -273,13 +277,15 @@ export function processMessage(msg) {
273
277
 
274
278
  case "update_available":
275
279
  // In multi-user mode, only show update UI to admins
276
- if (store.getState().isMultiUserMode) {
280
+ if (store.get('isMultiUserMode')) {
277
281
  checkAdminAccess().then(function (isAdmin) {
278
282
  if (!isAdmin) return;
279
283
  showUpdateAvailable(msg);
284
+ showUpdateBanner(msg);
280
285
  });
281
286
  } else {
282
287
  showUpdateAvailable(msg);
288
+ showUpdateBanner(msg);
283
289
  }
284
290
  break;
285
291
 
@@ -321,18 +327,24 @@ export function processMessage(msg) {
321
327
 
322
328
  case "slash_commands":
323
329
  var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
324
- store.setState({ slashCommands: (msg.commands || []).filter(function (name) {
330
+ store.set({ slashCommands: (msg.commands || []).filter(function (name) {
325
331
  return !reserved.has(name);
326
332
  }).map(function (name) {
327
333
  return { name: name, desc: "Skill" };
328
334
  }) });
329
335
  break;
330
336
 
331
- case "model_info":
332
- store.setState({ currentModel: msg.model || store.getState().currentModel, currentModels: msg.models || [] });
333
- updateConfigChip();
334
- updateSettingsModels(msg.model, msg.models || []);
337
+ case "model_info": {
338
+ var _modelVal = msg.model;
339
+ if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
340
+ var _miUpdate = { currentModel: _modelVal || store.get('currentModel'), currentModels: msg.models || [] };
341
+ if (msg.vendor) _miUpdate.currentVendor = msg.vendor;
342
+ if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
343
+ if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
344
+ store.set(_miUpdate);
345
+ updateSettingsModels(_modelVal, msg.models || []);
335
346
  break;
347
+ }
336
348
 
337
349
  case "config_state": {
338
350
  var _cs = {};
@@ -342,20 +354,27 @@ export function processMessage(msg) {
342
354
  if (msg.betas) _cs.currentBetas = msg.betas;
343
355
  if (msg.thinking) _cs.currentThinking = msg.thinking;
344
356
  if (msg.thinkingBudget) _cs.currentThinkingBudget = msg.thinkingBudget;
345
- store.setState(_cs);
357
+ store.set(_cs);
346
358
  // Validate effort against current model's supported levels
347
- var _csRead = store.getState();
359
+ var _csRead = store.snap();
348
360
  if (_csRead.currentModels.length > 0) {
349
361
  var levels = getModelEffortLevels();
350
362
  var effortValid = false;
351
363
  for (var ei = 0; ei < levels.length; ei++) {
352
364
  if (levels[ei] === _csRead.currentEffort) { effortValid = true; break; }
353
365
  }
354
- if (!effortValid) store.setState({ currentEffort: "medium" });
366
+ if (!effortValid) store.set({ currentEffort: "medium" });
355
367
  }
356
- updateConfigChip();
357
368
  } break;
358
369
 
370
+ case "codex_config":
371
+ store.set({
372
+ codexApproval: msg.approval,
373
+ codexSandbox: msg.sandbox,
374
+ codexWebSearch: msg.webSearch,
375
+ });
376
+ break;
377
+
359
378
  case "client_count":
360
379
  // Sidebar presence: current project's online users
361
380
  if (msg.users) {
@@ -382,13 +401,13 @@ export function processMessage(msg) {
382
401
 
383
402
  case "skill_installed":
384
403
  handleSkillInstalled(msg);
385
- if (msg.success) { var _kis = Object.assign({}, store.getState().knownInstalledSkills); _kis[msg.skill] = true; store.setState({ knownInstalledSkills: _kis }); }
404
+ if (msg.success) { var _kis = Object.assign({}, store.get('knownInstalledSkills')); _kis[msg.skill] = true; store.set({ knownInstalledSkills: _kis }); }
386
405
  handleSkillInstallWs(msg);
387
406
  break;
388
407
 
389
408
  case "skill_uninstalled":
390
409
  handleSkillUninstalled(msg);
391
- if (msg.success) { var _kis2 = Object.assign({}, store.getState().knownInstalledSkills); delete _kis2[msg.skill]; store.setState({ knownInstalledSkills: _kis2 }); }
410
+ if (msg.success) { var _kis2 = Object.assign({}, store.get('knownInstalledSkills')); delete _kis2[msg.skill]; store.set({ knownInstalledSkills: _kis2 }); }
392
411
  break;
393
412
 
394
413
  case "loop_registry_updated":
@@ -424,7 +443,7 @@ export function processMessage(msg) {
424
443
  break;
425
444
 
426
445
  case "input_sync":
427
- if (!store.getState().dmMode) handleInputSync(msg.text);
446
+ if (!store.get('dmMode')) handleInputSync(msg.text);
428
447
  break;
429
448
 
430
449
  case "session_list":
@@ -474,23 +493,47 @@ export function processMessage(msg) {
474
493
  case "session_switched":
475
494
  hideHomeHub();
476
495
  // Save draft from outgoing session
477
- var _prevSid = store.getState().activeSessionId;
496
+ var _prevSid = store.get('activeSessionId');
478
497
  if (_prevSid && inputEl.value) {
479
- store.getState().sessionDrafts[_prevSid] = inputEl.value;
498
+ store.get('sessionDrafts')[_prevSid] = inputEl.value;
480
499
  } else if (_prevSid) {
481
- delete store.getState().sessionDrafts[_prevSid];
500
+ delete store.get('sessionDrafts')[_prevSid];
501
+ }
502
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, vendorCapabilities: msg.capabilities || {} });
503
+ if (msg.vendor) {
504
+ store.set({ currentVendor: msg.vendor });
505
+ } else if (msg.hasHistory) {
506
+ // Existing session without explicit vendor: reset to claude
507
+ store.set({ currentVendor: "claude" });
508
+ } else if (!msg.hasHistory) {
509
+ // New session without vendor: use mate's default vendor if in DM mode
510
+ var _dmTarget = store.get('dmTargetUser');
511
+ if (_dmTarget && _dmTarget.isMate) {
512
+ var _mateList = store.get('cachedMatesList') || [];
513
+ for (var _mi = 0; _mi < _mateList.length; _mi++) {
514
+ if (_mateList[_mi].id === _dmTarget.id && _mateList[_mi].vendor) {
515
+ store.set({ currentVendor: _mateList[_mi].vendor });
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ }
521
+ // Show vendor toggle only for new sessions (no history)
522
+ var _vtw = document.getElementById("vendor-toggle-wrap");
523
+ if (_vtw) {
524
+ if (msg.hasHistory) { _vtw.classList.remove("hidden"); _vtw.classList.add("locked"); }
525
+ else { _vtw.classList.remove("locked"); _vtw.classList.remove("hidden"); }
482
526
  }
483
- store.setState({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null });
484
527
  // Session presence is now tracked server-side (user-presence.json)
485
528
  clearRemoteCursors();
486
529
  resetClientState();
487
- updateRalphBars();
530
+
488
531
  updateLoopInputVisibility(msg.loop);
489
532
  // Restore input area visibility (may have been hidden by auth_required)
490
533
  var inputAreaSw = document.getElementById("input-area");
491
534
  if (inputAreaSw) inputAreaSw.classList.remove("hidden");
492
535
  // Restore draft for incoming session
493
- var draft = store.getState().sessionDrafts[store.getState().activeSessionId] || "";
536
+ var draft = store.get('sessionDrafts')[store.get('activeSessionId')] || "";
494
537
  inputEl.value = draft;
495
538
  autoResize();
496
539
  if (!("ontouchstart" in window)) {
@@ -499,7 +542,7 @@ export function processMessage(msg) {
499
542
  break;
500
543
 
501
544
  case "session_id":
502
- store.setState({ cliSessionId: msg.cliSessionId });
545
+ store.set({ cliSessionId: msg.cliSessionId });
503
546
  break;
504
547
 
505
548
  case "message_uuid":
@@ -513,9 +556,9 @@ export function processMessage(msg) {
513
556
  }
514
557
  if (uuidTarget) {
515
558
  uuidTarget.dataset.uuid = msg.uuid;
516
- if (msg.messageType === "user") addRewindButton(uuidTarget);
559
+ if (msg.messageType === "user" && (store.get('vendorCapabilities') || {}).rewind !== false) addRewindButton(uuidTarget);
517
560
  }
518
- store.getState().messageUuidMap.push({ uuid: msg.uuid, type: msg.messageType });
561
+ store.get('messageUuidMap').push({ uuid: msg.uuid, type: msg.messageType });
519
562
  break;
520
563
 
521
564
  case "user_message":
@@ -595,7 +638,7 @@ export function processMessage(msg) {
595
638
  case "status":
596
639
  if (msg.status === "processing") {
597
640
  setStatus("processing");
598
- if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate) && !store.getState().matePreThinkingEl) {
641
+ if (!(store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) && !store.get('matePreThinkingEl')) {
599
642
  setActivity("thinking");
600
643
  }
601
644
  }
@@ -604,7 +647,7 @@ export function processMessage(msg) {
604
647
  case "compacting":
605
648
  if (msg.active) {
606
649
  setActivity("compacting");
607
- } else if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate)) {
650
+ } else if (!(store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate)) {
608
651
  setActivity("thinking");
609
652
  }
610
653
  break;
@@ -620,7 +663,7 @@ export function processMessage(msg) {
620
663
 
621
664
  case "thinking_stop":
622
665
  stopThinking(msg.duration);
623
- if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate)) {
666
+ if (!(store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate)) {
624
667
  setActivity("thinking");
625
668
  }
626
669
  break;
@@ -725,7 +768,7 @@ export function processMessage(msg) {
725
768
  break;
726
769
 
727
770
  case "permission_request":
728
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
771
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId, msg.vendor);
729
772
  startUrgentBlink();
730
773
  break;
731
774
 
@@ -740,7 +783,7 @@ export function processMessage(msg) {
740
783
  break;
741
784
 
742
785
  case "permission_request_pending":
743
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
786
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId, msg.vendor);
744
787
  startUrgentBlink();
745
788
  break;
746
789
 
@@ -805,23 +848,21 @@ export function processMessage(msg) {
805
848
  break;
806
849
 
807
850
  case "context_usage":
808
- if (msg.data && !store.getState().replayingHistory) {
809
- store.setState({ richContextUsage: msg.data });
810
- var _hce = store.getState().headerContextEl;
811
- if (_hce) _hce.removeAttribute("data-tip");
812
- if (store.getState().ctxPopoverVisible) renderCtxPopover();
851
+ if (msg.data && !store.get('replayingHistory')) {
852
+ store.set({ richContextUsage: msg.data });
853
+ // UI sync handled by store subscriber in app-panels.js
813
854
  }
814
855
  break;
815
856
 
816
857
  case "done":
858
+ removeMatePreThinking();
817
859
  setActivity(null);
818
860
  stopThinking();
819
861
  markAllToolsDone();
820
862
  closeToolGroup();
821
863
  finalizeAssistantBlock();
822
- store.setState({ processing: false });
823
864
  setStatus("connected");
824
- if (!store.getState().loopActive) enableMainInput();
865
+ if (!store.get('loopActive')) enableMainInput();
825
866
  resetToolState();
826
867
  stopUrgentBlink();
827
868
  if (document.hidden) {
@@ -835,6 +876,7 @@ export function processMessage(msg) {
835
876
  break;
836
877
 
837
878
  case "error":
879
+ removeMatePreThinking();
838
880
  setActivity(null);
839
881
  addSystemMessage(msg.text, true);
840
882
  break;
@@ -848,16 +890,19 @@ export function processMessage(msg) {
848
890
  break;
849
891
 
850
892
  case "process_conflict":
893
+ removeMatePreThinking();
851
894
  setActivity(null);
852
895
  addConflictMessage(msg);
853
896
  break;
854
897
 
855
898
  case "context_overflow":
899
+ removeMatePreThinking();
856
900
  setActivity(null);
857
901
  addContextOverflowMessage(msg);
858
902
  break;
859
903
 
860
904
  case "auth_required":
905
+ removeMatePreThinking();
861
906
  setActivity(null);
862
907
  addAuthRequiredMessage(msg);
863
908
  break;
@@ -878,7 +923,6 @@ export function processMessage(msg) {
878
923
  case "scheduled_message_sent":
879
924
  removeScheduledMessageBubble();
880
925
  setScheduleBtnDisabled(false);
881
- store.setState({ processing: true });
882
926
  setStatus("processing");
883
927
  break;
884
928
 
@@ -892,7 +936,6 @@ export function processMessage(msg) {
892
936
  break;
893
937
 
894
938
  case "auto_continue_fired":
895
- store.setState({ processing: true });
896
939
  setStatus("processing");
897
940
  break;
898
941
 
@@ -935,6 +978,10 @@ export function processMessage(msg) {
935
978
  handleFsList(msg);
936
979
  break;
937
980
 
981
+ case "fs_search_result":
982
+ handleFsSearch(msg);
983
+ break;
984
+
938
985
  case "fs_read_result":
939
986
  if (msg.path === "CLAUDE.md" && isProjectSettingsOpen()) {
940
987
  handleInstructionsRead(msg);
@@ -1041,9 +1088,9 @@ export function processMessage(msg) {
1041
1088
 
1042
1089
  case "term_created":
1043
1090
  handleTermCreated(msg);
1044
- if (store.getState().pendingTermCommand) {
1045
- var cmd = store.getState().pendingTermCommand;
1046
- store.setState({ pendingTermCommand: null });
1091
+ if (store.get('pendingTermCommand')) {
1092
+ var cmd = store.get('pendingTermCommand');
1093
+ store.set({ pendingTermCommand: null });
1047
1094
  // Small delay to let terminal initialize
1048
1095
  setTimeout(function() {
1049
1096
  sendTerminalCommand(cmd);
@@ -1124,10 +1171,11 @@ export function processMessage(msg) {
1124
1171
 
1125
1172
  case "projects_updated":
1126
1173
  updateProjectList(msg);
1174
+ renderUserStrip();
1127
1175
  break;
1128
1176
 
1129
1177
  case "project_owner_changed":
1130
- store.setState({ currentProjectOwnerId: msg.ownerId });
1178
+ store.set({ currentProjectOwnerId: msg.ownerId });
1131
1179
  handleProjectOwnerChanged(msg);
1132
1180
  break;
1133
1181
 
@@ -1139,12 +1187,12 @@ export function processMessage(msg) {
1139
1187
  }
1140
1188
  enterDmMode(msg.dmKey, msg.targetUser, msg.messages);
1141
1189
  // Auto-send first interview prompt after mate DM opens
1142
- if (store.getState().pendingMateInterview && msg.targetUser && msg.targetUser.isMate && msg.projectSlug) {
1143
- var interviewMate = store.getState().pendingMateInterview;
1144
- store.setState({ pendingMateInterview: null });
1190
+ if (store.get('pendingMateInterview') && msg.targetUser && msg.targetUser.isMate && msg.projectSlug) {
1191
+ var interviewMate = store.get('pendingMateInterview');
1192
+ store.set({ pendingMateInterview: null });
1145
1193
  // Wait for mate project WS to connect, then send interview prompt
1146
1194
  var checkMateReady = setInterval(function () {
1147
- if (getWs() && getWs().readyState === 1 && store.getState().mateProjectSlug) {
1195
+ if (getWs() && getWs().readyState === 1 && store.get('mateProjectSlug')) {
1148
1196
  clearInterval(checkMateReady);
1149
1197
  var interviewText = buildMateInterviewPrompt(interviewMate);
1150
1198
  getWs().send(JSON.stringify({ type: "message", text: interviewText }));
@@ -1155,25 +1203,25 @@ export function processMessage(msg) {
1155
1203
  break;
1156
1204
 
1157
1205
  case "dm_message":
1158
- if (store.getState().dmMode && msg.dmKey === store.getState().dmKey) {
1206
+ if (store.get('dmMode') && msg.dmKey === store.get('dmKey')) {
1159
1207
  showDmTypingIndicator(false); // hide typing when message arrives
1160
1208
  appendDmMessage(msg.message);
1161
1209
  scrollToBottom();
1162
1210
  } else if (msg.message) {
1163
1211
  // DM notification when not in that DM
1164
1212
  var fromId = msg.message.from;
1165
- var _s1 = store.getState();
1166
- if (fromId && fromId !== _s1.myUserId) {
1167
- _s1.dmUnread[fromId] = (_s1.dmUnread[fromId] || 0) + 1;
1168
- // Re-render strip so non-favorited sender appears
1169
- renderUserStrip(_s1.cachedAllUsers, _s1.cachedOnlineIds, _s1.myUserId, _s1.cachedDmFavorites, _s1.cachedDmConversations, _s1.dmUnread, _s1.dmRemovedUsers, _s1.cachedMatesList);
1170
- updateDmBadge(fromId, _s1.dmUnread[fromId]);
1213
+ if (fromId && fromId !== store.get('myUserId')) {
1214
+ var _du = Object.assign({}, store.get('dmUnread'));
1215
+ _du[fromId] = (_du[fromId] || 0) + 1;
1216
+ store.set({ dmUnread: _du });
1217
+ // renderUserStrip is handled by the store subscriber
1218
+ updateDmBadge(fromId, _du[fromId]);
1171
1219
  }
1172
1220
  }
1173
1221
  break;
1174
1222
 
1175
1223
  case "dm_typing":
1176
- if (store.getState().dmMode && msg.dmKey === store.getState().dmKey) {
1224
+ if (store.get('dmMode') && msg.dmKey === store.get('dmKey')) {
1177
1225
  showDmTypingIndicator(msg.typing);
1178
1226
  }
1179
1227
  break;
@@ -1184,23 +1232,22 @@ export function processMessage(msg) {
1184
1232
 
1185
1233
  case "dm_favorites_updated":
1186
1234
  // Track users explicitly removed from favorites
1187
- var _cdf = store.getState().cachedDmFavorites;
1235
+ var _cdf = store.get('cachedDmFavorites');
1188
1236
  if (_cdf && msg.dmFavorites) {
1189
1237
  for (var ri = 0; ri < _cdf.length; ri++) {
1190
1238
  if (msg.dmFavorites.indexOf(_cdf[ri]) === -1) {
1191
- store.getState().dmRemovedUsers[_cdf[ri]] = true;
1239
+ store.get('dmRemovedUsers')[_cdf[ri]] = true;
1192
1240
  }
1193
1241
  }
1194
1242
  }
1195
1243
  // Clear removed flag for users being added back
1196
1244
  if (msg.dmFavorites) {
1197
1245
  for (var ai = 0; ai < msg.dmFavorites.length; ai++) {
1198
- delete store.getState().dmRemovedUsers[msg.dmFavorites[ai]];
1246
+ delete store.get('dmRemovedUsers')[msg.dmFavorites[ai]];
1199
1247
  }
1200
1248
  }
1201
- store.setState({ cachedDmFavorites: msg.dmFavorites || [] });
1202
- var _s2 = store.getState();
1203
- renderUserStrip(_s2.cachedAllUsers, _s2.cachedOnlineIds, _s2.myUserId, _s2.cachedDmFavorites, _s2.cachedDmConversations, _s2.dmUnread, _s2.dmRemovedUsers, _s2.cachedMatesList);
1249
+ store.set({ cachedDmFavorites: msg.dmFavorites || [] });
1250
+ // renderUserStrip is handled by the store subscriber
1204
1251
  break;
1205
1252
 
1206
1253
  case "mate_created":
@@ -1208,41 +1255,39 @@ export function processMessage(msg) {
1208
1255
  break;
1209
1256
 
1210
1257
  case "mate_deleted":
1211
- store.setState({ cachedMatesList: store.getState().cachedMatesList.filter(function (m) { return m.id !== msg.mateId; }) });
1212
- if (msg.availableBuiltins) store.setState({ cachedAvailableBuiltins: msg.availableBuiltins });
1213
- var _s3 = store.getState();
1214
- renderUserStrip(_s3.cachedAllUsers, _s3.cachedOnlineIds, _s3.myUserId, _s3.cachedDmFavorites, _s3.cachedDmConversations, _s3.dmUnread, _s3.dmRemovedUsers, _s3.cachedMatesList);
1258
+ store.set({ cachedMatesList: store.get('cachedMatesList').filter(function (m) { return m.id !== msg.mateId; }) });
1259
+ if (msg.availableBuiltins) store.set({ cachedAvailableBuiltins: msg.availableBuiltins });
1260
+ // renderUserStrip is handled by the store subscriber
1215
1261
  // If currently in DM with this mate, exit DM mode
1216
- if (_s3.dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.id === msg.mateId) {
1262
+ if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').id === msg.mateId) {
1217
1263
  exitDmMode();
1218
1264
  }
1219
1265
  break;
1220
1266
 
1221
1267
  case "mate_updated":
1222
1268
  if (msg.mate) {
1223
- var _cml = store.getState().cachedMatesList.slice();
1269
+ var _cml = store.get('cachedMatesList').slice();
1224
1270
  for (var mi = 0; mi < _cml.length; mi++) {
1225
1271
  if (_cml[mi].id === msg.mate.id) {
1226
1272
  _cml[mi] = msg.mate;
1227
1273
  break;
1228
1274
  }
1229
1275
  }
1230
- store.setState({ cachedMatesList: _cml });
1231
- var _s4 = store.getState();
1232
- renderUserStrip(_s4.cachedAllUsers, _s4.cachedOnlineIds, _s4.myUserId, _s4.cachedDmFavorites, _s4.cachedDmConversations, _s4.dmUnread, _s4.dmRemovedUsers, _cml);
1276
+ store.set({ cachedMatesList: _cml });
1277
+ // renderUserStrip is handled by the store subscriber
1233
1278
  // Update mate sidebar if currently viewing this mate
1234
- if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate && store.getState().dmTargetUser.id === msg.mate.id) {
1279
+ if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate && store.get('dmTargetUser').id === msg.mate.id) {
1235
1280
  updateMateSidebarProfile(msg.mate);
1236
1281
  // Sync dmTargetUser so subsequent renders use fresh data
1237
1282
  var mp2 = msg.mate.profile || {};
1238
- store.getState().dmTargetUser.displayName = mp2.displayName || msg.mate.name || store.getState().dmTargetUser.displayName;
1239
- store.getState().dmTargetUser.avatarStyle = mp2.avatarStyle || store.getState().dmTargetUser.avatarStyle;
1240
- store.getState().dmTargetUser.avatarSeed = mp2.avatarSeed || store.getState().dmTargetUser.avatarSeed;
1241
- store.getState().dmTargetUser.avatarColor = mp2.avatarColor || store.getState().dmTargetUser.avatarColor;
1242
- store.getState().dmTargetUser.avatarCustom = mp2.avatarCustom || "";
1243
- store.getState().dmTargetUser.profile = mp2;
1283
+ store.get('dmTargetUser').displayName = mp2.displayName || msg.mate.name || store.get('dmTargetUser').displayName;
1284
+ store.get('dmTargetUser').avatarStyle = mp2.avatarStyle || store.get('dmTargetUser').avatarStyle;
1285
+ store.get('dmTargetUser').avatarSeed = mp2.avatarSeed || store.get('dmTargetUser').avatarSeed;
1286
+ store.get('dmTargetUser').avatarColor = mp2.avatarColor || store.get('dmTargetUser').avatarColor;
1287
+ store.get('dmTargetUser').avatarCustom = mp2.avatarCustom || "";
1288
+ store.get('dmTargetUser').profile = mp2;
1244
1289
  // Refresh body dataset so new chat bubbles use the updated avatar
1245
- document.body.dataset.mateAvatarUrl = mateAvatarUrl(store.getState().dmTargetUser, 36);
1290
+ document.body.dataset.mateAvatarUrl = mateAvatarUrl(store.get('dmTargetUser'), 36);
1246
1291
  document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
1247
1292
  // Update existing chat bubble avatars
1248
1293
  var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
@@ -1251,7 +1296,7 @@ export function processMessage(msg) {
1251
1296
  }
1252
1297
  }
1253
1298
  // Update DM header if currently chatting with this mate
1254
- if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.id === msg.mate.id) {
1299
+ if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').id === msg.mate.id) {
1255
1300
  var updatedName = (msg.mate.profile && msg.mate.profile.displayName) || msg.mate.name;
1256
1301
  if (updatedName) {
1257
1302
  var dmHeaderName = document.getElementById("dm-header-name");
@@ -1264,9 +1309,8 @@ export function processMessage(msg) {
1264
1309
  break;
1265
1310
 
1266
1311
  case "mate_list":
1267
- store.setState({ cachedMatesList: msg.mates || [], cachedAvailableBuiltins: msg.availableBuiltins || [] });
1268
- var _s5 = store.getState();
1269
- renderUserStrip(_s5.cachedAllUsers, _s5.cachedOnlineIds, _s5.myUserId, _s5.cachedDmFavorites, _s5.cachedDmConversations, _s5.dmUnread, _s5.dmRemovedUsers, _s5.cachedMatesList);
1312
+ store.set({ cachedMatesList: msg.mates || [], cachedAvailableBuiltins: msg.availableBuiltins || [] });
1313
+ // renderUserStrip is handled by the store subscriber
1270
1314
  break;
1271
1315
 
1272
1316
  case "mate_available_builtins":
@@ -1281,6 +1325,7 @@ export function processMessage(msg) {
1281
1325
  case "mention_processing":
1282
1326
  // Broadcast: show/hide activity dot on mate avatar across all tabs
1283
1327
  if (msg.mateId) {
1328
+ setMentionActive(msg.mateId, msg.active);
1284
1329
  var mateContainers = document.querySelectorAll('.icon-strip-mate[data-user-id="' + msg.mateId + '"]');
1285
1330
  for (var mi = 0; mi < mateContainers.length; mi++) {
1286
1331
  var dot = mateContainers[mi].querySelector(".icon-strip-status");
@@ -1292,6 +1337,7 @@ export function processMessage(msg) {
1292
1337
  mateContainers[mi].classList.remove("mention-active");
1293
1338
  }
1294
1339
  }
1340
+ updateCrossProjectBlink();
1295
1341
  }
1296
1342
  break;
1297
1343
 
@@ -1329,12 +1375,12 @@ export function processMessage(msg) {
1329
1375
 
1330
1376
  // --- Debate ---
1331
1377
  case "debate_preparing":
1332
- if (!store.getState().replayingHistory) showDebateSticky("preparing", msg);
1378
+ if (!store.get('replayingHistory')) showDebateSticky("preparing", msg);
1333
1379
  handleDebatePreparing(msg);
1334
1380
  break;
1335
1381
 
1336
1382
  case "debate_brief_ready":
1337
- if (store.getState().replayingHistory) {
1383
+ if (store.get('replayingHistory')) {
1338
1384
  renderDebateBriefReady(msg);
1339
1385
  } else {
1340
1386
  handleDebateBriefReady(msg);
@@ -1342,8 +1388,8 @@ export function processMessage(msg) {
1342
1388
  break;
1343
1389
 
1344
1390
  case "debate_started":
1345
- if (!store.getState().replayingHistory) showDebateSticky("live", msg);
1346
- if (store.getState().replayingHistory) {
1391
+ if (!store.get('replayingHistory')) showDebateSticky("live", msg);
1392
+ if (store.get('replayingHistory')) {
1347
1393
  renderDebateStarted(msg);
1348
1394
  } else {
1349
1395
  handleDebateStarted(msg);
@@ -1377,7 +1423,7 @@ export function processMessage(msg) {
1377
1423
  break;
1378
1424
 
1379
1425
  case "debate_comment_injected":
1380
- if (store.getState().replayingHistory) {
1426
+ if (store.get('replayingHistory')) {
1381
1427
  renderDebateCommentInjected(msg);
1382
1428
  } else {
1383
1429
  handleDebateCommentInjected(msg);
@@ -1385,11 +1431,11 @@ export function processMessage(msg) {
1385
1431
  break;
1386
1432
 
1387
1433
  case "debate_conclude_confirm":
1388
- if (!store.getState().replayingHistory) showDebateConcludeConfirm(msg);
1434
+ if (!store.get('replayingHistory')) showDebateConcludeConfirm(msg);
1389
1435
  break;
1390
1436
 
1391
1437
  case "debate_user_floor":
1392
- if (!store.getState().replayingHistory) showDebateUserFloor(msg);
1438
+ if (!store.get('replayingHistory')) showDebateUserFloor(msg);
1393
1439
  break;
1394
1440
 
1395
1441
  case "debate_user_floor_done":
@@ -1402,12 +1448,12 @@ export function processMessage(msg) {
1402
1448
 
1403
1449
  case "debate_resumed":
1404
1450
  handleDebateResumed(msg);
1405
- if (!store.getState().replayingHistory) showDebateSticky("live", msg);
1451
+ if (!store.get('replayingHistory')) showDebateSticky("live", msg);
1406
1452
  break;
1407
1453
 
1408
1454
  case "debate_ended":
1409
- if (!store.getState().replayingHistory) showDebateSticky("ended", msg);
1410
- if (store.getState().replayingHistory) {
1455
+ if (!store.get('replayingHistory')) showDebateSticky("ended", msg);
1456
+ if (store.get('replayingHistory')) {
1411
1457
  renderDebateEnded(msg);
1412
1458
  } else {
1413
1459
  handleDebateEnded(msg);
@@ -1420,7 +1466,7 @@ export function processMessage(msg) {
1420
1466
  break;
1421
1467
 
1422
1468
  case "daemon_config":
1423
- if (msg.config && msg.config.headless) store.setState({ isHeadlessMode: true });
1469
+ if (msg.config && msg.config.headless) store.set({ isHeadlessMode: true });
1424
1470
  updateDaemonConfig(msg.config);
1425
1471
  break;
1426
1472
 
@@ -1451,9 +1497,9 @@ export function processMessage(msg) {
1451
1497
 
1452
1498
  // --- Ralph Loop ---
1453
1499
  case "loop_available":
1454
- store.setState({ loopAvailable: msg.available, loopActive: msg.active, loopIteration: msg.iteration || 0, loopMaxIterations: msg.maxIterations || 20, loopBannerName: msg.name || null });
1455
- updateLoopButton();
1456
- var _la = store.getState();
1500
+ store.set({ loopAvailable: msg.available, loopActive: msg.active, loopIteration: msg.iteration || 0, loopMaxIterations: msg.maxIterations || 20, loopBannerName: msg.name || null });
1501
+
1502
+ var _la = store.snap();
1457
1503
  if (_la.loopActive) {
1458
1504
  showLoopBanner(true);
1459
1505
  if (_la.loopIteration > 0) {
@@ -1465,27 +1511,27 @@ export function processMessage(msg) {
1465
1511
  break;
1466
1512
 
1467
1513
  case "loop_started":
1468
- store.setState({ loopActive: true, ralphPhase: "executing", loopIteration: 0, loopMaxIterations: msg.maxIterations, loopBannerName: msg.name || null });
1514
+ store.set({ loopActive: true, ralphPhase: "executing", loopIteration: 0, loopMaxIterations: msg.maxIterations, loopBannerName: msg.name || null });
1469
1515
  showLoopBanner(true);
1470
- updateLoopButton();
1471
- var _lbn = store.getState().loopBannerName;
1516
+
1517
+ var _lbn = store.get('loopBannerName');
1472
1518
  addSystemMessage((_lbn || "Loop") + " started (max " + msg.maxIterations + " iterations)", false);
1473
1519
  inputEl.disabled = true;
1474
1520
  inputEl.placeholder = (_lbn || "Loop") + " is running...";
1475
1521
  break;
1476
1522
 
1477
1523
  case "loop_iteration":
1478
- store.setState({ loopIteration: msg.iteration, loopMaxIterations: msg.maxIterations });
1524
+ store.set({ loopIteration: msg.iteration, loopMaxIterations: msg.maxIterations });
1479
1525
  updateLoopBanner(msg.iteration, msg.maxIterations, "running");
1480
- updateLoopButton();
1481
- var _libn = store.getState().loopBannerName;
1526
+
1527
+ var _libn = store.get('loopBannerName');
1482
1528
  addSystemMessage((_libn || "Loop") + " iteration #" + msg.iteration + " started", false);
1483
1529
  inputEl.disabled = true;
1484
1530
  inputEl.placeholder = (_libn || "Loop") + " is running...";
1485
1531
  break;
1486
1532
 
1487
1533
  case "loop_judging":
1488
- var _ljs = store.getState();
1534
+ var _ljs = store.snap();
1489
1535
  updateLoopBanner(_ljs.loopIteration, _ljs.loopMaxIterations, "judging");
1490
1536
  addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
1491
1537
  inputEl.disabled = true;
@@ -1497,15 +1543,15 @@ export function processMessage(msg) {
1497
1543
  break;
1498
1544
 
1499
1545
  case "loop_stopping":
1500
- var _lss = store.getState();
1546
+ var _lss = store.snap();
1501
1547
  updateLoopBanner(_lss.loopIteration, _lss.loopMaxIterations, "stopping");
1502
1548
  break;
1503
1549
 
1504
1550
  case "loop_finished":
1505
- var _lfbn = store.getState().loopBannerName;
1506
- store.setState({ loopActive: false, ralphPhase: "done", loopBannerName: null });
1551
+ var _lfbn = store.get('loopBannerName');
1552
+ store.set({ loopActive: false, ralphPhase: "done", loopBannerName: null });
1507
1553
  showLoopBanner(false);
1508
- updateLoopButton();
1554
+
1509
1555
  enableMainInput();
1510
1556
  updateLoopInputVisibility(null);
1511
1557
  var loopLabel = _lfbn || "Loop";
@@ -1520,7 +1566,7 @@ export function processMessage(msg) {
1520
1566
  break;
1521
1567
 
1522
1568
  case "loop_error":
1523
- addSystemMessage((store.getState().loopBannerName || "Loop") + " error: " + msg.text, true);
1569
+ addSystemMessage((store.get('loopBannerName') || "Loop") + " error: " + msg.text, true);
1524
1570
  break;
1525
1571
 
1526
1572
  // --- Ralph Wizard / Crafting ---
@@ -1528,16 +1574,16 @@ export function processMessage(msg) {
1528
1574
  var _rps = { ralphPhase: msg.phase || "idle" };
1529
1575
  if (msg.craftingSessionId) _rps.ralphCraftingSessionId = msg.craftingSessionId;
1530
1576
  if (msg.source !== undefined) _rps.ralphCraftingSource = msg.source;
1531
- store.setState(_rps);
1532
- if (msg.wizardData) store.setState({ wizardData: msg.wizardData });
1533
- updateLoopButton();
1534
- updateRalphBars();
1577
+ store.set(_rps);
1578
+ if (msg.wizardData) store.set({ wizardData: msg.wizardData });
1579
+
1580
+
1535
1581
  break;
1536
1582
 
1537
1583
  case "ralph_crafting_started":
1538
- store.setState({ ralphPhase: "crafting", ralphCraftingSessionId: msg.sessionId || store.getState().activeSessionId, ralphCraftingSource: msg.source || null });
1539
- updateLoopButton();
1540
- updateRalphBars();
1584
+ store.set({ ralphPhase: "crafting", ralphCraftingSessionId: msg.sessionId || store.get('activeSessionId'), ralphCraftingSource: msg.source || null });
1585
+
1586
+
1541
1587
  if (msg.source !== "ralph") {
1542
1588
  // Task sessions open in the scheduler calendar window
1543
1589
  enterCraftingMode(msg.sessionId, msg.taskId);
@@ -1546,22 +1592,22 @@ export function processMessage(msg) {
1546
1592
  break;
1547
1593
 
1548
1594
  case "ralph_files_status":
1549
- store.setState({ ralphFilesReady: {
1595
+ store.set({ ralphFilesReady: {
1550
1596
  promptReady: msg.promptReady,
1551
1597
  judgeReady: msg.judgeReady,
1552
1598
  bothReady: msg.bothReady,
1553
1599
  } });
1554
1600
  if (msg.bothReady) {
1555
- var _rfs = store.getState();
1601
+ var _rfs = store.snap();
1556
1602
  if (_rfs.ralphPhase === "crafting" || _rfs.ralphPhase === "approval") {
1557
- store.setState({ ralphPhase: "approval" });
1603
+ store.set({ ralphPhase: "approval" });
1558
1604
  if (_rfs.ralphCraftingSource !== "ralph" || isSchedulerOpen()) {
1559
1605
  // Task crafting in scheduler: switch from crafting chat to detail view showing files
1560
1606
  exitCraftingMode(msg.taskId);
1561
1607
  } else {
1562
1608
  showRalphApprovalBar(true);
1563
1609
  // Auto-show execution modal (one-time) for Ralph source
1564
- if (!store.getState().execModalShown && _rfs.ralphCraftingSource === "ralph") {
1610
+ if (!store.get('execModalShown') && _rfs.ralphCraftingSource === "ralph") {
1565
1611
  showExecModal();
1566
1612
  }
1567
1613
  }
@@ -1576,7 +1622,7 @@ export function processMessage(msg) {
1576
1622
  break;
1577
1623
 
1578
1624
  case "ralph_files_content":
1579
- store.setState({ ralphPreviewContent: { prompt: msg.prompt || "", judge: msg.judge || "" } });
1625
+ store.set({ ralphPreviewContent: { prompt: msg.prompt || "", judge: msg.judge || "" } });
1580
1626
  openRalphPreviewModal();
1581
1627
  break;
1582
1628