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
package/lib/public/app.js CHANGED
@@ -1,20 +1,21 @@
1
1
  import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './modules/avatar.js';
2
2
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
3
- import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
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';
9
- import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
9
+ import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton, onRewindComplete, onRewindError } from './modules/rewind.js';
10
10
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
11
11
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent, setScheduleBtnDisabled, setScheduleDelayMs, clearScheduleDelay } from './modules/input.js';
12
12
  import { initQrCode, triggerShare } from './modules/qrcode.js';
13
13
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
14
- import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
14
+ import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
15
+ import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
15
16
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
16
- import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
17
- import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
17
+ import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme, getChatLayout } from './modules/theme.js';
18
+ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
18
19
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
19
20
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
20
21
  import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
@@ -31,7 +32,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
31
32
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
32
33
  import { initLongPress } from './modules/longpress.js';
33
34
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
34
- 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 } from './modules/debate.js';
35
36
 
36
37
  // --- Base path for multi-project routing ---
37
38
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -564,6 +565,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
564
565
  // --- DM Mode Functions ---
565
566
  function openDm(targetUserId) {
566
567
  if (!ws || ws.readyState !== 1) return;
568
+ // Persist DM state for refresh recovery
569
+ try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
567
570
  // Check mate skill updates before opening mate DM
568
571
  if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
569
572
  showMateOnboarding(function () {
@@ -815,6 +818,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
815
818
  dmMode = false;
816
819
  dmKey = null;
817
820
  dmTargetUser = null;
821
+ try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
818
822
  setCurrentDmUser(null);
819
823
 
820
824
  var mainCol = document.getElementById("main-column");
@@ -1797,9 +1801,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1797
1801
  };
1798
1802
  initSidebar(sidebarCtx);
1799
1803
  initIconStrip(sidebarCtx);
1800
- initMateSidebar(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1801
- initMateKnowledge(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1802
- 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(); } });
1803
1808
  initMateWizard(
1804
1809
  function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1805
1810
  function (mate) { handleMateCreatedInApp(mate); }
@@ -2082,12 +2087,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2082
2087
  activityEl = document.createElement("div");
2083
2088
  activityEl.className = "activity-inline";
2084
2089
  activityEl.innerHTML =
2085
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2086
- '<span class="activity-text"></span>';
2090
+ '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>';
2087
2091
  addToMessages(activityEl);
2088
- refreshIcons();
2089
2092
  }
2090
- activityEl.querySelector(".activity-text").textContent = text;
2091
2093
  scrollToBottom();
2092
2094
  } else {
2093
2095
  if (activityEl) {
@@ -2097,9 +2099,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2097
2099
  }
2098
2100
  }
2099
2101
 
2100
- // --- Mate pre-thinking (instant dots before server responds) ---
2102
+ // --- Pre-thinking (instant dots before server responds) ---
2101
2103
  var matePreThinkingEl = null;
2102
2104
  var matePreThinkingTimer = null;
2105
+ function showClaudePreThinking() {
2106
+ if (getChatLayout() !== "channel") return;
2107
+ var claudeAvatar = CLAUDE_CODE_AVATAR;
2108
+ doShowMatePreThinking("Claude Code", claudeAvatar);
2109
+ }
2103
2110
  function showMatePreThinking() {
2104
2111
  removeMatePreThinking();
2105
2112
  var mateName = dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate";
@@ -2113,10 +2120,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2113
2120
  '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="" style="display:block">' +
2114
2121
  '<div class="dm-bubble-content">' +
2115
2122
  '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
2116
- '<div class="activity-inline mate-pre-activity">' +
2117
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2118
- '<span class="activity-text">' + randomThinkingVerb() + '...</span>' +
2119
- '</div>' +
2123
+ '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>' +
2120
2124
  '</div>';
2121
2125
  if (activityEl && activityEl.parentNode) {
2122
2126
  activityEl.parentNode.insertBefore(matePreThinkingEl, activityEl);
@@ -2468,7 +2472,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2468
2472
  }
2469
2473
 
2470
2474
  function accumulateUsage(cost, usage) {
2471
- if (cost != null) sessionUsage.cost += cost;
2475
+ // cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
2476
+ // Assign directly instead of summing to avoid overcounting.
2477
+ if (cost != null) sessionUsage.cost = cost;
2472
2478
  if (usage) {
2473
2479
  sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
2474
2480
  sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
@@ -2596,6 +2602,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2596
2602
  var contextMiniLabel = $("context-mini-label");
2597
2603
  var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
2598
2604
  var headerContextEl = null;
2605
+ var richContextUsage = null;
2606
+ var ctxPopoverEl = null;
2607
+ var ctxPopoverVisible = false;
2599
2608
 
2600
2609
  // Known context window sizes per model (fallback when SDK omits feature flag)
2601
2610
  var KNOWN_CONTEXT_WINDOWS = {
@@ -2643,6 +2652,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2643
2652
  headerContextEl.className = "header-context";
2644
2653
  headerContextEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
2645
2654
  statusArea.insertBefore(headerContextEl, statusArea.firstChild);
2655
+ headerContextEl.addEventListener("mouseenter", function() {
2656
+ if (richContextUsage) {
2657
+ showCtxPopover();
2658
+ }
2659
+ });
2660
+ headerContextEl.addEventListener("mouseleave", function() {
2661
+ ctxHoverTimer = setTimeout(hideCtxPopover, 120);
2662
+ });
2646
2663
  }
2647
2664
  if (headerContextEl) {
2648
2665
  var hFill = headerContextEl.querySelector(".header-context-fill");
@@ -2650,7 +2667,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2650
2667
  hFill.style.width = pct.toFixed(1) + "%";
2651
2668
  hFill.className = "header-context-fill" + cls;
2652
2669
  hLabel.textContent = pct.toFixed(0) + "%";
2653
- headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
2670
+ // Use data-tip as fallback when rich data is not yet loaded
2671
+ if (richContextUsage) {
2672
+ headerContextEl.removeAttribute("data-tip");
2673
+ } else {
2674
+ headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
2675
+ }
2654
2676
  }
2655
2677
  }
2656
2678
  contextUsedEl.textContent = formatTokens(used);
@@ -2666,7 +2688,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2666
2688
  }
2667
2689
 
2668
2690
  function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
2669
- if (cost != null) contextData.cost += cost;
2691
+ // cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
2692
+ if (cost != null) contextData.cost = cost;
2670
2693
  // Use latest turn values (not cumulative) since each turn's input_tokens
2671
2694
  // already includes the full conversation context up to that point
2672
2695
  if (usage) {
@@ -2714,9 +2737,158 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2714
2737
 
2715
2738
  function resetContextData() {
2716
2739
  contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
2740
+ richContextUsage = null;
2741
+ hideCtxPopover();
2717
2742
  updateContextPanel();
2718
2743
  }
2719
2744
 
2745
+ // --- Rich context usage popover ---
2746
+
2747
+ var ctxHoverTimer = null;
2748
+
2749
+ function ensureCtxPopover() {
2750
+ if (ctxPopoverEl) return;
2751
+ ctxPopoverEl = document.createElement("div");
2752
+ ctxPopoverEl.className = "context-usage-popover hidden";
2753
+ // Keep popover open when hovering over it
2754
+ ctxPopoverEl.addEventListener("mouseenter", function() {
2755
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
2756
+ });
2757
+ ctxPopoverEl.addEventListener("mouseleave", function() {
2758
+ hideCtxPopover();
2759
+ });
2760
+ }
2761
+
2762
+ function showCtxPopover() {
2763
+ if (!headerContextEl || !richContextUsage) return;
2764
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
2765
+ ensureCtxPopover();
2766
+ headerContextEl.appendChild(ctxPopoverEl);
2767
+ renderCtxPopover();
2768
+ ctxPopoverEl.classList.remove("hidden");
2769
+ ctxPopoverVisible = true;
2770
+ }
2771
+
2772
+ function hideCtxPopover() {
2773
+ if (!ctxPopoverEl) return;
2774
+ ctxPopoverEl.classList.add("hidden");
2775
+ ctxPopoverVisible = false;
2776
+ }
2777
+
2778
+ // Categories to hide from the legend (noise, not actionable)
2779
+ var CTX_HIDDEN_CATS = { "Free space": 1, "Autocompact buffer": 1 };
2780
+
2781
+ function renderCtxPopover() {
2782
+ if (!ctxPopoverEl || !richContextUsage) return;
2783
+ var d = richContextUsage;
2784
+ var cats = d.categories || [];
2785
+ var total = d.totalTokens || 0;
2786
+ var max = d.maxTokens || 0;
2787
+ var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
2788
+
2789
+ var html = "";
2790
+
2791
+ // Header
2792
+ html += '<div class="ctx-pop-header">';
2793
+ html += '<span class="ctx-pop-model">' + escHtml(d.model || contextData.model || "-") + '</span>';
2794
+ html += '<span class="ctx-pop-pct">' + pct.toFixed(0) + '%';
2795
+ html += '<span class="ctx-pop-tokens">' + formatTokens(total) + ' / ' + formatTokens(max) + '</span>';
2796
+ html += '</span>';
2797
+ html += '</div>';
2798
+
2799
+ // Category emoji map
2800
+ var CTX_EMOJI = {
2801
+ "System prompt": "\ud83d\udcdc", "System tools": "\ud83d\udee0\ufe0f",
2802
+ "Memory files": "\ud83d\udcc1", "Skills": "\u26a1", "Messages": "\ud83d\udcac",
2803
+ "MCP tools": "\ud83d\udd0c", "Agents": "\ud83e\udd16", "Deferred tools": "\ud83d\udce6"
2804
+ };
2805
+
2806
+ // Stacked bar
2807
+ if (cats.length > 0 && max > 0) {
2808
+ html += '<div class="ctx-cat-bar">';
2809
+ for (var i = 0; i < cats.length; i++) {
2810
+ var cat = cats[i];
2811
+ if (cat.isDeferred || !cat.tokens || CTX_HIDDEN_CATS[cat.name]) continue;
2812
+ var w = Math.max(0.3, (cat.tokens / max) * 100);
2813
+ html += '<div style="width:' + w.toFixed(2) + '%;background:' + escHtml(cat.color) + '"></div>';
2814
+ }
2815
+ html += '</div>';
2816
+
2817
+ // Legend
2818
+ html += '<div class="ctx-cat-legend">';
2819
+ for (var j = 0; j < cats.length; j++) {
2820
+ var c = cats[j];
2821
+ if (c.isDeferred || !c.tokens || CTX_HIDDEN_CATS[c.name]) continue;
2822
+ var emoji = CTX_EMOJI[c.name] || "\ud83d\udcca";
2823
+ html += '<div class="ctx-cat-item">';
2824
+ html += '<span class="ctx-cat-name">' + em(emoji) + ' ' + escHtml(c.name) + '</span>';
2825
+ html += '<span class="ctx-cat-value">' + formatTokens(c.tokens) + '</span>';
2826
+ html += '</div>';
2827
+ }
2828
+ html += '</div>';
2829
+ }
2830
+
2831
+ // Message breakdown
2832
+ var mb = d.messageBreakdown;
2833
+ if (mb) {
2834
+ html += '<div class="ctx-pop-divider"></div>';
2835
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcac") + ' Messages</div>';
2836
+ if (mb.userMessageTokens) {
2837
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udc64") + ' User</span><span class="ctx-pop-row-value">' + formatTokens(mb.userMessageTokens) + '</span></div>';
2838
+ }
2839
+ if (mb.assistantMessageTokens) {
2840
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83e\udd16") + ' Assistant</span><span class="ctx-pop-row-value">' + formatTokens(mb.assistantMessageTokens) + '</span></div>';
2841
+ }
2842
+ if (mb.toolCallTokens) {
2843
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udee0\ufe0f") + ' Tool calls</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolCallTokens) + '</span></div>';
2844
+ }
2845
+ if (mb.toolResultTokens) {
2846
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udccb") + ' Tool results</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolResultTokens) + '</span></div>';
2847
+ }
2848
+ if (mb.attachmentTokens) {
2849
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcce") + ' Attachments</span><span class="ctx-pop-row-value">' + formatTokens(mb.attachmentTokens) + '</span></div>';
2850
+ }
2851
+ }
2852
+
2853
+ // Memory files
2854
+ var mf = d.memoryFiles;
2855
+ if (mf && mf.length > 0) {
2856
+ html += '<div class="ctx-pop-divider"></div>';
2857
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcc1") + ' Memory Files</div>';
2858
+ var baseCount = {};
2859
+ for (var mc = 0; mc < mf.length; mc++) {
2860
+ var bn = mf[mc].path.split("/").pop() || mf[mc].path;
2861
+ baseCount[bn] = (baseCount[bn] || 0) + 1;
2862
+ }
2863
+ for (var mi = 0; mi < mf.length; mi++) {
2864
+ var fpath = mf[mi].path;
2865
+ var fname = fpath.split("/").pop() || fpath;
2866
+ if (baseCount[fname] > 1) {
2867
+ var parts = fpath.split("/");
2868
+ fname = parts.length >= 2 ? parts[parts.length - 2] + "/" + fname : fpath;
2869
+ }
2870
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcc4") + ' ' + escHtml(fname) + '</span><span class="ctx-pop-row-value">' + formatTokens(mf[mi].tokens) + '</span></div>';
2871
+ }
2872
+ }
2873
+
2874
+ // Auto-compact note
2875
+ if (d.isAutoCompactEnabled && d.autoCompactThreshold) {
2876
+ html += '<div class="ctx-pop-note">' + em("\u267b\ufe0f") + ' Auto-compact at ' + formatTokens(d.autoCompactThreshold) + '</div>';
2877
+ }
2878
+
2879
+ ctxPopoverEl.innerHTML = html;
2880
+ }
2881
+
2882
+ function escHtml(s) {
2883
+ var div = document.createElement("div");
2884
+ div.textContent = s;
2885
+ return div.innerHTML;
2886
+ }
2887
+
2888
+ function em(emoji) {
2889
+ return '<span class="ctx-emoji">' + emoji + '</span>';
2890
+ }
2891
+
2720
2892
  function resetContext() {
2721
2893
  resetContextData();
2722
2894
  // Keep view state, just reset data
@@ -2851,6 +3023,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2851
3023
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
2852
3024
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
2853
3025
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
3026
+ getClaudeAvatar: function () { return CLAUDE_CODE_AVATAR; },
2854
3027
  getMateById: function (id) {
2855
3028
  if (!id || !cachedMatesList) return null;
2856
3029
  for (var i = 0; i < cachedMatesList.length; i++) {
@@ -2871,6 +3044,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2871
3044
  var div = document.createElement("div");
2872
3045
  div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
2873
3046
  div.dataset.turn = ++turnCounter;
3047
+ if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
2874
3048
  var bubble = document.createElement("div");
2875
3049
  bubble.className = "bubble";
2876
3050
  bubble.dir = "auto";
@@ -2961,8 +3135,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2961
3135
  header.appendChild(nameSpan);
2962
3136
  var timeSpan = document.createElement("span");
2963
3137
  timeSpan.className = "dm-bubble-time";
2964
- var nowH = new Date();
2965
- timeSpan.textContent = String(nowH.getHours()).padStart(2, "0") + ":" + String(nowH.getMinutes()).padStart(2, "0");
3138
+ timeSpan.textContent = getMsgTime();
2966
3139
  header.appendChild(timeSpan);
2967
3140
  contentWrap.appendChild(header);
2968
3141
  contentWrap.appendChild(bubble);
@@ -2971,10 +3144,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2971
3144
  // Action bar below bubble (icons visible on hover)
2972
3145
  var actions = document.createElement("div");
2973
3146
  actions.className = "msg-actions";
2974
- var now = new Date();
2975
- var timeStr = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
2976
3147
  actions.innerHTML =
2977
- '<span class="msg-action-time">' + timeStr + '</span>' +
3148
+ '<span class="msg-action-time">' + getMsgTime() + '</span>' +
2978
3149
  '<button class="msg-action-btn msg-action-copy" type="button" title="Copy">' + iconHtml("copy") + '</button>' +
2979
3150
  '<button class="msg-action-btn msg-action-fork" type="button" title="Fork">' + iconHtml("git-branch") + '</button>' +
2980
3151
  '<button class="msg-action-btn msg-action-rewind msg-user-rewind-btn" type="button" title="Rewind">' + iconHtml("rotate-ccw") + '</button>' +
@@ -2995,11 +3166,33 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2995
3166
  forceScrollToBottom();
2996
3167
  }
2997
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
+
2998
3187
  function ensureAssistantBlock() {
2999
3188
  if (!currentMsgEl) {
3000
3189
  currentMsgEl = document.createElement("div");
3001
3190
  currentMsgEl.className = "msg-assistant";
3002
3191
  currentMsgEl.dataset.turn = turnCounter;
3192
+
3193
+ var grouped = shouldGroupMessage("msg-assistant");
3194
+ if (grouped) currentMsgEl.classList.add("grouped");
3195
+
3003
3196
  // Always render avatar + header structure (CSS controls visibility)
3004
3197
  var _isDm2 = document.body.classList.contains("mate-dm-active") && document.body.dataset.mateAvatarUrl;
3005
3198
  var avi = document.createElement("img");
@@ -3018,8 +3211,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3018
3211
  header.appendChild(nameSpan);
3019
3212
  var timeSpan = document.createElement("span");
3020
3213
  timeSpan.className = "dm-bubble-time";
3021
- var nowA = new Date();
3022
- timeSpan.textContent = String(nowA.getHours()).padStart(2, "0") + ":" + String(nowA.getMinutes()).padStart(2, "0");
3214
+ timeSpan.textContent = getMsgTime();
3023
3215
  header.appendChild(timeSpan);
3024
3216
  contentWrap.appendChild(header);
3025
3217
 
@@ -3547,42 +3739,134 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3547
3739
 
3548
3740
  function addScheduledMessageBubble(text, resetsAt) {
3549
3741
  removeScheduledMessageBubble();
3742
+ var isChannel = document.body.classList.contains("wide-view");
3550
3743
  var wrap = document.createElement("div");
3551
3744
  wrap.className = "msg-user scheduled-msg-wrap";
3552
3745
  wrap.id = "scheduled-msg-bubble";
3553
3746
 
3554
- var bubble = document.createElement("div");
3555
- bubble.className = "bubble scheduled-msg-bubble";
3747
+ var countdownEl;
3748
+ var cancelBtn;
3556
3749
 
3557
- var textEl = document.createElement("span");
3558
- textEl.textContent = text;
3559
- bubble.appendChild(textEl);
3750
+ if (isChannel) {
3751
+ // Channel mode: avatar + header with scheduled badge + message
3752
+ var _me = cachedAllUsers.find(function (u) { return u.id === myUserId; });
3753
+ if (!_me) { try { _me = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {} }
3754
+ var _myName = document.body.dataset.myDisplayName || (_me && (_me.displayName || _me.username)) || "Me";
3560
3755
 
3561
- var metaEl = document.createElement("div");
3562
- metaEl.className = "scheduled-msg-meta";
3756
+ var avi = document.createElement("img");
3757
+ avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
3758
+ avi.src = document.body.dataset.myAvatarUrl || userAvatarUrl(_me || { id: myUserId }, 36);
3759
+ wrap.appendChild(avi);
3563
3760
 
3564
- var clockIcon = document.createElement("span");
3565
- clockIcon.className = "scheduled-msg-icon";
3566
- clockIcon.innerHTML = iconHtml("clock");
3567
- metaEl.appendChild(clockIcon);
3761
+ var content = document.createElement("div");
3762
+ content.className = "dm-bubble-content";
3568
3763
 
3569
- var countdownEl = document.createElement("span");
3570
- countdownEl.className = "scheduled-msg-countdown";
3571
- metaEl.appendChild(countdownEl);
3764
+ var header = document.createElement("div");
3765
+ header.className = "dm-bubble-header";
3572
3766
 
3573
- var cancelBtn = document.createElement("button");
3574
- cancelBtn.className = "scheduled-msg-cancel";
3575
- cancelBtn.title = "Cancel scheduled message";
3576
- cancelBtn.textContent = "\u00d7";
3577
- cancelBtn.addEventListener("click", function () {
3578
- if (ws && ws.readyState === 1) {
3579
- ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3580
- }
3581
- });
3582
- metaEl.appendChild(cancelBtn);
3767
+ var nameSpan = document.createElement("span");
3768
+ nameSpan.className = "dm-bubble-name";
3769
+ nameSpan.textContent = _myName;
3770
+ header.appendChild(nameSpan);
3771
+
3772
+ var badge = document.createElement("span");
3773
+ badge.className = "scheduled-msg-badge";
3774
+ badge.innerHTML = iconHtml("clock");
3775
+ countdownEl = document.createElement("span");
3776
+ countdownEl.className = "scheduled-msg-countdown";
3777
+ badge.appendChild(countdownEl);
3778
+ header.appendChild(badge);
3779
+
3780
+ var actions = document.createElement("span");
3781
+ actions.className = "scheduled-msg-actions";
3782
+
3783
+ var sendNowBtn = document.createElement("button");
3784
+ sendNowBtn.className = "scheduled-msg-send-now";
3785
+ sendNowBtn.textContent = "Send now";
3786
+ sendNowBtn.addEventListener("click", function () {
3787
+ if (ws && ws.readyState === 1) {
3788
+ ws.send(JSON.stringify({ type: "send_scheduled_now" }));
3789
+ }
3790
+ });
3791
+ actions.appendChild(sendNowBtn);
3792
+
3793
+ var sep = document.createElement("span");
3794
+ sep.className = "scheduled-msg-sep";
3795
+ sep.textContent = "\u00b7";
3796
+ actions.appendChild(sep);
3797
+
3798
+ cancelBtn = document.createElement("button");
3799
+ cancelBtn.className = "scheduled-msg-cancel";
3800
+ cancelBtn.textContent = "Cancel";
3801
+ cancelBtn.addEventListener("click", function () {
3802
+ if (ws && ws.readyState === 1) {
3803
+ ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3804
+ }
3805
+ });
3806
+ actions.appendChild(cancelBtn);
3807
+
3808
+ header.appendChild(actions);
3809
+
3810
+ content.appendChild(header);
3811
+
3812
+ var bubble = document.createElement("div");
3813
+ bubble.className = "bubble scheduled-msg-bubble";
3814
+ var textEl = document.createElement("span");
3815
+ textEl.textContent = text;
3816
+ bubble.appendChild(textEl);
3817
+ content.appendChild(bubble);
3818
+
3819
+ wrap.appendChild(content);
3820
+ } else {
3821
+ // Bubble mode: original layout
3822
+ var bubble = document.createElement("div");
3823
+ bubble.className = "bubble scheduled-msg-bubble";
3824
+
3825
+ var textEl = document.createElement("span");
3826
+ textEl.textContent = text;
3827
+ bubble.appendChild(textEl);
3828
+
3829
+ var metaEl = document.createElement("div");
3830
+ metaEl.className = "scheduled-msg-meta";
3831
+
3832
+ var clockIcon = document.createElement("span");
3833
+ clockIcon.className = "scheduled-msg-icon";
3834
+ clockIcon.innerHTML = iconHtml("clock");
3835
+ metaEl.appendChild(clockIcon);
3836
+
3837
+ countdownEl = document.createElement("span");
3838
+ countdownEl.className = "scheduled-msg-countdown";
3839
+ metaEl.appendChild(countdownEl);
3840
+
3841
+ var sendNowBtn2 = document.createElement("button");
3842
+ sendNowBtn2.className = "scheduled-msg-send-now";
3843
+ sendNowBtn2.textContent = "Send now";
3844
+ sendNowBtn2.addEventListener("click", function () {
3845
+ if (ws && ws.readyState === 1) {
3846
+ ws.send(JSON.stringify({ type: "send_scheduled_now" }));
3847
+ }
3848
+ });
3849
+ metaEl.appendChild(sendNowBtn2);
3850
+
3851
+ var sep2 = document.createElement("span");
3852
+ sep2.className = "scheduled-msg-sep";
3853
+ sep2.textContent = "\u00b7";
3854
+ metaEl.appendChild(sep2);
3855
+
3856
+ cancelBtn = document.createElement("button");
3857
+ cancelBtn.className = "scheduled-msg-cancel";
3858
+ cancelBtn.textContent = "Cancel";
3859
+ cancelBtn.addEventListener("click", function () {
3860
+ if (ws && ws.readyState === 1) {
3861
+ ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3862
+ }
3863
+ });
3864
+ metaEl.appendChild(cancelBtn);
3865
+
3866
+ wrap.appendChild(bubble);
3867
+ wrap.appendChild(metaEl);
3868
+ }
3583
3869
 
3584
- wrap.appendChild(bubble);
3585
- wrap.appendChild(metaEl);
3586
3870
  addToMessages(wrap);
3587
3871
  scheduledMsgEl = wrap;
3588
3872
  scrollToBottom();
@@ -3660,23 +3944,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3660
3944
  suggestionChipsEl.innerHTML = "";
3661
3945
  var chip = document.createElement("button");
3662
3946
  chip.className = "suggestion-chip";
3663
- chip.innerHTML =
3664
- '<span class="suggestion-chip-send">' + iconHtml("sparkles") +
3665
- '<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span></span>' +
3666
- '<span class="suggestion-chip-edit">' + iconHtml("pencil") + '</span>';
3947
+ chip.innerHTML = iconHtml("sparkles") +
3948
+ '<span class="suggestion-chip-text">' + escapeHtml(suggestion) + '</span>';
3667
3949
  chip.addEventListener("click", function () {
3668
3950
  inputEl.value = suggestion;
3669
3951
  hideSuggestionChips();
3670
3952
  sendMessage();
3671
3953
  });
3672
- chip.querySelector(".suggestion-chip-edit").addEventListener("click", function (e) {
3673
- e.stopPropagation();
3674
- inputEl.value = suggestion;
3675
- inputEl.focus();
3676
- inputEl.select();
3677
- autoResize();
3678
- hideSuggestionChips();
3679
- });
3680
3954
  suggestionChipsEl.appendChild(chip);
3681
3955
  suggestionChipsEl.classList.remove("hidden");
3682
3956
  refreshIcons();
@@ -3718,6 +3992,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3718
3992
  setStatus("connected");
3719
3993
  if (!loopActive) enableMainInput();
3720
3994
  resetUsage();
3995
+ resetTurnMetaCost();
3721
3996
  resetContext();
3722
3997
  // Clear header indicators
3723
3998
  clearRateLimitIndicator();
@@ -3868,6 +4143,31 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3868
4143
 
3869
4144
  // Session restore is now server-driven (user-presence.json).
3870
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
+ }
3871
4171
  // Safety: clear returningFromMateDm after initial messages settle
3872
4172
  // (handles case where we connect to a non-main project that won't send restore_mate_dm)
3873
4173
  if (returningFromMateDm) {
@@ -3966,6 +4266,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3966
4266
  }
3967
4267
 
3968
4268
  function processMessage(msg) {
4269
+ // Preserve original timestamp from history replay
4270
+ currentMsgTs = msg._ts || null;
3969
4271
  var isMateDm = dmMode && dmTargetUser && dmTargetUser.isMate;
3970
4272
 
3971
4273
  // DEBUG: trace session/history loading
@@ -3980,6 +4282,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3980
4282
  if (isMateDm) {
3981
4283
  if (msg.type === "session_list") {
3982
4284
  renderMateSessionList(msg.sessions || []);
4285
+ refreshMobileChatSheet();
3983
4286
  // Override title bar with mate name and re-apply color
3984
4287
  var _mdn = (dmTargetUser.displayName || "New Mate");
3985
4288
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
@@ -4057,6 +4360,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4057
4360
 
4058
4361
  case "history_done":
4059
4362
  replayingHistory = false;
4363
+ // Restore cached rich context usage BEFORE updateContextPanel runs
4364
+ if (msg.contextUsage) {
4365
+ richContextUsage = msg.contextUsage;
4366
+ }
4060
4367
  // Restore accurate context data from the last result in full history
4061
4368
  if (msg.lastUsage || msg.lastModelUsage) {
4062
4369
  accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
@@ -4321,9 +4628,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4321
4628
  break;
4322
4629
 
4323
4630
  case "session_list":
4324
- if (isMateDm) {
4325
- renderMateSessionList(msg.sessions || []);
4326
- }
4631
+ renderMateSessionList(msg.sessions || []);
4327
4632
  renderSessionList(msg.sessions || []);
4328
4633
  handlePaletteSessionSwitch();
4329
4634
  break;
@@ -4414,6 +4719,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4414
4719
  break;
4415
4720
 
4416
4721
  case "user_message":
4722
+ if (msg._internal) break;
4417
4723
  resetThinkingGroup();
4418
4724
  if (msg.planContent) {
4419
4725
  setPlanContent(msg.planContent);
@@ -4424,20 +4730,73 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4424
4730
  }
4425
4731
  break;
4426
4732
 
4733
+ case "context_preview":
4734
+ // Show a Context Card with tab screenshot between user message and assistant response
4735
+ if (msg.tab) {
4736
+ var card = document.createElement("div");
4737
+ card.className = "context-card";
4738
+
4739
+ // Header
4740
+ var header = document.createElement("div");
4741
+ header.className = "context-card-header";
4742
+ var icon = document.createElement("span");
4743
+ icon.className = "context-card-icon";
4744
+ icon.textContent = "\uD83D\uDC41";
4745
+ header.appendChild(icon);
4746
+ var label = document.createElement("span");
4747
+ label.textContent = "Viewing tab";
4748
+ header.appendChild(label);
4749
+ card.appendChild(header);
4750
+
4751
+ // Screenshot
4752
+ if (msg.tab.screenshotUrl) {
4753
+ var img = document.createElement("img");
4754
+ img.className = "context-card-screenshot";
4755
+ img.src = msg.tab.screenshotUrl;
4756
+ img.loading = "lazy";
4757
+ img.addEventListener("click", function () { showImageModal(this.src); });
4758
+ card.appendChild(img);
4759
+ }
4760
+
4761
+ // Meta: title + domain
4762
+ var tabTitle = msg.tab.title || "";
4763
+ var tabDomain = "";
4764
+ try { tabDomain = new URL(msg.tab.url).hostname; } catch (e) {}
4765
+ if (tabTitle || tabDomain) {
4766
+ var meta = document.createElement("div");
4767
+ meta.className = "context-card-meta";
4768
+ var titleEl = document.createElement("span");
4769
+ titleEl.className = "context-card-title";
4770
+ titleEl.textContent = tabTitle;
4771
+ meta.appendChild(titleEl);
4772
+ if (tabDomain) {
4773
+ var domainEl = document.createElement("span");
4774
+ domainEl.className = "context-card-domain";
4775
+ domainEl.textContent = tabDomain;
4776
+ meta.appendChild(domainEl);
4777
+ }
4778
+ card.appendChild(meta);
4779
+ }
4780
+
4781
+ messagesEl.appendChild(card);
4782
+ scrollToBottom();
4783
+ }
4784
+ break;
4785
+
4427
4786
  case "status":
4428
4787
  if (msg.status === "processing") {
4429
4788
  setStatus("processing");
4430
- if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4431
- setActivity(randomThinkingVerb() + "...");
4789
+ if (!(dmMode && dmTargetUser && dmTargetUser.isMate) && !matePreThinkingEl) {
4790
+ setActivity("thinking");
4432
4791
  }
4433
4792
  }
4434
4793
  break;
4435
4794
 
4436
4795
  case "compacting":
4437
4796
  if (msg.active) {
4438
- setActivity("Compacting conversation...");
4797
+ setActivity("compacting");
4439
4798
  } else if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4440
- setActivity(randomThinkingVerb() + "...");
4799
+ setActivity("thinking");
4441
4800
  }
4442
4801
  break;
4443
4802
 
@@ -4453,7 +4812,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4453
4812
  case "thinking_stop":
4454
4813
  stopThinking(msg.duration);
4455
4814
  if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4456
- setActivity(randomThinkingVerb() + "...");
4815
+ setActivity("thinking");
4457
4816
  }
4458
4817
  break;
4459
4818
 
@@ -4619,6 +4978,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4619
4978
  accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
4620
4979
  break;
4621
4980
 
4981
+ case "context_usage":
4982
+ if (msg.data && !replayingHistory) {
4983
+ richContextUsage = msg.data;
4984
+ if (headerContextEl) headerContextEl.removeAttribute("data-tip");
4985
+ if (ctxPopoverVisible) renderCtxPopover();
4986
+ }
4987
+ break;
4988
+
4622
4989
  case "done":
4623
4990
  setActivity(null);
4624
4991
  stopThinking();
@@ -4711,6 +5078,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4711
5078
  break;
4712
5079
 
4713
5080
  case "rewind_complete":
5081
+ onRewindComplete();
4714
5082
  setRewindMode(false);
4715
5083
  var rewindText = "Rewound to earlier point. Files have been restored.";
4716
5084
  if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
@@ -4719,6 +5087,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4719
5087
  break;
4720
5088
 
4721
5089
  case "rewind_error":
5090
+ onRewindError();
4722
5091
  clearPendingRewindUuid();
4723
5092
  addSystemMessage(msg.text || "Rewind failed.", true);
4724
5093
  break;
@@ -4791,6 +5160,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4791
5160
 
4792
5161
  case "term_list":
4793
5162
  handleTermList(msg);
5163
+ updateTerminalList(msg.terminals);
5164
+ break;
5165
+
5166
+ case "context_sources_state":
5167
+ handleContextSourcesState(msg);
5168
+ break;
5169
+
5170
+ case "extension_command":
5171
+ sendExtensionCommand(msg.command, msg.args, msg.requestId);
4794
5172
  break;
4795
5173
 
4796
5174
  case "term_created":
@@ -4809,6 +5187,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4809
5187
  handleTermOutput(msg);
4810
5188
  break;
4811
5189
 
5190
+ case "term_resized":
5191
+ handleTermResized(msg);
5192
+ break;
5193
+
4812
5194
  case "term_exited":
4813
5195
  handleTermExited(msg);
4814
5196
  break;
@@ -4976,6 +5358,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4976
5358
  // Update mate sidebar if currently viewing this mate
4977
5359
  if (dmMode && dmTargetUser && dmTargetUser.isMate && dmTargetUser.id === msg.mate.id) {
4978
5360
  updateMateSidebarProfile(msg.mate);
5361
+ // Sync dmTargetUser so subsequent renders use fresh data
5362
+ var mp2 = msg.mate.profile || {};
5363
+ dmTargetUser.displayName = mp2.displayName || msg.mate.name || dmTargetUser.displayName;
5364
+ dmTargetUser.avatarStyle = mp2.avatarStyle || dmTargetUser.avatarStyle;
5365
+ dmTargetUser.avatarSeed = mp2.avatarSeed || dmTargetUser.avatarSeed;
5366
+ dmTargetUser.avatarColor = mp2.avatarColor || dmTargetUser.avatarColor;
5367
+ dmTargetUser.avatarCustom = mp2.avatarCustom || "";
5368
+ dmTargetUser.profile = mp2;
5369
+ // Refresh body dataset so new chat bubbles use the updated avatar
5370
+ document.body.dataset.mateAvatarUrl = mateAvatarUrl(dmTargetUser, 36);
5371
+ document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
5372
+ // Update existing chat bubble avatars
5373
+ var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
5374
+ for (var mbi = 0; mbi < mateAvis.length; mbi++) {
5375
+ mateAvis[mbi].src = document.body.dataset.mateAvatarUrl;
5376
+ }
4979
5377
  }
4980
5378
  // Update DM header if currently chatting with this mate
4981
5379
  if (dmMode && dmTargetUser && dmTargetUser.id === msg.mate.id) {
@@ -5005,6 +5403,23 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5005
5403
  break;
5006
5404
 
5007
5405
  // --- @Mention ---
5406
+ case "mention_processing":
5407
+ // Broadcast: show/hide activity dot on mate avatar across all tabs
5408
+ if (msg.mateId) {
5409
+ var mateContainers = document.querySelectorAll('.icon-strip-mate[data-user-id="' + msg.mateId + '"]');
5410
+ for (var mi = 0; mi < mateContainers.length; mi++) {
5411
+ var dot = mateContainers[mi].querySelector(".icon-strip-status");
5412
+ if (msg.active) {
5413
+ if (dot) dot.classList.add("processing");
5414
+ mateContainers[mi].classList.add("mention-active");
5415
+ } else {
5416
+ if (dot) dot.classList.remove("processing");
5417
+ mateContainers[mi].classList.remove("mention-active");
5418
+ }
5419
+ }
5420
+ }
5421
+ break;
5422
+
5008
5423
  case "mention_start":
5009
5424
  handleMentionStart(msg);
5010
5425
  break;
@@ -5082,6 +5497,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5082
5497
  }
5083
5498
  break;
5084
5499
 
5500
+ case "debate_hand_raised":
5501
+ // Visual feedback: hand is raised, waiting for floor
5502
+ break;
5503
+
5085
5504
  case "debate_comment_queued":
5086
5505
  handleDebateCommentQueued(msg);
5087
5506
  break;
@@ -5098,6 +5517,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5098
5517
  showDebateConcludeConfirm(msg);
5099
5518
  break;
5100
5519
 
5520
+ case "debate_user_floor":
5521
+ showDebateUserFloor(msg);
5522
+ break;
5523
+
5524
+ case "debate_user_floor_done":
5525
+ renderDebateUserFloorDone(msg);
5526
+ break;
5527
+
5101
5528
  case "debate_user_resume":
5102
5529
  renderDebateUserResume(msg);
5103
5530
  break;
@@ -5443,10 +5870,17 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5443
5870
  isDmMode: function () { return dmMode && !(dmTargetUser && dmTargetUser.isMate); },
5444
5871
  getDmKey: function () { return dmKey; },
5445
5872
  handleDmSend: function () { handleDmSend(); },
5873
+ isDebateEndedMode: function () { return debateEndedMode; },
5874
+ handleDebateEndedSend: function () { handleDebateEndedSend(); },
5875
+ isDebateConcludeMode: function () { return debateConcludeMode; },
5876
+ handleDebateConcludeSend: function () { handleDebateConcludeSend(); },
5877
+ isDebateFloorMode: function () { return debateFloorMode; },
5878
+ handleDebateFloorSend: function () { handleDebateFloorSend(); },
5446
5879
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
5447
5880
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
5448
5881
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
5449
5882
  showMatePreThinking: function () { showMatePreThinking(); },
5883
+ showClaudePreThinking: function () { showClaudePreThinking(); },
5450
5884
  });
5451
5885
 
5452
5886
  // --- @Mention module ---
@@ -5475,6 +5909,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5475
5909
  availableBuiltins: function () { return cachedAvailableBuiltins || []; },
5476
5910
  currentMateId: function () { return (dmTargetUser && dmTargetUser.isMate) ? dmTargetUser.id : null; },
5477
5911
  requireSkills: requireSkills,
5912
+ showDebateEndedMode: function (msg) { showDebateEndedMode(msg); },
5478
5913
  });
5479
5914
 
5480
5915
  // --- STT module (voice input via Web Speech API) ---
@@ -5737,6 +6172,65 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5737
6172
  fileViewerEl: $("file-viewer"),
5738
6173
  });
5739
6174
 
6175
+ // --- Context Sources ---
6176
+ initContextSources({
6177
+ get ws() { return ws; },
6178
+ get connected() { return connected; },
6179
+ });
6180
+
6181
+ // --- Chrome Extension Bridge ---
6182
+ var _extRequestCallbacks = {}; // requestId -> callback function
6183
+
6184
+ function sendExtensionCommand(command, args, requestId) {
6185
+ window.postMessage({
6186
+ source: "clay-page",
6187
+ payload: {
6188
+ type: "clay_ext_command",
6189
+ command: command,
6190
+ args: args,
6191
+ requestId: requestId
6192
+ }
6193
+ }, "*");
6194
+ }
6195
+
6196
+ function handleExtensionResult(requestId, result) {
6197
+ // Check local callback first (for server-initiated requests)
6198
+ var cb = _extRequestCallbacks[requestId];
6199
+ if (cb) {
6200
+ delete _extRequestCallbacks[requestId];
6201
+ cb(result);
6202
+ return;
6203
+ }
6204
+ // Forward to server
6205
+ if (ws && ws.readyState === 1) {
6206
+ ws.send(JSON.stringify({
6207
+ type: "extension_result",
6208
+ requestId: requestId,
6209
+ result: result
6210
+ }));
6211
+ }
6212
+ }
6213
+
6214
+ window.addEventListener("message", function(event) {
6215
+ if (event.source !== window) return;
6216
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
6217
+ var msg = event.data.payload;
6218
+
6219
+ if (msg.type === "clay_ext_tab_list") {
6220
+ updateBrowserTabList(msg.tabs);
6221
+ // Also inform server about tab list
6222
+ if (ws && ws.readyState === 1) {
6223
+ ws.send(JSON.stringify({
6224
+ type: "browser_tab_list",
6225
+ tabs: msg.tabs
6226
+ }));
6227
+ }
6228
+ }
6229
+ if (msg.type === "clay_ext_result") {
6230
+ handleExtensionResult(msg.requestId, msg.result);
6231
+ }
6232
+ });
6233
+
5740
6234
  // --- Playbook Engine ---
5741
6235
  initPlaybook();
5742
6236
 
@@ -6706,6 +7200,223 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
6706
7200
  scrollToBottom();
6707
7201
  }
6708
7202
 
7203
+ var debateFloorMode = false;
7204
+ var debateConcludeMode = false;
7205
+
7206
+ function showDebateConcludeMode() {
7207
+ debateConcludeMode = true;
7208
+ removeDebateBottomBar();
7209
+ var inputArea = document.getElementById("input-area");
7210
+ if (inputArea) {
7211
+ inputArea.classList.add("debate-floor-mode");
7212
+ inputArea.style.display = "";
7213
+ }
7214
+ // Add conclude banner above input
7215
+ var existingBanner = document.getElementById("debate-floor-banner");
7216
+ if (existingBanner) existingBanner.remove();
7217
+ var banner = document.createElement("div");
7218
+ banner.id = "debate-floor-banner";
7219
+ banner.className = "debate-floor-banner";
7220
+ banner.innerHTML = iconHtml("check-circle") + " <span>The moderator is ready to conclude</span>" +
7221
+ '<button class="debate-floor-done-btn debate-floor-end-btn" id="debate-floor-end-btn">End Debate</button>';
7222
+ if (inputArea && inputArea.parentNode) {
7223
+ inputArea.parentNode.insertBefore(banner, inputArea);
7224
+ }
7225
+ refreshIcons();
7226
+ // End Debate button
7227
+ var endBtn = document.getElementById("debate-floor-end-btn");
7228
+ if (endBtn) {
7229
+ endBtn.addEventListener("click", function () {
7230
+ if (ws && ws.readyState === 1) {
7231
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "end" }));
7232
+ }
7233
+ exitDebateConcludeMode();
7234
+ });
7235
+ }
7236
+ // Update placeholder
7237
+ var inputEl = document.getElementById("input");
7238
+ if (inputEl) {
7239
+ inputEl._origPlaceholder = inputEl._origPlaceholder || inputEl.placeholder;
7240
+ inputEl.placeholder = "Add a direction to continue the debate...";
7241
+ inputEl.focus();
7242
+ }
7243
+ scrollToBottom();
7244
+ }
7245
+
7246
+ function exitDebateConcludeMode() {
7247
+ debateConcludeMode = false;
7248
+ var inputArea = document.getElementById("input-area");
7249
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7250
+ var banner = document.getElementById("debate-floor-banner");
7251
+ if (banner) banner.remove();
7252
+ var inputEl = document.getElementById("input");
7253
+ if (inputEl && inputEl._origPlaceholder) {
7254
+ inputEl.placeholder = inputEl._origPlaceholder;
7255
+ delete inputEl._origPlaceholder;
7256
+ }
7257
+ }
7258
+
7259
+ function handleDebateConcludeSend() {
7260
+ var text = inputEl.value.trim();
7261
+ if (ws && ws.readyState === 1) {
7262
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7263
+ }
7264
+ inputEl.value = "";
7265
+ exitDebateConcludeMode();
7266
+ showDebateBottomBar("live");
7267
+ }
7268
+
7269
+ var debateEndedMode = false;
7270
+
7271
+ function showDebateEndedMode(msg) {
7272
+ debateEndedMode = true;
7273
+ removeDebateBottomBar();
7274
+ var inputArea = document.getElementById("input-area");
7275
+ if (inputArea) {
7276
+ inputArea.classList.add("debate-floor-mode");
7277
+ inputArea.style.display = "";
7278
+ }
7279
+ var existingBanner = document.getElementById("debate-floor-banner");
7280
+ if (existingBanner) existingBanner.remove();
7281
+ var banner = document.createElement("div");
7282
+ banner.id = "debate-floor-banner";
7283
+ banner.className = "debate-floor-banner";
7284
+ banner.innerHTML = iconHtml("check-circle") + " <span>Debate ended</span>" +
7285
+ '<button class="debate-floor-done-btn" id="debate-ended-resume-btn">Resume</button>' +
7286
+ '<button class="debate-floor-done-btn" id="debate-ended-pdf-btn">' + iconHtml("download") + ' PDF</button>';
7287
+ if (inputArea && inputArea.parentNode) {
7288
+ inputArea.parentNode.insertBefore(banner, inputArea);
7289
+ }
7290
+ refreshIcons();
7291
+ // Resume button
7292
+ var resumeBtn = document.getElementById("debate-ended-resume-btn");
7293
+ if (resumeBtn) {
7294
+ resumeBtn.addEventListener("click", function () {
7295
+ handleDebateEndedSend();
7296
+ });
7297
+ }
7298
+ // PDF button
7299
+ var pdfBtn = document.getElementById("debate-ended-pdf-btn");
7300
+ if (pdfBtn) {
7301
+ pdfBtn.addEventListener("click", function () {
7302
+ pdfBtn.disabled = true;
7303
+ exportDebateAsPdf().then(function () { pdfBtn.disabled = false; }).catch(function () { pdfBtn.disabled = false; });
7304
+ });
7305
+ }
7306
+ var inputEl2 = document.getElementById("input");
7307
+ if (inputEl2) {
7308
+ inputEl2._origPlaceholder = inputEl2._origPlaceholder || inputEl2.placeholder;
7309
+ inputEl2.placeholder = "Continue with a new direction...";
7310
+ }
7311
+ scrollToBottom();
7312
+ }
7313
+
7314
+ function exitDebateEndedMode() {
7315
+ debateEndedMode = false;
7316
+ var inputArea = document.getElementById("input-area");
7317
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7318
+ var banner = document.getElementById("debate-floor-banner");
7319
+ if (banner) banner.remove();
7320
+ var inputEl2 = document.getElementById("input");
7321
+ if (inputEl2 && inputEl2._origPlaceholder) {
7322
+ inputEl2.placeholder = inputEl2._origPlaceholder;
7323
+ delete inputEl2._origPlaceholder;
7324
+ }
7325
+ }
7326
+
7327
+ function handleDebateEndedSend() {
7328
+ var text = inputEl.value.trim();
7329
+ if (ws && ws.readyState === 1) {
7330
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7331
+ }
7332
+ inputEl.value = "";
7333
+ exitDebateEndedMode();
7334
+ }
7335
+
7336
+ function showDebateUserFloor(msg) {
7337
+ debateFloorMode = true;
7338
+ // Remove debate bottom bar and show input area in floor mode
7339
+ removeDebateBottomBar();
7340
+ var inputArea = document.getElementById("input-area");
7341
+ if (inputArea) {
7342
+ inputArea.classList.add("debate-floor-mode");
7343
+ inputArea.style.display = "";
7344
+ }
7345
+ // Add floor banner above input
7346
+ var existingBanner = document.getElementById("debate-floor-banner");
7347
+ if (existingBanner) existingBanner.remove();
7348
+ var banner = document.createElement("div");
7349
+ banner.id = "debate-floor-banner";
7350
+ banner.className = "debate-floor-banner";
7351
+ banner.innerHTML = iconHtml("mic") + " <span>You have the floor</span>" +
7352
+ '<button class="debate-floor-done-btn" id="debate-floor-done-btn">Pass</button>';
7353
+ if (inputArea && inputArea.parentNode) {
7354
+ inputArea.parentNode.insertBefore(banner, inputArea);
7355
+ }
7356
+ refreshIcons();
7357
+ // Done button: exit floor mode without sending
7358
+ var doneBtn = document.getElementById("debate-floor-done-btn");
7359
+ if (doneBtn) {
7360
+ doneBtn.addEventListener("click", function () {
7361
+ // Pass without speaking: resume debate
7362
+ if (ws && ws.readyState === 1) {
7363
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: "(The user passed without speaking)" }));
7364
+ }
7365
+ exitDebateFloorMode();
7366
+ showDebateBottomBar("live");
7367
+ });
7368
+ }
7369
+ // Update placeholder
7370
+ var inputEl = document.getElementById("input");
7371
+ if (inputEl) {
7372
+ inputEl._origPlaceholder = inputEl.placeholder;
7373
+ inputEl.placeholder = "Share your thoughts with the panel...";
7374
+ inputEl.focus();
7375
+ }
7376
+ scrollToBottom();
7377
+ }
7378
+
7379
+ function exitDebateFloorMode() {
7380
+ debateFloorMode = false;
7381
+ var inputArea = document.getElementById("input-area");
7382
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7383
+ var banner = document.getElementById("debate-floor-banner");
7384
+ if (banner) banner.remove();
7385
+ var inputEl = document.getElementById("input");
7386
+ if (inputEl && inputEl._origPlaceholder) {
7387
+ inputEl.placeholder = inputEl._origPlaceholder;
7388
+ delete inputEl._origPlaceholder;
7389
+ }
7390
+ }
7391
+
7392
+ function handleDebateFloorSend() {
7393
+ var text = inputEl.value.trim();
7394
+ if (!text) return;
7395
+ if (ws && ws.readyState === 1) {
7396
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: text }));
7397
+ }
7398
+ inputEl.value = "";
7399
+ exitDebateFloorMode();
7400
+ showDebateBottomBar("live");
7401
+ }
7402
+
7403
+ function renderDebateUserFloorDone(msg) {
7404
+ if (!messagesEl) return;
7405
+ var el = document.createElement("div");
7406
+ el.className = "debate-user-comment";
7407
+ var label = document.createElement("span");
7408
+ label.className = "debate-comment-label";
7409
+ label.innerHTML = iconHtml("mic") + " User:";
7410
+ var textEl = document.createElement("div");
7411
+ textEl.className = "debate-comment-text";
7412
+ textEl.textContent = msg.text || "";
7413
+ el.appendChild(label);
7414
+ el.appendChild(textEl);
7415
+ messagesEl.appendChild(el);
7416
+ refreshIcons();
7417
+ scrollToBottom();
7418
+ }
7419
+
6709
7420
  // Legacy handler kept for compatibility
6710
7421
  function showDebateSticky(phase, msg) {
6711
7422
  if (phase === "ended" || phase === "hide") {
@@ -6769,6 +7480,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
6769
7480
  bar.innerHTML =
6770
7481
  '<div class="debate-bottom-inner">' +
6771
7482
  '<button class="debate-bottom-hand" id="debate-bottom-hand">' + iconHtml("hand") + ' Raise hand</button>' +
7483
+ '<span class="debate-bottom-waiting hidden" id="debate-bottom-waiting">' + iconHtml("loader") + ' You will get the floor after the current speaker</span>' +
6772
7484
  '<button class="debate-bottom-stop" id="debate-bottom-stop">' + iconHtml("square") + ' Stop</button>' +
6773
7485
  '</div>';
6774
7486
 
@@ -6776,6 +7488,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
6776
7488
  inputArea.style.display = "none";
6777
7489
  refreshIcons();
6778
7490
 
7491
+ // Restore raised state if hand was already raised
7492
+ if (debateHandRaiseOpen) {
7493
+ var handBtn = document.getElementById("debate-bottom-hand");
7494
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7495
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7496
+ if (waitingEl) waitingEl.classList.remove("hidden");
7497
+ }
7498
+
6779
7499
  document.getElementById("debate-bottom-hand").addEventListener("click", function () {
6780
7500
  toggleDebateHandRaise();
6781
7501
  });
@@ -6785,47 +7505,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
6785
7505
  }
6786
7506
  });
6787
7507
  } else if (mode === "conclude") {
6788
- bar.innerHTML =
6789
- '<div class="debate-bottom-inner debate-bottom-conclude">' +
6790
- '<div class="debate-bottom-conclude-label">' + iconHtml("check-circle") + ' The moderator is ready to conclude. End the debate?</div>' +
6791
- '<textarea class="debate-bottom-conclude-input" id="debate-bottom-conclude-input" rows="3" placeholder="Or add a direction to continue..."></textarea>' +
6792
- '<div class="debate-bottom-conclude-actions">' +
6793
- '<button class="debate-bottom-continue" id="debate-bottom-continue">Continue</button>' +
6794
- '<button class="debate-bottom-end" id="debate-bottom-end">End Debate</button>' +
6795
- '</div>' +
6796
- '</div>';
6797
-
6798
- inputArea.parentNode.insertBefore(bar, inputArea);
6799
- inputArea.style.display = "none";
6800
- refreshIcons();
6801
-
6802
- var textArea = document.getElementById("debate-bottom-conclude-input");
6803
- document.getElementById("debate-bottom-end").addEventListener("click", function () {
6804
- if (ws && ws.readyState === 1) {
6805
- ws.send(JSON.stringify({ type: "debate_conclude_response", action: "end" }));
6806
- }
6807
- removeDebateBottomBar();
6808
- });
6809
- document.getElementById("debate-bottom-continue").addEventListener("click", function () {
6810
- var text = textArea ? textArea.value.trim() : "";
6811
- if (ws && ws.readyState === 1) {
6812
- ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
6813
- }
6814
- removeDebateBottomBar();
6815
- showDebateBottomBar("live");
6816
- });
6817
- if (textArea) {
6818
- textArea.focus();
6819
- textArea.addEventListener("keydown", function (e) {
6820
- if (e.key === "Enter" && !e.shiftKey) {
6821
- e.preventDefault();
6822
- document.getElementById("debate-bottom-continue").click();
6823
- }
6824
- });
6825
- textArea.addEventListener("input", function () {
6826
- debateAutoResize(textArea, 12);
6827
- });
6828
- }
7508
+ // Use native input area with conclude banner
7509
+ showDebateConcludeMode();
7510
+ return; // don't create a separate bar
6829
7511
  }
6830
7512
  }
6831
7513
 
@@ -6845,74 +7527,34 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
6845
7527
  var handBar = document.getElementById("debate-hand-raise-bar");
6846
7528
  if (handBar) handBar.remove();
6847
7529
  debateHandRaiseOpen = false;
7530
+ // Clean up floor/conclude modes
7531
+ if (debateFloorMode) exitDebateFloorMode();
7532
+ if (debateConcludeMode) exitDebateConcludeMode();
7533
+ if (debateEndedMode) exitDebateEndedMode();
6848
7534
  // Restore input area
6849
7535
  var inputArea = document.getElementById("input-area");
6850
7536
  if (inputArea) inputArea.style.display = "";
6851
7537
  }
6852
7538
 
6853
7539
  function toggleDebateHandRaise(forceState) {
6854
- var show = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
6855
- debateHandRaiseOpen = show;
6856
-
6857
- var existing = document.getElementById("debate-hand-raise-bar");
6858
- if (!show) {
6859
- if (existing) existing.remove();
6860
- return;
6861
- }
6862
- if (existing) {
6863
- var inp = existing.querySelector(".debate-hand-input");
6864
- if (inp) { inp.value = ""; inp.focus(); }
6865
- return;
6866
- }
6867
-
6868
- // Create hand raise bar above input area
6869
- var bar = document.createElement("div");
6870
- bar.id = "debate-hand-raise-bar";
6871
- bar.className = "debate-hand-raise-bar";
6872
- bar.innerHTML =
6873
- '<div class="debate-hand-raise-inner">' +
6874
- '<span class="debate-hand-raise-label">' + iconHtml("hand") + ' Your comment:</span>' +
6875
- '<textarea class="debate-hand-input" rows="1" placeholder="Type your comment..."></textarea>' +
6876
- '<button class="debate-hand-send">Send</button>' +
6877
- '<button class="debate-hand-cancel">Cancel</button>' +
6878
- '</div>';
6879
-
6880
- var inputArea = document.getElementById("input-area");
6881
- if (inputArea && inputArea.parentNode) {
6882
- inputArea.parentNode.insertBefore(bar, inputArea);
6883
- }
6884
- refreshIcons();
6885
-
6886
- var textarea = bar.querySelector(".debate-hand-input");
6887
- var sendBtn = bar.querySelector(".debate-hand-send");
6888
- var cancelBtn = bar.querySelector(".debate-hand-cancel");
6889
-
6890
- if (textarea) {
6891
- textarea.focus();
6892
- textarea.addEventListener("input", function () {
6893
- debateAutoResize(textarea, 12);
6894
- });
7540
+ var raise = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
7541
+ debateHandRaiseOpen = raise;
7542
+
7543
+ // Update UI: hide hand button, show waiting message
7544
+ var handBtn = document.getElementById("debate-bottom-hand");
7545
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7546
+ if (raise) {
7547
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7548
+ if (waitingEl) waitingEl.classList.remove("hidden");
7549
+ } else {
7550
+ if (handBtn) { handBtn.classList.remove("raised"); handBtn.classList.remove("hidden"); }
7551
+ if (waitingEl) waitingEl.classList.add("hidden");
6895
7552
  }
6896
7553
 
6897
- sendBtn.addEventListener("click", function () {
6898
- var text = textarea ? textarea.value.trim() : "";
6899
- if (!text) return;
6900
- if (ws && ws.readyState === 1) {
6901
- ws.send(JSON.stringify({ type: "debate_comment", text: text }));
6902
- }
6903
- toggleDebateHandRaise(false);
6904
- });
6905
-
6906
- cancelBtn.addEventListener("click", function () {
6907
- toggleDebateHandRaise(false);
6908
- });
6909
-
6910
- if (textarea) {
6911
- textarea.addEventListener("keydown", function (e) {
6912
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendBtn.click(); }
6913
- if (e.key === "Escape") { toggleDebateHandRaise(false); }
6914
- });
7554
+ if (raise && ws && ws.readyState === 1) {
7555
+ ws.send(JSON.stringify({ type: "debate_hand_raise" }));
6915
7556
  }
7557
+ // Floor mode will be activated when server sends debate_user_floor
6916
7558
  }
6917
7559
 
6918
7560
  function sendDebateStickyComment() {