clay-server 2.26.0-beta.8 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { isSchedulerOpen, closeScheduler } from './scheduler.js';
6
6
  import { hideNotes } from './sticky-notes.js';
7
7
  import { openSearch as openSessionSearch } from './session-search.js';
8
8
 
9
- var getMateWs = null;
9
+ var getWs = null;
10
10
  var currentMateId = null;
11
11
  var currentMate = null;
12
12
  var cachedSessions = [];
@@ -27,8 +27,8 @@ var searchQuery = "";
27
27
  var searchMatchIds = null;
28
28
  var searchDebounce = null;
29
29
 
30
- export function initMateSidebar(mateWsGetter) {
31
- getMateWs = mateWsGetter;
30
+ export function initMateSidebar(wsGetter) {
31
+ getWs = wsGetter;
32
32
  columnEl = document.getElementById("mate-sidebar-column");
33
33
  listEl = document.getElementById("mate-session-list");
34
34
  avatarEl = document.getElementById("mate-sidebar-avatar");
@@ -37,7 +37,7 @@ export function initMateSidebar(mateWsGetter) {
37
37
 
38
38
  if (newSessionBtn) {
39
39
  newSessionBtn.addEventListener("click", function () {
40
- var ws = getMateWs ? getMateWs() : null;
40
+ var ws = getWs ? getWs() : null;
41
41
  if (ws && ws.readyState === 1) {
42
42
  ws.send(JSON.stringify({ type: "new_session" }));
43
43
  }
@@ -73,7 +73,7 @@ export function initMateSidebar(mateWsGetter) {
73
73
  return;
74
74
  }
75
75
  searchDebounce = setTimeout(function () {
76
- var ws = getMateWs ? getMateWs() : null;
76
+ var ws = getWs ? getWs() : null;
77
77
  if (ws && ws.readyState === 1) {
78
78
  ws.send(JSON.stringify({ type: "search_sessions", query: q }));
79
79
  }
@@ -316,6 +316,10 @@ function spawnSparks(cx, cy) {
316
316
  }
317
317
  }
318
318
 
319
+ export function getMateSessions() {
320
+ return cachedSessions;
321
+ }
322
+
319
323
  export function hideMateSidebar() {
320
324
  currentMateId = null;
321
325
  currentMate = null;
@@ -498,7 +502,7 @@ function renderDebateGroup(children, groupKey) {
498
502
  hideKnowledge();
499
503
  if (isSchedulerOpen()) closeScheduler();
500
504
  hideNotes();
501
- var ws = getMateWs ? getMateWs() : null;
505
+ var ws = getWs ? getWs() : null;
502
506
  if (ws && ws.readyState === 1) {
503
507
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
504
508
  }
@@ -534,7 +538,7 @@ function renderDebateGroup(children, groupKey) {
534
538
  hideKnowledge();
535
539
  if (isSchedulerOpen()) closeScheduler();
536
540
  hideNotes();
537
- var ws = getMateWs ? getMateWs() : null;
541
+ var ws = getWs ? getWs() : null;
538
542
  if (ws && ws.readyState === 1) {
539
543
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
540
544
  }
@@ -580,7 +584,7 @@ function renderMateSessionItem(s) {
580
584
  if (isSchedulerOpen()) closeScheduler();
581
585
  hideNotes();
582
586
  var pendingQuery = searchQuery || "";
583
- var ws = getMateWs ? getMateWs() : null;
587
+ var ws = getWs ? getWs() : null;
584
588
  if (ws && ws.readyState === 1) {
585
589
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
586
590
  }
@@ -642,7 +646,7 @@ function showMateSessionCtxMenu(anchorEl, sessionId, title) {
642
646
  overlay.querySelector(".mate-confirm-cancel").addEventListener("click", function () { overlay.remove(); });
643
647
  overlay.querySelector(".mate-confirm-delete").addEventListener("click", function () {
644
648
  overlay.remove();
645
- var ws = getMateWs ? getMateWs() : null;
649
+ var ws = getWs ? getWs() : null;
646
650
  if (ws && ws.readyState === 1) {
647
651
  ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
648
652
  }
@@ -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);
@@ -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" },
@@ -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)
@@ -352,6 +352,16 @@ function createXtermForTab(tab) {
352
352
 
353
353
  xterm.open(bodyEl);
354
354
 
355
+ // WebGL addon: pixel-perfect rendering (eliminates gaps in block characters)
356
+ // Must be loaded after xterm.open() so the rendering context is available.
357
+ if (typeof WebglAddon !== "undefined") {
358
+ try {
359
+ xterm.loadAddon(new WebglAddon.WebglAddon());
360
+ } catch (e) {
361
+ // WebGL not available, fall back to DOM renderer
362
+ }
363
+ }
364
+
355
365
  // Route input to server
356
366
  xterm.onData(function (data) {
357
367
  if (ctx.ws && ctx.connected) {
@@ -481,14 +491,14 @@ function renderTabBar() {
481
491
  startRenameTab(t, label);
482
492
  });
483
493
 
484
- var closeBtn = document.createElement("button");
485
- closeBtn.className = "terminal-tab-close";
486
- closeBtn.innerHTML = '<i data-lucide="trash-2" style="width:12px;height:12px"></i>';
487
- closeBtn.addEventListener("click", function (e) {
494
+ var moreBtn = document.createElement("button");
495
+ moreBtn.className = "terminal-tab-more";
496
+ moreBtn.innerHTML = '<i data-lucide="ellipsis" style="width:12px;height:12px"></i>';
497
+ moreBtn.addEventListener("click", function (e) {
488
498
  e.stopPropagation();
489
- closeTab(t.id);
499
+ showTabContextMenu(e, t, label);
490
500
  });
491
- el.appendChild(closeBtn);
501
+ el.appendChild(moreBtn);
492
502
 
493
503
  el.addEventListener("click", function () {
494
504
  if (t.id !== activeTabId) {
@@ -504,6 +514,52 @@ function renderTabBar() {
504
514
  refreshIcons();
505
515
  }
506
516
 
517
+ // --- Tab context menu (three-dot) ---
518
+ function showTabContextMenu(e, tab, labelEl) {
519
+ var existing = document.querySelector(".terminal-tab-ctx");
520
+ if (existing) existing.remove();
521
+
522
+ var menu = document.createElement("div");
523
+ menu.className = "terminal-tab-ctx";
524
+
525
+ var renameItem = document.createElement("button");
526
+ renameItem.className = "terminal-tab-ctx-item";
527
+ renameItem.innerHTML = '<i data-lucide="pencil" style="width:13px;height:13px"></i> Rename';
528
+ renameItem.addEventListener("click", function () {
529
+ menu.remove();
530
+ startRenameTab(tab, labelEl);
531
+ });
532
+ menu.appendChild(renameItem);
533
+
534
+ var closeItem = document.createElement("button");
535
+ closeItem.className = "terminal-tab-ctx-item terminal-tab-ctx-danger";
536
+ closeItem.innerHTML = '<i data-lucide="trash-2" style="width:13px;height:13px"></i> Close';
537
+ closeItem.addEventListener("click", function () {
538
+ menu.remove();
539
+ closeTab(tab.id);
540
+ });
541
+ menu.appendChild(closeItem);
542
+
543
+ document.body.appendChild(menu);
544
+ refreshIcons();
545
+
546
+ // Position near the button
547
+ var rect = e.currentTarget.getBoundingClientRect();
548
+ menu.style.top = (rect.bottom + 4) + "px";
549
+ menu.style.left = rect.left + "px";
550
+
551
+ // Dismiss on outside click
552
+ function dismiss(ev) {
553
+ if (!menu.contains(ev.target)) {
554
+ menu.remove();
555
+ document.removeEventListener("mousedown", dismiss, true);
556
+ }
557
+ }
558
+ setTimeout(function () {
559
+ document.addEventListener("mousedown", dismiss, true);
560
+ }, 0);
561
+ }
562
+
507
563
  // --- Rename tab inline ---
508
564
  function startRenameTab(tab, labelEl) {
509
565
  var input = document.createElement("input");
@@ -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,
@@ -1,9 +1,10 @@
1
1
  import { escapeHtml, copyToClipboard } from './utils.js';
2
- import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
4
  import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
5
5
  import { openFile } from './filebrowser.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
+ import { getChatLayout } from './theme.js';
7
8
 
8
9
  var ctx;
9
10
 
@@ -496,12 +497,17 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
496
497
  return;
497
498
  }
498
499
 
499
- // Mate DM: render as conversational chat bubble instead of formal dialog
500
- if (ctx.isMateDm && ctx.isMateDm()) {
501
- renderMatePermission(requestId, toolName, toolInput, mateId);
500
+ // Channel layout or Mate DM: conversational "Can I ...?" style
501
+ if ((ctx.isMateDm && ctx.isMateDm()) || getChatLayout() === "channel") {
502
+ renderConversationalPermission(requestId, toolName, toolInput, mateId);
502
503
  return;
503
504
  }
504
505
 
506
+ // Bubble layout: formal "Permission Required" dialog
507
+ renderFormalPermission(requestId, toolName, toolInput, decisionReason);
508
+ }
509
+
510
+ function renderFormalPermission(requestId, toolName, toolInput, decisionReason) {
505
511
  var container = document.createElement("div");
506
512
  container.className = "permission-container";
507
513
  container.dataset.requestId = requestId;
@@ -755,18 +761,40 @@ function matePermissionInfo(toolName, toolInput) {
755
761
  return { verb: verb, target: target };
756
762
  }
757
763
 
758
- function renderMatePermission(requestId, toolName, toolInput, mateId) {
759
- var mateName = ctx.getMateName();
760
- var mateAvatar = ctx.getMateAvatarUrl();
761
-
762
- // If mateId provided (e.g. @mention in DM), use that mate's info instead of DM target
764
+ function resolvePermissionIdentity(mateId) {
765
+ // Mate DM: use DM target mate info
766
+ if (ctx.isMateDm && ctx.isMateDm()) {
767
+ var name = ctx.getMateName();
768
+ var avatar = ctx.getMateAvatarUrl();
769
+ // Override if specific mateId provided (e.g. @mention)
770
+ if (mateId && ctx.getMateById) {
771
+ var mentionMate = ctx.getMateById(mateId);
772
+ if (mentionMate) {
773
+ name = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || name;
774
+ avatar = mateAvatarUrl(mentionMate, 36);
775
+ }
776
+ }
777
+ return { name: name, avatar: avatar };
778
+ }
779
+ // Channel with Mate mention
763
780
  if (mateId && ctx.getMateById) {
764
- var mentionMate = ctx.getMateById(mateId);
765
- if (mentionMate) {
766
- mateName = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || mateName;
767
- mateAvatar = mateAvatarUrl(mentionMate, 36);
781
+ var mate = ctx.getMateById(mateId);
782
+ if (mate) {
783
+ return {
784
+ name: (mate.profile && mate.profile.displayName) || mate.displayName || mate.name || "Mate",
785
+ avatar: mateAvatarUrl(mate, 36)
786
+ };
768
787
  }
769
788
  }
789
+ // Project chat (Claude Code)
790
+ return {
791
+ name: "Claude Code",
792
+ avatar: ctx.getClaudeAvatar ? ctx.getClaudeAvatar() : ""
793
+ };
794
+ }
795
+
796
+ function renderConversationalPermission(requestId, toolName, toolInput, mateId) {
797
+ var identity = resolvePermissionIdentity(mateId);
770
798
  var info = matePermissionInfo(toolName, toolInput);
771
799
  var askMsg = "Can I " + info.verb + (info.target ? " " + info.target : "") + "?";
772
800
 
@@ -777,7 +805,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
777
805
  // Avatar (left column)
778
806
  var avi = document.createElement("img");
779
807
  avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
780
- avi.src = mateAvatar;
808
+ avi.src = identity.avatar;
781
809
  avi.alt = "";
782
810
  container.appendChild(avi);
783
811
 
@@ -789,7 +817,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
789
817
  var headerRow = document.createElement("div");
790
818
  headerRow.className = "dm-bubble-header";
791
819
  headerRow.innerHTML =
792
- '<span class="dm-bubble-name">' + escapeHtml(mateName) + '</span>' +
820
+ '<span class="dm-bubble-name">' + escapeHtml(identity.name) + '</span>' +
793
821
  '<span class="dm-bubble-time">' + String(new Date().getHours()).padStart(2, "0") + ":" + String(new Date().getMinutes()).padStart(2, "0") + '</span>';
794
822
  content.appendChild(headerRow);
795
823
 
@@ -1409,12 +1437,11 @@ export function startThinking() {
1409
1437
  var el = thinkingGroup.el;
1410
1438
  el.classList.remove("done");
1411
1439
  el.querySelector(".thinking-content").textContent = "";
1412
- // Mate mode: restore sparkle activity row, hide thinking header
1440
+ // Mate mode: restore dots activity row, hide thinking header
1413
1441
  if (el.classList.contains("mate-thinking")) {
1414
1442
  var actRow = el.querySelector(".mate-thinking-activity");
1415
1443
  if (actRow) {
1416
1444
  actRow.style.display = "";
1417
- actRow.querySelector(".activity-text").textContent = randomThinkingVerb() + "...";
1418
1445
  }
1419
1446
  var header = el.querySelector(".thinking-header");
1420
1447
  if (header) header.style.display = "none";
@@ -1423,7 +1450,7 @@ export function startThinking() {
1423
1450
  refreshIcons();
1424
1451
  ctx.scrollToBottom();
1425
1452
  if (!el.classList.contains("mate-thinking")) {
1426
- ctx.setActivity(randomThinkingVerb() + "...");
1453
+ ctx.setActivity("thinking");
1427
1454
  }
1428
1455
  return;
1429
1456
  }
@@ -1439,10 +1466,7 @@ export function startThinking() {
1439
1466
  '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
1440
1467
  '<div class="dm-bubble-content">' +
1441
1468
  '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
1442
- '<div class="activity-inline mate-thinking-activity">' +
1443
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
1444
- '<span class="activity-text">' + randomThinkingVerb() + '...</span>' +
1445
- '</div>' +
1469
+ '<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
1446
1470
  '<div class="thinking-header" style="display:none">' +
1447
1471
  '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1448
1472
  '<span class="thinking-label">Thinking</span>' +
@@ -1472,7 +1496,7 @@ export function startThinking() {
1472
1496
  thinkingGroup = { el: el, count: 0, totalDuration: 0 };
1473
1497
  currentThinking = { el: el, fullText: "", startTime: Date.now() };
1474
1498
  if (!ctx.isMateDm()) {
1475
- ctx.setActivity(randomThinkingVerb() + "...");
1499
+ ctx.setActivity("thinking");
1476
1500
  }
1477
1501
  }
1478
1502