diff --git a/README.md b/README.md
index ca8754b..fb16ffd 100644
--- a/README.md
+++ b/README.md
@@ -100,16 +100,21 @@ Then in the plugin settings, paste `/Users/you/bin/codex` (or `~/bin/claude`) in
| `取消当前对照笔记生成` | Mark the active generation job as cancelled |
| `清除当前笔记的缓存` | Drop the cache entry for the active note |
| `清除所有缓存` | Wipe all cached summaries |
+| `聚焦上一张摘要卡片` / `Focus previous summary card` | Move the active summary card upward |
+| `聚焦下一张摘要卡片` / `Focus next summary card` | Move the active summary card downward |
+| `跳转到当前摘要卡片原文` / `Jump current summary card to source` | Jump the source editor to the active card |
## Interaction
| Action | Effect |
|--------|--------|
| Left-click a card body | Scroll source editor to that section |
-| Right-click a card | Context menu: Copy Markdown / Copy plain text / Copy anchor / Jump |
+| Right-click a card | Context menu: Copy Markdown / Copy plain text / Copy anchor / Jump / Edit / Delete card |
| Header icon buttons | Regenerate or cancel / copy all Markdown / export to Vault |
| File context menu | Generate / force regenerate / clear cache for a Markdown file |
| Ribbon icon | Open the comparison pane for the active note |
+| `Alt+↑` / `Alt+↓` | Move between summary cards |
+| `Enter` in the summary pane | Jump to the active card's source line |
| Drag to select text | Normal text selection (does not trigger jump) |
| Scroll source editor | Active card gets highlighted on the right |
@@ -132,10 +137,15 @@ The plugin keeps `main.js` as the generated Obsidian runtime bundle. Edit `main.
|------|----------------|
| `main.ts` | Obsidian lifecycle, commands, right-pane view, settings tab orchestration |
| `src/anchor.ts` | Anchor-to-line matching and whitespace-normalized fallback |
+| `src/cache.ts` | Cache entry touch semantics and compact cache-file serialization |
+| `src/cards.ts` | Card list edit/delete helpers |
| `src/i18n.ts` | Chinese/English UI strings and translation helper |
+| `src/navigation.ts` | Summary-card keyboard navigation helpers |
| `src/prompt.ts` | Prompt construction, language controls, and custom prompt templating |
| `src/settings.ts` | Defaults, provider presets, settings normalization, cache fingerprinting and pruning |
+| `src/vault.ts` | Vault path normalization and recursive folder creation |
| `src/schema.ts` | JSON extraction, card payload normalization, structured-output schemas |
+| `src/scroll.ts` | Scroll-sync requestAnimationFrame throttling helper |
| `src/providers.ts` | API provider request/response adapters |
| `src/generation-job-manager.ts` | Per-file generation state, cancellation, and error classification |
| `src/markdown.ts` | Card-to-Markdown/plain-text serialization |
diff --git a/main.js b/main.js
index 3b53b1d..8d1ac41 100644
--- a/main.js
+++ b/main.js
@@ -1,26 +1,51 @@
'use strict';
-"use strict";var Be=Object.create;var V=Object.defineProperty;var Ue=Object.getOwnPropertyDescriptor;var je=Object.getOwnPropertyNames;var Ge=Object.getPrototypeOf,$e=Object.prototype.hasOwnProperty;var He=(a,e)=>{for(var t in e)V(a,t,{get:e[t],enumerable:!0})},Te=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of je(e))!$e.call(a,i)&&i!==t&&V(a,i,{get:()=>e[i],enumerable:!(n=Ue(e,i))||n.enumerable});return a};var R=(a,e,t)=>(t=a!=null?Be(Ge(a)):{},Te(e||!a||!a.__esModule?V(t,"default",{value:a,enumerable:!0}):t,a)),Ke=a=>Te(V({},"__esModule",{value:!0}),a);var mt={};He(mt,{__test:()=>ht,default:()=>dt});module.exports=Ke(mt);var c=require("obsidian"),Ve=require("child_process"),ee=R(require("os")),v=R(require("path")),Re=R(require("fs"));function B(a,e){if(!e)return-1;let t=p=>p.replace(/\s+/g," ").trim(),n=p=>{let h=[],g=[],u=!1;for(let E=0;E
0;continue}u&&(h.push(" "),g.push(E),u=!1),h.push(Ae),g.push(E)}return{text:h.join(""),map:g}},i=p=>{if(!p)return-1;let h=a.indexOf(p);if(h===-1)return-1;let g=0;for(let u=0;u=0||(r=i(e.trim()),r>=0))return r;for(let p of[60,40,25,15]){let h=e.trim().slice(0,p);if(r=i(h),r>=0)return r}let o=n(a),s=t(e).slice(0,30);if(!s)return-1;let l=o.text.indexOf(s);if(l===-1)return-1;let d=o.map[l];if(d==null)return-1;let f=0;for(let p=0;p --version` \u9A8C\u8BC1 spawn \u80FD\u627E\u5230\u4E8C\u8FDB\u5236",backendTestFailed:"\u2717 \u540E\u7AEF\u6D4B\u8BD5\u5931\u8D25\uFF1A{error}",settingExportFolderName:"\u5BFC\u51FA\u6587\u4EF6\u5939",settingExportFolderDesc:"\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u4F4D\u7F6E\uFF08\u76F8\u5BF9 Vault \u6839\uFF09",cacheHeader:"\u7F13\u5B58",settingMaxCacheName:"\u6700\u5927\u7F13\u5B58\u7BC7\u6570",settingMaxCacheDesc:"\u8D85\u8FC7\u4E0A\u9650\u540E\u6309\u6700\u8FD1\u8BBF\u95EE\u65F6\u95F4\u6DD8\u6C70\u6700\u65E7\u7684\u7B14\u8BB0\u7F13\u5B58\uFF1B\u7F13\u5B58\u4FDD\u5B58\u5728\u63D2\u4EF6\u76EE\u5F55\u7684 cache.json",cachePruned:"\u5DF2\u6DD8\u6C70 {count} \u6761\u65E7\u7F13\u5B58",cachedNotesName:"\u5DF2\u7F13\u5B58\u7B14\u8BB0\uFF1A{count} \u7BC7",cachedNotesDesc:"\u7F13\u5B58\u4EE5\u6E90\u7B14\u8BB0 SHA1 + \u751F\u6210\u914D\u7F6E\u6307\u7EB9\u4F5C\u4E3A\u5931\u6548\u952E\uFF0C\u6E90\u7B14\u8BB0\u6216\u6A21\u578B\u914D\u7F6E\u4FEE\u6539\u540E\u4F1A\u663E\u793A stale \u63D0\u793A",clearAllCacheButton:"\u6E05\u9664\u6240\u6709\u7F13\u5B58"},en:{appTitle:"Parallel Reader",settingsTitle:"Parallel Reader Settings",emptyOpenNote:"Open a note, then run:",emptyNoCache:"This note has no cached parallel notes. Run:",commandGenerate:"Parallel Reader: Generate notes for current note",displayName:"Parallel Reader",ribbonOpen:"Open Parallel Reader pane",cmdRun:"Generate parallel notes for current note (cache first)",cmdRegen:"Regenerate parallel notes (bypass cache)",cmdOpenView:"Open Parallel Reader pane",cmdExport:"Export current parallel notes to Vault",cmdCopyMarkdown:"Copy current parallel notes as Markdown",cmdCancel:"Cancel current parallel-note generation",cmdClearCurrent:"Clear cache for current note",cmdClearAll:"Clear all caches",actionCancel:"Cancel generation",actionRegenerate:"Regenerate",actionCopyAll:"Copy all Markdown",actionExport:"Export to Vault",staleBanner:"The source note or generation settings changed. This is stale cache.",loadingDefault:"Generating parallel notes...",loadingGenerating:"Parallel Reader: asking the LLM to read and segment the full note...",loadingSubtitle:"You can keep reading. Cards will refresh when generation finishes.",errorTitle:"Generation failed",emptyCard:"(not generated)",anchorMismatch:"Anchor did not match; scroll sync is unavailable",menuCopyMarkdown:"Copy Markdown",menuCopyPlain:"Copy plain text",menuCopyAnchor:"Copy anchor quote",menuJumpSource:"Jump to source",copiedMarkdown:"Copied Markdown",copiedPlain:"Copied plain text",copiedAnchor:"Copied anchor quote",copiedAllMarkdown:"Copied all Markdown",copyFailed:"Copy failed: {error}",actionFailed:"{label} failed: {error}",exported:"Exported \u2192 {path}",noCurrentNote:"No current note",cacheClearedFile:"Cleared cache: {name}",cacheClearedAll:"Cleared {count} cache entries",noCancelableJob:"No cancellable generation job",cancelRequested:"Cancel requested",fileMenuGenerate:"Generate parallel notes",fileMenuRegen:"Regenerate parallel notes",fileMenuClear:"Clear parallel-note cache",noExportContent:"No parallel notes to export",noCopyContent:"No parallel notes to copy",openNoteFirst:"Open a note first",alreadyGenerating:"This note is already being generated",emptyNote:"The note is empty",longNoteTruncated:"Long note: only the first {count} characters will be sent to the model",generatingNotice:"Parallel Reader: asking the LLM to read and segment the full note...",noCardsReturned:"LLM returned no cards",generationDone:"Generated {count} sections{suffix}",unanchoredSuffix:" (\u26A0 {count} anchors unmatched)",cancelled:"Generation cancelled",cancelledError:"Generation cancelled",generationFailed:"Generation failed{kind}: {error}",noEditor:"Could not find the source note editor",settingUiLanguageName:"UI language",settingUiLanguageDesc:"Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.",settingBackendName:"Backend",settingBackendDesc:"Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.",settingCliPathName:"CLI path (optional)",settingCliPathDesc:"Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.",settingCliPathPlaceholder:"Example: /Users/you/bin/codex",settingCliTimeoutName:"CLI timeout (ms)",apiProviderHeader:"API Provider",settingProviderPresetName:"Provider preset",settingProviderPresetDesc:"OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.",settingApiFormatName:"API format",settingApiFormatDesc:"Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.",settingBaseUrlName:"Base URL",settingBaseUrlDesc:"Provider root URL, without /chat/completions. Blank uses the preset default.",settingApiKeyName:"API Key",settingApiKeyDesc:"Can be blank for local Ollama/LM Studio; most hosted providers require a key.",settingApiKeyEnvName:"API key env var",settingApiKeyEnvDesc:"Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.",settingAuthTypeName:"Auth type",settingAuthTypeDesc:"Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.",settingHeadersName:"Extra headers",settingHeadersDesc:"Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.",settingMaxTokensName:"Max output tokens",settingModelName:"Model",settingModelDescApi:"Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.",settingModelDescCli:"Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.",settingMaxInputName:"Max input characters",settingMaxInputDesc:"Longer notes are truncated before sending to the model. Raise this for long-context models.",promptHeader:"Prompt",settingPromptLanguageName:"Output language",settingPromptLanguageDesc:"Controls title/gist/bullets language. Anchor is always copied verbatim from source.",settingCardRangeName:"Card count range",settingCardRangeDesc:"The model adapts segmentation within this range.",settingCustomPromptName:"Custom system prompt",settingCustomPromptDesc:"Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}",settingCustomPromptPlaceholder:"Leave blank to use built-in prompt",settingTestBackendName:"Test current backend",settingTestBackendDescApi:"Sends a minimal LLM request to validate API settings.",settingTestBackendDescCli:"Runs ` --version` to verify the binary can be spawned.",backendTestFailed:"\u2717 Backend test failed: {error}",settingExportFolderName:"Export folder",settingExportFolderDesc:"Parallel-note output location, relative to the Vault root.",cacheHeader:"Cache",settingMaxCacheName:"Max cached notes",settingMaxCacheDesc:"Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.",cachePruned:"Pruned {count} old cache entries",cachedNotesName:"Cached notes: {count}",cachedNotesDesc:"Cache is invalidated by source SHA1 and generation settings fingerprint.",clearAllCacheButton:"Clear all caches"}};function Je(a){let e=a&&a.uiLanguage;return e==="zh"||e==="en"?e:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function ne(a,e,t){let n=Je(a),i=U[n]||U.en,r=U.en[e]||U.zh[e]||e,o=i[e]||r;return String(o).replace(/\{([a-zA-Z0-9_]+)\}/g,(s,l)=>t&&Object.prototype.hasOwnProperty.call(t,l)?String(t[l]):s)}function re(a){let e=[`## ${a.title}`];if(a.anchor){let t=a.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return a.gist&&e.push(a.gist),a.bullets&&a.bullets.length>0&&e.push(a.bullets.map(t=>`- ${t}`).join(`
+"use strict";var Ze=Object.create;var B=Object.defineProperty;var Qe=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var tt=Object.getPrototypeOf,at=Object.prototype.hasOwnProperty;var rt=(a,e)=>{for(var t in e)B(a,t,{get:e[t],enumerable:!0})},_e=(a,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of et(e))!at.call(a,i)&&i!==t&&B(a,i,{get:()=>e[i],enumerable:!(r=Qe(e,i))||r.enumerable});return a};var U=(a,e,t)=>(t=a!=null?Ze(tt(a)):{},_e(e||!a||!a.__esModule?B(t,"default",{value:a,enumerable:!0}):t,a)),nt=a=>_e(B({},"__esModule",{value:!0}),a);var Dt={};rt(Dt,{__test:()=>Ft,default:()=>Nt});module.exports=nt(Dt);var c=require("obsidian"),We=require("child_process"),te=U(require("os")),x=U(require("path")),Ye=U(require("fs"));function j(a,e){if(!e)return-1;let t=p=>p.replace(/\s+/g," ").trim(),r=p=>{let h=[],m=[],u=!1;for(let S=0;S0;continue}u&&(h.push(" "),m.push(S),u=!1),h.push(Oe),m.push(S)}return{text:h.join(""),map:m}},i=p=>{if(!p)return-1;let h=a.indexOf(p);if(h===-1)return-1;let m=0;for(let u=0;u=0||(n=i(e.trim()),n>=0))return n;for(let p of[60,40,25,15]){let h=e.trim().slice(0,p);if(n=i(h),n>=0)return n}let s=r(a),o=t(e).slice(0,30);if(!o)return-1;let l=s.text.indexOf(o);if(l===-1)return-1;let d=s.map[l];if(d==null)return-1;let f=0;for(let p=0;p=t.length||t.splice(e,1),t}function ce(a,e,t){if(!Number.isInteger(a)||!Number.isInteger(e)||e<=0||!Number.isInteger(t)||t<0||a<0||a>=e)return t;let r=e-1;return r<=0?-1:a=r.length||(r[e]=Object.assign({},r[e],t||{})),r}var H={zh:{appTitle:"\u5BF9\u7167\u9605\u8BFB\u7B14\u8BB0",settingsTitle:"Parallel Reader \u8BBE\u7F6E",emptyOpenNote:"\u6253\u5F00\u4E00\u7BC7\u7B14\u8BB0\uFF0C\u7136\u540E\u8FD0\u884C\u547D\u4EE4\uFF1A",emptyNoCache:"\u8BE5\u7B14\u8BB0\u5C1A\u65E0\u5BF9\u7167\u7B14\u8BB0\u7F13\u5B58\u3002\u8FD0\u884C\u547D\u4EE4\uFF1A",commandGenerate:"Parallel Reader: \u4E3A\u5F53\u524D\u7B14\u8BB0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0",displayName:"\u5BF9\u7167\u9605\u8BFB\u7B14\u8BB0",ribbonOpen:"\u6253\u5F00\u5BF9\u7167\u7B14\u8BB0\u9762\u677F",cmdRun:"\u4E3A\u5F53\u524D\u7B14\u8BB0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0\uFF08\u7F13\u5B58\u4F18\u5148\uFF09",cmdRegen:"\u5F3A\u5236\u91CD\u65B0\u751F\u6210\uFF08\u7ED5\u8FC7\u7F13\u5B58\uFF09",cmdOpenView:"\u6253\u5F00\u5BF9\u7167\u7B14\u8BB0\u9762\u677F",cmdExport:"\u5BFC\u51FA\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0\u5230 Vault",cmdCopyMarkdown:"\u590D\u5236\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0 Markdown",cmdCancel:"\u53D6\u6D88\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0\u751F\u6210",cmdClearCurrent:"\u6E05\u9664\u5F53\u524D\u7B14\u8BB0\u7684\u7F13\u5B58",cmdClearAll:"\u6E05\u9664\u6240\u6709\u7F13\u5B58",cmdCardPrev:"\u805A\u7126\u4E0A\u4E00\u5F20\u6458\u8981\u5361\u7247",cmdCardNext:"\u805A\u7126\u4E0B\u4E00\u5F20\u6458\u8981\u5361\u7247",cmdCardJump:"\u8DF3\u8F6C\u5230\u5F53\u524D\u6458\u8981\u5361\u7247\u539F\u6587",actionCancel:"\u53D6\u6D88\u751F\u6210",actionRegenerate:"\u91CD\u65B0\u751F\u6210",actionCopyAll:"\u590D\u5236\u5168\u90E8 Markdown",actionExport:"\u5BFC\u51FA\u5230 Vault",staleBanner:"\u6E90\u7B14\u8BB0\u6216\u751F\u6210\u914D\u7F6E\u5DF2\u4FEE\u6539\uFF0C\u5F53\u524D\u662F\u65E7\u7F13\u5B58\u3002",loadingDefault:"\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0...",loadingGenerating:"\u5BF9\u7167\u9605\u8BFB\uFF1A\u8BA9 LLM \u8BFB\u5168\u6587\u5E76\u81EA\u9002\u5E94\u5207\u6BB5...",loadingSubtitle:"\u53EF\u4EE5\u7EE7\u7EED\u9605\u8BFB\u539F\u6587\uFF0C\u751F\u6210\u5B8C\u6210\u540E\u4F1A\u81EA\u52A8\u5237\u65B0\u53F3\u4FA7\u5361\u7247\u3002",errorTitle:"\u751F\u6210\u5931\u8D25",emptyCard:"\uFF08\u672A\u751F\u6210\uFF09",anchorMismatch:"anchor \u5339\u914D\u5931\u8D25\uFF0C\u65E0\u6CD5\u6EDA\u52A8\u8054\u52A8",menuCopyMarkdown:"\u590D\u5236 Markdown",menuCopyPlain:"\u590D\u5236\u7EAF\u6587\u672C",menuCopyAnchor:"\u590D\u5236 anchor \u5F15\u7528",menuJumpSource:"\u8DF3\u8F6C\u5230\u539F\u6587",menuEditCard:"\u7F16\u8F91\u6B64\u5361\u7247",menuDeleteCard:"\u5220\u9664\u6B64\u5361\u7247",copiedMarkdown:"\u5DF2\u590D\u5236 Markdown",copiedPlain:"\u5DF2\u590D\u5236\u7EAF\u6587\u672C",copiedAnchor:"\u5DF2\u590D\u5236\u5F15\u7528\u539F\u6587",copiedAllMarkdown:"\u5DF2\u590D\u5236\u5168\u90E8 Markdown",copyFailed:"\u590D\u5236\u5931\u8D25\uFF1A{error}",actionFailed:"{label}\u5931\u8D25\uFF1A{error}",exported:"\u5DF2\u5BFC\u51FA \u2192 {path}",noCurrentNote:"\u6CA1\u6709\u5F53\u524D\u7B14\u8BB0",cacheClearedFile:"\u5DF2\u6E05\u9664\u7F13\u5B58\uFF1A{name}",cacheClearedAll:"\u5DF2\u6E05\u9664 {count} \u6761\u7F13\u5B58",noCancelableJob:"\u5F53\u524D\u6CA1\u6709\u53EF\u53D6\u6D88\u7684\u751F\u6210\u4EFB\u52A1",cancelRequested:"\u5DF2\u8BF7\u6C42\u53D6\u6D88\u751F\u6210",cancelRequestedApiInFlight:"\u5DF2\u8BF7\u6C42\u53D6\u6D88\u751F\u6210\uFF1B\u5F53\u524D API \u8BF7\u6C42\u65E0\u6CD5\u7ACB\u5373\u4E2D\u65AD\uFF0C\u8FD4\u56DE\u540E\u4F1A\u4E22\u5F03\u7ED3\u679C\u3002",fileMenuGenerate:"\u751F\u6210\u5BF9\u7167\u7B14\u8BB0",fileMenuRegen:"\u5F3A\u5236\u91CD\u65B0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0",fileMenuClear:"\u6E05\u9664\u5BF9\u7167\u7B14\u8BB0\u7F13\u5B58",noExportContent:"\u5F53\u524D\u6CA1\u6709\u53EF\u5BFC\u51FA\u7684\u5BF9\u7167\u7B14\u8BB0",noCopyContent:"\u5F53\u524D\u6CA1\u6709\u53EF\u590D\u5236\u7684\u5BF9\u7167\u7B14\u8BB0",noActiveCard:"\u5F53\u524D\u6CA1\u6709\u53EF\u8DF3\u8F6C\u7684\u6458\u8981\u5361\u7247",cardDeleted:"\u5DF2\u5220\u9664\u6B64\u5361\u7247",cardSaved:"\u5DF2\u4FDD\u5B58\u6B64\u5361\u7247",editCardTitle:"\u7F16\u8F91\u6458\u8981\u5361\u7247",editCardTitleField:"\u6807\u9898",editCardGistField:"\u9886\u8BFB",editCardBulletsField:"\u8981\u70B9\uFF08\u6BCF\u884C\u4E00\u6761\uFF09",editCardCancel:"\u53D6\u6D88",editCardSave:"\u4FDD\u5B58",openNoteFirst:"\u5148\u6253\u5F00\u4E00\u7BC7\u7B14\u8BB0",alreadyGenerating:"\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0",emptyNote:"\u7B14\u8BB0\u4E3A\u7A7A",longNoteTruncated:"\u7B14\u8BB0\u8F83\u957F\uFF1A\u4EC5\u53D1\u9001\u524D {count} \u4E2A\u5B57\u7B26\u7ED9\u6A21\u578B",generatingNotice:"\u5BF9\u7167\u9605\u8BFB\uFF1A\u8BA9 LLM \u8BFB\u5168\u6587\u5E76\u81EA\u9002\u5E94\u5207\u6BB5\u2026",noCardsReturned:"LLM \u672A\u8FD4\u56DE\u4EFB\u4F55 card",generationDone:"\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u5B8C\u6210\uFF1A{count} \u6BB5{suffix}",unanchoredSuffix:"\uFF08\u26A0 {count} \u6BB5 anchor \u672A\u5339\u914D\uFF09",cancelled:"\u5DF2\u53D6\u6D88\u751F\u6210",cancelledError:"\u751F\u6210\u5DF2\u53D6\u6D88",generationFailed:"\u751F\u6210\u5931\u8D25{kind}\uFF1A{error}",errorCustomProviderBaseUrlRequired:"\u81EA\u5B9A\u4E49 provider \u9700\u8981\u586B\u5199 API Base URL\u3002",errorApiBaseUrlMissing:"API Base URL \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 provider \u6216\u586B\u5199\u81EA\u5B9A\u4E49 base URL\u3002",errorModelMissing:"Model \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199\u6A21\u578B ID\u3002",errorApiKeyMissing:"API key \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 API Key{hint}\u3002",errorApiKeyEnvHint:" \u6216\u73AF\u5883\u53D8\u91CF {envVar}",errorLlmNonJson:`LLM \u8FD4\u56DE\u975E JSON\uFF1A
+{excerpt}`,errorCustomHeadersJsonParse:"\u81EA\u5B9A\u4E49 headers JSON \u89E3\u6790\u5931\u8D25\uFF1A{error}",errorCustomHeadersJsonObject:"\u81EA\u5B9A\u4E49 headers JSON \u5FC5\u987B\u662F\u5BF9\u8C61",errorCustomHeadersLineFormat:"\u81EA\u5B9A\u4E49 headers \u6BCF\u884C\u683C\u5F0F\u5E94\u4E3A `Header-Name: value`",errorProviderNonJson:`{label} \u8FD4\u56DE\u975E JSON\uFF1A
+{excerpt}`,errorProviderRequestFailed:"{label} \u8BF7\u6C42\u5931\u8D25\uFF1A{error}",errorProviderApiStatus:"{label} API \u8FD4\u56DE HTTP {status}\uFF1A{excerpt}",noEditor:"\u627E\u4E0D\u5230\u6E90\u7B14\u8BB0\u5BF9\u5E94\u7684\u7F16\u8F91\u5668\u7A97\u53E3",settingUiLanguageName:"\u754C\u9762\u8BED\u8A00",settingUiLanguageDesc:"\u63A7\u5236\u63D2\u4EF6\u754C\u9762\u3001\u547D\u4EE4\u548C\u63D0\u793A\u6587\u6848\uFF1BAuto \u8DDF\u968F Obsidian/\u7CFB\u7EDF\u8BED\u8A00",settingBackendName:"Backend",settingBackendDesc:"\u751F\u6210 bullet \u7684\u540E\u7AEF\uFF1ACLI \u590D\u7528\u672C\u673A\u767B\u5F55\uFF1BAPI \u652F\u6301 OpenAI/Anthropic/Gemini \u53CA\u517C\u5BB9\u4EE3\u7406",settingCliPathName:"CLI \u8DEF\u5F84\uFF08\u53EF\u9009\uFF09",settingCliPathDesc:"\u7559\u7A7A\u5219\u81EA\u52A8\u63A2\u6D4B\u5E38\u89C1\u4F4D\u7F6E\uFF1BObsidian GUI \u542F\u52A8\u65F6\u4E0D\u7EE7\u627F shell PATH\uFF0C\u5FC5\u8981\u65F6\u586B\u7EDD\u5BF9\u8DEF\u5F84",settingCliPathPlaceholder:"\u4F8B\uFF1A/Users/you/bin/codex",settingCliTimeoutName:"CLI \u8D85\u65F6 (ms)",apiProviderHeader:"API Provider",settingProviderPresetName:"Provider preset",settingProviderPresetDesc:"\u53C2\u8003 OpenClaw \u7684 provider/model \u601D\u8DEF\uFF1Apreset \u53EA\u8D1F\u8D23\u534F\u8BAE\u3001base URL \u548C\u8BA4\u8BC1\u9ED8\u8BA4\u503C",settingApiFormatName:"API format",settingApiFormatDesc:"\u4E0D\u540C provider \u7684 wire protocol\uFF1BOpenAI-compatible \u4EE3\u7406\u901A\u5E38\u9009 Chat Completions",settingBaseUrlName:"Base URL",settingBaseUrlDesc:"\u586B provider \u6839\u5730\u5740\uFF0C\u4E0D\u8981\u9644\u52A0 /chat/completions\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528 preset \u9ED8\u8BA4\u503C",settingApiKeyName:"API Key",settingApiKeyDesc:"\u672C\u5730 Ollama/LM Studio \u53EF\u7559\u7A7A\uFF1B\u5176\u4ED6 provider \u901A\u5E38\u9700\u8981 key",settingApiKeyEnvName:"API Key \u73AF\u5883\u53D8\u91CF",settingApiKeyEnvDesc:"\u53EF\u9009\uFF1BObsidian GUI \u4E0D\u4E00\u5B9A\u7EE7\u627F shell \u73AF\u5883\uFF0C\u76F4\u63A5\u586B API Key \u66F4\u7A33\u5B9A",settingAuthTypeName:"\u8BA4\u8BC1\u65B9\u5F0F",settingAuthTypeDesc:"Auto \u4F7F\u7528 provider preset\uFF1B\u81EA\u5B9A\u4E49\u4EE3\u7406\u53EF\u6309\u9700\u8981\u6539\u6210 Bearer\u3001x-api-key \u6216 none",settingHeadersName:"\u989D\u5916 headers",settingHeadersDesc:"\u53EF\u9009\u3002\u652F\u6301 JSON \u5BF9\u8C61\u6216\u6BCF\u884C `Header: value`\uFF0C\u7528\u4E8E Cloudflare AI Gateway \u7B49\u4EE3\u7406",settingMaxTokensName:"\u6700\u5927\u8F93\u51FA tokens",settingModelName:"Model",settingModelDescApi:"API \u8C03\u7528\u7684\u6A21\u578B ID\uFF1B\u652F\u6301 OpenClaw \u98CE\u683C provider/model\uFF0C\u82E5 provider \u524D\u7F00\u5339\u914D\u5F53\u524D preset \u4F1A\u81EA\u52A8\u5265\u79BB",settingModelDescCli:"Claude Code \u4E0B\u4F1A\u4F20 --model\uFF1BCodex \u4E0B\u901A\u5E38\u5FFD\u7565\uFF08\u7528 Codex \u9ED8\u8BA4\u914D\u7F6E\uFF09",settingMaxInputName:"\u6700\u5927\u8F93\u5165\u5B57\u7B26\u6570",settingMaxInputDesc:"\u8D85\u8FC7\u8BE5\u957F\u5EA6\u4F1A\u622A\u65AD\u540E\u518D\u53D1\u9001\u7ED9\u6A21\u578B\uFF1B\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u53EF\u9002\u5F53\u8C03\u5927",promptHeader:"Prompt",settingPromptLanguageName:"\u8F93\u51FA\u8BED\u8A00",settingPromptLanguageDesc:"\u63A7\u5236 title/gist/bullets \u7684\u8BED\u8A00\uFF1Banchor \u59CB\u7EC8\u9010\u5B57\u590D\u5236\u539F\u6587",settingCardRangeName:"\u5361\u7247\u6570\u91CF\u8303\u56F4",settingCardRangeDesc:"\u6A21\u578B\u4F1A\u5728\u8FD9\u4E2A\u8303\u56F4\u5185\u81EA\u9002\u5E94\u5207\u6BB5",settingCustomPromptName:"\u81EA\u5B9A\u4E49 system prompt",settingCustomPromptDesc:"\u53EF\u9009\u3002\u652F\u6301\u53D8\u91CF\uFF1A{minCards}\u3001{maxCards}\u3001{languageInstruction}\u3001{schema}\u3001{example}",settingCustomPromptPlaceholder:"\u7559\u7A7A\u4F7F\u7528\u5185\u7F6E prompt",settingTestBackendName:"\u6D4B\u8BD5\u5F53\u524D\u540E\u7AEF",settingTestBackendDescApi:"\u4F1A\u53D1\u8D77\u4E00\u6B21\u6700\u5C0F LLM \u8BF7\u6C42\u9A8C\u8BC1 API \u8BBE\u7F6E",settingTestBackendDescCli:"\u8C03\u7528 ` --version` \u9A8C\u8BC1 spawn \u80FD\u627E\u5230\u4E8C\u8FDB\u5236",backendTestFailed:"\u2717 \u540E\u7AEF\u6D4B\u8BD5\u5931\u8D25\uFF1A{error}",settingExportFolderName:"\u5BFC\u51FA\u6587\u4EF6\u5939",settingExportFolderDesc:"\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u4F4D\u7F6E\uFF08\u76F8\u5BF9 Vault \u6839\uFF09",cacheHeader:"\u7F13\u5B58",settingMaxCacheName:"\u6700\u5927\u7F13\u5B58\u7BC7\u6570",settingMaxCacheDesc:"\u8D85\u8FC7\u4E0A\u9650\u540E\u6309\u6700\u8FD1\u8BBF\u95EE\u65F6\u95F4\u6DD8\u6C70\u6700\u65E7\u7684\u7B14\u8BB0\u7F13\u5B58\uFF1B\u7F13\u5B58\u4FDD\u5B58\u5728\u63D2\u4EF6\u76EE\u5F55\u7684 cache.json",cachePruned:"\u5DF2\u6DD8\u6C70 {count} \u6761\u65E7\u7F13\u5B58",cachedNotesName:"\u5DF2\u7F13\u5B58\u7B14\u8BB0\uFF1A{count} \u7BC7",cachedNotesDesc:"\u7F13\u5B58\u4EE5\u6E90\u7B14\u8BB0 SHA1 + \u751F\u6210\u914D\u7F6E\u6307\u7EB9\u4F5C\u4E3A\u5931\u6548\u952E\uFF0C\u6E90\u7B14\u8BB0\u6216\u6A21\u578B\u914D\u7F6E\u4FEE\u6539\u540E\u4F1A\u663E\u793A stale \u63D0\u793A",clearAllCacheButton:"\u6E05\u9664\u6240\u6709\u7F13\u5B58"},en:{appTitle:"Parallel Reader",settingsTitle:"Parallel Reader Settings",emptyOpenNote:"Open a note, then run:",emptyNoCache:"This note has no cached parallel notes. Run:",commandGenerate:"Parallel Reader: Generate notes for current note",displayName:"Parallel Reader",ribbonOpen:"Open Parallel Reader pane",cmdRun:"Generate parallel notes for current note (cache first)",cmdRegen:"Regenerate parallel notes (bypass cache)",cmdOpenView:"Open Parallel Reader pane",cmdExport:"Export current parallel notes to Vault",cmdCopyMarkdown:"Copy current parallel notes as Markdown",cmdCancel:"Cancel current parallel-note generation",cmdClearCurrent:"Clear cache for current note",cmdClearAll:"Clear all caches",cmdCardPrev:"Focus previous summary card",cmdCardNext:"Focus next summary card",cmdCardJump:"Jump current summary card to source",actionCancel:"Cancel generation",actionRegenerate:"Regenerate",actionCopyAll:"Copy all Markdown",actionExport:"Export to Vault",staleBanner:"The source note or generation settings changed. This is stale cache.",loadingDefault:"Generating parallel notes...",loadingGenerating:"Parallel Reader: asking the LLM to read and segment the full note...",loadingSubtitle:"You can keep reading. Cards will refresh when generation finishes.",errorTitle:"Generation failed",emptyCard:"(not generated)",anchorMismatch:"Anchor did not match; scroll sync is unavailable",menuCopyMarkdown:"Copy Markdown",menuCopyPlain:"Copy plain text",menuCopyAnchor:"Copy anchor quote",menuJumpSource:"Jump to source",menuEditCard:"Edit this card",menuDeleteCard:"Delete this card",copiedMarkdown:"Copied Markdown",copiedPlain:"Copied plain text",copiedAnchor:"Copied anchor quote",copiedAllMarkdown:"Copied all Markdown",copyFailed:"Copy failed: {error}",actionFailed:"{label} failed: {error}",exported:"Exported \u2192 {path}",noCurrentNote:"No current note",cacheClearedFile:"Cleared cache: {name}",cacheClearedAll:"Cleared {count} cache entries",noCancelableJob:"No cancellable generation job",cancelRequested:"Cancel requested",cancelRequestedApiInFlight:"Cancel requested. The in-flight API request cannot be aborted immediately; its result will be ignored.",fileMenuGenerate:"Generate parallel notes",fileMenuRegen:"Regenerate parallel notes",fileMenuClear:"Clear parallel-note cache",noExportContent:"No parallel notes to export",noCopyContent:"No parallel notes to copy",noActiveCard:"No active summary card to jump",cardDeleted:"Deleted this card",cardSaved:"Saved this card",editCardTitle:"Edit summary card",editCardTitleField:"Title",editCardGistField:"Gist",editCardBulletsField:"Bullets (one per line)",editCardCancel:"Cancel",editCardSave:"Save",openNoteFirst:"Open a note first",alreadyGenerating:"This note is already being generated",emptyNote:"The note is empty",longNoteTruncated:"Long note: only the first {count} characters will be sent to the model",generatingNotice:"Parallel Reader: asking the LLM to read and segment the full note...",noCardsReturned:"LLM returned no cards",generationDone:"Generated {count} sections{suffix}",unanchoredSuffix:" (\u26A0 {count} anchors unmatched)",cancelled:"Generation cancelled",cancelledError:"Generation cancelled",generationFailed:"Generation failed{kind}: {error}",errorCustomProviderBaseUrlRequired:"Custom provider requires an API Base URL.",errorApiBaseUrlMissing:"API Base URL is not set. Choose a provider or enter a custom base URL in settings.",errorModelMissing:"Model is not set. Enter a model ID in settings.",errorApiKeyMissing:"API key is not set. Enter an API Key in settings{hint}.",errorApiKeyEnvHint:" or set environment variable {envVar}",errorLlmNonJson:`LLM returned non-JSON:
+{excerpt}`,errorCustomHeadersJsonParse:"Custom headers JSON parse failed: {error}",errorCustomHeadersJsonObject:"Custom headers JSON must be an object",errorCustomHeadersLineFormat:"Custom headers lines must use `Header-Name: value`",errorProviderNonJson:`{label} returned non-JSON:
+{excerpt}`,errorProviderRequestFailed:"{label} request failed: {error}",errorProviderApiStatus:"{label} API returned HTTP {status}: {excerpt}",noEditor:"Could not find the source note editor",settingUiLanguageName:"UI language",settingUiLanguageDesc:"Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.",settingBackendName:"Backend",settingBackendDesc:"Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.",settingCliPathName:"CLI path (optional)",settingCliPathDesc:"Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.",settingCliPathPlaceholder:"Example: /Users/you/bin/codex",settingCliTimeoutName:"CLI timeout (ms)",apiProviderHeader:"API Provider",settingProviderPresetName:"Provider preset",settingProviderPresetDesc:"OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.",settingApiFormatName:"API format",settingApiFormatDesc:"Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.",settingBaseUrlName:"Base URL",settingBaseUrlDesc:"Provider root URL, without /chat/completions. Blank uses the preset default.",settingApiKeyName:"API Key",settingApiKeyDesc:"Can be blank for local Ollama/LM Studio; most hosted providers require a key.",settingApiKeyEnvName:"API key env var",settingApiKeyEnvDesc:"Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.",settingAuthTypeName:"Auth type",settingAuthTypeDesc:"Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.",settingHeadersName:"Extra headers",settingHeadersDesc:"Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.",settingMaxTokensName:"Max output tokens",settingModelName:"Model",settingModelDescApi:"Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.",settingModelDescCli:"Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.",settingMaxInputName:"Max input characters",settingMaxInputDesc:"Longer notes are truncated before sending to the model. Raise this for long-context models.",promptHeader:"Prompt",settingPromptLanguageName:"Output language",settingPromptLanguageDesc:"Controls title/gist/bullets language. Anchor is always copied verbatim from source.",settingCardRangeName:"Card count range",settingCardRangeDesc:"The model adapts segmentation within this range.",settingCustomPromptName:"Custom system prompt",settingCustomPromptDesc:"Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}",settingCustomPromptPlaceholder:"Leave blank to use built-in prompt",settingTestBackendName:"Test current backend",settingTestBackendDescApi:"Sends a minimal LLM request to validate API settings.",settingTestBackendDescCli:"Runs ` --version` to verify the binary can be spawned.",backendTestFailed:"\u2717 Backend test failed: {error}",settingExportFolderName:"Export folder",settingExportFolderDesc:"Parallel-note output location, relative to the Vault root.",cacheHeader:"Cache",settingMaxCacheName:"Max cached notes",settingMaxCacheDesc:"Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.",cachePruned:"Pruned {count} old cache entries",cachedNotesName:"Cached notes: {count}",cachedNotesDesc:"Cache is invalidated by source SHA1 and generation settings fingerprint.",clearAllCacheButton:"Clear all caches"}};function it(a){let e=a&&a.uiLanguage;return e==="zh"||e==="en"?e:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function y(a,e,t){let r=it(a),i=H[r]||H.en,n=H.en[e]||H.zh[e]||e,s=i[e]||n;return String(s).replace(/\{([a-zA-Z0-9_]+)\}/g,(o,l)=>t&&Object.prototype.hasOwnProperty.call(t,l)?String(t[l]):o)}function ue(a){let e=[`## ${a.title}`];if(a.anchor){let t=a.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return a.gist&&e.push(a.gist),a.bullets&&a.bullets.length>0&&e.push(a.bullets.map(t=>`- ${t}`).join(`
`)),e.join(`
-`)}function Pe(a){return[a.title,a.gist||"",...(a.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(`
-`)}function j(a,e){let t=[`# ${a||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let n of e||[])t.push(re(n));return t.join(`
+`)}function Re(a){return[a.title,a.gist||"",...(a.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(`
+`)}function G(a,e){let t=[`# ${a||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let r of e||[])t.push(ue(r));return t.join(`
-`)}var Se=R(require("crypto")),se=2e4,ze=2,S=2,N=100,F={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},oe={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},m={uiLanguage:"auto",backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:se,maxCacheEntries:N,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},w={"anthropic-messages":{label:"Anthropic Messages",defaultBaseUrl:"https://api.anthropic.com/v1",defaultAuthType:"x-api-key"},"openai-chat":{label:"OpenAI Chat Completions",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer",tokenLimitField:"max_tokens"},"openai-responses":{label:"OpenAI Responses",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer"},"google-generative-ai":{label:"Google Gemini generateContent",defaultBaseUrl:"https://generativelanguage.googleapis.com/v1beta",defaultAuthType:"x-goog-api-key"}},ce={auto:"Auto",bearer:"Authorization: Bearer","x-api-key":"x-api-key","x-goog-api-key":"x-goog-api-key","api-key":"api-key",none:"None"},T={anthropic:{label:"Anthropic",format:"anthropic-messages",baseUrl:"https://api.anthropic.com/v1",authType:"x-api-key",envVar:"ANTHROPIC_API_KEY",model:"claude-sonnet-4-6"},openai:{label:"OpenAI",format:"openai-chat",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",tokenLimitField:"max_completion_tokens",model:"gpt-5.1"},"openai-responses":{label:"OpenAI Responses",format:"openai-responses",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",modelPrefix:"openai",model:"gpt-5.1"},google:{label:"Google Gemini",format:"google-generative-ai",baseUrl:"https://generativelanguage.googleapis.com/v1beta",authType:"x-goog-api-key",envVar:"GEMINI_API_KEY",model:"gemini-3-pro-preview"},openrouter:{label:"OpenRouter",format:"openai-chat",baseUrl:"https://openrouter.ai/api/v1",authType:"bearer",envVar:"OPENROUTER_API_KEY",model:""},groq:{label:"Groq",format:"openai-chat",baseUrl:"https://api.groq.com/openai/v1",authType:"bearer",envVar:"GROQ_API_KEY",model:""},deepseek:{label:"DeepSeek",format:"openai-chat",baseUrl:"https://api.deepseek.com",authType:"bearer",envVar:"DEEPSEEK_API_KEY",model:"deepseek-chat"},moonshot:{label:"Moonshot / Kimi",format:"openai-chat",baseUrl:"https://api.moonshot.ai/v1",authType:"bearer",envVar:"MOONSHOT_API_KEY",model:"kimi-k2.5"},qianfan:{label:"Baidu Qianfan",format:"openai-chat",baseUrl:"https://qianfan.baidubce.com/v2",authType:"bearer",envVar:"QIANFAN_API_KEY",model:"deepseek-v3.2"},minimax:{label:"MiniMax (Anthropic-compatible)",format:"anthropic-messages",baseUrl:"https://api.minimax.io/anthropic",authType:"bearer",envVar:"MINIMAX_API_KEY",model:"MiniMax-M2.1"},xai:{label:"xAI",format:"openai-chat",baseUrl:"https://api.x.ai/v1",authType:"bearer",envVar:"XAI_API_KEY",model:""},mistral:{label:"Mistral",format:"openai-chat",baseUrl:"https://api.mistral.ai/v1",authType:"bearer",envVar:"MISTRAL_API_KEY",model:""},cerebras:{label:"Cerebras",format:"openai-chat",baseUrl:"https://api.cerebras.ai/v1",authType:"bearer",envVar:"CEREBRAS_API_KEY",model:""},zai:{label:"Z.AI / GLM",format:"openai-chat",baseUrl:"https://api.z.ai/api/paas/v4",authType:"bearer",envVar:"ZAI_API_KEY",model:""},ollama:{label:"Ollama (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:11434/v1",authType:"none",envVar:"",model:""},lmstudio:{label:"LM Studio (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:1234/v1",authType:"none",envVar:"",model:""},"custom-openai":{label:"Custom OpenAI-compatible",format:"openai-chat",baseUrl:"",authType:"bearer",envVar:"",model:""},"custom-anthropic":{label:"Custom Anthropic-compatible",format:"anthropic-messages",baseUrl:"",authType:"x-api-key",envVar:"",model:""}};function P(a){return Se.default.createHash("sha1").update(a,"utf8").digest("hex")}function ie(a){return Array.isArray(a)?"["+a.map(ie).join(",")+"]":a&&typeof a=="object"?"{"+Object.keys(a).sort().map(e=>JSON.stringify(e)+":"+ie(a[e])).join(",")+"}":JSON.stringify(a)}function G(a){return a==="api"||a==="anthropic-api"}function y(a){return T[a.apiProvider]||T.anthropic}function b(a){let e=y(a),t=(a.apiFormat||e.format||"").trim();return w[t]?t:e.format}function D(a){let e=b(a),t=y(a),n=(a.apiBaseUrl||"").trim();if(n)return n.replace(/\/+$/,"");if((a.apiProvider||"").startsWith("custom-"))throw new Error("\u81EA\u5B9A\u4E49 provider \u9700\u8981\u586B\u5199 API Base URL\u3002");let i=(t.baseUrl||w[e].defaultBaseUrl||"").trim();if(!i)throw new Error("API Base URL \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 provider \u6216\u586B\u5199\u81EA\u5B9A\u4E49 base URL\u3002");return i.replace(/\/+$/,"")}function le(a){let e=(a.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=y(a),n=b(a);return t.authType||w[n].defaultAuthType||"bearer"}function Ne(a){let e=(a.apiKey||"").trim();if(e)return e;let t=(a.apiKeyEnvVar||y(a).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function I(a){let e=(a.model||"").trim();if(!e)throw new Error("Model \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199\u6A21\u578B ID\u3002");let t=y(a),n=[a.apiProvider,t.modelPrefix].map(r=>(r||"").trim()).filter(Boolean),i=e.toLowerCase();for(let r of n){let o=r.toLowerCase();if(i.startsWith(o+"/"))return e.slice(r.length+1).trim()}return e}function pe(a,e){let t=y(a),n=T[e]||T.anthropic,i=(a.model||"").trim(),r=!i||i===m.model||!!t.model&&i===t.model;a.apiProvider=e,a.apiFormat=n.format,a.apiBaseUrl=n.baseUrl,a.apiAuthType=n.authType||"auto",a.apiKeyEnvVar=n.envVar||"",r&&(a.model=n.model||"")}function ue(a){oe[a.uiLanguage]||(a.uiLanguage=m.uiLanguage),(!a.apiProvider||!T[a.apiProvider])&&(a.apiProvider="anthropic");let e=y(a);(!a.apiFormat||!w[a.apiFormat])&&(a.apiFormat=e.format),(!a.apiAuthType||!ce[a.apiAuthType])&&(a.apiAuthType="auto"),a.backend==="anthropic-api"&&(a.apiProvider=a.apiProvider||"anthropic",a.apiFormat=a.apiFormat||"anthropic-messages",a.apiBaseUrl=a.apiBaseUrl||T.anthropic.baseUrl,a.apiAuthType=a.apiAuthType||"x-api-key",a.apiKeyEnvVar=a.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(a.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(a.apiMaxTokens=m.apiMaxTokens);let n=Number(a.maxDocChars);return(!Number.isFinite(n)||n<1e3)&&(a.maxDocChars=m.maxDocChars),a.maxCacheEntries=Fe(a.maxCacheEntries),F[a.promptLanguage]||(a.promptLanguage=m.promptLanguage),a.minCards=ke(a.minCards,m.minCards),a.maxCards=ke(a.maxCards,m.maxCards),a.maxCardsEe(o)-Ee(l)||r.localeCompare(s)).slice(0,n.length-t).map(([r])=>r);for(let r of i)delete a[r];return i}function $(a){let e=ue(Object.assign({},m,a||{})),t=G(e.backend),n=y(e),i=b(e),r=t?(e.apiBaseUrl||n.baseUrl||w[i]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return P(ie({cacheSchemaVersion:S,promptVersion:ze,maxDocChars:Number(e.maxDocChars)||m.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:P(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?i:"",apiBaseUrl:r,apiAuthType:t?le(e):"",apiHeadersHash:t?P(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||m.apiMaxTokens:0,structuredOutputVersion:1}))}function H(a,e,t){return!!a&&a.schemaVersion===S&&a.contentHash===P(e)&&a.settingsHash===$(t)}function We(a){return a==="en"?"Write title, gist, and bullets in English.":a==="auto"?"Write title, gist, and bullets in the main language of the source document.":"\u7528\u4E2D\u6587\u8F93\u51FA title\u3001gist \u548C bullets\u3002"}function Ye(a){return a==="en"?`{"cards":[
+`)}function pe(a,e,t){if(!Number.isFinite(e)||e<=0)return-1;let r=Math.floor(e),i=t<0?-1:1;return a<0||a>=r?i<0?r-1:0:Math.min(r-1,Math.max(0,a+i))}function de(a,e){if(!Array.isArray(a)||e<0||e>=a.length)return-1;let t=Number(a[e]?.startLine);return Number.isFinite(t)&&t>=0?t:-1}var Ue=U(require("crypto"));var me=2e4,st=2,D=2,I=100,M={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},ge={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},g={uiLanguage:"auto",backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:me,maxCacheEntries:I,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},w={"anthropic-messages":{label:"Anthropic Messages",defaultBaseUrl:"https://api.anthropic.com/v1",defaultAuthType:"x-api-key"},"openai-chat":{label:"OpenAI Chat Completions",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer",tokenLimitField:"max_tokens"},"openai-responses":{label:"OpenAI Responses",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer"},"google-generative-ai":{label:"Google Gemini generateContent",defaultBaseUrl:"https://generativelanguage.googleapis.com/v1beta",defaultAuthType:"x-goog-api-key"}},fe={auto:"Auto",bearer:"Authorization: Bearer","x-api-key":"x-api-key","x-goog-api-key":"x-goog-api-key","api-key":"api-key",none:"None"},P={anthropic:{label:"Anthropic",format:"anthropic-messages",baseUrl:"https://api.anthropic.com/v1",authType:"x-api-key",envVar:"ANTHROPIC_API_KEY",model:"claude-sonnet-4-6"},openai:{label:"OpenAI",format:"openai-chat",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",tokenLimitField:"max_completion_tokens",model:"gpt-5.1"},"openai-responses":{label:"OpenAI Responses",format:"openai-responses",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",modelPrefix:"openai",model:"gpt-5.1"},google:{label:"Google Gemini",format:"google-generative-ai",baseUrl:"https://generativelanguage.googleapis.com/v1beta",authType:"x-goog-api-key",envVar:"GEMINI_API_KEY",model:"gemini-3-pro-preview"},openrouter:{label:"OpenRouter",format:"openai-chat",baseUrl:"https://openrouter.ai/api/v1",authType:"bearer",envVar:"OPENROUTER_API_KEY",model:""},groq:{label:"Groq",format:"openai-chat",baseUrl:"https://api.groq.com/openai/v1",authType:"bearer",envVar:"GROQ_API_KEY",model:""},deepseek:{label:"DeepSeek",format:"openai-chat",baseUrl:"https://api.deepseek.com",authType:"bearer",envVar:"DEEPSEEK_API_KEY",model:"deepseek-chat"},moonshot:{label:"Moonshot / Kimi",format:"openai-chat",baseUrl:"https://api.moonshot.ai/v1",authType:"bearer",envVar:"MOONSHOT_API_KEY",model:"kimi-k2.5"},qianfan:{label:"Baidu Qianfan",format:"openai-chat",baseUrl:"https://qianfan.baidubce.com/v2",authType:"bearer",envVar:"QIANFAN_API_KEY",model:"deepseek-v3.2"},minimax:{label:"MiniMax (Anthropic-compatible)",format:"anthropic-messages",baseUrl:"https://api.minimax.io/anthropic",authType:"bearer",envVar:"MINIMAX_API_KEY",model:"MiniMax-M2.1"},xai:{label:"xAI",format:"openai-chat",baseUrl:"https://api.x.ai/v1",authType:"bearer",envVar:"XAI_API_KEY",model:""},mistral:{label:"Mistral",format:"openai-chat",baseUrl:"https://api.mistral.ai/v1",authType:"bearer",envVar:"MISTRAL_API_KEY",model:""},cerebras:{label:"Cerebras",format:"openai-chat",baseUrl:"https://api.cerebras.ai/v1",authType:"bearer",envVar:"CEREBRAS_API_KEY",model:""},zai:{label:"Z.AI / GLM",format:"openai-chat",baseUrl:"https://api.z.ai/api/paas/v4",authType:"bearer",envVar:"ZAI_API_KEY",model:""},ollama:{label:"Ollama (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:11434/v1",authType:"none",envVar:"",model:""},lmstudio:{label:"LM Studio (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:1234/v1",authType:"none",envVar:"",model:""},"custom-openai":{label:"Custom OpenAI-compatible",format:"openai-chat",baseUrl:"",authType:"bearer",envVar:"",model:""},"custom-anthropic":{label:"Custom Anthropic-compatible",format:"anthropic-messages",baseUrl:"",authType:"x-api-key",envVar:"",model:""}};function N(a){return Ue.default.createHash("sha1").update(a,"utf8").digest("hex")}function he(a){return Array.isArray(a)?"["+a.map(he).join(",")+"]":a&&typeof a=="object"?"{"+Object.keys(a).sort().map(e=>JSON.stringify(e)+":"+he(a[e])).join(",")+"}":JSON.stringify(a)}function L(a){return a==="api"||a==="anthropic-api"}function C(a){return P[a.apiProvider]||P.anthropic}function b(a){let e=C(a),t=(a.apiFormat||e.format||"").trim();return w[t]?t:e.format}function E(a){let e=b(a),t=C(a),r=(a.apiBaseUrl||"").trim();if(r)return r.replace(/\/+$/,"");if((a.apiProvider||"").startsWith("custom-"))throw new Error(y(a,"errorCustomProviderBaseUrlRequired"));let i=(t.baseUrl||w[e].defaultBaseUrl||"").trim();if(!i)throw new Error(y(a,"errorApiBaseUrlMissing"));return i.replace(/\/+$/,"")}function ye(a){let e=(a.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=C(a),r=b(a);return t.authType||w[r].defaultAuthType||"bearer"}function je(a){let e=(a.apiKey||"").trim();if(e)return e;let t=(a.apiKeyEnvVar||C(a).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function k(a){let e=(a.model||"").trim();if(!e)throw new Error(y(a,"errorModelMissing"));let t=C(a),r=[a.apiProvider,t.modelPrefix].map(n=>(n||"").trim()).filter(Boolean),i=e.toLowerCase();for(let n of r){let s=n.toLowerCase();if(i.startsWith(s+"/"))return e.slice(n.length+1).trim()}return e}function Ce(a,e){let t=C(a),r=P[e]||P.anthropic,i=(a.model||"").trim(),n=!i||i===g.model||!!t.model&&i===t.model;a.apiProvider=e,a.apiFormat=r.format,a.apiBaseUrl=r.baseUrl,a.apiAuthType=r.authType||"auto",a.apiKeyEnvVar=r.envVar||"",n&&(a.model=r.model||"")}function ve(a){ge[a.uiLanguage]||(a.uiLanguage=g.uiLanguage),(!a.apiProvider||!P[a.apiProvider])&&(a.apiProvider="anthropic");let e=C(a);(!a.apiFormat||!w[a.apiFormat])&&(a.apiFormat=e.format),(!a.apiAuthType||!fe[a.apiAuthType])&&(a.apiAuthType="auto"),a.backend==="anthropic-api"&&(a.apiProvider=a.apiProvider||"anthropic",a.apiFormat=a.apiFormat||"anthropic-messages",a.apiBaseUrl=a.apiBaseUrl||P.anthropic.baseUrl,a.apiAuthType=a.apiAuthType||"x-api-key",a.apiKeyEnvVar=a.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(a.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(a.apiMaxTokens=g.apiMaxTokens);let r=Number(a.maxDocChars);return(!Number.isFinite(r)||r<1e3)&&(a.maxDocChars=g.maxDocChars),a.maxCacheEntries=He(a.maxCacheEntries),M[a.promptLanguage]||(a.promptLanguage=g.promptLanguage),a.minCards=Ve(a.minCards,g.minCards),a.maxCards=Ve(a.maxCards,g.maxCards),a.maxCardsBe(s)-Be(l)||n.localeCompare(o)).slice(0,r.length-t).map(([n])=>n);for(let n of i)delete a[n];return i}function J(a){let e=ve(Object.assign({},g,a||{})),t=L(e.backend),r=C(e),i=b(e),n=t?(e.apiBaseUrl||r.baseUrl||w[i]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return N(he({cacheSchemaVersion:D,promptVersion:st,maxDocChars:Number(e.maxDocChars)||g.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:N(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?i:"",apiBaseUrl:n,apiAuthType:t?ye(e):"",apiHeadersHash:t?N(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||g.apiMaxTokens:0,structuredOutputVersion:1}))}function K(a,e,t){return!!a&&a.schemaVersion===D&&a.contentHash===N(e)&&a.settingsHash===J(t)}function ot(a){return a==="en"?"Write title, gist, and bullets in English.":a==="auto"?"Write title, gist, and bullets in the main language of the source document.":"\u7528\u4E2D\u6587\u8F93\u51FA title\u3001gist \u548C bullets\u3002"}function ct(a){return a==="en"?`{"cards":[
{"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]}
]}`:`{"cards":[
{"title":"U \u578B\u6536\u76CA\u66F2\u7EBF","anchor":"\u90A3\u8C01\u53C8\u4F1A\u88AB AI \u6240\u53D7\u76CA\uFF1F\u6574\u4F53\u6765\u770B\uFF0C\u5B83\u628A\u6574\u4E2A\u5206\u6570\u53D8\u6210\u4E86\u4E00\u5206\u5230\u4E03\u5206","gist":"AI \u751F\u4EA7\u529B\u6536\u76CA\u5448 U \u578B\uFF0C\u4E24\u7AEF\u53D7\u76CA\u6700\u5927\u3001\u4E2D\u95F4\u5C42\u584C\u9677","bullets":["\u6700\u9AD8\u85AA\u5C97\u4F4D\uFF08\u8F6F\u4EF6\u7BA1\u7406\uFF09\u901A\u8FC7\u52A0\u901F\u65E2\u6709\u5DE5\u4F5C\u53D7\u76CA\u6700\u5927","\u6700\u4F4E\u85AA\u5C97\u4F4D\uFF08\u5916\u5356\u5458\u3001\u56ED\u827A\u5DE5\uFF09\u7528 AI \u5F00\u526F\u4E1A\u521B\u9020\u65B0\u6536\u5165","\u4E2D\u95F4\u5C42\u79D1\u5B66\u5BB6\u3001\u5F8B\u5E08\u6536\u76CA\u6700\u5C11\uFF0C\u90E8\u5206\u56E0\u5BF9 prompt \u7CBE\u5EA6\u4FE1\u4EFB\u4E0D\u8DB3","\u5168\u4F53\u5747\u5206 5.1/7\uFF0C42% \u62A5\u544A\u6536\u76CA\u6A21\u7CCA"]}
-]}`}function qe(a,e){return String(a||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,n)=>Object.prototype.hasOwnProperty.call(e,n)?String(e[n]):t)}function he(a,e){let t=Number(e.maxDocChars)||se,n=F[e.promptLanguage]?e.promptLanguage:m.promptLanguage,i=Math.max(1,Number(e.minCards)||m.minCards),r=Math.max(i,Number(e.maxCards)||m.maxCards),o=We(n),s=a.length>t?a.slice(0,t)+(n==="en"?`
+]}`}function lt(a,e){return String(a||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,r)=>Object.prototype.hasOwnProperty.call(e,r)?String(e[r]):t)}function ut(a,e,t,r,i,n){return a==="en"?`You are a long-form reading summary assistant. After reading the full document, split it into ${e}-${t} natural topic units. They do not need to match markdown headings; use a complete argument or topic as the unit, merging short sections and splitting long ones when needed.
-[Document truncated]`:`
+Each card has one guiding sentence plus several bullets. Bullets carry details; gist is the lead-in.
-[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):a,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',d=Ye(n),f={minCards:i,maxCards:r,languageInstruction:o,schema:l,example:d},p=qe(e.customSystemPrompt,f).trim(),h=`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${i}-${r} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002
+Language:
+- ${r}
+
+For each unit, output:
+
+- title: a concise 3-10 word heading that clearly states what the section is about; avoid vague labels like "Background" or "Introduction"
+- anchor: a verbatim quote from the start of this unit, copied 1:1 from the source, 40-80 characters where possible, preserving punctuation, spaces, and line breaks; it is only used internally for positioning
+- gist: one guiding sentence, 20-40 words, stating the core claim or conclusion
+- bullets: 3-6 supporting bullets, each 20-50 words, carrying data, comparisons, mechanisms, examples, or counterintuitive observations. Gist and bullets must not repeat each other.
+
+Rules:
+- anchor must exact-substring-match the source. Never paraphrase, translate, summarize, or alter it.
+- Choose the earliest sufficiently distinctive quote for each unit; avoid generic phrases.
+- Every card must include both gist and bullets.
+- Each bullet must be a standalone assertion; avoid sequencing phrases such as "first" or "then".
+- Output strict JSON only: no markdown fence, no explanation, no tool call.
+
+Output shape:
+${i}
+
+Example:
+${n}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${e}-${t} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002
**\u6BCF\u5F20\u5361\u7247\u7684\u7ED3\u6784\uFF1A\u4E00\u53E5\u8BDD\u9886\u8BFB + \u82E5\u5E72\u6761 bullet\u3002bullet \u627F\u8F7D\u7EC6\u8282\uFF0Cgist \u662F\u4E00\u53E5\u8BDD\u5BFC\u8BFB\u3002**
\u8BED\u8A00\uFF1A
-- ${o}
+- ${r}
\u5BF9\u6BCF\u4E2A\u5355\u5143\u8F93\u51FA\uFF1A
@@ -37,32 +62,40 @@
- \u4E25\u683C\u53EA\u8F93\u51FA JSON\uFF0C\u65E0 markdown fence\u3001\u65E0\u89E3\u91CA\u3001\u65E0 tool call
\u8F93\u51FA\u683C\u5F0F\uFF1A
-${l}
+${i}
\u793A\u4F8B\uFF1A
-${d}`,g=p?`${p}
-
-\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A
-- \u5FC5\u987B\u8F93\u51FA ${i}-${r} \u5F20 cards\u3002
-- ${o}
+${n}`}function pt(a,e,t,r,i){return a==="en"?`Non-overridable output contract:
+- Must output ${e}-${t} cards.
+- ${r}
+- anchor must be copied verbatim from the source and exact-substring-match the source.
+- Output strict JSON only: no markdown fence, no explanation, no tool call.
+- JSON shape: ${i}`:`\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A
+- \u5FC5\u987B\u8F93\u51FA ${e}-${t} \u5F20 cards\u3002
+- ${r}
- anchor \u5FC5\u987B\u4ECE\u539F\u6587\u9010\u5B57\u590D\u5236\uFF0C\u5FC5\u987B\u80FD\u5728\u539F\u6587 exact substring match \u627E\u5230\u3002
- \u4E25\u683C\u53EA\u8F93\u51FA JSON\uFF0C\u65E0 markdown fence\u3001\u65E0\u89E3\u91CA\u3001\u65E0 tool call\u3002
-- JSON shape: ${l}`:h,u=n==="en"?`Source document:
+- JSON shape: ${i}`}function be(a,e){let t=Number(e.maxDocChars)||me,r=M[e.promptLanguage]?e.promptLanguage:g.promptLanguage,i=Math.max(1,Number(e.minCards)||g.minCards),n=Math.max(i,Number(e.maxCards)||g.maxCards),s=ot(r),o=a.length>t?a.slice(0,t)+(r==="en"?`
-${s}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A
+[Document truncated]`:`
-${s}`;return{system:g,user:u}}var K="record_parallel_reader_cards";function Xe(a){let e=[],t=-1,n=0,i=!1,r=!1;for(let o=0;o0&&n--,n===0&&t>=0&&(e.push(a.slice(t,o+1)),t=-1))}return e}function me(a){let e=(a||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let i=t[1].trim();try{return JSON.parse(i),i}catch{}}let n=Xe(e);n.sort((i,r)=>r.length-i.length);for(let i of n)try{return JSON.parse(i),i}catch{}return e}function A(a){let e=me(a),t;try{t=JSON.parse(e)}catch{throw new Error(`LLM \u8FD4\u56DE\u975E JSON\uFF1A
-`+(a||"").slice(0,500))}return M(t)}function M(a){return(a&&Array.isArray(a.cards)?a.cards:[]).filter(t=>t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(n=>typeof n=="string"):[]}))}function L(a){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return a&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function De(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:L(!0)}}}function Ie(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:L(!0)}}}function Me(){return{name:K,description:"Return the generated Parallel Reader summary cards.",input_schema:L(!0)}}function ge(a,e){let t=a.replace(/\/+$/,"");for(let n of e)if(t.endsWith(n))return t;return t+e[0]}function Ze(a){let e=(a||"").trim();if(!e)return{};if(e.startsWith("{")){let n;try{n=JSON.parse(e)}catch(r){throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u89E3\u6790\u5931\u8D25\uFF1A"+r.message)}if(!n||typeof n!="object"||Array.isArray(n))throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u5FC5\u987B\u662F\u5BF9\u8C61");let i={};for(let[r,o]of Object.entries(n))typeof o=="string"&&r.trim()&&(i[r.trim()]=o);return i}let t={};for(let n of e.split(/\r?\n/)){let i=n.trim();if(!i||i.startsWith("#"))continue;let r=i.indexOf(":");if(r<=0)throw new Error("\u81EA\u5B9A\u4E49 headers \u6BCF\u884C\u683C\u5F0F\u5E94\u4E3A `Header-Name: value`");let o=i.slice(0,r).trim(),s=i.slice(r+1).trim();o&&(t[o]=s)}return t}function Qe(a){let e=le(a);if(e==="none")return{};let t=Ne(a);if(!t){let n=(a.apiKeyEnvVar||y(a).envVar||"").trim(),i=n?` \u6216\u73AF\u5883\u53D8\u91CF ${n}`:"";throw new Error(`API key \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 API Key${i}\u3002`)}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function q(a,e){return{"content-type":"application/json",...Qe(a),...e||{},...Ze(a.apiHeaders)}}function et(a,e){if(a.json&&typeof a.json=="object")return a.json;try{return JSON.parse(a.text||"{}")}catch{throw new Error(`${e} \u8FD4\u56DE\u975E JSON\uFF1A
-${(a.text||"").slice(0,500)}`)}}async function Le(a,e,t,n,i){let r;try{r=await a({url:t,method:"POST",headers:n,body:JSON.stringify(i),throw:!1})}catch(o){throw new Error(`${e} \u8BF7\u6C42\u5931\u8D25\uFF1A`+(o.message||o))}if(r.status>=400)throw new Error(`${e} API ${r.status}: ${(r.text||"").slice(0,500)}`);return et(r,e)}function tt(a){let e=String(a&&a.message?a.message:a);return/API (400|404|422):/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function X(a,e,t,n,i,r){try{return await Le(a,e,t,n,i)}catch(o){if(!r||!tt(o))throw o;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,o),Le(a,e+" fallback",t,n,r)}}function fe(a){return typeof a=="string"?a:Array.isArray(a)?a.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):a&&typeof a=="object"&&(a.text||a.output_text)||""}function at(a){if(typeof a.output_text=="string")return a.output_text;let e=[],t=n=>{if(n&&typeof n!="string"){if(Array.isArray(n)){n.forEach(t);return}typeof n=="object"&&(typeof n.text=="string"&&e.push(n.text),typeof n.output_text=="string"&&e.push(n.output_text),n.type==="output_text"&&typeof n.content=="string"&&e.push(n.content),n.content&&t(n.content),n.output&&t(n.output))}};return t(a.output),e.join("")}function ye(a){let e=y(a),t=w[b(a)]||{};return e.tokenLimitField||t.tokenLimitField||"max_tokens"}function J(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),max_tokens:Number(t.apiMaxTokens)||4096,system:a,messages:[{role:"user",content:e}]};return i&&(r.tools=[Me()],r.tool_choice={type:"tool",name:K}),r}function z(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),messages:[{role:"system",content:a},{role:"user",content:e}]};return r[ye(t)]=Number(t.apiMaxTokens)||4096,i&&(r.response_format=De()),r}function W(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),instructions:a,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return i&&(r.text=Ie()),r}function Y(a,e,t,n){let i=!n||n.structured!==!1,r={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return i&&(r.responseMimeType="application/json",r.responseJsonSchema=L(!1)),{systemInstruction:{parts:[{text:a}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:r}}function nt(a){let t=(Array.isArray(a&&a.content)?a.content:[]).find(n=>n&&n.type==="tool_use"&&n.name===K);return t?typeof t.input=="string"?A(t.input):t.input&&typeof t.input=="object"?M(t.input):[]:null}async function rt(a,e,t,n){let i=ge(D(n),["/messages"]),r=await X(a,"Anthropic-compatible",i,q(n,{"anthropic-version":"2023-06-01"}),J(e,t,n),J(e,t,n,{structured:!1})),o=nt(r);if(o)return o;let s=(r.content||[]).map(l=>fe(l)).join("").trim();return A(s)}async function it(a,e,t,n){let i=ge(D(n),["/chat/completions"]),o=((await X(a,"OpenAI-compatible Chat",i,q(n),z(e,t,n),z(e,t,n,{structured:!1}))).choices||[])[0]||{},s=fe(o.message?.content||o.text||"").trim();return A(s)}async function st(a,e,t,n){let i=ge(D(n),["/responses"]),r=await X(a,"OpenAI Responses",i,q(n),W(e,t,n),W(e,t,n,{structured:!1}));return A(at(r).trim())}async function ot(a,e,t,n){let i=encodeURIComponent(I(n)),r=D(n);/:generateContent(?:\?|$)/.test(r)||(r=`${r.replace(/\/+$/,"")}/models/${i}:generateContent`);let o=q(n),f=((((await X(a,"Google Gemini",r,o,Y(e,t,n),Y(e,t,n,{structured:!1}))).candidates||[])[0]||{}).content?.parts||[]).map(p=>fe(p)).join("").trim();return A(f)}async function O(a,e,t,n){switch(b(n)){case"openai-chat":return it(a,e,t,n);case"openai-responses":return st(a,e,t,n);case"google-generative-ai":return ot(a,e,t,n);case"anthropic-messages":default:return rt(a,e,t,n)}}async function Oe(a,e){await O(a,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=b(e);return`${y(e).label} / ${w[t].label}`}var k=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},x=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},we=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new x(this.key)}},_=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new k(e);let n=new we(e);this.jobs.set(e,n);try{n.setPhase("running");let i=await t(n);return n.throwIfCancelled(),i}catch(i){throw n.cancelled&&!(i instanceof x)?new x(e):i}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function Ce(a){if(a instanceof x||a?.code==="cancelled")return"cancelled";let e=String(a&&a.message?a.message:a);return/api key|unauthorized|401|403|认证|权限/i.test(e)?"auth":/timeout|超时|timed out/i.test(e)?"timeout":/429|rate limit|too many requests/i.test(e)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(e)?"schema":/model 未设置|base url|配置|config/i.test(e)?"config":"unknown"}var C="parallel-reader-view";function te(a,e){if(e&&e.trim())return e.trim();let t=ee.default.homedir(),n=[v.default.join(t,"bin",a),v.default.join(t,".local/bin",a),v.default.join(t,".claude/local",a),v.default.join(t,".codex/bin",a),v.default.join(t,".bun/bin",a),v.default.join(t,".npm-global/bin",a),v.default.join(t,".cargo/bin",a),"/opt/homebrew/bin/"+a,"/usr/local/bin/"+a];for(let i of n)try{if(Re.default.existsSync(i))return i}catch{}return a}function ae(a,e,t,n,i){return new Promise((r,o)=>{let s,l=!1,d,f=u=>{l||(l=!0,d&&clearTimeout(d),o(u))},p=u=>{l||(l=!0,d&&clearTimeout(d),r(u))};try{s=(0,Ve.spawn)(a,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",v.default.join(ee.default.homedir(),".local/bin"),v.default.join(ee.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return o(new Error(`Failed to start ${a}: ${u.message}`))}let h="",g="";if(d=setTimeout(()=>{try{s.kill("SIGKILL")}catch{}f(new Error(`CLI timed out (${n}ms)`))},n),i&&i.onCancel(()=>{try{s.kill("SIGKILL")}catch{}f(new x(i.key))}),s.stdout.on("data",u=>{h+=u.toString("utf8")}),s.stderr.on("data",u=>{g+=u.toString("utf8")}),s.on("error",u=>{f(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),s.on("close",u=>{if(!l){if(u!==0)return f(new Error(`CLI exited with code ${u}
+[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):a,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',d=ct(r),f={minCards:i,maxCards:n,languageInstruction:s,schema:l,example:d},p=lt(e.customSystemPrompt,f).trim(),h=pt(r,i,n,s,l),m=ut(r,i,n,s,l,d),u=p?`${p}
+
+${h}`:m,S=r==="en"?`Source document:
+
+${o}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A
+
+${o}`;return{system:u,user:S}}function Ae(a){return String(a||"").split("/").map(e=>e.trim()).filter(Boolean).join("/")}function xe(a){let e=Ae(a);if(!e)return[];let t=e.split("/");return t.map((r,i)=>t.slice(0,i+1).join("/"))}async function Ge(a,e){for(let t of xe(e))if(!a.vault.getAbstractFileByPath(t))try{await a.vault.createFolder(t)}catch(r){if(!a.vault.getAbstractFileByPath(t))throw r}}var $="record_parallel_reader_cards";function dt(a){let e=[],t=-1,r=0,i=!1,n=!1;for(let s=0;s0&&r--,r===0&&t>=0&&(e.push(a.slice(t,s+1)),t=-1))}return e}function Te(a){let e=(a||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let i=t[1].trim();try{return JSON.parse(i),i}catch{}}let r=dt(e);r.sort((i,n)=>n.length-i.length);for(let i of r)try{return JSON.parse(i),i}catch{}return e}function T(a,e){let t=Te(a),r;try{r=JSON.parse(t)}catch{throw new Error(y(e,"errorLlmNonJson",{excerpt:(a||"").slice(0,500)}))}return O(r)}function O(a){return(a&&Array.isArray(a.cards)?a.cards:[]).filter(t=>t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(r=>typeof r=="string"):[]}))}function _(a){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return a&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function Je(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:_(!0)}}}function Ke(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:_(!0)}}}function $e(){return{name:$,description:"Return the generated Parallel Reader summary cards.",input_schema:_(!0)}}function ht(a){return typeof requestAnimationFrame=="function"?requestAnimationFrame(a):setTimeout(()=>a(Date.now()),16)}function mt(a){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(a);return}clearTimeout(a)}function Pe(a,e=ht,t=mt){let r=null,i=()=>{r===null&&(r=e(()=>{r=null,a()}))};return i.cancel=()=>{r!==null&&(t(r),r=null)},i}function Se(a,e){let t=a.replace(/\/+$/,"");for(let r of e)if(t.endsWith(r))return t;return t+e[0]}function gt(a,e){let t=(a||"").trim();if(!t)return{};if(t.startsWith("{")){let i;try{i=JSON.parse(t)}catch(s){throw new Error(y(e,"errorCustomHeadersJsonParse",{error:s.message}))}if(!i||typeof i!="object"||Array.isArray(i))throw new Error(y(e,"errorCustomHeadersJsonObject"));let n={};for(let[s,o]of Object.entries(i))typeof o=="string"&&s.trim()&&(n[s.trim()]=o);return n}let r={};for(let i of t.split(/\r?\n/)){let n=i.trim();if(!n||n.startsWith("#"))continue;let s=n.indexOf(":");if(s<=0)throw new Error(y(e,"errorCustomHeadersLineFormat"));let o=n.slice(0,s).trim(),l=n.slice(s+1).trim();o&&(r[o]=l)}return r}function ft(a){let e=ye(a);if(e==="none")return{};let t=je(a);if(!t){let r=(a.apiKeyEnvVar||C(a).envVar||"").trim(),i=r?y(a,"errorApiKeyEnvHint",{envVar:r}):"";throw new Error(y(a,"errorApiKeyMissing",{hint:i}))}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function X(a,e){return{"content-type":"application/json",...ft(a),...e||{},...gt(a.apiHeaders,a)}}function yt(a,e,t){if(a.json&&typeof a.json=="object")return a.json;try{return JSON.parse(a.text||"{}")}catch{throw new Error(y(t,"errorProviderNonJson",{label:e,excerpt:(a.text||"").slice(0,500)}))}}async function ze(a,e,t,r,i,n){let s;try{s=await a({url:t,method:"POST",headers:r,body:JSON.stringify(i),throw:!1})}catch(o){throw new Error(y(n,"errorProviderRequestFailed",{label:e,error:o.message||o}))}if(s.status>=400)throw new Error(y(n,"errorProviderApiStatus",{label:e,status:s.status,excerpt:(s.text||"").slice(0,500)}));return yt(s,e,n)}function Ct(a){let e=String(a&&a.message?a.message:a);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function Z(a,e,t,r,i,n,s){try{return await ze(a,e,t,r,i,s)}catch(o){if(!n||!Ct(o))throw o;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,o),ze(a,e+" fallback",t,r,n,s)}}function Ee(a){return typeof a=="string"?a:Array.isArray(a)?a.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):a&&typeof a=="object"&&(a.text||a.output_text)||""}function vt(a){if(typeof a.output_text=="string")return a.output_text;let e=[],t=r=>{if(r&&typeof r!="string"){if(Array.isArray(r)){r.forEach(t);return}typeof r=="object"&&(typeof r.text=="string"&&e.push(r.text),typeof r.output_text=="string"&&e.push(r.output_text),r.type==="output_text"&&typeof r.content=="string"&&e.push(r.content),r.content&&t(r.content),r.output&&t(r.output))}};return t(a.output),e.join("")}function ke(a){let e=C(a),t=w[b(a)]||{};return e.tokenLimitField||t.tokenLimitField||"max_tokens"}function z(a,e,t,r){let i=!r||r.structured!==!1,n={model:k(t),max_tokens:Number(t.apiMaxTokens)||4096,system:a,messages:[{role:"user",content:e}]};return i&&(n.tools=[$e()],n.tool_choice={type:"tool",name:$}),n}function q(a,e,t,r){let i=!r||r.structured!==!1,n={model:k(t),messages:[{role:"system",content:a},{role:"user",content:e}]};return n[ke(t)]=Number(t.apiMaxTokens)||4096,i&&(n.response_format=Je()),n}function W(a,e,t,r){let i=!r||r.structured!==!1,n={model:k(t),instructions:a,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return i&&(n.text=Ke()),n}function Y(a,e,t,r){let i=!r||r.structured!==!1,n={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return i&&(n.responseMimeType="application/json",n.responseJsonSchema=_(!1)),{systemInstruction:{parts:[{text:a}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:n}}function wt(a,e){let r=(Array.isArray(a&&a.content)?a.content:[]).find(i=>i&&i.type==="tool_use"&&i.name===$);return r?typeof r.input=="string"?T(r.input,e):r.input&&typeof r.input=="object"?O(r.input):[]:null}async function bt(a,e,t,r){let i=Se(E(r),["/messages"]),n=await Z(a,"Anthropic-compatible",i,X(r,{"anthropic-version":"2023-06-01"}),z(e,t,r),z(e,t,r,{structured:!1}),r),s=wt(n,r);if(s)return s;let o=(n.content||[]).map(l=>Ee(l)).join("").trim();return T(o,r)}async function At(a,e,t,r){let i=Se(E(r),["/chat/completions"]),s=((await Z(a,"OpenAI-compatible Chat",i,X(r),q(e,t,r),q(e,t,r,{structured:!1}),r)).choices||[])[0]||{},o=Ee(s.message?.content||s.text||"").trim();return T(o,r)}async function xt(a,e,t,r){let i=Se(E(r),["/responses"]),n=await Z(a,"OpenAI Responses",i,X(r),W(e,t,r),W(e,t,r,{structured:!1}),r);return T(vt(n).trim(),r)}async function Tt(a,e,t,r){let i=encodeURIComponent(k(r)),n=E(r);/:generateContent(?:\?|$)/.test(n)||(n=`${n.replace(/\/+$/,"")}/models/${i}:generateContent`);let s=X(r),f=((((await Z(a,"Google Gemini",n,s,Y(e,t,r),Y(e,t,r,{structured:!1}),r)).candidates||[])[0]||{}).content?.parts||[]).map(p=>Ee(p)).join("").trim();return T(f,r)}async function R(a,e,t,r){switch(b(r)){case"openai-chat":return At(a,e,t,r);case"openai-responses":return xt(a,e,t,r);case"google-generative-ai":return Tt(a,e,t,r);case"anthropic-messages":default:return bt(a,e,t,r)}}async function qe(a,e){await R(a,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=b(e);return`${C(e).label} / ${w[t].label}`}var F=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},A=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},Ne=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new A(this.key)}},V=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new F(e);let r=new Ne(e);this.jobs.set(e,r);try{r.setPhase("running");let i=await t(r);return r.throwIfCancelled(),i}catch(i){throw r.cancelled&&!(i instanceof A)?new A(e):i}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function Fe(a){if(a instanceof A||a?.code==="cancelled")return"cancelled";let e=String(a&&a.message?a.message:a);return/api key|unauthorized|401|403|认证|权限/i.test(e)?"auth":/timeout|超时|timed out/i.test(e)?"timeout":/429|rate limit|too many requests/i.test(e)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(e)?"schema":/model 未设置|base url|配置|config/i.test(e)?"config":"unknown"}var v="parallel-reader-view";function ae(a,e){if(e&&e.trim())return e.trim();let t=te.default.homedir(),r=[x.default.join(t,"bin",a),x.default.join(t,".local/bin",a),x.default.join(t,".claude/local",a),x.default.join(t,".codex/bin",a),x.default.join(t,".bun/bin",a),x.default.join(t,".npm-global/bin",a),x.default.join(t,".cargo/bin",a),"/opt/homebrew/bin/"+a,"/usr/local/bin/"+a];for(let i of r)try{if(Ye.default.existsSync(i))return i}catch{}return a}function re(a,e,t,r,i){return new Promise((n,s)=>{let o,l=!1,d,f=u=>{l||(l=!0,d&&clearTimeout(d),s(u))},p=u=>{l||(l=!0,d&&clearTimeout(d),n(u))};try{o=(0,We.spawn)(a,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",x.default.join(te.default.homedir(),".local/bin"),x.default.join(te.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return s(new Error(`Failed to start ${a}: ${u.message}`))}let h="",m="";if(d=setTimeout(()=>{try{o.kill("SIGKILL")}catch{}f(new Error(`CLI timed out (${r}ms)`))},r),i&&i.onCancel(()=>{try{o.kill("SIGKILL")}catch{}f(new A(i.key))}),o.stdout.on("data",u=>{h+=u.toString("utf8")}),o.stderr.on("data",u=>{m+=u.toString("utf8")}),o.on("error",u=>{f(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),o.on("close",u=>{if(!l){if(u!==0)return f(new Error(`CLI exited with code ${u}
stderr:
-${g.slice(0,1e3)}`));p({stdout:h,stderr:g})}}),t)try{s.stdin.write(t),s.stdin.end()}catch{}else try{s.stdin.end()}catch{}})}async function ct(a,e,t,n){let i=te("claude",t.cliPath),r=["-p","--output-format","json","--append-system-prompt",a,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&r.push("--model",t.model);let{stdout:o}=await ae(i,r,e,t.cliTimeoutMs,n),s;try{s=JSON.parse(o)}catch{throw new Error(`claude CLI returned a non-JSON envelope:
-`+o.slice(0,500))}let l=s.result||s.content||"";return A(l)}async function lt(a,e,t,n){let i=te("codex",t.cliPath),r=`<>
+${m.slice(0,1e3)}`));p({stdout:h,stderr:m})}}),t)try{o.stdin.write(t),o.stdin.end()}catch{}else try{o.stdin.end()}catch{}})}async function Pt(a,e,t,r){let i=ae("claude",t.cliPath),n=["-p","--output-format","json","--append-system-prompt",a,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&n.push("--model",t.model);let{stdout:s}=await re(i,n,e,t.cliTimeoutMs,r),o;try{o=JSON.parse(s)}catch{throw new Error(`claude CLI returned a non-JSON envelope:
+`+s.slice(0,500))}let l=o.result||o.content||"";return T(l,t)}async function St(a,e,t,r){let i=ae("codex",t.cliPath),n=`<>
${a}
<>
${e}
-Output JSON directly with no explanation.`,o=["exec","--skip-git-repo-check","-"],{stdout:s}=await ae(i,o,r,t.cliTimeoutMs,n);return A(s)}async function pt(a){if(a.backend==="codex"){let e=te("codex",a.cliPath),{stdout:t}=await ae(e,["--version"],"",1e4);return`codex @ ${e}
-${t.trim()}`}if(a.backend==="claude-code"){let e=te("claude",a.cliPath),{stdout:t}=await ae(e,["--version"],"",1e4);return`claude @ ${e}
-${t.trim()}`}if(G(a.backend))return Oe(c.requestUrl,a);throw new Error("Unknown backend: "+a.backend)}async function ut(a,e,t){let{system:n,user:i}=he(a,e),r;switch(e.backend){case"codex":r=await lt(n,i,e,t);break;case"api":r=await O(c.requestUrl,n,i,e);break;case"anthropic-api":r=await O(c.requestUrl,n,i,e);break;case"claude-code":default:r=await ct(n,i,e,t);break}let o=r.map(s=>({title:s.title,level:2,anchor:s.anchor,gist:s.gist,startLine:B(a,s.anchor),bullets:s.bullets}));return o.sort((s,l)=>s.startLine<0&&l.startLine<0?0:s.startLine<0?1:l.startLine<0?-1:s.startLine-l.startLine),o}function Z(a,e,t,n){let i=a.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return i.title=t,typeof c.setIcon=="function"?(0,c.setIcon)(i,e):i.textContent=t,i.addEventListener("click",async r=>{r.preventDefault(),r.stopPropagation();try{await n()}catch(o){console.error(o),new c.Notice(`${t} failed: `+(o.message||o))}}),i}function _e(a,e,t,n,i){let r=a.createEl("button",{cls:i||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof c.setIcon=="function"&&(0,c.setIcon)(r,e),r.createSpan({text:t}),r.addEventListener("click",async o=>{o.preventDefault(),o.stopPropagation();try{await n()}catch(s){console.error(s),new c.Notice(`${t} failed: `+(s.message||s))}}),r}async function Q(a,e){try{await navigator.clipboard.writeText(a),new c.Notice(e)}catch(t){new c.Notice("Copy failed: "+(t.message||t))}}var be=class extends c.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return C}getDisplayText(){return this.plugin.t("displayName")}getIcon(){return"book-open"}async onOpen(){let e=this.containerEl.children[1];e.empty(),e.addClass("parallel-reader-container"),this.renderEmpty()}async onClose(){}renderEmpty(){this.sourceFile=null,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let e=this.containerEl.children[1];e.empty();let t=e.createDiv({cls:"parallel-reader-empty"});t.createEl("h3",{text:this.plugin.t("appTitle")}),t.createEl("p",{text:this.plugin.t("emptyOpenNote")}),t.createEl("code",{text:this.plugin.t("commandGenerate")})}async loadFor(e,t,n){this.sourceFile=e,this.sections=t,this.stale=!!n,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=t||this.plugin.t("loadingDefault"),this.errorMessage="",this.render()}async renderError(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=t||this.plugin.t("errorTitle"),this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let t=this.containerEl.children[1];t.empty();let n=t.createDiv({cls:"parallel-reader-empty"});n.createEl("h3",{text:e.basename}),n.createEl("p",{text:this.plugin.t("emptyNoCache")}),n.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let n=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});n.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=n.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?Z(i,"square",this.plugin.t("actionCancel"),()=>this.plugin.cancelGenerationForFile(this.sourceFile)):Z(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),Z(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),Z(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let s=e.createDiv({cls:"parallel-reader-stale-banner"});s.createSpan({text:this.plugin.t("staleBanner")}),_e(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});s.createDiv({cls:"parallel-reader-spinner"}),s.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});s.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),_e(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let r=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((s,l)=>{let d=r.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(l),s.startLine<0&&d.addClass("parallel-reader-card-unanchored");let f=d.createEl("div",{cls:"parallel-reader-card-title"});if(f.createSpan({text:s.title}),s.startLine<0&&f.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),s.gist){let h=d.createEl("div",{cls:"parallel-reader-gist"});c.MarkdownRenderer.render(this.app,s.gist,h,o,this).catch(()=>{h.setText(s.gist)})}let p=s.bullets||[];if(p.length>0){let h=d.createEl("div",{cls:"parallel-reader-bullets-md"}),g=p.map(u=>`- ${u}`).join(`
-`);c.MarkdownRenderer.render(this.app,g,h,o,this).catch(()=>{h.setText(g)})}else s.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",h=>{let g=window.getSelection();if(g&&g.toString().length>0)return;let u=h.target;u&&u.tagName==="A"||s.startLine>=0&&this.plugin.scrollEditorToLine(s.startLine,this.sourceFile)}),d.addEventListener("contextmenu",h=>{h.preventDefault();let g=new c.Menu;g.addItem(u=>u.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>Q(re(s),this.plugin.t("copiedMarkdown")))),g.addItem(u=>u.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>Q(Pe(s),this.plugin.t("copiedPlain")))),s.anchor&&g.addItem(u=>u.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>Q(s.anchor,this.plugin.t("copiedAnchor")))),g.addSeparator(),s.startLine>=0&&g.addItem(u=>u.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(s.startLine,this.sourceFile))),g.showAtMouseEvent(h)}),this.cards.push(d)})}setActiveSection(e){e!==this.activeIdx&&(this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].removeClass("is-active"),this.activeIdx=e,e>=0&&this.cards[e]&&(this.cards[e].addClass("is-active"),this.cards[e].scrollIntoView({block:"nearest",behavior:"smooth"})))}async exportToVault(){if(!this.sourceFile)return;let e=this.plugin.settings.exportFolder.replace(/\/$/,""),t=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,n=`${e}/${t}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",j(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(`
-`),r=this.plugin.app;if(!r.vault.getAbstractFileByPath(e))try{await r.vault.createFolder(e)}catch{}let s=r.vault.getAbstractFileByPath(n);s instanceof c.TFile?await r.vault.modify(s,i):await r.vault.create(n,i),new c.Notice(this.plugin.t("exported",{path:n}))}},xe=class extends c.Plugin{t(e,t){return ne(this.settings||m,e,t)}async onload(){await this.loadSettings(),this.jobs=new _,this.addRibbonIcon("book-open",this.t("ribbonOpen"),async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(C,e=>new be(e,this)),this.addCommand({id:"parallel-reader-run",name:this.t("cmdRun"),callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:this.t("cmdRegen"),callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:this.t("cmdOpenView"),callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:this.t("cmdExport"),callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:this.t("cmdCopyMarkdown"),callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:this.t("cmdCancel"),callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:this.t("cmdClearCurrent"),callback:async()=>{let e=this.getActiveView();if(!e?.file)return new c.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new c.Notice(this.t("cacheClearedFile",{name:e.file.basename}))}}),this.addCommand({id:"parallel-reader-clear-all",name:this.t("cmdClearAll"),callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new c.Notice(this.t("cacheClearedAll",{count:e}))}}),this.addSettingTab(new ve(this.app,this)),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>this.bindScrollSync())),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.syncViewToFile(e)})),this.registerEvent(this.app.workspace.on("file-menu",(e,t)=>this.addFileMenuItems(e,t))),this.registerEvent(this.app.vault.on("rename",(e,t)=>this.handleFileRename(e,t))),this.registerEvent(this.app.vault.on("delete",e=>this.handleFileDelete(e))),this.bindScrollSync()}async onunload(){await this.flushSettingsSave(),this.app.workspace.detachLeavesOfType(C)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=ue(Object.assign({},m,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this.pruneCache(),await this.writeCacheFile()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",t=this.manifest?.id||"parallel-reader";return`${e}/plugins/${t}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,t=this.app.vault.configDir||".obsidian",n=this.manifest?.id||"parallel-reader",i=`${t}/plugins/${n}`;try{if(typeof e.exists=="function"&&await e.exists(i))return;await e.mkdir(i)}catch{}}async readCacheFile(){let e=this.app.vault.adapter;try{let t=await e.read(this.cacheFilePath()),n=JSON.parse(t);if(n&&typeof n=="object"&&n.entries&&typeof n.entries=="object")return n.entries}catch(t){let n=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(n)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),JSON.stringify({version:1,entries:this.cache},null,2))}async loadCache(){let e=await this.readCacheFile();this.cache=e,this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return de(this.cache,this.settings?.maxCacheEntries||N)}async pruneCacheIfNeeded(){let e=this.pruneCache();return e.length>0&&await this.writeCacheFile(),e}cacheGet(e){let t=this.cache[e]||null;return t&&(t.lastAccessedAt=new Date().toISOString()),t}async cachePut(e,t,n,i){let r=new Date().toISOString();this.cache[e]={schemaVersion:S,contentHash:P(t),settingsHash:$(i||this.settings),cards:n,generatedAt:r,lastAccessedAt:r},await this.saveCache()}async cacheDelete(e){this.cache[e]&&(delete this.cache[e],await this.saveCache())}async cacheClear(){this.cache={},await this.saveCache()}async ensureView(){let{workspace:e}=this.app,t=e.getLeavesOfType(C)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:C,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(c.MarkdownView)}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new c.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.cancel(e.path);return new c.Notice(t?this.t("cancelRequested"):this.t("noCancelableJob")),t}viewIsShowingFile(e,t){return!!e&&!!t&&e.sourceFile?.path===t.path}activeFileStillMatches(e){let t=this.getActiveView();return!t?.file||t.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let t=this.app.workspace.getLeavesOfType(C)[0]?.view;t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new c.Notice(this.t("noCancelableJob"))}addFileMenuItems(e,t){!(t instanceof c.TFile)||!t.path.endsWith(".md")||(e.addSeparator(),e.addItem(n=>n.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(t,!1))),e.addItem(n=>n.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(t,!0))),this.cacheGet(t.path)&&e.addItem(n=>n.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheDelete(t.path),new c.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof c.TFile)||!t)return;let n=t.endsWith(".md"),i=e.path.endsWith(".md");if(!n&&!i)return;if(n&&!i){this.cache[t]&&(delete this.cache[t],await this.saveCache());let l=this.app.workspace.getLeavesOfType(C)[0]?.view;l?.sourceFile?.path===t&&l.renderEmpty();return}if(!n)return;this.cache[t]&&(this.cache[e.path]=this.cache[t],delete this.cache[t],await this.saveCache());let o=this.app.workspace.getLeavesOfType(C)[0]?.view;o?.sourceFile&&(o.sourceFile.path===t||o.sourceFile.path===e.path)&&(o.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof c.TFile))return;!!this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let i=this.app.workspace.getLeavesOfType(C)[0]?.view;i?.sourceFile?.path===e.path&&i.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noExportContent"));return}await t.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noCopyContent"));return}await Q(j(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new c.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new c.Notice(this.t("openNoteFirst"));return}let n=e.path;if(this.jobs.isRunning(n)){new c.Notice(this.t("alreadyGenerating"));return}let i=null;return this.jobs.start(n,async r=>{r.setPhase("reading");let o=await this.app.vault.read(e);if(r.throwIfCancelled(),!o.trim()){new c.Notice(this.t("emptyNote"));return}if(i=await this.ensureView(),r.throwIfCancelled(),r.setPhase("cache-check"),!t){let p=this.cacheGet(e.path);if(H(p,o,this.settings)){this.activeFileStillMatches(e)&&await i.loadFor(e,this.resolveCardAnchors(o,p.cards),!1);return}}await i.renderLoading(e,this.t("loadingGenerating"));let s=Number(this.settings.maxDocChars)||m.maxDocChars;o.length>s&&new c.Notice(this.t("longNoteTruncated",{count:s})),new c.Notice(this.t("generatingNotice")),r.setPhase("generating");let l=await ut(o,this.settings,r);if(r.throwIfCancelled(),l.length===0){new c.Notice(this.t("noCardsReturned"));return}let d=l.map(p=>({title:p.title,anchor:p.anchor,gist:p.gist,bullets:p.bullets}));r.setPhase("saving"),await this.cachePut(e.path,o,d,this.settings),r.throwIfCancelled(),this.viewIsShowingFile(i,e)&&await i.loadFor(e,l,!1);let f=l.filter(p=>p.startLine<0).length;new c.Notice(this.t("generationDone",{count:l.length,suffix:f?this.t("unanchoredSuffix",{count:f}):""}))}).catch(async r=>{if(r instanceof k){new c.Notice(r.message);return}if(r instanceof x){this.viewIsShowingFile(i,e)&&await i.renderError(e,this.t("cancelledError")),new c.Notice(this.t("cancelled"));return}let o=Ce(r);console.error(r),this.viewIsShowingFile(i,e)&&await i.renderError(e,r.message||String(r)),new c.Notice(this.t("generationFailed",{kind:o==="unknown"?"":` (${o})`,error:r.message||r}))})}resolveCardAnchors(e,t){let n=(t||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:B(e,i.anchor),bullets:i.bullets||[]}));return n.sort((i,r)=>i.startLine<0&&r.startLine<0?0:i.startLine<0?1:r.startLine<0?-1:i.startLine-r.startLine),n}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(C);if(t.length===0)return;let n=t[0].view;if(!n||!this.activeFileStillMatches(e))return;let i=this.cacheGet(e.path);if(!i){await n.loadFor(e,[],!1),n.renderEmptyWithHint(e);return}let r=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!H(i,r,this.settings),s=this.resolveCardAnchors(r,i.cards);await n.loadFor(e,s,o)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let t=e.editor,n=t&&t.cm,i=n&&n.scrollDOM?n.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let r=()=>this.handleEditorScroll(e);i.addEventListener("scroll",r,{passive:!0}),this._scrollDispose=()=>i.removeEventListener("scroll",r)}handleEditorScroll(e){let t=this.app.workspace.getLeavesOfType(C);if(t.length===0)return;let n=t[0].view;if(!n||!e.file||n.sourceFile?.path!==e.file.path)return;let i=e.editor,r=i&&i.cm;if(!r)return;let o=r.scrollDOM;if(!o)return;let s=o.getBoundingClientRect(),l=s.top+80,d=0;try{let p=r.posAtCoords({x:s.left+20,y:l});p!=null&&(d=r.state.doc.lineAt(p).number-1)}catch{return}let f=-1;for(let p=0;pthis.plugin.t(r,o);e.empty(),e.createEl("h2",{text:t("settingsTitle")}),new c.Setting(e).setName(t("settingUiLanguageName")).setDesc(t("settingUiLanguageDesc")).addDropdown(r=>{for(let[o,s]of Object.entries(oe))r.addOption(o,s);return r.setValue(this.plugin.settings.uiLanguage||m.uiLanguage).onChange(async o=>{this.plugin.settings.uiLanguage=o,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBackendName")).setDesc(t("settingBackendDesc")).addDropdown(r=>r.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async o=>{this.plugin.settings.backend=o,o==="api"&&!this.plugin.settings.apiBaseUrl&&pe(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let n=G(this.plugin.settings.backend);if(!n)new c.Setting(e).setName(t("settingCliPathName")).setDesc(t("settingCliPathDesc")).addText(r=>r.setPlaceholder(t("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async o=>{this.plugin.settings.cliPath=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingCliTimeoutName")).addText(r=>r.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.cliTimeoutMs=s,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:t("apiProviderHeader")});let r=y(this.plugin.settings);new c.Setting(e).setName(t("settingProviderPresetName")).setDesc(t("settingProviderPresetDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(T))o.addOption(s,l.label);return o.setValue(this.plugin.settings.apiProvider).onChange(async s=>{pe(this.plugin.settings,s),await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingApiFormatName")).setDesc(t("settingApiFormatDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(w))o.addOption(s,l.label);return o.setValue(b(this.plugin.settings)).onChange(async s=>{this.plugin.settings.apiFormat=s,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBaseUrlName")).setDesc(t("settingBaseUrlDesc")).addText(o=>o.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":r.baseUrl||w[b(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async s=>{this.plugin.settings.apiBaseUrl=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingApiKeyName")).setDesc(t("settingApiKeyDesc")).addText(o=>(o.inputEl.type="password",o.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async s=>{this.plugin.settings.apiKey=s.trim(),this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingApiKeyEnvName")).setDesc(t("settingApiKeyEnvDesc")).addText(o=>o.setPlaceholder(r.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async s=>{this.plugin.settings.apiKeyEnvVar=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingAuthTypeName")).setDesc(t("settingAuthTypeDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(ce))o.addOption(s,l);return o.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async s=>{this.plugin.settings.apiAuthType=s,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingHeadersName")).setDesc(t("settingHeadersDesc")).addTextArea(o=>o.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async s=>{this.plugin.settings.apiHeaders=s,this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxTokensName")).addText(o=>o.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async s=>{let l=parseInt(s,10);!isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())}))}new c.Setting(e).setName(t("settingModelName")).setDesc(t(n?"settingModelDescApi":"settingModelDescCli")).addText(r=>r.setPlaceholder(n?y(this.plugin.settings).model||"model-id":m.model).setValue(this.plugin.settings.model).onChange(async o=>{this.plugin.settings.model=o.trim()||(n?"":m.model),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxInputName")).setDesc(t("settingMaxInputDesc")).addText(r=>r.setValue(String(this.plugin.settings.maxDocChars||m.maxDocChars)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>=1e3&&(this.plugin.settings.maxDocChars=s,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:t("promptHeader")}),new c.Setting(e).setName(t("settingPromptLanguageName")).setDesc(t("settingPromptLanguageDesc")).addDropdown(r=>{for(let[o,s]of Object.entries(F))r.addOption(o,s);return r.setValue(this.plugin.settings.promptLanguage||m.promptLanguage).onChange(async o=>{this.plugin.settings.promptLanguage=o,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingCardRangeName")).setDesc(t("settingCardRangeDesc")).addText(r=>r.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||m.minCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.minCards=s,this.plugin.settings.maxCardsr.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||m.maxCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.maxCards=Math.max(s,this.plugin.settings.minCards||m.minCards),this.plugin.saveSettingsDebounced())})),new c.Setting(e).setName(t("settingCustomPromptName")).setDesc(t("settingCustomPromptDesc")).addTextArea(r=>(r.inputEl.rows=8,r.setPlaceholder(t("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async o=>{this.plugin.settings.customSystemPrompt=o,this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingTestBackendName")).setDesc(t(n?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(r=>r.setButtonText("Test").onClick(async()=>{try{let o=await pt(this.plugin.settings);new c.Notice(`\u2713 ${o.slice(0,180)}`,8e3)}catch(o){new c.Notice(t("backendTestFailed",{error:o.message}),1e4)}})),new c.Setting(e).setName(t("settingExportFolderName")).setDesc(t("settingExportFolderDesc")).addText(r=>r.setValue(this.plugin.settings.exportFolder).onChange(async o=>{this.plugin.settings.exportFolder=o.trim()||m.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:t("cacheHeader")}),new c.Setting(e).setName(t("settingMaxCacheName")).setDesc(t("settingMaxCacheDesc")).addText(r=>{r.setValue(String(this.plugin.settings.maxCacheEntries||N));let o=async()=>{let s=parseInt(r.getValue(),10);if(Number.isFinite(s)&&s>0){this.plugin.settings.maxCacheEntries=s,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new c.Notice(t("cachePruned",{count:l.length})),this.display()}};return r.inputEl.addEventListener("change",o),r.inputEl.addEventListener("keydown",s=>{s.key==="Enter"&&r.inputEl.blur()}),r});let i=Object.keys(this.plugin.cache).length;new c.Setting(e).setName(t("cachedNotesName",{count:i})).setDesc(t("cachedNotesDesc")).addButton(r=>r.setButtonText(t("clearAllCacheButton")).setWarning().onClick(async()=>{let o=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new c.Notice(t("cacheClearedAll",{count:o})),this.display()}))}},dt=xe,ht={CACHE_SCHEMA_VERSION:S,GenerationJobAlreadyRunningError:k,GenerationJobCancelledError:x,GenerationJobManager:_,buildAnthropicMessagesBody:J,buildGeminiBody:Y,buildOpenAiChatBody:z,buildOpenAiResponsesBody:W,buildPrompts:he,cardsToMarkdown:j,cacheEntryMatches:H,classifyGenerationError:Ce,extractJson:me,findLineForAnchor:B,generationFingerprint:$,normalizeCardsPayload:M,pruneCacheEntries:de,summarizeViaApi:O,translate:ne,tokenLimitFieldForOpenAiChat:ye};0&&(module.exports={__test});
+Output JSON directly with no explanation.`,s=["exec","--skip-git-repo-check","-"],{stdout:o}=await re(i,s,n,t.cliTimeoutMs,r);return T(o,t)}async function Et(a){if(a.backend==="codex"){let e=ae("codex",a.cliPath),{stdout:t}=await re(e,["--version"],"",1e4);return`codex @ ${e}
+${t.trim()}`}if(a.backend==="claude-code"){let e=ae("claude",a.cliPath),{stdout:t}=await re(e,["--version"],"",1e4);return`claude @ ${e}
+${t.trim()}`}if(L(a.backend))return qe(c.requestUrl,a);throw new Error("Unknown backend: "+a.backend)}async function kt(a,e,t){let{system:r,user:i}=be(a,e),n;switch(e.backend){case"codex":n=await St(r,i,e,t);break;case"api":n=await R(c.requestUrl,r,i,e);break;case"anthropic-api":n=await R(c.requestUrl,r,i,e);break;case"claude-code":default:n=await Pt(r,i,e,t);break}let s=n.map(o=>({title:o.title,level:2,anchor:o.anchor,gist:o.gist,startLine:j(a,o.anchor),bullets:o.bullets}));return s.sort((o,l)=>o.startLine<0&&l.startLine<0?0:o.startLine<0?1:l.startLine<0?-1:o.startLine-l.startLine),s}function Q(a,e,t,r){let i=a.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return i.title=t,typeof c.setIcon=="function"?(0,c.setIcon)(i,e):i.textContent=t,i.addEventListener("click",async n=>{n.preventDefault(),n.stopPropagation();try{await r()}catch(s){console.error(s),new c.Notice(`${t} failed: `+(s.message||s))}}),i}function ne(a,e,t,r,i){let n=a.createEl("button",{cls:i||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof c.setIcon=="function"&&(0,c.setIcon)(n,e),n.createSpan({text:t}),n.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await r()}catch(o){console.error(o),new c.Notice(`${t} failed: `+(o.message||o))}}),n}async function ee(a,e){try{await navigator.clipboard.writeText(a),new c.Notice(e)}catch(t){new c.Notice("Copy failed: "+(t.message||t))}}function Xe(a,e){return e?.phase==="generating"&&L(a?.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var De=class extends c.Modal{constructor(e,t,r,i){super(e),this.plugin=t,this.card=r||{},this.onSave=i}onOpen(){let{contentEl:e}=this;e.empty(),e.createEl("h2",{text:this.plugin.t("editCardTitle")});let t=this.createLabeledInput(e,this.plugin.t("editCardTitleField"),this.card.title||""),r=this.createLabeledTextarea(e,this.plugin.t("editCardGistField"),this.card.gist||"",3),i=this.createLabeledTextarea(e,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(`
+`),8),n=e.createDiv({cls:"parallel-reader-modal-actions"});ne(n,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),ne(n,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:t.value.trim()||this.card.title||"",gist:r.value.trim(),bullets:i.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(e,t,r){let i=e.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:t});let n=i.createEl("input",{attr:{type:"text"}});return n.value=r,n}createLabeledTextarea(e,t,r,i){let n=e.createDiv({cls:"parallel-reader-modal-field"});n.createEl("label",{text:t});let s=n.createEl("textarea");return s.rows=i,s.value=r,s}},Ie=class extends c.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return v}getDisplayText(){return this.plugin.t("displayName")}getIcon(){return"book-open"}onOpen(){let e=this.containerEl.children[1];return e.empty(),e.addClass("parallel-reader-container"),e.setAttr("tabindex","0"),e.addEventListener("keydown",t=>this.handleKeydown(t)),this.renderEmpty(),this.focusSummaryPane(),Promise.resolve()}onClose(){return Promise.resolve()}renderEmpty(){this.sourceFile=null,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let e=this.containerEl.children[1];e.empty();let t=e.createDiv({cls:"parallel-reader-empty"});t.createEl("h3",{text:this.plugin.t("appTitle")}),t.createEl("p",{text:this.plugin.t("emptyOpenNote")}),t.createEl("code",{text:this.plugin.t("commandGenerate")})}focusSummaryPane(){let e=this.containerEl.children[1];return!e||typeof e.focus!="function"?!1:(e.focus({preventScroll:!0}),!0)}async loadFor(e,t,r){this.sourceFile=e,this.sections=t,this.stale=!!r,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=t||this.plugin.t("loadingDefault"),this.errorMessage="",this.render()}async renderError(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=t||this.plugin.t("errorTitle"),this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let t=this.containerEl.children[1];t.empty();let r=t.createDiv({cls:"parallel-reader-empty"});r.createEl("h3",{text:e.basename}),r.createEl("p",{text:this.plugin.t("emptyNoCache")}),r.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let r=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});r.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=r.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?Q(i,"square",this.plugin.t("actionCancel"),()=>this.plugin.cancelGenerationForFile(this.sourceFile)):Q(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),Q(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),Q(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let o=e.createDiv({cls:"parallel-reader-stale-banner"});o.createSpan({text:this.plugin.t("staleBanner")}),ne(o,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});o.createDiv({cls:"parallel-reader-spinner"}),o.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),o.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});o.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),o.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),ne(o,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let n=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let s=this.sourceFile?.path||"";this.sections.forEach((o,l)=>{let d=n.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(l),o.startLine<0&&d.addClass("parallel-reader-card-unanchored");let f=d.createEl("div",{cls:"parallel-reader-card-title"});if(f.createSpan({text:o.title}),o.startLine<0&&f.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),o.gist){let h=d.createEl("div",{cls:"parallel-reader-gist"});c.MarkdownRenderer.render(this.app,o.gist,h,s,this).catch(()=>{h.setText(o.gist)})}let p=o.bullets||[];if(p.length>0){let h=d.createEl("div",{cls:"parallel-reader-bullets-md"}),m=p.map(u=>`- ${u}`).join(`
+`);c.MarkdownRenderer.render(this.app,m,h,s,this).catch(()=>{h.setText(m)})}else o.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",h=>{let m=window.getSelection();if(m&&m.toString().length>0)return;let u=h.target;u&&u.tagName==="A"||o.startLine>=0&&this.plugin.scrollEditorToLine(o.startLine,this.sourceFile)}),d.addEventListener("contextmenu",h=>{h.preventDefault();let m=new c.Menu;m.addItem(u=>u.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>ee(ue(o),this.plugin.t("copiedMarkdown")))),m.addItem(u=>u.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>ee(Re(o),this.plugin.t("copiedPlain")))),o.anchor&&m.addItem(u=>u.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>ee(o.anchor,this.plugin.t("copiedAnchor")))),m.addSeparator(),o.startLine>=0&&m.addItem(u=>u.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(o.startLine,this.sourceFile))),m.addSeparator(),m.addItem(u=>u.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(l))),m.addItem(u=>u.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(l))),m.showAtMouseEvent(h)}),this.cards.push(d)}),this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].addClass("is-active")}setActiveSection(e){e!==this.activeIdx&&(this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].removeClass("is-active"),this.activeIdx=e,e>=0&&this.cards[e]&&(this.cards[e].addClass("is-active"),this.cards[e].scrollIntoView({block:"nearest",behavior:"smooth"})))}moveActiveSection(e){let t=pe(this.activeIdx,this.sections.length,e);return this.setActiveSection(t),this.focusSummaryPane(),t}jumpToActiveSection(){let e=de(this.sections,this.activeIdx);return e<0||!this.sourceFile?-1:(this.plugin.scrollEditorToLine(e,this.sourceFile),e)}handleKeydown(e){if(e.altKey&&e.key==="ArrowUp"){e.preventDefault(),this.moveActiveSection(-1);return}if(e.altKey&&e.key==="ArrowDown"){e.preventDefault(),this.moveActiveSection(1);return}!e.altKey&&!e.metaKey&&!e.ctrlKey&&!e.shiftKey&&e.key==="Enter"&&this.jumpToActiveSection()>=0&&e.preventDefault()}async deleteCard(e){if(!this.sourceFile)return!1;let t=oe(this.sections,e);if(t.length===this.sections.length)return!1;let r=this.sections.length;return this.sections=t,this.activeIdx=ce(e,r,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,t),this.render(),new c.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new De(this.app,this.plugin,this.sections[e],t=>this.updateCard(e,t)).open(),!0)}async updateCard(e,t){if(!this.sourceFile)return!1;let r=le(this.sections,e,t);return r.length!==this.sections.length?!1:(this.sections=r,await this.plugin.cacheReplaceCards(this.sourceFile.path,r),this.render(),new c.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=Ae(this.plugin.settings.exportFolder),t=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,r=`${e}/${t}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",G(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(`
+`),n=this.plugin.app;await Ge(n,e);let s=n.vault.getAbstractFileByPath(r);s instanceof c.TFile?await n.vault.modify(s,i):await n.vault.create(r,i),new c.Notice(this.plugin.t("exported",{path:r}))}},Me=class extends c.Plugin{t(e,t){return y(this.settings||g,e,t)}async onload(){await this.loadSettings(),this.jobs=new V,this._cacheSaveTimer=null,this._cacheDirty=!1,this.addRibbonIcon("book-open",this.t("ribbonOpen"),async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(v,e=>new Ie(e,this)),this.addCommand({id:"parallel-reader-run",name:this.t("cmdRun"),callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:this.t("cmdRegen"),callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:this.t("cmdOpenView"),callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:this.t("cmdExport"),callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:this.t("cmdCopyMarkdown"),callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:this.t("cmdCancel"),callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:this.t("cmdClearCurrent"),callback:async()=>{let e=this.getActiveView();if(!e?.file)return new c.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new c.Notice(this.t("cacheClearedFile",{name:e.file.basename}))}}),this.addCommand({id:"parallel-reader-clear-all",name:this.t("cmdClearAll"),callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new c.Notice(this.t("cacheClearedAll",{count:e}))}}),this.addCommand({id:"parallel-reader-card-prev",name:this.t("cmdCardPrev"),hotkeys:[{modifiers:["Alt"],key:"ArrowUp"}],callback:()=>this.moveActiveCard(-1)}),this.addCommand({id:"parallel-reader-card-next",name:this.t("cmdCardNext"),hotkeys:[{modifiers:["Alt"],key:"ArrowDown"}],callback:()=>this.moveActiveCard(1)}),this.addCommand({id:"parallel-reader-card-jump",name:this.t("cmdCardJump"),callback:()=>this.jumpActiveCard()}),this.addSettingTab(new Le(this.app,this)),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>this.bindScrollSync())),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.syncViewToFile(e)})),this.registerEvent(this.app.workspace.on("file-menu",(e,t)=>this.addFileMenuItems(e,t))),this.registerEvent(this.app.vault.on("rename",(e,t)=>this.handleFileRename(e,t))),this.registerEvent(this.app.vault.on("delete",e=>this.handleFileDelete(e))),this.bindScrollSync()}async onunload(){await this.flushSettingsSave(),await this.flushCacheSave(),this.app.workspace.detachLeavesOfType(v)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=ve(Object.assign({},g,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this.pruneCache(),await this.writeCacheFile(),this._cacheDirty=!1}scheduleCacheSave(e=5e3){this._cacheDirty=!0,!this._cacheSaveTimer&&(this._cacheSaveTimer=setTimeout(()=>{this._cacheSaveTimer=null,this._cacheDirty&&this.saveCache().catch(t=>console.error("[parallel-reader] failed to save cache",t))},e))}async flushCacheSave(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this._cacheDirty&&await this.saveCache()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",t=this.manifest?.id||"parallel-reader";return`${e}/plugins/${t}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,t=this.app.vault.configDir||".obsidian",r=this.manifest?.id||"parallel-reader",i=`${t}/plugins/${r}`;try{if(typeof e.exists=="function"&&await e.exists(i))return;await e.mkdir(i)}catch{}}async readCacheFile(){let e=this.app.vault.adapter;try{let t=await e.read(this.cacheFilePath()),r=JSON.parse(t);if(r&&typeof r=="object"&&r.entries&&typeof r.entries=="object")return r.entries}catch(t){let r=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(r)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),se(this.cache))}async loadCache(){let e=await this.readCacheFile();this.cache=e,this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return we(this.cache,this.settings?.maxCacheEntries||I)}async pruneCacheIfNeeded(){let e=this.pruneCache();return e.length>0&&await this.saveCache(),e}cacheGet(e){return this.cache[e]||null}async cacheTouch(e){let t=ie(this.cache[e]||null);return t?(this.scheduleCacheSave(),t):null}async cachePut(e,t,r,i){let n=new Date().toISOString();this.cache[e]={schemaVersion:D,contentHash:N(t),settingsHash:J(i||this.settings),cards:r,generatedAt:n,lastAccessedAt:n},await this.saveCache()}async cacheReplaceCards(e,t){let r=this.cache[e];if(!r)return!1;let i=new Date().toISOString();return r.cards=(t||[]).map(n=>({title:n.title,anchor:n.anchor,gist:n.gist,bullets:n.bullets||[]})),r.updatedAt=i,r.lastAccessedAt=i,await this.saveCache(),!0}async cacheDelete(e){this.cache[e]&&(delete this.cache[e],await this.saveCache())}async cacheClear(){this.cache={},await this.saveCache()}async ensureView(){let{workspace:e}=this.app,t=e.getLeavesOfType(v)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:v,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(c.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(v)[0]?.view}moveActiveCard(e){let t=this.getParallelView();return!t||!t.sections.length?(new c.Notice(this.t("noActiveCard")),-1):t.moveActiveSection(e)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new c.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new c.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.get(e.path),r=Xe(this.settings,t),i=this.jobs.cancel(e.path);return new c.Notice(i?this.t(r):this.t("noCancelableJob")),i}viewIsShowingFile(e,t){return!!e&&!!t&&e.sourceFile?.path===t.path}activeFileStillMatches(e){let t=this.getActiveView();return!t?.file||t.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let t=this.app.workspace.getLeavesOfType(v)[0]?.view;t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new c.Notice(this.t("noCancelableJob"))}addFileMenuItems(e,t){!(t instanceof c.TFile)||!t.path.endsWith(".md")||(e.addSeparator(),e.addItem(r=>r.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(t,!1))),e.addItem(r=>r.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(t,!0))),this.cacheGet(t.path)&&e.addItem(r=>r.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheDelete(t.path),new c.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof c.TFile)||!t)return;let r=t.endsWith(".md"),i=e.path.endsWith(".md");if(!r&&!i)return;if(r&&!i){this.cache[t]&&(delete this.cache[t],await this.saveCache());let l=this.app.workspace.getLeavesOfType(v)[0]?.view;l?.sourceFile?.path===t&&l.renderEmpty();return}if(!r)return;this.cache[t]&&(this.cache[e.path]=this.cache[t],delete this.cache[t],await this.saveCache());let s=this.app.workspace.getLeavesOfType(v)[0]?.view;s?.sourceFile&&(s.sourceFile.path===t||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof c.TFile))return;!!this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let i=this.app.workspace.getLeavesOfType(v)[0]?.view;i?.sourceFile?.path===e.path&&i.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noExportContent"));return}await t.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noCopyContent"));return}await ee(G(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new c.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new c.Notice(this.t("openNoteFirst"));return}let r=e.path;if(this.jobs.isRunning(r)){new c.Notice(this.t("alreadyGenerating"));return}let i=null;return this.jobs.start(r,async n=>{n.setPhase("reading");let s=await this.app.vault.read(e);if(n.throwIfCancelled(),!s.trim()){new c.Notice(this.t("emptyNote"));return}if(i=await this.ensureView(),n.throwIfCancelled(),n.setPhase("cache-check"),!t){let p=this.cacheGet(e.path);if(K(p,s,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await i.loadFor(e,this.resolveCardAnchors(s,p.cards),!1);return}}await i.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||g.maxDocChars;s.length>o&&new c.Notice(this.t("longNoteTruncated",{count:o})),new c.Notice(this.t("generatingNotice")),n.setPhase("generating");let l=await kt(s,this.settings,n);if(n.throwIfCancelled(),l.length===0){new c.Notice(this.t("noCardsReturned"));return}let d=l.map(p=>({title:p.title,anchor:p.anchor,gist:p.gist,bullets:p.bullets}));n.setPhase("saving"),await this.cachePut(e.path,s,d,this.settings),n.throwIfCancelled(),this.viewIsShowingFile(i,e)&&await i.loadFor(e,l,!1);let f=l.filter(p=>p.startLine<0).length;new c.Notice(this.t("generationDone",{count:l.length,suffix:f?this.t("unanchoredSuffix",{count:f}):""}))}).catch(async n=>{if(n instanceof F){new c.Notice(n.message);return}if(n instanceof A){this.viewIsShowingFile(i,e)&&await i.renderError(e,this.t("cancelledError")),new c.Notice(this.t("cancelled"));return}let s=Fe(n);console.error(n),this.viewIsShowingFile(i,e)&&await i.renderError(e,n.message||String(n)),new c.Notice(this.t("generationFailed",{kind:s==="unknown"?"":` (${s})`,error:n.message||n}))})}resolveCardAnchors(e,t){let r=(t||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:j(e,i.anchor),bullets:i.bullets||[]}));return r.sort((i,n)=>i.startLine<0&&n.startLine<0?0:i.startLine<0?1:n.startLine<0?-1:i.startLine-n.startLine),r}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(v);if(t.length===0)return;let r=t[0].view;if(!r||!this.activeFileStillMatches(e))return;let i=this.cacheGet(e.path);if(!i){await r.loadFor(e,[],!1),r.renderEmptyWithHint(e);return}let n=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let s=!K(i,n,this.settings);await this.cacheTouch(e.path);let o=this.resolveCardAnchors(n,i.cards);await r.loadFor(e,o,s)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let t=e.editor,r=t&&t.cm,i=r&&r.scrollDOM?r.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let n=Pe(()=>this.handleEditorScroll(e));i.addEventListener("scroll",n,{passive:!0}),this._scrollDispose=()=>{n.cancel(),i.removeEventListener("scroll",n)}}handleEditorScroll(e){let t=this.app.workspace.getLeavesOfType(v);if(t.length===0)return;let r=t[0].view;if(!r||!e.file||r.sourceFile?.path!==e.file.path)return;let i=e.editor,n=i&&i.cm;if(!n)return;let s=n.scrollDOM;if(!s)return;let o=s.getBoundingClientRect(),l=o.top+80,d=0;try{let p=n.posAtCoords({x:o.left+20,y:l});p!=null&&(d=n.state.doc.lineAt(p).number-1)}catch{return}let f=-1;for(let p=0;pthis.plugin.t(n,s);e.empty(),e.createEl("h2",{text:t("settingsTitle")}),new c.Setting(e).setName(t("settingUiLanguageName")).setDesc(t("settingUiLanguageDesc")).addDropdown(n=>{for(let[s,o]of Object.entries(ge))n.addOption(s,o);return n.setValue(this.plugin.settings.uiLanguage||g.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBackendName")).setDesc(t("settingBackendDesc")).addDropdown(n=>n.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&Ce(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let r=L(this.plugin.settings.backend);if(!r)new c.Setting(e).setName(t("settingCliPathName")).setDesc(t("settingCliPathDesc")).addText(n=>n.setPlaceholder(t("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingCliTimeoutName")).addText(n=>n.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.cliTimeoutMs=o,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:t("apiProviderHeader")});let n=C(this.plugin.settings);new c.Setting(e).setName(t("settingProviderPresetName")).setDesc(t("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(P))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{Ce(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingApiFormatName")).setDesc(t("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(w))s.addOption(o,l.label);return s.setValue(b(this.plugin.settings)).onChange(async o=>{this.plugin.settings.apiFormat=o,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBaseUrlName")).setDesc(t("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":n.baseUrl||w[b(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingApiKeyName")).setDesc(t("settingApiKeyDesc")).addText(s=>(s.inputEl.type="password",s.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async o=>{this.plugin.settings.apiKey=o.trim(),this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingApiKeyEnvName")).setDesc(t("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(n.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingAuthTypeName")).setDesc(t("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(fe))s.addOption(o,l);return s.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async o=>{this.plugin.settings.apiAuthType=o,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingHeadersName")).setDesc(t("settingHeadersDesc")).addTextArea(s=>s.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async o=>{this.plugin.settings.apiHeaders=o,this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())}))}new c.Setting(e).setName(t("settingModelName")).setDesc(t(r?"settingModelDescApi":"settingModelDescCli")).addText(n=>n.setPlaceholder(r?C(this.plugin.settings).model||"model-id":g.model).setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(r?"":g.model),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxInputName")).setDesc(t("settingMaxInputDesc")).addText(n=>n.setValue(String(this.plugin.settings.maxDocChars||g.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:t("promptHeader")}),new c.Setting(e).setName(t("settingPromptLanguageName")).setDesc(t("settingPromptLanguageDesc")).addDropdown(n=>{for(let[s,o]of Object.entries(M))n.addOption(s,o);return n.setValue(this.plugin.settings.promptLanguage||g.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingCardRangeName")).setDesc(t("settingCardRangeDesc")).addText(n=>n.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||g.minCards)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsn.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||g.maxCards)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||g.minCards),this.plugin.saveSettingsDebounced())})),new c.Setting(e).setName(t("settingCustomPromptName")).setDesc(t("settingCustomPromptDesc")).addTextArea(n=>(n.inputEl.rows=8,n.setPlaceholder(t("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingTestBackendName")).setDesc(t(r?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(n=>n.setButtonText("Test").onClick(async()=>{try{let s=await Et(this.plugin.settings);new c.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new c.Notice(t("backendTestFailed",{error:s.message}),1e4)}})),new c.Setting(e).setName(t("settingExportFolderName")).setDesc(t("settingExportFolderDesc")).addText(n=>n.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||g.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:t("cacheHeader")}),new c.Setting(e).setName(t("settingMaxCacheName")).setDesc(t("settingMaxCacheDesc")).addText(n=>{n.setValue(String(this.plugin.settings.maxCacheEntries||I));let s=async()=>{let o=parseInt(n.getValue(),10);if(Number.isFinite(o)&&o>0){this.plugin.settings.maxCacheEntries=o,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new c.Notice(t("cachePruned",{count:l.length})),this.display()}};return n.inputEl.addEventListener("change",s),n.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&n.inputEl.blur()}),n});let i=Object.keys(this.plugin.cache).length;new c.Setting(e).setName(t("cachedNotesName",{count:i})).setDesc(t("cachedNotesDesc")).addButton(n=>n.setButtonText(t("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new c.Notice(t("cacheClearedAll",{count:s})),this.display()}))}},Nt=Me,Ft={CACHE_SCHEMA_VERSION:D,GenerationJobAlreadyRunningError:F,GenerationJobCancelledError:A,GenerationJobManager:V,activeIndexAfterCardDelete:ce,activeSectionLine:de,buildAnthropicMessagesBody:z,buildGeminiBody:Y,buildOpenAiChatBody:q,buildOpenAiResponsesBody:W,buildPrompts:be,cardsToMarkdown:G,cacheEntryMatches:K,cancellationNoticeKey:Xe,classifyGenerationError:Fe,createRafThrottledHandler:Pe,extractJson:Te,findLineForAnchor:j,folderPathsForTarget:xe,generationFingerprint:J,getApiBaseUrl:E,modelForApi:k,normalizeCardsPayload:O,nextCardIndex:pe,pruneCacheEntries:we,removeCardAt:oe,serializeCacheFile:se,summarizeViaApi:R,touchCacheEntry:ie,translate:y,tokenLimitFieldForOpenAiChat:ke,updateCardAt:le};0&&(module.exports={__test});
diff --git a/main.ts b/main.ts
index 97ffcd3..df9ebbd 100644
--- a/main.ts
+++ b/main.ts
@@ -1,18 +1,23 @@
'use strict';
-import { Plugin, ItemView, PluginSettingTab, Setting, Notice, MarkdownView, TFile, Menu, MarkdownRenderer, requestUrl, setIcon } from 'obsidian';
+import { Plugin, ItemView, PluginSettingTab, Setting, Notice, MarkdownView, TFile, Menu, Modal, MarkdownRenderer, requestUrl, setIcon } from 'obsidian';
import { spawn } from 'child_process';
import os from 'os';
import path from 'path';
import fs from 'fs';
import { findLineForAnchor } from './src/anchor';
+import { serializeCacheFile, touchCacheEntry } from './src/cache';
+import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
import { translate } from './src/i18n';
import { cardToMarkdown, cardToPlain, cardsToMarkdown } from './src/markdown';
+import { activeSectionLine, nextCardIndex } from './src/navigation';
import { buildPrompts } from './src/prompt';
+import { ensureVaultFolder, folderPathsForTarget, normalizeVaultPath } from './src/vault';
import {
extractJson,
normalizeCardsPayload,
parseCardsJson,
} from './src/schema';
+import { createRafThrottledHandler } from './src/scroll';
import {
buildAnthropicMessagesBody,
buildGeminiBody,
@@ -40,10 +45,12 @@ import {
applyApiProviderPreset,
cacheEntryMatches,
generationFingerprint,
+ getApiBaseUrl,
getApiFormat,
getApiPreset,
hashContent,
isApiBackend,
+ modelForApi,
normalizeSettings,
pruneCacheEntries,
} from './src/settings';
@@ -166,7 +173,7 @@ async function summarizeViaClaudeCode(system, user, settings, job) {
throw new Error('claude CLI returned a non-JSON envelope:\n' + stdout.slice(0, 500));
}
const resultText = envelope.result || envelope.content || '';
- return parseCardsJson(resultText);
+ return parseCardsJson(resultText, settings);
}
async function summarizeViaCodex(system, user, settings, job) {
@@ -174,7 +181,7 @@ async function summarizeViaCodex(system, user, settings, job) {
const combined = `<>\n${system}\n<>\n${user}\n\nOutput JSON directly with no explanation.`;
const args = ['exec', '--skip-git-repo-check', '-'];
const { stdout } = await runCli(cmd, args, combined, settings.cliTimeoutMs, job);
- return parseCardsJson(stdout);
+ return parseCardsJson(stdout, settings);
}
async function testBackend(settings) {
@@ -283,6 +290,64 @@ async function copyToClipboard(text, successMsg) {
}
}
+function cancellationNoticeKey(settings, job) {
+ if (job?.phase === 'generating' && isApiBackend(settings?.backend)) {
+ return 'cancelRequestedApiInFlight';
+ }
+ return 'cancelRequested';
+}
+
+class CardEditModal extends Modal {
+ plugin: ParallelReaderPlugin;
+ card: any;
+ onSave: (patch: any) => void | Promise;
+
+ constructor(app, plugin, card, onSave) {
+ super(app);
+ this.plugin = plugin;
+ this.card = card || {};
+ this.onSave = onSave;
+ }
+
+ onOpen() {
+ const { contentEl } = this;
+ contentEl.empty();
+ contentEl.createEl('h2', { text: this.plugin.t('editCardTitle') });
+
+ const titleInput = this.createLabeledInput(contentEl, this.plugin.t('editCardTitleField'), this.card.title || '');
+ const gistInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardGistField'), this.card.gist || '', 3);
+ const bulletsInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardBulletsField'), (this.card.bullets || []).join('\n'), 8);
+
+ const actions = contentEl.createDiv({ cls: 'parallel-reader-modal-actions' });
+ addTextButton(actions, null, this.plugin.t('editCardCancel'), () => this.close(), 'parallel-reader-text-button');
+ addTextButton(actions, null, this.plugin.t('editCardSave'), async () => {
+ await this.onSave({
+ title: titleInput.value.trim() || this.card.title || '',
+ gist: gistInput.value.trim(),
+ bullets: bulletsInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean),
+ });
+ this.close();
+ }, 'parallel-reader-text-button');
+ }
+
+ createLabeledInput(parent, label, value) {
+ const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' });
+ wrapper.createEl('label', { text: label });
+ const input = wrapper.createEl('input', { attr: { type: 'text' } });
+ input.value = value;
+ return input;
+ }
+
+ createLabeledTextarea(parent, label, value, rows) {
+ const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' });
+ wrapper.createEl('label', { text: label });
+ const textarea = wrapper.createEl('textarea');
+ textarea.rows = rows;
+ textarea.value = value;
+ return textarea;
+ }
+}
+
/* ---------- Right-pane view ---------- */
class ParallelReaderView extends ItemView {
@@ -310,14 +375,20 @@ class ParallelReaderView extends ItemView {
getDisplayText() { return this.plugin.t('displayName'); }
getIcon() { return 'book-open'; }
- async onOpen() {
+ onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.addClass('parallel-reader-container');
+ container.setAttr('tabindex', '0');
+ container.addEventListener('keydown', e => this.handleKeydown(e as KeyboardEvent));
this.renderEmpty();
+ this.focusSummaryPane();
+ return Promise.resolve();
}
- async onClose() {}
+ onClose() {
+ return Promise.resolve();
+ }
renderEmpty() {
this.sourceFile = null;
@@ -333,6 +404,13 @@ class ParallelReaderView extends ItemView {
hint.createEl('code', { text: this.plugin.t('commandGenerate') });
}
+ focusSummaryPane() {
+ const container = this.containerEl.children[1] as HTMLElement;
+ if (!container || typeof container.focus !== 'function') return false;
+ container.focus({ preventScroll: true });
+ return true;
+ }
+
async loadFor(file, sections, stale) {
this.sourceFile = file;
this.sections = sections;
@@ -489,11 +567,19 @@ class ParallelReaderView extends ItemView {
menu.addItem(it => it.setTitle(this.plugin.t('menuJumpSource')).setIcon('arrow-right')
.onClick(() => this.plugin.scrollEditorToLine(s.startLine, this.sourceFile)));
}
+ menu.addSeparator();
+ menu.addItem(it => it.setTitle(this.plugin.t('menuEditCard')).setIcon('pencil')
+ .onClick(() => this.openEditCardModal(i)));
+ menu.addItem(it => it.setTitle(this.plugin.t('menuDeleteCard')).setIcon('trash')
+ .onClick(() => this.deleteCard(i)));
menu.showAtMouseEvent(e);
});
this.cards.push(card);
});
+ if (this.activeIdx >= 0 && this.cards[this.activeIdx]) {
+ this.cards[this.activeIdx].addClass('is-active');
+ }
}
setActiveSection(idx) {
@@ -508,9 +594,70 @@ class ParallelReaderView extends ItemView {
}
}
+ moveActiveSection(delta) {
+ const nextIdx = nextCardIndex(this.activeIdx, this.sections.length, delta);
+ this.setActiveSection(nextIdx);
+ this.focusSummaryPane();
+ return nextIdx;
+ }
+
+ jumpToActiveSection() {
+ const line = activeSectionLine(this.sections, this.activeIdx);
+ if (line < 0 || !this.sourceFile) return -1;
+ this.plugin.scrollEditorToLine(line, this.sourceFile);
+ return line;
+ }
+
+ handleKeydown(e: KeyboardEvent) {
+ if (e.altKey && e.key === 'ArrowUp') {
+ e.preventDefault();
+ this.moveActiveSection(-1);
+ return;
+ }
+ if (e.altKey && e.key === 'ArrowDown') {
+ e.preventDefault();
+ this.moveActiveSection(1);
+ return;
+ }
+ if (!e.altKey && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.key === 'Enter') {
+ const line = this.jumpToActiveSection();
+ if (line >= 0) e.preventDefault();
+ }
+ }
+
+ async deleteCard(index) {
+ if (!this.sourceFile) return false;
+ const nextSections = removeCardAt(this.sections, index);
+ if (nextSections.length === this.sections.length) return false;
+ const previousLength = this.sections.length;
+ this.sections = nextSections;
+ this.activeIdx = activeIndexAfterCardDelete(index, previousLength, this.activeIdx);
+ await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections);
+ this.render();
+ new Notice(this.plugin.t('cardDeleted'));
+ return true;
+ }
+
+ openEditCardModal(index) {
+ if (!this.sourceFile || !this.sections[index]) return false;
+ new CardEditModal(this.app, this.plugin, this.sections[index], patch => this.updateCard(index, patch)).open();
+ return true;
+ }
+
+ async updateCard(index, patch) {
+ if (!this.sourceFile) return false;
+ const nextSections = updateCardAt(this.sections, index, patch);
+ if (nextSections.length !== this.sections.length) return false;
+ this.sections = nextSections;
+ await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections);
+ this.render();
+ new Notice(this.plugin.t('cardSaved'));
+ return true;
+ }
+
async exportToVault() {
if (!this.sourceFile) return;
- const folder = this.plugin.settings.exportFolder.replace(/\/$/, '');
+ const folder = normalizeVaultPath(this.plugin.settings.exportFolder);
const name = `${this.sourceFile.basename} - ${this.plugin.t('displayName')}.md`;
const targetPath = `${folder}/${name}`;
@@ -526,11 +673,7 @@ class ParallelReaderView extends ItemView {
].join('\n');
const app = this.plugin.app;
- // Ensure folder exists
- const folderTF = app.vault.getAbstractFileByPath(folder);
- if (!folderTF) {
- try { await app.vault.createFolder(folder); } catch (e) { /* exists race */ }
- }
+ await ensureVaultFolder(app, folder);
const existing = app.vault.getAbstractFileByPath(targetPath);
if (existing instanceof TFile) {
@@ -550,6 +693,8 @@ class ParallelReaderPlugin extends Plugin {
jobs: GenerationJobManager;
_scrollDispose: (() => void) | null;
_settingsSaveTimer: ReturnType | null;
+ _cacheSaveTimer: ReturnType | null;
+ _cacheDirty: boolean;
t(key, vars?) {
return translate(this.settings || DEFAULT_SETTINGS, key, vars);
@@ -558,6 +703,8 @@ class ParallelReaderPlugin extends Plugin {
async onload() {
await this.loadSettings();
this.jobs = new GenerationJobManager();
+ this._cacheSaveTimer = null;
+ this._cacheDirty = false;
this.addRibbonIcon('book-open', this.t('ribbonOpen'), async () => {
const active = this.getActiveView();
@@ -628,6 +775,26 @@ class ParallelReaderPlugin extends Plugin {
},
});
+ this.addCommand({
+ id: 'parallel-reader-card-prev',
+ name: this.t('cmdCardPrev'),
+ hotkeys: [{ modifiers: ['Alt'], key: 'ArrowUp' }],
+ callback: () => this.moveActiveCard(-1),
+ });
+
+ this.addCommand({
+ id: 'parallel-reader-card-next',
+ name: this.t('cmdCardNext'),
+ hotkeys: [{ modifiers: ['Alt'], key: 'ArrowDown' }],
+ callback: () => this.moveActiveCard(1),
+ });
+
+ this.addCommand({
+ id: 'parallel-reader-card-jump',
+ name: this.t('cmdCardJump'),
+ callback: () => this.jumpActiveCard(),
+ });
+
this.addSettingTab(new ParallelReaderSettingTab(this.app, this));
this.registerEvent(
@@ -652,6 +819,7 @@ class ParallelReaderPlugin extends Plugin {
async onunload() {
await this.flushSettingsSave();
+ await this.flushCacheSave();
this.app.workspace.detachLeavesOfType(VIEW_TYPE_PARALLEL);
}
@@ -686,8 +854,32 @@ class ParallelReaderPlugin extends Plugin {
}
async saveCache() {
+ if (this._cacheSaveTimer) {
+ clearTimeout(this._cacheSaveTimer);
+ this._cacheSaveTimer = null;
+ }
this.pruneCache();
await this.writeCacheFile();
+ this._cacheDirty = false;
+ }
+
+ scheduleCacheSave(delayMs = 5000) {
+ this._cacheDirty = true;
+ if (this._cacheSaveTimer) return;
+ this._cacheSaveTimer = setTimeout(() => {
+ this._cacheSaveTimer = null;
+ if (!this._cacheDirty) return;
+ this.saveCache().catch(e => console.error('[parallel-reader] failed to save cache', e));
+ }, delayMs);
+ }
+
+ async flushCacheSave() {
+ if (this._cacheSaveTimer) {
+ clearTimeout(this._cacheSaveTimer);
+ this._cacheSaveTimer = null;
+ }
+ if (!this._cacheDirty) return;
+ await this.saveCache();
}
cacheFilePath() {
@@ -730,10 +922,7 @@ class ParallelReaderPlugin extends Plugin {
await this.ensurePluginDataDir();
await this.app.vault.adapter.write(
this.cacheFilePath(),
- JSON.stringify({
- version: 1,
- entries: this.cache,
- }, null, 2)
+ serializeCacheFile(this.cache)
);
}
@@ -750,13 +939,18 @@ class ParallelReaderPlugin extends Plugin {
async pruneCacheIfNeeded() {
const removed = this.pruneCache();
- if (removed.length > 0) await this.writeCacheFile();
+ if (removed.length > 0) await this.saveCache();
return removed;
}
cacheGet(filePath) {
- const entry = this.cache[filePath] || null;
- if (entry) entry.lastAccessedAt = new Date().toISOString();
+ return this.cache[filePath] || null;
+ }
+
+ async cacheTouch(filePath) {
+ const entry = touchCacheEntry(this.cache[filePath] || null);
+ if (!entry) return null;
+ this.scheduleCacheSave();
return entry;
}
@@ -773,6 +967,22 @@ class ParallelReaderPlugin extends Plugin {
await this.saveCache();
}
+ async cacheReplaceCards(filePath, cards) {
+ const entry = this.cache[filePath];
+ if (!entry) return false;
+ const now = new Date().toISOString();
+ entry.cards = (cards || []).map(card => ({
+ title: card.title,
+ anchor: card.anchor,
+ gist: card.gist,
+ bullets: card.bullets || [],
+ }));
+ entry.updatedAt = now;
+ entry.lastAccessedAt = now;
+ await this.saveCache();
+ return true;
+ }
+
async cacheDelete(filePath) {
if (this.cache[filePath]) {
delete this.cache[filePath];
@@ -800,6 +1010,28 @@ class ParallelReaderPlugin extends Plugin {
return this.app.workspace.getActiveViewOfType(MarkdownView);
}
+ getParallelView() {
+ return this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]?.view as ParallelReaderView | undefined;
+ }
+
+ moveActiveCard(delta) {
+ const view = this.getParallelView();
+ if (!view || !view.sections.length) {
+ new Notice(this.t('noActiveCard'));
+ return -1;
+ }
+ return view.moveActiveSection(delta);
+ }
+
+ jumpActiveCard() {
+ const view = this.getParallelView();
+ if (!view || view.jumpToActiveSection() < 0) {
+ new Notice(this.t('noActiveCard'));
+ return false;
+ }
+ return true;
+ }
+
isGeneratingFile(file) {
return !!file && !!file.path && this.jobs.isRunning(file.path);
}
@@ -809,8 +1041,10 @@ class ParallelReaderPlugin extends Plugin {
new Notice(this.t('noCancelableJob'));
return false;
}
+ const job = this.jobs.get(file.path);
+ const noticeKey = cancellationNoticeKey(this.settings, job);
const cancelled = this.jobs.cancel(file.path);
- new Notice(cancelled ? this.t('cancelRequested') : this.t('noCancelableJob'));
+ new Notice(cancelled ? this.t(noticeKey) : this.t('noCancelableJob'));
return cancelled;
}
@@ -961,6 +1195,7 @@ class ParallelReaderPlugin extends Plugin {
if (!force) {
const entry = this.cacheGet(file.path);
if (cacheEntryMatches(entry, content, this.settings)) {
+ await this.cacheTouch(file.path);
if (this.activeFileStillMatches(file)) {
await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), false);
}
@@ -1059,6 +1294,7 @@ class ParallelReaderPlugin extends Plugin {
const content = await this.app.vault.read(file);
if (!this.activeFileStillMatches(file)) return;
const stale = !cacheEntryMatches(entry, content, this.settings);
+ await this.cacheTouch(file.path);
const resolved = this.resolveCardAnchors(content, entry.cards);
await view.loadFor(file, resolved, stale);
}
@@ -1079,9 +1315,12 @@ class ParallelReaderPlugin extends Plugin {
const scrollDom = cm && cm.scrollDOM ? cm.scrollDOM : mdView.contentEl.querySelector('.cm-scroller');
if (!scrollDom) return;
- const handler = () => this.handleEditorScroll(mdView);
+ const handler = createRafThrottledHandler(() => this.handleEditorScroll(mdView));
scrollDom.addEventListener('scroll', handler, { passive: true });
- this._scrollDispose = () => scrollDom.removeEventListener('scroll', handler);
+ this._scrollDispose = () => {
+ handler.cancel();
+ scrollDom.removeEventListener('scroll', handler);
+ };
}
handleEditorScroll(mdView) {
@@ -1508,6 +1747,8 @@ export const __test = {
GenerationJobAlreadyRunningError,
GenerationJobCancelledError,
GenerationJobManager,
+ activeIndexAfterCardDelete,
+ activeSectionLine,
buildAnthropicMessagesBody,
buildGeminiBody,
buildOpenAiChatBody,
@@ -1515,13 +1756,23 @@ export const __test = {
buildPrompts,
cardsToMarkdown,
cacheEntryMatches,
+ cancellationNoticeKey,
classifyGenerationError,
+ createRafThrottledHandler,
extractJson,
findLineForAnchor,
+ folderPathsForTarget,
generationFingerprint,
+ getApiBaseUrl,
+ modelForApi,
normalizeCardsPayload,
+ nextCardIndex,
pruneCacheEntries,
+ removeCardAt,
+ serializeCacheFile,
summarizeViaApi,
+ touchCacheEntry,
translate,
tokenLimitFieldForOpenAiChat,
+ updateCardAt,
};
diff --git a/src/cache.ts b/src/cache.ts
new file mode 100644
index 0000000..cf1db5f
--- /dev/null
+++ b/src/cache.ts
@@ -0,0 +1,14 @@
+'use strict';
+
+export function touchCacheEntry(entry, now?) {
+ if (!entry) return null;
+ entry.lastAccessedAt = now || new Date().toISOString();
+ return entry;
+}
+
+export function serializeCacheFile(entries) {
+ return JSON.stringify({
+ version: 1,
+ entries: entries || {},
+ });
+}
diff --git a/src/cards.ts b/src/cards.ts
new file mode 100644
index 0000000..df354cf
--- /dev/null
+++ b/src/cards.ts
@@ -0,0 +1,26 @@
+'use strict';
+
+export function removeCardAt(cards, index) {
+ const next = Array.isArray(cards) ? cards.slice() : [];
+ if (!Number.isInteger(index) || index < 0 || index >= next.length) return next;
+ next.splice(index, 1);
+ return next;
+}
+
+export function activeIndexAfterCardDelete(deleteIndex, previousLength, activeIdx) {
+ if (!Number.isInteger(deleteIndex) || !Number.isInteger(previousLength) || previousLength <= 0) return activeIdx;
+ if (!Number.isInteger(activeIdx) || activeIdx < 0) return activeIdx;
+ if (deleteIndex < 0 || deleteIndex >= previousLength) return activeIdx;
+ const nextLength = previousLength - 1;
+ if (nextLength <= 0) return -1;
+ if (deleteIndex < activeIdx) return Math.max(0, activeIdx - 1);
+ if (deleteIndex === activeIdx) return Math.min(activeIdx, nextLength - 1);
+ return activeIdx;
+}
+
+export function updateCardAt(cards, index, patch) {
+ const next = Array.isArray(cards) ? cards.slice() : [];
+ if (!Number.isInteger(index) || index < 0 || index >= next.length) return next;
+ next[index] = Object.assign({}, next[index], patch || {});
+ return next;
+}
diff --git a/src/i18n.ts b/src/i18n.ts
index 7ec43b0..7ce7e8c 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -17,6 +17,9 @@ export const STRINGS = {
cmdCancel: '取消当前对照笔记生成',
cmdClearCurrent: '清除当前笔记的缓存',
cmdClearAll: '清除所有缓存',
+ cmdCardPrev: '聚焦上一张摘要卡片',
+ cmdCardNext: '聚焦下一张摘要卡片',
+ cmdCardJump: '跳转到当前摘要卡片原文',
actionCancel: '取消生成',
actionRegenerate: '重新生成',
actionCopyAll: '复制全部 Markdown',
@@ -32,6 +35,8 @@ export const STRINGS = {
menuCopyPlain: '复制纯文本',
menuCopyAnchor: '复制 anchor 引用',
menuJumpSource: '跳转到原文',
+ menuEditCard: '编辑此卡片',
+ menuDeleteCard: '删除此卡片',
copiedMarkdown: '已复制 Markdown',
copiedPlain: '已复制纯文本',
copiedAnchor: '已复制引用原文',
@@ -44,11 +49,21 @@ export const STRINGS = {
cacheClearedAll: '已清除 {count} 条缓存',
noCancelableJob: '当前没有可取消的生成任务',
cancelRequested: '已请求取消生成',
+ cancelRequestedApiInFlight: '已请求取消生成;当前 API 请求无法立即中断,返回后会丢弃结果。',
fileMenuGenerate: '生成对照笔记',
fileMenuRegen: '强制重新生成对照笔记',
fileMenuClear: '清除对照笔记缓存',
noExportContent: '当前没有可导出的对照笔记',
noCopyContent: '当前没有可复制的对照笔记',
+ noActiveCard: '当前没有可跳转的摘要卡片',
+ cardDeleted: '已删除此卡片',
+ cardSaved: '已保存此卡片',
+ editCardTitle: '编辑摘要卡片',
+ editCardTitleField: '标题',
+ editCardGistField: '领读',
+ editCardBulletsField: '要点(每行一条)',
+ editCardCancel: '取消',
+ editCardSave: '保存',
openNoteFirst: '先打开一篇笔记',
alreadyGenerating: '该笔记正在生成对照笔记',
emptyNote: '笔记为空',
@@ -60,6 +75,18 @@ export const STRINGS = {
cancelled: '已取消生成',
cancelledError: '生成已取消',
generationFailed: '生成失败{kind}:{error}',
+ errorCustomProviderBaseUrlRequired: '自定义 provider 需要填写 API Base URL。',
+ errorApiBaseUrlMissing: 'API Base URL 未设置。请在设置里选择 provider 或填写自定义 base URL。',
+ errorModelMissing: 'Model 未设置。请在设置里填写模型 ID。',
+ errorApiKeyMissing: 'API key 未设置。请在设置里填写 API Key{hint}。',
+ errorApiKeyEnvHint: ' 或环境变量 {envVar}',
+ errorLlmNonJson: 'LLM 返回非 JSON:\n{excerpt}',
+ errorCustomHeadersJsonParse: '自定义 headers JSON 解析失败:{error}',
+ errorCustomHeadersJsonObject: '自定义 headers JSON 必须是对象',
+ errorCustomHeadersLineFormat: '自定义 headers 每行格式应为 `Header-Name: value`',
+ errorProviderNonJson: '{label} 返回非 JSON:\n{excerpt}',
+ errorProviderRequestFailed: '{label} 请求失败:{error}',
+ errorProviderApiStatus: '{label} API 返回 HTTP {status}:{excerpt}',
noEditor: '找不到源笔记对应的编辑器窗口',
settingUiLanguageName: '界面语言',
settingUiLanguageDesc: '控制插件界面、命令和提示文案;Auto 跟随 Obsidian/系统语言',
@@ -128,6 +155,9 @@ export const STRINGS = {
cmdCancel: 'Cancel current parallel-note generation',
cmdClearCurrent: 'Clear cache for current note',
cmdClearAll: 'Clear all caches',
+ cmdCardPrev: 'Focus previous summary card',
+ cmdCardNext: 'Focus next summary card',
+ cmdCardJump: 'Jump current summary card to source',
actionCancel: 'Cancel generation',
actionRegenerate: 'Regenerate',
actionCopyAll: 'Copy all Markdown',
@@ -143,6 +173,8 @@ export const STRINGS = {
menuCopyPlain: 'Copy plain text',
menuCopyAnchor: 'Copy anchor quote',
menuJumpSource: 'Jump to source',
+ menuEditCard: 'Edit this card',
+ menuDeleteCard: 'Delete this card',
copiedMarkdown: 'Copied Markdown',
copiedPlain: 'Copied plain text',
copiedAnchor: 'Copied anchor quote',
@@ -155,11 +187,21 @@ export const STRINGS = {
cacheClearedAll: 'Cleared {count} cache entries',
noCancelableJob: 'No cancellable generation job',
cancelRequested: 'Cancel requested',
+ cancelRequestedApiInFlight: 'Cancel requested. The in-flight API request cannot be aborted immediately; its result will be ignored.',
fileMenuGenerate: 'Generate parallel notes',
fileMenuRegen: 'Regenerate parallel notes',
fileMenuClear: 'Clear parallel-note cache',
noExportContent: 'No parallel notes to export',
noCopyContent: 'No parallel notes to copy',
+ noActiveCard: 'No active summary card to jump',
+ cardDeleted: 'Deleted this card',
+ cardSaved: 'Saved this card',
+ editCardTitle: 'Edit summary card',
+ editCardTitleField: 'Title',
+ editCardGistField: 'Gist',
+ editCardBulletsField: 'Bullets (one per line)',
+ editCardCancel: 'Cancel',
+ editCardSave: 'Save',
openNoteFirst: 'Open a note first',
alreadyGenerating: 'This note is already being generated',
emptyNote: 'The note is empty',
@@ -171,6 +213,18 @@ export const STRINGS = {
cancelled: 'Generation cancelled',
cancelledError: 'Generation cancelled',
generationFailed: 'Generation failed{kind}: {error}',
+ errorCustomProviderBaseUrlRequired: 'Custom provider requires an API Base URL.',
+ errorApiBaseUrlMissing: 'API Base URL is not set. Choose a provider or enter a custom base URL in settings.',
+ errorModelMissing: 'Model is not set. Enter a model ID in settings.',
+ errorApiKeyMissing: 'API key is not set. Enter an API Key in settings{hint}.',
+ errorApiKeyEnvHint: ' or set environment variable {envVar}',
+ errorLlmNonJson: 'LLM returned non-JSON:\n{excerpt}',
+ errorCustomHeadersJsonParse: 'Custom headers JSON parse failed: {error}',
+ errorCustomHeadersJsonObject: 'Custom headers JSON must be an object',
+ errorCustomHeadersLineFormat: 'Custom headers lines must use `Header-Name: value`',
+ errorProviderNonJson: '{label} returned non-JSON:\n{excerpt}',
+ errorProviderRequestFailed: '{label} request failed: {error}',
+ errorProviderApiStatus: '{label} API returned HTTP {status}: {excerpt}',
noEditor: 'Could not find the source note editor',
settingUiLanguageName: 'UI language',
settingUiLanguageDesc: 'Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.',
diff --git a/src/navigation.ts b/src/navigation.ts
new file mode 100644
index 0000000..273f14d
--- /dev/null
+++ b/src/navigation.ts
@@ -0,0 +1,15 @@
+'use strict';
+
+export function nextCardIndex(activeIdx, cardCount, delta) {
+ if (!Number.isFinite(cardCount) || cardCount <= 0) return -1;
+ const count = Math.floor(cardCount);
+ const direction = delta < 0 ? -1 : 1;
+ if (activeIdx < 0 || activeIdx >= count) return direction < 0 ? count - 1 : 0;
+ return Math.min(count - 1, Math.max(0, activeIdx + direction));
+}
+
+export function activeSectionLine(sections, activeIdx) {
+ if (!Array.isArray(sections) || activeIdx < 0 || activeIdx >= sections.length) return -1;
+ const line = Number(sections[activeIdx]?.startLine);
+ return Number.isFinite(line) && line >= 0 ? line : -1;
+}
diff --git a/src/prompt.ts b/src/prompt.ts
index 283e37d..fb55b28 100644
--- a/src/prompt.ts
+++ b/src/prompt.ts
@@ -29,22 +29,37 @@ export function renderPromptTemplate(template, vars) {
});
}
-export function buildPrompts(content, settings) {
- const maxDocChars = Number(settings.maxDocChars) || MAX_DOC_CHARS;
- const promptLanguage = PROMPT_LANGUAGES[settings.promptLanguage] ? settings.promptLanguage : DEFAULT_SETTINGS.promptLanguage;
- const minCards = Math.max(1, Number(settings.minCards) || DEFAULT_SETTINGS.minCards);
- const maxCards = Math.max(minCards, Number(settings.maxCards) || DEFAULT_SETTINGS.maxCards);
- const languageInstruction = promptLanguageInstruction(promptLanguage);
- const doc = content.length > maxDocChars
- ? content.slice(0, maxDocChars) + (promptLanguage === 'en' ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]')
- : content;
+function defaultSystemPrompt(language, minCards, maxCards, languageInstruction, schema, example) {
+ if (language === 'en') {
+ return `You are a long-form reading summary assistant. After reading the full document, split it into ${minCards}-${maxCards} natural topic units. They do not need to match markdown headings; use a complete argument or topic as the unit, merging short sections and splitting long ones when needed.
- const schema = '{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}';
- const example = promptSchemaExample(promptLanguage);
- const templateVars = { minCards, maxCards, languageInstruction, schema, example };
- const customSystem = renderPromptTemplate(settings.customSystemPrompt, templateVars).trim();
+Each card has one guiding sentence plus several bullets. Bullets carry details; gist is the lead-in.
- const defaultSystem = `你是一个长文阅读摘要助手。阅读全文后,把文章切成 ${minCards}-${maxCards} 个"自然主题单元"——不必对应 markdown heading,以"一个完整论点或话题"为单位自行判断粒度:短章节合并、长章节拆分。
+Language:
+- ${languageInstruction}
+
+For each unit, output:
+
+- title: a concise 3-10 word heading that clearly states what the section is about; avoid vague labels like "Background" or "Introduction"
+- anchor: a verbatim quote from the start of this unit, copied 1:1 from the source, 40-80 characters where possible, preserving punctuation, spaces, and line breaks; it is only used internally for positioning
+- gist: one guiding sentence, 20-40 words, stating the core claim or conclusion
+- bullets: 3-6 supporting bullets, each 20-50 words, carrying data, comparisons, mechanisms, examples, or counterintuitive observations. Gist and bullets must not repeat each other.
+
+Rules:
+- anchor must exact-substring-match the source. Never paraphrase, translate, summarize, or alter it.
+- Choose the earliest sufficiently distinctive quote for each unit; avoid generic phrases.
+- Every card must include both gist and bullets.
+- Each bullet must be a standalone assertion; avoid sequencing phrases such as "first" or "then".
+- Output strict JSON only: no markdown fence, no explanation, no tool call.
+
+Output shape:
+${schema}
+
+Example:
+${example}`;
+ }
+
+ return `你是一个长文阅读摘要助手。阅读全文后,把文章切成 ${minCards}-${maxCards} 个"自然主题单元"——不必对应 markdown heading,以"一个完整论点或话题"为单位自行判断粒度:短章节合并、长章节拆分。
**每张卡片的结构:一句话领读 + 若干条 bullet。bullet 承载细节,gist 是一句话导读。**
@@ -70,16 +85,46 @@ ${schema}
示例:
${example}`;
+}
- const system = customSystem
- ? `${customSystem}
-
-不可覆盖的输出契约:
+function systemPromptContract(language, minCards, maxCards, languageInstruction, schema) {
+ if (language === 'en') {
+ return `Non-overridable output contract:
+- Must output ${minCards}-${maxCards} cards.
+- ${languageInstruction}
+- anchor must be copied verbatim from the source and exact-substring-match the source.
+- Output strict JSON only: no markdown fence, no explanation, no tool call.
+- JSON shape: ${schema}`;
+ }
+ return `不可覆盖的输出契约:
- 必须输出 ${minCards}-${maxCards} 张 cards。
- ${languageInstruction}
- anchor 必须从原文逐字复制,必须能在原文 exact substring match 找到。
- 严格只输出 JSON,无 markdown fence、无解释、无 tool call。
-- JSON shape: ${schema}`
+- JSON shape: ${schema}`;
+}
+
+export function buildPrompts(content, settings) {
+ const maxDocChars = Number(settings.maxDocChars) || MAX_DOC_CHARS;
+ const promptLanguage = PROMPT_LANGUAGES[settings.promptLanguage] ? settings.promptLanguage : DEFAULT_SETTINGS.promptLanguage;
+ const minCards = Math.max(1, Number(settings.minCards) || DEFAULT_SETTINGS.minCards);
+ const maxCards = Math.max(minCards, Number(settings.maxCards) || DEFAULT_SETTINGS.maxCards);
+ const languageInstruction = promptLanguageInstruction(promptLanguage);
+ const doc = content.length > maxDocChars
+ ? content.slice(0, maxDocChars) + (promptLanguage === 'en' ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]')
+ : content;
+
+ const schema = '{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}';
+ const example = promptSchemaExample(promptLanguage);
+ const templateVars = { minCards, maxCards, languageInstruction, schema, example };
+ const customSystem = renderPromptTemplate(settings.customSystemPrompt, templateVars).trim();
+ const contract = systemPromptContract(promptLanguage, minCards, maxCards, languageInstruction, schema);
+ const defaultSystem = defaultSystemPrompt(promptLanguage, minCards, maxCards, languageInstruction, schema, example);
+
+ const system = customSystem
+ ? `${customSystem}
+
+${contract}`
: defaultSystem;
const user = promptLanguage === 'en'
diff --git a/src/providers.ts b/src/providers.ts
index da8e81b..66e24ba 100644
--- a/src/providers.ts
+++ b/src/providers.ts
@@ -9,6 +9,7 @@ import {
getApiPreset,
modelForApi,
} from './settings';
+import { translate } from './i18n';
import {
ANTHROPIC_CARD_TOOL_NAME,
anthropicCardTool,
@@ -27,7 +28,7 @@ function endpointUrl(baseUrl, suffixes) {
return base + suffixes[0];
}
-function parseApiHeaders(raw) {
+function parseApiHeaders(raw, settings?) {
const text = (raw || '').trim();
if (!text) return {};
if (text.startsWith('{')) {
@@ -35,10 +36,10 @@ function parseApiHeaders(raw) {
try {
parsed = JSON.parse(text);
} catch (e) {
- throw new Error('自定义 headers JSON 解析失败:' + e.message);
+ throw new Error(translate(settings, 'errorCustomHeadersJsonParse', { error: e.message }));
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
- throw new Error('自定义 headers JSON 必须是对象');
+ throw new Error(translate(settings, 'errorCustomHeadersJsonObject'));
}
const headers = {};
for (const [k, v] of Object.entries(parsed)) {
@@ -53,7 +54,7 @@ function parseApiHeaders(raw) {
if (!trimmed || trimmed.startsWith('#')) continue;
const idx = trimmed.indexOf(':');
if (idx <= 0) {
- throw new Error('自定义 headers 每行格式应为 `Header-Name: value`');
+ throw new Error(translate(settings, 'errorCustomHeadersLineFormat'));
}
const key = trimmed.slice(0, idx).trim();
const value = trimmed.slice(idx + 1).trim();
@@ -68,8 +69,8 @@ function authHeaders(settings) {
const key = getApiKey(settings);
if (!key) {
const envVar = (settings.apiKeyEnvVar || getApiPreset(settings).envVar || '').trim();
- const hint = envVar ? ` 或环境变量 ${envVar}` : '';
- throw new Error(`API key 未设置。请在设置里填写 API Key${hint}。`);
+ const hint = envVar ? translate(settings, 'errorApiKeyEnvHint', { envVar }) : '';
+ throw new Error(translate(settings, 'errorApiKeyMissing', { hint }));
}
if (authType === 'bearer') return { authorization: `Bearer ${key}` };
if (authType === 'x-api-key') return { 'x-api-key': key };
@@ -83,20 +84,23 @@ function buildApiHeaders(settings, extra?) {
'content-type': 'application/json',
...authHeaders(settings),
...(extra || {}),
- ...parseApiHeaders(settings.apiHeaders),
+ ...parseApiHeaders(settings.apiHeaders, settings),
};
}
-function responseJson(resp, label) {
+function responseJson(resp, label, settings?) {
if (resp.json && typeof resp.json === 'object') return resp.json;
try {
return JSON.parse(resp.text || '{}');
} catch (_) {
- throw new Error(`${label} 返回非 JSON:\n${(resp.text || '').slice(0, 500)}`);
+ throw new Error(translate(settings, 'errorProviderNonJson', {
+ label,
+ excerpt: (resp.text || '').slice(0, 500),
+ }));
}
}
-async function requestJsonBody(requestUrlImpl, label, url, headers, body) {
+async function requestJsonBody(requestUrlImpl, label, url, headers, body, settings?) {
let resp;
try {
resp = await requestUrlImpl({
@@ -107,28 +111,35 @@ async function requestJsonBody(requestUrlImpl, label, url, headers, body) {
throw: false,
});
} catch (e) {
- throw new Error(`${label} 请求失败:` + (e.message || e));
+ throw new Error(translate(settings, 'errorProviderRequestFailed', {
+ label,
+ error: e.message || e,
+ }));
}
if (resp.status >= 400) {
- throw new Error(`${label} API ${resp.status}: ${(resp.text || '').slice(0, 500)}`);
+ throw new Error(translate(settings, 'errorProviderApiStatus', {
+ label,
+ status: resp.status,
+ excerpt: (resp.text || '').slice(0, 500),
+ }));
}
- return responseJson(resp, label);
+ return responseJson(resp, label, settings);
}
function shouldRetryWithoutStructuredOutput(error) {
const message = String(error && error.message ? error.message : error);
- if (!/API (400|404|422):/.test(message)) return false;
+ if (!/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(message)) return false;
return /response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(message);
}
-async function requestJsonBodyWithStructuredFallback(requestUrlImpl, label, url, headers, structuredBody, fallbackBody) {
+async function requestJsonBodyWithStructuredFallback(requestUrlImpl, label, url, headers, structuredBody, fallbackBody, settings?) {
try {
- return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody);
+ return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody, settings);
} catch (e) {
if (!fallbackBody || !shouldRetryWithoutStructuredOutput(e)) throw e;
console.warn(`[parallel-reader] ${label} structured output rejected; retrying without structured output`, e);
- return requestJsonBody(requestUrlImpl, label + ' fallback', url, headers, fallbackBody);
+ return requestJsonBody(requestUrlImpl, label + ' fallback', url, headers, fallbackBody, settings);
}
}
@@ -237,11 +248,11 @@ export function buildGeminiBody(system, user, settings, options?) {
};
}
-function cardsFromAnthropicToolUse(json) {
+function cardsFromAnthropicToolUse(json, settings?) {
const content = Array.isArray(json && json.content) ? json.content : [];
const block = content.find(c => c && c.type === 'tool_use' && c.name === ANTHROPIC_CARD_TOOL_NAME);
if (!block) return null;
- if (typeof block.input === 'string') return parseCardsJson(block.input);
+ if (typeof block.input === 'string') return parseCardsJson(block.input, settings);
if (block.input && typeof block.input === 'object') return normalizeCardsPayload(block.input);
return [];
}
@@ -254,14 +265,15 @@ async function summarizeViaAnthropicMessages(requestUrlImpl, system, user, setti
url,
buildApiHeaders(settings, { 'anthropic-version': '2023-06-01' }),
buildAnthropicMessagesBody(system, user, settings),
- buildAnthropicMessagesBody(system, user, settings, { structured: false })
+ buildAnthropicMessagesBody(system, user, settings, { structured: false }),
+ settings
);
- const toolCards = cardsFromAnthropicToolUse(json);
+ const toolCards = cardsFromAnthropicToolUse(json, settings);
if (toolCards) return toolCards;
const text = (json.content || []).map(c => textFromContent(c)).join('').trim();
- return parseCardsJson(text);
+ return parseCardsJson(text, settings);
}
async function summarizeViaOpenAiChat(requestUrlImpl, system, user, settings) {
@@ -272,11 +284,12 @@ async function summarizeViaOpenAiChat(requestUrlImpl, system, user, settings) {
url,
buildApiHeaders(settings),
buildOpenAiChatBody(system, user, settings),
- buildOpenAiChatBody(system, user, settings, { structured: false })
+ buildOpenAiChatBody(system, user, settings, { structured: false }),
+ settings
);
const choice = (json.choices || [])[0] || {};
const text = textFromContent(choice.message?.content || choice.text || '').trim();
- return parseCardsJson(text);
+ return parseCardsJson(text, settings);
}
async function summarizeViaOpenAiResponses(requestUrlImpl, system, user, settings) {
@@ -287,9 +300,10 @@ async function summarizeViaOpenAiResponses(requestUrlImpl, system, user, setting
url,
buildApiHeaders(settings),
buildOpenAiResponsesBody(system, user, settings),
- buildOpenAiResponsesBody(system, user, settings, { structured: false })
+ buildOpenAiResponsesBody(system, user, settings, { structured: false }),
+ settings
);
- return parseCardsJson(textFromOpenAiResponses(json).trim());
+ return parseCardsJson(textFromOpenAiResponses(json).trim(), settings);
}
async function summarizeViaGoogleGenerativeAi(requestUrlImpl, system, user, settings) {
@@ -305,12 +319,13 @@ async function summarizeViaGoogleGenerativeAi(requestUrlImpl, system, user, sett
url,
headers,
buildGeminiBody(system, user, settings),
- buildGeminiBody(system, user, settings, { structured: false })
+ buildGeminiBody(system, user, settings, { structured: false }),
+ settings
);
const candidate = (json.candidates || [])[0] || {};
const parts = candidate.content?.parts || [];
const text = parts.map(p => textFromContent(p)).join('').trim();
- return parseCardsJson(text);
+ return parseCardsJson(text, settings);
}
export async function summarizeViaApi(requestUrlImpl, system, user, settings) {
diff --git a/src/schema.ts b/src/schema.ts
index 1949a0d..b854f6d 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -1,5 +1,7 @@
'use strict';
+import { translate } from './i18n';
+
export const ANTHROPIC_CARD_TOOL_NAME = 'record_parallel_reader_cards';
export function collectJsonObjectCandidates(raw) {
@@ -58,13 +60,15 @@ export function extractJson(text) {
return raw;
}
-export function parseCardsJson(text) {
+export function parseCardsJson(text, settings?) {
const jsonText = extractJson(text);
let parsed;
try {
parsed = JSON.parse(jsonText);
} catch (e) {
- throw new Error('LLM 返回非 JSON:\n' + (text || '').slice(0, 500));
+ throw new Error(translate(settings, 'errorLlmNonJson', {
+ excerpt: (text || '').slice(0, 500),
+ }));
}
return normalizeCardsPayload(parsed);
}
diff --git a/src/scroll.ts b/src/scroll.ts
new file mode 100644
index 0000000..88acf71
--- /dev/null
+++ b/src/scroll.ts
@@ -0,0 +1,40 @@
+'use strict';
+
+export type RafThrottledHandler = (() => void) & { cancel: () => void };
+
+function defaultSchedule(callback: FrameRequestCallback): number {
+ if (typeof requestAnimationFrame === 'function') return requestAnimationFrame(callback);
+ return setTimeout(() => callback(Date.now()), 16) as unknown as number;
+}
+
+function defaultCancel(frameId: number) {
+ if (typeof cancelAnimationFrame === 'function') {
+ cancelAnimationFrame(frameId);
+ return;
+ }
+ clearTimeout(frameId as unknown as ReturnType);
+}
+
+export function createRafThrottledHandler(
+ callback: () => void,
+ schedule: (callback: FrameRequestCallback) => number = defaultSchedule,
+ cancel: (frameId: number) => void = defaultCancel,
+): RafThrottledHandler {
+ let frameId: number | null = null;
+
+ const handler = (() => {
+ if (frameId !== null) return;
+ frameId = schedule(() => {
+ frameId = null;
+ callback();
+ });
+ }) as RafThrottledHandler;
+
+ handler.cancel = () => {
+ if (frameId === null) return;
+ cancel(frameId);
+ frameId = null;
+ };
+
+ return handler;
+}
diff --git a/src/settings.ts b/src/settings.ts
index 9d9984a..f155086 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -1,6 +1,7 @@
'use strict';
import crypto from 'crypto';
+import { translate } from './i18n';
export const MAX_DOC_CHARS = 20000;
export const PROMPT_VERSION = 2;
@@ -254,11 +255,11 @@ export function getApiBaseUrl(settings) {
const explicit = (settings.apiBaseUrl || '').trim();
if (explicit) return explicit.replace(/\/+$/, '');
if ((settings.apiProvider || '').startsWith('custom-')) {
- throw new Error('自定义 provider 需要填写 API Base URL。');
+ throw new Error(translate(settings, 'errorCustomProviderBaseUrlRequired'));
}
const base = (preset.baseUrl || API_FORMATS[format].defaultBaseUrl || '').trim();
if (!base) {
- throw new Error('API Base URL 未设置。请在设置里选择 provider 或填写自定义 base URL。');
+ throw new Error(translate(settings, 'errorApiBaseUrlMissing'));
}
return base.replace(/\/+$/, '');
}
@@ -284,7 +285,7 @@ export function getApiKey(settings) {
export function modelForApi(settings) {
const raw = (settings.model || '').trim();
if (!raw) {
- throw new Error('Model 未设置。请在设置里填写模型 ID。');
+ throw new Error(translate(settings, 'errorModelMissing'));
}
const preset = getApiPreset(settings);
const prefixes = [settings.apiProvider, preset.modelPrefix]
diff --git a/src/vault.ts b/src/vault.ts
new file mode 100644
index 0000000..ec6fed3
--- /dev/null
+++ b/src/vault.ts
@@ -0,0 +1,27 @@
+'use strict';
+
+export function normalizeVaultPath(path) {
+ return String(path || '')
+ .split('/')
+ .map(part => part.trim())
+ .filter(Boolean)
+ .join('/');
+}
+
+export function folderPathsForTarget(folderPath) {
+ const normalized = normalizeVaultPath(folderPath);
+ if (!normalized) return [];
+ const parts = normalized.split('/');
+ return parts.map((_, idx) => parts.slice(0, idx + 1).join('/'));
+}
+
+export async function ensureVaultFolder(app, folderPath) {
+ for (const folder of folderPathsForTarget(folderPath)) {
+ if (app.vault.getAbstractFileByPath(folder)) continue;
+ try {
+ await app.vault.createFolder(folder);
+ } catch (e) {
+ if (!app.vault.getAbstractFileByPath(folder)) throw e;
+ }
+ }
+}
diff --git a/styles.css b/styles.css
index 3f420eb..b19c487 100644
--- a/styles.css
+++ b/styles.css
@@ -310,6 +310,31 @@
to { transform: rotate(360deg); }
}
+.parallel-reader-modal-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin: 12px 0;
+}
+
+.parallel-reader-modal-field label {
+ color: var(--text-muted);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.parallel-reader-modal-field input,
+.parallel-reader-modal-field textarea {
+ width: 100%;
+}
+
+.parallel-reader-modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 16px;
+}
+
.parallel-reader-empty {
padding: 20px;
text-align: center;
diff --git a/tests/generation-job-manager.test.js b/tests/generation-job-manager.test.js
index fddb390..2cdaad8 100644
--- a/tests/generation-job-manager.test.js
+++ b/tests/generation-job-manager.test.js
@@ -12,6 +12,7 @@ Module._load = function load(request, parent, isMain) {
class MarkdownView {}
class TFile {}
class Menu {}
+ class Modal {}
return {
Plugin,
ItemView,
@@ -21,6 +22,7 @@ Module._load = function load(request, parent, isMain) {
MarkdownView,
TFile,
Menu,
+ Modal,
MarkdownRenderer: { render: async () => {} },
requestUrl: async () => ({ status: 200, json: {}, text: '{}' }),
setIcon: () => {},
diff --git a/tests/main.test.js b/tests/main.test.js
index b2c85c8..97aebc1 100644
--- a/tests/main.test.js
+++ b/tests/main.test.js
@@ -1,5 +1,7 @@
const assert = require('assert');
const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
const Module = require('module');
const originalLoad = Module._load;
@@ -18,6 +20,7 @@ Module._load = function load(request, parent, isMain) {
class MarkdownView {}
class TFile {}
class Menu {}
+ class Modal {}
return {
Plugin,
ItemView,
@@ -27,6 +30,7 @@ Module._load = function load(request, parent, isMain) {
MarkdownView,
TFile,
Menu,
+ Modal,
MarkdownRenderer: { render: async () => {} },
requestUrl: async () => ({ status: 200, json: {}, text: '{}' }),
setIcon: () => {},
@@ -39,15 +43,38 @@ const plugin = require('../main.js');
const t = plugin.__test;
assert.ok(t, 'test helpers should be exported');
+const mainSource = fs.readFileSync(path.join(__dirname, '..', 'main.ts'), 'utf8');
+assert.ok(!/\basync\s+onOpen\s*\(/.test(mainSource), 'ParallelReaderView.onOpen should not be async without await');
+assert.ok(!/\basync\s+onClose\s*\(\)\s*\{\s*\}/.test(mainSource), 'empty onClose should not be async');
+assert.ok(/focusSummaryPane\s*\(\)/.test(mainSource), 'summary pane should expose a focus helper');
+assert.ok(/\.focus\(\{\s*preventScroll:\s*true\s*\}\)/.test(mainSource), 'summary pane focus should not scroll the page');
+assert.ok(/moveActiveSection[\s\S]*focusSummaryPane/.test(mainSource), 'card navigation should focus the summary pane');
+assert.ok(/scheduleCacheSave\s*\(/.test(mainSource), 'cache touch should use a debounced cache save path');
+assert.ok(/flushCacheSave\s*\(/.test(mainSource), 'pending cache touches should be flushable');
+assert.ok(/onunload[\s\S]*flushCacheSave/.test(mainSource), 'plugin unload should flush pending cache touches');
+assert.ok(/cacheTouch[\s\S]*scheduleCacheSave/.test(mainSource), 'cacheTouch should schedule a cache save');
+assert.ok(!/cacheTouch[\s\S]{0,220}await this\.saveCache/.test(mainSource), 'cacheTouch should not synchronously write cache.json');
assert.strictEqual(typeof t.cardsToMarkdown, 'function');
+assert.strictEqual(typeof t.cancellationNoticeKey, 'function');
assert.strictEqual(typeof t.buildPrompts, 'function');
assert.strictEqual(typeof t.buildOpenAiChatBody, 'function');
assert.strictEqual(typeof t.extractJson, 'function');
assert.strictEqual(typeof t.findLineForAnchor, 'function');
+assert.strictEqual(typeof t.folderPathsForTarget, 'function');
+assert.strictEqual(typeof t.getApiBaseUrl, 'function');
assert.strictEqual(typeof t.generationFingerprint, 'function');
assert.strictEqual(typeof t.GenerationJobManager, 'function');
+assert.strictEqual(typeof t.modelForApi, 'function');
+assert.strictEqual(typeof t.activeSectionLine, 'function');
+assert.strictEqual(typeof t.touchCacheEntry, 'function');
+assert.strictEqual(typeof t.nextCardIndex, 'function');
assert.strictEqual(typeof t.pruneCacheEntries, 'function');
+assert.strictEqual(typeof t.removeCardAt, 'function');
+assert.strictEqual(typeof t.activeIndexAfterCardDelete, 'function');
+assert.strictEqual(typeof t.createRafThrottledHandler, 'function');
+assert.strictEqual(typeof t.serializeCacheFile, 'function');
assert.strictEqual(typeof t.translate, 'function');
+assert.strictEqual(typeof t.updateCardAt, 'function');
const baseSettings = {
backend: 'api',
@@ -90,6 +117,36 @@ assert.strictEqual(
t.generationFingerprint({ ...baseSettings, uiLanguage: 'en' }),
'cache fingerprint should not change when UI language changes'
);
+assert.strictEqual(
+ t.cancellationNoticeKey({ backend: 'api' }, { phase: 'generating' }),
+ 'cancelRequestedApiInFlight',
+ 'API cancellation should explain that the in-flight network request cannot be aborted'
+);
+assert.strictEqual(
+ t.cancellationNoticeKey({ backend: 'claude-code' }, { phase: 'generating' }),
+ 'cancelRequested',
+ 'CLI cancellation can use the generic cancellation notice'
+);
+assert.strictEqual(
+ t.cancellationNoticeKey({ backend: 'api' }, { phase: 'reading' }),
+ 'cancelRequested',
+ 'API cancellation outside the request phase can use the generic notice'
+);
+assert.throws(
+ () => t.modelForApi({ ...baseSettings, model: '', uiLanguage: 'en' }),
+ /Model is not set/,
+ 'settings errors should respect English UI mode'
+);
+assert.throws(
+ () => t.getApiBaseUrl({
+ ...baseSettings,
+ apiProvider: 'custom-openai-compatible',
+ apiBaseUrl: '',
+ uiLanguage: 'en',
+ }),
+ /Custom provider requires an API Base URL/,
+ 'custom provider base URL errors should respect English UI mode'
+);
const contentHash = crypto.createHash('sha1').update('hello', 'utf8').digest('hex');
assert.strictEqual(
@@ -145,6 +202,8 @@ const englishPrompt = t.buildPrompts('Hello world', {
});
assert.ok(englishPrompt.system.includes('2-4'));
assert.ok(englishPrompt.system.includes('Write title, gist, and bullets in English.'));
+assert.ok(englishPrompt.system.includes('You are a long-form reading summary assistant.'));
+assert.ok(!englishPrompt.system.includes('你是一个长文阅读摘要助手'));
assert.ok(englishPrompt.user.startsWith('Source document:'));
const customPrompt = t.buildPrompts('Hello world', {
@@ -161,6 +220,61 @@ assert.ok(customPrompt.system.includes('JSON shape'));
assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'cmdOpenView'), 'Open Parallel Reader pane');
assert.strictEqual(t.translate({ uiLanguage: 'zh' }, 'cmdOpenView'), '打开对照笔记面板');
assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'cacheClearedAll', { count: 3 }), 'Cleared 3 cache entries');
+assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'cmdCardNext'), 'Focus next summary card');
+
+assert.strictEqual(t.nextCardIndex(-1, 3, 1), 0);
+assert.strictEqual(t.nextCardIndex(-1, 3, -1), 2);
+assert.strictEqual(t.nextCardIndex(0, 3, 1), 1);
+assert.strictEqual(t.nextCardIndex(2, 3, 1), 2);
+assert.strictEqual(t.nextCardIndex(0, 3, -1), 0);
+assert.strictEqual(t.nextCardIndex(0, 0, 1), -1);
+assert.strictEqual(t.activeSectionLine([{ startLine: 3 }], 0), 3);
+assert.strictEqual(t.activeSectionLine([{ startLine: -1 }], 0), -1);
+assert.strictEqual(t.activeSectionLine([{ startLine: 3 }], 1), -1);
+const scheduledFrames = [];
+let throttledCalls = 0;
+const throttled = t.createRafThrottledHandler(() => { throttledCalls += 1; }, cb => {
+ scheduledFrames.push(cb);
+ return scheduledFrames.length;
+});
+throttled();
+throttled();
+throttled();
+assert.strictEqual(scheduledFrames.length, 1, 'scroll handler should be scheduled at most once per frame');
+assert.strictEqual(throttledCalls, 0, 'throttled handler should not run synchronously');
+scheduledFrames.shift()(0);
+assert.strictEqual(throttledCalls, 1);
+throttled();
+assert.strictEqual(scheduledFrames.length, 1, 'scroll handler should be schedulable after the frame runs');
+const cardList = [{ title: 'A' }, { title: 'B' }, { title: 'C' }];
+assert.deepStrictEqual(t.removeCardAt(cardList, 1), [{ title: 'A' }, { title: 'C' }]);
+assert.deepStrictEqual(t.removeCardAt(cardList, -1), cardList);
+assert.deepStrictEqual(t.removeCardAt(cardList, 3), cardList);
+assert.notStrictEqual(t.removeCardAt(cardList, 3), cardList);
+assert.strictEqual(t.activeIndexAfterCardDelete(1, 3, 1), 1, 'deleting active card should select the next card');
+assert.strictEqual(t.activeIndexAfterCardDelete(2, 3, 2), 1, 'deleting the last active card should select the previous card');
+assert.strictEqual(t.activeIndexAfterCardDelete(1, 3, 2), 1, 'deleting before active card should shift active index left');
+assert.strictEqual(t.activeIndexAfterCardDelete(2, 3, 0), 0, 'deleting after active card should keep active index');
+assert.deepStrictEqual(
+ t.updateCardAt(cardList, 1, { title: 'B2', gist: 'G', bullets: ['x'] }),
+ [{ title: 'A' }, { title: 'B2', gist: 'G', bullets: ['x'] }, { title: 'C' }]
+);
+assert.deepStrictEqual(t.updateCardAt(cardList, -1, { title: 'X' }), cardList);
+assert.deepStrictEqual(t.updateCardAt(cardList, 3, { title: 'X' }), cardList);
+assert.notStrictEqual(t.updateCardAt(cardList, 3, { title: 'X' }), cardList);
+assert.deepStrictEqual(t.folderPathsForTarget('Reading/Articles'), ['Reading', 'Reading/Articles']);
+assert.deepStrictEqual(t.folderPathsForTarget('/Reading//Articles/'), ['Reading', 'Reading/Articles']);
+assert.deepStrictEqual(t.folderPathsForTarget('Reading'), ['Reading']);
+assert.deepStrictEqual(t.folderPathsForTarget(''), []);
+const untouchedCacheEntry = { generatedAt: '2024-01-01T00:00:00.000Z' };
+assert.strictEqual(t.touchCacheEntry(null), null);
+assert.strictEqual(t.touchCacheEntry(untouchedCacheEntry, '2024-01-05T00:00:00.000Z'), untouchedCacheEntry);
+assert.strictEqual(untouchedCacheEntry.lastAccessedAt, '2024-01-05T00:00:00.000Z');
+const serializedCache = t.serializeCacheFile({
+ 'note.md': { generatedAt: '2024-01-01T00:00:00.000Z', cards: [{ title: 'A' }] },
+});
+assert.strictEqual(serializedCache.includes('\n'), false, 'cache.json should be compact');
+assert.deepStrictEqual(JSON.parse(serializedCache).entries['note.md'].cards, [{ title: 'A' }]);
const noisyJson = '说明文字 {"cards":[{"title":"A","anchor":"保留 { 花括号 } 字符","gist":"G","bullets":["B"]}]} trailing';
const extracted = t.extractJson(noisyJson);
@@ -248,6 +362,90 @@ async function testOpenAiStructuredFallback() {
assert.deepStrictEqual(cards, [{ title: 'T', anchor: 'A', gist: 'G', bullets: ['B'] }]);
}
+async function testProviderMissingApiKeyUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => {
+ throw new Error('request should not be sent');
+ }, 'system JSON', 'user', {
+ ...baseSettings,
+ apiKey: '',
+ apiKeyEnvVar: '',
+ uiLanguage: 'en',
+ }),
+ /API key is not set/
+ );
+}
+
+async function testProviderHeaderJsonErrorUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => {
+ throw new Error('request should not be sent');
+ }, 'system JSON', 'user', {
+ ...baseSettings,
+ apiHeaders: '{"x":',
+ uiLanguage: 'en',
+ }),
+ /Custom headers JSON parse failed/
+ );
+}
+
+async function testProviderResponseNonJsonUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => ({
+ status: 200,
+ text: 'not json',
+ }), 'system JSON', 'user', {
+ ...baseSettings,
+ uiLanguage: 'en',
+ }),
+ /OpenAI-compatible Chat returned non-JSON/
+ );
+}
+
+async function testProviderRequestFailureUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => {
+ throw new Error('network down');
+ }, 'system JSON', 'user', {
+ ...baseSettings,
+ uiLanguage: 'en',
+ }),
+ /OpenAI-compatible Chat request failed: network down/
+ );
+}
+
+async function testProviderApiStatusErrorUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => ({
+ status: 500,
+ text: 'upstream exploded',
+ }), 'system JSON', 'user', {
+ ...baseSettings,
+ uiLanguage: 'en',
+ }),
+ /OpenAI-compatible Chat API returned HTTP 500: upstream exploded/
+ );
+}
+
+async function testSchemaNonJsonErrorUsesEnglishUi() {
+ await assert.rejects(
+ () => t.summarizeViaApi(async () => ({
+ status: 200,
+ json: {
+ choices: [{
+ message: {
+ content: 'not json',
+ },
+ }],
+ },
+ }), 'system JSON', 'user', {
+ ...baseSettings,
+ uiLanguage: 'en',
+ }),
+ /LLM returned non-JSON/
+ );
+}
+
async function testAnthropicToolUseParsing() {
const requestUrlImpl = async req => {
const body = JSON.parse(req.body);
@@ -280,6 +478,12 @@ async function testAnthropicToolUseParsing() {
(async () => {
await testOpenAiStructuredFallback();
+ await testProviderMissingApiKeyUsesEnglishUi();
+ await testProviderHeaderJsonErrorUsesEnglishUi();
+ await testProviderResponseNonJsonUsesEnglishUi();
+ await testProviderRequestFailureUsesEnglishUi();
+ await testProviderApiStatusErrorUsesEnglishUi();
+ await testSchemaNonJsonErrorUsesEnglishUi();
await testAnthropicToolUseParsing();
console.log('tests passed');
})().catch(e => {