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

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/project.js CHANGED
@@ -855,7 +855,7 @@ function createProjectContext(opts) {
855
855
  }
856
856
 
857
857
  // --- DM messages (delegated to server-level handler) ---
858
- if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update" || msg.type === "mate_readd_builtin" || msg.type === "mate_list_available_builtins" || msg.type === "email_accounts_list" || msg.type === "email_account_add" || msg.type === "email_account_remove" || msg.type === "email_account_test") {
858
+ if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite" || msg.type === "mate_create" || msg.type === "mate_list" || msg.type === "mate_delete" || msg.type === "mate_update" || msg.type === "mate_readd_builtin" || msg.type === "mate_list_available_builtins" || msg.type === "email_accounts_list" || msg.type === "email_account_add" || msg.type === "email_account_remove" || msg.type === "email_account_test" || msg.type === "home_clay_open" || msg.type === "home_clay_send" || msg.type === "home_clay_new_session" || msg.type === "home_clay_close") {
859
859
  if (typeof opts.onDmMessage === "function") {
860
860
  opts.onDmMessage(ws, msg);
861
861
  }
@@ -6,32 +6,38 @@
6
6
  .home-chat-frame (rounded card with header / messages / input).
7
7
  Inspired by Vercel's persistent toolbar pattern. */
8
8
 
9
- /* --- FAB --- */
9
+ /* --- FAB ---
10
+ Draggable. Default position is bottom-right; user position is
11
+ restored from localStorage. Inline `top`/`left` set by JS override
12
+ the default `right`/`bottom`. */
10
13
 
11
14
  .clay-fab {
12
15
  position: fixed;
13
- right: 20px;
14
- bottom: 20px;
15
- width: 56px;
16
- height: 56px;
16
+ right: 18px;
17
+ bottom: 18px;
18
+ width: 44px;
19
+ height: 44px;
17
20
  border-radius: 50%;
18
21
  border: none;
19
22
  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;
23
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.08);
24
+ cursor: grab;
22
25
  z-index: 9000;
23
26
  display: flex;
24
27
  align-items: center;
25
28
  justify-content: center;
26
29
  padding: 0;
27
- transition: transform 0.18s cubic-bezier(.2,.7,.3,1.3), box-shadow 0.18s, opacity 0.18s;
30
+ transition: box-shadow 0.18s, opacity 0.18s, transform 0.18s cubic-bezier(.2,.7,.3,1.3);
31
+ user-select: none;
32
+ touch-action: none;
28
33
  }
29
34
  .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);
35
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 3px 6px rgba(0, 0, 0, 0.10);
32
36
  }
33
- .clay-fab:active {
34
- transform: scale(0.96);
37
+ .clay-fab.dragging {
38
+ cursor: grabbing;
39
+ transition: none;
40
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.26), 0 4px 8px rgba(0, 0, 0, 0.14);
35
41
  }
36
42
  .clay-fab.open {
37
43
  /* Hide while popover is open — the popover X button is the close path */
@@ -41,8 +47,8 @@
41
47
  }
42
48
 
43
49
  .clay-fab-icon {
44
- width: 36px;
45
- height: 36px;
50
+ width: 30px;
51
+ height: 30px;
46
52
  border-radius: 50%;
47
53
  object-fit: cover;
48
54
  pointer-events: none;
@@ -68,16 +74,16 @@
68
74
 
69
75
  .clay-popover {
70
76
  position: fixed;
71
- right: 20px;
72
- bottom: 20px;
73
- width: 380px;
74
- height: 560px;
77
+ right: 18px;
78
+ bottom: 18px;
79
+ width: 320px;
80
+ height: 480px;
75
81
  max-width: calc(100vw - 32px);
76
82
  max-height: calc(100vh - 40px);
77
83
  z-index: 9001;
78
84
  display: flex;
79
85
  transform-origin: bottom right;
80
- animation: clay-popover-in 0.22s cubic-bezier(.2,.7,.3,1.05);
86
+ animation: clay-popover-in 0.18s cubic-bezier(.2,.7,.3,1.05);
81
87
  }
82
88
  .clay-popover.hidden {
83
89
  display: none;
@@ -326,16 +332,20 @@
326
332
  inset, and the FAB shrinks slightly. */
327
333
 
328
334
  @media (max-width: 768px) {
329
- /* Lift the FAB above the mobile tab bar (56px + safe-bottom). */
330
- .clay-fab {
335
+ /* Lift the FAB above the mobile tab bar (56px + safe-bottom).
336
+ Only applies when JS hasn't placed the FAB at a custom position
337
+ (custom positions use inline top/left and override these defaults). */
338
+ .clay-fab:not(.user-positioned) {
331
339
  right: 14px;
332
340
  bottom: calc(56px + var(--safe-bottom, 0px) + 14px);
333
- width: 50px;
334
- height: 50px;
341
+ }
342
+ .clay-fab {
343
+ width: 44px;
344
+ height: 44px;
335
345
  }
336
346
  .clay-fab-icon {
337
- width: 32px;
338
- height: 32px;
347
+ width: 30px;
348
+ height: 30px;
339
349
  }
340
350
  }
341
351
 
@@ -18,6 +18,11 @@ var typingEl = null;
18
18
  var newBtnEl = null;
19
19
  var closeBtnEl = null;
20
20
 
21
+ // Drag state for the FAB.
22
+ var FAB_POS_KEY = "clay-fab-pos";
23
+ var DRAG_THRESHOLD_PX = 5;
24
+ var dragState = null;
25
+
21
26
  // Per-turn assembly state. Server may emit many delta events for a single
22
27
  // assistant turn; we accumulate text and render incrementally into the
23
28
  // last bubble.
@@ -40,8 +45,20 @@ export function initHomeChat() {
40
45
 
41
46
  if (!fabBtn || !popoverEl || !messagesEl || !inputEl || !sendBtn) return;
42
47
 
43
- // --- FAB toggle ---
44
- fabBtn.addEventListener("click", toggleOpen);
48
+ // --- Restore persisted FAB position ---
49
+ restoreFabPosition();
50
+ // Re-clamp on viewport resize so a saved position from a wider window
51
+ // doesn't strand the FAB off-screen.
52
+ window.addEventListener("resize", function () {
53
+ if (fabBtn.classList.contains("user-positioned")) clampFabIntoView();
54
+ });
55
+
56
+ // --- FAB drag + click ---
57
+ // mousedown/touchstart begins a potential drag. We only treat it as a
58
+ // click (toggle popover) if the pointer didn't move past DRAG_THRESHOLD_PX.
59
+ fabBtn.addEventListener("mousedown", onPointerDown);
60
+ fabBtn.addEventListener("touchstart", onPointerDown, { passive: false });
61
+
45
62
  if (closeBtnEl) closeBtnEl.addEventListener("click", closePopover);
46
63
 
47
64
  // ESC closes the popover.
@@ -77,9 +94,159 @@ export function initHomeChat() {
77
94
  }
78
95
  }
79
96
 
97
+ // --- FAB drag mechanics ---
98
+
99
+ function getPointer(e) {
100
+ if (e.touches && e.touches.length > 0) {
101
+ return { x: e.touches[0].clientX, y: e.touches[0].clientY };
102
+ }
103
+ return { x: e.clientX, y: e.clientY };
104
+ }
105
+
106
+ function onPointerDown(e) {
107
+ // Ignore right-clicks and modifier-clicks.
108
+ if (e.button && e.button !== 0) return;
109
+ var p = getPointer(e);
110
+ var rect = fabBtn.getBoundingClientRect();
111
+ dragState = {
112
+ startX: p.x,
113
+ startY: p.y,
114
+ offsetX: p.x - rect.left,
115
+ offsetY: p.y - rect.top,
116
+ moved: false,
117
+ };
118
+ document.addEventListener("mousemove", onPointerMove);
119
+ document.addEventListener("mouseup", onPointerUp);
120
+ document.addEventListener("touchmove", onPointerMove, { passive: false });
121
+ document.addEventListener("touchend", onPointerUp);
122
+ document.addEventListener("touchcancel", onPointerUp);
123
+ // Don't preventDefault yet — we let the browser distinguish between a
124
+ // tap (which should fire click → toggle) and a drag.
125
+ }
126
+
127
+ function onPointerMove(e) {
128
+ if (!dragState) return;
129
+ var p = getPointer(e);
130
+ var dx = p.x - dragState.startX;
131
+ var dy = p.y - dragState.startY;
132
+ if (!dragState.moved && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD_PX) {
133
+ return;
134
+ }
135
+ dragState.moved = true;
136
+ fabBtn.classList.add("dragging");
137
+ // Touch needs explicit prevent so the page doesn't scroll.
138
+ if (e.cancelable) e.preventDefault();
139
+ var x = p.x - dragState.offsetX;
140
+ var y = p.y - dragState.offsetY;
141
+ setFabPosition(x, y);
142
+ }
143
+
144
+ function onPointerUp() {
145
+ document.removeEventListener("mousemove", onPointerMove);
146
+ document.removeEventListener("mouseup", onPointerUp);
147
+ document.removeEventListener("touchmove", onPointerMove);
148
+ document.removeEventListener("touchend", onPointerUp);
149
+ document.removeEventListener("touchcancel", onPointerUp);
150
+ if (!dragState) return;
151
+ if (dragState.moved) {
152
+ fabBtn.classList.remove("dragging");
153
+ persistFabPosition();
154
+ clampFabIntoView();
155
+ } else {
156
+ // Pure tap → toggle popover.
157
+ toggleOpen();
158
+ }
159
+ dragState = null;
160
+ }
161
+
162
+ function setFabPosition(x, y) {
163
+ // Ensure the FAB stays within the viewport with a small margin.
164
+ var margin = 4;
165
+ var w = fabBtn.offsetWidth;
166
+ var h = fabBtn.offsetHeight;
167
+ var maxX = window.innerWidth - w - margin;
168
+ var maxY = window.innerHeight - h - margin;
169
+ if (x < margin) x = margin;
170
+ if (y < margin) y = margin;
171
+ if (x > maxX) x = maxX;
172
+ if (y > maxY) y = maxY;
173
+ fabBtn.classList.add("user-positioned");
174
+ fabBtn.style.left = x + "px";
175
+ fabBtn.style.top = y + "px";
176
+ fabBtn.style.right = "auto";
177
+ fabBtn.style.bottom = "auto";
178
+ // If the popover is open, keep it anchored to the FAB.
179
+ if (openState) anchorPopoverToFab();
180
+ }
181
+
182
+ function clampFabIntoView() {
183
+ if (!fabBtn) return;
184
+ var rect = fabBtn.getBoundingClientRect();
185
+ setFabPosition(rect.left, rect.top);
186
+ }
187
+
188
+ function persistFabPosition() {
189
+ if (!fabBtn) return;
190
+ try {
191
+ var rect = fabBtn.getBoundingClientRect();
192
+ localStorage.setItem(FAB_POS_KEY, JSON.stringify({ x: rect.left, y: rect.top }));
193
+ } catch (e) {}
194
+ }
195
+
196
+ function restoreFabPosition() {
197
+ try {
198
+ var raw = localStorage.getItem(FAB_POS_KEY);
199
+ if (!raw) return;
200
+ var pos = JSON.parse(raw);
201
+ if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
202
+ setFabPosition(pos.x, pos.y);
203
+ }
204
+ } catch (e) {}
205
+ }
206
+
207
+ // Anchor the popover so its corner sits next to the FAB. We pick the
208
+ // corner that gives the most room — popover opens "into" the screen,
209
+ // not off-screen.
210
+ function anchorPopoverToFab() {
211
+ if (!fabBtn || !popoverEl) return;
212
+ var fr = fabBtn.getBoundingClientRect();
213
+ var pw = popoverEl.offsetWidth || 320;
214
+ var ph = popoverEl.offsetHeight || 480;
215
+ var margin = 12;
216
+ // Decide vertical: open above FAB if there's more room above.
217
+ var roomAbove = fr.top;
218
+ var roomBelow = window.innerHeight - fr.bottom;
219
+ var openUp = roomAbove >= roomBelow;
220
+ // Decide horizontal: align right edge of popover with right edge of FAB
221
+ // when the FAB is on the right half of the screen, else left edge.
222
+ var fabRightSide = (fr.left + fr.width / 2) > window.innerWidth / 2;
223
+
224
+ var top, left;
225
+ if (openUp) {
226
+ top = Math.max(margin, fr.top - ph - 8);
227
+ popoverEl.style.transformOrigin = fabRightSide ? "bottom right" : "bottom left";
228
+ } else {
229
+ top = Math.min(window.innerHeight - ph - margin, fr.bottom + 8);
230
+ popoverEl.style.transformOrigin = fabRightSide ? "top right" : "top left";
231
+ }
232
+ if (fabRightSide) {
233
+ left = Math.max(margin, fr.right - pw);
234
+ } else {
235
+ left = Math.min(window.innerWidth - pw - margin, fr.left);
236
+ }
237
+ popoverEl.style.top = top + "px";
238
+ popoverEl.style.left = left + "px";
239
+ popoverEl.style.right = "auto";
240
+ popoverEl.style.bottom = "auto";
241
+ }
242
+
80
243
  function openPopover() {
81
244
  if (!popoverEl || openState) return;
82
245
  openState = true;
246
+ // Anchor BEFORE unhiding so the slide-up animation uses the correct
247
+ // transform-origin (top vs bottom, left vs right) for the FAB's
248
+ // current corner.
249
+ anchorPopoverToFab();
83
250
  popoverEl.classList.remove("hidden");
84
251
  if (fabBtn) fabBtn.classList.add("open");
85
252
  // Pull session history on first open. If WS isn't ready yet, leave
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.38.0-beta.4",
3
+ "version": "2.38.0-beta.6",
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",