clay-server 2.40.1-beta.1 → 2.41.0-beta.1
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/daemon.js +1 -1
- package/lib/project-sessions.js +38 -0
- package/lib/public/index.html +3 -0
- package/lib/public/modules/app-messages.js +8 -1
- package/lib/public/modules/app-notifications.js +91 -2
- package/lib/public/modules/header-tui-font.js +1 -0
- package/lib/public/modules/session-tui-view.js +10 -1
- package/lib/public/modules/sidebar-projects.js +11 -3
- package/lib/public/modules/terminal-prefs.js +51 -4
- package/lib/public/modules/tui-grab.js +723 -0
- package/lib/server-global-ws.js +189 -0
- package/lib/server-settings.js +2 -2
- package/lib/server.js +40 -7
- package/lib/tui-transcript-index.js +125 -0
- package/lib/users-preferences.js +1 -1
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// --- Global WebSocket handler (no project context) ---
|
|
2
|
+
//
|
|
3
|
+
// Lets a logged-in user with no projects yet (or no accessible projects)
|
|
4
|
+
// load the regular app shell and create / add / clone their first project,
|
|
5
|
+
// instead of being trapped on a static "no projects" page.
|
|
6
|
+
//
|
|
7
|
+
// Bound to the slug-less `/ws` endpoint by lib/server.js. Only handles the
|
|
8
|
+
// small set of messages needed to bootstrap into a project context:
|
|
9
|
+
// - ping -> pong keep-alive
|
|
10
|
+
// - browse_dir -> directory picker for the add-project modal
|
|
11
|
+
// - add_project -> register an existing directory
|
|
12
|
+
// - create_project -> make a new empty project
|
|
13
|
+
// - clone_project -> clone a git repo into a new project
|
|
14
|
+
//
|
|
15
|
+
// All four mirror the same handlers in lib/project-sessions.js, but with
|
|
16
|
+
// the per-project `ctx` dropped since none of them need it.
|
|
17
|
+
|
|
18
|
+
var fs = require("fs");
|
|
19
|
+
var path = require("path");
|
|
20
|
+
var config = require("./config");
|
|
21
|
+
|
|
22
|
+
// Mirrored from lib/project.js — keep in sync if entries change there.
|
|
23
|
+
var IGNORED_DIRS = new Set([
|
|
24
|
+
"node_modules", ".git", ".next", "__pycache__",
|
|
25
|
+
".cache", "dist", "build", ".clay", ".claude-relay",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function sendTo(ws, msg) {
|
|
29
|
+
if (ws && ws.readyState === 1) {
|
|
30
|
+
try { ws.send(JSON.stringify(msg)); } catch (e) {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function attachGlobalWs(opts) {
|
|
35
|
+
var osUsers = opts.osUsers || false;
|
|
36
|
+
var usersModule = opts.usersModule;
|
|
37
|
+
var onAddProject = opts.onAddProject;
|
|
38
|
+
var onCreateProject = opts.onCreateProject;
|
|
39
|
+
var onCloneProject = opts.onCloneProject;
|
|
40
|
+
|
|
41
|
+
function handleMessage(ws, msg) {
|
|
42
|
+
if (!msg || typeof msg !== "object") return;
|
|
43
|
+
|
|
44
|
+
if (msg.type === "ping") {
|
|
45
|
+
sendTo(ws, { type: "pong" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Directory picker for the add-project modal ---
|
|
50
|
+
if (msg.type === "browse_dir") {
|
|
51
|
+
var rawPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
52
|
+
var absTarget = path.resolve(rawPath);
|
|
53
|
+
// Multi-user mode: non-admins can only browse their home directory.
|
|
54
|
+
if (osUsers && ws._clayUser && ws._clayUser.role !== "admin") {
|
|
55
|
+
var browseHome = ws._clayUser.linuxUser ? "/home/" + ws._clayUser.linuxUser : null;
|
|
56
|
+
if (!browseHome || (absTarget !== browseHome && (absTarget + "/").indexOf(browseHome + "/") !== 0)) {
|
|
57
|
+
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: "Access restricted to your home directory" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var parentDir, prefix;
|
|
62
|
+
try {
|
|
63
|
+
var stat = fs.statSync(absTarget);
|
|
64
|
+
if (stat.isDirectory()) {
|
|
65
|
+
// Existing directory -- list its children.
|
|
66
|
+
parentDir = absTarget;
|
|
67
|
+
prefix = "";
|
|
68
|
+
} else {
|
|
69
|
+
parentDir = path.dirname(absTarget);
|
|
70
|
+
prefix = path.basename(absTarget).toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Doesn't exist -- list parent and filter by typed prefix.
|
|
74
|
+
parentDir = path.dirname(absTarget);
|
|
75
|
+
prefix = path.basename(absTarget).toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
79
|
+
var dirEntries = [];
|
|
80
|
+
for (var di = 0; di < dirItems.length; di++) {
|
|
81
|
+
var d = dirItems[di];
|
|
82
|
+
if (!d.isDirectory()) continue;
|
|
83
|
+
if (d.name.charAt(0) === ".") continue;
|
|
84
|
+
if (IGNORED_DIRS.has(d.name)) continue;
|
|
85
|
+
if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
|
|
86
|
+
dirEntries.push({ name: d.name, path: path.join(parentDir, d.name) });
|
|
87
|
+
}
|
|
88
|
+
dirEntries.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
89
|
+
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: dirEntries });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: e.message });
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Register an existing directory as a project ---
|
|
97
|
+
if (msg.type === "add_project") {
|
|
98
|
+
var addPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
99
|
+
var addAbs = path.resolve(addPath);
|
|
100
|
+
if (osUsers && ws._clayUser && ws._clayUser.role !== "admin") {
|
|
101
|
+
if (!ws._clayUser.linuxUser) {
|
|
102
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "No Linux user assigned" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
var userHome = "/home/" + ws._clayUser.linuxUser;
|
|
106
|
+
if (addAbs !== userHome && (addAbs + "/").indexOf(userHome + "/") !== 0) {
|
|
107
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Path not allowed. You can only add directories under " + userHome });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
var addStat = fs.statSync(addAbs);
|
|
113
|
+
if (!addStat.isDirectory()) {
|
|
114
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Not a directory" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Directory not found" });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (typeof onAddProject === "function") {
|
|
122
|
+
var result = onAddProject(addAbs, ws._clayUser);
|
|
123
|
+
sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
|
|
124
|
+
} else {
|
|
125
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Permission gate shared by create_project and clone_project.
|
|
131
|
+
if (msg.type === "create_project" || msg.type === "clone_project") {
|
|
132
|
+
if (ws._clayUser && usersModule) {
|
|
133
|
+
var cpPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
134
|
+
if (!cpPerms.createProject) {
|
|
135
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "You do not have permission to create projects" });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (msg.type === "create_project") {
|
|
142
|
+
var createName = (msg.name || "").trim();
|
|
143
|
+
if (!createName || !/^[a-zA-Z0-9_-]+$/.test(createName)) {
|
|
144
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid name. Use only letters, numbers, dashes, and underscores." });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof onCreateProject === "function") {
|
|
148
|
+
var createResult = onCreateProject(createName, ws._clayUser);
|
|
149
|
+
sendTo(ws, { type: "add_project_result", ok: createResult.ok, slug: createResult.slug, error: createResult.error });
|
|
150
|
+
} else {
|
|
151
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (msg.type === "clone_project") {
|
|
157
|
+
var cloneUrl = (msg.url || "").trim();
|
|
158
|
+
if (!cloneUrl || (!/^https?:\/\//.test(cloneUrl) && !/^git@/.test(cloneUrl))) {
|
|
159
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Invalid URL. Use https:// or git@ format." });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
sendTo(ws, { type: "clone_project_progress", status: "cloning" });
|
|
163
|
+
if (typeof onCloneProject === "function") {
|
|
164
|
+
onCloneProject(cloneUrl, ws._clayUser, function (cloneResult) {
|
|
165
|
+
sendTo(ws, { type: "add_project_result", ok: cloneResult.ok, slug: cloneResult.slug, error: cloneResult.error });
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleConnection(ws, wsUser) {
|
|
175
|
+
ws._clayUser = wsUser || null;
|
|
176
|
+
ws.on("message", function (data) {
|
|
177
|
+
var msg;
|
|
178
|
+
try { msg = JSON.parse(data); } catch (e) { return; }
|
|
179
|
+
handleMessage(ws, msg);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
handleConnection: handleConnection,
|
|
185
|
+
handleMessage: handleMessage,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { attachGlobalWs: attachGlobalWs };
|
package/lib/server-settings.js
CHANGED
|
@@ -56,10 +56,10 @@ function attachSettings(ctx) {
|
|
|
56
56
|
if (dc.terminalFont && typeof dc.terminalFont === "object") {
|
|
57
57
|
profile.terminalFont = {
|
|
58
58
|
family: dc.terminalFont.family || "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
|
|
59
|
-
size: typeof dc.terminalFont.size === "number" ? dc.terminalFont.size :
|
|
59
|
+
size: typeof dc.terminalFont.size === "number" ? dc.terminalFont.size : 14,
|
|
60
60
|
};
|
|
61
61
|
} else {
|
|
62
|
-
profile.terminalFont = { family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace", size:
|
|
62
|
+
profile.terminalFont = { family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace", size: 14 };
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
// Check if custom avatar file exists
|
package/lib/server.js
CHANGED
|
@@ -18,6 +18,7 @@ var serverAdmin = require("./server-admin");
|
|
|
18
18
|
var serverSettings = require("./server-settings");
|
|
19
19
|
var serverPalette = require("./server-palette");
|
|
20
20
|
var serverEmail = require("./server-email");
|
|
21
|
+
var serverGlobalWs = require("./server-global-ws");
|
|
21
22
|
|
|
22
23
|
var { CONFIG_DIR } = require("./config");
|
|
23
24
|
var { provisionLinuxUser } = require("./os-users");
|
|
@@ -684,14 +685,23 @@ function createServer(opts) {
|
|
|
684
685
|
return;
|
|
685
686
|
}
|
|
686
687
|
}
|
|
687
|
-
// No accessible projects
|
|
688
|
+
// No accessible projects. Users who can create one fall through to
|
|
689
|
+
// the regular app shell (index.html) — the slug-less /ws and the
|
|
690
|
+
// sidebar's + button take over from there. The static "ask an admin"
|
|
691
|
+
// page is only for multi-user users who genuinely can't do anything
|
|
692
|
+
// (no createProject permission and no projects shared with them).
|
|
688
693
|
if (users.isMultiUser()) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
694
|
+
var rootUser = getMultiUserFromReq(req);
|
|
695
|
+
var rootPerms = rootUser ? users.getEffectivePermissions(rootUser, osUsers) : null;
|
|
696
|
+
if (!rootPerms || !rootPerms.createProject) {
|
|
697
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
698
|
+
res.end(pages.noProjectsPageHtml());
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
692
701
|
}
|
|
693
|
-
|
|
694
|
-
res.
|
|
702
|
+
if (serveStatic("/index.html", res)) return;
|
|
703
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
704
|
+
res.end("App shell missing.");
|
|
695
705
|
return;
|
|
696
706
|
}
|
|
697
707
|
|
|
@@ -868,6 +878,18 @@ function createServer(opts) {
|
|
|
868
878
|
// --- WebSocket ---
|
|
869
879
|
var wss = new WebSocketServer({ noServer: true });
|
|
870
880
|
|
|
881
|
+
// Slug-less /ws handler: lets a user with no projects yet load the regular
|
|
882
|
+
// app shell and create/add/clone their first one. Only knows about the
|
|
883
|
+
// small set of bootstrap messages (browse_dir, add_project, create_project,
|
|
884
|
+
// clone_project, ping).
|
|
885
|
+
var globalWs = serverGlobalWs.attachGlobalWs({
|
|
886
|
+
osUsers: osUsers,
|
|
887
|
+
usersModule: users,
|
|
888
|
+
onAddProject: onAddProject,
|
|
889
|
+
onCreateProject: onCreateProject,
|
|
890
|
+
onCloneProject: onCloneProject,
|
|
891
|
+
});
|
|
892
|
+
|
|
871
893
|
server.on("upgrade", function (req, socket, head) {
|
|
872
894
|
// Origin validation (CSRF prevention)
|
|
873
895
|
var origin = req.headers.origin;
|
|
@@ -907,7 +929,18 @@ function createServer(opts) {
|
|
|
907
929
|
// Extract slug from WS URL: /p/{slug}/ws
|
|
908
930
|
var wsSlug = extractSlug(req.url);
|
|
909
931
|
if (!wsSlug) {
|
|
910
|
-
|
|
932
|
+
// Slug-less /ws: bootstrap channel for a client that hasn't entered
|
|
933
|
+
// any project yet (no projects exist, or none accessible to this user
|
|
934
|
+
// but they can still create one). Anything other than exactly /ws is
|
|
935
|
+
// rejected.
|
|
936
|
+
if (req.url !== "/ws") {
|
|
937
|
+
socket.destroy();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
var globalUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
941
|
+
wss.handleUpgrade(req, socket, head, function (ws) {
|
|
942
|
+
globalWs.handleConnection(ws, globalUser);
|
|
943
|
+
});
|
|
911
944
|
return;
|
|
912
945
|
}
|
|
913
946
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// tui-transcript-index.js
|
|
2
|
+
//
|
|
3
|
+
// Build a per-session index of assistant text messages from a Claude Code
|
|
4
|
+
// TUI transcript (~/.claude/projects/{encoded-cwd}/{cliSessionId}.jsonl).
|
|
5
|
+
//
|
|
6
|
+
// The TUI message-grab feature uses this index on the client to map a
|
|
7
|
+
// hovered xterm region back to its original markdown source. We only
|
|
8
|
+
// extract assistant text blocks. Tool calls and tool results render in
|
|
9
|
+
// the terminal in a shape that's quite different from their JSONL form,
|
|
10
|
+
// so matching them would be noisy and not particularly useful.
|
|
11
|
+
//
|
|
12
|
+
// Codex sessions don't write transcripts at all; callers should skip
|
|
13
|
+
// indexing for non-claude vendors.
|
|
14
|
+
|
|
15
|
+
var fs = require("fs");
|
|
16
|
+
var path = require("path");
|
|
17
|
+
var utils = require("./utils");
|
|
18
|
+
var { REAL_HOME } = require("./config");
|
|
19
|
+
|
|
20
|
+
var encodeCwd = utils.encodeCwd;
|
|
21
|
+
|
|
22
|
+
// Min length of a normalized assistant message to bother indexing.
|
|
23
|
+
// Anything shorter ("ok", "sure", "yes") would generate too many false
|
|
24
|
+
// positives when matched against a hovered terminal block.
|
|
25
|
+
var MIN_MATCH_LEN = 20;
|
|
26
|
+
|
|
27
|
+
function transcriptFilePath(cwd, cliSessionId) {
|
|
28
|
+
if (!cwd || !cliSessionId) return null;
|
|
29
|
+
return path.join(REAL_HOME, ".claude", "projects", encodeCwd(cwd), cliSessionId + ".jsonl");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// matchKey is just whitespace-normalized raw markdown. We intentionally
|
|
33
|
+
// do NOT strip markdown decoration here.
|
|
34
|
+
//
|
|
35
|
+
// Reasoning: TUI rendering drops markdown markers (**bold** -> bold,
|
|
36
|
+
// `code` -> code, [text](url) -> text, etc.) and the visible text is
|
|
37
|
+
// therefore always a substring of the JSONL source's character stream.
|
|
38
|
+
// If we strip server-side we risk getting the rules wrong on corner
|
|
39
|
+
// cases (double-backtick code spans, list-bullet rendering quirks,
|
|
40
|
+
// nested formatting) and the resulting matchKey diverges from both
|
|
41
|
+
// sides at once. Leaving the source alone and letting the client do a
|
|
42
|
+
// multi-offset substring probe against block text is more robust and
|
|
43
|
+
// much less code.
|
|
44
|
+
function normalizeForMatch(s) {
|
|
45
|
+
if (!s) return "";
|
|
46
|
+
return s.replace(/\s+/g, " ").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Stable per-message id. Claude's JSONL usually carries one of these but
|
|
50
|
+
// fall back to the parsed line index so the client always has a handle.
|
|
51
|
+
function messageId(obj, index) {
|
|
52
|
+
if (obj && obj.uuid) return String(obj.uuid);
|
|
53
|
+
if (obj && obj.message && obj.message.id) return String(obj.message.id);
|
|
54
|
+
return "msg-" + index;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse a single JSONL line into one assistant-text record, or null
|
|
58
|
+
// when the line isn't one we index (user prompts, tool_use blocks,
|
|
59
|
+
// tool_result records, etc.). Text-only assistant blocks get joined
|
|
60
|
+
// in order so a multi-block response surfaces as a single grabbable
|
|
61
|
+
// message.
|
|
62
|
+
function parseAssistantTextLine(line, index) {
|
|
63
|
+
var obj;
|
|
64
|
+
try { obj = JSON.parse(line); } catch (e) { return null; }
|
|
65
|
+
if (!obj || !obj.message) return null;
|
|
66
|
+
if (obj.message.role !== "assistant") return null;
|
|
67
|
+
if (!Array.isArray(obj.message.content)) return null;
|
|
68
|
+
|
|
69
|
+
var text = "";
|
|
70
|
+
for (var i = 0; i < obj.message.content.length; i++) {
|
|
71
|
+
var block = obj.message.content[i];
|
|
72
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
73
|
+
text += block.text;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
text = text.trim();
|
|
77
|
+
if (!text) return null;
|
|
78
|
+
|
|
79
|
+
var normalized = normalizeForMatch(text);
|
|
80
|
+
if (normalized.length < MIN_MATCH_LEN) return null;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id: messageId(obj, index),
|
|
84
|
+
text: text,
|
|
85
|
+
matchKey: normalized,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Read the whole transcript and return the assistant text index. The
|
|
90
|
+
// transcripts are small enough (KB to a few MB) that a sync read on
|
|
91
|
+
// session open or on a watcher-driven update is fine. We also return
|
|
92
|
+
// the mtime/size so callers can short-circuit re-reads when nothing
|
|
93
|
+
// changed.
|
|
94
|
+
function readAssistantIndex(cwd, cliSessionId) {
|
|
95
|
+
var file = transcriptFilePath(cwd, cliSessionId);
|
|
96
|
+
if (!file) return { messages: [], mtimeMs: 0, byteLength: 0 };
|
|
97
|
+
var raw;
|
|
98
|
+
var stat;
|
|
99
|
+
try {
|
|
100
|
+
raw = fs.readFileSync(file, "utf8");
|
|
101
|
+
stat = fs.statSync(file);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return { messages: [], mtimeMs: 0, byteLength: 0 };
|
|
104
|
+
}
|
|
105
|
+
var lines = raw.split("\n");
|
|
106
|
+
var messages = [];
|
|
107
|
+
for (var i = 0; i < lines.length; i++) {
|
|
108
|
+
if (!lines[i]) continue;
|
|
109
|
+
var entry = parseAssistantTextLine(lines[i], i);
|
|
110
|
+
if (entry) messages.push(entry);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
messages: messages,
|
|
114
|
+
mtimeMs: stat.mtimeMs || 0,
|
|
115
|
+
byteLength: stat.size || raw.length,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
MIN_MATCH_LEN: MIN_MATCH_LEN,
|
|
121
|
+
transcriptFilePath: transcriptFilePath,
|
|
122
|
+
readAssistantIndex: readAssistantIndex,
|
|
123
|
+
parseAssistantTextLine: parseAssistantTextLine,
|
|
124
|
+
normalizeForMatch: normalizeForMatch,
|
|
125
|
+
};
|
package/lib/users-preferences.js
CHANGED
|
@@ -412,7 +412,7 @@ function attachPreferences(deps) {
|
|
|
412
412
|
// single object so the two values stay together.
|
|
413
413
|
|
|
414
414
|
var DEFAULT_TERM_FONT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
415
|
-
var DEFAULT_TERM_FONT_SIZE =
|
|
415
|
+
var DEFAULT_TERM_FONT_SIZE = 14;
|
|
416
416
|
var MIN_TERM_FONT_SIZE = 9;
|
|
417
417
|
var MAX_TERM_FONT_SIZE = 32;
|
|
418
418
|
|
package/lib/ws-schema.js
CHANGED
|
@@ -31,6 +31,7 @@ var schema = {
|
|
|
31
31
|
"load_more_history": { direction: "c2s", handler: "lib/project-sessions.js", description: "Request older history entries for the current session" },
|
|
32
32
|
"fork_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Fork a session from a given message UUID" },
|
|
33
33
|
"input_sync": { direction: "c2s", handler: "lib/project-sessions.js", description: "Sync the current input field text to other clients" },
|
|
34
|
+
"tui_transcript_request": { direction: "c2s", handler: "lib/project-sessions.js", description: "Ask for the assistant text index of a Claude TUI session (for hover-to-grab)" },
|
|
34
35
|
|
|
35
36
|
"session_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of sessions for the sidebar" },
|
|
36
37
|
"session_switched": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Confirmation that active session changed" },
|
|
@@ -42,6 +43,7 @@ var schema = {
|
|
|
42
43
|
"search_content_results": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full-text content search results" },
|
|
43
44
|
"fork_complete": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fork succeeded, includes new session ID" },
|
|
44
45
|
"input_sync_broadcast": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Broadcast input text from another client" },
|
|
46
|
+
"tui_transcript_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Assistant text index for a Claude TUI session (full replace; sent on request and after each new assistant message)" },
|
|
45
47
|
|
|
46
48
|
// -----------------------------------------------------------------------
|
|
47
49
|
// History replay
|
package/package.json
CHANGED