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

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.
@@ -1,35 +1,102 @@
1
1
  /* ==========================================================================
2
- Home Chat (Clay) — phablet-style mini chat embedded in the home hub
2
+ Clay FAB + popover — phablet-style chat reachable from anywhere
3
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;
4
+ The FAB (#clay-fab) sits at the bottom-right corner over all page
5
+ content. Clicking it toggles #clay-popover, which contains the
6
+ .home-chat-frame (rounded card with header / messages / input).
7
+ Inspired by Vercel's persistent toolbar pattern. */
8
+
9
+ /* --- FAB --- */
10
+
11
+ .clay-fab {
12
+ position: fixed;
13
+ right: 20px;
14
+ bottom: 20px;
15
+ width: 56px;
16
+ height: 56px;
17
+ border-radius: 50%;
18
+ border: none;
19
+ background: var(--bg, #fff);
20
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18), 0 2px 4px rgba(0, 0, 0, 0.08);
21
+ cursor: pointer;
22
+ z-index: 9000;
14
23
  display: flex;
15
- align-items: stretch;
24
+ align-items: center;
16
25
  justify-content: center;
17
- padding: 32px 16px 24px;
18
- background: var(--bg);
19
- border-right: 1px solid var(--border);
26
+ padding: 0;
27
+ transition: transform 0.18s cubic-bezier(.2,.7,.3,1.3), box-shadow 0.18s, opacity 0.18s;
28
+ }
29
+ .clay-fab:hover {
30
+ transform: translateY(-1px) scale(1.04);
31
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), 0 3px 6px rgba(0, 0, 0, 0.10);
32
+ }
33
+ .clay-fab:active {
34
+ transform: scale(0.96);
35
+ }
36
+ .clay-fab.open {
37
+ /* Hide while popover is open — the popover X button is the close path */
38
+ transform: scale(0.6);
39
+ opacity: 0;
40
+ pointer-events: none;
41
+ }
42
+
43
+ .clay-fab-icon {
44
+ width: 36px;
45
+ height: 36px;
46
+ border-radius: 50%;
47
+ object-fit: cover;
48
+ pointer-events: none;
20
49
  }
21
50
 
22
- .home-chat-frame {
51
+ /* Subtle pulse ring to draw the eye on first paint */
52
+ .clay-fab-pulse {
53
+ position: absolute;
54
+ inset: -2px;
55
+ border-radius: 50%;
56
+ border: 2px solid var(--accent);
57
+ opacity: 0;
58
+ pointer-events: none;
59
+ animation: clay-fab-pulse 2.4s ease-out 1s 2;
60
+ }
61
+ @keyframes clay-fab-pulse {
62
+ 0% { opacity: 0; transform: scale(1); }
63
+ 20% { opacity: 0.55; }
64
+ 100% { opacity: 0; transform: scale(1.6); }
65
+ }
66
+
67
+ /* --- Popover --- */
68
+
69
+ .clay-popover {
70
+ position: fixed;
71
+ right: 20px;
72
+ bottom: 20px;
73
+ width: 380px;
74
+ height: 560px;
75
+ max-width: calc(100vw - 32px);
76
+ max-height: calc(100vh - 40px);
77
+ z-index: 9001;
78
+ display: flex;
79
+ transform-origin: bottom right;
80
+ animation: clay-popover-in 0.22s cubic-bezier(.2,.7,.3,1.05);
81
+ }
82
+ .clay-popover.hidden {
83
+ display: none;
84
+ }
85
+ @keyframes clay-popover-in {
86
+ from { transform: translateY(8px) scale(0.96); opacity: 0; }
87
+ to { transform: translateY(0) scale(1); opacity: 1; }
88
+ }
89
+
90
+ .clay-popover .home-chat-frame {
23
91
  width: 100%;
24
- max-width: 480px;
92
+ height: 100%;
25
93
  display: flex;
26
94
  flex-direction: column;
27
95
  background: var(--bg-alt, var(--bg));
28
96
  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);
97
+ border-radius: 18px;
98
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22), 0 4px 10px rgba(0, 0, 0, 0.10);
31
99
  overflow: hidden;
32
- height: 100%;
33
100
  }
34
101
 
35
102
  /* --- Header --- */
@@ -37,15 +104,15 @@
37
104
  .home-chat-header {
38
105
  display: flex;
39
106
  align-items: center;
40
- gap: 12px;
41
- padding: 14px 16px;
107
+ gap: 10px;
108
+ padding: 12px 12px 12px 14px;
42
109
  border-bottom: 1px solid var(--border);
43
110
  background: var(--bg);
44
111
  }
45
112
 
46
113
  .home-chat-avatar-wrap {
47
- width: 38px;
48
- height: 38px;
114
+ width: 34px;
115
+ height: 34px;
49
116
  border-radius: 50%;
50
117
  overflow: hidden;
51
118
  flex-shrink: 0;
@@ -68,14 +135,14 @@
68
135
  }
69
136
 
70
137
  .home-chat-title {
71
- font-size: 15px;
138
+ font-size: 14px;
72
139
  font-weight: 600;
73
140
  color: var(--text);
74
141
  line-height: 1.2;
75
142
  }
76
143
 
77
144
  .home-chat-subtitle {
78
- font-size: 12px;
145
+ font-size: 11px;
79
146
  color: var(--text-muted, var(--text-dimmer));
80
147
  line-height: 1.2;
81
148
  margin-top: 2px;
@@ -87,8 +154,8 @@
87
154
  .home-chat-icon-btn {
88
155
  background: none;
89
156
  border: none;
90
- width: 32px;
91
- height: 32px;
157
+ width: 30px;
158
+ height: 30px;
92
159
  border-radius: 8px;
93
160
  display: flex;
94
161
  align-items: center;
@@ -102,8 +169,8 @@
102
169
  color: var(--text);
103
170
  }
104
171
  .home-chat-icon-btn .lucide {
105
- width: 18px;
106
- height: 18px;
172
+ width: 16px;
173
+ height: 16px;
107
174
  }
108
175
 
109
176
  /* --- Messages list --- */
@@ -111,19 +178,19 @@
111
178
  .home-chat-messages {
112
179
  flex: 1;
113
180
  overflow-y: auto;
114
- padding: 16px 14px;
181
+ padding: 14px 12px;
115
182
  display: flex;
116
183
  flex-direction: column;
117
- gap: 10px;
184
+ gap: 8px;
118
185
  background: var(--input-bg, var(--bg));
119
186
  scroll-behavior: smooth;
120
187
  }
121
188
 
122
189
  .home-chat-bubble {
123
190
  max-width: 88%;
124
- padding: 10px 14px;
125
- border-radius: 18px;
126
- font-size: 14px;
191
+ padding: 9px 12px;
192
+ border-radius: 16px;
193
+ font-size: 13px;
127
194
  line-height: 1.5;
128
195
  word-wrap: break-word;
129
196
  white-space: pre-wrap;
@@ -147,15 +214,15 @@
147
214
 
148
215
  .home-chat-bubble-system {
149
216
  align-self: center;
150
- font-size: 12px;
217
+ font-size: 11px;
151
218
  color: var(--text-muted, var(--text-dimmer));
152
219
  background: transparent;
153
220
  padding: 4px 10px;
154
221
  font-style: italic;
222
+ text-align: center;
223
+ max-width: 100%;
155
224
  }
156
225
 
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
226
  .home-chat-ref {
160
227
  display: inline-block;
161
228
  background: rgba(124, 58, 237, 0.08);
@@ -163,7 +230,7 @@
163
230
  padding: 1px 8px;
164
231
  border-radius: 10px;
165
232
  font-family: "Roboto Mono", "Courier New", monospace;
166
- font-size: 12px;
233
+ font-size: 11px;
167
234
  cursor: pointer;
168
235
  margin: 0 2px;
169
236
  border: 1px solid rgba(124, 58, 237, 0.18);
@@ -177,14 +244,14 @@
177
244
  .home-chat-typing {
178
245
  display: flex;
179
246
  gap: 4px;
180
- padding: 6px 18px 0;
247
+ padding: 4px 16px 0;
181
248
  align-items: center;
182
249
  }
183
250
  .home-chat-typing.hidden { display: none; }
184
251
 
185
252
  .home-chat-typing-dot {
186
- width: 6px;
187
- height: 6px;
253
+ width: 5px;
254
+ height: 5px;
188
255
  border-radius: 50%;
189
256
  background: var(--text-dimmer);
190
257
  animation: home-chat-bounce 1.2s infinite ease-in-out;
@@ -204,7 +271,7 @@
204
271
  display: flex;
205
272
  align-items: flex-end;
206
273
  gap: 8px;
207
- padding: 10px 12px 12px;
274
+ padding: 10px 10px 12px;
208
275
  border-top: 1px solid var(--border);
209
276
  background: var(--bg);
210
277
  }
@@ -212,15 +279,15 @@
212
279
  .home-chat-input {
213
280
  flex: 1;
214
281
  border: 1px solid var(--border);
215
- border-radius: 18px;
216
- padding: 10px 14px;
282
+ border-radius: 16px;
283
+ padding: 9px 12px;
217
284
  background: var(--input-bg, var(--bg));
218
285
  color: var(--text);
219
- font-size: 14px;
286
+ font-size: 13px;
220
287
  line-height: 1.5;
221
288
  font-family: inherit;
222
289
  resize: none;
223
- max-height: 140px;
290
+ max-height: 120px;
224
291
  outline: none;
225
292
  transition: border-color 0.15s, box-shadow 0.15s;
226
293
  }
@@ -231,8 +298,8 @@
231
298
 
232
299
  .home-chat-send-btn {
233
300
  flex-shrink: 0;
234
- width: 38px;
235
- height: 38px;
301
+ width: 34px;
302
+ height: 34px;
236
303
  border-radius: 50%;
237
304
  border: none;
238
305
  background: var(--accent);
@@ -247,47 +314,57 @@
247
314
  opacity: 0.4;
248
315
  cursor: default;
249
316
  }
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
- }
317
+ .home-chat-send-btn:not(:disabled):hover { opacity: 0.9; }
318
+ .home-chat-send-btn:not(:disabled):active { transform: scale(0.95); }
256
319
  .home-chat-send-btn .lucide {
257
- width: 18px;
258
- height: 18px;
320
+ width: 16px;
321
+ height: 16px;
259
322
  }
260
323
 
261
- /* --- Right widget pane --- */
324
+ /* --- Mobile / narrow ---
325
+ On phones the popover fills the screen edge-to-edge with a small
326
+ inset, and the FAB shrinks slightly. */
262
327
 
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;
328
+ @media (max-width: 768px) {
329
+ /* Lift the FAB above the mobile tab bar (56px + safe-bottom). */
330
+ .clay-fab {
331
+ right: 14px;
332
+ bottom: calc(56px + var(--safe-bottom, 0px) + 14px);
333
+ width: 50px;
334
+ height: 50px;
335
+ }
336
+ .clay-fab-icon {
337
+ width: 32px;
338
+ height: 32px;
339
+ }
271
340
  }
272
341
 
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 {
342
+ @media (max-width: 600px) {
343
+ .clay-popover {
344
+ right: 8px;
345
+ bottom: calc(56px + var(--safe-bottom, 0px) + 8px);
346
+ left: 8px;
280
347
  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;
348
+ height: 70vh;
349
+ max-height: calc(100vh - 80px - var(--safe-bottom, 0px));
350
+ }
351
+ .clay-popover .home-chat-frame {
352
+ border-radius: 16px;
288
353
  }
289
- .home-hub-inner {
290
- flex: 1 1 auto;
291
- padding: 24px 16px;
354
+ }
355
+
356
+ /* When the mobile tab bar is hidden by the keyboard, drop the FAB back
357
+ down so it doesn't float in dead space. */
358
+ @media (max-width: 768px) {
359
+ #mobile-tab-bar.keyboard-hidden ~ .clay-fab,
360
+ body:has(#mobile-tab-bar.keyboard-hidden) .clay-fab {
361
+ bottom: 14px;
292
362
  }
293
363
  }
364
+
365
+ /* Hide FAB while user is on auth/setup pages or any modal-heavy screens.
366
+ Add the class .clay-fab-suppressed on <body> when needed. */
367
+ body.clay-fab-suppressed .clay-fab,
368
+ body.clay-fab-suppressed .clay-popover {
369
+ display: none !important;
370
+ }
@@ -3,18 +3,18 @@
3
3
  ========================================================================== */
4
4
 
5
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
+ Pure widget surface Clay chat is reachable via the global FAB
7
+ instead of being embedded here. */
9
8
  #home-hub {
10
9
  position: absolute;
11
10
  inset: 0;
12
11
  display: flex;
13
- flex-direction: row;
14
- align-items: stretch;
15
- overflow: hidden;
12
+ flex-direction: column;
13
+ align-items: center;
14
+ overflow-y: auto;
16
15
  background: var(--bg);
17
16
  z-index: 200;
17
+ padding: 48px 24px 40px;
18
18
  border-top-left-radius: 8px;
19
19
  animation: hubFadeIn 0.35s ease;
20
20
  }
@@ -115,39 +115,6 @@
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>
151
118
  <button id="home-hub-close" class="home-hub-close-btn hidden">
152
119
  <i data-lucide="x"></i>
153
120
  <span>ESC</span>
@@ -2326,5 +2293,49 @@
2326
2293
  </div>
2327
2294
  </div>
2328
2295
  </div>
2296
+
2297
+ <!-- === Clay FAB + Popover ===
2298
+ Persistent floating-action button (bottom-right) that opens a
2299
+ phablet-style chat with Clay (the host agent). The popover overlays
2300
+ the page content and does NOT interfere with the active project
2301
+ session — talks via dedicated home_clay_* WS messages. -->
2302
+ <button id="clay-fab" class="clay-fab" type="button" aria-label="Open Clay" title="Ask Clay">
2303
+ <img class="clay-fab-icon" src="/icon-banded-76.png" alt="Clay">
2304
+ <span class="clay-fab-pulse"></span>
2305
+ </button>
2306
+
2307
+ <div id="clay-popover" class="clay-popover hidden" role="dialog" aria-modal="false" aria-labelledby="home-chat-title-text">
2308
+ <div class="home-chat-frame">
2309
+ <div class="home-chat-header">
2310
+ <div class="home-chat-avatar-wrap">
2311
+ <img class="home-chat-avatar" src="/icon-banded-76.png" alt="Clay">
2312
+ </div>
2313
+ <div class="home-chat-title-block">
2314
+ <div class="home-chat-title" id="home-chat-title-text">Clay</div>
2315
+ <div class="home-chat-subtitle">Your workspace memory</div>
2316
+ </div>
2317
+ <button id="home-chat-new-btn" class="home-chat-icon-btn" title="Start a new conversation" aria-label="New conversation">
2318
+ <i data-lucide="plus"></i>
2319
+ </button>
2320
+ <button id="home-chat-close-btn" class="home-chat-icon-btn" title="Close" aria-label="Close">
2321
+ <i data-lucide="x"></i>
2322
+ </button>
2323
+ </div>
2324
+ <div id="home-chat-messages" class="home-chat-messages">
2325
+ <!-- messages rendered by home-chat.js -->
2326
+ </div>
2327
+ <div class="home-chat-typing hidden" id="home-chat-typing">
2328
+ <span class="home-chat-typing-dot"></span>
2329
+ <span class="home-chat-typing-dot"></span>
2330
+ <span class="home-chat-typing-dot"></span>
2331
+ </div>
2332
+ <div class="home-chat-input-row">
2333
+ <textarea id="home-chat-input" class="home-chat-input" rows="1" placeholder="Ask Clay…" autocomplete="off"></textarea>
2334
+ <button id="home-chat-send-btn" class="home-chat-send-btn" disabled aria-label="Send">
2335
+ <i data-lucide="arrow-up"></i>
2336
+ </button>
2337
+ </div>
2338
+ </div>
2339
+ </div>
2329
2340
  </body>
2330
2341
  </html>
@@ -574,18 +574,11 @@ function renderHomeHubMates() {
574
574
  }
575
575
 
576
576
  export function showHomeHub() {
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.
577
+ // Home hub is a pure widget surface. Clay chat is reachable from
578
+ // anywhere via the persistent FAB (#clay-fab), not embedded here.
582
579
  if (store.get('dmMode')) exitDmMode();
583
580
  homeHubVisible = true;
584
581
  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) {}
589
582
  // Show close button only if there's a project to return to
590
583
  if (hubCloseBtn) {
591
584
  if (store.get('currentSlug')) hubCloseBtn.classList.remove("hidden");
@@ -1,46 +1,55 @@
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.
1
+ // Clay FAB + popover chat — phablet-style, persistent across the app.
2
+ // Self-contained: own DOM, own renderer, own WS protocol (home_clay_*).
4
3
  // Does not interfere with the active project session.
5
4
 
6
5
  import { escapeHtml } from './utils.js';
7
6
  import { getWs } from './ws-ref.js';
8
7
  import { renderMarkdown } from './markdown.js';
9
- import { refreshIcons } from './icons.js';
10
8
  import { switchProject } from './app-projects.js';
11
9
 
12
10
  var initialized = false;
11
+ var openState = false;
12
+ var fabBtn = null;
13
+ var popoverEl = null;
13
14
  var messagesEl = null;
14
15
  var inputEl = null;
15
16
  var sendBtn = null;
16
17
  var typingEl = null;
17
18
  var newBtnEl = null;
19
+ var closeBtnEl = null;
18
20
 
19
21
  // Per-turn assembly state. Server may emit many delta events for a single
20
22
  // assistant turn; we accumulate text and render incrementally into the
21
23
  // last bubble.
22
24
  var currentAssistantBubble = null;
23
25
  var currentAssistantText = "";
24
- var lastSenderWasUser = false;
26
+ var openedOnce = false; // gate the initial home_clay_open request
25
27
 
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
28
  export function initHomeChat() {
30
- if (initialized) {
31
- // Re-mount idempotent: just ensure the WS subscription is open.
32
- requestSession();
33
- return;
34
- }
29
+ if (initialized) return;
35
30
  initialized = true;
36
31
 
32
+ fabBtn = document.getElementById("clay-fab");
33
+ popoverEl = document.getElementById("clay-popover");
37
34
  messagesEl = document.getElementById("home-chat-messages");
38
35
  inputEl = document.getElementById("home-chat-input");
39
36
  sendBtn = document.getElementById("home-chat-send-btn");
40
37
  typingEl = document.getElementById("home-chat-typing");
41
38
  newBtnEl = document.getElementById("home-chat-new-btn");
39
+ closeBtnEl = document.getElementById("home-chat-close-btn");
40
+
41
+ if (!fabBtn || !popoverEl || !messagesEl || !inputEl || !sendBtn) return;
42
42
 
43
- if (!messagesEl || !inputEl || !sendBtn) return;
43
+ // --- FAB toggle ---
44
+ fabBtn.addEventListener("click", toggleOpen);
45
+ if (closeBtnEl) closeBtnEl.addEventListener("click", closePopover);
46
+
47
+ // ESC closes the popover.
48
+ document.addEventListener("keydown", function (e) {
49
+ if (e.key === "Escape" && openState) {
50
+ closePopover();
51
+ }
52
+ });
44
53
 
45
54
  // --- Input handling ---
46
55
  inputEl.addEventListener("input", function () {
@@ -62,19 +71,49 @@ export function initHomeChat() {
62
71
  messagesEl.innerHTML = "";
63
72
  currentAssistantBubble = null;
64
73
  currentAssistantText = "";
65
- lastSenderWasUser = false;
66
74
  hideTyping();
67
75
  addSystemBubble("New conversation started.");
68
76
  });
69
77
  }
78
+ }
79
+
80
+ function openPopover() {
81
+ if (!popoverEl || openState) return;
82
+ openState = true;
83
+ popoverEl.classList.remove("hidden");
84
+ if (fabBtn) fabBtn.classList.add("open");
85
+ // Pull session history on first open. If WS isn't ready yet, leave
86
+ // openedOnce false so the next open retries.
87
+ if (!openedOnce) {
88
+ var ws = getWs();
89
+ if (ws && ws.readyState === 1) {
90
+ openedOnce = true;
91
+ requestSession();
92
+ } else {
93
+ addSystemBubble("Connecting…");
94
+ }
95
+ }
96
+ // Focus the input so the user can start typing immediately.
97
+ setTimeout(function () { if (inputEl) inputEl.focus(); }, 60);
98
+ }
99
+
100
+ function closePopover() {
101
+ if (!openState) return;
102
+ openState = false;
103
+ if (popoverEl) popoverEl.classList.add("hidden");
104
+ if (fabBtn) {
105
+ fabBtn.classList.remove("open");
106
+ fabBtn.focus();
107
+ }
108
+ }
70
109
 
71
- // --- Initial state pull ---
72
- requestSession();
110
+ function toggleOpen() {
111
+ if (openState) closePopover(); else openPopover();
73
112
  }
74
113
 
75
114
  function autoResize() {
76
115
  inputEl.style.height = "auto";
77
- inputEl.style.height = Math.min(140, inputEl.scrollHeight) + "px";
116
+ inputEl.style.height = Math.min(120, inputEl.scrollHeight) + "px";
78
117
  }
79
118
 
80
119
  function requestSession() {
@@ -102,14 +141,12 @@ function doSend() {
102
141
  // --- Rendering ---
103
142
 
104
143
  function addUserBubble(text) {
105
- // Finalize any open assistant bubble before adding the next user turn.
106
144
  finalizeAssistant();
107
145
  var bubble = document.createElement("div");
108
146
  bubble.className = "home-chat-bubble home-chat-bubble-user";
109
147
  bubble.textContent = text;
110
148
  messagesEl.appendChild(bubble);
111
149
  scrollToBottom();
112
- lastSenderWasUser = true;
113
150
  }
114
151
 
115
152
  function ensureAssistantBubble() {
@@ -119,21 +156,18 @@ function ensureAssistantBubble() {
119
156
  messagesEl.appendChild(bubble);
120
157
  currentAssistantBubble = bubble;
121
158
  currentAssistantText = "";
122
- lastSenderWasUser = false;
123
159
  return bubble;
124
160
  }
125
161
 
126
162
  function appendAssistantText(text) {
127
163
  var bubble = ensureAssistantBubble();
128
164
  currentAssistantText += text;
129
- // Render markdown + linkify session refs after sanitization.
130
165
  bubble.innerHTML = linkifyRefs(renderMarkdown(currentAssistantText));
131
166
  scrollToBottom();
132
167
  }
133
168
 
134
169
  function finalizeAssistant() {
135
170
  if (currentAssistantBubble && !currentAssistantText) {
136
- // Empty assistant turn (no text produced). Drop the empty bubble.
137
171
  currentAssistantBubble.remove();
138
172
  }
139
173
  currentAssistantBubble = null;
@@ -148,11 +182,9 @@ function addSystemBubble(text) {
148
182
  scrollToBottom();
149
183
  }
150
184
 
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
185
  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_.
186
+ // Match [slug/sess_id - date]. Conservative: slug is alphanumeric/-/_,
187
+ // sess id starts with sess_.
156
188
  var re = /\[([a-zA-Z0-9_\-]+)\/(sess_[a-zA-Z0-9_\-]+)(?:\s+[—-]\s+([0-9]{4}-[0-9]{2}-[0-9]{2}))?\]/g;
157
189
  return html.replace(re, function (_full, slug, sessId, date) {
158
190
  var label = slug + "/" + sessId.substring(0, 14) + (date ? " · " + date : "");
@@ -162,18 +194,13 @@ function linkifyRefs(html) {
162
194
 
163
195
  function scrollToBottom() {
164
196
  if (!messagesEl) return;
165
- // Always pin: home chat is short, no need for scroll-up detection.
166
197
  requestAnimationFrame(function () {
167
198
  messagesEl.scrollTop = messagesEl.scrollHeight;
168
199
  });
169
200
  }
170
201
 
171
- function showTyping() {
172
- if (typingEl) typingEl.classList.remove("hidden");
173
- }
174
- function hideTyping() {
175
- if (typingEl) typingEl.classList.add("hidden");
176
- }
202
+ function showTyping() { if (typingEl) typingEl.classList.remove("hidden"); }
203
+ function hideTyping() { if (typingEl) typingEl.classList.add("hidden"); }
177
204
 
178
205
  // --- Server message handlers (called from app-messages.js dispatcher) ---
179
206
 
@@ -193,7 +220,6 @@ export function handleHomeClayHistory(msg) {
193
220
  if (e.role === "user") {
194
221
  addUserBubble(e.text || "");
195
222
  } else if (e.role === "assistant") {
196
- // Replay finalized assistant text in one shot.
197
223
  appendAssistantText(e.text || "");
198
224
  finalizeAssistant();
199
225
  }
@@ -223,17 +249,18 @@ document.addEventListener("click", function (e) {
223
249
  if (!chip) return;
224
250
  var slug = chip.dataset.slug;
225
251
  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.
252
+ closePopover();
229
253
  if (typeof switchProject === "function") {
230
- var hubBtn = document.getElementById("home-hub-close");
231
- if (hubBtn) hubBtn.click();
232
254
  switchProject(slug);
233
255
  }
234
256
  });
235
257
 
236
- // Expose init for app-home-hub.js without adding an import edge.
237
- if (typeof window !== "undefined") {
238
- window.__initHomeChat = initHomeChat;
258
+ // --- Initialize on DOM ready ---
259
+
260
+ if (typeof document !== "undefined") {
261
+ if (document.readyState === "loading") {
262
+ document.addEventListener("DOMContentLoaded", initHomeChat);
263
+ } else {
264
+ initHomeChat();
265
+ }
239
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.38.0-beta.3",
3
+ "version": "2.38.0-beta.4",
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",