clay-server 4.0.0-beta.9 → 4.0.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/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
package/lib/notes.js
CHANGED
|
@@ -3,6 +3,7 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var config = require("./config");
|
|
5
5
|
var utils = require("./utils");
|
|
6
|
+
var lifecycle = require("./notes-lifecycle");
|
|
6
7
|
|
|
7
8
|
function createNotesManager(opts) {
|
|
8
9
|
var cwd = opts.cwd;
|
|
@@ -19,11 +20,14 @@ function createNotesManager(opts) {
|
|
|
19
20
|
return "n_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Legacy files are normalized on load, never on read. Nothing is rewritten
|
|
24
|
+
// just because a note was looked at; the projected fields reach disk the next
|
|
25
|
+
// time some other write happens.
|
|
22
26
|
function loadFromDisk() {
|
|
23
27
|
try {
|
|
24
28
|
var data = fs.readFileSync(notesFile, "utf8");
|
|
25
29
|
var parsed = JSON.parse(data);
|
|
26
|
-
return parsed.notes || [];
|
|
30
|
+
return lifecycle.normalizeAll(parsed.notes || []).notes;
|
|
27
31
|
} catch (e) {
|
|
28
32
|
return [];
|
|
29
33
|
}
|
|
@@ -56,6 +60,10 @@ function createNotesManager(opts) {
|
|
|
56
60
|
color: data.color || "purple",
|
|
57
61
|
opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
|
|
58
62
|
minimized: false,
|
|
63
|
+
// A new note is always an open attention item.
|
|
64
|
+
state: lifecycle.STATE_OPEN,
|
|
65
|
+
closedAt: null,
|
|
66
|
+
closedBy: null,
|
|
59
67
|
zIndex: notes.length + 1,
|
|
60
68
|
createdAt: now,
|
|
61
69
|
updatedAt: now,
|
|
@@ -74,13 +82,20 @@ function createNotesManager(opts) {
|
|
|
74
82
|
function update(id, changes) {
|
|
75
83
|
for (var i = 0; i < notes.length; i++) {
|
|
76
84
|
if (notes[i].id === id) {
|
|
77
|
-
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "
|
|
85
|
+
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "zIndex", "opacity"];
|
|
78
86
|
for (var j = 0; j < allowed.length; j++) {
|
|
79
87
|
var key = allowed[j];
|
|
80
88
|
if (changes[key] !== undefined) {
|
|
81
89
|
notes[i][key] = changes[key];
|
|
82
90
|
}
|
|
83
91
|
}
|
|
92
|
+
// `hidden` is the legacy spelling of the lifecycle. Route it through the
|
|
93
|
+
// transitions so state, hidden, and the close provenance can never
|
|
94
|
+
// disagree, and so an old client cannot half-apply a close.
|
|
95
|
+
if (changes.hidden !== undefined) {
|
|
96
|
+
if (changes.hidden === true) lifecycle.applyClose(notes[i], changes.actor || null, Date.now());
|
|
97
|
+
else lifecycle.applyReopen(notes[i]);
|
|
98
|
+
}
|
|
84
99
|
notes[i].updatedAt = Date.now();
|
|
85
100
|
saveToDisk();
|
|
86
101
|
return notes[i];
|
|
@@ -89,6 +104,44 @@ function createNotesManager(opts) {
|
|
|
89
104
|
return null;
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Close a note. This is what completion means: the record stays and only its
|
|
108
|
+
// place on the active canvas changes. Idempotent.
|
|
109
|
+
function close(id, actor) {
|
|
110
|
+
for (var i = 0; i < notes.length; i++) {
|
|
111
|
+
if (notes[i].id !== id) continue;
|
|
112
|
+
if (lifecycle.applyClose(notes[i], actor || null, Date.now())) {
|
|
113
|
+
notes[i].updatedAt = Date.now();
|
|
114
|
+
saveToDisk();
|
|
115
|
+
}
|
|
116
|
+
return notes[i];
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reopen a closed note. Idempotent.
|
|
122
|
+
function reopen(id) {
|
|
123
|
+
for (var i = 0; i < notes.length; i++) {
|
|
124
|
+
if (notes[i].id !== id) continue;
|
|
125
|
+
if (lifecycle.applyReopen(notes[i])) {
|
|
126
|
+
notes[i].updatedAt = Date.now();
|
|
127
|
+
saveToDisk();
|
|
128
|
+
}
|
|
129
|
+
return notes[i];
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openList() {
|
|
135
|
+
return lifecycle.openNotes(notes);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function closedList() {
|
|
139
|
+
return lifecycle.closedNotes(notes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Permanent deletion. Deliberately not reachable from any WebSocket message,
|
|
143
|
+
// MCP tool, or UI control: completion closes, it never erases. Retained only
|
|
144
|
+
// for maintenance callers that must truly discard a record.
|
|
92
145
|
function remove(id) {
|
|
93
146
|
for (var i = 0; i < notes.length; i++) {
|
|
94
147
|
if (notes[i].id === id) {
|
|
@@ -123,7 +176,7 @@ function createNotesManager(opts) {
|
|
|
123
176
|
function getActiveNotesText() {
|
|
124
177
|
var active = [];
|
|
125
178
|
for (var i = 0; i < notes.length; i++) {
|
|
126
|
-
if (
|
|
179
|
+
if (lifecycle.isOpen(notes[i]) && notes[i].text) active.push(notes[i]);
|
|
127
180
|
}
|
|
128
181
|
if (active.length === 0) return "";
|
|
129
182
|
var lines = [];
|
|
@@ -137,8 +190,12 @@ function createNotesManager(opts) {
|
|
|
137
190
|
|
|
138
191
|
return {
|
|
139
192
|
list: list,
|
|
193
|
+
openList: openList,
|
|
194
|
+
closedList: closedList,
|
|
140
195
|
create: create,
|
|
141
196
|
update: update,
|
|
197
|
+
close: close,
|
|
198
|
+
reopen: reopen,
|
|
142
199
|
remove: remove,
|
|
143
200
|
bringToFront: bringToFront,
|
|
144
201
|
getActiveNotesText: getActiveNotesText,
|
package/lib/os-users.js
CHANGED
|
@@ -161,7 +161,9 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
161
161
|
"var fs = require('fs');",
|
|
162
162
|
"var f = " + JSON.stringify(args.file) + ";",
|
|
163
163
|
"var content = " + JSON.stringify(args.content || "") + ";",
|
|
164
|
-
"
|
|
164
|
+
"var flag = " + JSON.stringify(args.flag === "wx" ? "wx" : "w") + ";",
|
|
165
|
+
"var mode = " + JSON.stringify(typeof args.mode === "number" ? args.mode : 0o666) + ";",
|
|
166
|
+
"fs.writeFileSync(f, content, { encoding: 'utf8', flag: flag, mode: mode });",
|
|
165
167
|
"process.stdout.write(JSON.stringify({ ok: true }));",
|
|
166
168
|
].join(" ");
|
|
167
169
|
} else if (op === "mkdir") {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// Bounded progressive-disclosure catalog for Capsules visible to a Mate.
|
|
2
|
+
//
|
|
3
|
+
// Skills are gated by Display: a Capsule the human cannot operate by hand is a
|
|
4
|
+
// Capsule no Mate may reach either, so an entry appears here only while its
|
|
5
|
+
// declarative Display floor validates. The gate reads the Capsule's own tree,
|
|
6
|
+
// carried alongside each manifest, and fails closed on a scan error, a missing
|
|
7
|
+
// ID, or a missing or invalid Display.
|
|
8
|
+
|
|
9
|
+
var capsuleFloor = require("./capsule-display-floor");
|
|
2
10
|
|
|
3
11
|
var MAX_NAME_LENGTH = 80;
|
|
4
12
|
var MAX_DESCRIPTION_LENGTH = 240;
|
|
@@ -35,7 +43,7 @@ function discoveryEntry(manifest) {
|
|
|
35
43
|
function buildCapsuleCatalogPrompt(manifests) {
|
|
36
44
|
if (!Array.isArray(manifests) || manifests.length === 0) return "";
|
|
37
45
|
var sorted = manifests.filter(function (manifest) {
|
|
38
|
-
return
|
|
46
|
+
return capsuleFloor.hasUsableFloor(manifest);
|
|
39
47
|
}).slice().sort(function (a, b) {
|
|
40
48
|
var first = String(a.id);
|
|
41
49
|
var second = String(b.id);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Delivers a Capsule turn prompt into this Mate project.
|
|
2
|
+
//
|
|
3
|
+
// A Capsule game against a Mate needs the Mate to actually take its turns:
|
|
4
|
+
// when Logic passes the turn to the mate seat, this module wakes the Mate
|
|
5
|
+
// with a prompt telling it to snapshot the game and act. One session hosts
|
|
6
|
+
// the whole game (found again by session.capsuleGame.toolId), so the user
|
|
7
|
+
// watches a single running conversation of the Mate playing rather than a
|
|
8
|
+
// new session per turn. The prompt is recorded as an internal user message,
|
|
9
|
+
// the same shape the debate engine uses, so the transcript shows the nudge
|
|
10
|
+
// without pretending the human typed it.
|
|
11
|
+
//
|
|
12
|
+
// This module only carries words to the Mate. Every actual game mutation the
|
|
13
|
+
// Mate then makes goes through the same clay_tool_act pipeline as always.
|
|
14
|
+
|
|
15
|
+
function attachCapsuleTurn(ctx) {
|
|
16
|
+
var sm = ctx.sm;
|
|
17
|
+
|
|
18
|
+
function ownsSession(session, principal) {
|
|
19
|
+
if (ctx.isMultiUser()) return !!principal.userId && session.ownerId === principal.userId;
|
|
20
|
+
return !session.ownerId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findGameSession(principal, toolId) {
|
|
24
|
+
var found = null;
|
|
25
|
+
sm.sessions.forEach(function (session) {
|
|
26
|
+
if (found || !session || session.hidden || session.destroying) return;
|
|
27
|
+
if (!session.capsuleGame || session.capsuleGame.toolId !== toolId) return;
|
|
28
|
+
if (!ownsSession(session, principal)) return;
|
|
29
|
+
found = session;
|
|
30
|
+
});
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createGameSession(principal, options) {
|
|
35
|
+
var selection = await ctx.resolveModel(principal);
|
|
36
|
+
if (!selection || selection.status !== "ready" || !selection.vendor || !selection.model) {
|
|
37
|
+
throw new Error(selection && selection.error ? selection.error : "No configured model is available for the Mate.");
|
|
38
|
+
}
|
|
39
|
+
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
40
|
+
var session = create.call(sm, {
|
|
41
|
+
ownerId: principal.userId || null,
|
|
42
|
+
vendor: selection.vendor,
|
|
43
|
+
model: selection.model,
|
|
44
|
+
});
|
|
45
|
+
session.title = (options.toolName || options.toolId) + " game";
|
|
46
|
+
session.capsuleGame = { toolId: options.toolId };
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function deliverCapsuleTurn(principal, options) {
|
|
51
|
+
if (!principal || !options || !options.toolId || !options.text) {
|
|
52
|
+
throw new Error("A bound owner, toolId, and turn text are required.");
|
|
53
|
+
}
|
|
54
|
+
var sdk = ctx.getSdk();
|
|
55
|
+
if (!sdk || typeof sdk.startQuery !== "function") throw new Error("Mate project runtime is unavailable.");
|
|
56
|
+
var session = findGameSession(principal, options.toolId);
|
|
57
|
+
var created = false;
|
|
58
|
+
if (!session) {
|
|
59
|
+
session = await createGameSession(principal, options);
|
|
60
|
+
created = true;
|
|
61
|
+
}
|
|
62
|
+
// Recorded as a dedicated event, not a user message: the transcript shows
|
|
63
|
+
// a short system note (see historyToHomeChat) instead of pretending the
|
|
64
|
+
// human typed the delivery prompt. The full prompt still reaches the
|
|
65
|
+
// model through the query below.
|
|
66
|
+
sm.sendAndRecord(session, {
|
|
67
|
+
type: "capsule_turn",
|
|
68
|
+
text: options.text,
|
|
69
|
+
toolId: options.toolId,
|
|
70
|
+
toolName: options.toolName || options.toolId,
|
|
71
|
+
kind: options.kind || "turn",
|
|
72
|
+
});
|
|
73
|
+
session.isProcessing = true;
|
|
74
|
+
session.lastActivity = Date.now();
|
|
75
|
+
sm.broadcastSessionList();
|
|
76
|
+
try {
|
|
77
|
+
var delivered = typeof sdk.pushMessage === "function" ? sdk.pushMessage(session, options.text) : false;
|
|
78
|
+
if (!delivered) await Promise.resolve(sdk.startQuery(session, options.text, undefined, ctx.getLinuxUserForSession(session)));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
session.isProcessing = false;
|
|
81
|
+
sm.sendAndRecord(session, { type: "error", text: "Capsule turn could not start: " + (error && error.message ? error.message : String(error)) });
|
|
82
|
+
sm.broadcastSessionList();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
// The reference shape the home chat surface opens sessions by.
|
|
86
|
+
return {
|
|
87
|
+
session: session,
|
|
88
|
+
created: created,
|
|
89
|
+
reference: (typeof session.cliSessionId === "string" && session.cliSessionId) || "local:" + session.localId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { deliverCapsuleTurn: deliverCapsuleTurn };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { attachCapsuleTurn: attachCapsuleTurn };
|
|
@@ -233,7 +233,7 @@ function attachConnection(ctx) {
|
|
|
233
233
|
// so vendor badges showed Claude until the next broadcast).
|
|
234
234
|
sendTo(ws, {
|
|
235
235
|
type: "session_list",
|
|
236
|
-
sessions:
|
|
236
|
+
sessions: sm.mapSessionsForClient(allSessions),
|
|
237
237
|
});
|
|
238
238
|
if (_splitGroups) _splitGroups.sendConnectionState(ws);
|
|
239
239
|
|
|
@@ -342,6 +342,7 @@ function attachConnection(ctx) {
|
|
|
342
342
|
tm.detachAll(ws);
|
|
343
343
|
clients.delete(ws);
|
|
344
344
|
stopFileWatch(ws);
|
|
345
|
+
stopAllDirWatches(ws);
|
|
345
346
|
if (clients.size === 0) {
|
|
346
347
|
stopFileWatch();
|
|
347
348
|
stopAllDirWatches();
|
|
@@ -350,9 +351,22 @@ function attachConnection(ctx) {
|
|
|
350
351
|
broadcastPresence();
|
|
351
352
|
}
|
|
352
353
|
|
|
354
|
+
// Keep-alive for the project socket, mirroring the `/ws` global handler.
|
|
355
|
+
//
|
|
356
|
+
// A reverse proxy resets its upstream read timeout on data flowing back from
|
|
357
|
+
// the server, so a client ping is only useful if the server answers it. This
|
|
358
|
+
// reads nothing, mutates nothing, and reveals nothing about the connection:
|
|
359
|
+
// it is a fixed reply on a fixed message type.
|
|
360
|
+
function handleKeepaliveMessage(ws, msg) {
|
|
361
|
+
if (!msg || typeof msg !== "object" || msg.type !== "ping") return false;
|
|
362
|
+
sendTo(ws, { type: "pong" });
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
353
366
|
return {
|
|
354
367
|
handleConnection: handleConnection,
|
|
355
368
|
handleDisconnection: handleDisconnection,
|
|
369
|
+
handleKeepaliveMessage: handleKeepaliveMessage,
|
|
356
370
|
};
|
|
357
371
|
}
|
|
358
372
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
var path = require("path");
|
|
2
|
+
var execFileSync = require("child_process").execFileSync;
|
|
3
|
+
var wrapSpawnAsUser = require("./os-users").wrapSpawnAsUser;
|
|
4
|
+
|
|
5
|
+
function attachFileHistory(ctx, access) {
|
|
6
|
+
var cwd = ctx.cwd;
|
|
7
|
+
var sm = ctx.sm;
|
|
8
|
+
var sendTo = ctx.sendTo;
|
|
9
|
+
|
|
10
|
+
function runGit(ws, args, options) {
|
|
11
|
+
options = Object.assign({}, options, { stdio: ["ignore", "pipe", "pipe"] });
|
|
12
|
+
var identity = access.osIdentity(ws);
|
|
13
|
+
if (identity) options = Object.assign({}, options, { uid: identity.uid, gid: identity.gid });
|
|
14
|
+
var command = wrapSpawnAsUser("git", args, options);
|
|
15
|
+
return execFileSync(command.command, command.args, command.options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handleFileHistory(ws, msg) {
|
|
19
|
+
if (!/^fs_(file_history|git_diff|file_at)$/.test(msg.type)) return false;
|
|
20
|
+
if (typeof msg.path !== "string" || !msg.path || path.isAbsolute(msg.path) ||
|
|
21
|
+
path.relative(cwd, path.resolve(cwd, msg.path)).split(path.sep).indexOf("..") !== -1 ||
|
|
22
|
+
(msg.hash && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash)) ||
|
|
23
|
+
(msg.hash2 && !/^[a-fA-F0-9]{4,64}$/.test(msg.hash2))) {
|
|
24
|
+
sendTo(ws, { type: msg.type + "_result", path: msg.path, error: "Invalid file history request" });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// --- File edit history ---
|
|
28
|
+
if (msg.type === "fs_file_history") {
|
|
29
|
+
var histPath = msg.path;
|
|
30
|
+
if (!histPath) {
|
|
31
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: [] });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
var absHistPath = path.resolve(cwd, histPath);
|
|
35
|
+
var entries = [];
|
|
36
|
+
|
|
37
|
+
// Collect session edits
|
|
38
|
+
sm.sessions.forEach(function (session) {
|
|
39
|
+
if (!access.canReadSession(ws, session)) return;
|
|
40
|
+
var sessionLocalId = session.localId;
|
|
41
|
+
var sessionTitle = session.title || "Untitled";
|
|
42
|
+
var histLen = session.history.length || 1;
|
|
43
|
+
|
|
44
|
+
for (var hi = 0; hi < session.history.length; hi++) {
|
|
45
|
+
var entry = session.history[hi];
|
|
46
|
+
if (entry.type !== "tool_executing") continue;
|
|
47
|
+
if (entry.name !== "Edit" && entry.name !== "Write") continue;
|
|
48
|
+
if (!entry.input || !entry.input.file_path) continue;
|
|
49
|
+
if (entry.input.file_path !== absHistPath) continue;
|
|
50
|
+
|
|
51
|
+
// Find parent assistant UUID + message snippet by scanning backwards
|
|
52
|
+
var assistantUuid = null;
|
|
53
|
+
var uuidIndex = -1;
|
|
54
|
+
for (var hj = hi - 1; hj >= 0; hj--) {
|
|
55
|
+
if (session.history[hj].type === "message_uuid" && session.history[hj].messageType === "assistant") {
|
|
56
|
+
assistantUuid = session.history[hj].uuid;
|
|
57
|
+
uuidIndex = hj;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find user prompt by scanning backwards from the assistant uuid
|
|
63
|
+
var messageSnippet = "";
|
|
64
|
+
var searchFrom = uuidIndex >= 0 ? uuidIndex : hi;
|
|
65
|
+
for (var hk = searchFrom - 1; hk >= 0; hk--) {
|
|
66
|
+
if (session.history[hk].type === "user_message" && session.history[hk].text) {
|
|
67
|
+
messageSnippet = session.history[hk].text.trim().substring(0, 100);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Collect Claude's explanation: scan backwards from tool_executing
|
|
73
|
+
// to find the nearest delta text block (skipping tool_start).
|
|
74
|
+
// If no delta found immediately before this tool, scan past
|
|
75
|
+
// intervening tool blocks to find the last delta text within
|
|
76
|
+
// the same assistant turn.
|
|
77
|
+
var assistantSnippet = "";
|
|
78
|
+
var deltaChunks = [];
|
|
79
|
+
for (var hd = hi - 1; hd >= 0; hd--) {
|
|
80
|
+
var hEntry = session.history[hd];
|
|
81
|
+
if (hEntry.type === "tool_start") continue;
|
|
82
|
+
if (hEntry.type === "delta" && hEntry.text) {
|
|
83
|
+
deltaChunks.unshift(hEntry.text);
|
|
84
|
+
} else {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (deltaChunks.length === 0) {
|
|
89
|
+
// No delta immediately before; scan past tool blocks
|
|
90
|
+
// to find the nearest preceding delta in the same turn
|
|
91
|
+
for (var hd2 = hi - 1; hd2 >= 0; hd2--) {
|
|
92
|
+
var hEntry2 = session.history[hd2];
|
|
93
|
+
if (hEntry2.type === "tool_start" || hEntry2.type === "tool_executing" || hEntry2.type === "tool_result") continue;
|
|
94
|
+
if (hEntry2.type === "delta" && hEntry2.text) {
|
|
95
|
+
// Found a delta before an earlier tool in the same turn.
|
|
96
|
+
// Collect this contiguous block of deltas.
|
|
97
|
+
for (var hd3 = hd2; hd3 >= 0; hd3--) {
|
|
98
|
+
var hEntry3 = session.history[hd3];
|
|
99
|
+
if (hEntry3.type === "tool_start") continue;
|
|
100
|
+
if (hEntry3.type === "delta" && hEntry3.text) {
|
|
101
|
+
deltaChunks.unshift(hEntry3.text);
|
|
102
|
+
} else {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
} else {
|
|
108
|
+
// Hit message_uuid, user_message, etc. Stop.
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
assistantSnippet = deltaChunks.join("").trim().substring(0, 150);
|
|
114
|
+
|
|
115
|
+
// Approximate timestamp: interpolate between session creation and last activity
|
|
116
|
+
var tStart = session.createdAt || 0;
|
|
117
|
+
var tEnd = session.lastActivity || tStart;
|
|
118
|
+
var ts = tStart + Math.floor((hi / histLen) * (tEnd - tStart));
|
|
119
|
+
|
|
120
|
+
var editRecord = {
|
|
121
|
+
source: "session",
|
|
122
|
+
timestamp: ts,
|
|
123
|
+
sessionLocalId: sessionLocalId,
|
|
124
|
+
sessionTitle: sessionTitle,
|
|
125
|
+
assistantUuid: assistantUuid,
|
|
126
|
+
toolId: entry.id,
|
|
127
|
+
messageSnippet: messageSnippet,
|
|
128
|
+
assistantSnippet: assistantSnippet,
|
|
129
|
+
toolName: entry.name,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (entry.name === "Edit") {
|
|
133
|
+
editRecord.old_string = entry.input.old_string || "";
|
|
134
|
+
editRecord.new_string = entry.input.new_string || "";
|
|
135
|
+
} else {
|
|
136
|
+
editRecord.isFullWrite = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
entries.push(editRecord);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Collect git commits
|
|
144
|
+
try {
|
|
145
|
+
var gitLog = runGit(ws,
|
|
146
|
+
["log", "--format=%H|%at|%an|%s", "--follow", "--", histPath],
|
|
147
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 }
|
|
148
|
+
);
|
|
149
|
+
var gitLines = gitLog.trim().split("\n");
|
|
150
|
+
for (var gi = 0; gi < gitLines.length; gi++) {
|
|
151
|
+
if (!gitLines[gi]) continue;
|
|
152
|
+
var parts = gitLines[gi].split("|");
|
|
153
|
+
if (parts.length < 4) continue;
|
|
154
|
+
entries.push({
|
|
155
|
+
source: "git",
|
|
156
|
+
hash: parts[0],
|
|
157
|
+
timestamp: parseInt(parts[1], 10) * 1000,
|
|
158
|
+
author: parts[2],
|
|
159
|
+
message: parts.slice(3).join("|"),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Not a git repo or file not tracked, that is fine
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by timestamp descending (newest first)
|
|
167
|
+
entries.sort(function (a, b) { return b.timestamp - a.timestamp; });
|
|
168
|
+
|
|
169
|
+
sendTo(ws, { type: "fs_file_history_result", path: histPath, entries: entries });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Git diff for file history ---
|
|
174
|
+
if (msg.type === "fs_git_diff") {
|
|
175
|
+
var diffPath = msg.path;
|
|
176
|
+
var hash = msg.hash;
|
|
177
|
+
var hash2 = msg.hash2 || null;
|
|
178
|
+
if (!diffPath || !hash) {
|
|
179
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, path: diffPath, diff: "", error: "Missing params" });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
var diff;
|
|
184
|
+
if (hash2) {
|
|
185
|
+
diff = runGit(ws, ["diff", hash, hash2, "--", diffPath],
|
|
186
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
187
|
+
} else {
|
|
188
|
+
diff = runGit(ws, ["show", hash, "--format=", "--", diffPath],
|
|
189
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
190
|
+
}
|
|
191
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: diff || "" });
|
|
192
|
+
} catch (e) {
|
|
193
|
+
sendTo(ws, { type: "fs_git_diff_result", hash: hash, hash2: hash2, path: diffPath, diff: "", error: e.message });
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- File content at a git commit ---
|
|
199
|
+
if (msg.type === "fs_file_at") {
|
|
200
|
+
var atPath = msg.path;
|
|
201
|
+
var atHash = msg.hash;
|
|
202
|
+
if (!atPath || !atHash) {
|
|
203
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: "Missing params" });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// Convert to repo-relative path (git show requires hash:relative/path)
|
|
208
|
+
var atAbsPath = path.resolve(cwd, atPath);
|
|
209
|
+
var atRelPath = path.relative(cwd, atAbsPath);
|
|
210
|
+
var content = runGit(ws, ["show", atHash + ":" + atRelPath],
|
|
211
|
+
{ cwd: cwd, encoding: "utf8", timeout: 5000 });
|
|
212
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: content });
|
|
213
|
+
} catch (e) {
|
|
214
|
+
sendTo(ws, { type: "fs_file_at_result", hash: atHash, path: atPath, content: "", error: e.message });
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return { handleFileHistory: handleFileHistory };
|
|
222
|
+
}
|
|
223
|
+
module.exports = { attachFileHistory: attachFileHistory };
|