clay-server 2.26.0-beta.7 → 2.26.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/daemon.js CHANGED
@@ -807,7 +807,7 @@ var relay = createServer({
807
807
  var isMateProject = slug.indexOf("mate-") === 0;
808
808
  return {
809
809
  slug: slug,
810
- visibility: isMateProject ? "private" : (config.projects[i].visibility || "public"),
810
+ visibility: isMateProject ? "private" : (config.projects[i].visibility || (config.osUsers ? "private" : "public")),
811
811
  allowedUsers: config.projects[i].allowedUsers || [],
812
812
  ownerId: config.projects[i].ownerId || null,
813
813
  };
package/lib/project.js CHANGED
@@ -3884,6 +3884,26 @@ function createProjectContext(opts) {
3884
3884
  return;
3885
3885
  }
3886
3886
 
3887
+ if (msg.type === "send_scheduled_now") {
3888
+ var nowSession = getSessionForWs(ws);
3889
+ if (!nowSession || !nowSession.scheduledMessage) return;
3890
+ var schedText = nowSession.scheduledMessage.text;
3891
+ clearTimeout(nowSession.scheduledMessage.timer);
3892
+ nowSession.scheduledMessage = null;
3893
+ console.log("[project] Scheduled message sent immediately for session " + nowSession.localId);
3894
+ sm.sendAndRecord(nowSession, { type: "scheduled_message_sent" });
3895
+ var userMsg = { type: "user_message", text: schedText };
3896
+ nowSession.history.push(userMsg);
3897
+ sm.appendToSessionFile(nowSession, userMsg);
3898
+ sendToSession(nowSession.localId, userMsg);
3899
+ nowSession.isProcessing = true;
3900
+ onProcessingChanged();
3901
+ sendToSession(nowSession.localId, { type: "status", status: "processing" });
3902
+ sdk.startQuery(nowSession, schedText, null, getLinuxUserForSession(nowSession));
3903
+ sm.broadcastSessionList();
3904
+ return;
3905
+ }
3906
+
3887
3907
  if (msg.type !== "message") return;
3888
3908
  if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
3889
3909
 
@@ -4418,7 +4438,7 @@ function createProjectContext(opts) {
4418
4438
  return;
4419
4439
  }
4420
4440
  // Sanitize filename — strip path separators
4421
- var safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._\-\(\)\[\] ]/g, "_");
4441
+ var safeName = path.basename(fileName).replace(/[\x00-\x1f\/\\:*?"<>|]/g, "_");
4422
4442
  if (!safeName) safeName = "upload";
4423
4443
 
4424
4444
  // Check size
package/lib/public/app.js CHANGED
@@ -1,6 +1,6 @@
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
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';
6
6
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
@@ -14,7 +14,7 @@ import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFs
14
14
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
15
15
  import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
16
16
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
17
- import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
17
+ import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme, getChatLayout } from './modules/theme.js';
18
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';
19
19
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
20
20
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
@@ -2083,12 +2083,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2083
2083
  activityEl = document.createElement("div");
2084
2084
  activityEl.className = "activity-inline";
2085
2085
  activityEl.innerHTML =
2086
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2087
- '<span class="activity-text"></span>';
2086
+ '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>';
2088
2087
  addToMessages(activityEl);
2089
- refreshIcons();
2090
2088
  }
2091
- activityEl.querySelector(".activity-text").textContent = text;
2092
2089
  scrollToBottom();
2093
2090
  } else {
2094
2091
  if (activityEl) {
@@ -2098,9 +2095,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2098
2095
  }
2099
2096
  }
2100
2097
 
2101
- // --- Mate pre-thinking (instant dots before server responds) ---
2098
+ // --- Pre-thinking (instant dots before server responds) ---
2102
2099
  var matePreThinkingEl = null;
2103
2100
  var matePreThinkingTimer = null;
2101
+ function showClaudePreThinking() {
2102
+ if (getChatLayout() !== "channel") return;
2103
+ var claudeAvatar = CLAUDE_CODE_AVATAR;
2104
+ doShowMatePreThinking("Claude Code", claudeAvatar);
2105
+ }
2104
2106
  function showMatePreThinking() {
2105
2107
  removeMatePreThinking();
2106
2108
  var mateName = dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate";
@@ -2114,10 +2116,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2114
2116
  '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="" style="display:block">' +
2115
2117
  '<div class="dm-bubble-content">' +
2116
2118
  '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
2117
- '<div class="activity-inline mate-pre-activity">' +
2118
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2119
- '<span class="activity-text">' + randomThinkingVerb() + '...</span>' +
2120
- '</div>' +
2119
+ '<div class="mate-thinking-dots"><span></span><span></span><span></span></div>' +
2121
2120
  '</div>';
2122
2121
  if (activityEl && activityEl.parentNode) {
2123
2122
  activityEl.parentNode.insertBefore(matePreThinkingEl, activityEl);
@@ -3020,6 +3019,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3020
3019
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
3021
3020
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
3022
3021
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
3022
+ getClaudeAvatar: function () { return CLAUDE_CODE_AVATAR; },
3023
3023
  getMateById: function (id) {
3024
3024
  if (!id || !cachedMatesList) return null;
3025
3025
  for (var i = 0; i < cachedMatesList.length; i++) {
@@ -3716,42 +3716,134 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3716
3716
 
3717
3717
  function addScheduledMessageBubble(text, resetsAt) {
3718
3718
  removeScheduledMessageBubble();
3719
+ var isChannel = document.body.classList.contains("wide-view");
3719
3720
  var wrap = document.createElement("div");
3720
3721
  wrap.className = "msg-user scheduled-msg-wrap";
3721
3722
  wrap.id = "scheduled-msg-bubble";
3722
3723
 
3723
- var bubble = document.createElement("div");
3724
- bubble.className = "bubble scheduled-msg-bubble";
3724
+ var countdownEl;
3725
+ var cancelBtn;
3725
3726
 
3726
- var textEl = document.createElement("span");
3727
- textEl.textContent = text;
3728
- bubble.appendChild(textEl);
3727
+ if (isChannel) {
3728
+ // Channel mode: avatar + header with scheduled badge + message
3729
+ var _me = cachedAllUsers.find(function (u) { return u.id === myUserId; });
3730
+ if (!_me) { try { _me = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {} }
3731
+ var _myName = document.body.dataset.myDisplayName || (_me && (_me.displayName || _me.username)) || "Me";
3729
3732
 
3730
- var metaEl = document.createElement("div");
3731
- metaEl.className = "scheduled-msg-meta";
3733
+ var avi = document.createElement("img");
3734
+ avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
3735
+ avi.src = document.body.dataset.myAvatarUrl || userAvatarUrl(_me || { id: myUserId }, 36);
3736
+ wrap.appendChild(avi);
3732
3737
 
3733
- var clockIcon = document.createElement("span");
3734
- clockIcon.className = "scheduled-msg-icon";
3735
- clockIcon.innerHTML = iconHtml("clock");
3736
- metaEl.appendChild(clockIcon);
3738
+ var content = document.createElement("div");
3739
+ content.className = "dm-bubble-content";
3737
3740
 
3738
- var countdownEl = document.createElement("span");
3739
- countdownEl.className = "scheduled-msg-countdown";
3740
- metaEl.appendChild(countdownEl);
3741
+ var header = document.createElement("div");
3742
+ header.className = "dm-bubble-header";
3741
3743
 
3742
- var cancelBtn = document.createElement("button");
3743
- cancelBtn.className = "scheduled-msg-cancel";
3744
- cancelBtn.title = "Cancel scheduled message";
3745
- cancelBtn.textContent = "\u00d7";
3746
- cancelBtn.addEventListener("click", function () {
3747
- if (ws && ws.readyState === 1) {
3748
- ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3749
- }
3750
- });
3751
- metaEl.appendChild(cancelBtn);
3744
+ var nameSpan = document.createElement("span");
3745
+ nameSpan.className = "dm-bubble-name";
3746
+ nameSpan.textContent = _myName;
3747
+ header.appendChild(nameSpan);
3748
+
3749
+ var badge = document.createElement("span");
3750
+ badge.className = "scheduled-msg-badge";
3751
+ badge.innerHTML = iconHtml("clock");
3752
+ countdownEl = document.createElement("span");
3753
+ countdownEl.className = "scheduled-msg-countdown";
3754
+ badge.appendChild(countdownEl);
3755
+ header.appendChild(badge);
3756
+
3757
+ var actions = document.createElement("span");
3758
+ actions.className = "scheduled-msg-actions";
3759
+
3760
+ var sendNowBtn = document.createElement("button");
3761
+ sendNowBtn.className = "scheduled-msg-send-now";
3762
+ sendNowBtn.textContent = "Send now";
3763
+ sendNowBtn.addEventListener("click", function () {
3764
+ if (ws && ws.readyState === 1) {
3765
+ ws.send(JSON.stringify({ type: "send_scheduled_now" }));
3766
+ }
3767
+ });
3768
+ actions.appendChild(sendNowBtn);
3769
+
3770
+ var sep = document.createElement("span");
3771
+ sep.className = "scheduled-msg-sep";
3772
+ sep.textContent = "\u00b7";
3773
+ actions.appendChild(sep);
3774
+
3775
+ cancelBtn = document.createElement("button");
3776
+ cancelBtn.className = "scheduled-msg-cancel";
3777
+ cancelBtn.textContent = "Cancel";
3778
+ cancelBtn.addEventListener("click", function () {
3779
+ if (ws && ws.readyState === 1) {
3780
+ ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3781
+ }
3782
+ });
3783
+ actions.appendChild(cancelBtn);
3784
+
3785
+ header.appendChild(actions);
3786
+
3787
+ content.appendChild(header);
3788
+
3789
+ var bubble = document.createElement("div");
3790
+ bubble.className = "bubble scheduled-msg-bubble";
3791
+ var textEl = document.createElement("span");
3792
+ textEl.textContent = text;
3793
+ bubble.appendChild(textEl);
3794
+ content.appendChild(bubble);
3795
+
3796
+ wrap.appendChild(content);
3797
+ } else {
3798
+ // Bubble mode: original layout
3799
+ var bubble = document.createElement("div");
3800
+ bubble.className = "bubble scheduled-msg-bubble";
3801
+
3802
+ var textEl = document.createElement("span");
3803
+ textEl.textContent = text;
3804
+ bubble.appendChild(textEl);
3805
+
3806
+ var metaEl = document.createElement("div");
3807
+ metaEl.className = "scheduled-msg-meta";
3808
+
3809
+ var clockIcon = document.createElement("span");
3810
+ clockIcon.className = "scheduled-msg-icon";
3811
+ clockIcon.innerHTML = iconHtml("clock");
3812
+ metaEl.appendChild(clockIcon);
3813
+
3814
+ countdownEl = document.createElement("span");
3815
+ countdownEl.className = "scheduled-msg-countdown";
3816
+ metaEl.appendChild(countdownEl);
3817
+
3818
+ var sendNowBtn2 = document.createElement("button");
3819
+ sendNowBtn2.className = "scheduled-msg-send-now";
3820
+ sendNowBtn2.textContent = "Send now";
3821
+ sendNowBtn2.addEventListener("click", function () {
3822
+ if (ws && ws.readyState === 1) {
3823
+ ws.send(JSON.stringify({ type: "send_scheduled_now" }));
3824
+ }
3825
+ });
3826
+ metaEl.appendChild(sendNowBtn2);
3827
+
3828
+ var sep2 = document.createElement("span");
3829
+ sep2.className = "scheduled-msg-sep";
3830
+ sep2.textContent = "\u00b7";
3831
+ metaEl.appendChild(sep2);
3832
+
3833
+ cancelBtn = document.createElement("button");
3834
+ cancelBtn.className = "scheduled-msg-cancel";
3835
+ cancelBtn.textContent = "Cancel";
3836
+ cancelBtn.addEventListener("click", function () {
3837
+ if (ws && ws.readyState === 1) {
3838
+ ws.send(JSON.stringify({ type: "cancel_scheduled_message" }));
3839
+ }
3840
+ });
3841
+ metaEl.appendChild(cancelBtn);
3842
+
3843
+ wrap.appendChild(bubble);
3844
+ wrap.appendChild(metaEl);
3845
+ }
3752
3846
 
3753
- wrap.appendChild(bubble);
3754
- wrap.appendChild(metaEl);
3755
3847
  addToMessages(wrap);
3756
3848
  scheduledMsgEl = wrap;
3757
3849
  scrollToBottom();
@@ -4644,17 +4736,17 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4644
4736
  case "status":
4645
4737
  if (msg.status === "processing") {
4646
4738
  setStatus("processing");
4647
- if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4648
- setActivity(randomThinkingVerb() + "...");
4739
+ if (!(dmMode && dmTargetUser && dmTargetUser.isMate) && !matePreThinkingEl) {
4740
+ setActivity("thinking");
4649
4741
  }
4650
4742
  }
4651
4743
  break;
4652
4744
 
4653
4745
  case "compacting":
4654
4746
  if (msg.active) {
4655
- setActivity("Compacting conversation...");
4747
+ setActivity("compacting");
4656
4748
  } else if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4657
- setActivity(randomThinkingVerb() + "...");
4749
+ setActivity("thinking");
4658
4750
  }
4659
4751
  break;
4660
4752
 
@@ -4670,7 +4762,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4670
4762
  case "thinking_stop":
4671
4763
  stopThinking(msg.duration);
4672
4764
  if (!(dmMode && dmTargetUser && dmTargetUser.isMate)) {
4673
- setActivity(randomThinkingVerb() + "...");
4765
+ setActivity("thinking");
4674
4766
  }
4675
4767
  break;
4676
4768
 
@@ -5703,6 +5795,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5703
5795
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
5704
5796
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
5705
5797
  showMatePreThinking: function () { showMatePreThinking(); },
5798
+ showClaudePreThinking: function () { showClaudePreThinking(); },
5706
5799
  });
5707
5800
 
5708
5801
  // --- @Mention module ---
@@ -466,15 +466,14 @@
466
466
  display: flex;
467
467
  flex-direction: column;
468
468
  background: var(--input-bg);
469
- border: 1px solid var(--border);
469
+ border: none;
470
470
  border-radius: 8px;
471
- padding: 6px;
472
- transition: border-color 0.2s, box-shadow 0.2s;
471
+ padding: 7px;
472
+ transition: box-shadow 0.2s;
473
473
  }
474
474
 
475
475
  #input-row:focus-within {
476
- border-color: var(--text-dimmer);
477
- box-shadow: 0 0 0 1px rgba(109, 104, 96, 0.15);
476
+ box-shadow: 0 0 0 1px var(--border);
478
477
  }
479
478
 
480
479
  #input {
@@ -553,7 +552,7 @@
553
552
  border-radius: 10px;
554
553
  border: 1px solid transparent;
555
554
  background:
556
- linear-gradient(var(--bg, #282a36), var(--bg, #282a36)) padding-box,
555
+ linear-gradient(var(--ask-mate-bg, var(--bg)), var(--ask-mate-bg, var(--bg))) padding-box,
557
556
  linear-gradient(135deg, #4ecdc4 0%, #4ecdc4 25%, #556bf7, #a855f7, #f857a6, #ff6b6b) border-box;
558
557
  color: transparent;
559
558
  cursor: pointer;
@@ -934,27 +933,83 @@
934
933
  flex: 1;
935
934
  }
936
935
 
937
- .scheduled-msg-cancel {
938
- display: flex;
936
+
937
+ /* Channel mode: scheduled badge in header row */
938
+ .scheduled-msg-badge {
939
+ display: inline-flex;
939
940
  align-items: center;
940
- justify-content: center;
941
- width: 22px;
942
- height: 22px;
941
+ gap: 5px;
942
+ padding: 3px 10px;
943
+ border-radius: 12px;
944
+ font-size: 12px;
945
+ font-weight: 600;
946
+ line-height: 1;
947
+ color: var(--accent);
948
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
949
+ vertical-align: middle;
950
+ }
951
+ .scheduled-msg-badge svg,
952
+ .scheduled-msg-badge .lucide {
953
+ width: 13px;
954
+ height: 13px;
955
+ }
956
+ /* Scheduled message action links (shared by both modes) */
957
+ .scheduled-msg-actions {
958
+ display: inline-flex;
959
+ align-items: center;
960
+ gap: 6px;
961
+ margin-left: 4px;
962
+ }
963
+ .scheduled-msg-send-now {
964
+ padding: 0;
943
965
  border: none;
944
- border-radius: 4px;
945
966
  background: none;
946
- color: var(--text-dimmer);
967
+ color: var(--accent);
968
+ font-size: 12px;
969
+ font-weight: 500;
970
+ font-family: inherit;
947
971
  cursor: pointer;
972
+ transition: text-decoration 0.15s;
973
+ }
974
+ .scheduled-msg-send-now:hover {
975
+ text-decoration: underline;
976
+ }
977
+ .scheduled-msg-sep {
978
+ color: var(--text-dimmer);
979
+ font-size: 12px;
980
+ user-select: none;
981
+ }
982
+ .scheduled-msg-cancel {
948
983
  padding: 0;
949
- transition: background 0.15s, color 0.15s;
984
+ border: none;
985
+ background: none;
986
+ color: var(--text-dimmer);
987
+ font-size: 12px;
988
+ font-weight: 500;
989
+ font-family: inherit;
990
+ cursor: pointer;
991
+ transition: color 0.15s;
950
992
  }
951
-
952
993
  .scheduled-msg-cancel:hover {
953
- background: rgba(var(--overlay-rgb), 0.08);
954
994
  color: var(--error);
955
995
  }
956
996
 
957
- .scheduled-msg-cancel .lucide {
958
- width: 14px;
959
- height: 14px;
997
+ /* Channel mode: strip bubble box styling, render as plain text */
998
+ body.wide-view .scheduled-msg-wrap .bubble {
999
+ background: none;
1000
+ border: none;
1001
+ box-shadow: none;
1002
+ padding: 0;
1003
+ border-radius: 0;
1004
+ }
1005
+
1006
+ /* Channel mode: header alignment */
1007
+ body.wide-view .scheduled-msg-wrap .dm-bubble-header {
1008
+ display: flex;
1009
+ align-items: center;
1010
+ gap: 6px;
1011
+ }
1012
+ body.wide-view .scheduled-msg-wrap .scheduled-msg-countdown {
1013
+ font-size: inherit;
1014
+ color: inherit;
960
1015
  }
@@ -2342,7 +2342,9 @@ body.mate-dm-active .turn-meta {
2342
2342
  ========================================================================== */
2343
2343
 
2344
2344
  /* --- Mate Thinking: flex layout matching msg-assistant --- */
2345
- body.mate-dm-active .mate-thinking {
2345
+ /* Applies in both Mate DM and channel project chat */
2346
+ body.mate-dm-active .mate-thinking,
2347
+ body.wide-view .mate-thinking {
2346
2348
  display: flex;
2347
2349
  flex-direction: row;
2348
2350
  align-items: flex-start;
@@ -2351,10 +2353,12 @@ body.mate-dm-active .mate-thinking {
2351
2353
  margin: 0;
2352
2354
  max-width: 100%;
2353
2355
  }
2354
- body.mate-dm-active .mate-thinking:hover {
2356
+ body.mate-dm-active .mate-thinking:hover,
2357
+ body.wide-view .mate-thinking:hover {
2355
2358
  background: var(--bg-alt);
2356
2359
  }
2357
- body.mate-dm-active .mate-thinking > .dm-bubble-avatar {
2360
+ body.mate-dm-active .mate-thinking > .dm-bubble-avatar,
2361
+ body.wide-view .mate-thinking > .dm-bubble-avatar {
2358
2362
  display: block;
2359
2363
  width: 36px;
2360
2364
  height: 36px;
@@ -2362,7 +2366,8 @@ body.mate-dm-active .mate-thinking > .dm-bubble-avatar {
2362
2366
  flex-shrink: 0;
2363
2367
  margin-top: 2px;
2364
2368
  }
2365
- body.mate-dm-active .mate-thinking > .dm-bubble-content {
2369
+ body.mate-dm-active .mate-thinking > .dm-bubble-content,
2370
+ body.wide-view .mate-thinking > .dm-bubble-content {
2366
2371
  flex: 1;
2367
2372
  min-width: 0;
2368
2373
  }
@@ -2373,7 +2378,8 @@ body.mate-dm-active .mate-thinking > .dm-bubble-content {
2373
2378
  gap: 4px;
2374
2379
  padding: 4px 0;
2375
2380
  }
2376
- body.mate-dm-active .mate-thinking:not(.done) .mate-thinking-row {
2381
+ body.mate-dm-active .mate-thinking:not(.done) .mate-thinking-row,
2382
+ body.wide-view .mate-thinking:not(.done) .mate-thinking-row {
2377
2383
  display: flex;
2378
2384
  }
2379
2385
  .mate-thinking-dots {
@@ -2400,10 +2406,16 @@ body.mate-dm-active .mate-thinking:not(.done) .mate-thinking-row {
2400
2406
  }
2401
2407
 
2402
2408
  /* When done, hide mate row (JS does this too), show compact expandable header */
2403
- body.mate-dm-active .mate-thinking.done .mate-thinking-row {
2409
+ body.mate-dm-active .mate-thinking.done .mate-thinking-row,
2410
+ body.wide-view .mate-thinking.done .mate-thinking-row {
2404
2411
  display: none;
2405
2412
  }
2406
- body.mate-dm-active .mate-thinking.done .thinking-header {
2413
+ body.mate-dm-active .mate-thinking.done .mate-thinking-activity,
2414
+ body.wide-view .mate-thinking.done .mate-thinking-activity {
2415
+ display: none;
2416
+ }
2417
+ body.mate-dm-active .mate-thinking.done .thinking-header,
2418
+ body.wide-view .mate-thinking.done .thinking-header {
2407
2419
  display: inline-flex !important;
2408
2420
  font-size: 12px;
2409
2421
  padding: 4px 10px;
@@ -2412,16 +2424,19 @@ body.mate-dm-active .mate-thinking.done .thinking-header {
2412
2424
  opacity: 0.7;
2413
2425
  transition: opacity 0.15s;
2414
2426
  }
2415
- body.mate-dm-active .mate-thinking.done .thinking-header:hover {
2427
+ body.mate-dm-active .mate-thinking.done .thinking-header:hover,
2428
+ body.wide-view .mate-thinking.done .thinking-header:hover {
2416
2429
  opacity: 1;
2417
2430
  background: rgba(var(--overlay-rgb), 0.08);
2418
2431
  }
2419
- body.mate-dm-active .mate-thinking .thinking-content {
2432
+ body.mate-dm-active .mate-thinking .thinking-content,
2433
+ body.wide-view .mate-thinking .thinking-content {
2420
2434
  max-height: 0;
2421
2435
  overflow: hidden;
2422
2436
  transition: max-height 0.25s ease;
2423
2437
  }
2424
- body.mate-dm-active .mate-thinking.expanded .thinking-content {
2438
+ body.mate-dm-active .mate-thinking.expanded .thinking-content,
2439
+ body.wide-view .mate-thinking.expanded .thinking-content {
2425
2440
  max-height: 2000px;
2426
2441
  }
2427
2442
 
@@ -2476,8 +2491,10 @@ body.mate-dm-active .mate-tool-group .tool-name {
2476
2491
  color: var(--text-muted);
2477
2492
  }
2478
2493
 
2479
- /* --- Mate Permission: flex layout matching msg-assistant --- */
2480
- body.mate-dm-active .mate-permission {
2494
+ /* --- Conversational Permission: flex layout matching msg-assistant --- */
2495
+ /* Applies in both Mate DM and channel project chat */
2496
+ body.mate-dm-active .mate-permission,
2497
+ body.wide-view .mate-permission {
2481
2498
  display: flex;
2482
2499
  flex-direction: row;
2483
2500
  align-items: flex-start;
@@ -2490,10 +2507,12 @@ body.mate-dm-active .mate-permission {
2490
2507
  margin: 0;
2491
2508
  max-width: 100%;
2492
2509
  }
2493
- body.mate-dm-active .mate-permission:hover {
2510
+ body.mate-dm-active .mate-permission:hover,
2511
+ body.wide-view .mate-permission:hover {
2494
2512
  background: var(--bg-alt);
2495
2513
  }
2496
- body.mate-dm-active .mate-permission > .dm-bubble-avatar {
2514
+ body.mate-dm-active .mate-permission > .dm-bubble-avatar,
2515
+ body.wide-view .mate-permission > .dm-bubble-avatar {
2497
2516
  display: block;
2498
2517
  width: 36px;
2499
2518
  height: 36px;
@@ -2501,7 +2520,8 @@ body.mate-dm-active .mate-permission > .dm-bubble-avatar {
2501
2520
  flex-shrink: 0;
2502
2521
  margin-top: 2px;
2503
2522
  }
2504
- body.mate-dm-active .mate-permission > .dm-bubble-content {
2523
+ body.mate-dm-active .mate-permission > .dm-bubble-content,
2524
+ body.wide-view .mate-permission > .dm-bubble-content {
2505
2525
  flex: 1;
2506
2526
  min-width: 0;
2507
2527
  }
@@ -2569,14 +2589,14 @@ body.mate-dm-active .mate-permission > .dm-bubble-content {
2569
2589
  .mate-permission-deny {
2570
2590
  color: var(--text-muted);
2571
2591
  }
2572
- /* Resolved state */
2573
- body.mate-dm-active .mate-permission.resolved .mate-permission-actions {
2592
+ /* Resolved state (works in both Mate DM and channel project chat) */
2593
+ .mate-permission.resolved .mate-permission-actions {
2574
2594
  pointer-events: none;
2575
2595
  }
2576
- body.mate-dm-active .mate-permission.resolved .mate-permission-reply {
2596
+ .mate-permission.resolved .mate-permission-reply {
2577
2597
  display: none;
2578
2598
  }
2579
- body.mate-dm-active .mate-permission.resolved .permission-decision-label {
2599
+ .mate-permission.resolved .permission-decision-label {
2580
2600
  font-size: 12px;
2581
2601
  color: var(--text-dimmer);
2582
2602
  }
@@ -107,8 +107,8 @@
107
107
  padding: 3px 6px 3px 4px;
108
108
  margin: 0 0 4px 0;
109
109
  border-radius: 6px;
110
- background: color-mix(in srgb, var(--chip-color, #6c5ce7) 12%, transparent);
111
- border: 1px solid color-mix(in srgb, var(--chip-color, #6c5ce7) 25%, transparent);
110
+ background: color-mix(in srgb, var(--chip-color, #6c5ce7) 15%, var(--bg-alt));
111
+ border: 1px solid color-mix(in srgb, var(--chip-color, #6c5ce7) 30%, var(--border));
112
112
  cursor: default;
113
113
  flex-shrink: 0;
114
114
  width: fit-content;
@@ -131,19 +131,22 @@
131
131
  font-size: 13px;
132
132
  font-weight: 600;
133
133
  white-space: nowrap;
134
+ line-height: 18px;
134
135
  }
135
136
 
136
137
  .input-mention-chip-remove {
137
138
  background: none;
138
139
  border: none;
139
- padding: 0 2px;
140
+ padding: 0;
140
141
  margin: 0;
141
142
  cursor: pointer;
142
143
  font-size: 15px;
143
- line-height: 1;
144
+ line-height: 18px;
144
145
  color: var(--text-dimmer);
145
146
  opacity: 0.6;
146
147
  transition: opacity 0.1s;
148
+ display: flex;
149
+ align-items: center;
147
150
  }
148
151
 
149
152
  .input-mention-chip-remove:hover {
@@ -193,9 +193,11 @@ export function sendMessage() {
193
193
  }
194
194
  ctx.ws.send(JSON.stringify(payload));
195
195
 
196
- // Mate DM: show pre-thinking dots after a short delay (before server responds)
196
+ // Show pre-thinking dots before server responds
197
197
  if (ctx.isMateDm && ctx.isMateDm()) {
198
198
  ctx.showMatePreThinking();
199
+ } else if (ctx.showClaudePreThinking) {
200
+ ctx.showClaudePreThinking();
199
201
  }
200
202
 
201
203
  ctx.inputEl.value = "";
@@ -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);
@@ -3946,8 +3946,8 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
3946
3946
  // All other users
3947
3947
  var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
3948
3948
 
3949
- // Hide section if no other users (single-user mode or alone)
3950
- if (allOthers.length === 0) {
3949
+ // Hide section if no other users and no mates
3950
+ if (allOthers.length === 0 && cachedMates.length === 0) {
3951
3951
  container.innerHTML = "";
3952
3952
  container.classList.add("hidden");
3953
3953
  return;
@@ -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
 
@@ -204,7 +205,7 @@ export function renderAskUserQuestion(toolId, input) {
204
205
 
205
206
  var avi = document.createElement("img");
206
207
  avi.className = "dm-bubble-avatar";
207
- avi.src = mateAvatar;
208
+ avi.src = identity.avatar;
208
209
  container.appendChild(avi);
209
210
 
210
211
  mateContentWrap = document.createElement("div");
@@ -213,7 +214,7 @@ export function renderAskUserQuestion(toolId, input) {
213
214
  var headerEl = document.createElement("div");
214
215
  headerEl.className = "dm-bubble-header";
215
216
  headerEl.innerHTML =
216
- '<span class="dm-bubble-name">' + escapeHtml(mateName) + '</span>' +
217
+ '<span class="dm-bubble-name">' + escapeHtml(identity.name) + '</span>' +
217
218
  '<span class="dm-bubble-time">' + String(new Date().getHours()).padStart(2, "0") + ":" + String(new Date().getMinutes()).padStart(2, "0") + '</span>';
218
219
  mateContentWrap.appendChild(headerEl);
219
220
  }
@@ -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
 
package/lib/sdk-bridge.js CHANGED
@@ -1515,6 +1515,18 @@ function createSDKBridge(opts) {
1515
1515
  return Promise.resolve({ behavior: "allow", updatedInput: input });
1516
1516
  }
1517
1517
 
1518
+ // Auto-approve safe browser MCP tools.
1519
+ // Only watch/unwatch: user explicitly chose which tab to share.
1520
+ // Everything else (screenshot, read_page, list_tabs, etc.) can expose
1521
+ // content from tabs the user didn't intend to share, so require approval.
1522
+ var safeBrowserTools = { browser_watch_tab: true, browser_unwatch_tab: true };
1523
+ if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
1524
+ var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
1525
+ if (safeBrowserTools[mcpToolName]) {
1526
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1527
+ }
1528
+ }
1529
+
1518
1530
  // Auto-approve safe Bash commands (read-only, non-destructive)
1519
1531
  // Applies to ALL sessions (mates and regular projects alike).
1520
1532
  // These are purely read-only commands that cannot modify files, install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.7",
3
+ "version": "2.26.0-beta.9",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",