clay-server 5.2.0-beta.8 → 5.2.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/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +78 -29
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +16 -4
- package/lib/public/app.js +41 -18
- package/lib/public/css/contextual-clay.css +2 -2
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +37 -11
- package/lib/public/css/title-bar.css +83 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-message-router.js +13 -0
- package/lib/public/modules/app-messages.js +35 -7
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-projects.js +6 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-owner-navigation.js +0 -3
- package/lib/public/modules/clay-studio-render.js +3 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/command-palette.js +52 -14
- package/lib/public/modules/contextual-clay-workspace.js +14 -2
- package/lib/public/modules/contextual-clay.js +19 -4
- package/lib/public/modules/contextual-mate-context.js +6 -4
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/history-replay-batch.js +8 -0
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/input.js +55 -11
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/message-delivery.js +10 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +5 -2
- package/lib/public/modules/pane-browser-bridge.js +5 -3
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +32 -44
- package/lib/public/modules/projects-hub.js +39 -28
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +51 -52
- package/lib/public/modules/sticky-notes-checklist.js +51 -6
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/theme.js +31 -3
- package/lib/public/modules/tools.js +18 -2
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +12 -34
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +323 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +11 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +27 -10
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +5 -3
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -46,6 +46,10 @@ function attachMessageProcessor(ctx) {
|
|
|
46
46
|
sm.sendAndRecord(session, obj, clientObj);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function observeQuota(session, message, observedAtMs) {
|
|
50
|
+
if (typeof sm.notifyQuotaObserved === "function") sm.notifyQuotaObserved(session, message, observedAtMs);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
function recordTurnActivity(session, activity) {
|
|
50
54
|
// An authoritative input card owns progress until its request settles.
|
|
51
55
|
var awaitingInput = ["pendingPermissions", "pendingAskUser", "pendingElicitations", "pendingUserDialogs"].some(function (key) {
|
|
@@ -892,7 +896,7 @@ function attachMessageProcessor(ctx) {
|
|
|
892
896
|
} else if (parsed.yokeType === "rate_limit") {
|
|
893
897
|
var info = parsed.rateLimitInfo;
|
|
894
898
|
if (info.source === "account_snapshot") {
|
|
895
|
-
quotaSnapshots.recordQuotaSnapshot(session, info, sendAndRecord);
|
|
899
|
+
quotaSnapshots.recordQuotaSnapshot(session, info, sendAndRecord, observeQuota);
|
|
896
900
|
return;
|
|
897
901
|
}
|
|
898
902
|
console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
|
|
@@ -907,6 +911,7 @@ function attachMessageProcessor(ctx) {
|
|
|
907
911
|
resetsAt: info.resetsAt * 1000,
|
|
908
912
|
status: info.status,
|
|
909
913
|
});
|
|
914
|
+
observeQuota(session, { type: "rate_limit_usage", vendor: session.vendor || "claude", rateLimitType: info.rateLimitType, resetsAt: info.resetsAt * 1000, status: info.status }, Date.now());
|
|
910
915
|
}
|
|
911
916
|
|
|
912
917
|
// Warning/rejection handling (existing behavior)
|
|
@@ -925,6 +930,9 @@ function attachMessageProcessor(ctx) {
|
|
|
925
930
|
overageResetsAt: info.overageResetsAt ? info.overageResetsAt * 1000 : null,
|
|
926
931
|
overageDisabledReason: info.overageDisabledReason || null,
|
|
927
932
|
});
|
|
933
|
+
if (info.rateLimitType && info.utilization) {
|
|
934
|
+
observeQuota(session, { type: "rate_limit", vendor: session.vendor || "claude", rateLimitType: info.rateLimitType, utilization: info.utilization }, Date.now());
|
|
935
|
+
}
|
|
928
936
|
// Track rejection for auto-continue / scheduled message support
|
|
929
937
|
if (info.status === "rejected" && info.resetsAt) {
|
|
930
938
|
session.rateLimitResetsAt = info.resetsAt * 1000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function recordQuotaSnapshot(session, info, sendAndRecord) {
|
|
1
|
+
function recordQuotaSnapshot(session, info, sendAndRecord, observe) {
|
|
2
2
|
var message = {
|
|
3
3
|
type: "rate_limit_usage",
|
|
4
4
|
sessionId: session.localId,
|
|
@@ -10,6 +10,9 @@ function recordQuotaSnapshot(session, info, sendAndRecord) {
|
|
|
10
10
|
}),
|
|
11
11
|
};
|
|
12
12
|
if (Number.isFinite(info.observedAtMs)) message.observedAtMs = info.observedAtMs;
|
|
13
|
+
// The overview records every live observation, including an unchanged one:
|
|
14
|
+
// deduplication below only avoids re-recording identical history.
|
|
15
|
+
if (typeof observe === "function") observe(session, message, Number.isFinite(info.observedAtMs) ? info.observedAtMs : Date.now());
|
|
13
16
|
var snapshots = session._quotaSnapshots || (session._quotaSnapshots = Object.create(null));
|
|
14
17
|
var key = JSON.stringify([session.ownerId, session.vendor, info.limitId]);
|
|
15
18
|
var serialized = JSON.stringify(message);
|
|
@@ -18,7 +21,22 @@ function recordQuotaSnapshot(session, info, sendAndRecord) {
|
|
|
18
21
|
sendAndRecord(session, message);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
// Observation listeners feed the owner-scoped vendor overview. They never
|
|
25
|
+
// influence rejection handling or scheduling.
|
|
26
|
+
function attachQuotaObservers(sm) {
|
|
27
|
+
// Idempotent: the processor and the overview may attach in either order.
|
|
28
|
+
if (typeof sm.addOnQuotaObserved === "function") return;
|
|
29
|
+
var listeners = [];
|
|
30
|
+
sm.addOnQuotaObserved = function (fn) { if (typeof fn === "function") listeners.push(fn); };
|
|
31
|
+
sm.notifyQuotaObserved = function (session, message, observedAtMs) {
|
|
32
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
33
|
+
try { listeners[i](session, message, observedAtMs); } catch (e) { /* the overview is optional */ }
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
function attachQuotaSnapshots(sm) {
|
|
39
|
+
attachQuotaObservers(sm);
|
|
22
40
|
if (typeof sm.addOnSessionViewed !== "function") return;
|
|
23
41
|
sm.addOnSessionViewed(function (session, ws, send) {
|
|
24
42
|
var seen = Object.create(null);
|
|
@@ -34,4 +52,4 @@ function attachQuotaSnapshots(sm) {
|
|
|
34
52
|
});
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
module.exports = { recordQuotaSnapshot: recordQuotaSnapshot, attachQuotaSnapshots: attachQuotaSnapshots };
|
|
55
|
+
module.exports = { recordQuotaSnapshot: recordQuotaSnapshot, attachQuotaSnapshots: attachQuotaSnapshots, attachQuotaObservers: attachQuotaObservers };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// server-github-stars.js - GET /api/github-stars for the top-bar Star link.
|
|
2
|
+
//
|
|
3
|
+
// Read-only. Returns the cached public star count of the fixed Clay
|
|
4
|
+
// repository (null when not yet known). No request input reaches GitHub, and
|
|
5
|
+
// nothing about the caller is sent or stored.
|
|
6
|
+
|
|
7
|
+
var githubStarCount = require("./github-star-count");
|
|
8
|
+
|
|
9
|
+
function attachGithubStars(ctx) {
|
|
10
|
+
var cache = ctx.cache || githubStarCount.createGithubStarCount();
|
|
11
|
+
|
|
12
|
+
function json(res, status, body) {
|
|
13
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
14
|
+
res.end(JSON.stringify(body));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function handleRequest(req, res, url) {
|
|
18
|
+
if (url !== "/api/github-stars") return false;
|
|
19
|
+
if (req.method !== "GET") {
|
|
20
|
+
res.writeHead(405, { "Content-Type": "application/json", "Allow": "GET" });
|
|
21
|
+
res.end(JSON.stringify({ error: "Method not allowed." }));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (!ctx.isRequestAuthed(req)) {
|
|
25
|
+
json(res, 401, { error: "Sign in to continue." });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
cache.get().then(function (snapshot) {
|
|
29
|
+
json(res, 200, snapshot);
|
|
30
|
+
}, function () {
|
|
31
|
+
json(res, 200, { repo: githubStarCount.REPO, stars: null, fetchedAt: null });
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { handleRequest: handleRequest };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { attachGithubStars: attachGithubStars };
|
package/lib/server-global-ws.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
var fs = require("fs");
|
|
20
20
|
var path = require("path");
|
|
21
21
|
var config = require("./config");
|
|
22
|
+
var startProtocol = require("./project-start-protocol");
|
|
23
|
+
var startReply = startProtocol.withRequestId;
|
|
24
|
+
var browseMeta = startProtocol.browseMeta;
|
|
22
25
|
|
|
23
26
|
// Mirrored from lib/project.js — keep in sync if entries change there.
|
|
24
27
|
var IGNORED_DIRS = new Set([
|
|
@@ -79,8 +82,9 @@ function attachGlobalWs(opts) {
|
|
|
79
82
|
parentDir = path.dirname(absTarget);
|
|
80
83
|
prefix = path.basename(absTarget).toLowerCase();
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
var canList = function (candidate) { return !opts.canUsePath || opts.canUsePath(ws._clayUser && ws._clayUser.id, candidate); };
|
|
86
|
+
if (!canList(parentDir)) {
|
|
87
|
+
sendTo(ws, startReply(msg, { type: "browse_dir_result", path: msg.path, entries: [], error: "Directory access is not permitted" }));
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
try {
|
|
@@ -96,9 +100,9 @@ function attachGlobalWs(opts) {
|
|
|
96
100
|
dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
|
|
97
101
|
}
|
|
98
102
|
dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
99
|
-
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
|
|
103
|
+
sendTo(ws, Object.assign(browseMeta(msg, parentDir, prefix, canList), { type: "browse_dir_result", path: msg.path, entries: dirEntries }));
|
|
100
104
|
} catch (e) {
|
|
101
|
-
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
|
|
105
|
+
sendTo(ws, startReply(msg, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message }));
|
|
102
106
|
}
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
@@ -108,24 +112,24 @@ function attachGlobalWs(opts) {
|
|
|
108
112
|
var addPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
109
113
|
var addAbs = path.resolve(addPath);
|
|
110
114
|
if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, addAbs)) {
|
|
111
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Directory access is not permitted" });
|
|
115
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Directory access is not permitted" }));
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
114
118
|
try {
|
|
115
119
|
var addStat = fs.statSync(addAbs);
|
|
116
120
|
if (!addStat.isDirectory()) {
|
|
117
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
|
|
121
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not a directory" }));
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
120
124
|
} catch (e) {
|
|
121
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
|
|
125
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Directory not found" }));
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
128
|
if (typeof onAddProject === "function") {
|
|
125
129
|
var result = onAddProject(addAbs, ws._clayUser);
|
|
126
|
-
sendTo(ws, Object.assign({}, result, { type: "add_project_result" }));
|
|
130
|
+
sendTo(ws, startReply(msg, Object.assign({}, result, { type: "add_project_result" })));
|
|
127
131
|
} else {
|
|
128
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
132
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
|
|
129
133
|
}
|
|
130
134
|
return;
|
|
131
135
|
}
|
|
@@ -135,7 +139,7 @@ function attachGlobalWs(opts) {
|
|
|
135
139
|
if (ws._clayUser && usersModule) {
|
|
136
140
|
var cpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
137
141
|
if (!cpPerms.createProject) {
|
|
138
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" });
|
|
142
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" }));
|
|
139
143
|
return;
|
|
140
144
|
}
|
|
141
145
|
}
|
|
@@ -144,14 +148,14 @@ function attachGlobalWs(opts) {
|
|
|
144
148
|
if (msg.type === "create_project") {
|
|
145
149
|
var createName = (msg.name || "").trim();
|
|
146
150
|
if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
|
|
147
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." });
|
|
151
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." }));
|
|
148
152
|
return;
|
|
149
153
|
}
|
|
150
154
|
if (typeof onCreateProject === "function") {
|
|
151
155
|
var createResult = onCreateProject(createName, ws._clayUser);
|
|
152
|
-
sendTo(ws, Object.assign({}, createResult, { type: "add_project_result" }));
|
|
156
|
+
sendTo(ws, startReply(msg, Object.assign({}, createResult, { type: "add_project_result" })));
|
|
153
157
|
} else {
|
|
154
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
158
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
|
|
155
159
|
}
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
@@ -159,16 +163,16 @@ function attachGlobalWs(opts) {
|
|
|
159
163
|
if (msg.type === "clone_project") {
|
|
160
164
|
var cloneUrl = (msg.url || "").trim();
|
|
161
165
|
if (!cloneUrl || (!/^https?:\/\//.test(cloneUrl) && !/^git@/.test(cloneUrl))) {
|
|
162
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." });
|
|
166
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." }));
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
165
|
-
sendTo(ws, { type: "clone_project_progress", status: "cloning" });
|
|
169
|
+
sendTo(ws, startReply(msg, { type: "clone_project_progress", status: "cloning" }));
|
|
166
170
|
if (typeof onCloneProject === "function") {
|
|
167
171
|
onCloneProject(cloneUrl, ws._clayUser, function (cloneResult) {
|
|
168
|
-
sendTo(ws, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error });
|
|
172
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error }));
|
|
169
173
|
});
|
|
170
174
|
} else {
|
|
171
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
175
|
+
sendTo(ws, startReply(msg, { type: "add_project_result", ok: false, error: "Not supported" }));
|
|
172
176
|
}
|
|
173
177
|
return;
|
|
174
178
|
}
|
package/lib/server.js
CHANGED
|
@@ -507,6 +507,8 @@ function createServer(opts) {
|
|
|
507
507
|
var linear = require('./server-linear').attachLinear({ users: users, getMultiUserFromReq: getMultiUserFromReq,
|
|
508
508
|
isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR });
|
|
509
509
|
|
|
510
|
+
var githubStars = require('./server-github-stars').attachGithubStars({ isRequestAuthed: isRequestAuthed });
|
|
511
|
+
|
|
510
512
|
var speech = serverSpeech.attachSpeech({ users: users, getMultiUserFromReq: getMultiUserFromReq,
|
|
511
513
|
isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR, getAllowedOrigins: getAllowedOrigins });
|
|
512
514
|
|
|
@@ -925,6 +927,7 @@ function createServer(opts) {
|
|
|
925
927
|
|
|
926
928
|
if (linear.handleRequest(req, res, fullUrl)) return;
|
|
927
929
|
if (speech.handleRequest(req, res, fullUrl)) return;
|
|
930
|
+
if (githubStars.handleRequest(req, res, fullUrl)) return;
|
|
928
931
|
if (networkSettings.handleRequest(req, res, fullUrl)) return;
|
|
929
932
|
if (storageSettings.handleRequest(req, res, fullUrl)) return;
|
|
930
933
|
if (settings.handleRequest(req, res, fullUrl)) return;
|
|
@@ -967,6 +970,13 @@ function createServer(opts) {
|
|
|
967
970
|
res.end(auth.getAuthPage());
|
|
968
971
|
return;
|
|
969
972
|
}
|
|
973
|
+
// The standalone project list was removed. A legacy /projects link resolves
|
|
974
|
+
// through the same server-backed workspace selection as the root path.
|
|
975
|
+
if (fullUrl !== "/") {
|
|
976
|
+
res.writeHead(302, { "Location": "/", "Cache-Control": "no-store" });
|
|
977
|
+
res.end();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
970
980
|
if (projects.size > 0) {
|
|
971
981
|
var targetSlug = null;
|
|
972
982
|
var reqUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
@@ -1590,6 +1600,7 @@ function createServer(opts) {
|
|
|
1590
1600
|
return ctx.getSchedules().length;
|
|
1591
1601
|
},
|
|
1592
1602
|
canAccessProjectSlug: canUserAccessSlug,
|
|
1603
|
+
onGetProjectAccess: onGetProjectAccess,
|
|
1593
1604
|
canUsePath: canUsePath,
|
|
1594
1605
|
onPresenceChange: broadcastPresenceChange,
|
|
1595
1606
|
onProcessingChanged: broadcastProcessingChange,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Sticky-note memory tools for project sessions.
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
|
+
var z;
|
|
5
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
4
6
|
|
|
5
7
|
var MEMORY_CONTRACT =
|
|
6
8
|
"The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
|
|
@@ -8,15 +10,28 @@ var MEMORY_CONTRACT =
|
|
|
8
10
|
"Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; confirmed Issues also get short link notes as described below. 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. " +
|
|
9
11
|
require("./issues-filing-guidance").NOTE_GUIDANCE +
|
|
10
12
|
"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. " +
|
|
13
|
+
"One exception: when the user explicitly asks for a working checklist, keep that one native checklist note and check its items off as they are done; never start a checklist the user did not ask for. " +
|
|
11
14
|
"Updates are visible too: update only when durable state materially changes. " +
|
|
12
15
|
"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. " +
|
|
13
16
|
"Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
|
|
14
17
|
|
|
18
|
+
var CHECKLIST_USAGE =
|
|
19
|
+
" Native checklists: pass checklist: true to create one. The first line is the title and every later non-empty, non-heading line is an item; write items as '- [ ] task' and '- [x] task' for items already done. " +
|
|
20
|
+
"To check or uncheck items, call list_notes, then pass id, that note's current version and checks with the exact item keys it returned. " +
|
|
21
|
+
"Any change to a checklist note, switching checklist mode, or changing checks needs the current version; a stale version or unknown item is refused, so list again and retry. " +
|
|
22
|
+
"Text, saved checks and colour you do not change are kept, and switching mode never erases content. Change text and checks in separate calls. " +
|
|
23
|
+
"When rewriting a checklist's text, start from checklist.effectiveText so each line's - [x] marker carries its real completion; droppedSavedChecks lists saved checks whose exact line no longer exists.";
|
|
24
|
+
|
|
25
|
+
var CHECK_ENTRY = z ? z.object({
|
|
26
|
+
item: z.string().describe("Exact item key from list_notes checklist.items[].item."),
|
|
27
|
+
checked: z.boolean().describe("true marks the item done; false marks it not done."),
|
|
28
|
+
}) : null;
|
|
29
|
+
|
|
15
30
|
function getToolDefs(handlers) {
|
|
16
31
|
return [
|
|
17
32
|
{
|
|
18
33
|
name: "list_notes",
|
|
19
|
-
description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones.",
|
|
34
|
+
description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones. Each note includes its version and checklistMode; checklist notes also include checklist.items with exact item keys, each item's effective checked value (a saved check overrides the Markdown marker), its saved value, and completed/total counts.",
|
|
20
35
|
inputSchema: buildShape({
|
|
21
36
|
state: { type: "string", enum: ["open", "closed", "all"], description: "Which notes to list. Defaults to open, the notes still asking for action." },
|
|
22
37
|
}),
|
|
@@ -24,12 +39,16 @@ function getToolDefs(handlers) {
|
|
|
24
39
|
},
|
|
25
40
|
{
|
|
26
41
|
name: "write_note",
|
|
27
|
-
description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target.",
|
|
42
|
+
description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes and a working checklist the user explicitly asked for, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target." + CHECKLIST_USAGE,
|
|
28
43
|
inputSchema: buildShape({
|
|
29
44
|
id: { type: "string", description: "Existing note id to update. Omit to create a note." },
|
|
30
|
-
text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it." },
|
|
45
|
+
text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it. Required when creating; omit on update to keep the current text." },
|
|
31
46
|
color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
|
|
32
|
-
|
|
47
|
+
checklist: { type: "boolean", description: "true creates or switches to a native checklist, false switches back to an ordinary note. Switching keeps the text and saved checks." },
|
|
48
|
+
version: { type: "number", description: "The note's current version from list_notes. Required for any change to a checklist note, a checklist mode switch, or checks." },
|
|
49
|
+
// buildShape returns no fields when zod is unavailable.
|
|
50
|
+
checks: { schema: CHECK_ENTRY && z.array(CHECK_ENTRY).min(1).max(1000), description: "Checklist items to check or uncheck on an existing checklist note, using exact item keys from list_notes." },
|
|
51
|
+
}),
|
|
33
52
|
handler: function (args) { return handlers.write(args || {}); },
|
|
34
53
|
},
|
|
35
54
|
{
|
|
@@ -60,6 +79,9 @@ function getToolDefs(handlers) {
|
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
module.exports = {
|
|
82
|
+
// 2: native checklist fields. Conversation-scoped catalogs older than this
|
|
83
|
+
// receive the current schema through exact-query recovery.
|
|
84
|
+
CATALOG_VERSION: 2,
|
|
63
85
|
MEMORY_CONTRACT: MEMORY_CONTRACT,
|
|
64
86
|
getToolDefs: getToolDefs,
|
|
65
87
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pure planning for agent sticky-note updates, including native checklists.
|
|
2
|
+
// Every checklist-affecting write must name the note's current version and
|
|
3
|
+
// exact item keys, so a stale view can never overwrite newer content or marks.
|
|
4
|
+
|
|
5
|
+
var checklist = require("./notes-checklist");
|
|
6
|
+
|
|
7
|
+
var MAX_CHECKS = 1000;
|
|
8
|
+
|
|
9
|
+
function validateArgs(args) {
|
|
10
|
+
if (args.checklist !== undefined && typeof args.checklist !== "boolean") return "checklist must be true or false";
|
|
11
|
+
if (args.version !== undefined && !Number.isFinite(args.version)) return "version must be the number returned by list_notes";
|
|
12
|
+
if (args.checks === undefined) return null;
|
|
13
|
+
if (!Array.isArray(args.checks) || args.checks.length === 0 || args.checks.length > MAX_CHECKS) {
|
|
14
|
+
return "checks must be a non-empty array of at most " + MAX_CHECKS + " { item, checked } entries";
|
|
15
|
+
}
|
|
16
|
+
for (var i = 0; i < args.checks.length; i++) {
|
|
17
|
+
var entry = args.checks[i];
|
|
18
|
+
if (!entry || typeof entry !== "object" || typeof entry.item !== "string" || typeof entry.checked !== "boolean") {
|
|
19
|
+
return "each checks entry needs an item key from list_notes and checked true or false";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function staleError(note) {
|
|
26
|
+
return "note " + note.id + " has changed or the version is missing; call list_notes, review its current text and items, and retry with that version";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Saved marks whose exact item line no longer exists after a text change.
|
|
30
|
+
function droppedChecks(note, nextText) {
|
|
31
|
+
var kept = checklist.normalizeState(note.checklistState, nextText);
|
|
32
|
+
return checklist.describe(note).filter(function (entry) {
|
|
33
|
+
return entry.saved !== null && kept[decodeURIComponent(entry.item)] === undefined;
|
|
34
|
+
}).map(function (entry) {
|
|
35
|
+
return { text: entry.text, checked: entry.saved };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function planUpdate(note, args, text, color) {
|
|
40
|
+
var isChecklist = note.checklistMode === true;
|
|
41
|
+
var modeChange = typeof args.checklist === "boolean" && args.checklist !== isChecklist;
|
|
42
|
+
var hasText = typeof args.text === "string";
|
|
43
|
+
var hasChecks = Array.isArray(args.checks);
|
|
44
|
+
var needsVersion = isChecklist || modeChange || hasChecks;
|
|
45
|
+
if ((needsVersion || args.version !== undefined) && args.version !== note.updatedAt) return { error: staleError(note) };
|
|
46
|
+
if (hasText && !text) return { error: "text must not be empty; close the note instead of erasing it" };
|
|
47
|
+
if (hasText && hasChecks) return { error: "change text and checks in separate calls so item keys refer to one exact version" };
|
|
48
|
+
|
|
49
|
+
var changes = {};
|
|
50
|
+
var dropped = [];
|
|
51
|
+
if (hasText && text !== note.text) {
|
|
52
|
+
changes.text = text;
|
|
53
|
+
dropped = droppedChecks(note, text);
|
|
54
|
+
if (isChecklist && !modeChange) {
|
|
55
|
+
// Storage repeats the same exact-source and version guard atomically.
|
|
56
|
+
changes.checklistEdit = true;
|
|
57
|
+
changes.checklistSource = note.text;
|
|
58
|
+
changes.checklistVersion = note.updatedAt;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (color && color !== note.color) changes.color = color;
|
|
62
|
+
if (modeChange) changes.checklistMode = args.checklist;
|
|
63
|
+
|
|
64
|
+
if (hasChecks) {
|
|
65
|
+
var resultMode = modeChange ? args.checklist : isChecklist;
|
|
66
|
+
if (!resultMode) return { error: "checks apply only to checklist notes; pass checklist: true to switch this note to a checklist without changing its text" };
|
|
67
|
+
var state = checklist.normalizeState(note.checklistState, note.text);
|
|
68
|
+
var requested = Object.create(null);
|
|
69
|
+
for (var i = 0; i < args.checks.length; i++) {
|
|
70
|
+
var found = checklist.findItem(note, args.checks[i].item);
|
|
71
|
+
if (!found) return { error: "checklist item not found in note " + note.id + "; call list_notes and use its current item keys" };
|
|
72
|
+
if (requested[found.key] !== undefined && requested[found.key] !== args.checks[i].checked) {
|
|
73
|
+
return { error: "checks names the same item more than once with different values" };
|
|
74
|
+
}
|
|
75
|
+
requested[found.key] = args.checks[i].checked;
|
|
76
|
+
state[found.key] = args.checks[i].checked;
|
|
77
|
+
}
|
|
78
|
+
changes.checklistState = state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!hasText && !hasChecks && !color && args.checklist === undefined) {
|
|
82
|
+
return { error: "nothing to change; pass text, color, checklist, or checks" };
|
|
83
|
+
}
|
|
84
|
+
// An identical request is acknowledged without bumping the version.
|
|
85
|
+
return { changes: Object.keys(changes).length ? changes : null, droppedSavedChecks: dropped };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { validateArgs: validateArgs, planUpdate: planUpdate, MAX_CHECKS: MAX_CHECKS };
|
|
@@ -36,16 +36,29 @@ function handleDefaultChange(ctx, ws, msg) {
|
|
|
36
36
|
defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
var denied = { ok: false, error: "Project settings access is not permitted" };
|
|
40
|
+
// The target is named by the client, so it is authorised here, never trusted:
|
|
41
|
+
// another project must be accessible to this account and its own access
|
|
42
|
+
// record (owner, worktree, Mate) is the one checked below.
|
|
43
|
+
var target = msg.slug && typeof msg.slug === "string" ? msg.slug : ctx.slug;
|
|
44
|
+
var access;
|
|
45
|
+
if (target === ctx.slug) {
|
|
46
|
+
if (ctx.isMate === true) { defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" }); return true; }
|
|
47
|
+
access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
48
|
+
} else {
|
|
49
|
+
if (/^mate-/.test(target) || typeof ctx.getTargetAccess !== "function" ||
|
|
50
|
+
typeof ctx.canAccessProjectSlug !== "function" || ctx.canAccessProjectSlug(ws, target) !== true) {
|
|
51
|
+
defaultChangeResult(ctx, ws, msg, denied);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
access = ctx.getTargetAccess(target);
|
|
55
|
+
if (!access || access.error || access.isMate === true) { defaultChangeResult(ctx, ws, msg, denied); return true; }
|
|
42
56
|
}
|
|
43
|
-
if (
|
|
57
|
+
if (!ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
|
|
44
58
|
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
|
|
45
59
|
return true;
|
|
46
60
|
}
|
|
47
61
|
var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
|
|
48
|
-
var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
49
62
|
if (access && access.isWorktree === true) {
|
|
50
63
|
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
|
|
51
64
|
return true;
|
|
@@ -53,16 +66,15 @@ function handleDefaultChange(ctx, ws, msg) {
|
|
|
53
66
|
var permissions = user && ctx.usersModule.getEffectivePermissions
|
|
54
67
|
? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
|
|
55
68
|
var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
|
|
56
|
-
var isAdmin = !!(user && user.role === "admin");
|
|
57
69
|
if (!user || !permissions.projectSettings || !isOwner) {
|
|
58
|
-
defaultChangeResult(ctx, ws, msg,
|
|
70
|
+
defaultChangeResult(ctx, ws, msg, denied);
|
|
59
71
|
return true;
|
|
60
72
|
}
|
|
61
73
|
if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
|
|
62
74
|
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
|
|
63
75
|
return true;
|
|
64
76
|
}
|
|
65
|
-
var result = ctx.opts.onSetProjectSessionVisibilityDefault(
|
|
77
|
+
var result = ctx.opts.onSetProjectSessionVisibilityDefault(target, msg.visibility);
|
|
66
78
|
defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
|
|
67
79
|
return true;
|
|
68
80
|
}
|
package/lib/sessions.js
CHANGED
|
@@ -166,6 +166,7 @@ function createSessionManager(opts) {
|
|
|
166
166
|
if (session.codexLinearToolCatalogVersion) metaObj.codexLinearToolCatalogVersion = session.codexLinearToolCatalogVersion;
|
|
167
167
|
if (session.codexSharedBrowserToolCatalogVersion) metaObj.codexSharedBrowserToolCatalogVersion = session.codexSharedBrowserToolCatalogVersion;
|
|
168
168
|
if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
|
|
169
|
+
if (session.codexNotesToolCatalogVersion) metaObj.codexNotesToolCatalogVersion = session.codexNotesToolCatalogVersion;
|
|
169
170
|
if (session.model) metaObj.model = session.model;
|
|
170
171
|
if (session.effort) metaObj.effort = session.effort;
|
|
171
172
|
// Persist the session's "born" mode so TUI sessions reappear after a
|
|
@@ -358,6 +359,7 @@ function createSessionManager(opts) {
|
|
|
358
359
|
session.codexLinearToolCatalogVersion = Number(m.codexLinearToolCatalogVersion || 0);
|
|
359
360
|
session.codexSharedBrowserToolCatalogVersion = Number(m.codexSharedBrowserToolCatalogVersion || 0);
|
|
360
361
|
session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
|
|
362
|
+
session.codexNotesToolCatalogVersion = Number(m.codexNotesToolCatalogVersion || 0);
|
|
361
363
|
session.model = m.model || deriveSessionModel(loaded[i].history);
|
|
362
364
|
session.effort = m.effort || null;
|
|
363
365
|
if (m.loop) session.loop = m.loop;
|
|
@@ -789,8 +791,9 @@ function createSessionManager(opts) {
|
|
|
789
791
|
return 0;
|
|
790
792
|
}
|
|
791
793
|
|
|
792
|
-
function replayHistory(session, fromIndex, targetWs, transform) {
|
|
793
|
-
var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
|
|
794
|
+
function replayHistory(session, fromIndex, targetWs, transform, snapshotSend) {
|
|
795
|
+
var _send = snapshotSend || ((targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send);
|
|
796
|
+
var sendHistory = snapshotSend && snapshotSend.history || _send;
|
|
794
797
|
var total = session.history.length;
|
|
795
798
|
if (typeof fromIndex !== "number") {
|
|
796
799
|
if (total <= INITIAL_HISTORY_SIZE) {
|
|
@@ -800,7 +803,7 @@ function createSessionManager(opts) {
|
|
|
800
803
|
}
|
|
801
804
|
}
|
|
802
805
|
|
|
803
|
-
|
|
806
|
+
sendHistory({ type: "history_meta", total: total, from: fromIndex });
|
|
804
807
|
|
|
805
808
|
for (var i = fromIndex; i < total; i++) {
|
|
806
809
|
var _item = session.history[i];
|
|
@@ -812,7 +815,7 @@ function createSessionManager(opts) {
|
|
|
812
815
|
var normalizedItem = require("./yoke/usage").clientEvent(_item, session.vendor || "claude");
|
|
813
816
|
var replayItem = transform ? transform(normalizedItem) : normalizedItem;
|
|
814
817
|
if (replayItem && typeof replayItem === "object") replayItem = Object.assign({}, replayItem, { _historyIndex: i });
|
|
815
|
-
|
|
818
|
+
sendHistory(replayItem);
|
|
816
819
|
}
|
|
817
820
|
|
|
818
821
|
// Find the last result message in the full history for accurate context data
|
|
@@ -833,16 +836,17 @@ function createSessionManager(opts) {
|
|
|
833
836
|
}
|
|
834
837
|
}
|
|
835
838
|
|
|
836
|
-
|
|
839
|
+
sendHistory({ 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") });
|
|
837
840
|
for (var vi = 0; vi < sessionViewedListeners.length; vi++) {
|
|
838
841
|
try { sessionViewedListeners[vi](session, targetWs, _send); } catch (e) {}
|
|
839
842
|
}
|
|
840
843
|
}
|
|
841
844
|
|
|
842
|
-
function switchSession(localId, targetWs, transform) {
|
|
845
|
+
function switchSession(localId, targetWs, transform, snapshotSend, snapshotGuard) {
|
|
843
846
|
var session = sessions.get(localId);
|
|
844
847
|
if (!session) return;
|
|
845
848
|
if (typeof onSessionHydrate === "function") onSessionHydrate(session);
|
|
849
|
+
if (snapshotGuard && !snapshotGuard()) return;
|
|
846
850
|
|
|
847
851
|
// Pane sockets (split-view iframes) pin their session via switch_session
|
|
848
852
|
// but must not move the project-global active pointer: reloads and
|
|
@@ -863,7 +867,7 @@ function createSessionManager(opts) {
|
|
|
863
867
|
singleUserUnread[localId] = 0;
|
|
864
868
|
|
|
865
869
|
// In multi-user mode with a specific client, only send to that client
|
|
866
|
-
var _send = (targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send;
|
|
870
|
+
var _send = snapshotSend || ((targetWs && sendTo) ? function (obj) { sendTo(targetWs, obj); } : send);
|
|
867
871
|
|
|
868
872
|
var _capsByVendor = capabilitiesByVendor || {};
|
|
869
873
|
var _sessionVendor = session.vendor || defaultVendor || "claude";
|
|
@@ -876,7 +880,7 @@ function createSessionManager(opts) {
|
|
|
876
880
|
|| [];
|
|
877
881
|
_send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
|
|
878
882
|
broadcastSessionList();
|
|
879
|
-
replayHistory(session, undefined, targetWs, transform);
|
|
883
|
+
replayHistory(session, undefined, targetWs, transform, snapshotSend);
|
|
880
884
|
|
|
881
885
|
if (session.isProcessing) {
|
|
882
886
|
_send({ type: "status", status: "processing" });
|
|
@@ -894,10 +898,15 @@ function createSessionManager(opts) {
|
|
|
894
898
|
toolInput: p.toolInput,
|
|
895
899
|
toolUseId: p.toolUseId,
|
|
896
900
|
decisionReason: p.decisionReason,
|
|
901
|
+
defaultToNo: p.defaultToNo,
|
|
902
|
+
suppressAlwaysAllowRule: p.suppressAlwaysAllowRule,
|
|
903
|
+
mateId: p.mateId || undefined,
|
|
904
|
+
vendor: p.vendor || session.vendor || undefined,
|
|
897
905
|
});
|
|
898
906
|
}
|
|
899
907
|
|
|
900
908
|
// Re-send active mention indicator so returning clients restore the mate avatar state
|
|
909
|
+
if (snapshotSend) require('./pane-pending-interactions').send(session, _send);
|
|
901
910
|
if (session._mentionInProgress && session._mentionActiveMateId) {
|
|
902
911
|
_send({ type: "mention_processing", mateId: session._mentionActiveMateId, active: true });
|
|
903
912
|
}
|
|
@@ -1030,6 +1039,14 @@ function createSessionManager(opts) {
|
|
|
1030
1039
|
}
|
|
1031
1040
|
}
|
|
1032
1041
|
|
|
1042
|
+
// An activated pane socket only accepts frames carrying its exact binding;
|
|
1043
|
+
// the transport adds it, or withholds the frame if the binding is stale.
|
|
1044
|
+
function sendLiveToSocket(ws, session, obj, data) {
|
|
1045
|
+
if (!ws._clayPaneActivation) { ws.send(data); return; }
|
|
1046
|
+
var panePayload = require("./pane-session-transport").outbound(ws, obj, session.localId);
|
|
1047
|
+
if (panePayload) ws.send(JSON.stringify(panePayload));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1033
1050
|
function doSendToSession(session, obj) {
|
|
1034
1051
|
// Send to active clients without recording to history/disk (ephemeral data)
|
|
1035
1052
|
if (sendEach) {
|
|
@@ -1038,7 +1055,7 @@ function createSessionManager(opts) {
|
|
|
1038
1055
|
if (filterFn && !filterFn(session)) return;
|
|
1039
1056
|
if (session.homeDebatePlanning === true) return;
|
|
1040
1057
|
if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
|
|
1041
|
-
ws
|
|
1058
|
+
sendLiveToSocket(ws, session, obj, data);
|
|
1042
1059
|
}
|
|
1043
1060
|
});
|
|
1044
1061
|
} else if (session.localId === activeSessionId) {
|
|
@@ -1070,7 +1087,7 @@ function createSessionManager(opts) {
|
|
|
1070
1087
|
if (filterFn && !filterFn(session)) return;
|
|
1071
1088
|
if (session.homeDebatePlanning === true) return;
|
|
1072
1089
|
if (ws._clayActiveSession === session.localId) {
|
|
1073
|
-
if (ws.readyState === 1) ws
|
|
1090
|
+
if (ws.readyState === 1) sendLiveToSocket(ws, session, outgoingObj, data);
|
|
1074
1091
|
} else if (session.isProcessing && !session._ioThrottle) {
|
|
1075
1092
|
if (!ioData) ioData = JSON.stringify({ type: "session_io", id: session.localId });
|
|
1076
1093
|
if (ws.readyState === 1) ws.send(ioData);
|