clay-server 2.31.0 → 2.32.0-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.
Files changed (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +91 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -40,10 +40,6 @@
40
40
  body.wide-view:not(.mate-dm-active) .tool-group{max-width:100%!important}
41
41
  body.wide-view:not(.mate-dm-active) .turn-meta{max-width:100%!important}
42
42
  }
43
- #ask-mate-btn::before{content:""!important;position:absolute!important;inset:0!important;background:linear-gradient(135deg,#4ecdc4 0%,#556bf7 33%,#a855f7 55%,#f857a6 78%,#ff6b6b 100%)!important;border-radius:inherit!important;opacity:0;transform:scale(0.3);transition:opacity 0.3s ease,transform 0.35s cubic-bezier(0.34,1.56,0.64,1);z-index:-1}
44
- #ask-mate-btn:hover::before{opacity:1!important;transform:scale(1)!important}
45
- #ask-mate-btn:hover{border-color:transparent!important;background:transparent!important;transform:translateY(-1px);box-shadow:0 2px 12px rgba(148,130,247,0.4)}
46
- #ask-mate-btn:hover .ask-mate-label{background:none!important;-webkit-text-fill-color:#fff!important}
47
43
  </style>
48
44
  </head>
49
45
  <body>
@@ -219,15 +215,7 @@
219
215
  <button id="session-search-clear" type="button" aria-label="Clear search"><i data-lucide="x"></i></button>
220
216
  </div>
221
217
  </div>
222
- <div id="files-header-content" class="hidden">
223
- <div class="session-list-header">
224
- <span>File Browser</span>
225
- <div class="session-list-header-actions">
226
- <button id="file-panel-refresh" type="button" title="Refresh file tree"><i data-lucide="refresh-cw"></i></button>
227
- <button id="file-panel-close" type="button" title="Close file browser"><i data-lucide="x"></i></button>
228
- </div>
229
- </div>
230
- </div>
218
+ <div id="files-header-content" class="hidden"></div>
231
219
  </div>
232
220
  <div id="ralph-loop-section"></div>
233
221
  <div id="sidebar-panel-projects" class="sidebar-panel hidden">
@@ -244,6 +232,15 @@
244
232
  </div>
245
233
  </div>
246
234
  <div id="sidebar-panel-files" class="sidebar-panel hidden">
235
+ <div class="fb-titlebar">
236
+ <button id="file-panel-refresh" type="button" title="Refresh file tree"><i data-lucide="refresh-cw"></i></button>
237
+ <span class="fb-titlebar-title">File Browser</span>
238
+ <button id="file-panel-close" type="button" title="Close file browser"><i data-lucide="x"></i></button>
239
+ </div>
240
+ <div class="fb-search-bar">
241
+ <i data-lucide="search" class="fb-search-icon"></i>
242
+ <input id="fb-search-input" type="text" placeholder="Search files..." autocomplete="off" spellcheck="false" />
243
+ </div>
247
244
  <div id="file-tree"></div>
248
245
  </div>
249
246
  </div>
@@ -256,6 +253,7 @@
256
253
  <div class="mate-sidebar-header" id="mate-sidebar-header">
257
254
  <img id="mate-sidebar-avatar" class="mate-sidebar-avatar" alt="">
258
255
  <span id="mate-sidebar-name" class="mate-sidebar-name"></span>
256
+ <div id="mate-vendor-toggle" class="mate-vendor-toggle"></div>
259
257
  <div id="mate-sidebar-seed-tooltip" class="mate-seed-tooltip hidden"></div>
260
258
  <button id="mate-sidebar-toggle-btn" class="sidebar-collapse-btn" title="Collapse sidebar"><i data-lucide="panel-left-close"></i></button>
261
259
  </div>
@@ -446,15 +444,6 @@
446
444
  <div id="input-wrapper">
447
445
  <div id="mention-menu"></div>
448
446
  <div id="slash-menu"></div>
449
- <div id="context-sources-bar">
450
- <div id="context-sources-chips"></div>
451
- <button id="context-sources-add" type="button" title="Add context source"><i data-lucide="plus"></i><span>Context Sources</span></button>
452
- <div id="context-sources-picker" class="hidden">
453
- <div class="context-picker-section" id="context-picker-email"></div>
454
- <div class="context-picker-section" id="context-picker-terminals"></div>
455
- <div class="context-picker-section" id="context-picker-tabs"></div>
456
- </div>
457
- </div>
458
447
  <div id="input-row">
459
448
  <div id="context-mini" class="hidden">
460
449
  <div class="context-mini-bar">
@@ -464,16 +453,37 @@
464
453
  </div>
465
454
  <div id="image-preview-bar"></div>
466
455
  <div id="suggestion-chips" class="hidden"></div>
467
- <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send" dir="auto"></textarea>
456
+ <div id="input-textarea-wrap">
457
+ <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send" dir="auto"></textarea>
458
+ <div id="ghost-suggestion" class="hidden" aria-hidden="true"></div>
459
+ </div>
468
460
  <div id="input-bottom">
469
461
  <div id="attach-wrap">
470
462
  <button id="attach-file-btn" type="button" aria-label="Attach file" title="Attach file"><i data-lucide="paperclip"></i></button>
471
463
  <button id="attach-image-btn" type="button" aria-label="Attach image" title="Attach image"><i data-lucide="image"></i></button>
472
464
  <button id="stt-btn" type="button" aria-label="Voice input" title="Voice input"><i data-lucide="mic"></i></button>
473
465
  <button id="schedule-btn" type="button" aria-label="Schedule message" title="Schedule message"><i data-lucide="clock"></i></button>
474
- <button id="ask-mate-btn" type="button" aria-label="Ask Mate"><span class="ask-mate-label">@ Ask Mate</span></button>
466
+ <button id="ask-mate-btn" type="button" aria-label="Ask Mate" title="Ask a Mate for advice on this session"><i data-lucide="at-sign"></i></button>
467
+ <div id="context-sources-btn-wrap">
468
+ <button id="context-sources-add" type="button" title="Add context sources"><i data-lucide="plus"></i><span class="ctx-label">Context</span></button>
469
+ <div id="context-sources-picker" class="hidden">
470
+ <div class="context-picker-section" id="context-picker-email"></div>
471
+ <div class="context-picker-section" id="context-picker-terminals"></div>
472
+ <div class="context-picker-section" id="context-picker-tabs"></div>
473
+ </div>
474
+ </div>
475
475
  </div>
476
476
  <div id="input-bottom-right">
477
+ <div id="vendor-toggle-wrap">
478
+ <button id="vendor-btn-claude" class="vendor-toggle-btn active" data-vendor="claude">
479
+ <img src="/claude-code-avatar.png" class="vendor-toggle-icon" alt="Claude">
480
+ <span class="vendor-toggle-label">Claude Code</span>
481
+ </button>
482
+ <button id="vendor-btn-codex" class="vendor-toggle-btn" data-vendor="codex">
483
+ <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
484
+ <span class="vendor-toggle-label">Codex</span>
485
+ </button>
486
+ </div>
477
487
  <div id="config-chip-wrap" class="hidden">
478
488
  <button id="config-chip" title="Model, mode, and effort settings">
479
489
  <i class="config-chip-icon" data-lucide="sliders-horizontal"></i>
@@ -511,6 +521,18 @@
511
521
  </button>
512
522
  </div>
513
523
  </div>
524
+ <div id="config-approval-section" class="config-section" style="display:none">
525
+ <div class="config-section-label">APPROVAL</div>
526
+ <div id="config-approval-bar" class="config-segmented"></div>
527
+ </div>
528
+ <div id="config-sandbox-section" class="config-section" style="display:none">
529
+ <div class="config-section-label">SANDBOX</div>
530
+ <div id="config-sandbox-bar" class="config-segmented"></div>
531
+ </div>
532
+ <div id="config-websearch-section" class="config-section" style="display:none">
533
+ <div class="config-section-label">WEB SEARCH</div>
534
+ <div id="config-websearch-bar" class="config-segmented"></div>
535
+ </div>
514
536
  </div>
515
537
  </div>
516
538
  <button id="send-btn" disabled aria-label="Send"><i data-lucide="arrow-up"></i></button>
@@ -1,56 +1,158 @@
1
1
  // app-connection.js - WebSocket connection, reconnect, status
2
2
  // Extracted from app.js (PR-22)
3
3
 
4
- var _ctx = null;
4
+ import { store } from './store.js';
5
+ import { getWs, setWs } from './ws-ref.js';
6
+ import { getStatusDot, getSendBtn } from './dom-refs.js';
7
+ import { setSendBtnMode, blinkIO, setActivity } from './app-favicon.js';
8
+ import { startLogoAnimation, stopLogoAnimation } from './ascii-logo.js';
9
+ import { hasSendableContent } from './input.js';
10
+ import { isNotifAlertEnabled } from './notifications.js';
11
+ import { processMessage } from './app-messages.js';
12
+ import { flushPendingExtMessages } from './app-misc.js';
13
+ import { resetTerminals } from './terminal.js';
14
+ import { closeDmUserPicker } from './sidebar-mates.js';
15
+ import { openDm } from './app-dm.js';
16
+
5
17
  var wasConnected = false;
6
18
  var reconnectTimer = null;
7
19
  var reconnectDelay = 1000;
8
20
  var connectTimeoutId = null;
9
21
  var disconnectNotifTimer = null;
10
22
  var disconnectNotifShown = false;
23
+ var connectOverlay = null;
24
+
25
+ export function initConnection() {
26
+ connectOverlay = document.getElementById("connect-overlay");
11
27
 
12
- export function initConnection(ctx) {
13
- _ctx = ctx;
28
+ // --- Reactive UI sync for connected/processing state ---
29
+ store.subscribe(function (state, prev) {
30
+ // Status dot (depends on both connected and processing)
31
+ if (state.connected !== prev.connected || state.processing !== prev.processing) {
32
+ var dot = getStatusDot();
33
+ if (dot) {
34
+ dot.className = "icon-strip-status";
35
+ if (state.connected) {
36
+ dot.classList.add("connected");
37
+ if (state.processing) dot.classList.add("processing");
38
+ }
39
+ }
40
+ }
41
+
42
+ // Connected state changed
43
+ if (state.connected !== prev.connected) {
44
+ var sendBtn = getSendBtn();
45
+ if (state.connected) {
46
+ if (sendBtn) sendBtn.disabled = false;
47
+ if (connectOverlay) connectOverlay.classList.add("hidden");
48
+ var updPill = document.getElementById("update-pill-wrap");
49
+ if (updPill) updPill.classList.add("hidden");
50
+ stopLogoAnimation();
51
+ } else {
52
+ if (sendBtn) sendBtn.disabled = true;
53
+ if (connectOverlay) connectOverlay.classList.remove("hidden");
54
+ startLogoAnimation();
55
+ }
56
+ }
57
+
58
+ // Processing state changed
59
+ if (state.processing !== prev.processing) {
60
+ if (state.processing) {
61
+ setSendBtnMode(hasSendableContent() ? "send" : "stop");
62
+ } else if (state.connected) {
63
+ setSendBtnMode("send");
64
+ }
65
+ }
66
+ });
14
67
  }
15
68
 
69
+ // setStatus: now just sets state. UI sync is handled by the subscriber above.
16
70
  export function setStatus(status) {
17
- var dot = _ctx.getStatusDot();
18
- if (dot) dot.className = "icon-strip-status";
19
71
  if (status === "connected") {
20
- if (dot) dot.classList.add("connected");
21
- _ctx.setConnected(true);
22
- _ctx.setProcessing(false);
23
- _ctx.sendBtn.disabled = false;
24
- _ctx.setSendBtnMode("send");
25
- _ctx.connectOverlay.classList.add("hidden");
26
- // Hide update banner on reconnect; server will re-send update_available if still needed
27
- var updPill = document.getElementById("update-pill-wrap");
28
- if (updPill) updPill.classList.add("hidden");
29
- _ctx.stopVerbCycle();
72
+ store.set({ connected: true, processing: false });
30
73
  } else if (status === "processing") {
31
- if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
32
- _ctx.setProcessing(true);
33
- _ctx.setSendBtnMode(_ctx.hasSendableContent() ? "send" : "stop");
74
+ store.set({ processing: true });
34
75
  } else {
35
- _ctx.setConnected(false);
36
- _ctx.sendBtn.disabled = true;
37
- _ctx.connectOverlay.classList.remove("hidden");
38
- _ctx.startVerbCycle();
76
+ store.set({ connected: false, processing: false });
77
+ }
78
+ }
79
+
80
+ function onConnected() {
81
+ // Flush any extension messages that arrived before WS was ready
82
+ flushPendingExtMessages();
83
+
84
+ // Reset terminal xterm instances (server will send fresh term_list)
85
+ resetTerminals();
86
+
87
+ // Re-send push subscription on reconnect
88
+ var ws = getWs();
89
+ if (window._pushSubscription) {
90
+ try {
91
+ ws.send(JSON.stringify({
92
+ type: "push_subscribe",
93
+ subscription: window._pushSubscription.toJSON(),
94
+ }));
95
+ } catch(e) {}
96
+ }
97
+
98
+ // Request mates list
99
+ try {
100
+ ws.send(JSON.stringify({ type: "mate_list" }));
101
+ } catch(e) {}
102
+
103
+ // If connecting to a mate project, request knowledge list for badge
104
+ if (store.get('mateProjectSlug')) {
105
+ try { ws.send(JSON.stringify({ type: "knowledge_list" })); } catch(e) {}
106
+ }
107
+
108
+ // Session restore is now server-driven (user-presence.json).
109
+ // Mate DM restore is also server-driven via "restore_mate_dm" message.
110
+ // Fallback: if server doesn't restore DM within 2s, try localStorage
111
+ var savedDm = null;
112
+ try { savedDm = localStorage.getItem("clay-active-dm"); } catch (e) {}
113
+ if (savedDm && !store.get('dmMode') && !store.get('mateProjectSlug')) {
114
+ var dmFallbackTimer = setTimeout(function () {
115
+ if (!store.get('dmMode') && savedDm) {
116
+ console.log("[dm-restore] Server did not restore DM, using localStorage fallback:", savedDm);
117
+ openDm(savedDm);
118
+ }
119
+ }, 2000);
120
+ // Cancel fallback if server restores DM first
121
+ var patchedOnce = false;
122
+ var checkRestore = function (evt) {
123
+ try {
124
+ var d = JSON.parse(evt.data);
125
+ if (d.type === "restore_mate_dm" && !patchedOnce) {
126
+ patchedOnce = true;
127
+ clearTimeout(dmFallbackTimer);
128
+ }
129
+ } catch (e) {}
130
+ };
131
+ ws.addEventListener("message", checkRestore);
132
+ setTimeout(function () { ws.removeEventListener("message", checkRestore); }, 3000);
133
+ }
134
+ // Safety: clear returningFromMateDm after initial messages settle
135
+ if (store.get('returningFromMateDm')) {
136
+ setTimeout(function () {
137
+ if (store.get('returningFromMateDm')) {
138
+ store.set({ returningFromMateDm: false });
139
+ }
140
+ }, 2000);
39
141
  }
40
142
  }
41
143
 
42
144
  export function connect() {
43
- var ws = _ctx.getWs();
145
+ var ws = getWs();
44
146
  if (ws) { ws.onclose = null; ws.close(); }
45
147
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
46
148
 
47
149
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
48
- var newWs = new WebSocket(protocol + "//" + location.host + _ctx.getWsPath());
49
- _ctx.setWs(newWs);
150
+ var newWs = new WebSocket(protocol + "//" + location.host + store.get('wsPath'));
151
+ setWs(newWs);
50
152
 
51
153
  // If not connected within 3s, force retry
52
154
  connectTimeoutId = setTimeout(function () {
53
- if (!_ctx.isConnected()) {
155
+ if (!store.get('connected')) {
54
156
  newWs.onclose = null;
55
157
  newWs.onerror = null;
56
158
  newWs.close();
@@ -68,7 +170,7 @@ export function connect() {
68
170
  // Only show "restored" notification if "lost" was actually shown
69
171
  var isMobileDevice = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
70
172
  (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
71
- if (wasConnected && disconnectNotifShown && !isMobileDevice && _ctx.isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator && Notification.permission === "granted") {
173
+ if (wasConnected && disconnectNotifShown && !isMobileDevice && isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator && Notification.permission === "granted") {
72
174
  navigator.serviceWorker.ready.then(function (reg) {
73
175
  return reg.showNotification("Clay", {
74
176
  body: "Server connection restored",
@@ -83,22 +185,21 @@ export function connect() {
83
185
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
84
186
 
85
187
  // Wrap ws.send to blink LED on outgoing traffic
86
- var currentWs = _ctx.getWs();
188
+ var currentWs = getWs();
87
189
  var _origSend = currentWs.send.bind(currentWs);
88
190
  currentWs.send = function (data) {
89
- _ctx.blinkIO();
191
+ blinkIO();
90
192
  return _origSend(data);
91
193
  };
92
194
 
93
- _ctx.onConnected();
195
+ onConnected();
94
196
  };
95
197
 
96
198
  newWs.onclose = function (e) {
97
199
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
98
- _ctx.closeDmUserPicker();
200
+ closeDmUserPicker();
99
201
  setStatus("disconnected");
100
- _ctx.setProcessing(false);
101
- _ctx.setActivity(null);
202
+ setActivity(null);
102
203
  // Delay "connection lost" notification by 5s to suppress brief disconnects
103
204
  if (!disconnectNotifTimer) {
104
205
  disconnectNotifTimer = setTimeout(function () {
@@ -106,7 +207,7 @@ export function connect() {
106
207
  disconnectNotifShown = true;
107
208
  var isMobileDevice = /Mobi|Android|iPad|iPhone|iPod/.test(navigator.userAgent) ||
108
209
  (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
109
- if (!isMobileDevice && _ctx.isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator && Notification.permission === "granted") {
210
+ if (!isMobileDevice && isNotifAlertEnabled() && !document.hasFocus() && "serviceWorker" in navigator && Notification.permission === "granted") {
110
211
  navigator.serviceWorker.ready.then(function (reg) {
111
212
  return reg.showNotification("Clay", {
112
213
  body: "Server connection lost",
@@ -123,16 +224,16 @@ export function connect() {
123
224
 
124
225
  newWs.onmessage = function (event) {
125
226
  // Backup: if we're receiving messages, we're connected
126
- if (!_ctx.isConnected()) {
227
+ if (!store.get('connected')) {
127
228
  setStatus("connected");
128
229
  reconnectDelay = 1000;
129
230
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
130
231
  }
131
232
 
132
- _ctx.blinkIO();
233
+ blinkIO();
133
234
  var msg;
134
235
  try { msg = JSON.parse(event.data); } catch (e) { return; }
135
- _ctx.processMessage(msg);
236
+ processMessage(msg);
136
237
  };
137
238
  }
138
239
 
@@ -2,8 +2,10 @@
2
2
  // Extracted from app.js (PR-27)
3
3
 
4
4
  import { avatarUrl } from './avatar.js';
5
-
6
- var _ctx = null;
5
+ import { getWs } from './ws-ref.js';
6
+ import { store } from './store.js';
7
+ import { getMessagesEl } from './dom-refs.js';
8
+ import { registerTooltip } from './tooltip.js';
7
9
 
8
10
  // --- Module-owned state ---
9
11
  var cursorSharingEnabled = localStorage.getItem("cursorSharing") !== "off";
@@ -122,7 +124,7 @@ function getNodeAtCharOffset(container, charOffset) {
122
124
 
123
125
  // Find parent [data-turn] element from a DOM node
124
126
  function findParentTurn(node) {
125
- var messagesEl = _ctx.messagesEl;
127
+ var messagesEl = getMessagesEl();
126
128
  var el = node.nodeType === 3 ? node.parentElement : node;
127
129
  while (el && el !== messagesEl) {
128
130
  if (el.dataset && el.dataset.turn != null) return el;
@@ -158,7 +160,7 @@ function createOffscreenIndicator(userId, displayName, color) {
158
160
  }
159
161
 
160
162
  function updateCursorVisibility(entry) {
161
- var messagesEl = _ctx.messagesEl;
163
+ var messagesEl = getMessagesEl();
162
164
  var visibleTop = messagesEl.scrollTop;
163
165
  var visibleBottom = visibleTop + messagesEl.clientHeight;
164
166
  var y = entry.lastY || 0;
@@ -176,7 +178,7 @@ function updateCursorVisibility(entry) {
176
178
 
177
179
  // Find the closest [data-turn] element to a given clientY
178
180
  function findClosestTurn(clientY) {
179
- var messagesEl = _ctx.messagesEl;
181
+ var messagesEl = getMessagesEl();
180
182
  var turns = messagesEl.querySelectorAll("[data-turn]");
181
183
  if (!turns.length) return null;
182
184
  // First: exact hit
@@ -198,7 +200,7 @@ function findClosestTurn(clientY) {
198
200
 
199
201
  // Cursor sharing toggle button in user island (multi-user only)
200
202
  export function initCursorToggle() {
201
- if (!_ctx.isMultiUserMode) return;
203
+ if (!store.get('isMultiUserMode')) return;
202
204
  var actionsEl = document.querySelector(".user-island-actions");
203
205
  if (!actionsEl) return;
204
206
  if (document.getElementById("cursor-share-toggle")) return;
@@ -218,11 +220,11 @@ export function initCursorToggle() {
218
220
  if (cursorSharingEnabled) {
219
221
  btn.classList.remove("off");
220
222
  btn.classList.add("on");
221
- _ctx.registerTooltip(btn, "Cursor sharing on");
223
+ registerTooltip(btn, "Cursor sharing on");
222
224
  } else {
223
225
  btn.classList.remove("on");
224
226
  btn.classList.add("off");
225
- _ctx.registerTooltip(btn, "Cursor sharing off");
227
+ registerTooltip(btn, "Cursor sharing off");
226
228
  }
227
229
  }
228
230
 
@@ -233,7 +235,7 @@ export function initCursorToggle() {
233
235
  cursorSharingEnabled = !cursorSharingEnabled;
234
236
  localStorage.setItem("cursorSharing", cursorSharingEnabled ? "on" : "off");
235
237
  updateToggleStyle();
236
- var ws = _ctx.ws;
238
+ var ws = getWs();
237
239
  if (!cursorSharingEnabled && ws && ws.readyState === 1) {
238
240
  ws.send(JSON.stringify({ type: "cursor_leave" }));
239
241
  ws.send(JSON.stringify({ type: "text_select", ranges: [] }));
@@ -244,7 +246,7 @@ export function initCursorToggle() {
244
246
  // --- Exported functions ---
245
247
 
246
248
  export function handleRemoteSelection(msg) {
247
- var messagesEl = _ctx.messagesEl;
249
+ var messagesEl = getMessagesEl();
248
250
  var userId = msg.userId;
249
251
  var color = getCursorColor(userId);
250
252
 
@@ -305,7 +307,7 @@ export function handleRemoteSelection(msg) {
305
307
  }
306
308
 
307
309
  export function handleRemoteCursorMove(msg) {
308
- var messagesEl = _ctx.messagesEl;
310
+ var messagesEl = getMessagesEl();
309
311
  var userId = msg.userId;
310
312
 
311
313
  var entry = remoteCursors[userId];
@@ -379,16 +381,15 @@ export function clearRemoteCursors() {
379
381
  remoteSelections = {};
380
382
  }
381
383
 
382
- export function initCursors(ctx) {
383
- _ctx = ctx;
384
- var messagesEl = _ctx.messagesEl;
384
+ export function initCursors() {
385
+ var messagesEl = getMessagesEl();
385
386
 
386
387
  initCursorToggle();
387
388
 
388
389
  // Track local cursor and send to server
389
390
  messagesEl.addEventListener("mousemove", function (e) {
390
391
  if (!cursorSharingEnabled) return;
391
- var ws = _ctx.ws;
392
+ var ws = getWs();
392
393
  if (!ws || ws.readyState !== 1) return;
393
394
  if (cursorThrottleTimer) return;
394
395
  cursorThrottleTimer = setTimeout(function () { cursorThrottleTimer = null; }, CURSOR_THROTTLE_MS);
@@ -412,7 +413,7 @@ export function initCursors(ctx) {
412
413
 
413
414
  messagesEl.addEventListener("mouseleave", function () {
414
415
  if (!cursorSharingEnabled) return;
415
- var ws = _ctx.ws;
416
+ var ws = getWs();
416
417
  if (!ws || ws.readyState !== 1) return;
417
418
  ws.send(JSON.stringify({ type: "cursor_leave" }));
418
419
  });
@@ -429,7 +430,7 @@ export function initCursors(ctx) {
429
430
  // Track local text selection and send to server
430
431
  document.addEventListener("selectionchange", function () {
431
432
  if (!cursorSharingEnabled) return;
432
- var ws = _ctx.ws;
433
+ var ws = getWs();
433
434
  if (!ws || ws.readyState !== 1) return;
434
435
  if (selectionThrottleTimer) return;
435
436
  selectionThrottleTimer = setTimeout(function () { selectionThrottleTimer = null; }, 100);
@@ -20,7 +20,7 @@ export function showDebateConcludeConfirm(msg) {
20
20
 
21
21
  function showDebateConcludeMode() {
22
22
  removeDebateBottomBar();
23
- store.setState({ debateConcludeMode: true });
23
+ store.set({ debateConcludeMode: true });
24
24
  var inputArea = document.getElementById("input-area");
25
25
  if (inputArea) {
26
26
  inputArea.classList.add("debate-floor-mode");
@@ -57,7 +57,7 @@ function showDebateConcludeMode() {
57
57
  }
58
58
 
59
59
  export function exitDebateConcludeMode() {
60
- store.setState({ debateConcludeMode: false });
60
+ store.set({ debateConcludeMode: false });
61
61
  var inputArea = document.getElementById("input-area");
62
62
  if (inputArea) inputArea.classList.remove("debate-floor-mode");
63
63
  var banner = document.getElementById("debate-floor-banner");
@@ -82,7 +82,7 @@ export function handleDebateConcludeSend() {
82
82
 
83
83
  export function showDebateEndedMode(msg) {
84
84
  removeDebateBottomBar();
85
- store.setState({ debateEndedMode: true });
85
+ store.set({ debateEndedMode: true });
86
86
  var inputArea = document.getElementById("input-area");
87
87
  if (inputArea) {
88
88
  inputArea.classList.add("debate-floor-mode");
@@ -122,7 +122,7 @@ export function showDebateEndedMode(msg) {
122
122
  }
123
123
 
124
124
  export function exitDebateEndedMode() {
125
- store.setState({ debateEndedMode: false });
125
+ store.set({ debateEndedMode: false });
126
126
  var inputArea = document.getElementById("input-area");
127
127
  if (inputArea) inputArea.classList.remove("debate-floor-mode");
128
128
  var banner = document.getElementById("debate-floor-banner");
@@ -146,7 +146,7 @@ function handleDebateEndedSend() {
146
146
 
147
147
  export function showDebateUserFloor(msg) {
148
148
  removeDebateBottomBar();
149
- store.setState({ debateFloorMode: true });
149
+ store.set({ debateFloorMode: true });
150
150
  var inputArea = document.getElementById("input-area");
151
151
  if (inputArea) {
152
152
  inputArea.classList.add("debate-floor-mode");
@@ -184,7 +184,7 @@ export function showDebateUserFloor(msg) {
184
184
  }
185
185
 
186
186
  export function exitDebateFloorMode() {
187
- store.setState({ debateFloorMode: false });
187
+ store.set({ debateFloorMode: false });
188
188
  var inputArea = document.getElementById("input-area");
189
189
  if (inputArea) inputArea.classList.remove("debate-floor-mode");
190
190
  var banner = document.getElementById("debate-floor-banner");
@@ -230,9 +230,9 @@ export function renderDebateUserFloorDone(msg) {
230
230
 
231
231
  export function showDebateSticky(phase, msg) {
232
232
  if (phase === "ended" || phase === "hide") {
233
- store.setState({ debateStickyState: null });
233
+ store.set({ debateStickyState: null });
234
234
  } else {
235
- store.setState({ debateStickyState: { phase: phase, msg: msg } });
235
+ store.set({ debateStickyState: { phase: phase, msg: msg } });
236
236
  }
237
237
 
238
238
  var stickyEl = document.getElementById("debate-sticky");
@@ -343,7 +343,7 @@ export function removeDebateBottomBar() {
343
343
  var handBar = document.getElementById("debate-hand-raise-bar");
344
344
  if (handBar) handBar.remove();
345
345
  debateHandRaiseOpen = false;
346
- var _ds = store.getState();
346
+ var _ds = store.snap();
347
347
  if (_ds.debateFloorMode) exitDebateFloorMode();
348
348
  if (_ds.debateConcludeMode) exitDebateConcludeMode();
349
349
  if (_ds.debateEndedMode) exitDebateEndedMode();