clay-server 5.0.0-beta.1 → 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.
- package/lib/daemon.js +9 -0
- package/lib/new-session-default.js +45 -0
- package/lib/project-connection.js +4 -2
- package/lib/project-session-creation.js +107 -0
- package/lib/project-session-delete.js +21 -3
- package/lib/project-session-document.js +9 -3
- package/lib/project-session-folder-delete.js +169 -0
- package/lib/project-session-folders.js +272 -0
- package/lib/project-sessions.js +46 -26
- package/lib/project.js +2 -0
- package/lib/public/app.js +10 -0
- package/lib/public/css/filebrowser.css +3 -3
- package/lib/public/css/mates.css +1 -1
- package/lib/public/css/mobile-nav.css +0 -128
- package/lib/public/css/session-folders.css +383 -0
- package/lib/public/css/sidebar.css +28 -545
- package/lib/public/index.html +3 -21
- package/lib/public/modules/app-favicon.js +5 -4
- package/lib/public/modules/app-messages.js +13 -0
- package/lib/public/modules/input.js +3 -0
- package/lib/public/modules/project-switcher.js +4 -4
- package/lib/public/modules/session-create-form.js +428 -0
- package/lib/public/modules/session-create.js +338 -0
- package/lib/public/modules/session-folder-collapse.js +192 -0
- package/lib/public/modules/session-folder-context.js +215 -0
- package/lib/public/modules/session-folder-delete.js +313 -0
- package/lib/public/modules/session-folder-dialogs.js +173 -0
- package/lib/public/modules/session-folder-dnd.js +214 -0
- package/lib/public/modules/session-folder-draft.js +184 -0
- package/lib/public/modules/session-folder-layout.js +133 -0
- package/lib/public/modules/session-folder-toolbar.js +185 -0
- package/lib/public/modules/session-folder-view-menu.js +187 -0
- package/lib/public/modules/session-folders.js +95 -0
- package/lib/public/modules/session-list-search.js +108 -0
- package/lib/public/modules/session-presence-ui.js +44 -0
- package/lib/public/modules/sidebar-mobile.js +38 -182
- package/lib/public/modules/sidebar-session-folders-ui.js +382 -0
- package/lib/public/modules/sidebar-sessions.js +141 -718
- package/lib/public/modules/sidebar.js +10 -2
- package/lib/public/modules/terminal.js +2 -2
- package/lib/public/modules/tool-palette.js +37 -219
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +21 -14
- package/lib/server.js +4 -0
- package/lib/session-folders.js +414 -0
- package/lib/sessions.js +7 -5
- package/lib/users-session-folder-preferences.js +69 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +11 -0
- package/lib/yoke/adapters/codex.js +2 -2
- package/package.json +1 -1
- package/lib/public/modules/session-creation-cta.js +0 -46
- 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 };
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
763
|
-
|
|
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
|
|
8
|
-
|
|
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:
|
|
13
|
+
padding: 4px 8px 4px;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/* Badge becomes a small corner indicator in icon-only grid */
|
package/lib/public/css/mates.css
CHANGED
|
@@ -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:
|
|
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;
|