clay-server 2.18.0-beta.9 → 2.18.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.
package/lib/public/app.js CHANGED
@@ -7,7 +7,7 @@ import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionLis
7
7
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
8
8
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
9
9
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
10
- import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
10
+ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent } from './modules/input.js';
11
11
  import { initQrCode, triggerShare } from './modules/qrcode.js';
12
12
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
13
13
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
@@ -29,7 +29,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
29
29
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
30
30
  import { initLongPress } from './modules/longpress.js';
31
31
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
32
- import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, openDebateModal, closeDebateModal } from './modules/debate.js';
32
+ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal } from './modules/debate.js';
33
33
 
34
34
  // --- Base path for multi-project routing ---
35
35
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -568,6 +568,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
568
568
  }
569
569
 
570
570
  function enterDmMode(key, targetUser, messages) {
571
+ console.log("[DEBUG enterDmMode] key=" + key, "isMate=" + (targetUser && targetUser.isMate), "messages=" + (messages ? messages.length : 0));
571
572
  // Clean up previous DM/mate state before entering new one
572
573
  if (dmMode) {
573
574
  disconnectMateWs();
@@ -603,9 +604,9 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
603
604
  dmKey = key;
604
605
  dmTargetUser = targetUser;
605
606
 
606
- // Persist active DM for restore after hard refresh
607
- if (targetUser && targetUser.isMate) {
608
- try { localStorage.setItem("clay_active_mate_dm", targetUser.id); } catch(e) {}
607
+ // Notify server of active mate DM (server-side presence tracking)
608
+ if (targetUser && targetUser.isMate && ws && ws.readyState === 1) {
609
+ try { ws.send(JSON.stringify({ type: "set_mate_dm", mateId: targetUser.id })); } catch(e) {}
609
610
  }
610
611
 
611
612
  // Clear unread for this user
@@ -747,7 +748,10 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
747
748
  dmKey = null;
748
749
  dmTargetUser = null;
749
750
  setCurrentDmUser(null);
750
- try { localStorage.removeItem("clay_active_mate_dm"); } catch(e) {}
751
+ // Notify server that mate DM is closed
752
+ if (ws && ws.readyState === 1) {
753
+ try { ws.send(JSON.stringify({ type: "set_mate_dm", mateId: null })); } catch(e) {}
754
+ }
751
755
 
752
756
  var mainCol = document.getElementById("main-column");
753
757
  if (mainCol) mainCol.classList.remove("dm-mode");
@@ -979,6 +983,9 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
979
983
  }, 100);
980
984
  }
981
985
  // Feed everything into the main message handler (renders text, tools, etc.)
986
+ if (msg.type === "session_switched" || msg.type === "history_meta") {
987
+ 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);
988
+ }
982
989
  processMessage(msg);
983
990
  }
984
991
 
@@ -1076,13 +1083,16 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
1076
1083
  delete backgroundMateWs[slug];
1077
1084
  }
1078
1085
 
1086
+ // Block main WS messages immediately (before mate WS connects)
1087
+ // so project history doesn't leak into the DM chat
1088
+ savedMainWs = ws;
1089
+ savedActiveSessionId = activeSessionId;
1090
+
1079
1091
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
1080
1092
  mateWs = new WebSocket(protocol + "//" + location.host + "/p/" + slug + "/ws");
1081
1093
 
1082
1094
  mateWs.onopen = function () {
1083
1095
  // Swap main ws to mateWs so all UI (input, model selector, etc.) routes through mate project
1084
- savedMainWs = ws;
1085
- savedActiveSessionId = activeSessionId;
1086
1096
  ws = mateWs;
1087
1097
  connected = true;
1088
1098
  // Wrap send to blink IO on outgoing traffic
@@ -1099,6 +1109,10 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
1099
1109
  if (ws === mateWs) {
1100
1110
  ws = savedMainWs;
1101
1111
  savedMainWs = null;
1112
+ } else if (savedMainWs && !ws) {
1113
+ // Mate WS failed before swap completed; restore main WS
1114
+ ws = savedMainWs;
1115
+ savedMainWs = null;
1102
1116
  }
1103
1117
  mateWs = null;
1104
1118
  delete backgroundMateWs[slug];
@@ -1136,6 +1150,11 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
1136
1150
  savedActiveSessionId = null;
1137
1151
  }
1138
1152
  mateProjectSlug = null;
1153
+ // Hide debate sticky when leaving mate DM
1154
+ showDebateSticky("hide", null);
1155
+ // Hide debate info float
1156
+ var debateFloat = document.getElementById("debate-info-float");
1157
+ if (debateFloat) { debateFloat.classList.add("hidden"); debateFloat.innerHTML = ""; }
1139
1158
  // If main WS was disconnected while in mate DM, reconnect now
1140
1159
  if (ws && ws.readyState !== 1) {
1141
1160
  connect();
@@ -1602,6 +1621,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
1602
1621
  var loopAvailable = false;
1603
1622
  var loopIteration = 0;
1604
1623
  var loopMaxIterations = 0;
1624
+ var loopBannerName = null;
1605
1625
  var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
1606
1626
  var ralphCraftingSessionId = null;
1607
1627
  var ralphCraftingSource = null; // "ralph" or null (task)
@@ -2118,7 +2138,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
2118
2138
  } else if (status === "processing") {
2119
2139
  if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
2120
2140
  processing = true;
2121
- setSendBtnMode(inputEl.value.trim() ? "send" : "stop");
2141
+ setSendBtnMode(hasSendableContent() ? "send" : "stop");
2122
2142
  } else {
2123
2143
  connected = false;
2124
2144
  sendBtn.disabled = true;
@@ -2171,7 +2191,11 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
2171
2191
  '<span class="mate-thinking-dots"><span></span><span></span><span></span></span>' +
2172
2192
  '</div>' +
2173
2193
  '</div>';
2174
- addToMessages(matePreThinkingEl);
2194
+ if (activityEl && activityEl.parentNode) {
2195
+ activityEl.parentNode.insertBefore(matePreThinkingEl, activityEl);
2196
+ } else {
2197
+ addToMessages(matePreThinkingEl);
2198
+ }
2175
2199
  scrollToBottom();
2176
2200
  }
2177
2201
  function removeMatePreThinking() {
@@ -2896,6 +2920,13 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
2896
2920
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
2897
2921
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
2898
2922
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
2923
+ getMateById: function (id) {
2924
+ if (!id || !cachedMatesList) return null;
2925
+ for (var i = 0; i < cachedMatesList.length; i++) {
2926
+ if (cachedMatesList[i].id === id) return cachedMatesList[i];
2927
+ }
2928
+ return null;
2929
+ },
2899
2930
  });
2900
2931
 
2901
2932
  // isPlanFile, toolSummary, toolActivityText, shortPath -> modules/tools.js
@@ -3692,30 +3723,8 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
3692
3723
  ws.send(JSON.stringify({ type: "mate_list" }));
3693
3724
  } catch(e) {}
3694
3725
 
3695
- // Restore the last active session for this project (overrides server global)
3696
- try {
3697
- var savedSessionId = localStorage.getItem("clay_active_session_" + basePath);
3698
- if (savedSessionId) {
3699
- ws.send(JSON.stringify({ type: "switch_session", id: parseInt(savedSessionId, 10) }));
3700
- }
3701
- } catch(e) {}
3702
-
3703
- // Restore mate DM after hard refresh or server restart
3704
- try {
3705
- var savedMateDm = localStorage.getItem("clay_active_mate_dm");
3706
- if (savedMateDm) {
3707
- // If dmMode is stale (server restarted while in mate DM), clean up first
3708
- if (dmMode) {
3709
- dmMode = false;
3710
- savedMainWs = null;
3711
- mateWs = null;
3712
- document.body.classList.remove("mate-dm-active");
3713
- }
3714
- pendingMateDmRestore = true;
3715
- messagesEl.innerHTML = ""; // prevent regular history flash
3716
- openDm(savedMateDm);
3717
- }
3718
- } catch(e) {}
3726
+ // Session restore is now server-driven (user-presence.json).
3727
+ // Mate DM restore is also server-driven via "restore_mate_dm" message.
3719
3728
  };
3720
3729
 
3721
3730
  ws.onclose = function (e) {
@@ -3780,11 +3789,18 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
3780
3789
  }
3781
3790
 
3782
3791
  function processMessage(msg) {
3792
+ // DEBUG: trace session/history loading
3793
+ if (msg.type === "session_switched" || msg.type === "history_meta" || msg.type === "history_done" || msg.type === "mention_user" || msg.type === "mention_response") {
3794
+ 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));
3795
+ }
3796
+
3783
3797
  // Suppress regular project messages while restoring mate DM
3784
3798
  if (pendingMateDmRestore) {
3785
3799
  if (msg.type === "dm_history" || msg.type === "dm_list" || msg.type === "mate_list" || msg.type === "mate_created" || msg.type === "info") {
3786
3800
  // Let these through
3801
+ console.log("[DEBUG] pendingRestore: letting through", msg.type);
3787
3802
  } else {
3803
+ console.log("[DEBUG] pendingRestore: BLOCKING", msg.type);
3788
3804
  return; // skip regular session messages
3789
3805
  }
3790
3806
  }
@@ -3841,6 +3857,21 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
3841
3857
  }
3842
3858
  break;
3843
3859
 
3860
+ case "restore_mate_dm":
3861
+ if (msg.mateId) {
3862
+ // Server-driven mate DM restore on reconnect
3863
+ if (dmMode) {
3864
+ dmMode = false;
3865
+ savedMainWs = null;
3866
+ mateWs = null;
3867
+ document.body.classList.remove("mate-dm-active");
3868
+ }
3869
+ pendingMateDmRestore = true;
3870
+ messagesEl.innerHTML = "";
3871
+ openDm(msg.mateId);
3872
+ }
3873
+ break;
3874
+
3844
3875
  case "info":
3845
3876
  if (msg.text && !msg.project && !msg.cwd) {
3846
3877
  addSystemMessage(msg.text, false);
@@ -4081,8 +4112,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4081
4112
  }
4082
4113
  activeSessionId = msg.id;
4083
4114
  cliSessionId = msg.cliSessionId || null;
4084
- // Persist active session per project so reconnect restores it
4085
- try { localStorage.setItem("clay_active_session_" + basePath, String(msg.id)); } catch(e) {}
4115
+ // Session presence is now tracked server-side (user-presence.json)
4086
4116
  clearRemoteCursors();
4087
4117
  resetClientState();
4088
4118
  updateRalphBars();
@@ -4246,7 +4276,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4246
4276
  break;
4247
4277
 
4248
4278
  case "permission_request":
4249
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
4279
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
4250
4280
  startUrgentBlink();
4251
4281
  break;
4252
4282
 
@@ -4261,7 +4291,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4261
4291
  break;
4262
4292
 
4263
4293
  case "permission_request_pending":
4264
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
4294
+ renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason, msg.mateId);
4265
4295
  startUrgentBlink();
4266
4296
  break;
4267
4297
 
@@ -4702,12 +4732,13 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4702
4732
  break;
4703
4733
 
4704
4734
  case "mention_user":
4705
- // History replay: render mention user message from another client
4735
+ // Finalize current assistant block so mention renders in correct DOM position
4736
+ finalizeAssistantBlock();
4706
4737
  renderMentionUser(msg);
4707
4738
  break;
4708
4739
 
4709
4740
  case "mention_response":
4710
- // History replay: render mention response from another client
4741
+ finalizeAssistantBlock();
4711
4742
  renderMentionResponse(msg);
4712
4743
  break;
4713
4744
 
@@ -4718,7 +4749,11 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4718
4749
 
4719
4750
  case "debate_started":
4720
4751
  showDebateSticky("live", msg);
4721
- handleDebateStarted(msg);
4752
+ if (replayingHistory) {
4753
+ renderDebateStarted(msg);
4754
+ } else {
4755
+ handleDebateStarted(msg);
4756
+ }
4722
4757
  break;
4723
4758
 
4724
4759
  case "debate_turn":
@@ -4735,7 +4770,12 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4735
4770
  break;
4736
4771
 
4737
4772
  case "debate_turn_done":
4738
- handleDebateTurnDone(msg);
4773
+ if (msg.round) updateDebateRound(msg.round);
4774
+ if (replayingHistory) {
4775
+ renderDebateTurnDone(msg);
4776
+ } else {
4777
+ handleDebateTurnDone(msg);
4778
+ }
4739
4779
  break;
4740
4780
 
4741
4781
  case "debate_comment_queued":
@@ -4743,12 +4783,33 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4743
4783
  break;
4744
4784
 
4745
4785
  case "debate_comment_injected":
4746
- handleDebateCommentInjected(msg);
4786
+ if (replayingHistory) {
4787
+ renderDebateCommentInjected(msg);
4788
+ } else {
4789
+ handleDebateCommentInjected(msg);
4790
+ }
4791
+ break;
4792
+
4793
+ case "debate_conclude_confirm":
4794
+ showDebateConcludeConfirm(msg);
4795
+ break;
4796
+
4797
+ case "debate_user_resume":
4798
+ renderDebateUserResume(msg);
4799
+ break;
4800
+
4801
+ case "debate_resumed":
4802
+ handleDebateResumed(msg);
4803
+ showDebateSticky("live", msg);
4747
4804
  break;
4748
4805
 
4749
4806
  case "debate_ended":
4750
4807
  showDebateSticky("ended", msg);
4751
- handleDebateEnded(msg);
4808
+ if (replayingHistory) {
4809
+ renderDebateEnded(msg);
4810
+ } else {
4811
+ handleDebateEnded(msg);
4812
+ }
4752
4813
  break;
4753
4814
 
4754
4815
  case "debate_error":
@@ -4786,6 +4847,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4786
4847
  loopActive = msg.active;
4787
4848
  loopIteration = msg.iteration || 0;
4788
4849
  loopMaxIterations = msg.maxIterations || 20;
4850
+ loopBannerName = msg.name || null;
4789
4851
  updateLoopButton();
4790
4852
  if (loopActive) {
4791
4853
  showLoopBanner(true);
@@ -4793,7 +4855,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4793
4855
  updateLoopBanner(loopIteration, loopMaxIterations, "running");
4794
4856
  }
4795
4857
  inputEl.disabled = true;
4796
- inputEl.placeholder = "Ralph Loop is running...";
4858
+ inputEl.placeholder = (loopBannerName || "Loop") + " is running...";
4797
4859
  }
4798
4860
  break;
4799
4861
 
@@ -4802,11 +4864,12 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4802
4864
  ralphPhase = "executing";
4803
4865
  loopIteration = 0;
4804
4866
  loopMaxIterations = msg.maxIterations;
4867
+ loopBannerName = msg.name || null;
4805
4868
  showLoopBanner(true);
4806
4869
  updateLoopButton();
4807
- addSystemMessage("Ralph Loop started (max " + msg.maxIterations + " iterations)", false);
4870
+ addSystemMessage((loopBannerName || "Loop") + " started (max " + msg.maxIterations + " iterations)", false);
4808
4871
  inputEl.disabled = true;
4809
- inputEl.placeholder = "Ralph Loop is running...";
4872
+ inputEl.placeholder = (loopBannerName || "Loop") + " is running...";
4810
4873
  break;
4811
4874
 
4812
4875
  case "loop_iteration":
@@ -4814,16 +4877,16 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4814
4877
  loopMaxIterations = msg.maxIterations;
4815
4878
  updateLoopBanner(msg.iteration, msg.maxIterations, "running");
4816
4879
  updateLoopButton();
4817
- addSystemMessage("Ralph Loop iteration #" + msg.iteration + " started", false);
4880
+ addSystemMessage((loopBannerName || "Loop") + " iteration #" + msg.iteration + " started", false);
4818
4881
  inputEl.disabled = true;
4819
- inputEl.placeholder = "Ralph Loop is running...";
4882
+ inputEl.placeholder = (loopBannerName || "Loop") + " is running...";
4820
4883
  break;
4821
4884
 
4822
4885
  case "loop_judging":
4823
4886
  updateLoopBanner(loopIteration, loopMaxIterations, "judging");
4824
4887
  addSystemMessage("Judging iteration #" + msg.iteration + "...", false);
4825
4888
  inputEl.disabled = true;
4826
- inputEl.placeholder = "Ralph Loop is judging...";
4889
+ inputEl.placeholder = (loopBannerName || "Loop") + " is judging...";
4827
4890
  break;
4828
4891
 
4829
4892
  case "loop_verdict":
@@ -4837,21 +4900,23 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
4837
4900
  case "loop_finished":
4838
4901
  loopActive = false;
4839
4902
  ralphPhase = "done";
4903
+ loopBannerName = null;
4840
4904
  showLoopBanner(false);
4841
4905
  updateLoopButton();
4842
4906
  enableMainInput();
4907
+ var loopLabel = loopBannerName || "Loop";
4843
4908
  var finishMsg = msg.reason === "pass"
4844
- ? "Ralph Loop completed successfully after " + msg.iterations + " iteration(s)."
4909
+ ? loopLabel + " completed successfully after " + msg.iterations + " iteration(s)."
4845
4910
  : msg.reason === "max_iterations"
4846
- ? "Ralph Loop reached maximum iterations (" + msg.iterations + ")."
4911
+ ? loopLabel + " reached maximum iterations (" + msg.iterations + ")."
4847
4912
  : msg.reason === "stopped"
4848
- ? "Ralph Loop stopped."
4849
- : "Ralph Loop ended with error.";
4913
+ ? loopLabel + " stopped."
4914
+ : loopLabel + " ended with error.";
4850
4915
  addSystemMessage(finishMsg, false);
4851
4916
  break;
4852
4917
 
4853
4918
  case "loop_error":
4854
- addSystemMessage("Ralph Loop error: " + msg.text, true);
4919
+ addSystemMessage((loopBannerName || "Loop") + " error: " + msg.text, true);
4855
4920
  break;
4856
4921
 
4857
4922
  // --- Ralph Wizard / Crafting ---
@@ -5086,6 +5151,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
5086
5151
  scrollToBottom: scrollToBottom,
5087
5152
  addUserMessage: addUserMessage,
5088
5153
  addCopyHandler: addCopyHandler,
5154
+ addToMessages: addToMessages,
5089
5155
  });
5090
5156
 
5091
5157
  // --- Debate module ---
@@ -5515,7 +5581,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
5515
5581
  modal.querySelector(".loop-status-dialog-stop").addEventListener("click", function(e) {
5516
5582
  e.stopPropagation();
5517
5583
  closeModal();
5518
- showConfirm("Stop the running Ralph Loop?", function() {
5584
+ showConfirm("Stop the running " + (loopBannerName || "loop") + "?", function() {
5519
5585
  if (ws && ws.readyState === 1) {
5520
5586
  ws.send(JSON.stringify({ type: "loop_stop" }));
5521
5587
  }
@@ -5534,11 +5600,12 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
5534
5600
  return;
5535
5601
  }
5536
5602
 
5603
+ var bannerLabel = loopBannerName || "Loop";
5537
5604
  stickyEl.innerHTML =
5538
5605
  '<div class="ralph-sticky-inner">' +
5539
5606
  '<div class="ralph-sticky-header">' +
5540
5607
  '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
5541
- '<span class="ralph-sticky-label">Ralph Loop</span>' +
5608
+ '<span class="ralph-sticky-label">' + escapeHtml(bannerLabel) + '</span>' +
5542
5609
  '<span class="ralph-sticky-status" id="loop-status">Starting\u2026</span>' +
5543
5610
  '<button class="ralph-sticky-action ralph-sticky-stop" title="Stop loop">' + iconHtml("square") + '</button>' +
5544
5611
  '</div>' +
@@ -5559,10 +5626,16 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
5559
5626
  function updateLoopBanner(iteration, maxIterations, phase) {
5560
5627
  var statusEl = document.getElementById("loop-status");
5561
5628
  if (!statusEl) return;
5562
- var text = "#" + iteration + "/" + maxIterations;
5563
- if (phase === "judging") text += " judging\u2026";
5564
- else if (phase === "stopping") text = "Stopping\u2026";
5565
- else text += " running";
5629
+ var text;
5630
+ if (phase === "stopping") {
5631
+ text = "Stopping\u2026";
5632
+ } else if (maxIterations <= 1) {
5633
+ text = phase === "judging" ? "judging\u2026" : "running";
5634
+ } else {
5635
+ text = "#" + iteration + "/" + maxIterations;
5636
+ if (phase === "judging") text += " judging\u2026";
5637
+ else text += " running";
5638
+ }
5566
5639
  statusEl.textContent = text;
5567
5640
  }
5568
5641
 
@@ -6293,111 +6366,222 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
6293
6366
  var debateStickyState = null;
6294
6367
  var debateHandRaiseOpen = false;
6295
6368
 
6369
+ function showDebateConcludeConfirm(msg) {
6370
+ showDebateBottomBar("conclude", msg);
6371
+ scrollToBottom();
6372
+ }
6373
+
6374
+ // Legacy handler kept for compatibility
6296
6375
  function showDebateSticky(phase, msg) {
6297
6376
  if (phase === "ended" || phase === "hide") {
6298
6377
  debateStickyState = null;
6299
6378
  } else {
6300
6379
  debateStickyState = { phase: phase, msg: msg };
6301
6380
  }
6381
+
6382
+ // Hide the old sticky element (no longer used for content)
6302
6383
  var stickyEl = document.getElementById("debate-sticky");
6303
- if (!stickyEl) return;
6384
+ if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
6385
+
6386
+ // Remove existing header badges
6387
+ var oldBadges = document.querySelectorAll(".debate-header-badge");
6388
+ for (var i = 0; i < oldBadges.length; i++) oldBadges[i].remove();
6304
6389
 
6305
6390
  if (phase === "ended" || phase === "hide") {
6306
- stickyEl.classList.add("hidden");
6307
- stickyEl.innerHTML = "";
6308
6391
  debateHandRaiseOpen = false;
6392
+ removeDebateBottomBar();
6309
6393
  return;
6310
6394
  }
6311
6395
 
6312
- var topic = (msg && msg.topic) || "Debate";
6313
- var truncTopic = topic.length > 30 ? topic.slice(0, 30) + "\u2026" : topic;
6396
+ // Add badges next to header title
6397
+ var headerTitle = document.getElementById("header-title");
6398
+ if (!headerTitle) return;
6314
6399
 
6315
6400
  if (phase === "preparing") {
6316
- stickyEl.innerHTML =
6317
- '<div class="debate-sticky-inner">' +
6318
- '<div class="debate-sticky-header">' +
6319
- '<span class="debate-sticky-icon">' + iconHtml("mic") + '</span>' +
6320
- '<span class="debate-sticky-label">' + escapeHtml(truncTopic) + '</span>' +
6321
- '<span class="debate-sticky-status" id="debate-sticky-status">Setting up\u2026</span>' +
6322
- '<button class="debate-sticky-action debate-sticky-cancel" title="Cancel debate">' + iconHtml("x") + '</button>' +
6323
- '</div>' +
6401
+ var badge = document.createElement("span");
6402
+ badge.className = "debate-header-badge preparing";
6403
+ badge.textContent = "Setting up\u2026";
6404
+ headerTitle.after(badge);
6405
+ } else if (phase === "live") {
6406
+ var liveBadge = document.createElement("span");
6407
+ liveBadge.className = "debate-header-badge live";
6408
+ liveBadge.textContent = "Live";
6409
+ headerTitle.after(liveBadge);
6410
+
6411
+ var roundBadge = document.createElement("span");
6412
+ roundBadge.className = "debate-header-badge round";
6413
+ roundBadge.id = "debate-header-round";
6414
+ roundBadge.textContent = "R" + ((msg && msg.round) || 1);
6415
+ liveBadge.after(roundBadge);
6416
+
6417
+ debateHandRaiseOpen = false;
6418
+ showDebateBottomBar("live");
6419
+ }
6420
+ }
6421
+
6422
+ // --- Debate bottom bar (replaces input-area during debate) ---
6423
+ function showDebateBottomBar(mode, msg) {
6424
+ removeDebateBottomBar();
6425
+
6426
+ var inputArea = document.getElementById("input-area");
6427
+ if (!inputArea || !inputArea.parentNode) return;
6428
+
6429
+ var bar = document.createElement("div");
6430
+ bar.id = "debate-bottom-bar";
6431
+ bar.className = "debate-bottom-bar";
6432
+
6433
+ if (mode === "live") {
6434
+ bar.innerHTML =
6435
+ '<div class="debate-bottom-inner">' +
6436
+ '<button class="debate-bottom-hand" id="debate-bottom-hand">' + iconHtml("hand") + ' Raise hand</button>' +
6437
+ '<button class="debate-bottom-stop" id="debate-bottom-stop">' + iconHtml("square") + ' Stop</button>' +
6324
6438
  '</div>';
6325
- stickyEl.classList.remove("hidden");
6326
- stickyEl.className = "debate-sticky preparing";
6439
+
6440
+ inputArea.parentNode.insertBefore(bar, inputArea);
6441
+ inputArea.style.display = "none";
6327
6442
  refreshIcons();
6328
6443
 
6329
- stickyEl.querySelector(".debate-sticky-cancel").addEventListener("click", function (e) {
6330
- e.stopPropagation();
6444
+ document.getElementById("debate-bottom-hand").addEventListener("click", function () {
6445
+ toggleDebateHandRaise();
6446
+ });
6447
+ document.getElementById("debate-bottom-stop").addEventListener("click", function () {
6331
6448
  if (ws && ws.readyState === 1) {
6332
6449
  ws.send(JSON.stringify({ type: "debate_stop" }));
6333
6450
  }
6334
- stickyEl.classList.add("hidden");
6335
- stickyEl.innerHTML = "";
6336
6451
  });
6337
- } else if (phase === "live") {
6338
- stickyEl.innerHTML =
6339
- '<div class="debate-sticky-inner">' +
6340
- '<div class="debate-sticky-header">' +
6341
- '<span class="debate-sticky-icon">' + iconHtml("mic") + '</span>' +
6342
- '<span class="debate-sticky-label">' + escapeHtml(truncTopic) + '</span>' +
6343
- '<span class="debate-sticky-status" id="debate-sticky-status">Live</span>' +
6344
- '<span class="debate-sticky-round" id="debate-sticky-round">R1</span>' +
6345
- '<button class="debate-sticky-action debate-sticky-hand" title="Raise hand">' + iconHtml("hand") + '</button>' +
6346
- '<button class="debate-sticky-action debate-sticky-stop" title="Stop debate">' + iconHtml("square") + '</button>' +
6347
- '</div>' +
6348
- '<div class="debate-sticky-hand-input hidden" id="debate-sticky-hand-input">' +
6349
- '<textarea id="debate-sticky-comment" rows="1" placeholder="Your comment\u2026"></textarea>' +
6350
- '<button class="debate-sticky-send" id="debate-sticky-send">Send</button>' +
6351
- '<button class="debate-sticky-send-cancel" id="debate-sticky-send-cancel">Cancel</button>' +
6452
+ } else if (mode === "conclude") {
6453
+ bar.innerHTML =
6454
+ '<div class="debate-bottom-inner debate-bottom-conclude">' +
6455
+ '<div class="debate-bottom-conclude-label">' + iconHtml("check-circle") + ' The moderator is ready to conclude. End the debate?</div>' +
6456
+ '<textarea class="debate-bottom-conclude-input" id="debate-bottom-conclude-input" rows="3" placeholder="Or add a direction to continue..."></textarea>' +
6457
+ '<div class="debate-bottom-conclude-actions">' +
6458
+ '<button class="debate-bottom-continue" id="debate-bottom-continue">Continue</button>' +
6459
+ '<button class="debate-bottom-end" id="debate-bottom-end">End Debate</button>' +
6352
6460
  '</div>' +
6353
6461
  '</div>';
6354
- stickyEl.classList.remove("hidden");
6355
- stickyEl.className = "debate-sticky live";
6462
+
6463
+ inputArea.parentNode.insertBefore(bar, inputArea);
6464
+ inputArea.style.display = "none";
6356
6465
  refreshIcons();
6357
- debateHandRaiseOpen = false;
6358
6466
 
6359
- stickyEl.querySelector(".debate-sticky-hand").addEventListener("click", function (e) {
6360
- e.stopPropagation();
6361
- toggleDebateHandRaise();
6467
+ var textArea = document.getElementById("debate-bottom-conclude-input");
6468
+ document.getElementById("debate-bottom-end").addEventListener("click", function () {
6469
+ if (ws && ws.readyState === 1) {
6470
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "end" }));
6471
+ }
6472
+ removeDebateBottomBar();
6362
6473
  });
6363
-
6364
- stickyEl.querySelector(".debate-sticky-stop").addEventListener("click", function (e) {
6365
- e.stopPropagation();
6474
+ document.getElementById("debate-bottom-continue").addEventListener("click", function () {
6475
+ var text = textArea ? textArea.value.trim() : "";
6366
6476
  if (ws && ws.readyState === 1) {
6367
- ws.send(JSON.stringify({ type: "debate_stop" }));
6477
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
6368
6478
  }
6479
+ removeDebateBottomBar();
6480
+ showDebateBottomBar("live");
6369
6481
  });
6370
-
6371
- var sendBtn = document.getElementById("debate-sticky-send");
6372
- var cancelBtn = document.getElementById("debate-sticky-send-cancel");
6373
- var commentInput = document.getElementById("debate-sticky-comment");
6374
-
6375
- if (sendBtn) sendBtn.addEventListener("click", function () { sendDebateStickyComment(); });
6376
- if (cancelBtn) cancelBtn.addEventListener("click", function () { toggleDebateHandRaise(false); });
6377
- if (commentInput) {
6378
- commentInput.addEventListener("keydown", function (e) {
6379
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendDebateStickyComment(); }
6380
- if (e.key === "Escape") { toggleDebateHandRaise(false); }
6482
+ if (textArea) {
6483
+ textArea.focus();
6484
+ textArea.addEventListener("keydown", function (e) {
6485
+ if (e.key === "Enter" && !e.shiftKey) {
6486
+ e.preventDefault();
6487
+ document.getElementById("debate-bottom-continue").click();
6488
+ }
6489
+ });
6490
+ textArea.addEventListener("input", function () {
6491
+ debateAutoResize(textArea, 12);
6381
6492
  });
6382
6493
  }
6383
6494
  }
6384
6495
  }
6385
6496
 
6497
+ function debateAutoResize(textarea, maxRows) {
6498
+ textarea.style.height = "auto";
6499
+ var lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20;
6500
+ var maxHeight = lineHeight * maxRows;
6501
+ var newHeight = Math.min(textarea.scrollHeight, maxHeight);
6502
+ textarea.style.height = newHeight + "px";
6503
+ textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
6504
+ }
6505
+
6506
+ function removeDebateBottomBar() {
6507
+ var existing = document.getElementById("debate-bottom-bar");
6508
+ if (existing) existing.remove();
6509
+ // Also remove hand raise bar if open
6510
+ var handBar = document.getElementById("debate-hand-raise-bar");
6511
+ if (handBar) handBar.remove();
6512
+ debateHandRaiseOpen = false;
6513
+ // Restore input area
6514
+ var inputArea = document.getElementById("input-area");
6515
+ if (inputArea) inputArea.style.display = "";
6516
+ }
6517
+
6386
6518
  function toggleDebateHandRaise(forceState) {
6387
- var inputWrap = document.getElementById("debate-sticky-hand-input");
6388
- var commentInput = document.getElementById("debate-sticky-comment");
6389
- if (!inputWrap) return;
6390
6519
  var show = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
6391
6520
  debateHandRaiseOpen = show;
6392
- if (show) {
6393
- inputWrap.classList.remove("hidden");
6394
- if (commentInput) { commentInput.value = ""; commentInput.focus(); }
6395
- } else {
6396
- inputWrap.classList.add("hidden");
6521
+
6522
+ var existing = document.getElementById("debate-hand-raise-bar");
6523
+ if (!show) {
6524
+ if (existing) existing.remove();
6525
+ return;
6526
+ }
6527
+ if (existing) {
6528
+ var inp = existing.querySelector(".debate-hand-input");
6529
+ if (inp) { inp.value = ""; inp.focus(); }
6530
+ return;
6531
+ }
6532
+
6533
+ // Create hand raise bar above input area
6534
+ var bar = document.createElement("div");
6535
+ bar.id = "debate-hand-raise-bar";
6536
+ bar.className = "debate-hand-raise-bar";
6537
+ bar.innerHTML =
6538
+ '<div class="debate-hand-raise-inner">' +
6539
+ '<span class="debate-hand-raise-label">' + iconHtml("hand") + ' Your comment:</span>' +
6540
+ '<textarea class="debate-hand-input" rows="1" placeholder="Type your comment..."></textarea>' +
6541
+ '<button class="debate-hand-send">Send</button>' +
6542
+ '<button class="debate-hand-cancel">Cancel</button>' +
6543
+ '</div>';
6544
+
6545
+ var inputArea = document.getElementById("input-area");
6546
+ if (inputArea && inputArea.parentNode) {
6547
+ inputArea.parentNode.insertBefore(bar, inputArea);
6548
+ }
6549
+ refreshIcons();
6550
+
6551
+ var textarea = bar.querySelector(".debate-hand-input");
6552
+ var sendBtn = bar.querySelector(".debate-hand-send");
6553
+ var cancelBtn = bar.querySelector(".debate-hand-cancel");
6554
+
6555
+ if (textarea) {
6556
+ textarea.focus();
6557
+ textarea.addEventListener("input", function () {
6558
+ debateAutoResize(textarea, 12);
6559
+ });
6560
+ }
6561
+
6562
+ sendBtn.addEventListener("click", function () {
6563
+ var text = textarea ? textarea.value.trim() : "";
6564
+ if (!text) return;
6565
+ if (ws && ws.readyState === 1) {
6566
+ ws.send(JSON.stringify({ type: "debate_comment", text: text }));
6567
+ }
6568
+ toggleDebateHandRaise(false);
6569
+ });
6570
+
6571
+ cancelBtn.addEventListener("click", function () {
6572
+ toggleDebateHandRaise(false);
6573
+ });
6574
+
6575
+ if (textarea) {
6576
+ textarea.addEventListener("keydown", function (e) {
6577
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendBtn.click(); }
6578
+ if (e.key === "Escape") { toggleDebateHandRaise(false); }
6579
+ });
6397
6580
  }
6398
6581
  }
6399
6582
 
6400
6583
  function sendDebateStickyComment() {
6584
+ // Legacy fallback (kept for compatibility)
6401
6585
  var commentInput = document.getElementById("debate-sticky-comment");
6402
6586
  if (!commentInput) return;
6403
6587
  var text = commentInput.value.trim();
@@ -6409,7 +6593,7 @@ import { initDebate, handleDebateStarted, handleDebateTurn, handleDebateActivity
6409
6593
  }
6410
6594
 
6411
6595
  function updateDebateRound(round) {
6412
- var roundEl = document.getElementById("debate-sticky-round");
6596
+ var roundEl = document.getElementById("debate-header-round");
6413
6597
  if (roundEl) roundEl.textContent = "R" + round;
6414
6598
  }
6415
6599