clay-server 2.26.0-beta.1 → 2.26.0-beta.11

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 (42) hide show
  1. package/bin/cli.js +5 -9
  2. package/lib/browser-mcp-server.js +496 -0
  3. package/lib/daemon.js +1 -1
  4. package/lib/os-users.js +23 -0
  5. package/lib/project-debate.js +243 -95
  6. package/lib/project-mate-interaction.js +766 -0
  7. package/lib/project-memory.js +677 -0
  8. package/lib/project.js +546 -1361
  9. package/lib/public/app.js +817 -175
  10. package/lib/public/css/debate.css +224 -2
  11. package/lib/public/css/icon-strip.css +10 -10
  12. package/lib/public/css/input.css +296 -83
  13. package/lib/public/css/mates.css +56 -57
  14. package/lib/public/css/mention.css +7 -4
  15. package/lib/public/css/menus.css +7 -0
  16. package/lib/public/css/messages.css +17 -0
  17. package/lib/public/css/mobile-nav.css +3 -1
  18. package/lib/public/css/overlays.css +181 -0
  19. package/lib/public/css/rewind.css +79 -0
  20. package/lib/public/css/server-settings.css +1 -0
  21. package/lib/public/css/sidebar.css +10 -0
  22. package/lib/public/css/title-bar.css +189 -3
  23. package/lib/public/index.html +53 -16
  24. package/lib/public/modules/context-sources.js +328 -0
  25. package/lib/public/modules/debate.js +184 -97
  26. package/lib/public/modules/input.js +18 -1
  27. package/lib/public/modules/mate-knowledge.js +11 -11
  28. package/lib/public/modules/mate-memory.js +5 -5
  29. package/lib/public/modules/mate-sidebar.js +13 -9
  30. package/lib/public/modules/mention.js +40 -2
  31. package/lib/public/modules/notifications.js +109 -1
  32. package/lib/public/modules/rewind.js +36 -0
  33. package/lib/public/modules/sidebar.js +107 -28
  34. package/lib/public/modules/terminal.js +8 -0
  35. package/lib/public/modules/theme.js +2 -1
  36. package/lib/public/modules/tools.js +69 -24
  37. package/lib/sdk-bridge.js +81 -7
  38. package/lib/sdk-worker.js +13 -1
  39. package/lib/server.js +42 -0
  40. package/lib/sessions.js +39 -7
  41. package/lib/terminal-manager.js +36 -6
  42. package/package.json +2 -2
@@ -156,14 +156,16 @@ function attachDebate(ctx) {
156
156
  setupStartedAt: d.setupStartedAt || null,
157
157
  round: d.round || 1,
158
158
  awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
159
+ awaitingUserFloor: !!d.awaitingUserFloor,
160
+ ownerId: d.ownerId || null,
159
161
  };
160
162
  ctx.sm.saveSessionFile(session);
161
163
  }
162
164
 
163
- function restoreDebateFromState(session) {
165
+ function restoreDebateFromState(session, restoreUserId) {
164
166
  var ds = session.debateState;
165
167
  if (!ds) return null;
166
- var userId = null; // Will be set when WS connects
168
+ var userId = restoreUserId || ds.ownerId || null;
167
169
  var mateCtx = matesModule.buildMateCtx(userId);
168
170
  var debate = {
169
171
  phase: ds.phase,
@@ -186,6 +188,8 @@ function attachDebate(ctx) {
186
188
  setupStartedAt: ds.setupStartedAt || null,
187
189
  briefPath: ds.briefPath || null,
188
190
  awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
191
+ awaitingUserFloor: !!ds.awaitingUserFloor,
192
+ ownerId: ds.ownerId || userId,
189
193
  };
190
194
 
191
195
  // Fallback: if awaitingConcludeConfirm was not persisted, detect from history
@@ -255,17 +259,11 @@ function attachDebate(ctx) {
255
259
  debate.context = brief.context || "";
256
260
  debate.specialRequests = brief.specialRequests || null;
257
261
 
258
- // Update panelists with roles from the brief
262
+ // Replace panelists with those selected in the brief
259
263
  if (brief.panelists && brief.panelists.length) {
260
- for (var i = 0; i < brief.panelists.length; i++) {
261
- var bp = brief.panelists[i];
262
- for (var j = 0; j < debate.panelists.length; j++) {
263
- if (debate.panelists[j].mateId === bp.mateId) {
264
- debate.panelists[j].role = bp.role || "";
265
- debate.panelists[j].brief = bp.brief || "";
266
- }
267
- }
268
- }
264
+ debate.panelists = brief.panelists.map(function (bp) {
265
+ return { mateId: bp.mateId, role: bp.role || "", brief: bp.brief || "" };
266
+ });
269
267
  }
270
268
 
271
269
  // Rebuild name map with updated roles
@@ -338,8 +336,8 @@ function attachDebate(ctx) {
338
336
  var phase = session.debateState.phase;
339
337
  if (phase !== "preparing" && phase !== "reviewing" && phase !== "live") return;
340
338
 
341
- // Restore _debate from persisted state
342
- var debate = restoreDebateFromState(session);
339
+ // Restore _debate from persisted state (pass userId for correct mateCtx)
340
+ var debate = restoreDebateFromState(session, userId);
343
341
  if (!debate) return;
344
342
 
345
343
  // Update mateCtx with the connected user's context
@@ -358,20 +356,17 @@ function attachDebate(ctx) {
358
356
  console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
359
357
  startDebateBriefWatcher(session, debate, briefPath);
360
358
 
361
- // Only show preparing indicator for quick start (standard setup shows skill in real-time)
362
- if (debate.quickStart) {
363
- ctx.sendTo(ws, {
364
- type: "debate_preparing",
365
- topic: debate.topic,
366
- moderatorId: debate.moderatorId,
367
- moderatorName: moderatorProfile.name,
368
- setupSessionId: debate.setupSessionId,
369
- panelists: debate.panelists.map(function (p) {
370
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
371
- return { mateId: p.mateId, name: prof.name };
372
- }),
373
- });
374
- }
359
+ ctx.sendTo(ws, {
360
+ type: "debate_preparing",
361
+ topic: debate.topic,
362
+ moderatorId: debate.moderatorId,
363
+ moderatorName: moderatorProfile.name,
364
+ setupSessionId: debate.setupSessionId,
365
+ panelists: debate.panelists.map(function (p) {
366
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
367
+ return { mateId: p.mateId, name: prof.name };
368
+ }),
369
+ });
375
370
  } else if (phase === "reviewing") {
376
371
  console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
377
372
  ctx.sendTo(ws, {
@@ -470,6 +465,7 @@ function attachDebate(ctx) {
470
465
  setupSessionId: null,
471
466
  debateId: debateId,
472
467
  briefPath: briefPath,
468
+ ownerId: mateCtx.userId || null,
473
469
  };
474
470
  debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
475
471
  session._debate = debate;
@@ -500,8 +496,27 @@ function attachDebate(ctx) {
500
496
  var session = ctx.getSessionForWs(ws);
501
497
  if (!session) return;
502
498
 
503
- if (!msg.moderatorId || !msg.topic || !msg.panelists || !msg.panelists.length) {
504
- ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic, panelists." });
499
+ if (!msg.moderatorId || !msg.topic) {
500
+ ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic." });
501
+ return;
502
+ }
503
+
504
+ // delegatePanelists: moderator picks panelists, populate all available mates
505
+ if (msg.delegatePanelists) {
506
+ var userId = ws._clayUser ? ws._clayUser.id : null;
507
+ var tmpCtx = matesModule.buildMateCtx(userId);
508
+ var matesData = matesModule.loadMates(tmpCtx);
509
+ var allMates = matesData.mates || [];
510
+ msg.panelists = [];
511
+ for (var mi = 0; mi < allMates.length; mi++) {
512
+ if (allMates[mi].id !== msg.moderatorId && allMates[mi].status !== "interviewing") {
513
+ msg.panelists.push({ mateId: allMates[mi].id, role: "", brief: "" });
514
+ }
515
+ }
516
+ }
517
+
518
+ if (!msg.panelists || !msg.panelists.length) {
519
+ ctx.sendTo(ws, { type: "debate_error", error: "No panelists available." });
505
520
  return;
506
521
  }
507
522
 
@@ -538,6 +553,7 @@ function attachDebate(ctx) {
538
553
  round: 1,
539
554
  history: [],
540
555
  setupSessionId: null,
556
+ ownerId: userId,
541
557
  };
542
558
  session._debate = debate;
543
559
 
@@ -565,7 +581,8 @@ function attachDebate(ctx) {
565
581
  var debateId = debate.debateId;
566
582
 
567
583
  // Create setup session (still needed for session grouping)
568
- var setupSession = ctx.sm.createSession();
584
+ var setupOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
585
+ var setupSession = ctx.sm.createSession(setupOpts);
569
586
  setupSession.title = "Debate Setup: " + (msg.topic || "Quick").slice(0, 40);
570
587
  setupSession.debateSetupMode = true;
571
588
  setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: (msg.topic || "Quick").slice(0, 40), source: "debate", startedAt: Date.now() };
@@ -597,7 +614,9 @@ function attachDebate(ctx) {
597
614
  "",
598
615
  "## Your Task",
599
616
  "Based on the conversation context, create a debate brief. You know the topic well because you were just discussing it.",
600
- "Assign each panelist a role and perspective that will create the most productive debate.",
617
+ msg.delegatePanelists
618
+ ? "IMPORTANT: Select only 2-4 panelists who are most relevant to this specific topic. Do NOT include all of them. Be selective. Only pick mates whose expertise or personality directly contributes to this debate."
619
+ : "The user already selected these panelists. Assign each one a role and perspective that will create the most productive debate.",
601
620
  "",
602
621
  "Output ONLY a valid JSON object (no markdown fences, no extra text):",
603
622
  "{",
@@ -688,7 +707,8 @@ function attachDebate(ctx) {
688
707
  var debateId = debate.debateId;
689
708
 
690
709
  // Create a new session for the setup skill (like Ralph crafting)
691
- var setupSession = ctx.sm.createSession();
710
+ var skillSetupOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
711
+ var setupSession = ctx.sm.createSession(skillSetupOpts);
692
712
  setupSession.title = "Debate Setup: " + msg.topic.slice(0, 40);
693
713
  setupSession.debateSetupMode = true;
694
714
  setupSession.loop = { active: true, iteration: 0, role: "crafting", loopId: debateId, name: msg.topic.slice(0, 40), source: "debate", startedAt: Date.now() };
@@ -723,13 +743,24 @@ function attachDebate(ctx) {
723
743
  // Watch for brief.json in the debate-specific directory
724
744
  startDebateBriefWatcher(session, debate, briefPath);
725
745
 
726
- // Standard setup: no preparing indicator needed because the user
727
- // sees the skill working in real-time in the setup session.
746
+ // Notify clients that setup is in progress
747
+ var preparingMsg = {
748
+ type: "debate_preparing",
749
+ topic: debate.topic || "(Setting up...)",
750
+ moderatorId: debate.moderatorId,
751
+ moderatorName: moderatorProfile.name,
752
+ setupSessionId: setupSession.localId,
753
+ panelists: debate.panelists.map(function (p) {
754
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
755
+ return { mateId: p.mateId, name: prof.name };
756
+ }),
757
+ };
758
+ ctx.sendTo(ws, preparingMsg);
759
+ ctx.sendToSession(session.localId, preparingMsg);
728
760
 
729
- // Start the setup skill session
730
- setupSession.history.push({ type: "user_message", text: craftingPrompt });
731
- ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt });
732
- ctx.sendToSession(setupSession.localId, { type: "user_message", text: craftingPrompt });
761
+ // Start the setup skill session (don't send user_message to client — it's an internal prompt)
762
+ setupSession.history.push({ type: "user_message", text: craftingPrompt, _internal: true });
763
+ ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt, _internal: true });
733
764
  setupSession.isProcessing = true;
734
765
  ctx.onProcessingChanged();
735
766
  setupSession.sentToolResults = {};
@@ -737,6 +768,13 @@ function attachDebate(ctx) {
737
768
  ctx.sdk.startQuery(setupSession, craftingPrompt, undefined, ctx.getLinuxUserForSession(setupSession));
738
769
  }
739
770
 
771
+ // --- Mate strip processing indicator ---
772
+ // Broadcast mention_processing so the correct mate's active dot lights up
773
+ // on the mate strip during debate turns (instead of always the moderator's).
774
+ function debateMateProcessing(mateId, active) {
775
+ ctx.send({ type: "mention_processing", mateId: mateId, active: active });
776
+ }
777
+
740
778
  // --- Live debate ---
741
779
 
742
780
  function startDebateLive(session) {
@@ -751,13 +789,10 @@ function attachDebate(ctx) {
751
789
  var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
752
790
 
753
791
  // Create a dedicated debate session, grouped with the setup session
754
- var debateSession = ctx.sm.createSession();
792
+ var liveOpts = debate.ownerId ? { ownerId: debate.ownerId } : null;
793
+ var debateSession = ctx.sm.createSession(liveOpts);
755
794
  debateSession.title = debate.topic.slice(0, 50);
756
795
  debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
757
- // Assign cliSessionId manually so saveSessionFile works (no SDK query for debate sessions)
758
- if (!debateSession.cliSessionId) {
759
- debateSession.cliSessionId = crypto.randomUUID();
760
- }
761
796
  ctx.sm.saveSessionFile(debateSession);
762
797
  ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
763
798
  debate.liveSessionId = debateSession.localId;
@@ -789,6 +824,7 @@ function attachDebate(ctx) {
789
824
  ctx.sendToSession(debateSession.localId, debateStartEntry);
790
825
 
791
826
  // Signal moderator's first turn
827
+ debateMateProcessing(debate.moderatorId, true);
792
828
  ctx.sendToSession(debateSession.localId, {
793
829
  type: "debate_turn",
794
830
  mateId: debate.moderatorId,
@@ -843,6 +879,7 @@ function attachDebate(ctx) {
843
879
  var debate = session._debate;
844
880
  if (!debate || debate.phase === "ended") return;
845
881
 
882
+ debateMateProcessing(debate.moderatorId, false);
846
883
  debate.turnInProgress = false;
847
884
 
848
885
  // Record in debate history
@@ -886,6 +923,12 @@ function attachDebate(ctx) {
886
923
  return;
887
924
  }
888
925
 
926
+ // Check if user raised hand
927
+ if (debate.handRaised) {
928
+ yieldFloorToUser(session);
929
+ return;
930
+ }
931
+
889
932
  // Trigger the first mentioned panelist
890
933
  triggerPanelist(session, mentionedIds[0], fullText);
891
934
  }
@@ -908,6 +951,7 @@ function attachDebate(ctx) {
908
951
  }
909
952
  if (!panelistInfo) {
910
953
  console.error("[debate] Panelist not found:", mateId);
954
+ debateMateProcessing(mateId, false);
911
955
  debate._currentTurnMateId = null;
912
956
  // Feed error back to moderator
913
957
  feedBackToModerator(session, mateId, "[This panelist is not part of the debate panel.]");
@@ -915,6 +959,7 @@ function attachDebate(ctx) {
915
959
  }
916
960
 
917
961
  // Notify clients of new turn
962
+ debateMateProcessing(mateId, true);
918
963
  ctx.sendToSession(session.localId, {
919
964
  type: "debate_turn",
920
965
  mateId: mateId,
@@ -943,6 +988,7 @@ function attachDebate(ctx) {
943
988
  },
944
989
  onError: function (errMsg) {
945
990
  console.error("[debate] Panelist error for " + mateId + ":", errMsg);
991
+ debateMateProcessing(mateId, false);
946
992
  debate.turnInProgress = false;
947
993
  // Feed error back to moderator so the debate can continue
948
994
  feedBackToModerator(session, mateId, "[" + profile.name + " encountered an error and could not respond. Please continue with other panelists or wrap up.]");
@@ -1003,6 +1049,7 @@ function attachDebate(ctx) {
1003
1049
  }
1004
1050
  }).catch(function (err) {
1005
1051
  console.error("[debate] Failed to create panelist session for " + mateId + ":", err.message || err);
1052
+ debateMateProcessing(mateId, false);
1006
1053
  debate.turnInProgress = false;
1007
1054
  feedBackToModerator(session, mateId, "[" + profile.name + " is unavailable. Please continue with other panelists or wrap up.]");
1008
1055
  });
@@ -1013,6 +1060,7 @@ function attachDebate(ctx) {
1013
1060
  var debate = session._debate;
1014
1061
  if (!debate || debate.phase === "ended") return;
1015
1062
 
1063
+ debateMateProcessing(mateId, false);
1016
1064
  debate.turnInProgress = false;
1017
1065
  debate._currentTurnMateId = null;
1018
1066
  debate._currentTurnText = "";
@@ -1041,12 +1089,18 @@ function attachDebate(ctx) {
1041
1089
  return;
1042
1090
  }
1043
1091
 
1044
- // Check for pending user comment
1092
+ // Check for pending user comment (legacy)
1045
1093
  if (debate.pendingComment) {
1046
1094
  injectUserComment(session);
1047
1095
  return;
1048
1096
  }
1049
1097
 
1098
+ // Check if user raised hand (no comment, just wants the floor)
1099
+ if (debate.handRaised) {
1100
+ yieldFloorToUser(session);
1101
+ return;
1102
+ }
1103
+
1050
1104
  // Feed panelist response back to moderator
1051
1105
  feedBackToModerator(session, mateId, fullText);
1052
1106
  }
@@ -1070,6 +1124,7 @@ function attachDebate(ctx) {
1070
1124
  var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1071
1125
 
1072
1126
  // Notify clients of moderator turn
1127
+ debateMateProcessing(debate.moderatorId, true);
1073
1128
  ctx.sendToSession(session.localId, {
1074
1129
  type: "debate_turn",
1075
1130
  mateId: debate.moderatorId,
@@ -1115,73 +1170,75 @@ function attachDebate(ctx) {
1115
1170
 
1116
1171
  // --- User interaction during debate ---
1117
1172
 
1118
- function handleDebateComment(ws, msg) {
1173
+ function handleDebateHandRaise(ws) {
1119
1174
  var session = ctx.getSessionForWs(ws);
1120
1175
  if (!session) return;
1121
1176
 
1122
1177
  var debate = session._debate;
1123
- if (!debate || debate.phase !== "live") {
1124
- ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1125
- return;
1126
- }
1127
-
1128
- // If awaiting conclude confirmation, re-send the confirm prompt instead
1178
+ if (!debate || debate.phase !== "live") return;
1179
+ if (debate.awaitingUserFloor || debate.handRaised) return;
1129
1180
  if (debate.awaitingConcludeConfirm) {
1130
1181
  ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1131
1182
  return;
1132
1183
  }
1133
1184
 
1185
+ debate.handRaised = true;
1186
+ ctx.sendToSession(session.localId, { type: "debate_hand_raised" });
1187
+
1188
+ // If no one is speaking, yield floor immediately
1189
+ if (!debate.turnInProgress) {
1190
+ yieldFloorToUser(session);
1191
+ }
1192
+ // Otherwise: current speaker finishes -> handRaised detected -> yieldFloorToUser
1193
+ }
1194
+
1195
+ function handleDebateComment(ws, msg) {
1196
+ // Legacy: kept for compatibility but now hand raise is separate
1197
+ var session = ctx.getSessionForWs(ws);
1198
+ if (!session) return;
1199
+
1200
+ var debate = session._debate;
1201
+ if (!debate || debate.phase !== "live") {
1202
+ ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1203
+ return;
1204
+ }
1205
+ if (debate.awaitingUserFloor) return;
1134
1206
  if (!msg.text) return;
1135
1207
 
1136
1208
  debate.pendingComment = { text: msg.text };
1209
+ debate.handRaised = true;
1137
1210
  ctx.sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
1138
1211
 
1139
- // If a panelist turn is in progress, abort it and go straight to moderator
1140
- if (debate.turnInProgress && debate._currentTurnMateId && debate._currentTurnMateId !== debate.moderatorId) {
1141
- var abortMateId = debate._currentTurnMateId;
1142
- console.log("[debate] User raised hand during panelist turn, aborting " + abortMateId);
1143
-
1144
- // Close the panelist's mention session to stop generation
1145
- if (debate.panelistSessions[abortMateId]) {
1146
- try { debate.panelistSessions[abortMateId].close(); } catch (e) {}
1147
- delete debate.panelistSessions[abortMateId];
1148
- }
1212
+ if (!debate.turnInProgress) {
1213
+ injectUserComment(session);
1214
+ }
1215
+ }
1149
1216
 
1150
- // Save partial text as interrupted turn
1151
- var partialText = debate._currentTurnText || "(interrupted by audience)";
1152
- var profile = ctx.getMateProfile(debate.mateCtx, abortMateId);
1153
- var panelistInfo = null;
1154
- for (var pi = 0; pi < debate.panelists.length; pi++) {
1155
- if (debate.panelists[pi].mateId === abortMateId) { panelistInfo = debate.panelists[pi]; break; }
1156
- }
1217
+ function yieldFloorToUser(session) {
1218
+ var debate = session._debate;
1219
+ if (!debate || !debate.moderatorSession || debate.phase === "ended") return;
1157
1220
 
1158
- ctx.sendToSession(session.localId, {
1159
- type: "debate_turn_done",
1160
- mateId: abortMateId,
1161
- mateName: profile.name,
1162
- role: panelistInfo ? panelistInfo.role : "",
1163
- text: partialText,
1164
- interrupted: true,
1165
- avatarStyle: profile.avatarStyle,
1166
- avatarSeed: profile.avatarSeed,
1167
- avatarColor: profile.avatarColor,
1168
- });
1221
+ debate.handRaised = false;
1222
+ debate.turnInProgress = true;
1223
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1169
1224
 
1170
- var turnEntry = { type: "debate_turn_done", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", round: debate.round, text: partialText, avatarStyle: profile.avatarStyle, avatarSeed: profile.avatarSeed, avatarColor: profile.avatarColor, interrupted: true };
1171
- session.history.push(turnEntry);
1172
- ctx.sm.appendToSessionFile(session, turnEntry);
1173
- debate.history.push({ speaker: "panelist", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: partialText });
1225
+ ctx.sendToSession(session.localId, {
1226
+ type: "debate_turn",
1227
+ mateId: debate.moderatorId,
1228
+ mateName: moderatorProfile.name,
1229
+ role: "moderator",
1230
+ round: debate.round,
1231
+ avatarColor: moderatorProfile.avatarColor,
1232
+ avatarStyle: moderatorProfile.avatarStyle,
1233
+ avatarSeed: moderatorProfile.avatarSeed,
1234
+ });
1174
1235
 
1175
- debate.turnInProgress = false;
1176
- debate._currentTurnMateId = null;
1177
- debate._currentTurnText = "";
1178
- }
1236
+ var feedText = "[The user raised their hand to speak.]\n" +
1237
+ "[Acknowledge this briefly and yield the floor to the user. Say something like " +
1238
+ "\"Go ahead\" or \"The floor is yours\". Do NOT call on any panelist (no @mentions). " +
1239
+ "The debate will pause for the user to speak.]";
1179
1240
 
1180
- // Inject to moderator immediately if no turn in progress (or just aborted)
1181
- if (!debate.turnInProgress) {
1182
- injectUserComment(session);
1183
- }
1184
- // If moderator is currently speaking, pendingComment will be picked up after moderator's onDone
1241
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1185
1242
  }
1186
1243
 
1187
1244
  function injectUserComment(session) {
@@ -1190,6 +1247,7 @@ function attachDebate(ctx) {
1190
1247
 
1191
1248
  var comment = debate.pendingComment;
1192
1249
  debate.pendingComment = null;
1250
+ debate.handRaised = false;
1193
1251
 
1194
1252
  // Record in debate history
1195
1253
  debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
@@ -1199,7 +1257,7 @@ function attachDebate(ctx) {
1199
1257
  ctx.sm.appendToSessionFile(session, commentEntry);
1200
1258
  ctx.sendToSession(session.localId, commentEntry);
1201
1259
 
1202
- // Feed to moderator
1260
+ // Feed to moderator: yield the floor to the user
1203
1261
  debate.turnInProgress = true;
1204
1262
  var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1205
1263
 
@@ -1216,9 +1274,90 @@ function attachDebate(ctx) {
1216
1274
 
1217
1275
  var feedText = "[The user raised their hand and said:]\n" +
1218
1276
  comment.text + "\n" +
1219
- "[Please acknowledge this and weave it into the discussion. Then continue the debate.]";
1277
+ "[Acknowledge the user's input. Briefly respond, then YIELD THE FLOOR to the user by saying something like " +
1278
+ "\"The floor is yours\" or \"Go ahead\". Do NOT call on any panelist (no @mentions). " +
1279
+ "The debate will pause for the user to speak.]";
1280
+
1281
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1282
+ }
1283
+
1284
+ function buildModeratorYieldCallbacks(session) {
1285
+ var debate = session._debate;
1286
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1287
+ return {
1288
+ onActivity: function (activity) {
1289
+ if (session._debate && session._debate.phase !== "ended") {
1290
+ ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1291
+ }
1292
+ },
1293
+ onDelta: function (delta) {
1294
+ if (session._debate && session._debate.phase !== "ended") {
1295
+ ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
1296
+ }
1297
+ },
1298
+ onDone: function (fullText) {
1299
+ if (!debate || debate.phase === "ended") return;
1300
+ debate.turnInProgress = false;
1301
+
1302
+ // Record moderator yield turn
1303
+ debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
1304
+ var turnEntry = { type: "debate_turn_done", mateId: debate.moderatorId, mateName: moderatorProfile.name, role: "moderator", round: debate.round, text: fullText, avatarStyle: moderatorProfile.avatarStyle, avatarSeed: moderatorProfile.avatarSeed, avatarColor: moderatorProfile.avatarColor };
1305
+ session.history.push(turnEntry);
1306
+ ctx.sm.appendToSessionFile(session, turnEntry);
1307
+ ctx.sendToSession(session.localId, turnEntry);
1308
+
1309
+ // Enter user floor mode: pause debate and show input
1310
+ debate.awaitingUserFloor = true;
1311
+ persistDebateState(session);
1312
+ ctx.sendToSession(session.localId, { type: "debate_user_floor", topic: debate.topic, round: debate.round });
1313
+ },
1314
+ onError: function (errMsg) {
1315
+ console.error("[debate] Moderator yield error:", errMsg);
1316
+ endDebate(session, "error");
1317
+ },
1318
+ };
1319
+ }
1320
+
1321
+ function handleDebateUserFloorResponse(ws, msg) {
1322
+ var session = ctx.getSessionForWs(ws);
1323
+ if (!session) return;
1324
+
1325
+ var debate = session._debate;
1326
+ if (!debate || !debate.awaitingUserFloor || debate.phase !== "live") return;
1327
+
1328
+ debate.awaitingUserFloor = false;
1329
+ var userText = (msg && msg.text) ? msg.text.trim() : "";
1330
+ if (!userText) return;
1331
+
1332
+ // Record user's floor contribution
1333
+ debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: userText });
1334
+ var floorEntry = { type: "debate_user_floor_done", text: userText };
1335
+ session.history.push(floorEntry);
1336
+ ctx.sm.appendToSessionFile(session, floorEntry);
1337
+ ctx.sendToSession(session.localId, floorEntry);
1338
+
1339
+ // Feed to moderator to resume debate
1340
+ debate.turnInProgress = true;
1341
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1342
+
1343
+ debateMateProcessing(debate.moderatorId, true);
1344
+ ctx.sendToSession(session.localId, {
1345
+ type: "debate_turn",
1346
+ mateId: debate.moderatorId,
1347
+ mateName: moderatorProfile.name,
1348
+ role: "moderator",
1349
+ round: debate.round,
1350
+ avatarColor: moderatorProfile.avatarColor,
1351
+ avatarStyle: moderatorProfile.avatarStyle,
1352
+ avatarSeed: moderatorProfile.avatarSeed,
1353
+ });
1354
+
1355
+ var feedText = "[The user took the floor and said:]\n" +
1356
+ userText + "\n" +
1357
+ "[Acknowledge the user's contribution and resume the debate. Call on the next panelist with @TheirName.]";
1220
1358
 
1221
1359
  debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
1360
+ persistDebateState(session);
1222
1361
  }
1223
1362
 
1224
1363
  function handleDebateConfirmBrief(ws) {
@@ -1391,6 +1530,7 @@ function attachDebate(ctx) {
1391
1530
  ctx.sendToSession(session.localId, resumedMsg);
1392
1531
 
1393
1532
  debate.turnInProgress = true;
1533
+ debateMateProcessing(debate.moderatorId, true);
1394
1534
  ctx.sendToSession(session.localId, {
1395
1535
  type: "debate_turn",
1396
1536
  mateId: debate.moderatorId,
@@ -1464,6 +1604,12 @@ function attachDebate(ctx) {
1464
1604
  var debate = session._debate;
1465
1605
  if (!debate || debate.phase === "ended") return;
1466
1606
 
1607
+ // Clear all mate strip processing dots
1608
+ debateMateProcessing(debate.moderatorId, false);
1609
+ for (var ei = 0; ei < debate.panelists.length; ei++) {
1610
+ debateMateProcessing(debate.panelists[ei].mateId, false);
1611
+ }
1612
+
1467
1613
  debate.phase = "ended";
1468
1614
  debate.turnInProgress = false;
1469
1615
  persistDebateState(session);
@@ -1592,10 +1738,12 @@ function attachDebate(ctx) {
1592
1738
 
1593
1739
  return {
1594
1740
  handleDebateStart: handleDebateStart,
1741
+ handleDebateHandRaise: handleDebateHandRaise,
1595
1742
  handleDebateComment: handleDebateComment,
1596
1743
  handleDebateStop: handleDebateStop,
1597
1744
  handleDebateConcludeResponse: handleDebateConcludeResponse,
1598
1745
  handleDebateConfirmBrief: handleDebateConfirmBrief,
1746
+ handleDebateUserFloorResponse: handleDebateUserFloorResponse,
1599
1747
  restoreDebateState: restoreDebateState,
1600
1748
  checkForDmDebateBrief: checkForDmDebateBrief,
1601
1749
  };