diff --git a/biome.json b/biome.json index 756918a..ae01b13 100644 --- a/biome.json +++ b/biome.json @@ -8,12 +8,12 @@ "rules": { "recommended": true, "suspicious": { - "noExplicitAny": "warn", + "noExplicitAny": "error", "noRedundantUseStrict": "off", "noImplicitAnyLet": "off" }, "style": { - "noNonNullAssertion": "off", + "noNonNullAssertion": "warn", "useConst": "warn", "useNodejsImportProtocol": "off", "useTemplate": "off" diff --git a/main.js b/main.js index 607145d..a0e7820 100644 --- a/main.js +++ b/main.js @@ -1,35 +1,35 @@ 'use strict'; -"use strict";var dt=Object.create;var X=Object.defineProperty;var pt=Object.getOwnPropertyDescriptor;var ht=Object.getOwnPropertyNames;var mt=Object.getPrototypeOf,gt=Object.prototype.hasOwnProperty;var ft=(r,n)=>{for(var e in n)X(r,e,{get:n[e],enumerable:!0})},Ke=(r,n,e,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of ht(n))!gt.call(r,a)&&a!==e&&X(r,a,{get:()=>n[a],enumerable:!(t=pt(n,a))||t.enumerable});return r};var Z=(r,n,e)=>(e=r!=null?dt(mt(r)):{},Ke(n||!r||!r.__esModule?X(e,"default",{value:r,enumerable:!0}):e,r)),yt=r=>Ke(X({},"__esModule",{value:!0}),r);var $t={};ft($t,{__test:()=>Kt,default:()=>Jt});module.exports=yt($t);var h=require("obsidian");function Q(r,n){if(!n)return-1;let e=m=>m.replace(/\s+/g," ").trim(),t=m=>{let p=[],f=[],u=!1;for(let C=0;C0;continue}u&&(p.push(" "),f.push(C),u=!1),p.push(Je),f.push(C)}return{text:p.join(""),map:f}},a=m=>{if(!m)return-1;let p=r.indexOf(m);if(p===-1)return-1;let f=0;for(let u=0;u=0||(i=a(n.trim()),i>=0))return i;for(let m of[60,40,25,15]){let p=n.trim().slice(0,m);if(i=a(p),i>=0)return i}let s=t(r),o=e(n).slice(0,30);if(!o)return-1;let l=s.text.indexOf(o);if(l===-1)return-1;let c=s.map[l];if(c==null)return-1;let d=0;for(let m=0;m0}function ee(r,n){let e=Array.isArray(r)?r.slice():[];return!Number.isInteger(n)||n<0||n>=e.length||e.splice(n,1),e}function te(r,n,e){if(!Number.isInteger(r)||!Number.isInteger(n)||n<=0||!Number.isInteger(e)||e<0||r<0||r>=n)return e;let t=n-1;return t<=0?-1:r=t.length||(t[n]=Object.assign({},t[n],e||{})),t}var We=require("child_process"),Ye=Z(require("fs")),ie=Z(require("os")),w=Z(require("path"));var O=class extends Error{constructor(n){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=n}},P=class extends Error{constructor(n){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=n}},we=class{constructor(n){this.key=n,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(n){this.phase=n,this.updatedAt=new Date().toISOString()}onCancel(n){if(typeof n=="function"){if(this.cancelled){n();return}this._cancelHandlers.push(n)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let n of this._cancelHandlers.splice(0))try{n()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new P(this.key)}},U=class{constructor(){this.jobs=new Map}get(n){return this.jobs.get(n)||null}isRunning(n){return this.jobs.has(n)}async start(n,e){if(this.jobs.has(n))throw new O(n);let t=new we(n);this.jobs.set(n,t);try{t.setPhase("running");let a=await e(t);return t.throwIfCancelled(),a}catch(a){throw t.cancelled&&!(a instanceof P)?new P(n):a}finally{this.jobs.delete(n)}}cancel(n){let e=this.jobs.get(n);return e?e.cancel():!1}};function ve(r){if(r instanceof P)return"cancelled";let n=r;if(n?.code==="cancelled")return"cancelled";let e=String(n?.message||r);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 ne={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",confirmRegenerateEditedCards:"\u8FD9\u7BC7\u7B14\u8BB0\u7684\u5BF9\u7167\u5361\u7247\u5DF2\u88AB\u624B\u52A8\u7F16\u8F91\u3002\u91CD\u65B0\u751F\u6210\u4F1A\u8986\u76D6\u8FD9\u4E9B\u4FEE\u6539\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",regenerateCancelled:"\u5DF2\u53D6\u6D88\u91CD\u65B0\u751F\u6210",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 +"use strict";var pt=Object.create;var Q=Object.defineProperty;var ht=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var mt=Object.getPrototypeOf,ft=Object.prototype.hasOwnProperty;var yt=(r,t)=>{for(var e in t)Q(r,e,{get:t[e],enumerable:!0})},$e=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of gt(t))!ft.call(r,a)&&a!==e&&Q(r,a,{get:()=>t[a],enumerable:!(n=ht(t,a))||n.enumerable});return r};var ee=(r,t,e)=>(e=r!=null?pt(mt(r)):{},$e(t||!r||!r.__esModule?Q(e,"default",{value:r,enumerable:!0}):e,r)),bt=r=>$e(Q({},"__esModule",{value:!0}),r);var zt={};yt(zt,{__test:()=>qt,default:()=>$t});module.exports=bt(zt);var h=require("obsidian");function te(r,t){if(!t)return-1;let e=g=>g.replace(/\s+/g," ").trim(),n=g=>{let p=[],f=[],u=!1;for(let b=0;b0;continue}u&&(p.push(" "),f.push(b),u=!1),p.push(Ke),f.push(b)}return{text:p.join(""),map:f}},a=g=>{if(!g)return-1;let p=r.indexOf(g);if(p===-1)return-1;let f=0;for(let u=0;u=0||(i=a(t.trim()),i>=0))return i;for(let g of[60,40,25,15]){let p=t.trim().slice(0,g);if(i=a(p),i>=0)return i}let s=n(r),o=e(t).slice(0,30);if(!o)return-1;let l=s.text.indexOf(o);if(l===-1)return-1;let c=s.map[l];if(c==null)return-1;let d=0;for(let g=0;g0}var We=ee(require("crypto"));var ae={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",confirmRegenerateEditedCards:"\u8FD9\u7BC7\u7B14\u8BB0\u7684\u5BF9\u7167\u5361\u7247\u5DF2\u88AB\u624B\u52A8\u7F16\u8F91\u3002\u91CD\u65B0\u751F\u6210\u4F1A\u8986\u76D6\u8FD9\u4E9B\u4FEE\u6539\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F",regenerateCancelled:"\u5DF2\u53D6\u6D88\u91CD\u65B0\u751F\u6210",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",settingStreamingName:"\u6D41\u5F0F\u8F93\u51FA",settingStreamingDesc:"\u542F\u7528\u540E\u751F\u6210\u65F6\u5B9E\u65F6\u663E\u793A LLM \u8F93\u51FA\u8FDB\u5EA6\uFF08\u4EC5 OpenAI Chat \u548C Anthropic \u683C\u5F0F\u652F\u6301\uFF09"},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",confirmRegenerateEditedCards:"These parallel-reader cards were edited manually. Regenerating will overwrite those edits. Continue?",regenerateCancelled:"Regeneration cancelled",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}`,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",settingStreamingName:"\u6D41\u5F0F\u8F93\u51FA",settingStreamingDesc:"\u542F\u7528\u540E\u751F\u6210\u65F6\u5B9E\u65F6\u663E\u793A LLM \u8F93\u51FA\u8FDB\u5EA6\uFF08\u4EC5 OpenAI Chat \u548C Anthropic \u683C\u5F0F\u652F\u6301\uFF09",cmdBatchGenerate:"\u6279\u91CF\u751F\u6210\u5BF9\u7167\u7B14\u8BB0\uFF08\u5F53\u524D\u6587\u4EF6\u5939\uFF09",batchSelectFolder:"\u8BF7\u8F93\u5165\u8981\u6279\u91CF\u5904\u7406\u7684\u6587\u4EF6\u5939\u8DEF\u5F84\uFF08\u7559\u7A7A\u4E3A Vault \u6839\u76EE\u5F55\uFF09",batchFolderPrompt:"\u6587\u4EF6\u5939\u8DEF\u5F84",batchNoMarkdown:"\u8BE5\u6587\u4EF6\u5939\u4E0B\u6CA1\u6709 Markdown \u6587\u4EF6",batchProgress:"\u6279\u91CF\u751F\u6210\uFF1A{current}/{total}",batchDone:"\u6279\u91CF\u751F\u6210\u5B8C\u6210\uFF1A\u5171\u5904\u7406 {total} \u7BC7\uFF0C\u8DF3\u8FC7\u7F13\u5B58 {skipped} \u7BC7"},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",confirmRegenerateEditedCards:"These parallel-reader cards were edited manually. Regenerating will overwrite those edits. Continue?",regenerateCancelled:"Regeneration cancelled",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",settingStreamingName:"Streaming output",settingStreamingDesc:"Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)"}};function Ct(r){let n=r?.uiLanguage;return n==="zh"||n==="en"?n:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function b(r,n,e){let t=Ct(r),a=ne[t]||ne.en,i=ne.en[n]||ne.zh[n]||n,s=a[n]||i;return String(s).replace(/\{([a-zA-Z0-9_]+)\}/g,(o,l)=>e&&Object.hasOwn(e,l)?String(e[l]):o)}var ae="record_parallel_reader_cards";function bt(r){let n=[],e=-1,t=0,a=!1,i=!1;for(let s=0;s0&&t--,t===0&&e>=0&&(n.push(r.slice(e,s+1)),e=-1))}return n}function xe(r){let n=(r||"").trim();if(!n)return n;try{return JSON.parse(n),n}catch{}let e=n.match(/```(?:json)?\s*([\s\S]*?)```/);if(e){let a=e[1].trim();try{return JSON.parse(a),a}catch{}}let t=bt(n);t.sort((a,i)=>i.length-a.length);for(let a of t)try{return JSON.parse(a),a}catch{}return n}function S(r,n){let e=xe(r),t;try{t=JSON.parse(e)}catch{throw new Error(b(n||null,"errorLlmNonJson",{excerpt:(r||"").slice(0,500)}))}return G(t)}function G(r){return(r&&Array.isArray(r.cards)?r.cards:[]).filter(e=>!!e&&typeof e=="object").map(e=>({title:typeof e.title=="string"?e.title:"(\u65E0\u6807\u9898)",anchor:typeof e.anchor=="string"?e.anchor:"",gist:typeof e.gist=="string"?e.gist:"",bullets:Array.isArray(e.bullets)?e.bullets.filter(t=>typeof t=="string"):[]}))}function H(r){let n={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},e={type:"object",properties:{cards:{type:"array",items:n}},required:["cards"]};return r&&(n.additionalProperties=!1,e.additionalProperties=!1),e}function $e(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:H(!0)}}}function ze(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:H(!0)}}}function qe(){return{name:ae,description:"Return the generated Parallel Reader summary cards.",input_schema:H(!0)}}function M(r,n){if(n?.trim())return n.trim();let e=ie.default.homedir(),t=process.platform==="win32",a=t?[".cmd",".exe",""]:[""],i=t?[w.default.join(e,"AppData","Roaming","npm"),w.default.join(e,"AppData","Local","Programs","claude-code"),w.default.join(e,"AppData","Local","Programs","codex"),w.default.join(e,".claude","local"),w.default.join(e,".codex","bin"),w.default.join(e,".bun","bin"),w.default.join(e,"scoop","shims"),"C:\\Program Files\\nodejs"]:[w.default.join(e,"bin"),w.default.join(e,".local","bin"),w.default.join(e,".claude","local"),w.default.join(e,".codex","bin"),w.default.join(e,".bun","bin"),w.default.join(e,".npm-global","bin"),w.default.join(e,".cargo","bin"),"/opt/homebrew/bin","/usr/local/bin"];for(let s of i)for(let o of a){let l=w.default.join(s,r+o);try{if(Ye.default.existsSync(l))return l}catch{}}return r}function J(r,n,e,t,a){return new Promise((i,s)=>{let o,l=!1,c=null,d=u=>{l||(l=!0,c&&clearTimeout(c),s(u))},m=u=>{l||(l=!0,c&&clearTimeout(c),i(u))};try{o=(0,We.spawn)(r,n,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",w.default.join(ie.default.homedir(),".local/bin"),w.default.join(ie.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return s(new Error(`Failed to start ${r}: ${u.message}`))}let p="",f="";if(c=setTimeout(()=>{try{o.kill("SIGKILL")}catch{}d(new Error(`CLI timed out (${t}ms)`))},t),a&&a.onCancel(()=>{try{o.kill("SIGKILL")}catch{}d(new P(a.key))}),o.stdout.on("data",u=>{p+=u.toString("utf8")}),o.stderr.on("data",u=>{f+=u.toString("utf8")}),o.on("error",u=>{d(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),o.on("close",u=>{if(!l){if(u!==0)return d(new Error(`CLI exited with code ${u} +{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",settingStreamingName:"Streaming output",settingStreamingDesc:"Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)",cmdBatchGenerate:"Batch generate parallel notes (current folder)",batchSelectFolder:"Enter the folder path to batch-process (leave blank for Vault root)",batchFolderPrompt:"Folder path",batchNoMarkdown:"No Markdown files found in that folder",batchProgress:"Batch generating: {current}/{total}",batchDone:"Batch complete: processed {total}, skipped {skipped} (cached)"}};function wt(r){let t=r?.uiLanguage;return t==="zh"||t==="en"?t:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function w(r,t,e){let n=wt(r),a=ae[n]||ae.en,i=ae.en[t]||ae.zh[t]||t,s=a[t]||i;return String(s).replace(/\{([a-zA-Z0-9_]+)\}/g,(o,l)=>e&&Object.hasOwn(e,l)?String(e[l]):o)}var xe=1e5,Ct=2,_=2,V=100,G={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},Pe={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},y={uiLanguage:"auto",backend:"api",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:xe,maxCacheEntries:V,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4,streaming:!0,streamingTimeoutMs:12e4},P={"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"}},Se={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"},F={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 O(r){return We.default.createHash("sha1").update(r,"utf8").digest("hex")}function Ae(r){return Array.isArray(r)?"["+r.map(Ae).join(",")+"]":r&&typeof r=="object"?"{"+Object.keys(r).sort().map(t=>JSON.stringify(t)+":"+Ae(r[t])).join(",")+"}":JSON.stringify(r)}function Te(r){return r==="api"||r==="anthropic-api"}function v(r){return F[r.apiProvider]||F.anthropic}function x(r){let t=v(r),e=(r.apiFormat||t.format||"").trim();return P[e]?e:t.format}function k(r){let t=x(r),e=v(r),n=(r.apiBaseUrl||"").trim();if(n)return n.replace(/\/+$/,"");if((r.apiProvider||"").startsWith("custom-"))throw new Error(w(r,"errorCustomProviderBaseUrlRequired"));let a=(e.baseUrl||P[t].defaultBaseUrl||"").trim();if(!a)throw new Error(w(r,"errorApiBaseUrlMissing"));return a.replace(/\/+$/,"")}function Ee(r){let t=(r.apiAuthType||"auto").trim();if(t&&t!=="auto")return t;let e=v(r),n=x(r);return e.authType||P[n].defaultAuthType||"bearer"}function Ye(r){let t=(r.apiKey||"").trim();if(t)return t;let e=(r.apiKeyEnvVar||v(r).envVar||"").trim();return e&&process.env?.[e]?String(process.env[e]).trim():""}function D(r){let t=(r.model||"").trim();if(!t)throw new Error(w(r,"errorModelMissing"));let e=v(r),n=[r.apiProvider,e.modelPrefix].map(i=>(i||"").trim()).filter(Boolean),a=t.toLowerCase();for(let i of n){let s=i.toLowerCase();if(a.startsWith(s+"/"))return t.slice(i.length+1).trim()}return t}function ke(r,t){let e=v(r),n=F[t]||F.anthropic,a=(r.model||"").trim(),i=!a||a===y.model||!!e.model&&a===e.model;r.apiProvider=t,r.apiFormat=n.format,r.apiBaseUrl=n.baseUrl,r.apiAuthType=n.authType||"auto",r.apiKeyEnvVar=n.envVar||"",i&&(r.model=n.model||"")}function Fe(r){Pe[r.uiLanguage]||(r.uiLanguage=y.uiLanguage),(!r.apiProvider||!F[r.apiProvider])&&(r.apiProvider="anthropic");let t=v(r);(!r.apiFormat||!P[r.apiFormat])&&(r.apiFormat=t.format),(!r.apiAuthType||!Se[r.apiAuthType])&&(r.apiAuthType="auto"),r.backend==="anthropic-api"&&(r.apiProvider=r.apiProvider||"anthropic",r.apiFormat=r.apiFormat||"anthropic-messages",r.apiBaseUrl=r.apiBaseUrl||F.anthropic.baseUrl,r.apiAuthType=r.apiAuthType||"x-api-key",r.apiKeyEnvVar=r.apiKeyEnvVar||"ANTHROPIC_API_KEY");let e=Number(r.apiMaxTokens);(!Number.isFinite(e)||e<=0)&&(r.apiMaxTokens=y.apiMaxTokens);let n=Number(r.maxDocChars);return(!Number.isFinite(n)||n<1e3)&&(r.maxDocChars=y.maxDocChars),r.maxCacheEntries=Xe(r.maxCacheEntries),G[r.promptLanguage]||(r.promptLanguage=y.promptLanguage),r.minCards=qe(r.minCards,y.minCards),r.maxCards=qe(r.maxCards,y.maxCards),r.maxCardsze(s)-ze(l)||i.localeCompare(o)).slice(0,n.length-e).map(([i])=>i);for(let i of a)delete r[i];return a}function H(r){let t=Fe(Object.assign({},y,r||{})),e=Te(t.backend),n=v(t),a=x(t),i=e?(t.apiBaseUrl||n.baseUrl||P[a]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return O(Ae({cacheSchemaVersion:_,promptVersion:Ct,maxDocChars:Number(t.maxDocChars)||y.maxDocChars,promptLanguage:t.promptLanguage,minCards:t.minCards,maxCards:t.maxCards,customSystemPromptHash:O(t.customSystemPrompt||""),backend:t.backend,model:t.model,apiProvider:e?t.apiProvider:"",apiFormat:e?a:"",apiBaseUrl:i,apiAuthType:e?Ee(t):"",apiHeadersHash:e?O(t.apiHeaders||""):"",apiMaxTokens:e?Number(t.apiMaxTokens)||y.apiMaxTokens:0,structuredOutputVersion:1}))}function J(r,t,e){return!!r&&r.schemaVersion===_&&r.contentHash===O(t)&&r.settingsHash===H(e)}var se=class{constructor(t,e,n,a){this.adapter=t;this.configDir=e;this.pluginId=n;this.getSettings=a;this.cache={};this._timer=null;this._dirty=!1}filePath(){return`${this.configDir}/plugins/${this.pluginId}/cache.json`}async ensureDir(){let t=`${this.configDir}/plugins/${this.pluginId}`;try{if(typeof this.adapter.exists=="function"&&await this.adapter.exists(t))return;await this.adapter.mkdir(t)}catch{}}async readFile(){try{let t=await this.adapter.read(this.filePath()),e=JSON.parse(t);if(e&&typeof e=="object"&&e.entries&&typeof e.entries=="object")return e.entries}catch(t){let e=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(e)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeFile(){await this.ensureDir(),await this.adapter.write(this.filePath(),ne(this.cache))}async load(){this.cache=await this.readFile(),this.prune().length>0&&await this.writeFile()}prune(){let t=this.getSettings();return ie(this.cache,t?.maxCacheEntries||V)}async pruneIfNeeded(){let t=this.prune();return t.length>0&&await this.save(),t}async save(){this._timer&&(clearTimeout(this._timer),this._timer=null),this.prune(),await this.writeFile(),this._dirty=!1}scheduleSave(t=5e3){this._dirty=!0,!this._timer&&(this._timer=setTimeout(()=>{this._timer=null,this._dirty&&this.save().catch(e=>console.error("[parallel-reader] failed to save cache",e))},t))}async flush(){this._timer&&(clearTimeout(this._timer),this._timer=null),this._dirty&&await this.save()}get(t){return this.cache[t]||null}async touch(t){let e=re(this.cache[t]||null);return e?(this.cache[t]=e,this.scheduleSave(),e):null}async put(t,e,n,a){let i=new Date().toISOString();this.cache[t]={schemaVersion:_,contentHash:O(e),settingsHash:H(a||this.getSettings()),cards:n,generatedAt:i,lastAccessedAt:i},await this.save()}async replaceCards(t,e){let n=this.cache[t];if(!n)return!1;let a=new Date().toISOString();return this.cache[t]={...n,cards:(e||[]).map(i=>({title:i.title,anchor:i.anchor,gist:i.gist,bullets:i.bullets||[]})),updatedAt:a,lastAccessedAt:a},await this.save(),!0}async delete(t){this.cache[t]&&(delete this.cache[t],await this.save())}async clear(){this.cache={},await this.save()}};function oe(r,t){let e=Array.isArray(r)?r.slice():[];return!Number.isInteger(t)||t<0||t>=e.length||e.splice(t,1),e}function le(r,t,e){if(!Number.isInteger(r)||!Number.isInteger(t)||t<=0||!Number.isInteger(e)||e<0||r<0||r>=t)return e;let n=t-1;return n<=0?-1:r=n.length||(n[t]=Object.assign({},n[t],e||{})),n}var tt=require("child_process"),rt=ee(require("fs")),de=ee(require("os")),C=ee(require("path"));var B=class extends Error{constructor(t){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=t}},S=class extends Error{constructor(t){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=t}},Me=class{constructor(t){this.key=t,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(t){this.phase=t,this.updatedAt=new Date().toISOString()}onCancel(t){if(typeof t=="function"){if(this.cancelled){t();return}this._cancelHandlers.push(t)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let t of this._cancelHandlers.splice(0))try{t()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new S(this.key)}},K=class{constructor(){this.jobs=new Map}get(t){return this.jobs.get(t)||null}isRunning(t){return this.jobs.has(t)}async start(t,e){if(this.jobs.has(t))throw new B(t);let n=new Me(t);this.jobs.set(t,n);try{n.setPhase("running");let a=await e(n);return n.throwIfCancelled(),a}catch(a){throw n.cancelled&&!(a instanceof S)?new S(t):a}finally{this.jobs.delete(t)}}cancel(t){let e=this.jobs.get(t);return e?e.cancel():!1}};function Re(r){if(r instanceof S)return"cancelled";let t=r;if(t?.code==="cancelled")return"cancelled";let e=String(t?.message||r);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 ue="record_parallel_reader_cards";function vt(r){let t=[],e=-1,n=0,a=!1,i=!1;for(let s=0;s0&&n--,n===0&&e>=0&&(t.push(r.slice(e,s+1)),e=-1))}return t}function Ne(r){let t=(r||"").trim();if(!t)return t;try{return JSON.parse(t),t}catch{}let e=t.match(/```(?:json)?\s*([\s\S]*?)```/);if(e){let a=e[1].trim();try{return JSON.parse(a),a}catch{}}let n=vt(t);n.sort((a,i)=>i.length-a.length);for(let a of n)try{return JSON.parse(a),a}catch{}return t}function T(r,t){let e=Ne(r),n;try{n=JSON.parse(e)}catch{throw new Error(w(t||null,"errorLlmNonJson",{excerpt:(r||"").slice(0,500)}))}return $(n)}function $(r){return(r&&Array.isArray(r.cards)?r.cards:[]).filter(e=>!!e&&typeof e=="object").map(e=>({title:typeof e.title=="string"?e.title:"(\u65E0\u6807\u9898)",anchor:typeof e.anchor=="string"?e.anchor:"",gist:typeof e.gist=="string"?e.gist:"",bullets:Array.isArray(e.bullets)?e.bullets.filter(n=>typeof n=="string"):[]}))}function q(r){let t={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},e={type:"object",properties:{cards:{type:"array",items:t}},required:["cards"]};return r&&(t.additionalProperties=!1,e.additionalProperties=!1),e}function Ze(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:q(!0)}}}function Qe(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:q(!0)}}}function et(){return{name:ue,description:"Return the generated Parallel Reader summary cards.",input_schema:q(!0)}}function I(r,t){if(t?.trim())return t.trim();let e=de.default.homedir(),n=process.platform==="win32",a=n?[".cmd",".exe",""]:[""],i=n?[C.default.join(e,"AppData","Roaming","npm"),C.default.join(e,"AppData","Local","Programs","claude-code"),C.default.join(e,"AppData","Local","Programs","codex"),C.default.join(e,".claude","local"),C.default.join(e,".codex","bin"),C.default.join(e,".bun","bin"),C.default.join(e,"scoop","shims"),"C:\\Program Files\\nodejs"]:[C.default.join(e,"bin"),C.default.join(e,".local","bin"),C.default.join(e,".claude","local"),C.default.join(e,".codex","bin"),C.default.join(e,".bun","bin"),C.default.join(e,".npm-global","bin"),C.default.join(e,".cargo","bin"),"/opt/homebrew/bin","/usr/local/bin"];for(let s of i)for(let o of a){let l=C.default.join(s,r+o);try{if(rt.default.existsSync(l))return l}catch{}}return r}function z(r,t,e,n,a){return new Promise((i,s)=>{let o,l=!1,c=null,d=u=>{l||(l=!0,c&&clearTimeout(c),s(u))},g=u=>{l||(l=!0,c&&clearTimeout(c),i(u))};try{o=(0,tt.spawn)(r,t,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",C.default.join(de.default.homedir(),".local/bin"),C.default.join(de.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return s(new Error(`Failed to start ${r}: ${u.message}`))}let p="",f="";if(c=setTimeout(()=>{try{o.kill("SIGKILL")}catch{}d(new Error(`CLI timed out (${n}ms)`))},n),a&&a.onCancel(()=>{try{o.kill("SIGKILL")}catch{}d(new S(a.key))}),o.stdout.on("data",u=>{p+=u.toString("utf8")}),o.stderr.on("data",u=>{f+=u.toString("utf8")}),o.on("error",u=>{d(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),o.on("close",u=>{if(!l){if(u!==0)return d(new Error(`CLI exited with code ${u} stderr: -${f.slice(0,1e3)}`));m({stdout:p,stderr:f})}}),e)try{o.stdin.write(e),o.stdin.end()}catch{}else try{o.stdin.end()}catch{}})}async function Xe(r,n,e,t){let a=M("claude",e.cliPath),i=["-p","--output-format","json","--append-system-prompt",r,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"],{stdout:s}=await J(a,i,n,e.cliTimeoutMs,t),o="";try{let l=JSON.parse(s);if(Array.isArray(l)){let c=l.find(d=>d.type==="result");c&&typeof c.result=="string"&&(o=c.result)}else l&&typeof l=="object"&&(o=l.result||l.content||"")}catch{throw new Error(`claude CLI returned unexpected output: +${f.slice(0,1e3)}`));g({stdout:p,stderr:f})}}),e)try{o.stdin.write(e),o.stdin.end()}catch{}else try{o.stdin.end()}catch{}})}async function nt(r,t,e,n){let a=I("claude",e.cliPath),i=["-p","--output-format","json","--append-system-prompt",r,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"],{stdout:s}=await z(a,i,t,e.cliTimeoutMs,n),o="";try{let l=JSON.parse(s);if(Array.isArray(l)){let c=l.find(d=>d.type==="result");c&&typeof c.result=="string"&&(o=c.result)}else l&&typeof l=="object"&&(o=l.result||l.content||"")}catch{throw new Error(`claude CLI returned unexpected output: `+s.slice(0,500))}if(!o)throw new Error(`claude CLI returned no result. Output: -`+s.slice(0,500));return S(o,e)}async function Ze(r,n,e,t){let a=M("codex",e.cliPath),i=`<> +`+s.slice(0,500));return T(o,e)}async function at(r,t,e,n){let a=I("codex",e.cliPath),i=`<> ${r} <> -${n} +${t} -Output JSON directly with no explanation.`,s=["exec","--skip-git-repo-check","-"],{stdout:o}=await J(a,s,i,e.cliTimeoutMs,t);return S(o,e)}function Ae(r){let n=[`## ${r.title}`];if(r.anchor){let e=r.anchor.replace(/\s+/g," ").trim();n.push(`> ${e}`)}return r.gist&&n.push(r.gist),r.bullets&&r.bullets.length>0&&n.push(r.bullets.map(e=>`- ${e}`).join(` -`)),n.join(` +Output JSON directly with no explanation.`,s=["exec","--skip-git-repo-check","-"],{stdout:o}=await z(a,s,i,e.cliTimeoutMs,n);return T(o,e)}function De(r){let t=[`## ${r.title}`];if(r.anchor){let e=r.anchor.replace(/\s+/g," ").trim();t.push(`> ${e}`)}return r.gist&&t.push(r.gist),r.bullets&&r.bullets.length>0&&t.push(r.bullets.map(e=>`- ${e}`).join(` +`)),t.join(` -`)}function Qe(r){return[r.title,r.gist||"",...(r.bullets||[]).map(n=>"\u2022 "+n)].filter(Boolean).join(` -`)}function K(r,n){let e=[`# ${r||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let t of n||[])e.push(Ae(t));return e.join(` +`)}function it(r){return[r.title,r.gist||"",...(r.bullets||[]).map(t=>"\u2022 "+t)].filter(Boolean).join(` +`)}function W(r,t){let e=[`# ${r||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let n of t||[])e.push(De(n));return e.join(` -`)}function se(r,n,e){if(!Number.isFinite(n)||n<=0)return-1;let t=Math.floor(n),a=e<0?-1:1;return r<0||r>=t?a<0?t-1:0:Math.min(t-1,Math.max(0,r+a))}function oe(r,n){if(!Array.isArray(r)||n<0||n>=r.length)return-1;let e=Number(r[n]?.startLine);return Number.isFinite(e)&&e>=0?e:-1}var rt=Z(require("crypto"));var Se=1e5,wt=2,$=2,V=100,z={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},Te={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},y={uiLanguage:"auto",backend:"api",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:Se,maxCacheEntries:V,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4,streaming:!0},T={"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"}},Ee={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"},k={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 _(r){return rt.default.createHash("sha1").update(r,"utf8").digest("hex")}function Pe(r){return Array.isArray(r)?"["+r.map(Pe).join(",")+"]":r&&typeof r=="object"?"{"+Object.keys(r).sort().map(n=>JSON.stringify(n)+":"+Pe(r[n])).join(",")+"}":JSON.stringify(r)}function Fe(r){return r==="api"||r==="anthropic-api"}function v(r){return k[r.apiProvider]||k.anthropic}function A(r){let n=v(r),e=(r.apiFormat||n.format||"").trim();return T[e]?e:n.format}function F(r){let n=A(r),e=v(r),t=(r.apiBaseUrl||"").trim();if(t)return t.replace(/\/+$/,"");if((r.apiProvider||"").startsWith("custom-"))throw new Error(b(r,"errorCustomProviderBaseUrlRequired"));let a=(e.baseUrl||T[n].defaultBaseUrl||"").trim();if(!a)throw new Error(b(r,"errorApiBaseUrlMissing"));return a.replace(/\/+$/,"")}function ke(r){let n=(r.apiAuthType||"auto").trim();if(n&&n!=="auto")return n;let e=v(r),t=A(r);return e.authType||T[t].defaultAuthType||"bearer"}function nt(r){let n=(r.apiKey||"").trim();if(n)return n;let e=(r.apiKeyEnvVar||v(r).envVar||"").trim();return e&&process.env?.[e]?String(process.env[e]).trim():""}function I(r){let n=(r.model||"").trim();if(!n)throw new Error(b(r,"errorModelMissing"));let e=v(r),t=[r.apiProvider,e.modelPrefix].map(i=>(i||"").trim()).filter(Boolean),a=n.toLowerCase();for(let i of t){let s=i.toLowerCase();if(a.startsWith(s+"/"))return n.slice(i.length+1).trim()}return n}function Ne(r,n){let e=v(r),t=k[n]||k.anthropic,a=(r.model||"").trim(),i=!a||a===y.model||!!e.model&&a===e.model;r.apiProvider=n,r.apiFormat=t.format,r.apiBaseUrl=t.baseUrl,r.apiAuthType=t.authType||"auto",r.apiKeyEnvVar=t.envVar||"",i&&(r.model=t.model||"")}function De(r){Te[r.uiLanguage]||(r.uiLanguage=y.uiLanguage),(!r.apiProvider||!k[r.apiProvider])&&(r.apiProvider="anthropic");let n=v(r);(!r.apiFormat||!T[r.apiFormat])&&(r.apiFormat=n.format),(!r.apiAuthType||!Ee[r.apiAuthType])&&(r.apiAuthType="auto"),r.backend==="anthropic-api"&&(r.apiProvider=r.apiProvider||"anthropic",r.apiFormat=r.apiFormat||"anthropic-messages",r.apiBaseUrl=r.apiBaseUrl||k.anthropic.baseUrl,r.apiAuthType=r.apiAuthType||"x-api-key",r.apiKeyEnvVar=r.apiKeyEnvVar||"ANTHROPIC_API_KEY");let e=Number(r.apiMaxTokens);(!Number.isFinite(e)||e<=0)&&(r.apiMaxTokens=y.apiMaxTokens);let t=Number(r.maxDocChars);return(!Number.isFinite(t)||t<1e3)&&(r.maxDocChars=y.maxDocChars),r.maxCacheEntries=at(r.maxCacheEntries),z[r.promptLanguage]||(r.promptLanguage=y.promptLanguage),r.minCards=et(r.minCards,y.minCards),r.maxCards=et(r.maxCards,y.maxCards),r.maxCardstt(s)-tt(l)||i.localeCompare(o)).slice(0,t.length-e).map(([i])=>i);for(let i of a)delete r[i];return a}function le(r){let n=De(Object.assign({},y,r||{})),e=Fe(n.backend),t=v(n),a=A(n),i=e?(n.apiBaseUrl||t.baseUrl||T[a]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return _(Pe({cacheSchemaVersion:$,promptVersion:wt,maxDocChars:Number(n.maxDocChars)||y.maxDocChars,promptLanguage:n.promptLanguage,minCards:n.minCards,maxCards:n.maxCards,customSystemPromptHash:_(n.customSystemPrompt||""),backend:n.backend,model:n.model,apiProvider:e?n.apiProvider:"",apiFormat:e?a:"",apiBaseUrl:i,apiAuthType:e?ke(n):"",apiHeadersHash:e?_(n.apiHeaders||""):"",apiMaxTokens:e?Number(n.apiMaxTokens)||y.apiMaxTokens:0,structuredOutputVersion:1}))}function ce(r,n,e){return!!r&&r.schemaVersion===$&&r.contentHash===_(n)&&r.settingsHash===le(e)}function vt(r){return r==="en"?"Write title, gist, and bullets in English.":r==="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 xt(r){return r==="en"?`{"cards":[ +`)}function pe(r,t,e){if(!Number.isFinite(t)||t<=0)return-1;let n=Math.floor(t),a=e<0?-1:1;return r<0||r>=n?a<0?n-1:0:Math.min(n-1,Math.max(0,r+a))}function he(r,t){if(!Array.isArray(r)||t<0||t>=r.length)return-1;let e=Number(r[t]?.startLine);return Number.isFinite(e)&&e>=0?e:-1}function At(r){return r==="en"?"Write title, gist, and bullets in English.":r==="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 xt(r){return r==="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 At(r,n){return String(r||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(e,t)=>Object.hasOwn(n,t)?String(n[t]):e)}function Pt(r,n,e,t,a,i){return r==="en"?`You are a long-form reading summary assistant. After reading the full document, split it into ${n}-${e} 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. +]}`}function Pt(r,t){return String(r||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(e,n)=>Object.hasOwn(t,n)?String(t[n]):e)}function St(r,t,e,n,a,i){return r==="en"?`You are a long-form reading summary assistant. After reading the full document, split it into ${t}-${e} 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. Each card has one guiding sentence plus several bullets. Bullets carry details; gist is the lead-in. Language: -- ${t} +- ${n} For each unit, output: @@ -49,12 +49,12 @@ Output shape: ${a} Example: -${i}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${n}-${e} \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 +${i}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${t}-${e} \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 -- ${t} +- ${n} \u5BF9\u6BCF\u4E2A\u5355\u5143\u8F93\u51FA\uFF1A @@ -74,30 +74,30 @@ ${i}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\ ${a} \u793A\u4F8B\uFF1A -${i}`}function St(r,n,e,t,a){return r==="en"?`Non-overridable output contract: -- Must output ${n}-${e} cards. -- ${t} +${i}`}function Tt(r,t,e,n,a){return r==="en"?`Non-overridable output contract: +- Must output ${t}-${e} cards. +- ${n} - 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: ${a}`:`\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A -- \u5FC5\u987B\u8F93\u51FA ${n}-${e} \u5F20 cards\u3002 -- ${t} +- \u5FC5\u987B\u8F93\u51FA ${t}-${e} \u5F20 cards\u3002 +- ${n} - 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: ${a}`}function Me(r,n){let e=Number(n.maxDocChars)||Se,t=z[n.promptLanguage]?n.promptLanguage:y.promptLanguage,a=Math.max(1,Number(n.minCards)||y.minCards),i=Math.max(a,Number(n.maxCards)||y.maxCards),s=vt(t),o=r.length>e?r.slice(0,e)+(t==="en"?` +- JSON shape: ${a}`}function Ie(r,t){let e=Number(t.maxDocChars)||xe,n=G[t.promptLanguage]?t.promptLanguage:y.promptLanguage,a=Math.max(1,Number(t.minCards)||y.minCards),i=Math.max(a,Number(t.maxCards)||y.maxCards),s=At(n),o=r.length>e?r.slice(0,e)+(n==="en"?` [Document truncated]`:` -[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):r,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',c=xt(t),d={minCards:a,maxCards:i,languageInstruction:s,schema:l,example:c},m=At(n.customSystemPrompt,d).trim(),p=St(t,a,i,s,l),f=Pt(t,a,i,s,l,c),u=m?`${m} +[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):r,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',c=xt(n),d={minCards:a,maxCards:i,languageInstruction:s,schema:l,example:c},g=Pt(t.customSystemPrompt,d).trim(),p=Tt(n,a,i,s,l),f=St(n,a,i,s,l,c),u=g?`${g} -${p}`:f,C=t==="en"?`Source document: +${p}`:f,b=n==="en"?`Source document: ${o}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A -${o}`;return{system:u,user:C}}function Tt(r){return r.choices?.[0]?.delta?.content||""}function Et(r){return r.type==="content_block_delta"&&r.delta?.text||""}function B(r){switch(r){case"openai-chat":return Tt;case"anthropic-messages":return Et;default:return null}}function Ie(r,n){let e=[],t=r.split(` -`),a=t.pop();for(let i of t){let s=i.trim();if(!s||s==="data: [DONE]"||s.startsWith("event:")||!s.startsWith("data:"))continue;let o=s.slice(s.startsWith("data: ")?6:5);try{let l=JSON.parse(o),c=n(l);c&&e.push(c)}catch{}}return{deltas:e,rest:a}}async function Le(r,n,e,t,a,i,s){if(typeof globalThis.fetch!="function")throw new Error("Streaming requires fetch API");let o=await globalThis.fetch(r,{method:"POST",headers:n,body:JSON.stringify(e),signal:i});if(!o.ok){let p=await o.text();throw new Error(b(s||null,"errorProviderApiStatus",{label:"Streaming",status:o.status,excerpt:p.slice(0,500)}))}if(!o.body)throw new Error("Response has no body for streaming");let l=o.body.getReader(),c=new TextDecoder,d="",m="";try{for(;;){let{done:p,value:f}=await l.read();if(p)break;m+=c.decode(f,{stream:!0});let u=Ie(m,t);m=u.rest;for(let C of u.deltas)d+=C;u.deltas.length>0&&a?.({accumulated:d,done:!1})}}finally{l.releaseLock()}return a?.({accumulated:d,done:!0}),d}function Y(r,n){let e=r.replace(/\/+$/,"");for(let t of n)if(e.endsWith(t))return e;return e+n[0]}function Ft(r,n){let e=(r||"").trim();if(!e)return{};if(e.startsWith("{")){let a;try{a=JSON.parse(e)}catch(s){throw new Error(b(n||null,"errorCustomHeadersJsonParse",{error:s.message}))}if(!a||typeof a!="object"||Array.isArray(a))throw new Error(b(n||null,"errorCustomHeadersJsonObject"));let i={};for(let[s,o]of Object.entries(a))typeof o=="string"&&s.trim()&&(i[s.trim()]=o);return i}let t={};for(let a of e.split(/\r?\n/)){let i=a.trim();if(!i||i.startsWith("#"))continue;let s=i.indexOf(":");if(s<=0)throw new Error(b(n||null,"errorCustomHeadersLineFormat"));let o=i.slice(0,s).trim(),l=i.slice(s+1).trim();o&&(t[o]=l)}return t}function kt(r){let n=ke(r);if(n==="none")return{};let e=nt(r);if(!e){let t=(r.apiKeyEnvVar||v(r).envVar||"").trim(),a=t?b(r,"errorApiKeyEnvHint",{envVar:t}):"";throw new Error(b(r,"errorApiKeyMissing",{hint:a}))}return n==="bearer"?{authorization:`Bearer ${e}`}:n==="x-api-key"?{"x-api-key":e}:n==="x-goog-api-key"?{"x-goog-api-key":e}:n==="api-key"?{"api-key":e}:{authorization:`Bearer ${e}`}}function j(r,n){return{"content-type":"application/json",...kt(r),...n||{},...Ft(r.apiHeaders,r)}}function Nt(r,n,e){if(r.json&&typeof r.json=="object")return r.json;try{return JSON.parse(r.text||"{}")}catch{throw new Error(b(e||null,"errorProviderNonJson",{label:n,excerpt:(r.text||"").slice(0,500)}))}}async function it(r,n,e,t,a,i){let s;try{s=await r({url:e,method:"POST",headers:t,body:JSON.stringify(a),throw:!1})}catch(o){throw new Error(b(i||null,"errorProviderRequestFailed",{label:n,error:o.message||o}))}if(s.status>=400)throw new Error(b(i||null,"errorProviderApiStatus",{label:n,status:s.status,excerpt:(s.text||"").slice(0,500)}));return Nt(s,n,i)}function Dt(r){let n=String(r?.message?r.message:r);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(n)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(n):!1}async function pe(r,n,e,t,a,i,s){try{return await it(r,n,e,t,a,s)}catch(o){if(!i||!Dt(o))throw o;return console.warn(`[parallel-reader] ${n} structured output rejected; retrying without structured output`,o),it(r,n+" fallback",e,t,i,s)}}function Oe(r){return typeof r=="string"?r:Array.isArray(r)?r.map(n=>typeof n=="string"?n:n&&typeof n=="object"&&(n.text||n.output_text)||"").join(""):r&&typeof r=="object"&&(r.text||r.output_text)||""}function Rt(r){if(typeof r.output_text=="string")return r.output_text;let n=[],e=t=>{if(t&&typeof t!="string"){if(Array.isArray(t)){t.forEach(e);return}typeof t=="object"&&(typeof t.text=="string"&&n.push(t.text),typeof t.output_text=="string"&&n.push(t.output_text),t.type==="output_text"&&typeof t.content=="string"&&n.push(t.content),t.content&&e(t.content),t.output&&e(t.output))}};return e(r.output),n.join("")}function _e(r){let n=v(r),e=T[A(r)];return n.tokenLimitField||e?.tokenLimitField||"max_tokens"}function q(r,n,e,t){let a=!t||t.structured!==!1,i={model:I(e),max_tokens:Number(e.apiMaxTokens)||4096,system:r,messages:[{role:"user",content:n}]};return a&&(i.tools=[qe()],i.tool_choice={type:"tool",name:ae}),i}function W(r,n,e,t){let a=!t||t.structured!==!1,i={model:I(e),messages:[{role:"system",content:r},{role:"user",content:n}]};return i[_e(e)]=Number(e.apiMaxTokens)||4096,a&&(i.response_format=$e()),i}function ue(r,n,e,t){let a=!t||t.structured!==!1,i={model:I(e),instructions:r,input:n,max_output_tokens:Number(e.apiMaxTokens)||4096};return a&&(i.text=ze()),i}function de(r,n,e,t){let a=!t||t.structured!==!1,i={temperature:0,maxOutputTokens:Number(e.apiMaxTokens)||4096};return a&&(i.responseMimeType="application/json",i.responseJsonSchema=H(!1)),{systemInstruction:{parts:[{text:r}]},contents:[{role:"user",parts:[{text:n}]}],generationConfig:i}}function Mt(r,n){let t=(Array.isArray(r?.content)?r.content:[]).find(a=>a&&a.type==="tool_use"&&a.name===ae);return t?typeof t.input=="string"?S(t.input,n):t.input&&typeof t.input=="object"?G(t.input):[]:null}async function It(r,n,e,t){let a=Y(F(t),["/messages"]),i=await pe(r,"Anthropic-compatible",a,j(t,{"anthropic-version":"2023-06-01"}),q(n,e,t),q(n,e,t,{structured:!1}),t),s=Mt(i,t);if(s)return s;let o=(i.content||[]).map(l=>Oe(l)).join("").trim();return S(o,t)}async function Lt(r,n,e,t){let a=Y(F(t),["/chat/completions"]),s=((await pe(r,"OpenAI-compatible Chat",a,j(t),W(n,e,t),W(n,e,t,{structured:!1}),t)).choices||[])[0]||{},o=Oe(s.message?.content||s.text||"").trim();return S(o,t)}async function Ot(r,n,e,t){let a=Y(F(t),["/responses"]),i=await pe(r,"OpenAI Responses",a,j(t),ue(n,e,t),ue(n,e,t,{structured:!1}),t);return S(Rt(i).trim(),t)}async function _t(r,n,e,t){let a=encodeURIComponent(I(t)),i=F(t);/:generateContent(?:\?|$)/.test(i)||(i=`${i.replace(/\/+$/,"")}/models/${a}:generateContent`);let s=j(t),d=((((await pe(r,"Google Gemini",i,s,de(n,e,t),de(n,e,t,{structured:!1}),t)).candidates||[])[0]||{}).content?.parts||[]).map(m=>Oe(m)).join("").trim();return S(d,t)}async function he(r,n,e,t){switch(A(t)){case"openai-chat":return Lt(r,n,e,t);case"openai-responses":return Ot(r,n,e,t);case"google-generative-ai":return _t(r,n,e,t);default:return It(r,n,e,t)}}function Ve(r){if(!r.streaming)return!1;let n=A(r);return!!B(n)}async function Vt(r,n,e,t,a){let i=Y(F(e),["/chat/completions"]),s=j(e),o=W(r,n,e,{structured:!1});o.stream=!0;let l=B("openai-chat"),c=await Le(i,s,o,l,t,a,e);return S(c.trim(),e)}async function Bt(r,n,e,t,a){let i=Y(F(e),["/messages"]),s=j(e,{"anthropic-version":"2023-06-01"}),o=q(r,n,e,{structured:!1});o.stream=!0;let l=B("anthropic-messages"),c=await Le(i,s,o,l,t,a,e);return S(c.trim(),e)}async function st(r,n,e,t,a){let i=A(e);switch(i){case"openai-chat":return Vt(r,n,e,t,a);case"anthropic-messages":return Bt(r,n,e,t,a);default:throw new Error(`Streaming not supported for format: ${i}`)}}async function ot(r,n){await he(r,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',n);let e=A(n);return`${v(n).label} / ${T[e].label}`}function Be(r,n=80,e=.1){let t=Number(r?.top)||0,a=Math.max(0,Number(r?.height)||0),i=Math.min(Number(n)||0,a*e);return t+Math.max(0,i)}function jt(r){return typeof requestAnimationFrame=="function"?requestAnimationFrame(r):setTimeout(()=>r(Date.now()),16)}function Ut(r){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(r);return}clearTimeout(r)}function je(r,n=jt,e=Ut){let t=null,a=()=>{t===null&&(t=n(()=>{t=null,r()}))};return a.cancel=()=>{t!==null&&(e(t),t=null)},a}var g=require("obsidian");async function Gt(r){if(r.backend==="claude-code"){let n=M("claude",r.cliPath),{stdout:e}=await J(n,["--version"],"",1e4);return`claude @ ${n} -${e.trim()}`}if(r.backend==="codex"){let n=M("codex",r.cliPath),{stdout:e}=await J(n,["--version"],"",1e4);return`codex @ ${n} -${e.trim()}`}return ot(g.requestUrl,r)}var me=class extends g.PluginSettingTab{constructor(n,e){super(n,e),this.plugin=e}display(){let{containerEl:n}=this,e=(i,s)=>this.plugin.t(i,s);n.empty(),n.createEl("h2",{text:e("settingsTitle")}),new g.Setting(n).setName(e("settingUiLanguageName")).setDesc(e("settingUiLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(Te))i.addOption(s,o);return i.setValue(this.plugin.settings.uiLanguage||y.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBackendName")).setDesc(e("settingBackendDesc")).addDropdown(i=>i.addOption("api","API / Provider").addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&Ne(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let t=this.plugin.settings.backend==="claude-code"||this.plugin.settings.backend==="codex";if(t&&new g.Setting(n).setName(e("settingCliPathName")).setDesc(e("settingCliPathDesc")).addText(i=>i.setPlaceholder(e("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),t||n.createEl("h3",{text:e("apiProviderHeader")}),!t){let i=v(this.plugin.settings);new g.Setting(n).setName(e("settingProviderPresetName")).setDesc(e("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(k))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{Ne(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingApiFormatName")).setDesc(e("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(T))s.addOption(o,l.label);return s.setValue(A(this.plugin.settings)).onChange(async o=>{this.plugin.settings.apiFormat=o,await this.plugin.saveSettings(),this.display()})}),new g.Setting(n).setName(e("settingBaseUrlName")).setDesc(e("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":i.baseUrl||T[A(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingApiKeyName")).setDesc(e("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 g.Setting(n).setName(e("settingApiKeyEnvName")).setDesc(e("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(i.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingAuthTypeName")).setDesc(e("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(Ee))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 g.Setting(n).setName(e("settingHeadersName")).setDesc(e("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 g.Setting(n).setName(e("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!Number.isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingStreamingName")).setDesc(e("settingStreamingDesc")).addToggle(s=>s.setValue(this.plugin.settings.streaming??!0).onChange(async o=>{this.plugin.settings.streaming=o,await this.plugin.saveSettings()}))}new g.Setting(n).setName(e("settingModelName")).setDesc(e(t?"settingModelDescCli":"settingModelDescApi")).addText(i=>i.setPlaceholder(t?y.model:v(this.plugin.settings).model||"model-id").setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(t?y.model:""),this.plugin.saveSettingsDebounced()})),new g.Setting(n).setName(e("settingMaxInputName")).setDesc(e("settingMaxInputDesc")).addText(i=>i.setValue(String(this.plugin.settings.maxDocChars||y.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),n.createEl("h3",{text:e("promptHeader")}),new g.Setting(n).setName(e("settingPromptLanguageName")).setDesc(e("settingPromptLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(z))i.addOption(s,o);return i.setValue(this.plugin.settings.promptLanguage||y.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new g.Setting(n).setName(e("settingCardRangeName")).setDesc(e("settingCardRangeDesc")).addText(i=>i.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||y.minCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsi.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||y.maxCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||y.minCards),this.plugin.saveSettingsDebounced())})),new g.Setting(n).setName(e("settingCustomPromptName")).setDesc(e("settingCustomPromptDesc")).addTextArea(i=>(i.inputEl.rows=8,i.setPlaceholder(e("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new g.Setting(n).setName(e("settingTestBackendName")).setDesc(e(t?"settingTestBackendDescCli":"settingTestBackendDescApi")).addButton(i=>i.setButtonText("Test").onClick(async()=>{try{let s=await Gt(this.plugin.settings);new g.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new g.Notice(e("backendTestFailed",{error:s.message}),1e4)}})),new g.Setting(n).setName(e("settingExportFolderName")).setDesc(e("settingExportFolderDesc")).addText(i=>i.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||y.exportFolder,this.plugin.saveSettingsDebounced()})),n.createEl("h3",{text:e("cacheHeader")}),new g.Setting(n).setName(e("settingMaxCacheName")).setDesc(e("settingMaxCacheDesc")).addText(i=>{i.setValue(String(this.plugin.settings.maxCacheEntries||V));let s=async()=>{let o=parseInt(i.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 g.Notice(e("cachePruned",{count:l.length})),this.display()}};return i.inputEl.addEventListener("change",s),i.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&i.inputEl.blur()}),i});let a=Object.keys(this.plugin.cache).length;new g.Setting(n).setName(e("cachedNotesName",{count:a})).setDesc(e("cachedNotesDesc")).addButton(i=>i.setButtonText(e("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new g.Notice(e("cacheClearedAll",{count:s})),this.display()}))}};var E=require("obsidian");function N(r,n,e,t){let a=r.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":e}});return a.title=e,typeof E.setIcon=="function"?(0,E.setIcon)(a,n):a.textContent=e,a.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await t()}catch(s){console.error(s),new E.Notice(`${e} failed: `+(s.message||s))}}),a}function D(r,n,e,t,a){let i=r.createEl("button",{cls:a||"parallel-reader-text-button",attr:{type:"button"}});return n&&typeof E.setIcon=="function"&&(0,E.setIcon)(i,n),i.createSpan({text:e}),i.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await t()}catch(o){console.error(o),new E.Notice(`${e} failed: `+(o.message||o))}}),i}async function L(r,n){try{await navigator.clipboard.writeText(r),new E.Notice(n)}catch(e){new E.Notice("Copy failed: "+(e.message||e))}}function Ue(r){return String(r||"").split("/").map(n=>n.trim()).filter(n=>!!n&&n!==".."&&n!==".").join("/")}function Ge(r){let n=Ue(r);if(!n)return[];let e=n.split("/");return e.map((t,a)=>e.slice(0,a+1).join("/"))}async function lt(r,n){for(let e of Ge(n))if(!r.vault.getAbstractFileByPath(e))try{await r.vault.createFolder(e)}catch(t){if(!r.vault.getAbstractFileByPath(e))throw t}}var x=require("obsidian");var ct=require("obsidian");var ge=class extends ct.Modal{constructor(n,e,t,a){super(n),this.plugin=e,this.card=t||{},this.onSave=a}onOpen(){let{contentEl:n}=this;n.empty(),n.createEl("h2",{text:this.plugin.t("editCardTitle")});let e=this.createLabeledInput(n,this.plugin.t("editCardTitleField"),this.card.title||""),t=this.createLabeledTextarea(n,this.plugin.t("editCardGistField"),this.card.gist||"",3),a=this.createLabeledTextarea(n,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` -`),8),i=n.createDiv({cls:"parallel-reader-modal-actions"});D(i,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),D(i,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:e.value.trim()||this.card.title||"",gist:t.value.trim(),bullets:a.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(n,e,t){let a=n.createDiv({cls:"parallel-reader-modal-field"});a.createEl("label",{text:e});let i=a.createEl("input",{attr:{type:"text"}});return i.value=t,i}createLabeledTextarea(n,e,t,a){let i=n.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:e});let s=i.createEl("textarea");return s.rows=a,s.value=t,s}};var R="parallel-reader-view",fe=class extends x.ItemView{constructor(e,t){super(e);this.stale=!1;this.loadingMessage="";this.errorMessage="";this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1}getViewType(){return R}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,a){this.sourceFile=e,this.sections=t,this.stale=!!a,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()}renderStreamingPreview(e,t){this.sourceFile?.path!==e.path&&(this.sourceFile=e);let a=this.containerEl.children[1],i=a.querySelector(".parallel-reader-streaming-preview");if(i){let p=i.querySelector("pre");p&&(p.textContent=t.slice(-2e3));let f=i.querySelector(".parallel-reader-stream-counter");f&&(f.textContent=`${t.length} chars`);return}a.empty();let o=a.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});o.createEl("div",{text:e.basename,cls:"parallel-reader-title"});let l=o.createDiv({cls:"parallel-reader-actions"});N(l,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(e)});let c=a.createDiv({cls:"parallel-reader-state parallel-reader-loading parallel-reader-streaming-preview"});c.createDiv({cls:"parallel-reader-spinner"});let d=c.createEl("div",{cls:"parallel-reader-state-title"});d.createSpan({text:this.plugin.t("loadingGenerating")+" "}),d.createSpan({cls:"parallel-reader-stream-counter",text:`${t.length} chars`});let m=c.createEl("pre",{cls:"parallel-reader-stream-text"});m.textContent=t.slice(-2e3)}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 a=t.createDiv({cls:"parallel-reader-empty"});a.createEl("h3",{text:e.basename}),a.createEl("p",{text:this.plugin.t("emptyNoCache")}),a.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let a=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});a.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=a.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?N(i,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(this.sourceFile)}):N(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),N(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),N(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let l=e.createDiv({cls:"parallel-reader-stale-banner"});l.createSpan({text:this.plugin.t("staleBanner")}),D(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});l.createDiv({cls:"parallel-reader-spinner"}),l.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});l.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),D(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let s=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((l,c)=>{let d=s.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(c),l.startLine<0&&d.addClass("parallel-reader-card-unanchored");let m=d.createEl("div",{cls:"parallel-reader-card-title"});if(m.createSpan({text:l.title}),l.startLine<0&&m.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),l.gist){let f=d.createEl("div",{cls:"parallel-reader-gist"});x.MarkdownRenderer.render(this.app,l.gist,f,o,this).catch(()=>{f.setText(l.gist)})}let p=l.bullets||[];if(p.length>0){let f=d.createEl("div",{cls:"parallel-reader-bullets-md"}),u=p.map(C=>`- ${C}`).join(` -`);x.MarkdownRenderer.render(this.app,u,f,o,this).catch(()=>{f.setText(u)})}else l.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",f=>{let u=window.getSelection();if(u&&u.toString().length>0)return;let C=f.target;C&&C.tagName==="A"||l.startLine>=0&&this.plugin.scrollEditorToLine(l.startLine,this.sourceFile)}),d.addEventListener("contextmenu",f=>{f.preventDefault();let u=new x.Menu;u.addItem(C=>C.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>L(Ae(l),this.plugin.t("copiedMarkdown")))),u.addItem(C=>C.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>L(Qe(l),this.plugin.t("copiedPlain")))),l.anchor&&u.addItem(C=>C.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>L(l.anchor,this.plugin.t("copiedAnchor")))),u.addSeparator(),l.startLine>=0&&u.addItem(C=>C.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(l.startLine,this.sourceFile))),u.addSeparator(),u.addItem(C=>C.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(c))),u.addItem(C=>C.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(c))),u.showAtMouseEvent(f)}),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=se(this.activeIdx,this.sections.length,e);return this.setActiveSection(t),this.focusSummaryPane(),t}jumpToActiveSection(){let e=oe(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=ee(this.sections,e);if(t.length===this.sections.length)return!1;let a=this.sections.length;return this.sections=t,this.activeIdx=te(e,a,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,t),this.render(),new x.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new ge(this.app,this.plugin,this.sections[e],async t=>{await this.updateCard(e,t)}).open(),!0)}async updateCard(e,t){if(!this.sourceFile)return!1;let a=re(this.sections,e,t);return a.length!==this.sections.length?!1:(this.sections=a,await this.plugin.cacheReplaceCards(this.sourceFile.path,a),this.render(),new x.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=Ue(this.plugin.settings.exportFolder),t=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,a=`${e}/${t}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",K(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(` -`),s=this.plugin.app;await lt(s,e);let o=s.vault.getAbstractFileByPath(a);o instanceof x.TFile?await s.vault.modify(o,i):await s.vault.create(a,i),new x.Notice(this.plugin.t("exported",{path:a}))}};async function Ht(r,n,e,t){let{system:a,user:i}=Me(r,n),s;switch(n.backend){case"claude-code":s=await Xe(a,i,n,e);break;case"codex":s=await Ze(a,i,n,e);break;default:{let l=Ve(n),c=l?new AbortController:null;c&&e.onCancel(()=>c.abort()),l?s=await st(a,i,n,t,c.signal):s=await he(h.requestUrl,a,i,n);break}}let o=s.map(l=>({title:l.title,level:2,anchor:l.anchor,gist:l.gist,startLine:Q(r,l.anchor),bullets:l.bullets}));return o.sort((l,c)=>l.startLine<0&&c.startLine<0?0:l.startLine<0?1:c.startLine<0?-1:l.startLine-c.startLine),o}function ut(r,n){return n?.phase==="generating"&&r&&Fe(r.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var He=class extends h.Plugin{constructor(){super(...arguments);this._scrollDispose=null;this._settingsSaveTimer=null;this._cacheSaveTimer=null;this._cacheDirty=!1}t(e,t){return b(this.settings||y,e,t)}async onload(){await this.loadSettings(),this.jobs=new U,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(R,e=>new fe(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 h.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new h.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 h.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 me(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(R)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=De(Object.assign({},y,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",a=this.manifest?.id||"parallel-reader",i=`${t}/plugins/${a}`;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()),a=JSON.parse(t);if(a&&typeof a=="object"&&a.entries&&typeof a.entries=="object")return a.entries}catch(t){let a=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(a)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),Ce(this.cache))}async loadCache(){this.cache=await this.readCacheFile(),this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return Re(this.cache,this.settings?.maxCacheEntries||V)}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=ye(this.cache[e]||null);return t?(this.scheduleCacheSave(),t):null}async cachePut(e,t,a,i){let s=new Date().toISOString();this.cache[e]={schemaVersion:$,contentHash:_(t),settingsHash:le(i||this.settings),cards:a,generatedAt:s,lastAccessedAt:s},await this.saveCache()}async cacheReplaceCards(e,t){let a=this.cache[e];if(!a)return!1;let i=new Date().toISOString();return a.cards=(t||[]).map(s=>({title:s.title,anchor:s.anchor,gist:s.gist,bullets:s.bullets||[]})),a.updatedAt=i,a.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(R)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:R,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(h.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(R)[0]?.view}moveActiveCard(e){let t=this.getParallelView();return t?.sections.length?t.moveActiveSection(e):(new h.Notice(this.t("noActiveCard")),-1)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new h.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e?.path)return new h.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.get(e.path),a=ut(this.settings,t),i=this.jobs.cancel(e.path);return new h.Notice(i?this.t(a):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.getParallelView();t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new h.Notice(this.t("noCancelableJob"))}confirmRegenerateEditedCards(){let e=this.t("confirmRegenerateEditedCards");return typeof window<"u"&&typeof window.confirm=="function"?window.confirm(e):!0}addFileMenuItems(e,t){!(t instanceof h.TFile)||!t.path.endsWith(".md")||(e.addSeparator(),e.addItem(a=>a.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(t,!1))),e.addItem(a=>a.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(t,!0))),this.cacheGet(t.path)&&e.addItem(a=>a.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheDelete(t.path),new h.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof h.TFile)||!t)return;let a=t.endsWith(".md"),i=e.path.endsWith(".md");if(!a&&!i)return;if(a&&!i){this.cache[t]&&(delete this.cache[t],await this.saveCache());let o=this.getParallelView();o&&o.sourceFile?.path===t&&o.renderEmpty();return}if(!a)return;this.cache[t]&&(this.cache[e.path]=this.cache[t],delete this.cache[t],await this.saveCache());let s=this.getParallelView();s?.sourceFile&&(s.sourceFile.path===t||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof h.TFile))return;this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let t=this.getParallelView();t?.sourceFile?.path===e.path&&t.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 h.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 h.Notice(this.t("noCopyContent"));return}await L(K(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t?.file){new h.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new h.Notice(this.t("openNoteFirst"));return}if(this.jobs.isRunning(e.path)){new h.Notice(this.t("alreadyGenerating"));return}if(be(this.cacheGet(e.path),t)&&!this.confirmRegenerateEditedCards()){new h.Notice(this.t("regenerateCancelled"));return}let a=null;return this.jobs.start(e.path,async i=>{i.setPhase("reading");let s=await this.app.vault.read(e);if(i.throwIfCancelled(),!s.trim()){new h.Notice(this.t("emptyNote"));return}if(a=await this.ensureView(),i.throwIfCancelled(),i.setPhase("cache-check"),!t){let p=this.cacheGet(e.path);if(ce(p,s,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await a.loadFor(e,this.resolveCardAnchors(s,p.cards),!1);return}}await a.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||y.maxDocChars;s.length>o&&new h.Notice(this.t("longNoteTruncated",{count:o})),new h.Notice(this.t("generatingNotice")),i.setPhase("generating");let l=a?p=>{!p.done&&this.viewIsShowingFile(a,e)&&a.renderStreamingPreview(e,p.accumulated)}:void 0,c=await Ht(s,this.settings,i,l);if(i.throwIfCancelled(),c.length===0){new h.Notice(this.t("noCardsReturned"));return}let d=c.map(p=>({title:p.title,anchor:p.anchor,gist:p.gist,bullets:p.bullets}));i.setPhase("saving"),await this.cachePut(e.path,s,d,this.settings),i.throwIfCancelled(),this.viewIsShowingFile(a,e)&&await a.loadFor(e,c,!1);let m=c.filter(p=>p.startLine<0).length;new h.Notice(this.t("generationDone",{count:c.length,suffix:m?this.t("unanchoredSuffix",{count:m}):""}))}).catch(async i=>{if(i instanceof O){new h.Notice(i.message);return}if(i instanceof P){a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,this.t("cancelledError")),new h.Notice(this.t("cancelled"));return}let s=ve(i);console.error(i),a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,i.message||String(i)),new h.Notice(this.t("generationFailed",{kind:s==="unknown"?"":` (${s})`,error:i.message||i}))})}resolveCardAnchors(e,t){let a=(t||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:Q(e,i.anchor),bullets:i.bullets||[]}));return a.sort((i,s)=>i.startLine<0&&s.startLine<0?0:i.startLine<0?1:s.startLine<0?-1:i.startLine-s.startLine),a}async syncViewToFile(e){if(!e?.path?.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(R);if(t.length===0)return;let a=t[0].view;if(!a||!this.activeFileStillMatches(e))return;let i=this.cacheGet(e.path);if(!i){await a.loadFor(e,[],!1),a.renderEmptyWithHint(e);return}let s=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!ce(i,s,this.settings);await this.cacheTouch(e.path),await a.loadFor(e,this.resolveCardAnchors(s,i.cards),o)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let t=e.editor,a=t&&t.cm,i=a?.scrollDOM?a.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let s=je(()=>this.handleEditorScroll(e));i.addEventListener("scroll",s,{passive:!0}),this._scrollDispose=()=>{s.cancel(),i.removeEventListener("scroll",s)}}handleEditorScroll(e){let t=this.getParallelView();if(!t||!e.file||t.sourceFile?.path!==e.file.path)return;let a=e.editor,i=a&&a.cm;if(!i?.scrollDOM)return;let s=i.scrollDOM.getBoundingClientRect(),o=Be(s),l=0;try{let d=i.posAtCoords({x:s.left+20,y:o});d!=null&&(l=i.state.doc.lineAt(d).number-1)}catch{return}let c=-1;for(let d=0;d0&&a?.({accumulated:d,done:!1})}}finally{l.releaseLock()}return a?.({accumulated:d,done:!0}),d}async function Oe(r,t,e,n,a,i,s){if(typeof globalThis.fetch!="function")throw new Error("Streaming requires fetch API");let o=s?.streamingTimeoutMs??12e4,l=new AbortController;i&&(i.aborted?l.abort():i.addEventListener("abort",()=>l.abort(),{once:!0}));let c=null,d=new Promise((g,p)=>{c=setTimeout(()=>{l.abort(),p(new Error(`Streaming timed out after ${o}ms`))},o)});try{return await Promise.race([Ft(r,t,e,n,a,l.signal,s),d])}finally{c!==null&&clearTimeout(c)}}function Z(r,t){let e=r.replace(/\/+$/,"");for(let n of t)if(e.endsWith(n))return e;return e+t[0]}function Mt(r,t){let e=(r||"").trim();if(!e)return{};if(e.startsWith("{")){let a;try{a=JSON.parse(e)}catch(s){throw new Error(w(t||null,"errorCustomHeadersJsonParse",{error:s.message}))}if(!a||typeof a!="object"||Array.isArray(a))throw new Error(w(t||null,"errorCustomHeadersJsonObject"));let i={};for(let[s,o]of Object.entries(a))typeof o=="string"&&s.trim()&&(i[s.trim()]=o);return i}let n={};for(let a of e.split(/\r?\n/)){let i=a.trim();if(!i||i.startsWith("#"))continue;let s=i.indexOf(":");if(s<=0)throw new Error(w(t||null,"errorCustomHeadersLineFormat"));let o=i.slice(0,s).trim(),l=i.slice(s+1).trim();o&&(n[o]=l)}return n}function Rt(r){let t=Ee(r);if(t==="none")return{};let e=Ye(r);if(!e){let n=(r.apiKeyEnvVar||v(r).envVar||"").trim(),a=n?w(r,"errorApiKeyEnvHint",{envVar:n}):"";throw new Error(w(r,"errorApiKeyMissing",{hint:a}))}return t==="bearer"?{authorization:`Bearer ${e}`}:t==="x-api-key"?{"x-api-key":e}:t==="x-goog-api-key"?{"x-goog-api-key":e}:t==="api-key"?{"api-key":e}:{authorization:`Bearer ${e}`}}function j(r,t){return{"content-type":"application/json",...Rt(r),...t||{},...Mt(r.apiHeaders,r)}}function Nt(r,t,e){if(r.json&&typeof r.json=="object")return r.json;try{return JSON.parse(r.text||"{}")}catch{throw new Error(w(e||null,"errorProviderNonJson",{label:t,excerpt:(r.text||"").slice(0,500)}))}}async function st(r,t,e,n,a,i){let s;try{s=await r({url:e,method:"POST",headers:n,body:JSON.stringify(a),throw:!1})}catch(o){throw new Error(w(i||null,"errorProviderRequestFailed",{label:t,error:o.message||String(o)}))}if(s.status>=400)throw new Error(w(i||null,"errorProviderApiStatus",{label:t,status:s.status,excerpt:(s.text||"").slice(0,500)}));return Nt(s,t,i)}function Dt(r){let t=String(r?.message??r);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(t)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(t):!1}async function fe(r,t,e,n,a,i,s){try{return await st(r,t,e,n,a,s)}catch(o){if(!i||!Dt(o))throw o;return console.warn(`[parallel-reader] ${t} structured output rejected; retrying without structured output`,o),st(r,t+" fallback",e,n,i,s)}}function _e(r){if(typeof r=="string")return r;if(Array.isArray(r))return r.map(t=>{if(typeof t=="string")return t;if(t&&typeof t=="object"){let e=t;return typeof e.text=="string"?e.text:typeof e.output_text=="string"?e.output_text:""}return""}).join("");if(r&&typeof r=="object"){let t=r;return typeof t.text=="string"?t.text:typeof t.output_text=="string"?t.output_text:""}return""}function It(r){if(typeof r.output_text=="string")return r.output_text;let t=[],e=n=>{if(n&&typeof n!="string"){if(Array.isArray(n)){n.forEach(e);return}if(typeof n=="object"){let a=n;typeof a.text=="string"&&t.push(a.text),typeof a.output_text=="string"&&t.push(a.output_text),a.type==="output_text"&&typeof a.content=="string"&&t.push(a.content),a.content&&e(a.content),a.output&&e(a.output)}}};return e(r.output),t.join("")}function Ve(r){let t=v(r),e=P[x(r)];return t.tokenLimitField||e?.tokenLimitField||"max_tokens"}function Y(r,t,e,n){let a=!n||n.structured!==!1,i={model:D(e),max_tokens:Number(e.apiMaxTokens)||4096,system:r,messages:[{role:"user",content:t}]};return a&&(i.tools=[et()],i.tool_choice={type:"tool",name:ue}),i}function X(r,t,e,n){let a=!n||n.structured!==!1,i={model:D(e),messages:[{role:"system",content:r},{role:"user",content:t}]};return i[Ve(e)]=Number(e.apiMaxTokens)||4096,a&&(i.response_format=Ze()),i}function ge(r,t,e,n){let a=!n||n.structured!==!1,i={model:D(e),instructions:r,input:t,max_output_tokens:Number(e.apiMaxTokens)||4096};return a&&(i.text=Qe()),i}function me(r,t,e,n){let a=!n||n.structured!==!1,i={temperature:0,maxOutputTokens:Number(e.apiMaxTokens)||4096};return a&&(i.responseMimeType="application/json",i.responseJsonSchema=q(!1)),{systemInstruction:{parts:[{text:r}]},contents:[{role:"user",parts:[{text:t}]}],generationConfig:i}}function Lt(r,t){let n=(Array.isArray(r?.content)?r.content:[]).find(a=>a&&a.type==="tool_use"&&a.name===ue);return n?typeof n.input=="string"?T(n.input,t):n.input&&typeof n.input=="object"?$(n.input):[]:null}async function Ot(r,t,e,n){let a=Z(k(n),["/messages"]),i=await fe(r,"Anthropic-compatible",a,j(n,{"anthropic-version":"2023-06-01"}),Y(t,e,n),Y(t,e,n,{structured:!1}),n),s=Lt(i,n);if(s)return s;let l=(Array.isArray(i.content)?i.content:[]).map(c=>_e(c)).join("").trim();return T(l,n)}async function _t(r,t,e,n){let a=Z(k(n),["/chat/completions"]),i=await fe(r,"OpenAI-compatible Chat",a,j(n),X(t,e,n),X(t,e,n,{structured:!1}),n),o=(Array.isArray(i.choices)?i.choices:[])[0]||{},l=o.message,c=_e(l?.content??o.text??"").trim();return T(c,n)}async function Vt(r,t,e,n){let a=Z(k(n),["/responses"]),i=await fe(r,"OpenAI Responses",a,j(n),ge(t,e,n),ge(t,e,n,{structured:!1}),n);return T(It(i).trim(),n)}async function Bt(r,t,e,n){let a=encodeURIComponent(D(n)),i=k(n);/:generateContent(?:\?|$)/.test(i)||(i=`${i.replace(/\/+$/,"")}/models/${a}:generateContent`);let s=j(n),o=await fe(r,"Google Gemini",i,s,me(t,e,n),me(t,e,n,{structured:!1}),n),d=((Array.isArray(o.candidates)?o.candidates:[])[0]||{}).content,p=(Array.isArray(d?.parts)?d.parts:[]).map(f=>_e(f)).join("").trim();return T(p,n)}async function ye(r,t,e,n){switch(x(n)){case"openai-chat":return _t(r,t,e,n);case"openai-responses":return Vt(r,t,e,n);case"google-generative-ai":return Bt(r,t,e,n);default:return Ot(r,t,e,n)}}function Be(r){if(!r.streaming)return!1;let t=x(r);return!!U(t)}async function Ut(r,t,e,n,a){let i=Z(k(e),["/chat/completions"]),s=j(e),o=X(r,t,e,{structured:!1});o.stream=!0;let l=U("openai-chat"),c=await Oe(i,s,o,l,n,a,e);return T(c.trim(),e)}async function jt(r,t,e,n,a){let i=Z(k(e),["/messages"]),s=j(e,{"anthropic-version":"2023-06-01"}),o=Y(r,t,e,{structured:!1});o.stream=!0;let l=U("anthropic-messages"),c=await Oe(i,s,o,l,n,a,e);return T(c.trim(),e)}async function ot(r,t,e,n,a){let i=x(e);switch(i){case"openai-chat":return Ut(r,t,e,n,a);case"anthropic-messages":return jt(r,t,e,n,a);default:throw new Error(`Streaming not supported for format: ${i}`)}}async function lt(r,t){await ye(r,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',t);let e=x(t);return`${v(t).label} / ${P[e].label}`}function Ue(r,t=80,e=.1){let n=Number(r?.top)||0,a=Math.max(0,Number(r?.height)||0),i=Math.min(Number(t)||0,a*e);return n+Math.max(0,i)}function Gt(r){return typeof requestAnimationFrame=="function"?requestAnimationFrame(r):setTimeout(()=>r(Date.now()),16)}function Ht(r){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(r);return}clearTimeout(r)}function je(r,t=Gt,e=Ht){let n=null,a=()=>{n===null&&(n=t(()=>{n=null,r()}))};return a.cancel=()=>{n!==null&&(e(n),n=null)},a}var m=require("obsidian");async function Jt(r){if(r.backend==="claude-code"){let t=I("claude",r.cliPath),{stdout:e}=await z(t,["--version"],"",1e4);return`claude @ ${t} +${e.trim()}`}if(r.backend==="codex"){let t=I("codex",r.cliPath),{stdout:e}=await z(t,["--version"],"",1e4);return`codex @ ${t} +${e.trim()}`}return lt(m.requestUrl,r)}var be=class extends m.PluginSettingTab{constructor(t,e){super(t,e),this.plugin=e}display(){let{containerEl:t}=this,e=(i,s)=>this.plugin.t(i,s);t.empty(),t.createEl("h2",{text:e("settingsTitle")}),new m.Setting(t).setName(e("settingUiLanguageName")).setDesc(e("settingUiLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(Pe))i.addOption(s,o);return i.setValue(this.plugin.settings.uiLanguage||y.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new m.Setting(t).setName(e("settingBackendName")).setDesc(e("settingBackendDesc")).addDropdown(i=>i.addOption("api","API / Provider").addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&ke(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let n=this.plugin.settings.backend==="claude-code"||this.plugin.settings.backend==="codex";if(n&&new m.Setting(t).setName(e("settingCliPathName")).setDesc(e("settingCliPathDesc")).addText(i=>i.setPlaceholder(e("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),n||t.createEl("h3",{text:e("apiProviderHeader")}),!n){let i=v(this.plugin.settings);new m.Setting(t).setName(e("settingProviderPresetName")).setDesc(e("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(F))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{ke(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new m.Setting(t).setName(e("settingApiFormatName")).setDesc(e("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(P))s.addOption(o,l.label);return s.setValue(x(this.plugin.settings)).onChange(async o=>{this.plugin.settings.apiFormat=o,await this.plugin.saveSettings(),this.display()})}),new m.Setting(t).setName(e("settingBaseUrlName")).setDesc(e("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":i.baseUrl||P[x(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new m.Setting(t).setName(e("settingApiKeyName")).setDesc(e("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 m.Setting(t).setName(e("settingApiKeyEnvName")).setDesc(e("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(i.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new m.Setting(t).setName(e("settingAuthTypeName")).setDesc(e("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(Se))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 m.Setting(t).setName(e("settingHeadersName")).setDesc(e("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 m.Setting(t).setName(e("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!Number.isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())})),new m.Setting(t).setName(e("settingStreamingName")).setDesc(e("settingStreamingDesc")).addToggle(s=>s.setValue(this.plugin.settings.streaming??!0).onChange(async o=>{this.plugin.settings.streaming=o,await this.plugin.saveSettings()}))}new m.Setting(t).setName(e("settingModelName")).setDesc(e(n?"settingModelDescCli":"settingModelDescApi")).addText(i=>i.setPlaceholder(n?y.model:v(this.plugin.settings).model||"model-id").setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(n?y.model:""),this.plugin.saveSettingsDebounced()})),new m.Setting(t).setName(e("settingMaxInputName")).setDesc(e("settingMaxInputDesc")).addText(i=>i.setValue(String(this.plugin.settings.maxDocChars||y.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),t.createEl("h3",{text:e("promptHeader")}),new m.Setting(t).setName(e("settingPromptLanguageName")).setDesc(e("settingPromptLanguageDesc")).addDropdown(i=>{for(let[s,o]of Object.entries(G))i.addOption(s,o);return i.setValue(this.plugin.settings.promptLanguage||y.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new m.Setting(t).setName(e("settingCardRangeName")).setDesc(e("settingCardRangeDesc")).addText(i=>i.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||y.minCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsi.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||y.maxCards)).onChange(async s=>{let o=parseInt(s,10);!Number.isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||y.minCards),this.plugin.saveSettingsDebounced())})),new m.Setting(t).setName(e("settingCustomPromptName")).setDesc(e("settingCustomPromptDesc")).addTextArea(i=>(i.inputEl.rows=8,i.setPlaceholder(e("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new m.Setting(t).setName(e("settingTestBackendName")).setDesc(e(n?"settingTestBackendDescCli":"settingTestBackendDescApi")).addButton(i=>i.setButtonText("Test").onClick(async()=>{try{let s=await Jt(this.plugin.settings);new m.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new m.Notice(e("backendTestFailed",{error:s.message}),1e4)}})),new m.Setting(t).setName(e("settingExportFolderName")).setDesc(e("settingExportFolderDesc")).addText(i=>i.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||y.exportFolder,this.plugin.saveSettingsDebounced()})),t.createEl("h3",{text:e("cacheHeader")}),new m.Setting(t).setName(e("settingMaxCacheName")).setDesc(e("settingMaxCacheDesc")).addText(i=>{i.setValue(String(this.plugin.settings.maxCacheEntries||V));let s=async()=>{let o=parseInt(i.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 m.Notice(e("cachePruned",{count:l.length})),this.display()}};return i.inputEl.addEventListener("change",s),i.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&i.inputEl.blur()}),i});let a=Object.keys(this.plugin.cache).length;new m.Setting(t).setName(e("cachedNotesName",{count:a})).setDesc(e("cachedNotesDesc")).addButton(i=>i.setButtonText(e("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new m.Notice(e("cacheClearedAll",{count:s})),this.display()}))}};var E=require("obsidian");function M(r,t,e,n){let a=r.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":e}});return a.title=e,typeof E.setIcon=="function"?(0,E.setIcon)(a,t):a.textContent=e,a.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await n()}catch(s){console.error(s),new E.Notice(`${e} failed: `+(s.message||s))}}),a}function R(r,t,e,n,a){let i=r.createEl("button",{cls:a||"parallel-reader-text-button",attr:{type:"button"}});return t&&typeof E.setIcon=="function"&&(0,E.setIcon)(i,t),i.createSpan({text:e}),i.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await n()}catch(o){console.error(o),new E.Notice(`${e} failed: `+(o.message||o))}}),i}async function L(r,t){try{await navigator.clipboard.writeText(r),new E.Notice(t)}catch(e){new E.Notice("Copy failed: "+(e.message||e))}}function Ge(r){return String(r||"").split("/").map(t=>t.trim()).filter(t=>!!t&&t!==".."&&t!==".").join("/")}function He(r){let t=Ge(r);if(!t)return[];let e=t.split("/");return e.map((n,a)=>e.slice(0,a+1).join("/"))}async function ct(r,t){for(let e of He(t))if(!r.vault.getAbstractFileByPath(e))try{await r.vault.createFolder(e)}catch(n){if(!r.vault.getAbstractFileByPath(e))throw n}}var A=require("obsidian");var ut=require("obsidian");var we=class extends ut.Modal{constructor(t,e,n,a){super(t),this.plugin=e,this.card=n||{},this.onSave=a}onOpen(){let{contentEl:t}=this;t.empty(),t.createEl("h2",{text:this.plugin.t("editCardTitle")});let e=this.createLabeledInput(t,this.plugin.t("editCardTitleField"),this.card.title||""),n=this.createLabeledTextarea(t,this.plugin.t("editCardGistField"),this.card.gist||"",3),a=this.createLabeledTextarea(t,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` +`),8),i=t.createDiv({cls:"parallel-reader-modal-actions"});R(i,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),R(i,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:e.value.trim()||this.card.title||"",gist:n.value.trim(),bullets:a.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(t,e,n){let a=t.createDiv({cls:"parallel-reader-modal-field"});a.createEl("label",{text:e});let i=a.createEl("input",{attr:{type:"text"}});return i.value=n,i}createLabeledTextarea(t,e,n,a){let i=t.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:e});let s=i.createEl("textarea");return s.rows=a,s.value=n,s}};var N="parallel-reader-view",Ce=class extends A.ItemView{constructor(e,n){super(e);this.stale=!1;this.loadingMessage="";this.errorMessage="";this.plugin=n,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1}getViewType(){return N}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",n=>this.handleKeydown(n)),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 n=e.createDiv({cls:"parallel-reader-empty"});n.createEl("h3",{text:this.plugin.t("appTitle")}),n.createEl("p",{text:this.plugin.t("emptyOpenNote")}),n.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,n,a){this.sourceFile=e,this.sections=n,this.stale=!!a,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,n){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=n||this.plugin.t("loadingDefault"),this.errorMessage="",this.render()}renderStreamingPreview(e,n){this.sourceFile?.path!==e.path&&(this.sourceFile=e);let a=this.containerEl.children[1],i=a.querySelector(".parallel-reader-streaming-preview");if(i){let p=i.querySelector("pre");p&&(p.textContent=n.slice(-2e3));let f=i.querySelector(".parallel-reader-stream-counter");f&&(f.textContent=`${n.length} chars`);return}a.empty();let o=a.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});o.createEl("div",{text:e.basename,cls:"parallel-reader-title"});let l=o.createDiv({cls:"parallel-reader-actions"});M(l,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(e)});let c=a.createDiv({cls:"parallel-reader-state parallel-reader-loading parallel-reader-streaming-preview"});c.createDiv({cls:"parallel-reader-spinner"});let d=c.createEl("div",{cls:"parallel-reader-state-title"});d.createSpan({text:this.plugin.t("loadingGenerating")+" "}),d.createSpan({cls:"parallel-reader-stream-counter",text:`${n.length} chars`});let g=c.createEl("pre",{cls:"parallel-reader-stream-text"});g.textContent=n.slice(-2e3)}async renderError(e,n){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=n||this.plugin.t("errorTitle"),this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let n=this.containerEl.children[1];n.empty();let a=n.createDiv({cls:"parallel-reader-empty"});a.createEl("h3",{text:e.basename}),a.createEl("p",{text:this.plugin.t("emptyNoCache")}),a.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let a=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});a.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=a.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?M(i,"square",this.plugin.t("actionCancel"),()=>{this.plugin.cancelGenerationForFile(this.sourceFile)}):M(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),M(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),M(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let l=e.createDiv({cls:"parallel-reader-stale-banner"});l.createSpan({text:this.plugin.t("staleBanner")}),R(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});l.createDiv({cls:"parallel-reader-spinner"}),l.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let l=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});l.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),l.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),R(l,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let s=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((l,c)=>{let d=s.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(c),l.startLine<0&&d.addClass("parallel-reader-card-unanchored");let g=d.createEl("div",{cls:"parallel-reader-card-title"});if(g.createSpan({text:l.title}),l.startLine<0&&g.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),l.gist){let f=d.createEl("div",{cls:"parallel-reader-gist"});A.MarkdownRenderer.render(this.app,l.gist,f,o,this).catch(()=>{f.setText(l.gist)})}let p=l.bullets||[];if(p.length>0){let f=d.createEl("div",{cls:"parallel-reader-bullets-md"}),u=p.map(b=>`- ${b}`).join(` +`);A.MarkdownRenderer.render(this.app,u,f,o,this).catch(()=>{f.setText(u)})}else l.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",f=>{let u=window.getSelection();if(u&&u.toString().length>0)return;let b=f.target;b&&b.tagName==="A"||l.startLine>=0&&this.plugin.scrollEditorToLine(l.startLine,this.sourceFile)}),d.addEventListener("contextmenu",f=>{f.preventDefault();let u=new A.Menu;u.addItem(b=>b.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>L(De(l),this.plugin.t("copiedMarkdown")))),u.addItem(b=>b.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>L(it(l),this.plugin.t("copiedPlain")))),l.anchor&&u.addItem(b=>b.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>L(l.anchor,this.plugin.t("copiedAnchor")))),u.addSeparator(),l.startLine>=0&&u.addItem(b=>b.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(l.startLine,this.sourceFile))),u.addSeparator(),u.addItem(b=>b.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(c))),u.addItem(b=>b.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(c))),u.showAtMouseEvent(f)}),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 n=pe(this.activeIdx,this.sections.length,e);return this.setActiveSection(n),this.focusSummaryPane(),n}jumpToActiveSection(){let e=he(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 n=oe(this.sections,e);if(n.length===this.sections.length)return!1;let a=this.sections.length;return this.sections=n,this.activeIdx=le(e,a,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,n),this.render(),new A.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new we(this.app,this.plugin,this.sections[e],async n=>{await this.updateCard(e,n)}).open(),!0)}async updateCard(e,n){if(!this.sourceFile)return!1;let a=ce(this.sections,e,n);return a.length!==this.sections.length?!1:(this.sections=a,await this.plugin.cacheReplaceCards(this.sourceFile.path,a),this.render(),new A.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=Ge(this.plugin.settings.exportFolder),n=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,a=`${e}/${n}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",W(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(` +`),s=this.plugin.app;await ct(s,e);let o=s.vault.getAbstractFileByPath(a);o instanceof A.TFile?await s.vault.modify(o,i):await s.vault.create(a,i),new A.Notice(this.plugin.t("exported",{path:a}))}};async function Kt(r,t,e,n){let{system:a,user:i}=Ie(r,t),s;switch(t.backend){case"claude-code":s=await nt(a,i,t,e);break;case"codex":s=await at(a,i,t,e);break;default:{let l=Be(t),c=l?new AbortController:null;c&&e.onCancel(()=>c.abort()),l?s=await ot(a,i,t,n,c.signal):s=await ye(h.requestUrl,a,i,t);break}}let o=s.map(l=>({title:l.title,level:2,anchor:l.anchor,gist:l.gist,startLine:te(r,l.anchor),bullets:l.bullets}));return o.sort((l,c)=>l.startLine<0&&c.startLine<0?0:l.startLine<0?1:c.startLine<0?-1:l.startLine-c.startLine),o}function dt(r,t){return t?.phase==="generating"&&r&&Te(r.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var Je=class extends h.Plugin{constructor(){super(...arguments);this._scrollDispose=null;this._settingsSaveTimer=null}get cache(){return this.cacheManager.cache}t(e,n){return w(this.settings||y,e,n)}async onload(){await this.loadSettings(),this.jobs=new K,this.addRibbonIcon("book-open",this.t("ribbonOpen"),async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(N,e=>new Ce(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 h.Notice(this.t("noCurrentNote"));await this.cacheManager.delete(e.file.path),new h.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.cacheManager.cache).length;await this.cacheManager.clear(),new h.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.addCommand({id:"parallel-reader-batch-generate",name:this.t("cmdBatchGenerate"),callback:()=>this.runBatchForFolder()}),this.addSettingTab(new be(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,n)=>this.addFileMenuItems(e,n))),this.registerEvent(this.app.vault.on("rename",(e,n)=>this.handleFileRename(e,n))),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(N)}async loadSettings(){let n=(await this.loadData()||{}).settings||{};this.settings=Fe(Object.assign({},y,n)),this.cacheManager=new se(this.app.vault.adapter,this.app.vault.configDir||".obsidian",this.manifest?.id||"parallel-reader",()=>this.settings),await this.cacheManager.load()}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(n=>console.error("[parallel-reader] failed to save settings",n))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async cacheTouch(e){return this.cacheManager.touch(e)}scheduleCacheSave(e=5e3){this.cacheManager.scheduleSave(e)}async flushCacheSave(){await this.cacheManager.flush()}async cacheReplaceCards(e,n){return this.cacheManager.replaceCards(e,n)}async cacheClear(){return this.cacheManager.clear()}async pruneCacheIfNeeded(){return this.cacheManager.pruneIfNeeded()}async ensureView(){let{workspace:e}=this.app,n=e.getLeavesOfType(N)[0];return n||(n=e.getRightLeaf(!1),await n.setViewState({type:N,active:!0})),e.revealLeaf(n),n.view}getActiveView(){return this.app.workspace.getActiveViewOfType(h.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(N)[0]?.view}moveActiveCard(e){let n=this.getParallelView();return n?.sections.length?n.moveActiveSection(e):(new h.Notice(this.t("noActiveCard")),-1)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new h.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e?.path)return new h.Notice(this.t("noCancelableJob")),!1;let n=this.jobs.get(e.path),a=dt(this.settings,n),i=this.jobs.cancel(e.path);return new h.Notice(i?this.t(a):this.t("noCancelableJob")),i}viewIsShowingFile(e,n){return!!e&&!!n&&e.sourceFile?.path===n.path}activeFileStillMatches(e){let n=this.getActiveView();return!n?.file||n.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let n=this.getParallelView();n?.sourceFile?this.cancelGenerationForFile(n.sourceFile):new h.Notice(this.t("noCancelableJob"))}confirmRegenerateEditedCards(){let e=this.t("confirmRegenerateEditedCards");return typeof window<"u"&&typeof window.confirm=="function"?window.confirm(e):!0}addFileMenuItems(e,n){!(n instanceof h.TFile)||!n.path.endsWith(".md")||(e.addSeparator(),e.addItem(a=>a.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(n,!1))),e.addItem(a=>a.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(n,!0))),this.cacheManager.get(n.path)&&e.addItem(a=>a.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheManager.delete(n.path),new h.Notice(this.t("cacheClearedFile",{name:n.basename}))})))}async handleFileRename(e,n){if(!(e instanceof h.TFile)||!n)return;let a=n.endsWith(".md"),i=e.path.endsWith(".md");if(!a&&!i)return;if(a&&!i){await this.cacheManager.delete(n);let o=this.getParallelView();o&&o.sourceFile?.path===n&&o.renderEmpty();return}if(!a)return;this.cacheManager.cache[n]&&(this.cacheManager.cache[e.path]=this.cacheManager.cache[n],delete this.cacheManager.cache[n],await this.cacheManager.save());let s=this.getParallelView();s?.sourceFile&&(s.sourceFile.path===n||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof h.TFile))return;await this.cacheManager.delete(e.path);let n=this.getParallelView();n?.sourceFile?.path===e.path&&n.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),n=await this.ensureView();if((!n.sourceFile||!n.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!n.sourceFile||!n.sections.length){new h.Notice(this.t("noExportContent"));return}await n.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),n=await this.ensureView();if((!n.sourceFile||!n.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!n.sourceFile||!n.sections.length){new h.Notice(this.t("noCopyContent"));return}await L(W(`${n.sourceFile.basename} \xB7 ${this.t("displayName")}`,n.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let n=this.getActiveView();if(!n?.file){new h.Notice(this.t("openNoteFirst"));return}return this.runForFile(n.file,e)}async runForFile(e,n){if(!e){new h.Notice(this.t("openNoteFirst"));return}if(this.jobs.isRunning(e.path)){new h.Notice(this.t("alreadyGenerating"));return}if(ve(this.cacheManager.get(e.path),n)&&!this.confirmRegenerateEditedCards()){new h.Notice(this.t("regenerateCancelled"));return}let a=null;return this.jobs.start(e.path,async i=>{i.setPhase("reading");let s=await this.app.vault.read(e);if(i.throwIfCancelled(),!s.trim()){new h.Notice(this.t("emptyNote"));return}if(a=await this.ensureView(),i.throwIfCancelled(),i.setPhase("cache-check"),!n){let p=this.cacheManager.get(e.path);if(p&&J(p,s,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await a.loadFor(e,this.resolveCardAnchors(s,p.cards),!1);return}}await a.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||y.maxDocChars;s.length>o&&new h.Notice(this.t("longNoteTruncated",{count:o})),new h.Notice(this.t("generatingNotice")),i.setPhase("generating");let l=a?p=>{!p.done&&this.viewIsShowingFile(a,e)&&a.renderStreamingPreview(e,p.accumulated)}:void 0,c=await Kt(s,this.settings,i,l);if(i.throwIfCancelled(),c.length===0){new h.Notice(this.t("noCardsReturned"));return}let d=c.map(p=>({title:p.title,anchor:p.anchor,gist:p.gist,bullets:p.bullets}));i.setPhase("saving"),await this.cacheManager.put(e.path,s,d,this.settings),i.throwIfCancelled(),this.viewIsShowingFile(a,e)&&await a.loadFor(e,c,!1);let g=c.filter(p=>p.startLine<0).length;new h.Notice(this.t("generationDone",{count:c.length,suffix:g?this.t("unanchoredSuffix",{count:g}):""}))}).catch(async i=>{if(i instanceof B){new h.Notice(i.message);return}if(i instanceof S){a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,this.t("cancelledError")),new h.Notice(this.t("cancelled"));return}let s=Re(i);console.error(i),a&&this.viewIsShowingFile(a,e)&&await a.renderError(e,i.message||String(i)),new h.Notice(this.t("generationFailed",{kind:s==="unknown"?"":` (${s})`,error:i.message||i}))})}async runBatchForFolder(){let e=this,n=await new Promise(s=>{class o extends h.Modal{onOpen(){this.contentEl.createEl("p",{text:e.t("batchSelectFolder")}),this.input=this.contentEl.createEl("input",{type:"text"}),this.input.placeholder=e.t("batchFolderPrompt"),this.input.style.width="100%",this.input.addEventListener("keydown",c=>{c.key==="Enter"&&(s(this.input.value.trim()),this.close())}),this.contentEl.createEl("button",{text:"OK"}).addEventListener("click",()=>{s(this.input.value.trim()),this.close()})}onClose(){s(null)}}new o(e.app).open()});if(n===null)return;let a=this.app.vault.getMarkdownFiles().filter(s=>n===""?!s.path.includes("/"):s.parent?.path===n);if(a.length===0){new h.Notice(this.t("batchNoMarkdown"));return}let i=0;for(let s=0;s({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:te(e,i.anchor),bullets:i.bullets||[]}));return a.sort((i,s)=>i.startLine<0&&s.startLine<0?0:i.startLine<0?1:s.startLine<0?-1:i.startLine-s.startLine),a}async syncViewToFile(e){if(!e?.path?.endsWith(".md"))return;let n=this.app.workspace.getLeavesOfType(N);if(n.length===0)return;let a=n[0].view;if(!a||!this.activeFileStillMatches(e))return;let i=this.cacheManager.get(e.path);if(!i){await a.loadFor(e,[],!1),a.renderEmptyWithHint(e);return}let s=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!J(i,s,this.settings);await this.cacheTouch(e.path),await a.loadFor(e,this.resolveCardAnchors(s,i.cards),o)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let n=e.editor,a=n&&n.cm,i=a?.scrollDOM?a.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let s=je(()=>this.handleEditorScroll(e));i.addEventListener("scroll",s,{passive:!0}),this._scrollDispose=()=>{s.cancel(),i.removeEventListener("scroll",s)}}handleEditorScroll(e){let n=this.getParallelView();if(!n||!e.file||n.sourceFile?.path!==e.file.path)return;let a=e.editor,i=a&&a.cm;if(!i?.scrollDOM)return;let s=i.scrollDOM.getBoundingClientRect(),o=Ue(s),l=0;try{let d=i.posAtCoords({x:s.left+20,y:o});d!=null&&(l=i.state.doc.lineAt(d).number-1)}catch{return}let c=-1;for(let d=0;d; + cacheManager!: CacheManager; jobs!: GenerationJobManager; _scrollDispose: (() => void) | null = null; _settingsSaveTimer: ReturnType | null = null; - _cacheSaveTimer: ReturnType | null = null; - _cacheDirty = false; + + get cache(): Record { + return this.cacheManager.cache; + } t(key: string, vars?: Record) { return translate(this.settings || DEFAULT_SETTINGS, key, vars); @@ -120,8 +129,6 @@ 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(); @@ -171,7 +178,7 @@ class ParallelReaderPlugin extends Plugin { callback: async () => { const active = this.getActiveView(); if (!active?.file) return new Notice(this.t('noCurrentNote')); - await this.cacheDelete(active.file.path); + await this.cacheManager.delete(active.file.path); new Notice(this.t('cacheClearedFile', { name: active.file.basename })); }, }); @@ -179,8 +186,8 @@ class ParallelReaderPlugin extends Plugin { id: 'parallel-reader-clear-all', name: this.t('cmdClearAll'), callback: async () => { - const n = Object.keys(this.cache).length; - await this.cacheClear(); + const n = Object.keys(this.cacheManager.cache).length; + await this.cacheManager.clear(); new Notice(this.t('cacheClearedAll', { count: n })); }, }); @@ -201,6 +208,11 @@ class ParallelReaderPlugin extends Plugin { name: this.t('cmdCardJump'), callback: () => this.jumpActiveCard(), }); + this.addCommand({ + id: 'parallel-reader-batch-generate', + name: this.t('cmdBatchGenerate'), + callback: () => this.runBatchForFolder(), + }); this.addSettingTab(new ParallelReaderSettingTab(this.app, this)); @@ -228,7 +240,13 @@ class ParallelReaderPlugin extends Plugin { const data = (await this.loadData()) || {}; const settingsBlob = data.settings || {}; this.settings = normalizeSettings(Object.assign({}, DEFAULT_SETTINGS, settingsBlob)); - await this.loadCache(); + this.cacheManager = new CacheManager( + this.app.vault.adapter, + this.app.vault.configDir || '.obsidian', + this.manifest?.id || 'parallel-reader', + () => this.settings, + ); + await this.cacheManager.load(); } async saveSettings() { @@ -254,139 +272,25 @@ class ParallelReaderPlugin extends Plugin { await this.saveSettings(); } - /* ---------- Cache persistence ---------- */ - - 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() { - const configDir = this.app.vault.configDir || '.obsidian'; - const pluginId = this.manifest?.id || 'parallel-reader'; - return `${configDir}/plugins/${pluginId}/cache.json`; - } - - async ensurePluginDataDir() { - const adapter = this.app.vault.adapter; - const configDir = this.app.vault.configDir || '.obsidian'; - const pluginId = this.manifest?.id || 'parallel-reader'; - const dir = `${configDir}/plugins/${pluginId}`; - try { - if (typeof adapter.exists === 'function' && (await adapter.exists(dir))) return; - await adapter.mkdir(dir); - } catch (_) { - /* ignore race */ - } - } - - async readCacheFile() { - const adapter = this.app.vault.adapter; - try { - const raw = await adapter.read(this.cacheFilePath()); - const parsed = JSON.parse(raw); - if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object') - return parsed.entries; - } catch (e: unknown) { - const message = String((e as Error)?.message || e || ''); - if (!/not found|does not exist|ENOENT/i.test(message)) - console.warn('[parallel-reader] failed to read cache.json', e); - } - return {}; - } - - async writeCacheFile() { - await this.ensurePluginDataDir(); - await this.app.vault.adapter.write(this.cacheFilePath(), serializeCacheFile(this.cache)); - } - - async loadCache() { - this.cache = await this.readCacheFile(); - const pruned = this.pruneCache(); - if (pruned.length > 0) await this.writeCacheFile(); - } - - pruneCache() { - return pruneCacheEntries(this.cache, this.settings?.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES); - } - async pruneCacheIfNeeded() { - const removed = this.pruneCache(); - if (removed.length > 0) await this.saveCache(); - return removed; - } - cacheGet(filePath: string) { - return this.cache[filePath] || null; - } + /* ---------- Cache delegation ---------- */ async cacheTouch(filePath: string) { - const entry = touchCacheEntry(this.cache[filePath] || null); - if (!entry) return null; - this.scheduleCacheSave(); - return entry; + return this.cacheManager.touch(filePath); } - - async cachePut(filePath: string, content: string, cards: RawCard[], settings: PluginSettings) { - const now = new Date().toISOString(); - this.cache[filePath] = { - schemaVersion: CACHE_SCHEMA_VERSION, - contentHash: hashContent(content), - settingsHash: generationFingerprint(settings || this.settings), - cards, - generatedAt: now, - lastAccessedAt: now, - }; - await this.saveCache(); + scheduleCacheSave(delayMs = 5000) { + this.cacheManager.scheduleSave(delayMs); + } + async flushCacheSave() { + await this.cacheManager.flush(); } - async cacheReplaceCards(filePath: string, cards: ResolvedCard[]) { - const entry = this.cache[filePath]; - if (!entry) return false; - const now = new Date().toISOString(); - entry.cards = (cards || []).map((card: ResolvedCard) => ({ - 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: string) { - if (this.cache[filePath]) { - delete this.cache[filePath]; - await this.saveCache(); - } + return this.cacheManager.replaceCards(filePath, cards); } async cacheClear() { - this.cache = {}; - await this.saveCache(); + return this.cacheManager.clear(); + } + async pruneCacheIfNeeded() { + return this.cacheManager.pruneIfNeeded(); } /* ---------- View management ---------- */ @@ -465,29 +369,29 @@ class ParallelReaderPlugin extends Plugin { return true; } - addFileMenuItems(menu: any, file: any) { + addFileMenuItems(menu: ObsidianMenu, file: unknown) { if (!(file instanceof TFile) || !file.path.endsWith('.md')) return; menu.addSeparator(); - menu.addItem((it: any) => + menu.addItem((it: ObsidianMenuItem) => it .setTitle(this.t('fileMenuGenerate')) .setIcon('book-open') .onClick(() => this.runForFile(file, false)), ); - menu.addItem((it: any) => + menu.addItem((it: ObsidianMenuItem) => it .setTitle(this.t('fileMenuRegen')) .setIcon('refresh-cw') .onClick(() => this.runForFile(file, true)), ); - if (this.cacheGet(file.path)) { - menu.addItem((it: any) => + if (this.cacheManager.get(file.path)) { + menu.addItem((it: ObsidianMenuItem) => it .setTitle(this.t('fileMenuClear')) .setIcon('trash') .onClick(async () => { - await this.cacheDelete(file.path); - new Notice(this.t('cacheClearedFile', { name: file.basename })); + await this.cacheManager.delete(file.path); + new Notice(this.t('cacheClearedFile', { name: (file as TFile).basename })); }), ); } @@ -499,19 +403,16 @@ class ParallelReaderPlugin extends Plugin { const isMarkdown = file.path.endsWith('.md'); if (!wasMarkdown && !isMarkdown) return; if (wasMarkdown && !isMarkdown) { - if (this.cache[oldPath]) { - delete this.cache[oldPath]; - await this.saveCache(); - } + await this.cacheManager.delete(oldPath); const view = this.getParallelView(); if (view && view.sourceFile?.path === oldPath) view.renderEmpty(); return; } if (!wasMarkdown) return; - if (this.cache[oldPath]) { - this.cache[file.path] = this.cache[oldPath]; - delete this.cache[oldPath]; - await this.saveCache(); + if (this.cacheManager.cache[oldPath]) { + this.cacheManager.cache[file.path] = this.cacheManager.cache[oldPath]; + delete this.cacheManager.cache[oldPath]; + await this.cacheManager.save(); } const view = this.getParallelView(); if (view?.sourceFile && (view.sourceFile.path === oldPath || view.sourceFile.path === file.path)) { @@ -522,10 +423,7 @@ class ParallelReaderPlugin extends Plugin { async handleFileDelete(file: TFile) { if (!(file instanceof TFile)) return; - if (this.cache[file.path]) { - delete this.cache[file.path]; - await this.saveCache(); - } + await this.cacheManager.delete(file.path); const view = this.getParallelView(); if (view?.sourceFile?.path === file.path) view.renderEmpty(); } @@ -579,7 +477,7 @@ class ParallelReaderPlugin extends Plugin { new Notice(this.t('alreadyGenerating')); return; } - if (shouldConfirmRegenerate(this.cacheGet(file.path), force) && !this.confirmRegenerateEditedCards()) { + if (shouldConfirmRegenerate(this.cacheManager.get(file.path), force) && !this.confirmRegenerateEditedCards()) { new Notice(this.t('regenerateCancelled')); return; } @@ -600,8 +498,8 @@ class ParallelReaderPlugin extends Plugin { job.setPhase('cache-check'); if (!force) { - const entry = this.cacheGet(file.path); - if (cacheEntryMatches(entry, content, this.settings)) { + const entry = this.cacheManager.get(file.path); + if (entry && cacheEntryMatches(entry, content, this.settings)) { await this.cacheTouch(file.path); if (this.activeFileStillMatches(file)) await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), false); @@ -630,7 +528,7 @@ class ParallelReaderPlugin extends Plugin { } const rawCards = sections.map((s) => ({ title: s.title, anchor: s.anchor, gist: s.gist, bullets: s.bullets })); job.setPhase('saving'); - await this.cachePut(file.path, content, rawCards, this.settings); + await this.cacheManager.put(file.path, content, rawCards, this.settings); job.throwIfCancelled(); if (this.viewIsShowingFile(view, file)) await view.loadFor(file, sections, false); const unanchored = sections.filter((s) => s.startLine < 0).length; @@ -658,6 +556,57 @@ class ParallelReaderPlugin extends Plugin { }); } + async runBatchForFolder() { + const plugin = this; + const folderPath = await new Promise((resolve) => { + class FolderPromptModal extends Modal { + private input!: HTMLInputElement; + onOpen() { + this.contentEl.createEl('p', { text: plugin.t('batchSelectFolder') }); + this.input = this.contentEl.createEl('input', { type: 'text' }); + this.input.placeholder = plugin.t('batchFolderPrompt'); + this.input.style.width = '100%'; + this.input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + resolve(this.input.value.trim()); + this.close(); + } + }); + this.contentEl.createEl('button', { text: 'OK' }).addEventListener('click', () => { + resolve(this.input.value.trim()); + this.close(); + }); + } + onClose() { + resolve(null); + } + } + new FolderPromptModal(plugin.app).open(); + }); + if (folderPath === null) return; + const allFiles = this.app.vault.getMarkdownFiles().filter((f) => { + if (folderPath === '') return !f.path.includes('/'); + return f.parent?.path === folderPath; + }); + if (allFiles.length === 0) { + new Notice(this.t('batchNoMarkdown')); + return; + } + let skipped = 0; + for (let i = 0; i < allFiles.length; i++) { + const file = allFiles[i]; + new Notice(this.t('batchProgress', { current: i + 1, total: allFiles.length })); + const content = await this.app.vault.read(file); + const entry = this.cacheManager.get(file.path); + if (entry && cacheEntryMatches(entry, content, this.settings)) { + skipped++; + continue; + } + await this.runForFile(file, false); + } + new Notice(this.t('batchDone', { total: allFiles.length, skipped })); + } + resolveCardAnchors(content: string, rawCards: RawCard[]) { const resolved: ResolvedCard[] = (rawCards || []).map((c: RawCard) => ({ title: c.title, @@ -684,7 +633,7 @@ class ParallelReaderPlugin extends Plugin { if (leaves.length === 0) return; const view = leaves[0].view as ParallelReaderView; if (!view || !this.activeFileStillMatches(file)) return; - const entry = this.cacheGet(file.path); + const entry = this.cacheManager.get(file.path); if (!entry) { await view.loadFor(file, [], false); view.renderEmptyWithHint(file); @@ -705,7 +654,7 @@ class ParallelReaderPlugin extends Plugin { const mdView = this.getActiveView(); if (!mdView) return; const editor = mdView.editor; - const cm = editor && (editor as any).cm; + const cm = editor && (editor as unknown as ObsidianEditorWithCm).cm; const scrollDom = cm?.scrollDOM ? cm.scrollDOM : mdView.contentEl.querySelector('.cm-scroller'); if (!scrollDom) return; const handler = createRafThrottledHandler(() => this.handleEditorScroll(mdView)); @@ -720,7 +669,7 @@ class ParallelReaderPlugin extends Plugin { const view = this.getParallelView(); if (!view || !mdView.file || view.sourceFile?.path !== mdView.file.path) return; const editor = mdView.editor; - const cm = editor && (editor as any).cm; + const cm = editor && (editor as unknown as ObsidianEditorWithCm).cm; if (!cm?.scrollDOM) return; const rect = cm.scrollDOM.getBoundingClientRect(); const topY = visibleTopProbeY(rect); @@ -744,7 +693,7 @@ class ParallelReaderPlugin extends Plugin { findLeafForFile(file: TFile | null) { if (!file) return null; for (const leaf of this.app.workspace.getLeavesOfType('markdown')) { - const v = leaf.view as any; + const v = leaf.view as { file?: TFile }; if (v?.file && v.file.path === file.path) return leaf; } return null; diff --git a/src/cache-manager.ts b/src/cache-manager.ts new file mode 100644 index 0000000..5a3ea6e --- /dev/null +++ b/src/cache-manager.ts @@ -0,0 +1,160 @@ +'use strict'; + +import type { DataAdapter } from 'obsidian'; +import { serializeCacheFile, touchCacheEntry } from './cache'; +import { + CACHE_SCHEMA_VERSION, + DEFAULT_MAX_CACHE_ENTRIES, + generationFingerprint, + hashContent, + pruneCacheEntries, +} from './settings'; +import type { CacheEntry, PluginSettings, RawCard, ResolvedCard } from './types'; + +export class CacheManager { + cache: Record = {}; + private _timer: ReturnType | null = null; + private _dirty = false; + + constructor( + private readonly adapter: DataAdapter, + private readonly configDir: string, + private readonly pluginId: string, + private readonly getSettings: () => PluginSettings, + ) {} + + filePath(): string { + return `${this.configDir}/plugins/${this.pluginId}/cache.json`; + } + + async ensureDir(): Promise { + const dir = `${this.configDir}/plugins/${this.pluginId}`; + try { + if (typeof this.adapter.exists === 'function' && (await this.adapter.exists(dir))) return; + await this.adapter.mkdir(dir); + } catch (_) { + /* ignore race */ + } + } + + async readFile(): Promise> { + try { + const raw = await this.adapter.read(this.filePath()); + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object') + return parsed.entries; + } catch (e: unknown) { + const message = String((e as Error)?.message || e || ''); + if (!/not found|does not exist|ENOENT/i.test(message)) + console.warn('[parallel-reader] failed to read cache.json', e); + } + return {}; + } + + async writeFile(): Promise { + await this.ensureDir(); + await this.adapter.write(this.filePath(), serializeCacheFile(this.cache)); + } + + async load(): Promise { + this.cache = await this.readFile(); + const pruned = this.prune(); + if (pruned.length > 0) await this.writeFile(); + } + + prune(): string[] { + const settings = this.getSettings(); + return pruneCacheEntries(this.cache, settings?.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES); + } + + async pruneIfNeeded(): Promise { + const removed = this.prune(); + if (removed.length > 0) await this.save(); + return removed; + } + + async save(): Promise { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + this.prune(); + await this.writeFile(); + this._dirty = false; + } + + scheduleSave(delayMs = 5000): void { + this._dirty = true; + if (this._timer) return; + this._timer = setTimeout(() => { + this._timer = null; + if (!this._dirty) return; + this.save().catch((e) => console.error('[parallel-reader] failed to save cache', e)); + }, delayMs); + } + + async flush(): Promise { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + if (!this._dirty) return; + await this.save(); + } + + get(filePath: string): CacheEntry | null { + return this.cache[filePath] || null; + } + + async touch(filePath: string): Promise { + const entry = touchCacheEntry(this.cache[filePath] || null); + if (!entry) return null; + this.cache[filePath] = entry; + this.scheduleSave(); + return entry; + } + + async put(filePath: string, content: string, cards: RawCard[], settings: PluginSettings): Promise { + const now = new Date().toISOString(); + this.cache[filePath] = { + schemaVersion: CACHE_SCHEMA_VERSION, + contentHash: hashContent(content), + settingsHash: generationFingerprint(settings || this.getSettings()), + cards, + generatedAt: now, + lastAccessedAt: now, + }; + await this.save(); + } + + async replaceCards(filePath: string, cards: ResolvedCard[]): Promise { + const entry = this.cache[filePath]; + if (!entry) return false; + const now = new Date().toISOString(); + this.cache[filePath] = { + ...entry, + cards: (cards || []).map((card: ResolvedCard) => ({ + title: card.title, + anchor: card.anchor, + gist: card.gist, + bullets: card.bullets || [], + })), + updatedAt: now, + lastAccessedAt: now, + }; + await this.save(); + return true; + } + + async delete(filePath: string): Promise { + if (this.cache[filePath]) { + delete this.cache[filePath]; + await this.save(); + } + } + + async clear(): Promise { + this.cache = {}; + await this.save(); + } +} diff --git a/src/cache.ts b/src/cache.ts index 3718f9a..6b8b846 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -4,8 +4,7 @@ import type { CacheEntry } from './types'; export function touchCacheEntry(entry: CacheEntry | null, now?: string): CacheEntry | null { if (!entry) return null; - entry.lastAccessedAt = now || new Date().toISOString(); - return entry; + return { ...entry, lastAccessedAt: now || new Date().toISOString() }; } export function serializeCacheFile(entries: Record): string { diff --git a/src/i18n.ts b/src/i18n.ts index 703dada..fa73899 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -145,6 +145,12 @@ export const STRINGS: Record> = { clearAllCacheButton: '清除所有缓存', settingStreamingName: '流式输出', settingStreamingDesc: '启用后生成时实时显示 LLM 输出进度(仅 OpenAI Chat 和 Anthropic 格式支持)', + cmdBatchGenerate: '批量生成对照笔记(当前文件夹)', + batchSelectFolder: '请输入要批量处理的文件夹路径(留空为 Vault 根目录)', + batchFolderPrompt: '文件夹路径', + batchNoMarkdown: '该文件夹下没有 Markdown 文件', + batchProgress: '批量生成:{current}/{total}', + batchDone: '批量生成完成:共处理 {total} 篇,跳过缓存 {skipped} 篇', }, en: { appTitle: 'Parallel Reader', @@ -295,6 +301,12 @@ export const STRINGS: Record> = { settingStreamingName: 'Streaming output', settingStreamingDesc: 'Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)', + cmdBatchGenerate: 'Batch generate parallel notes (current folder)', + batchSelectFolder: 'Enter the folder path to batch-process (leave blank for Vault root)', + batchFolderPrompt: 'Folder path', + batchNoMarkdown: 'No Markdown files found in that folder', + batchProgress: 'Batch generating: {current}/{total}', + batchDone: 'Batch complete: processed {total}, skipped {skipped} (cached)', }, }; diff --git a/src/providers.ts b/src/providers.ts index 948bca0..9172036 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -22,6 +22,58 @@ import { import { deltaExtractorForFormat, type StreamProgress, streamingFetch } from './streaming'; import type { PluginSettings, RawCard } from './types'; +/* ---------- Typed request/response shapes ---------- */ + +type RequestUrlFunction = (params: { + url: string; + method: string; + headers: Record; + body: string; + throw?: boolean; +}) => Promise<{ status: number; json: unknown; text: string }>; + +interface AnthropicMessagesBody { + model: string; + max_tokens: number; + system: string; + messages: Array<{ role: string; content: string }>; + tools?: unknown[]; + tool_choice?: { type: string; name: string }; + stream?: boolean; +} + +interface OpenAiChatBody { + model: string; + messages: Array<{ role: string; content: string }>; + response_format?: unknown; + stream?: boolean; + [tokenField: string]: unknown; +} + +interface OpenAiResponsesBody { + model: string; + instructions: string; + input: string; + max_output_tokens: number; + text?: unknown; + stream?: boolean; +} + +interface GeminiGenerationConfig { + temperature: number; + maxOutputTokens: number; + responseMimeType?: string; + responseJsonSchema?: unknown; +} + +interface GeminiBody { + systemInstruction: { parts: Array<{ text: string }> }; + contents: Array<{ role: string; parts: Array<{ text: string }> }>; + generationConfig: GeminiGenerationConfig; +} + +/* ---------- Helpers ---------- */ + function endpointUrl(baseUrl: string, suffixes: string[]) { const base = baseUrl.replace(/\/+$/, ''); for (const suffix of suffixes) { @@ -37,8 +89,8 @@ function parseApiHeaders(raw: string, settings?: PluginSettings | null): Record< let parsed: Record; try { parsed = JSON.parse(text); - } catch (e: any) { - throw new Error(translate(settings || null, 'errorCustomHeadersJsonParse', { error: e.message })); + } catch (e: unknown) { + throw new Error(translate(settings || null, 'errorCustomHeadersJsonParse', { error: (e as Error).message })); } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error(translate(settings || null, 'errorCustomHeadersJsonObject')); @@ -90,10 +142,14 @@ function buildApiHeaders(settings: PluginSettings, extra?: Record { + if (resp.json && typeof resp.json === 'object') return resp.json as Record; try { - return JSON.parse(resp.text || '{}'); + return JSON.parse(resp.text || '{}') as Record; } catch (_) { throw new Error( translate(settings || null, 'errorProviderNonJson', { @@ -105,14 +161,14 @@ function responseJson(resp: any, label: string, settings?: PluginSettings | null } async function requestJsonBody( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, label: string, url: string, headers: Record, - body: any, + body: unknown, settings?: PluginSettings | null, -) { - let resp: any; +): Promise> { + let resp: { status: number; json: unknown; text: string }; try { resp = await requestUrlImpl({ url, @@ -121,11 +177,11 @@ async function requestJsonBody( body: JSON.stringify(body), throw: false, }); - } catch (e: any) { + } catch (e: unknown) { throw new Error( translate(settings || null, 'errorProviderRequestFailed', { label, - error: e.message || e, + error: (e as Error).message || String(e), }), ); } @@ -142,8 +198,8 @@ async function requestJsonBody( return responseJson(resp, label, settings); } -function shouldRetryWithoutStructuredOutput(error: any) { - const message = String(error?.message ? error.message : error); +function shouldRetryWithoutStructuredOutput(error: unknown): boolean { + const message = String((error as Error)?.message ?? error); 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( @@ -152,14 +208,14 @@ function shouldRetryWithoutStructuredOutput(error: any) { } async function requestJsonBodyWithStructuredFallback( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, label: string, url: string, headers: Record, - structuredBody: any, - fallbackBody: any, + structuredBody: unknown, + fallbackBody: unknown, settings?: PluginSettings | null, -) { +): Promise> { try { return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody, settings); } catch (e) { @@ -169,27 +225,31 @@ async function requestJsonBodyWithStructuredFallback( } } -function textFromContent(content: any): string { +function textFromContent(content: unknown): string { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content .map((part) => { if (typeof part === 'string') return part; - if (part && typeof part === 'object') return part.text || part.output_text || ''; + if (part && typeof part === 'object') { + const p = part as Record; + return typeof p.text === 'string' ? p.text : typeof p.output_text === 'string' ? p.output_text : ''; + } return ''; }) .join(''); } if (content && typeof content === 'object') { - return content.text || content.output_text || ''; + const c = content as Record; + return typeof c.text === 'string' ? c.text : typeof c.output_text === 'string' ? c.output_text : ''; } return ''; } -function textFromOpenAiResponses(json: any): string { +function textFromOpenAiResponses(json: Record): string { if (typeof json.output_text === 'string') return json.output_text; const parts: string[] = []; - const walk = (value: any) => { + const walk = (value: unknown) => { if (!value) return; if (typeof value === 'string') return; if (Array.isArray(value)) { @@ -197,11 +257,12 @@ function textFromOpenAiResponses(json: any): string { return; } if (typeof value === 'object') { - if (typeof value.text === 'string') parts.push(value.text); - if (typeof value.output_text === 'string') parts.push(value.output_text); - if (value.type === 'output_text' && typeof value.content === 'string') parts.push(value.content); - if (value.content) walk(value.content); - if (value.output) walk(value.output); + const v = value as Record; + if (typeof v.text === 'string') parts.push(v.text); + if (typeof v.output_text === 'string') parts.push(v.output_text); + if (v.type === 'output_text' && typeof v.content === 'string') parts.push(v.content); + if (v.content) walk(v.content); + if (v.output) walk(v.output); } }; walk(json.output); @@ -219,9 +280,9 @@ export function buildAnthropicMessagesBody( user: string, settings: PluginSettings, options?: { structured?: boolean }, -) { +): AnthropicMessagesBody { const structured = !options || options.structured !== false; - const body: any = { + const body: AnthropicMessagesBody = { model: modelForApi(settings), max_tokens: Number(settings.apiMaxTokens) || 4096, system, @@ -239,9 +300,9 @@ export function buildOpenAiChatBody( user: string, settings: PluginSettings, options?: { structured?: boolean }, -) { +): OpenAiChatBody { const structured = !options || options.structured !== false; - const body: any = { + const body: OpenAiChatBody = { model: modelForApi(settings), messages: [ { role: 'system', content: system }, @@ -260,9 +321,9 @@ export function buildOpenAiResponsesBody( user: string, settings: PluginSettings, options?: { structured?: boolean }, -) { +): OpenAiResponsesBody { const structured = !options || options.structured !== false; - const body: any = { + const body: OpenAiResponsesBody = { model: modelForApi(settings), instructions: system, input: user, @@ -279,9 +340,9 @@ export function buildGeminiBody( user: string, settings: PluginSettings, options?: { structured?: boolean }, -) { +): GeminiBody { const structured = !options || options.structured !== false; - const generationConfig: any = { + const generationConfig: GeminiGenerationConfig = { temperature: 0, maxOutputTokens: Number(settings.apiMaxTokens) || 4096, }; @@ -296,17 +357,18 @@ export function buildGeminiBody( }; } -function cardsFromAnthropicToolUse(json: any, settings?: PluginSettings | null) { - const content = Array.isArray(json?.content) ? json.content : []; - const block = content.find((c: any) => c && c.type === 'tool_use' && c.name === ANTHROPIC_CARD_TOOL_NAME); +function cardsFromAnthropicToolUse(json: Record, settings?: PluginSettings | null) { + const content = Array.isArray(json?.content) ? (json.content as Array>) : []; + 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, settings); - if (block.input && typeof block.input === 'object') return normalizeCardsPayload(block.input); + if (block.input && typeof block.input === 'object') + return normalizeCardsPayload(block.input as Record); return []; } async function summarizeViaAnthropicMessages( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, system: string, user: string, settings: PluginSettings, @@ -325,14 +387,20 @@ async function summarizeViaAnthropicMessages( const toolCards = cardsFromAnthropicToolUse(json, settings); if (toolCards) return toolCards; - const text = (json.content || []) - .map((c: any) => textFromContent(c)) + const contentBlocks = Array.isArray(json.content) ? (json.content as Array) : []; + const text = contentBlocks + .map((c) => textFromContent(c)) .join('') .trim(); return parseCardsJson(text, settings); } -async function summarizeViaOpenAiChat(requestUrlImpl: any, system: string, user: string, settings: PluginSettings) { +async function summarizeViaOpenAiChat( + requestUrlImpl: RequestUrlFunction, + system: string, + user: string, + settings: PluginSettings, +) { const url = endpointUrl(getApiBaseUrl(settings), ['/chat/completions']); const json = await requestJsonBodyWithStructuredFallback( requestUrlImpl, @@ -343,13 +411,15 @@ async function summarizeViaOpenAiChat(requestUrlImpl: any, system: string, user: buildOpenAiChatBody(system, user, settings, { structured: false }), settings, ); - const choice = (json.choices || [])[0] || {}; - const text = textFromContent(choice.message?.content || choice.text || '').trim(); + const choices = Array.isArray(json.choices) ? (json.choices as Array>) : []; + const choice = choices[0] || {}; + const message = choice.message as Record | undefined; + const text = textFromContent(message?.content ?? choice.text ?? '').trim(); return parseCardsJson(text, settings); } async function summarizeViaOpenAiResponses( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, system: string, user: string, settings: PluginSettings, @@ -368,7 +438,7 @@ async function summarizeViaOpenAiResponses( } async function summarizeViaGoogleGenerativeAi( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, system: string, user: string, settings: PluginSettings, @@ -388,18 +458,20 @@ async function summarizeViaGoogleGenerativeAi( buildGeminiBody(system, user, settings, { structured: false }), settings, ); - const candidate = (json.candidates || [])[0] || {}; - const parts = candidate.content?.parts || []; + const candidates = Array.isArray(json.candidates) ? (json.candidates as Array>) : []; + const candidate = candidates[0] || {}; + const contentObj = candidate.content as Record | undefined; + const parts = Array.isArray(contentObj?.parts) ? (contentObj.parts as Array) : []; const text = parts - .map((p: any) => textFromContent(p)) + .map((p) => textFromContent(p)) .join('') .trim(); return parseCardsJson(text, settings); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Obsidian's requestUrl is not compatible with fetch +// Obsidian's requestUrl is not directly compatible with fetch — we accept it as a typed callback export async function summarizeViaApi( - requestUrlImpl: any, + requestUrlImpl: RequestUrlFunction, system: string, user: string, settings: PluginSettings, @@ -473,8 +545,8 @@ export async function summarizeViaApiStreaming( } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Obsidian's requestUrl is not compatible with fetch -export async function testApiBackend(requestUrlImpl: any, settings: PluginSettings): Promise { +// Obsidian's requestUrl is not directly compatible with fetch — we accept it as a typed callback +export async function testApiBackend(requestUrlImpl: RequestUrlFunction, settings: PluginSettings): Promise { await summarizeViaApi(requestUrlImpl, '只输出 JSON:{"cards":[]}', '连通性测试:请原样输出 {"cards":[]}', settings); const format = getApiFormat(settings); return `${getApiPreset(settings).label} / ${API_FORMATS[format].label}`; diff --git a/src/schema.ts b/src/schema.ts index 7398d10..05c9ff3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -105,8 +105,16 @@ export function normalizeCardsPayload(parsed: { cards?: unknown[] }): RawCard[] })); } -export function cardOutputSchema(strict: boolean) { - const cardSchema: any = { +interface JsonSchema { + type: string; + properties?: Record; + items?: unknown; + required?: string[]; + additionalProperties?: boolean; +} + +export function cardOutputSchema(strict: boolean): JsonSchema { + const cardSchema: JsonSchema = { type: 'object', properties: { title: { type: 'string' }, @@ -119,7 +127,7 @@ export function cardOutputSchema(strict: boolean) { }, required: ['title', 'anchor', 'gist', 'bullets'], }; - const rootSchema: any = { + const rootSchema: JsonSchema = { type: 'object', properties: { cards: { diff --git a/src/settings.ts b/src/settings.ts index 88c8b15..70c69ff 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -41,6 +41,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { exportFolder: 'Reading/Articles', cliTimeoutMs: 120000, streaming: true, + streamingTimeoutMs: 120000, }; export const API_FORMATS: Record = { diff --git a/src/streaming.ts b/src/streaming.ts index 4d898d0..dccfaef 100644 --- a/src/streaming.ts +++ b/src/streaming.ts @@ -70,24 +70,15 @@ export interface StreamProgress { done: boolean; } -/** - * Perform a streaming fetch with SSE parsing. - * Uses the native Fetch API (available in Electron/Obsidian). - * Returns the full accumulated text when done. - */ -export async function streamingFetch( +async function doStreamingFetch( url: string, headers: Record, body: unknown, extractDelta: DeltaExtractor, - onProgress?: (progress: StreamProgress) => void, - signal?: AbortSignal, - settings?: PluginSettings | null, + onProgress: ((progress: StreamProgress) => void) | undefined, + signal: AbortSignal, + settings: PluginSettings | null | undefined, ): Promise { - if (typeof globalThis.fetch !== 'function') { - throw new Error('Streaming requires fetch API'); - } - const response = await globalThis.fetch(url, { method: 'POST', headers, @@ -138,3 +129,50 @@ export async function streamingFetch( onProgress?.({ accumulated, done: true }); return accumulated; } + +/** + * Perform a streaming fetch with SSE parsing and configurable timeout. + * Uses the native Fetch API (available in Electron/Obsidian). + * Returns the full accumulated text when done. + */ +export async function streamingFetch( + url: string, + headers: Record, + body: unknown, + extractDelta: DeltaExtractor, + onProgress?: (progress: StreamProgress) => void, + signal?: AbortSignal, + settings?: PluginSettings | null, +): Promise { + if (typeof globalThis.fetch !== 'function') { + throw new Error('Streaming requires fetch API'); + } + + const timeoutMs = settings?.streamingTimeoutMs ?? 120000; + const timeoutController = new AbortController(); + + if (signal) { + if (signal.aborted) { + timeoutController.abort(); + } else { + signal.addEventListener('abort', () => timeoutController.abort(), { once: true }); + } + } + + let timeoutId: ReturnType | null = null; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + timeoutController.abort(); + reject(new Error(`Streaming timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + + try { + return await Promise.race([ + doStreamingFetch(url, headers, body, extractDelta, onProgress, timeoutController.signal, settings), + timeoutPromise, + ]); + } finally { + if (timeoutId !== null) clearTimeout(timeoutId); + } +} diff --git a/src/types.ts b/src/types.ts index 7c82973..a519a0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,6 +66,7 @@ export interface PluginSettings { exportFolder: string; cliTimeoutMs: number; streaming: boolean; + streamingTimeoutMs: number; } /* ---------- Provider types ---------- */ @@ -109,6 +110,37 @@ export interface PromptPair { user: string; } +/* ---------- Obsidian internal API types ---------- */ + +/** Minimal CodeMirror 6 EditorView shape used for scroll synchronization. */ +export interface CmEditorView { + scrollDOM: HTMLElement; + state: { + doc: { + lineAt(pos: number): { number: number }; + }; + }; + posAtCoords(coords: { x: number; y: number }): number | null; +} + +/** Obsidian Editor with optional CodeMirror 6 view attached at `.cm`. */ +export interface ObsidianEditorWithCm { + cm?: CmEditorView; +} + +/** Minimal Obsidian MenuItem builder API used in file-menu callbacks. */ +export interface ObsidianMenuItem { + setTitle(title: string): this; + setIcon(icon: string): this; + onClick(callback: () => unknown): this; +} + +/** Minimal Obsidian Menu API used to build context-menu entries. */ +export interface ObsidianMenu { + addSeparator(): void; + addItem(cb: (item: ObsidianMenuItem) => void): void; +} + /* ---------- Plugin host interface ---------- */ /** diff --git a/tests/main.test.js b/tests/main.test.js index 84fd7cd..387b5bd 100644 --- a/tests/main.test.js +++ b/tests/main.test.js @@ -281,8 +281,10 @@ 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 touchedEntry = t.touchCacheEntry(untouchedCacheEntry, '2024-01-05T00:00:00.000Z'); +assert.notStrictEqual(touchedEntry, untouchedCacheEntry, 'touchCacheEntry returns new object'); +assert.strictEqual(touchedEntry.lastAccessedAt, '2024-01-05T00:00:00.000Z', 'touched entry has updated timestamp'); +assert.strictEqual(untouchedCacheEntry.lastAccessedAt, undefined, 'original entry not mutated'); assert.strictEqual(t.shouldConfirmRegenerate({ updatedAt: '2024-01-05T00:00:00.000Z' }, true), true); assert.strictEqual(t.shouldConfirmRegenerate({ updatedAt: '2024-01-05T00:00:00.000Z' }, false), false); assert.strictEqual(t.shouldConfirmRegenerate({ generatedAt: '2024-01-01T00:00:00.000Z' }, true), false); diff --git a/tests/modules.test.js b/tests/modules.test.js index 77e251e..73e8439 100644 --- a/tests/modules.test.js +++ b/tests/modules.test.js @@ -90,8 +90,9 @@ assert.deepStrictEqual( assert.strictEqual(t.touchCacheEntry(null), null, 'touchCacheEntry on null returns null'); const entry = { generatedAt: '2024-01-01T00:00:00.000Z' }; -t.touchCacheEntry(entry, '2024-06-01T00:00:00.000Z'); -assert.strictEqual(entry.lastAccessedAt, '2024-06-01T00:00:00.000Z', 'touchCacheEntry sets lastAccessedAt'); +const touched = t.touchCacheEntry(entry, '2024-06-01T00:00:00.000Z'); +assert.strictEqual(touched.lastAccessedAt, '2024-06-01T00:00:00.000Z', 'touchCacheEntry sets lastAccessedAt on returned entry'); +assert.strictEqual(entry.lastAccessedAt, undefined, 'touchCacheEntry does not mutate original entry'); const serialized = t.serializeCacheFile({ 'a.md': { cards: [] } }); const parsed = JSON.parse(serialized); @@ -455,4 +456,71 @@ const anthSse = t.parseSseBuffer( ); assert.deepStrictEqual(anthSse.deltas, ['token'], 'Anthropic SSE extracts text delta'); +// parseSseBuffer: partial line (no trailing newline → stays in rest) +const ssePartial = t.parseSseBuffer('data: {"choices":[{"delta":{"content":"ok"}}]}', openaiExtract); +assert.deepStrictEqual(ssePartial.deltas, [], 'incomplete line yields no deltas'); +assert.ok(ssePartial.rest.includes('"ok"'), 'partial line kept in rest'); + +// parseSseBuffer: multi-event in one chunk +const sseMulti = t.parseSseBuffer( + 'data: {"choices":[{"delta":{"content":"a"}}]}\ndata: {"choices":[{"delta":{"content":"b"}}]}\ndata: {"choices":[{"delta":{"content":"c"}}]}\n', + openaiExtract, +); +assert.deepStrictEqual(sseMulti.deltas, ['a', 'b', 'c'], 'three events in one chunk'); +assert.strictEqual(sseMulti.rest, '', 'nothing left in rest when chunk ends with newline'); + +// parseSseBuffer: non-data lines are skipped +const sseMixed = t.parseSseBuffer( + ': comment\nevent: ping\ndata: {"choices":[{"delta":{"content":"x"}}]}\n', + openaiExtract, +); +assert.deepStrictEqual(sseMixed.deltas, ['x'], 'comment and event lines ignored'); + +// parseSseBuffer: malformed JSON is silently skipped +const sseBad = t.parseSseBuffer('data: not_json\ndata: {"choices":[{"delta":{"content":"y"}}]}\n', openaiExtract); +assert.deepStrictEqual(sseBad.deltas, ['y'], 'bad JSON line skipped, good line extracted'); + +// i18n: additional edge cases +assert.strictEqual( + t.translate({ uiLanguage: 'en' }, 'generationDone', { count: 3, suffix: '' }), + 'Generated 3 sections', + 'variable interpolation with multiple vars', +); +assert.strictEqual( + t.translate({ uiLanguage: 'zh' }, 'appTitle'), + '对照阅读笔记', + 'zh translation for appTitle', +); +// missing key fallback chain: should return the key itself +assert.strictEqual(t.translate({ uiLanguage: 'en' }, '__no_such_key__'), '__no_such_key__', 'missing key returns key'); + +// ── cli.ts: resolveCliPath ── + +// Override path: should return the trimmed override immediately +assert.strictEqual(t.resolveCliPath('claude', ' /usr/bin/claude '), '/usr/bin/claude', 'trims override path'); +assert.strictEqual(t.resolveCliPath('claude', '/custom/path'), '/custom/path', 'returns custom path unchanged'); + +// Empty override: falls back to filesystem search; mock fs.existsSync to control behavior +const fsModule = require('fs'); +const origExistsSync = fsModule.existsSync; + +// Mock existsSync to pretend a specific path exists +const mockPath = require('path').join(require('os').homedir(), '.local', 'bin', 'claude'); +fsModule.existsSync = (p) => p === mockPath; +try { + const resolved = t.resolveCliPath('claude', ''); + assert.strictEqual(resolved, mockPath, 'resolveCliPath finds binary in mocked ~/.local/bin'); +} finally { + fsModule.existsSync = origExistsSync; +} + +// No match: falls back to bare name +fsModule.existsSync = () => false; +try { + const fallback = t.resolveCliPath('claude', ''); + assert.strictEqual(fallback, 'claude', 'resolveCliPath falls back to bare name when not found'); +} finally { + fsModule.existsSync = origExistsSync; +} + console.log('modules tests passed');