clay-server 2.26.0-beta.9 → 2.26.1-beta.1

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
@@ -2,7 +2,7 @@ import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './modules/avatar.js';
2
2
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
3
3
  import { refreshIcons, iconHtml } from './modules/icons.js';
4
4
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
5
- import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles, openMobileSheet, setMobileSheetMateData } from './modules/sidebar.js';
5
+ import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles, openMobileSheet, setMobileSheetMateData, refreshMobileChatSheet } from './modules/sidebar.js';
6
6
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
7
7
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
8
8
  import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
@@ -32,7 +32,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
32
32
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
33
33
  import { initLongPress } from './modules/longpress.js';
34
34
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
35
- import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState } from './modules/debate.js';
35
+ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf, renderMcpDebateProposal } from './modules/debate.js';
36
36
 
37
37
  // --- Base path for multi-project routing ---
38
38
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -565,6 +565,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
565
565
  // --- DM Mode Functions ---
566
566
  function openDm(targetUserId) {
567
567
  if (!ws || ws.readyState !== 1) return;
568
+ // Persist DM state for refresh recovery
569
+ try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
568
570
  // Check mate skill updates before opening mate DM
569
571
  if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
570
572
  showMateOnboarding(function () {
@@ -816,6 +818,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
816
818
  dmMode = false;
817
819
  dmKey = null;
818
820
  dmTargetUser = null;
821
+ try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
819
822
  setCurrentDmUser(null);
820
823
 
821
824
  var mainCol = document.getElementById("main-column");
@@ -1798,9 +1801,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1798
1801
  };
1799
1802
  initSidebar(sidebarCtx);
1800
1803
  initIconStrip(sidebarCtx);
1801
- initMateSidebar(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1802
- initMateKnowledge(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1803
- initMateMemory(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; }, { onShow: function () { hideKnowledge(); hideNotes(); } });
1804
+ var wsGetter = function () { return ws; };
1805
+ initMateSidebar(wsGetter);
1806
+ initMateKnowledge(wsGetter);
1807
+ initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
1804
1808
  initMateWizard(
1805
1809
  function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1806
1810
  function (mate) { handleMateCreatedInApp(mate); }
@@ -3040,6 +3044,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3040
3044
  var div = document.createElement("div");
3041
3045
  div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
3042
3046
  div.dataset.turn = ++turnCounter;
3047
+ if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
3043
3048
  var bubble = document.createElement("div");
3044
3049
  bubble.className = "bubble";
3045
3050
  bubble.dir = "auto";
@@ -3130,8 +3135,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3130
3135
  header.appendChild(nameSpan);
3131
3136
  var timeSpan = document.createElement("span");
3132
3137
  timeSpan.className = "dm-bubble-time";
3133
- var nowH = new Date();
3134
- timeSpan.textContent = String(nowH.getHours()).padStart(2, "0") + ":" + String(nowH.getMinutes()).padStart(2, "0");
3138
+ timeSpan.textContent = getMsgTime();
3135
3139
  header.appendChild(timeSpan);
3136
3140
  contentWrap.appendChild(header);
3137
3141
  contentWrap.appendChild(bubble);
@@ -3140,10 +3144,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3140
3144
  // Action bar below bubble (icons visible on hover)
3141
3145
  var actions = document.createElement("div");
3142
3146
  actions.className = "msg-actions";
3143
- var now = new Date();
3144
- var timeStr = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
3145
3147
  actions.innerHTML =
3146
- '<span class="msg-action-time">' + timeStr + '</span>' +
3148
+ '<span class="msg-action-time">' + getMsgTime() + '</span>' +
3147
3149
  '<button class="msg-action-btn msg-action-copy" type="button" title="Copy">' + iconHtml("copy") + '</button>' +
3148
3150
  '<button class="msg-action-btn msg-action-fork" type="button" title="Fork">' + iconHtml("git-branch") + '</button>' +
3149
3151
  '<button class="msg-action-btn msg-action-rewind msg-user-rewind-btn" type="button" title="Rewind">' + iconHtml("rotate-ccw") + '</button>' +
@@ -3164,11 +3166,33 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3164
3166
  forceScrollToBottom();
3165
3167
  }
3166
3168
 
3169
+ // Track the timestamp of the current message being processed (from history _ts or now)
3170
+ var currentMsgTs = null;
3171
+
3172
+ function getMsgTime() {
3173
+ var d = currentMsgTs ? new Date(currentMsgTs) : new Date();
3174
+ return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
3175
+ }
3176
+
3177
+ function shouldGroupMessage(senderClass) {
3178
+ // Skip grouping during history replay if no timestamp data
3179
+ if (replayingHistory && !currentMsgTs) return false;
3180
+ var prev = messagesEl.lastElementChild;
3181
+ if (!prev || !prev.classList.contains(senderClass)) return false;
3182
+ var prevTime = prev.querySelector(".dm-bubble-time");
3183
+ if (!prevTime) return false;
3184
+ return prevTime.textContent === getMsgTime();
3185
+ }
3186
+
3167
3187
  function ensureAssistantBlock() {
3168
3188
  if (!currentMsgEl) {
3169
3189
  currentMsgEl = document.createElement("div");
3170
3190
  currentMsgEl.className = "msg-assistant";
3171
3191
  currentMsgEl.dataset.turn = turnCounter;
3192
+
3193
+ var grouped = shouldGroupMessage("msg-assistant");
3194
+ if (grouped) currentMsgEl.classList.add("grouped");
3195
+
3172
3196
  // Always render avatar + header structure (CSS controls visibility)
3173
3197
  var _isDm2 = document.body.classList.contains("mate-dm-active") && document.body.dataset.mateAvatarUrl;
3174
3198
  var avi = document.createElement("img");
@@ -3187,8 +3211,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3187
3211
  header.appendChild(nameSpan);
3188
3212
  var timeSpan = document.createElement("span");
3189
3213
  timeSpan.className = "dm-bubble-time";
3190
- var nowA = new Date();
3191
- timeSpan.textContent = String(nowA.getHours()).padStart(2, "0") + ":" + String(nowA.getMinutes()).padStart(2, "0");
3214
+ timeSpan.textContent = getMsgTime();
3192
3215
  header.appendChild(timeSpan);
3193
3216
  contentWrap.appendChild(header);
3194
3217
 
@@ -3692,7 +3715,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3692
3715
  // Auto-switch input to schedule mode: any message typed will be queued for after reset
3693
3716
  var delayUntilReset = msg.resetsAt - Date.now();
3694
3717
  if (delayUntilReset > 0) {
3695
- setScheduleDelayMs(delayUntilReset + 180000); // +3min buffer after reset
3718
+ setScheduleDelayMs(delayUntilReset + 60000); // +1min buffer after reset
3696
3719
  }
3697
3720
  rateLimitResetTimer = setTimeout(function () {
3698
3721
  rateLimitResetsAt = null;
@@ -4120,6 +4143,31 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4120
4143
 
4121
4144
  // Session restore is now server-driven (user-presence.json).
4122
4145
  // Mate DM restore is also server-driven via "restore_mate_dm" message.
4146
+ // Fallback: if server doesn't restore DM within 2s, try localStorage
4147
+ var savedDm = null;
4148
+ try { savedDm = localStorage.getItem("clay-active-dm"); } catch (e) {}
4149
+ if (savedDm && !dmMode && !mateProjectSlug) {
4150
+ var dmFallbackTimer = setTimeout(function () {
4151
+ if (!dmMode && savedDm) {
4152
+ console.log("[dm-restore] Server did not restore DM, using localStorage fallback:", savedDm);
4153
+ openDm(savedDm);
4154
+ }
4155
+ }, 2000);
4156
+ // Cancel fallback if server restores DM first
4157
+ var origHandler = ws.onmessage;
4158
+ var patchedOnce = false;
4159
+ var checkRestore = function (evt) {
4160
+ try {
4161
+ var d = JSON.parse(evt.data);
4162
+ if (d.type === "restore_mate_dm" && !patchedOnce) {
4163
+ patchedOnce = true;
4164
+ clearTimeout(dmFallbackTimer);
4165
+ }
4166
+ } catch (e) {}
4167
+ };
4168
+ ws.addEventListener("message", checkRestore);
4169
+ setTimeout(function () { ws.removeEventListener("message", checkRestore); }, 3000);
4170
+ }
4123
4171
  // Safety: clear returningFromMateDm after initial messages settle
4124
4172
  // (handles case where we connect to a non-main project that won't send restore_mate_dm)
4125
4173
  if (returningFromMateDm) {
@@ -4218,6 +4266,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4218
4266
  }
4219
4267
 
4220
4268
  function processMessage(msg) {
4269
+ // Preserve original timestamp from history replay
4270
+ currentMsgTs = msg._ts || null;
4221
4271
  var isMateDm = dmMode && dmTargetUser && dmTargetUser.isMate;
4222
4272
 
4223
4273
  // DEBUG: trace session/history loading
@@ -4232,6 +4282,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4232
4282
  if (isMateDm) {
4233
4283
  if (msg.type === "session_list") {
4234
4284
  renderMateSessionList(msg.sessions || []);
4285
+ refreshMobileChatSheet();
4235
4286
  // Override title bar with mate name and re-apply color
4236
4287
  var _mdn = (dmTargetUser.displayName || "New Mate");
4237
4288
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
@@ -4337,6 +4388,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4337
4388
  if (dhBar) dhBar.remove();
4338
4389
  var dbBadges = document.querySelectorAll(".debate-header-badge");
4339
4390
  for (var dbi = 0; dbi < dbBadges.length; dbi++) dbBadges[dbi].remove();
4391
+ // Clean up all debate mode banners if debate is not active on this session
4392
+ if (debateFloorMode) exitDebateFloorMode();
4393
+ if (debateConcludeMode) exitDebateConcludeMode();
4394
+ if (debateEndedMode) exitDebateEndedMode();
4395
+ var dbBanner = document.getElementById("debate-floor-banner");
4396
+ if (dbBanner) dbBanner.remove();
4340
4397
  }
4341
4398
  scrollToBottom();
4342
4399
  // Scroll to tool element if navigating from file edit history
@@ -4577,9 +4634,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4577
4634
  break;
4578
4635
 
4579
4636
  case "session_list":
4580
- if (isMateDm) {
4581
- renderMateSessionList(msg.sessions || []);
4582
- }
4637
+ renderMateSessionList(msg.sessions || []);
4583
4638
  renderSessionList(msg.sessions || []);
4584
4639
  handlePaletteSessionSwitch();
4585
4640
  break;
@@ -4670,6 +4725,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4670
4725
  break;
4671
4726
 
4672
4727
  case "user_message":
4728
+ if (msg._internal) break;
4673
4729
  resetThinkingGroup();
4674
4730
  if (msg.planContent) {
4675
4731
  setPlanContent(msg.planContent);
@@ -4691,7 +4747,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4691
4747
  header.className = "context-card-header";
4692
4748
  var icon = document.createElement("span");
4693
4749
  icon.className = "context-card-icon";
4694
- icon.textContent = "\uD83D\uDC41";
4750
+ icon.innerHTML = iconHtml("globe");
4695
4751
  header.appendChild(icon);
4696
4752
  var label = document.createElement("span");
4697
4753
  label.textContent = "Viewing tab";
@@ -4715,6 +4771,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4715
4771
  if (tabTitle || tabDomain) {
4716
4772
  var meta = document.createElement("div");
4717
4773
  meta.className = "context-card-meta";
4774
+ if (msg.tab.favIconUrl) {
4775
+ var fav = document.createElement("img");
4776
+ fav.className = "context-card-favicon";
4777
+ fav.src = msg.tab.favIconUrl;
4778
+ fav.width = 14;
4779
+ fav.height = 14;
4780
+ fav.onerror = function () { this.style.display = "none"; };
4781
+ meta.appendChild(fav);
4782
+ }
4718
4783
  var titleEl = document.createElement("span");
4719
4784
  titleEl.className = "context-card-title";
4720
4785
  titleEl.textContent = tabTitle;
@@ -4788,6 +4853,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4788
4853
  }
4789
4854
  renderPlanBanner("exit");
4790
4855
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4856
+ } else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
4857
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4791
4858
  } else if (getTodoTools()[msg.name]) {
4792
4859
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4793
4860
  } else {
@@ -4796,7 +4863,18 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4796
4863
  break;
4797
4864
 
4798
4865
  case "tool_executing":
4799
- if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4866
+ if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
4867
+ var _dpTool = getTools()[msg.id];
4868
+ if (_dpTool) {
4869
+ if (_dpTool.el) _dpTool.el.style.display = "none";
4870
+ _dpTool.done = true;
4871
+ _dpTool.hidden = true;
4872
+ removeToolFromGroup(msg.id);
4873
+ }
4874
+ finalizeAssistantBlock();
4875
+ renderMcpDebateProposal(msg.id, msg.input);
4876
+ startUrgentBlink();
4877
+ } else if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4800
4878
  var askTool = getTools()[msg.id];
4801
4879
  if (askTool) {
4802
4880
  if (askTool.el) askTool.el.style.display = "none";
@@ -5353,6 +5431,23 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5353
5431
  break;
5354
5432
 
5355
5433
  // --- @Mention ---
5434
+ case "mention_processing":
5435
+ // Broadcast: show/hide activity dot on mate avatar across all tabs
5436
+ if (msg.mateId) {
5437
+ var mateContainers = document.querySelectorAll('.icon-strip-mate[data-user-id="' + msg.mateId + '"]');
5438
+ for (var mi = 0; mi < mateContainers.length; mi++) {
5439
+ var dot = mateContainers[mi].querySelector(".icon-strip-status");
5440
+ if (msg.active) {
5441
+ if (dot) dot.classList.add("processing");
5442
+ mateContainers[mi].classList.add("mention-active");
5443
+ } else {
5444
+ if (dot) dot.classList.remove("processing");
5445
+ mateContainers[mi].classList.remove("mention-active");
5446
+ }
5447
+ }
5448
+ }
5449
+ break;
5450
+
5356
5451
  case "mention_start":
5357
5452
  handleMentionStart(msg);
5358
5453
  break;
@@ -5387,7 +5482,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5387
5482
 
5388
5483
  // --- Debate ---
5389
5484
  case "debate_preparing":
5390
- showDebateSticky("preparing", msg);
5485
+ if (!replayingHistory) showDebateSticky("preparing", msg);
5391
5486
  handleDebatePreparing(msg);
5392
5487
  break;
5393
5488
 
@@ -5400,7 +5495,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5400
5495
  break;
5401
5496
 
5402
5497
  case "debate_started":
5403
- showDebateSticky("live", msg);
5498
+ if (!replayingHistory) showDebateSticky("live", msg);
5404
5499
  if (replayingHistory) {
5405
5500
  renderDebateStarted(msg);
5406
5501
  } else {
@@ -5430,6 +5525,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5430
5525
  }
5431
5526
  break;
5432
5527
 
5528
+ case "debate_hand_raised":
5529
+ // Visual feedback: hand is raised, waiting for floor
5530
+ break;
5531
+
5433
5532
  case "debate_comment_queued":
5434
5533
  handleDebateCommentQueued(msg);
5435
5534
  break;
@@ -5443,7 +5542,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5443
5542
  break;
5444
5543
 
5445
5544
  case "debate_conclude_confirm":
5446
- showDebateConcludeConfirm(msg);
5545
+ if (!replayingHistory) showDebateConcludeConfirm(msg);
5546
+ break;
5547
+
5548
+ case "debate_user_floor":
5549
+ if (!replayingHistory) showDebateUserFloor(msg);
5550
+ break;
5551
+
5552
+ case "debate_user_floor_done":
5553
+ renderDebateUserFloorDone(msg);
5447
5554
  break;
5448
5555
 
5449
5556
  case "debate_user_resume":
@@ -5452,11 +5559,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5452
5559
 
5453
5560
  case "debate_resumed":
5454
5561
  handleDebateResumed(msg);
5455
- showDebateSticky("live", msg);
5562
+ if (!replayingHistory) showDebateSticky("live", msg);
5456
5563
  break;
5457
5564
 
5458
5565
  case "debate_ended":
5459
- showDebateSticky("ended", msg);
5566
+ if (!replayingHistory) showDebateSticky("ended", msg);
5460
5567
  if (replayingHistory) {
5461
5568
  renderDebateEnded(msg);
5462
5569
  } else {
@@ -5791,6 +5898,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5791
5898
  isDmMode: function () { return dmMode && !(dmTargetUser && dmTargetUser.isMate); },
5792
5899
  getDmKey: function () { return dmKey; },
5793
5900
  handleDmSend: function () { handleDmSend(); },
5901
+ isDebateEndedMode: function () { return debateEndedMode; },
5902
+ handleDebateEndedSend: function () { handleDebateEndedSend(); },
5903
+ isDebateConcludeMode: function () { return debateConcludeMode; },
5904
+ handleDebateConcludeSend: function () { handleDebateConcludeSend(); },
5905
+ isDebateFloorMode: function () { return debateFloorMode; },
5906
+ handleDebateFloorSend: function () { handleDebateFloorSend(); },
5794
5907
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
5795
5908
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
5796
5909
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
@@ -5817,13 +5930,16 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5817
5930
  // --- Debate module ---
5818
5931
  initDebate({
5819
5932
  get ws() { return ws; },
5933
+ sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
5820
5934
  messagesEl: messagesEl,
5935
+ addToMessages: function (el) { addToMessages(el); },
5821
5936
  scrollToBottom: scrollToBottom,
5822
5937
  addCopyHandler: addCopyHandler,
5823
5938
  matesList: function () { return cachedMatesList || []; },
5824
5939
  availableBuiltins: function () { return cachedAvailableBuiltins || []; },
5825
5940
  currentMateId: function () { return (dmTargetUser && dmTargetUser.isMate) ? dmTargetUser.id : null; },
5826
5941
  requireSkills: requireSkills,
5942
+ showDebateEndedMode: function (msg) { showDebateEndedMode(msg); },
5827
5943
  });
5828
5944
 
5829
5945
  // --- STT module (voice input via Web Speech API) ---
@@ -7110,7 +7226,220 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7110
7226
  var debateHandRaiseOpen = false;
7111
7227
 
7112
7228
  function showDebateConcludeConfirm(msg) {
7113
- showDebateBottomBar("conclude", msg);
7229
+ showDebateConcludeMode();
7230
+ }
7231
+
7232
+ var debateFloorMode = false;
7233
+ var debateConcludeMode = false;
7234
+
7235
+ function showDebateConcludeMode() {
7236
+ removeDebateBottomBar();
7237
+ debateConcludeMode = true;
7238
+ var inputArea = document.getElementById("input-area");
7239
+ if (inputArea) {
7240
+ inputArea.classList.add("debate-floor-mode");
7241
+ inputArea.style.display = "";
7242
+ }
7243
+ var existingBanner = document.getElementById("debate-floor-banner");
7244
+ if (existingBanner) existingBanner.remove();
7245
+ var banner = document.createElement("div");
7246
+ banner.id = "debate-floor-banner";
7247
+ banner.className = "debate-floor-banner";
7248
+ banner.innerHTML = iconHtml("check-circle") + " <span>The moderator is ready to conclude</span>" +
7249
+ '<button class="debate-floor-done-btn debate-floor-end-btn" id="debate-floor-end-btn">End Debate</button>';
7250
+ if (inputArea && inputArea.parentNode) {
7251
+ inputArea.parentNode.insertBefore(banner, inputArea);
7252
+ }
7253
+ refreshIcons();
7254
+ var endBtn = document.getElementById("debate-floor-end-btn");
7255
+ if (endBtn) {
7256
+ endBtn.addEventListener("click", function () {
7257
+ if (ws && ws.readyState === 1) {
7258
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "end" }));
7259
+ }
7260
+ exitDebateConcludeMode();
7261
+ });
7262
+ }
7263
+ var inputEl = document.getElementById("input");
7264
+ if (inputEl) {
7265
+ inputEl._origPlaceholder = inputEl._origPlaceholder || inputEl.placeholder;
7266
+ inputEl.placeholder = "Add a direction to continue the debate...";
7267
+ inputEl.focus();
7268
+ }
7269
+ scrollToBottom();
7270
+ }
7271
+
7272
+ function exitDebateConcludeMode() {
7273
+ debateConcludeMode = false;
7274
+ var inputArea = document.getElementById("input-area");
7275
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7276
+ var banner = document.getElementById("debate-floor-banner");
7277
+ if (banner) banner.remove();
7278
+ var inputEl = document.getElementById("input");
7279
+ if (inputEl && inputEl._origPlaceholder) {
7280
+ inputEl.placeholder = inputEl._origPlaceholder;
7281
+ delete inputEl._origPlaceholder;
7282
+ }
7283
+ }
7284
+
7285
+ function handleDebateConcludeSend() {
7286
+ var text = inputEl.value.trim();
7287
+ if (ws && ws.readyState === 1) {
7288
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7289
+ }
7290
+ inputEl.value = "";
7291
+ exitDebateConcludeMode();
7292
+ showDebateBottomBar("live");
7293
+ }
7294
+
7295
+ var debateEndedMode = false;
7296
+
7297
+ function showDebateEndedMode(msg) {
7298
+ removeDebateBottomBar();
7299
+ debateEndedMode = true;
7300
+ var inputArea = document.getElementById("input-area");
7301
+ if (inputArea) {
7302
+ inputArea.classList.add("debate-floor-mode");
7303
+ inputArea.style.display = "";
7304
+ }
7305
+ var existingBanner = document.getElementById("debate-floor-banner");
7306
+ if (existingBanner) existingBanner.remove();
7307
+ var banner = document.createElement("div");
7308
+ banner.id = "debate-floor-banner";
7309
+ banner.className = "debate-floor-banner";
7310
+ banner.innerHTML = iconHtml("check-circle") + " <span>Debate ended</span>" +
7311
+ '<button class="debate-floor-done-btn" id="debate-ended-resume-btn">Resume</button>' +
7312
+ '<button class="debate-floor-done-btn" id="debate-ended-pdf-btn">' + iconHtml("download") + ' PDF</button>';
7313
+ if (inputArea && inputArea.parentNode) {
7314
+ inputArea.parentNode.insertBefore(banner, inputArea);
7315
+ }
7316
+ refreshIcons();
7317
+ // Resume button
7318
+ var resumeBtn = document.getElementById("debate-ended-resume-btn");
7319
+ if (resumeBtn) {
7320
+ resumeBtn.addEventListener("click", function () {
7321
+ handleDebateEndedSend();
7322
+ });
7323
+ }
7324
+ // PDF button
7325
+ var pdfBtn = document.getElementById("debate-ended-pdf-btn");
7326
+ if (pdfBtn) {
7327
+ pdfBtn.addEventListener("click", function () {
7328
+ pdfBtn.disabled = true;
7329
+ exportDebateAsPdf().then(function () { pdfBtn.disabled = false; }).catch(function () { pdfBtn.disabled = false; });
7330
+ });
7331
+ }
7332
+ var inputEl2 = document.getElementById("input");
7333
+ if (inputEl2) {
7334
+ inputEl2._origPlaceholder = inputEl2._origPlaceholder || inputEl2.placeholder;
7335
+ inputEl2.placeholder = "Continue with a new direction...";
7336
+ }
7337
+ scrollToBottom();
7338
+ }
7339
+
7340
+ function exitDebateEndedMode() {
7341
+ debateEndedMode = false;
7342
+ var inputArea = document.getElementById("input-area");
7343
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7344
+ var banner = document.getElementById("debate-floor-banner");
7345
+ if (banner) banner.remove();
7346
+ var inputEl2 = document.getElementById("input");
7347
+ if (inputEl2 && inputEl2._origPlaceholder) {
7348
+ inputEl2.placeholder = inputEl2._origPlaceholder;
7349
+ delete inputEl2._origPlaceholder;
7350
+ }
7351
+ }
7352
+
7353
+ function handleDebateEndedSend() {
7354
+ var text = inputEl.value.trim();
7355
+ if (ws && ws.readyState === 1) {
7356
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7357
+ }
7358
+ inputEl.value = "";
7359
+ exitDebateEndedMode();
7360
+ }
7361
+
7362
+ function showDebateUserFloor(msg) {
7363
+ // Remove debate bottom bar and show input area in floor mode
7364
+ removeDebateBottomBar();
7365
+ debateFloorMode = true;
7366
+ var inputArea = document.getElementById("input-area");
7367
+ if (inputArea) {
7368
+ inputArea.classList.add("debate-floor-mode");
7369
+ inputArea.style.display = "";
7370
+ }
7371
+ // Add floor banner above input
7372
+ var existingBanner = document.getElementById("debate-floor-banner");
7373
+ if (existingBanner) existingBanner.remove();
7374
+ var banner = document.createElement("div");
7375
+ banner.id = "debate-floor-banner";
7376
+ banner.className = "debate-floor-banner";
7377
+ banner.innerHTML = iconHtml("mic") + " <span>You have the floor</span>" +
7378
+ '<button class="debate-floor-done-btn" id="debate-floor-done-btn">Pass</button>';
7379
+ if (inputArea && inputArea.parentNode) {
7380
+ inputArea.parentNode.insertBefore(banner, inputArea);
7381
+ }
7382
+ refreshIcons();
7383
+ // Done button: exit floor mode without sending
7384
+ var doneBtn = document.getElementById("debate-floor-done-btn");
7385
+ if (doneBtn) {
7386
+ doneBtn.addEventListener("click", function () {
7387
+ // Pass without speaking: resume debate
7388
+ if (ws && ws.readyState === 1) {
7389
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: "(The user passed without speaking)" }));
7390
+ }
7391
+ exitDebateFloorMode();
7392
+ showDebateBottomBar("live");
7393
+ });
7394
+ }
7395
+ // Update placeholder
7396
+ var inputEl = document.getElementById("input");
7397
+ if (inputEl) {
7398
+ inputEl._origPlaceholder = inputEl.placeholder;
7399
+ inputEl.placeholder = "Share your thoughts with the panel...";
7400
+ inputEl.focus();
7401
+ }
7402
+ scrollToBottom();
7403
+ }
7404
+
7405
+ function exitDebateFloorMode() {
7406
+ debateFloorMode = false;
7407
+ var inputArea = document.getElementById("input-area");
7408
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7409
+ var banner = document.getElementById("debate-floor-banner");
7410
+ if (banner) banner.remove();
7411
+ var inputEl = document.getElementById("input");
7412
+ if (inputEl && inputEl._origPlaceholder) {
7413
+ inputEl.placeholder = inputEl._origPlaceholder;
7414
+ delete inputEl._origPlaceholder;
7415
+ }
7416
+ }
7417
+
7418
+ function handleDebateFloorSend() {
7419
+ var text = inputEl.value.trim();
7420
+ if (!text) return;
7421
+ if (ws && ws.readyState === 1) {
7422
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: text }));
7423
+ }
7424
+ inputEl.value = "";
7425
+ exitDebateFloorMode();
7426
+ showDebateBottomBar("live");
7427
+ }
7428
+
7429
+ function renderDebateUserFloorDone(msg) {
7430
+ if (!messagesEl) return;
7431
+ var el = document.createElement("div");
7432
+ el.className = "debate-user-comment";
7433
+ var label = document.createElement("span");
7434
+ label.className = "debate-comment-label";
7435
+ label.innerHTML = iconHtml("mic") + " User:";
7436
+ var textEl = document.createElement("div");
7437
+ textEl.className = "debate-comment-text";
7438
+ textEl.textContent = msg.text || "";
7439
+ el.appendChild(label);
7440
+ el.appendChild(textEl);
7441
+ messagesEl.appendChild(el);
7442
+ refreshIcons();
7114
7443
  scrollToBottom();
7115
7444
  }
7116
7445
 
@@ -7136,7 +7465,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7136
7465
  return;
7137
7466
  }
7138
7467
 
7139
- // Add badges next to header title
7468
+ // Show bottom bar regardless of header availability
7469
+ if (phase === "live") {
7470
+ debateHandRaiseOpen = false;
7471
+ showDebateBottomBar("live");
7472
+ }
7473
+
7474
+ // Add badges next to header title (optional, may not exist on mobile)
7140
7475
  var headerTitle = document.getElementById("header-title");
7141
7476
  if (!headerTitle) return;
7142
7477
 
@@ -7156,9 +7491,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7156
7491
  roundBadge.id = "debate-header-round";
7157
7492
  roundBadge.textContent = "R" + ((msg && msg.round) || 1);
7158
7493
  liveBadge.after(roundBadge);
7159
-
7160
- debateHandRaiseOpen = false;
7161
- showDebateBottomBar("live");
7162
7494
  }
7163
7495
  }
7164
7496
 
@@ -7177,6 +7509,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7177
7509
  bar.innerHTML =
7178
7510
  '<div class="debate-bottom-inner">' +
7179
7511
  '<button class="debate-bottom-hand" id="debate-bottom-hand">' + iconHtml("hand") + ' Raise hand</button>' +
7512
+ '<span class="debate-bottom-waiting hidden" id="debate-bottom-waiting">' + iconHtml("loader") + ' You will get the floor after the current speaker</span>' +
7180
7513
  '<button class="debate-bottom-stop" id="debate-bottom-stop">' + iconHtml("square") + ' Stop</button>' +
7181
7514
  '</div>';
7182
7515
 
@@ -7184,6 +7517,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7184
7517
  inputArea.style.display = "none";
7185
7518
  refreshIcons();
7186
7519
 
7520
+ // Restore raised state if hand was already raised
7521
+ if (debateHandRaiseOpen) {
7522
+ var handBtn = document.getElementById("debate-bottom-hand");
7523
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7524
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7525
+ if (waitingEl) waitingEl.classList.remove("hidden");
7526
+ }
7527
+
7187
7528
  document.getElementById("debate-bottom-hand").addEventListener("click", function () {
7188
7529
  toggleDebateHandRaise();
7189
7530
  });
@@ -7192,48 +7533,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7192
7533
  ws.send(JSON.stringify({ type: "debate_stop" }));
7193
7534
  }
7194
7535
  });
7195
- } else if (mode === "conclude") {
7196
- bar.innerHTML =
7197
- '<div class="debate-bottom-inner debate-bottom-conclude">' +
7198
- '<div class="debate-bottom-conclude-label">' + iconHtml("check-circle") + ' The moderator is ready to conclude. End the debate?</div>' +
7199
- '<textarea class="debate-bottom-conclude-input" id="debate-bottom-conclude-input" rows="3" placeholder="Or add a direction to continue..."></textarea>' +
7200
- '<div class="debate-bottom-conclude-actions">' +
7201
- '<button class="debate-bottom-continue" id="debate-bottom-continue">Continue</button>' +
7202
- '<button class="debate-bottom-end" id="debate-bottom-end">End Debate</button>' +
7203
- '</div>' +
7204
- '</div>';
7205
-
7206
- inputArea.parentNode.insertBefore(bar, inputArea);
7207
- inputArea.style.display = "none";
7208
- refreshIcons();
7209
-
7210
- var textArea = document.getElementById("debate-bottom-conclude-input");
7211
- document.getElementById("debate-bottom-end").addEventListener("click", function () {
7212
- if (ws && ws.readyState === 1) {
7213
- ws.send(JSON.stringify({ type: "debate_conclude_response", action: "end" }));
7214
- }
7215
- removeDebateBottomBar();
7216
- });
7217
- document.getElementById("debate-bottom-continue").addEventListener("click", function () {
7218
- var text = textArea ? textArea.value.trim() : "";
7219
- if (ws && ws.readyState === 1) {
7220
- ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7221
- }
7222
- removeDebateBottomBar();
7223
- showDebateBottomBar("live");
7224
- });
7225
- if (textArea) {
7226
- textArea.focus();
7227
- textArea.addEventListener("keydown", function (e) {
7228
- if (e.key === "Enter" && !e.shiftKey) {
7229
- e.preventDefault();
7230
- document.getElementById("debate-bottom-continue").click();
7231
- }
7232
- });
7233
- textArea.addEventListener("input", function () {
7234
- debateAutoResize(textArea, 12);
7235
- });
7236
- }
7237
7536
  }
7238
7537
  }
7239
7538
 
@@ -7253,74 +7552,34 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7253
7552
  var handBar = document.getElementById("debate-hand-raise-bar");
7254
7553
  if (handBar) handBar.remove();
7255
7554
  debateHandRaiseOpen = false;
7555
+ // Clean up floor/conclude/ended modes
7556
+ if (debateFloorMode) exitDebateFloorMode();
7557
+ if (debateConcludeMode) exitDebateConcludeMode();
7558
+ if (debateEndedMode) exitDebateEndedMode();
7256
7559
  // Restore input area
7257
7560
  var inputArea = document.getElementById("input-area");
7258
7561
  if (inputArea) inputArea.style.display = "";
7259
7562
  }
7260
7563
 
7261
7564
  function toggleDebateHandRaise(forceState) {
7262
- var show = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
7263
- debateHandRaiseOpen = show;
7264
-
7265
- var existing = document.getElementById("debate-hand-raise-bar");
7266
- if (!show) {
7267
- if (existing) existing.remove();
7268
- return;
7269
- }
7270
- if (existing) {
7271
- var inp = existing.querySelector(".debate-hand-input");
7272
- if (inp) { inp.value = ""; inp.focus(); }
7273
- return;
7274
- }
7275
-
7276
- // Create hand raise bar above input area
7277
- var bar = document.createElement("div");
7278
- bar.id = "debate-hand-raise-bar";
7279
- bar.className = "debate-hand-raise-bar";
7280
- bar.innerHTML =
7281
- '<div class="debate-hand-raise-inner">' +
7282
- '<span class="debate-hand-raise-label">' + iconHtml("hand") + ' Your comment:</span>' +
7283
- '<textarea class="debate-hand-input" rows="1" placeholder="Type your comment..."></textarea>' +
7284
- '<button class="debate-hand-send">Send</button>' +
7285
- '<button class="debate-hand-cancel">Cancel</button>' +
7286
- '</div>';
7287
-
7288
- var inputArea = document.getElementById("input-area");
7289
- if (inputArea && inputArea.parentNode) {
7290
- inputArea.parentNode.insertBefore(bar, inputArea);
7291
- }
7292
- refreshIcons();
7293
-
7294
- var textarea = bar.querySelector(".debate-hand-input");
7295
- var sendBtn = bar.querySelector(".debate-hand-send");
7296
- var cancelBtn = bar.querySelector(".debate-hand-cancel");
7297
-
7298
- if (textarea) {
7299
- textarea.focus();
7300
- textarea.addEventListener("input", function () {
7301
- debateAutoResize(textarea, 12);
7302
- });
7565
+ var raise = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
7566
+ debateHandRaiseOpen = raise;
7567
+
7568
+ // Update UI: hide hand button, show waiting message
7569
+ var handBtn = document.getElementById("debate-bottom-hand");
7570
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7571
+ if (raise) {
7572
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7573
+ if (waitingEl) waitingEl.classList.remove("hidden");
7574
+ } else {
7575
+ if (handBtn) { handBtn.classList.remove("raised"); handBtn.classList.remove("hidden"); }
7576
+ if (waitingEl) waitingEl.classList.add("hidden");
7303
7577
  }
7304
7578
 
7305
- sendBtn.addEventListener("click", function () {
7306
- var text = textarea ? textarea.value.trim() : "";
7307
- if (!text) return;
7308
- if (ws && ws.readyState === 1) {
7309
- ws.send(JSON.stringify({ type: "debate_comment", text: text }));
7310
- }
7311
- toggleDebateHandRaise(false);
7312
- });
7313
-
7314
- cancelBtn.addEventListener("click", function () {
7315
- toggleDebateHandRaise(false);
7316
- });
7317
-
7318
- if (textarea) {
7319
- textarea.addEventListener("keydown", function (e) {
7320
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendBtn.click(); }
7321
- if (e.key === "Escape") { toggleDebateHandRaise(false); }
7322
- });
7579
+ if (raise && ws && ws.readyState === 1) {
7580
+ ws.send(JSON.stringify({ type: "debate_hand_raise" }));
7323
7581
  }
7582
+ // Floor mode will be activated when server sends debate_user_floor
7324
7583
  }
7325
7584
 
7326
7585
  function sendDebateStickyComment() {