finch-markdown-editor 0.1.8 → 0.1.9

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/panel.html CHANGED
@@ -69,6 +69,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
69
69
  .doc-title{font-size:13px;font-weight:600;line-height:1.35;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
70
70
  .doc-sub{margin-top:2px;display:flex;gap:6px;font-size:11px;line-height:1.4;color:var(--muted)}
71
71
  .doc-sub .doc-path{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
72
+ .doc-sub .doc-path.absolute-path{direction:ltr}
72
73
  .doc-sub .doc-time{flex:none;opacity:.8}
73
74
  .doc-reveal{position:absolute;top:6px;right:6px;z-index:1;display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:7px;background:color-mix(in srgb, var(--bg) 72%, transparent);color:var(--muted);cursor:pointer;opacity:0;transition:opacity .12s ease,background .12s ease,color .12s ease}
74
75
  .doc-reveal svg{width:14px;height:14px}
@@ -80,6 +81,20 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
80
81
  .status a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
81
82
  .status.error{color:#c54232}
82
83
  .status:empty{padding:0;border-top:0}
84
+ /* Lines an external revision (AI apply, or an on-disk edit) just rewrote.
85
+ The decoration is dropped again after the animation, so the highlight is
86
+ purely transient — it marks the change without leaving the doc annotated.
87
+ It eases in rather than appearing at full strength, and leaves a thin
88
+ change-bar on the left the way a diff view would, so the eye reads it as
89
+ "this got rewritten" instead of a highlighter pen being slapped down. */
90
+ .cm-line.cm-ai-changed{border-radius:6px;animation:ai-edit-flash 1.9s cubic-bezier(.33,0,.2,1) both}
91
+ @keyframes ai-edit-flash{
92
+ 0%{background:transparent;box-shadow:inset 2px 0 0 transparent}
93
+ 26%{background:color-mix(in srgb,var(--accent) 13%,transparent);box-shadow:inset 2px 0 0 color-mix(in srgb,var(--accent) 52%,transparent)}
94
+ 55%{background:color-mix(in srgb,var(--accent) 11%,transparent);box-shadow:inset 2px 0 0 color-mix(in srgb,var(--accent) 44%,transparent)}
95
+ 100%{background:transparent;box-shadow:inset 2px 0 0 transparent}
96
+ }
97
+ @media (prefers-reduced-motion:reduce){.cm-line.cm-ai-changed{animation:none;background:transparent;box-shadow:none}}
83
98
  .popup{position:fixed;z-index:20;display:flex;align-items:center;gap:6px;background:var(--card);border:1px solid var(--border);border-radius:9px;box-shadow:0 8px 22px rgba(0,0,0,.22);padding:5px;transform-origin:center bottom;animation:popup-in .18s cubic-bezier(.2,.8,.2,1)}
84
99
  .popup.hidden{display:none}
85
100
  @keyframes popup-in{from{opacity:0;transform:translateY(5px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
@@ -247,6 +262,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
247
262
  'home.revealInFileManager': '在文件管理器中定位',
248
263
  'home.emptyDoc': '(空文档)',
249
264
  'home.cwdTitle': '在访达 / 文件资源管理器中打开:{path}',
265
+ 'home.cwdFallback': 'homePath(无 cwd 回退)',
250
266
  'home.today': '今天 {clock}',
251
267
  'home.yesterday': '昨天 {clock}',
252
268
  'editor.ariaLabel': '写字编辑器',
@@ -278,6 +294,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
278
294
  'toolbar.mode.label': '预览',
279
295
  'toolbar.mode.tooltipToPreview': '切换到预览',
280
296
  'toolbar.mode.tooltipToEdit': '切换到编辑',
297
+ 'toolbar.mode.tooltipRendering': '正在渲染预览…',
281
298
  'toolbar.style.tooltip': '排版风格',
282
299
  'toolbar.style.tooltipNeedsPreview': '请先切换到预览',
283
300
  'toolbar.style.customSlot': '自定义风格 {n} · {label}',
@@ -303,20 +320,20 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
303
320
  'annotate.requirementWith': '要求:{comment}',
304
321
  'annotate.requirementDefault': '让表达更清晰',
305
322
  'annotate.pathMissing': '(未关联本地文件,请先粘贴/保存全部内容)',
306
- 'annotate.promptText': '请改写这段 Markdown 内容(位置:{lineLabel}),{requirement}:\n\n{text}\n\n给出可直接替换的片段;我确认后调用 markdown_editor_document 的 apply 动作,用更新后的完整内容覆盖源文件(path={path})。',
307
- 'annotate.reminder': '仅给出改写建议,不要在未确认前直接写入源文件。',
323
+ 'annotate.promptText': '请改写这段 Markdown 内容(位置:{lineLabel}),{requirement}:\n\n{text}\n\n先给出可直接替换的片段;我确认后,用针对这一段的局部替换写回源文件({path}),不用重发全文。',
324
+ 'annotate.reminder': '仅给出改写建议,不要在未确认前直接写入源文件。回复用自然的说法(如“确认后我就改到文件里”),不要提工具名或动作名。【强制要求,不可省略】只要本回合给出了改写建议并等我确认,就必须在回复文本之后、本回合结束前调用一次 Session action=suggest,附上 1–3 个一键确认选项(如“就用这版”“用更短的那版”“再换个语气”)。只写完回复文字、不调用 suggest,视为任务未完成——“把方案写给用户看”和“让用户能一键确认”是同一个任务的两半,缺一不可,不要因为文字已给完就认为确认环节已经处理完毕。',
308
325
  'newdoc.needChat': '请在 Finch 对话中使用“新建文档”。',
309
326
  'newdoc.label': '新建 Markdown 文档',
310
- 'newdoc.promptText': '我想新建一篇 Markdown 文档。请先根据我的要求确定标题、内容与一个尚不存在的绝对保存路径;信息不足时先问我。确认后调用 markdown_editor_document 的 create 动作,传入完整 Markdown 正文和绝对 path,并在编辑器中打开新文件。',
311
- 'newdoc.reminder': '新建文件时必须使用 markdown_editor_document create 动作;不要覆盖已有文件。',
327
+ 'newdoc.promptText': '我想新建一篇 Markdown 文档。请先根据我的要求确定标题、内容与一个尚不存在的绝对保存路径;信息不足时先问我。确认后把完整正文写入那个路径,并在编辑器中打开这篇新文档。',
328
+ 'newdoc.reminder': '只能新建文件,不要覆盖已有文件。回复用自然的说法,不要提工具名或动作名。【强制要求,不可省略】只要本回合需要我确认标题、方向或保存位置,就必须在回合结束前调用一次 Session action=suggest,给出 1–3 个一键选项,不要让我手打。光在文字里问问题而不调 suggest,视为这次询问没完成。',
312
329
  'newdoc.added': '已把新建文档引导加入输入框。',
313
330
  'aiStyle.label': '根据 {file} 设计风格',
314
331
  'aiStyle.note': 'AI 一键排版',
315
332
  'aiStyle.baseNoteCustom': '当前基础风格是 kami(自定义 CSS 叠加其上)',
316
333
  'aiStyle.baseNoteOther': '当前基础风格是 {style}',
317
334
  'aiStyle.pathMissing': '(未关联本地文件)',
318
- 'aiStyle.promptText': '请为这篇公众号文章设计一套自定义排版 CSS。{baseNote},你的 CSS 会叠加在基础风格之上。要求:只写普通 CSS 规则,选择器限定在 #bm-md 下的标签/结构(如 #bm-md h1、#bm-md p、#bm-md blockquote、#bm-md pre code、#bm-md a、#bm-md strong、#bm-md table 等),不要使用 class(bm.md 输出没有 class,只有内联样式),必要时用 !important 覆盖基础风格。可参考 bm.md 内置风格的设计语言:kami(暖色纸感)、bauhaus(几何撞色)、blueprint(技术蓝图网格)、botanical(清新绿意)、newsprint(报刊衬线)、retro(复古怀旧)、sketch(手绘风)、terminal(等宽暗色终端风)。文章路径:{path}。动手生成 CSS 前,先询问用户要覆盖「自定义风格 1、2、3」中的哪一个槽位;用户选择后,再调用 markdown_editor_document set_style,必须传入 css、简短 label 和 slot(1/2/3)。该调用会覆盖并持久化那个槽位,同时立即应用预览。',
319
- 'aiStyle.reminder': '必须先询问槽位,不能自行猜测或覆盖;用户明确选定 1/2/3 后才调用 set_style。',
335
+ 'aiStyle.promptText': '请为这篇公众号文章设计一套自定义排版 CSS。{baseNote},你的 CSS 会叠加在基础风格之上。要求:只写普通 CSS 规则,选择器限定在 #bm-md 下的标签/结构(如 #bm-md h1、#bm-md p、#bm-md blockquote、#bm-md pre code、#bm-md a、#bm-md strong、#bm-md table 等),不要使用 class(bm.md 输出没有 class,只有内联样式),必要时用 !important 覆盖基础风格。可参考 bm.md 内置风格的设计语言:kami(暖色纸感)、bauhaus(几何撞色)、blueprint(技术蓝图网格)、botanical(清新绿意)、newsprint(报刊衬线)、retro(复古怀旧)、sketch(手绘风)、terminal(等宽暗色终端风)。文章路径:{path}。动手生成 CSS 前,先询问用户要覆盖「自定义风格 1、2、3」中的哪一个槽位;用户选择后,再把设计好的 CSS 应用到那个槽位(需要 css、简短标签和槽位编号 1/2/3)。这会覆盖并持久化那个槽位,同时立即应用到预览。',
336
+ 'aiStyle.reminder': '必须先询问槽位,不能自行猜测或覆盖;用户明确选定 1/2/3 后才应用。回复用自然的说法,不要提工具名或动作名。【强制要求,不可省略】询问槽位时必须在回合结束前调用一次 Session action=suggest,给出“用自定义风格 1/2/3”三个一键选项,不要让我手打。只在文字里问选哪个槽位、不调 suggest,视为询问未完成。',
320
337
  'aiStyle.requested': '已请求 AI 设计自定义排版。',
321
338
  'status.saved': '已保存。',
322
339
  'status.savedWithNewChanges': '已保存,编辑器中仍有新改动。',
@@ -333,6 +350,9 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
333
350
  'status.opening': '正在打开…',
334
351
  'status.opened': '已打开 {file}。',
335
352
  'status.contentUpdated': '内容已更新。',
353
+ 'status.contentUpdatedLine': '已更新第 {line} 行。',
354
+ 'status.contentUpdatedLines': '已更新第 {from}–{to} 行。',
355
+ 'status.contentUpdatedSpots': '已更新 {count} 处,共 {lines} 行。',
336
356
  'status.pasteImageTimeout': '粘贴图片超时',
337
357
  'status.pasteImageFailed': '粘贴图片失败:{msg}',
338
358
  'status.clipboardPrepareTimeout': '准备公众号图片超时',
@@ -364,7 +384,6 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
364
384
  'status.appliedStyleSlot': '已应用「{label}」。',
365
385
  'status.needCustomStyleFirst': '请先应用一个自定义/AI 设计的风格,再保存到槽位。',
366
386
  'status.savedToSlot': '已保存到槽位 {slot}:{label}',
367
- 'status.rendering': '正在渲染…',
368
387
  'status.serializeFailed': '无法序列化预览内容:{err}',
369
388
  'status.imageConversionFailed': '预览内容无法转成图片(可能含有无法跨域读取的外部图片)',
370
389
  'status.imageGenFailed': '生成图片数据失败',
@@ -386,6 +405,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
386
405
  'home.revealInFileManager': 'Show in file manager',
387
406
  'home.emptyDoc': '(Empty document)',
388
407
  'home.cwdTitle': 'Open in Finder / File Explorer: {path}',
408
+ 'home.cwdFallback': 'homePath (no-cwd fallback)',
389
409
  'home.today': 'Today {clock}',
390
410
  'home.yesterday': 'Yesterday {clock}',
391
411
  'editor.ariaLabel': 'Writing editor',
@@ -417,6 +437,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
417
437
  'toolbar.mode.label': 'Preview',
418
438
  'toolbar.mode.tooltipToPreview': 'Switch to preview',
419
439
  'toolbar.mode.tooltipToEdit': 'Switch to edit',
440
+ 'toolbar.mode.tooltipRendering': 'Rendering preview…',
420
441
  'toolbar.style.tooltip': 'Layout style',
421
442
  'toolbar.style.tooltipNeedsPreview': 'Switch to preview first',
422
443
  'toolbar.style.customSlot': 'Custom style {n} · {label}',
@@ -442,20 +463,20 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
442
463
  'annotate.requirementWith': 'Requirement: {comment}',
443
464
  'annotate.requirementDefault': 'make the wording clearer',
444
465
  'annotate.pathMissing': '(No local file linked — ask me to paste/save the full text first)',
445
- 'annotate.promptText': 'Please rewrite this Markdown passage (location: {lineLabel}), {requirement}:\n\n{text}\n\nGive a drop-in replacement snippet; once I confirm, call markdown_editor_document\u2019s apply action to overwrite the source file (path={path}) with the updated content.',
446
- 'annotate.reminder': "Only propose the rewrite — don't write to the source file before I confirm.",
466
+ 'annotate.promptText': 'Please rewrite this Markdown passage (location: {lineLabel}), {requirement}:\n\n{text}\n\nGive a drop-in replacement snippet first; once I confirm, write it back to the source file ({path}) as a targeted local replacement, not a full resend of the document.',
467
+ 'annotate.reminder': "Only propose the rewrite — don't write to the source file before I confirm. Reply in natural language (e.g. “I'll put it into the file once you confirm”); never name the tool or its actions. [MANDATORY, not optional] Whenever this turn proposes a rewrite and is waiting on my confirmation, calling Session action=suggest with 1–3 one-tap confirmations (e.g. “Use this one”, “Use the shorter version”, “Try another tone”) is part of completing that same turn — writing the proposal text is only half the job. Do not treat the turn as done, and do not skip the call, just because the proposal text itself was already sent.",
447
468
  'newdoc.needChat': 'Use \u201cNew Document\u201d from the Finch chat instead.',
448
469
  'newdoc.label': 'New Markdown Document',
449
- 'newdoc.promptText': "I want to create a new Markdown document. First work out the title, content, and a not-yet-existing absolute save path from my request — ask me first if anything is missing. Once confirmed, call markdown_editor_document's create action with the full Markdown body and absolute path to write and open the new file.",
450
- 'newdoc.reminder': "Always use markdown_editor_document's create action for new files; never overwrite an existing file.",
470
+ 'newdoc.promptText': 'I want to create a new Markdown document. First work out the title, content, and a not-yet-existing absolute save path from my request — ask me first if anything is missing. Once confirmed, write the full body to that path and open the new document in the editor.',
471
+ 'newdoc.reminder': 'Only create a new file; never overwrite an existing one. Reply in natural language and never name the tool or its actions. [MANDATORY, not optional] Whenever this turn ends still waiting on me to confirm a title, an angle, or a save location, it must call Session action=suggest with 1–3 one-tap options before the turn ends — asking the question in plain text is not enough on its own; skipping the call leaves the question unfinished.',
451
472
  'newdoc.added': 'Added the new-document prompt to the composer.',
452
473
  'aiStyle.label': 'Design a layout based on {file}',
453
474
  'aiStyle.note': 'One-click AI layout',
454
475
  'aiStyle.baseNoteCustom': 'The current base style is kami (your custom CSS layers on top of it)',
455
476
  'aiStyle.baseNoteOther': 'The current base style is {style}',
456
477
  'aiStyle.pathMissing': '(No local file linked)',
457
- 'aiStyle.promptText': 'Please design a custom layout CSS for this WeChat article. {baseNote}, and your CSS will layer on top of the base style. Requirements: write plain CSS rules only, with selectors scoped to tags/structure under #bm-md (e.g. #bm-md h1, #bm-md p, #bm-md blockquote, #bm-md pre code, #bm-md a, #bm-md strong, #bm-md table); don\u2019t use classes (bm.md\u2019s output has no classes, only inline styles), and use !important where needed to override the base style. You can draw on bm.md\u2019s built-in style language: kami (warm paper feel), bauhaus (geometric color-blocking), blueprint (technical grid), botanical (fresh green), newsprint (editorial serif), retro (nostalgic), sketch (hand-drawn), terminal (monospace dark). Article path: {path}. Before generating any CSS, ask the user which of \u201cCustom style 1/2/3\u201d slots to overwrite; once they choose, call markdown_editor_document\u2019s set_style with css, a short label, and slot (1/2/3). That call overwrites and persists that slot and applies it to the preview immediately.',
458
- 'aiStyle.reminder': "Always ask which slot first — never guess or overwrite on your own; call set_style only after the user picks 1/2/3.",
478
+ 'aiStyle.promptText': 'Please design a custom layout CSS for this WeChat article. {baseNote}, and your CSS will layer on top of the base style. Requirements: write plain CSS rules only, with selectors scoped to tags/structure under #bm-md (e.g. #bm-md h1, #bm-md p, #bm-md blockquote, #bm-md pre code, #bm-md a, #bm-md strong, #bm-md table); don\u2019t use classes (bm.md\u2019s output has no classes, only inline styles), and use !important where needed to override the base style. You can draw on bm.md\u2019s built-in style language: kami (warm paper feel), bauhaus (geometric color-blocking), blueprint (technical grid), botanical (fresh green), newsprint (editorial serif), retro (nostalgic), sketch (hand-drawn), terminal (monospace dark). Article path: {path}. Before generating any CSS, ask the user which of \u201cCustom style 1/2/3\u201d slots to overwrite; once they choose, apply the CSS you designed to that slot (it needs the css, a short label, and the slot number 1/2/3). That overwrites and persists the slot and applies it to the preview immediately.',
479
+ 'aiStyle.reminder': 'Always ask which slot first — never guess or overwrite on your own; apply it only after the user picks 1/2/3. Reply in natural language and never name the tool or its actions. [MANDATORY, not optional] Asking which slot must always be paired with calling Session action=suggest (“Use custom style 1/2/3” as one-tap options) before the turn ends — the question is not considered asked until that call happens, not merely the text of the question.',
459
480
  'aiStyle.requested': 'Asked AI to design a custom layout.',
460
481
  'status.saved': 'Saved.',
461
482
  'status.savedWithNewChanges': "Saved — but the editor already has newer changes.",
@@ -472,6 +493,9 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
472
493
  'status.opening': 'Opening…',
473
494
  'status.opened': 'Opened {file}.',
474
495
  'status.contentUpdated': 'Content updated.',
496
+ 'status.contentUpdatedLine': 'Updated line {line}.',
497
+ 'status.contentUpdatedLines': 'Updated lines {from}\u2013{to}.',
498
+ 'status.contentUpdatedSpots': 'Updated {count} spots, {lines} lines in total.',
475
499
  'status.pasteImageTimeout': 'Paste image timed out',
476
500
  'status.pasteImageFailed': 'Paste image failed: {msg}',
477
501
  'status.clipboardPrepareTimeout': 'Preparing WeChat images timed out',
@@ -503,7 +527,6 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
503
527
  'status.appliedStyleSlot': 'Applied \u201c{label}\u201d.',
504
528
  'status.needCustomStyleFirst': 'Apply a custom/AI-designed style first before saving it to a slot.',
505
529
  'status.savedToSlot': 'Saved to slot {slot}: {label}',
506
- 'status.rendering': 'Rendering…',
507
530
  'status.serializeFailed': 'Failed to serialize the preview content: {err}',
508
531
  'status.imageConversionFailed': "The preview couldn't be converted to an image (it may contain external images that can't be read cross-origin)",
509
532
  'status.imageGenFailed': 'Failed to generate image data',
@@ -621,6 +644,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
621
644
  var html = '';
622
645
  var frameReady = false;
623
646
  var renderId = 0;
647
+ var renderingPreview = false;
624
648
  var saveId = 0;
625
649
  var pendingSaveContents = {};
626
650
  // Resolvers for callers that need to *await* a save round-trip (the
@@ -699,6 +723,14 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
699
723
  // the backend scopes its recent-file list to whatever we hand back, so a
700
724
  // panel in another Space never shows this Space's documents.
701
725
  var envCwd = '';
726
+ var envSessionId = '';
727
+ var envSpaceId = '';
728
+ var fallbackCwd = '';
729
+ // Some Home views arrive without cwd/session. The backend may return the
730
+ // persisted ordinary-chat homePath as `fallbackCwd`, so empty envCwd must
731
+ // not block the request. Only "no `finch:env` yet" should, which this
732
+ // separate flag tracks instead of overloading envCwd.
733
+ var envReceived = false;
702
734
  var recentRequested = false;
703
735
  var assistantName = '';
704
736
  var homeDir = '';
@@ -758,14 +790,19 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
758
790
 
759
791
  function renderHomeCwd() {
760
792
  if (!homeCwd) return;
761
- homeCwd.textContent = truncateFriendlyPath(formatFriendlyPath(envCwd));
762
- homeCwd.title = envCwd ? t('home.cwdTitle', { path: envCwd }) : '';
763
- homeCwd.hidden = !envCwd;
793
+ // `fallbackCwd` is the persisted real cwd from an ordinary no-Space
794
+ // Session, not a virtual filesystem path. It makes the Home view and
795
+ // that Session share exactly one recent-document scope.
796
+ var effectiveCwd = envCwd || fallbackCwd;
797
+ homeCwd.textContent = truncateFriendlyPath(formatFriendlyPath(effectiveCwd));
798
+ homeCwd.title = effectiveCwd ? t('home.cwdTitle', { path: effectiveCwd }) : '';
799
+ homeCwd.hidden = !effectiveCwd;
764
800
  }
765
801
 
766
802
  if (homeCwd) homeCwd.addEventListener('click', function () {
767
- if (!envCwd || !api || !api.postMessage) return;
768
- api.postMessage({ type: 'openPath', path: envCwd });
803
+ var effectiveCwd = envCwd || fallbackCwd;
804
+ if (!effectiveCwd || !api || !api.postMessage) return;
805
+ api.postMessage({ type: 'openPath', path: effectiveCwd });
769
806
  });
770
807
 
771
808
  function formatDocTime(ts) {
@@ -783,9 +820,9 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
783
820
  }
784
821
 
785
822
  function requestRecentDocuments() {
786
- if (!api || !api.postMessage || !envCwd) return;
823
+ if (!api || !api.postMessage || !envReceived) return;
787
824
  recentRequested = true;
788
- api.postMessage({ type: 'requestRecentDocuments', cwd: envCwd });
825
+ api.postMessage({ type: 'requestRecentDocuments', cwd: envCwd, sessionId: envSessionId, spaceId: envSpaceId });
789
826
  }
790
827
 
791
828
  function renderRecentDocuments(documents) {
@@ -802,6 +839,11 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
802
839
  var cards = '';
803
840
  for (var i = 0; i < list.length; i++) {
804
841
  var doc = list[i] || {};
842
+ // Under the virtual homePath scope relativePath is necessarily an
843
+ // absolute path. The normal RTL tail-truncation presentation would
844
+ // visually move its leading '/' to the far right, so keep it LTR.
845
+ var docPath = doc.relativePath || doc.fileName || '';
846
+ var docPathClass = /^(\/|[A-Za-z]:[\\/])/.test(docPath) ? 'doc-path absolute-path' : 'doc-path';
805
847
  cards += '<button class="doc-card" type="button" data-path="' + escapeHtml(doc.path || '') + '" title="' + escapeHtml(doc.path || '') + '">'
806
848
  + '<span class="doc-reveal" data-reveal-path="' + escapeHtml(doc.path || '') + '" role="button" tabindex="0" title="' + escapeHtml(t('home.revealInFileManager')) + '" aria-label="' + escapeHtml(t('home.revealInFileManager')) + '">'
807
849
  + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>'
@@ -809,7 +851,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
809
851
  + '<div class="doc-preview">' + escapeHtml(doc.preview || t('home.emptyDoc')) + '</div>'
810
852
  + '<div class="doc-foot">'
811
853
  + '<div class="doc-title">' + escapeHtml(doc.title || doc.fileName || t('home.untitled')) + '</div>'
812
- + '<div class="doc-sub"><span class="doc-path">' + escapeHtml(doc.relativePath || doc.fileName || '') + '</span>'
854
+ + '<div class="doc-sub"><span class="' + docPathClass + '">' + escapeHtml(docPath) + '</span>'
813
855
  + '<span class="doc-time">' + escapeHtml(formatDocTime(doc.modifiedAt)) + '</span></div>'
814
856
  + '</div></button>';
815
857
  }
@@ -860,7 +902,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
860
902
  if (actions) actions.hidden = !html;
861
903
  // Refresh whenever the home page comes back into view: mtimes change while
862
904
  // a document is open, and a file may have been created since last render.
863
- if (!empty.hidden && envCwd) requestRecentDocuments();
905
+ if (!empty.hidden && envReceived) requestRecentDocuments();
864
906
  }
865
907
 
866
908
  function showPane() {
@@ -945,8 +987,8 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
945
987
  // icon plus `checked` already communicate on/off; flipping the word
946
988
  // itself between "预览"/"编辑" read as ambiguous ("is this the mode
947
989
  // I'm in, or the one I'm switching to?").
948
- id: 'mode', icon: 'ext:markdown-editor-icons/swatch-book',
949
- label: t('toolbar.mode.label'), tooltip: hasDoc ? (mode === 'edit' ? t('toolbar.mode.tooltipToPreview') : t('toolbar.mode.tooltipToEdit')) : t('toolbar.needDoc'),
990
+ id: 'mode', icon: renderingPreview ? 'ext:markdown-editor-icons/hourglass' : 'ext:markdown-editor-icons/swatch-book',
991
+ label: t('toolbar.mode.label'), tooltip: hasDoc ? (renderingPreview ? t('toolbar.mode.tooltipRendering') : mode === 'edit' ? t('toolbar.mode.tooltipToPreview') : t('toolbar.mode.tooltipToEdit')) : t('toolbar.needDoc'),
950
992
  checked: mode === 'preview', disabled: !hasDoc,
951
993
  },
952
994
  {
@@ -1079,7 +1121,8 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
1079
1121
  function render() {
1080
1122
  if (!markdown || !api) return;
1081
1123
  var id = ++renderId;
1082
- setStatus(t('status.rendering'));
1124
+ renderingPreview = true;
1125
+ syncToolbar();
1083
1126
  api.postMessage({
1084
1127
  type: 'renderBm',
1085
1128
  markdown: markdown,
@@ -1565,6 +1608,10 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
1565
1608
  fileHandle = null;
1566
1609
  }
1567
1610
 
1611
+ // Line range touched by the most recent in-place external revision, so the
1612
+ // status line can say *what* moved instead of just "content updated".
1613
+ var lastExternalChange = null;
1614
+
1568
1615
  function applyDocument(nextMarkdown, nextName, nextPath, force, diskBaseline) {
1569
1616
  // Deliberately does NOT check cm.hasFocus(): unsaved local edits must
1570
1617
  // never be silently clobbered by an external update, whether or not the
@@ -1575,6 +1622,10 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
1575
1622
  setStatus(t('status.externalUpdateSkipped'), true);
1576
1623
  return false;
1577
1624
  }
1625
+ // Is this a revision of the document already on screen (an AI apply, or
1626
+ // an external save), rather than a switch to a different file? Must be
1627
+ // decided before `sourcePath` is reassigned below.
1628
+ var revisesOpenDocument = savedMarkdown !== null && (!nextPath || !sourcePath || nextPath === sourcePath);
1578
1629
  markdown = nextMarkdown;
1579
1630
  name = nextName;
1580
1631
  if (nextPath) sourcePath = nextPath;
@@ -1588,7 +1639,11 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
1588
1639
  // content sitting unchanged at the real baseline and wrongly report
1589
1640
  // an external-edit conflict that never happened.
1590
1641
  savedMarkdown = typeof diskBaseline === 'string' ? diskBaseline : markdown;
1591
- cm.setValue(markdown);
1642
+ // Patching only the changed span keeps the scroll offset and the caret
1643
+ // where the user left them, and flashes the rewritten lines. A fresh
1644
+ // document still gets the plain full replacement.
1645
+ lastExternalChange = revisesOpenDocument ? cm.applyExternalValue(markdown) : null;
1646
+ if (!lastExternalChange) cm.setValue(markdown);
1592
1647
  setDirty(false); // also rebuilds+syncs the whole toolbar, picking up the new sourcePath/fileName below
1593
1648
  setPanelTitle(fileName);
1594
1649
  updateEmptyState();
@@ -2457,8 +2512,14 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
2457
2512
  // Panel scope changed (first open, or Home → Session relocation): rescope
2458
2513
  // the recent list to this panel's own working directory.
2459
2514
  var incomingCwd = m.cwd || '';
2460
- if (incomingCwd !== envCwd) {
2515
+ var incomingSessionId = m.sessionId || '';
2516
+ var incomingSpaceId = m.spaceId || '';
2517
+ if (incomingCwd !== envCwd || incomingSessionId !== envSessionId || incomingSpaceId !== envSpaceId || !envReceived) {
2461
2518
  envCwd = incomingCwd;
2519
+ envSessionId = incomingSessionId;
2520
+ envSpaceId = incomingSpaceId;
2521
+ fallbackCwd = '';
2522
+ envReceived = true;
2462
2523
  renderHomeCwd();
2463
2524
  renderRecentDocuments([]);
2464
2525
  }
@@ -2468,6 +2529,8 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
2468
2529
  if (m.type === 'recentDocuments') {
2469
2530
  // Ignore a late reply for a directory we no longer belong to.
2470
2531
  if ((m.cwd || '') !== envCwd) return;
2532
+ fallbackCwd = m.fallbackCwd || '';
2533
+ renderHomeCwd();
2471
2534
  renderRecentDocuments(m.documents);
2472
2535
  return;
2473
2536
  }
@@ -2511,7 +2574,16 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
2511
2574
  } else if (wasLoadingFromHome) {
2512
2575
  setStatus(t('status.opened', { file: fileName }));
2513
2576
  } else if (!isFirst) {
2514
- setStatus(t('status.contentUpdated'));
2577
+ var touched = lastExternalChange;
2578
+ if (touched && touched.hunks > 1) {
2579
+ setStatus(t('status.contentUpdatedSpots', { count: touched.hunks, lines: touched.changedLines }));
2580
+ } else if (touched && touched.fromLine === touched.toLine) {
2581
+ setStatus(t('status.contentUpdatedLine', { line: touched.fromLine }));
2582
+ } else if (touched) {
2583
+ setStatus(t('status.contentUpdatedLines', { from: touched.fromLine, to: touched.toLine }));
2584
+ } else {
2585
+ setStatus(t('status.contentUpdated'));
2586
+ }
2515
2587
  }
2516
2588
  return;
2517
2589
  }
@@ -2523,6 +2595,7 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
2523
2595
  if (m.requestId !== renderId) return;
2524
2596
  showHtml(m.html || '');
2525
2597
  updateEmptyState();
2598
+ renderingPreview = false;
2526
2599
  syncToolbar(); // `html` just became available (or changed) — refresh the copy menu's disabled state
2527
2600
  setStatus('');
2528
2601
  return;
@@ -2587,6 +2660,9 @@ body{margin:0;background:var(--bg);color:var(--text);font:13px var(--finch-font-
2587
2660
  nativePickPending = false;
2588
2661
  clearNativePickTimer();
2589
2662
  if (m.fallback) pickFileSupported = false;
2663
+ // renderBm failures arrive through the same error channel; never
2664
+ // leave the preview button stuck on the hourglass after one.
2665
+ renderingPreview = false;
2590
2666
  syncToolbar();
2591
2667
  setStatus(m.message, true);
2592
2668
  return;
package/i18n/zh-CN.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "写字",
3
3
  "description": "在 Finch 中编辑 Markdown,并可预览为微信公众号文章样式;支持框选文字请求 AI 修改和自定义排版。",
4
- "systemPrompt": "当用户要编辑或预览本地 Markdown 文件时,调用 markdown_editor_document,action 使用 open,并提供绝对路径。用户说“写文章”“开始写作”“写一篇”“写个帖子”或“起草内容”时,即使未提 Markdown,也视为要使用写字。新建文档时,若缺少标题/主题或保存位置,先引导用户补齐;信息足够后,调用 action=create,传入一个尚不存在的绝对 .md 路径和 Markdown 内容。用户提供了写作需求时,直接起草完整 Markdown;用户只想开始写作时,创建带标题的最小起稿文档。该工具会一次性写入文件并在 Markdown 编辑器中打开——用户要在编辑器开始写作时,不要只在聊天中返回一段文字。编辑器界面故意不提供“新建”按钮。当用户要修改编辑器或预览中框选的文字时,给出精确的替换 Markdown,确认后调用 action=apply 并提供完整更新后的 Markdown。当用户要求自定义/AI 设计公众号排版风格时,编写限定在 #bm-md 下的普通 CSS(标签/id 选择器,不用 class,必要时用 !important 覆盖基础风格),可参考 bm.md 内置风格(kami、bauhaus、blueprint、botanical、newsprint、retro、sketch、terminal),然后调用 action=set_style 并提供 css 与简短 label,会立即应用到当前打开的预览。",
4
+ "systemPrompt": "当用户要编辑或预览本地 Markdown 文件时,调用 markdown_editor_document,action 使用 open,并提供绝对路径。用户说“写文章”“开始写作”“写一篇”“写个帖子”或“起草内容”时,即使未提 Markdown,也视为要使用写字。新建文档时,若缺少标题/主题或保存位置,先引导用户补齐;信息足够后,调用 action=create,传入一个尚不存在的绝对 .md 路径和 Markdown 内容。用户提供了写作需求时,直接起草完整 Markdown;用户只想开始写作时,创建带标题的最小起稿文档。该工具会一次性写入文件并在 Markdown 编辑器中打开——用户要在编辑器开始写作时,不要只在聊天中返回一段文字。编辑器界面故意不提供“新建”按钮。当用户要修改编辑器或预览中框选的文字时,给出精确的替换 Markdown,确认后调用 action=apply——改动小的场合传 edits(一组针对文件当前内容的 {old_string, new_string} 局部替换),不要整篇重发;只有真正的全文重写才传完整的 markdown。一旦当前对话已通过写字开始编辑某个 Markdown 文件,后续对同一 .md 文件的所有修改都【必须优先】继续走写字的 apply/edits 路径,即使系统内置 Edit 也能完成同样的文字替换:这样才能让已打开的编辑器刷新并精准高亮改动。只有写字的 apply 实际失败后才退回系统内置 Edit;不能因为 Edit 看起来更短或更方便就切换。当用户要求自定义/AI 设计公众号排版风格时,编写限定在 #bm-md 下的普通 CSS(标签/id 选择器,不用 class,必要时用 !important 覆盖基础风格),可参考 bm.md 内置风格(kami、bauhaus、blueprint、botanical、newsprint、retro、sketch、terminal),然后调用 action=set_style 并提供 css 与简短 label,会立即应用到当前打开的预览。回复用户时,用用户能感知的说法(如“确认后我就改到文件里”“已在编辑器中打开”),不要出现工具名或 action 名——那是内部实现,照搬进回复会像在背 API。凡是给出改写方案、标题/路径或风格槽位后要等用户确认的场合,调用 Session action=suggest 给出 1-3 个可点击的简短确认项(如“就用这版”“用更短的那版”“再换个语气”)是【强制要求,不是可选项】——它和给出方案本身是同一个回合的两个必需部分,不是事后可以补的动作。不要因为方案文字已经写完、读起来像是一个完整的问句或提议,就认为确认这一步已经处理好了;只要没有实际调用 action=suggest,这个回合就还没完成。",
5
5
  "appPanel": {
6
6
  "title": "写字",
7
7
  "toolbar": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finch-markdown-editor",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "A Markdown editor and WeChat Official Account style preview, with selection-based AI edits and AI-generated custom styles.",
5
5
  "author": {
6
6
  "name": "PuterJam",
@@ -36,7 +36,7 @@
36
36
  "minVersion": "1.6.1",
37
37
  "name": "Writing",
38
38
  "description": "Edit Markdown and preview it as a WeChat Official Account article. Select text for AI edits, or ask AI to design a custom style.",
39
- "systemPrompt": "When the user wants to edit or preview a local Markdown file, call markdown_editor_document with action 'open' and its absolute path. Treat requests such as \"write an article\", \"start writing\", \"write a post\", or \"draft something\" as an intent to use Writing even when Markdown is not mentioned. For a new document, guide the user to supply a title/topic and destination when they are missing; once enough information is available, call action 'create' with a new absolute .md path and Markdown content. If the user supplied a writing brief, draft the full Markdown; if they only want to begin, create a minimal titled starter document. The tool writes the file and opens it in Markdown Editor in one step — do not merely return prose in chat when the user wants to begin writing in the editor. Markdown Editor's own UI intentionally has no 'new file' button. When the user asks to revise a passage selected in Markdown Editor, propose precise replacement Markdown and, once approved, call action 'apply' with the full updated Markdown. When the user asks for a custom/AI-designed WeChat layout style, write plain CSS scoped under #bm-md (tag/id selectors only, use !important where needed to override the base style) inspired by bm.md's built-in styles (kami, bauhaus, blueprint, botanical, newsprint, retro, sketch, terminal), then call action 'set_style' with that css and a short label — it applies immediately in the open preview.",
39
+ "systemPrompt": "When the user wants to edit or preview a local Markdown file, call markdown_editor_document with action 'open' and its absolute path. Treat requests such as \"write an article\", \"start writing\", \"write a post\", or \"draft something\" as an intent to use Writing even when Markdown is not mentioned. For a new document, guide the user to supply a title/topic and destination when they are missing; once enough information is available, call action 'create' with a new absolute .md path and Markdown content. If the user supplied a writing brief, draft the full Markdown; if they only want to begin, create a minimal titled starter document. The tool writes the file and opens it in Markdown Editor in one step — do not merely return prose in chat when the user wants to begin writing in the editor. Markdown Editor's own UI intentionally has no 'new file' button. When the user asks to revise a passage selected in Markdown Editor, propose precise replacement Markdown and, once approved, call action 'apply' for a small change pass edits (an array of {old_string, new_string} targeted replacements matched against the file's current content) instead of resending the whole document; reserve the full markdown parameter for a genuine full rewrite. Once this conversation has started editing a Markdown document through Writing, ALL subsequent changes to that .md file MUST keep using Writing's apply/edits path first, even though the built-in Edit tool can make the same textual replacement: Writing refreshes the open editor and highlights exactly what changed. Use the built-in Edit tool only if Writing's apply actually fails, and do not switch merely because Edit appears shorter or more convenient. When the user asks for a custom/AI-designed WeChat layout style, write plain CSS scoped under #bm-md (tag/id selectors only, use !important where needed to override the base style) inspired by bm.md's built-in styles (kami, bauhaus, blueprint, botanical, newsprint, retro, sketch, terminal), then call action 'set_style' with that css and a short label — it applies immediately in the open preview. In your visible replies, describe this capability the way the user experiences it (\"I'll write it into the file\", \"opened it in the editor\"); never surface the tool name or its action names, which are internal plumbing — quoting them back makes the reply read like a machine reciting an API. Whenever you propose a rewrite, a title/path, or a style slot and then wait for the user's approval, calling Session action=suggest with 1-3 short click-to-send confirmations (e.g. 'Use this one', 'Use the shorter version', 'Try another tone') is MANDATORY, not optional — it is part of the same turn as the proposal, not a separate follow-up. Do not treat sending the proposal text as having finished the confirmation step; the turn is incomplete until action=suggest has actually been called, even if the wording already reads as a complete question or offer.",
40
40
  "main": "dist/index.js",
41
41
  "activationEvents": [
42
42
  "onStartup"