clay-server 4.0.0-beta.20 → 4.0.0-beta.22
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/public/app.js +4 -0
- package/lib/public/css/admin.css +2 -2
- package/lib/public/modules/app-connection.js +15 -2
- package/lib/public/modules/app-home-hub.js +5 -13
- package/lib/public/modules/app-messages.js +3 -3
- package/lib/public/modules/app-projects.js +46 -13
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/sidebar-sessions.js +2 -2
- package/lib/session-visibility.js +3 -1
- package/lib/users-permissions.js +4 -4
- package/package.json +1 -1
package/lib/public/app.js
CHANGED
|
@@ -277,6 +277,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
277
277
|
projectName: projectName,
|
|
278
278
|
cwd: "",
|
|
279
279
|
currentSlug: currentSlug,
|
|
280
|
+
activeProjectSlug: null,
|
|
281
|
+
sessionActivatedProjectSlug: null,
|
|
282
|
+
socketPath: null,
|
|
283
|
+
pendingHomeProjectSlug: null,
|
|
280
284
|
currentProjectOwnerId: null,
|
|
281
285
|
isOsUsers: false,
|
|
282
286
|
skipPermsEnabled: false,
|
package/lib/public/css/admin.css
CHANGED
|
@@ -651,7 +651,7 @@
|
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
/* --- Session visibility indicator --- */
|
|
654
|
-
.session-
|
|
654
|
+
.session-shared-icon {
|
|
655
655
|
display: inline-flex;
|
|
656
656
|
align-items: center;
|
|
657
657
|
margin-right: 4px;
|
|
@@ -659,7 +659,7 @@
|
|
|
659
659
|
vertical-align: middle;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
-
.session-
|
|
662
|
+
.session-shared-icon svg {
|
|
663
663
|
width: 12px;
|
|
664
664
|
height: 12px;
|
|
665
665
|
}
|
|
@@ -221,15 +221,24 @@ export function connect() {
|
|
|
221
221
|
// two sockets can never both be pinging.
|
|
222
222
|
stopHeartbeat();
|
|
223
223
|
if (ws && ws.readyState === 1) setStatus("disconnected");
|
|
224
|
-
if (ws) {
|
|
224
|
+
if (ws) {
|
|
225
|
+
ws.onopen = null;
|
|
226
|
+
ws.onmessage = null;
|
|
227
|
+
ws.onerror = null;
|
|
228
|
+
ws.onclose = null;
|
|
229
|
+
ws.close();
|
|
230
|
+
}
|
|
225
231
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
226
232
|
|
|
227
233
|
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
228
|
-
var
|
|
234
|
+
var socketPath = store.get('wsPath');
|
|
235
|
+
store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
|
|
236
|
+
var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
|
|
229
237
|
setWs(newWs);
|
|
230
238
|
|
|
231
239
|
// If not connected within 3s, force retry
|
|
232
240
|
connectTimeoutId = setTimeout(function () {
|
|
241
|
+
if (getWs() !== newWs) return;
|
|
233
242
|
if (!store.get('connected')) {
|
|
234
243
|
newWs.onclose = null;
|
|
235
244
|
newWs.onerror = null;
|
|
@@ -239,6 +248,7 @@ export function connect() {
|
|
|
239
248
|
}, 3000);
|
|
240
249
|
|
|
241
250
|
newWs.onopen = function () {
|
|
251
|
+
if (getWs() !== newWs) return;
|
|
242
252
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
243
253
|
if (hasConnectedOnce && disconnectedAt) {
|
|
244
254
|
console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
|
|
@@ -267,6 +277,7 @@ export function connect() {
|
|
|
267
277
|
};
|
|
268
278
|
|
|
269
279
|
newWs.onclose = function (e) {
|
|
280
|
+
if (getWs() !== newWs) return;
|
|
270
281
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
271
282
|
stopHeartbeat();
|
|
272
283
|
disconnectedAt = Date.now();
|
|
@@ -277,6 +288,7 @@ export function connect() {
|
|
|
277
288
|
" reason=" + (e.reason || "(none)") + " wasClean=false");
|
|
278
289
|
}
|
|
279
290
|
closeDmUserPicker();
|
|
291
|
+
store.set({ activeProjectSlug: null, sessionActivatedProjectSlug: null });
|
|
280
292
|
setStatus("disconnected");
|
|
281
293
|
setActivity(null);
|
|
282
294
|
scheduleReconnect();
|
|
@@ -285,6 +297,7 @@ export function connect() {
|
|
|
285
297
|
newWs.onerror = function () {};
|
|
286
298
|
|
|
287
299
|
newWs.onmessage = function (event) {
|
|
300
|
+
if (getWs() !== newWs) return;
|
|
288
301
|
// Backup: if we're receiving messages, we're connected
|
|
289
302
|
if (!store.get('connected')) {
|
|
290
303
|
setStatus("connected");
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { store } from './store.js';
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
5
|
import { getCachedProjects, switchProject } from './app-projects.js';
|
|
6
|
+
import { chooseProjectActivationTarget } from './project-activation.js';
|
|
6
7
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
8
|
import { exitDmMode } from './app-dm.js';
|
|
8
9
|
import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
|
|
@@ -45,18 +46,10 @@ function syncHomeCloseControl() {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function getHomeReturnSlug() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (var pi = 0; pi < projects.length; pi++) {
|
|
53
|
-
if (projects[pi] && !projects[pi].isMate && projects[pi].slug === candidates[ci]) return candidates[ci];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
for (var i = 0; i < projects.length; i++) {
|
|
57
|
-
if (projects[i] && !projects[i].isMate && projects[i].slug) return projects[i].slug;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
49
|
+
return chooseProjectActivationTarget(
|
|
50
|
+
getCachedProjects(),
|
|
51
|
+
[store.get('currentSlug'), store.get('homeSurfaceProjectSlug')]
|
|
52
|
+
);
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
function getVisibleMates() {
|
|
@@ -407,7 +400,6 @@ export function hideHomeHub() {
|
|
|
407
400
|
export function minimizeHomeHub() {
|
|
408
401
|
var slug = getHomeReturnSlug();
|
|
409
402
|
if (!slug || !homeHubVisible) return;
|
|
410
|
-
rememberHomePrimarySurface("project");
|
|
411
403
|
switchProject(slug);
|
|
412
404
|
var projectInput = document.getElementById("input");
|
|
413
405
|
if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
|
|
@@ -63,7 +63,7 @@ import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
|
63
63
|
import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
|
|
64
64
|
import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
|
|
65
65
|
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
66
|
-
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
|
|
66
|
+
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress, finishProjectSessionActivation } from './app-projects.js';
|
|
67
67
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
68
68
|
import { hideHomeHub, showHomeHub } from './app-home-hub.js';
|
|
69
69
|
import { handleToolsState, handleToolInstalled, handleToolRemoved, handleToolStorageResult, handleToolLlmResult, handleToolsError, handleToolControlRequest, handleToolServerState, handleToolServerEvent, handleCapsuleGameSession } from './home-tools.js';
|
|
@@ -311,7 +311,7 @@ export function processMessage(msg) {
|
|
|
311
311
|
detachTuiView();
|
|
312
312
|
store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
|
|
313
313
|
if (msg.cwd) store.set({ cwd: msg.cwd });
|
|
314
|
-
if (msg.slug) store.set({ currentSlug: msg.slug });
|
|
314
|
+
if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug });
|
|
315
315
|
try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
|
|
316
316
|
// In mate DM, keep title as mate name and re-apply mate color
|
|
317
317
|
if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
|
|
@@ -762,7 +762,6 @@ export function processMessage(msg) {
|
|
|
762
762
|
break;
|
|
763
763
|
|
|
764
764
|
case "session_switched":
|
|
765
|
-
hideHomeHub();
|
|
766
765
|
closeWhatsNewArticle();
|
|
767
766
|
// Save draft from outgoing session
|
|
768
767
|
var _prevSid = store.get('activeSessionId');
|
|
@@ -860,6 +859,7 @@ export function processMessage(msg) {
|
|
|
860
859
|
// Reload survival: a restored active session that is a split-group
|
|
861
860
|
// member reopens its group (no-op when a split is already open).
|
|
862
861
|
maybeRestoreSplitGroup();
|
|
862
|
+
if (finishProjectSessionActivation()) hideHomeHub();
|
|
863
863
|
break;
|
|
864
864
|
|
|
865
865
|
case "session_full_access_changed":
|
|
@@ -36,6 +36,8 @@ import { resetDebateState } from './debate.js';
|
|
|
36
36
|
import { removeDebateBottomBar } from './app-debate-ui.js';
|
|
37
37
|
import { closeProjectSettings } from './project-settings.js';
|
|
38
38
|
import { chooseProjectAfterRemoval } from './project-removal-target.js';
|
|
39
|
+
import { isProjectActivated, isProjectActivationPending, isProjectContextConnected, projectWsPath } from './project-activation.js';
|
|
40
|
+
import { rememberHomePrimarySurface } from './home-surface.js';
|
|
39
41
|
|
|
40
42
|
// --- Module-owned state ---
|
|
41
43
|
var cachedProjects = [];
|
|
@@ -417,30 +419,32 @@ export function switchProject(slug) {
|
|
|
417
419
|
if (!slug) return;
|
|
418
420
|
document.cookie = "clay_last_project=" + encodeURIComponent(slug) + "; Path=/; SameSite=Strict; Max-Age=31536000";
|
|
419
421
|
var st = store.snap();
|
|
420
|
-
var
|
|
421
|
-
var
|
|
422
|
+
var ws = getWs();
|
|
423
|
+
var targetWsPath = projectWsPath(slug);
|
|
424
|
+
var homeVisible = isHomeHubVisible();
|
|
425
|
+
var alreadyInProject = isProjectActivated(st, slug, ws);
|
|
422
426
|
var wasDm = st.dmMode;
|
|
423
427
|
var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
|
|
424
428
|
if (st.dmMode) exitDmMode(wasMate);
|
|
425
429
|
closeWhatsNewArticle();
|
|
426
|
-
if (
|
|
430
|
+
if (homeVisible && alreadyInProject) {
|
|
431
|
+
rememberHomePrimarySurface("project", slug);
|
|
427
432
|
hideHomeHub();
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
history.pushState(null, "", "/p/" + slug + "/");
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
433
|
+
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
434
|
+
history.replaceState(null, "", "/p/" + slug + "/");
|
|
435
|
+
} else {
|
|
436
|
+
history.pushState(null, "", "/p/" + slug + "/");
|
|
435
437
|
}
|
|
438
|
+
return;
|
|
436
439
|
}
|
|
437
440
|
if (alreadyInProject) {
|
|
438
|
-
var ws = getWs();
|
|
439
441
|
if (wasDm && ws && ws.readyState === 1) {
|
|
440
442
|
ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
|
|
441
443
|
}
|
|
442
444
|
return;
|
|
443
445
|
}
|
|
446
|
+
if (homeVisible) store.set({ pendingHomeProjectSlug: slug });
|
|
447
|
+
if (isProjectActivationPending(store.snap(), slug, ws)) return;
|
|
444
448
|
resetFileBrowser();
|
|
445
449
|
closeNotesBrowser();
|
|
446
450
|
hideMemory();
|
|
@@ -449,13 +453,42 @@ export function switchProject(slug) {
|
|
|
449
453
|
store.set({ currentSlug: slug });
|
|
450
454
|
store.set({ basePath: "/p/" + slug + "/" });
|
|
451
455
|
store.set({ wsPath: targetWsPath });
|
|
456
|
+
if (!homeVisible) {
|
|
457
|
+
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
458
|
+
history.replaceState(null, "", "/p/" + slug + "/");
|
|
459
|
+
} else {
|
|
460
|
+
history.pushState(null, "", "/p/" + slug + "/");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
resetClientState();
|
|
464
|
+
connect();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function completePendingProjectActivation() {
|
|
468
|
+
var slug = store.get('pendingHomeProjectSlug');
|
|
469
|
+
if (!slug || !isProjectActivated(store.snap(), slug, getWs())) return false;
|
|
470
|
+
store.set({ pendingHomeProjectSlug: null });
|
|
471
|
+
rememberHomePrimarySurface("project", slug);
|
|
452
472
|
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
453
473
|
history.replaceState(null, "", "/p/" + slug + "/");
|
|
454
474
|
} else {
|
|
455
475
|
history.pushState(null, "", "/p/" + slug + "/");
|
|
456
476
|
}
|
|
457
|
-
|
|
458
|
-
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function confirmCurrentProjectSessionActivation() {
|
|
481
|
+
var slug = store.get('activeProjectSlug');
|
|
482
|
+
if (!slug || !isProjectContextConnected(store.snap(), slug, getWs())) return false;
|
|
483
|
+
store.set({ sessionActivatedProjectSlug: slug });
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function finishProjectSessionActivation() {
|
|
488
|
+
var confirmed = confirmCurrentProjectSessionActivation();
|
|
489
|
+
if (!confirmed) return false;
|
|
490
|
+
if (!store.get('pendingHomeProjectSlug')) return true;
|
|
491
|
+
return completePendingProjectActivation();
|
|
459
492
|
}
|
|
460
493
|
|
|
461
494
|
export function showUpdateAvailable(msg) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure project activation and Home-return selection helpers.
|
|
2
|
+
|
|
3
|
+
export function projectWsPath(slug) {
|
|
4
|
+
return slug ? "/p/" + slug + "/ws" : null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function chooseProjectActivationTarget(projects, candidates) {
|
|
8
|
+
var available = Array.isArray(projects) ? projects : [];
|
|
9
|
+
var preferred = Array.isArray(candidates) ? candidates : [];
|
|
10
|
+
for (var ci = 0; ci < preferred.length; ci++) {
|
|
11
|
+
if (!preferred[ci]) continue;
|
|
12
|
+
for (var pi = 0; pi < available.length; pi++) {
|
|
13
|
+
if (available[pi] && !available[pi].isMate && available[pi].slug === preferred[ci]) return preferred[ci];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (var i = 0; i < available.length; i++) {
|
|
17
|
+
if (available[i] && !available[i].isMate && available[i].slug) return available[i].slug;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isProjectActivationPending(state, slug, socket) {
|
|
23
|
+
var target = projectWsPath(slug);
|
|
24
|
+
return !!target
|
|
25
|
+
&& state.currentSlug === slug
|
|
26
|
+
&& state.wsPath === target
|
|
27
|
+
&& state.socketPath === target
|
|
28
|
+
&& !!socket
|
|
29
|
+
&& (socket.readyState === 0 || socket.readyState === 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isProjectActivated(state, slug, socket) {
|
|
33
|
+
return isProjectActivationPending(state, slug, socket)
|
|
34
|
+
&& socket.readyState === 1
|
|
35
|
+
&& state.connected === true
|
|
36
|
+
&& state.activeProjectSlug === slug
|
|
37
|
+
&& state.sessionActivatedProjectSlug === slug;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isProjectContextConnected(state, slug, socket) {
|
|
41
|
+
return isProjectActivationPending(state, slug, socket)
|
|
42
|
+
&& socket.readyState === 1
|
|
43
|
+
&& state.connected === true
|
|
44
|
+
&& state.activeProjectSlug === slug;
|
|
45
|
+
}
|
|
@@ -1290,8 +1290,8 @@ function renderSessionItem(s, options) {
|
|
|
1290
1290
|
if (s.loop && s.loop.source === "debate") {
|
|
1291
1291
|
textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
|
|
1292
1292
|
}
|
|
1293
|
-
if (store.get('isMultiUserMode') && s.sessionVisibility
|
|
1294
|
-
textHtml += '<span class="session-
|
|
1293
|
+
if (store.get('isMultiUserMode') && s.sessionVisibility === "shared") {
|
|
1294
|
+
textHtml += '<span class="session-shared-icon" title="Shared session" aria-label="Shared session">' + iconHtml("users") + '</span>';
|
|
1295
1295
|
}
|
|
1296
1296
|
textHtml += highlightMatch(s.title || "New Session", searchQuery);
|
|
1297
1297
|
textSpan.innerHTML = textHtml;
|
|
@@ -25,9 +25,11 @@ function handleChange(ctx, ws, msg) {
|
|
|
25
25
|
|
|
26
26
|
function revokeViewers(ctx, session) {
|
|
27
27
|
if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
|
|
28
|
+
var project = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
28
29
|
ctx.clients.forEach(function(client) {
|
|
29
30
|
if (client._clayActiveSession !== session.localId) return;
|
|
30
|
-
|
|
31
|
+
var userId = client._clayUser && client._clayUser.id;
|
|
32
|
+
if (userId && ctx.usersModule.canAccessSession(userId, session, project)) return;
|
|
31
33
|
// Clear the server-side selection before reconnecting so no further
|
|
32
34
|
// session events or input can cross the revoked access boundary.
|
|
33
35
|
client._clayActiveSession = null;
|
package/lib/users-permissions.js
CHANGED
|
@@ -89,11 +89,11 @@ function attachPermissions(deps) {
|
|
|
89
89
|
function canAccessSession(userId, session, project) {
|
|
90
90
|
// Must have project access first
|
|
91
91
|
if (!canAccessProject(userId, project)) return false;
|
|
92
|
+
// Resolve the stored user record so client-supplied roles cannot elevate access.
|
|
93
|
+
var user = findUserById(userId);
|
|
94
|
+
if (user && user.role === "admin") return true;
|
|
92
95
|
// Sessions without ownerId are legacy -- only admin can see them
|
|
93
|
-
if (!session.ownerId)
|
|
94
|
-
var user = findUserById(userId);
|
|
95
|
-
return !!(user && user.role === "admin");
|
|
96
|
-
}
|
|
96
|
+
if (!session.ownerId) return false;
|
|
97
97
|
// Owner can always see their own sessions
|
|
98
98
|
if (session.ownerId === userId) return true;
|
|
99
99
|
// Sharing is opt-in; missing or invalid visibility stays private.
|
package/package.json
CHANGED