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
@@ -0,0 +1,108 @@
1
+ // Session list search: query, open state and results live in the store
2
+ // (sessionSearch) so the folder toolbar on every surface can drive and show them.
3
+ // Behavior is unchanged from the former header search: a debounced
4
+ // search_sessions request, stale responses ignored, matches filter the list while
5
+ // keeping Driver/Worker hierarchy.
6
+
7
+ import { store } from './store.js';
8
+ import { getWs } from './ws-ref.js';
9
+ import { hierarchyItemMatches } from './sidebar-session-hierarchy.js';
10
+
11
+ var DEBOUNCE_MS = 200;
12
+
13
+ function current() {
14
+ return store.get('sessionSearch') || { open: false, query: "", matchIds: null, timer: null, focus: false, selStart: null, selEnd: null };
15
+ }
16
+
17
+ function patch(change) {
18
+ store.set({ sessionSearch: Object.assign({}, current(), change) });
19
+ }
20
+
21
+ export function getListSearchQuery() {
22
+ return current().query;
23
+ }
24
+
25
+ // null means no search; otherwise a Set of matching session ids.
26
+ export function getListSearchMatchIds() {
27
+ return current().matchIds;
28
+ }
29
+
30
+ export function listSearchActive() {
31
+ var state = current();
32
+ return state.open || !!state.query;
33
+ }
34
+
35
+ function cancelTimer() {
36
+ var timer = current().timer;
37
+ if (timer) clearTimeout(timer);
38
+ }
39
+
40
+ export function openListSearch() {
41
+ patch({ open: true, focus: true, selStart: null, selEnd: null });
42
+ }
43
+
44
+ // Close always clears the query, so the list returns to its normal state.
45
+ export function closeListSearch() {
46
+ cancelTimer();
47
+ patch({ open: false, query: "", matchIds: null, timer: null, focus: false, selStart: null, selEnd: null });
48
+ }
49
+
50
+ // Clear empties the text but keeps the search open for another query.
51
+ export function clearListSearchText() {
52
+ cancelTimer();
53
+ patch({ query: "", matchIds: null, timer: null, focus: true, selStart: 0, selEnd: 0 });
54
+ }
55
+
56
+ export function setListSearchQuery(raw) {
57
+ var query = raw || "";
58
+ cancelTimer();
59
+ if (!query.trim()) {
60
+ patch({ query: query, matchIds: null, timer: null });
61
+ return;
62
+ }
63
+ var timer = setTimeout(function () {
64
+ var ws = getWs();
65
+ var state = current();
66
+ if (ws && store.get('connected') && state.query === query) ws.send(JSON.stringify({ type: "search_sessions", query: query }));
67
+ }, DEBOUNCE_MS);
68
+ patch({ query: query, timer: timer });
69
+ }
70
+
71
+ export function rememberListSearchCaret(focus, selStart, selEnd) {
72
+ var state = current();
73
+ if (state.focus === focus && state.selStart === selStart && state.selEnd === selEnd) return;
74
+ patch({ focus: focus, selStart: selStart, selEnd: selEnd });
75
+ }
76
+
77
+ export function applyListSearchResults(msg) {
78
+ if (msg.query !== current().query) return; // stale response
79
+ var ids = new Set();
80
+ for (var i = 0; i < msg.results.length; i++) ids.add(msg.results[i].id);
81
+ patch({ matchIds: ids });
82
+ }
83
+
84
+ // What a list rerender needs to react to; typing and caret movement do not.
85
+ export function listSearchNeedsRender(state, previous) {
86
+ var a = state.sessionSearch;
87
+ var b = previous.sessionSearch;
88
+ if (a === b) return false;
89
+ var after = a || { open: false, query: "", matchIds: null };
90
+ var before = b || { open: false, query: "", matchIds: null };
91
+ if (after.open !== before.open || after.matchIds !== before.matchIds) return true;
92
+ return after.query !== before.query && !after.query.trim();
93
+ }
94
+
95
+ // Shared by the desktop list and the mobile sheet: drops items with no match and
96
+ // keeps a Driver visible when it or any of its Workers match.
97
+ export function filterItemsBySearch(items, matchIds) {
98
+ if (matchIds === null) return items;
99
+ return items.filter(function (item) {
100
+ if (item.type === "session") return !!(item.data && matchIds.has(item.data.id));
101
+ if (item.type === "driver-hierarchy" || item.type === "orphan-workers") return hierarchyItemMatches(item, matchIds);
102
+ if (item.type === "split-group") {
103
+ for (var i = 0; i < item.members.length; i++) if (matchIds.has(item.members[i].id)) return true;
104
+ return false;
105
+ }
106
+ return true;
107
+ });
108
+ }
@@ -0,0 +1,44 @@
1
+ import { avatarUrl, userAvatarUrl } from './avatar.js';
2
+ import { store } from './store.js';
3
+
4
+ function avatarFor(userOrStyle, seed) {
5
+ if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
6
+ return avatarUrl(userOrStyle || "imprint", seed, 24);
7
+ }
8
+
9
+ export function setSessionPresence(presence) {
10
+ store.set({ sessionPresence: presence || {} });
11
+ }
12
+
13
+ export function renderSessionPresence(row, sessionId) {
14
+ var existing = row.querySelector(".session-presence");
15
+ if (existing) existing.remove();
16
+ var all = store.get('sessionPresence') || {};
17
+ var users = all[String(sessionId)] || all[sessionId];
18
+ if (!users || !users.length) return null;
19
+
20
+ var container = document.createElement("span");
21
+ container.className = "session-presence";
22
+ container.setAttribute("aria-label", users.length + (users.length === 1 ? " person viewing" : " people viewing"));
23
+ var max = 3;
24
+ var shown = Math.min(max, users.length);
25
+ for (var i = 0; i < shown; i++) {
26
+ var user = users[i];
27
+ var image = document.createElement("img");
28
+ image.className = "session-presence-avatar";
29
+ image.src = avatarFor(user);
30
+ image.alt = user.displayName || "Viewer";
31
+ image.dataset.tip = (user.displayName || "Viewer") + (user.username ? " (@" + user.username + ")" : "");
32
+ container.appendChild(image);
33
+ }
34
+ if (users.length > max) {
35
+ var more = document.createElement("span");
36
+ more.className = "session-presence-more";
37
+ more.textContent = "+" + (users.length - max);
38
+ container.appendChild(more);
39
+ }
40
+
41
+ var anchor = row.querySelector(".session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
42
+ row.insertBefore(container, anchor || null);
43
+ return container;
44
+ }
@@ -9,15 +9,15 @@ import { parseEmojis } from './markdown.js';
9
9
  import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
10
10
  import { openCommandPalette } from './command-palette.js';
11
11
  import { openProjectSettings } from './project-settings.js';
12
+ import { getCachedSessions, openSessionContextMenu } from './sidebar-sessions.js';
13
+ import { VENDOR_AVATARS } from './app-rendering.js';
14
+ import { renderSessionPresence } from './session-presence-ui.js';
12
15
  import {
13
- getCachedSessions,
14
- getDateGroup,
15
- resolveDefaultVendor,
16
- startNewSession
17
- } from './sidebar-sessions.js';
18
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
19
- import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
20
- import { renderSessionCreationCta } from './session-creation-cta.js';
16
+ canOrganizeSession, computeLayout, createStarButton, renderFavoritesSection, renderOrdinarySections
17
+ } from './sidebar-session-folders-ui.js';
18
+ import { renderFolderToolbar, captureFolderInputs } from './session-folder-toolbar.js';
19
+ import { captureFolderCollapseAnimations } from './session-folder-collapse.js';
20
+ import { getListSearchMatchIds, filterItemsBySearch } from './session-list-search.js';
21
21
  import {
22
22
  getCachedProjectList,
23
23
  getCachedCurrentSlug,
@@ -124,7 +124,7 @@ export function openMobileSheet(type) {
124
124
  refreshIcons();
125
125
  }
126
126
 
127
- function closeMobileSheet() {
127
+ export function closeMobileSheet() {
128
128
  var sheet = document.getElementById("mobile-sheet");
129
129
  if (!sheet || sheet.classList.contains("hidden")) return;
130
130
 
@@ -387,6 +387,7 @@ function createMobileSessionItem(s, options) {
387
387
  var itemOptions = options || {};
388
388
  var el = document.createElement("div");
389
389
  el.className = "mobile-session-item" + (s.active ? " active" : "");
390
+ el.dataset.sessionId = String(s.id);
390
391
  if (itemOptions.worker) el.classList.add("mobile-worker-item");
391
392
 
392
393
  // Vendor badge (legacy sessions without a vendor are Claude)
@@ -414,6 +415,7 @@ function createMobileSessionItem(s, options) {
414
415
  titleSpan.appendChild(document.createTextNode(s.title || "New Session"));
415
416
  el.appendChild(titleSpan);
416
417
  appendGithubWork(el, s, titleSpan);
418
+ renderSessionPresence(el, String(s.id));
417
419
 
418
420
  if (itemOptions.worker) {
419
421
  var generation = document.createElement("span");
@@ -430,6 +432,16 @@ function createMobileSessionItem(s, options) {
430
432
  el.appendChild(badge);
431
433
  }
432
434
 
435
+ if (!itemOptions.worker && canOrganizeSession(s)) {
436
+ el.appendChild(createStarButton(s, "mobile-session-star"));
437
+ }
438
+
439
+ el.addEventListener("contextmenu", function (event) {
440
+ event.preventDefault();
441
+ event.stopPropagation();
442
+ openSessionContextMenu(el, s);
443
+ });
444
+
433
445
  (function (id) {
434
446
  el.addEventListener("click", function () {
435
447
  if (getWs() && store.get('connected')) {
@@ -680,125 +692,6 @@ function createMobileLoopGroup(loopId, children, groupKey) {
680
692
 
681
693
  // Helper: render sorted sessions into a container with date groups (with loop session grouping)
682
694
  export function renderMobileSessionsInto(container) {
683
- var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
684
- var mobileInstalled = store.get('installedVendors') || [];
685
- var mobileDefaultState = store.get('defaultVendorState') || {};
686
-
687
- var vendorList = document.createElement("div");
688
- vendorList.className = "mobile-vendor-list hidden";
689
- vendorList.id = "mobile-session-provider-menu";
690
- var mobileVendorName = VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor;
691
- var mobileCreateTitle = "Create a " + mobileVendorName + " session";
692
- if (defaultVendorUnavailable(mobileInstalled)) mobileCreateTitle += " (saved default is unavailable; using installed priority)";
693
- var controls;
694
- controls = renderSessionCreationCta({
695
- rowClass: "mobile-session-new-row",
696
- createClass: "mobile-session-new mobile-session-new-main",
697
- providerClass: "mobile-session-new mobile-session-new-provider",
698
- iconClass: "mobile-session-new-icon",
699
- vendorKey: mobileDefaultVendor,
700
- vendorName: mobileVendorName,
701
- vendorAvatar: VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude,
702
- createTitle: mobileCreateTitle,
703
- providerTitle: "Switch AI provider (current: " + mobileVendorName + ")",
704
- menuId: vendorList.id,
705
- onCreate: function () { startNewSession(mobileDefaultVendor); closeMobileSheet(); },
706
- onProvider: function () {
707
- var opening = vendorList.classList.contains("hidden");
708
- vendorList.classList.toggle("hidden", !opening);
709
- controls.providerButton.classList.toggle("expanded", opening);
710
- controls.providerButton.setAttribute("aria-expanded", opening ? "true" : "false");
711
- if (opening) {
712
- var first = vendorList.querySelector("button:not(:disabled)");
713
- if (first) first.focus();
714
- }
715
- },
716
- });
717
- var newRow = controls.element;
718
- vendorList.addEventListener("keydown", function (e) {
719
- if (e.key !== "Escape") return;
720
- e.preventDefault();
721
- vendorList.classList.add("hidden");
722
- controls.providerButton.classList.remove("expanded");
723
- controls.providerButton.setAttribute("aria-expanded", "false");
724
- controls.providerButton.focus();
725
- });
726
-
727
- if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
728
- var mobileFallbackNote = document.createElement("div");
729
- mobileFallbackNote.className = "mobile-session-new-preference-status mobile-session-new-fallback-note";
730
- mobileFallbackNote.textContent = "Saved default " + (VENDOR_NAMES[mobileDefaultState.preference] || mobileDefaultState.preference) + " is unavailable. Using " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ".";
731
- }
732
-
733
- // Every known vendor is listed. Uninstalled ones stay visible but disabled,
734
- // and tapping one opens the vendor's homepage.
735
- for (var vi = 0; vi < VENDOR_ORDER.length; vi++) {
736
- (function (vendor) {
737
- var isInstalled = mobileInstalled.indexOf(vendor) !== -1;
738
- var name = VENDOR_NAMES[vendor] || vendor;
739
- var vendorRow = document.createElement("div");
740
- vendorRow.className = "mobile-vendor-row";
741
- var vBtn = document.createElement("button");
742
- vBtn.className = "mobile-session-new mobile-session-new-vendor";
743
- if (!isInstalled) vBtn.classList.add("disabled");
744
- if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
745
- vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
746
- '<span>' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
747
- (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
748
- (isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
749
- vBtn.addEventListener("click", function () {
750
- if (!isInstalled) {
751
- window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
752
- return;
753
- }
754
- vendorList.classList.add("hidden");
755
- controls.providerButton.classList.remove("expanded");
756
- controls.providerButton.setAttribute("aria-expanded", "false");
757
- startNewSession(vendor);
758
- closeMobileSheet();
759
- });
760
- vendorRow.appendChild(vBtn);
761
- if (isInstalled) {
762
- var defaultBtn = document.createElement("button");
763
- defaultBtn.type = "button";
764
- defaultBtn.className = "mobile-vendor-set-default";
765
- defaultBtn.textContent = mobileDefaultState.saving ? "Saving…" : (vendor === mobileDefaultVendor && !defaultVendorUnavailable(mobileInstalled) ? "Default" : "Set default");
766
- defaultBtn.disabled = mobileDefaultState.saving === true;
767
- defaultBtn.setAttribute("aria-label", "Set " + name + " as default vendor");
768
- defaultBtn.addEventListener("click", function (e) {
769
- e.preventDefault();
770
- e.stopPropagation();
771
- saveDefaultVendor(vendor);
772
- });
773
- vendorRow.appendChild(defaultBtn);
774
- }
775
- vendorList.appendChild(vendorRow);
776
- })(VENDOR_ORDER[vi]);
777
- }
778
-
779
- var automaticBtn = document.createElement("button");
780
- automaticBtn.className = "mobile-session-new mobile-session-new-vendor mobile-session-new-automatic";
781
- automaticBtn.type = "button";
782
- automaticBtn.textContent = "Use automatic default";
783
- automaticBtn.disabled = mobileDefaultState.saving === true;
784
- automaticBtn.addEventListener("click", function (e) {
785
- e.preventDefault();
786
- e.stopPropagation();
787
- saveDefaultVendor("");
788
- });
789
- vendorList.appendChild(automaticBtn);
790
- if (mobileDefaultState.error) {
791
- var mobileStatus = document.createElement("div");
792
- mobileStatus.className = "mobile-session-new-preference-status";
793
- mobileStatus.setAttribute("role", "alert");
794
- mobileStatus.textContent = mobileDefaultState.error;
795
- vendorList.appendChild(mobileStatus);
796
- }
797
-
798
- container.appendChild(newRow);
799
- if (mobileFallbackNote) container.appendChild(mobileFallbackNote);
800
- container.appendChild(vendorList);
801
-
802
695
  // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
803
696
  var sessions = getCachedSessions();
804
697
  var hierarchy = prepareSidebarHierarchy(sessions);
@@ -846,60 +739,21 @@ export function renderMobileSessionsInto(container) {
846
739
  items.push({ type: "loop", loopId: realLoopId, groupKey: gk, children: children, lastActivity: maxActivity });
847
740
  }
848
741
 
849
- // Sort by lastActivity descending
850
- items.sort(function (a, b) {
851
- var aBookmarked = !!(a.type === "loop" || a.type === "orphan-workers" ? false : a.data && a.data.bookmarked);
852
- var bBookmarked = !!(b.type === "loop" || b.type === "orphan-workers" ? false : b.data && b.data.bookmarked);
853
- if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
854
- return (b.lastActivity || 0) - (a.lastActivity || 0);
855
- });
856
-
857
- var bookmarkedItems = [];
858
- var regularItems = [];
859
- for (var n = 0; n < items.length; n++) {
860
- var item = items[n];
861
- if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
862
- bookmarkedItems.push(item);
863
- } else {
864
- regularItems.push(item);
865
- }
866
- }
867
-
868
- if (bookmarkedItems.length > 0) {
869
- var bookmarkedHeader = document.createElement("div");
870
- bookmarkedHeader.className = "mobile-sheet-group";
871
- bookmarkedHeader.textContent = "Favorites";
872
- container.appendChild(bookmarkedHeader);
873
-
874
- for (var bi = 0; bi < bookmarkedItems.length; bi++) {
875
- if (bookmarkedItems[bi].type === "driver-hierarchy") container.appendChild(renderMobileDriverHierarchy(bookmarkedItems[bi].root, createMobileSessionItem, refreshMobileChatSheet));
876
- else container.appendChild(createMobileSessionItem(bookmarkedItems[bi].data));
877
- }
878
- }
879
-
880
- var currentGroup = "";
881
- for (var ri = 0; ri < regularItems.length; ri++) {
882
- var item = regularItems[ri];
883
- var group = getDateGroup(item.lastActivity || 0);
884
- if (group !== currentGroup) {
885
- currentGroup = group;
886
- if (group !== "Today") {
887
- var header = document.createElement("div");
888
- header.className = "mobile-sheet-group";
889
- header.textContent = group;
890
- container.appendChild(header);
891
- }
892
- }
893
- if (item.type === "loop") {
894
- container.appendChild(createMobileLoopGroup(item.loopId, item.children, item.groupKey));
895
- } else if (item.type === "driver-hierarchy") {
896
- container.appendChild(renderMobileDriverHierarchy(item.root, createMobileSessionItem, refreshMobileChatSheet));
897
- } else if (item.type === "orphan-workers") {
898
- container.appendChild(renderMobileOrphanHierarchy(item.workers, createMobileSessionItem, refreshMobileChatSheet));
899
- } else {
900
- container.appendChild(createMobileSessionItem(item.data));
901
- }
902
- }
742
+ var matchIds = getListSearchMatchIds();
743
+ var layout = computeLayout(filterItemsBySearch(items, matchIds), matchIds !== null, "mobile");
744
+ var folderCtx = {
745
+ surface: "mobile",
746
+ searching: matchIds !== null,
747
+ renderItem: function (it) {
748
+ if (it.type === "loop") return createMobileLoopGroup(it.loopId, it.children, it.groupKey);
749
+ if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet);
750
+ if (it.type === "orphan-workers") return renderMobileOrphanHierarchy(it.workers, createMobileSessionItem, refreshMobileChatSheet);
751
+ return createMobileSessionItem(it.data);
752
+ },
753
+ };
754
+ container.appendChild(renderFolderToolbar("mobile"));
755
+ container.appendChild(renderFavoritesSection(layout, folderCtx));
756
+ renderOrdinarySections(layout, folderCtx, container);
903
757
  }
904
758
 
905
759
  // Refresh mobile chat sheet when session data updates (called from renderSessionList)
@@ -944,6 +798,8 @@ export function refreshMobileChatSheet() {
944
798
  }
945
799
 
946
800
  // Re-render sessions for current context
801
+ captureFolderInputs();
802
+ captureFolderCollapseAnimations(sessionListEl);
947
803
  sessionListEl.innerHTML = "";
948
804
  renderMobileSessionsInto(sessionListEl);
949
805