clay-server 3.1.0 → 3.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/lib/project-connection.js +1 -0
- package/lib/project-file-watch.js +59 -22
- package/lib/project-filesystem.js +2 -2
- package/lib/project-session-document.js +107 -0
- package/lib/project-session-notes.js +1 -1
- package/lib/project.js +60 -4
- package/lib/public/css/filebrowser.css +113 -0
- package/lib/public/index.html +6 -1
- package/lib/public/modules/app-messages.js +16 -4
- package/lib/public/modules/filebrowser.js +75 -3
- package/lib/public/modules/markdown-live-edit.js +305 -0
- package/lib/public/modules/rich-clipboard.js +83 -0
- package/lib/sdk-bridge.js +8 -0
- package/lib/session-document-mcp-server.js +16 -0
- package/lib/session-notes-mcp-server.js +3 -3
- package/lib/ws-schema.js +1 -0
- package/lib/yoke/adapters/codex.js +3 -0
- package/package.json +1 -1
|
@@ -5,56 +5,93 @@ var path = require("path");
|
|
|
5
5
|
* Attach file/directory watcher engine to a project context.
|
|
6
6
|
*
|
|
7
7
|
* ctx fields:
|
|
8
|
-
* cwd, send, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS
|
|
8
|
+
* cwd, send, sendTo, safePath, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS
|
|
9
9
|
*/
|
|
10
10
|
function attachFileWatch(ctx) {
|
|
11
11
|
var cwd = ctx.cwd;
|
|
12
12
|
var send = ctx.send;
|
|
13
|
+
var sendTo = ctx.sendTo;
|
|
13
14
|
var safePath = ctx.safePath;
|
|
14
15
|
var BINARY_EXTS = ctx.BINARY_EXTS;
|
|
15
16
|
var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
|
|
16
17
|
var IGNORED_DIRS = ctx.IGNORED_DIRS;
|
|
17
18
|
|
|
18
19
|
// --- File watcher ---
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
var
|
|
20
|
+
// One open file per websocket client. A project-wide singleton watcher made
|
|
21
|
+
// one browser tab silently replace another tab's live preview subscription.
|
|
22
|
+
var fileWatchers = new Map();
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
+
function closeFileWatch(key) {
|
|
25
|
+
var entry = fileWatchers.get(key);
|
|
26
|
+
if (!entry) return;
|
|
27
|
+
clearTimeout(entry.debounce);
|
|
28
|
+
try { entry.watcher.close(); } catch (e) {}
|
|
29
|
+
fileWatchers.delete(key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sendFileChanged(client, message) {
|
|
33
|
+
if (client && typeof sendTo === "function") {
|
|
34
|
+
sendTo(client, message);
|
|
35
|
+
} else {
|
|
36
|
+
send(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function startFileWatch(client, relPath) {
|
|
41
|
+
// Preserve the old single-argument API for callers outside the websocket
|
|
42
|
+
// file browser. They share one legacy subscription.
|
|
43
|
+
if (typeof relPath !== "string") {
|
|
44
|
+
relPath = client;
|
|
45
|
+
client = null;
|
|
46
|
+
}
|
|
24
47
|
var absPath = safePath(cwd, relPath);
|
|
25
48
|
if (!absPath) return;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
var key = client || "_legacy";
|
|
50
|
+
var existing = fileWatchers.get(key);
|
|
51
|
+
if (existing && existing.relPath === relPath) return;
|
|
52
|
+
closeFileWatch(key);
|
|
53
|
+
|
|
54
|
+
// Watch the parent directory rather than the file inode. Editors and agent
|
|
55
|
+
// tools commonly save with write-temp + rename; watching the old inode then
|
|
56
|
+
// misses later edits even though the path still exists.
|
|
57
|
+
var parentPath = path.dirname(absPath);
|
|
58
|
+
var baseName = path.basename(absPath);
|
|
29
59
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
60
|
+
var watcher = fs.watch(parentPath, function (eventType, filename) {
|
|
61
|
+
if (filename && String(filename) !== baseName) return;
|
|
62
|
+
var active = fileWatchers.get(key);
|
|
63
|
+
if (!active || active.relPath !== relPath) return;
|
|
64
|
+
clearTimeout(active.debounce);
|
|
65
|
+
active.debounce = setTimeout(function () {
|
|
66
|
+
var latest = fileWatchers.get(key);
|
|
67
|
+
if (!latest || latest.relPath !== relPath) return;
|
|
33
68
|
try {
|
|
34
69
|
var stat = fs.statSync(absPath);
|
|
35
70
|
var ext = path.extname(absPath).toLowerCase();
|
|
36
71
|
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
|
|
37
72
|
var content = fs.readFileSync(absPath, "utf8");
|
|
38
|
-
|
|
73
|
+
sendFileChanged(client, { type: "fs_file_changed", path: relPath, content: content, size: stat.size });
|
|
39
74
|
} catch (e) {
|
|
40
|
-
|
|
75
|
+
// Atomic saves can briefly remove the destination path between
|
|
76
|
+
// rename events. Keep the parent watcher alive for the next event.
|
|
77
|
+
if (e.code !== "ENOENT") closeFileWatch(key);
|
|
41
78
|
}
|
|
42
79
|
}, 200);
|
|
43
80
|
});
|
|
44
|
-
|
|
81
|
+
fileWatchers.set(key, { watcher: watcher, relPath: relPath, debounce: null });
|
|
82
|
+
watcher.on("error", function () { closeFileWatch(key); });
|
|
45
83
|
} catch (e) {
|
|
46
|
-
|
|
84
|
+
closeFileWatch(key);
|
|
47
85
|
}
|
|
48
86
|
}
|
|
49
87
|
|
|
50
|
-
function stopFileWatch() {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
88
|
+
function stopFileWatch(client) {
|
|
89
|
+
if (arguments.length > 0) {
|
|
90
|
+
closeFileWatch(client || "_legacy");
|
|
91
|
+
return;
|
|
54
92
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
watchedPath = null;
|
|
93
|
+
var keys = Array.from(fileWatchers.keys());
|
|
94
|
+
for (var i = 0; i < keys.length; i++) closeFileWatch(keys[i]);
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
// --- Directory watcher ---
|
|
@@ -317,12 +317,12 @@ function attachFilesystem(ctx) {
|
|
|
317
317
|
|
|
318
318
|
// --- File watcher ---
|
|
319
319
|
if (msg.type === "fs_watch") {
|
|
320
|
-
if (msg.path) startFileWatch(msg.path);
|
|
320
|
+
if (msg.path) startFileWatch(ws, msg.path);
|
|
321
321
|
return true;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
if (msg.type === "fs_unwatch") {
|
|
325
|
-
stopFileWatch();
|
|
325
|
+
stopFileWatch(ws);
|
|
326
326
|
return true;
|
|
327
327
|
}
|
|
328
328
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var sessionDocumentMcp = require("./session-document-mcp-server");
|
|
4
|
+
|
|
5
|
+
var DOCUMENT_PROMPT = "When the user's primary request is to create or revise Markdown, call present_markdown_edit once per targeted document with its resolved .md/.mdx path, immediately before that document's first Edit or Write. Do not call it for incidental Markdown changes during coding, maintenance, tests, or refactoring.";
|
|
6
|
+
|
|
7
|
+
function toolResult(value) {
|
|
8
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toolError(message) {
|
|
12
|
+
return Promise.resolve({
|
|
13
|
+
content: [{ type: "text", text: "Error: " + message }],
|
|
14
|
+
isError: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isInside(root, target) {
|
|
19
|
+
return target === root || target.startsWith(root + path.sep);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function attachSessionDocument(ctx) {
|
|
23
|
+
var cwd = fs.realpathSync(ctx.cwd);
|
|
24
|
+
var isMate = ctx.isMate;
|
|
25
|
+
var sendToSession = ctx.sendToSession;
|
|
26
|
+
var fsMaxSize = ctx.FS_MAX_SIZE || 512 * 1024;
|
|
27
|
+
var getOsUserInfoForSession = ctx.getOsUserInfoForSession || function () { return null; };
|
|
28
|
+
var fsAsUser = ctx.fsAsUser;
|
|
29
|
+
|
|
30
|
+
function resolveMarkdownPath(requested) {
|
|
31
|
+
if (typeof requested !== "string" || !requested.trim()) return null;
|
|
32
|
+
var target = path.resolve(cwd, requested.trim());
|
|
33
|
+
if (!isInside(cwd, target) || !/\.mdx?$/i.test(target)) return null;
|
|
34
|
+
try {
|
|
35
|
+
var realTarget = fs.realpathSync(target);
|
|
36
|
+
return isInside(cwd, realTarget) ? realTarget : null;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
try {
|
|
39
|
+
var realParent = fs.realpathSync(path.dirname(target));
|
|
40
|
+
return isInside(cwd, realParent) ? path.join(realParent, path.basename(target)) : null;
|
|
41
|
+
} catch (parentError) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readSnapshot(target, caller) {
|
|
48
|
+
if (!fs.existsSync(target)) return { content: "", exists: false, size: 0 };
|
|
49
|
+
var osUserInfo = getOsUserInfoForSession(caller);
|
|
50
|
+
if (osUserInfo && typeof fsAsUser === "function") {
|
|
51
|
+
var statResult = fsAsUser("stat", { file: target }, osUserInfo);
|
|
52
|
+
if (statResult.size > fsMaxSize) throw new Error("Markdown file is too large to present live");
|
|
53
|
+
var readResult = fsAsUser("read", { file: target, readContent: true }, osUserInfo);
|
|
54
|
+
return { content: readResult.content || "", exists: true, size: statResult.size };
|
|
55
|
+
}
|
|
56
|
+
var stat = fs.statSync(target);
|
|
57
|
+
if (stat.size > fsMaxSize) throw new Error("Markdown file is too large to present live");
|
|
58
|
+
return { content: fs.readFileSync(target, "utf8"), exists: true, size: stat.size };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function present(args, caller) {
|
|
62
|
+
if (!caller) return toolError("present_markdown_edit requires a session-bound tool server");
|
|
63
|
+
var target = resolveMarkdownPath(args.path);
|
|
64
|
+
if (!target) return toolError("path must be a .md or .mdx file inside the project");
|
|
65
|
+
try {
|
|
66
|
+
var snapshot = readSnapshot(target, caller);
|
|
67
|
+
var relativePath = path.relative(cwd, target).split(path.sep).join("/");
|
|
68
|
+
sendToSession(caller.localId, {
|
|
69
|
+
type: "markdown_edit_present",
|
|
70
|
+
path: relativePath,
|
|
71
|
+
content: snapshot.content,
|
|
72
|
+
exists: snapshot.exists,
|
|
73
|
+
size: snapshot.size,
|
|
74
|
+
});
|
|
75
|
+
return toolResult({ ready: true, path: relativePath });
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return toolError(e.message || String(e));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getToolDefs(boundSession) {
|
|
82
|
+
if (isMate) return [];
|
|
83
|
+
return sessionDocumentMcp.getToolDefs({
|
|
84
|
+
present: function (args) { return present(args, boundSession || null); },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createMcpServer(adapter, boundSession) {
|
|
89
|
+
if (isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
|
|
90
|
+
return adapter.createToolServer({
|
|
91
|
+
name: "clay-documents",
|
|
92
|
+
version: "1.0.0",
|
|
93
|
+
tools: getToolDefs(boundSession || null),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
createMcpServer: createMcpServer,
|
|
99
|
+
getSystemPrompt: function () { return isMate ? "" : DOCUMENT_PROMPT; },
|
|
100
|
+
getToolDefs: getToolDefs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
DOCUMENT_PROMPT: DOCUMENT_PROMPT,
|
|
106
|
+
attachSessionDocument: attachSessionDocument,
|
|
107
|
+
};
|
|
@@ -6,7 +6,7 @@ var MAX_PROMPT_CHARS = 4000;
|
|
|
6
6
|
var MAX_NOTE_PROMPT_CHARS = 800;
|
|
7
7
|
var NOTES_LABEL = "--- Project sticky notes (cross-session work memory; manage via clay-notes tools) ---";
|
|
8
8
|
var NOTES_HINT = "- More notes are available; call list_notes for the rest.";
|
|
9
|
-
var PROACTIVE_POLICY =
|
|
9
|
+
var PROACTIVE_POLICY = sessionNotesMcp.MEMORY_CONTRACT + " If an injected preview is relevant or truncated, call list_notes before acting or writing so you do not create a duplicate.";
|
|
10
10
|
var NOTE_COLORS = ["yellow", "blue", "green", "pink", "orange", "purple"];
|
|
11
11
|
|
|
12
12
|
function toolResult(value) {
|
package/lib/project.js
CHANGED
|
@@ -33,6 +33,7 @@ var { attachEmail: attachEmailModule } = require("./project-email");
|
|
|
33
33
|
var { attachSessionSpawn } = require("./project-session-spawn");
|
|
34
34
|
var { attachSessionPair } = require("./project-session-pair");
|
|
35
35
|
var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
|
|
36
|
+
var { attachSessionDocument } = require("./project-session-document");
|
|
36
37
|
var { attachSplitGroups } = require("./session-split-groups");
|
|
37
38
|
// project-notifications is attached globally in server.js, passed via opts.notificationsModule
|
|
38
39
|
|
|
@@ -385,6 +386,7 @@ function createProjectContext(opts) {
|
|
|
385
386
|
var _fileWatch = attachFileWatch({
|
|
386
387
|
cwd: cwd,
|
|
387
388
|
send: send,
|
|
389
|
+
sendTo: sendTo,
|
|
388
390
|
safePath: safePath,
|
|
389
391
|
BINARY_EXTS: BINARY_EXTS,
|
|
390
392
|
FS_MAX_SIZE: FS_MAX_SIZE,
|
|
@@ -507,6 +509,17 @@ function createProjectContext(opts) {
|
|
|
507
509
|
}
|
|
508
510
|
},
|
|
509
511
|
});
|
|
512
|
+
var _sessionDocument = attachSessionDocument({
|
|
513
|
+
cwd: cwd,
|
|
514
|
+
isMate: isMate,
|
|
515
|
+
sendToSession: sendToSession,
|
|
516
|
+
FS_MAX_SIZE: FS_MAX_SIZE,
|
|
517
|
+
getOsUserInfoForSession: function (session) {
|
|
518
|
+
var linuxUser = getLinuxUserForSession(session);
|
|
519
|
+
return linuxUser ? resolveOsUserInfo(linuxUser) : null;
|
|
520
|
+
},
|
|
521
|
+
fsAsUser: fsAsUser,
|
|
522
|
+
});
|
|
510
523
|
|
|
511
524
|
// The SDK bridge is created after local MCP servers. Session spawning uses
|
|
512
525
|
// a getter so tool handlers see the initialized bridge when they run.
|
|
@@ -565,6 +578,16 @@ function createProjectContext(opts) {
|
|
|
565
578
|
}
|
|
566
579
|
}
|
|
567
580
|
|
|
581
|
+
// Explicit agent signal for user-requested Markdown presentation.
|
|
582
|
+
if (!isMate) {
|
|
583
|
+
try {
|
|
584
|
+
var sessionDocumentMcpConfig = _sessionDocument.createMcpServer(adapter);
|
|
585
|
+
if (sessionDocumentMcpConfig) servers[sessionDocumentMcpConfig.name || "clay-documents"] = sessionDocumentMcpConfig;
|
|
586
|
+
} catch (e) {
|
|
587
|
+
console.error("[project] Failed to create session document MCP server:", e.message);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
568
591
|
// Debate MCP server (available to both mates and main project)
|
|
569
592
|
try {
|
|
570
593
|
var debateMcp = require("./debate-mcp-server");
|
|
@@ -728,9 +751,9 @@ function createProjectContext(opts) {
|
|
|
728
751
|
// clay-email -> only when the user has an account or server SMTP
|
|
729
752
|
//
|
|
730
753
|
// forSession (optional): the session whose query these servers are mounted
|
|
731
|
-
// into.
|
|
732
|
-
//
|
|
733
|
-
//
|
|
754
|
+
// into. Session, notes, and document tools must know their caller, so they
|
|
755
|
+
// are re-instantiated bound to that session; static instances only serve
|
|
756
|
+
// descriptor listing and fail closed on calls.
|
|
734
757
|
function getLocalMcpServers(forSession) {
|
|
735
758
|
var extWs = browserState._extensionWs;
|
|
736
759
|
var extConnected = !!(extWs && extWs.readyState === 1);
|
|
@@ -760,6 +783,15 @@ function createProjectContext(opts) {
|
|
|
760
783
|
}
|
|
761
784
|
continue;
|
|
762
785
|
}
|
|
786
|
+
if (name === "clay-documents" && forSession) {
|
|
787
|
+
try {
|
|
788
|
+
var boundDocuments = _sessionDocument.createMcpServer(adapter, forSession);
|
|
789
|
+
if (boundDocuments) { filtered[name] = boundDocuments; hasAny = true; }
|
|
790
|
+
} catch (e) {
|
|
791
|
+
console.error("[project] Failed to bind session document MCP server:", e.message);
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
763
795
|
filtered[name] = mcpServers[name];
|
|
764
796
|
hasAny = true;
|
|
765
797
|
}
|
|
@@ -807,10 +839,13 @@ function createProjectContext(opts) {
|
|
|
807
839
|
return composeSystemPrompts([
|
|
808
840
|
_sessionPair.getSystemPrompt(session),
|
|
809
841
|
_sessionNotes.getSystemPrompt(session),
|
|
842
|
+
_sessionDocument.getSystemPrompt(session),
|
|
810
843
|
]);
|
|
811
844
|
},
|
|
812
845
|
getSessionToolDefs: function (session) {
|
|
813
|
-
return _sessionPair.getToolDefs(session)
|
|
846
|
+
return _sessionPair.getToolDefs(session)
|
|
847
|
+
.concat(_sessionNotes.getToolDefs(session))
|
|
848
|
+
.concat(_sessionDocument.getToolDefs(session));
|
|
814
849
|
},
|
|
815
850
|
});
|
|
816
851
|
|
|
@@ -1482,6 +1517,15 @@ function createProjectContext(opts) {
|
|
|
1482
1517
|
inputSchema: normalizeToolSchema(noteTools[nti].inputSchema),
|
|
1483
1518
|
});
|
|
1484
1519
|
}
|
|
1520
|
+
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
1521
|
+
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
1522
|
+
tools.push({
|
|
1523
|
+
server: "clay-documents",
|
|
1524
|
+
name: documentTools[dti].name,
|
|
1525
|
+
description: documentTools[dti].description || documentTools[dti].name,
|
|
1526
|
+
inputSchema: normalizeToolSchema(documentTools[dti].inputSchema),
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1485
1529
|
}
|
|
1486
1530
|
|
|
1487
1531
|
if (sessionOnly) return Promise.resolve(tools);
|
|
@@ -1493,6 +1537,7 @@ function createProjectContext(opts) {
|
|
|
1493
1537
|
if (localMcp) {
|
|
1494
1538
|
var inAppNames = Object.keys(localMcp);
|
|
1495
1539
|
for (var i = 0; i < inAppNames.length; i++) {
|
|
1540
|
+
if (inAppNames[i] === "clay-sessions" || inAppNames[i] === "clay-notes" || inAppNames[i] === "clay-documents") continue;
|
|
1496
1541
|
extractServerTools(inAppNames[i], localMcp[inAppNames[i]]);
|
|
1497
1542
|
}
|
|
1498
1543
|
}
|
|
@@ -1527,6 +1572,17 @@ function createProjectContext(opts) {
|
|
|
1527
1572
|
}
|
|
1528
1573
|
}
|
|
1529
1574
|
}
|
|
1575
|
+
if (boundSession && serverName === "clay-documents") {
|
|
1576
|
+
var documentTools = _sessionDocument.getToolDefs(boundSession);
|
|
1577
|
+
for (var dti = 0; dti < documentTools.length; dti++) {
|
|
1578
|
+
if (documentTools[dti].name === toolName && typeof documentTools[dti].handler === "function") {
|
|
1579
|
+
return Promise.resolve(documentTools[dti].handler(args || {}));
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (serverName === "clay-sessions" || serverName === "clay-notes" || serverName === "clay-documents") {
|
|
1584
|
+
return Promise.reject(new Error("Session-bound tool requires a valid Clay session: " + serverName + "/" + toolName));
|
|
1585
|
+
}
|
|
1530
1586
|
if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
|
|
1531
1587
|
// Try in-app servers first (gated by extension connectivity for clay-browser).
|
|
1532
1588
|
var localMcp = getLocalMcpServers();
|
|
@@ -1036,6 +1036,41 @@
|
|
|
1036
1036
|
.file-viewer-btn.hidden { display: none; }
|
|
1037
1037
|
.file-viewer-btn .lucide { width: 16px; height: 16px; }
|
|
1038
1038
|
|
|
1039
|
+
.file-viewer-live-status {
|
|
1040
|
+
display: inline-flex;
|
|
1041
|
+
align-items: center;
|
|
1042
|
+
gap: 6px;
|
|
1043
|
+
flex-shrink: 0;
|
|
1044
|
+
padding: 4px 8px;
|
|
1045
|
+
border: 1px solid var(--accent-25);
|
|
1046
|
+
border-radius: 999px;
|
|
1047
|
+
background: var(--accent-8);
|
|
1048
|
+
color: var(--accent);
|
|
1049
|
+
font-size: 11px;
|
|
1050
|
+
font-weight: 600;
|
|
1051
|
+
letter-spacing: 0.01em;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.file-viewer-live-status.hidden { display: none; }
|
|
1055
|
+
|
|
1056
|
+
.file-viewer-live-dot {
|
|
1057
|
+
width: 6px;
|
|
1058
|
+
height: 6px;
|
|
1059
|
+
border-radius: 50%;
|
|
1060
|
+
background: currentColor;
|
|
1061
|
+
box-shadow: 0 0 0 0 var(--accent-25);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.file-viewer-live-status.editing .file-viewer-live-dot {
|
|
1065
|
+
animation: markdown-live-pulse 1.15s ease-out infinite;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.file-viewer-live-status.updated {
|
|
1069
|
+
border-color: var(--success-25);
|
|
1070
|
+
background: var(--success-8);
|
|
1071
|
+
color: var(--success);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1039
1074
|
/* --- Markdown rendered view --- */
|
|
1040
1075
|
.file-viewer-markdown {
|
|
1041
1076
|
padding: 20px 24px;
|
|
@@ -1113,6 +1148,84 @@
|
|
|
1113
1148
|
|
|
1114
1149
|
.file-viewer-markdown img { max-width: 100%; border-radius: 8px; }
|
|
1115
1150
|
|
|
1151
|
+
.file-viewer-markdown > .markdown-live-added,
|
|
1152
|
+
.file-viewer-markdown > .markdown-live-changed {
|
|
1153
|
+
position: relative;
|
|
1154
|
+
margin-left: -10px;
|
|
1155
|
+
margin-right: -10px;
|
|
1156
|
+
padding-left: 10px;
|
|
1157
|
+
padding-right: 10px;
|
|
1158
|
+
border-radius: 6px;
|
|
1159
|
+
background: var(--success-12);
|
|
1160
|
+
box-shadow: inset 2px 0 0 var(--success);
|
|
1161
|
+
animation: markdown-live-arrive 0.46s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
1162
|
+
transition: background 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.file-viewer-markdown > .markdown-live-removed {
|
|
1166
|
+
margin-left: -10px;
|
|
1167
|
+
margin-right: -10px;
|
|
1168
|
+
padding-left: 10px;
|
|
1169
|
+
padding-right: 10px;
|
|
1170
|
+
border-radius: 6px;
|
|
1171
|
+
background: var(--error-8);
|
|
1172
|
+
box-shadow: inset 2px 0 0 var(--error);
|
|
1173
|
+
color: color-mix(in srgb, var(--text-secondary) 72%, transparent);
|
|
1174
|
+
text-decoration-color: var(--error);
|
|
1175
|
+
transition: background 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.file-viewer-markdown > .markdown-live-focus {
|
|
1179
|
+
z-index: 1;
|
|
1180
|
+
opacity: 1;
|
|
1181
|
+
box-shadow: 0 10px 30px rgba(var(--shadow-rgb), 0.22);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.file-viewer-markdown > .markdown-live-focus.markdown-live-added,
|
|
1185
|
+
.file-viewer-markdown > .markdown-live-focus.markdown-live-changed {
|
|
1186
|
+
box-shadow: inset 3px 0 0 var(--success), 0 0 0 1px var(--success), 0 10px 30px rgba(var(--shadow-rgb), 0.22);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.file-viewer-markdown > .markdown-live-focus.markdown-live-removed {
|
|
1190
|
+
box-shadow: inset 3px 0 0 var(--error), 0 0 0 1px var(--error), 0 10px 30px rgba(var(--shadow-rgb), 0.22);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.file-viewer-markdown > .markdown-live-seen.markdown-live-added,
|
|
1194
|
+
.file-viewer-markdown > .markdown-live-seen.markdown-live-changed {
|
|
1195
|
+
background: var(--success-8);
|
|
1196
|
+
box-shadow: inset 2px 0 0 var(--success-25);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.file-viewer-markdown > .markdown-live-seen.markdown-live-removed {
|
|
1200
|
+
animation: markdown-live-depart 0.42s ease both;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@keyframes markdown-live-pulse {
|
|
1204
|
+
0% { box-shadow: 0 0 0 0 var(--accent-25); }
|
|
1205
|
+
70% { box-shadow: 0 0 0 5px transparent; }
|
|
1206
|
+
100% { box-shadow: 0 0 0 0 transparent; }
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
@keyframes markdown-live-arrive {
|
|
1210
|
+
from { opacity: 0; transform: translateY(5px); }
|
|
1211
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
@keyframes markdown-live-depart {
|
|
1215
|
+
0%, 58% { opacity: 0.82; max-height: 800px; }
|
|
1216
|
+
100% { opacity: 0; max-height: 0; margin-top: 0; margin-bottom: 0; overflow: hidden; }
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1220
|
+
.file-viewer-live-status.editing .file-viewer-live-dot,
|
|
1221
|
+
.file-viewer-markdown > .markdown-live-added,
|
|
1222
|
+
.file-viewer-markdown > .markdown-live-changed,
|
|
1223
|
+
.file-viewer-markdown > .markdown-live-removed,
|
|
1224
|
+
.file-viewer-markdown > .markdown-live-seen.markdown-live-removed {
|
|
1225
|
+
animation: none;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1116
1229
|
.file-viewer-body {
|
|
1117
1230
|
flex: 1;
|
|
1118
1231
|
overflow: auto;
|
package/lib/public/index.html
CHANGED
|
@@ -636,11 +636,16 @@
|
|
|
636
636
|
<div id="file-viewer" class="hidden">
|
|
637
637
|
<div class="file-viewer-header">
|
|
638
638
|
<span class="file-viewer-path" id="file-viewer-path"></span>
|
|
639
|
+
<span class="file-viewer-live-status hidden" id="file-viewer-live-status" aria-live="polite">
|
|
640
|
+
<span class="file-viewer-live-dot"></span>
|
|
641
|
+
<span class="file-viewer-live-label">Editing</span>
|
|
642
|
+
</span>
|
|
639
643
|
<button class="file-viewer-btn hidden" id="file-viewer-render" title="Toggle rendered view"><i data-lucide="book-open"></i></button>
|
|
640
644
|
<button class="file-viewer-btn hidden" id="file-viewer-pdf" title="Export PDF"><i data-lucide="file-down"></i></button>
|
|
641
645
|
<button class="file-viewer-btn hidden" id="file-viewer-history" title="Edit history"><i data-lucide="clock"></i></button>
|
|
642
646
|
<button class="file-viewer-btn" id="file-viewer-refresh" title="Refresh"><i data-lucide="refresh-cw"></i></button>
|
|
643
|
-
<button class="file-viewer-btn" id="file-viewer-copy" title="Copy contents"><i data-lucide="copy"></i></button>
|
|
647
|
+
<button class="file-viewer-btn" id="file-viewer-copy" title="Copy contents" aria-label="Copy contents"><i data-lucide="copy"></i></button>
|
|
648
|
+
<button class="file-viewer-btn hidden" id="file-viewer-copy-formatted" title="Copy Markdown formatting" aria-label="Copy Markdown formatting"><i data-lucide="clipboard-copy"></i></button>
|
|
644
649
|
<button class="file-viewer-btn" id="file-viewer-fullscreen" title="Toggle fullscreen"><i data-lucide="maximize-2"></i></button>
|
|
645
650
|
<button class="file-viewer-btn" id="file-viewer-close" title="Close"><i data-lucide="x"></i></button>
|
|
646
651
|
</div>
|
|
@@ -24,7 +24,8 @@ import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDel
|
|
|
24
24
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
|
|
25
25
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
26
26
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
27
|
-
import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch } from './filebrowser.js';
|
|
27
|
+
import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
|
|
28
|
+
import { beginMarkdownTurn, finishMarkdownTurn, markdownPathFromToolInput } from './markdown-live-edit.js';
|
|
28
29
|
import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
|
|
29
30
|
import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleInheritGroupsChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
|
|
30
31
|
import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
|
|
@@ -783,6 +784,7 @@ export function processMessage(msg) {
|
|
|
783
784
|
|
|
784
785
|
case "user_message":
|
|
785
786
|
if (msg._internal) break;
|
|
787
|
+
if (!store.get('replayingHistory')) beginMarkdownTurn();
|
|
786
788
|
resetThinkingGroup();
|
|
787
789
|
if (msg.planContent) {
|
|
788
790
|
setPlanContent(msg.planContent);
|
|
@@ -934,6 +936,8 @@ export function processMessage(msg) {
|
|
|
934
936
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
935
937
|
} else if (msg.name === "ask_user_questions") {
|
|
936
938
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
939
|
+
} else if (msg.name === "present_markdown_edit" || (msg.name && msg.name.indexOf("present_markdown_edit") !== -1)) {
|
|
940
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
937
941
|
} else if (getTodoTools()[msg.name]) {
|
|
938
942
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
939
943
|
} else {
|
|
@@ -997,9 +1001,12 @@ export function processMessage(msg) {
|
|
|
997
1001
|
if (msg.content != null || msg.images || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
|
|
998
1002
|
updateToolResult(msg.id, msg.content || "", msg.is_error || false, msg.images);
|
|
999
1003
|
}
|
|
1000
|
-
// Refresh file browser if an Edit/Write tool modified the open file
|
|
1001
|
-
|
|
1002
|
-
|
|
1004
|
+
// Refresh file browser if an Edit/Write tool modified the open file.
|
|
1005
|
+
// Multi-file Codex changes expose file_paths instead of file_path.
|
|
1006
|
+
var changedMarkdownPath = tr && tr.input ? markdownPathFromToolInput(tr.input) : null;
|
|
1007
|
+
var changedFilePath = tr && tr.input ? (tr.input.file_path || changedMarkdownPath) : null;
|
|
1008
|
+
if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && changedFilePath) {
|
|
1009
|
+
refreshIfOpen(changedFilePath);
|
|
1003
1010
|
}
|
|
1004
1011
|
}
|
|
1005
1012
|
break;
|
|
@@ -1121,6 +1128,7 @@ export function processMessage(msg) {
|
|
|
1121
1128
|
setStatus("connected");
|
|
1122
1129
|
if (!store.get('loopActive')) enableMainInput();
|
|
1123
1130
|
resetToolState();
|
|
1131
|
+
finishMarkdownTurn();
|
|
1124
1132
|
stopUrgentBlink();
|
|
1125
1133
|
if (document.hidden) {
|
|
1126
1134
|
if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
|
|
@@ -1326,6 +1334,10 @@ export function processMessage(msg) {
|
|
|
1326
1334
|
handleFileChanged(msg);
|
|
1327
1335
|
break;
|
|
1328
1336
|
|
|
1337
|
+
case "markdown_edit_present":
|
|
1338
|
+
if (!store.get('replayingHistory') && !store.get('dmMode')) presentMarkdownEdit(msg);
|
|
1339
|
+
break;
|
|
1340
|
+
|
|
1329
1341
|
case "fs_dir_changed":
|
|
1330
1342
|
handleDirChanged(msg);
|
|
1331
1343
|
break;
|
|
@@ -5,6 +5,8 @@ import { closeSidebar } from './sidebar.js';
|
|
|
5
5
|
import { closeTerminal } from './terminal.js';
|
|
6
6
|
import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
|
|
7
7
|
import { initFileIcons, getFileIconSvg, getFolderIconSvg } from './fileicons.js';
|
|
8
|
+
import { copyMarkdownFormatting } from './rich-clipboard.js';
|
|
9
|
+
import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow, isFollowingMarkdown } from './markdown-live-edit.js';
|
|
8
10
|
|
|
9
11
|
var ctx;
|
|
10
12
|
var showDropHint = function () {};
|
|
@@ -147,10 +149,18 @@ export function initFileBrowser(_ctx) {
|
|
|
147
149
|
if (currentContent) copyToClipboard(currentContent);
|
|
148
150
|
});
|
|
149
151
|
|
|
152
|
+
document.getElementById("file-viewer-copy-formatted").addEventListener("click", function () {
|
|
153
|
+
if (!isRendered || !currentIsMarkdown) return;
|
|
154
|
+
copyMarkdownFormatting(currentContent).catch(function (err) {
|
|
155
|
+
console.error("Markdown formatting copy failed:", err);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
150
159
|
// Markdown render toggle
|
|
151
160
|
document.getElementById("file-viewer-render").addEventListener("click", function () {
|
|
152
161
|
if (!currentContent || !currentIsMarkdown) return;
|
|
153
162
|
isRendered = !isRendered;
|
|
163
|
+
if (!isRendered && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
|
|
154
164
|
renderBody();
|
|
155
165
|
});
|
|
156
166
|
|
|
@@ -172,6 +182,7 @@ export function initFileBrowser(_ctx) {
|
|
|
172
182
|
// History button
|
|
173
183
|
document.getElementById("file-viewer-history").addEventListener("click", function () {
|
|
174
184
|
if (currentHistoryEntries.length === 0) return;
|
|
185
|
+
if (isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
|
|
175
186
|
historyVisible = !historyVisible;
|
|
176
187
|
inlineDiffActive = false;
|
|
177
188
|
compareMode = false;
|
|
@@ -191,6 +202,9 @@ export function initFileBrowser(_ctx) {
|
|
|
191
202
|
if (!currentFilePath) return;
|
|
192
203
|
viewerRefreshBtn.classList.add("spinning");
|
|
193
204
|
setTimeout(function () { viewerRefreshBtn.classList.remove("spinning"); }, 500);
|
|
205
|
+
// Refresh the content without kicking a rendered Markdown document back
|
|
206
|
+
// to source mode. Automatic watcher refreshes use the same state path.
|
|
207
|
+
pendingRefresh = true;
|
|
194
208
|
requestFileContent(currentFilePath);
|
|
195
209
|
});
|
|
196
210
|
}
|
|
@@ -395,6 +409,7 @@ function sendUnwatch() {
|
|
|
395
409
|
}
|
|
396
410
|
|
|
397
411
|
export function closeFileViewer() {
|
|
412
|
+
if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
|
|
398
413
|
sendUnwatch();
|
|
399
414
|
inlineDiffActive = false;
|
|
400
415
|
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
@@ -439,9 +454,14 @@ export function resetFileBrowser() {
|
|
|
439
454
|
}
|
|
440
455
|
|
|
441
456
|
var pendingOpenMode = null; // { type: "diff", oldStr, newStr } or null
|
|
457
|
+
var pendingRenderedOpen = false;
|
|
442
458
|
|
|
443
459
|
export function openFile(filePath, opts) {
|
|
444
460
|
if (!filePath) return;
|
|
461
|
+
if (followedFileChanged(filePath) || (opts && opts.diff && isFollowingMarkdown(filePath))) {
|
|
462
|
+
cancelMarkdownFollow();
|
|
463
|
+
}
|
|
464
|
+
pendingRenderedOpen = !!(opts && opts.rendered);
|
|
445
465
|
if (opts && opts.diff) {
|
|
446
466
|
pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
|
|
447
467
|
} else {
|
|
@@ -450,10 +470,27 @@ export function openFile(filePath, opts) {
|
|
|
450
470
|
requestFileContent(filePath);
|
|
451
471
|
}
|
|
452
472
|
|
|
453
|
-
function
|
|
473
|
+
export function presentMarkdownEdit(msg) {
|
|
474
|
+
if (!msg || !beginMarkdownPresentation(msg.path)) return;
|
|
475
|
+
pendingOpenMode = null;
|
|
476
|
+
pendingRenderedOpen = true;
|
|
477
|
+
pendingRefresh = false;
|
|
478
|
+
showFileContent({
|
|
479
|
+
path: msg.path,
|
|
480
|
+
content: typeof msg.content === "string" ? msg.content : "",
|
|
481
|
+
size: msg.size || 0,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function followedFileChanged(filePath) {
|
|
486
|
+
return currentFilePath && isFollowingMarkdown(currentFilePath) && !isFollowingMarkdown(filePath);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderBody(previousMarkdown) {
|
|
454
490
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
455
491
|
var renderBtn = document.getElementById("file-viewer-render");
|
|
456
492
|
var pdfBtn = document.getElementById("file-viewer-pdf");
|
|
493
|
+
var formattedCopyBtn = document.getElementById("file-viewer-copy-formatted");
|
|
457
494
|
|
|
458
495
|
if (isRendered) {
|
|
459
496
|
bodyEl.innerHTML = '<div class="file-viewer-markdown">' + renderMarkdown(currentContent) + '</div>';
|
|
@@ -467,11 +504,16 @@ function renderBody() {
|
|
|
467
504
|
imgs[i].src = "api/file?path=" + encodeURIComponent(resolvedPath);
|
|
468
505
|
}
|
|
469
506
|
}
|
|
507
|
+
var markdownEl = bodyEl.querySelector(".file-viewer-markdown");
|
|
508
|
+
if (previousMarkdown != null && isFollowingMarkdown(currentFilePath)) {
|
|
509
|
+
animateMarkdownChange(markdownEl, previousMarkdown, currentContent, renderMarkdown);
|
|
510
|
+
}
|
|
470
511
|
highlightCodeBlocks(bodyEl);
|
|
471
512
|
renderMermaidBlocks(bodyEl);
|
|
472
513
|
renderBtn.classList.add("active");
|
|
473
514
|
renderBtn.title = "Show raw";
|
|
474
515
|
pdfBtn.classList.remove("hidden");
|
|
516
|
+
formattedCopyBtn.classList.remove("hidden");
|
|
475
517
|
} else {
|
|
476
518
|
var pre = document.createElement("pre");
|
|
477
519
|
var code = document.createElement("code");
|
|
@@ -486,6 +528,7 @@ function renderBody() {
|
|
|
486
528
|
renderBtn.classList.remove("active");
|
|
487
529
|
renderBtn.title = "Render markdown";
|
|
488
530
|
pdfBtn.classList.add("hidden");
|
|
531
|
+
formattedCopyBtn.classList.add("hidden");
|
|
489
532
|
}
|
|
490
533
|
refreshIcons();
|
|
491
534
|
}
|
|
@@ -661,6 +704,7 @@ function renderFilteredTree(container, tree, depth, query) {
|
|
|
661
704
|
(function (filePath, rowEl) {
|
|
662
705
|
rowEl.addEventListener("click", function (e) {
|
|
663
706
|
e.stopPropagation();
|
|
707
|
+
if (followedFileChanged(filePath)) cancelMarkdownFollow();
|
|
664
708
|
var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
|
|
665
709
|
if (prev) prev.classList.remove("active");
|
|
666
710
|
rowEl.classList.add("active");
|
|
@@ -891,6 +935,7 @@ function renderEntries(container, entries, depth) {
|
|
|
891
935
|
(function (filePath, rowEl) {
|
|
892
936
|
rowEl.addEventListener("click", function (e) {
|
|
893
937
|
e.stopPropagation();
|
|
938
|
+
if (followedFileChanged(filePath)) cancelMarkdownFollow();
|
|
894
939
|
// Mark active
|
|
895
940
|
var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
|
|
896
941
|
if (prev) prev.classList.remove("active");
|
|
@@ -915,6 +960,10 @@ function showFileContent(msg) {
|
|
|
915
960
|
var pathEl = document.getElementById("file-viewer-path");
|
|
916
961
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
917
962
|
var renderBtn = document.getElementById("file-viewer-render");
|
|
963
|
+
var copyBtn = document.getElementById("file-viewer-copy");
|
|
964
|
+
var previousContent = currentContent;
|
|
965
|
+
var previousPath = currentFilePath;
|
|
966
|
+
var previousWasMarkdown = currentIsMarkdown;
|
|
918
967
|
|
|
919
968
|
pathEl.textContent = msg.path;
|
|
920
969
|
var keepRenderState = pendingRefresh && msg.path === currentFilePath;
|
|
@@ -924,9 +973,15 @@ function showFileContent(msg) {
|
|
|
924
973
|
currentFilePath = msg.path;
|
|
925
974
|
currentIsMarkdown = false;
|
|
926
975
|
if (!keepRenderState) isRendered = false;
|
|
976
|
+
var requestedExt = msg.path.split(".").pop().toLowerCase();
|
|
977
|
+
if (pendingRenderedOpen && (requestedExt === "md" || requestedExt === "mdx")) {
|
|
978
|
+
currentIsMarkdown = true;
|
|
979
|
+
isRendered = true;
|
|
980
|
+
}
|
|
927
981
|
renderBtn.classList.add("hidden");
|
|
928
982
|
renderBtn.classList.remove("active");
|
|
929
983
|
document.getElementById("file-viewer-pdf").classList.add("hidden");
|
|
984
|
+
document.getElementById("file-viewer-copy-formatted").classList.add("hidden");
|
|
930
985
|
|
|
931
986
|
if (msg.error) {
|
|
932
987
|
bodyEl.innerHTML = '<div class="file-tree-error">' + escapeHtml(msg.error) + '</div>';
|
|
@@ -938,17 +993,25 @@ function showFileContent(msg) {
|
|
|
938
993
|
}
|
|
939
994
|
} else {
|
|
940
995
|
currentContent = msg.content;
|
|
941
|
-
var ext =
|
|
996
|
+
var ext = requestedExt;
|
|
942
997
|
currentIsMarkdown = (ext === "md" || ext === "mdx");
|
|
998
|
+
if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
|
|
943
999
|
|
|
944
1000
|
if (currentIsMarkdown) {
|
|
945
1001
|
renderBtn.classList.remove("hidden");
|
|
946
1002
|
renderBtn.title = "Render markdown";
|
|
1003
|
+
copyBtn.title = "Copy Markdown source";
|
|
1004
|
+
} else {
|
|
1005
|
+
copyBtn.title = "Copy contents";
|
|
947
1006
|
}
|
|
948
1007
|
|
|
949
1008
|
// Show raw by default, use renderBody for markdown toggle
|
|
950
1009
|
if (currentIsMarkdown) {
|
|
951
|
-
|
|
1010
|
+
var transitionFrom = keepRenderState && prevRendered && previousWasMarkdown &&
|
|
1011
|
+
isFollowingMarkdown(msg.path) && pathsReferToSameFile(previousPath, msg.path)
|
|
1012
|
+
? (previousContent == null ? "" : previousContent)
|
|
1013
|
+
: null;
|
|
1014
|
+
renderBody(transitionFrom);
|
|
952
1015
|
} else {
|
|
953
1016
|
renderCodeWithLineNumbers(bodyEl, msg.content, ext);
|
|
954
1017
|
}
|
|
@@ -974,9 +1037,11 @@ function showFileContent(msg) {
|
|
|
974
1037
|
historyBtn2.classList.remove("active");
|
|
975
1038
|
requestFileHistory(msg.path);
|
|
976
1039
|
showInlineDiff(diffOpts.oldStr, diffOpts.newStr);
|
|
1040
|
+
pendingRenderedOpen = false;
|
|
977
1041
|
return;
|
|
978
1042
|
}
|
|
979
1043
|
pendingOpenMode = null;
|
|
1044
|
+
pendingRenderedOpen = false;
|
|
980
1045
|
|
|
981
1046
|
// Request edit history for this file (skip on auto-refresh)
|
|
982
1047
|
if (!keepRenderState) {
|
|
@@ -993,6 +1058,13 @@ function showFileContent(msg) {
|
|
|
993
1058
|
}
|
|
994
1059
|
}
|
|
995
1060
|
|
|
1061
|
+
function pathsReferToSameFile(left, right) {
|
|
1062
|
+
if (!left || !right) return false;
|
|
1063
|
+
var a = String(left).replace(/\\/g, "/");
|
|
1064
|
+
var b = String(right).replace(/\\/g, "/");
|
|
1065
|
+
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
996
1068
|
export function handleFileChanged(msg) {
|
|
997
1069
|
if (!msg.path || msg.path !== currentFilePath) return;
|
|
998
1070
|
if (ctx.fileViewerEl.classList.contains("hidden")) return;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
var followedPath = null;
|
|
2
|
+
var suppressedForTurn = false;
|
|
3
|
+
var cleanupTimer = null;
|
|
4
|
+
var tourTimer = null;
|
|
5
|
+
var statusTimer = null;
|
|
6
|
+
var followExpiryTimer = null;
|
|
7
|
+
var sawChange = false;
|
|
8
|
+
var tourVersion = 0;
|
|
9
|
+
var touring = false;
|
|
10
|
+
var tourViewer = null;
|
|
11
|
+
|
|
12
|
+
function normalizedPath(filePath) {
|
|
13
|
+
return String(filePath || "").replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isMarkdownPath(filePath) {
|
|
17
|
+
return /\.mdx?$/i.test(normalizedPath(filePath));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function markdownPathFromToolInput(input) {
|
|
21
|
+
if (!input || typeof input !== "object") return null;
|
|
22
|
+
if (isMarkdownPath(input.file_path)) return input.file_path;
|
|
23
|
+
var paths = Array.isArray(input.file_paths) ? input.file_paths : [];
|
|
24
|
+
for (var i = 0; i < paths.length; i++) {
|
|
25
|
+
if (isMarkdownPath(paths[i])) return paths[i];
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function pathsMatch(left, right) {
|
|
31
|
+
var a = normalizedPath(left);
|
|
32
|
+
var b = normalizedPath(right);
|
|
33
|
+
if (!a || !b) return false;
|
|
34
|
+
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function statusElement() {
|
|
38
|
+
return document.getElementById("file-viewer-live-status");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setStatus(state, label) {
|
|
42
|
+
var el = statusElement();
|
|
43
|
+
if (!el) return;
|
|
44
|
+
clearTimeout(statusTimer);
|
|
45
|
+
el.classList.remove("hidden", "editing", "updated");
|
|
46
|
+
el.classList.add(state);
|
|
47
|
+
var labelEl = el.querySelector(".file-viewer-live-label");
|
|
48
|
+
if (labelEl) labelEl.textContent = label;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hideStatusSoon() {
|
|
52
|
+
clearTimeout(statusTimer);
|
|
53
|
+
statusTimer = setTimeout(function () {
|
|
54
|
+
var el = statusElement();
|
|
55
|
+
if (el) el.classList.add("hidden");
|
|
56
|
+
}, 1800);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function beginMarkdownTurn() {
|
|
60
|
+
stopChangeTour();
|
|
61
|
+
clearTimeout(cleanupTimer);
|
|
62
|
+
clearLiveClasses();
|
|
63
|
+
suppressedForTurn = false;
|
|
64
|
+
followedPath = null;
|
|
65
|
+
sawChange = false;
|
|
66
|
+
clearTimeout(followExpiryTimer);
|
|
67
|
+
clearTimeout(statusTimer);
|
|
68
|
+
var el = statusElement();
|
|
69
|
+
if (el) el.classList.add("hidden");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function beginMarkdownPresentation(filePath) {
|
|
73
|
+
if (suppressedForTurn || !isMarkdownPath(filePath)) return false;
|
|
74
|
+
clearTimeout(followExpiryTimer);
|
|
75
|
+
if (!pathsMatch(followedPath, filePath)) sawChange = false;
|
|
76
|
+
followedPath = normalizedPath(filePath);
|
|
77
|
+
setStatus("editing", "Editing");
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isFollowingMarkdown(filePath) {
|
|
82
|
+
return !suppressedForTurn && pathsMatch(followedPath, filePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function cancelMarkdownFollow() {
|
|
86
|
+
followedPath = null;
|
|
87
|
+
suppressedForTurn = true;
|
|
88
|
+
clearTimeout(cleanupTimer);
|
|
89
|
+
stopChangeTour();
|
|
90
|
+
clearTimeout(statusTimer);
|
|
91
|
+
clearTimeout(followExpiryTimer);
|
|
92
|
+
var status = statusElement();
|
|
93
|
+
if (status) status.classList.add("hidden");
|
|
94
|
+
clearLiveClasses();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function finishMarkdownTurn() {
|
|
98
|
+
if (!followedPath || suppressedForTurn) return;
|
|
99
|
+
if (sawChange && !touring) {
|
|
100
|
+
setStatus("updated", "Updated");
|
|
101
|
+
hideStatusSoon();
|
|
102
|
+
} else {
|
|
103
|
+
var el = statusElement();
|
|
104
|
+
if (el) el.classList.add("hidden");
|
|
105
|
+
}
|
|
106
|
+
clearTimeout(followExpiryTimer);
|
|
107
|
+
followExpiryTimer = setTimeout(function () { followedPath = null; }, 4000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function blockSignature(node) {
|
|
111
|
+
return node.outerHTML.replace(/\s+/g, " ").trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function diffBlockSignatures(oldSignatures, newSignatures) {
|
|
115
|
+
var oldLength = oldSignatures.length;
|
|
116
|
+
var newLength = newSignatures.length;
|
|
117
|
+
var rows = new Array(oldLength + 1);
|
|
118
|
+
var i;
|
|
119
|
+
var j;
|
|
120
|
+
for (i = 0; i <= oldLength; i++) rows[i] = new Uint32Array(newLength + 1);
|
|
121
|
+
|
|
122
|
+
for (i = oldLength - 1; i >= 0; i--) {
|
|
123
|
+
for (j = newLength - 1; j >= 0; j--) {
|
|
124
|
+
rows[i][j] = oldSignatures[i] === newSignatures[j]
|
|
125
|
+
? rows[i + 1][j + 1] + 1
|
|
126
|
+
: Math.max(rows[i + 1][j], rows[i][j + 1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
var matches = [];
|
|
131
|
+
i = 0;
|
|
132
|
+
j = 0;
|
|
133
|
+
while (i < oldLength && j < newLength) {
|
|
134
|
+
if (oldSignatures[i] === newSignatures[j]) {
|
|
135
|
+
matches.push({ oldIndex: i, newIndex: j });
|
|
136
|
+
i++;
|
|
137
|
+
j++;
|
|
138
|
+
} else if (rows[i + 1][j] >= rows[i][j + 1]) {
|
|
139
|
+
i++;
|
|
140
|
+
} else {
|
|
141
|
+
j++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return matches;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clearLiveClasses() {
|
|
148
|
+
var viewer = document.getElementById("file-viewer-body");
|
|
149
|
+
if (!viewer) return;
|
|
150
|
+
var removed = viewer.querySelectorAll(".markdown-live-removed");
|
|
151
|
+
for (var i = 0; i < removed.length; i++) removed[i].remove();
|
|
152
|
+
var active = viewer.querySelectorAll(".markdown-live-added, .markdown-live-changed, .markdown-live-focus, .markdown-live-seen");
|
|
153
|
+
for (var j = 0; j < active.length; j++) {
|
|
154
|
+
active[j].classList.remove("markdown-live-added", "markdown-live-changed", "markdown-live-focus", "markdown-live-seen");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function detachTourInterruption() {
|
|
159
|
+
if (tourViewer) {
|
|
160
|
+
tourViewer.removeEventListener("wheel", interruptChangeTour);
|
|
161
|
+
tourViewer.removeEventListener("pointerdown", interruptChangeTour);
|
|
162
|
+
tourViewer.removeEventListener("touchstart", interruptChangeTour);
|
|
163
|
+
}
|
|
164
|
+
document.removeEventListener("keydown", interruptChangeTour);
|
|
165
|
+
tourViewer = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stopChangeTour() {
|
|
169
|
+
tourVersion++;
|
|
170
|
+
touring = false;
|
|
171
|
+
clearTimeout(tourTimer);
|
|
172
|
+
detachTourInterruption();
|
|
173
|
+
var focused = document.querySelectorAll(".markdown-live-focus");
|
|
174
|
+
for (var i = 0; i < focused.length; i++) focused[i].classList.remove("markdown-live-focus");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function interruptChangeTour() {
|
|
178
|
+
if (!touring) return;
|
|
179
|
+
stopChangeTour();
|
|
180
|
+
setStatus("updated", "Updated");
|
|
181
|
+
clearTimeout(cleanupTimer);
|
|
182
|
+
cleanupTimer = setTimeout(function () {
|
|
183
|
+
clearLiveClasses();
|
|
184
|
+
hideStatusSoon();
|
|
185
|
+
}, 1800);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function attachTourInterruption(viewer) {
|
|
189
|
+
tourViewer = viewer;
|
|
190
|
+
viewer.addEventListener("wheel", interruptChangeTour, { passive: true });
|
|
191
|
+
viewer.addEventListener("pointerdown", interruptChangeTour);
|
|
192
|
+
viewer.addEventListener("touchstart", interruptChangeTour, { passive: true });
|
|
193
|
+
document.addEventListener("keydown", interruptChangeTour);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function changeTourDelay(text) {
|
|
197
|
+
var length = String(text || "").trim().length;
|
|
198
|
+
return Math.min(1800, Math.max(850, 700 + length * 3));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function startChangeTour(viewer, targets) {
|
|
202
|
+
stopChangeTour();
|
|
203
|
+
touring = true;
|
|
204
|
+
attachTourInterruption(viewer);
|
|
205
|
+
var version = tourVersion;
|
|
206
|
+
var index = 0;
|
|
207
|
+
var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
208
|
+
|
|
209
|
+
function visitNext() {
|
|
210
|
+
if (!touring || version !== tourVersion) return;
|
|
211
|
+
if (index >= targets.length) {
|
|
212
|
+
var last = targets[targets.length - 1];
|
|
213
|
+
if (last && last.isConnected) {
|
|
214
|
+
last.classList.remove("markdown-live-focus");
|
|
215
|
+
last.classList.add("markdown-live-seen");
|
|
216
|
+
}
|
|
217
|
+
touring = false;
|
|
218
|
+
detachTourInterruption();
|
|
219
|
+
setStatus("updated", "Updated");
|
|
220
|
+
clearTimeout(cleanupTimer);
|
|
221
|
+
cleanupTimer = setTimeout(function () {
|
|
222
|
+
clearLiveClasses();
|
|
223
|
+
hideStatusSoon();
|
|
224
|
+
}, 1400);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
var previous = index > 0 ? targets[index - 1] : null;
|
|
229
|
+
if (previous && previous.isConnected) {
|
|
230
|
+
previous.classList.remove("markdown-live-focus");
|
|
231
|
+
previous.classList.add("markdown-live-seen");
|
|
232
|
+
}
|
|
233
|
+
var target = targets[index];
|
|
234
|
+
index++;
|
|
235
|
+
if (!target || !target.isConnected) {
|
|
236
|
+
visitNext();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
target.classList.add("markdown-live-focus");
|
|
240
|
+
setStatus("editing", "Change " + index + " of " + targets.length);
|
|
241
|
+
target.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "center" });
|
|
242
|
+
tourTimer = setTimeout(visitNext, reduceMotion ? 250 : changeTourDelay(target.textContent));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
requestAnimationFrame(visitNext);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function changedRegionStart(matches, oldIndex) {
|
|
249
|
+
var newIndex = 0;
|
|
250
|
+
for (var i = 0; i < matches.length; i++) {
|
|
251
|
+
if (matches[i].oldIndex >= oldIndex) break;
|
|
252
|
+
newIndex = matches[i].newIndex + 1;
|
|
253
|
+
}
|
|
254
|
+
return newIndex;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function animateMarkdownChange(markdownEl, oldMarkdown, newMarkdown, renderFn) {
|
|
258
|
+
if (!markdownEl || oldMarkdown === newMarkdown) return false;
|
|
259
|
+
clearLiveClasses();
|
|
260
|
+
|
|
261
|
+
var oldRoot = document.createElement("div");
|
|
262
|
+
var newRoot = document.createElement("div");
|
|
263
|
+
oldRoot.innerHTML = renderFn(oldMarkdown);
|
|
264
|
+
newRoot.innerHTML = renderFn(newMarkdown);
|
|
265
|
+
|
|
266
|
+
var oldBlocks = Array.from(oldRoot.children);
|
|
267
|
+
var newBlocks = Array.from(newRoot.children);
|
|
268
|
+
var liveBlocks = Array.from(markdownEl.children);
|
|
269
|
+
var oldSignatures = oldBlocks.map(blockSignature);
|
|
270
|
+
var newSignatures = newBlocks.map(blockSignature);
|
|
271
|
+
var matches = diffBlockSignatures(oldSignatures, newSignatures);
|
|
272
|
+
var matchedOld = new Set(matches.map(function (match) { return match.oldIndex; }));
|
|
273
|
+
var matchedNew = new Set(matches.map(function (match) { return match.newIndex; }));
|
|
274
|
+
for (var i = 0; i < liveBlocks.length; i++) {
|
|
275
|
+
if (matchedNew.has(i)) continue;
|
|
276
|
+
liveBlocks[i].classList.add("markdown-live-added");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (var j = 0; j < oldBlocks.length; j++) {
|
|
280
|
+
if (matchedOld.has(j)) continue;
|
|
281
|
+
var clone = oldBlocks[j].cloneNode(true);
|
|
282
|
+
clone.classList.add("markdown-live-removed");
|
|
283
|
+
clone.setAttribute("aria-hidden", "true");
|
|
284
|
+
var insertionIndex = changedRegionStart(matches, j);
|
|
285
|
+
var anchor = insertionIndex < liveBlocks.length ? liveBlocks[insertionIndex] : null;
|
|
286
|
+
markdownEl.insertBefore(clone, anchor);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var changedTargets = Array.from(markdownEl.children).filter(function (element) {
|
|
290
|
+
return element.classList.contains("markdown-live-added") || element.classList.contains("markdown-live-removed");
|
|
291
|
+
});
|
|
292
|
+
if (changedTargets.length === 0) return false;
|
|
293
|
+
sawChange = true;
|
|
294
|
+
var hasRemoved = oldBlocks.length !== matchedOld.size;
|
|
295
|
+
var hasAdded = newBlocks.length !== matchedNew.size;
|
|
296
|
+
if (hasRemoved && hasAdded) {
|
|
297
|
+
for (var k = 0; k < liveBlocks.length; k++) {
|
|
298
|
+
if (!matchedNew.has(k)) liveBlocks[k].classList.add("markdown-live-changed");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
clearTimeout(cleanupTimer);
|
|
303
|
+
startChangeTour(document.getElementById("file-viewer-body"), changedTargets);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { renderMarkdown } from './markdown.js';
|
|
2
|
+
import { showToast } from './utils.js';
|
|
3
|
+
|
|
4
|
+
// Rich-text clipboards use HTML as their transport, but this payload contains
|
|
5
|
+
// only Markdown semantics. Clay's rendered DOM, theme, syntax highlighting,
|
|
6
|
+
// spacing, and typography never enter the clipboard.
|
|
7
|
+
export function buildMarkdownClipboardContent(markdown) {
|
|
8
|
+
var container = document.createElement("div");
|
|
9
|
+
container.innerHTML = renderMarkdown(markdown || "");
|
|
10
|
+
|
|
11
|
+
var elements = container.querySelectorAll("*");
|
|
12
|
+
for (var i = 0; i < elements.length; i++) {
|
|
13
|
+
var el = elements[i];
|
|
14
|
+
el.removeAttribute("class");
|
|
15
|
+
el.removeAttribute("style");
|
|
16
|
+
el.removeAttribute("id");
|
|
17
|
+
el.removeAttribute("contenteditable");
|
|
18
|
+
el.removeAttribute("target");
|
|
19
|
+
el.removeAttribute("rel");
|
|
20
|
+
var dataNames = [];
|
|
21
|
+
for (var ai = 0; ai < el.attributes.length; ai++) {
|
|
22
|
+
if (el.attributes[ai].name.indexOf("data-") === 0) dataNames.push(el.attributes[ai].name);
|
|
23
|
+
}
|
|
24
|
+
for (var di = 0; di < dataNames.length; di++) el.removeAttribute(dataNames[di]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
html: '<meta charset="utf-8">' + container.innerHTML,
|
|
29
|
+
text: container.innerText || container.textContent || "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function legacyCopyHtml(html) {
|
|
34
|
+
var container = document.createElement("div");
|
|
35
|
+
container.contentEditable = "true";
|
|
36
|
+
container.style.cssText = "position:fixed;left:-10000px;top:0;width:1px;height:1px;overflow:hidden";
|
|
37
|
+
container.innerHTML = html;
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
|
|
40
|
+
var selection = window.getSelection();
|
|
41
|
+
var savedRanges = [];
|
|
42
|
+
for (var i = 0; selection && i < selection.rangeCount; i++) {
|
|
43
|
+
savedRanges.push(selection.getRangeAt(i).cloneRange());
|
|
44
|
+
}
|
|
45
|
+
var range = document.createRange();
|
|
46
|
+
range.selectNodeContents(container);
|
|
47
|
+
if (selection) {
|
|
48
|
+
selection.removeAllRanges();
|
|
49
|
+
selection.addRange(range);
|
|
50
|
+
}
|
|
51
|
+
var copied = document.execCommand("copy");
|
|
52
|
+
if (selection) {
|
|
53
|
+
selection.removeAllRanges();
|
|
54
|
+
for (var ri = 0; ri < savedRanges.length; ri++) selection.addRange(savedRanges[ri]);
|
|
55
|
+
}
|
|
56
|
+
container.remove();
|
|
57
|
+
if (!copied) return Promise.reject(new Error("Formatted copy is not supported by this browser"));
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function copyMarkdownFormatting(markdown) {
|
|
62
|
+
var content = buildMarkdownClipboardContent(markdown);
|
|
63
|
+
var copyPromise;
|
|
64
|
+
|
|
65
|
+
if (navigator.clipboard && navigator.clipboard.write && typeof ClipboardItem !== "undefined") {
|
|
66
|
+
var item = new ClipboardItem({
|
|
67
|
+
"text/html": new Blob([content.html], { type: "text/html" }),
|
|
68
|
+
"text/plain": new Blob([content.text], { type: "text/plain" }),
|
|
69
|
+
});
|
|
70
|
+
copyPromise = navigator.clipboard.write([item]).catch(function () {
|
|
71
|
+
return legacyCopyHtml(content.html);
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
copyPromise = legacyCopyHtml(content.html);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return copyPromise.then(function () {
|
|
78
|
+
showToast("Copied Markdown formatting");
|
|
79
|
+
}).catch(function (err) {
|
|
80
|
+
showToast("Could not copy Markdown formatting", "warn");
|
|
81
|
+
throw err;
|
|
82
|
+
});
|
|
83
|
+
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -577,6 +577,14 @@ function createSDKBridge(opts) {
|
|
|
577
577
|
return { behavior: "allow", updatedInput: input };
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
// This tool only prepares a read-only UI projection for the document the
|
|
581
|
+
// agent is about to edit. The subsequent Edit/Write keeps its own normal
|
|
582
|
+
// permission behavior.
|
|
583
|
+
if (toolName.indexOf("mcp__clay-documents__present_markdown_edit") === 0 ||
|
|
584
|
+
toolName.indexOf("clay-documents__present_markdown_edit") !== -1) {
|
|
585
|
+
return { behavior: "allow", updatedInput: input };
|
|
586
|
+
}
|
|
587
|
+
|
|
580
588
|
// Auto-approve read-only email MCP tools.
|
|
581
589
|
// These only read data from accounts the user explicitly checked.
|
|
582
590
|
// Write operations (send, reply, mark_read) still require permission.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Live Markdown presentation tool for project sessions.
|
|
2
|
+
|
|
3
|
+
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
|
+
|
|
5
|
+
function getToolDefs(handlers) {
|
|
6
|
+
return [{
|
|
7
|
+
name: "present_markdown_edit",
|
|
8
|
+
description: "Call once per targeted document, immediately before its first Edit or Write, when the user's primary request is to create or revise Markdown. Pass the resolved .md or .mdx path. Do not call for incidental documentation changes made as part of coding, maintenance, tests, or refactoring. This prepares Clay's rendered document view so the user can watch every change.",
|
|
9
|
+
inputSchema: buildShape({
|
|
10
|
+
path: { type: "string", description: "Project-relative or absolute path to the Markdown document that will be edited." },
|
|
11
|
+
}, ["path"]),
|
|
12
|
+
handler: function (args) { return handlers.present(args || {}); },
|
|
13
|
+
}];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { getToolDefs: getToolDefs };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
|
|
5
|
-
var MEMORY_CONTRACT = "
|
|
5
|
+
var MEMORY_CONTRACT = "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. 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. Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
|
|
6
6
|
|
|
7
7
|
function getToolDefs(handlers) {
|
|
8
8
|
return [
|
|
@@ -14,10 +14,10 @@ function getToolDefs(handlers) {
|
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
name: "write_note",
|
|
17
|
-
description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id.
|
|
17
|
+
description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. Before creating, 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.",
|
|
18
18
|
inputSchema: buildShape({
|
|
19
19
|
id: { type: "string", description: "Existing note id to update. Omit to create a note." },
|
|
20
|
-
text: { type: "string", description: "
|
|
20
|
+
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." },
|
|
21
21
|
color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
|
|
22
22
|
}, ["text"]),
|
|
23
23
|
handler: function (args) { return handlers.write(args || {}); },
|
package/lib/ws-schema.js
CHANGED
|
@@ -106,6 +106,7 @@ var schema = {
|
|
|
106
106
|
"subagent_done": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Subagent task completed" },
|
|
107
107
|
"task_started": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Background task started" },
|
|
108
108
|
"task_progress": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Background task progress update" },
|
|
109
|
+
"markdown_edit_present": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Open a user-requested Markdown edit with a before snapshot" },
|
|
109
110
|
|
|
110
111
|
// -----------------------------------------------------------------------
|
|
111
112
|
// Permissions
|
|
@@ -470,6 +470,7 @@ function flattenEvent(notification, state) {
|
|
|
470
470
|
toolName: "Edit",
|
|
471
471
|
input: {
|
|
472
472
|
changes: changeDesc,
|
|
473
|
+
file_paths: changes.map(function(c) { return c.path; }).filter(Boolean),
|
|
473
474
|
file_path: primaryPath || undefined,
|
|
474
475
|
},
|
|
475
476
|
});
|
|
@@ -842,6 +843,8 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
842
843
|
permissionToolName = "mcp__clay-notes__" + permissionToolName;
|
|
843
844
|
} else if (permissionToolName === "send_to_partner" || permissionToolName === "read_partner") {
|
|
844
845
|
permissionToolName = "mcp__clay-sessions__" + permissionToolName;
|
|
846
|
+
} else if (permissionToolName === "present_markdown_edit") {
|
|
847
|
+
permissionToolName = "mcp__clay-documents__" + permissionToolName;
|
|
845
848
|
}
|
|
846
849
|
var permission = canUseTool
|
|
847
850
|
? Promise.resolve(canUseTool(permissionToolName, dynamicParams.arguments || {}, {
|
package/package.json
CHANGED