clay-server 2.38.0-beta.2 → 2.38.0-beta.3

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.
@@ -0,0 +1,293 @@
1
+ /* ==========================================================================
2
+ Home Chat (Clay) — phablet-style mini chat embedded in the home hub
3
+ ==========================================================================
4
+ The home hub is a flex row: this pane sits on the left, the widget
5
+ column flows to the right. The pane wraps a phone-shaped chat "frame"
6
+ (rounded card with shadow) so it reads as a discrete surface, not a
7
+ restyle of the main chat. */
8
+
9
+ .home-chat-pane {
10
+ flex: 0 0 auto;
11
+ width: 50%;
12
+ min-width: 360px;
13
+ max-width: 720px;
14
+ display: flex;
15
+ align-items: stretch;
16
+ justify-content: center;
17
+ padding: 32px 16px 24px;
18
+ background: var(--bg);
19
+ border-right: 1px solid var(--border);
20
+ }
21
+
22
+ .home-chat-frame {
23
+ width: 100%;
24
+ max-width: 480px;
25
+ display: flex;
26
+ flex-direction: column;
27
+ background: var(--bg-alt, var(--bg));
28
+ border: 1px solid var(--border);
29
+ border-radius: 24px;
30
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.10), 0 2px 6px rgba(0, 0, 0, 0.06);
31
+ overflow: hidden;
32
+ height: 100%;
33
+ }
34
+
35
+ /* --- Header --- */
36
+
37
+ .home-chat-header {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 12px;
41
+ padding: 14px 16px;
42
+ border-bottom: 1px solid var(--border);
43
+ background: var(--bg);
44
+ }
45
+
46
+ .home-chat-avatar-wrap {
47
+ width: 38px;
48
+ height: 38px;
49
+ border-radius: 50%;
50
+ overflow: hidden;
51
+ flex-shrink: 0;
52
+ background: var(--accent);
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ }
57
+
58
+ .home-chat-avatar {
59
+ width: 100%;
60
+ height: 100%;
61
+ object-fit: cover;
62
+ display: block;
63
+ }
64
+
65
+ .home-chat-title-block {
66
+ flex: 1;
67
+ min-width: 0;
68
+ }
69
+
70
+ .home-chat-title {
71
+ font-size: 15px;
72
+ font-weight: 600;
73
+ color: var(--text);
74
+ line-height: 1.2;
75
+ }
76
+
77
+ .home-chat-subtitle {
78
+ font-size: 12px;
79
+ color: var(--text-muted, var(--text-dimmer));
80
+ line-height: 1.2;
81
+ margin-top: 2px;
82
+ white-space: nowrap;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ }
86
+
87
+ .home-chat-icon-btn {
88
+ background: none;
89
+ border: none;
90
+ width: 32px;
91
+ height: 32px;
92
+ border-radius: 8px;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ cursor: pointer;
97
+ color: var(--text-muted, var(--text-dimmer));
98
+ flex-shrink: 0;
99
+ }
100
+ .home-chat-icon-btn:hover {
101
+ background: var(--hover-bg, rgba(0,0,0,0.05));
102
+ color: var(--text);
103
+ }
104
+ .home-chat-icon-btn .lucide {
105
+ width: 18px;
106
+ height: 18px;
107
+ }
108
+
109
+ /* --- Messages list --- */
110
+
111
+ .home-chat-messages {
112
+ flex: 1;
113
+ overflow-y: auto;
114
+ padding: 16px 14px;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 10px;
118
+ background: var(--input-bg, var(--bg));
119
+ scroll-behavior: smooth;
120
+ }
121
+
122
+ .home-chat-bubble {
123
+ max-width: 88%;
124
+ padding: 10px 14px;
125
+ border-radius: 18px;
126
+ font-size: 14px;
127
+ line-height: 1.5;
128
+ word-wrap: break-word;
129
+ white-space: pre-wrap;
130
+ box-shadow: 0 1px 1px rgba(0,0,0,0.04);
131
+ }
132
+
133
+ .home-chat-bubble-user {
134
+ align-self: flex-end;
135
+ background: var(--accent);
136
+ color: #fff;
137
+ border-bottom-right-radius: 6px;
138
+ }
139
+
140
+ .home-chat-bubble-clay {
141
+ align-self: flex-start;
142
+ background: var(--bg);
143
+ color: var(--text);
144
+ border: 1px solid var(--border);
145
+ border-bottom-left-radius: 6px;
146
+ }
147
+
148
+ .home-chat-bubble-system {
149
+ align-self: center;
150
+ font-size: 12px;
151
+ color: var(--text-muted, var(--text-dimmer));
152
+ background: transparent;
153
+ padding: 4px 10px;
154
+ font-style: italic;
155
+ }
156
+
157
+ /* Inline session reference: [project/sess_id — 2026-04-22]. Rendered as
158
+ a subtle chip when the home-chat renderer detects the pattern. */
159
+ .home-chat-ref {
160
+ display: inline-block;
161
+ background: rgba(124, 58, 237, 0.08);
162
+ color: var(--accent);
163
+ padding: 1px 8px;
164
+ border-radius: 10px;
165
+ font-family: "Roboto Mono", "Courier New", monospace;
166
+ font-size: 12px;
167
+ cursor: pointer;
168
+ margin: 0 2px;
169
+ border: 1px solid rgba(124, 58, 237, 0.18);
170
+ }
171
+ .home-chat-ref:hover {
172
+ background: rgba(124, 58, 237, 0.16);
173
+ }
174
+
175
+ /* --- Typing indicator --- */
176
+
177
+ .home-chat-typing {
178
+ display: flex;
179
+ gap: 4px;
180
+ padding: 6px 18px 0;
181
+ align-items: center;
182
+ }
183
+ .home-chat-typing.hidden { display: none; }
184
+
185
+ .home-chat-typing-dot {
186
+ width: 6px;
187
+ height: 6px;
188
+ border-radius: 50%;
189
+ background: var(--text-dimmer);
190
+ animation: home-chat-bounce 1.2s infinite ease-in-out;
191
+ }
192
+ .home-chat-typing-dot:nth-child(1) { animation-delay: 0s; }
193
+ .home-chat-typing-dot:nth-child(2) { animation-delay: 0.15s; }
194
+ .home-chat-typing-dot:nth-child(3) { animation-delay: 0.30s; }
195
+
196
+ @keyframes home-chat-bounce {
197
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
198
+ 40% { transform: translateY(-4px); opacity: 1; }
199
+ }
200
+
201
+ /* --- Input row --- */
202
+
203
+ .home-chat-input-row {
204
+ display: flex;
205
+ align-items: flex-end;
206
+ gap: 8px;
207
+ padding: 10px 12px 12px;
208
+ border-top: 1px solid var(--border);
209
+ background: var(--bg);
210
+ }
211
+
212
+ .home-chat-input {
213
+ flex: 1;
214
+ border: 1px solid var(--border);
215
+ border-radius: 18px;
216
+ padding: 10px 14px;
217
+ background: var(--input-bg, var(--bg));
218
+ color: var(--text);
219
+ font-size: 14px;
220
+ line-height: 1.5;
221
+ font-family: inherit;
222
+ resize: none;
223
+ max-height: 140px;
224
+ outline: none;
225
+ transition: border-color 0.15s, box-shadow 0.15s;
226
+ }
227
+ .home-chat-input:focus {
228
+ border-color: var(--accent);
229
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.18);
230
+ }
231
+
232
+ .home-chat-send-btn {
233
+ flex-shrink: 0;
234
+ width: 38px;
235
+ height: 38px;
236
+ border-radius: 50%;
237
+ border: none;
238
+ background: var(--accent);
239
+ color: #fff;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ cursor: pointer;
244
+ transition: opacity 0.15s, transform 0.1s;
245
+ }
246
+ .home-chat-send-btn:disabled {
247
+ opacity: 0.4;
248
+ cursor: default;
249
+ }
250
+ .home-chat-send-btn:not(:disabled):hover {
251
+ opacity: 0.9;
252
+ }
253
+ .home-chat-send-btn:not(:disabled):active {
254
+ transform: scale(0.95);
255
+ }
256
+ .home-chat-send-btn .lucide {
257
+ width: 18px;
258
+ height: 18px;
259
+ }
260
+
261
+ /* --- Right widget pane --- */
262
+
263
+ .home-hub-inner {
264
+ flex: 1 1 auto;
265
+ overflow-y: auto;
266
+ padding: 48px 24px 40px;
267
+ display: flex;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ min-width: 0;
271
+ }
272
+
273
+ /* --- Mobile / narrow: stack the chat and widgets --- */
274
+
275
+ @media (max-width: 900px) {
276
+ #home-hub {
277
+ flex-direction: column;
278
+ }
279
+ .home-chat-pane {
280
+ width: auto;
281
+ max-width: none;
282
+ min-width: 0;
283
+ border-right: none;
284
+ border-bottom: 1px solid var(--border);
285
+ flex: 0 0 auto;
286
+ height: 60vh;
287
+ padding: 16px 12px;
288
+ }
289
+ .home-hub-inner {
290
+ flex: 1 1 auto;
291
+ padding: 24px 16px;
292
+ }
293
+ }
@@ -2,53 +2,22 @@
2
2
  Home Hub — Personalized command center & dashboard
3
3
  ========================================================================== */
4
4
 
5
- /* Hub covers #main-area (sidebar + chat), sits inside it with absolute */
5
+ /* Hub covers #main-area (sidebar + chat), sits inside it with absolute.
6
+ Internally it splits into a Clay chat panel on the left and the
7
+ existing widget column on the right. See home-chat.css for the chat
8
+ panel styling. */
6
9
  #home-hub {
7
10
  position: absolute;
8
11
  inset: 0;
9
12
  display: flex;
10
- flex-direction: column;
11
- align-items: center;
12
- overflow-y: auto;
13
+ flex-direction: row;
14
+ align-items: stretch;
15
+ overflow: hidden;
13
16
  background: var(--bg);
14
17
  z-index: 200;
15
- padding: 48px 24px 40px;
16
18
  border-top-left-radius: 8px;
17
19
  animation: hubFadeIn 0.35s ease;
18
20
  }
19
-
20
- /* Clay home: split layout. The hub sits on the right half as a side panel,
21
- the chat (Clay DM) occupies the left. The body class is toggled by
22
- showHomeHub / hideHomeHub when a Clay mate exists for the user. */
23
- body.clay-home-split #home-hub {
24
- inset: 0 0 0 auto;
25
- width: 50%;
26
- min-width: 360px;
27
- max-width: 720px;
28
- border-left: 1px solid var(--border);
29
- z-index: 5; /* sit alongside the chat, not over it */
30
- padding: 32px 20px 32px;
31
- border-top-left-radius: 0;
32
- animation: hubSlideInRight 0.25s ease;
33
- }
34
- @media (max-width: 900px) {
35
- /* Below 900px there isn't room for a meaningful split; collapse back
36
- to the legacy full-overlay hub for narrow viewports. */
37
- body.clay-home-split #home-hub {
38
- inset: 0;
39
- width: auto;
40
- max-width: none;
41
- min-width: 0;
42
- border-left: none;
43
- z-index: 200;
44
- padding: 48px 24px 40px;
45
- border-top-left-radius: 8px;
46
- }
47
- }
48
- @keyframes hubSlideInRight {
49
- from { transform: translateX(8px); opacity: 0; }
50
- to { transform: translateX(0); opacity: 1; }
51
- }
52
21
  /* Close button (X / ESC) */
53
22
  .home-hub-close-btn {
54
23
  position: absolute;
@@ -115,6 +115,39 @@
115
115
  <!-- === Main Area (sidebar + resize-handle + main-column) === -->
116
116
  <div id="main-area">
117
117
  <div id="home-hub" class="hidden">
118
+ <!-- Clay chat panel: phablet-style, self-contained. Talks to the
119
+ user's Clay mate via dedicated WS messages (home_clay_*) and
120
+ does not interfere with the active project session. -->
121
+ <div id="home-chat-pane" class="home-chat-pane">
122
+ <div class="home-chat-frame">
123
+ <div class="home-chat-header">
124
+ <div class="home-chat-avatar-wrap">
125
+ <img class="home-chat-avatar" src="/icon-banded-76.png" alt="Clay">
126
+ </div>
127
+ <div class="home-chat-title-block">
128
+ <div class="home-chat-title">Clay</div>
129
+ <div class="home-chat-subtitle">Your workspace memory</div>
130
+ </div>
131
+ <button id="home-chat-new-btn" class="home-chat-icon-btn" title="Start a new conversation" aria-label="New conversation">
132
+ <i data-lucide="plus"></i>
133
+ </button>
134
+ </div>
135
+ <div id="home-chat-messages" class="home-chat-messages">
136
+ <!-- messages rendered by home-chat.js -->
137
+ </div>
138
+ <div class="home-chat-typing hidden" id="home-chat-typing">
139
+ <span class="home-chat-typing-dot"></span>
140
+ <span class="home-chat-typing-dot"></span>
141
+ <span class="home-chat-typing-dot"></span>
142
+ </div>
143
+ <div class="home-chat-input-row">
144
+ <textarea id="home-chat-input" class="home-chat-input" rows="1" placeholder="Ask Clay…" autocomplete="off"></textarea>
145
+ <button id="home-chat-send-btn" class="home-chat-send-btn" disabled aria-label="Send">
146
+ <i data-lucide="arrow-up"></i>
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
118
151
  <button id="home-hub-close" class="home-hub-close-btn hidden">
119
152
  <i data-lucide="x"></i>
120
153
  <span>ESC</span>
@@ -574,29 +574,18 @@ function renderHomeHubMates() {
574
574
  }
575
575
 
576
576
  export function showHomeHub() {
577
- // Open Clay DM (the host agent) on the left while showing widgets on
578
- // the right. The body class drives a split layout — see home-hub.css.
579
- // Existing users without a Clay mate yet (cachedMatesList not yet
580
- // delivered, or syncArchivedBuiltinMates hasn't run) fall back to the
581
- // legacy full-screen hub so the home button never feels broken.
582
- var clayMate = findClayMate();
583
- if (clayMate) {
584
- document.body.classList.add("clay-home-split");
585
- var dmTarget = store.get('dmTargetUser');
586
- var inClayDm = store.get('dmMode') && dmTarget && dmTarget.id === clayMate.id;
587
- if (!inClayDm) {
588
- // Open Clay DM. Skip the generic mate-onboarding modal — Clay is
589
- // the host agent, not a learn-about-Mates moment; that intro
590
- // fires the first time the user opens a regular Mate.
591
- openDm(clayMate.id, { skipOnboarding: true });
592
- }
593
- } else {
594
- // Fallback: legacy behavior (full overlay, exit any DM).
595
- document.body.classList.remove("clay-home-split");
596
- if (store.get('dmMode')) exitDmMode();
597
- }
577
+ // Home hub hosts its own Clay chat panel on the left (see home-chat.js)
578
+ // and the existing widgets on the right. The chat is a self-contained
579
+ // surface with its own renderer and its own WS protocol — it does NOT
580
+ // hijack the user's main project session. Any active DM stays open
581
+ // underneath; we just exit it visually so the hub layer is clean.
582
+ if (store.get('dmMode')) exitDmMode();
598
583
  homeHubVisible = true;
599
584
  homeHub.classList.remove("hidden");
585
+ // Mount/refresh the in-hub Clay chat panel.
586
+ try {
587
+ if (typeof window.__initHomeChat === "function") window.__initHomeChat();
588
+ } catch (e) {}
600
589
  // Show close button only if there's a project to return to
601
590
  if (hubCloseBtn) {
602
591
  if (store.get('currentSlug')) hubCloseBtn.classList.remove("hidden");
@@ -630,19 +619,7 @@ export function hideHomeHub() {
630
619
  if (!homeHubVisible) return;
631
620
  homeHubVisible = false;
632
621
  homeHub.classList.add("hidden");
633
- document.body.classList.remove("clay-home-split");
634
622
  stopTipRotation();
635
623
  var mobileHome = document.getElementById("mobile-home-btn");
636
624
  if (mobileHome) mobileHome.classList.remove("active");
637
625
  }
638
-
639
- // Locate the user's Clay (host agent) mate from the cached list. Returns
640
- // null if cachedMatesList hasn't arrived yet or the user predates Clay.
641
- function findClayMate() {
642
- var list = store.get('cachedMatesList');
643
- if (!list || !list.length) return null;
644
- for (var i = 0; i < list.length; i++) {
645
- if (list[i] && list[i].builtinKey === "clay") return list[i];
646
- }
647
- return null;
648
- }
@@ -15,6 +15,7 @@ import { updateDmBadge, renderSidebarPresence, setMentionActive, renderUserStrip
15
15
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
16
16
  import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
17
17
  import { handleMateDatastoreTablesResult, handleMateDatastoreDescribeResult, handleMateDatastoreQueryResult, handleMateDatastoreError, handleMateDatastoreChange } from './mate-datastore-ui.js';
18
+ import { handleHomeClayHistory, handleHomeClayDelta, handleHomeClayDone, handleHomeClayError } from './home-chat.js';
18
19
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
19
20
  import { renderMemoryList } from './mate-memory.js';
20
21
  import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
@@ -379,6 +380,22 @@ export function processMessage(msg) {
379
380
  connectOverlay.classList.remove("hidden");
380
381
  break;
381
382
 
383
+ case "home_clay_history":
384
+ handleHomeClayHistory(msg);
385
+ break;
386
+
387
+ case "home_clay_delta":
388
+ handleHomeClayDelta(msg);
389
+ break;
390
+
391
+ case "home_clay_done":
392
+ handleHomeClayDone();
393
+ break;
394
+
395
+ case "home_clay_error":
396
+ handleHomeClayError(msg);
397
+ break;
398
+
382
399
  case "slash_commands":
383
400
  var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
384
401
  store.set({ slashCommands: (msg.commands || []).filter(function (name) {
@@ -0,0 +1,239 @@
1
+ // Home Chat (Clay) — phablet-style chat embedded in the home hub.
2
+ // Self-contained: own DOM, own renderer, own WS protocol.
3
+ // Talks to the user's Clay mate session via home_clay_* messages.
4
+ // Does not interfere with the active project session.
5
+
6
+ import { escapeHtml } from './utils.js';
7
+ import { getWs } from './ws-ref.js';
8
+ import { renderMarkdown } from './markdown.js';
9
+ import { refreshIcons } from './icons.js';
10
+ import { switchProject } from './app-projects.js';
11
+
12
+ var initialized = false;
13
+ var messagesEl = null;
14
+ var inputEl = null;
15
+ var sendBtn = null;
16
+ var typingEl = null;
17
+ var newBtnEl = null;
18
+
19
+ // Per-turn assembly state. Server may emit many delta events for a single
20
+ // assistant turn; we accumulate text and render incrementally into the
21
+ // last bubble.
22
+ var currentAssistantBubble = null;
23
+ var currentAssistantText = "";
24
+ var lastSenderWasUser = false;
25
+
26
+ // Initialize on first showHomeHub. The init function is exposed on
27
+ // window.__initHomeChat so app-home-hub.js (which already imports too
28
+ // many things) can call it without adding another import edge.
29
+ export function initHomeChat() {
30
+ if (initialized) {
31
+ // Re-mount idempotent: just ensure the WS subscription is open.
32
+ requestSession();
33
+ return;
34
+ }
35
+ initialized = true;
36
+
37
+ messagesEl = document.getElementById("home-chat-messages");
38
+ inputEl = document.getElementById("home-chat-input");
39
+ sendBtn = document.getElementById("home-chat-send-btn");
40
+ typingEl = document.getElementById("home-chat-typing");
41
+ newBtnEl = document.getElementById("home-chat-new-btn");
42
+
43
+ if (!messagesEl || !inputEl || !sendBtn) return;
44
+
45
+ // --- Input handling ---
46
+ inputEl.addEventListener("input", function () {
47
+ autoResize();
48
+ sendBtn.disabled = inputEl.value.trim().length === 0;
49
+ });
50
+ inputEl.addEventListener("keydown", function (e) {
51
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
52
+ e.preventDefault();
53
+ doSend();
54
+ }
55
+ });
56
+ sendBtn.addEventListener("click", doSend);
57
+ if (newBtnEl) {
58
+ newBtnEl.addEventListener("click", function () {
59
+ var ws = getWs();
60
+ if (!ws || ws.readyState !== 1) return;
61
+ ws.send(JSON.stringify({ type: "home_clay_new_session" }));
62
+ messagesEl.innerHTML = "";
63
+ currentAssistantBubble = null;
64
+ currentAssistantText = "";
65
+ lastSenderWasUser = false;
66
+ hideTyping();
67
+ addSystemBubble("New conversation started.");
68
+ });
69
+ }
70
+
71
+ // --- Initial state pull ---
72
+ requestSession();
73
+ }
74
+
75
+ function autoResize() {
76
+ inputEl.style.height = "auto";
77
+ inputEl.style.height = Math.min(140, inputEl.scrollHeight) + "px";
78
+ }
79
+
80
+ function requestSession() {
81
+ var ws = getWs();
82
+ if (!ws || ws.readyState !== 1) return;
83
+ ws.send(JSON.stringify({ type: "home_clay_open" }));
84
+ }
85
+
86
+ function doSend() {
87
+ var text = inputEl.value.trim();
88
+ if (!text) return;
89
+ var ws = getWs();
90
+ if (!ws || ws.readyState !== 1) return;
91
+
92
+ // Optimistic render of the user's message.
93
+ addUserBubble(text);
94
+ inputEl.value = "";
95
+ autoResize();
96
+ sendBtn.disabled = true;
97
+
98
+ ws.send(JSON.stringify({ type: "home_clay_send", text: text }));
99
+ showTyping();
100
+ }
101
+
102
+ // --- Rendering ---
103
+
104
+ function addUserBubble(text) {
105
+ // Finalize any open assistant bubble before adding the next user turn.
106
+ finalizeAssistant();
107
+ var bubble = document.createElement("div");
108
+ bubble.className = "home-chat-bubble home-chat-bubble-user";
109
+ bubble.textContent = text;
110
+ messagesEl.appendChild(bubble);
111
+ scrollToBottom();
112
+ lastSenderWasUser = true;
113
+ }
114
+
115
+ function ensureAssistantBubble() {
116
+ if (currentAssistantBubble) return currentAssistantBubble;
117
+ var bubble = document.createElement("div");
118
+ bubble.className = "home-chat-bubble home-chat-bubble-clay";
119
+ messagesEl.appendChild(bubble);
120
+ currentAssistantBubble = bubble;
121
+ currentAssistantText = "";
122
+ lastSenderWasUser = false;
123
+ return bubble;
124
+ }
125
+
126
+ function appendAssistantText(text) {
127
+ var bubble = ensureAssistantBubble();
128
+ currentAssistantText += text;
129
+ // Render markdown + linkify session refs after sanitization.
130
+ bubble.innerHTML = linkifyRefs(renderMarkdown(currentAssistantText));
131
+ scrollToBottom();
132
+ }
133
+
134
+ function finalizeAssistant() {
135
+ if (currentAssistantBubble && !currentAssistantText) {
136
+ // Empty assistant turn (no text produced). Drop the empty bubble.
137
+ currentAssistantBubble.remove();
138
+ }
139
+ currentAssistantBubble = null;
140
+ currentAssistantText = "";
141
+ }
142
+
143
+ function addSystemBubble(text) {
144
+ var bubble = document.createElement("div");
145
+ bubble.className = "home-chat-bubble home-chat-bubble-system";
146
+ bubble.textContent = text;
147
+ messagesEl.appendChild(bubble);
148
+ scrollToBottom();
149
+ }
150
+
151
+ // Convert [project-slug/sess_xxx — date] tokens in the rendered HTML
152
+ // into clickable chips. Server-side Clay is instructed to emit these.
153
+ function linkifyRefs(html) {
154
+ // Match [slug/sess_id - date] inside text but not inside HTML attributes.
155
+ // Conservative: the slug is alphanumeric/-/_, sess id starts with sess_.
156
+ var re = /\[([a-zA-Z0-9_\-]+)\/(sess_[a-zA-Z0-9_\-]+)(?:\s+[—-]\s+([0-9]{4}-[0-9]{2}-[0-9]{2}))?\]/g;
157
+ return html.replace(re, function (_full, slug, sessId, date) {
158
+ var label = slug + "/" + sessId.substring(0, 14) + (date ? " · " + date : "");
159
+ return '<span class="home-chat-ref" data-slug="' + escapeHtml(slug) + '" data-session="' + escapeHtml(sessId) + '">' + escapeHtml(label) + '</span>';
160
+ });
161
+ }
162
+
163
+ function scrollToBottom() {
164
+ if (!messagesEl) return;
165
+ // Always pin: home chat is short, no need for scroll-up detection.
166
+ requestAnimationFrame(function () {
167
+ messagesEl.scrollTop = messagesEl.scrollHeight;
168
+ });
169
+ }
170
+
171
+ function showTyping() {
172
+ if (typingEl) typingEl.classList.remove("hidden");
173
+ }
174
+ function hideTyping() {
175
+ if (typingEl) typingEl.classList.add("hidden");
176
+ }
177
+
178
+ // --- Server message handlers (called from app-messages.js dispatcher) ---
179
+
180
+ export function handleHomeClayHistory(msg) {
181
+ if (!messagesEl) return;
182
+ messagesEl.innerHTML = "";
183
+ currentAssistantBubble = null;
184
+ currentAssistantText = "";
185
+ hideTyping();
186
+ var entries = msg.messages || [];
187
+ if (entries.length === 0) {
188
+ addSystemBubble("Hi — I'm Clay. I can search every session, project, and decision in your workspace. What are you trying to find?");
189
+ return;
190
+ }
191
+ for (var i = 0; i < entries.length; i++) {
192
+ var e = entries[i];
193
+ if (e.role === "user") {
194
+ addUserBubble(e.text || "");
195
+ } else if (e.role === "assistant") {
196
+ // Replay finalized assistant text in one shot.
197
+ appendAssistantText(e.text || "");
198
+ finalizeAssistant();
199
+ }
200
+ }
201
+ }
202
+
203
+ export function handleHomeClayDelta(msg) {
204
+ hideTyping();
205
+ if (typeof msg.text === "string") appendAssistantText(msg.text);
206
+ }
207
+
208
+ export function handleHomeClayDone() {
209
+ hideTyping();
210
+ finalizeAssistant();
211
+ }
212
+
213
+ export function handleHomeClayError(msg) {
214
+ hideTyping();
215
+ finalizeAssistant();
216
+ addSystemBubble("Error: " + (msg.text || "unknown"));
217
+ }
218
+
219
+ // --- Click delegation for session ref chips ---
220
+
221
+ document.addEventListener("click", function (e) {
222
+ var chip = e.target && e.target.closest && e.target.closest(".home-chat-ref");
223
+ if (!chip) return;
224
+ var slug = chip.dataset.slug;
225
+ if (!slug) return;
226
+ // Clicking a chip jumps the user out of the home hub into the source
227
+ // project. Session selection inside that project is up to the existing
228
+ // session restore mechanism.
229
+ if (typeof switchProject === "function") {
230
+ var hubBtn = document.getElementById("home-hub-close");
231
+ if (hubBtn) hubBtn.click();
232
+ switchProject(slug);
233
+ }
234
+ });
235
+
236
+ // Expose init for app-home-hub.js without adding an import edge.
237
+ if (typeof window !== "undefined") {
238
+ window.__initHomeChat = initHomeChat;
239
+ }
@@ -385,16 +385,15 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
385
385
  // mate list is still reachable from the DM picker.
386
386
  var favoriteMates = cachedMates.filter(function (m) {
387
387
  if (m.archived) return false;
388
+ // Clay is the host agent reachable only via the Home button — never
389
+ // shown alongside regular mates in the sidebar list.
390
+ if (m.builtinKey === "clay") return false;
388
391
  if (cachedDmRemovedUsers[m.id]) return false;
389
392
  if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
390
393
  if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
391
394
  return false;
392
395
  });
393
396
  var sortedMates = favoriteMates.sort(function (a, b) {
394
- // Clay (host agent) pins to the top, then other built-ins, then user mates.
395
- var aClay = a.builtinKey === "clay" ? 1 : 0;
396
- var bClay = b.builtinKey === "clay" ? 1 : 0;
397
- if (aClay !== bClay) return bClay - aClay;
398
397
  var aBuiltin = a.builtinKey ? 1 : 0;
399
398
  var bBuiltin = b.builtinKey ? 1 : 0;
400
399
  if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
@@ -18,6 +18,7 @@
18
18
  @import url("css/scheduler.css");
19
19
  @import url("css/scheduler-modal.css");
20
20
  @import url("css/home-hub.css");
21
+ @import url("css/home-chat.css");
21
22
  @import url("css/playbook.css");
22
23
  @import url("css/stt.css");
23
24
  @import url("css/profile.css");
@@ -0,0 +1,245 @@
1
+ // Home Chat (Clay) — server-side handler.
2
+ // Routes home_clay_* WS messages to the user's Clay (host agent) mate
3
+ // project session, independent of which project the WS is currently
4
+ // bound to. Mirrors session events back to the WS as home_clay_* so the
5
+ // client renders into its own home-chat panel without disturbing the
6
+ // active project view.
7
+
8
+ function attachClayHome(deps) {
9
+ var users = deps.users;
10
+ var mates = deps.mates;
11
+ var projects = deps.projects;
12
+ var addProject = deps.addProject;
13
+
14
+ // Per-WS subscription state.
15
+ // ws._homeClayTap = { unsubscribe, sessionId, claySlug }
16
+ // Stored on the ws itself rather than a side map so it's GC'd with the
17
+ // socket and so handleDisconnection sees it without a registry lookup.
18
+
19
+ function findClayProject(userId, ensureRegistered) {
20
+ var mateCtx = mates.buildMateCtx(userId);
21
+ var allMates = mates.getAllMates(mateCtx);
22
+ var clay = null;
23
+ for (var i = 0; i < allMates.length; i++) {
24
+ if (allMates[i] && allMates[i].builtinKey === "clay") { clay = allMates[i]; break; }
25
+ }
26
+ if (!clay) return null;
27
+ var slug = "mate-" + clay.id;
28
+ if (!projects.has(slug)) {
29
+ if (!ensureRegistered) return null;
30
+ var dir = mates.getMateDir(mateCtx, clay.id);
31
+ var fs = require("fs");
32
+ try { fs.mkdirSync(dir, { recursive: true }); } catch (e) {}
33
+ var name = (clay.profile && clay.profile.displayName) || clay.name || "Clay";
34
+ addProject(dir, slug, name, null, clay.createdBy || userId, null, { isMate: true, mateDisplayName: name, isHostAgent: true });
35
+ }
36
+ var ctx = projects.get(slug);
37
+ return ctx ? { ctx: ctx, slug: slug, mate: clay } : null;
38
+ }
39
+
40
+ // Pick the most recent visible session in Clay's project owned by this
41
+ // user. Create one if none exists. The home chat is "single thread per
42
+ // user" by default; home_clay_new_session forks a fresh one on demand.
43
+ function getOrCreateHomeSession(found, userId) {
44
+ var sm = found.ctx.getSessionManager();
45
+ if (!sm) return null;
46
+ var best = null;
47
+ sm.sessions.forEach(function (s) {
48
+ if (s.hidden) return;
49
+ if (s.ownerId && s.ownerId !== userId) return;
50
+ if (!best || (s.lastActivity || 0) > (best.lastActivity || 0)) best = s;
51
+ });
52
+ if (best) return best;
53
+ var sess = sm.createSession({ ownerId: userId, vendor: "claude" }, null);
54
+ return sess;
55
+ }
56
+
57
+ // Convert a session.history entry stream into the simplified home-chat
58
+ // shape (alternating user / assistant turns, assistant text coalesced
59
+ // across deltas). Tool calls and intermediate events are dropped — the
60
+ // home chat surface intentionally hides them.
61
+ function historyToHomeChat(history) {
62
+ var msgs = [];
63
+ var pending = "";
64
+ function flushAssistant() {
65
+ if (pending) {
66
+ msgs.push({ role: "assistant", text: pending });
67
+ pending = "";
68
+ }
69
+ }
70
+ for (var i = 0; i < history.length; i++) {
71
+ var e = history[i];
72
+ if (!e) continue;
73
+ if (e.type === "user_message" && e.text) {
74
+ flushAssistant();
75
+ msgs.push({ role: "user", text: e.text });
76
+ } else if (e.type === "delta" && typeof e.text === "string") {
77
+ pending += e.text;
78
+ } else if (e.type === "result" || e.type === "done") {
79
+ flushAssistant();
80
+ } else if (e.type === "error" && e.text) {
81
+ flushAssistant();
82
+ msgs.push({ role: "assistant", text: "[error] " + e.text });
83
+ }
84
+ }
85
+ flushAssistant();
86
+ return msgs;
87
+ }
88
+
89
+ function transformEvent(obj) {
90
+ if (!obj || typeof obj.type !== "string") return null;
91
+ if (obj.type === "delta" && typeof obj.text === "string") {
92
+ return { type: "home_clay_delta", text: obj.text };
93
+ }
94
+ if (obj.type === "result" || obj.type === "done") {
95
+ return { type: "home_clay_done" };
96
+ }
97
+ if (obj.type === "error") {
98
+ return { type: "home_clay_error", text: obj.text || "Unknown error" };
99
+ }
100
+ // intentionally skip: tool_*, thinking_*, status, plan_*, debate, etc.
101
+ return null;
102
+ }
103
+
104
+ function teardownTap(ws) {
105
+ if (ws && ws._homeClayTap && typeof ws._homeClayTap.unsubscribe === "function") {
106
+ try { ws._homeClayTap.unsubscribe(); } catch (e) {}
107
+ }
108
+ if (ws) ws._homeClayTap = null;
109
+ }
110
+
111
+ function setupTap(ws, ctx, sessionId) {
112
+ teardownTap(ws);
113
+ var sm = ctx.getSessionManager();
114
+ if (!sm || typeof sm.subscribeSession !== "function") return;
115
+ var unsubscribe = sm.subscribeSession(sessionId, function (obj) {
116
+ if (ws.readyState !== 1) return;
117
+ var transformed = transformEvent(obj);
118
+ if (!transformed) return;
119
+ try { ws.send(JSON.stringify(transformed)); } catch (e) {}
120
+ });
121
+ if (!unsubscribe) return;
122
+ ws._homeClayTap = { unsubscribe: unsubscribe, sessionId: sessionId, claySlug: ctx.slug || "" };
123
+ }
124
+
125
+ function sendError(ws, text) {
126
+ if (ws.readyState !== 1) return;
127
+ try {
128
+ ws.send(JSON.stringify({ type: "home_clay_error", text: text }));
129
+ } catch (e) {}
130
+ }
131
+
132
+ function handleMessage(ws, msg) {
133
+ if (!msg || typeof msg.type !== "string") return false;
134
+ if (msg.type !== "home_clay_open" && msg.type !== "home_clay_send" && msg.type !== "home_clay_new_session" && msg.type !== "home_clay_close") {
135
+ return false;
136
+ }
137
+
138
+ if (msg.type === "home_clay_close") {
139
+ teardownTap(ws);
140
+ return true;
141
+ }
142
+
143
+ var userId = ws._clayUser ? ws._clayUser.id : null;
144
+ if (users.isMultiUser() && !userId) {
145
+ sendError(ws, "Not authenticated.");
146
+ return true;
147
+ }
148
+
149
+ var found = findClayProject(userId, true);
150
+ if (!found) {
151
+ sendError(ws, "Clay mate not available yet — open the Mates panel once to seed.");
152
+ return true;
153
+ }
154
+
155
+ if (msg.type === "home_clay_open") {
156
+ var session = getOrCreateHomeSession(found, userId);
157
+ if (!session) {
158
+ sendError(ws, "Could not open Clay session.");
159
+ return true;
160
+ }
161
+ setupTap(ws, found.ctx, session.localId);
162
+ try {
163
+ ws.send(JSON.stringify({
164
+ type: "home_clay_history",
165
+ sessionId: session.localId,
166
+ messages: historyToHomeChat(session.history || []),
167
+ }));
168
+ } catch (e) {}
169
+ return true;
170
+ }
171
+
172
+ if (msg.type === "home_clay_new_session") {
173
+ var sm = found.ctx.getSessionManager();
174
+ if (!sm) { sendError(ws, "Session manager unavailable."); return true; }
175
+ var fresh = sm.createSession({ ownerId: userId, vendor: "claude" }, null);
176
+ setupTap(ws, found.ctx, fresh.localId);
177
+ try {
178
+ ws.send(JSON.stringify({
179
+ type: "home_clay_history",
180
+ sessionId: fresh.localId,
181
+ messages: [],
182
+ }));
183
+ } catch (e) {}
184
+ return true;
185
+ }
186
+
187
+ if (msg.type === "home_clay_send") {
188
+ var text = (msg.text || "").trim();
189
+ if (!text) return true;
190
+
191
+ var sm2 = found.ctx.getSessionManager();
192
+ if (!sm2) { sendError(ws, "Session manager unavailable."); return true; }
193
+
194
+ // Resume the tap if the WS reconnected since open.
195
+ var tap = ws._homeClayTap;
196
+ var sessionId = tap ? tap.sessionId : null;
197
+ if (!sessionId) {
198
+ var s2 = getOrCreateHomeSession(found, userId);
199
+ if (!s2) { sendError(ws, "Could not open Clay session."); return true; }
200
+ sessionId = s2.localId;
201
+ setupTap(ws, found.ctx, sessionId);
202
+ }
203
+ var session2 = sm2.sessions.get(sessionId);
204
+ if (!session2) {
205
+ sendError(ws, "Session not found: " + sessionId);
206
+ return true;
207
+ }
208
+
209
+ // Drive the SDK exactly the way the regular project user-message
210
+ // path does. The session's own subscriber forwards events back as
211
+ // home_clay_* via the tap installed above.
212
+ var sdk = found.ctx.sdk;
213
+ if (!sdk) {
214
+ sendError(ws, "Clay SDK bridge unavailable.");
215
+ return true;
216
+ }
217
+ try {
218
+ if (!session2.isProcessing) {
219
+ session2.isProcessing = true;
220
+ session2.sentToolResults = {};
221
+ if (!session2.queryInstance && (!session2.worker || session2.messageQueue !== "worker")) {
222
+ sdk.startQuery(session2, text, null, null);
223
+ } else {
224
+ sdk.pushMessage(session2, text, null);
225
+ }
226
+ } else {
227
+ sdk.pushMessage(session2, text, null);
228
+ }
229
+ } catch (e) {
230
+ sendError(ws, "Failed to dispatch: " + (e.message || String(e)));
231
+ }
232
+ return true;
233
+ }
234
+
235
+ return false;
236
+ }
237
+
238
+ function handleDisconnection(ws) {
239
+ teardownTap(ws);
240
+ }
241
+
242
+ return { handleMessage: handleMessage, handleDisconnection: handleDisconnection };
243
+ }
244
+
245
+ module.exports = { attachClayHome: attachClayHome };
@@ -116,8 +116,9 @@ function attachMates(ctx) {
116
116
  // Auto-archive Ally and any other archived built-ins for existing users
117
117
  try { mates.syncArchivedBuiltinMates(mateCtx5); } catch (e) {}
118
118
  // Ensure core built-in mates are in favorites (unless user explicitly removed them)
119
- // Auto-favorites: Clay (host agent), Arch (architect), Buzz (marketer)
120
- var coreMateKeys = ["clay", "arch", "buzz"];
119
+ // Clay is reachable via Home, not via the mate sidebar, so it is
120
+ // intentionally NOT in this list.
121
+ var coreMateKeys = ["arch", "buzz"];
121
122
  var mateList = mates.getAllMates(mateCtx5);
122
123
  var currentFavs = users.getDmFavorites(userId);
123
124
  var hiddenIds = users.getDmHidden(userId);
package/lib/server.js CHANGED
@@ -12,6 +12,7 @@ var serverAuth = require("./server-auth");
12
12
  var serverSkills = require("./server-skills");
13
13
  var serverDm = require("./server-dm");
14
14
  var serverMates = require("./server-mates");
15
+ var serverClayHome = require("./server-clay-home");
15
16
  var serverAdmin = require("./server-admin");
16
17
  var serverSettings = require("./server-settings");
17
18
  var serverPalette = require("./server-palette");
@@ -793,6 +794,11 @@ function createServer(opts) {
793
794
  unreadMap[wsSlug] = 0;
794
795
  }
795
796
  ctx.handleConnection(ws, wsUser);
797
+ // Tear down the home-chat subscription (if any) on socket close so
798
+ // we don't leak callbacks against Clay's session manager.
799
+ ws.on("close", function () {
800
+ try { clayHomeHandler.handleDisconnection(ws); } catch (e) {}
801
+ });
796
802
  });
797
803
  });
798
804
 
@@ -1075,6 +1081,14 @@ function createServer(opts) {
1075
1081
  // --- Email account handler (per-user email account management) ---
1076
1082
  var emailHandler = serverEmail.attachEmail({ users: users });
1077
1083
 
1084
+ // --- Clay home chat handler (host agent chat embedded in home hub) ---
1085
+ var clayHomeHandler = serverClayHome.attachClayHome({
1086
+ users: users,
1087
+ mates: mates,
1088
+ projects: projects,
1089
+ addProject: addProject,
1090
+ });
1091
+
1078
1092
  // --- Mate handler ---
1079
1093
  // Forward reference: mateHandler is set up after removeProject is defined
1080
1094
  var mateHandler = null;
@@ -1086,6 +1100,7 @@ function createServer(opts) {
1086
1100
  if (dmHandler.handleMessage(ws, msg)) return;
1087
1101
  if (mateHandler && mateHandler.handleMessage(ws, msg)) return;
1088
1102
  if (emailHandler.handleMessage(ws, msg)) return;
1103
+ if (clayHomeHandler.handleMessage(ws, msg)) return;
1089
1104
  }
1090
1105
 
1091
1106
  function removeProject(slug) {
package/lib/sessions.js CHANGED
@@ -584,6 +584,15 @@ function createSessionManager(opts) {
584
584
  if (!obj._ts) obj._ts = Date.now();
585
585
  session.history.push(obj);
586
586
  appendToSessionFile(session, obj);
587
+ // Per-session out-of-band subscribers (used by home-chat to mirror
588
+ // Clay session events into a parallel UI without joining the project's
589
+ // ws clients set). Subscribers receive the same obj that goes to ws
590
+ // clients; they are responsible for any transform + dispatch.
591
+ if (session._subscribers && session._subscribers.size > 0) {
592
+ for (var sub of session._subscribers) {
593
+ try { sub(obj); } catch (e) { /* swallow — subscriber is optional */ }
594
+ }
595
+ }
587
596
  if (sendEach) {
588
597
  // Multi-user: send to clients whose active session matches this one
589
598
  var data = JSON.stringify(obj);
@@ -901,6 +910,15 @@ function createSessionManager(opts) {
901
910
  saveSessionFile: saveSessionFile,
902
911
  appendToSessionFile: appendToSessionFile,
903
912
  sendAndRecord: doSendAndRecord,
913
+ subscribeSession: function (localId, cb) {
914
+ var session = sessions.get(localId);
915
+ if (!session) return null;
916
+ if (!session._subscribers) session._subscribers = new Set();
917
+ session._subscribers.add(cb);
918
+ return function unsubscribe() {
919
+ if (session._subscribers) session._subscribers.delete(cb);
920
+ };
921
+ },
904
922
  sendToSession: doSendToSession,
905
923
  findTurnBoundary: findTurnBoundary,
906
924
  replayHistory: replayHistory,
package/lib/ws-schema.js CHANGED
@@ -521,7 +521,19 @@ var schema = {
521
521
  "ralph_phase": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Current ralph wizard phase" },
522
522
  "ralph_crafting_started": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "File crafting session started" },
523
523
  "ralph_files_status": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Prompt/judge file readiness status" },
524
- "ralph_files_content": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Loop file contents (prompt and judge)" }
524
+ "ralph_files_content": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Loop file contents (prompt and judge)" },
525
+
526
+ // -----------------------------------------------------------------------
527
+ // Home Chat (Clay host agent)
528
+ // -----------------------------------------------------------------------
529
+ "home_clay_open": { direction: "c2s", handler: "lib/server-clay-home.js", description: "Open/restore the user's Clay home chat session" },
530
+ "home_clay_send": { direction: "c2s", handler: "lib/server-clay-home.js", description: "Send a message to Clay from the home chat" },
531
+ "home_clay_new_session": { direction: "c2s", handler: "lib/server-clay-home.js", description: "Start a fresh Clay home chat session" },
532
+ "home_clay_close": { direction: "c2s", handler: "lib/server-clay-home.js", description: "Tear down the home-chat tap on the user's WS" },
533
+ "home_clay_history": { direction: "s2c", handler: "lib/public/modules/home-chat.js", description: "Initial / refreshed Clay home chat history" },
534
+ "home_clay_delta": { direction: "s2c", handler: "lib/public/modules/home-chat.js", description: "Streaming assistant text delta for home chat" },
535
+ "home_clay_done": { direction: "s2c", handler: "lib/public/modules/home-chat.js", description: "Clay turn finished" },
536
+ "home_clay_error": { direction: "s2c", handler: "lib/public/modules/home-chat.js", description: "Home chat error" }
525
537
  };
526
538
 
527
539
  module.exports = { schema: schema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.38.0-beta.2",
3
+ "version": "2.38.0-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",