finch-markdown-editor 0.1.9 → 0.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/dist/codemirror.js +36 -34
- package/dist/index.js +356 -199
- package/dist/panel.css +207 -0
- package/dist/panel.html +33 -2615
- package/dist/panel.js +43 -0
- package/i18n/en-US.json +3 -0
- package/i18n/zh-CN.json +3 -0
- package/package.json +12 -3
package/dist/index.js
CHANGED
|
@@ -1,69 +1,13 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, realpath,
|
|
2
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3
|
+
import { mkdir as mkdir2, readFile as readFile2, realpath, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
6
|
+
import path3 from "node:path";
|
|
8
7
|
import os from "node:os";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"image/jpg": "jpg",
|
|
13
|
-
"image/gif": "gif",
|
|
14
|
-
"image/webp": "webp",
|
|
15
|
-
"image/svg+xml": "svg",
|
|
16
|
-
"image/bmp": "bmp"
|
|
17
|
-
};
|
|
18
|
-
var IMAGE_MIME_BY_EXT = {
|
|
19
|
-
".png": "image/png",
|
|
20
|
-
".jpg": "image/jpeg",
|
|
21
|
-
".jpeg": "image/jpeg",
|
|
22
|
-
".gif": "image/gif",
|
|
23
|
-
".webp": "image/webp",
|
|
24
|
-
".svg": "image/svg+xml",
|
|
25
|
-
".bmp": "image/bmp"
|
|
26
|
-
};
|
|
27
|
-
var MAX_CLIPBOARD_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
28
|
-
async function readClipboardImageDataUrls(ctx, urls) {
|
|
29
|
-
const result2 = {};
|
|
30
|
-
const assetsRoot = await realpath(path.join(ctx.storagePath, "assets"));
|
|
31
|
-
for (const originalUrl of urls.slice(0, 30)) {
|
|
32
|
-
try {
|
|
33
|
-
const parsed = new URL(originalUrl);
|
|
34
|
-
const requested = parsed.protocol === "finch-file:" && parsed.hostname === "local" ? parsed.searchParams.get("path") : null;
|
|
35
|
-
if (!requested) continue;
|
|
36
|
-
const target = await realpath(requested);
|
|
37
|
-
const relative = path.relative(assetsRoot, target);
|
|
38
|
-
const mimeType = IMAGE_MIME_BY_EXT[path.extname(target).toLowerCase()];
|
|
39
|
-
if ((!relative || !relative.startsWith(".." + path.sep) && relative !== "..") && mimeType) {
|
|
40
|
-
const info = await stat(target);
|
|
41
|
-
if (info.size > MAX_CLIPBOARD_INLINE_IMAGE_BYTES) continue;
|
|
42
|
-
result2[originalUrl] = `data:${mimeType};base64,${(await readFile(target)).toString("base64")}`;
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return result2;
|
|
48
|
-
}
|
|
49
|
-
async function openLocalImagePreview(ctx, filePath) {
|
|
50
|
-
if (!path.isAbsolute(filePath) || !IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()]) {
|
|
51
|
-
throw new Error("Unsupported local image file.");
|
|
52
|
-
}
|
|
53
|
-
await ctx.ui.openFilePreview(filePath);
|
|
54
|
-
}
|
|
55
|
-
async function openMarkdownImagePreview(ctx, rawUrl) {
|
|
56
|
-
const url = new URL(rawUrl);
|
|
57
|
-
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
58
|
-
await ctx.browser.open(url.href);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
throw new Error(`Unsupported image URL: ${url.protocol}`);
|
|
62
|
-
}
|
|
63
|
-
var STYLE_SLOT_COUNT = 3;
|
|
64
|
-
function result(message, isError = false) {
|
|
65
|
-
return { content: [{ type: "text", text: message }], isError };
|
|
66
|
-
}
|
|
8
|
+
|
|
9
|
+
// src/document.ts
|
|
10
|
+
import path from "node:path";
|
|
67
11
|
function unwrapTextEnvelope(content) {
|
|
68
12
|
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
69
13
|
const text = bom ? content.slice(1) : content;
|
|
@@ -119,43 +63,14 @@ function documentTitle(markdown, filePath) {
|
|
|
119
63
|
const heading = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
120
64
|
return heading || (filePath ? path.basename(filePath, path.extname(filePath)) : "Untitled article");
|
|
121
65
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
function normalizeStyleSlots(raw) {
|
|
129
|
-
const arr = Array.isArray(raw) ? raw : [];
|
|
130
|
-
const slots = [];
|
|
131
|
-
for (let i = 0; i < STYLE_SLOT_COUNT; i++) {
|
|
132
|
-
const item = arr[i];
|
|
133
|
-
if (item && typeof item === "object" && typeof item.css === "string") {
|
|
134
|
-
slots.push({ css: item.css, label: String(item.label ?? "\u81EA\u5B9A\u4E49\u98CE\u683C") });
|
|
135
|
-
} else {
|
|
136
|
-
slots.push(null);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return slots;
|
|
140
|
-
}
|
|
141
|
-
async function readStyleSlots(ctx) {
|
|
142
|
-
try {
|
|
143
|
-
const raw = await readFile(styleSlotsFile(ctx), "utf8");
|
|
144
|
-
return normalizeStyleSlots(JSON.parse(raw));
|
|
145
|
-
} catch {
|
|
146
|
-
return normalizeStyleSlots([]);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async function writeStyleSlot(ctx, slot, value) {
|
|
150
|
-
const slots = await readStyleSlots(ctx);
|
|
151
|
-
slots[slot] = value;
|
|
152
|
-
await mkdir(ctx.storagePath, { recursive: true });
|
|
153
|
-
await writeFile(styleSlotsFile(ctx), JSON.stringify(slots), "utf8");
|
|
154
|
-
return slots;
|
|
155
|
-
}
|
|
66
|
+
|
|
67
|
+
// src/drafts.ts
|
|
68
|
+
import { createHash } from "node:crypto";
|
|
69
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
70
|
+
import path2 from "node:path";
|
|
156
71
|
function draftPathFor(ctx, sourcePath) {
|
|
157
72
|
const digest = createHash("sha256").update(sourcePath).digest("hex").slice(0, 32);
|
|
158
|
-
return
|
|
73
|
+
return path2.join(ctx.storagePath, "drafts", `${digest}.json`);
|
|
159
74
|
}
|
|
160
75
|
function hashText(text) {
|
|
161
76
|
return createHash("sha256").update(text).digest("hex");
|
|
@@ -171,7 +86,7 @@ async function readDraft(ctx, sourcePath) {
|
|
|
171
86
|
async function writeDraft(ctx, sourcePath, markdown, base) {
|
|
172
87
|
try {
|
|
173
88
|
const file = draftPathFor(ctx, sourcePath);
|
|
174
|
-
await mkdir(
|
|
89
|
+
await mkdir(path2.dirname(file), { recursive: true });
|
|
175
90
|
const entry = { path: sourcePath, markdown, savedAt: Date.now() };
|
|
176
91
|
if (typeof base === "string") entry.baseHash = hashText(base);
|
|
177
92
|
await writeFile(file, JSON.stringify(entry), "utf8");
|
|
@@ -216,23 +131,165 @@ async function readFileWithDraft(ctx, sourcePath) {
|
|
|
216
131
|
if (diskMatchesBaseline) return { markdown: draft.markdown, diskMarkdown, draftRestored: true, draftConflict: false };
|
|
217
132
|
return { markdown: diskMarkdown, diskMarkdown, draftRestored: false, draftConflict: true };
|
|
218
133
|
}
|
|
134
|
+
|
|
135
|
+
// src/renderer.ts
|
|
136
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
137
|
+
import { spawn } from "node:child_process";
|
|
138
|
+
import { fileURLToPath } from "node:url";
|
|
139
|
+
var BMMD_BIN_PATH = fileURLToPath(new URL("./bmmd/bin/bmmd.mjs", import.meta.url));
|
|
140
|
+
async function runBmmd(args, input) {
|
|
141
|
+
const binPath = BMMD_BIN_PATH;
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const child = spawn(process.execPath, [binPath, ...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
144
|
+
let output = "";
|
|
145
|
+
let errors = "";
|
|
146
|
+
child.stdout.setEncoding("utf8");
|
|
147
|
+
child.stderr.setEncoding("utf8");
|
|
148
|
+
child.stdout.on("data", (chunk) => {
|
|
149
|
+
output += chunk;
|
|
150
|
+
});
|
|
151
|
+
child.stderr.on("data", (chunk) => {
|
|
152
|
+
errors += chunk;
|
|
153
|
+
});
|
|
154
|
+
child.on("error", reject);
|
|
155
|
+
child.on("close", (code) => {
|
|
156
|
+
if (code === 0) resolve(output);
|
|
157
|
+
else reject(new Error(errors.trim() || `bmmd exited with code ${code}`));
|
|
158
|
+
});
|
|
159
|
+
child.stdin.end(input, "utf8");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
var FINCH_FILE_IMAGE_RE = /finch-file:\/\/local\?path=[^\s)"']+/g;
|
|
163
|
+
var FINCH_IMAGE_PLACEHOLDER_ORIGIN = "https://finch-local.invalid/markdown-image/";
|
|
164
|
+
function substituteFinchFileImagesForBm(markdown) {
|
|
165
|
+
const urls = /* @__PURE__ */ new Map();
|
|
166
|
+
let sequence = 0;
|
|
167
|
+
const substituted = markdown.replace(FINCH_FILE_IMAGE_RE, (originalUrl) => {
|
|
168
|
+
const placeholder = `${FINCH_IMAGE_PLACEHOLDER_ORIGIN}${createHash2("sha256").update(`${originalUrl}:${sequence++}`).digest("hex")}`;
|
|
169
|
+
urls.set(placeholder, originalUrl);
|
|
170
|
+
return placeholder;
|
|
171
|
+
});
|
|
172
|
+
return { markdown: substituted, urls };
|
|
173
|
+
}
|
|
174
|
+
async function renderWithBm(markdown, markdownStyle, customCss) {
|
|
175
|
+
const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
|
|
176
|
+
if (customCss && customCss.trim()) args.push("--custom-css", customCss);
|
|
177
|
+
const prepared = substituteFinchFileImagesForBm(markdown);
|
|
178
|
+
let html = await runBmmd(args, prepared.markdown);
|
|
179
|
+
for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
|
|
180
|
+
return html;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/index.ts
|
|
184
|
+
var PASTE_IMAGE_EXT = {
|
|
185
|
+
"image/png": "png",
|
|
186
|
+
"image/jpeg": "jpg",
|
|
187
|
+
"image/jpg": "jpg",
|
|
188
|
+
"image/gif": "gif",
|
|
189
|
+
"image/webp": "webp",
|
|
190
|
+
"image/svg+xml": "svg",
|
|
191
|
+
"image/bmp": "bmp"
|
|
192
|
+
};
|
|
193
|
+
var IMAGE_MIME_BY_EXT = {
|
|
194
|
+
".png": "image/png",
|
|
195
|
+
".jpg": "image/jpeg",
|
|
196
|
+
".jpeg": "image/jpeg",
|
|
197
|
+
".gif": "image/gif",
|
|
198
|
+
".webp": "image/webp",
|
|
199
|
+
".svg": "image/svg+xml",
|
|
200
|
+
".bmp": "image/bmp"
|
|
201
|
+
};
|
|
202
|
+
var MAX_CLIPBOARD_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
203
|
+
async function readClipboardImageDataUrls(ctx, urls) {
|
|
204
|
+
const result2 = {};
|
|
205
|
+
const assetsRoot = await realpath(path3.join(ctx.storagePath, "assets"));
|
|
206
|
+
for (const originalUrl of urls.slice(0, 30)) {
|
|
207
|
+
try {
|
|
208
|
+
const parsed = new URL(originalUrl);
|
|
209
|
+
const requested = parsed.protocol === "finch-file:" && parsed.hostname === "local" ? parsed.searchParams.get("path") : null;
|
|
210
|
+
if (!requested) continue;
|
|
211
|
+
const target = await realpath(requested);
|
|
212
|
+
const relative = path3.relative(assetsRoot, target);
|
|
213
|
+
const mimeType = IMAGE_MIME_BY_EXT[path3.extname(target).toLowerCase()];
|
|
214
|
+
if ((!relative || !relative.startsWith(".." + path3.sep) && relative !== "..") && mimeType) {
|
|
215
|
+
const info = await stat(target);
|
|
216
|
+
if (info.size > MAX_CLIPBOARD_INLINE_IMAGE_BYTES) continue;
|
|
217
|
+
result2[originalUrl] = `data:${mimeType};base64,${(await readFile2(target)).toString("base64")}`;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result2;
|
|
223
|
+
}
|
|
224
|
+
async function openLocalImagePreview(ctx, filePath) {
|
|
225
|
+
if (!path3.isAbsolute(filePath) || !IMAGE_MIME_BY_EXT[path3.extname(filePath).toLowerCase()]) {
|
|
226
|
+
throw new Error("Unsupported local image file.");
|
|
227
|
+
}
|
|
228
|
+
await ctx.ui.openFilePreview(filePath);
|
|
229
|
+
}
|
|
230
|
+
async function openMarkdownImagePreview(ctx, rawUrl) {
|
|
231
|
+
const url = new URL(rawUrl);
|
|
232
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
233
|
+
await ctx.browser.open(url.href);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Unsupported image URL: ${url.protocol}`);
|
|
237
|
+
}
|
|
238
|
+
var STYLE_SLOT_COUNT = 3;
|
|
239
|
+
function result(message, isError = false) {
|
|
240
|
+
return { content: [{ type: "text", text: message }], isError };
|
|
241
|
+
}
|
|
242
|
+
function stateFile(ctx) {
|
|
243
|
+
return path3.join(ctx.storagePath, "state.json");
|
|
244
|
+
}
|
|
245
|
+
function styleSlotsFile(ctx) {
|
|
246
|
+
return path3.join(ctx.storagePath, "style-slots.json");
|
|
247
|
+
}
|
|
248
|
+
function normalizeStyleSlots(raw) {
|
|
249
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
250
|
+
const slots = [];
|
|
251
|
+
for (let i = 0; i < STYLE_SLOT_COUNT; i++) {
|
|
252
|
+
const item = arr[i];
|
|
253
|
+
if (item && typeof item === "object" && typeof item.css === "string") {
|
|
254
|
+
slots.push({ css: item.css, label: String(item.label ?? "\u81EA\u5B9A\u4E49\u98CE\u683C") });
|
|
255
|
+
} else {
|
|
256
|
+
slots.push(null);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return slots;
|
|
260
|
+
}
|
|
261
|
+
async function readStyleSlots(ctx) {
|
|
262
|
+
try {
|
|
263
|
+
const raw = await readFile2(styleSlotsFile(ctx), "utf8");
|
|
264
|
+
return normalizeStyleSlots(JSON.parse(raw));
|
|
265
|
+
} catch {
|
|
266
|
+
return normalizeStyleSlots([]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function writeStyleSlot(ctx, slot, value) {
|
|
270
|
+
const slots = await readStyleSlots(ctx);
|
|
271
|
+
slots[slot] = value;
|
|
272
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
273
|
+
await writeFile2(styleSlotsFile(ctx), JSON.stringify(slots), "utf8");
|
|
274
|
+
return slots;
|
|
275
|
+
}
|
|
219
276
|
function sessionBucketKey(panel) {
|
|
220
277
|
return panel.sessionId || "__global__";
|
|
221
278
|
}
|
|
222
279
|
async function readLastPathState(ctx) {
|
|
223
280
|
try {
|
|
224
|
-
const raw = await
|
|
281
|
+
const raw = await readFile2(stateFile(ctx), "utf8");
|
|
225
282
|
return JSON.parse(raw);
|
|
226
283
|
} catch {
|
|
227
284
|
return {};
|
|
228
285
|
}
|
|
229
286
|
}
|
|
230
287
|
function resolveRecentScope(state, cwd, sessionId, spaceId) {
|
|
231
|
-
if (
|
|
288
|
+
if (path3.isAbsolute(cwd)) {
|
|
232
289
|
if (sessionId && !spaceId) state.homePath = cwd;
|
|
233
290
|
return { scope: cwd };
|
|
234
291
|
}
|
|
235
|
-
if (!cwd && !sessionId && !spaceId &&
|
|
292
|
+
if (!cwd && !sessionId && !spaceId && path3.isAbsolute(state.homePath ?? "")) {
|
|
236
293
|
return { scope: state.homePath, fallbackCwd: state.homePath };
|
|
237
294
|
}
|
|
238
295
|
return {};
|
|
@@ -241,12 +298,33 @@ function addRecentPath(state, scope, sourcePath) {
|
|
|
241
298
|
const existing = state.recentPathsByScope?.[scope] ?? [];
|
|
242
299
|
state.recentPathsByScope = {
|
|
243
300
|
...state.recentPathsByScope,
|
|
244
|
-
[scope]: [sourcePath, ...existing].filter((value, index, values) =>
|
|
301
|
+
[scope]: [sourcePath, ...existing].filter((value, index, values) => path3.isAbsolute(value) && values.indexOf(value) === index).slice(0, 50)
|
|
245
302
|
};
|
|
246
303
|
}
|
|
304
|
+
function pathBelongsTo(root, target) {
|
|
305
|
+
const relative = path3.relative(root, target);
|
|
306
|
+
return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
|
|
307
|
+
}
|
|
308
|
+
async function resolveDocumentScope(ctx, sourcePath, fallbackCwd) {
|
|
309
|
+
const spaces = await ctx.spaces.list().catch(() => []);
|
|
310
|
+
const matches = spaces.filter((space2) => path3.isAbsolute(space2.directoryPath ?? "") && pathBelongsTo(space2.directoryPath, sourcePath)).sort((a, b) => (b.directoryPath?.length ?? 0) - (a.directoryPath?.length ?? 0));
|
|
311
|
+
const space = matches[0];
|
|
312
|
+
if (space?.directoryPath) {
|
|
313
|
+
const spaceName = space.name || space.alias || path3.basename(space.directoryPath);
|
|
314
|
+
return { scope: space.directoryPath, spaceId: space.id, spaceName, scopeLabel: spaceName, scopeKind: "space" };
|
|
315
|
+
}
|
|
316
|
+
const workspaceRoot = ctx.workspace.projectPath;
|
|
317
|
+
if (workspaceRoot && pathBelongsTo(workspaceRoot, sourcePath)) {
|
|
318
|
+
return { scope: workspaceRoot, scopeKind: "workspace" };
|
|
319
|
+
}
|
|
320
|
+
if (fallbackCwd && path3.isAbsolute(fallbackCwd) && pathBelongsTo(fallbackCwd, sourcePath)) {
|
|
321
|
+
return { scope: fallbackCwd, scopeKind: "workspace" };
|
|
322
|
+
}
|
|
323
|
+
return { scope: path3.dirname(sourcePath), scopeKind: "external" };
|
|
324
|
+
}
|
|
247
325
|
async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
|
|
248
326
|
try {
|
|
249
|
-
await
|
|
327
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
250
328
|
const state = await readLastPathState(ctx);
|
|
251
329
|
const resolved = resolveRecentScope(state, cwd, sessionId, spaceId);
|
|
252
330
|
if (resolved.scope) {
|
|
@@ -254,7 +332,7 @@ async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
|
|
|
254
332
|
const panelPath = state.panels?.[panel.id];
|
|
255
333
|
if (panelPath) addRecentPath(state, resolved.scope, panelPath);
|
|
256
334
|
}
|
|
257
|
-
await
|
|
335
|
+
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
258
336
|
return resolved;
|
|
259
337
|
} catch (error) {
|
|
260
338
|
ctx.logger.warn(`Could not persist panel recent scope: ${String(error)}`);
|
|
@@ -263,24 +341,28 @@ async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
|
|
|
263
341
|
}
|
|
264
342
|
async function rememberLastPath(ctx, panel, sourcePath) {
|
|
265
343
|
try {
|
|
266
|
-
await
|
|
344
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
267
345
|
const state = await readLastPathState(ctx);
|
|
268
346
|
state.panels = { ...state.panels, [panel.id]: sourcePath };
|
|
269
347
|
state.sessions = { ...state.sessions, [sessionBucketKey(panel)]: sourcePath };
|
|
270
|
-
|
|
348
|
+
let scope = state.panelRecentScopes?.[panel.id];
|
|
349
|
+
if (!scope && panel.view === "appView") {
|
|
350
|
+
scope = (await resolveDocumentScope(ctx, sourcePath)).scope;
|
|
351
|
+
state.panelRecentScopes = { ...state.panelRecentScopes, [panel.id]: scope };
|
|
352
|
+
}
|
|
271
353
|
if (scope) addRecentPath(state, scope, sourcePath);
|
|
272
|
-
await
|
|
354
|
+
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
273
355
|
} catch (error) {
|
|
274
356
|
ctx.logger.warn(`Could not persist last-opened path: ${String(error)}`);
|
|
275
357
|
}
|
|
276
358
|
}
|
|
277
359
|
async function rememberRecentPath(ctx, sourcePath, panel) {
|
|
278
360
|
try {
|
|
279
|
-
await
|
|
361
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
280
362
|
const state = await readLastPathState(ctx);
|
|
281
363
|
const scope = panel ? state.panelRecentScopes?.[panel.id] : resolveRecentScope(state, ctx.session.cwd ?? "", ctx.session.id ?? "", ctx.session.spaceId ?? "").scope;
|
|
282
364
|
if (scope) addRecentPath(state, scope, sourcePath);
|
|
283
|
-
await
|
|
365
|
+
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
284
366
|
} catch (error) {
|
|
285
367
|
ctx.logger.warn(`Could not persist recent path: ${String(error)}`);
|
|
286
368
|
}
|
|
@@ -321,18 +403,18 @@ async function collectRecentDocuments(ctx, requestedCwd, sessionId, spaceId) {
|
|
|
321
403
|
if (!resolved.scope) return { documents: [] };
|
|
322
404
|
const scope = resolved.scope;
|
|
323
405
|
const candidates = (state.recentPathsByScope?.[scope] ?? []).filter(
|
|
324
|
-
(value, index, values) => typeof value === "string" &&
|
|
406
|
+
(value, index, values) => typeof value === "string" && path3.isAbsolute(value) && isMarkdownPath(value) && values.indexOf(value) === index
|
|
325
407
|
);
|
|
326
408
|
const documents = await Promise.all(
|
|
327
409
|
candidates.map(async (filePath) => {
|
|
328
410
|
try {
|
|
329
411
|
const info = await stat(filePath);
|
|
330
412
|
if (!info.isFile()) return void 0;
|
|
331
|
-
const markdown = await
|
|
332
|
-
const fileName =
|
|
413
|
+
const markdown = await readFile2(filePath, "utf8");
|
|
414
|
+
const fileName = path3.basename(filePath);
|
|
333
415
|
return {
|
|
334
416
|
path: filePath,
|
|
335
|
-
relativePath:
|
|
417
|
+
relativePath: path3.relative(scope, filePath),
|
|
336
418
|
fileName,
|
|
337
419
|
title: deriveTitle(markdown, fileName.replace(/\.[^.]+$/, "")),
|
|
338
420
|
preview: derivePreview(markdown),
|
|
@@ -348,13 +430,39 @@ async function collectRecentDocuments(ctx, requestedCwd, sessionId, spaceId) {
|
|
|
348
430
|
fallbackCwd: resolved.fallbackCwd
|
|
349
431
|
};
|
|
350
432
|
}
|
|
433
|
+
async function collectLibraryDocuments(ctx) {
|
|
434
|
+
const state = await readLastPathState(ctx);
|
|
435
|
+
const candidates = Object.values(state.recentPathsByScope ?? {}).flat().concat(Object.values(state.panels ?? {})).filter((value, index, values) => path3.isAbsolute(value) && isMarkdownPath(value) && values.indexOf(value) === index);
|
|
436
|
+
const documents = await Promise.all(candidates.map(async (filePath) => {
|
|
437
|
+
try {
|
|
438
|
+
const [info, markdown, scope] = await Promise.all([stat(filePath), readFile2(filePath, "utf8"), resolveDocumentScope(ctx, filePath, state.homePath)]);
|
|
439
|
+
if (!info.isFile()) return void 0;
|
|
440
|
+
const fileName = path3.basename(filePath);
|
|
441
|
+
return {
|
|
442
|
+
path: filePath,
|
|
443
|
+
relativePath: path3.relative(scope.scope, filePath),
|
|
444
|
+
fileName,
|
|
445
|
+
title: deriveTitle(markdown, fileName.replace(/\.[^.]+$/, "")),
|
|
446
|
+
preview: derivePreview(markdown),
|
|
447
|
+
modifiedAt: info.mtimeMs,
|
|
448
|
+
spaceId: scope.spaceId,
|
|
449
|
+
spaceName: scope.spaceName,
|
|
450
|
+
scopeLabel: scope.scopeLabel,
|
|
451
|
+
scopeKind: scope.scopeKind
|
|
452
|
+
};
|
|
453
|
+
} catch {
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
}));
|
|
457
|
+
return documents.filter((entry) => Boolean(entry)).sort((a, b) => b.modifiedAt - a.modifiedAt).slice(0, RECENT_LIMIT);
|
|
458
|
+
}
|
|
351
459
|
var livePanelDocuments = /* @__PURE__ */ new Map();
|
|
352
460
|
async function sendDocument(panel, state) {
|
|
353
461
|
livePanelDocuments.set(panel.id, state);
|
|
354
462
|
await panel.postMessage({ type: "document", ...state });
|
|
355
463
|
}
|
|
356
464
|
async function sendLiveDocument(ctx, panel, liveDocument) {
|
|
357
|
-
if (liveDocument.path &&
|
|
465
|
+
if (liveDocument.path && path3.isAbsolute(liveDocument.path)) {
|
|
358
466
|
try {
|
|
359
467
|
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, liveDocument.path);
|
|
360
468
|
await sendDocument(panel, { ...liveDocument, markdown, title: documentTitle(markdown, liveDocument.path), draftRestored, draftConflict, diskMarkdown });
|
|
@@ -369,10 +477,10 @@ function payloadPath(panel) {
|
|
|
369
477
|
const payload = panel.payload;
|
|
370
478
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
|
|
371
479
|
const value = payload.path;
|
|
372
|
-
return typeof value === "string" &&
|
|
480
|
+
return typeof value === "string" && path3.isAbsolute(value) ? value : void 0;
|
|
373
481
|
}
|
|
374
482
|
async function restoreDocument(ctx, panel) {
|
|
375
|
-
if (!panel.sessionId) return false;
|
|
483
|
+
if (!panel.sessionId && panel.view !== "appView") return false;
|
|
376
484
|
const sourcePath = await readLastPath(ctx, panel) ?? payloadPath(panel);
|
|
377
485
|
if (!sourcePath) return false;
|
|
378
486
|
try {
|
|
@@ -386,49 +494,6 @@ async function restoreDocument(ctx, panel) {
|
|
|
386
494
|
return false;
|
|
387
495
|
}
|
|
388
496
|
}
|
|
389
|
-
var BMMD_BIN_PATH = fileURLToPath(new URL("./bmmd/bin/bmmd.mjs", import.meta.url));
|
|
390
|
-
async function runBmmd(args, input) {
|
|
391
|
-
const binPath = BMMD_BIN_PATH;
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
const child = spawn(process.execPath, [binPath, ...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
394
|
-
let output = "";
|
|
395
|
-
let errors = "";
|
|
396
|
-
child.stdout.setEncoding("utf8");
|
|
397
|
-
child.stderr.setEncoding("utf8");
|
|
398
|
-
child.stdout.on("data", (chunk) => {
|
|
399
|
-
output += chunk;
|
|
400
|
-
});
|
|
401
|
-
child.stderr.on("data", (chunk) => {
|
|
402
|
-
errors += chunk;
|
|
403
|
-
});
|
|
404
|
-
child.on("error", reject);
|
|
405
|
-
child.on("close", (code) => {
|
|
406
|
-
if (code === 0) resolve(output);
|
|
407
|
-
else reject(new Error(errors.trim() || `bmmd exited with code ${code}`));
|
|
408
|
-
});
|
|
409
|
-
child.stdin.end(input, "utf8");
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
var FINCH_FILE_IMAGE_RE = /finch-file:\/\/local\?path=[^\s)"']+/g;
|
|
413
|
-
var FINCH_IMAGE_PLACEHOLDER_ORIGIN = "https://finch-local.invalid/markdown-image/";
|
|
414
|
-
function substituteFinchFileImagesForBm(markdown) {
|
|
415
|
-
const urls = /* @__PURE__ */ new Map();
|
|
416
|
-
let sequence = 0;
|
|
417
|
-
const substituted = markdown.replace(FINCH_FILE_IMAGE_RE, (originalUrl) => {
|
|
418
|
-
const placeholder = `${FINCH_IMAGE_PLACEHOLDER_ORIGIN}${createHash("sha256").update(`${originalUrl}:${sequence++}`).digest("hex")}`;
|
|
419
|
-
urls.set(placeholder, originalUrl);
|
|
420
|
-
return placeholder;
|
|
421
|
-
});
|
|
422
|
-
return { markdown: substituted, urls };
|
|
423
|
-
}
|
|
424
|
-
async function renderWithBm(markdown, markdownStyle, customCss) {
|
|
425
|
-
const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
|
|
426
|
-
if (customCss && customCss.trim()) args.push("--custom-css", customCss);
|
|
427
|
-
const prepared = substituteFinchFileImagesForBm(markdown);
|
|
428
|
-
let html = await runBmmd(args, prepared.markdown);
|
|
429
|
-
for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
|
|
430
|
-
return html;
|
|
431
|
-
}
|
|
432
497
|
var panelWatchers = /* @__PURE__ */ new Map();
|
|
433
498
|
var lastPanel;
|
|
434
499
|
function stopWatching(panelId) {
|
|
@@ -447,7 +512,7 @@ function watchSource(ctx, panel, sourcePath) {
|
|
|
447
512
|
if (entry.timer) clearTimeout(entry.timer);
|
|
448
513
|
entry.timer = setTimeout(async () => {
|
|
449
514
|
try {
|
|
450
|
-
const markdown = await
|
|
515
|
+
const markdown = await readFile2(sourcePath, "utf8");
|
|
451
516
|
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
452
517
|
} catch (error) {
|
|
453
518
|
ctx.logger.warn(`Source refresh failed: ${String(error)}`);
|
|
@@ -496,16 +561,98 @@ async function revealInFileManager(ctx, targetPath) {
|
|
|
496
561
|
const info = await stat(targetPath).catch(() => void 0);
|
|
497
562
|
const isFile = info?.isFile() ?? false;
|
|
498
563
|
if (process.platform === "darwin") {
|
|
499
|
-
|
|
564
|
+
spawn2("open", isFile ? ["-R", targetPath] : [targetPath], { stdio: "ignore", detached: true }).unref();
|
|
500
565
|
} else if (process.platform === "win32") {
|
|
501
|
-
|
|
566
|
+
spawn2("explorer", isFile ? [`/select,${targetPath}`] : [targetPath], { stdio: "ignore", detached: true }).unref();
|
|
502
567
|
} else {
|
|
503
|
-
|
|
568
|
+
spawn2("xdg-open", [isFile ? path3.dirname(targetPath) : targetPath], { stdio: "ignore", detached: true }).unref();
|
|
504
569
|
}
|
|
505
570
|
} catch (error) {
|
|
506
571
|
ctx.logger.warn(`Could not open file manager for ${targetPath}: ${String(error)}`);
|
|
507
572
|
}
|
|
508
573
|
}
|
|
574
|
+
async function readRewriteSession(ctx, sourcePath) {
|
|
575
|
+
const state = await readLastPathState(ctx);
|
|
576
|
+
const id = state.rewriteSessions?.[sourcePath];
|
|
577
|
+
if (!id) return void 0;
|
|
578
|
+
const session = await ctx.sessions.get(id).catch(() => void 0);
|
|
579
|
+
return session ? id : void 0;
|
|
580
|
+
}
|
|
581
|
+
async function rememberRewriteSession(ctx, sourcePath, sessionId) {
|
|
582
|
+
try {
|
|
583
|
+
const state = await readLastPathState(ctx);
|
|
584
|
+
state.rewriteSessions = { ...state.rewriteSessions, [sourcePath]: sessionId };
|
|
585
|
+
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
586
|
+
} catch (error) {
|
|
587
|
+
ctx.logger.warn(`Could not persist rewrite session: ${String(error)}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function startRewriteSession(ctx, panel, message) {
|
|
591
|
+
const sourcePath = String(message.path ?? "").trim();
|
|
592
|
+
const selectedText = String(message.selectedText ?? "").trim();
|
|
593
|
+
const rewriteMode = message.rewriteMode === "continue" ? "continue" : "replace";
|
|
594
|
+
const requirement = String(message.requirement ?? "").trim() || (rewriteMode === "continue" ? "\u81EA\u7136\u5730\u627F\u63A5\u4E0A\u4E0B\u6587\u7EE7\u7EED\u5199\u4F5C" : "\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u3001\u81EA\u7136\uFF0C\u5E76\u4FDD\u6301\u539F\u610F");
|
|
595
|
+
const hasTarget = rewriteMode === "continue" ? !!message.startLine : !!selectedText;
|
|
596
|
+
if (panel.view !== "appView" || !path3.isAbsolute(sourcePath) || !hasTarget) {
|
|
597
|
+
await panel.postMessage({ type: "rewriteSessionFailed", message: "\u6539\u5199\u9700\u8981 App View \u4E2D\u5DF2\u4FDD\u5B58\u7684\u672C\u5730\u6587\u6863\u548C\u9009\u4E2D\u6587\u672C\u6216\u7EED\u5199\u4F4D\u7F6E\u3002" });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const scope = await resolveDocumentScope(ctx, sourcePath);
|
|
601
|
+
let sessionId = await readRewriteSession(ctx, sourcePath);
|
|
602
|
+
if (!sessionId) {
|
|
603
|
+
const session = await ctx.sessions.create({
|
|
604
|
+
...scope.spaceId ? { space: { spaceId: scope.spaceId } } : {},
|
|
605
|
+
title: `\u6539\u5199\uFF1A${path3.basename(sourcePath)}`,
|
|
606
|
+
activity: "interactive",
|
|
607
|
+
permissionMode: "acceptCalls"
|
|
608
|
+
});
|
|
609
|
+
sessionId = session.sessionId;
|
|
610
|
+
await rememberRewriteSession(ctx, sourcePath, sessionId);
|
|
611
|
+
}
|
|
612
|
+
const lineText = message.startLine ? `\u4F4D\u7F6E\uFF1A\u7B2C ${message.startLine}${message.endLine && message.endLine !== message.startLine ? `\u2013${message.endLine}` : ""} \u884C\u3002` : "";
|
|
613
|
+
const prompt = rewriteMode === "continue" ? `\u8BF7\u5728\u4E0B\u9762\u8FD9\u4EFD Markdown \u6587\u4EF6\u7684\u6307\u5B9A\u4F4D\u7F6E\u7EED\u5199\u5185\u5BB9\uFF0C\u5E76\u628A\u7ED3\u679C\u5199\u56DE\u6587\u4EF6\u3002
|
|
614
|
+
|
|
615
|
+
\u6587\u4EF6\uFF1A${sourcePath}
|
|
616
|
+
${lineText}
|
|
617
|
+
\u8981\u6C42\uFF1A${requirement}
|
|
618
|
+
|
|
619
|
+
\u8BF7\u8BFB\u53D6\u6587\u4EF6\u5F53\u524D\u5185\u5BB9\uFF1A\u7B2C ${message.startLine} \u884C\u5F53\u524D\u662F\u4E00\u4E2A\u7A7A\u884C\uFF0C\u8BF7\u628A\u7EED\u5199\u7684\u65B0\u5185\u5BB9\u76F4\u63A5\u5199\u5165\u8FD9\u4E00\u884C\u672C\u8EAB\uFF08\u628A\u8FD9\u4E2A\u7A7A\u884C\u66FF\u6362\u6210\u65B0\u5185\u5BB9\uFF09\uFF0C\u4E0D\u8981\u5728\u5B83\u524D\u540E\u989D\u5916\u63D2\u5165\u65B0\u7684\u7A7A\u884C\uFF0C\u4E5F\u4E0D\u8981\u6539\u52A8\u7B2C ${message.startLine} \u884C\u4E4B\u5916\u7684\u539F\u6709\u5185\u5BB9\uFF1B\u8C03\u7528 markdown_editor_document \u7684 apply\uFF0C\u4EE5 edits \u505A\u7CBE\u786E\u7684\u5C40\u90E8\u66FF\u6362\u3002\u4E0D\u8981\u53EA\u7ED9\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u91CD\u53D1\u5168\u6587\uFF1B\u5B8C\u6210\u5199\u56DE\u540E\u7B80\u77ED\u8BF4\u660E\u3002` : `\u8BF7\u76F4\u63A5\u6539\u5199\u4E0B\u9762\u8FD9\u6BB5 Markdown\uFF0C\u5E76\u628A\u7ED3\u679C\u5199\u56DE\u6587\u4EF6\u3002
|
|
620
|
+
|
|
621
|
+
\u6587\u4EF6\uFF1A${sourcePath}
|
|
622
|
+
${lineText}
|
|
623
|
+
\u8981\u6C42\uFF1A${requirement}
|
|
624
|
+
|
|
625
|
+
\u539F\u6587\uFF1A
|
|
626
|
+
${selectedText}
|
|
627
|
+
|
|
628
|
+
\u8BF7\u8BFB\u53D6\u6587\u4EF6\u5F53\u524D\u5185\u5BB9\uFF0C\u8C03\u7528 markdown_editor_document \u7684 apply\uFF0C\u4EE5 edits \u505A\u552F\u4E00\u3001\u7CBE\u786E\u7684\u5C40\u90E8\u66FF\u6362\u3002\u4E0D\u8981\u53EA\u7ED9\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u91CD\u53D1\u5168\u6587\uFF1B\u5B8C\u6210\u5199\u56DE\u540E\u7B80\u77ED\u8BF4\u660E\u3002`;
|
|
629
|
+
const receipt = await ctx.sessions.send(sessionId, {
|
|
630
|
+
text: prompt,
|
|
631
|
+
idempotencyKey: `rewrite-${createHash3("sha256").update(`${sourcePath}:${rewriteMode}:${selectedText}:${requirement}:${message.startLine ?? ""}:${Date.now()}`).digest("hex")}`
|
|
632
|
+
});
|
|
633
|
+
if (receipt.state === "rejected") {
|
|
634
|
+
await panel.postMessage({ type: "rewriteSessionFailed", message: "\u6539\u5199\u4F1A\u8BDD\u961F\u5217\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
await panel.postMessage({
|
|
638
|
+
type: "rewriteSessionStarted",
|
|
639
|
+
sessionId,
|
|
640
|
+
spaceName: scope.spaceName,
|
|
641
|
+
title: `${rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199"}\uFF1A${path3.basename(sourcePath)}`,
|
|
642
|
+
startLine: message.startLine,
|
|
643
|
+
endLine: message.endLine ?? message.startLine,
|
|
644
|
+
rewriteMode
|
|
645
|
+
});
|
|
646
|
+
void ctx.sessions.waitForTurn(sessionId, receipt.turnId, { timeoutMs: 6e5 }).then(async (result2) => {
|
|
647
|
+
const verb = rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199";
|
|
648
|
+
await panel.postMessage({
|
|
649
|
+
type: result2.state === "completed" ? "rewriteSessionFinished" : "rewriteSessionFailed",
|
|
650
|
+
sessionId,
|
|
651
|
+
message: result2.state === "completed" ? `${verb}\u5DF2\u5B8C\u6210\u3002` : result2.state === "timeout" ? `${verb}\u4ECD\u5728\u4F1A\u8BDD\u4E2D\u7EE7\u7EED\u3002` : `${verb}\u4F1A\u8BDD\u672A\u5B8C\u6210\u3002`
|
|
652
|
+
}).catch(() => {
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
}
|
|
509
656
|
async function handleMessage(ctx, panel, raw) {
|
|
510
657
|
const message = raw;
|
|
511
658
|
switch (message.type) {
|
|
@@ -555,7 +702,8 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
555
702
|
try {
|
|
556
703
|
const handle = ctx.ui.pickFile({
|
|
557
704
|
title: "\u9009\u62E9Markdown\u6587\u4EF6",
|
|
558
|
-
filter: { extensions: [".md", ".markdown"] }
|
|
705
|
+
filter: { extensions: [".md", ".markdown"] },
|
|
706
|
+
allowSpaceSwitch: panel.view === "appView"
|
|
559
707
|
});
|
|
560
708
|
ctx.logger.info("ctx.ui.pickFile() call returned a handle, awaiting resolution\u2026");
|
|
561
709
|
const picked = await handle;
|
|
@@ -581,7 +729,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
581
729
|
}
|
|
582
730
|
case "loadPath": {
|
|
583
731
|
const sourcePath = String(message.path ?? "").trim();
|
|
584
|
-
if (!
|
|
732
|
+
if (!path3.isAbsolute(sourcePath)) {
|
|
585
733
|
await panel.postMessage({ type: "error", message: "Please provide an absolute Markdown path." });
|
|
586
734
|
return;
|
|
587
735
|
}
|
|
@@ -597,7 +745,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
597
745
|
}
|
|
598
746
|
case "watchPath": {
|
|
599
747
|
const sourcePath = String(message.path ?? "").trim();
|
|
600
|
-
if (!
|
|
748
|
+
if (!path3.isAbsolute(sourcePath)) return;
|
|
601
749
|
watchSource(ctx, panel, sourcePath);
|
|
602
750
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
603
751
|
await panel.postMessage({ type: "watchStarted", path: sourcePath });
|
|
@@ -614,7 +762,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
614
762
|
}
|
|
615
763
|
case "openPath": {
|
|
616
764
|
const targetPath = String(message.path ?? "").trim();
|
|
617
|
-
if (targetPath &&
|
|
765
|
+
if (targetPath && path3.isAbsolute(targetPath)) await revealInFileManager(ctx, targetPath);
|
|
618
766
|
return;
|
|
619
767
|
}
|
|
620
768
|
case "goHome": {
|
|
@@ -627,20 +775,29 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
627
775
|
const sessionId = String(message.sessionId ?? "").trim();
|
|
628
776
|
const spaceId = String(message.spaceId ?? "").trim();
|
|
629
777
|
try {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
778
|
+
if (panel.view === "appView") {
|
|
779
|
+
const documents = await collectLibraryDocuments(ctx);
|
|
780
|
+
await panel.postMessage({ type: "recentDocuments", cwd, documents, library: true });
|
|
781
|
+
} else {
|
|
782
|
+
await rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId);
|
|
783
|
+
const recent = await collectRecentDocuments(ctx, cwd, sessionId, spaceId);
|
|
784
|
+
await panel.postMessage({ type: "recentDocuments", cwd, documents: recent.documents, fallbackCwd: recent.fallbackCwd });
|
|
785
|
+
}
|
|
633
786
|
} catch (error) {
|
|
634
787
|
ctx.logger.warn(`Could not collect recent documents: ${String(error)}`);
|
|
635
|
-
await panel.postMessage({ type: "recentDocuments", cwd, documents: [] });
|
|
788
|
+
await panel.postMessage({ type: "recentDocuments", cwd, documents: [], library: panel.view === "appView" });
|
|
636
789
|
}
|
|
637
790
|
return;
|
|
638
791
|
}
|
|
792
|
+
case "requestRewrite": {
|
|
793
|
+
await startRewriteSession(ctx, panel, message);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
639
796
|
case "saveMarkdown": {
|
|
640
797
|
const sourcePath = String(message.path ?? "").trim();
|
|
641
|
-
if (!
|
|
798
|
+
if (!path3.isAbsolute(sourcePath)) return;
|
|
642
799
|
try {
|
|
643
|
-
await
|
|
800
|
+
await writeFile2(sourcePath, String(message.markdown ?? ""), "utf8");
|
|
644
801
|
cancelPendingDraftWrite(panel.id);
|
|
645
802
|
await deleteDraft(ctx, sourcePath);
|
|
646
803
|
await panel.postMessage({ type: "savedMarkdown", path: sourcePath, requestId: message.requestId });
|
|
@@ -651,14 +808,14 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
651
808
|
}
|
|
652
809
|
case "saveDraft": {
|
|
653
810
|
const sourcePath = String(message.path ?? "").trim();
|
|
654
|
-
if (!
|
|
811
|
+
if (!path3.isAbsolute(sourcePath)) return;
|
|
655
812
|
const base = typeof message.base === "string" ? message.base : void 0;
|
|
656
813
|
scheduleDraftWrite(ctx, panel.id, sourcePath, String(message.markdown ?? ""), base);
|
|
657
814
|
return;
|
|
658
815
|
}
|
|
659
816
|
case "discardDraft": {
|
|
660
817
|
const sourcePath = String(message.path ?? "").trim();
|
|
661
|
-
if (!
|
|
818
|
+
if (!path3.isAbsolute(sourcePath)) return;
|
|
662
819
|
cancelPendingDraftWrite(panel.id);
|
|
663
820
|
await deleteDraft(ctx, sourcePath);
|
|
664
821
|
return;
|
|
@@ -694,14 +851,14 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
694
851
|
case "applyReplacement": {
|
|
695
852
|
const sourcePath = String(message.path ?? "").trim();
|
|
696
853
|
const markdown = String(message.markdown ?? "");
|
|
697
|
-
if (!sourcePath || !
|
|
854
|
+
if (!sourcePath || !path3.isAbsolute(sourcePath)) {
|
|
698
855
|
await panel.postMessage({ type: "error", message: "Pasted documents can be revised in the editor, but source-file apply needs an absolute path." });
|
|
699
856
|
return;
|
|
700
857
|
}
|
|
701
858
|
try {
|
|
702
|
-
const current = await
|
|
859
|
+
const current = await readFile2(sourcePath, "utf8");
|
|
703
860
|
const appliedMarkdown = preserveTextEnvelope(current, markdown);
|
|
704
|
-
await
|
|
861
|
+
await writeFile2(sourcePath, appliedMarkdown, "utf8");
|
|
705
862
|
await rememberRecentPath(ctx, sourcePath, panel);
|
|
706
863
|
await panel.postMessage({ type: "applied", path: sourcePath, title: documentTitle(appliedMarkdown, sourcePath) });
|
|
707
864
|
} catch (error) {
|
|
@@ -727,12 +884,12 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
727
884
|
try {
|
|
728
885
|
const ext = PASTE_IMAGE_EXT[String(message.mimeType ?? "").toLowerCase()] ?? "png";
|
|
729
886
|
const buffer = Buffer.from(dataBase64, "base64");
|
|
730
|
-
const digest =
|
|
731
|
-
const dir =
|
|
732
|
-
await
|
|
733
|
-
const targetPath =
|
|
887
|
+
const digest = createHash3("sha256").update(buffer).digest("hex").slice(0, 20);
|
|
888
|
+
const dir = path3.join(ctx.storagePath, "assets");
|
|
889
|
+
await mkdir2(dir, { recursive: true });
|
|
890
|
+
const targetPath = path3.join(dir, `${digest}.${ext}`);
|
|
734
891
|
const alreadyExists = await stat(targetPath).then(() => true).catch(() => false);
|
|
735
|
-
if (!alreadyExists) await
|
|
892
|
+
if (!alreadyExists) await writeFile2(targetPath, buffer);
|
|
736
893
|
const url = `finch-file://local?path=${encodeURIComponent(targetPath)}`;
|
|
737
894
|
await panel.postMessage({ type: "pastedImage", requestId: message.requestId, url, path: targetPath });
|
|
738
895
|
} catch (error) {
|
|
@@ -765,15 +922,15 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
765
922
|
}
|
|
766
923
|
try {
|
|
767
924
|
const sourcePath = String(message.path ?? "").trim();
|
|
768
|
-
let dir =
|
|
925
|
+
let dir = path3.isAbsolute(sourcePath) ? path3.dirname(sourcePath) : "";
|
|
769
926
|
if (!dir) {
|
|
770
|
-
const downloads =
|
|
927
|
+
const downloads = path3.join(os.homedir(), "Downloads");
|
|
771
928
|
dir = await stat(downloads).then((s) => s.isDirectory()).catch(() => false) ? downloads : ctx.storagePath;
|
|
772
929
|
}
|
|
773
930
|
const rawName = String(message.fileName ?? "").trim() || documentTitle(String(message.markdown ?? ""), sourcePath || void 0);
|
|
774
931
|
const safeName = rawName.replace(/[\\/:*?"<>|]/g, " ").trim().slice(0, 120) || "article";
|
|
775
|
-
const targetPath =
|
|
776
|
-
await
|
|
932
|
+
const targetPath = path3.join(dir, `${safeName}.${ext}`);
|
|
933
|
+
await writeFile2(targetPath, Buffer.from(dataBase64, "base64"));
|
|
777
934
|
await panel.postMessage({ type: "exported", path: targetPath, requestId: message.requestId });
|
|
778
935
|
} catch (error) {
|
|
779
936
|
await panel.postMessage({ type: "error", message: `Could not export file: ${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -873,7 +1030,7 @@ action:
|
|
|
873
1030
|
const action = String(input.action ?? "");
|
|
874
1031
|
if (action === "open" || action === "create" || action === "apply") {
|
|
875
1032
|
const sourcePath = String(input.path ?? "").trim();
|
|
876
|
-
if (!
|
|
1033
|
+
if (!path3.isAbsolute(sourcePath)) return result("`path` must be an absolute local path.", true);
|
|
877
1034
|
if (action === "open") {
|
|
878
1035
|
try {
|
|
879
1036
|
const { markdown: markdown2, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
|
|
@@ -882,7 +1039,7 @@ action:
|
|
|
882
1039
|
watchSource(ctx, panel, sourcePath);
|
|
883
1040
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
884
1041
|
await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath), draftRestored, draftConflict, diskMarkdown });
|
|
885
|
-
return result(`Opened Markdown Editor for ${
|
|
1042
|
+
return result(`Opened Markdown Editor for ${path3.basename(sourcePath)}.`);
|
|
886
1043
|
} catch (error) {
|
|
887
1044
|
return result(`Could not read ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
888
1045
|
}
|
|
@@ -892,14 +1049,14 @@ action:
|
|
|
892
1049
|
try {
|
|
893
1050
|
const alreadyExists = await stat(sourcePath).then(() => true).catch(() => false);
|
|
894
1051
|
if (alreadyExists) return result(`${sourcePath} already exists. Use action 'open' to view it or 'apply' to revise it instead.`, true);
|
|
895
|
-
await
|
|
896
|
-
await
|
|
1052
|
+
await mkdir2(path3.dirname(sourcePath), { recursive: true });
|
|
1053
|
+
await writeFile2(sourcePath, markdown2, "utf8");
|
|
897
1054
|
const panel = ctx.ui.createPanel({ instanceMode: "single", payload: { path: sourcePath } });
|
|
898
1055
|
await panel.reveal();
|
|
899
1056
|
watchSource(ctx, panel, sourcePath);
|
|
900
1057
|
await rememberLastPath(ctx, panel, sourcePath);
|
|
901
1058
|
await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath) });
|
|
902
|
-
return result(`Created ${
|
|
1059
|
+
return result(`Created ${path3.basename(sourcePath)} and opened it in Markdown Editor.`);
|
|
903
1060
|
} catch (error) {
|
|
904
1061
|
return result(`Could not create ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
905
1062
|
}
|
|
@@ -915,12 +1072,12 @@ action:
|
|
|
915
1072
|
};
|
|
916
1073
|
});
|
|
917
1074
|
try {
|
|
918
|
-
const current = await
|
|
1075
|
+
const current = await readFile2(sourcePath, "utf8");
|
|
919
1076
|
const applied = applyEditSpecs(current, edits);
|
|
920
1077
|
if (!applied.ok) return result(applied.error, true);
|
|
921
|
-
await
|
|
1078
|
+
await writeFile2(sourcePath, applied.content, "utf8");
|
|
922
1079
|
await rememberRecentPath(ctx, sourcePath);
|
|
923
|
-
return result(`Applied ${edits.length} targeted edit${edits.length > 1 ? "s" : ""} to ${
|
|
1080
|
+
return result(`Applied ${edits.length} targeted edit${edits.length > 1 ? "s" : ""} to ${path3.basename(sourcePath)}.`);
|
|
924
1081
|
} catch (error) {
|
|
925
1082
|
return result(`Could not apply edits: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
926
1083
|
}
|
|
@@ -928,10 +1085,10 @@ action:
|
|
|
928
1085
|
const markdown = String(input.markdown ?? "");
|
|
929
1086
|
if (!markdown) return result("`apply` requires either `edits` (targeted replacements) or non-empty `markdown` (full document).", true);
|
|
930
1087
|
try {
|
|
931
|
-
const current = await
|
|
932
|
-
await
|
|
1088
|
+
const current = await readFile2(sourcePath, "utf8");
|
|
1089
|
+
await writeFile2(sourcePath, preserveTextEnvelope(current, markdown), "utf8");
|
|
933
1090
|
await rememberRecentPath(ctx, sourcePath);
|
|
934
|
-
return result(`Applied reviewed Markdown to ${
|
|
1091
|
+
return result(`Applied reviewed Markdown to ${path3.basename(sourcePath)}.`);
|
|
935
1092
|
} catch (error) {
|
|
936
1093
|
return result(`Could not apply revision: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
937
1094
|
}
|