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,272 @@
1
+ // WebSocket surface for sidebar session folders. Everything is scoped to the
2
+ // authenticated user and this project: the caller's identity comes from the
3
+ // socket, never from the message, and a session key is only accepted when it
4
+ // names a session that user can see and may organize.
5
+
6
+ var folders = require("./session-folders");
7
+ var sessionProvenance = require("./session-provenance");
8
+ var folderDeleteModule = require("./project-session-folder-delete");
9
+
10
+ function attachSessionFolders(ctx) {
11
+ var sm = ctx.sm;
12
+ var usersModule = ctx.usersModule;
13
+ var slug = ctx.slug;
14
+ var clients = ctx.clients;
15
+ var sendTo = ctx.sendTo;
16
+ var getProjectAccess = ctx.getProjectAccess || function () { return { visibility: "public" }; };
17
+
18
+ function userIdFor(ws) {
19
+ if (!usersModule.isMultiUser()) return "default";
20
+ return ws && ws._clayUser ? ws._clayUser.id : null;
21
+ }
22
+
23
+ function sessionsByKey() {
24
+ var map = Object.create(null);
25
+ sm.sessions.forEach(function (session) {
26
+ var key = sessionProvenance.ensureOrigin(session);
27
+ if (key) map[key] = session;
28
+ });
29
+ return map;
30
+ }
31
+
32
+ function visibleTo(userId, session) {
33
+ if (session.hidden) return false;
34
+ if (!usersModule.isMultiUser()) return !session.ownerId;
35
+ return !!usersModule.canAccessSession(userId, session, getProjectAccess());
36
+ }
37
+
38
+ // A movable unit is a standalone session or a Driver root. Workers travel
39
+ // with their Driver and loop runs stay grouped, so neither is accepted.
40
+ function organizable(userId, session) {
41
+ if (!session || !visibleTo(userId, session)) return false;
42
+ if (sessionProvenance.isWorker(session)) return false;
43
+ if (session.loop && session.loop.loopId) return false;
44
+ return true;
45
+ }
46
+
47
+ function buildEnv(userId) {
48
+ var byKey = sessionsByKey();
49
+ return {
50
+ canOrganize: function (key) { return organizable(userId, byKey[key]); },
51
+ exists: function (key) { return !!byKey[key]; },
52
+ byKey: byKey,
53
+ };
54
+ }
55
+
56
+ // Clients address sessions by their local id; durable origin keys never
57
+ // leave the server. Anything that does not resolve to a session this user
58
+ // may organize is dropped from outgoing state and rejected on the way in.
59
+ function toClientState(state, env) {
60
+ var out = { folders: state.folders, collapsed: state.collapsed, view: state.view, assignments: {}, favorites: [], orders: {} };
61
+ function idOf(key) { return env.canOrganize(key) ? env.byKey[key].localId : null; }
62
+ // Favorites are tags: ids in curated order. A favorite also keeps its
63
+ // assignment below, so it appears in both places.
64
+ out.favorites = state.favorites.map(idOf).filter(function (id) { return id !== null; });
65
+ Object.keys(state.assignments).forEach(function (key) {
66
+ var id = idOf(key);
67
+ if (id !== null) out.assignments[id] = state.assignments[key];
68
+ });
69
+ Object.keys(state.orders).forEach(function (container) {
70
+ out.orders[container] = state.orders[container].map(idOf).filter(function (id) { return id !== null; });
71
+ });
72
+ return out;
73
+ }
74
+
75
+ function keyOfId(env, id) {
76
+ if (typeof id !== "number") return null;
77
+ var session = sm.sessions.get(id);
78
+ return session && env.canOrganize(sessionProvenance.ensureOrigin(session)) ? sessionProvenance.ensureOrigin(session) : null;
79
+ }
80
+
81
+ function idsOf(env, keys) {
82
+ return keys.filter(function (k) { return env.canOrganize(k); }).map(function (k) { return env.byKey[k].localId; });
83
+ }
84
+
85
+ // Rewrites a client operation from local ids to origin keys.
86
+ function translateOp(op, env) {
87
+ if (!op || typeof op !== "object") return { error: "Invalid folder request" };
88
+ var out = Object.assign({}, op);
89
+ if (op.sessionId !== undefined) {
90
+ out.sessionKey = keyOfId(env, op.sessionId);
91
+ delete out.sessionId;
92
+ if (!out.sessionKey) return { error: "Session not found" };
93
+ } else {
94
+ delete out.sessionKey;
95
+ }
96
+ if (op.order !== undefined) {
97
+ if (!Array.isArray(op.order)) return { error: "Invalid order" };
98
+ var keys = [];
99
+ for (var i = 0; i < op.order.length; i++) {
100
+ var key = keyOfId(env, op.order[i]);
101
+ if (!key) return { error: "Session not found" };
102
+ keys.push(key);
103
+ }
104
+ out.order = keys;
105
+ }
106
+ return { op: out };
107
+ }
108
+
109
+ function legacyFavoriteKeys(userId, env) {
110
+ var list = [];
111
+ sm.sessions.forEach(function (session) {
112
+ if (!session.bookmarked) return;
113
+ var key = sessionProvenance.ensureOrigin(session);
114
+ if (!key || !env.canOrganize(key)) return;
115
+ if (session.ownerId && session.ownerId !== userId) return;
116
+ list.push({ key: key, order: typeof session.favoriteOrder === "number" ? session.favoriteOrder : Number.MAX_SAFE_INTEGER, activity: session.lastActivity || 0 });
117
+ });
118
+ list.sort(function (a, b) {
119
+ if (a.order !== b.order) return a.order - b.order;
120
+ return b.activity - a.activity;
121
+ });
122
+ return list.map(function (entry) { return entry.key; });
123
+ }
124
+
125
+ // Loads the caller's state, applying the one-time legacy favorites seed.
126
+ function loadState(userId, env) {
127
+ var state = usersModule.getSessionFolders(userId, slug);
128
+ if (!state.legacyFavoritesMigrated) {
129
+ state = folders.seedLegacyFavorites(state, legacyFavoriteKeys(userId, env));
130
+ var saved = usersModule.setSessionFolders(userId, slug, state);
131
+ if (saved.state) state = saved.state;
132
+ }
133
+ return state;
134
+ }
135
+
136
+ function stateMessage(state, env, extra) {
137
+ return Object.assign({ type: "session_folders_state", slug: slug, state: toClientState(state, env) }, extra || {});
138
+ }
139
+
140
+ function pushToUser(userId, state, env) {
141
+ clients.forEach(function (other) {
142
+ if (userIdFor(other) !== userId) return;
143
+ sendTo(other, stateMessage(state, env));
144
+ });
145
+ }
146
+
147
+ function sendStateTo(ws) {
148
+ var userId = userIdFor(ws);
149
+ if (!userId) return;
150
+ var env = buildEnv(userId);
151
+ sendTo(ws, stateMessage(loadState(userId, env), env));
152
+ }
153
+
154
+ // True only when the operation was accepted and saved.
155
+ function apply(ws, rawOp, requestId) {
156
+ var userId = userIdFor(ws);
157
+ if (!userId) return false;
158
+ var env = buildEnv(userId);
159
+ var current = loadState(userId, env);
160
+ var rid = requestId || null;
161
+ var translated = translateOp(rawOp, env);
162
+ if (translated.error) {
163
+ sendTo(ws, stateMessage(current, env, { error: translated.error, requestId: rid }));
164
+ return false;
165
+ }
166
+ var result = folders.applyOperation(current, translated.op, env);
167
+ if (result.error) {
168
+ sendTo(ws, stateMessage(current, env, { error: result.error, requestId: rid }));
169
+ return false;
170
+ }
171
+ var saved = usersModule.setSessionFolders(userId, slug, result.state);
172
+ if (saved.error) {
173
+ sendTo(ws, stateMessage(current, env, { error: saved.error, requestId: rid }));
174
+ return false;
175
+ }
176
+ pushToUser(userId, saved.state, env);
177
+ if (rid) sendTo(ws, stateMessage(saved.state, env, { requestId: rid, folderId: result.folderId || null }));
178
+ return true;
179
+ }
180
+
181
+ // Older clients still send the original favorite messages; they now edit the
182
+ // same personal state instead of the shared session flag.
183
+ function legacyBookmark(ws, msg) {
184
+ var userId = userIdFor(ws);
185
+ if (!userId || typeof msg.sessionId !== "number") return;
186
+ var env = buildEnv(userId);
187
+ var key = keyOfId(env, msg.sessionId);
188
+ if (!key) { apply(ws, { op: "set_favorite", sessionId: msg.sessionId, favorite: false }); return; }
189
+ // The bookmark is the Favorites tag; the session's folder is not touched.
190
+ apply(ws, { op: "set_favorite", sessionId: msg.sessionId, favorite: msg.bookmarked === true });
191
+ }
192
+
193
+ function legacyReorder(ws, msg) {
194
+ var userId = userIdFor(ws);
195
+ if (!userId) return;
196
+ var env = buildEnv(userId);
197
+ var sourceKey = keyOfId(env, msg.sourceId);
198
+ var targetKey = keyOfId(env, msg.targetId);
199
+ if (!sourceKey || !targetKey) return;
200
+ var current = loadState(userId, env);
201
+ var order = current.favorites.filter(function (k) { return k !== sourceKey; });
202
+ var at = order.indexOf(targetKey);
203
+ if (at === -1 || current.favorites.indexOf(sourceKey) === -1) return;
204
+ order.splice(msg.insertBefore === false ? at + 1 : at, 0, sourceKey);
205
+ apply(ws, { op: "set_order", containerKey: folders.FAVORITES_ID, order: idsOf(env, order) });
206
+ }
207
+
208
+ // A request carries the project it was built for; a stale dialog or a
209
+ // reconnect to another project must never mutate this project's folders.
210
+ function wrongProject(ws, claimedSlug, requestId) {
211
+ if (claimedSlug === slug) return false;
212
+ var userId = userIdFor(ws);
213
+ if (userId) {
214
+ var env = buildEnv(userId);
215
+ sendTo(ws, stateMessage(loadState(userId, env), env, { error: "These folders belong to a different project", requestId: requestId || null }));
216
+ }
217
+ return true;
218
+ }
219
+
220
+ // Files a session that was just created from a folder menu. The session
221
+ // exists either way; a refused placement only reports the error.
222
+ function placeNewSession(ws, session, folderId, claimedSlug) {
223
+ if (wrongProject(ws, claimedSlug, null)) return false;
224
+ return apply(ws, { op: "place_session", sessionId: session.localId, folderId: folderId });
225
+ }
226
+
227
+ // Checked before a session is created for a folder, so a folder that was
228
+ // deleted (or never existed for this user) refuses the request instead of
229
+ // silently filing the session somewhere else. Unfiled needs no folder.
230
+ function checkNewSessionFolder(ws, folderId, claimedSlug) {
231
+ if (folderId === undefined) return { ok: true };
232
+ if (claimedSlug !== slug) return { ok: false, error: "These folders belong to a different project" };
233
+ var userId = userIdFor(ws);
234
+ if (!userId) return { ok: false, error: "Your account is no longer available." };
235
+ if (folderId === folders.FAVORITES_ID) return { ok: false, error: "Favorites is a tag. Create the session in a folder or Unfiled." };
236
+ if (folderId === null || folderId === folders.UNFILED_ID) return { ok: true };
237
+ var state = loadState(userId, buildEnv(userId));
238
+ for (var i = 0; i < state.folders.length; i++) if (state.folders[i].id === folderId) return { ok: true };
239
+ return { ok: false, error: "That folder no longer exists. Pick another folder and try again." };
240
+ }
241
+
242
+ function cleanRequestId(value) {
243
+ return typeof value === "string" && value.length <= 64 ? value : null;
244
+ }
245
+
246
+ // Choosing what happens to a folder's sessions lives in its own module; it
247
+ // reuses this module's authenticated state helpers and the shared session
248
+ // deletion service.
249
+ var folderDelete = folderDeleteModule.attachFolderDelete({
250
+ sm: sm, slug: slug, sendTo: sendTo, usersModule: usersModule, sessionDelete: ctx.sessionDelete,
251
+ getProjectAccess: getProjectAccess, userIdFor: userIdFor, buildEnv: buildEnv, loadState: loadState,
252
+ stateMessage: stateMessage, pushToUser: pushToUser, wrongProject: wrongProject, cleanRequestId: cleanRequestId,
253
+ });
254
+
255
+ function handleMessage(ws, msg) {
256
+ if (folderDelete.handleMessage(ws, msg)) return true;
257
+ if (msg.type === "session_folders_get") { sendStateTo(ws); return true; }
258
+ if (msg.type === "session_folders_op") {
259
+ var rid = typeof msg.requestId === "string" && msg.requestId.length <= 64 ? msg.requestId : null;
260
+ if (wrongProject(ws, msg.slug, rid)) return true;
261
+ apply(ws, msg.op, typeof msg.requestId === "string" && msg.requestId.length <= 64 ? msg.requestId : null);
262
+ return true;
263
+ }
264
+ if (msg.type === "set_session_bookmark") { legacyBookmark(ws, msg); return true; }
265
+ if (msg.type === "reorder_session_bookmarks") { legacyReorder(ws, msg); return true; }
266
+ return false;
267
+ }
268
+
269
+ return { handleMessage: handleMessage, sendStateTo: sendStateTo, placeNewSession: placeNewSession, checkNewSessionFolder: checkNewSessionFolder };
270
+ }
271
+
272
+ module.exports = { attachSessionFolders: attachSessionFolders };
@@ -8,7 +8,9 @@ var yoke = require("./yoke");
8
8
  var sessionVisibility = require("./session-visibility");
9
9
  var contextUsageModule = require("./context-usage");
10
10
  var sessionDeleteModule = require("./project-session-delete");
11
+ var sessionFoldersModule = require("./project-session-folders");
11
12
  var elicitationResponse = require("./project-elicitation-response");
13
+ var sessionCreationModule = require("./project-session-creation");
12
14
 
13
15
  function vendorSupportsTui(vendor) {
14
16
  var info = yoke.getVendorInfo(vendor);
@@ -344,6 +346,16 @@ function attachSessions(ctx) {
344
346
  getProjectAccess: getProjectAccess, stopTitleWatcher: stopTitleWatcher,
345
347
  });
346
348
 
349
+ var sessionFolders = sessionFoldersModule.attachSessionFolders({
350
+ sm: sm, usersModule: usersModule, slug: slug, clients: clients, sendTo: sendTo, getProjectAccess: getProjectAccess,
351
+ sessionDelete: sessionDelete,
352
+ });
353
+
354
+ var sessionCreation = sessionCreationModule.attachSessionCreation({
355
+ slug: slug, isMate: isMate, osUsers: osUsers, usersModule: usersModule, opts: opts, sendTo: sendTo, send: send,
356
+ getProjectAccess: getProjectAccess,
357
+ getVendorAvailability: function (ws) { return ctx.getVendorAvailability(ws); },
358
+ });
347
359
  // Kick off watchers for any TUI sessions already loaded from disk so
348
360
  // their titles refresh as soon as Claude Code rewrites the jsonl, even
349
361
  // before the user clicks them.
@@ -623,6 +635,28 @@ function attachSessions(ctx) {
623
635
  ws._scheduledInterviewSessionRequests[scheduledRequestId] = pendingScheduled;
624
636
  return true;
625
637
  }
638
+ var creationRequestId = typeof msg.requestId === "string" ? msg.requestId.substring(0, 200) : "";
639
+ var creationFailed = function (text) {
640
+ if (creationRequestId) sendTo(ws, { type: "new_session_result", requestId: creationRequestId, ok: false, error: text });
641
+ else sendTo(ws, { type: "error", text: text });
642
+ };
643
+ // The same request id is created at most once per socket.
644
+ if (creationRequestId) {
645
+ if (!ws._createdRequests) ws._createdRequests = [];
646
+ if (ws._createdRequests.indexOf(creationRequestId) !== -1) return true;
647
+ ws._createdRequests.push(creationRequestId);
648
+ if (ws._createdRequests.length > 50) ws._createdRequests.shift();
649
+ }
650
+ // A destination folder is checked before anything is created, so a stale
651
+ // folder refuses the request instead of filing the session elsewhere.
652
+ // An acknowledged creation (the sidebar rows) only runs a provider that is
653
+ // installed and authorized for this caller.
654
+ if (creationRequestId && msg.vendor && !sessionCreation.isInstalled(ws, msg.vendor)) {
655
+ creationFailed("That provider is not installed or authorized.");
656
+ return true;
657
+ }
658
+ var folderCheck = sessionFolders.checkNewSessionFolder(ws, msg.folderId, msg.folderSlug);
659
+ if (!folderCheck.ok) { creationFailed(folderCheck.error); return true; }
626
660
  // Opportunistic hygiene: drop blanks that sat untouched past the grace
627
661
  // period before deciding whether to reuse or create.
628
662
  sm.sweepBlankSessions();
@@ -726,7 +760,15 @@ function attachSessions(ctx) {
726
760
  }
727
761
  }
728
762
  ws._clayActiveSession = newSess.localId;
729
- if (msg.requestId) sendTo(ws, { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: true, sessionId: newSess.localId });
763
+ var folderPlaced = msg.folderId === undefined ? null : sessionFolders.placeNewSession(ws, newSess, msg.folderId, msg.folderSlug);
764
+ if (msg.requestId) {
765
+ var created = { type: "new_session_result", requestId: String(msg.requestId).substring(0, 200), ok: true, sessionId: newSess.localId };
766
+ if (folderPlaced !== null) {
767
+ created.folderPlaced = folderPlaced === true;
768
+ created.folderId = msg.folderId === null ? "unfiled" : msg.folderId;
769
+ }
770
+ sendTo(ws, created);
771
+ }
730
772
  // Remember the vendor only when the client asked for one explicitly.
731
773
  // new_session without a vendor (mate sidebar, notification banner,
732
774
  // debate) falls through to the default adapter and must not clobber
@@ -759,31 +801,8 @@ function attachSessions(ctx) {
759
801
  return true;
760
802
  }
761
803
 
762
- if (msg.type === "set_session_bookmark") {
763
- if (typeof msg.sessionId === "number") {
764
- var bookmarkTarget = sm.sessions.get(msg.sessionId);
765
- if (!bookmarkTarget) return true;
766
- if (usersModule.isMultiUser() && ws._clayUser) {
767
- if (!usersModule.canAccessSession(ws._clayUser.id, bookmarkTarget, { visibility: "public" })) return true;
768
- }
769
- sm.setSessionBookmarked(msg.sessionId, !!msg.bookmarked);
770
- }
771
- return true;
772
- }
773
-
774
- if (msg.type === "reorder_session_bookmarks") {
775
- if (typeof msg.sourceId === "number" && typeof msg.targetId === "number" && msg.sourceId !== msg.targetId) {
776
- var source = sm.sessions.get(msg.sourceId);
777
- var target = sm.sessions.get(msg.targetId);
778
- if (!source || !target) return true;
779
- if (usersModule.isMultiUser() && ws._clayUser) {
780
- if (!usersModule.canAccessSession(ws._clayUser.id, source, { visibility: "public" })) return true;
781
- if (!usersModule.canAccessSession(ws._clayUser.id, target, { visibility: "public" })) return true;
782
- }
783
- sm.reorderBookmarkedSessions(msg.sourceId, msg.targetId, msg.insertBefore !== false);
784
- }
785
- return true;
786
- }
804
+ if (sessionFolders.handleMessage(ws, msg)) return true;
805
+ if (sessionCreation.handleMessage(ws, msg)) return true;
787
806
 
788
807
  if (msg.type === "bulk_delete_sessions") {
789
808
  sessionDelete.bulkDeleteSessions(ws, msg);
@@ -2254,6 +2273,7 @@ function attachSessions(ctx) {
2254
2273
  return {
2255
2274
  handleSessionsMessage: handleSessionsMessage,
2256
2275
  resolveSessionForView: resolveSessionForView,
2276
+ sendSessionFoldersState: sessionFolders.sendStateTo,
2257
2277
  respondToAskUser: respondToAskUser,
2258
2278
  respondToHomePermission: function (ws, msg, session) {
2259
2279
  if (!session || !msg || !session.pendingPermissions || !session.pendingPermissions[msg.requestId]) return false;
package/lib/project.js CHANGED
@@ -2072,6 +2072,7 @@ function createProjectContext(opts) {
2072
2072
  pushModule: pushModule,
2073
2073
  getSessionForWs: getSessionForWs,
2074
2074
  resolveDefaultAi: opts.resolveDefaultAi,
2075
+ getVendorAvailability: function (ws) { return _models.getVendorAvailability(ws); },
2075
2076
  canCreateScheduledTaskInterview: function (ws, session) { return !!(_scheduledTasks && _scheduledTasks.canCreateInterview(ws, session)); },
2076
2077
  canStartScheduledTaskInterview: function (ws, session) { return !!(_scheduledTasks && _scheduledTasks.canStartInterview(ws, session)); },
2077
2078
  onHumanPairStop: function (session) {
@@ -2615,6 +2616,7 @@ function createProjectContext(opts) {
2615
2616
  _vendorLogin: _vendorLogin,
2616
2617
  autonomousRun: _autonomousRun,
2617
2618
  resolveSessionForView: _sessions.resolveSessionForView,
2619
+ sendSessionFoldersState: _sessions.sendSessionFoldersState,
2618
2620
  hydrateImageRefs: hydrateImageRefs,
2619
2621
  broadcastClientCount: broadcastClientCount,
2620
2622
  broadcastPresence: broadcastPresence,
package/lib/public/app.js CHANGED
@@ -284,6 +284,16 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
284
284
  panePinPending: paneMode && !!paneSessionId,
285
285
  splitPanes: null,
286
286
  splitGroups: [],
287
+ sessionFolders: null,
288
+ sessionFoldersSlug: null,
289
+ sessionFolderCreate: null,
290
+ sessionSearch: null,
291
+ sessionFolderViewMenu: null,
292
+ sessionFolderLayouts: {},
293
+ sessionFolderDrag: null,
294
+ sessionFolderMenu: null,
295
+ sessionFolderContext: null,
296
+ sessionFolderModal: null,
287
297
  splitDelegations: {},
288
298
  pairResultStatus: [],
289
299
  notesTemporarilyHidden: false,
@@ -4,13 +4,13 @@
4
4
 
5
5
  /* --- Session actions ---
6
6
  Compact icon+caption grid. Tile styles come from .palette-tile in
7
- sidebar.css so the same tiles render correctly in both the active
8
- grid here and the hidden-grid when edit-mode moves them. */
7
+ sidebar.css so the same tiles render in the session and Mate tool
8
+ grids. */
9
9
  #session-actions {
10
10
  display: grid;
11
11
  grid-template-columns: repeat(4, 1fr);
12
12
  gap: 4px;
13
- padding: 0 8px 4px;
13
+ padding: 4px 8px 4px;
14
14
  }
15
15
 
16
16
  /* Badge becomes a small corner indicator in icon-only grid */
@@ -1050,7 +1050,7 @@ body.mate-dm-active #layout.sidebar-collapsed .mate-collapsed-info {
1050
1050
  display: grid;
1051
1051
  grid-template-columns: repeat(4, 1fr);
1052
1052
  gap: 4px;
1053
- padding: 0 8px 4px;
1053
+ padding: 4px 8px 4px;
1054
1054
  }
1055
1055
 
1056
1056
  /* Knowledge editor panel (overlays main content) */
@@ -332,134 +332,6 @@
332
332
  padding-bottom: calc(var(--safe-bottom) + 16px);
333
333
  }
334
334
 
335
- /* --- New session button --- */
336
- .mobile-session-new {
337
- display: flex;
338
- align-items: center;
339
- gap: 8px;
340
- width: 100%;
341
- padding: 12px 12px;
342
- margin-bottom: 4px;
343
- background: none;
344
- border: none;
345
- color: var(--accent);
346
- font-family: var(--font-ui);
347
- font-size: 15px;
348
- font-weight: 600;
349
- cursor: pointer;
350
- text-align: left;
351
- -webkit-tap-highlight-color: transparent;
352
- }
353
- .mobile-session-new:last-of-type {
354
- border-bottom: 1px solid var(--border-subtle);
355
- }
356
-
357
- .mobile-session-new:active {
358
- background: rgba(var(--overlay-rgb), 0.06);
359
- }
360
-
361
- .mobile-session-new:focus-visible {
362
- outline: 2px solid var(--accent);
363
- outline-offset: 2px;
364
- }
365
-
366
- /* Primary creation action with an adjacent icon-only provider selector. */
367
- .mobile-session-new-row {
368
- display: flex;
369
- gap: 0;
370
- padding: 6px 8px 8px;
371
- }
372
-
373
- .mobile-session-new-row .mobile-session-new-main {
374
- flex: 1 1 auto;
375
- width: auto;
376
- margin-bottom: 0;
377
- min-width: 0;
378
- padding: 10px 12px;
379
- border: 1px solid var(--border-subtle);
380
- border-radius: 8px;
381
- background: rgba(var(--overlay-rgb), 0.03);
382
- color: var(--text);
383
- gap: 8px;
384
- border-radius: 8px 0 0 8px;
385
- white-space: normal;
386
- line-height: 1.2;
387
- }
388
- .mobile-session-new-row .mobile-session-new-provider {
389
- flex: 0 0 34px;
390
- width: 34px;
391
- min-width: 34px;
392
- margin-bottom: 0;
393
- padding: 0;
394
- border: 1px solid var(--border-subtle);
395
- border-left-color: var(--border);
396
- border-radius: 0 8px 8px 0;
397
- background: rgba(var(--overlay-rgb), 0.03);
398
- color: var(--text-muted);
399
- display: inline-flex;
400
- align-items: center;
401
- gap: 0;
402
- justify-content: center;
403
- }
404
- .mobile-session-new-row .mobile-session-new-main:active,
405
- .mobile-session-new-row .mobile-session-new-provider:active {
406
- background: rgba(var(--overlay-rgb), 0.1);
407
- }
408
-
409
- .mobile-session-new-row .session-create-provider-chevron .lucide,
410
- .mobile-session-new-row .session-create-provider-chevron svg {
411
- width: 14px;
412
- height: 14px;
413
- }
414
-
415
- .mobile-vendor-list {
416
- display: flex;
417
- flex-direction: column;
418
- gap: 4px;
419
- padding: 0 8px 8px;
420
- border-bottom: 1px solid var(--border-subtle);
421
- }
422
- .mobile-vendor-list.hidden { display: none; }
423
-
424
- .mobile-vendor-list .mobile-session-new-vendor {
425
- width: 100%;
426
- margin-bottom: 0;
427
- padding: 10px 12px;
428
- border: 1px solid var(--border-subtle);
429
- border-radius: 8px;
430
- background: rgba(var(--overlay-rgb), 0.03);
431
- color: var(--text);
432
- font-weight: 500;
433
- gap: 8px;
434
- }
435
- .mobile-vendor-list .mobile-session-new-vendor.active { font-weight: 700; }
436
- /* Uninstalled vendors stay tappable: the tap opens the vendor's homepage. */
437
- .mobile-vendor-list .mobile-session-new-vendor.disabled { opacity: 0.45; }
438
- .mobile-vendor-list .mobile-session-new-vendor:active {
439
- background: rgba(var(--overlay-rgb), 0.1);
440
- }
441
- .mobile-vendor-row { display: flex; align-items: stretch; gap: 4px; }
442
- .mobile-vendor-row .mobile-session-new-vendor { flex: 1 1 auto; min-width: 0; }
443
- .mobile-vendor-set-default { border: 1px solid var(--border-subtle); border-radius: 8px; background: transparent; color: var(--text-dimmer); padding: 0 8px; font-size: 11px; white-space: nowrap; }
444
- .mobile-vendor-set-default:active { background: rgba(var(--overlay-rgb), 0.1); }
445
- .mobile-vendor-set-default:disabled { opacity: 0.55; }
446
- .mobile-session-new-preference-status { padding: 4px 8px 8px; color: var(--error); font-size: 11px; }
447
- .mobile-session-new-fallback-note { color: var(--text-muted); }
448
- .mobile-vendor-note {
449
- margin-left: auto;
450
- font-size: 12px;
451
- font-weight: 500;
452
- color: var(--text-dimmer);
453
- }
454
-
455
- .mobile-session-new-icon {
456
- width: 18px;
457
- height: 18px;
458
- flex-shrink: 0;
459
- border-radius: 4px;
460
- object-fit: cover;
461
- }
462
-
463
335
  /* No hover on touch: rest state stays dimmed, the active session shows color. */
464
336
  .mobile-session-vendor-icon {
465
337
  width: 14px;