finch-markdown-editor 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/bmmd/bin/bmmd.mjs +2 -2
- package/dist/bmmd/bin/{html-CCefjxd3.mjs → html-BUXlQGSS.mjs} +217 -217
- package/dist/bmmd/bin/remark-highlight-DGWP7eu3.mjs +7 -0
- package/dist/bmmd/bin/text-CtjMWodY.mjs +7 -0
- package/dist/codemirror.js +36 -36
- package/dist/index.js +399 -80
- package/dist/panel.css +64 -8
- package/dist/panel.html +20 -8
- package/dist/panel.js +19 -18
- package/package.json +2 -2
- package/dist/bmmd/bin/lib-BKH2iTnt.mjs +0 -7
- package/dist/bmmd/bin/text-3YfHIFOO.mjs +0 -7
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
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";
|
|
3
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, realpath, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
5
|
import { spawn as spawn2 } from "node:child_process";
|
|
6
6
|
import path3 from "node:path";
|
|
@@ -161,6 +161,39 @@ async function runBmmd(args, input) {
|
|
|
161
161
|
}
|
|
162
162
|
var FINCH_FILE_IMAGE_RE = /finch-file:\/\/local\?path=[^\s)"']+/g;
|
|
163
163
|
var FINCH_IMAGE_PLACEHOLDER_ORIGIN = "https://finch-local.invalid/markdown-image/";
|
|
164
|
+
var MARKDOWN_IMAGE_ALT_RE = /!\[([^\]\n]*)\](?=\()/g;
|
|
165
|
+
function prepareObsidianImageWidths(markdown) {
|
|
166
|
+
const markers = [];
|
|
167
|
+
const prepared = markdown.replace(MARKDOWN_IMAGE_ALT_RE, (whole, rawAlt) => {
|
|
168
|
+
const sized = /^(.*)\|(\d+)(?:x\d+)?$/.exec(rawAlt);
|
|
169
|
+
const width = sized ? Number(sized[2]) : NaN;
|
|
170
|
+
if (!sized || !Number.isFinite(width) || width <= 0) return whole;
|
|
171
|
+
const normalizedWidth = Math.round(width);
|
|
172
|
+
const token = `FINCHIMGSIZE${markers.length}X${normalizedWidth}X`;
|
|
173
|
+
markers.push({ token, width: normalizedWidth, emptyCaption: !sized[1] });
|
|
174
|
+
return `![${sized[1]}${token}]`;
|
|
175
|
+
});
|
|
176
|
+
return { markdown: prepared, markers };
|
|
177
|
+
}
|
|
178
|
+
function applyObsidianImageWidths(html, markers) {
|
|
179
|
+
let rendered = html;
|
|
180
|
+
for (const marker of markers) {
|
|
181
|
+
const widthStyle = `width: ${marker.width}px; max-width: 100%; height: auto;`;
|
|
182
|
+
rendered = rendered.replace(/<img\b[^>]*>/gi, (tag) => {
|
|
183
|
+
if (!tag.includes(marker.token)) return tag;
|
|
184
|
+
const cleanTag = tag.split(marker.token).join("");
|
|
185
|
+
if (/\sstyle="[^"]*"/i.test(cleanTag)) {
|
|
186
|
+
return cleanTag.replace(/\sstyle="([^"]*)"/i, (_styleAttr, style) => ` style="${style} ${widthStyle}"`);
|
|
187
|
+
}
|
|
188
|
+
return cleanTag.replace(/>$/, ` style="${widthStyle}">`);
|
|
189
|
+
});
|
|
190
|
+
if (marker.emptyCaption) {
|
|
191
|
+
rendered = rendered.replace(/<figcaption\b[^>]*>[\s\S]*?<\/figcaption>/gi, (caption) => caption.includes(marker.token) ? "" : caption);
|
|
192
|
+
}
|
|
193
|
+
rendered = rendered.split(marker.token).join("");
|
|
194
|
+
}
|
|
195
|
+
return rendered;
|
|
196
|
+
}
|
|
164
197
|
function substituteFinchFileImagesForBm(markdown) {
|
|
165
198
|
const urls = /* @__PURE__ */ new Map();
|
|
166
199
|
let sequence = 0;
|
|
@@ -174,8 +207,10 @@ function substituteFinchFileImagesForBm(markdown) {
|
|
|
174
207
|
async function renderWithBm(markdown, markdownStyle, customCss) {
|
|
175
208
|
const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
|
|
176
209
|
if (customCss && customCss.trim()) args.push("--custom-css", customCss);
|
|
177
|
-
const
|
|
210
|
+
const sized = prepareObsidianImageWidths(markdown);
|
|
211
|
+
const prepared = substituteFinchFileImagesForBm(sized.markdown);
|
|
178
212
|
let html = await runBmmd(args, prepared.markdown);
|
|
213
|
+
html = applyObsidianImageWidths(html, sized.markers);
|
|
179
214
|
for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
|
|
180
215
|
return html;
|
|
181
216
|
}
|
|
@@ -236,6 +271,8 @@ async function openMarkdownImagePreview(ctx, rawUrl) {
|
|
|
236
271
|
throw new Error(`Unsupported image URL: ${url.protocol}`);
|
|
237
272
|
}
|
|
238
273
|
var STYLE_SLOT_COUNT = 3;
|
|
274
|
+
var WRITING_STYLE_IDS = /* @__PURE__ */ new Set(["kami", "bauhaus", "blueprint", "botanical", "newsprint", "retro", "sketch", "terminal", "custom"]);
|
|
275
|
+
var MAX_CUSTOM_STYLE_CSS_LENGTH = 2e5;
|
|
239
276
|
function result(message, isError = false) {
|
|
240
277
|
return { content: [{ type: "text", text: message }], isError };
|
|
241
278
|
}
|
|
@@ -245,6 +282,39 @@ function stateFile(ctx) {
|
|
|
245
282
|
function styleSlotsFile(ctx) {
|
|
246
283
|
return path3.join(ctx.storagePath, "style-slots.json");
|
|
247
284
|
}
|
|
285
|
+
function writingPreferencesFile(ctx) {
|
|
286
|
+
return path3.join(ctx.storagePath, "writing-preferences.json");
|
|
287
|
+
}
|
|
288
|
+
function normalizeWritingPreferences(raw) {
|
|
289
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
290
|
+
const fontSize = value.fontSize === 16 || value.fontSize === 18 ? value.fontSize : 14;
|
|
291
|
+
const fontFamily = value.fontFamily === "songti" ? "songti" : "rounded";
|
|
292
|
+
const customCss = typeof value.customCss === "string" ? value.customCss.slice(0, MAX_CUSTOM_STYLE_CSS_LENGTH) : "";
|
|
293
|
+
const requestedStyle = typeof value.style === "string" ? value.style : "kami";
|
|
294
|
+
const style = WRITING_STYLE_IDS.has(requestedStyle) && (requestedStyle !== "custom" || customCss) ? requestedStyle : "kami";
|
|
295
|
+
return {
|
|
296
|
+
fontSize,
|
|
297
|
+
fontFamily,
|
|
298
|
+
comfortWriting: value.comfortWriting === true,
|
|
299
|
+
style,
|
|
300
|
+
customCss: style === "custom" ? customCss : "",
|
|
301
|
+
customStyleLabel: style === "custom" && typeof value.customStyleLabel === "string" ? value.customStyleLabel.slice(0, 120) : ""
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function readWritingPreferences(ctx) {
|
|
305
|
+
try {
|
|
306
|
+
const raw = await readFile2(writingPreferencesFile(ctx), "utf8");
|
|
307
|
+
return normalizeWritingPreferences(JSON.parse(raw));
|
|
308
|
+
} catch {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function writeWritingPreferences(ctx, raw) {
|
|
313
|
+
const preferences = normalizeWritingPreferences(raw);
|
|
314
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
315
|
+
await writeFile2(writingPreferencesFile(ctx), JSON.stringify(preferences), "utf8");
|
|
316
|
+
return preferences;
|
|
317
|
+
}
|
|
248
318
|
function normalizeStyleSlots(raw) {
|
|
249
319
|
const arr = Array.isArray(raw) ? raw : [];
|
|
250
320
|
const slots = [];
|
|
@@ -284,6 +354,34 @@ async function readLastPathState(ctx) {
|
|
|
284
354
|
return {};
|
|
285
355
|
}
|
|
286
356
|
}
|
|
357
|
+
async function loadStateForWrite(ctx) {
|
|
358
|
+
let raw;
|
|
359
|
+
try {
|
|
360
|
+
raw = await readFile2(stateFile(ctx), "utf8");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error?.code === "ENOENT") return {};
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
if (!raw.trim()) throw new Error("state file is present but empty");
|
|
366
|
+
const parsed = JSON.parse(raw);
|
|
367
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("state file is not an object");
|
|
368
|
+
return parsed;
|
|
369
|
+
}
|
|
370
|
+
var stateWriteChain = Promise.resolve();
|
|
371
|
+
async function mutateState(ctx, mutate) {
|
|
372
|
+
const run = stateWriteChain.then(async () => {
|
|
373
|
+
const state = await loadStateForWrite(ctx);
|
|
374
|
+
if (await mutate(state) === false) return;
|
|
375
|
+
await mkdir2(ctx.storagePath, { recursive: true });
|
|
376
|
+
const target = stateFile(ctx);
|
|
377
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
378
|
+
await writeFile2(temp, JSON.stringify(state), "utf8");
|
|
379
|
+
await rename(temp, target);
|
|
380
|
+
});
|
|
381
|
+
stateWriteChain = run.catch(() => {
|
|
382
|
+
});
|
|
383
|
+
return run;
|
|
384
|
+
}
|
|
287
385
|
function resolveRecentScope(state, cwd, sessionId, spaceId) {
|
|
288
386
|
if (path3.isAbsolute(cwd)) {
|
|
289
387
|
if (sessionId && !spaceId) state.homePath = cwd;
|
|
@@ -313,26 +411,39 @@ async function resolveDocumentScope(ctx, sourcePath, fallbackCwd) {
|
|
|
313
411
|
const spaceName = space.name || space.alias || path3.basename(space.directoryPath);
|
|
314
412
|
return { scope: space.directoryPath, spaceId: space.id, spaceName, scopeLabel: spaceName, scopeKind: "space" };
|
|
315
413
|
}
|
|
316
|
-
const workspaceRoot = ctx.workspace.projectPath;
|
|
317
|
-
if (workspaceRoot && pathBelongsTo(workspaceRoot, sourcePath)) {
|
|
318
|
-
return { scope: workspaceRoot, scopeKind: "workspace" };
|
|
319
|
-
}
|
|
320
414
|
if (fallbackCwd && path3.isAbsolute(fallbackCwd) && pathBelongsTo(fallbackCwd, sourcePath)) {
|
|
321
415
|
return { scope: fallbackCwd, scopeKind: "workspace" };
|
|
322
416
|
}
|
|
323
417
|
return { scope: path3.dirname(sourcePath), scopeKind: "external" };
|
|
324
418
|
}
|
|
419
|
+
async function describePanelScope(ctx, scopeDir, sessionSpaceId) {
|
|
420
|
+
const spaces = await ctx.spaces.list().catch(() => []);
|
|
421
|
+
const named = (space) => space.name || space.alias || (space.directoryPath ? path3.basename(space.directoryPath) : space.id);
|
|
422
|
+
if (sessionSpaceId) {
|
|
423
|
+
const space = spaces.find((entry) => entry.id === sessionSpaceId);
|
|
424
|
+
if (space) {
|
|
425
|
+
const label = named(space);
|
|
426
|
+
return { spaceId: space.id, spaceName: label, scopeLabel: label, scopeKind: "space", scopePath: space.directoryPath || scopeDir };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const match = spaces.filter((space) => path3.isAbsolute(space.directoryPath ?? "") && pathBelongsTo(space.directoryPath, scopeDir)).sort((a, b) => (b.directoryPath?.length ?? 0) - (a.directoryPath?.length ?? 0))[0];
|
|
430
|
+
if (match?.directoryPath) {
|
|
431
|
+
const label = named(match);
|
|
432
|
+
return { spaceId: match.id, spaceName: label, scopeLabel: label, scopeKind: "space", scopePath: match.directoryPath };
|
|
433
|
+
}
|
|
434
|
+
return { scopeKind: "workspace", scopePath: scopeDir };
|
|
435
|
+
}
|
|
325
436
|
async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
|
|
326
437
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
438
|
+
let resolved = {};
|
|
439
|
+
await mutateState(ctx, (state) => {
|
|
440
|
+
resolved = resolveRecentScope(state, cwd, sessionId, spaceId);
|
|
441
|
+
if (resolved.scope) {
|
|
442
|
+
state.panelRecentScopes = { ...state.panelRecentScopes, [panel.id]: resolved.scope };
|
|
443
|
+
const panelPath = state.panels?.[panel.id];
|
|
444
|
+
if (panelPath) addRecentPath(state, resolved.scope, panelPath);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
336
447
|
return resolved;
|
|
337
448
|
} catch (error) {
|
|
338
449
|
ctx.logger.warn(`Could not persist panel recent scope: ${String(error)}`);
|
|
@@ -341,32 +452,42 @@ async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
|
|
|
341
452
|
}
|
|
342
453
|
async function rememberLastPath(ctx, panel, sourcePath) {
|
|
343
454
|
try {
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
455
|
+
await mutateState(ctx, async (state) => {
|
|
456
|
+
state.panels = { ...state.panels, [panel.id]: sourcePath };
|
|
457
|
+
state.sessions = { ...state.sessions, [sessionBucketKey(panel)]: sourcePath };
|
|
458
|
+
let scope = state.panelRecentScopes?.[panel.id];
|
|
459
|
+
if (!scope && panel.view === "appView") {
|
|
460
|
+
scope = (await resolveDocumentScope(ctx, sourcePath)).scope;
|
|
461
|
+
state.panelRecentScopes = { ...state.panelRecentScopes, [panel.id]: scope };
|
|
462
|
+
}
|
|
463
|
+
if (scope) addRecentPath(state, scope, sourcePath);
|
|
464
|
+
});
|
|
355
465
|
} catch (error) {
|
|
356
466
|
ctx.logger.warn(`Could not persist last-opened path: ${String(error)}`);
|
|
357
467
|
}
|
|
358
468
|
}
|
|
359
469
|
async function rememberRecentPath(ctx, sourcePath, panel) {
|
|
360
470
|
try {
|
|
361
|
-
await
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
|
|
471
|
+
await mutateState(ctx, (state) => {
|
|
472
|
+
const scope = panel ? state.panelRecentScopes?.[panel.id] : resolveRecentScope(state, ctx.session.cwd ?? "", ctx.session.id ?? "", ctx.session.spaceId ?? "").scope;
|
|
473
|
+
if (scope) addRecentPath(state, scope, sourcePath);
|
|
474
|
+
});
|
|
366
475
|
} catch (error) {
|
|
367
476
|
ctx.logger.warn(`Could not persist recent path: ${String(error)}`);
|
|
368
477
|
}
|
|
369
478
|
}
|
|
479
|
+
async function forgetLastPath(ctx, panel) {
|
|
480
|
+
try {
|
|
481
|
+
await mutateState(ctx, (state) => {
|
|
482
|
+
const key = sessionBucketKey(panel);
|
|
483
|
+
state.panels = { ...state.panels, [panel.id]: "" };
|
|
484
|
+
state.sessions = { ...state.sessions, [key]: "" };
|
|
485
|
+
if (key === "__global__" && typeof state.lastPath === "string") state.lastPath = "";
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
ctx.logger.warn(`Could not clear last-opened path: ${String(error)}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
370
491
|
async function readLastPath(ctx, panel) {
|
|
371
492
|
const state = await readLastPathState(ctx);
|
|
372
493
|
const perPanel = state.panels?.[panel.id];
|
|
@@ -402,36 +523,24 @@ async function collectRecentDocuments(ctx, requestedCwd, sessionId, spaceId) {
|
|
|
402
523
|
const resolved = resolveRecentScope(state, cwd, sessionId, spaceId);
|
|
403
524
|
if (!resolved.scope) return { documents: [] };
|
|
404
525
|
const scope = resolved.scope;
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
);
|
|
408
|
-
const documents =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
relativePath: path3.relative(scope, filePath),
|
|
418
|
-
fileName,
|
|
419
|
-
title: deriveTitle(markdown, fileName.replace(/\.[^.]+$/, "")),
|
|
420
|
-
preview: derivePreview(markdown),
|
|
421
|
-
modifiedAt: info.mtimeMs
|
|
422
|
-
};
|
|
423
|
-
} catch {
|
|
424
|
-
return void 0;
|
|
425
|
-
}
|
|
426
|
-
})
|
|
427
|
-
);
|
|
526
|
+
const descriptor = await describePanelScope(ctx, scope, spaceId);
|
|
527
|
+
const scopeRoot = descriptor.scopePath && path3.isAbsolute(descriptor.scopePath) ? descriptor.scopePath : scope;
|
|
528
|
+
const classified = await classifyTrackedDocuments(ctx, state);
|
|
529
|
+
const documents = classified.filter((doc) => descriptor.spaceId ? doc.spaceId === descriptor.spaceId : !doc.spaceId && pathBelongsTo(scopeRoot, doc.path)).map((doc) => ({
|
|
530
|
+
...doc,
|
|
531
|
+
relativePath: path3.relative(scopeRoot, doc.path),
|
|
532
|
+
spaceId: descriptor.spaceId,
|
|
533
|
+
spaceName: descriptor.spaceName,
|
|
534
|
+
scopeLabel: descriptor.scopeLabel,
|
|
535
|
+
scopeKind: descriptor.scopeKind,
|
|
536
|
+
scopePath: descriptor.scopePath
|
|
537
|
+
}));
|
|
428
538
|
return {
|
|
429
|
-
documents: documents.
|
|
539
|
+
documents: documents.slice(0, RECENT_LIMIT),
|
|
430
540
|
fallbackCwd: resolved.fallbackCwd
|
|
431
541
|
};
|
|
432
542
|
}
|
|
433
|
-
async function
|
|
434
|
-
const state = await readLastPathState(ctx);
|
|
543
|
+
async function classifyTrackedDocuments(ctx, state) {
|
|
435
544
|
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
545
|
const documents = await Promise.all(candidates.map(async (filePath) => {
|
|
437
546
|
try {
|
|
@@ -448,24 +557,55 @@ async function collectLibraryDocuments(ctx) {
|
|
|
448
557
|
spaceId: scope.spaceId,
|
|
449
558
|
spaceName: scope.spaceName,
|
|
450
559
|
scopeLabel: scope.scopeLabel,
|
|
451
|
-
scopeKind: scope.scopeKind
|
|
560
|
+
scopeKind: scope.scopeKind,
|
|
561
|
+
scopePath: scope.scope
|
|
452
562
|
};
|
|
453
563
|
} catch {
|
|
454
564
|
return void 0;
|
|
455
565
|
}
|
|
456
566
|
}));
|
|
457
|
-
return documents.filter((entry) => Boolean(entry)).sort((a, b) => b.modifiedAt - a.modifiedAt)
|
|
567
|
+
return documents.filter((entry) => Boolean(entry)).sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
568
|
+
}
|
|
569
|
+
async function collectLibraryDocuments(ctx) {
|
|
570
|
+
const state = await readLastPathState(ctx);
|
|
571
|
+
const classified = await classifyTrackedDocuments(ctx, state);
|
|
572
|
+
const perCategory = /* @__PURE__ */ new Map();
|
|
573
|
+
return classified.filter((doc) => {
|
|
574
|
+
const key = doc.spaceId ? `space:${doc.spaceId}` : `${doc.scopeKind}:${doc.scopePath ?? ""}`;
|
|
575
|
+
const used = perCategory.get(key) ?? 0;
|
|
576
|
+
if (used >= RECENT_LIMIT) return false;
|
|
577
|
+
perCategory.set(key, used + 1);
|
|
578
|
+
return true;
|
|
579
|
+
});
|
|
458
580
|
}
|
|
459
581
|
var livePanelDocuments = /* @__PURE__ */ new Map();
|
|
582
|
+
var openPanels = /* @__PURE__ */ new Map();
|
|
583
|
+
var fileRevisions = /* @__PURE__ */ new Map();
|
|
584
|
+
function nextFileRevision(sourcePath) {
|
|
585
|
+
const next = (fileRevisions.get(sourcePath) ?? 0) + 1;
|
|
586
|
+
fileRevisions.set(sourcePath, next);
|
|
587
|
+
return next;
|
|
588
|
+
}
|
|
460
589
|
async function sendDocument(panel, state) {
|
|
461
|
-
|
|
462
|
-
|
|
590
|
+
const revision = state.path && path3.isAbsolute(state.path) ? state.revision ?? nextFileRevision(state.path) : state.revision;
|
|
591
|
+
const delivered = revision === void 0 ? state : { ...state, revision };
|
|
592
|
+
livePanelDocuments.set(panel.id, delivered);
|
|
593
|
+
await panel.postMessage({ type: "document", ...delivered });
|
|
594
|
+
}
|
|
595
|
+
async function publishFileUpdate(ctx, sourcePath) {
|
|
596
|
+
const markdown = await readFile2(sourcePath, "utf8");
|
|
597
|
+
const revision = nextFileRevision(sourcePath);
|
|
598
|
+
const state = { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), revision };
|
|
599
|
+
const targets = Array.from(openPanels.values()).filter((panel) => livePanelDocuments.get(panel.id)?.path === sourcePath);
|
|
600
|
+
await Promise.all(targets.map((panel) => sendDocument(panel, state).catch((error) => {
|
|
601
|
+
ctx.logger.warn(`Could not deliver written file to panel ${panel.id}: ${String(error)}`);
|
|
602
|
+
})));
|
|
463
603
|
}
|
|
464
604
|
async function sendLiveDocument(ctx, panel, liveDocument) {
|
|
465
605
|
if (liveDocument.path && path3.isAbsolute(liveDocument.path)) {
|
|
466
606
|
try {
|
|
467
607
|
const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, liveDocument.path);
|
|
468
|
-
await sendDocument(panel, { ...liveDocument, markdown, title: documentTitle(markdown, liveDocument.path), draftRestored, draftConflict, diskMarkdown });
|
|
608
|
+
await sendDocument(panel, { ...liveDocument, revision: void 0, markdown, title: documentTitle(markdown, liveDocument.path), draftRestored, draftConflict, diskMarkdown });
|
|
469
609
|
return;
|
|
470
610
|
} catch (error) {
|
|
471
611
|
ctx.logger.warn(`Could not re-read ${liveDocument.path} for a reconnecting panel, resending cached copy: ${String(error)}`);
|
|
@@ -511,8 +651,10 @@ function watchSource(ctx, panel, sourcePath) {
|
|
|
511
651
|
entry.watcher = watch(sourcePath, () => {
|
|
512
652
|
if (entry.timer) clearTimeout(entry.timer);
|
|
513
653
|
entry.timer = setTimeout(async () => {
|
|
654
|
+
const revisionBeforeRead = fileRevisions.get(sourcePath) ?? 0;
|
|
514
655
|
try {
|
|
515
656
|
const markdown = await readFile2(sourcePath, "utf8");
|
|
657
|
+
if (panelWatchers.get(panel.id) !== entry || (fileRevisions.get(sourcePath) ?? 0) !== revisionBeforeRead) return;
|
|
516
658
|
await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
|
|
517
659
|
} catch (error) {
|
|
518
660
|
ctx.logger.warn(`Source refresh failed: ${String(error)}`);
|
|
@@ -542,14 +684,18 @@ async function getAssistantName(ctx) {
|
|
|
542
684
|
}
|
|
543
685
|
async function sendReady(ctx, panel) {
|
|
544
686
|
const pickFileSupported = ctx.api.supports("ui.pickFile");
|
|
545
|
-
const styleSlots = await
|
|
546
|
-
|
|
687
|
+
const [styleSlots, writingPreferences, assistantName] = await Promise.all([
|
|
688
|
+
readStyleSlots(ctx),
|
|
689
|
+
readWritingPreferences(ctx),
|
|
690
|
+
getAssistantName(ctx)
|
|
691
|
+
]);
|
|
547
692
|
ctx.logger.info(`sending ready to panel; pickFileSupported = ${pickFileSupported}`);
|
|
548
693
|
await panel.postMessage({
|
|
549
694
|
type: "ready",
|
|
550
695
|
locale: ctx.i18n.locale,
|
|
551
696
|
pickFileSupported,
|
|
552
697
|
styleSlots,
|
|
698
|
+
writingPreferences,
|
|
553
699
|
assistantName,
|
|
554
700
|
// So the page can render `cwd` the OS-friendly way (`~/…`) without a
|
|
555
701
|
// round trip — it never needs the raw value for anything but display.
|
|
@@ -580,13 +726,60 @@ async function readRewriteSession(ctx, sourcePath) {
|
|
|
580
726
|
}
|
|
581
727
|
async function rememberRewriteSession(ctx, sourcePath, sessionId) {
|
|
582
728
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
729
|
+
await mutateState(ctx, (state) => {
|
|
730
|
+
state.rewriteSessions = { ...state.rewriteSessions, [sourcePath]: sessionId };
|
|
731
|
+
});
|
|
586
732
|
} catch (error) {
|
|
587
733
|
ctx.logger.warn(`Could not persist rewrite session: ${String(error)}`);
|
|
588
734
|
}
|
|
589
735
|
}
|
|
736
|
+
async function rememberStyleOperation(ctx, sourcePath, operation) {
|
|
737
|
+
await mutateState(ctx, (state) => {
|
|
738
|
+
state.styleOperations = { ...state.styleOperations, [sourcePath]: operation };
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async function clearStyleOperation(ctx, sourcePath, turnId) {
|
|
742
|
+
await mutateState(ctx, (state) => {
|
|
743
|
+
if (state.styleOperations?.[sourcePath]?.turnId !== turnId) return false;
|
|
744
|
+
const operations = { ...state.styleOperations };
|
|
745
|
+
delete operations[sourcePath];
|
|
746
|
+
state.styleOperations = operations;
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
async function rememberPendingStyle(ctx, sourcePath, pending) {
|
|
750
|
+
await mutateState(ctx, (state) => {
|
|
751
|
+
state.pendingStyles = { ...state.pendingStyles, [sourcePath]: pending };
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
async function clearPendingStyle(ctx, sourcePath) {
|
|
755
|
+
await mutateState(ctx, (state) => {
|
|
756
|
+
if (!state.pendingStyles?.[sourcePath]) return false;
|
|
757
|
+
const pending = { ...state.pendingStyles };
|
|
758
|
+
delete pending[sourcePath];
|
|
759
|
+
state.pendingStyles = pending;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
function findPanelsForPath(sourcePath) {
|
|
763
|
+
return Array.from(openPanels.values()).filter((panel) => livePanelDocuments.get(panel.id)?.path === sourcePath);
|
|
764
|
+
}
|
|
765
|
+
async function rememberRewriteOperation(ctx, sourcePath, operation) {
|
|
766
|
+
await mutateState(ctx, (state) => {
|
|
767
|
+
state.rewriteOperations = { ...state.rewriteOperations, [sourcePath]: operation };
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async function clearRewriteOperation(ctx, sourcePath, turnId) {
|
|
771
|
+
await mutateState(ctx, (state) => {
|
|
772
|
+
if (state.rewriteOperations?.[sourcePath]?.turnId !== turnId) return false;
|
|
773
|
+
const operations = { ...state.rewriteOperations };
|
|
774
|
+
delete operations[sourcePath];
|
|
775
|
+
state.rewriteOperations = operations;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async function notifyRewritePanels(sourcePath, message) {
|
|
779
|
+
const targets = Array.from(openPanels.values()).filter((panel) => livePanelDocuments.get(panel.id)?.path === sourcePath);
|
|
780
|
+
await Promise.all(targets.map((panel) => panel.postMessage(message).catch(() => {
|
|
781
|
+
})));
|
|
782
|
+
}
|
|
590
783
|
async function startRewriteSession(ctx, panel, message) {
|
|
591
784
|
const sourcePath = String(message.path ?? "").trim();
|
|
592
785
|
const selectedText = String(message.selectedText ?? "").trim();
|
|
@@ -634,30 +827,104 @@ ${selectedText}
|
|
|
634
827
|
await panel.postMessage({ type: "rewriteSessionFailed", message: "\u6539\u5199\u4F1A\u8BDD\u961F\u5217\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
|
|
635
828
|
return;
|
|
636
829
|
}
|
|
637
|
-
|
|
830
|
+
const operation = {
|
|
831
|
+
sessionId,
|
|
832
|
+
turnId: receipt.turnId,
|
|
833
|
+
startLine: message.startLine,
|
|
834
|
+
endLine: message.endLine ?? message.startLine,
|
|
835
|
+
rewriteMode,
|
|
836
|
+
startedAt: Date.now()
|
|
837
|
+
};
|
|
838
|
+
await rememberRewriteOperation(ctx, sourcePath, operation).catch((error) => ctx.logger.warn(`Could not persist rewrite operation: ${String(error)}`));
|
|
839
|
+
await notifyRewritePanels(sourcePath, {
|
|
638
840
|
type: "rewriteSessionStarted",
|
|
639
841
|
sessionId,
|
|
640
842
|
spaceName: scope.spaceName,
|
|
641
843
|
title: `${rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199"}\uFF1A${path3.basename(sourcePath)}`,
|
|
642
|
-
startLine:
|
|
643
|
-
endLine:
|
|
844
|
+
startLine: operation.startLine,
|
|
845
|
+
endLine: operation.endLine,
|
|
644
846
|
rewriteMode
|
|
645
847
|
});
|
|
646
848
|
void ctx.sessions.waitForTurn(sessionId, receipt.turnId, { timeoutMs: 6e5 }).then(async (result2) => {
|
|
647
849
|
const verb = rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199";
|
|
648
|
-
await
|
|
850
|
+
await clearRewriteOperation(ctx, sourcePath, receipt.turnId).catch((error) => ctx.logger.warn(`Could not clear rewrite operation: ${String(error)}`));
|
|
851
|
+
await notifyRewritePanels(sourcePath, {
|
|
649
852
|
type: result2.state === "completed" ? "rewriteSessionFinished" : "rewriteSessionFailed",
|
|
650
853
|
sessionId,
|
|
651
854
|
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
855
|
});
|
|
654
856
|
});
|
|
655
857
|
}
|
|
858
|
+
async function readStyleSession(ctx, sourcePath) {
|
|
859
|
+
const state = await readLastPathState(ctx);
|
|
860
|
+
const id = state.styleSessions?.[sourcePath];
|
|
861
|
+
if (!id) return void 0;
|
|
862
|
+
const session = await ctx.sessions.get(id).catch(() => void 0);
|
|
863
|
+
return session ? id : void 0;
|
|
864
|
+
}
|
|
865
|
+
async function rememberStyleSession(ctx, sourcePath, sessionId) {
|
|
866
|
+
try {
|
|
867
|
+
await mutateState(ctx, (state) => {
|
|
868
|
+
state.styleSessions = { ...state.styleSessions, [sourcePath]: sessionId };
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
ctx.logger.warn(`Could not persist style session: ${String(error)}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function startStyleSession(ctx, panel, message) {
|
|
875
|
+
const sourcePath = String(message.path ?? "").trim();
|
|
876
|
+
if (panel.view !== "appView" || !path3.isAbsolute(sourcePath)) {
|
|
877
|
+
await panel.postMessage({ type: "styleSessionFailed", message: "\u8BA9 AI \u8BBE\u8BA1\u6392\u7248\u9700\u8981 App View \u4E2D\u5DF2\u4FDD\u5B58\u7684\u672C\u5730\u6587\u6863\u3002" });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const requirement = String(message.requirement ?? "").trim() || "\u8BA9\u6392\u7248\u66F4\u6E05\u6670\u7F8E\u89C2\uFF0C\u8D34\u5408\u6587\u7AE0\u5185\u5BB9\u548C\u8BED\u6C14";
|
|
881
|
+
const baseStyle = String(message.baseStyle ?? "").trim();
|
|
882
|
+
const baseNote = baseStyle === "custom" ? "\u5F53\u524D\u57FA\u7840\u98CE\u683C\u662F kami\uFF08\u81EA\u5B9A\u4E49 CSS \u53E0\u52A0\u5176\u4E0A\uFF09" : baseStyle ? `\u5F53\u524D\u57FA\u7840\u98CE\u683C\u662F ${baseStyle}` : "";
|
|
883
|
+
const scope = await resolveDocumentScope(ctx, sourcePath);
|
|
884
|
+
let sessionId = await readStyleSession(ctx, sourcePath);
|
|
885
|
+
if (!sessionId) {
|
|
886
|
+
const session = await ctx.sessions.create({
|
|
887
|
+
...scope.spaceId ? { space: { spaceId: scope.spaceId } } : {},
|
|
888
|
+
title: `\u8BBE\u8BA1\u6392\u7248\uFF1A${path3.basename(sourcePath)}`,
|
|
889
|
+
activity: "interactive",
|
|
890
|
+
permissionMode: "acceptCalls"
|
|
891
|
+
});
|
|
892
|
+
sessionId = session.sessionId;
|
|
893
|
+
await rememberStyleSession(ctx, sourcePath, sessionId);
|
|
894
|
+
}
|
|
895
|
+
const prompt = `\u8BF7\u4E3A\u8FD9\u7BC7\u516C\u4F17\u53F7\u6587\u7AE0\u8BBE\u8BA1\u4E00\u5957\u81EA\u5B9A\u4E49\u6392\u7248 CSS\u3002${baseNote ? baseNote + "\uFF0C" : ""}\u4F60\u7684 CSS \u4F1A\u53E0\u52A0\u5728\u57FA\u7840\u98CE\u683C\u4E4B\u4E0A\u3002\u8981\u6C42\uFF1A\u53EA\u5199\u666E\u901A CSS \u89C4\u5219\uFF0C\u9009\u62E9\u5668\u9650\u5B9A\u5728 #bm-md \u4E0B\u7684\u6807\u7B7E/\u7ED3\u6784\uFF08\u5982 #bm-md h1\u3001#bm-md p\u3001#bm-md blockquote\u3001#bm-md pre code\u3001#bm-md a\u3001#bm-md strong\u3001#bm-md table \u7B49\uFF09\uFF0C\u4E0D\u8981\u4F7F\u7528 class\uFF0C\u5FC5\u8981\u65F6\u7528 !important \u8986\u76D6\u57FA\u7840\u98CE\u683C\u3002\u53EF\u53C2\u8003 bm.md \u5185\u7F6E\u98CE\u683C\u7684\u8BBE\u8BA1\u8BED\u8A00\uFF1Akami\uFF08\u6696\u8272\u7EB8\u611F\uFF09\u3001bauhaus\uFF08\u51E0\u4F55\u649E\u8272\uFF09\u3001blueprint\uFF08\u6280\u672F\u84DD\u56FE\u7F51\u683C\uFF09\u3001botanical\uFF08\u6E05\u65B0\u7EFF\u610F\uFF09\u3001newsprint\uFF08\u62A5\u520A\u886C\u7EBF\uFF09\u3001retro\uFF08\u590D\u53E4\u6000\u65E7\uFF09\u3001sketch\uFF08\u624B\u7ED8\u98CE\uFF09\u3001terminal\uFF08\u7B49\u5BBD\u6697\u8272\u7EC8\u7AEF\u98CE\uFF09\u3002\u6587\u7AE0\u8DEF\u5F84\uFF1A${sourcePath}\u3002\u8981\u6C42\uFF1A${requirement}\u3002\u8BBE\u8BA1\u597D\u540E\u76F4\u63A5\u8C03\u7528 markdown_editor_document \u7684 set_style\uFF08\u4F20 path="${sourcePath}"\uFF0Ccss \u548C\u7B80\u77ED label\uFF0C\u4E0D\u8981\u4F20 slot\u2014\u2014\u4F20 path \u662F\u4E3A\u4E86\u8BA9\u5B83\u80FD\u627E\u5230\u8FD9\u7BC7\u6587\u6863\u5BF9\u5E94\u7684\u9884\u89C8\u7A97\u53E3\uFF0C\u5373\u4F7F\u7528\u6237\u5DF2\u7ECF\u5207\u6362\u5230\u522B\u7684\u754C\u9762\uFF09\uFF0C\u8BA9\u5B83\u5E94\u7528\u5230\u9884\u89C8\uFF1B\u4E0D\u8981\u5728\u8FD9\u91CC\u8BE2\u95EE\u8981\u8986\u76D6\u54EA\u4E2A\u69FD\u4F4D\u2014\u2014\u9762\u677F\u4F1A\u81EA\u5DF1\u7ED9\u7528\u6237\u4E00\u4E2A\u8F7B\u91CF\u7684\u201C\u4FDD\u5B58\u4E3A\u81EA\u5B9A\u4E49\u98CE\u683C\u201D\u6309\u94AE\uFF0C\u7528\u6237\u56DE\u5230\u8FD9\u7BC7\u6587\u6863\u65F6\u4E5F\u8FD8\u80FD\u770B\u5230\u3002\u5B8C\u6210\u540E\u7528\u4E00\u4E24\u53E5\u8BDD\u7B80\u77ED\u8BF4\u660E\u8BBE\u8BA1\u601D\u8DEF\u5373\u53EF\u3002`;
|
|
896
|
+
const receipt = await ctx.sessions.send(sessionId, {
|
|
897
|
+
text: prompt,
|
|
898
|
+
idempotencyKey: `style-${createHash3("sha256").update(`${sourcePath}:${requirement}:${Date.now()}`).digest("hex")}`
|
|
899
|
+
});
|
|
900
|
+
if (receipt.state === "rejected") {
|
|
901
|
+
await panel.postMessage({ type: "styleSessionFailed", message: "\u6392\u7248\u8BBE\u8BA1\u4F1A\u8BDD\u961F\u5217\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
await rememberStyleOperation(ctx, sourcePath, { sessionId, turnId: receipt.turnId, startedAt: Date.now() });
|
|
905
|
+
await panel.postMessage({ type: "styleSessionStarted", sessionId, spaceName: scope.spaceName });
|
|
906
|
+
void ctx.sessions.waitForTurn(sessionId, receipt.turnId, { timeoutMs: 6e5 }).then(async (result2) => {
|
|
907
|
+
await clearStyleOperation(ctx, sourcePath, receipt.turnId);
|
|
908
|
+
const targets = findPanelsForPath(sourcePath);
|
|
909
|
+
await Promise.all((targets.length ? targets : [panel]).map((target) => target.postMessage({
|
|
910
|
+
type: result2.state === "completed" ? "styleSessionFinished" : "styleSessionFailed",
|
|
911
|
+
sessionId,
|
|
912
|
+
message: result2.state === "completed" ? "\u6392\u7248\u8BBE\u8BA1\u5DF2\u5B8C\u6210\u3002" : result2.state === "timeout" ? "\u6392\u7248\u8BBE\u8BA1\u4ECD\u5728\u4F1A\u8BDD\u4E2D\u7EE7\u7EED\u3002" : "\u6392\u7248\u8BBE\u8BA1\u4F1A\u8BDD\u672A\u5B8C\u6210\u3002"
|
|
913
|
+
}).catch(() => {
|
|
914
|
+
})));
|
|
915
|
+
});
|
|
916
|
+
}
|
|
656
917
|
async function handleMessage(ctx, panel, raw) {
|
|
657
918
|
const message = raw;
|
|
658
919
|
switch (message.type) {
|
|
659
920
|
case "clientLog": {
|
|
660
|
-
|
|
921
|
+
const text = `[panel] ${String(message.message ?? "")}`;
|
|
922
|
+
ctx.logger.info(text);
|
|
923
|
+
try {
|
|
924
|
+
await appendFile(path3.join(ctx.storagePath, "panel-errors.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${text}
|
|
925
|
+
`);
|
|
926
|
+
} catch (_) {
|
|
927
|
+
}
|
|
661
928
|
return;
|
|
662
929
|
}
|
|
663
930
|
case "panelReady": {
|
|
@@ -668,6 +935,22 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
668
935
|
} else if (!await restoreDocument(ctx, panel)) {
|
|
669
936
|
await panel.postMessage({ type: "lastFileUnavailable" });
|
|
670
937
|
}
|
|
938
|
+
const currentPath = livePanelDocuments.get(panel.id)?.path;
|
|
939
|
+
if (currentPath) {
|
|
940
|
+
const state = await readLastPathState(ctx);
|
|
941
|
+
const operation = state.rewriteOperations?.[currentPath];
|
|
942
|
+
if (operation) await panel.postMessage({
|
|
943
|
+
type: "rewriteSessionStarted",
|
|
944
|
+
sessionId: operation.sessionId,
|
|
945
|
+
startLine: operation.startLine,
|
|
946
|
+
endLine: operation.endLine,
|
|
947
|
+
rewriteMode: operation.rewriteMode
|
|
948
|
+
});
|
|
949
|
+
const styleOperation = state.styleOperations?.[currentPath];
|
|
950
|
+
if (styleOperation) await panel.postMessage({ type: "styleSessionStarted", sessionId: styleOperation.sessionId });
|
|
951
|
+
const pendingStyle = state.pendingStyles?.[currentPath];
|
|
952
|
+
if (pendingStyle) await panel.postMessage({ type: "customStyleSet", css: pendingStyle.css, label: pendingStyle.label });
|
|
953
|
+
}
|
|
671
954
|
return;
|
|
672
955
|
}
|
|
673
956
|
case "openImage": {
|
|
@@ -768,6 +1051,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
768
1051
|
case "goHome": {
|
|
769
1052
|
stopWatching(panel.id);
|
|
770
1053
|
livePanelDocuments.delete(panel.id);
|
|
1054
|
+
await forgetLastPath(ctx, panel);
|
|
771
1055
|
return;
|
|
772
1056
|
}
|
|
773
1057
|
case "requestRecentDocuments": {
|
|
@@ -793,6 +1077,10 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
793
1077
|
await startRewriteSession(ctx, panel, message);
|
|
794
1078
|
return;
|
|
795
1079
|
}
|
|
1080
|
+
case "requestStyleSession": {
|
|
1081
|
+
await startStyleSession(ctx, panel, message);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
796
1084
|
case "saveMarkdown": {
|
|
797
1085
|
const sourcePath = String(message.path ?? "").trim();
|
|
798
1086
|
if (!path3.isAbsolute(sourcePath)) return;
|
|
@@ -806,6 +1094,14 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
806
1094
|
}
|
|
807
1095
|
return;
|
|
808
1096
|
}
|
|
1097
|
+
case "saveWritingPreferences": {
|
|
1098
|
+
try {
|
|
1099
|
+
await writeWritingPreferences(ctx, message.preferences);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
ctx.logger.warn(`Could not save writing preferences: ${String(error)}`);
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
809
1105
|
case "saveDraft": {
|
|
810
1106
|
const sourcePath = String(message.path ?? "").trim();
|
|
811
1107
|
if (!path3.isAbsolute(sourcePath)) return;
|
|
@@ -859,6 +1155,7 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
859
1155
|
const current = await readFile2(sourcePath, "utf8");
|
|
860
1156
|
const appliedMarkdown = preserveTextEnvelope(current, markdown);
|
|
861
1157
|
await writeFile2(sourcePath, appliedMarkdown, "utf8");
|
|
1158
|
+
await publishFileUpdate(ctx, sourcePath);
|
|
862
1159
|
await rememberRecentPath(ctx, sourcePath, panel);
|
|
863
1160
|
await panel.postMessage({ type: "applied", path: sourcePath, title: documentTitle(appliedMarkdown, sourcePath) });
|
|
864
1161
|
} catch (error) {
|
|
@@ -907,6 +1204,8 @@ async function handleMessage(ctx, panel, raw) {
|
|
|
907
1204
|
}
|
|
908
1205
|
try {
|
|
909
1206
|
const slots = await writeStyleSlot(ctx, slot, { css, label: String(message.label ?? "").trim() || "\u81EA\u5B9A\u4E49\u98CE\u683C" });
|
|
1207
|
+
const sourcePath = String(message.path ?? "").trim();
|
|
1208
|
+
if (sourcePath) await clearPendingStyle(ctx, sourcePath);
|
|
910
1209
|
await panel.postMessage({ type: "styleSlots", styleSlots: slots, savedSlot: slot });
|
|
911
1210
|
} catch (error) {
|
|
912
1211
|
await panel.postMessage({ type: "error", message: `Could not save style slot: ${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -973,6 +1272,7 @@ function activate(ctx) {
|
|
|
973
1272
|
}
|
|
974
1273
|
}));
|
|
975
1274
|
ctx.subscriptions.push(ctx.ui.onDidOpenPanel((panel) => {
|
|
1275
|
+
openPanels.set(panel.id, panel);
|
|
976
1276
|
if (panel.visible) lastPanel = panel;
|
|
977
1277
|
ctx.subscriptions.push(panel.onDidReceiveMessage((message) => {
|
|
978
1278
|
handleMessage(ctx, panel, message).catch((error) => {
|
|
@@ -985,6 +1285,7 @@ function activate(ctx) {
|
|
|
985
1285
|
}));
|
|
986
1286
|
ctx.subscriptions.push(panel.onDidDispose(() => {
|
|
987
1287
|
stopWatching(panel.id);
|
|
1288
|
+
openPanels.delete(panel.id);
|
|
988
1289
|
livePanelDocuments.delete(panel.id);
|
|
989
1290
|
flushPendingDraftWrite(ctx, panel.id);
|
|
990
1291
|
if (lastPanel === panel) lastPanel = void 0;
|
|
@@ -999,12 +1300,12 @@ action:
|
|
|
999
1300
|
open \u2014 read an absolute local Markdown path and open it as an editable WeChat article preview
|
|
1000
1301
|
create \u2014 write brand-new Markdown content to an absolute path that does not exist yet, then open it in Markdown Editor. Use this whenever the user asks to write an article, start writing, write a post, create, or draft a new document \u2014 even if they do not mention Markdown. If title/topic or destination is missing, guide the user to provide it; once known, create and open the document rather than returning prose only. If they only want to begin, create a minimal titled starter document. Markdown Editor's own UI has no "new file" button on purpose \u2014 this tool action is the intended way to start a new document
|
|
1001
1302
|
apply \u2014 revise a source document (requires path). For a small, targeted change, pass edits instead of markdown: an array of {old_string, new_string} replacements matched against the file's current on-disk content, the same find-and-replace contract as a code editor's Edit tool \u2014 this avoids resending the whole document and keeps the on-screen highlight scoped to what actually changed. Reserve markdown (the full updated document) for a genuine full rewrite. Once this conversation has started editing a .md document through Markdown Editor, always use this apply/edits path for subsequent changes to that same file before considering the built-in Edit tool: it refreshes the panel and highlights the exact change. Fall back to the built-in Edit tool only after this apply actually fails. The open panel refreshes in place, no Diff window. Whenever you propose a rewrite and wait for approval before applying it, calling Session action=suggest with 1-3 one-tap confirmations is MANDATORY, not optional, and part of that same turn \u2014 sending the proposal text alone does not complete the confirmation step, so do not end the turn without also calling it
|
|
1002
|
-
set_style \u2014 apply an AI-designed custom CSS layout to the currently open Markdown Editor preview (requires css). Write plain CSS scoped under #bm-md using tag/id selectors (no classes), use !important where needed to override the base style, and take inspiration from bm.md's built-in styles: kami (warm paper), bauhaus (geometric primary colors), blueprint (technical grid), botanical (soft green), newsprint (editorial serif), retro (nostalgic), sketch (hand-drawn), terminal (monospace dark).`,
|
|
1303
|
+
set_style \u2014 apply an AI-designed custom CSS layout to the currently open Markdown Editor preview (requires css). Apply it right away, without asking the user which reusable slot to use first \u2014 omit \`slot\` and it only updates the live preview; the panel itself then shows a lightweight one-tap prompt so the user decides whether to save it into a reusable custom-style slot, no chat back-and-forth needed. Only pass \`slot\` when the user already told you which of the 3 slots (1/2/3) to save into. Write plain CSS scoped under #bm-md using tag/id selectors (no classes), use !important where needed to override the base style, and take inspiration from bm.md's built-in styles: kami (warm paper), bauhaus (geometric primary colors), blueprint (technical grid), botanical (soft green), newsprint (editorial serif), retro (nostalgic), sketch (hand-drawn), terminal (monospace dark).`,
|
|
1003
1304
|
inputSchema: {
|
|
1004
1305
|
type: "object",
|
|
1005
1306
|
properties: {
|
|
1006
1307
|
action: { type: "string", enum: ["open", "create", "apply", "set_style"], description: "Operation to perform." },
|
|
1007
|
-
path: { type: "string", description: "Absolute path to the Markdown file. Required for open, create, and apply. For create, the file must not already exist." },
|
|
1308
|
+
path: { type: "string", description: "Absolute path to the Markdown file. Required for open, create, and apply. For create, the file must not already exist. Recommended (though optional) for set_style: passing it lets Markdown Editor find the right panel for this document even if it is not the most recently focused one \u2014 e.g. the user switched away while an App View design Session was still working. Without it, set_style falls back to whichever panel was last focused." },
|
|
1008
1309
|
markdown: { type: "string", description: "Full Markdown content. Required for create. For apply, use this only for a genuine full rewrite \u2014 prefer `edits` for a small, targeted change." },
|
|
1009
1310
|
edits: {
|
|
1010
1311
|
type: "array",
|
|
@@ -1021,7 +1322,7 @@ action:
|
|
|
1021
1322
|
},
|
|
1022
1323
|
css: { type: "string", description: "Custom CSS to layer on top of the current base style, required for set_style." },
|
|
1023
1324
|
label: { type: "string", description: "Short label describing the custom style, optional for set_style." },
|
|
1024
|
-
slot: { type: "number", enum: [1, 2, 3], description: "
|
|
1325
|
+
slot: { type: "number", enum: [1, 2, 3], description: "Optional for set_style. Omit it to just apply the design to the live preview \u2014 the panel will offer the user a one-tap way to save it afterward. Only pass this when the user already picked which of the 3 reusable custom style slots (1, 2, or 3) to overwrite." }
|
|
1025
1326
|
},
|
|
1026
1327
|
required: ["action"]
|
|
1027
1328
|
},
|
|
@@ -1076,6 +1377,7 @@ action:
|
|
|
1076
1377
|
const applied = applyEditSpecs(current, edits);
|
|
1077
1378
|
if (!applied.ok) return result(applied.error, true);
|
|
1078
1379
|
await writeFile2(sourcePath, applied.content, "utf8");
|
|
1380
|
+
await publishFileUpdate(ctx, sourcePath);
|
|
1079
1381
|
await rememberRecentPath(ctx, sourcePath);
|
|
1080
1382
|
return result(`Applied ${edits.length} targeted edit${edits.length > 1 ? "s" : ""} to ${path3.basename(sourcePath)}.`);
|
|
1081
1383
|
} catch (error) {
|
|
@@ -1087,6 +1389,7 @@ action:
|
|
|
1087
1389
|
try {
|
|
1088
1390
|
const current = await readFile2(sourcePath, "utf8");
|
|
1089
1391
|
await writeFile2(sourcePath, preserveTextEnvelope(current, markdown), "utf8");
|
|
1392
|
+
await publishFileUpdate(ctx, sourcePath);
|
|
1090
1393
|
await rememberRecentPath(ctx, sourcePath);
|
|
1091
1394
|
return result(`Applied reviewed Markdown to ${path3.basename(sourcePath)}.`);
|
|
1092
1395
|
} catch (error) {
|
|
@@ -1096,16 +1399,32 @@ action:
|
|
|
1096
1399
|
if (action === "set_style") {
|
|
1097
1400
|
const css = String(input.css ?? "").trim();
|
|
1098
1401
|
if (!css) return result("`set_style` requires non-empty `css`.", true);
|
|
1099
|
-
|
|
1402
|
+
const pathHint = String(input.path ?? "").trim();
|
|
1403
|
+
const targets = pathHint ? findPanelsForPath(pathHint) : [];
|
|
1404
|
+
const panel = targets[0] ?? lastPanel;
|
|
1405
|
+
if (!panel && !pathHint) return result("No Markdown Editor panel is open. Ask the user to open a document first.", true);
|
|
1100
1406
|
try {
|
|
1101
1407
|
const label = String(input.label ?? "") || "AI style";
|
|
1408
|
+
const hasSlot = input.slot !== void 0 && input.slot !== null && String(input.slot).trim() !== "";
|
|
1409
|
+
if (!hasSlot) {
|
|
1410
|
+
if (pathHint) await rememberPendingStyle(ctx, pathHint, { css, label });
|
|
1411
|
+
if (panel) {
|
|
1412
|
+
await panel.postMessage({ type: "customStyleSet", css, label });
|
|
1413
|
+
return result("Custom style applied to the open Markdown Editor preview. It is not saved to a reusable slot yet \u2014 the panel now shows a one-tap prompt for the user to save it themselves; do not ask which slot in chat unless the user asks you to save it directly.");
|
|
1414
|
+
}
|
|
1415
|
+
return result("No Markdown Editor panel is currently open for this document, so the style was saved for later \u2014 it will apply automatically (with the same one-tap save prompt) the next time the user opens this file in Markdown Editor.");
|
|
1416
|
+
}
|
|
1102
1417
|
const slot = Number(input.slot);
|
|
1103
1418
|
if (!Number.isInteger(slot) || slot < 1 || slot > STYLE_SLOT_COUNT) {
|
|
1104
|
-
return result("
|
|
1419
|
+
return result("`slot` must be 1, 2, or 3 when provided.", true);
|
|
1105
1420
|
}
|
|
1106
1421
|
const slots = await writeStyleSlot(ctx, slot - 1, { css, label });
|
|
1107
|
-
|
|
1108
|
-
|
|
1422
|
+
if (pathHint) await clearPendingStyle(ctx, pathHint);
|
|
1423
|
+
if (panel) {
|
|
1424
|
+
await panel.postMessage({ type: "customStyleSet", css, label, styleSlots: slots, savedSlot: slot - 1 });
|
|
1425
|
+
return result(`Custom style saved to slot ${slot} and applied to the open Markdown Editor panel.`);
|
|
1426
|
+
}
|
|
1427
|
+
return result(`Custom style saved to slot ${slot}. No Markdown Editor panel is currently open for this document \u2014 it will apply automatically the next time the user opens this file.`);
|
|
1109
1428
|
} catch (error) {
|
|
1110
1429
|
return result(`Could not apply custom style: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
1111
1430
|
}
|