clay-server 5.0.0-beta.2 → 5.0.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.
Files changed (50) hide show
  1. package/lib/daemon.js +9 -0
  2. package/lib/new-session-default.js +45 -0
  3. package/lib/project-connection.js +4 -2
  4. package/lib/project-session-creation.js +107 -0
  5. package/lib/project-session-delete.js +21 -3
  6. package/lib/project-session-folder-delete.js +169 -0
  7. package/lib/project-session-folders.js +272 -0
  8. package/lib/project-sessions.js +46 -26
  9. package/lib/project.js +2 -0
  10. package/lib/public/app.js +10 -0
  11. package/lib/public/css/filebrowser.css +3 -3
  12. package/lib/public/css/mates.css +1 -1
  13. package/lib/public/css/mobile-nav.css +0 -128
  14. package/lib/public/css/session-folders.css +383 -0
  15. package/lib/public/css/sidebar.css +28 -545
  16. package/lib/public/index.html +3 -21
  17. package/lib/public/modules/app-favicon.js +5 -4
  18. package/lib/public/modules/app-messages.js +13 -0
  19. package/lib/public/modules/input.js +3 -0
  20. package/lib/public/modules/project-switcher.js +4 -4
  21. package/lib/public/modules/session-create-form.js +428 -0
  22. package/lib/public/modules/session-create.js +338 -0
  23. package/lib/public/modules/session-folder-collapse.js +192 -0
  24. package/lib/public/modules/session-folder-context.js +215 -0
  25. package/lib/public/modules/session-folder-delete.js +313 -0
  26. package/lib/public/modules/session-folder-dialogs.js +173 -0
  27. package/lib/public/modules/session-folder-dnd.js +214 -0
  28. package/lib/public/modules/session-folder-draft.js +184 -0
  29. package/lib/public/modules/session-folder-layout.js +133 -0
  30. package/lib/public/modules/session-folder-toolbar.js +185 -0
  31. package/lib/public/modules/session-folder-view-menu.js +187 -0
  32. package/lib/public/modules/session-folders.js +95 -0
  33. package/lib/public/modules/session-list-search.js +108 -0
  34. package/lib/public/modules/session-presence-ui.js +44 -0
  35. package/lib/public/modules/sidebar-mobile.js +38 -182
  36. package/lib/public/modules/sidebar-session-folders-ui.js +382 -0
  37. package/lib/public/modules/sidebar-sessions.js +141 -718
  38. package/lib/public/modules/sidebar.js +10 -2
  39. package/lib/public/modules/terminal.js +2 -2
  40. package/lib/public/modules/tool-palette.js +37 -219
  41. package/lib/public/style.css +1 -0
  42. package/lib/server.js +4 -0
  43. package/lib/session-folders.js +414 -0
  44. package/lib/sessions.js +7 -5
  45. package/lib/users-session-folder-preferences.js +69 -0
  46. package/lib/users.js +6 -0
  47. package/lib/ws-schema.js +11 -0
  48. package/package.json +1 -1
  49. package/lib/public/modules/session-creation-cta.js +0 -46
  50. package/lib/public/modules/tool-palette-overlays.js +0 -159
@@ -65,6 +65,15 @@ export function closeSidebar() {
65
65
  export function initSidebar(_ctx) {
66
66
  ctx = _ctx;
67
67
 
68
+ // Suppress native menus across the sidebar, including its empty space,
69
+ // while allowing the target's Clay context-menu handlers to run.
70
+ ["icon-strip", "sidebar-column", "mate-sidebar-column", "user-island"].forEach(function (id) {
71
+ var region = document.getElementById(id);
72
+ if (region) region.addEventListener("contextmenu", function (event) {
73
+ event.preventDefault();
74
+ }, true);
75
+ });
76
+
68
77
  // Initialize sidebar sub-modules (they import their own deps directly)
69
78
  initSidebarSessions();
70
79
  initSidebarProjects();
@@ -191,8 +200,7 @@ export function initSidebar(_ctx) {
191
200
 
192
201
  if (fileBrowserBtn) {
193
202
  // Clicking the tile toggles: open if closed, close (back to sessions)
194
- // if it's already the visible panel. Same behavior via the Cmd+O
195
- // hotkey since that calls .click().
203
+ // if it's already the visible panel.
196
204
  fileBrowserBtn.addEventListener("click", function () {
197
205
  if (filesPanel && !filesPanel.classList.contains("hidden")) {
198
206
  hideFilesPanel(function () { showSessionsPanel(); });
@@ -165,8 +165,8 @@ export function initTerminal(_ctx) {
165
165
  });
166
166
  }
167
167
 
168
- // Sidebar terminal button — toggles open/close. Second click (or the
169
- // Cmd+O hotkey on the same tile) dismisses the terminal panel.
168
+ // Sidebar terminal button — toggles open/close. Second click dismisses
169
+ // the terminal panel.
170
170
  var sidebarTermBtn = document.getElementById("terminal-sidebar-btn");
171
171
  if (sidebarTermBtn) {
172
172
  sidebarTermBtn.addEventListener("click", function () {
@@ -1,26 +1,21 @@
1
- // tool-palette.js — Customizable sidebar tool palette
1
+ // tool-palette.js — Sidebar tool palette
2
2
  //
3
- // Renders the per-session and per-mate tool grids from a data registry,
4
- // so users can reorder tools and hide ones they don't use. Preserves the
5
- // original button IDs so existing click handlers (attached by sidebar.js,
6
- // terminal.js, mcp-ui.js, etc.) keep working on the rendered nodes.
3
+ // Renders the per-session and per-mate tool grids from a data registry and
4
+ // lets users reorder them. Preserves the original button IDs so existing
5
+ // click handlers (attached by sidebar.js, terminal.js, mcp-ui.js, etc.) keep
6
+ // working on the rendered nodes.
7
7
  //
8
- // Design notes (see issue #325 for the broader context):
9
- // - Buttons are created once on init and never destroyed; hide/reorder
10
- // moves existing DOM nodes, so event listeners bound elsewhere survive.
11
- // - Edit mode is a state on the palette container (class="edit-mode").
12
- // CSS shows the × remove affordance and enables drag reordering.
13
- // - Preferences persist per-user via the /api/user/tool-palettes endpoint.
14
- // An in-flight save is debounced so rapid drag reorders don't spam.
8
+ // Design notes:
9
+ // - Buttons are created once on init and never destroyed; reorder moves
10
+ // existing DOM nodes, so event listeners bound elsewhere survive.
11
+ // - Tools can no longer be hidden or added back. A tool a saved preference
12
+ // marks hidden is simply shown, appended after the ordered tools, and the
13
+ // stored preference is not rewritten on load.
14
+ // - Order persists per-user via the /api/user/tool-palettes endpoint. An
15
+ // in-flight save is debounced so rapid drag reorders don't spam.
15
16
 
16
17
  import { refreshIcons } from './icons.js';
17
18
  import { PALETTES, normalizeToolPreferences } from './tool-palette-order.js';
18
- import {
19
- enterToolPickMode,
20
- exitToolPickMode,
21
- isToolPickActive,
22
- openPaletteContextMenu
23
- } from './tool-palette-overlays.js';
24
19
 
25
20
  var _saveTimers = {};
26
21
  var _draggingEl = null;
@@ -31,35 +26,6 @@ export function initToolPalettes() {
31
26
  buildPalette(name);
32
27
  }
33
28
 
34
- // Edit pills toggle edit mode on their associated palette.
35
- var editBtns = document.querySelectorAll('.tool-palette-edit-btn');
36
- for (var i = 0; i < editBtns.length; i++) {
37
- (function (btn) {
38
- btn.addEventListener('click', function () {
39
- toggleEditMode(btn.dataset.palette);
40
- });
41
- })(editBtns[i]);
42
- }
43
-
44
- // Keyboard-hotkey hint pill. Matches the icon-strip shortcut pills
45
- // in shape — just the chord, no prose. Clicking the pill opens the
46
- // hotkey overlay, same as pressing the shortcut.
47
- var isMac = /Mac|iPhone|iPod|iPad/i.test(navigator.platform);
48
- var hintLabel = isMac ? '\u2318O' : 'Ctrl+O';
49
- var hints = document.querySelectorAll('.sidebar-tools-hint');
50
- for (var h = 0; h < hints.length; h++) {
51
- hints[h].textContent = hintLabel;
52
- hints[h].title = 'Show keyboard hotkeys (' + hintLabel + ')';
53
- hints[h].addEventListener('click', function (e) {
54
- // Stop the click from bubbling to the document-level dismiss
55
- // handler below, which would immediately tear down the pick-mode
56
- // we just entered.
57
- e.stopPropagation();
58
- if (isToolPickActive()) exitToolPickMode();
59
- else enterToolPickMode();
60
- });
61
- }
62
-
63
29
  // Load saved preferences and apply to both palettes once buttons exist.
64
30
  loadPreferences();
65
31
 
@@ -76,22 +42,8 @@ function buildPalette(name) {
76
42
  active.appendChild(buildToolButton(palette.tools[i], name));
77
43
  }
78
44
 
79
- // Scaffold the hidden section (its grid fills in when items are hidden).
80
- var hiddenSection = document.getElementById(palette.hiddenSectionId);
81
- if (hiddenSection) {
82
- hiddenSection.innerHTML = '';
83
- var labelEl = document.createElement('div');
84
- labelEl.className = 'tool-palette-hidden-label';
85
- labelEl.textContent = 'Add back';
86
- var gridEl = document.createElement('div');
87
- gridEl.className = 'tool-palette-hidden-grid';
88
- hiddenSection.appendChild(labelEl);
89
- hiddenSection.appendChild(gridEl);
90
- }
91
-
92
- // Drag-reorder within the active container. Works in both normal and
93
- // edit mode so users can rearrange without entering a separate mode
94
- // (matches the macOS dock pattern). HTML5 drag has a small movement
45
+ // Drag-reorder within the active container. Users can rearrange in
46
+ // place (matches the macOS dock pattern). HTML5 drag has a small movement
95
47
  // threshold, so quick clicks still activate the tool normally.
96
48
  active.addEventListener('dragover', function (e) {
97
49
  if (_draggingPaletteName !== name || !_draggingEl) return;
@@ -121,9 +73,7 @@ function buildToolButton(tool, paletteName) {
121
73
  btn.setAttribute('aria-label', tool.label);
122
74
  btn.dataset.toolId = tool.id;
123
75
  btn.dataset.palette = paletteName;
124
- // Draggable at all times so users can reorder without entering edit
125
- // mode. Drag is gated to the active grid in the handlers below; tiles
126
- // in the hidden grid get draggable=false set on move.
76
+ // Draggable at all times so users can reorder in place.
127
77
  btn.draggable = true;
128
78
 
129
79
  var icon = document.createElement('i');
@@ -142,61 +92,8 @@ function buildToolButton(tool, paletteName) {
142
92
  btn.appendChild(count);
143
93
  }
144
94
 
145
- // × affordance — shown only in edit mode via CSS.
146
- var remove = document.createElement('span');
147
- remove.className = 'tool-palette-remove';
148
- remove.setAttribute('role', 'button');
149
- remove.setAttribute('aria-label', 'Remove ' + tool.label + ' from palette');
150
- remove.textContent = '\u00D7';
151
- btn.appendChild(remove);
152
-
153
- // Click interceptor, registered before any other module attaches to
154
- // the button by ID. stopImmediatePropagation prevents the tool's own
155
- // handler from running in three cases: clicking the × in edit mode,
156
- // clicking a tile in the hidden-grid (which should restore it), and
157
- // clicking any tile while edit mode is active (tiles are inert then).
158
- btn.addEventListener('click', function (e) {
159
- if (e.target.closest('.tool-palette-remove')) {
160
- e.preventDefault();
161
- e.stopImmediatePropagation();
162
- moveToHidden(paletteName, tool.id);
163
- return;
164
- }
165
- var container = btn.parentNode;
166
- if (!container) return;
167
- if (container.classList.contains('tool-palette-hidden-grid')) {
168
- e.preventDefault();
169
- e.stopImmediatePropagation();
170
- moveToActive(paletteName, tool.id);
171
- return;
172
- }
173
- if (container.classList.contains('edit-mode')) {
174
- e.preventDefault();
175
- e.stopImmediatePropagation();
176
- }
177
- });
178
-
179
- btn.addEventListener('contextmenu', function (e) {
180
- e.preventDefault();
181
- e.stopPropagation();
182
- var container = btn.parentNode;
183
- var inHidden = container && container.classList.contains('tool-palette-hidden-grid');
184
- openPaletteContextMenu(e.clientX, e.clientY, inHidden ? [
185
- { label: 'Add back to palette', action: function () { moveToActive(paletteName, tool.id); } },
186
- ] : [
187
- { label: 'Remove from palette', action: function () { moveToHidden(paletteName, tool.id); } },
188
- { label: 'Edit palette\u2026', action: function () {
189
- if (!document.getElementById(PALETTES[paletteName].activeContainerId).classList.contains('edit-mode')) {
190
- toggleEditMode(paletteName);
191
- }
192
- } },
193
- ]);
194
- });
195
-
196
95
  btn.addEventListener('dragstart', function (e) {
197
96
  var container = btn.parentNode;
198
- // Only allow drag from the active grid; hidden tiles are added back
199
- // via click, not drag.
200
97
  if (!container || container.id !== PALETTES[paletteName].activeContainerId) {
201
98
  e.preventDefault();
202
99
  return;
@@ -218,60 +115,6 @@ function buildToolButton(tool, paletteName) {
218
115
  return btn;
219
116
  }
220
117
 
221
- function toggleEditMode(name) {
222
- var palette = PALETTES[name];
223
- if (!palette) return;
224
- var active = document.getElementById(palette.activeContainerId);
225
- var hidden = document.getElementById(palette.hiddenSectionId);
226
- var editBtn = document.querySelector('.tool-palette-edit-btn[data-palette="' + name + '"]');
227
- if (!active || !editBtn) return;
228
-
229
- var entering = !active.classList.contains('edit-mode');
230
- active.classList.toggle('edit-mode', entering);
231
- if (hidden) {
232
- // Show hidden-section in edit mode if any items exist there (or to
233
- // allow adding back even when empty, keep visible so the state is
234
- // legible). We show whenever edit mode is on.
235
- hidden.classList.toggle('hidden', !entering);
236
- }
237
- // Pencil icon stays; the .active class provides visual feedback by
238
- // swapping to the accent background. No label text swap needed now.
239
- editBtn.classList.toggle('active', entering);
240
-
241
- if (!entering) queueSave(name);
242
- }
243
-
244
- function moveToHidden(name, toolId) {
245
- var palette = PALETTES[name];
246
- if (!palette) return;
247
- var btn = document.getElementById(toolId);
248
- var hiddenSection = document.getElementById(palette.hiddenSectionId);
249
- if (!btn || !hiddenSection) return;
250
- var grid = hiddenSection.querySelector('.tool-palette-hidden-grid');
251
- if (!grid) return;
252
- // Hidden tiles stay draggable=false so users don't drag them around
253
- // before adding them back. The active-grid dragstart gate also
254
- // enforces this.
255
- btn.draggable = false;
256
- grid.appendChild(btn);
257
- queueSave(name);
258
- }
259
-
260
- function moveToActive(name, toolId) {
261
- var palette = PALETTES[name];
262
- if (!palette) return;
263
- var btn = document.getElementById(toolId);
264
- var active = document.getElementById(palette.activeContainerId);
265
- if (!btn || !active) return;
266
- active.appendChild(btn);
267
- btn.draggable = true;
268
- queueSave(name);
269
- }
270
-
271
- // (Hidden-tile clicks are handled in each button's own click listener,
272
- // attached in buildToolButton before any external module attaches
273
- // handlers by ID. See the stopImmediatePropagation logic there.)
274
-
275
118
  function getDragAfterElement(container, x, y) {
276
119
  var tiles = Array.prototype.slice.call(
277
120
  container.querySelectorAll('[data-tool-id]:not(.dragging)')
@@ -314,50 +157,32 @@ function applyPreferences(name, prefs) {
314
157
  var palette = PALETTES[name];
315
158
  if (!palette) return;
316
159
  var active = document.getElementById(palette.activeContainerId);
317
- var hiddenSection = document.getElementById(palette.hiddenSectionId);
318
- var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
319
- if (!active || !hiddenGrid) return;
160
+ if (!active) return;
320
161
 
321
162
  var normalized = normalizeToolPreferences(name, prefs);
322
-
323
- var hiddenSet = {};
324
- var hiddenList = normalized.hidden;
325
- for (var i = 0; i < hiddenList.length; i++) hiddenSet[hiddenList[i]] = true;
326
-
327
- var orderList = normalized.order;
328
- // Any tools not in the stored order are appended in registry order so
329
- // newly-added tools surface automatically for existing users. This is how
330
- // a restored tool reaches a saved palette that predates it: no rewrite, no
331
- // server state, just the normal new-registry append.
332
163
  var placed = {};
333
- for (var j = 0; j < orderList.length; j++) {
334
- var id = orderList[j];
335
- if (hiddenSet[id]) continue;
164
+ function place(id) {
165
+ if (placed[id]) return;
336
166
  var btn = document.getElementById(id);
337
- if (btn) {
338
- active.appendChild(btn);
339
- placed[id] = true;
340
- }
167
+ if (!btn) return;
168
+ active.appendChild(btn);
169
+ placed[id] = true;
341
170
  }
342
- for (var k = 0; k < palette.tools.length; k++) {
343
- var tid = palette.tools[k].id;
344
- if (placed[tid] || hiddenSet[tid]) continue;
345
- var tbtn = document.getElementById(tid);
346
- if (tbtn) active.appendChild(tbtn);
171
+
172
+ // Saved order first, then tools the saved list predates in registry order,
173
+ // then tools an older client had hidden. Hiding is no longer offered, so
174
+ // those tools surface at the end instead of staying unreachable. Nothing
175
+ // is written back here; the stored preference only changes on a reorder.
176
+ var hiddenSet = {};
177
+ for (var i = 0; i < normalized.hidden.length; i++) hiddenSet[normalized.hidden[i]] = true;
178
+ for (var j = 0; j < normalized.order.length; j++) {
179
+ if (!hiddenSet[normalized.order[j]]) place(normalized.order[j]);
347
180
  }
348
- for (var h = 0; h < hiddenList.length; h++) {
349
- var hbtn = document.getElementById(hiddenList[h]);
350
- if (hbtn) {
351
- hbtn.draggable = false;
352
- hiddenGrid.appendChild(hbtn);
353
- }
181
+ for (var k = 0; k < palette.tools.length; k++) {
182
+ if (!hiddenSet[palette.tools[k].id]) place(palette.tools[k].id);
354
183
  }
355
-
356
- // Write the rewritten preference back once, through the normal save path, so
357
- // the migration survives reloads and reaches the user's other devices. The
358
- // save reads the DOM this function just arranged, so it records exactly what
359
- // is on screen.
360
- if (normalized.migrated) queueSave(name);
184
+ for (var h = 0; h < normalized.hidden.length; h++) place(normalized.hidden[h]);
185
+ for (var m = 0; m < palette.tools.length; m++) place(palette.tools[m].id);
361
186
  }
362
187
 
363
188
  function queueSave(name) {
@@ -372,24 +197,17 @@ function savePreferences(name) {
372
197
  var palette = PALETTES[name];
373
198
  if (!palette) return;
374
199
  var active = document.getElementById(palette.activeContainerId);
375
- var hiddenSection = document.getElementById(palette.hiddenSectionId);
376
- var hiddenGrid = hiddenSection ? hiddenSection.querySelector('.tool-palette-hidden-grid') : null;
377
200
  if (!active) return;
378
201
 
379
202
  var order = [];
380
203
  var activeTiles = active.querySelectorAll('[data-tool-id]');
381
204
  for (var i = 0; i < activeTiles.length; i++) order.push(activeTiles[i].dataset.toolId);
382
205
 
383
- var hidden = [];
384
- if (hiddenGrid) {
385
- var hiddenTiles = hiddenGrid.querySelectorAll('[data-tool-id]');
386
- for (var j = 0; j < hiddenTiles.length; j++) hidden.push(hiddenTiles[j].dataset.toolId);
387
- }
388
-
206
+ // Every tool is visible, so the explicit order is the whole preference.
389
207
  fetch('/api/user/tool-palettes', {
390
208
  method: 'PUT',
391
209
  credentials: 'same-origin',
392
210
  headers: { 'Content-Type': 'application/json' },
393
- body: JSON.stringify({ palette: name, order: order, hidden: hidden }),
211
+ body: JSON.stringify({ palette: name, order: order, hidden: [] }),
394
212
  }).catch(function () { /* save best-effort */ });
395
213
  }
@@ -12,6 +12,7 @@
12
12
  @import url("css/workspace-assignment.css");
13
13
  @import url("css/session-actions.css?v=20260828-contrast1");
14
14
  @import url("css/sidebar.css?v=20260828-session-list3");
15
+ @import url("css/session-folders.css");
15
16
  @import url("css/overlays.css?v=20260827-brand3");
16
17
  @import url("css/menus.css");
17
18
  @import url("css/messages.css?v=20260828-delegated-muted1");
package/lib/server.js CHANGED
@@ -181,6 +181,8 @@ function createServer(opts) {
181
181
  var onSetProjectDefaultMode = opts.onSetProjectDefaultMode || null;
182
182
  var onGetProjectLastVendor = opts.onGetProjectLastVendor || null;
183
183
  var onSetProjectLastVendor = opts.onSetProjectLastVendor || null;
184
+ var onGetProjectNewSessionDefault = opts.onGetProjectNewSessionDefault || null;
185
+ var onSetProjectNewSessionDefault = opts.onSetProjectNewSessionDefault || null;
184
186
  var onGetProjectMcpServers = opts.onGetProjectMcpServers || null;
185
187
  var onSetProjectMcpServers = opts.onSetProjectMcpServers || null;
186
188
  var onGetDaemonConfig = opts.onGetDaemonConfig || null;
@@ -1468,6 +1470,8 @@ function createServer(opts) {
1468
1470
  onSetProjectDefaultMode: onSetProjectDefaultMode,
1469
1471
  onGetProjectLastVendor: onGetProjectLastVendor,
1470
1472
  onSetProjectLastVendor: onSetProjectLastVendor,
1473
+ onGetProjectNewSessionDefault: onGetProjectNewSessionDefault,
1474
+ onSetProjectNewSessionDefault: onSetProjectNewSessionDefault,
1471
1475
  listControllableTools: function (userId) {
1472
1476
  return toolsHandler ? toolsHandler.installedManifests(userId) : [];
1473
1477
  },