clay-server 2.38.0-beta.1 → 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.
- package/lib/builtin-mates.js +1 -1
- package/lib/mates.js +11 -0
- package/lib/public/css/home-chat.css +293 -0
- package/lib/public/css/home-hub.css +7 -38
- package/lib/public/index.html +33 -0
- package/lib/public/modules/app-home-hub.js +10 -33
- package/lib/public/modules/app-messages.js +17 -0
- package/lib/public/modules/home-chat.js +239 -0
- package/lib/public/modules/sidebar-mates.js +3 -4
- package/lib/public/style.css +1 -0
- package/lib/server-clay-home.js +245 -0
- package/lib/server-mates.js +3 -2
- package/lib/server.js +15 -0
- package/lib/sessions.js +18 -0
- package/lib/ws-schema.js +13 -1
- package/package.json +1 -1
package/lib/builtin-mates.js
CHANGED
|
@@ -20,7 +20,7 @@ var BUILTIN_MATES = [
|
|
|
20
20
|
bio: "Your workspace memory. Searches every session, project, and decision you've made and answers from the receipts. The chat surface for the home screen.",
|
|
21
21
|
avatarColor: "#7c3aed",
|
|
22
22
|
avatarStyle: "bottts",
|
|
23
|
-
avatarCustom: "/
|
|
23
|
+
avatarCustom: "/icon-banded-76.png", // Clay is the app — use the app icon
|
|
24
24
|
avatarLocked: true,
|
|
25
25
|
primary: true, // code-managed, auto-updated on startup
|
|
26
26
|
globalSearch: true, // searches all mates' sessions and projects
|
package/lib/mates.js
CHANGED
|
@@ -642,6 +642,17 @@ function syncPrimaryMates(ctx) {
|
|
|
642
642
|
if (mate.profile.displayName !== def.displayName) {
|
|
643
643
|
mate.profile.displayName = def.displayName; changed = true;
|
|
644
644
|
}
|
|
645
|
+
// Locked avatars are code-managed — refresh them from the def so a
|
|
646
|
+
// built-in's avatar can be updated by changing the source file
|
|
647
|
+
// (e.g. Clay flipping from a stale Ally asset to the app icon).
|
|
648
|
+
if (def.avatarLocked) {
|
|
649
|
+
if (def.avatarCustom != null && mate.profile.avatarCustom !== def.avatarCustom) {
|
|
650
|
+
mate.profile.avatarCustom = def.avatarCustom; changed = true;
|
|
651
|
+
}
|
|
652
|
+
if (def.avatarColor && mate.profile.avatarColor !== def.avatarColor) {
|
|
653
|
+
mate.profile.avatarColor = def.avatarColor; changed = true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
645
656
|
}
|
|
646
657
|
|
|
647
658
|
// --- Sync CLAUDE.md identity (only when templateVersion changes) ---
|
|
@@ -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:
|
|
11
|
-
align-items:
|
|
12
|
-
overflow
|
|
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;
|
package/lib/public/index.html
CHANGED
|
@@ -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
|
-
//
|
|
578
|
-
// the right. The
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
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;
|
package/lib/public/style.css
CHANGED
|
@@ -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 };
|
package/lib/server-mates.js
CHANGED
|
@@ -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
|
-
//
|
|
120
|
-
|
|
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