clay-server 4.1.0-beta.8 → 4.1.0
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/context-usage.js +47 -0
- package/lib/issues-mcp-server.js +1 -1
- package/lib/issues-service.js +3 -1
- package/lib/loop-registry-occurrence.js +1 -0
- package/lib/notes.js +20 -3
- package/lib/os-users.js +6 -0
- package/lib/project-connection.js +14 -3
- package/lib/project-file-path.js +13 -0
- package/lib/project-file-watch.js +19 -8
- package/lib/project-filesystem.js +10 -16
- package/lib/project-http.js +53 -48
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project-logs-schema.js +1 -1
- package/lib/project-logs-service.js +46 -13
- package/lib/project-logs-store.js +17 -15
- package/lib/project-logs.js +70 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-pending-message-queue.js +71 -7
- package/lib/project-scheduled-messages.js +235 -52
- package/lib/project-scheduled-result-store.js +131 -0
- package/lib/project-scheduled-task-execution.js +35 -2
- package/lib/project-session-notes.js +11 -4
- package/lib/project-sessions.js +7 -0
- package/lib/project-user-message.js +72 -5
- package/lib/project.js +60 -5
- package/lib/public/app.js +27 -12
- package/lib/public/css/avatar-imprints.css +1 -1
- package/lib/public/css/command-palette.css +20 -0
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- package/lib/public/css/input.css +0 -109
- package/lib/public/css/issues.css +78 -31
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/notifications-center.css +3 -3
- package/lib/public/css/pending-message-queue.css +3 -0
- package/lib/public/css/project-logs.css +15 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +29 -0
- package/lib/public/modules/app-connection.js +9 -1
- package/lib/public/modules/app-favicon.js +10 -0
- package/lib/public/modules/app-messages.js +49 -37
- package/lib/public/modules/app-notifications.js +2 -0
- package/lib/public/modules/app-panels.js +77 -23
- package/lib/public/modules/app-projects.js +3 -0
- package/lib/public/modules/app-rate-limit.js +148 -243
- package/lib/public/modules/avatar-imprint.js +25 -70
- package/lib/public/modules/avatar.js +12 -4
- package/lib/public/modules/chat-render-runtime.js +20 -2
- package/lib/public/modules/clay-issue-links.js +23 -2
- package/lib/public/modules/clay-log-links.js +11 -1
- package/lib/public/modules/context-view-preference.js +143 -0
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/filebrowser-tabs.js +1 -0
- package/lib/public/modules/filebrowser.js +45 -18
- package/lib/public/modules/input.js +81 -20
- package/lib/public/modules/issues-render.js +36 -10
- package/lib/public/modules/issues.js +95 -13
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/pane-bridge.js +14 -8
- package/lib/public/modules/pane-issue-bridge.js +27 -0
- package/lib/public/modules/pending-message-queue.js +43 -1
- package/lib/public/modules/permission-hints.js +13 -0
- package/lib/public/modules/profile.js +9 -9
- package/lib/public/modules/project-activation.js +49 -0
- package/lib/public/modules/project-logs-render.js +13 -3
- package/lib/public/modules/project-logs.js +45 -15
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-message-state.js +12 -36
- package/lib/public/modules/scheduled-result-client.js +33 -0
- package/lib/public/modules/scheduled-result-display.js +50 -0
- package/lib/public/modules/scheduled-result-notification.js +31 -0
- package/lib/public/modules/scheduled-task-presentation.js +15 -0
- package/lib/public/modules/scheduled-tasks.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/split-session-boundary.js +28 -0
- package/lib/public/modules/split-view.js +42 -7
- package/lib/public/modules/sticky-notes-browser.js +130 -18
- package/lib/public/modules/sticky-notes.js +2 -3
- package/lib/public/modules/terminal.js +4 -4
- package/lib/public/modules/thinking-lifecycle.js +4 -1
- package/lib/public/modules/tools.js +5 -1
- package/lib/scheduled-run-state.js +10 -2
- package/lib/scheduler.js +1 -0
- package/lib/sdk-bridge.js +97 -61
- package/lib/sdk-message-processor.js +44 -13
- package/lib/sdk-skill-discovery.js +6 -28
- package/lib/sdk-turn-queue.js +14 -0
- package/lib/server-context-view.js +53 -0
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +23 -6
- package/lib/session-notes-mcp-server.js +3 -5
- package/lib/session-pair-factory.js +2 -2
- package/lib/session-pair-mcp-server.js +5 -4
- package/lib/session-title-generator.js +89 -0
- package/lib/session-title-policy.js +8 -2
- package/lib/sessions.js +26 -1
- package/lib/user-presence.js +6 -0
- package/lib/users-context-view-preferences.js +65 -0
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +12 -0
- package/lib/ws-schema.js +11 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/acp.js +3 -2
- package/lib/yoke/adapters/antigravity.js +2 -2
- package/lib/yoke/adapters/claude-worker.js +11 -12
- package/lib/yoke/adapters/claude.js +75 -22
- package/lib/yoke/adapters/codex.js +216 -29
- package/lib/yoke/adapters/kiro.js +6 -5
- package/lib/yoke/claude-context-usage.js +57 -0
- package/lib/yoke/claude-message-correlation.js +31 -0
- package/lib/yoke/claude-model-switch.js +15 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/mcp-bridge-server.js +18 -5
- package/lib/yoke/session-skill-catalog.js +45 -0
- package/lib/yoke/skill-discovery.js +60 -20
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +5 -7
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Authenticated user-wide context display preference service.
|
|
2
|
+
|
|
3
|
+
function attachContextViewService(deps) {
|
|
4
|
+
var users = deps.users;
|
|
5
|
+
var forEachAppClient = deps.forEachAppClient;
|
|
6
|
+
var revisions = {};
|
|
7
|
+
var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
|
|
8
|
+
|
|
9
|
+
function actor(ws) {
|
|
10
|
+
if (users.isMultiUser() !== true) return { userId: "default", user: true };
|
|
11
|
+
var claimed = ws && ws._clayUser && ws._clayUser.id;
|
|
12
|
+
var user = claimed ? users.findUserById(claimed) : null;
|
|
13
|
+
return user ? { userId: user.id, user: user } : { userId: null, user: null };
|
|
14
|
+
}
|
|
15
|
+
function sameActor(ws, userId) {
|
|
16
|
+
var current = actor(ws);
|
|
17
|
+
return current.user && current.userId === userId;
|
|
18
|
+
}
|
|
19
|
+
function send(ws, message) {
|
|
20
|
+
if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message)));
|
|
21
|
+
}
|
|
22
|
+
function revision(userId) { return revisions[userId] || 0; }
|
|
23
|
+
function broadcast(userId, message, except) {
|
|
24
|
+
forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
|
|
25
|
+
}
|
|
26
|
+
function handleMessage(ws, msg) {
|
|
27
|
+
if (!msg || ["context_view_get", "context_view_set"].indexOf(msg.type) === -1) return false;
|
|
28
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
|
|
29
|
+
var who = actor(ws);
|
|
30
|
+
if (!who.user) {
|
|
31
|
+
send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: false, accountId: null, mode: null, error: "Your account is no longer available." });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (msg.type === "context_view_get") {
|
|
35
|
+
var current = users.getContextViewPreference(who.userId);
|
|
36
|
+
send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, mode: current.mode, preferencePresent: current.present, canonicalRevision: revision(who.userId), ready: !current.error, error: current.error || "" });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
var saved = users.setContextViewPreference(who.userId, msg.mode);
|
|
40
|
+
if (!saved.ok) {
|
|
41
|
+
send(ws, { type: "context_view_state", requestId: requestId, accountAvailable: true, accountId: who.userId, canonicalRevision: revision(who.userId), ready: false, error: saved.error });
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
revisions[who.userId] = revision(who.userId) + 1;
|
|
45
|
+
var state = { type: "context_view_state", requestId: null, accountAvailable: true, accountId: who.userId, mode: msg.mode, preferencePresent: true, canonicalRevision: revisions[who.userId], ready: true, error: "" };
|
|
46
|
+
broadcast(who.userId, state, ws);
|
|
47
|
+
send(ws, Object.assign({}, state, { requestId: requestId }));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return { handleMessage: handleMessage };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { attachContextViewService: attachContextViewService };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Authenticated user-wide New session vendor preference service.
|
|
2
|
+
|
|
3
|
+
function attachDefaultVendorService(deps) {
|
|
4
|
+
var users = deps.users;
|
|
5
|
+
var homeChatHandler = deps.homeChatHandler;
|
|
6
|
+
var forEachAppClient = deps.forEachAppClient;
|
|
7
|
+
var revisions = {};
|
|
8
|
+
var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
|
|
9
|
+
|
|
10
|
+
function multiUser() { return users.isMultiUser() === true; }
|
|
11
|
+
function actor(ws) {
|
|
12
|
+
if (!multiUser()) return { userId: "default", user: true };
|
|
13
|
+
var claimed = ws && ws._clayUser && ws._clayUser.id;
|
|
14
|
+
var user = claimed ? users.findUserById(claimed) : null;
|
|
15
|
+
return user ? { userId: user.id, user: user } : { userId: null, user: null };
|
|
16
|
+
}
|
|
17
|
+
function sameActor(ws, userId) { var current = actor(ws); return current.user && current.userId === userId; }
|
|
18
|
+
function send(ws, message) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message))); }
|
|
19
|
+
function revision(userId) { return revisions[userId] || 0; }
|
|
20
|
+
function installedVendors(ws, who) {
|
|
21
|
+
var found;
|
|
22
|
+
try { found = homeChatHandler.findMateProject(multiUser() ? who.userId : null, null, true); } catch (e) { return []; }
|
|
23
|
+
if (!found || !found.ctx || typeof found.ctx.getVendorModelAvailability !== "function") return [];
|
|
24
|
+
var socket = Object.create(ws || null);
|
|
25
|
+
socket._clayUser = who.user === true ? null : who.user;
|
|
26
|
+
var choices = found.ctx.getVendorModelAvailability(socket) || [];
|
|
27
|
+
return choices.filter(function (choice) { return choice && choice.installed === true; }).map(function (choice) { return choice.id; });
|
|
28
|
+
}
|
|
29
|
+
function broadcast(userId, message, except) {
|
|
30
|
+
forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
|
|
31
|
+
}
|
|
32
|
+
function handle(ws, msg, projectSlug) {
|
|
33
|
+
if (!msg || ["default_vendor_get", "default_vendor_set"].indexOf(msg.type) === -1) return false;
|
|
34
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
|
|
35
|
+
var who = actor(ws);
|
|
36
|
+
if (!who.user) { send(ws, { type: "default_vendor_state", requestId: requestId, accountAvailable: false, accountId: null, projectSlug: projectSlug || null, installedVendors: [], preference: null, error: "Your account is no longer available." }); return true; }
|
|
37
|
+
var installed = installedVendors(ws, who);
|
|
38
|
+
if (msg.type === "default_vendor_get") {
|
|
39
|
+
var saved = users.getDefaultVendorPreference(who.userId);
|
|
40
|
+
send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: saved.vendor, preferencePresent: saved.present, ready: !saved.error, canonicalRevision: revision(who.userId), error: saved.error || "" });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (msg.vendor !== "" && (typeof msg.vendor !== "string" || installed.indexOf(msg.vendor) === -1)) {
|
|
44
|
+
send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: users.getDefaultVendorPreference(who.userId).vendor, ready: false, canonicalRevision: revision(who.userId), error: "That vendor is not installed or authorized." });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
var savedResult = users.setDefaultVendorPreference(who.userId, msg.vendor);
|
|
48
|
+
if (!savedResult.ok) { send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, ready: false, error: savedResult.error }); return true; }
|
|
49
|
+
revisions[who.userId] = revision(who.userId) + 1;
|
|
50
|
+
var state = { type: "default_vendor_state", requestId: null, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: msg.vendor || null, preferencePresent: !!msg.vendor, ready: true, canonicalRevision: revisions[who.userId], error: "" };
|
|
51
|
+
var broadcastState = Object.assign({}, state);
|
|
52
|
+
delete broadcastState.projectSlug;
|
|
53
|
+
broadcast(who.userId, broadcastState, ws);
|
|
54
|
+
send(ws, Object.assign({}, state, { requestId: requestId }));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return { handleMessage: handle };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { attachDefaultVendorService: attachDefaultVendorService };
|
package/lib/server.js
CHANGED
|
@@ -17,6 +17,8 @@ var serverHomeChat = require("./server-home-chat");
|
|
|
17
17
|
var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
|
|
18
18
|
var serverHomePreferences = require("./server-home-preferences");
|
|
19
19
|
var serverDefaultAi = require("./server-default-ai");
|
|
20
|
+
var serverDefaultVendor = require("./server-default-vendor");
|
|
21
|
+
var serverContextView = require("./server-context-view");
|
|
20
22
|
var serverCursorSharing = require("./server-cursor-sharing");
|
|
21
23
|
var serverTools = require("./server-tools");
|
|
22
24
|
var serverAdmin = require("./server-admin");
|
|
@@ -943,9 +945,9 @@ function createServer(opts) {
|
|
|
943
945
|
return;
|
|
944
946
|
}
|
|
945
947
|
|
|
946
|
-
// Auth check for project routes
|
|
947
|
-
//
|
|
948
|
-
//
|
|
948
|
+
// Auth check for project routes. The local MCP bridge gets a narrowly
|
|
949
|
+
// scoped project capability from its parent process; localhost alone is
|
|
950
|
+
// not sufficient because browser/local callers must not reach the bridge.
|
|
949
951
|
var projectUrlForAuth = stripPrefix(req.url.split("?")[0], slug);
|
|
950
952
|
// req.socket.remoteAddress may differ between HTTP/HTTPS (TLSSocket wraps net.Socket).
|
|
951
953
|
// Also check req.connection.remoteAddress for compatibility.
|
|
@@ -956,10 +958,18 @@ function createServer(opts) {
|
|
|
956
958
|
var isMcpBridgeLocal = projectUrlForAuth === "/api/mcp-bridge"
|
|
957
959
|
&& req.method === "POST"
|
|
958
960
|
&& isLocalhost;
|
|
961
|
+
var isMcpBridgeAuthenticated = isMcpBridgeLocal
|
|
962
|
+
&& typeof ctx.isMcpBridgeAuthenticated === "function"
|
|
963
|
+
&& ctx.isMcpBridgeAuthenticated(req);
|
|
959
964
|
if (projectUrlForAuth === "/api/mcp-bridge") {
|
|
960
|
-
console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + "
|
|
965
|
+
console.log("[server] MCP bridge auth: method=" + req.method + " addr=" + remoteAddr + " capability=" + isMcpBridgeAuthenticated);
|
|
961
966
|
}
|
|
962
|
-
if (
|
|
967
|
+
if (projectUrlForAuth === "/api/mcp-bridge" && !isMcpBridgeAuthenticated) {
|
|
968
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
969
|
+
res.end('{"error":"MCP bridge capability required"}');
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (!isMcpBridgeAuthenticated && !isRequestAuthed(req)) {
|
|
963
973
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
964
974
|
res.end(auth.getAuthPage());
|
|
965
975
|
return;
|
|
@@ -969,7 +979,7 @@ function createServer(opts) {
|
|
|
969
979
|
res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
|
|
970
980
|
|
|
971
981
|
// Multi-user: check project access for HTTP requests
|
|
972
|
-
if (users.isMultiUser()) {
|
|
982
|
+
if (users.isMultiUser() && !isMcpBridgeAuthenticated) {
|
|
973
983
|
var httpUser = getMultiUserFromReq(req);
|
|
974
984
|
if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
|
|
975
985
|
res.writeHead(302, { "Location": "/" });
|
|
@@ -1067,6 +1077,8 @@ function createServer(opts) {
|
|
|
1067
1077
|
var wss = new WebSocketServer({ noServer: true });
|
|
1068
1078
|
var homePreferencesHandler = null;
|
|
1069
1079
|
var defaultAiService = null;
|
|
1080
|
+
var defaultVendorService = null;
|
|
1081
|
+
var contextViewService = null;
|
|
1070
1082
|
var cursorSharingService = null;
|
|
1071
1083
|
|
|
1072
1084
|
// Slug-less /ws handler: lets a user with no projects yet load the regular
|
|
@@ -1321,6 +1333,7 @@ function createServer(opts) {
|
|
|
1321
1333
|
projectOwnerId: projectOwnerId || null,
|
|
1322
1334
|
projectKnowledgeId: extra.projectKnowledgeId || (worktreeMeta && worktreeMeta.projectKnowledgeId) || null,
|
|
1323
1335
|
worktreeMeta: worktreeMeta || null,
|
|
1336
|
+
mcpBridgeToken: extra.mcpBridgeToken || null,
|
|
1324
1337
|
isMate: extra.isMate || false,
|
|
1325
1338
|
mateId: extra.mateId || (extra.isMate ? path.basename(cwd) : null),
|
|
1326
1339
|
mateDisplayName: extra.mateDisplayName || "",
|
|
@@ -1552,6 +1565,8 @@ function createServer(opts) {
|
|
|
1552
1565
|
});
|
|
1553
1566
|
|
|
1554
1567
|
defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
|
|
1568
|
+
defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
|
|
1569
|
+
contextViewService = serverContextView.attachContextViewService({ users: users, forEachAppClient: forEachAppClient });
|
|
1555
1570
|
cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
|
|
1556
1571
|
|
|
1557
1572
|
// --- User-installed home tools handler ---
|
|
@@ -1580,6 +1595,8 @@ function createServer(opts) {
|
|
|
1580
1595
|
|
|
1581
1596
|
function handleDmMessage(ws, msg, routedProjectSlug) {
|
|
1582
1597
|
if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
|
|
1598
|
+
if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
|
|
1599
|
+
if (contextViewService && contextViewService.handleMessage(ws, msg)) return true;
|
|
1583
1600
|
if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
|
|
1584
1601
|
if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
|
|
1585
1602
|
if (dmHandler.handleMessage(ws, msg)) return true;
|
|
@@ -4,10 +4,8 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
|
4
4
|
|
|
5
5
|
var MEMORY_CONTRACT =
|
|
6
6
|
"The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
|
|
7
|
-
"Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
|
|
8
|
-
"
|
|
9
|
-
"Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. For a Project Driver session with Issues authority, search, reuse, or create the Issue first and put the primary details there: observable evidence, affected component, impact, and next action. Then make the sticky note only a concise title, one-line actionable cue, and the actual opaque issue: reference; do not duplicate the full defect details in the note. If you lack Issues authority, route through an eligible Project Driver; never invent an issue reference or drop the alert. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
|
|
10
|
-
"Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
|
|
7
|
+
"Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
|
|
8
|
+
"Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
|
|
11
9
|
"Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
|
|
12
10
|
"Updates are visible too: update only when durable state materially changes. " +
|
|
13
11
|
"A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
|
|
@@ -35,7 +33,7 @@ function getToolDefs(handlers) {
|
|
|
35
33
|
},
|
|
36
34
|
{
|
|
37
35
|
name: "close_note",
|
|
38
|
-
description: MEMORY_CONTRACT + " Close a note
|
|
36
|
+
description: MEMORY_CONTRACT + " Close a note once what it asked for is done. Authorized Project Drivers may close any note in their project, including notes created by people or other sessions. Other sessions may only close their own notes. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened.",
|
|
39
37
|
inputSchema: buildShape({
|
|
40
38
|
id: { type: "string", description: "Id of the note to close." },
|
|
41
39
|
}, ["id"]),
|
|
@@ -235,12 +235,12 @@ function attachPairFactory(ctx) {
|
|
|
235
235
|
var request = args || {};
|
|
236
236
|
var driverRuntime = preflightRuntime(request.driver || {}, "claude");
|
|
237
237
|
var workerRuntime = preflightRuntime(request.worker || {}, null);
|
|
238
|
-
var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort });
|
|
238
|
+
var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort, hidden: request.hidden === true });
|
|
239
239
|
var worker = null;
|
|
240
240
|
try {
|
|
241
241
|
driver.title = request.name ? request.name + " · Driver" : "Scheduled task · Driver";
|
|
242
242
|
if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
|
|
243
|
-
worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort });
|
|
243
|
+
worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort, hidden: request.hidden === true });
|
|
244
244
|
worker.title = request.name ? request.name + " · Split Worker" : "Scheduled task · Split Worker";
|
|
245
245
|
if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
|
|
246
246
|
sessionProvenance.markWorker(driver, worker, sm.sessions);
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
var z;
|
|
5
5
|
try { z = require("zod"); } catch (e) { z = null; }
|
|
6
|
+
var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
|
|
6
7
|
var evaluationSchema = z ? z.union([
|
|
7
|
-
z.
|
|
8
|
-
z.object({ outcome: z.
|
|
8
|
+
z.enum(EVALUATION_OUTCOMES),
|
|
9
|
+
z.object({ outcome: z.enum(EVALUATION_OUTCOMES), note: z.string().optional() }),
|
|
9
10
|
]) : null;
|
|
10
11
|
|
|
11
12
|
// `options.lifecycle` adds the autonomous management tools. They are omitted
|
|
@@ -75,7 +76,7 @@ function getToolDefs(handlers, options) {
|
|
|
75
76
|
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
|
|
76
77
|
evaluation: {
|
|
77
78
|
schema: evaluationSchema,
|
|
78
|
-
description: "Optional
|
|
79
|
+
description: "Optional assessment of the Worker being replaced. Use succeeded, partial, failed, or abandoned, or an object with outcome set to one of those values and an optional note. Omit when no assessment is needed.",
|
|
79
80
|
},
|
|
80
81
|
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
81
82
|
supersedeProposalId: { type: "string", description: "Exact pending replacement proposal id to supersede with this new configuration." },
|
|
@@ -86,7 +87,7 @@ function getToolDefs(handlers, options) {
|
|
|
86
87
|
name: "record_partner_evaluation",
|
|
87
88
|
description: "Record a bounded assessment of one Split Worker generation so your later model choices can use observed outcomes instead of re-reading transcripts. Records your judgement alongside the objective signals Clay already measured (turns, errors, interruption, context used). This is scoped to your own pair and forms no global model ranking.",
|
|
88
89
|
inputSchema: buildShape({
|
|
89
|
-
outcome: {
|
|
90
|
+
outcome: { enum: EVALUATION_OUTCOMES, description: 'One of "succeeded", "partial", "failed", "abandoned".' },
|
|
90
91
|
note: { type: "string", description: "Optional short reason, up to 400 characters." },
|
|
91
92
|
generation: { type: "number", description: "Optional generation number. Defaults to the current Worker." },
|
|
92
93
|
}, ["outcome"]),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Coordinates bounded semantic title generation for completed conversations.
|
|
2
|
+
|
|
3
|
+
var MAX_ATTEMPTS = 3;
|
|
4
|
+
|
|
5
|
+
function isWorker(session) {
|
|
6
|
+
return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hasUserMessage(session) {
|
|
10
|
+
var history = session && Array.isArray(session.history) ? session.history : [];
|
|
11
|
+
for (var i = 0; i < history.length; i++) {
|
|
12
|
+
if (history[i] && history[i].type === "user_message" && typeof history[i].text === "string" && history[i].text.trim()) return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function attachSessionTitleGenerator(ctx) {
|
|
18
|
+
var sm = ctx.sm;
|
|
19
|
+
var getAdapter = ctx.getAdapter;
|
|
20
|
+
var getLinuxUser = ctx.getLinuxUser || function () { return null; };
|
|
21
|
+
var getRuntimeEnv = ctx.getRuntimeEnv || function () { return process.env; };
|
|
22
|
+
var cwd = ctx.cwd;
|
|
23
|
+
|
|
24
|
+
function isLive(session) {
|
|
25
|
+
if (!session || session.destroying || isWorker(session)) return false;
|
|
26
|
+
if (!sm || !sm.sessions || typeof sm.sessions.get !== "function") return true;
|
|
27
|
+
return sm.sessions.get(session.localId) === session;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function messagesFor(session) {
|
|
31
|
+
var result = [];
|
|
32
|
+
var history = Array.isArray(session.history) ? session.history : [];
|
|
33
|
+
for (var i = 0; i < history.length; i++) {
|
|
34
|
+
var entry = history[i];
|
|
35
|
+
if (entry && entry.type === "user_message" && entry.text) {
|
|
36
|
+
result.push(entry.text.substring(0, 200));
|
|
37
|
+
if (result.length >= 5) break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generate(session) {
|
|
44
|
+
if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || !hasUserMessage(session)) return false;
|
|
45
|
+
if (session._autoTitleInFlight || (session._autoTitleAttempts || 0) >= MAX_ATTEMPTS) return false;
|
|
46
|
+
var sessionAdapter = getAdapter(session);
|
|
47
|
+
if (!sessionAdapter || typeof sessionAdapter.generateTitle !== "function") return false;
|
|
48
|
+
var messages = messagesFor(session);
|
|
49
|
+
if (!messages.length) return false;
|
|
50
|
+
|
|
51
|
+
session._autoTitleAttempts = (session._autoTitleAttempts || 0) + 1;
|
|
52
|
+
session._autoTitleInFlight = true;
|
|
53
|
+
var requestTitle = session.title;
|
|
54
|
+
return Promise.resolve().then(function () {
|
|
55
|
+
return sessionAdapter.generateTitle(messages, {
|
|
56
|
+
cwd: cwd,
|
|
57
|
+
linuxUser: getLinuxUser(session) || undefined,
|
|
58
|
+
env: getRuntimeEnv(session),
|
|
59
|
+
});
|
|
60
|
+
}).then(function (title) {
|
|
61
|
+
title = typeof title === "string" ? title.trim() : "";
|
|
62
|
+
if (title.length < 2) return false;
|
|
63
|
+
title = title.substring(0, 100);
|
|
64
|
+
if (!isLive(session) || session.titleManuallySet || session.titleAutoGenerated || session.title !== requestTitle) return false;
|
|
65
|
+
session.title = title;
|
|
66
|
+
session.titleProvisional = false;
|
|
67
|
+
session.titleAutoGenerated = true;
|
|
68
|
+
sm.saveSessionFile(session);
|
|
69
|
+
sm.broadcastSessionList();
|
|
70
|
+
if (session.cliSessionId && typeof sessionAdapter.renameSession === "function") {
|
|
71
|
+
return Promise.resolve(sessionAdapter.renameSession(session.cliSessionId, title, { dir: cwd })).then(function () { return true; });
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}).catch(function (e) {
|
|
75
|
+
console.error("[auto-title] Failed:", e.message || e);
|
|
76
|
+
return false;
|
|
77
|
+
}).then(function (result) {
|
|
78
|
+
session._autoTitleInFlight = false;
|
|
79
|
+
return result;
|
|
80
|
+
}, function (e) {
|
|
81
|
+
session._autoTitleInFlight = false;
|
|
82
|
+
throw e;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { generate: generate, maxAttempts: MAX_ATTEMPTS };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { attachSessionTitleGenerator: attachSessionTitleGenerator };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Initial titles are immediate, provisional UI labels. Model-generated titles
|
|
3
3
|
// replace them after enough conversation exists to describe the actual topic.
|
|
4
4
|
|
|
5
|
-
var DEFAULT_GENERATION_TURN =
|
|
5
|
+
var DEFAULT_GENERATION_TURN = 1;
|
|
6
6
|
var HOME_GENERATION_TURN = 1;
|
|
7
7
|
var MAX_PROVISIONAL_CHARS = 60;
|
|
8
8
|
var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
|
|
@@ -25,6 +25,7 @@ function firstRecordedUserMessage(session) {
|
|
|
25
25
|
|
|
26
26
|
function selectInitialTitle(session, transportText) {
|
|
27
27
|
var existing = cleanText(session && session.title);
|
|
28
|
+
if (existing && session && session.titleProvisional === true) return { title: existing, provisional: true };
|
|
28
29
|
if (existing) return { title: existing, provisional: false };
|
|
29
30
|
|
|
30
31
|
// Prefer the visible message recorded in history. Query transports may wrap
|
|
@@ -41,9 +42,13 @@ function generationTurn(isMate) {
|
|
|
41
42
|
return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function isLegacySearchTitle(value) {
|
|
46
|
+
return cleanText(value).indexOf(LEGACY_SEARCH_TITLE) === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
function repairLegacySearchTitle(session) {
|
|
45
50
|
var title = cleanText(session && session.title);
|
|
46
|
-
if (!session || title
|
|
51
|
+
if (!session || !isLegacySearchTitle(title)) return false;
|
|
47
52
|
var source = firstRecordedUserMessage(session);
|
|
48
53
|
if (!source) return false;
|
|
49
54
|
session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
|
|
@@ -56,5 +61,6 @@ module.exports = {
|
|
|
56
61
|
HOME_GENERATION_TURN: HOME_GENERATION_TURN,
|
|
57
62
|
selectInitialTitle: selectInitialTitle,
|
|
58
63
|
generationTurn: generationTurn,
|
|
64
|
+
isLegacySearchTitle: isLegacySearchTitle,
|
|
59
65
|
repairLegacySearchTitle: repairLegacySearchTitle,
|
|
60
66
|
};
|
package/lib/sessions.js
CHANGED
|
@@ -8,6 +8,7 @@ var hygiene = require("./session-hygiene");
|
|
|
8
8
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
9
9
|
var sessionProvenance = require("./session-provenance");
|
|
10
10
|
var sessionVisibility = require("./session-visibility");
|
|
11
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
11
12
|
var projectAutonomousRun = require("./project-autonomous-run").projectAutonomousRun;
|
|
12
13
|
var restoreStoredRun = require("./project-autonomous-run").restoreStoredRun;
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ function createSessionManager(opts) {
|
|
|
23
24
|
var sessionViewedListeners = [];
|
|
24
25
|
var onSessionRenamed = opts.onSessionRenamed || function () {};
|
|
25
26
|
var onSessionIdentityAssigned = opts.onSessionIdentityAssigned || function () {};
|
|
27
|
+
var onSessionHydrate = opts.onSessionHydrate || null;
|
|
26
28
|
|
|
27
29
|
// --- Multi-session state ---
|
|
28
30
|
var nextLocalId = 1;
|
|
@@ -153,11 +155,15 @@ function createSessionManager(opts) {
|
|
|
153
155
|
localId: session.localId,
|
|
154
156
|
cliSessionId: session.cliSessionId,
|
|
155
157
|
title: session.title,
|
|
158
|
+
titleProvisional: session.titleProvisional === true,
|
|
159
|
+
titleAutoGenerated: session.titleAutoGenerated === true,
|
|
160
|
+
titleManuallySet: session.titleManuallySet === true,
|
|
156
161
|
createdAt: session.createdAt,
|
|
157
162
|
};
|
|
158
163
|
if (session.ownerId) metaObj.ownerId = session.ownerId;
|
|
159
164
|
if (session.vendor) metaObj.vendor = session.vendor;
|
|
160
165
|
if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
|
|
166
|
+
if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
|
|
161
167
|
if (session.model) metaObj.model = session.model;
|
|
162
168
|
if (session.effort) metaObj.effort = session.effort;
|
|
163
169
|
// Persist the session's "born" mode so TUI sessions reappear after a
|
|
@@ -177,6 +183,7 @@ function createSessionManager(opts) {
|
|
|
177
183
|
if (session.loopInterviewHandoff) metaObj.loopInterviewHandoff = session.loopInterviewHandoff;
|
|
178
184
|
if (session.scheduledTaskDraft) metaObj.scheduledTaskDraft = session.scheduledTaskDraft;
|
|
179
185
|
if (session.scheduledTaskRun) metaObj.scheduledTaskRun = session.scheduledTaskRun;
|
|
186
|
+
if (session.hidden) metaObj.hidden = true;
|
|
180
187
|
metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
|
|
181
188
|
if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
|
|
182
189
|
if (session.bookmarked) metaObj.bookmarked = true;
|
|
@@ -285,6 +292,11 @@ function createSessionManager(opts) {
|
|
|
285
292
|
messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
|
|
286
293
|
}
|
|
287
294
|
}
|
|
295
|
+
var hasTitleProvenance = Object.prototype.hasOwnProperty.call(m, "titleProvisional")
|
|
296
|
+
|| Object.prototype.hasOwnProperty.call(m, "titleAutoGenerated")
|
|
297
|
+
|| Object.prototype.hasOwnProperty.call(m, "titleManuallySet");
|
|
298
|
+
var legacyTitleProtected = !hasTitleProvenance && typeof m.title === "string"
|
|
299
|
+
&& m.title.trim() && !sessionTitlePolicy.isLegacySearchTitle(m.title);
|
|
288
300
|
var session = {
|
|
289
301
|
localId: localId,
|
|
290
302
|
queryInstance: null,
|
|
@@ -296,6 +308,9 @@ function createSessionManager(opts) {
|
|
|
296
308
|
pendingAskUser: {},
|
|
297
309
|
isProcessing: false,
|
|
298
310
|
title: m.title || "",
|
|
311
|
+
titleProvisional: m.titleProvisional === true,
|
|
312
|
+
titleAutoGenerated: m.titleAutoGenerated === true,
|
|
313
|
+
titleManuallySet: m.titleManuallySet === true || legacyTitleProtected,
|
|
299
314
|
createdAt: m.createdAt || Date.now(),
|
|
300
315
|
lastActivity: loaded[i].mtime || m.createdAt || Date.now(),
|
|
301
316
|
history: loaded[i].history,
|
|
@@ -304,6 +319,7 @@ function createSessionManager(opts) {
|
|
|
304
319
|
};
|
|
305
320
|
if (m.vendor) session.vendor = m.vendor;
|
|
306
321
|
session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
|
|
322
|
+
session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
|
|
307
323
|
session.model = m.model || deriveSessionModel(loaded[i].history);
|
|
308
324
|
session.effort = m.effort || null;
|
|
309
325
|
if (m.loop) session.loop = m.loop;
|
|
@@ -338,6 +354,7 @@ function createSessionManager(opts) {
|
|
|
338
354
|
session.loopInterviewHandoff = m.loopInterviewHandoff && typeof m.loopInterviewHandoff === "object" ? m.loopInterviewHandoff : null;
|
|
339
355
|
session.scheduledTaskDraft = m.scheduledTaskDraft && typeof m.scheduledTaskDraft === "object" ? m.scheduledTaskDraft : null;
|
|
340
356
|
session.scheduledTaskRun = m.scheduledTaskRun && typeof m.scheduledTaskRun === "object" ? m.scheduledTaskRun : null;
|
|
357
|
+
session.hidden = m.hidden === true || !!session.scheduledTaskRun;
|
|
341
358
|
session.terminalId = null;
|
|
342
359
|
session.runtimeMode = null;
|
|
343
360
|
session.runtimeTerminalId = null;
|
|
@@ -641,6 +658,7 @@ function createSessionManager(opts) {
|
|
|
641
658
|
allowedTools: {},
|
|
642
659
|
isProcessing: false,
|
|
643
660
|
title: "",
|
|
661
|
+
titleProvisional: false,
|
|
644
662
|
titleAutoGenerated: false,
|
|
645
663
|
turnCount: 0,
|
|
646
664
|
createdAt: Date.now(),
|
|
@@ -664,6 +682,7 @@ function createSessionManager(opts) {
|
|
|
664
682
|
loopInterviewHandoff: null,
|
|
665
683
|
scheduledTaskDraft: null,
|
|
666
684
|
scheduledTaskRun: null,
|
|
685
|
+
hidden: !!(sessionOpts && sessionOpts.hidden),
|
|
667
686
|
terminalId: null,
|
|
668
687
|
sessionOriginId: null,
|
|
669
688
|
};
|
|
@@ -688,6 +707,7 @@ function createSessionManager(opts) {
|
|
|
688
707
|
allowedTools: {},
|
|
689
708
|
isProcessing: false,
|
|
690
709
|
title: "",
|
|
710
|
+
titleProvisional: false,
|
|
691
711
|
titleAutoGenerated: false,
|
|
692
712
|
turnCount: 0,
|
|
693
713
|
createdAt: Date.now(),
|
|
@@ -711,6 +731,7 @@ function createSessionManager(opts) {
|
|
|
711
731
|
loopInterviewHandoff: null,
|
|
712
732
|
scheduledTaskDraft: null,
|
|
713
733
|
scheduledTaskRun: null,
|
|
734
|
+
hidden: !!(sessionOpts && sessionOpts.hidden),
|
|
714
735
|
terminalId: null,
|
|
715
736
|
sessionOriginId: null,
|
|
716
737
|
};
|
|
@@ -779,6 +800,7 @@ function createSessionManager(opts) {
|
|
|
779
800
|
function switchSession(localId, targetWs, transform) {
|
|
780
801
|
var session = sessions.get(localId);
|
|
781
802
|
if (!session) return;
|
|
803
|
+
if (typeof onSessionHydrate === "function") onSessionHydrate(session);
|
|
782
804
|
|
|
783
805
|
// Pane sockets (split-view iframes) pin their session via switch_session
|
|
784
806
|
// but must not move the project-global active pointer: reloads and
|
|
@@ -806,7 +828,9 @@ function createSessionManager(opts) {
|
|
|
806
828
|
var _vendorCaps = _capsByVendor[_sessionVendor] || {};
|
|
807
829
|
_send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
|
|
808
830
|
// Send vendor-specific slash commands
|
|
809
|
-
var _vendorCmds = slashCommandsByVendor
|
|
831
|
+
var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
|
|
832
|
+
|| session.slashCommands
|
|
833
|
+
|| [];
|
|
810
834
|
_send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
|
|
811
835
|
broadcastSessionList();
|
|
812
836
|
replayHistory(session, undefined, targetWs, transform);
|
|
@@ -1270,6 +1294,7 @@ function createSessionManager(opts) {
|
|
|
1270
1294
|
}
|
|
1271
1295
|
|
|
1272
1296
|
return {
|
|
1297
|
+
setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
|
|
1273
1298
|
get activeSessionId() { return activeSessionId; },
|
|
1274
1299
|
get nextLocalId() { return nextLocalId; },
|
|
1275
1300
|
get slashCommands() { return slashCommands; },
|
package/lib/user-presence.js
CHANGED
|
@@ -12,6 +12,11 @@ function sessionIdForPersistence(session) {
|
|
|
12
12
|
return session.cliSessionId || session.localId || null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function shouldPersistDisconnectPresence(existing, sessionId, existingSession, currentSession) {
|
|
16
|
+
return !!sessionId && (!existing || existing.sessionId === sessionId ||
|
|
17
|
+
(!!existingSession && !!currentSession && existingSession === currentSession));
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
function findSession(sessions, sessionId) {
|
|
16
21
|
if (!sessions || sessionId == null) return null;
|
|
17
22
|
var found = null;
|
|
@@ -100,6 +105,7 @@ load();
|
|
|
100
105
|
|
|
101
106
|
module.exports = {
|
|
102
107
|
sessionIdForPersistence: sessionIdForPersistence,
|
|
108
|
+
shouldPersistDisconnectPresence: shouldPersistDisconnectPresence,
|
|
103
109
|
findSession: findSession,
|
|
104
110
|
setPresence: setPresence,
|
|
105
111
|
getPresence: getPresence,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Durable user-scoped context display preference storage.
|
|
2
|
+
|
|
3
|
+
var PREFERENCE_KEY = "contextViewPreference";
|
|
4
|
+
var MODES = ["off", "mini", "panel"];
|
|
5
|
+
|
|
6
|
+
function validMode(value) {
|
|
7
|
+
return MODES.indexOf(value) !== -1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function attachContextViewPreferences(deps) {
|
|
11
|
+
var loadUsers = deps.loadUsers;
|
|
12
|
+
var saveUsers = deps.saveUsers;
|
|
13
|
+
var isMultiUser = deps.isMultiUser || function () { return false; };
|
|
14
|
+
var configModule = deps.configModule || require("./config");
|
|
15
|
+
|
|
16
|
+
function findRecord(data, userId) {
|
|
17
|
+
var records = Array.isArray(data.users) ? data.users : [];
|
|
18
|
+
for (var i = 0; i < records.length; i++) if (records[i] && records[i].id === userId) return records[i];
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function result(value, source) {
|
|
23
|
+
if (value === undefined) return { present: false, mode: "off", source: null };
|
|
24
|
+
if (!validMode(value)) return { present: true, mode: "off", source: source, error: "Context display preference is invalid." };
|
|
25
|
+
return { present: true, mode: value, source: source };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function get(userId) {
|
|
29
|
+
var data;
|
|
30
|
+
try { data = loadUsers(); } catch (error) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (error.message || error) }; }
|
|
31
|
+
var record = findRecord(data, userId);
|
|
32
|
+
if (isMultiUser() && !record) return { present: false, mode: "off", source: null, error: "User not found." };
|
|
33
|
+
if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return result(record[PREFERENCE_KEY], "explicit");
|
|
34
|
+
if (record) return { present: false, mode: "off", source: null };
|
|
35
|
+
if (userId === "default") {
|
|
36
|
+
var config;
|
|
37
|
+
try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, mode: "off", source: null, error: "Unable to read context display preference: " + (configError.message || configError) }; }
|
|
38
|
+
if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return result(config[PREFERENCE_KEY], "explicit");
|
|
39
|
+
}
|
|
40
|
+
return { present: false, mode: "off", source: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function set(userId, mode) {
|
|
44
|
+
if (!validMode(mode)) return { ok: false, error: "Context display preference is invalid." };
|
|
45
|
+
var data;
|
|
46
|
+
try { data = loadUsers(); } catch (error) { return { ok: false, error: "Unable to read context display preference: " + (error.message || error) }; }
|
|
47
|
+
var record = findRecord(data, userId);
|
|
48
|
+
if (isMultiUser() && !record) return { ok: false, error: "User not found." };
|
|
49
|
+
if (record) {
|
|
50
|
+
var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) { return item === record ? Object.assign({}, item, { contextViewPreference: mode }) : item; }) });
|
|
51
|
+
try { saveUsers(nextData); } catch (saveError) { return { ok: false, error: "Unable to save context display preference: " + (saveError.message || saveError) }; }
|
|
52
|
+
return { ok: true, mode: mode };
|
|
53
|
+
}
|
|
54
|
+
if (userId !== "default") return { ok: false, error: "User not found." };
|
|
55
|
+
try {
|
|
56
|
+
var current = configModule.loadConfig() || {};
|
|
57
|
+
configModule.saveConfig(Object.assign({}, current, { contextViewPreference: mode }));
|
|
58
|
+
return { ok: true, mode: mode };
|
|
59
|
+
} catch (configSaveError) { return { ok: false, error: "Unable to save context display preference: " + (configSaveError.message || configSaveError) }; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { get: get, set: set, validMode: validMode, MODES: MODES };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { attachContextViewPreferences: attachContextViewPreferences, validMode: validMode, MODES: MODES, PREFERENCE_KEY: PREFERENCE_KEY };
|