clay-server 2.22.3-beta.1 → 2.23.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/public/app.js CHANGED
@@ -31,7 +31,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
31
31
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
32
32
  import { initLongPress } from './modules/longpress.js';
33
33
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
34
- import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal } from './modules/debate.js';
34
+ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, openQuickDebateModal, handleDebateBriefReady, renderDebateBriefReady } from './modules/debate.js';
35
35
 
36
36
  // --- Base path for multi-project routing ---
37
37
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -65,9 +65,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
65
65
 
66
66
  // --- DM Mode ---
67
67
  var dmMode = false;
68
- var pendingMateDmRestore = false; // suppress regular history while restoring mate DM
69
68
  var dmKey = null;
70
69
  var dmTargetUser = null;
70
+ var dmMessageCache = []; // cached DM messages for quick debate context
71
71
  var dmUnread = {}; // { otherUserId: count }
72
72
  var cachedAllUsers = [];
73
73
  var cachedOnlineIds = [];
@@ -77,10 +77,10 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
77
77
  var cachedMatesList = []; // Cached list of mates for user strip
78
78
  var cachedAvailableBuiltins = []; // Deleted built-in mates available for re-add
79
79
 
80
- // --- Mate WS (separate connection to mate project) ---
81
- var mateWs = null;
80
+ // --- Mate project switching ---
82
81
  var mateProjectSlug = null;
83
- var savedActiveSessionId = null; // main project session ID saved during mate DM
82
+ var savedMainSlug = null; // main project slug saved during mate DM
83
+ var returningFromMateDm = false; // suppress restore_mate_dm after intentional exit
84
84
 
85
85
  // --- Home Hub ---
86
86
  var homeHub = $("home-hub");
@@ -574,7 +574,6 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
574
574
  console.log("[DEBUG enterDmMode] key=" + key, "isMate=" + (targetUser && targetUser.isMate), "messages=" + (messages ? messages.length : 0));
575
575
  // Clean up previous DM/mate state before entering new one
576
576
  if (dmMode) {
577
- disconnectMateWs();
578
577
  hideMateSidebar();
579
578
  hideKnowledge();
580
579
  // Reset dm-header-bar
@@ -645,11 +644,12 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
645
644
  var resizeHandle = document.getElementById("sidebar-resize-handle");
646
645
  if (resizeHandle) resizeHandle.classList.add("dm-mode");
647
646
  if (isMate && targetUser.projectSlug) {
648
- // Mate DM: connect to mate's own project via separate WS
649
- // Main column stays visible (regular project chat UI), sidebar swaps to mate sidebar
647
+ // Mate DM: switch to mate's project (same as project switching)
650
648
  showMateSidebar(targetUser.id, targetUser);
651
- connectMateWs(targetUser.projectSlug);
652
- // Hide terminal button (not relevant for mate)
649
+ connectMateProject(targetUser.projectSlug);
650
+ // Close file viewer and terminal panel, hide terminal button (not relevant for mate)
651
+ closeFileViewer();
652
+ closeTerminal();
653
653
  var termBtn = document.getElementById("terminal-toggle-btn");
654
654
  if (termBtn) termBtn.style.display = "none";
655
655
  // Apply mate color to chat title bar and panels
@@ -705,6 +705,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
705
705
  if (userIsland && !isMate) userIsland.classList.add("dm-hidden");
706
706
 
707
707
  // Render DM messages
708
+ dmMessageCache = messages ? messages.slice() : [];
708
709
  messagesEl.innerHTML = "";
709
710
  if (messages && messages.length > 0) {
710
711
  for (var i = 0; i < messages.length; i++) {
@@ -744,17 +745,13 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
744
745
  }
745
746
  }
746
747
 
747
- function exitDmMode() {
748
+ function exitDmMode(skipProjectSwitch) {
748
749
  if (!dmMode) return;
750
+ var wasMate = dmTargetUser && dmTargetUser.isMate;
749
751
  dmMode = false;
750
- pendingMateDmRestore = false;
751
752
  dmKey = null;
752
753
  dmTargetUser = null;
753
754
  setCurrentDmUser(null);
754
- // Notify server that mate DM is closed
755
- if (ws && ws.readyState === 1) {
756
- try { ws.send(JSON.stringify({ type: "set_mate_dm", mateId: null })); } catch(e) {}
757
- }
758
755
 
759
756
  var mainCol = document.getElementById("main-column");
760
757
  if (mainCol) mainCol.classList.remove("dm-mode");
@@ -766,15 +763,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
766
763
  hideKnowledge();
767
764
  hideMemory();
768
765
  if (isSchedulerOpen()) closeScheduler();
769
- disconnectMateWs();
770
766
  // Restore terminal button
771
767
  var termBtn = document.getElementById("terminal-toggle-btn");
772
768
  if (termBtn) termBtn.style.display = "";
773
- // Re-request session list and notes from main project to refresh state
774
- if (ws && ws.readyState === 1) {
775
- ws.send(JSON.stringify({ type: "switch_session", id: activeSessionId }));
776
- ws.send(JSON.stringify({ type: "note_list_request" }));
777
- }
778
769
 
779
770
  // Reset DM header
780
771
  var dmHeaderBar = document.getElementById("dm-header-bar");
@@ -807,8 +798,26 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
807
798
  var userIsland = document.getElementById("user-island");
808
799
  if (userIsland) userIsland.classList.remove("dm-hidden");
809
800
 
810
- // Restore project UI
811
801
  if (inputEl) inputEl.placeholder = "";
802
+
803
+ // Switch back to main project (same as project switching)
804
+ if (wasMate && !skipProjectSwitch) {
805
+ disconnectMateProject();
806
+ } else if (wasMate && skipProjectSwitch) {
807
+ // Just clean up mate state, caller will handle project switch
808
+ returningFromMateDm = true;
809
+ mateProjectSlug = null;
810
+ savedMainSlug = null;
811
+ showDebateSticky("hide", null);
812
+ var debateFloat = document.getElementById("debate-info-float");
813
+ if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
814
+ } else {
815
+ // Human DM: just re-request state from main project
816
+ if (ws && ws.readyState === 1) {
817
+ ws.send(JSON.stringify({ type: "switch_session", id: activeSessionId }));
818
+ ws.send(JSON.stringify({ type: "note_list_request" }));
819
+ }
820
+ }
812
821
  renderProjectList();
813
822
  }
814
823
 
@@ -933,307 +942,64 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
933
942
  "Seed Data:\n" + parts.join("\n");
934
943
  }
935
944
 
936
- // --- Mate WS connection ---
937
- var savedMainWs = null; // stash main project ws while in mate DM
938
- var backgroundMateWs = {}; // { slug: WebSocket } - kept alive after exiting mate DM
945
+ // --- Mate icon IO blink ---
946
+ var bgMateIoTimers = {};
939
947
 
940
- function handleMateWsMessage(ev) {
941
- blinkIO();
942
- var msg;
943
- try { msg = JSON.parse(ev.data); } catch (e) { return; }
944
- // IO blink on mate icon strip dot (same as background handler)
948
+ function updateMateIconStatus(msg) {
949
+ if (!mateProjectSlug) return;
950
+ var slug = mateProjectSlug;
945
951
  if (msg.type === "content" || msg.type === "tool" || msg.type === "tool_use" || msg.type === "thinking") {
946
- var slug = mateProjectSlug;
947
- if (slug) {
948
- var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
949
- if (ioDot) {
950
- ioDot.classList.add("io");
951
- clearTimeout(bgMateIoTimers[slug]);
952
- bgMateIoTimers[slug] = setTimeout(function () {
953
- ioDot.classList.remove("io");
954
- }, 80);
955
- }
952
+ var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
953
+ if (ioDot) {
954
+ ioDot.classList.add("io");
955
+ clearTimeout(bgMateIoTimers[slug]);
956
+ bgMateIoTimers[slug] = setTimeout(function () { ioDot.classList.remove("io"); }, 80);
956
957
  }
957
958
  }
958
- // Update processing status on mate icon (same as background handler)
959
959
  if (msg.type === "status" && msg.status === "processing") {
960
- var slug = mateProjectSlug;
961
- if (slug) {
962
- var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
963
- if (dot) { dot.classList.add("processing"); }
964
- }
965
- // Show session processing dot
960
+ var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
961
+ if (dot) dot.classList.add("processing");
966
962
  var mateSessionDot = document.querySelector(".mate-session-item.active .session-processing");
967
963
  if (mateSessionDot) mateSessionDot.style.display = "";
968
964
  }
969
965
  if (msg.type === "done") {
970
- var slug = mateProjectSlug;
971
- if (slug) {
972
- var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
973
- if (dot) { dot.classList.remove("processing"); }
974
- }
975
- // Hide session processing dot
966
+ var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
967
+ if (dot) dot.classList.remove("processing");
976
968
  var mateSessionDot = document.querySelector(".mate-session-item.active .session-processing");
977
969
  if (mateSessionDot) mateSessionDot.style.display = "none";
978
970
  }
979
- // Handle skill_installed in mate DM context (for skill install modal)
980
- if (msg.type === "skill_installed") {
981
- handleSkillInstalled(msg);
982
- if (msg.success) knownInstalledSkills[msg.skill] = true;
983
- handleSkillInstallWs(msg);
984
- }
985
-
986
- // Intercept session_list for mate sidebar
987
- if (msg.type === "init" && msg.sessions) {
988
- renderMateSessionList(msg.sessions);
989
- handlePaletteSessionSwitch();
990
- // Override title bar with mate name (not session/project title)
991
- if (dmTargetUser && dmTargetUser.isMate) {
992
- var mateDN = dmTargetUser.displayName || "New Mate";
993
- if (headerTitleEl) headerTitleEl.textContent = mateDN;
994
- var tbPN = document.getElementById("title-bar-project-name");
995
- if (tbPN) tbPN.textContent = mateDN;
996
- updatePageTitle();
997
- }
998
- }
999
- if (msg.type === "session_list") {
1000
- renderMateSessionList(msg.sessions || []);
1001
- }
1002
- // Intercept search results for mate sessions
1003
- if (msg.type === "search_results") {
1004
- handleMateSearchResults(msg);
1005
- return;
1006
- }
1007
- // Intercept knowledge messages
1008
- if (msg.type === "knowledge_list") {
1009
- renderKnowledgeList(msg.files);
1010
- return;
1011
- }
1012
- if (msg.type === "knowledge_content") {
1013
- handleKnowledgeContent(msg);
1014
- return;
1015
- }
1016
- if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted" || msg.type === "knowledge_promoted" || msg.type === "knowledge_depromoted") {
1017
- return; // list update follows separately
1018
- }
1019
- if (msg.type === "memory_list") {
1020
- renderMemoryList(msg.entries, msg.summary);
1021
- return;
1022
- }
1023
- if (msg.type === "memory_deleted") {
1024
- return; // list update follows separately
1025
- }
1026
- // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
1027
- if (msg.type === "done" && dmTargetUser && dmTargetUser.isMate) {
1028
- // Ensure last message is fully visible after rendering settles
1029
- setTimeout(function () { scrollToBottom(); }, 100);
1030
- setTimeout(function () { scrollToBottom(); }, 400);
1031
- // Let processMessage render first, then scan DOM
1032
- setTimeout(function () {
1033
- var fullText = messagesEl ? messagesEl.textContent : "";
1034
- var readyMatch = fullText.match(/\[\[MATE_READY:\s*(.+?)\]\]/);
1035
- if (readyMatch) {
1036
- var newName = readyMatch[1].trim();
1037
- dmTargetUser.displayName = newName;
1038
- updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: dmTargetUser.avatarColor, avatarStyle: dmTargetUser.avatarStyle, avatarSeed: dmTargetUser.avatarSeed } });
1039
- if (savedMainWs && savedMainWs.readyState === 1) {
1040
- savedMainWs.send(JSON.stringify({
1041
- type: "mate_update",
1042
- mateId: dmTargetUser.id,
1043
- updates: { name: newName, status: "ready", profile: { displayName: newName } },
1044
- }));
1045
- }
1046
- }
1047
- // Strip all MATE_READY markers from visible elements
1048
- var walker = document.createTreeWalker(messagesEl, NodeFilter.SHOW_TEXT, null, false);
1049
- var node;
1050
- while (node = walker.nextNode()) {
1051
- if (node.nodeValue.indexOf("[[MATE_READY:") !== -1) {
1052
- node.nodeValue = node.nodeValue.replace(/\[\[MATE_READY:\s*.+?\]\]/g, "").trim();
1053
- }
1054
- }
1055
- }, 100);
1056
- }
1057
- // Feed everything into the main message handler (renders text, tools, etc.)
1058
- if (msg.type === "session_switched" || msg.type === "history_meta") {
1059
- console.log("[DEBUG mateWs]", msg.type, msg.type === "session_switched" ? "id=" + msg.id + " cli=" + (msg.cliSessionId || "").substring(0, 8) : "from=" + msg.from + " total=" + msg.total);
1060
- }
1061
- processMessage(msg);
1062
- }
1063
-
1064
- // Background handler: update mate icon status when not in DM mode
1065
- var bgMateIoTimers = {};
1066
- function handleBackgroundMateMsg(slug, ev) {
1067
- var msg;
1068
- try { msg = JSON.parse(ev.data); } catch (e) { return; }
1069
- // Update processing status on mate icon
1070
- if (msg.type === "status" && msg.status === "processing") {
1071
- var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
1072
- if (dot) {
1073
- dot.classList.add("processing");
1074
- updateCrossProjectBlink();
1075
- }
1076
- }
1077
- if (msg.type === "done") {
1078
- var dot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
1079
- if (dot) {
1080
- dot.classList.remove("processing");
1081
- updateCrossProjectBlink();
1082
- }
1083
- }
1084
- // IO blink on mate icon for streaming content (same as project IO blink)
1085
- if (msg.type === "content" || msg.type === "tool" || msg.type === "tool_use" || msg.type === "thinking") {
1086
- var ioDot = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"] .icon-strip-status');
1087
- if (ioDot) {
1088
- ioDot.classList.add("io");
1089
- clearTimeout(bgMateIoTimers[slug]);
1090
- bgMateIoTimers[slug] = setTimeout(function () {
1091
- ioDot.classList.remove("io");
1092
- }, 80);
1093
- }
1094
- }
1095
- // Permission requests while backgrounded: show shake on mate icon
1096
- if (msg.type === "permission_request" || msg.type === "permission_request_pending") {
1097
- var mateEl = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"]');
1098
- if (mateEl && !mateEl.classList.contains("active")) {
1099
- mateEl.classList.add("has-pending-perm");
1100
- }
1101
- }
1102
- if (msg.type === "permission_cancel") {
1103
- var mateEl = document.querySelector('.icon-strip-mate[data-mate-slug="' + slug + '"]');
1104
- if (mateEl) mateEl.classList.remove("has-pending-perm");
1105
- }
1106
971
  }
1107
972
 
1108
- function connectMateWs(slug) {
1109
- disconnectMateWs();
973
+ function connectMateProject(slug) {
1110
974
  mateProjectSlug = slug;
1111
-
1112
- // Reuse background WS if available for this mate
1113
- var bgWs = backgroundMateWs[slug];
1114
- if (bgWs && bgWs.readyState === 1) {
1115
- mateWs = bgWs;
1116
- delete backgroundMateWs[slug];
1117
- // Re-attach foreground handler
1118
- mateWs.onmessage = handleMateWsMessage;
1119
- mateWs.onclose = function () {
1120
- if (ws === mateWs) {
1121
- ws = savedMainWs;
1122
- savedMainWs = null;
1123
- }
1124
- mateWs = null;
1125
- delete backgroundMateWs[slug];
1126
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1127
- setStatus("disconnected");
1128
- scheduleReconnect();
1129
- }
1130
- };
1131
- // Swap main ws
1132
- savedMainWs = ws;
1133
- savedActiveSessionId = activeSessionId;
1134
- ws = mateWs;
1135
- connected = true;
1136
- // Wrap send to blink IO on outgoing traffic (if not already wrapped)
1137
- if (!mateWs._ioWrapped) {
1138
- var _origSend = mateWs.send.bind(mateWs);
1139
- mateWs.send = function (data) { blinkIO(); return _origSend(data); };
1140
- mateWs._ioWrapped = true;
1141
- }
1142
- // Re-request state since we were backgrounded
1143
- try { mateWs.send(JSON.stringify({ type: "knowledge_list" })); } catch(e) {}
1144
- // Re-switch to the active session to get fresh history replay
1145
- if (mateWs._bgActiveSession) {
1146
- try { mateWs.send(JSON.stringify({ type: "switch_session", id: mateWs._bgActiveSession })); } catch(e) {}
1147
- }
1148
- return;
1149
- }
1150
- // Clean up stale background WS if any
1151
- if (bgWs) {
1152
- bgWs.onclose = null;
1153
- bgWs.onmessage = null;
1154
- bgWs.close();
1155
- delete backgroundMateWs[slug];
1156
- }
1157
-
1158
- // Block main WS messages immediately (before mate WS connects)
1159
- // so project history doesn't leak into the DM chat
1160
- savedMainWs = ws;
1161
- savedActiveSessionId = activeSessionId;
1162
-
1163
- var protocol = location.protocol === "https:" ? "wss:" : "ws:";
1164
- mateWs = new WebSocket(protocol + "//" + location.host + "/p/" + slug + "/ws");
1165
-
1166
- mateWs.onopen = function () {
1167
- // Swap main ws to mateWs so all UI (input, model selector, etc.) routes through mate project
1168
- ws = mateWs;
1169
- connected = true;
1170
- // Wrap send to blink IO on outgoing traffic
1171
- var _origSend = mateWs.send.bind(mateWs);
1172
- mateWs.send = function (data) { blinkIO(); return _origSend(data); };
1173
- mateWs._ioWrapped = true;
1174
- // Request knowledge list for badge immediately
1175
- try { mateWs.send(JSON.stringify({ type: "knowledge_list" })); } catch(e) {}
1176
- };
1177
-
1178
- mateWs.onmessage = handleMateWsMessage;
1179
-
1180
- mateWs.onclose = function () {
1181
- if (ws === mateWs) {
1182
- ws = savedMainWs;
1183
- savedMainWs = null;
1184
- } else if (savedMainWs && !ws) {
1185
- // Mate WS failed before swap completed; restore main WS
1186
- ws = savedMainWs;
1187
- savedMainWs = null;
1188
- }
1189
- mateWs = null;
1190
- delete backgroundMateWs[slug];
1191
- // If main ws is also closed (server shutdown), show disconnect screen
1192
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1193
- setStatus("disconnected");
1194
- scheduleReconnect();
1195
- }
1196
- };
975
+ savedMainSlug = currentSlug;
976
+ currentSlug = slug;
977
+ wsPath = "/p/" + slug + "/ws";
978
+ resetClientState();
979
+ connect();
1197
980
  }
1198
981
 
1199
- function disconnectMateWs() {
1200
- if (mateWs) {
1201
- // Restore main ws before backgrounding
1202
- if (ws === mateWs && savedMainWs) {
1203
- ws = savedMainWs;
1204
- savedMainWs = null;
1205
- }
1206
- // Keep mate WS alive in background instead of closing
1207
- if (mateProjectSlug && mateWs.readyState === 1) {
1208
- var bgSlug = mateProjectSlug;
1209
- mateWs._bgActiveSession = activeSessionId;
1210
- backgroundMateWs[bgSlug] = mateWs;
1211
- mateWs.onmessage = function (ev) { handleBackgroundMateMsg(bgSlug, ev); };
1212
- mateWs.onclose = function () { delete backgroundMateWs[bgSlug]; };
1213
- } else {
1214
- mateWs.onclose = null;
1215
- mateWs.close();
1216
- }
1217
- mateWs = null;
1218
- }
1219
- // Restore main project's active session ID
1220
- if (savedActiveSessionId) {
1221
- activeSessionId = savedActiveSessionId;
1222
- savedActiveSessionId = null;
1223
- }
982
+ function disconnectMateProject() {
1224
983
  mateProjectSlug = null;
1225
984
  // Hide debate sticky when leaving mate DM
1226
985
  showDebateSticky("hide", null);
1227
986
  // Hide debate info float
1228
987
  var debateFloat = document.getElementById("debate-info-float");
1229
988
  if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
1230
- // If main WS was disconnected while in mate DM, reconnect now
1231
- if (ws && ws.readyState !== 1) {
989
+ // Switch back to main project
990
+ if (savedMainSlug) {
991
+ returningFromMateDm = true;
992
+ currentSlug = savedMainSlug;
993
+ basePath = "/p/" + savedMainSlug + "/";
994
+ wsPath = "/p/" + savedMainSlug + "/ws";
995
+ savedMainSlug = null;
996
+ resetClientState();
1232
997
  connect();
1233
998
  }
1234
999
  }
1235
1000
 
1236
1001
  function appendDmMessage(msg) {
1002
+ if (dmMode) dmMessageCache.push(msg);
1237
1003
  var isMe = msg.from === myUserId;
1238
1004
  var d = new Date(msg.ts);
1239
1005
  var timeStr = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0");
@@ -1949,9 +1715,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
1949
1715
  };
1950
1716
  initSidebar(sidebarCtx);
1951
1717
  initIconStrip(sidebarCtx);
1952
- initMateSidebar(function () { return mateWs; });
1953
- initMateKnowledge(function () { return mateWs; });
1954
- initMateMemory(function () { return mateWs; }, { onShow: function () { hideKnowledge(); hideNotes(); } });
1718
+ initMateSidebar(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1719
+ initMateKnowledge(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1720
+ initMateMemory(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; }, { onShow: function () { hideKnowledge(); hideNotes(); } });
1955
1721
  initMateWizard(
1956
1722
  function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1957
1723
  function (mate) { handleMateCreatedInApp(mate); }
@@ -1959,7 +1725,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
1959
1725
 
1960
1726
  initCommandPalette({
1961
1727
  switchProject: function (slug) { switchProject(slug); },
1962
- currentSlug: function () { return mateProjectSlug || currentSlug; },
1728
+ currentSlug: function () { return currentSlug; },
1963
1729
  projectList: function () { return cachedProjects || []; },
1964
1730
  matesList: function () { return cachedMatesList || []; },
1965
1731
  availableBuiltins: function () { return cachedAvailableBuiltins || []; },
@@ -3874,7 +3640,8 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
3874
3640
  function switchProject(slug) {
3875
3641
  if (!slug) return;
3876
3642
  var wasDm = dmMode;
3877
- if (dmMode) exitDmMode();
3643
+ var wasMate = dmMode && dmTargetUser && dmTargetUser.isMate;
3644
+ if (dmMode) exitDmMode(/* skipProjectSwitch */ wasMate);
3878
3645
  if (homeHubVisible) {
3879
3646
  hideHomeHub();
3880
3647
  if (slug === currentSlug) return;
@@ -3930,9 +3697,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
3930
3697
  ws = new WebSocket(protocol + "//" + location.host + wsPath);
3931
3698
 
3932
3699
 
3933
- // If not connected within 3s, force retry (skip if stashed during mate DM)
3700
+ // If not connected within 3s, force retry
3934
3701
  connectTimeoutId = setTimeout(function () {
3935
- if (!connected && !savedMainWs) {
3702
+ if (!connected) {
3936
3703
  ws.onclose = null;
3937
3704
  ws.onerror = null;
3938
3705
  ws.close();
@@ -3989,14 +3756,25 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
3989
3756
  ws.send(JSON.stringify({ type: "mate_list" }));
3990
3757
  } catch(e) {}
3991
3758
 
3759
+ // If connecting to a mate project, request knowledge list for badge
3760
+ if (mateProjectSlug) {
3761
+ try { ws.send(JSON.stringify({ type: "knowledge_list" })); } catch(e) {}
3762
+ }
3763
+
3992
3764
  // Session restore is now server-driven (user-presence.json).
3993
3765
  // Mate DM restore is also server-driven via "restore_mate_dm" message.
3766
+ // Safety: clear returningFromMateDm after initial messages settle
3767
+ // (handles case where we connect to a non-main project that won't send restore_mate_dm)
3768
+ if (returningFromMateDm) {
3769
+ setTimeout(function () {
3770
+ if (returningFromMateDm) {
3771
+ returningFromMateDm = false;
3772
+ }
3773
+ }, 2000);
3774
+ }
3994
3775
  };
3995
3776
 
3996
3777
  ws.onclose = function (e) {
3997
- // If this WS is stashed while in mate DM, ignore close events
3998
- if (savedMainWs === this) return;
3999
-
4000
3778
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
4001
3779
  closeDmUserPicker();
4002
3780
  setStatus("disconnected");
@@ -4022,10 +3800,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4022
3800
  scheduleReconnect();
4023
3801
  };
4024
3802
 
4025
- ws.onerror = function () {
4026
- // If this WS is stashed while in mate DM, ignore error events
4027
- if (savedMainWs === this) return;
4028
- };
3803
+ ws.onerror = function () {};
4029
3804
 
4030
3805
  function showUpdateAvailable(msg) {
4031
3806
  var updatePillWrap = $("update-pill-wrap");
@@ -4071,18 +3846,6 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4071
3846
 
4072
3847
  ws.onmessage = function (event) {
4073
3848
  // If this WS is stashed while in mate DM, only allow skill_installed through
4074
- if (savedMainWs === this) {
4075
- try {
4076
- var stashedMsg = JSON.parse(event.data);
4077
- if (stashedMsg.type === "skill_installed") {
4078
- handleSkillInstalled(stashedMsg);
4079
- if (stashedMsg.success) knownInstalledSkills[stashedMsg.skill] = true;
4080
- handleSkillInstallWs(stashedMsg);
4081
- }
4082
- } catch (e) {}
4083
- return;
4084
- }
4085
-
4086
3849
  // Backup: if we're receiving messages, we're connected
4087
3850
  if (!connected) {
4088
3851
  setStatus("connected");
@@ -4098,19 +3861,77 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4098
3861
  }
4099
3862
 
4100
3863
  function processMessage(msg) {
3864
+ var isMateDm = dmMode && dmTargetUser && dmTargetUser.isMate;
3865
+
4101
3866
  // DEBUG: trace session/history loading
4102
3867
  if (msg.type === "session_switched" || msg.type === "history_meta" || msg.type === "history_done" || msg.type === "mention_user" || msg.type === "mention_response") {
4103
- 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=" + dmMode, "pending=" + pendingMateDmRestore, "ws===mateWs?" + (ws === mateWs));
3868
+ 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=" + dmMode);
4104
3869
  }
4105
3870
 
4106
- // Suppress regular project messages while restoring mate DM
4107
- if (pendingMateDmRestore) {
4108
- if (msg.type === "dm_history" || msg.type === "dm_list" || msg.type === "mate_list" || msg.type === "mate_created" || msg.type === "info") {
4109
- // Let these through
4110
- console.log("[DEBUG] pendingRestore: letting through", msg.type);
4111
- } else {
4112
- console.log("[DEBUG] pendingRestore: BLOCKING", msg.type);
4113
- return; // skip regular session messages
3871
+ // Mate DM: update mate icon status indicators
3872
+ if (isMateDm) updateMateIconStatus(msg);
3873
+
3874
+ // Mate DM: intercept mate-specific messages
3875
+ if (isMateDm) {
3876
+ if (msg.type === "session_list") {
3877
+ renderMateSessionList(msg.sessions || []);
3878
+ // Also override title bar with mate name
3879
+ var _mdn = (dmTargetUser.displayName || "New Mate");
3880
+ if (headerTitleEl) headerTitleEl.textContent = _mdn;
3881
+ var _tbpn = document.getElementById("title-bar-project-name");
3882
+ if (_tbpn) _tbpn.textContent = _mdn;
3883
+ updatePageTitle();
3884
+ // Still let normal session_list handler run below
3885
+ }
3886
+ if (msg.type === "search_results") {
3887
+ handleMateSearchResults(msg);
3888
+ return;
3889
+ }
3890
+ if (msg.type === "knowledge_list") {
3891
+ renderKnowledgeList(msg.files);
3892
+ return;
3893
+ }
3894
+ if (msg.type === "knowledge_content") {
3895
+ handleKnowledgeContent(msg);
3896
+ return;
3897
+ }
3898
+ if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted" || msg.type === "knowledge_promoted" || msg.type === "knowledge_depromoted") {
3899
+ return;
3900
+ }
3901
+ if (msg.type === "memory_list") {
3902
+ renderMemoryList(msg.entries, msg.summary);
3903
+ return;
3904
+ }
3905
+ if (msg.type === "memory_deleted") {
3906
+ return;
3907
+ }
3908
+ // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
3909
+ if (msg.type === "done") {
3910
+ setTimeout(function () { scrollToBottom(); }, 100);
3911
+ setTimeout(function () { scrollToBottom(); }, 400);
3912
+ setTimeout(function () {
3913
+ var fullText = messagesEl ? messagesEl.textContent : "";
3914
+ var readyMatch = fullText.match(/\[\[MATE_READY:\s*(.+?)\]\]/);
3915
+ if (readyMatch) {
3916
+ var newName = readyMatch[1].trim();
3917
+ dmTargetUser.displayName = newName;
3918
+ updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: dmTargetUser.avatarColor, avatarStyle: dmTargetUser.avatarStyle, avatarSeed: dmTargetUser.avatarSeed } });
3919
+ if (ws && ws.readyState === 1) {
3920
+ ws.send(JSON.stringify({
3921
+ type: "mate_update",
3922
+ mateId: dmTargetUser.id,
3923
+ updates: { name: newName, status: "ready", profile: { displayName: newName } },
3924
+ }));
3925
+ }
3926
+ }
3927
+ var walker = document.createTreeWalker(messagesEl, NodeFilter.SHOW_TEXT, null, false);
3928
+ var node;
3929
+ while (node = walker.nextNode()) {
3930
+ if (node.nodeValue.indexOf("[[MATE_READY:") !== -1) {
3931
+ node.nodeValue = node.nodeValue.replace(/\[\[MATE_READY:\s*.+?\]\]/g, "").trim();
3932
+ }
3933
+ }
3934
+ }, 100);
4114
3935
  }
4115
3936
  }
4116
3937
 
@@ -4167,18 +3988,22 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4167
3988
  break;
4168
3989
 
4169
3990
  case "restore_mate_dm":
4170
- if (msg.mateId) {
3991
+ if (msg.mateId && !returningFromMateDm) {
4171
3992
  // Server-driven mate DM restore on reconnect
4172
3993
  if (dmMode) {
4173
3994
  dmMode = false;
4174
- savedMainWs = null;
4175
- mateWs = null;
4176
3995
  document.body.classList.remove("mate-dm-active");
4177
3996
  }
4178
- pendingMateDmRestore = true;
4179
3997
  messagesEl.innerHTML = "";
4180
3998
  openDm(msg.mateId);
4181
3999
  }
4000
+ // Clear the flag and notify server that mate DM is closed
4001
+ if (returningFromMateDm) {
4002
+ returningFromMateDm = false;
4003
+ if (ws && ws.readyState === 1) {
4004
+ try { ws.send(JSON.stringify({ type: "set_mate_dm", mateId: null })); } catch(e) {}
4005
+ }
4006
+ }
4182
4007
  break;
4183
4008
 
4184
4009
  case "info":
@@ -4372,6 +4197,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4372
4197
  break;
4373
4198
 
4374
4199
  case "session_list":
4200
+ if (isMateDm) {
4201
+ renderMateSessionList(msg.sessions || []);
4202
+ }
4375
4203
  renderSessionList(msg.sessions || []);
4376
4204
  handlePaletteSessionSwitch();
4377
4205
  break;
@@ -4931,7 +4759,6 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4931
4759
 
4932
4760
  // --- DM ---
4933
4761
  case "dm_history":
4934
- pendingMateDmRestore = false; // DM data arrived, resume normal processing
4935
4762
  // Attach projectSlug to targetUser for mate DMs
4936
4763
  if (msg.projectSlug && msg.targetUser) {
4937
4764
  msg.targetUser.projectSlug = msg.projectSlug;
@@ -4941,12 +4768,11 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4941
4768
  if (pendingMateInterview && msg.targetUser && msg.targetUser.isMate && msg.projectSlug) {
4942
4769
  var interviewMate = pendingMateInterview;
4943
4770
  pendingMateInterview = null;
4944
- // Wait for mateWs to be swapped in as main ws, then send interview prompt
4771
+ // Wait for mate project WS to connect, then send interview prompt
4945
4772
  var checkMateReady = setInterval(function () {
4946
- if (ws && ws === mateWs && ws.readyState === 1) {
4773
+ if (ws && ws.readyState === 1 && mateProjectSlug) {
4947
4774
  clearInterval(checkMateReady);
4948
4775
  var interviewText = buildMateInterviewPrompt(interviewMate);
4949
- // Send through normal input flow (ws is now mateWs)
4950
4776
  ws.send(JSON.stringify({ type: "message", text: interviewText }));
4951
4777
  }
4952
4778
  }, 100);
@@ -5008,13 +4834,6 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5008
4834
  cachedMatesList = cachedMatesList.filter(function (m) { return m.id !== msg.mateId; });
5009
4835
  if (msg.availableBuiltins) cachedAvailableBuiltins = msg.availableBuiltins;
5010
4836
  renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
5011
- // Clean up background WS for deleted mate
5012
- var delSlug = "mate-" + msg.mateId;
5013
- if (backgroundMateWs[delSlug]) {
5014
- backgroundMateWs[delSlug].onclose = null;
5015
- backgroundMateWs[delSlug].close();
5016
- delete backgroundMateWs[delSlug];
5017
- }
5018
4837
  // If currently in DM with this mate, exit DM mode
5019
4838
  if (dmMode && dmTargetUser && dmTargetUser.id === msg.mateId) {
5020
4839
  exitDmMode();
@@ -5099,6 +4918,14 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5099
4918
  showDebateSticky("preparing", msg);
5100
4919
  break;
5101
4920
 
4921
+ case "debate_brief_ready":
4922
+ if (replayingHistory) {
4923
+ renderDebateBriefReady(msg);
4924
+ } else {
4925
+ handleDebateBriefReady(msg);
4926
+ }
4927
+ break;
4928
+
5102
4929
  case "debate_started":
5103
4930
  showDebateSticky("live", msg);
5104
4931
  if (replayingHistory) {
@@ -5449,8 +5276,6 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5449
5276
 
5450
5277
  function scheduleReconnect() {
5451
5278
  if (reconnectTimer) return;
5452
- // Don't reconnect main WS while in mate DM
5453
- if (savedMainWs) return;
5454
5279
  reconnectTimer = setTimeout(function () {
5455
5280
  reconnectTimer = null;
5456
5281
  // Check if auth is still valid before reconnecting
@@ -6291,9 +6116,18 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
6291
6116
  var debateBtn = document.getElementById("mate-debate-btn");
6292
6117
  if (debateBtn) {
6293
6118
  debateBtn.addEventListener("click", function () {
6294
- requireClayDebateSetup(function () {
6295
- openDebateModal();
6296
- });
6119
+ if (dmMode && dmTargetUser && dmTargetUser.isMate) {
6120
+ // Quick debate: moderator is the current DM mate, uses conversation context
6121
+ // Build messages with isMate flag for context extraction
6122
+ var contextMessages = dmMessageCache.map(function (m) {
6123
+ return { text: m.text, isMate: m.from !== myUserId };
6124
+ });
6125
+ openQuickDebateModal(contextMessages);
6126
+ } else {
6127
+ requireClayDebateSetup(function () {
6128
+ openDebateModal();
6129
+ });
6130
+ }
6297
6131
  });
6298
6132
  }
6299
6133