clay-server 5.2.0-beta.6 → 5.2.0-beta.8
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/clay-control-presentation-binding.js +96 -0
- package/lib/clay-control-presentation.js +128 -0
- package/lib/clay-control-target-projection.js +26 -0
- package/lib/clay-mate-chat.js +1 -0
- package/lib/clay-mate-knowledge.js +1 -1
- package/lib/clay-product-control-tools.js +1 -1
- package/lib/clay-product-control.js +22 -12
- package/lib/clay-project-records.js +2 -1
- package/lib/clay-scheduling.js +1 -1
- package/lib/clay-screen-controls.js +13 -5
- package/lib/clay-workspace-controls.js +2 -1
- package/lib/contextual-screen-snapshot.js +38 -0
- package/lib/mate-context-service.js +138 -0
- package/lib/mate-context-tools.js +21 -0
- package/lib/project-connection.js +8 -5
- package/lib/project-control-git-view.js +12 -0
- package/lib/project-filesystem.js +5 -1
- package/lib/project-http.js +3 -0
- package/lib/project-notifications.js +5 -4
- package/lib/project-product-control.js +3 -1
- package/lib/project-scheduled-messages.js +4 -4
- package/lib/project-scheduled-task-control.js +1 -1
- package/lib/project-session-document.js +20 -5
- package/lib/project-shared-browser.js +22 -5
- package/lib/project-studio-presentation.js +58 -0
- package/lib/project-user-message.js +36 -0
- package/lib/project-workspace-query.js +11 -1
- package/lib/project.js +24 -7
- package/lib/public/app.js +24 -0
- package/lib/public/appearance-bootstrap.js +2 -0
- package/lib/public/css/contextual-clay.css +240 -0
- package/lib/public/modules/app-favicon.js +7 -7
- package/lib/public/modules/app-message-router.js +5 -1
- package/lib/public/modules/app-messages.js +14 -1
- package/lib/public/modules/app-notifications.js +10 -3
- package/lib/public/modules/app-projects.js +3 -0
- package/lib/public/modules/clay-control-activity.js +214 -0
- package/lib/public/modules/clay-control-copy.js +36 -0
- package/lib/public/modules/clay-control-detail-targets.js +44 -0
- package/lib/public/modules/clay-control-object-targets.js +52 -0
- package/lib/public/modules/clay-control-target-visibility.js +12 -0
- package/lib/public/modules/clay-control-targets.js +44 -0
- package/lib/public/modules/clay-navigation-render.js +52 -0
- package/lib/public/modules/clay-owner-navigation.js +11 -2
- package/lib/public/modules/clay-session-links.js +31 -0
- package/lib/public/modules/clay-studio-bridge.js +20 -0
- package/lib/public/modules/clay-studio-document.js +67 -0
- package/lib/public/modules/clay-studio-render.js +144 -0
- package/lib/public/modules/clay-tool-presentation.js +4 -0
- package/lib/public/modules/command-palette.js +26 -31
- package/lib/public/modules/contextual-ask-session.js +26 -0
- package/lib/public/modules/contextual-clay-geometry.js +155 -0
- package/lib/public/modules/contextual-clay-session.js +130 -0
- package/lib/public/modules/contextual-clay-workspace.js +157 -0
- package/lib/public/modules/contextual-clay.js +237 -0
- package/lib/public/modules/contextual-composer.js +91 -0
- package/lib/public/modules/contextual-mate-context.js +115 -0
- package/lib/public/modules/contextual-mate-hint.js +40 -0
- package/lib/public/modules/contextual-mate-identity.js +23 -0
- package/lib/public/modules/contextual-mate-sessions.js +134 -0
- package/lib/public/modules/contextual-search.js +137 -0
- package/lib/public/modules/contextual-session-citations.js +58 -0
- package/lib/public/modules/filebrowser.js +20 -1
- package/lib/public/modules/generation-stop.js +8 -5
- package/lib/public/modules/git-panel.js +3 -1
- package/lib/public/modules/icon-strip-tooltip.js +2 -1
- package/lib/public/modules/input.js +35 -1
- package/lib/public/modules/issues-render.js +3 -2
- package/lib/public/modules/knowledge-db-workbench.js +4 -4
- package/lib/public/modules/markdown.js +2 -34
- package/lib/public/modules/mate-knowledge-workbench.js +1 -1
- package/lib/public/modules/message-delivery.js +10 -0
- package/lib/public/modules/pane-bridge.js +2 -2
- package/lib/public/modules/pending-message-queue.js +1 -0
- package/lib/public/modules/product-review.js +26 -0
- package/lib/public/modules/project-chat-identity.js +13 -4
- package/lib/public/modules/project-logs-render.js +3 -0
- package/lib/public/modules/project-mate-navigation.js +58 -21
- package/lib/public/modules/scheduled-tasks.js +2 -0
- package/lib/public/modules/search-clay-chat.js +5 -454
- package/lib/public/modules/shared-browser.js +2 -0
- package/lib/public/modules/sidebar-mobile.js +4 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-sessions.js +1 -0
- package/lib/public/modules/split-view.js +7 -16
- package/lib/public/modules/stt.js +7 -2
- package/lib/public/modules/terminal.js +9 -0
- package/lib/public/modules/tools.js +4 -1
- package/lib/public/modules/wireframe-workbench.js +5 -2
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +4 -0
- package/lib/server-contextual-clay-settings.js +40 -0
- package/lib/server-contextual-mate-sessions.js +70 -0
- package/lib/server-home-chat.js +7 -1
- package/lib/server-home-clay-entry.js +23 -4
- package/lib/server-home-clay-session-links.js +12 -0
- package/lib/server-owner-ui.js +49 -8
- package/lib/server-settings.js +2 -0
- package/lib/server.js +17 -0
- package/lib/sessions.js +10 -3
- package/lib/terminal-manager.js +18 -11
- package/lib/users-contextual-clay-preferences.js +31 -0
- package/lib/users.js +3 -0
- package/lib/workspace-assignment-service.js +25 -0
- package/lib/workspace-query-mcp-server.js +1 -1
- package/lib/ws-request.js +1 -0
- package/lib/ws-schema.js +10 -0
- package/lib/yoke/claude-hook-installer.js +4 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { forwardStudioPresentation, registerStudioPaint } from './clay-studio-render.js';
|
|
1
2
|
import { sketchBrand, sketchCredit } from './wireframe-style.js';
|
|
2
3
|
import { store } from './store.js';
|
|
3
4
|
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
@@ -80,7 +81,7 @@ function createPanel() {
|
|
|
80
81
|
export function openWireframe(message) {
|
|
81
82
|
if (!message || typeof message.source !== 'string' || new TextEncoder().encode(message.source).length > 100000) return;
|
|
82
83
|
if (store.get('paneMode') && window.parent !== window) {
|
|
83
|
-
|
|
84
|
+
forwardStudioPresentation(message, 'clay-pane-present-wireframe');
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
claimRightWorkbench('wireframe');
|
|
@@ -92,7 +93,8 @@ export function openWireframe(message) {
|
|
|
92
93
|
el.querySelector('header strong').textContent = draft.title;
|
|
93
94
|
el.querySelector('.wireframe-save-status').textContent = '';
|
|
94
95
|
el.querySelector('form').hidden = true;
|
|
95
|
-
draw(draft, el).
|
|
96
|
+
var painted = draw(draft, el); registerStudioPaint(Object.assign({}, message, { source: draft.source }), el.querySelector("header"), painted, el.querySelector(".wireframe-workbench-canvas"));
|
|
97
|
+
painted.catch(function () {});
|
|
96
98
|
}
|
|
97
99
|
function draw(draft, el) {
|
|
98
100
|
var canvas = el.querySelector('.wireframe-workbench-canvas');
|
|
@@ -104,6 +106,7 @@ function draw(draft, el) {
|
|
|
104
106
|
var image = wireframeImage(svg, draft.title);
|
|
105
107
|
canvas.replaceChildren(image);
|
|
106
108
|
mountWireframeViewport(canvas, image, el.querySelector('.wireframe-zoom'));
|
|
109
|
+
return image.decode ? image.decode() : undefined;
|
|
107
110
|
}).catch(function (error) {
|
|
108
111
|
if (store.get('wireframeDraft') !== active) throw error;
|
|
109
112
|
status.textContent = error.name === 'AbortError' ? 'Rendering was interrupted. Reopen the wireframe to retry.' : error.message;
|
package/lib/public/style.css
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -711,6 +711,10 @@ function createSDKBridge(opts) {
|
|
|
711
711
|
// Workspace inspection and proposal posting are session-bound. Keep this
|
|
712
712
|
// list exact so a future execution/mutation tool cannot inherit approval.
|
|
713
713
|
var safeWorkspaceTools = {
|
|
714
|
+
"mcp__clay-workspace__read_current_context": true,
|
|
715
|
+
"mcp__clay-workspace__read_selected_context": true,
|
|
716
|
+
"mcp__clay-workspace__open_context_conversation": true,
|
|
717
|
+
"mcp__clay-workspace__read_context_navigation_receipt": true,
|
|
714
718
|
"mcp__clay-workspace__read_owner_screen": true,
|
|
715
719
|
"mcp__clay-workspace__inspect_owner_screen_change": true,
|
|
716
720
|
"mcp__clay-workspace__control_owner_screen": true,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Authenticated personal preferences, using the shared users/config persistence.
|
|
2
|
+
function attachContextualClaySettings(ctx) {
|
|
3
|
+
function reply(res, status, data) {
|
|
4
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
5
|
+
res.end(JSON.stringify(data));
|
|
6
|
+
}
|
|
7
|
+
function identity(req) {
|
|
8
|
+
if (!ctx.isRequestAuthed(req)) return null;
|
|
9
|
+
return ctx.users.isMultiUser() ? ctx.getMultiUserFromReq(req) : { id: 'default' };
|
|
10
|
+
}
|
|
11
|
+
function handleRequest(req, res, url) {
|
|
12
|
+
if (url !== '/api/user/contextual-clay-geometry') return false;
|
|
13
|
+
var user = identity(req);
|
|
14
|
+
if (!user) { reply(res, 401, { error: 'Sign in to manage popover preferences.' }); return true; }
|
|
15
|
+
if (req.method === 'GET') {
|
|
16
|
+
try { reply(res, 200, { geometry: ctx.users.getContextualClayGeometry(user.id) }); }
|
|
17
|
+
catch (error) { reply(res, 500, { error: 'Could not read popover preference.' }); }
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
21
|
+
if (req.headers['x-clay-settings'] !== '1' || (req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0)) {
|
|
22
|
+
reply(res, 403, { error: 'Save preferences from Clay.' }); return true;
|
|
23
|
+
}
|
|
24
|
+
var body = ''; var tooLarge = false;
|
|
25
|
+
req.setEncoding('utf8');
|
|
26
|
+
req.on('data', function (chunk) { body += chunk; if (body.length > 1024 && !tooLarge) { tooLarge = true; reply(res, 413, { error: 'Request too large.' }); } });
|
|
27
|
+
req.on('end', function () {
|
|
28
|
+
if (tooLarge) return;
|
|
29
|
+
var live = identity(req);
|
|
30
|
+
if (!live || live.id !== user.id) { reply(res, 401, { error: 'Sign in again.' }); return; }
|
|
31
|
+
try {
|
|
32
|
+
var result = ctx.users.setContextualClayGeometry(live.id, JSON.parse(body));
|
|
33
|
+
reply(res, result.error ? 400 : 200, result);
|
|
34
|
+
} catch (error) { reply(res, 400, { error: 'Could not save popover preference.' }); }
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return { handleRequest: handleRequest };
|
|
39
|
+
}
|
|
40
|
+
module.exports = { attachContextualClaySettings: attachContextualClaySettings };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Presentation-only session operations share Home's authoritative permissions,
|
|
2
|
+
// model policy, saved-session projection and session manager. No transcript tap.
|
|
3
|
+
function attachContextualMateSessions(ctx) {
|
|
4
|
+
var receipts = new Map();
|
|
5
|
+
var lifetime = 30 * 60 * 1000;
|
|
6
|
+
function stamp(mate) { return JSON.stringify([mate.id, mate.createdBy || null, mate.createdAt || null, mate.builtinKey || null]); }
|
|
7
|
+
function live(ws, userId, msg, original) {
|
|
8
|
+
if (!ctx.isActorLive(ws, userId)) throw new Error('Your account is no longer available.');
|
|
9
|
+
var found = ctx.findMateProject(userId, msg.mateId, true);
|
|
10
|
+
if (!found || found.mate.archived || found.mate.lifecycleDeletion || found.mate.createdBy && found.mate.createdBy !== userId || stamp(found.mate) !== msg.mateStamp) throw new Error('This Mate is no longer available.');
|
|
11
|
+
var status = found.ctx.getStatus();
|
|
12
|
+
if (!status || !status.isMate || status.mateId !== found.mate.id || userId && status.projectOwnerId !== userId) throw new Error('This private Mate workspace is unavailable.');
|
|
13
|
+
found.contextualManager = found.ctx.getSessionManager();
|
|
14
|
+
found.contextualRevision = JSON.stringify([status.path, status.projectOwnerId, status.mateId, stamp(found.mate)]);
|
|
15
|
+
if (original && (found.ctx !== original.ctx || found.contextualRevision !== original.contextualRevision || found.contextualManager !== original.contextualManager)) throw new Error('This Mate conversation changed.');
|
|
16
|
+
return found;
|
|
17
|
+
}
|
|
18
|
+
function reply(ws, msg, data) { ctx.sendMessage(ws, Object.assign({ type: 'contextual_mate_sessions_result', requestId: msg.requestId, mateId: msg.mateId, mateStamp: msg.mateStamp }, data)); }
|
|
19
|
+
async function perform(ws, userId, msg) {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof msg.requestId !== 'string' || !/^mate-conversations-[a-z0-9-]{36}$/.test(msg.requestId)) throw new Error('Invalid conversation request.');
|
|
22
|
+
var found = live(ws, userId, msg), manager = found.ctx.getSessionManager();
|
|
23
|
+
if (!manager) throw new Error('Conversation history is unavailable.');
|
|
24
|
+
if (msg.type === 'home_mate_sessions_list') {
|
|
25
|
+
var list = ctx.listHomeSessions(found, userId);
|
|
26
|
+
reply(ws, msg, { ok: true, sessions: list.slice(0, 500), truncated: list.length > 500 }); return;
|
|
27
|
+
}
|
|
28
|
+
var session;
|
|
29
|
+
if (msg.type === 'home_mate_session_open') {
|
|
30
|
+
session = ctx.resolveHomeSession(found, userId, msg.sessionId);
|
|
31
|
+
if (!session || typeof msg.sessionOriginId !== 'string' || msg.sessionOriginId !== session.sessionOriginId) throw new Error('This conversation is no longer available.');
|
|
32
|
+
} else {
|
|
33
|
+
var now = Date.now();
|
|
34
|
+
receipts.forEach(function (entry, key) { if (now - entry.at > lifetime) receipts.delete(key); });
|
|
35
|
+
var key = JSON.stringify([userId, msg.mateId, msg.requestId]);
|
|
36
|
+
var receipt = receipts.get(key);
|
|
37
|
+
manager.sessions.forEach(function (candidate) { if (candidate.contextualCreationRequestId === msg.requestId && ctx.ownsSession(candidate, userId)) session = candidate; });
|
|
38
|
+
if (session && session.contextualCreationMateStamp !== msg.mateStamp) throw new Error('This request belongs to a replaced Mate.');
|
|
39
|
+
if (receipt && (!session || receipt.origin !== session.sessionOriginId || receipt.stamp !== msg.mateStamp)) throw new Error('The previously created conversation is no longer available.');
|
|
40
|
+
if (!session && msg.lookupOnly) throw new Error('Creation was not confirmed. Start a new chat explicitly if you still want one.');
|
|
41
|
+
if (!session) {
|
|
42
|
+
if (receipts.size >= 1024) throw new Error('Please try starting a conversation later.');
|
|
43
|
+
var resolved = await ctx.homeModels.resolveMateModel(ws, found, userId);
|
|
44
|
+
found = live(ws, userId, msg, found);
|
|
45
|
+
// Recheck after model resolution: concurrent retries select one exact session.
|
|
46
|
+
manager.sessions.forEach(function (candidate) { if (candidate.contextualCreationRequestId === msg.requestId && ctx.ownsSession(candidate, userId)) session = candidate; });
|
|
47
|
+
receipt = receipts.get(key);
|
|
48
|
+
if (receipt && (!session || receipt.origin !== session.sessionOriginId || receipt.stamp !== msg.mateStamp)) throw new Error('The previously created conversation is no longer available.');
|
|
49
|
+
if (session && session.contextualCreationMateStamp !== msg.mateStamp) throw new Error('This request belongs to a replaced Mate.');
|
|
50
|
+
if (!session) {
|
|
51
|
+
if (receipts.size >= 1024) throw new Error('Please try starting a conversation later.');
|
|
52
|
+
session = manager.createSessionRaw({ ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null });
|
|
53
|
+
session.contextualCreationRequestId = msg.requestId;
|
|
54
|
+
session.contextualCreationMateStamp = msg.mateStamp;
|
|
55
|
+
manager.saveSessionFile(session);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!ctx.ownsSession(session, userId)) throw new Error('This conversation is no longer available.');
|
|
59
|
+
receipts.set(key, { at: now, origin: session.sessionOriginId, stamp: msg.mateStamp });
|
|
60
|
+
}
|
|
61
|
+
live(ws, userId, msg, found);
|
|
62
|
+
reply(ws, msg, { ok: true, sessionId: session.localId, sessionOriginId: session.sessionOriginId });
|
|
63
|
+
} catch (error) { reply(ws, msg, { ok: false, error: error.message || 'Conversation unavailable.' }); }
|
|
64
|
+
}
|
|
65
|
+
return { handle: function (ws, userId, msg) {
|
|
66
|
+
if (msg.presentation !== 'contextual' || !/^home_mate_(sessions_list|session_open|new_session)$/.test(msg.type)) return false;
|
|
67
|
+
perform(ws, userId, msg); return true;
|
|
68
|
+
} };
|
|
69
|
+
}
|
|
70
|
+
module.exports = { attachContextualMateSessions: attachContextualMateSessions };
|
package/lib/server-home-chat.js
CHANGED
|
@@ -123,6 +123,7 @@ function attachHomeChat(deps) {
|
|
|
123
123
|
model: sessionString(session.model, 240),
|
|
124
124
|
createdAt: createdAt,
|
|
125
125
|
lastActivity: lastActivity,
|
|
126
|
+
sessionOriginId: session.sessionOriginId || null,
|
|
126
127
|
isProcessing: !!session.isProcessing,
|
|
127
128
|
sessionRole: hierarchy[session.localId].role,
|
|
128
129
|
parentSessionId: hierarchy[session.localId].parentSessionId == null ? null : sessionReference(sessionManager.sessions.get(hierarchy[session.localId].parentSessionId)),
|
|
@@ -228,6 +229,7 @@ function attachHomeChat(deps) {
|
|
|
228
229
|
var homeDebates = attachHomeDebates({ mates: mates, findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, sendMessage: sendMessage });
|
|
229
230
|
var homeClaySessionLinks = attachHomeClaySessionLinks({
|
|
230
231
|
projects: projects,
|
|
232
|
+
findMateProject: findMateProject, ownsSession: ownsSession, isActorLive: isActorLive,
|
|
231
233
|
workspaceQueryService: deps.workspaceQueryService,
|
|
232
234
|
projectLogsService: deps.projectLogsService,
|
|
233
235
|
sendMessage: sendMessage,
|
|
@@ -300,7 +302,9 @@ function attachHomeChat(deps) {
|
|
|
300
302
|
});
|
|
301
303
|
var homeMateCreation = attachHomeMateCreation({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, sessionReference: sessionReference, setupTap: setupTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive, persistActiveSession: persistActiveSession });
|
|
302
304
|
var homeMcpConsent = require('./server-home-mcp-consent').attachHomeMcpConsent({ findMateProject: findMateProject, resolveHomeSession: resolveHomeSession, messageTap: messageTap, isActorLive: isActorLive, sendError: sendError });
|
|
303
|
-
var homeClayEntry = attachHomeClayEntry({ findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, setupSearchTap: setupSearchTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, homeModels: homeModels, isActorLive: isActorLive });
|
|
305
|
+
var homeClayEntry = attachHomeClayEntry({ findMateProject: findMateProject, ownsSession: ownsSession, sessionReference: sessionReference, setupSearchTap: setupSearchTap, sendHistory: sendHistory, sendSessionList: sendSessionList, sendError: sendError, sendModelError: sendModelError, sendMessage: sendMessage, homeModels: homeModels, isActorLive: isActorLive });
|
|
306
|
+
|
|
307
|
+
var contextualSessions = require('./server-contextual-mate-sessions').attachContextualMateSessions({ findMateProject: findMateProject, ownsSession: ownsSession, resolveHomeSession: resolveHomeSession, listHomeSessions: listHomeSessions, isActorLive: isActorLive, homeModels: homeModels, sendMessage: sendMessage });
|
|
304
308
|
|
|
305
309
|
async function openExactSession(ws, found, userId, selected, msg) {
|
|
306
310
|
await ensureSessionModel(ws, found, userId, selected, null);
|
|
@@ -448,6 +452,8 @@ function attachHomeChat(deps) {
|
|
|
448
452
|
return false;
|
|
449
453
|
}
|
|
450
454
|
|
|
455
|
+
if (contextualSessions.handle(ws, ws._clayUser ? ws._clayUser.id : null, msg)) return true;
|
|
456
|
+
|
|
451
457
|
if (msg.type === "home_mate_present") {
|
|
452
458
|
ws._homeChatPresented = msg.visible === true;
|
|
453
459
|
return true;
|
|
@@ -15,10 +15,12 @@ function attachHomeClayEntry(ctx) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async function start(ws, userId, msg) {
|
|
18
|
+
var contextual = msg.presentation === 'contextual';
|
|
18
19
|
var text = typeof msg.text === "string" ? msg.text.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 12000) : "";
|
|
19
20
|
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 240) : null;
|
|
21
|
+
if (ctx.isActorLive && !ctx.isActorLive(ws, userId)) { ctx.sendError(ws, null, 'Your account is no longer available.', requestId, null, 'actor_unavailable'); return; }
|
|
20
22
|
var found = ctx.findMateProject(userId, null, true);
|
|
21
|
-
if (!found || found.mate.builtinKey !== "clay") {
|
|
23
|
+
if (!found || found.mate.builtinKey !== "clay" || found.mate.archived || ctx.isActorLive && !ctx.isActorLive(ws, userId)) {
|
|
22
24
|
ctx.sendError(ws, null, "Clay is not available.", requestId, null, "mate_unavailable");
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
@@ -27,26 +29,43 @@ function attachHomeClayEntry(ctx) {
|
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
var manager = found.ctx.getSessionManager();
|
|
32
|
+
var mateStamp = JSON.stringify([found.mate.id, found.mate.createdAt || null, found.mate.createdBy || null]);
|
|
30
33
|
if (!manager || typeof manager.sendAndRecord !== "function") {
|
|
31
34
|
ctx.sendError(ws, found.mate.id, "Conversation history is unavailable.", requestId, null, "session_unavailable");
|
|
32
35
|
return;
|
|
33
36
|
}
|
|
34
37
|
var existing = findExisting(manager, userId, requestId);
|
|
35
38
|
if (existing) {
|
|
39
|
+
if (contextual) {
|
|
40
|
+
if (existing.homeClayEntryMode !== 'contextual' || existing.homeClayEntryText !== text || msg.sessionOriginId && msg.sessionOriginId !== existing.sessionOriginId) { ctx.sendError(ws, found.mate.id, 'This request belongs to a different conversation.', requestId, null, 'request_mismatch'); return; }
|
|
41
|
+
ctx.sendMessage(ws, { type: 'home_clay_contextual_session', requestId: requestId, mateId: found.mate.id, sessionId: existing.localId, sessionOriginId: existing.sessionOriginId }); return;
|
|
42
|
+
}
|
|
36
43
|
ctx.setupSearchTap(ws, found, existing.localId, requestId);
|
|
37
44
|
ctx.sendHistory(ws, found, existing, requestId);
|
|
38
45
|
ctx.sendSessionList(ws, found, userId);
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
48
|
+
if (contextual && msg.sessionOriginId) { ctx.sendError(ws, found.mate.id, 'This new conversation is no longer available. Start a new Ask.', requestId, null, 'session_unavailable'); return; }
|
|
41
49
|
var session = null;
|
|
42
50
|
try {
|
|
43
51
|
var resolved = await ctx.homeModels.resolveMateModel(ws, found, userId);
|
|
44
52
|
if (ctx.isActorLive && !ctx.isActorLive(ws, userId)) throw new Error("Your account is no longer available.");
|
|
45
53
|
var liveFound = ctx.findMateProject(userId, null, true);
|
|
46
|
-
if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.mate.builtinKey !== "clay") throw new Error("Clay is no longer available.");
|
|
47
|
-
|
|
54
|
+
if (!liveFound || liveFound.ctx !== found.ctx || !liveFound.mate || liveFound.ctx.getSessionManager() !== manager || liveFound.mate.builtinKey !== "clay" || JSON.stringify([liveFound.mate.id, liveFound.mate.createdAt || null, liveFound.mate.createdBy || null]) !== mateStamp || liveFound.mate.archived) throw new Error("Clay is no longer available.");
|
|
55
|
+
// Another request may have completed while model resolution was awaiting.
|
|
56
|
+
if (findExisting(manager, userId, requestId)) return start(ws, userId, msg);
|
|
57
|
+
var options = { ownerId: userId, vendor: resolved.vendor, model: resolved.model, effort: resolved.effort || null };
|
|
58
|
+
// Background creation must not switch any existing pane/native writer.
|
|
59
|
+
session = contextual ? manager.createSessionRaw(options) : manager.createSession(options, null);
|
|
48
60
|
session.homeClayEntryRequestId = requestId;
|
|
49
|
-
session.homeClayEntryMode =
|
|
61
|
+
session.homeClayEntryMode = contextual ? 'contextual' : 'search';
|
|
62
|
+
if (contextual) {
|
|
63
|
+
session.homeClayEntryText = text;
|
|
64
|
+
session.title = text.slice(0, 80);
|
|
65
|
+
manager.saveSessionFile(session);
|
|
66
|
+
ctx.sendMessage(ws, { type: 'home_clay_contextual_session', requestId: requestId, mateId: found.mate.id, sessionId: session.localId, sessionOriginId: session.sessionOriginId });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
50
69
|
manager.sendAndRecord(session, { type: "user_message", text: text });
|
|
51
70
|
session.isProcessing = true;
|
|
52
71
|
session.sentToolResults = {};
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
function attachHomeClaySessionLinks(ctx) {
|
|
5
5
|
function source(ws, msg, routedProjectSlug) {
|
|
6
|
+
if (Object.prototype.hasOwnProperty.call(msg, "contextualSource")) {
|
|
7
|
+
var hint = msg.contextualSource, userId = ws && ws._clayUser && ws._clayUser.id;
|
|
8
|
+
if (!hint || typeof hint.mateId !== "string" || !Number.isSafeInteger(hint.sessionId) || typeof hint.sessionOriginId !== "string" || typeof hint.mateStamp !== "string" || !ctx.isActorLive || !ctx.isActorLive(ws, userId)) return {};
|
|
9
|
+
var found = ctx.findMateProject && ctx.findMateProject(userId, hint.mateId, false);
|
|
10
|
+
if (!found || found.mate.archived || found.mate.lifecycleDeletion || found.mate.createdBy && found.mate.createdBy !== userId) return {};
|
|
11
|
+
var stamp = JSON.stringify([found.mate.id, found.mate.createdBy || null, found.mate.createdAt || null, found.mate.builtinKey || null]);
|
|
12
|
+
if (stamp !== hint.mateStamp || ctx.projects.get(found.slug) !== found.ctx) return {};
|
|
13
|
+
var manager = found.ctx.getSessionManager && found.ctx.getSessionManager();
|
|
14
|
+
var session = manager && manager.sessions.get(hint.sessionId);
|
|
15
|
+
if (!ctx.ownsSession(session, userId) || session.sessionOriginId !== hint.sessionOriginId) return {};
|
|
16
|
+
return { tap: { mateSlug: found.slug, mateId: found.mate.id, sessionId: hint.sessionId }, project: found.ctx, session: session };
|
|
17
|
+
}
|
|
6
18
|
var tap = msg.surface === "search" ? ws._searchClayTap : ws._homeChatTap;
|
|
7
19
|
if (!tap && routedProjectSlug) {
|
|
8
20
|
var routedProject = ctx.projects.get(routedProjectSlug);
|
package/lib/server-owner-ui.js
CHANGED
|
@@ -2,6 +2,27 @@ var crypto = require('crypto');
|
|
|
2
2
|
// Only authenticated application sockets enumerated by the server are targets.
|
|
3
3
|
function attachOwnerUi(ctx) {
|
|
4
4
|
var identities = new WeakMap(), capabilities = new WeakMap(), deliveries = new Map();
|
|
5
|
+
var activityOrder = 0;
|
|
6
|
+
function activity(owner, slug, sessionId, mateId, selected, source, validateSource, navigationTarget) {
|
|
7
|
+
var order = ++activityOrder;
|
|
8
|
+
var viewers = selected ? [check(owner, selected)] : sockets(owner).filter(function (viewer) {
|
|
9
|
+
var tap = viewer.ws._homeChatTap;
|
|
10
|
+
return viewer.slug === slug && viewer.sessionId === sessionId || tap && tap.mateSlug === slug && tap.sessionId === sessionId;
|
|
11
|
+
});
|
|
12
|
+
function send(message, receiptViewer) {
|
|
13
|
+
viewers.forEach(function (viewer) {
|
|
14
|
+
try {
|
|
15
|
+
if (receiptViewer && viewer.ws !== receiptViewer || !alive(viewer.ws, owner)) return;
|
|
16
|
+
if (message.phase !== 'clear' && validateSource) validateSource();
|
|
17
|
+
var live = inspect(owner, viewer.ref), tap = live.ws._homeChatTap;
|
|
18
|
+
if (message.phase !== 'clear' && (selected ? (live.slug !== viewer.slug || live.sessionId !== viewer.sessionId) && !(message.phase === 'rendered' && navigationTarget && live.slug === navigationTarget.projectSlug && live.sessionId === navigationTarget.sessionId) : !(live.slug === slug && live.sessionId === sessionId || tap && tap.mateSlug === slug && tap.sessionId === sessionId))) return;
|
|
19
|
+
live.ws.send(JSON.stringify(Object.assign({ type: 'clay_control_activity', ownerId: owner, mateId: mateId,
|
|
20
|
+
sourceSlug: slug, sourceSessionId: sessionId, order: order, recipientViewerRef: live.ref }, source, message)));
|
|
21
|
+
} catch (error) { /* Presentation cannot alter the authoritative effect. */ }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return { send: send, clear: function (id, sequence) { send({ version: 1, id: id, sequence: sequence, phase: 'clear' }); } };
|
|
25
|
+
}
|
|
5
26
|
function alive(ws, owner) {
|
|
6
27
|
return ws && ws.readyState === 1 && (!ws._clayAuthCheck || ws._clayAuthCheck()) &&
|
|
7
28
|
(ctx.isMultiUser() ? !!owner && ws._clayUser && ws._clayUser.id === owner && !!ctx.findUser(owner) : !ws._clayUser);
|
|
@@ -27,7 +48,7 @@ function attachOwnerUi(ctx) {
|
|
|
27
48
|
if (live.ws !== original.ws || live.slug !== original.slug || live.sessionId !== original.sessionId || live.supported !== original.supported) throw new Error('The selected viewer context changed. Inspect again.');
|
|
28
49
|
return live;
|
|
29
50
|
}
|
|
30
|
-
function deliver(owner, original, target, revalidate) {
|
|
51
|
+
function deliver(owner, original, target, revalidate, activity) {
|
|
31
52
|
revalidate(); var viewer = check(owner, original);
|
|
32
53
|
if (!viewer.supported) return { status: 'unsupported', delivered: false, acknowledged: false, visible: 'unknown' };
|
|
33
54
|
if (deliveries.size >= 100) {
|
|
@@ -35,9 +56,15 @@ function attachOwnerUi(ctx) {
|
|
|
35
56
|
if (deliveries.size >= 100) throw new Error('UI delivery capacity is full; wait for earlier attempts to expire.');
|
|
36
57
|
}
|
|
37
58
|
var id = 'navigation:' + crypto.randomUUID();
|
|
38
|
-
var
|
|
59
|
+
var renderToken = crypto.randomUUID();
|
|
60
|
+
var record = { owner: owner, viewer: viewer, target: target, revalidate: revalidate, activity: activity, created: Date.now(), claimed: false, renderToken: renderToken, renderClosed: false,
|
|
39
61
|
result: { status: 'delivered', deliveryId: id, delivered: false, acknowledged: false, visible: 'unknown' } };
|
|
40
62
|
deliveries.set(id, record);
|
|
63
|
+
record.renderTimer = setTimeout(function () {
|
|
64
|
+
if (record.renderClosed) return;
|
|
65
|
+
record.renderClosed = true; record.result.renderStatus = 'unknown';
|
|
66
|
+
if (record.activity) record.activity.navigation('unknown');
|
|
67
|
+
}, ctx.renderTimeoutMs || 5000); record.renderTimer.unref();
|
|
41
68
|
try {
|
|
42
69
|
viewer.ws.send(JSON.stringify({ type: 'clay_navigation_offer', deliveryId: id }));
|
|
43
70
|
if (!alive(viewer.ws, owner)) throw new Error('Viewer authorization or connection ended during delivery.');
|
|
@@ -48,30 +75,44 @@ function attachOwnerUi(ctx) {
|
|
|
48
75
|
function read(owner, id) {
|
|
49
76
|
var record = deliveries.get(id);
|
|
50
77
|
if (!record || record.owner !== owner) throw new Error('No delivery exists for this owner.');
|
|
78
|
+
if (Date.now() - record.created > 300000) { deliveries.delete(id); throw new Error('The five-minute navigation receipt expired. Inspect the current view before any new reviewed request.'); }
|
|
51
79
|
record.revalidate();
|
|
52
80
|
return Object.assign({}, record.result);
|
|
53
81
|
}
|
|
54
82
|
function handleMessage(ws, msg) {
|
|
55
|
-
if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack'].indexOf(msg.type) < 0) return false;
|
|
83
|
+
if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack', 'clay_navigation_rendered'].indexOf(msg.type) < 0) return false;
|
|
56
84
|
var owner = ctx.isMultiUser() ? ws._clayUser && ws._clayUser.id : null;
|
|
57
85
|
if (!sockets(owner).some(function (item) { return item.ws === ws; })) return true;
|
|
58
|
-
if (msg.type === 'clay_ui_ready') {
|
|
86
|
+
if (msg.type === 'clay_ui_ready') {
|
|
87
|
+
if (msg.version === 1) { capabilities.set(ws, 1); ws.send(JSON.stringify({ type: 'clay_control_viewer', viewerRef: identities.get(ws) })); }
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
59
90
|
var record = deliveries.get(msg.deliveryId);
|
|
60
91
|
if (!record || record.viewer.ws !== ws || record.owner !== owner || Date.now() - record.created > 30000) return true;
|
|
61
92
|
try {
|
|
62
93
|
record.revalidate();
|
|
63
|
-
if (msg.type === '
|
|
94
|
+
if (msg.type === 'clay_navigation_rendered') {
|
|
95
|
+
if (!record.renderClosed && Date.now() > record.created + (ctx.renderTimeoutMs || 5000)) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.renderStatus = 'unknown'; if (record.activity) record.activity.navigation('unknown'); }
|
|
96
|
+
if (record.renderClosed || !record.claimed || !record.result.acknowledged || record.result.clientStatus !== 'accepted' || msg.renderToken !== record.renderToken) return true;
|
|
97
|
+
var current = inspect(owner, record.viewer.ref);
|
|
98
|
+
if (current.ws !== ws || msg.sessionOriginId !== record.target.sessionOriginId || !record.target.sessionOriginId || msg.projectSlug !== record.target.projectSlug || msg.sessionId !== record.target.sessionId || msg.surface !== 'conversation' || msg.entryIndex !== record.target.entryIndex || current.slug !== record.target.projectSlug || current.sessionId !== record.target.sessionId) throw new Error('Rendered target or authenticated viewer changed.');
|
|
99
|
+
record.renderClosed = true; clearTimeout(record.renderTimer);
|
|
100
|
+
record.result.renderStatus = msg.visible === true ? 'rendered' : 'unknown'; record.result.visible = msg.visible === true ? true : 'unknown';
|
|
101
|
+
if (record.activity) record.activity.navigation(msg.visible === true ? 'rendered' : 'unknown');
|
|
102
|
+
} else if (msg.type === 'clay_navigation_claim') {
|
|
64
103
|
if (record.claimed) return true;
|
|
65
104
|
check(owner, record.viewer); record.claimed = true;
|
|
66
|
-
ws.send(JSON.stringify({ type: 'clay_navigation_apply', deliveryId: msg.deliveryId, target: record.target }));
|
|
105
|
+
ws.send(JSON.stringify({ type: 'clay_navigation_apply', deliveryId: msg.deliveryId, target: record.target, renderToken: record.renderToken, renderDeadline: record.created + (ctx.renderTimeoutMs || 5000) }));
|
|
67
106
|
} else if (record.claimed && !record.result.acknowledged && ['accepted', 'refused', 'failed'].indexOf(msg.status) >= 0) {
|
|
68
107
|
record.result.acknowledged = true;
|
|
69
108
|
// Accepted only confirms invocation of the existing navigator, never rendering.
|
|
70
109
|
record.result.clientStatus = msg.status;
|
|
110
|
+
if (msg.status !== 'accepted') { record.renderClosed = true; clearTimeout(record.renderTimer); }
|
|
111
|
+
if (record.activity) record.activity.navigation(msg.status === 'accepted' ? 'acknowledged' : msg.status === 'refused' ? 'cancelled' : 'failed');
|
|
71
112
|
}
|
|
72
|
-
} catch (error) { record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; }
|
|
113
|
+
} catch (error) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.visible = 'unknown'; record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; if (record.activity) record.activity.cancel(); }
|
|
73
114
|
return true;
|
|
74
115
|
}
|
|
75
|
-
return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage };
|
|
116
|
+
return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage, activity: activity };
|
|
76
117
|
}
|
|
77
118
|
module.exports = { attachOwnerUi: attachOwnerUi };
|
package/lib/server-settings.js
CHANGED
|
@@ -12,11 +12,13 @@ function attachSettings(ctx) {
|
|
|
12
12
|
var CONFIG_DIR = ctx.CONFIG_DIR;
|
|
13
13
|
|
|
14
14
|
var profilePath = path.join(CONFIG_DIR, "profile.json");
|
|
15
|
+
var contextualClaySettings = require('./server-contextual-clay-settings').attachContextualClaySettings(ctx);
|
|
15
16
|
var appearanceSettings = require('./server-appearance-settings').attachAppearanceSettings(ctx);
|
|
16
17
|
var notificationSettings = require('./server-notification-settings').attachNotificationSettings(ctx);
|
|
17
18
|
var experimentalSettings = attachExperimentalSettings(ctx);
|
|
18
19
|
|
|
19
20
|
function handleRequest(req, res, fullUrl) {
|
|
21
|
+
if (contextualClaySettings.handleRequest(req, res, fullUrl)) return true;
|
|
20
22
|
if (appearanceSettings.handleRequest(req, res, fullUrl)) return true;
|
|
21
23
|
if (notificationSettings.handleRequest(req, res, fullUrl)) return true;
|
|
22
24
|
if (experimentalSettings.handleRequest(req, res, fullUrl)) return true;
|
package/lib/server.js
CHANGED
|
@@ -357,6 +357,12 @@ function createServer(opts) {
|
|
|
357
357
|
|
|
358
358
|
// --- Project registry ---
|
|
359
359
|
var assignmentService = attachWorkspaceAssignmentService({
|
|
360
|
+
getPresentation: function (principal) {
|
|
361
|
+
var project = projects.get(principal.sourceProjectSlug), manager = project && project.getSessionManager(), status = project && project.getStatus();
|
|
362
|
+
var session = manager && manager.sessions.get(principal.sourceSessionId);
|
|
363
|
+
var bound = clayProductControl && session && clayProductControl.bind({ projectSlug: principal.sourceProjectSlug, projectOwnerId: status.projectOwnerId, isMate: status.isMate, mateId: status.mateId, session: session });
|
|
364
|
+
return bound && bound._presentation;
|
|
365
|
+
},
|
|
360
366
|
storageDir: path.join(CONFIG_DIR, "workspace-assignments"),
|
|
361
367
|
getProjects: function () { return projects; },
|
|
362
368
|
isMultiUser: function () { return users.isMultiUser(); },
|
|
@@ -384,6 +390,13 @@ function createServer(opts) {
|
|
|
384
390
|
projects.forEach(function (project, slug) { project.forEachClient(function (ws) { visit(ws, slug); }); });
|
|
385
391
|
}
|
|
386
392
|
});
|
|
393
|
+
var mateContextService = require('./mate-context-service').attachMateContextService({
|
|
394
|
+
getOwnerUi: function () { return ownerUiService; }, workspace: workspaceQueryService,
|
|
395
|
+
getProjects: function () { return projects; }, isMultiUser: users.isMultiUser,
|
|
396
|
+
findUser: users.findUserById, onGetProjectAccess: onGetProjectAccess,
|
|
397
|
+
canAccessProject: users.canAccessProject,
|
|
398
|
+
resolveMate: function (owner, id) { return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? owner : null), id); }
|
|
399
|
+
});
|
|
387
400
|
var clayProductControl = require("./clay-product-control").attachClayProductControl({
|
|
388
401
|
getOwnerUi: function () { return ownerUiService; },
|
|
389
402
|
getNotifications: function () { return _globalNotifications; },
|
|
@@ -1284,6 +1297,7 @@ function createServer(opts) {
|
|
|
1284
1297
|
if (!require('./account-auth-socket').guardSocket(ws, req, isRequestAuthed, auth.touchSession)) return;
|
|
1285
1298
|
ws._clayPane = wsRequest.pane;
|
|
1286
1299
|
ws._clayPaneSession = wsRequest.paneSession;
|
|
1300
|
+
ws._clayContextual = wsRequest.contextual;
|
|
1287
1301
|
globalWs.handleConnection(ws, globalUser);
|
|
1288
1302
|
});
|
|
1289
1303
|
return;
|
|
@@ -1329,6 +1343,7 @@ function createServer(opts) {
|
|
|
1329
1343
|
ws._clayUser = wsUser; // attach user context
|
|
1330
1344
|
ws._clayPane = wsRequest.pane;
|
|
1331
1345
|
ws._clayPaneSession = wsRequest.paneSession;
|
|
1346
|
+
ws._clayContextual = wsRequest.contextual;
|
|
1332
1347
|
var remoteAddr = req.socket.remoteAddress || "";
|
|
1333
1348
|
ws._clayLocal = (remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1");
|
|
1334
1349
|
// Clear cross-project unread for this project when client connects
|
|
@@ -1450,6 +1465,7 @@ function createServer(opts) {
|
|
|
1450
1465
|
},
|
|
1451
1466
|
workspaceQueryService: workspaceQueryService,
|
|
1452
1467
|
clayProductControl: clayProductControl,
|
|
1468
|
+
mateContextService: mateContextService,
|
|
1453
1469
|
resumeLifecycleProject: function (userId) { return opts.projectLifecycle ? opts.projectLifecycle.resumeRecovered(slug, userId) : true; },
|
|
1454
1470
|
projectLogsService: projectLogsService,
|
|
1455
1471
|
issuesService: issuesService,
|
|
@@ -1729,6 +1745,7 @@ function createServer(opts) {
|
|
|
1729
1745
|
|
|
1730
1746
|
function handleDmMessage(ws, msg, routedProjectSlug) {
|
|
1731
1747
|
if (ownerUiService.handleMessage(ws, msg)) return true;
|
|
1748
|
+
if (mateContextService.handleMessage(ws, msg)) return true;
|
|
1732
1749
|
if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
|
|
1733
1750
|
if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
|
|
1734
1751
|
if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
|
package/lib/sessions.js
CHANGED
|
@@ -211,8 +211,11 @@ function createSessionManager(opts) {
|
|
|
211
211
|
if (session.claySchedulingReceipts) metaObj.claySchedulingReceipts = session.claySchedulingReceipts;
|
|
212
212
|
if (session.mateCreationMode) metaObj.mateCreationMode = true;
|
|
213
213
|
if (session.homeMateCreationPhase) metaObj.homeMateCreationPhase = session.homeMateCreationPhase;
|
|
214
|
+
if (session.contextualCreationMateStamp) metaObj.contextualCreationMateStamp = session.contextualCreationMateStamp;
|
|
215
|
+
if (session.contextualCreationRequestId) metaObj.contextualCreationRequestId = session.contextualCreationRequestId;
|
|
214
216
|
if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
|
|
215
217
|
if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
|
|
218
|
+
if (session.homeClayEntryText) metaObj.homeClayEntryText = session.homeClayEntryText;
|
|
216
219
|
metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
|
|
217
220
|
if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
|
|
218
221
|
if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
|
|
@@ -375,8 +378,11 @@ function createSessionManager(opts) {
|
|
|
375
378
|
if (Array.isArray(m.claySchedulingReceipts)) session.claySchedulingReceipts = m.claySchedulingReceipts;
|
|
376
379
|
if (m.mateCreationMode) session.mateCreationMode = true;
|
|
377
380
|
if (m.homeMateCreationPhase) session.homeMateCreationPhase = m.homeMateCreationPhase;
|
|
381
|
+
if (m.contextualCreationMateStamp) session.contextualCreationMateStamp = m.contextualCreationMateStamp;
|
|
382
|
+
if (m.contextualCreationRequestId) session.contextualCreationRequestId = m.contextualCreationRequestId;
|
|
378
383
|
if (m.homeClayEntryRequestId) session.homeClayEntryRequestId = m.homeClayEntryRequestId;
|
|
379
384
|
if (m.homeClayEntryMode) session.homeClayEntryMode = m.homeClayEntryMode;
|
|
385
|
+
if (m.homeClayEntryText) session.homeClayEntryText = m.homeClayEntryText;
|
|
380
386
|
if (m.ownerId) session.ownerId = m.ownerId;
|
|
381
387
|
session.sessionOriginId = m.sessionOriginId || null;
|
|
382
388
|
session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
|
|
@@ -561,6 +567,7 @@ function createSessionManager(opts) {
|
|
|
561
567
|
var unreadMap = wsUnread || singleUserUnread;
|
|
562
568
|
return {
|
|
563
569
|
id: s.localId,
|
|
570
|
+
sessionOriginId: s.sessionOriginId || null,
|
|
564
571
|
cliSessionId: s.cliSessionId || null,
|
|
565
572
|
createdAt: s.createdAt || 0,
|
|
566
573
|
title: s.title || "New Session",
|
|
@@ -771,8 +778,8 @@ function createSessionManager(opts) {
|
|
|
771
778
|
}
|
|
772
779
|
|
|
773
780
|
// History entries include tool activity and streamed output, so opening a
|
|
774
|
-
// conversation
|
|
775
|
-
var INITIAL_HISTORY_SIZE =
|
|
781
|
+
// conversation starts with a 300-entry target, aligned to a complete user turn.
|
|
782
|
+
var INITIAL_HISTORY_SIZE = 300;
|
|
776
783
|
var HISTORY_PAGE_SIZE = 100;
|
|
777
784
|
|
|
778
785
|
function findTurnBoundary(history, targetIndex) {
|
|
@@ -826,7 +833,7 @@ function createSessionManager(opts) {
|
|
|
826
833
|
}
|
|
827
834
|
}
|
|
828
835
|
|
|
829
|
-
_send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
|
|
836
|
+
_send({ type: "history_done", controlHistoryRevision: require("./clay-control-target-projection").digest(session.history || []), sessionId: session.localId, sessionOriginId: session.sessionOriginId || null, lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, context: lastContext, contextUsage: require("./yoke/usage").context(session.lastContextUsage, session.vendor || "claude") });
|
|
830
837
|
for (var vi = 0; vi < sessionViewedListeners.length; vi++) {
|
|
831
838
|
try { sessionViewedListeners[vi](session, targetWs, _send); } catch (e) {}
|
|
832
839
|
}
|
package/lib/terminal-manager.js
CHANGED
|
@@ -83,7 +83,7 @@ function createTerminalManager(opts) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Broadcast to subscribers
|
|
86
|
-
var msg = JSON.stringify({ type: "term_output", id: id, data: data });
|
|
86
|
+
var msg = JSON.stringify(Object.assign({ type: "term_output", id: id, data: data }, controlProjection(id)));
|
|
87
87
|
for (var ws of session.subscribers) {
|
|
88
88
|
if (ws.readyState === 1) ws.send(msg);
|
|
89
89
|
}
|
|
@@ -108,7 +108,7 @@ function createTerminalManager(opts) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Broadcast updated list
|
|
111
|
-
send({ type: "term_list", terminals: list() });
|
|
111
|
+
send({ type: "term_list", terminals: list(true) });
|
|
112
112
|
|
|
113
113
|
// Caller-supplied hook: e.g. TUI session manager uses this to delete
|
|
114
114
|
// its session record once `claude` exits, so stale entries don't pile
|
|
@@ -181,10 +181,11 @@ function createTerminalManager(opts) {
|
|
|
181
181
|
session.cols = cols;
|
|
182
182
|
session.rows = rows;
|
|
183
183
|
// Notify other subscribers about the resize so their xterm stays in sync
|
|
184
|
-
var msg = JSON.stringify({ type: "term_resized", id: id, cols: cols, rows: rows });
|
|
184
|
+
var msg = JSON.stringify(Object.assign({ type: "term_resized", id: id, cols: cols, rows: rows }, controlProjection(id)));
|
|
185
185
|
for (var ws of session.subscribers) {
|
|
186
186
|
if (ws.readyState === 1 && ws !== sourceWs) ws.send(msg);
|
|
187
187
|
}
|
|
188
|
+
if (sourceWs && sourceWs.readyState === 1) sourceWs.send(JSON.stringify(Object.assign({ type: "term_control_projection", id: id }, controlProjection(id))));
|
|
188
189
|
} catch (e) {}
|
|
189
190
|
}
|
|
190
191
|
}
|
|
@@ -244,15 +245,24 @@ function createTerminalManager(opts) {
|
|
|
244
245
|
session.title = String(title).substring(0, 50);
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
function
|
|
248
|
+
function inspect(id) {
|
|
249
|
+
var terminal = terminals.get(id);
|
|
250
|
+
return terminal ? { id: id, identity: terminal.identity, ownerId: terminal.ownerId, osIdentity: terminal.osIdentity, title: terminal.title, kind: terminal.kind, exited: terminal.exited, cols: terminal.cols, rows: terminal.rows, totalBytesWritten: terminal.totalBytesWritten } : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function controlProjection(id) { var item = inspect(id); return item ? { controlIdentity: item.identity, controlRevision: require('./clay-control-target-projection').digest(item) } : {}; }
|
|
254
|
+
|
|
255
|
+
function list(control) {
|
|
248
256
|
var result = [];
|
|
249
257
|
for (var session of terminals.values()) {
|
|
250
|
-
|
|
258
|
+
var entry = {
|
|
251
259
|
id: session.id,
|
|
252
260
|
title: session.title,
|
|
253
261
|
kind: session.kind,
|
|
254
262
|
exited: session.exited,
|
|
255
|
-
}
|
|
263
|
+
};
|
|
264
|
+
if (control) { entry.controlIdentity = session.identity; entry.controlRevision = require('./clay-control-target-projection').digest(inspect(session.id)); }
|
|
265
|
+
result.push(entry);
|
|
256
266
|
}
|
|
257
267
|
return result;
|
|
258
268
|
}
|
|
@@ -333,10 +343,7 @@ function createTerminalManager(opts) {
|
|
|
333
343
|
resize: resize,
|
|
334
344
|
close: close,
|
|
335
345
|
closeAndWait: closeAndWait,
|
|
336
|
-
inspect:
|
|
337
|
-
var terminal = terminals.get(id);
|
|
338
|
-
return terminal ? { id: id, identity: terminal.identity, ownerId: terminal.ownerId, osIdentity: terminal.osIdentity, title: terminal.title, kind: terminal.kind, exited: terminal.exited, cols: terminal.cols, rows: terminal.rows, totalBytesWritten: terminal.totalBytesWritten } : null;
|
|
339
|
-
},
|
|
346
|
+
inspect: inspect,
|
|
340
347
|
resizeExact: function (id, cols, rows, identity, revalidate) {
|
|
341
348
|
revalidate();
|
|
342
349
|
var terminal = terminals.get(id);
|
|
@@ -345,7 +352,7 @@ function createTerminalManager(opts) {
|
|
|
345
352
|
revalidate();
|
|
346
353
|
if (terminals.get(id) !== terminal) throw new Error('Terminal was replaced during resize.');
|
|
347
354
|
terminal.cols = cols; terminal.rows = rows;
|
|
348
|
-
var message = JSON.stringify({ type: 'term_resized', id: id, cols: cols, rows: rows });
|
|
355
|
+
var message = JSON.stringify(Object.assign({ type: 'term_resized', id: id, cols: cols, rows: rows }, controlProjection(id)));
|
|
349
356
|
terminal.subscribers.forEach(function (ws) { if (ws.readyState === 1) ws.send(message); });
|
|
350
357
|
return true;
|
|
351
358
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Personal geometry is never a canonical product-control operation.
|
|
2
|
+
function validGeometry(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
4
|
+
var keys = Object.keys(value);
|
|
5
|
+
if (keys.length !== 4 || keys.some(function (key) { return ['left', 'top', 'width', 'height'].indexOf(key) < 0; })) return false;
|
|
6
|
+
return keys.every(function (key) { return typeof value[key] === 'number' && Number.isFinite(value[key]); }) &&
|
|
7
|
+
value.left >= 0 && value.left <= 16384 && value.top >= 0 && value.top <= 16384 &&
|
|
8
|
+
value.width >= 340 && value.width <= 2560 && value.height >= 360 && value.height <= 2160;
|
|
9
|
+
}
|
|
10
|
+
function attachContextualClayPreferences(deps) {
|
|
11
|
+
function get(userId) {
|
|
12
|
+
var user = deps.loadUsers().users.find(function (entry) { return entry.id === userId; });
|
|
13
|
+
var saved = user && user.contextualClayGeometry;
|
|
14
|
+
if (!user && userId === 'default') saved = require('./config').loadConfig().contextualClayGeometry;
|
|
15
|
+
return validGeometry(saved) ? Object.assign({}, saved) : null;
|
|
16
|
+
}
|
|
17
|
+
function set(userId, geometry) {
|
|
18
|
+
if (!validGeometry(geometry)) return { error: 'Invalid popover geometry.' };
|
|
19
|
+
var value = Object.assign({}, geometry);
|
|
20
|
+
var data = deps.loadUsers();
|
|
21
|
+
var user = data.users.find(function (entry) { return entry.id === userId; });
|
|
22
|
+
if (user) { user.contextualClayGeometry = value; deps.saveUsers(data); }
|
|
23
|
+
else if (userId === 'default') {
|
|
24
|
+
var config = require('./config');
|
|
25
|
+
config.saveConfig(Object.assign({}, config.loadConfig(), { contextualClayGeometry: value }));
|
|
26
|
+
} else return { error: 'Account no longer available.' };
|
|
27
|
+
return { geometry: value };
|
|
28
|
+
}
|
|
29
|
+
return { getContextualClayGeometry: get, setContextualClayGeometry: set };
|
|
30
|
+
}
|
|
31
|
+
module.exports = { attachContextualClayPreferences: attachContextualClayPreferences, validGeometry: validGeometry };
|