clay-server 2.27.0-beta.8 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -0,0 +1,1560 @@
1
+ // app-messages.js - WebSocket message router
2
+ // Extracted from app.js (PR-23)
3
+ // All dependencies are direct imports; no context injection needed.
4
+
5
+ import { store } from './store.js';
6
+ import { getWs } from './ws-ref.js';
7
+
8
+ // --- Leaf module imports ---
9
+ import { showToast } from './utils.js';
10
+ import { refreshIcons, iconHtml } from './icons.js';
11
+ import { renderMarkdown } from './markdown.js';
12
+ import { updatePageTitle } from './sidebar.js';
13
+ import { renderSessionList, updateSessionPresence, populateCliSessionList, handleSearchResults, updateSessionBadge } from './sidebar-sessions.js';
14
+ import { renderUserStrip, updateDmBadge, renderSidebarPresence } from './sidebar-mates.js';
15
+ import { refreshMobileChatSheet } from './sidebar-mobile.js';
16
+ import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
17
+ import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
18
+ import { renderMemoryList } from './mate-memory.js';
19
+ import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
20
+ import { handleFindInSessionResults } from './session-search.js';
21
+ import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
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, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
23
+ import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
24
+ import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate } from './filebrowser.js';
25
+ import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
26
+ import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
27
+ import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
28
+ import { updateTerminalList, handleContextSourcesState } from './context-sources.js';
29
+ import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './sticky-notes.js';
30
+ import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
31
+ import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
32
+ import { checkAdminAccess } from './admin.js';
33
+ import { mateAvatarUrl } from './avatar.js';
34
+ import { showImageModal, sendExtensionCommand } from './app-misc.js';
35
+ import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
36
+
37
+ // --- App module imports ---
38
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, addAuthRequiredMessage, showSuggestionChips } from './app-rendering.js';
39
+ import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot } from './app-favicon.js';
40
+ import { setStatus } from './app-connection.js';
41
+ import { updateConfigChip, getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
42
+ import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
43
+ import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
44
+ import { hideHomeHub, handleHubSchedules } from './app-home-hub.js';
45
+ import { openDm, enterDmMode, exitDmMode, handleMateCreatedInApp, updateMateIconStatus, appendDmMessage, showDmTypingIndicator, buildMateInterviewPrompt } from './app-dm.js';
46
+ import { handleRateLimitEvent, updateRateLimitUsage, addScheduledMessageBubble, removeScheduledMessageBubble, handleFastModeState } from './app-rate-limit.js';
47
+ import { handleRemoteCursorMove, handleRemoteCursorLeave, handleRemoteSelection, clearRemoteCursors } from './app-cursors.js';
48
+ import { updateLoopButton, showLoopBanner, updateLoopBanner, updateRalphBars, updateLoopInputVisibility, showRalphApprovalBar, updateRalphApprovalStatus, openRalphPreviewModal, showExecModal, updateExecModalStatus } from './app-loop-ui.js';
49
+ import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitDebateFloorMode, exitDebateConcludeMode, exitDebateEndedMode, updateDebateRound, renderDebateUserFloorDone } from './app-debate-ui.js';
50
+ import { handleSkillInstallWs } from './app-skills-install.js';
51
+ import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll } from './app-notifications.js';
52
+ import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
53
+ import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse } from './mention.js';
54
+
55
+ // --- DOM refs (cached once, stable for page lifetime) ---
56
+ var messagesEl = document.getElementById("messages");
57
+ var headerTitleEl = document.getElementById("header-title");
58
+ var inputEl = document.getElementById("input");
59
+ var connectOverlay = document.getElementById("connect-overlay");
60
+
61
+ export function processMessage(msg) {
62
+ // Preserve original timestamp from history replay
63
+ store.setState({ currentMsgTs: msg._ts || null });
64
+ var isMateDm = store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate;
65
+
66
+ // DEBUG: trace session/history loading
67
+ if (msg.type === "session_switched" || msg.type === "history_meta" || msg.type === "history_done" || msg.type === "mention_user" || msg.type === "mention_response") {
68
+ console.log("[DEBUG msg]", msg.type, msg.type === "session_switched" ? "id=" + msg.id + " cli=" + (msg.cliSessionId || "").substring(0, 8) : "", msg.type === "history_meta" ? "from=" + msg.from + " total=" + msg.total : "", msg.type === "mention_user" ? "mate=" + msg.mateName : "", "dmMode=" + store.getState().dmMode);
69
+ }
70
+
71
+ // Mate DM: update mate icon status indicators
72
+ if (isMateDm) updateMateIconStatus(msg);
73
+
74
+ // Mate DM: intercept mate-specific messages
75
+ if (isMateDm) {
76
+ if (msg.type === "session_list") {
77
+ renderMateSessionList(msg.sessions || []);
78
+ refreshMobileChatSheet();
79
+ // Override title bar with mate name and re-apply color
80
+ var _mdn = (store.getState().dmTargetUser.displayName || "New Mate");
81
+ if (headerTitleEl) headerTitleEl.textContent = _mdn;
82
+ var _tbpn = document.getElementById("title-bar-project-name");
83
+ if (_tbpn) _tbpn.textContent = _mdn;
84
+ var _mc2 = (store.getState().dmTargetUser.profile && store.getState().dmTargetUser.profile.avatarColor) || store.getState().dmTargetUser.avatarColor || "#7c3aed";
85
+ var _tbc2 = document.querySelector(".title-bar-content");
86
+ if (_tbc2) { _tbc2.style.background = _mc2; _tbc2.classList.add("mate-dm-active"); }
87
+ document.body.classList.add("mate-dm-active");
88
+ // Still let normal session_list handler run below
89
+ }
90
+ if (msg.type === "search_results") {
91
+ handleMateSearchResults(msg);
92
+ return;
93
+ }
94
+ if (msg.type === "knowledge_list") {
95
+ renderKnowledgeList(msg.files);
96
+ return;
97
+ }
98
+ if (msg.type === "knowledge_content") {
99
+ handleKnowledgeContent(msg);
100
+ return;
101
+ }
102
+ if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted" || msg.type === "knowledge_promoted" || msg.type === "knowledge_depromoted") {
103
+ return;
104
+ }
105
+ if (msg.type === "memory_list") {
106
+ renderMemoryList(msg.entries, msg.summary);
107
+ return;
108
+ }
109
+ if (msg.type === "memory_deleted") {
110
+ return;
111
+ }
112
+ // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
113
+ if (msg.type === "done") {
114
+ setTimeout(function () { scrollToBottom(); }, 100);
115
+ setTimeout(function () { scrollToBottom(); }, 400);
116
+ setTimeout(function () {
117
+ var fullText = messagesEl ? messagesEl.textContent : "";
118
+ var readyMatch = fullText.match(/\[\[MATE_READY:\s*(.+?)\]\]/);
119
+ if (readyMatch) {
120
+ var newName = readyMatch[1].trim();
121
+ store.getState().dmTargetUser.displayName = newName;
122
+ updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: store.getState().dmTargetUser.avatarColor, avatarStyle: store.getState().dmTargetUser.avatarStyle, avatarSeed: store.getState().dmTargetUser.avatarSeed } });
123
+ if (getWs() && getWs().readyState === 1) {
124
+ getWs().send(JSON.stringify({
125
+ type: "mate_update",
126
+ mateId: store.getState().dmTargetUser.id,
127
+ updates: { name: newName, status: "ready", profile: { displayName: newName } },
128
+ }));
129
+ }
130
+ }
131
+ var walker = document.createTreeWalker(messagesEl, NodeFilter.SHOW_TEXT, null, false);
132
+ var node;
133
+ while (node = walker.nextNode()) {
134
+ if (node.nodeValue.indexOf("[[MATE_READY:") !== -1) {
135
+ node.nodeValue = node.nodeValue.replace(/\[\[MATE_READY:\s*.+?\]\]/g, "").trim();
136
+ }
137
+ }
138
+ }, 100);
139
+ }
140
+ }
141
+
142
+ switch (msg.type) {
143
+ case "history_meta":
144
+ store.setState({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
145
+ updateHistorySentinel();
146
+ break;
147
+
148
+ case "history_prepend":
149
+ prependOlderHistory(msg.items, msg.meta);
150
+ break;
151
+
152
+ case "history_done":
153
+ store.setState({ replayingHistory: false });
154
+ // Restore cached rich context usage BEFORE updateContextPanel runs
155
+ if (msg.contextUsage) {
156
+ store.setState({ richContextUsage: msg.contextUsage });
157
+ }
158
+ // Restore accurate context data from the last result in full history
159
+ if (msg.lastUsage || msg.lastModelUsage) {
160
+ accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
161
+ }
162
+ updateContextPanel();
163
+ updateUsagePanel();
164
+ // Render + finalize any incomplete turn from the replayed history
165
+ var _hs = store.getState();
166
+ if (_hs.currentMsgEl && _hs.currentFullText) {
167
+ var replayContentEl = _hs.currentMsgEl.querySelector(".md-content");
168
+ if (replayContentEl) {
169
+ replayContentEl.innerHTML = renderMarkdown(_hs.currentFullText);
170
+ }
171
+ }
172
+ markAllToolsDone();
173
+ finalizeAssistantBlock();
174
+ stopUrgentBlink();
175
+ // Clean up debate UI if debate is not active after replay
176
+ if (!isDebateActive()) {
177
+ var dbBar = document.getElementById("debate-bottom-bar");
178
+ if (dbBar) dbBar.remove();
179
+ var dhBar = document.getElementById("debate-hand-raise-bar");
180
+ if (dhBar) dhBar.remove();
181
+ var dbBadges = document.querySelectorAll(".debate-header-badge");
182
+ for (var dbi = 0; dbi < dbBadges.length; dbi++) dbBadges[dbi].remove();
183
+ // Clean up all debate mode banners if debate is not active on this session
184
+ var _ds = store.getState();
185
+ if (_ds.debateFloorMode) exitDebateFloorMode();
186
+ if (_ds.debateConcludeMode) exitDebateConcludeMode();
187
+ if (_ds.debateEndedMode) exitDebateEndedMode();
188
+ var dbBanner = document.getElementById("debate-floor-banner");
189
+ if (dbBanner) dbBanner.remove();
190
+ }
191
+ scrollToBottom();
192
+ // Scroll to tool element if navigating from file edit history
193
+ var nav = getPendingNavigate();
194
+ if (nav && (nav.toolId || nav.assistantUuid)) {
195
+ requestAnimationFrame(function() {
196
+ // Prefer scrolling to the exact tool element
197
+ var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
198
+ if (!target && nav.assistantUuid) {
199
+ target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
200
+ }
201
+ if (target) {
202
+ // Auto-expand parent tool group if collapsed
203
+ var parentGroup = target.closest(".tool-group");
204
+ if (parentGroup) parentGroup.classList.remove("collapsed");
205
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
206
+ target.classList.add("message-blink");
207
+ setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
208
+ }
209
+ });
210
+ }
211
+ break;
212
+
213
+ case "restore_mate_dm":
214
+ if (msg.mateId && !store.getState().returningFromMateDm) {
215
+ // Server-driven mate DM restore on reconnect
216
+ // Note: do NOT remove mate-dm-active here; openDm is async (skill check)
217
+ // and removing the class causes a flash where mate UI is lost.
218
+ // enterDmMode will properly set/reset the class when DM is entered.
219
+ if (store.getState().dmMode) {
220
+ store.setState({ dmMode: false });
221
+ }
222
+ messagesEl.innerHTML = "";
223
+ openDm(msg.mateId);
224
+ }
225
+ // Clear the flag and notify server that mate DM is closed
226
+ if (store.getState().returningFromMateDm) {
227
+ store.setState({ returningFromMateDm: false });
228
+ if (getWs() && getWs().readyState === 1) {
229
+ try { getWs().send(JSON.stringify({ type: "set_mate_dm", mateId: null })); } catch(e) {}
230
+ }
231
+ }
232
+ break;
233
+
234
+ case "info":
235
+ if (msg.text && !msg.project && !msg.cwd) {
236
+ addSystemMessage(msg.text, false);
237
+ break;
238
+ }
239
+ store.setState({ projectName: msg.project || msg.cwd });
240
+ if (msg.slug) store.setState({ currentSlug: msg.slug });
241
+ try { var _is = store.getState(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
242
+ // In mate DM, keep title as mate name and re-apply mate color
243
+ if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate) {
244
+ var _mateDN = store.getState().dmTargetUser.displayName || "New Mate";
245
+ headerTitleEl.textContent = _mateDN;
246
+ var tbProjectName = document.getElementById("title-bar-project-name");
247
+ if (tbProjectName) tbProjectName.textContent = _mateDN;
248
+ // Re-apply mate title bar styling (may be lost during project switch)
249
+ var _mc = (store.getState().dmTargetUser.profile && store.getState().dmTargetUser.profile.avatarColor) || store.getState().dmTargetUser.avatarColor || "#7c3aed";
250
+ var _tbc = document.querySelector(".title-bar-content");
251
+ if (_tbc) { _tbc.style.background = _mc; _tbc.classList.add("mate-dm-active"); }
252
+ document.body.classList.add("mate-dm-active");
253
+ } else {
254
+ headerTitleEl.textContent = store.getState().projectName;
255
+ var tbProjectName = document.getElementById("title-bar-project-name");
256
+ if (tbProjectName) tbProjectName.textContent = msg.title || store.getState().projectName;
257
+ }
258
+ updatePageTitle();
259
+ if (msg.version) {
260
+ setPaletteVersion(msg.version);
261
+ var serverVersionEl = document.getElementById("settings-server-version");
262
+ if (serverVersionEl) serverVersionEl.textContent = msg.version;
263
+ }
264
+ if (msg.projectOwnerId !== undefined) store.setState({ currentProjectOwnerId: msg.projectOwnerId });
265
+ if (msg.osUsers !== undefined) store.setState({ isOsUsers: !!msg.osUsers });
266
+ if (msg.lanHost) window.__lanHost = msg.lanHost;
267
+ if (msg.dangerouslySkipPermissions) {
268
+ store.setState({ skipPermsEnabled: true });
269
+ var spBanner = document.getElementById("skip-perms-pill");
270
+ if (spBanner) spBanner.classList.remove("hidden");
271
+ }
272
+ updateProjectList(msg);
273
+ break;
274
+
275
+ case "update_available":
276
+ // In multi-user mode, only show update UI to admins
277
+ if (store.getState().isMultiUserMode) {
278
+ checkAdminAccess().then(function (isAdmin) {
279
+ if (!isAdmin) return;
280
+ showUpdateAvailable(msg);
281
+ });
282
+ } else {
283
+ showUpdateAvailable(msg);
284
+ }
285
+ break;
286
+
287
+ case "up_to_date":
288
+ var utdBtn = document.getElementById("settings-update-check");
289
+ if (utdBtn) {
290
+ utdBtn.innerHTML = "";
291
+ var utdIcon = document.createElement("i");
292
+ utdIcon.setAttribute("data-lucide", "check");
293
+ utdBtn.appendChild(utdIcon);
294
+ utdBtn.appendChild(document.createTextNode(" Up to date (v" + msg.version + ")"));
295
+ utdBtn.disabled = true;
296
+ refreshIcons();
297
+ setTimeout(function () {
298
+ utdBtn.innerHTML = "";
299
+ var rwIcon = document.createElement("i");
300
+ rwIcon.setAttribute("data-lucide", "refresh-cw");
301
+ utdBtn.appendChild(rwIcon);
302
+ utdBtn.appendChild(document.createTextNode(" Check for updates"));
303
+ utdBtn.disabled = false;
304
+ utdBtn.classList.remove("settings-btn-update-available");
305
+ refreshIcons();
306
+ }, 3000);
307
+ }
308
+ break;
309
+
310
+ case "update_started":
311
+ var updNowBtn = document.getElementById("update-now");
312
+ if (updNowBtn) {
313
+ updNowBtn.innerHTML = '<i data-lucide="loader"></i> Updating...';
314
+ updNowBtn.disabled = true;
315
+ refreshIcons();
316
+ var spinIcon = updNowBtn.querySelector(".lucide");
317
+ if (spinIcon) spinIcon.classList.add("icon-spin-inline");
318
+ }
319
+ // Block the entire screen with the connect overlay
320
+ connectOverlay.classList.remove("hidden");
321
+ break;
322
+
323
+ case "slash_commands":
324
+ var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
325
+ store.setState({ slashCommands: (msg.commands || []).filter(function (name) {
326
+ return !reserved.has(name);
327
+ }).map(function (name) {
328
+ return { name: name, desc: "Skill" };
329
+ }) });
330
+ break;
331
+
332
+ case "model_info":
333
+ store.setState({ currentModel: msg.model || store.getState().currentModel, currentModels: msg.models || [] });
334
+ updateConfigChip();
335
+ updateSettingsModels(msg.model, msg.models || []);
336
+ break;
337
+
338
+ case "config_state": {
339
+ var _cs = {};
340
+ if (msg.model) _cs.currentModel = msg.model;
341
+ if (msg.mode) _cs.currentMode = msg.mode;
342
+ if (msg.effort) _cs.currentEffort = msg.effort;
343
+ if (msg.betas) _cs.currentBetas = msg.betas;
344
+ if (msg.thinking) _cs.currentThinking = msg.thinking;
345
+ if (msg.thinkingBudget) _cs.currentThinkingBudget = msg.thinkingBudget;
346
+ store.setState(_cs);
347
+ // Validate effort against current model's supported levels
348
+ var _csRead = store.getState();
349
+ if (_csRead.currentModels.length > 0) {
350
+ var levels = getModelEffortLevels();
351
+ var effortValid = false;
352
+ for (var ei = 0; ei < levels.length; ei++) {
353
+ if (levels[ei] === _csRead.currentEffort) { effortValid = true; break; }
354
+ }
355
+ if (!effortValid) store.setState({ currentEffort: "medium" });
356
+ }
357
+ updateConfigChip();
358
+ } break;
359
+
360
+ case "client_count":
361
+ // Sidebar presence: current project's online users
362
+ if (msg.users) {
363
+ renderSidebarPresence(msg.users);
364
+ }
365
+ // Non-multi-user mode: simple count in topbar
366
+ if (!msg.users) {
367
+ var countEl = document.getElementById("client-count");
368
+ var countTextEl = document.getElementById("client-count-text");
369
+ if (countEl && countTextEl) {
370
+ if (msg.count > 1) {
371
+ countTextEl.textContent = msg.count + " connected";
372
+ countEl.classList.remove("hidden");
373
+ } else {
374
+ countEl.classList.add("hidden");
375
+ }
376
+ }
377
+ }
378
+ break;
379
+
380
+ case "toast":
381
+ showToast(msg.message, msg.level, msg.detail);
382
+ break;
383
+
384
+ case "skill_installed":
385
+ handleSkillInstalled(msg);
386
+ if (msg.success) { var _kis = Object.assign({}, store.getState().knownInstalledSkills); _kis[msg.skill] = true; store.setState({ knownInstalledSkills: _kis }); }
387
+ handleSkillInstallWs(msg);
388
+ break;
389
+
390
+ case "skill_uninstalled":
391
+ handleSkillUninstalled(msg);
392
+ if (msg.success) { var _kis2 = Object.assign({}, store.getState().knownInstalledSkills); delete _kis2[msg.skill]; store.setState({ knownInstalledSkills: _kis2 }); }
393
+ break;
394
+
395
+ case "loop_registry_updated":
396
+ handleLoopRegistryUpdated(msg);
397
+ break;
398
+
399
+ case "schedule_run_started":
400
+ handleScheduleRunStarted(msg);
401
+ break;
402
+
403
+ case "schedule_run_finished":
404
+ handleScheduleRunFinished(msg);
405
+ break;
406
+
407
+ case "loop_scheduled":
408
+ handleLoopScheduled(msg);
409
+ break;
410
+
411
+ case "schedule_move_result":
412
+ if (msg.ok) {
413
+ showToast("Task moved", "success");
414
+ } else {
415
+ showToast(msg.error || "Failed to move task", "error");
416
+ }
417
+ break;
418
+
419
+ case "remove_project_check_result":
420
+ handleRemoveProjectCheckResult(msg);
421
+ break;
422
+
423
+ case "hub_schedules":
424
+ handleHubSchedules(msg);
425
+ break;
426
+
427
+ case "input_sync":
428
+ if (!store.getState().dmMode) handleInputSync(msg.text);
429
+ break;
430
+
431
+ case "session_list":
432
+ renderMateSessionList(msg.sessions || []);
433
+ renderSessionList(msg.sessions || []);
434
+ handlePaletteSessionSwitch();
435
+ break;
436
+
437
+ case "session_presence":
438
+ updateSessionPresence(msg.presence || {});
439
+ break;
440
+
441
+ case "cursor_move":
442
+ handleRemoteCursorMove(msg);
443
+ break;
444
+
445
+ case "cursor_leave":
446
+ handleRemoteCursorLeave(msg);
447
+ break;
448
+
449
+ case "text_select":
450
+ handleRemoteSelection(msg);
451
+ break;
452
+
453
+ case "session_io":
454
+ blinkSessionDot(msg.id);
455
+ break;
456
+
457
+ case "session_unread":
458
+ updateSessionBadge(msg.id, msg.count);
459
+ break;
460
+
461
+ case "search_results":
462
+ handleSearchResults(msg);
463
+ break;
464
+
465
+ case "search_content_results":
466
+ if (msg.source === "find_in_session") {
467
+ handleFindInSessionResults(msg);
468
+ }
469
+ break;
470
+
471
+ case "cli_session_list":
472
+ populateCliSessionList(msg.sessions || []);
473
+ break;
474
+
475
+ case "session_switched":
476
+ hideHomeHub();
477
+ // Save draft from outgoing session
478
+ var _prevSid = store.getState().activeSessionId;
479
+ if (_prevSid && inputEl.value) {
480
+ store.getState().sessionDrafts[_prevSid] = inputEl.value;
481
+ } else if (_prevSid) {
482
+ delete store.getState().sessionDrafts[_prevSid];
483
+ }
484
+ store.setState({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null });
485
+ // Session presence is now tracked server-side (user-presence.json)
486
+ clearRemoteCursors();
487
+ resetClientState();
488
+ updateRalphBars();
489
+ updateLoopInputVisibility(msg.loop);
490
+ // Restore input area visibility (may have been hidden by auth_required)
491
+ var inputAreaSw = document.getElementById("input-area");
492
+ if (inputAreaSw) inputAreaSw.classList.remove("hidden");
493
+ // Restore draft for incoming session
494
+ var draft = store.getState().sessionDrafts[store.getState().activeSessionId] || "";
495
+ inputEl.value = draft;
496
+ autoResize();
497
+ if (!("ontouchstart" in window)) {
498
+ inputEl.focus();
499
+ }
500
+ break;
501
+
502
+ case "session_id":
503
+ store.setState({ cliSessionId: msg.cliSessionId });
504
+ break;
505
+
506
+ case "message_uuid":
507
+ var uuidTarget;
508
+ if (msg.messageType === "user") {
509
+ var allUsers = messagesEl.querySelectorAll(".msg-user:not([data-uuid])");
510
+ if (allUsers.length > 0) uuidTarget = allUsers[allUsers.length - 1];
511
+ } else {
512
+ var allAssistants = messagesEl.querySelectorAll(".msg-assistant:not([data-uuid])");
513
+ if (allAssistants.length > 0) uuidTarget = allAssistants[allAssistants.length - 1];
514
+ }
515
+ if (uuidTarget) {
516
+ uuidTarget.dataset.uuid = msg.uuid;
517
+ if (msg.messageType === "user") addRewindButton(uuidTarget);
518
+ }
519
+ store.getState().messageUuidMap.push({ uuid: msg.uuid, type: msg.messageType });
520
+ break;
521
+
522
+ case "user_message":
523
+ if (msg._internal) break;
524
+ resetThinkingGroup();
525
+ if (msg.planContent) {
526
+ setPlanContent(msg.planContent);
527
+ renderPlanCard(msg.planContent);
528
+ addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null, msg.from, msg.fromName);
529
+ } else {
530
+ addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName);
531
+ }
532
+ break;
533
+
534
+ case "context_preview":
535
+ // Show a Context Card with tab screenshot between user message and assistant response
536
+ if (msg.tab) {
537
+ var card = document.createElement("div");
538
+ card.className = "context-card";
539
+
540
+ // Header
541
+ var header = document.createElement("div");
542
+ header.className = "context-card-header";
543
+ var icon = document.createElement("span");
544
+ icon.className = "context-card-icon";
545
+ icon.innerHTML = iconHtml("globe");
546
+ header.appendChild(icon);
547
+ var label = document.createElement("span");
548
+ label.textContent = "Viewing tab";
549
+ header.appendChild(label);
550
+ card.appendChild(header);
551
+
552
+ // Screenshot
553
+ if (msg.tab.screenshotUrl) {
554
+ var img = document.createElement("img");
555
+ img.className = "context-card-screenshot";
556
+ img.src = msg.tab.screenshotUrl;
557
+ img.loading = "lazy";
558
+ img.addEventListener("click", function () { showImageModal(this.src); });
559
+ card.appendChild(img);
560
+ }
561
+
562
+ // Meta: title + domain
563
+ var tabTitle = msg.tab.title || "";
564
+ var tabDomain = "";
565
+ try { tabDomain = new URL(msg.tab.url).hostname; } catch (e) {}
566
+ if (tabTitle || tabDomain) {
567
+ var meta = document.createElement("div");
568
+ meta.className = "context-card-meta";
569
+ if (msg.tab.favIconUrl) {
570
+ var fav = document.createElement("img");
571
+ fav.className = "context-card-favicon";
572
+ fav.src = msg.tab.favIconUrl;
573
+ fav.width = 14;
574
+ fav.height = 14;
575
+ fav.onerror = function () { this.style.display = "none"; };
576
+ meta.appendChild(fav);
577
+ }
578
+ var titleEl = document.createElement("span");
579
+ titleEl.className = "context-card-title";
580
+ titleEl.textContent = tabTitle;
581
+ meta.appendChild(titleEl);
582
+ if (tabDomain) {
583
+ var domainEl = document.createElement("span");
584
+ domainEl.className = "context-card-domain";
585
+ domainEl.textContent = tabDomain;
586
+ meta.appendChild(domainEl);
587
+ }
588
+ card.appendChild(meta);
589
+ }
590
+
591
+ messagesEl.appendChild(card);
592
+ scrollToBottom();
593
+ }
594
+ break;
595
+
596
+ case "status":
597
+ if (msg.status === "processing") {
598
+ setStatus("processing");
599
+ if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate) && !store.getState().matePreThinkingEl) {
600
+ setActivity("thinking");
601
+ }
602
+ }
603
+ break;
604
+
605
+ case "compacting":
606
+ if (msg.active) {
607
+ setActivity("compacting");
608
+ } else if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate)) {
609
+ setActivity("thinking");
610
+ }
611
+ break;
612
+
613
+ case "thinking_start":
614
+ removeMatePreThinking();
615
+ startThinking();
616
+ break;
617
+
618
+ case "thinking_delta":
619
+ if (typeof msg.text === "string") appendThinking(msg.text);
620
+ break;
621
+
622
+ case "thinking_stop":
623
+ stopThinking(msg.duration);
624
+ if (!(store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate)) {
625
+ setActivity("thinking");
626
+ }
627
+ break;
628
+
629
+ case "delta":
630
+ if (typeof msg.text !== "string") break;
631
+ removeMatePreThinking();
632
+ stopThinking();
633
+ resetThinkingGroup();
634
+ setActivity(null);
635
+ appendDelta(msg.text);
636
+ break;
637
+
638
+ case "tool_start":
639
+ removeMatePreThinking();
640
+ stopThinking();
641
+ markAllToolsDone();
642
+ if (msg.name === "EnterPlanMode") {
643
+ renderPlanBanner("enter");
644
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
645
+ } else if (msg.name === "ExitPlanMode") {
646
+ if (getPlanContent()) {
647
+ renderPlanCard(getPlanContent());
648
+ }
649
+ renderPlanBanner("exit");
650
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
651
+ } else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
652
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
653
+ } else if (getTodoTools()[msg.name]) {
654
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
655
+ } else {
656
+ createToolItem(msg.id, msg.name);
657
+ }
658
+ break;
659
+
660
+ case "tool_executing":
661
+ if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
662
+ var _dpTool = getTools()[msg.id];
663
+ if (_dpTool) {
664
+ if (_dpTool.el) _dpTool.el.style.display = "none";
665
+ _dpTool.done = true;
666
+ _dpTool.hidden = true;
667
+ removeToolFromGroup(msg.id);
668
+ }
669
+ finalizeAssistantBlock();
670
+ renderMcpDebateProposal(msg.id, msg.input);
671
+ startUrgentBlink();
672
+ } else if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
673
+ var askTool = getTools()[msg.id];
674
+ if (askTool) {
675
+ if (askTool.el) askTool.el.style.display = "none";
676
+ askTool.done = true;
677
+ removeToolFromGroup(msg.id);
678
+ }
679
+ renderAskUserQuestion(msg.id, msg.input);
680
+ startUrgentBlink();
681
+ } else if (msg.name === "Write" && msg.input && isPlanFilePath(msg.input.file_path)) {
682
+ setPlanContent(msg.input.content || "");
683
+ updateToolExecuting(msg.id, msg.name, msg.input);
684
+ } else if (msg.name === "Edit" && msg.input && isPlanFilePath(msg.input.file_path)) {
685
+ var pc = getPlanContent() || "";
686
+ if (msg.input.old_string && pc.indexOf(msg.input.old_string) !== -1) {
687
+ if (msg.input.replace_all) {
688
+ setPlanContent(pc.split(msg.input.old_string).join(msg.input.new_string || ""));
689
+ } else {
690
+ setPlanContent(pc.replace(msg.input.old_string, msg.input.new_string || ""));
691
+ }
692
+ }
693
+ updateToolExecuting(msg.id, msg.name, msg.input);
694
+ } else if (msg.name === "TodoWrite") {
695
+ handleTodoWrite(msg.input);
696
+ } else if (msg.name === "TaskCreate") {
697
+ handleTaskCreate(msg.input);
698
+ } else if (msg.name === "TaskUpdate") {
699
+ handleTaskUpdate(msg.input);
700
+ } else if (getTodoTools()[msg.name]) {
701
+ // TaskList, TaskGet - silently skip
702
+ } else {
703
+ var t = getTools()[msg.id];
704
+ if (t && t.hidden) break;
705
+ updateToolExecuting(msg.id, msg.name, msg.input);
706
+ }
707
+ break;
708
+
709
+ case "tool_result": {
710
+ var tr = getTools()[msg.id];
711
+ if (tr && tr.hidden) break; // skip hidden plan tools
712
+ // Always call updateToolResult for Edit (to show diff from input), or when content exists
713
+ if (msg.content != null || msg.images || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
714
+ updateToolResult(msg.id, msg.content || "", msg.is_error || false, msg.images);
715
+ }
716
+ // Refresh file browser if an Edit/Write tool modified the open file
717
+ if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && tr.input && tr.input.file_path) {
718
+ refreshIfOpen(tr.input.file_path);
719
+ }
720
+ }
721
+ break;
722
+
723
+ case "ask_user_answered":
724
+ markAskUserAnswered(msg.toolId, msg.answers);
725
+ stopUrgentBlink();
726
+ break;
727
+
728
+ case "permission_request":
729
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
730
+ startUrgentBlink();
731
+ break;
732
+
733
+ case "permission_cancel":
734
+ markPermissionCancelled(msg.requestId);
735
+ stopUrgentBlink();
736
+ break;
737
+
738
+ case "permission_resolved":
739
+ markPermissionResolved(msg.requestId, msg.decision);
740
+ stopUrgentBlink();
741
+ break;
742
+
743
+ case "permission_request_pending":
744
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
745
+ startUrgentBlink();
746
+ break;
747
+
748
+ case "elicitation_request":
749
+ renderElicitationRequest(msg);
750
+ startUrgentBlink();
751
+ break;
752
+
753
+ case "elicitation_resolved":
754
+ markElicitationResolved(msg.requestId, msg.action);
755
+ stopUrgentBlink();
756
+ break;
757
+
758
+ case "slash_command_result":
759
+ finalizeAssistantBlock();
760
+ var cmdBlock = document.createElement("div");
761
+ cmdBlock.className = "assistant-block";
762
+ cmdBlock.style.maxWidth = "var(--content-width)";
763
+ cmdBlock.style.margin = "12px auto";
764
+ cmdBlock.style.padding = "0 20px";
765
+ var pre = document.createElement("pre");
766
+ pre.style.cssText = "background:var(--code-bg);border:1px solid var(--border-subtle);border-radius:10px;padding:12px 14px;font-family:'SF Mono',Menlo,Monaco,monospace;font-size:12px;line-height:1.55;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto;margin:0";
767
+ pre.textContent = msg.text;
768
+ cmdBlock.appendChild(pre);
769
+ addToMessages(cmdBlock);
770
+ scrollToBottom();
771
+ break;
772
+
773
+ case "subagent_activity":
774
+ updateSubagentActivity(msg.parentToolId, msg.text);
775
+ break;
776
+
777
+ case "subagent_tool":
778
+ addSubagentToolEntry(msg.parentToolId, msg.toolName, msg.toolId, msg.text);
779
+ break;
780
+
781
+ case "subagent_done":
782
+ markSubagentDone(msg.parentToolId, msg.status, msg.summary, msg.usage);
783
+ break;
784
+
785
+ case "task_started":
786
+ initSubagentStop(msg.parentToolId, msg.taskId);
787
+ break;
788
+
789
+ case "task_progress":
790
+ updateSubagentProgress(msg.parentToolId, msg.usage, msg.lastToolName, msg.summary);
791
+ break;
792
+
793
+ case "result":
794
+ setActivity(null);
795
+ stopThinking();
796
+ markAllToolsDone();
797
+ closeToolGroup();
798
+ finalizeAssistantBlock();
799
+ addTurnMeta(msg.cost, msg.duration);
800
+ accumulateUsage(msg.cost, msg.usage);
801
+ accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
802
+ break;
803
+
804
+ case "context_usage":
805
+ if (msg.data && !store.getState().replayingHistory) {
806
+ store.setState({ richContextUsage: msg.data });
807
+ var _hce = store.getState().headerContextEl;
808
+ if (_hce) _hce.removeAttribute("data-tip");
809
+ if (store.getState().ctxPopoverVisible) renderCtxPopover();
810
+ }
811
+ break;
812
+
813
+ case "done":
814
+ setActivity(null);
815
+ stopThinking();
816
+ markAllToolsDone();
817
+ closeToolGroup();
818
+ finalizeAssistantBlock();
819
+ store.setState({ processing: false });
820
+ setStatus("connected");
821
+ if (!store.getState().loopActive) enableMainInput();
822
+ resetToolState();
823
+ stopUrgentBlink();
824
+ if (document.hidden) {
825
+ if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
826
+ if (isNotifSoundEnabled()) playDoneSound();
827
+ }
828
+ break;
829
+
830
+ case "stderr":
831
+ addSystemMessage(msg.text, false);
832
+ break;
833
+
834
+ case "error":
835
+ setActivity(null);
836
+ addSystemMessage(msg.text, true);
837
+ break;
838
+
839
+ case "system_info":
840
+ addSystemMessage(msg.text, false);
841
+ break;
842
+
843
+ case "process_conflict":
844
+ setActivity(null);
845
+ addConflictMessage(msg);
846
+ break;
847
+
848
+ case "context_overflow":
849
+ setActivity(null);
850
+ addContextOverflowMessage(msg);
851
+ break;
852
+
853
+ case "auth_required":
854
+ setActivity(null);
855
+ addAuthRequiredMessage(msg);
856
+ break;
857
+
858
+ case "rate_limit":
859
+ handleRateLimitEvent(msg);
860
+ break;
861
+
862
+ case "rate_limit_usage":
863
+ updateRateLimitUsage(msg);
864
+ break;
865
+
866
+ case "scheduled_message_queued":
867
+ addScheduledMessageBubble(msg.text, msg.resetsAt);
868
+ setScheduleBtnDisabled(true);
869
+ break;
870
+
871
+ case "scheduled_message_sent":
872
+ removeScheduledMessageBubble();
873
+ setScheduleBtnDisabled(false);
874
+ store.setState({ processing: true });
875
+ setStatus("processing");
876
+ break;
877
+
878
+ case "scheduled_message_cancelled":
879
+ removeScheduledMessageBubble();
880
+ setScheduleBtnDisabled(false);
881
+ break;
882
+
883
+ case "auto_continue_scheduled":
884
+ // Scheduler auto-continue, just show info
885
+ break;
886
+
887
+ case "auto_continue_fired":
888
+ store.setState({ processing: true });
889
+ setStatus("processing");
890
+ break;
891
+
892
+ case "prompt_suggestion":
893
+ showSuggestionChips(msg.suggestion);
894
+ break;
895
+
896
+ case "fast_mode_state":
897
+ handleFastModeState(msg.state);
898
+ break;
899
+
900
+ case "process_killed":
901
+ addSystemMessage("Process " + msg.pid + " has been terminated. You can retry your message now.", false);
902
+ break;
903
+
904
+ case "rewind_preview_result":
905
+ showRewindModal(msg);
906
+ break;
907
+
908
+ case "rewind_complete":
909
+ onRewindComplete();
910
+ setRewindMode(false);
911
+ var rewindText = "Rewound to earlier point. Files have been restored.";
912
+ if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
913
+ else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
914
+ addSystemMessage(rewindText, false);
915
+ break;
916
+
917
+ case "rewind_error":
918
+ onRewindError();
919
+ clearPendingRewindUuid();
920
+ addSystemMessage(msg.text || "Rewind failed.", true);
921
+ break;
922
+
923
+ case "fork_complete":
924
+ addSystemMessage("Session forked successfully.");
925
+ break;
926
+
927
+ case "fs_list_result":
928
+ handleFsList(msg);
929
+ break;
930
+
931
+ case "fs_read_result":
932
+ if (msg.path === "CLAUDE.md" && isProjectSettingsOpen()) {
933
+ handleInstructionsRead(msg);
934
+ } else {
935
+ handleFsRead(msg);
936
+ }
937
+ break;
938
+
939
+ case "fs_write_result":
940
+ handleInstructionsWrite(msg);
941
+ break;
942
+
943
+ case "project_env_result":
944
+ handleProjectEnv(msg);
945
+ break;
946
+
947
+ case "set_project_env_result":
948
+ handleProjectEnvSaved(msg);
949
+ break;
950
+
951
+ case "global_claude_md_result":
952
+ handleGlobalClaudeMdRead(msg);
953
+ break;
954
+
955
+ case "write_global_claude_md_result":
956
+ handleGlobalClaudeMdWrite(msg);
957
+ break;
958
+
959
+ case "shared_env_result":
960
+ handleSharedEnv(msg);
961
+ handleProjectSharedEnv(msg);
962
+ break;
963
+
964
+ case "set_shared_env_result":
965
+ handleSharedEnvSaved(msg);
966
+ handleProjectSharedEnvSaved(msg);
967
+ break;
968
+
969
+ case "fs_file_changed":
970
+ handleFileChanged(msg);
971
+ break;
972
+
973
+ case "fs_dir_changed":
974
+ handleDirChanged(msg);
975
+ break;
976
+
977
+ case "fs_file_history_result":
978
+ handleFileHistory(msg);
979
+ break;
980
+
981
+ case "fs_git_diff_result":
982
+ handleGitDiff(msg);
983
+ break;
984
+
985
+ case "fs_file_at_result":
986
+ handleFileAt(msg);
987
+ break;
988
+
989
+ case "term_list":
990
+ handleTermList(msg);
991
+ updateTerminalList(msg.terminals);
992
+ break;
993
+
994
+ case "context_sources_state":
995
+ handleContextSourcesState(msg);
996
+ break;
997
+
998
+ case "extension_command":
999
+ sendExtensionCommand(msg.command, msg.args, msg.requestId);
1000
+ break;
1001
+
1002
+ case "term_created":
1003
+ handleTermCreated(msg);
1004
+ if (store.getState().pendingTermCommand) {
1005
+ var cmd = store.getState().pendingTermCommand;
1006
+ store.setState({ pendingTermCommand: null });
1007
+ // Small delay to let terminal initialize
1008
+ setTimeout(function() {
1009
+ sendTerminalCommand(cmd);
1010
+ }, 300);
1011
+ }
1012
+ break;
1013
+
1014
+ case "term_output":
1015
+ handleTermOutput(msg);
1016
+ break;
1017
+
1018
+ case "term_resized":
1019
+ handleTermResized(msg);
1020
+ break;
1021
+
1022
+ case "term_exited":
1023
+ handleTermExited(msg);
1024
+ break;
1025
+
1026
+ case "term_closed":
1027
+ handleTermClosed(msg);
1028
+ break;
1029
+
1030
+ case "notes_list":
1031
+ handleNotesList(msg);
1032
+ break;
1033
+
1034
+ case "note_created":
1035
+ handleNoteCreated(msg);
1036
+ break;
1037
+
1038
+ case "note_updated":
1039
+ handleNoteUpdated(msg);
1040
+ break;
1041
+
1042
+ case "note_deleted":
1043
+ handleNoteDeleted(msg);
1044
+ break;
1045
+
1046
+ case "process_stats":
1047
+ updateStatusPanel(msg);
1048
+ updateSettingsStats(msg);
1049
+ break;
1050
+
1051
+ case "browse_dir_result":
1052
+ handleBrowseDirResult(msg);
1053
+ break;
1054
+
1055
+ case "add_project_result":
1056
+ handleAddProjectResult(msg);
1057
+ break;
1058
+
1059
+ case "clone_project_progress":
1060
+ handleCloneProgress(msg);
1061
+ break;
1062
+
1063
+ case "remove_project_result":
1064
+ handleRemoveProjectResult(msg);
1065
+ break;
1066
+
1067
+ case "reorder_projects_result":
1068
+ if (!msg.ok) {
1069
+ showToast(msg.error || "Failed to reorder projects", "error");
1070
+ }
1071
+ break;
1072
+
1073
+ case "set_project_title_result":
1074
+ if (!msg.ok) {
1075
+ showToast(msg.error || "Failed to rename project", "error");
1076
+ }
1077
+ break;
1078
+
1079
+ case "set_project_icon_result":
1080
+ if (!msg.ok) {
1081
+ showToast(msg.error || "Failed to set icon", "error");
1082
+ }
1083
+ break;
1084
+
1085
+ case "projects_updated":
1086
+ updateProjectList(msg);
1087
+ break;
1088
+
1089
+ case "project_owner_changed":
1090
+ store.setState({ currentProjectOwnerId: msg.ownerId });
1091
+ handleProjectOwnerChanged(msg);
1092
+ break;
1093
+
1094
+ // --- DM ---
1095
+ case "dm_history":
1096
+ // Attach projectSlug to targetUser for mate DMs
1097
+ if (msg.projectSlug && msg.targetUser) {
1098
+ msg.targetUser.projectSlug = msg.projectSlug;
1099
+ }
1100
+ enterDmMode(msg.dmKey, msg.targetUser, msg.messages);
1101
+ // Auto-send first interview prompt after mate DM opens
1102
+ if (store.getState().pendingMateInterview && msg.targetUser && msg.targetUser.isMate && msg.projectSlug) {
1103
+ var interviewMate = store.getState().pendingMateInterview;
1104
+ store.setState({ pendingMateInterview: null });
1105
+ // Wait for mate project WS to connect, then send interview prompt
1106
+ var checkMateReady = setInterval(function () {
1107
+ if (getWs() && getWs().readyState === 1 && store.getState().mateProjectSlug) {
1108
+ clearInterval(checkMateReady);
1109
+ var interviewText = buildMateInterviewPrompt(interviewMate);
1110
+ getWs().send(JSON.stringify({ type: "message", text: interviewText }));
1111
+ }
1112
+ }, 100);
1113
+ setTimeout(function () { clearInterval(checkMateReady); }, 5000);
1114
+ }
1115
+ break;
1116
+
1117
+ case "dm_message":
1118
+ if (store.getState().dmMode && msg.dmKey === store.getState().dmKey) {
1119
+ showDmTypingIndicator(false); // hide typing when message arrives
1120
+ appendDmMessage(msg.message);
1121
+ scrollToBottom();
1122
+ } else if (msg.message) {
1123
+ // DM notification when not in that DM
1124
+ var fromId = msg.message.from;
1125
+ var _s1 = store.getState();
1126
+ if (fromId && fromId !== _s1.myUserId) {
1127
+ _s1.dmUnread[fromId] = (_s1.dmUnread[fromId] || 0) + 1;
1128
+ // Re-render strip so non-favorited sender appears
1129
+ renderUserStrip(_s1.cachedAllUsers, _s1.cachedOnlineIds, _s1.myUserId, _s1.cachedDmFavorites, _s1.cachedDmConversations, _s1.dmUnread, _s1.dmRemovedUsers, _s1.cachedMatesList);
1130
+ updateDmBadge(fromId, _s1.dmUnread[fromId]);
1131
+ }
1132
+ }
1133
+ break;
1134
+
1135
+ case "dm_typing":
1136
+ if (store.getState().dmMode && msg.dmKey === store.getState().dmKey) {
1137
+ showDmTypingIndicator(msg.typing);
1138
+ }
1139
+ break;
1140
+
1141
+ case "dm_list":
1142
+ // Could be used for DM list view later
1143
+ break;
1144
+
1145
+ case "dm_favorites_updated":
1146
+ // Track users explicitly removed from favorites
1147
+ var _cdf = store.getState().cachedDmFavorites;
1148
+ if (_cdf && msg.dmFavorites) {
1149
+ for (var ri = 0; ri < _cdf.length; ri++) {
1150
+ if (msg.dmFavorites.indexOf(_cdf[ri]) === -1) {
1151
+ store.getState().dmRemovedUsers[_cdf[ri]] = true;
1152
+ }
1153
+ }
1154
+ }
1155
+ // Clear removed flag for users being added back
1156
+ if (msg.dmFavorites) {
1157
+ for (var ai = 0; ai < msg.dmFavorites.length; ai++) {
1158
+ delete store.getState().dmRemovedUsers[msg.dmFavorites[ai]];
1159
+ }
1160
+ }
1161
+ store.setState({ cachedDmFavorites: msg.dmFavorites || [] });
1162
+ var _s2 = store.getState();
1163
+ renderUserStrip(_s2.cachedAllUsers, _s2.cachedOnlineIds, _s2.myUserId, _s2.cachedDmFavorites, _s2.cachedDmConversations, _s2.dmUnread, _s2.dmRemovedUsers, _s2.cachedMatesList);
1164
+ break;
1165
+
1166
+ case "mate_created":
1167
+ handleMateCreatedInApp(msg.mate, msg);
1168
+ break;
1169
+
1170
+ case "mate_deleted":
1171
+ store.setState({ cachedMatesList: store.getState().cachedMatesList.filter(function (m) { return m.id !== msg.mateId; }) });
1172
+ if (msg.availableBuiltins) store.setState({ cachedAvailableBuiltins: msg.availableBuiltins });
1173
+ var _s3 = store.getState();
1174
+ renderUserStrip(_s3.cachedAllUsers, _s3.cachedOnlineIds, _s3.myUserId, _s3.cachedDmFavorites, _s3.cachedDmConversations, _s3.dmUnread, _s3.dmRemovedUsers, _s3.cachedMatesList);
1175
+ // If currently in DM with this mate, exit DM mode
1176
+ if (_s3.dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.id === msg.mateId) {
1177
+ exitDmMode();
1178
+ }
1179
+ break;
1180
+
1181
+ case "mate_updated":
1182
+ if (msg.mate) {
1183
+ var _cml = store.getState().cachedMatesList.slice();
1184
+ for (var mi = 0; mi < _cml.length; mi++) {
1185
+ if (_cml[mi].id === msg.mate.id) {
1186
+ _cml[mi] = msg.mate;
1187
+ break;
1188
+ }
1189
+ }
1190
+ store.setState({ cachedMatesList: _cml });
1191
+ var _s4 = store.getState();
1192
+ renderUserStrip(_s4.cachedAllUsers, _s4.cachedOnlineIds, _s4.myUserId, _s4.cachedDmFavorites, _s4.cachedDmConversations, _s4.dmUnread, _s4.dmRemovedUsers, _cml);
1193
+ // Update mate sidebar if currently viewing this mate
1194
+ if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.isMate && store.getState().dmTargetUser.id === msg.mate.id) {
1195
+ updateMateSidebarProfile(msg.mate);
1196
+ // Sync dmTargetUser so subsequent renders use fresh data
1197
+ var mp2 = msg.mate.profile || {};
1198
+ store.getState().dmTargetUser.displayName = mp2.displayName || msg.mate.name || store.getState().dmTargetUser.displayName;
1199
+ store.getState().dmTargetUser.avatarStyle = mp2.avatarStyle || store.getState().dmTargetUser.avatarStyle;
1200
+ store.getState().dmTargetUser.avatarSeed = mp2.avatarSeed || store.getState().dmTargetUser.avatarSeed;
1201
+ store.getState().dmTargetUser.avatarColor = mp2.avatarColor || store.getState().dmTargetUser.avatarColor;
1202
+ store.getState().dmTargetUser.avatarCustom = mp2.avatarCustom || "";
1203
+ store.getState().dmTargetUser.profile = mp2;
1204
+ // Refresh body dataset so new chat bubbles use the updated avatar
1205
+ document.body.dataset.mateAvatarUrl = mateAvatarUrl(store.getState().dmTargetUser, 36);
1206
+ document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
1207
+ // Update existing chat bubble avatars
1208
+ var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
1209
+ for (var mbi = 0; mbi < mateAvis.length; mbi++) {
1210
+ mateAvis[mbi].src = document.body.dataset.mateAvatarUrl;
1211
+ }
1212
+ }
1213
+ // Update DM header if currently chatting with this mate
1214
+ if (store.getState().dmMode && store.getState().dmTargetUser && store.getState().dmTargetUser.id === msg.mate.id) {
1215
+ var updatedName = (msg.mate.profile && msg.mate.profile.displayName) || msg.mate.name;
1216
+ if (updatedName) {
1217
+ var dmHeaderName = document.getElementById("dm-header-name");
1218
+ if (dmHeaderName) dmHeaderName.textContent = updatedName;
1219
+ var dmInput = document.getElementById("dm-input");
1220
+ if (dmInput) dmInput.placeholder = "Message " + updatedName;
1221
+ }
1222
+ }
1223
+ }
1224
+ break;
1225
+
1226
+ case "mate_list":
1227
+ store.setState({ cachedMatesList: msg.mates || [], cachedAvailableBuiltins: msg.availableBuiltins || [] });
1228
+ var _s5 = store.getState();
1229
+ renderUserStrip(_s5.cachedAllUsers, _s5.cachedOnlineIds, _s5.myUserId, _s5.cachedDmFavorites, _s5.cachedDmConversations, _s5.dmUnread, _s5.dmRemovedUsers, _s5.cachedMatesList);
1230
+ break;
1231
+
1232
+ case "mate_available_builtins":
1233
+ // Handled via mate_list.availableBuiltins now
1234
+ break;
1235
+
1236
+ case "mate_error":
1237
+ showToast(msg.error || "Mate operation failed", "error");
1238
+ break;
1239
+
1240
+ // --- @Mention ---
1241
+ case "mention_processing":
1242
+ // Broadcast: show/hide activity dot on mate avatar across all tabs
1243
+ if (msg.mateId) {
1244
+ var mateContainers = document.querySelectorAll('.icon-strip-mate[data-user-id="' + msg.mateId + '"]');
1245
+ for (var mi = 0; mi < mateContainers.length; mi++) {
1246
+ var dot = mateContainers[mi].querySelector(".icon-strip-status");
1247
+ if (msg.active) {
1248
+ if (dot) dot.classList.add("processing");
1249
+ mateContainers[mi].classList.add("mention-active");
1250
+ } else {
1251
+ if (dot) dot.classList.remove("processing");
1252
+ mateContainers[mi].classList.remove("mention-active");
1253
+ }
1254
+ }
1255
+ }
1256
+ break;
1257
+
1258
+ case "mention_start":
1259
+ handleMentionStart(msg);
1260
+ break;
1261
+
1262
+ case "mention_activity":
1263
+ handleMentionActivity(msg);
1264
+ break;
1265
+
1266
+ case "mention_stream":
1267
+ handleMentionStream(msg);
1268
+ break;
1269
+
1270
+ case "mention_done":
1271
+ handleMentionDone(msg);
1272
+ break;
1273
+
1274
+ case "mention_error":
1275
+ handleMentionError(msg);
1276
+ if (msg.error) showToast("@Mention: " + msg.error, "error");
1277
+ break;
1278
+
1279
+ case "mention_user":
1280
+ // Finalize current assistant block so mention renders in correct DOM position
1281
+ finalizeAssistantBlock();
1282
+ renderMentionUser(msg);
1283
+ break;
1284
+
1285
+ case "mention_response":
1286
+ finalizeAssistantBlock();
1287
+ renderMentionResponse(msg);
1288
+ break;
1289
+
1290
+ // --- Debate ---
1291
+ case "debate_preparing":
1292
+ if (!store.getState().replayingHistory) showDebateSticky("preparing", msg);
1293
+ handleDebatePreparing(msg);
1294
+ break;
1295
+
1296
+ case "debate_brief_ready":
1297
+ if (store.getState().replayingHistory) {
1298
+ renderDebateBriefReady(msg);
1299
+ } else {
1300
+ handleDebateBriefReady(msg);
1301
+ }
1302
+ break;
1303
+
1304
+ case "debate_started":
1305
+ if (!store.getState().replayingHistory) showDebateSticky("live", msg);
1306
+ if (store.getState().replayingHistory) {
1307
+ renderDebateStarted(msg);
1308
+ } else {
1309
+ handleDebateStarted(msg);
1310
+ }
1311
+ break;
1312
+
1313
+ case "debate_turn":
1314
+ handleDebateTurn(msg);
1315
+ if (msg.round) updateDebateRound(msg.round);
1316
+ break;
1317
+
1318
+ case "debate_activity":
1319
+ handleDebateActivity(msg);
1320
+ break;
1321
+
1322
+ case "debate_stream":
1323
+ handleDebateStream(msg);
1324
+ break;
1325
+
1326
+ case "debate_turn_done":
1327
+ if (msg.round) updateDebateRound(msg.round);
1328
+ handleDebateTurnDone(msg);
1329
+ break;
1330
+
1331
+ case "debate_hand_raised":
1332
+ // Visual feedback: hand is raised, waiting for floor
1333
+ break;
1334
+
1335
+ case "debate_comment_queued":
1336
+ handleDebateCommentQueued(msg);
1337
+ break;
1338
+
1339
+ case "debate_comment_injected":
1340
+ if (store.getState().replayingHistory) {
1341
+ renderDebateCommentInjected(msg);
1342
+ } else {
1343
+ handleDebateCommentInjected(msg);
1344
+ }
1345
+ break;
1346
+
1347
+ case "debate_conclude_confirm":
1348
+ if (!store.getState().replayingHistory) showDebateConcludeConfirm(msg);
1349
+ break;
1350
+
1351
+ case "debate_user_floor":
1352
+ if (!store.getState().replayingHistory) showDebateUserFloor(msg);
1353
+ break;
1354
+
1355
+ case "debate_user_floor_done":
1356
+ renderDebateUserFloorDone(msg);
1357
+ break;
1358
+
1359
+ case "debate_user_resume":
1360
+ renderDebateUserResume(msg);
1361
+ break;
1362
+
1363
+ case "debate_resumed":
1364
+ handleDebateResumed(msg);
1365
+ if (!store.getState().replayingHistory) showDebateSticky("live", msg);
1366
+ break;
1367
+
1368
+ case "debate_ended":
1369
+ if (!store.getState().replayingHistory) showDebateSticky("ended", msg);
1370
+ if (store.getState().replayingHistory) {
1371
+ renderDebateEnded(msg);
1372
+ } else {
1373
+ handleDebateEnded(msg);
1374
+ }
1375
+ break;
1376
+
1377
+ case "debate_error":
1378
+ handleDebateError(msg);
1379
+ if (msg.error) showToast("Debate: " + msg.error, "error");
1380
+ break;
1381
+
1382
+ case "daemon_config":
1383
+ if (msg.config && msg.config.headless) store.setState({ isHeadlessMode: true });
1384
+ updateDaemonConfig(msg.config);
1385
+ break;
1386
+
1387
+ case "set_pin_result":
1388
+ handleSetPinResult(msg);
1389
+ break;
1390
+
1391
+ case "set_keep_awake_result":
1392
+ handleKeepAwakeChanged(msg);
1393
+ break;
1394
+
1395
+ case "keep_awake_changed":
1396
+ handleKeepAwakeChanged(msg);
1397
+ break;
1398
+
1399
+ case "set_auto_continue_result":
1400
+ case "auto_continue_changed":
1401
+ handleAutoContinueChanged(msg);
1402
+ break;
1403
+
1404
+ case "restart_server_result":
1405
+ handleRestartResult(msg);
1406
+ break;
1407
+
1408
+ case "shutdown_server_result":
1409
+ handleShutdownResult(msg);
1410
+ break;
1411
+
1412
+ // --- Ralph Loop ---
1413
+ case "loop_available":
1414
+ store.setState({ loopAvailable: msg.available, loopActive: msg.active, loopIteration: msg.iteration || 0, loopMaxIterations: msg.maxIterations || 20, loopBannerName: msg.name || null });
1415
+ updateLoopButton();
1416
+ var _la = store.getState();
1417
+ if (_la.loopActive) {
1418
+ showLoopBanner(true);
1419
+ if (_la.loopIteration > 0) {
1420
+ updateLoopBanner(_la.loopIteration, _la.loopMaxIterations, "running");
1421
+ }
1422
+ inputEl.disabled = true;
1423
+ inputEl.placeholder = (_la.loopBannerName || "Loop") + " is running...";
1424
+ }
1425
+ break;
1426
+
1427
+ case "loop_started":
1428
+ store.setState({ loopActive: true, ralphPhase: "executing", loopIteration: 0, loopMaxIterations: msg.maxIterations, loopBannerName: msg.name || null });
1429
+ showLoopBanner(true);
1430
+ updateLoopButton();
1431
+ var _lbn = store.getState().loopBannerName;
1432
+ addSystemMessage((_lbn || "Loop") + " started (max " + msg.maxIterations + " iterations)", false);
1433
+ inputEl.disabled = true;
1434
+ inputEl.placeholder = (_lbn || "Loop") + " is running...";
1435
+ break;
1436
+
1437
+ case "loop_iteration":
1438
+ store.setState({ loopIteration: msg.iteration, loopMaxIterations: msg.maxIterations });
1439
+ updateLoopBanner(msg.iteration, msg.maxIterations, "running");
1440
+ updateLoopButton();
1441
+ var _libn = store.getState().loopBannerName;
1442
+ addSystemMessage((_libn || "Loop") + " iteration #" + msg.iteration + " started", false);
1443
+ inputEl.disabled = true;
1444
+ inputEl.placeholder = (_libn || "Loop") + " is running...";
1445
+ break;
1446
+
1447
+ case "loop_judging":
1448
+ var _ljs = store.getState();
1449
+ updateLoopBanner(_ljs.loopIteration, _ljs.loopMaxIterations, "judging");
1450
+ addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
1451
+ inputEl.disabled = true;
1452
+ inputEl.placeholder = (_ljs.loopBannerName || "Loop") + " is judging...";
1453
+ break;
1454
+
1455
+ case "loop_verdict":
1456
+ addSystemMessage("Judge: " + msg.verdict.toUpperCase() + " - " + (msg.summary || ""), false);
1457
+ break;
1458
+
1459
+ case "loop_stopping":
1460
+ var _lss = store.getState();
1461
+ updateLoopBanner(_lss.loopIteration, _lss.loopMaxIterations, "stopping");
1462
+ break;
1463
+
1464
+ case "loop_finished":
1465
+ var _lfbn = store.getState().loopBannerName;
1466
+ store.setState({ loopActive: false, ralphPhase: "done", loopBannerName: null });
1467
+ showLoopBanner(false);
1468
+ updateLoopButton();
1469
+ enableMainInput();
1470
+ var loopLabel = _lfbn || "Loop";
1471
+ var finishMsg = msg.reason === "pass"
1472
+ ? loopLabel + " completed successfully after " + msg.iterations + " iteration(s)."
1473
+ : msg.reason === "max_iterations"
1474
+ ? loopLabel + " reached maximum iterations (" + msg.iterations + ")."
1475
+ : msg.reason === "stopped"
1476
+ ? loopLabel + " stopped."
1477
+ : loopLabel + " ended with error.";
1478
+ addSystemMessage(finishMsg, false);
1479
+ break;
1480
+
1481
+ case "loop_error":
1482
+ addSystemMessage((store.getState().loopBannerName || "Loop") + " error: " + msg.text, true);
1483
+ break;
1484
+
1485
+ // --- Ralph Wizard / Crafting ---
1486
+ case "ralph_phase":
1487
+ var _rps = { ralphPhase: msg.phase || "idle" };
1488
+ if (msg.craftingSessionId) _rps.ralphCraftingSessionId = msg.craftingSessionId;
1489
+ if (msg.source !== undefined) _rps.ralphCraftingSource = msg.source;
1490
+ store.setState(_rps);
1491
+ if (msg.wizardData) store.setState({ wizardData: msg.wizardData });
1492
+ updateLoopButton();
1493
+ updateRalphBars();
1494
+ break;
1495
+
1496
+ case "ralph_crafting_started":
1497
+ store.setState({ ralphPhase: "crafting", ralphCraftingSessionId: msg.sessionId || store.getState().activeSessionId, ralphCraftingSource: msg.source || null });
1498
+ updateLoopButton();
1499
+ updateRalphBars();
1500
+ if (msg.source !== "ralph") {
1501
+ // Task sessions open in the scheduler calendar window
1502
+ enterCraftingMode(msg.sessionId, msg.taskId);
1503
+ }
1504
+ // Ralph crafting sessions show in session list as part of the loop group
1505
+ break;
1506
+
1507
+ case "ralph_files_status":
1508
+ store.setState({ ralphFilesReady: {
1509
+ promptReady: msg.promptReady,
1510
+ judgeReady: msg.judgeReady,
1511
+ bothReady: msg.bothReady,
1512
+ } });
1513
+ if (msg.bothReady) {
1514
+ var _rfs = store.getState();
1515
+ if (_rfs.ralphPhase === "crafting" || _rfs.ralphPhase === "approval") {
1516
+ store.setState({ ralphPhase: "approval" });
1517
+ if (_rfs.ralphCraftingSource !== "ralph" || isSchedulerOpen()) {
1518
+ // Task crafting in scheduler: switch from crafting chat to detail view showing files
1519
+ exitCraftingMode(msg.taskId);
1520
+ } else {
1521
+ showRalphApprovalBar(true);
1522
+ // Auto-show execution modal (one-time) for Ralph source
1523
+ if (!store.getState().execModalShown && _rfs.ralphCraftingSource === "ralph") {
1524
+ showExecModal();
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ updateRalphApprovalStatus();
1530
+ updateExecModalStatus();
1531
+ break;
1532
+
1533
+ case "loop_registry_files_content":
1534
+ handleLoopRegistryFiles(msg);
1535
+ break;
1536
+
1537
+ case "ralph_files_content":
1538
+ store.setState({ ralphPreviewContent: { prompt: msg.prompt || "", judge: msg.judge || "" } });
1539
+ openRalphPreviewModal();
1540
+ break;
1541
+
1542
+ case "loop_registry_error":
1543
+ addSystemMessage("Error: " + msg.text, true);
1544
+ break;
1545
+
1546
+ // --- Notifications ---
1547
+ case "notifications_state":
1548
+ handleNotificationsState(msg);
1549
+ break;
1550
+ case "notification_created":
1551
+ handleNotificationCreated(msg);
1552
+ break;
1553
+ case "notification_dismissed":
1554
+ handleNotificationDismissed(msg);
1555
+ break;
1556
+ case "notification_dismissed_all":
1557
+ handleNotificationDismissedAll();
1558
+ break;
1559
+ }
1560
+ }