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
@@ -176,13 +176,42 @@ function selectMentionItem(idx) {
176
176
  hideMentionMenu();
177
177
  }
178
178
 
179
+ function ensureChipContrast(hex) {
180
+ if (!hex || hex.charAt(0) !== "#") return hex;
181
+ var r = parseInt(hex.substring(1, 3), 16);
182
+ var g = parseInt(hex.substring(3, 5), 16);
183
+ var b = parseInt(hex.substring(5, 7), 16);
184
+ // Relative luminance (sRGB)
185
+ var lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
186
+ var isDark = document.documentElement.classList.contains("dark") ||
187
+ document.body.classList.contains("dark-mode");
188
+ if (isDark) {
189
+ // Dark mode: lighten if too dark
190
+ return lum < 0.4 ? color_mix_lighten(r, g, b, 0.35) : hex;
191
+ }
192
+ // Light mode: darken if too bright
193
+ return lum > 0.55 ? color_mix_darken(r, g, b, 0.4) : hex;
194
+ }
195
+
196
+ function color_mix_darken(r, g, b, amount) {
197
+ var f = 1 - amount;
198
+ return "#" + [Math.round(r * f), Math.round(g * f), Math.round(b * f)]
199
+ .map(function (v) { return v.toString(16).padStart(2, "0"); }).join("");
200
+ }
201
+
202
+ function color_mix_lighten(r, g, b, amount) {
203
+ return "#" + [Math.round(r + (255 - r) * amount), Math.round(g + (255 - g) * amount), Math.round(b + (255 - b) * amount)]
204
+ .map(function (v) { return v.toString(16).padStart(2, "0"); }).join("");
205
+ }
206
+
179
207
  function showInputMentionChip(name, color, avatarSrc) {
180
208
  removeInputMentionChip();
209
+ var textColor = ensureChipContrast(color);
181
210
  var chip = document.createElement("div");
182
211
  chip.id = "input-mention-chip";
183
212
  chip.innerHTML =
184
213
  '<img class="input-mention-chip-avatar" src="' + escapeHtml(avatarSrc) + '" width="18" height="18" />' +
185
- '<span class="input-mention-chip-name" style="color:' + escapeHtml(color) + '">@' + escapeHtml(name) + '</span>' +
214
+ '<span class="input-mention-chip-name" style="color:' + escapeHtml(textColor) + '">@' + escapeHtml(name) + '</span>' +
186
215
  '<button class="input-mention-chip-remove" type="button" aria-label="Remove mention">&times;</button>';
187
216
  chip.style.setProperty("--chip-color", color);
188
217
 
@@ -277,7 +306,16 @@ export function sendMention(mateId, text, pastes, images) {
277
306
 
278
307
  // Recreate the mention block if it was lost (e.g. session switch)
279
308
  function ensureMentionBlock() {
280
- if (currentMentionEl && currentMentionEl.parentNode) return; // still in DOM
309
+ if (currentMentionEl && currentMentionEl.parentNode) {
310
+ // If other elements (e.g. permission requests) were added after the mention
311
+ // block, move it to the bottom to maintain chronological order.
312
+ var parent = currentMentionEl.parentNode;
313
+ if (parent.lastElementChild !== currentMentionEl) {
314
+ parent.appendChild(currentMentionEl);
315
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
316
+ }
317
+ return;
318
+ }
281
319
  if (!activeMentionMeta) return;
282
320
  // Recreate from saved meta
283
321
  handleMentionStart(activeMentionMeta);
@@ -1,4 +1,4 @@
1
- import { copyToClipboard } from './utils.js';
1
+ import { copyToClipboard, showToast } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
 
4
4
  var ctx;
@@ -155,6 +155,114 @@ export function initNotifications(_ctx) {
155
155
  });
156
156
  })();
157
157
 
158
+ // --- Extension pill popover ---
159
+ (function () {
160
+ var extPillWrap = $("ext-pill-wrap");
161
+ var extPillBtn = $("ext-pill");
162
+ var extPopover = $("ext-popover");
163
+ var extDownloadBtn = $("ext-download-btn");
164
+ var extDownloadStatus = $("ext-download-status");
165
+ var extCopyUrl = $("ext-copy-url");
166
+ if (!extPillWrap || !extPillBtn || !extPopover) return;
167
+
168
+ // Detect extension connection via postMessage from content.js
169
+ var extConnected = false;
170
+ var connectedBanner = $("ext-connected-banner");
171
+ var extDesc = $("ext-popover-desc");
172
+ var extDivider = $("ext-popover-divider");
173
+ var extGuideTitle = $("ext-popover-guide-title");
174
+ var extSteps = extPopover.querySelector(".ext-popover-steps");
175
+
176
+ function setExtConnected() {
177
+ if (extConnected) return;
178
+ extConnected = true;
179
+ extPillBtn.classList.add("ext-connected");
180
+ extPillBtn.innerHTML = '<i data-lucide="check"></i> Extension';
181
+ refreshIcons(extPillBtn);
182
+ if (connectedBanner) connectedBanner.classList.remove("hidden");
183
+ if (extDownloadBtn) extDownloadBtn.style.display = "none";
184
+ if (extDownloadStatus) extDownloadStatus.classList.add("hidden");
185
+ if (extDesc) extDesc.style.display = "none";
186
+ if (extDivider) extDivider.style.display = "none";
187
+ if (extGuideTitle) extGuideTitle.style.display = "none";
188
+ if (extSteps) extSteps.style.display = "none";
189
+ }
190
+
191
+ window.addEventListener("message", function (event) {
192
+ if (event.source !== window) return;
193
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
194
+ setExtConnected();
195
+ });
196
+
197
+ // Toggle popover
198
+ extPillBtn.addEventListener("click", function (e) {
199
+ e.stopPropagation();
200
+ extPopover.classList.toggle("visible");
201
+ refreshIcons(extPopover);
202
+ });
203
+
204
+ document.addEventListener("click", function (e) {
205
+ if (!extPopover.contains(e.target) && e.target !== extPillBtn && !extPillBtn.contains(e.target)) {
206
+ extPopover.classList.remove("visible");
207
+ }
208
+ });
209
+
210
+ // Download button
211
+ if (extDownloadBtn) {
212
+ extDownloadBtn.addEventListener("click", function (e) {
213
+ e.stopPropagation();
214
+ extDownloadBtn.disabled = true;
215
+ extDownloadBtn.innerHTML = iconHtml("loader") + " Downloading...";
216
+ refreshIcons(extDownloadBtn);
217
+ var loaderIcon = extDownloadBtn.querySelector(".lucide");
218
+ if (loaderIcon) loaderIcon.style.animation = "spin 1s linear infinite";
219
+ if (extDownloadStatus) {
220
+ extDownloadStatus.classList.remove("hidden");
221
+ extDownloadStatus.textContent = "Fetching from GitHub...";
222
+ extDownloadStatus.style.color = "";
223
+ }
224
+ fetch("/api/extension/download").then(function (resp) {
225
+ if (!resp.ok) throw new Error("Download failed (" + resp.status + ")");
226
+ return resp.blob();
227
+ }).then(function (blob) {
228
+ var url = URL.createObjectURL(blob);
229
+ var a = document.createElement("a");
230
+ a.href = url;
231
+ a.download = "clay-chrome-extension.zip";
232
+ document.body.appendChild(a);
233
+ a.click();
234
+ document.body.removeChild(a);
235
+ URL.revokeObjectURL(url);
236
+ if (extDownloadStatus) {
237
+ extDownloadStatus.textContent = "Download complete!";
238
+ extDownloadStatus.style.color = "var(--accent)";
239
+ }
240
+ showToast("Extension downloaded");
241
+ }).catch(function (err) {
242
+ if (extDownloadStatus) {
243
+ extDownloadStatus.textContent = "Failed: " + err.message;
244
+ extDownloadStatus.style.color = "var(--danger, #e53935)";
245
+ }
246
+ showToast("Download failed");
247
+ }).finally(function () {
248
+ extDownloadBtn.disabled = false;
249
+ extDownloadBtn.innerHTML = iconHtml("download") + " Download Extension (.zip)";
250
+ refreshIcons(extDownloadBtn);
251
+ });
252
+ });
253
+ }
254
+
255
+ // Copy chrome://extensions URL
256
+ if (extCopyUrl) {
257
+ extCopyUrl.addEventListener("click", function (e) {
258
+ e.stopPropagation();
259
+ copyToClipboard("chrome://extensions").then(function () {
260
+ showToast("Copied chrome://extensions");
261
+ });
262
+ });
263
+ }
264
+ })();
265
+
158
266
  // --- Settings: Check for updates ---
159
267
  (function () {
160
268
  var settingsUpdateCheck = $("settings-update-check");
@@ -4,6 +4,8 @@ import { iconHtml, refreshIcons } from './icons.js';
4
4
  var ctx;
5
5
  var rewindMode = false;
6
6
  var pendingRewindUuid = null;
7
+ var rewindPreviewInFlight = false;
8
+ var rewindExecuting = false;
7
9
  var rewindBannerEl = null;
8
10
  var rewindScrollHandler = null;
9
11
  var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
@@ -50,6 +52,17 @@ export function clearPendingRewindUuid() {
50
52
  pendingRewindUuid = null;
51
53
  }
52
54
 
55
+ export function onRewindComplete() {
56
+ rewindExecuting = false;
57
+ rewindPreviewInFlight = false;
58
+ pendingRewindUuid = null;
59
+ }
60
+
61
+ export function onRewindError() {
62
+ rewindExecuting = false;
63
+ rewindPreviewInFlight = false;
64
+ }
65
+
53
66
  function initiateRewind(uuid) {
54
67
  if (ctx.processing) {
55
68
  ctx.addSystemMessage("Cannot rewind while processing. Stop the current operation first.", true);
@@ -59,7 +72,16 @@ function initiateRewind(uuid) {
59
72
  ctx.addSystemMessage("No rewind point found for this turn.", true);
60
73
  return;
61
74
  }
75
+ if (rewindPreviewInFlight) {
76
+ // Debounce: ignore clicks while a preview is already in flight
77
+ return;
78
+ }
79
+ if (rewindExecuting) {
80
+ ctx.addSystemMessage("Rewind already in progress.", true);
81
+ return;
82
+ }
62
83
  pendingRewindUuid = uuid;
84
+ rewindPreviewInFlight = true;
63
85
  if (ctx.ws && ctx.connected) {
64
86
  ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
65
87
  }
@@ -98,6 +120,17 @@ function updateSummaryForMode() {
98
120
  }
99
121
 
100
122
  export function showRewindModal(data) {
123
+ rewindPreviewInFlight = false;
124
+
125
+ // Ignore stale preview results that don't match current pending UUID
126
+ if (data.uuid && pendingRewindUuid && data.uuid !== pendingRewindUuid) {
127
+ return;
128
+ }
129
+ // Ignore if a rewind is already executing
130
+ if (rewindExecuting) {
131
+ return;
132
+ }
133
+
101
134
  var p = data.preview || data;
102
135
  var filePaths = p.filesChanged || p.filePaths || p.files || [];
103
136
  var fileCount = filePaths.length;
@@ -171,6 +204,7 @@ export function showRewindModal(data) {
171
204
  export function hideRewindModal() {
172
205
  rewindModal.classList.add("hidden");
173
206
  pendingRewindUuid = null;
207
+ rewindPreviewInFlight = false;
174
208
  }
175
209
 
176
210
  export function renderDiffPre(text) {
@@ -333,7 +367,9 @@ export function initRewind(_ctx) {
333
367
  });
334
368
 
335
369
  rewindConfirmBtn.addEventListener("click", function() {
370
+ if (rewindExecuting) return;
336
371
  if (pendingRewindUuid && ctx.ws && ctx.connected) {
372
+ rewindExecuting = true;
337
373
  var mode = getSelectedMode();
338
374
  ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
339
375
  }
@@ -10,6 +10,7 @@ import { closeArchive } from './sticky-notes.js';
10
10
  import { closeScheduler } from './scheduler.js';
11
11
  import { openSearch as openSessionSearch } from './session-search.js';
12
12
  import { openCommandPalette } from './command-palette.js';
13
+ import { getMateSessions } from './mate-sidebar.js';
13
14
 
14
15
  var ctx;
15
16
 
@@ -351,6 +352,7 @@ function renderLoopGroup(loopId, children, groupKey) {
351
352
 
352
353
  var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
353
354
  var isRalph = children[0].loop && children[0].loop.source === "ralph";
355
+ var isDebate = children[0].loop && children[0].loop.source === "debate";
354
356
  var isCrafting = false;
355
357
  for (var j = 0; j < children.length; j++) {
356
358
  if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
@@ -363,7 +365,10 @@ function renderLoopGroup(loopId, children, groupKey) {
363
365
 
364
366
  // Group header row
365
367
  var el = document.createElement("div");
366
- el.className = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
368
+ var groupClass = "session-loop-group" + (hasActive ? " active" : "") + (expanded ? " expanded" : "");
369
+ if (isDebate) groupClass += " debate";
370
+ else if (!isRalph) groupClass += " scheduled";
371
+ el.className = groupClass;
367
372
  el.dataset.loopId = loopId;
368
373
 
369
374
  var chevron = document.createElement("button");
@@ -388,14 +393,16 @@ function renderLoopGroup(loopId, children, groupKey) {
388
393
  if (anyProcessing) {
389
394
  textHtml += '<span class="session-processing"></span>';
390
395
  }
391
- var groupIcon = isRalph ? "repeat" : "calendar-clock";
392
- textHtml += '<span class="session-loop-icon' + (isRalph ? "" : " scheduled") + '">' + iconHtml(groupIcon) + '</span>';
396
+ var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
397
+ var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
398
+ textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
393
399
  textHtml += '<span class="session-loop-name">' + escapeHtml(loopName) + '</span>';
394
400
  if (isCrafting && children.length === 1) {
395
401
  textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
396
402
  } else {
397
403
  var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
398
- textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + countLabel + '</span>';
404
+ var countClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
405
+ textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
399
406
  }
400
407
  textSpan.innerHTML = textHtml;
401
408
  el.appendChild(textSpan);
@@ -538,6 +545,9 @@ function renderSessionItem(s) {
538
545
  if (s.isProcessing) {
539
546
  textHtml += '<span class="session-processing"></span>';
540
547
  }
548
+ if (s.loop && s.loop.source === "debate") {
549
+ textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
550
+ }
541
551
  if (ctx.multiUser && s.sessionVisibility === "private") {
542
552
  textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
543
553
  }
@@ -598,8 +608,8 @@ export function renderSessionList(sessions) {
598
608
  var normalSessions = [];
599
609
  for (var i = 0; i < cachedSessions.length; i++) {
600
610
  var s = cachedSessions[i];
601
- if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
602
- // Task crafting sessions live in the scheduler calendar, not the main list
611
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
612
+ // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
603
613
  continue;
604
614
  } else if (s.loop && s.loop.loopId) {
605
615
  var startedAt = s.loop.startedAt || 0;
@@ -923,7 +933,8 @@ function renderSheetSessions(listEl) {
923
933
  (function (p) {
924
934
  var chip = document.createElement("button");
925
935
  chip.className = "mobile-chat-chip";
926
- if (p.slug === cachedCurrentSlug) chip.classList.add("active");
936
+ var isDmActive = document.body.classList.contains("mate-dm-active");
937
+ if (p.slug === cachedCurrentSlug && !isDmActive) chip.classList.add("active");
927
938
  chip.dataset.type = "project";
928
939
  chip.dataset.slug = p.slug;
929
940
 
@@ -975,6 +986,7 @@ function renderSheetSessions(listEl) {
975
986
  var mp = mate.profile || {};
976
987
  var chip = document.createElement("button");
977
988
  chip.className = "mobile-chat-chip";
989
+ if (currentDmUserId === mate.id) chip.classList.add("active");
978
990
  chip.dataset.type = "mate";
979
991
  chip.dataset.mateId = mate.id;
980
992
 
@@ -1029,10 +1041,9 @@ function renderSheetSessions(listEl) {
1029
1041
  if (type === "project") {
1030
1042
  renderMobileSessionsInto(sessionListEl);
1031
1043
  } else if (type === "mate") {
1032
- // Mate DM: just open the DM
1044
+ // Mate DM: open the DM and show mate actions
1033
1045
  if (ctx.openDm) ctx.openDm(mateId);
1034
- closeMobileSheet();
1035
- return;
1046
+ renderMateMobileActions(sessionListEl);
1036
1047
  }
1037
1048
 
1038
1049
  refreshIcons();
@@ -1051,15 +1062,23 @@ function renderSheetSessions(listEl) {
1051
1062
  var type = chip.dataset.type;
1052
1063
  if (type === "project") {
1053
1064
  var slug = chip.dataset.slug;
1054
- if (slug !== cachedCurrentSlug) {
1055
- // Switch project, show loading, keep sheet open
1065
+ var isDmNow = !!currentDmUserId;
1066
+ if (slug !== cachedCurrentSlug || isDmNow) {
1067
+ // Switch project (or exit DM back to same project)
1056
1068
  sessionListEl.innerHTML = "";
1057
- var loading = document.createElement("div");
1058
- loading.className = "mobile-chat-context-note";
1059
- loading.textContent = "Loading sessions...";
1060
- sessionListEl.appendChild(loading);
1069
+ if (slug !== cachedCurrentSlug) {
1070
+ var loading = document.createElement("div");
1071
+ loading.className = "mobile-chat-context-note";
1072
+ loading.textContent = "Loading sessions...";
1073
+ sessionListEl.appendChild(loading);
1074
+ }
1061
1075
  if (ctx.switchProject) ctx.switchProject(slug);
1062
- // renderSessionList will be called by WS, which calls refreshMobileChatSheet
1076
+ if (!isDmNow || slug !== cachedCurrentSlug) {
1077
+ // renderSessionList will be called by WS, which calls refreshMobileChatSheet
1078
+ } else {
1079
+ // Exited DM, same project - render sessions now
1080
+ renderSessionsForContext("project", slug, null);
1081
+ }
1063
1082
  } else {
1064
1083
  renderSessionsForContext("project", slug, null);
1065
1084
  }
@@ -1073,8 +1092,12 @@ function renderSheetSessions(listEl) {
1073
1092
  // Track that chat sheet is open
1074
1093
  mobileChatSheetOpen = true;
1075
1094
 
1076
- // --- Initial render: current project sessions ---
1077
- renderSessionsForContext("project", cachedCurrentSlug, null);
1095
+ // --- Initial render: show mate actions if DM active, otherwise project sessions ---
1096
+ if (currentDmUserId) {
1097
+ renderSessionsForContext("mate", null, currentDmUserId);
1098
+ } else {
1099
+ renderSessionsForContext("project", cachedCurrentSlug, null);
1100
+ }
1078
1101
  }
1079
1102
 
1080
1103
  // Helper: create a mobile session item element
@@ -1350,6 +1373,54 @@ function createMobileLoopGroup(loopId, children, groupKey) {
1350
1373
  return wrapper;
1351
1374
  }
1352
1375
 
1376
+ function renderMateMobileActions(container) {
1377
+ var newSessionBtn = document.createElement("button");
1378
+ newSessionBtn.className = "mobile-session-new";
1379
+ newSessionBtn.innerHTML = '<i data-lucide="plus" style="width:16px;height:16px"></i> New session';
1380
+ newSessionBtn.addEventListener("click", function () {
1381
+ if (ctx.ws && ctx.connected) {
1382
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
1383
+ }
1384
+ closeMobileSheet();
1385
+ });
1386
+ container.appendChild(newSessionBtn);
1387
+
1388
+ var debateBtn = document.createElement("button");
1389
+ debateBtn.className = "mobile-session-new";
1390
+ debateBtn.innerHTML = '<i data-lucide="mic" style="width:16px;height:16px"></i> New debate';
1391
+ debateBtn.addEventListener("click", function () {
1392
+ closeMobileSheet();
1393
+ var targetBtn = document.getElementById("mate-debate-btn");
1394
+ if (targetBtn) setTimeout(function () { targetBtn.click(); }, 250);
1395
+ });
1396
+ container.appendChild(debateBtn);
1397
+
1398
+ // Render mate session list
1399
+ var mateSessions = getMateSessions();
1400
+ if (mateSessions.length > 0) {
1401
+ var sorted = mateSessions.slice().sort(function (a, b) {
1402
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
1403
+ });
1404
+
1405
+ var currentGroup = "";
1406
+ for (var i = 0; i < sorted.length; i++) {
1407
+ var s = sorted[i];
1408
+ var group = getDateGroup(s.lastActivity || 0);
1409
+ if (group !== currentGroup) {
1410
+ currentGroup = group;
1411
+ var header = document.createElement("div");
1412
+ header.className = "mobile-sheet-group";
1413
+ header.textContent = group;
1414
+ container.appendChild(header);
1415
+ }
1416
+ var mateItem = createMobileSessionItem(s);
1417
+ container.appendChild(mateItem);
1418
+ }
1419
+ }
1420
+
1421
+ refreshIcons();
1422
+ }
1423
+
1353
1424
  // Helper: render sorted sessions into a container with date groups (with loop session grouping)
1354
1425
  function renderMobileSessionsInto(container) {
1355
1426
  var newBtn = document.createElement("button");
@@ -1368,7 +1439,7 @@ function renderMobileSessionsInto(container) {
1368
1439
  var normalSessions = [];
1369
1440
  for (var i = 0; i < cachedSessions.length; i++) {
1370
1441
  var s = cachedSessions[i];
1371
- if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
1442
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1372
1443
  continue;
1373
1444
  } else if (s.loop && s.loop.loopId) {
1374
1445
  var startedAt = s.loop.startedAt || 0;
@@ -1424,7 +1495,7 @@ function renderMobileSessionsInto(container) {
1424
1495
  }
1425
1496
 
1426
1497
  // Refresh mobile chat sheet when session data updates (called from renderSessionList)
1427
- function refreshMobileChatSheet() {
1498
+ export function refreshMobileChatSheet() {
1428
1499
  if (!mobileChatSheetOpen) return;
1429
1500
  var sheet = document.getElementById("mobile-sheet");
1430
1501
  if (!sheet || sheet.classList.contains("hidden")) {
@@ -1441,7 +1512,10 @@ function refreshMobileChatSheet() {
1441
1512
  chip.classList.remove("active");
1442
1513
 
1443
1514
  // Update active state
1444
- if (chip.dataset.type === "project" && chip.dataset.slug === cachedCurrentSlug) {
1515
+ var isDmActive = !!currentDmUserId;
1516
+ if (chip.dataset.type === "project" && chip.dataset.slug === cachedCurrentSlug && !isDmActive) {
1517
+ chip.classList.add("active");
1518
+ } else if (chip.dataset.type === "mate" && chip.dataset.mateId === currentDmUserId) {
1445
1519
  chip.classList.add("active");
1446
1520
  }
1447
1521
 
@@ -1461,9 +1535,13 @@ function refreshMobileChatSheet() {
1461
1535
  }
1462
1536
  }
1463
1537
 
1464
- // Re-render sessions for current project
1538
+ // Re-render sessions for current context
1465
1539
  sessionListEl.innerHTML = "";
1466
- renderMobileSessionsInto(sessionListEl);
1540
+ if (currentDmUserId) {
1541
+ renderMateMobileActions(sessionListEl);
1542
+ } else {
1543
+ renderMobileSessionsInto(sessionListEl);
1544
+ }
1467
1545
 
1468
1546
  refreshIcons();
1469
1547
  }
@@ -1619,8 +1697,7 @@ function renderSheetTools(listEl) {
1619
1697
  { icon: "book-open", label: "Knowledge", action: "mate-knowledge" },
1620
1698
  { icon: "sticky-note", label: "Sticky Notes", action: "mate-sticky" },
1621
1699
  { icon: "puzzle", label: "Skills", action: "mate-skills" },
1622
- { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" },
1623
- { icon: "mic", label: "Debate", action: "mate-debate" }
1700
+ { icon: "calendar-clock", label: "Scheduled Tasks", action: "mate-scheduler" }
1624
1701
  ] : [
1625
1702
  { icon: "folder-tree", label: "Files", action: "files" },
1626
1703
  { icon: "square-terminal", label: "Terminal", action: "terminal" },
@@ -3946,8 +4023,8 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
3946
4023
  // All other users
3947
4024
  var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
3948
4025
 
3949
- // Hide section if no other users (single-user mode or alone)
3950
- if (allOthers.length === 0) {
4026
+ // Hide section if no other users and no mates
4027
+ if (allOthers.length === 0 && cachedMates.length === 0) {
3951
4028
  container.innerHTML = "";
3952
4029
  container.classList.add("hidden");
3953
4030
  return;
@@ -4062,6 +4139,8 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
4062
4139
  avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
4063
4140
  avatar.src = mateAvatarUrl(mate, 34);
4064
4141
  avatar.alt = mp.displayName || mate.name || "Mate";
4142
+ var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
4143
+ avatar.style.background = mateColor + "30";
4065
4144
  el.appendChild(avatar);
4066
4145
 
4067
4146
  // Processing status dot (IO blink)
@@ -653,6 +653,14 @@ export function handleTermOutput(msg) {
653
653
  }
654
654
  }
655
655
 
656
+ export function handleTermResized(msg) {
657
+ if (!msg.id) return;
658
+ var tab = tabs.get(msg.id);
659
+ if (tab && tab.xterm && msg.cols > 0 && msg.rows > 0) {
660
+ tab.xterm.resize(msg.cols, msg.rows);
661
+ }
662
+ }
663
+
656
664
  export function handleTermExited(msg) {
657
665
  if (!msg.id) return;
658
666
  var tab = tabs.get(msg.id);
@@ -155,7 +155,8 @@ function computeVars(theme) {
155
155
  "--code-bg": isLight ? darken(b.base00, 0.03) : darken(b.base00, 0.15),
156
156
  "--border": b.base02,
157
157
  "--border-subtle": mixColors(b.base00, b.base02, 0.6),
158
- "--input-bg": mixColors(b.base01, b.base02, 0.5),
158
+ "--input-bg": isLight ? darken(b.base00, 0.04) : mixColors(b.base01, b.base02, 0.5),
159
+ "--ask-mate-bg": isLight ? mixColors("#ffffff", darken(b.base00, 0.04), 0.6) : mixColors(b.base00, mixColors(b.base01, b.base02, 0.5), 0.6),
159
160
  "--user-bubble": isLight ? darken(b.base01, 0.03) : mixColors(b.base01, b.base02, 0.3),
160
161
  "--error": b.base08,
161
162
  "--success": b.base0B,