From 532cd31ef481f6adff9b508a7316e0296ab4f198 Mon Sep 17 00:00:00 2001 From: wujunchen Date: Sat, 25 Apr 2026 23:40:56 +0800 Subject: [PATCH] refactor: extract View, Modal, and SettingsTab from main.ts Move ParallelReaderView to src/view.ts (341 lines), CardEditModal to src/modal.ts (56 lines), ParallelReaderSettingTab to src/settings-tab.ts (370 lines). Add PluginHost interface in types.ts to break circular deps. main.ts reduced from 1613 to 588 lines. Change-Id: I90902e914f162ff92b9a7f7c190b4d397a2c8c12 --- main.js | 52 +- main.ts | 1252 ++++--------------------------------------- src/modal.ts | 56 ++ src/settings-tab.ts | 370 +++++++++++++ src/types.ts | 25 + src/view.ts | 341 ++++++++++++ tests/main.test.js | 11 +- 7 files changed, 937 insertions(+), 1170 deletions(-) create mode 100644 src/modal.ts create mode 100644 src/settings-tab.ts create mode 100644 src/view.ts diff --git a/main.js b/main.js index 64dd988..07f0b5a 100644 --- a/main.js +++ b/main.js @@ -1,29 +1,29 @@ 'use strict'; -"use strict";var at=Object.create;var K=Object.defineProperty;var nt=Object.getOwnPropertyDescriptor;var it=Object.getOwnPropertyNames;var st=Object.getPrototypeOf,ot=Object.prototype.hasOwnProperty;var ct=(r,e)=>{for(var t in e)K(r,t,{get:e[t],enumerable:!0})},Be=(r,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of it(e))!ot.call(r,i)&&i!==t&&K(r,i,{get:()=>e[i],enumerable:!(a=nt(e,i))||a.enumerable});return r};var $=(r,e,t)=>(t=r!=null?at(st(r)):{},Be(e||!r||!r.__esModule?K(t,"default",{value:r,enumerable:!0}):t,r)),lt=r=>Be(K({},"__esModule",{value:!0}),r);var Lt={};ct(Lt,{__test:()=>It,default:()=>Mt});module.exports=lt(Lt);var c=require("obsidian");function z(r,e){if(!e)return-1;let t=d=>d.replace(/\s+/g," ").trim(),a=d=>{let h=[],m=[],u=!1;for(let E=0;E0;continue}u&&(h.push(" "),m.push(E),u=!1),h.push(Ve),m.push(E)}return{text:h.join(""),map:m}},i=d=>{if(!d)return-1;let h=r.indexOf(d);if(h===-1)return-1;let m=0;for(let u=0;u=0||(n=i(e.trim()),n>=0))return n;for(let d of[60,40,25,15]){let h=e.trim().slice(0,d);if(n=i(h),n>=0)return n}let s=a(r),o=t(e).slice(0,30);if(!o)return-1;let l=s.text.indexOf(o);if(l===-1)return-1;let p=s.map[l];if(p==null)return-1;let f=0;for(let d=0;d0}function le(r,e){let t=Array.isArray(r)?r.slice():[];return!Number.isInteger(e)||e<0||e>=t.length||t.splice(e,1),t}function ue(r,e,t){if(!Number.isInteger(r)||!Number.isInteger(e)||e<=0||!Number.isInteger(t)||t<0||r<0||r>=e)return t;let a=e-1;return a<=0?-1:r=a.length||(a[e]=Object.assign({},a[e],t||{})),a}var He=require("child_process"),Y=$(require("os")),A=$(require("path")),Je=$(require("fs"));var F=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},w=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},pe=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new w(this.key)}},O=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new F(e);let a=new pe(e);this.jobs.set(e,a);try{a.setPhase("running");let i=await t(a);return a.throwIfCancelled(),i}catch(i){throw a.cancelled&&!(i instanceof w)?new w(e):i}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function he(r){if(r instanceof w)return"cancelled";let e=r;if(e?.code==="cancelled")return"cancelled";let t=String(e?.message||r);return/api key|unauthorized|401|403|认证|权限/i.test(t)?"auth":/timeout|超时|timed out/i.test(t)?"timeout":/429|rate limit|too many requests/i.test(t)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(t)?"schema":/model 未设置|base url|配置|config/i.test(t)?"config":"unknown"}var q={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 st=Object.create;var q=Object.defineProperty;var ot=Object.getOwnPropertyDescriptor;var ct=Object.getOwnPropertyNames;var lt=Object.getPrototypeOf,ut=Object.prototype.hasOwnProperty;var dt=(r,e)=>{for(var t in e)q(r,t,{get:e[t],enumerable:!0})},je=(r,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ct(e))!ut.call(r,n)&&n!==t&&q(r,n,{get:()=>e[n],enumerable:!(a=ot(e,n))||a.enumerable});return r};var W=(r,e,t)=>(t=r!=null?st(lt(r)):{},je(e||!r||!r.__esModule?q(t,"default",{value:r,enumerable:!0}):t,r)),pt=r=>je(q({},"__esModule",{value:!0}),r);var _t={};dt(_t,{__test:()=>Ot,default:()=>Rt});module.exports=pt(_t);var u=require("obsidian");function Y(r,e){if(!e)return-1;let t=y=>y.replace(/\s+/g," ").trim(),a=y=>{let f=[],h=[],l=!1;for(let F=0;F0;continue}l&&(f.push(" "),h.push(F),l=!1),f.push(Ue),h.push(F)}return{text:f.join(""),map:h}},n=y=>{if(!y)return-1;let f=r.indexOf(y);if(f===-1)return-1;let h=0;for(let l=0;l=0||(i=n(e.trim()),i>=0))return i;for(let y of[60,40,25,15]){let f=e.trim().slice(0,y);if(i=n(f),i>=0)return i}let o=a(r),s=t(e).slice(0,30);if(!s)return-1;let c=o.text.indexOf(s);if(c===-1)return-1;let d=o.map[c];if(d==null)return-1;let g=0;for(let y=0;y0}var Ke=require("child_process"),Q=W(require("os")),x=W(require("path")),$e=W(require("fs"));var O=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},v=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},be=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new v(this.key)}},U=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new O(e);let a=new be(e);this.jobs.set(e,a);try{a.setPhase("running");let n=await t(a);return a.throwIfCancelled(),n}catch(n){throw a.cancelled&&!(n instanceof v)?new v(e):n}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function we(r){if(r instanceof v)return"cancelled";let e=r;if(e?.code==="cancelled")return"cancelled";let t=String(e?.message||r);return/api key|unauthorized|401|403|认证|权限/i.test(t)?"auth":/timeout|超时|timed out/i.test(t)?"timeout":/429|rate limit|too many requests/i.test(t)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(t)?"schema":/model 未设置|base url|配置|config/i.test(t)?"config":"unknown"}var X={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"},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"}};function ut(r){let e=r&&r.uiLanguage;return e==="zh"||e==="en"?e:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function y(r,e,t){let a=ut(r),i=q[a]||q.en,n=q.en[e]||q.zh[e]||e,s=i[e]||n;return String(s).replace(/\{([a-zA-Z0-9_]+)\}/g,(o,l)=>t&&Object.prototype.hasOwnProperty.call(t,l)?String(t[l]):o)}var W="record_parallel_reader_cards";function dt(r){let e=[],t=-1,a=0,i=!1,n=!1;for(let s=0;s0&&a--,a===0&&t>=0&&(e.push(r.slice(t,s+1)),t=-1))}return e}function me(r){let e=(r||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let i=t[1].trim();try{return JSON.parse(i),i}catch{}}let a=dt(e);a.sort((i,n)=>n.length-i.length);for(let i of a)try{return JSON.parse(i),i}catch{}return e}function T(r,e){let t=me(r),a;try{a=JSON.parse(t)}catch{throw new Error(y(e,"errorLlmNonJson",{excerpt:(r||"").slice(0,500)}))}return _(a)}function _(r){return(r&&Array.isArray(r.cards)?r.cards:[]).filter(t=>!!t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(a=>typeof a=="string"):[]}))}function V(r){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return r&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function Ue(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:V(!0)}}}function je(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:V(!0)}}}function Ge(){return{name:W,description:"Return the generated Parallel Reader summary cards.",input_schema:V(!0)}}function D(r,e){if(e&&e.trim())return e.trim();let t=Y.default.homedir(),a=[A.default.join(t,"bin",r),A.default.join(t,".local/bin",r),A.default.join(t,".claude/local",r),A.default.join(t,".codex/bin",r),A.default.join(t,".bun/bin",r),A.default.join(t,".npm-global/bin",r),A.default.join(t,".cargo/bin",r),"/opt/homebrew/bin/"+r,"/usr/local/bin/"+r];for(let i of a)try{if(Je.default.existsSync(i))return i}catch{}return r}function B(r,e,t,a,i){return new Promise((n,s)=>{let o,l=!1,p,f=u=>{l||(l=!0,p&&clearTimeout(p),s(u))},d=u=>{l||(l=!0,p&&clearTimeout(p),n(u))};try{o=(0,He.spawn)(r,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",A.default.join(Y.default.homedir(),".local/bin"),A.default.join(Y.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return s(new Error(`Failed to start ${r}: ${u.message}`))}let h="",m="";if(p=setTimeout(()=>{try{o.kill("SIGKILL")}catch{}f(new Error(`CLI timed out (${a}ms)`))},a),i&&i.onCancel(()=>{try{o.kill("SIGKILL")}catch{}f(new w(i.key))}),o.stdout.on("data",u=>{h+=u.toString("utf8")}),o.stderr.on("data",u=>{m+=u.toString("utf8")}),o.on("error",u=>{f(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),o.on("close",u=>{if(!l){if(u!==0)return f(new Error(`CLI exited with code ${u} +{excerpt}`,errorProviderRequestFailed:"{label} request failed: {error}",errorProviderApiStatus:"{label} API returned HTTP {status}: {excerpt}",noEditor:"Could not find the source note editor",settingUiLanguageName:"UI language",settingUiLanguageDesc:"Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.",settingBackendName:"Backend",settingBackendDesc:"Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.",settingCliPathName:"CLI path (optional)",settingCliPathDesc:"Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.",settingCliPathPlaceholder:"Example: /Users/you/bin/codex",settingCliTimeoutName:"CLI timeout (ms)",apiProviderHeader:"API Provider",settingProviderPresetName:"Provider preset",settingProviderPresetDesc:"OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.",settingApiFormatName:"API format",settingApiFormatDesc:"Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.",settingBaseUrlName:"Base URL",settingBaseUrlDesc:"Provider root URL, without /chat/completions. Blank uses the preset default.",settingApiKeyName:"API Key",settingApiKeyDesc:"Can be blank for local Ollama/LM Studio; most hosted providers require a key.",settingApiKeyEnvName:"API key env var",settingApiKeyEnvDesc:"Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.",settingAuthTypeName:"Auth type",settingAuthTypeDesc:"Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.",settingHeadersName:"Extra headers",settingHeadersDesc:"Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.",settingMaxTokensName:"Max output tokens",settingModelName:"Model",settingModelDescApi:"Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.",settingModelDescCli:"Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.",settingMaxInputName:"Max input characters",settingMaxInputDesc:"Longer notes are truncated before sending to the model. Raise this for long-context models.",promptHeader:"Prompt",settingPromptLanguageName:"Output language",settingPromptLanguageDesc:"Controls title/gist/bullets language. Anchor is always copied verbatim from source.",settingCardRangeName:"Card count range",settingCardRangeDesc:"The model adapts segmentation within this range.",settingCustomPromptName:"Custom system prompt",settingCustomPromptDesc:"Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}",settingCustomPromptPlaceholder:"Leave blank to use built-in prompt",settingTestBackendName:"Test current backend",settingTestBackendDescApi:"Sends a minimal LLM request to validate API settings.",settingTestBackendDescCli:"Runs ` --version` to verify the binary can be spawned.",backendTestFailed:"\u2717 Backend test failed: {error}",settingExportFolderName:"Export folder",settingExportFolderDesc:"Parallel-note output location, relative to the Vault root.",cacheHeader:"Cache",settingMaxCacheName:"Max cached notes",settingMaxCacheDesc:"Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.",cachePruned:"Pruned {count} old cache entries",cachedNotesName:"Cached notes: {count}",cachedNotesDesc:"Cache is invalidated by source SHA1 and generation settings fingerprint.",clearAllCacheButton:"Clear all caches"}};function ht(r){let e=r&&r.uiLanguage;return e==="zh"||e==="en"?e:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function C(r,e,t){let a=ht(r),n=X[a]||X.en,i=X.en[e]||X.zh[e]||e,o=n[e]||i;return String(o).replace(/\{([a-zA-Z0-9_]+)\}/g,(s,c)=>t&&Object.prototype.hasOwnProperty.call(t,c)?String(t[c]):s)}var Z="record_parallel_reader_cards";function mt(r){let e=[],t=-1,a=0,n=!1,i=!1;for(let o=0;o0&&a--,a===0&&t>=0&&(e.push(r.slice(t,o+1)),t=-1))}return e}function ve(r){let e=(r||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let n=t[1].trim();try{return JSON.parse(n),n}catch{}}let a=mt(e);a.sort((n,i)=>i.length-n.length);for(let n of a)try{return JSON.parse(n),n}catch{}return e}function S(r,e){let t=ve(r),a;try{a=JSON.parse(t)}catch{throw new Error(C(e,"errorLlmNonJson",{excerpt:(r||"").slice(0,500)}))}return j(a)}function j(r){return(r&&Array.isArray(r.cards)?r.cards:[]).filter(t=>!!t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(a=>typeof a=="string"):[]}))}function G(r){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return r&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function Ge(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:G(!0)}}}function He(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:G(!0)}}}function Je(){return{name:Z,description:"Return the generated Parallel Reader summary cards.",input_schema:G(!0)}}function D(r,e){if(e&&e.trim())return e.trim();let t=Q.default.homedir(),a=[x.default.join(t,"bin",r),x.default.join(t,".local/bin",r),x.default.join(t,".claude/local",r),x.default.join(t,".codex/bin",r),x.default.join(t,".bun/bin",r),x.default.join(t,".npm-global/bin",r),x.default.join(t,".cargo/bin",r),"/opt/homebrew/bin/"+r,"/usr/local/bin/"+r];for(let n of a)try{if($e.default.existsSync(n))return n}catch{}return r}function H(r,e,t,a,n){return new Promise((i,o)=>{let s,c=!1,d,g=l=>{c||(c=!0,d&&clearTimeout(d),o(l))},y=l=>{c||(c=!0,d&&clearTimeout(d),i(l))};try{s=(0,Ke.spawn)(r,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",x.default.join(Q.default.homedir(),".local/bin"),x.default.join(Q.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(l){return o(new Error(`Failed to start ${r}: ${l.message}`))}let f="",h="";if(d=setTimeout(()=>{try{s.kill("SIGKILL")}catch{}g(new Error(`CLI timed out (${a}ms)`))},a),n&&n.onCancel(()=>{try{s.kill("SIGKILL")}catch{}g(new v(n.key))}),s.stdout.on("data",l=>{f+=l.toString("utf8")}),s.stderr.on("data",l=>{h+=l.toString("utf8")}),s.on("error",l=>{g(new Error(`CLI startup error: ${l.message}. Try setting an absolute CLI path.`))}),s.on("close",l=>{if(!c){if(l!==0)return g(new Error(`CLI exited with code ${l} stderr: -${m.slice(0,1e3)}`));d({stdout:h,stderr:m})}}),t)try{o.stdin.write(t),o.stdin.end()}catch{}else try{o.stdin.end()}catch{}})}async function Ke(r,e,t,a){let i=D("claude",t.cliPath),n=["-p","--output-format","json","--append-system-prompt",r,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&n.push("--model",t.model);let{stdout:s}=await B(i,n,e,t.cliTimeoutMs,a),o;try{o=JSON.parse(s)}catch{throw new Error(`claude CLI returned a non-JSON envelope: -`+s.slice(0,500))}let l=o.result||o.content||"";return T(l,t)}async function $e(r,e,t,a){let i=D("codex",t.cliPath),n=`<> +${h.slice(0,1e3)}`));y({stdout:f,stderr:h})}}),t)try{s.stdin.write(t),s.stdin.end()}catch{}else try{s.stdin.end()}catch{}})}async function ze(r,e,t,a){let n=D("claude",t.cliPath),i=["-p","--output-format","json","--append-system-prompt",r,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&i.push("--model",t.model);let{stdout:o}=await H(n,i,e,t.cliTimeoutMs,a),s;try{s=JSON.parse(o)}catch{throw new Error(`claude CLI returned a non-JSON envelope: +`+o.slice(0,500))}let c=s.result||s.content||"";return S(c,t)}async function qe(r,e,t,a){let n=D("codex",t.cliPath),i=`<> ${r} <> ${e} -Output JSON directly with no explanation.`,s=["exec","--skip-git-repo-check","-"],{stdout:o}=await B(i,s,n,t.cliTimeoutMs,a);return T(o,t)}function ge(r){let e=[`## ${r.title}`];if(r.anchor){let t=r.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return r.gist&&e.push(r.gist),r.bullets&&r.bullets.length>0&&e.push(r.bullets.map(t=>`- ${t}`).join(` +Output JSON directly with no explanation.`,o=["exec","--skip-git-repo-check","-"],{stdout:s}=await H(n,o,i,t.cliTimeoutMs,a);return S(s,t)}function Ae(r){let e=[`## ${r.title}`];if(r.anchor){let t=r.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return r.gist&&e.push(r.gist),r.bullets&&r.bullets.length>0&&e.push(r.bullets.map(t=>`- ${t}`).join(` `)),e.join(` -`)}function ze(r){return[r.title,r.gist||"",...(r.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(` -`)}function X(r,e){let t=[`# ${r||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let a of e||[])t.push(ge(a));return t.join(` +`)}function We(r){return[r.title,r.gist||"",...(r.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(` +`)}function J(r,e){let t=[`# ${r||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let a of e||[])t.push(Ae(a));return t.join(` -`)}function fe(r,e,t){if(!Number.isFinite(e)||e<=0)return-1;let a=Math.floor(e),i=t<0?-1:1;return r<0||r>=a?i<0?a-1:0:Math.min(a-1,Math.max(0,r+i))}function ye(r,e){if(!Array.isArray(r)||e<0||e>=r.length)return-1;let t=Number(r[e]?.startLine);return Number.isFinite(t)&&t>=0?t:-1}var Ye=$(require("crypto"));var be=2e4,pt=2,U=2,j=100,G={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},we={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},g={uiLanguage:"auto",backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:be,maxCacheEntries:j,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},v={"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"}},ve={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"},S={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 M(r){return Ye.default.createHash("sha1").update(r,"utf8").digest("hex")}function Ce(r){return Array.isArray(r)?"["+r.map(Ce).join(",")+"]":r&&typeof r=="object"?"{"+Object.keys(r).sort().map(e=>JSON.stringify(e)+":"+Ce(r[e])).join(",")+"}":JSON.stringify(r)}function H(r){return r==="api"||r==="anthropic-api"}function C(r){return S[r.apiProvider]||S.anthropic}function x(r){let e=C(r),t=(r.apiFormat||e.format||"").trim();return v[t]?t:e.format}function k(r){let e=x(r),t=C(r),a=(r.apiBaseUrl||"").trim();if(a)return a.replace(/\/+$/,"");if((r.apiProvider||"").startsWith("custom-"))throw new Error(y(r,"errorCustomProviderBaseUrlRequired"));let i=(t.baseUrl||v[e].defaultBaseUrl||"").trim();if(!i)throw new Error(y(r,"errorApiBaseUrlMissing"));return i.replace(/\/+$/,"")}function Ae(r){let e=(r.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=C(r),a=x(r);return t.authType||v[a].defaultAuthType||"bearer"}function Xe(r){let e=(r.apiKey||"").trim();if(e)return e;let t=(r.apiKeyEnvVar||C(r).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function N(r){let e=(r.model||"").trim();if(!e)throw new Error(y(r,"errorModelMissing"));let t=C(r),a=[r.apiProvider,t.modelPrefix].map(n=>(n||"").trim()).filter(Boolean),i=e.toLowerCase();for(let n of a){let s=n.toLowerCase();if(i.startsWith(s+"/"))return e.slice(n.length+1).trim()}return e}function xe(r,e){let t=C(r),a=S[e]||S.anthropic,i=(r.model||"").trim(),n=!i||i===g.model||!!t.model&&i===t.model;r.apiProvider=e,r.apiFormat=a.format,r.apiBaseUrl=a.baseUrl,r.apiAuthType=a.authType||"auto",r.apiKeyEnvVar=a.envVar||"",n&&(r.model=a.model||"")}function Pe(r){we[r.uiLanguage]||(r.uiLanguage=g.uiLanguage),(!r.apiProvider||!S[r.apiProvider])&&(r.apiProvider="anthropic");let e=C(r);(!r.apiFormat||!v[r.apiFormat])&&(r.apiFormat=e.format),(!r.apiAuthType||!ve[r.apiAuthType])&&(r.apiAuthType="auto"),r.backend==="anthropic-api"&&(r.apiProvider=r.apiProvider||"anthropic",r.apiFormat=r.apiFormat||"anthropic-messages",r.apiBaseUrl=r.apiBaseUrl||S.anthropic.baseUrl,r.apiAuthType=r.apiAuthType||"x-api-key",r.apiKeyEnvVar=r.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(r.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(r.apiMaxTokens=g.apiMaxTokens);let a=Number(r.maxDocChars);return(!Number.isFinite(a)||a<1e3)&&(r.maxDocChars=g.maxDocChars),r.maxCacheEntries=Ze(r.maxCacheEntries),G[r.promptLanguage]||(r.promptLanguage=g.promptLanguage),r.minCards=qe(r.minCards,g.minCards),r.maxCards=qe(r.maxCards,g.maxCards),r.maxCardsWe(s)-We(l)||n.localeCompare(o)).slice(0,a.length-t).map(([n])=>n);for(let n of i)delete r[n];return i}function Z(r){let e=Pe(Object.assign({},g,r||{})),t=H(e.backend),a=C(e),i=x(e),n=t?(e.apiBaseUrl||a.baseUrl||v[i]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return M(Ce({cacheSchemaVersion:U,promptVersion:pt,maxDocChars:Number(e.maxDocChars)||g.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:M(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?i:"",apiBaseUrl:n,apiAuthType:t?Ae(e):"",apiHeadersHash:t?M(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||g.apiMaxTokens:0,structuredOutputVersion:1}))}function Q(r,e,t){return!!r&&r.schemaVersion===U&&r.contentHash===M(e)&&r.settingsHash===Z(t)}function ht(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 mt(r){return r==="en"?`{"cards":[ +`)}var Ze=W(require("crypto"));var Pe=2e4,gt=2,K=2,V=100,$={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},Te={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},m={uiLanguage:"auto",backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:Pe,maxCacheEntries:V,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},A={"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"},E={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 Ze.default.createHash("sha1").update(r,"utf8").digest("hex")}function xe(r){return Array.isArray(r)?"["+r.map(xe).join(",")+"]":r&&typeof r=="object"?"{"+Object.keys(r).sort().map(e=>JSON.stringify(e)+":"+xe(r[e])).join(",")+"}":JSON.stringify(r)}function B(r){return r==="api"||r==="anthropic-api"}function b(r){return E[r.apiProvider]||E.anthropic}function P(r){let e=b(r),t=(r.apiFormat||e.format||"").trim();return A[t]?t:e.format}function I(r){let e=P(r),t=b(r),a=(r.apiBaseUrl||"").trim();if(a)return a.replace(/\/+$/,"");if((r.apiProvider||"").startsWith("custom-"))throw new Error(C(r,"errorCustomProviderBaseUrlRequired"));let n=(t.baseUrl||A[e].defaultBaseUrl||"").trim();if(!n)throw new Error(C(r,"errorApiBaseUrlMissing"));return n.replace(/\/+$/,"")}function Ee(r){let e=(r.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=b(r),a=P(r);return t.authType||A[a].defaultAuthType||"bearer"}function Qe(r){let e=(r.apiKey||"").trim();if(e)return e;let t=(r.apiKeyEnvVar||b(r).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function M(r){let e=(r.model||"").trim();if(!e)throw new Error(C(r,"errorModelMissing"));let t=b(r),a=[r.apiProvider,t.modelPrefix].map(i=>(i||"").trim()).filter(Boolean),n=e.toLowerCase();for(let i of a){let o=i.toLowerCase();if(n.startsWith(o+"/"))return e.slice(i.length+1).trim()}return e}function ke(r,e){let t=b(r),a=E[e]||E.anthropic,n=(r.model||"").trim(),i=!n||n===m.model||!!t.model&&n===t.model;r.apiProvider=e,r.apiFormat=a.format,r.apiBaseUrl=a.baseUrl,r.apiAuthType=a.authType||"auto",r.apiKeyEnvVar=a.envVar||"",i&&(r.model=a.model||"")}function Ne(r){Te[r.uiLanguage]||(r.uiLanguage=m.uiLanguage),(!r.apiProvider||!E[r.apiProvider])&&(r.apiProvider="anthropic");let e=b(r);(!r.apiFormat||!A[r.apiFormat])&&(r.apiFormat=e.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||E.anthropic.baseUrl,r.apiAuthType=r.apiAuthType||"x-api-key",r.apiKeyEnvVar=r.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(r.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(r.apiMaxTokens=m.apiMaxTokens);let a=Number(r.maxDocChars);return(!Number.isFinite(a)||a<1e3)&&(r.maxDocChars=m.maxDocChars),r.maxCacheEntries=et(r.maxCacheEntries),$[r.promptLanguage]||(r.promptLanguage=m.promptLanguage),r.minCards=Ye(r.minCards,m.minCards),r.maxCards=Ye(r.maxCards,m.maxCards),r.maxCardsXe(o)-Xe(c)||i.localeCompare(s)).slice(0,a.length-t).map(([i])=>i);for(let i of n)delete r[i];return n}function ee(r){let e=Ne(Object.assign({},m,r||{})),t=B(e.backend),a=b(e),n=P(e),i=t?(e.apiBaseUrl||a.baseUrl||A[n]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return _(xe({cacheSchemaVersion:K,promptVersion:gt,maxDocChars:Number(e.maxDocChars)||m.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:_(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?n:"",apiBaseUrl:i,apiAuthType:t?Ee(e):"",apiHeadersHash:t?_(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||m.apiMaxTokens:0,structuredOutputVersion:1}))}function te(r,e,t){return!!r&&r.schemaVersion===K&&r.contentHash===_(e)&&r.settingsHash===ee(t)}function ft(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 yt(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 gt(r,e){return String(r||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,a)=>Object.prototype.hasOwnProperty.call(e,a)?String(e[a]):t)}function ft(r,e,t,a,i,n){return r==="en"?`You are a long-form reading summary assistant. After reading the full document, split it into ${e}-${t} natural topic units. They do not need to match markdown headings; use a complete argument or topic as the unit, merging short sections and splitting long ones when needed. +]}`}function Ct(r,e){return String(r||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,a)=>Object.prototype.hasOwnProperty.call(e,a)?String(e[a]):t)}function bt(r,e,t,a,n,i){return r==="en"?`You are a long-form reading summary assistant. After reading the full document, split it into ${e}-${t} natural topic units. They do not need to match markdown headings; use a complete argument or topic as the unit, merging short sections and splitting long ones when needed. Each card has one guiding sentence plus several bullets. Bullets carry details; gist is the lead-in. @@ -45,10 +45,10 @@ Rules: - Output strict JSON only: no markdown fence, no explanation, no tool call. Output shape: -${i} +${n} Example: -${n}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${e}-${t} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002 +${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 ${e}-${t} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002 **\u6BCF\u5F20\u5361\u7247\u7684\u7ED3\u6784\uFF1A\u4E00\u53E5\u8BDD\u9886\u8BFB + \u82E5\u5E72\u6761 bullet\u3002bullet \u627F\u8F7D\u7EC6\u8282\uFF0Cgist \u662F\u4E00\u53E5\u8BDD\u5BFC\u8BFB\u3002** @@ -70,32 +70,32 @@ ${n}`:`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\ - \u4E25\u683C\u53EA\u8F93\u51FA JSON\uFF0C\u65E0 markdown fence\u3001\u65E0\u89E3\u91CA\u3001\u65E0 tool call \u8F93\u51FA\u683C\u5F0F\uFF1A -${i} +${n} \u793A\u4F8B\uFF1A -${n}`}function yt(r,e,t,a,i){return r==="en"?`Non-overridable output contract: +${i}`}function wt(r,e,t,a,n){return r==="en"?`Non-overridable output contract: - Must output ${e}-${t} cards. - ${a} - anchor must be copied verbatim from the source and exact-substring-match the source. - Output strict JSON only: no markdown fence, no explanation, no tool call. -- JSON shape: ${i}`:`\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A +- JSON shape: ${n}`:`\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A - \u5FC5\u987B\u8F93\u51FA ${e}-${t} \u5F20 cards\u3002 - ${a} - 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: ${i}`}function Se(r,e){let t=Number(e.maxDocChars)||be,a=G[e.promptLanguage]?e.promptLanguage:g.promptLanguage,i=Math.max(1,Number(e.minCards)||g.minCards),n=Math.max(i,Number(e.maxCards)||g.maxCards),s=ht(a),o=r.length>t?r.slice(0,t)+(a==="en"?` +- JSON shape: ${n}`}function De(r,e){let t=Number(e.maxDocChars)||Pe,a=$[e.promptLanguage]?e.promptLanguage:m.promptLanguage,n=Math.max(1,Number(e.minCards)||m.minCards),i=Math.max(n,Number(e.maxCards)||m.maxCards),o=ft(a),s=r.length>t?r.slice(0,t)+(a==="en"?` [Document truncated]`:` -[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):r,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',p=mt(a),f={minCards:i,maxCards:n,languageInstruction:s,schema:l,example:p},d=gt(e.customSystemPrompt,f).trim(),h=yt(a,i,n,s,l),m=ft(a,i,n,s,l,p),u=d?`${d} +[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):r,c='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',d=yt(a),g={minCards:n,maxCards:i,languageInstruction:o,schema:c,example:d},y=Ct(e.customSystemPrompt,g).trim(),f=wt(a,n,i,o,c),h=bt(a,n,i,o,c,d),l=y?`${y} -${h}`:m,E=a==="en"?`Source document: +${f}`:h,F=a==="en"?`Source document: -${o}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A +${s}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A -${o}`;return{system:u,user:E}}function Ee(r){return String(r||"").split("/").map(e=>e.trim()).filter(Boolean).join("/")}function ke(r){let e=Ee(r);if(!e)return[];let t=e.split("/");return t.map((a,i)=>t.slice(0,i+1).join("/"))}async function Qe(r,e){for(let t of ke(e))if(!r.vault.getAbstractFileByPath(t))try{await r.vault.createFolder(t)}catch(a){if(!r.vault.getAbstractFileByPath(t))throw a}}function Ne(r,e=80,t=.1){let a=Number(r?.top)||0,i=Math.max(0,Number(r?.height)||0),n=Math.min(Number(e)||0,i*t);return a+Math.max(0,n)}function Ct(r){return typeof requestAnimationFrame=="function"?requestAnimationFrame(r):setTimeout(()=>r(Date.now()),16)}function bt(r){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(r);return}clearTimeout(r)}function Fe(r,e=Ct,t=bt){let a=null,i=()=>{a===null&&(a=e(()=>{a=null,r()}))};return i.cancel=()=>{a!==null&&(t(a),a=null)},i}function De(r,e){let t=r.replace(/\/+$/,"");for(let a of e)if(t.endsWith(a))return t;return t+e[0]}function wt(r,e){let t=(r||"").trim();if(!t)return{};if(t.startsWith("{")){let i;try{i=JSON.parse(t)}catch(s){throw new Error(y(e,"errorCustomHeadersJsonParse",{error:s.message}))}if(!i||typeof i!="object"||Array.isArray(i))throw new Error(y(e,"errorCustomHeadersJsonObject"));let n={};for(let[s,o]of Object.entries(i))typeof o=="string"&&s.trim()&&(n[s.trim()]=o);return n}let a={};for(let i of t.split(/\r?\n/)){let n=i.trim();if(!n||n.startsWith("#"))continue;let s=n.indexOf(":");if(s<=0)throw new Error(y(e,"errorCustomHeadersLineFormat"));let o=n.slice(0,s).trim(),l=n.slice(s+1).trim();o&&(a[o]=l)}return a}function vt(r){let e=Ae(r);if(e==="none")return{};let t=Xe(r);if(!t){let a=(r.apiKeyEnvVar||C(r).envVar||"").trim(),i=a?y(r,"errorApiKeyEnvHint",{envVar:a}):"";throw new Error(y(r,"errorApiKeyMissing",{hint:i}))}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function ne(r,e){return{"content-type":"application/json",...vt(r),...e||{},...wt(r.apiHeaders,r)}}function At(r,e,t){if(r.json&&typeof r.json=="object")return r.json;try{return JSON.parse(r.text||"{}")}catch{throw new Error(y(t,"errorProviderNonJson",{label:e,excerpt:(r.text||"").slice(0,500)}))}}async function et(r,e,t,a,i,n){let s;try{s=await r({url:t,method:"POST",headers:a,body:JSON.stringify(i),throw:!1})}catch(o){throw new Error(y(n,"errorProviderRequestFailed",{label:e,error:o.message||o}))}if(s.status>=400)throw new Error(y(n,"errorProviderApiStatus",{label:e,status:s.status,excerpt:(s.text||"").slice(0,500)}));return At(s,e,n)}function xt(r){let e=String(r&&r.message?r.message:r);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function ie(r,e,t,a,i,n,s){try{return await et(r,e,t,a,i,s)}catch(o){if(!n||!xt(o))throw o;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,o),et(r,e+" fallback",t,a,n,s)}}function Me(r){return typeof r=="string"?r:Array.isArray(r)?r.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):r&&typeof r=="object"&&(r.text||r.output_text)||""}function Pt(r){if(typeof r.output_text=="string")return r.output_text;let e=[],t=a=>{if(a&&typeof a!="string"){if(Array.isArray(a)){a.forEach(t);return}typeof a=="object"&&(typeof a.text=="string"&&e.push(a.text),typeof a.output_text=="string"&&e.push(a.output_text),a.type==="output_text"&&typeof a.content=="string"&&e.push(a.content),a.content&&t(a.content),a.output&&t(a.output))}};return t(r.output),e.join("")}function Ie(r){let e=C(r),t=v[x(r)];return e.tokenLimitField||t&&t.tokenLimitField||"max_tokens"}function ee(r,e,t,a){let i=!a||a.structured!==!1,n={model:N(t),max_tokens:Number(t.apiMaxTokens)||4096,system:r,messages:[{role:"user",content:e}]};return i&&(n.tools=[Ge()],n.tool_choice={type:"tool",name:W}),n}function te(r,e,t,a){let i=!a||a.structured!==!1,n={model:N(t),messages:[{role:"system",content:r},{role:"user",content:e}]};return n[Ie(t)]=Number(t.apiMaxTokens)||4096,i&&(n.response_format=Ue()),n}function re(r,e,t,a){let i=!a||a.structured!==!1,n={model:N(t),instructions:r,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return i&&(n.text=je()),n}function ae(r,e,t,a){let i=!a||a.structured!==!1,n={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return i&&(n.responseMimeType="application/json",n.responseJsonSchema=V(!1)),{systemInstruction:{parts:[{text:r}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:n}}function Tt(r,e){let a=(Array.isArray(r&&r.content)?r.content:[]).find(i=>i&&i.type==="tool_use"&&i.name===W);return a?typeof a.input=="string"?T(a.input,e):a.input&&typeof a.input=="object"?_(a.input):[]:null}async function St(r,e,t,a){let i=De(k(a),["/messages"]),n=await ie(r,"Anthropic-compatible",i,ne(a,{"anthropic-version":"2023-06-01"}),ee(e,t,a),ee(e,t,a,{structured:!1}),a),s=Tt(n,a);if(s)return s;let o=(n.content||[]).map(l=>Me(l)).join("").trim();return T(o,a)}async function Et(r,e,t,a){let i=De(k(a),["/chat/completions"]),s=((await ie(r,"OpenAI-compatible Chat",i,ne(a),te(e,t,a),te(e,t,a,{structured:!1}),a)).choices||[])[0]||{},o=Me(s.message?.content||s.text||"").trim();return T(o,a)}async function kt(r,e,t,a){let i=De(k(a),["/responses"]),n=await ie(r,"OpenAI Responses",i,ne(a),re(e,t,a),re(e,t,a,{structured:!1}),a);return T(Pt(n).trim(),a)}async function Nt(r,e,t,a){let i=encodeURIComponent(N(a)),n=k(a);/:generateContent(?:\?|$)/.test(n)||(n=`${n.replace(/\/+$/,"")}/models/${i}:generateContent`);let s=ne(a),f=((((await ie(r,"Google Gemini",n,s,ae(e,t,a),ae(e,t,a,{structured:!1}),a)).candidates||[])[0]||{}).content?.parts||[]).map(d=>Me(d)).join("").trim();return T(f,a)}async function J(r,e,t,a){switch(x(a)){case"openai-chat":return Et(r,e,t,a);case"openai-responses":return kt(r,e,t,a);case"google-generative-ai":return Nt(r,e,t,a);case"anthropic-messages":default:return St(r,e,t,a)}}async function tt(r,e){await J(r,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=x(e);return`${C(e).label} / ${v[t].label}`}var P=require("obsidian");function I(r,e,t,a){let i=r.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return i.title=t,typeof P.setIcon=="function"?(0,P.setIcon)(i,e):i.textContent=t,i.addEventListener("click",async n=>{n.preventDefault(),n.stopPropagation();try{await a()}catch(s){console.error(s),new P.Notice(`${t} failed: `+(s.message||s))}}),i}function L(r,e,t,a,i){let n=r.createEl("button",{cls:i||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof P.setIcon=="function"&&(0,P.setIcon)(n,e),n.createSpan({text:t}),n.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await a()}catch(o){console.error(o),new P.Notice(`${t} failed: `+(o.message||o))}}),n}async function R(r,e){try{await navigator.clipboard.writeText(r),new P.Notice(e)}catch(t){new P.Notice("Copy failed: "+(t.message||t))}}var b="parallel-reader-view";async function Ft(r){if(r.backend==="codex"){let e=D("codex",r.cliPath),{stdout:t}=await B(e,["--version"],"",1e4);return`codex @ ${e} -${t.trim()}`}if(r.backend==="claude-code"){let e=D("claude",r.cliPath),{stdout:t}=await B(e,["--version"],"",1e4);return`claude @ ${e} -${t.trim()}`}if(H(r.backend))return tt(c.requestUrl,r);throw new Error("Unknown backend: "+r.backend)}async function Dt(r,e,t){let{system:a,user:i}=Se(r,e),n;switch(e.backend){case"codex":n=await $e(a,i,e,t);break;case"api":n=await J(c.requestUrl,a,i,e);break;case"anthropic-api":n=await J(c.requestUrl,a,i,e);break;case"claude-code":default:n=await Ke(a,i,e,t);break}let s=n.map(o=>({title:o.title,level:2,anchor:o.anchor,gist:o.gist,startLine:z(r,o.anchor),bullets:o.bullets}));return s.sort((o,l)=>o.startLine<0&&l.startLine<0?0:o.startLine<0?1:l.startLine<0?-1:o.startLine-l.startLine),s}function rt(r,e){return e?.phase==="generating"&&H(r?.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var Le=class extends c.Modal{constructor(e,t,a,i){super(e),this.plugin=t,this.card=a||{},this.onSave=i}onOpen(){let{contentEl:e}=this;e.empty(),e.createEl("h2",{text:this.plugin.t("editCardTitle")});let t=this.createLabeledInput(e,this.plugin.t("editCardTitleField"),this.card.title||""),a=this.createLabeledTextarea(e,this.plugin.t("editCardGistField"),this.card.gist||"",3),i=this.createLabeledTextarea(e,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` -`),8),n=e.createDiv({cls:"parallel-reader-modal-actions"});L(n,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),L(n,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:t.value.trim()||this.card.title||"",gist:a.value.trim(),bullets:i.value.split(/\r?\n/).map(s=>s.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(e,t,a){let i=e.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:t});let n=i.createEl("input",{attr:{type:"text"}});return n.value=a,n}createLabeledTextarea(e,t,a,i){let n=e.createDiv({cls:"parallel-reader-modal-field"});n.createEl("label",{text:t});let s=n.createEl("textarea");return s.rows=i,s.value=a,s}},Re=class extends c.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return b}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()}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)?I(i,"square",this.plugin.t("actionCancel"),()=>this.plugin.cancelGenerationForFile(this.sourceFile)):I(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),I(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),I(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let o=e.createDiv({cls:"parallel-reader-stale-banner"});o.createSpan({text:this.plugin.t("staleBanner")}),L(o,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});o.createDiv({cls:"parallel-reader-spinner"}),o.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),o.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});o.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),o.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),L(o,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let n=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let s=this.sourceFile?.path||"";this.sections.forEach((o,l)=>{let p=n.createDiv({cls:"parallel-reader-card"});p.dataset.idx=String(l),o.startLine<0&&p.addClass("parallel-reader-card-unanchored");let f=p.createEl("div",{cls:"parallel-reader-card-title"});if(f.createSpan({text:o.title}),o.startLine<0&&f.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),o.gist){let h=p.createEl("div",{cls:"parallel-reader-gist"});c.MarkdownRenderer.render(this.app,o.gist,h,s,this).catch(()=>{h.setText(o.gist)})}let d=o.bullets||[];if(d.length>0){let h=p.createEl("div",{cls:"parallel-reader-bullets-md"}),m=d.map(u=>`- ${u}`).join(` -`);c.MarkdownRenderer.render(this.app,m,h,s,this).catch(()=>{h.setText(m)})}else o.gist||p.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});p.addEventListener("click",h=>{let m=window.getSelection();if(m&&m.toString().length>0)return;let u=h.target;u&&u.tagName==="A"||o.startLine>=0&&this.plugin.scrollEditorToLine(o.startLine,this.sourceFile)}),p.addEventListener("contextmenu",h=>{h.preventDefault();let m=new c.Menu;m.addItem(u=>u.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>R(ge(o),this.plugin.t("copiedMarkdown")))),m.addItem(u=>u.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>R(ze(o),this.plugin.t("copiedPlain")))),o.anchor&&m.addItem(u=>u.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>R(o.anchor,this.plugin.t("copiedAnchor")))),m.addSeparator(),o.startLine>=0&&m.addItem(u=>u.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(o.startLine,this.sourceFile))),m.addSeparator(),m.addItem(u=>u.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(l))),m.addItem(u=>u.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(l))),m.showAtMouseEvent(h)}),this.cards.push(p)}),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=fe(this.activeIdx,this.sections.length,e);return this.setActiveSection(t),this.focusSummaryPane(),t}jumpToActiveSection(){let e=ye(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=le(this.sections,e);if(t.length===this.sections.length)return!1;let a=this.sections.length;return this.sections=t,this.activeIdx=ue(e,a,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,t),this.render(),new c.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new Le(this.app,this.plugin,this.sections[e],t=>this.updateCard(e,t)).open(),!0)}async updateCard(e,t){if(!this.sourceFile)return!1;let a=de(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 c.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=Ee(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","---","",X(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(` -`),n=this.plugin.app;await Qe(n,e);let s=n.vault.getAbstractFileByPath(a);s instanceof c.TFile?await n.vault.modify(s,i):await n.vault.create(a,i),new c.Notice(this.plugin.t("exported",{path:a}))}},Oe=class extends c.Plugin{t(e,t){return y(this.settings||g,e,t)}async onload(){await this.loadSettings(),this.jobs=new O,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(b,e=>new Re(e,this)),this.addCommand({id:"parallel-reader-run",name:this.t("cmdRun"),callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:this.t("cmdRegen"),callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:this.t("cmdOpenView"),callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:this.t("cmdExport"),callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:this.t("cmdCopyMarkdown"),callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:this.t("cmdCancel"),callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:this.t("cmdClearCurrent"),callback:async()=>{let e=this.getActiveView();if(!e?.file)return new c.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new c.Notice(this.t("cacheClearedFile",{name:e.file.basename}))}}),this.addCommand({id:"parallel-reader-clear-all",name:this.t("cmdClearAll"),callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new c.Notice(this.t("cacheClearedAll",{count:e}))}}),this.addCommand({id:"parallel-reader-card-prev",name:this.t("cmdCardPrev"),hotkeys:[{modifiers:["Alt"],key:"ArrowUp"}],callback:()=>this.moveActiveCard(-1)}),this.addCommand({id:"parallel-reader-card-next",name:this.t("cmdCardNext"),hotkeys:[{modifiers:["Alt"],key:"ArrowDown"}],callback:()=>this.moveActiveCard(1)}),this.addCommand({id:"parallel-reader-card-jump",name:this.t("cmdCardJump"),callback:()=>this.jumpActiveCard()}),this.addSettingTab(new _e(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(b)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=Pe(Object.assign({},g,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this.pruneCache(),await this.writeCacheFile(),this._cacheDirty=!1}scheduleCacheSave(e=5e3){this._cacheDirty=!0,!this._cacheSaveTimer&&(this._cacheSaveTimer=setTimeout(()=>{this._cacheSaveTimer=null,this._cacheDirty&&this.saveCache().catch(t=>console.error("[parallel-reader] failed to save cache",t))},e))}async flushCacheSave(){this._cacheSaveTimer&&(clearTimeout(this._cacheSaveTimer),this._cacheSaveTimer=null),this._cacheDirty&&await this.saveCache()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",t=this.manifest?.id||"parallel-reader";return`${e}/plugins/${t}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,t=this.app.vault.configDir||".obsidian",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(),oe(this.cache))}async loadCache(){let e=await this.readCacheFile();this.cache=e,this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return Te(this.cache,this.settings?.maxCacheEntries||j)}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=se(this.cache[e]||null);return t?(this.scheduleCacheSave(),t):null}async cachePut(e,t,a,i){let n=new Date().toISOString();this.cache[e]={schemaVersion:U,contentHash:M(t),settingsHash:Z(i||this.settings),cards:a,generatedAt:n,lastAccessedAt:n},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(n=>({title:n.title,anchor:n.anchor,gist:n.gist,bullets:n.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(b)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:b,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(c.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(b)[0]?.view}moveActiveCard(e){let t=this.getParallelView();return!t||!t.sections.length?(new c.Notice(this.t("noActiveCard")),-1):t.moveActiveSection(e)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new c.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new c.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.get(e.path),a=rt(this.settings,t),i=this.jobs.cancel(e.path);return new c.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.app.workspace.getLeavesOfType(b)[0]?.view;t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new c.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 c.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 c.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof c.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 l=this.app.workspace.getLeavesOfType(b)[0]?.view;l?.sourceFile?.path===t&&l.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.app.workspace.getLeavesOfType(b)[0]?.view;s?.sourceFile&&(s.sourceFile.path===t||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof c.TFile))return;!!this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let i=this.app.workspace.getLeavesOfType(b)[0]?.view;i?.sourceFile?.path===e.path&&i.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noExportContent"));return}await t.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noCopyContent"));return}await R(X(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new c.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new c.Notice(this.t("openNoteFirst"));return}let a=e.path;if(this.jobs.isRunning(a)){new c.Notice(this.t("alreadyGenerating"));return}if(ce(this.cacheGet(e.path),t)&&!this.confirmRegenerateEditedCards())return new c.Notice(this.t("regenerateCancelled")),!1;let i=null;return this.jobs.start(a,async n=>{n.setPhase("reading");let s=await this.app.vault.read(e);if(n.throwIfCancelled(),!s.trim()){new c.Notice(this.t("emptyNote"));return}if(i=await this.ensureView(),n.throwIfCancelled(),n.setPhase("cache-check"),!t){let d=this.cacheGet(e.path);if(Q(d,s,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await i.loadFor(e,this.resolveCardAnchors(s,d.cards),!1);return}}await i.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||g.maxDocChars;s.length>o&&new c.Notice(this.t("longNoteTruncated",{count:o})),new c.Notice(this.t("generatingNotice")),n.setPhase("generating");let l=await Dt(s,this.settings,n);if(n.throwIfCancelled(),l.length===0){new c.Notice(this.t("noCardsReturned"));return}let p=l.map(d=>({title:d.title,anchor:d.anchor,gist:d.gist,bullets:d.bullets}));n.setPhase("saving"),await this.cachePut(e.path,s,p,this.settings),n.throwIfCancelled(),this.viewIsShowingFile(i,e)&&await i.loadFor(e,l,!1);let f=l.filter(d=>d.startLine<0).length;new c.Notice(this.t("generationDone",{count:l.length,suffix:f?this.t("unanchoredSuffix",{count:f}):""}))}).catch(async n=>{if(n instanceof F){new c.Notice(n.message);return}if(n instanceof w){this.viewIsShowingFile(i,e)&&await i.renderError(e,this.t("cancelledError")),new c.Notice(this.t("cancelled"));return}let s=he(n);console.error(n),this.viewIsShowingFile(i,e)&&await i.renderError(e,n.message||String(n)),new c.Notice(this.t("generationFailed",{kind:s==="unknown"?"":` (${s})`,error:n.message||n}))})}resolveCardAnchors(e,t){let a=(t||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:z(e,i.anchor),bullets:i.bullets||[]}));return a.sort((i,n)=>i.startLine<0&&n.startLine<0?0:i.startLine<0?1:n.startLine<0?-1:i.startLine-n.startLine),a}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(b);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 n=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let s=!Q(i,n,this.settings);await this.cacheTouch(e.path);let o=this.resolveCardAnchors(n,i.cards);await a.loadFor(e,o,s)}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&&a.scrollDOM?a.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let n=Fe(()=>this.handleEditorScroll(e));i.addEventListener("scroll",n,{passive:!0}),this._scrollDispose=()=>{n.cancel(),i.removeEventListener("scroll",n)}}handleEditorScroll(e){let t=this.app.workspace.getLeavesOfType(b);if(t.length===0)return;let a=t[0].view;if(!a||!e.file||a.sourceFile?.path!==e.file.path)return;let i=e.editor,n=i&&i.cm;if(!n)return;let s=n.scrollDOM;if(!s)return;let o=s.getBoundingClientRect(),l=Ne(o),p=0;try{let d=n.posAtCoords({x:o.left+20,y:l});d!=null&&(p=n.state.doc.lineAt(d).number-1)}catch{return}let f=-1;for(let d=0;dthis.plugin.t(n,s);e.empty(),e.createEl("h2",{text:t("settingsTitle")}),new c.Setting(e).setName(t("settingUiLanguageName")).setDesc(t("settingUiLanguageDesc")).addDropdown(n=>{for(let[s,o]of Object.entries(we))n.addOption(s,o);return n.setValue(this.plugin.settings.uiLanguage||g.uiLanguage).onChange(async s=>{this.plugin.settings.uiLanguage=s,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBackendName")).setDesc(t("settingBackendDesc")).addDropdown(n=>n.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async s=>{this.plugin.settings.backend=s,s==="api"&&!this.plugin.settings.apiBaseUrl&&xe(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let a=H(this.plugin.settings.backend);if(!a)new c.Setting(e).setName(t("settingCliPathName")).setDesc(t("settingCliPathDesc")).addText(n=>n.setPlaceholder(t("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async s=>{this.plugin.settings.cliPath=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingCliTimeoutName")).addText(n=>n.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.cliTimeoutMs=o,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:t("apiProviderHeader")});let n=C(this.plugin.settings);new c.Setting(e).setName(t("settingProviderPresetName")).setDesc(t("settingProviderPresetDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(S))s.addOption(o,l.label);return s.setValue(this.plugin.settings.apiProvider).onChange(async o=>{xe(this.plugin.settings,o),await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingApiFormatName")).setDesc(t("settingApiFormatDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(v))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 c.Setting(e).setName(t("settingBaseUrlName")).setDesc(t("settingBaseUrlDesc")).addText(s=>s.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":n.baseUrl||v[x(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async o=>{this.plugin.settings.apiBaseUrl=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingApiKeyName")).setDesc(t("settingApiKeyDesc")).addText(s=>(s.inputEl.type="password",s.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async o=>{this.plugin.settings.apiKey=o.trim(),this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingApiKeyEnvName")).setDesc(t("settingApiKeyEnvDesc")).addText(s=>s.setPlaceholder(n.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async o=>{this.plugin.settings.apiKeyEnvVar=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingAuthTypeName")).setDesc(t("settingAuthTypeDesc")).addDropdown(s=>{for(let[o,l]of Object.entries(ve))s.addOption(o,l);return s.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async o=>{this.plugin.settings.apiAuthType=o,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingHeadersName")).setDesc(t("settingHeadersDesc")).addTextArea(s=>s.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async o=>{this.plugin.settings.apiHeaders=o,this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxTokensName")).addText(s=>s.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async o=>{let l=parseInt(o,10);!isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())}))}new c.Setting(e).setName(t("settingModelName")).setDesc(t(a?"settingModelDescApi":"settingModelDescCli")).addText(n=>n.setPlaceholder(a?C(this.plugin.settings).model||"model-id":g.model).setValue(this.plugin.settings.model).onChange(async s=>{this.plugin.settings.model=s.trim()||(a?"":g.model),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxInputName")).setDesc(t("settingMaxInputDesc")).addText(n=>n.setValue(String(this.plugin.settings.maxDocChars||g.maxDocChars)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>=1e3&&(this.plugin.settings.maxDocChars=o,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:t("promptHeader")}),new c.Setting(e).setName(t("settingPromptLanguageName")).setDesc(t("settingPromptLanguageDesc")).addDropdown(n=>{for(let[s,o]of Object.entries(G))n.addOption(s,o);return n.setValue(this.plugin.settings.promptLanguage||g.promptLanguage).onChange(async s=>{this.plugin.settings.promptLanguage=s,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingCardRangeName")).setDesc(t("settingCardRangeDesc")).addText(n=>n.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||g.minCards)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.minCards=o,this.plugin.settings.maxCardsn.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||g.maxCards)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.maxCards=Math.max(o,this.plugin.settings.minCards||g.minCards),this.plugin.saveSettingsDebounced())})),new c.Setting(e).setName(t("settingCustomPromptName")).setDesc(t("settingCustomPromptDesc")).addTextArea(n=>(n.inputEl.rows=8,n.setPlaceholder(t("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async s=>{this.plugin.settings.customSystemPrompt=s,this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingTestBackendName")).setDesc(t(a?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(n=>n.setButtonText("Test").onClick(async()=>{try{let s=await Ft(this.plugin.settings);new c.Notice(`\u2713 ${s.slice(0,180)}`,8e3)}catch(s){new c.Notice(t("backendTestFailed",{error:s.message}),1e4)}})),new c.Setting(e).setName(t("settingExportFolderName")).setDesc(t("settingExportFolderDesc")).addText(n=>n.setValue(this.plugin.settings.exportFolder).onChange(async s=>{this.plugin.settings.exportFolder=s.trim()||g.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:t("cacheHeader")}),new c.Setting(e).setName(t("settingMaxCacheName")).setDesc(t("settingMaxCacheDesc")).addText(n=>{n.setValue(String(this.plugin.settings.maxCacheEntries||j));let s=async()=>{let o=parseInt(n.getValue(),10);if(Number.isFinite(o)&&o>0){this.plugin.settings.maxCacheEntries=o,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new c.Notice(t("cachePruned",{count:l.length})),this.display()}};return n.inputEl.addEventListener("change",s),n.inputEl.addEventListener("keydown",o=>{o.key==="Enter"&&n.inputEl.blur()}),n});let i=Object.keys(this.plugin.cache).length;new c.Setting(e).setName(t("cachedNotesName",{count:i})).setDesc(t("cachedNotesDesc")).addButton(n=>n.setButtonText(t("clearAllCacheButton")).setWarning().onClick(async()=>{let s=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new c.Notice(t("cacheClearedAll",{count:s})),this.display()}))}},Mt=Oe,It={CACHE_SCHEMA_VERSION:U,GenerationJobAlreadyRunningError:F,GenerationJobCancelledError:w,GenerationJobManager:O,addIconButton:I,addTextButton:L,activeIndexAfterCardDelete:ue,activeSectionLine:ye,buildAnthropicMessagesBody:ee,buildGeminiBody:ae,buildOpenAiChatBody:te,buildOpenAiResponsesBody:re,buildPrompts:Se,cardsToMarkdown:X,cacheEntryMatches:Q,cancellationNoticeKey:rt,classifyGenerationError:he,copyToClipboard:R,createRafThrottledHandler:Fe,extractJson:me,findLineForAnchor:z,folderPathsForTarget:ke,generationFingerprint:Z,getApiBaseUrl:k,modelForApi:N,normalizeCardsPayload:_,nextCardIndex:fe,pruneCacheEntries:Te,removeCardAt:le,resolveCliPath:D,serializeCacheFile:oe,shouldConfirmRegenerate:ce,summarizeViaApi:J,touchCacheEntry:se,translate:y,tokenLimitFieldForOpenAiChat:Ie,updateCardAt:de,visibleTopProbeY:Ne};0&&(module.exports={__test}); +${s}`;return{system:l,user:F}}function Ie(r,e=80,t=.1){let a=Number(r?.top)||0,n=Math.max(0,Number(r?.height)||0),i=Math.min(Number(e)||0,n*t);return a+Math.max(0,i)}function vt(r){return typeof requestAnimationFrame=="function"?requestAnimationFrame(r):setTimeout(()=>r(Date.now()),16)}function At(r){if(typeof cancelAnimationFrame=="function"){cancelAnimationFrame(r);return}clearTimeout(r)}function Me(r,e=vt,t=At){let a=null,n=()=>{a===null&&(a=e(()=>{a=null,r()}))};return n.cancel=()=>{a!==null&&(t(a),a=null)},n}function Le(r,e){let t=r.replace(/\/+$/,"");for(let a of e)if(t.endsWith(a))return t;return t+e[0]}function xt(r,e){let t=(r||"").trim();if(!t)return{};if(t.startsWith("{")){let n;try{n=JSON.parse(t)}catch(o){throw new Error(C(e,"errorCustomHeadersJsonParse",{error:o.message}))}if(!n||typeof n!="object"||Array.isArray(n))throw new Error(C(e,"errorCustomHeadersJsonObject"));let i={};for(let[o,s]of Object.entries(n))typeof s=="string"&&o.trim()&&(i[o.trim()]=s);return i}let a={};for(let n of t.split(/\r?\n/)){let i=n.trim();if(!i||i.startsWith("#"))continue;let o=i.indexOf(":");if(o<=0)throw new Error(C(e,"errorCustomHeadersLineFormat"));let s=i.slice(0,o).trim(),c=i.slice(o+1).trim();s&&(a[s]=c)}return a}function Pt(r){let e=Ee(r);if(e==="none")return{};let t=Qe(r);if(!t){let a=(r.apiKeyEnvVar||b(r).envVar||"").trim(),n=a?C(r,"errorApiKeyEnvHint",{envVar:a}):"";throw new Error(C(r,"errorApiKeyMissing",{hint:n}))}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function se(r,e){return{"content-type":"application/json",...Pt(r),...e||{},...xt(r.apiHeaders,r)}}function Tt(r,e,t){if(r.json&&typeof r.json=="object")return r.json;try{return JSON.parse(r.text||"{}")}catch{throw new Error(C(t,"errorProviderNonJson",{label:e,excerpt:(r.text||"").slice(0,500)}))}}async function tt(r,e,t,a,n,i){let o;try{o=await r({url:t,method:"POST",headers:a,body:JSON.stringify(n),throw:!1})}catch(s){throw new Error(C(i,"errorProviderRequestFailed",{label:e,error:s.message||s}))}if(o.status>=400)throw new Error(C(i,"errorProviderApiStatus",{label:e,status:o.status,excerpt:(o.text||"").slice(0,500)}));return Tt(o,e,i)}function St(r){let e=String(r&&r.message?r.message:r);return/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function oe(r,e,t,a,n,i,o){try{return await tt(r,e,t,a,n,o)}catch(s){if(!i||!St(s))throw s;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,s),tt(r,e+" fallback",t,a,i,o)}}function Re(r){return typeof r=="string"?r:Array.isArray(r)?r.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):r&&typeof r=="object"&&(r.text||r.output_text)||""}function Et(r){if(typeof r.output_text=="string")return r.output_text;let e=[],t=a=>{if(a&&typeof a!="string"){if(Array.isArray(a)){a.forEach(t);return}typeof a=="object"&&(typeof a.text=="string"&&e.push(a.text),typeof a.output_text=="string"&&e.push(a.output_text),a.type==="output_text"&&typeof a.content=="string"&&e.push(a.content),a.content&&t(a.content),a.output&&t(a.output))}};return t(r.output),e.join("")}function Oe(r){let e=b(r),t=A[P(r)];return e.tokenLimitField||t&&t.tokenLimitField||"max_tokens"}function re(r,e,t,a){let n=!a||a.structured!==!1,i={model:M(t),max_tokens:Number(t.apiMaxTokens)||4096,system:r,messages:[{role:"user",content:e}]};return n&&(i.tools=[Je()],i.tool_choice={type:"tool",name:Z}),i}function ae(r,e,t,a){let n=!a||a.structured!==!1,i={model:M(t),messages:[{role:"system",content:r},{role:"user",content:e}]};return i[Oe(t)]=Number(t.apiMaxTokens)||4096,n&&(i.response_format=Ge()),i}function ne(r,e,t,a){let n=!a||a.structured!==!1,i={model:M(t),instructions:r,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return n&&(i.text=He()),i}function ie(r,e,t,a){let n=!a||a.structured!==!1,i={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return n&&(i.responseMimeType="application/json",i.responseJsonSchema=G(!1)),{systemInstruction:{parts:[{text:r}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:i}}function kt(r,e){let a=(Array.isArray(r&&r.content)?r.content:[]).find(n=>n&&n.type==="tool_use"&&n.name===Z);return a?typeof a.input=="string"?S(a.input,e):a.input&&typeof a.input=="object"?j(a.input):[]:null}async function Nt(r,e,t,a){let n=Le(I(a),["/messages"]),i=await oe(r,"Anthropic-compatible",n,se(a,{"anthropic-version":"2023-06-01"}),re(e,t,a),re(e,t,a,{structured:!1}),a),o=kt(i,a);if(o)return o;let s=(i.content||[]).map(c=>Re(c)).join("").trim();return S(s,a)}async function Ft(r,e,t,a){let n=Le(I(a),["/chat/completions"]),o=((await oe(r,"OpenAI-compatible Chat",n,se(a),ae(e,t,a),ae(e,t,a,{structured:!1}),a)).choices||[])[0]||{},s=Re(o.message?.content||o.text||"").trim();return S(s,a)}async function Dt(r,e,t,a){let n=Le(I(a),["/responses"]),i=await oe(r,"OpenAI Responses",n,se(a),ne(e,t,a),ne(e,t,a,{structured:!1}),a);return S(Et(i).trim(),a)}async function It(r,e,t,a){let n=encodeURIComponent(M(a)),i=I(a);/:generateContent(?:\?|$)/.test(i)||(i=`${i.replace(/\/+$/,"")}/models/${n}:generateContent`);let o=se(a),g=((((await oe(r,"Google Gemini",i,o,ie(e,t,a),ie(e,t,a,{structured:!1}),a)).candidates||[])[0]||{}).content?.parts||[]).map(y=>Re(y)).join("").trim();return S(g,a)}async function z(r,e,t,a){switch(P(a)){case"openai-chat":return Ft(r,e,t,a);case"openai-responses":return Dt(r,e,t,a);case"google-generative-ai":return It(r,e,t,a);case"anthropic-messages":default:return Nt(r,e,t,a)}}async function rt(r,e){await z(r,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=P(e);return`${b(e).label} / ${A[t].label}`}var T=require("obsidian");function L(r,e,t,a){let n=r.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return n.title=t,typeof T.setIcon=="function"?(0,T.setIcon)(n,e):n.textContent=t,n.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await a()}catch(o){console.error(o),new T.Notice(`${t} failed: `+(o.message||o))}}),n}function k(r,e,t,a,n){let i=r.createEl("button",{cls:n||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof T.setIcon=="function"&&(0,T.setIcon)(i,e),i.createSpan({text:t}),i.addEventListener("click",async o=>{o.preventDefault(),o.stopPropagation();try{await a()}catch(s){console.error(s),new T.Notice(`${t} failed: `+(s.message||s))}}),i}async function R(r,e){try{await navigator.clipboard.writeText(r),new T.Notice(e)}catch(t){new T.Notice("Copy failed: "+(t.message||t))}}function ce(r,e){let t=Array.isArray(r)?r.slice():[];return!Number.isInteger(e)||e<0||e>=t.length||t.splice(e,1),t}function le(r,e,t){if(!Number.isInteger(r)||!Number.isInteger(e)||e<=0||!Number.isInteger(t)||t<0||r<0||r>=e)return t;let a=e-1;return a<=0?-1:r=a.length||(a[e]=Object.assign({},a[e],t||{})),a}function de(r,e,t){if(!Number.isFinite(e)||e<=0)return-1;let a=Math.floor(e),n=t<0?-1:1;return r<0||r>=a?n<0?a-1:0:Math.min(a-1,Math.max(0,r+n))}function pe(r,e){if(!Array.isArray(r)||e<0||e>=r.length)return-1;let t=Number(r[e]?.startLine);return Number.isFinite(t)&&t>=0?t:-1}function _e(r){return String(r||"").split("/").map(e=>e.trim()).filter(Boolean).join("/")}function Ve(r){let e=_e(r);if(!e)return[];let t=e.split("/");return t.map((a,n)=>t.slice(0,n+1).join("/"))}async function at(r,e){for(let t of Ve(e))if(!r.vault.getAbstractFileByPath(t))try{await r.vault.createFolder(t)}catch(a){if(!r.vault.getAbstractFileByPath(t))throw a}}var w=require("obsidian");var nt=require("obsidian");var he=class extends nt.Modal{constructor(e,t,a,n){super(e),this.plugin=t,this.card=a||{},this.onSave=n}onOpen(){let{contentEl:e}=this;e.empty(),e.createEl("h2",{text:this.plugin.t("editCardTitle")});let t=this.createLabeledInput(e,this.plugin.t("editCardTitleField"),this.card.title||""),a=this.createLabeledTextarea(e,this.plugin.t("editCardGistField"),this.card.gist||"",3),n=this.createLabeledTextarea(e,this.plugin.t("editCardBulletsField"),(this.card.bullets||[]).join(` +`),8),i=e.createDiv({cls:"parallel-reader-modal-actions"});k(i,null,this.plugin.t("editCardCancel"),()=>this.close(),"parallel-reader-text-button"),k(i,null,this.plugin.t("editCardSave"),async()=>{await this.onSave({title:t.value.trim()||this.card.title||"",gist:a.value.trim(),bullets:n.value.split(/\r?\n/).map(o=>o.trim()).filter(Boolean)}),this.close()},"parallel-reader-text-button")}createLabeledInput(e,t,a){let n=e.createDiv({cls:"parallel-reader-modal-field"});n.createEl("label",{text:t});let i=n.createEl("input",{attr:{type:"text"}});return i.value=a,i}createLabeledTextarea(e,t,a,n){let i=e.createDiv({cls:"parallel-reader-modal-field"});i.createEl("label",{text:t});let o=i.createEl("textarea");return o.rows=n,o.value=a,o}};var N="parallel-reader-view",me=class extends w.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return 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",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()}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 n=a.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?L(n,"square",this.plugin.t("actionCancel"),()=>this.plugin.cancelGenerationForFile(this.sourceFile)):L(n,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),L(n,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),L(n,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let s=e.createDiv({cls:"parallel-reader-stale-banner"});s.createSpan({text:this.plugin.t("staleBanner")}),k(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});s.createDiv({cls:"parallel-reader-spinner"}),s.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});s.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),k(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let i=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((s,c)=>{let d=i.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(c),s.startLine<0&&d.addClass("parallel-reader-card-unanchored");let g=d.createEl("div",{cls:"parallel-reader-card-title"});if(g.createSpan({text:s.title}),s.startLine<0&&g.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),s.gist){let f=d.createEl("div",{cls:"parallel-reader-gist"});w.MarkdownRenderer.render(this.app,s.gist,f,o,this).catch(()=>{f.setText(s.gist)})}let y=s.bullets||[];if(y.length>0){let f=d.createEl("div",{cls:"parallel-reader-bullets-md"}),h=y.map(l=>`- ${l}`).join(` +`);w.MarkdownRenderer.render(this.app,h,f,o,this).catch(()=>{f.setText(h)})}else s.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",f=>{let h=window.getSelection();if(h&&h.toString().length>0)return;let l=f.target;l&&l.tagName==="A"||s.startLine>=0&&this.plugin.scrollEditorToLine(s.startLine,this.sourceFile)}),d.addEventListener("contextmenu",f=>{f.preventDefault();let h=new w.Menu;h.addItem(l=>l.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>R(Ae(s),this.plugin.t("copiedMarkdown")))),h.addItem(l=>l.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>R(We(s),this.plugin.t("copiedPlain")))),s.anchor&&h.addItem(l=>l.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>R(s.anchor,this.plugin.t("copiedAnchor")))),h.addSeparator(),s.startLine>=0&&h.addItem(l=>l.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(s.startLine,this.sourceFile))),h.addSeparator(),h.addItem(l=>l.setTitle(this.plugin.t("menuEditCard")).setIcon("pencil").onClick(()=>this.openEditCardModal(c))),h.addItem(l=>l.setTitle(this.plugin.t("menuDeleteCard")).setIcon("trash").onClick(()=>this.deleteCard(c))),h.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=de(this.activeIdx,this.sections.length,e);return this.setActiveSection(t),this.focusSummaryPane(),t}jumpToActiveSection(){let e=pe(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=ce(this.sections,e);if(t.length===this.sections.length)return!1;let a=this.sections.length;return this.sections=t,this.activeIdx=le(e,a,this.activeIdx),await this.plugin.cacheReplaceCards(this.sourceFile.path,t),this.render(),new w.Notice(this.plugin.t("cardDeleted")),!0}openEditCardModal(e){return!this.sourceFile||!this.sections[e]?!1:(new he(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=ue(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 w.Notice(this.plugin.t("cardSaved")),!0)}async exportToVault(){if(!this.sourceFile)return;let e=_e(this.plugin.settings.exportFolder),t=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,a=`${e}/${t}`,n=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",J(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(` +`),i=this.plugin.app;await at(i,e);let o=i.vault.getAbstractFileByPath(a);o instanceof w.TFile?await i.vault.modify(o,n):await i.vault.create(a,n),new w.Notice(this.plugin.t("exported",{path:a}))}};var p=require("obsidian");async function Mt(r){if(r.backend==="codex"){let e=D("codex",r.cliPath),{stdout:t}=await H(e,["--version"],"",1e4);return`codex @ ${e} +${t.trim()}`}if(r.backend==="claude-code"){let e=D("claude",r.cliPath),{stdout:t}=await H(e,["--version"],"",1e4);return`claude @ ${e} +${t.trim()}`}if(B(r.backend))return rt(p.requestUrl,r);throw new Error("Unknown backend: "+r.backend)}var ge=class extends p.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this,t=(i,o)=>this.plugin.t(i,o);e.empty(),e.createEl("h2",{text:t("settingsTitle")}),new p.Setting(e).setName(t("settingUiLanguageName")).setDesc(t("settingUiLanguageDesc")).addDropdown(i=>{for(let[o,s]of Object.entries(Te))i.addOption(o,s);return i.setValue(this.plugin.settings.uiLanguage||m.uiLanguage).onChange(async o=>{this.plugin.settings.uiLanguage=o,await this.plugin.saveSettings(),this.display()})}),new p.Setting(e).setName(t("settingBackendName")).setDesc(t("settingBackendDesc")).addDropdown(i=>i.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async o=>{this.plugin.settings.backend=o,o==="api"&&!this.plugin.settings.apiBaseUrl&&ke(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let a=B(this.plugin.settings.backend);if(!a)new p.Setting(e).setName(t("settingCliPathName")).setDesc(t("settingCliPathDesc")).addText(i=>i.setPlaceholder(t("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async o=>{this.plugin.settings.cliPath=o.trim(),this.plugin.saveSettingsDebounced()})),new p.Setting(e).setName(t("settingCliTimeoutName")).addText(i=>i.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.cliTimeoutMs=s,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:t("apiProviderHeader")});let i=b(this.plugin.settings);new p.Setting(e).setName(t("settingProviderPresetName")).setDesc(t("settingProviderPresetDesc")).addDropdown(o=>{for(let[s,c]of Object.entries(E))o.addOption(s,c.label);return o.setValue(this.plugin.settings.apiProvider).onChange(async s=>{ke(this.plugin.settings,s),await this.plugin.saveSettings(),this.display()})}),new p.Setting(e).setName(t("settingApiFormatName")).setDesc(t("settingApiFormatDesc")).addDropdown(o=>{for(let[s,c]of Object.entries(A))o.addOption(s,c.label);return o.setValue(P(this.plugin.settings)).onChange(async s=>{this.plugin.settings.apiFormat=s,await this.plugin.saveSettings(),this.display()})}),new p.Setting(e).setName(t("settingBaseUrlName")).setDesc(t("settingBaseUrlDesc")).addText(o=>o.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":i.baseUrl||A[P(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async s=>{this.plugin.settings.apiBaseUrl=s.trim(),this.plugin.saveSettingsDebounced()})),new p.Setting(e).setName(t("settingApiKeyName")).setDesc(t("settingApiKeyDesc")).addText(o=>(o.inputEl.type="password",o.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async s=>{this.plugin.settings.apiKey=s.trim(),this.plugin.saveSettingsDebounced()}))),new p.Setting(e).setName(t("settingApiKeyEnvName")).setDesc(t("settingApiKeyEnvDesc")).addText(o=>o.setPlaceholder(i.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async s=>{this.plugin.settings.apiKeyEnvVar=s.trim(),this.plugin.saveSettingsDebounced()})),new p.Setting(e).setName(t("settingAuthTypeName")).setDesc(t("settingAuthTypeDesc")).addDropdown(o=>{for(let[s,c]of Object.entries(Se))o.addOption(s,c);return o.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async s=>{this.plugin.settings.apiAuthType=s,await this.plugin.saveSettings()})}),new p.Setting(e).setName(t("settingHeadersName")).setDesc(t("settingHeadersDesc")).addTextArea(o=>o.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async s=>{this.plugin.settings.apiHeaders=s,this.plugin.saveSettingsDebounced()})),new p.Setting(e).setName(t("settingMaxTokensName")).addText(o=>o.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async s=>{let c=parseInt(s,10);!isNaN(c)&&c>0&&(this.plugin.settings.apiMaxTokens=c,this.plugin.saveSettingsDebounced())}))}new p.Setting(e).setName(t("settingModelName")).setDesc(t(a?"settingModelDescApi":"settingModelDescCli")).addText(i=>i.setPlaceholder(a?b(this.plugin.settings).model||"model-id":m.model).setValue(this.plugin.settings.model).onChange(async o=>{this.plugin.settings.model=o.trim()||(a?"":m.model),this.plugin.saveSettingsDebounced()})),new p.Setting(e).setName(t("settingMaxInputName")).setDesc(t("settingMaxInputDesc")).addText(i=>i.setValue(String(this.plugin.settings.maxDocChars||m.maxDocChars)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>=1e3&&(this.plugin.settings.maxDocChars=s,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:t("promptHeader")}),new p.Setting(e).setName(t("settingPromptLanguageName")).setDesc(t("settingPromptLanguageDesc")).addDropdown(i=>{for(let[o,s]of Object.entries($))i.addOption(o,s);return i.setValue(this.plugin.settings.promptLanguage||m.promptLanguage).onChange(async o=>{this.plugin.settings.promptLanguage=o,await this.plugin.saveSettings()})}),new p.Setting(e).setName(t("settingCardRangeName")).setDesc(t("settingCardRangeDesc")).addText(i=>i.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||m.minCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.minCards=s,this.plugin.settings.maxCardsi.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||m.maxCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.maxCards=Math.max(s,this.plugin.settings.minCards||m.minCards),this.plugin.saveSettingsDebounced())})),new p.Setting(e).setName(t("settingCustomPromptName")).setDesc(t("settingCustomPromptDesc")).addTextArea(i=>(i.inputEl.rows=8,i.setPlaceholder(t("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async o=>{this.plugin.settings.customSystemPrompt=o,this.plugin.saveSettingsDebounced()}))),new p.Setting(e).setName(t("settingTestBackendName")).setDesc(t(a?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(i=>i.setButtonText("Test").onClick(async()=>{try{let o=await Mt(this.plugin.settings);new p.Notice(`\u2713 ${o.slice(0,180)}`,8e3)}catch(o){new p.Notice(t("backendTestFailed",{error:o.message}),1e4)}})),new p.Setting(e).setName(t("settingExportFolderName")).setDesc(t("settingExportFolderDesc")).addText(i=>i.setValue(this.plugin.settings.exportFolder).onChange(async o=>{this.plugin.settings.exportFolder=o.trim()||m.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:t("cacheHeader")}),new p.Setting(e).setName(t("settingMaxCacheName")).setDesc(t("settingMaxCacheDesc")).addText(i=>{i.setValue(String(this.plugin.settings.maxCacheEntries||V));let o=async()=>{let s=parseInt(i.getValue(),10);if(Number.isFinite(s)&&s>0){this.plugin.settings.maxCacheEntries=s,await this.plugin.saveSettings();let c=await this.plugin.pruneCacheIfNeeded();c.length>0&&new p.Notice(t("cachePruned",{count:c.length})),this.display()}};return i.inputEl.addEventListener("change",o),i.inputEl.addEventListener("keydown",s=>{s.key==="Enter"&&i.inputEl.blur()}),i});let n=Object.keys(this.plugin.cache).length;new p.Setting(e).setName(t("cachedNotesName",{count:n})).setDesc(t("cachedNotesDesc")).addButton(i=>i.setButtonText(t("clearAllCacheButton")).setWarning().onClick(async()=>{let o=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new p.Notice(t("cacheClearedAll",{count:o})),this.display()}))}};async function Lt(r,e,t){let{system:a,user:n}=De(r,e),i;switch(e.backend){case"codex":i=await qe(a,n,e,t);break;case"api":i=await z(u.requestUrl,a,n,e);break;case"anthropic-api":i=await z(u.requestUrl,a,n,e);break;case"claude-code":default:i=await ze(a,n,e,t);break}let o=i.map(s=>({title:s.title,level:2,anchor:s.anchor,gist:s.gist,startLine:Y(r,s.anchor),bullets:s.bullets}));return o.sort((s,c)=>s.startLine<0&&c.startLine<0?0:s.startLine<0?1:c.startLine<0?-1:s.startLine-c.startLine),o}function it(r,e){return e?.phase==="generating"&&B(r?.backend)?"cancelRequestedApiInFlight":"cancelRequested"}var Be=class extends u.Plugin{t(e,t){return C(this.settings||m,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(N,e=>new me(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 u.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new u.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 u.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 ge(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(N)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=Ne(Object.assign({},m,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this._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",n=`${t}/plugins/${a}`;try{if(typeof e.exists=="function"&&await e.exists(n))return;await e.mkdir(n)}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(),ye(this.cache))}async loadCache(){this.cache=await this.readCacheFile(),this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return Fe(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=fe(this.cache[e]||null);return t?(this.scheduleCacheSave(),t):null}async cachePut(e,t,a,n){let i=new Date().toISOString();this.cache[e]={schemaVersion:K,contentHash:_(t),settingsHash:ee(n||this.settings),cards:a,generatedAt:i,lastAccessedAt:i},await this.saveCache()}async cacheReplaceCards(e,t){let a=this.cache[e];if(!a)return!1;let n=new Date().toISOString();return a.cards=(t||[]).map(i=>({title:i.title,anchor:i.anchor,gist:i.gist,bullets:i.bullets||[]})),a.updatedAt=n,a.lastAccessedAt=n,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(N)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:N,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(u.MarkdownView)}getParallelView(){return this.app.workspace.getLeavesOfType(N)[0]?.view}moveActiveCard(e){let t=this.getParallelView();return!t||!t.sections.length?(new u.Notice(this.t("noActiveCard")),-1):t.moveActiveSection(e)}jumpActiveCard(){let e=this.getParallelView();return!e||e.jumpToActiveSection()<0?(new u.Notice(this.t("noActiveCard")),!1):!0}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new u.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.get(e.path),a=it(this.settings,t),n=this.jobs.cancel(e.path);return new u.Notice(n?this.t(a):this.t("noCancelableJob")),n}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 u.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 u.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 u.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof u.TFile)||!t)return;let a=t.endsWith(".md"),n=e.path.endsWith(".md");if(!a&&!n)return;if(a&&!n){this.cache[t]&&(delete this.cache[t],await this.saveCache());let o=this.getParallelView();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 i=this.getParallelView();i?.sourceFile&&(i.sourceFile.path===t||i.sourceFile.path===e.path)&&(i.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof u.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 u.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 u.Notice(this.t("noCopyContent"));return}await R(J(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new u.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new u.Notice(this.t("openNoteFirst"));return}if(this.jobs.isRunning(e.path)){new u.Notice(this.t("alreadyGenerating"));return}if(Ce(this.cacheGet(e.path),t)&&!this.confirmRegenerateEditedCards()){new u.Notice(this.t("regenerateCancelled"));return}let a=null;return this.jobs.start(e.path,async n=>{n.setPhase("reading");let i=await this.app.vault.read(e);if(n.throwIfCancelled(),!i.trim()){new u.Notice(this.t("emptyNote"));return}if(a=await this.ensureView(),n.throwIfCancelled(),n.setPhase("cache-check"),!t){let g=this.cacheGet(e.path);if(te(g,i,this.settings)){await this.cacheTouch(e.path),this.activeFileStillMatches(e)&&await a.loadFor(e,this.resolveCardAnchors(i,g.cards),!1);return}}await a.renderLoading(e,this.t("loadingGenerating"));let o=Number(this.settings.maxDocChars)||m.maxDocChars;i.length>o&&new u.Notice(this.t("longNoteTruncated",{count:o})),new u.Notice(this.t("generatingNotice")),n.setPhase("generating");let s=await Lt(i,this.settings,n);if(n.throwIfCancelled(),s.length===0){new u.Notice(this.t("noCardsReturned"));return}let c=s.map(g=>({title:g.title,anchor:g.anchor,gist:g.gist,bullets:g.bullets}));n.setPhase("saving"),await this.cachePut(e.path,i,c,this.settings),n.throwIfCancelled(),this.viewIsShowingFile(a,e)&&await a.loadFor(e,s,!1);let d=s.filter(g=>g.startLine<0).length;new u.Notice(this.t("generationDone",{count:s.length,suffix:d?this.t("unanchoredSuffix",{count:d}):""}))}).catch(async n=>{if(n instanceof O){new u.Notice(n.message);return}if(n instanceof v){this.viewIsShowingFile(a,e)&&await a.renderError(e,this.t("cancelledError")),new u.Notice(this.t("cancelled"));return}let i=we(n);console.error(n),this.viewIsShowingFile(a,e)&&await a.renderError(e,n.message||String(n)),new u.Notice(this.t("generationFailed",{kind:i==="unknown"?"":` (${i})`,error:n.message||n}))})}resolveCardAnchors(e,t){let a=(t||[]).map(n=>({title:n.title,level:2,anchor:n.anchor,gist:n.gist,startLine:Y(e,n.anchor),bullets:n.bullets||[]}));return a.sort((n,i)=>n.startLine<0&&i.startLine<0?0:n.startLine<0?1:i.startLine<0?-1:n.startLine-i.startLine),a}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(N);if(t.length===0)return;let a=t[0].view;if(!a||!this.activeFileStillMatches(e))return;let n=this.cacheGet(e.path);if(!n){await a.loadFor(e,[],!1),a.renderEmptyWithHint(e);return}let i=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!te(n,i,this.settings);await this.cacheTouch(e.path),await a.loadFor(e,this.resolveCardAnchors(i,n.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,n=a&&a.scrollDOM?a.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!n)return;let i=Me(()=>this.handleEditorScroll(e));n.addEventListener("scroll",i,{passive:!0}),this._scrollDispose=()=>{i.cancel(),n.removeEventListener("scroll",i)}}handleEditorScroll(e){let t=this.getParallelView();if(!t||!e.file||t.sourceFile?.path!==e.file.path)return;let a=e.editor,n=a&&a.cm;if(!n||!n.scrollDOM)return;let i=n.scrollDOM.getBoundingClientRect(),o=Ie(i),s=0;try{let d=n.posAtCoords({x:i.left+20,y:o});d!=null&&(s=n.state.doc.lineAt(d).number-1)}catch{return}let c=-1;for(let d=0;d ({ + const resolved: ResolvedCard[] = cards.map(c => ({ title: c.title, level: 2, anchor: c.anchor, @@ -115,394 +88,6 @@ function cancellationNoticeKey(settings, job) { return 'cancelRequested'; } -class CardEditModal extends Modal { - plugin: ParallelReaderPlugin; - card: ResolvedCard; - onSave: (patch: CardPatch) => void | Promise; - - constructor(app, plugin, card, onSave) { - super(app); - this.plugin = plugin; - this.card = card || {}; - this.onSave = onSave; - } - - onOpen() { - const { contentEl } = this; - contentEl.empty(); - contentEl.createEl('h2', { text: this.plugin.t('editCardTitle') }); - - const titleInput = this.createLabeledInput(contentEl, this.plugin.t('editCardTitleField'), this.card.title || ''); - const gistInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardGistField'), this.card.gist || '', 3); - const bulletsInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardBulletsField'), (this.card.bullets || []).join('\n'), 8); - - const actions = contentEl.createDiv({ cls: 'parallel-reader-modal-actions' }); - addTextButton(actions, null, this.plugin.t('editCardCancel'), () => this.close(), 'parallel-reader-text-button'); - addTextButton(actions, null, this.plugin.t('editCardSave'), async () => { - await this.onSave({ - title: titleInput.value.trim() || this.card.title || '', - gist: gistInput.value.trim(), - bullets: bulletsInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean), - }); - this.close(); - }, 'parallel-reader-text-button'); - } - - createLabeledInput(parent, label, value) { - const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' }); - wrapper.createEl('label', { text: label }); - const input = wrapper.createEl('input', { attr: { type: 'text' } }); - input.value = value; - return input; - } - - createLabeledTextarea(parent, label, value, rows) { - const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' }); - wrapper.createEl('label', { text: label }); - const textarea = wrapper.createEl('textarea'); - textarea.rows = rows; - textarea.value = value; - return textarea; - } -} - -/* ---------- Right-pane view ---------- */ - -class ParallelReaderView extends ItemView { - plugin: ParallelReaderPlugin; - sections: ResolvedCard[]; - sourceFile: TFile | null; - cards: HTMLElement[]; - activeIdx: number; - stale: boolean; - loadingMessage: string; - errorMessage: string; - - constructor(leaf, plugin) { - super(leaf); - this.plugin = plugin; - this.sections = []; // [{title, level, anchor, startLine, bullets}] - this.sourceFile = null; - this.cards = []; // DOM refs - this.activeIdx = -1; - this.loadingMessage = ''; - this.errorMessage = ''; - } - - getViewType() { return VIEW_TYPE_PARALLEL; } - getDisplayText() { return this.plugin.t('displayName'); } - getIcon() { return 'book-open'; } - - onOpen() { - const container = this.containerEl.children[1]; - container.empty(); - container.addClass('parallel-reader-container'); - container.setAttr('tabindex', '0'); - container.addEventListener('keydown', e => this.handleKeydown(e as KeyboardEvent)); - this.renderEmpty(); - this.focusSummaryPane(); - return Promise.resolve(); - } - - onClose() { - return Promise.resolve(); - } - - renderEmpty() { - this.sourceFile = null; - this.sections = []; - this.stale = false; - this.loadingMessage = ''; - this.errorMessage = ''; - const container = this.containerEl.children[1]; - container.empty(); - const hint = container.createDiv({ cls: 'parallel-reader-empty' }); - hint.createEl('h3', { text: this.plugin.t('appTitle') }); - hint.createEl('p', { text: this.plugin.t('emptyOpenNote') }); - hint.createEl('code', { text: this.plugin.t('commandGenerate') }); - } - - focusSummaryPane() { - const container = this.containerEl.children[1] as HTMLElement; - if (!container || typeof container.focus !== 'function') return false; - container.focus({ preventScroll: true }); - return true; - } - - async loadFor(file, sections, stale) { - this.sourceFile = file; - this.sections = sections; - this.stale = !!stale; - this.loadingMessage = ''; - this.errorMessage = ''; - this.render(); - } - - async renderLoading(file, message) { - this.sourceFile = file; - this.sections = []; - this.stale = false; - this.loadingMessage = message || this.plugin.t('loadingDefault'); - this.errorMessage = ''; - this.render(); - } - - async renderError(file, message) { - this.sourceFile = file; - this.sections = []; - this.stale = false; - this.loadingMessage = ''; - this.errorMessage = message || this.plugin.t('errorTitle'); - this.render(); - } - - renderEmptyWithHint(file) { - this.sourceFile = file; - this.sections = []; - this.stale = false; - this.loadingMessage = ''; - this.errorMessage = ''; - const container = this.containerEl.children[1]; - container.empty(); - const hint = container.createDiv({ cls: 'parallel-reader-empty' }); - hint.createEl('h3', { text: file.basename }); - hint.createEl('p', { text: this.plugin.t('emptyNoCache') }); - hint.createEl('code', { text: this.plugin.t('commandGenerate') }); - } - - render() { - const container = this.containerEl.children[1]; - container.empty(); - - const header = container.createDiv({ cls: 'parallel-reader-header' }); - const headerRow = header.createDiv({ cls: 'parallel-reader-header-row' }); - headerRow.createEl('div', { text: this.sourceFile?.basename || '', cls: 'parallel-reader-title' }); - const actions = headerRow.createDiv({ cls: 'parallel-reader-actions' }); - if (this.sourceFile) { - if (this.plugin.isGeneratingFile(this.sourceFile)) { - addIconButton(actions, 'square', this.plugin.t('actionCancel'), () => this.plugin.cancelGenerationForFile(this.sourceFile)); - } else { - addIconButton(actions, 'refresh-cw', this.plugin.t('actionRegenerate'), () => this.plugin.runForFile(this.sourceFile, true)); - } - addIconButton(actions, 'copy', this.plugin.t('actionCopyAll'), () => this.plugin.copyCurrentViewMarkdown()); - addIconButton(actions, 'download', this.plugin.t('actionExport'), () => this.exportToVault()); - } - - if (this.stale) { - const banner = container.createDiv({ cls: 'parallel-reader-stale-banner' }); - banner.createSpan({ text: this.plugin.t('staleBanner') }); - addTextButton( - banner, - 'refresh-cw', - this.plugin.t('actionRegenerate'), - () => this.plugin.runForFile(this.sourceFile, true), - 'parallel-reader-stale-button' - ); - } - - if (this.loadingMessage) { - const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-loading' }); - state.createDiv({ cls: 'parallel-reader-spinner' }); - state.createEl('div', { text: this.loadingMessage, cls: 'parallel-reader-state-title' }); - state.createEl('div', { text: this.plugin.t('loadingSubtitle'), cls: 'parallel-reader-state-subtitle' }); - return; - } - - if (this.errorMessage) { - const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-error' }); - state.createEl('div', { text: this.plugin.t('errorTitle'), cls: 'parallel-reader-state-title' }); - state.createEl('div', { text: this.errorMessage, cls: 'parallel-reader-state-subtitle' }); - addTextButton( - state, - 'refresh-cw', - this.plugin.t('actionRegenerate'), - () => this.plugin.runForFile(this.sourceFile, true), - 'parallel-reader-text-button' - ); - return; - } - - const list = container.createDiv({ cls: 'parallel-reader-cards' }); - this.cards = []; - const sourcePath = this.sourceFile?.path || ''; - this.sections.forEach((s, i) => { - const card = list.createDiv({ cls: 'parallel-reader-card' }); - card.dataset.idx = String(i); - if (s.startLine < 0) card.addClass('parallel-reader-card-unanchored'); - - // --- Title row --- - const title = card.createEl('div', { cls: 'parallel-reader-card-title' }); - title.createSpan({ text: s.title }); - if (s.startLine < 0) { - title.createEl('span', { text: ' ⚠', cls: 'parallel-reader-warn', title: this.plugin.t('anchorMismatch') }); - } - - // --- Gist (rendered as markdown so inline bold/code/links work) --- - if (s.gist) { - const gistEl = card.createEl('div', { cls: 'parallel-reader-gist' }); - MarkdownRenderer.render(this.app, s.gist, gistEl, sourcePath, this).catch(() => { - gistEl.setText(s.gist); - }); - } - - // --- Bullets (rendered as markdown list — handles tables, bold, etc.) --- - const bs = s.bullets || []; - if (bs.length > 0) { - const bulletsEl = card.createEl('div', { cls: 'parallel-reader-bullets-md' }); - const md = bs.map(b => `- ${b}`).join('\n'); - MarkdownRenderer.render(this.app, md, bulletsEl, sourcePath, this).catch(() => { - bulletsEl.setText(md); - }); - } else if (!s.gist) { - card.createEl('div', { cls: 'parallel-reader-empty-li', text: this.plugin.t('emptyCard') }); - } - - // Left click → jump to source line - card.addEventListener('click', (e) => { - // Don't hijack when user is selecting text - const sel = window.getSelection(); - if (sel && sel.toString().length > 0) return; - // Let clicks on anchor links pass through (e.g. wikilinks inside rendered markdown) - const target = e.target as HTMLElement | null; - if (target && target.tagName === 'A') return; - if (s.startLine >= 0) this.plugin.scrollEditorToLine(s.startLine, this.sourceFile); - }); - - // Right click → context menu with copy actions - card.addEventListener('contextmenu', (e) => { - e.preventDefault(); - const menu = new Menu(); - menu.addItem(it => it.setTitle(this.plugin.t('menuCopyMarkdown')).setIcon('copy') - .onClick(() => copyToClipboard(cardToMarkdown(s), this.plugin.t('copiedMarkdown')))); - menu.addItem(it => it.setTitle(this.plugin.t('menuCopyPlain')).setIcon('clipboard-copy') - .onClick(() => copyToClipboard(cardToPlain(s), this.plugin.t('copiedPlain')))); - if (s.anchor) { - menu.addItem(it => it.setTitle(this.plugin.t('menuCopyAnchor')).setIcon('quote-glyph') - .onClick(() => copyToClipboard(s.anchor, this.plugin.t('copiedAnchor')))); - } - menu.addSeparator(); - if (s.startLine >= 0) { - menu.addItem(it => it.setTitle(this.plugin.t('menuJumpSource')).setIcon('arrow-right') - .onClick(() => this.plugin.scrollEditorToLine(s.startLine, this.sourceFile))); - } - menu.addSeparator(); - menu.addItem(it => it.setTitle(this.plugin.t('menuEditCard')).setIcon('pencil') - .onClick(() => this.openEditCardModal(i))); - menu.addItem(it => it.setTitle(this.plugin.t('menuDeleteCard')).setIcon('trash') - .onClick(() => this.deleteCard(i))); - menu.showAtMouseEvent(e); - }); - - this.cards.push(card); - }); - if (this.activeIdx >= 0 && this.cards[this.activeIdx]) { - this.cards[this.activeIdx].addClass('is-active'); - } - } - - setActiveSection(idx) { - if (idx === this.activeIdx) return; - if (this.activeIdx >= 0 && this.cards[this.activeIdx]) { - this.cards[this.activeIdx].removeClass('is-active'); - } - this.activeIdx = idx; - if (idx >= 0 && this.cards[idx]) { - this.cards[idx].addClass('is-active'); - this.cards[idx].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - } - - moveActiveSection(delta) { - const nextIdx = nextCardIndex(this.activeIdx, this.sections.length, delta); - this.setActiveSection(nextIdx); - this.focusSummaryPane(); - return nextIdx; - } - - jumpToActiveSection() { - const line = activeSectionLine(this.sections, this.activeIdx); - if (line < 0 || !this.sourceFile) return -1; - this.plugin.scrollEditorToLine(line, this.sourceFile); - return line; - } - - handleKeydown(e: KeyboardEvent) { - if (e.altKey && e.key === 'ArrowUp') { - e.preventDefault(); - this.moveActiveSection(-1); - return; - } - if (e.altKey && e.key === 'ArrowDown') { - e.preventDefault(); - this.moveActiveSection(1); - return; - } - if (!e.altKey && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.key === 'Enter') { - const line = this.jumpToActiveSection(); - if (line >= 0) e.preventDefault(); - } - } - - async deleteCard(index) { - if (!this.sourceFile) return false; - const nextSections = removeCardAt(this.sections, index); - if (nextSections.length === this.sections.length) return false; - const previousLength = this.sections.length; - this.sections = nextSections; - this.activeIdx = activeIndexAfterCardDelete(index, previousLength, this.activeIdx); - await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections); - this.render(); - new Notice(this.plugin.t('cardDeleted')); - return true; - } - - openEditCardModal(index) { - if (!this.sourceFile || !this.sections[index]) return false; - new CardEditModal(this.app, this.plugin, this.sections[index], patch => this.updateCard(index, patch)).open(); - return true; - } - - async updateCard(index, patch) { - if (!this.sourceFile) return false; - const nextSections = updateCardAt(this.sections, index, patch); - if (nextSections.length !== this.sections.length) return false; - this.sections = nextSections; - await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections); - this.render(); - new Notice(this.plugin.t('cardSaved')); - return true; - } - - async exportToVault() { - if (!this.sourceFile) return; - const folder = normalizeVaultPath(this.plugin.settings.exportFolder); - const name = `${this.sourceFile.basename} - ${this.plugin.t('displayName')}.md`; - const targetPath = `${folder}/${name}`; - - const markdown = [ - '---', - `source: [[${this.sourceFile.basename}]]`, - `generated: ${new Date().toISOString().slice(0, 10)}`, - 'tool: parallel-reader', - '---', - '', - cardsToMarkdown(`${this.sourceFile.basename} · ${this.plugin.t('displayName')}`, this.sections), - '', - ].join('\n'); - - const app = this.plugin.app; - await ensureVaultFolder(app, folder); - - const existing = app.vault.getAbstractFileByPath(targetPath); - if (existing instanceof TFile) { - await app.vault.modify(existing, markdown); - } else { - await app.vault.create(targetPath, markdown); - } - new Notice(this.plugin.t('exported', { path: targetPath })); - } -} - /* ---------- Plugin ---------- */ class ParallelReaderPlugin extends Plugin { @@ -532,106 +117,44 @@ class ParallelReaderPlugin extends Plugin { this.registerView(VIEW_TYPE_PARALLEL, (leaf) => new ParallelReaderView(leaf, this)); + this.addCommand({ id: 'parallel-reader-run', name: this.t('cmdRun'), callback: () => this.runForActiveFile(false) }); + this.addCommand({ id: 'parallel-reader-regen', name: this.t('cmdRegen'), callback: () => this.runForActiveFile(true) }); this.addCommand({ - id: 'parallel-reader-run', - name: this.t('cmdRun'), - callback: () => this.runForActiveFile(false), - }); - - this.addCommand({ - id: 'parallel-reader-regen', - name: this.t('cmdRegen'), - callback: () => this.runForActiveFile(true), - }); - - this.addCommand({ - id: 'parallel-reader-open-view', - name: this.t('cmdOpenView'), - callback: async () => { + id: 'parallel-reader-open-view', name: this.t('cmdOpenView'), callback: async () => { const active = this.getActiveView(); await this.ensureView(); if (active?.file) this.syncViewToFile(active.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-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 () => { + id: 'parallel-reader-clear-current', name: this.t('cmdClearCurrent'), callback: async () => { const active = this.getActiveView(); if (!active?.file) return new Notice(this.t('noCurrentNote')); await this.cacheDelete(active.file.path); new Notice(this.t('cacheClearedFile', { name: active.file.basename })); }, }); - this.addCommand({ - id: 'parallel-reader-clear-all', - name: this.t('cmdClearAll'), - callback: async () => { + id: 'parallel-reader-clear-all', name: this.t('cmdClearAll'), callback: async () => { const n = Object.keys(this.cache).length; await this.cacheClear(); new Notice(this.t('cacheClearedAll', { count: n })); }, }); - - 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-card-prev', name: this.t('cmdCardPrev'), hotkeys: [{ modifiers: ['Alt'], key: 'ArrowUp' }], callback: () => this.moveActiveCard(-1) }); + this.addCommand({ id: 'parallel-reader-card-next', name: this.t('cmdCardNext'), hotkeys: [{ modifiers: ['Alt'], key: 'ArrowDown' }], callback: () => this.moveActiveCard(1) }); + this.addCommand({ id: 'parallel-reader-card-jump', name: this.t('cmdCardJump'), callback: () => this.jumpActiveCard() }); this.addSettingTab(new ParallelReaderSettingTab(this.app, this)); - this.registerEvent( - this.app.workspace.on('active-leaf-change', () => this.bindScrollSync()) - ); - this.registerEvent( - this.app.workspace.on('file-open', (file) => { - if (file) this.syncViewToFile(file); - }) - ); - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file) => this.addFileMenuItems(menu, file)) - ); - this.registerEvent( - this.app.vault.on('rename', (file, oldPath) => this.handleFileRename(file, oldPath)) - ); - this.registerEvent( - this.app.vault.on('delete', (file) => this.handleFileDelete(file)) - ); + this.registerEvent(this.app.workspace.on('active-leaf-change', () => this.bindScrollSync())); + this.registerEvent(this.app.workspace.on('file-open', (file) => { if (file) this.syncViewToFile(file); })); + this.registerEvent(this.app.workspace.on('file-menu', (menu, file) => this.addFileMenuItems(menu, file))); + this.registerEvent(this.app.vault.on('rename', (file, oldPath) => this.handleFileRename(file, oldPath))); + this.registerEvent(this.app.vault.on('delete', (file) => this.handleFileDelete(file))); this.bindScrollSync(); } @@ -641,6 +164,8 @@ class ParallelReaderPlugin extends Plugin { this.app.workspace.detachLeavesOfType(VIEW_TYPE_PARALLEL); } + /* ---------- Settings persistence ---------- */ + async loadSettings() { const data = (await this.loadData()) || {}; const settingsBlob = data.settings || {}; @@ -649,10 +174,7 @@ class ParallelReaderPlugin extends Plugin { } async saveSettings() { - if (this._settingsSaveTimer) { - clearTimeout(this._settingsSaveTimer); - this._settingsSaveTimer = null; - } + if (this._settingsSaveTimer) { clearTimeout(this._settingsSaveTimer); this._settingsSaveTimer = null; } await this.saveData({ settings: this.settings }); } @@ -666,16 +188,14 @@ class ParallelReaderPlugin extends Plugin { async flushSettingsSave() { if (!this._settingsSaveTimer) return; - clearTimeout(this._settingsSaveTimer); - this._settingsSaveTimer = null; + clearTimeout(this._settingsSaveTimer); this._settingsSaveTimer = null; await this.saveSettings(); } + /* ---------- Cache persistence ---------- */ + async saveCache() { - if (this._cacheSaveTimer) { - clearTimeout(this._cacheSaveTimer); - this._cacheSaveTimer = null; - } + if (this._cacheSaveTimer) { clearTimeout(this._cacheSaveTimer); this._cacheSaveTimer = null; } this.pruneCache(); await this.writeCacheFile(); this._cacheDirty = false; @@ -692,10 +212,7 @@ class ParallelReaderPlugin extends Plugin { } async flushCacheSave() { - if (this._cacheSaveTimer) { - clearTimeout(this._cacheSaveTimer); - this._cacheSaveTimer = null; - } + if (this._cacheSaveTimer) { clearTimeout(this._cacheSaveTimer); this._cacheSaveTimer = null; } if (!this._cacheDirty) return; await this.saveCache(); } @@ -714,9 +231,7 @@ class ParallelReaderPlugin extends Plugin { try { if (typeof adapter.exists === 'function' && await adapter.exists(dir)) return; await adapter.mkdir(dir); - } catch (_) { - // The plugin directory normally already exists; ignore create races. - } + } catch (_) { /* ignore race */ } } async readCacheFile() { @@ -724,46 +239,28 @@ class ParallelReaderPlugin extends Plugin { 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; - } + if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object') return parsed.entries; } catch (e) { const message = String(e?.message || e || ''); - if (!/not found|does not exist|ENOENT/i.test(message)) { - console.warn('[parallel-reader] failed to read cache.json', 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) - ); + await this.app.vault.adapter.write(this.cacheFilePath(), serializeCacheFile(this.cache)); } async loadCache() { - const fileCache = await this.readCacheFile(); - this.cache = fileCache; + 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) { - return this.cache[filePath] || null; - } + 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) { return this.cache[filePath] || null; } async cacheTouch(filePath) { const entry = touchCacheEntry(this.cache[filePath] || null); @@ -774,14 +271,7 @@ class ParallelReaderPlugin extends Plugin { async cachePut(filePath, content, cards, settings) { 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, - }; + this.cache[filePath] = { schemaVersion: CACHE_SCHEMA_VERSION, contentHash: hashContent(content), settingsHash: generationFingerprint(settings || this.settings), cards, generatedAt: now, lastAccessedAt: now }; await this.saveCache(); } @@ -789,76 +279,45 @@ class ParallelReaderPlugin extends Plugin { const entry = this.cache[filePath]; if (!entry) return false; const now = new Date().toISOString(); - entry.cards = (cards || []).map(card => ({ - title: card.title, - anchor: card.anchor, - gist: card.gist, - bullets: card.bullets || [], - })); + entry.cards = (cards || []).map(card => ({ title: card.title, anchor: card.anchor, gist: card.gist, bullets: card.bullets || [] })); entry.updatedAt = now; entry.lastAccessedAt = now; await this.saveCache(); return true; } - async cacheDelete(filePath) { - if (this.cache[filePath]) { - delete this.cache[filePath]; - await this.saveCache(); - } - } + async cacheDelete(filePath) { if (this.cache[filePath]) { delete this.cache[filePath]; await this.saveCache(); } } + async cacheClear() { this.cache = {}; await this.saveCache(); } - async cacheClear() { - this.cache = {}; - await this.saveCache(); - } + /* ---------- View management ---------- */ async ensureView() { const { workspace } = this.app; let leaf = workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]; - if (!leaf) { - leaf = workspace.getRightLeaf(false); - await leaf.setViewState({ type: VIEW_TYPE_PARALLEL, active: true }); - } + if (!leaf) { leaf = workspace.getRightLeaf(false); await leaf.setViewState({ type: VIEW_TYPE_PARALLEL, active: true }); } workspace.revealLeaf(leaf); return leaf.view as ParallelReaderView; } - getActiveView() { - return this.app.workspace.getActiveViewOfType(MarkdownView); - } - - getParallelView() { - return this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]?.view as ParallelReaderView | undefined; - } + getActiveView() { return this.app.workspace.getActiveViewOfType(MarkdownView); } + getParallelView() { return this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]?.view as ParallelReaderView | undefined; } moveActiveCard(delta) { const view = this.getParallelView(); - if (!view || !view.sections.length) { - new Notice(this.t('noActiveCard')); - return -1; - } + if (!view || !view.sections.length) { new Notice(this.t('noActiveCard')); return -1; } return view.moveActiveSection(delta); } jumpActiveCard() { const view = this.getParallelView(); - if (!view || view.jumpToActiveSection() < 0) { - new Notice(this.t('noActiveCard')); - return false; - } + if (!view || view.jumpToActiveSection() < 0) { new Notice(this.t('noActiveCard')); return false; } return true; } - isGeneratingFile(file) { - return !!file && !!file.path && this.jobs.isRunning(file.path); - } + isGeneratingFile(file) { return !!file && !!file.path && this.jobs.isRunning(file.path); } cancelGenerationForFile(file) { - if (!file || !file.path) { - new Notice(this.t('noCancelableJob')); - return false; - } + if (!file || !file.path) { new Notice(this.t('noCancelableJob')); return false; } const job = this.jobs.get(file.path); const noticeKey = cancellationNoticeKey(this.settings, job); const cancelled = this.jobs.cancel(file.path); @@ -866,50 +325,30 @@ class ParallelReaderPlugin extends Plugin { return cancelled; } - viewIsShowingFile(view, file) { - return !!view && !!file && view.sourceFile?.path === file.path; - } - - activeFileStillMatches(file) { - const active = this.getActiveView(); - return !active?.file || active.file.path === file.path; - } + viewIsShowingFile(view, file) { return !!view && !!file && view.sourceFile?.path === file.path; } + activeFileStillMatches(file) { const active = this.getActiveView(); return !active?.file || active.file.path === file.path; } cancelActiveGeneration() { const active = this.getActiveView(); if (active?.file && this.cancelGenerationForFile(active.file)) return; - const view = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]?.view as ParallelReaderView | undefined; + const view = this.getParallelView(); if (view?.sourceFile) this.cancelGenerationForFile(view.sourceFile); else new Notice(this.t('noCancelableJob')); } confirmRegenerateEditedCards() { const message = this.t('confirmRegenerateEditedCards'); - if (typeof window !== 'undefined' && typeof window.confirm === 'function') { - return window.confirm(message); - } + if (typeof window !== 'undefined' && typeof window.confirm === 'function') return window.confirm(message); return true; } addFileMenuItems(menu, file) { if (!(file instanceof TFile) || !file.path.endsWith('.md')) return; menu.addSeparator(); - menu.addItem(it => it - .setTitle(this.t('fileMenuGenerate')) - .setIcon('book-open') - .onClick(() => this.runForFile(file, false))); - menu.addItem(it => it - .setTitle(this.t('fileMenuRegen')) - .setIcon('refresh-cw') - .onClick(() => this.runForFile(file, true))); + menu.addItem(it => it.setTitle(this.t('fileMenuGenerate')).setIcon('book-open').onClick(() => this.runForFile(file, false))); + menu.addItem(it => it.setTitle(this.t('fileMenuRegen')).setIcon('refresh-cw').onClick(() => this.runForFile(file, true))); if (this.cacheGet(file.path)) { - menu.addItem(it => it - .setTitle(this.t('fileMenuClear')) - .setIcon('trash') - .onClick(async () => { - await this.cacheDelete(file.path); - new Notice(this.t('cacheClearedFile', { name: file.basename })); - })); + menu.addItem(it => it.setTitle(this.t('fileMenuClear')).setIcon('trash').onClick(async () => { await this.cacheDelete(file.path); new Notice(this.t('cacheClearedFile', { name: file.basename })); })); } } @@ -919,23 +358,14 @@ 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(); - } - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL); - const view = leaves[0]?.view as ParallelReaderView | undefined; + if (this.cache[oldPath]) { delete this.cache[oldPath]; await this.saveCache(); } + const view = this.getParallelView(); if (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(); - } - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL); - const view = leaves[0]?.view as ParallelReaderView | undefined; + if (this.cache[oldPath]) { this.cache[file.path] = this.cache[oldPath]; delete this.cache[oldPath]; await this.saveCache(); } + const view = this.getParallelView(); if (view?.sourceFile && (view.sourceFile.path === oldPath || view.sourceFile.path === file.path)) { view.sourceFile = file; await this.syncViewToFile(file); @@ -944,157 +374,90 @@ class ParallelReaderPlugin extends Plugin { async handleFileDelete(file) { if (!(file instanceof TFile)) return; - const hadCache = !!this.cache[file.path]; - if (hadCache) { - delete this.cache[file.path]; - await this.saveCache(); - } - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL); - const view = leaves[0]?.view as ParallelReaderView | undefined; + if (this.cache[file.path]) { delete this.cache[file.path]; await this.saveCache(); } + const view = this.getParallelView(); if (view?.sourceFile?.path === file.path) view.renderEmpty(); } async exportCurrentView() { const active = this.getActiveView(); const view = await this.ensureView(); - if (!view.sourceFile || !view.sections.length) { - if (active?.file) await this.syncViewToFile(active.file); - } - if (!view.sourceFile || !view.sections.length) { - new Notice(this.t('noExportContent')); - return; - } + if (!view.sourceFile || !view.sections.length) { if (active?.file) await this.syncViewToFile(active.file); } + if (!view.sourceFile || !view.sections.length) { new Notice(this.t('noExportContent')); return; } await view.exportToVault(); } async copyCurrentViewMarkdown() { const active = this.getActiveView(); const view = await this.ensureView(); - if (!view.sourceFile || !view.sections.length) { - if (active?.file) await this.syncViewToFile(active.file); - } - if (!view.sourceFile || !view.sections.length) { - new Notice(this.t('noCopyContent')); - return; - } - await copyToClipboard( - cardsToMarkdown(`${view.sourceFile.basename} · ${this.t('displayName')}`, view.sections), - this.t('copiedAllMarkdown') - ); + if (!view.sourceFile || !view.sections.length) { if (active?.file) await this.syncViewToFile(active.file); } + if (!view.sourceFile || !view.sections.length) { new Notice(this.t('noCopyContent')); return; } + await copyToClipboard(cardsToMarkdown(`${view.sourceFile.basename} · ${this.t('displayName')}`, view.sections), this.t('copiedAllMarkdown')); } + /* ---------- Generation ---------- */ + async runForActiveFile(force) { const mdView = this.getActiveView(); - if (!mdView || !mdView.file) { - new Notice(this.t('openNoteFirst')); - return; - } + if (!mdView || !mdView.file) { new Notice(this.t('openNoteFirst')); return; } return this.runForFile(mdView.file, force); } async runForFile(file, force) { - if (!file) { - new Notice(this.t('openNoteFirst')); - return; - } - const runningKey = file.path; - if (this.jobs.isRunning(runningKey)) { - new Notice(this.t('alreadyGenerating')); - return; - } - if (shouldConfirmRegenerate(this.cacheGet(file.path), force) && !this.confirmRegenerateEditedCards()) { - new Notice(this.t('regenerateCancelled')); - return false; - } + if (!file) { new Notice(this.t('openNoteFirst')); return; } + if (this.jobs.isRunning(file.path)) { new Notice(this.t('alreadyGenerating')); return; } + if (shouldConfirmRegenerate(this.cacheGet(file.path), force) && !this.confirmRegenerateEditedCards()) { new Notice(this.t('regenerateCancelled')); return; } - let view = null; - return this.jobs.start(runningKey, async job => { + let view: ParallelReaderView | null = null; + return this.jobs.start(file.path, async job => { job.setPhase('reading'); const content = await this.app.vault.read(file); job.throwIfCancelled(); - if (!content.trim()) { - new Notice(this.t('emptyNote')); - return; - } + if (!content.trim()) { new Notice(this.t('emptyNote')); return; } view = await this.ensureView(); job.throwIfCancelled(); - // Try cache unless user explicitly forced regeneration job.setPhase('cache-check'); if (!force) { const entry = this.cacheGet(file.path); if (cacheEntryMatches(entry, content, this.settings)) { await this.cacheTouch(file.path); - if (this.activeFileStillMatches(file)) { - await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), false); - } + if (this.activeFileStillMatches(file)) await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), false); return; } } await view.renderLoading(file, this.t('loadingGenerating')); const maxDocChars = Number(this.settings.maxDocChars) || DEFAULT_SETTINGS.maxDocChars; - if (content.length > maxDocChars) { - new Notice(this.t('longNoteTruncated', { count: maxDocChars })); - } + if (content.length > maxDocChars) new Notice(this.t('longNoteTruncated', { count: maxDocChars })); new Notice(this.t('generatingNotice')); job.setPhase('generating'); const sections = await summarizeDocument(content, this.settings, job); job.throwIfCancelled(); - if (sections.length === 0) { - new Notice(this.t('noCardsReturned')); - return; - } - // Persist raw cards (without computed startLine — re-resolve on load, in case source was renamed/edited) - const rawCards = sections.map(s => ({ - title: s.title, - anchor: s.anchor, - gist: s.gist, - bullets: s.bullets, - })); + if (sections.length === 0) { new Notice(this.t('noCardsReturned')); return; } + 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); job.throwIfCancelled(); - - if (this.viewIsShowingFile(view, file)) { - await view.loadFor(file, sections, false); - } + if (this.viewIsShowingFile(view, file)) await view.loadFor(file, sections, false); const unanchored = sections.filter(s => s.startLine < 0).length; - new Notice(this.t('generationDone', { - count: sections.length, - suffix: unanchored ? this.t('unanchoredSuffix', { count: unanchored }) : '', - })); + new Notice(this.t('generationDone', { count: sections.length, suffix: unanchored ? this.t('unanchoredSuffix', { count: unanchored }) : '' })); }).catch(async e => { - if (e instanceof GenerationJobAlreadyRunningError) { - new Notice(e.message); - return; - } - if (e instanceof GenerationJobCancelledError) { - if (this.viewIsShowingFile(view, file)) await view.renderError(file, this.t('cancelledError')); - new Notice(this.t('cancelled')); - return; - } + if (e instanceof GenerationJobAlreadyRunningError) { new Notice(e.message); return; } + if (e instanceof GenerationJobCancelledError) { if (this.viewIsShowingFile(view, file)) await view.renderError(file, this.t('cancelledError')); new Notice(this.t('cancelled')); return; } const kind = classifyGenerationError(e); console.error(e); if (this.viewIsShowingFile(view, file)) await view.renderError(file, e.message || String(e)); - new Notice(this.t('generationFailed', { - kind: kind === 'unknown' ? '' : ` (${kind})`, - error: e.message || e, - })); + new Notice(this.t('generationFailed', { kind: kind === 'unknown' ? '' : ` (${kind})`, error: e.message || e })); }); } - // Rehydrate cached cards: compute startLine from anchor against current content resolveCardAnchors(content, rawCards) { - const resolved = (rawCards || []).map(c => ({ - title: c.title, - level: 2, - anchor: c.anchor, - gist: c.gist, - startLine: findLineForAnchor(content, c.anchor), - bullets: c.bullets || [], + const resolved: ResolvedCard[] = (rawCards || []).map(c => ({ + title: c.title, level: 2, anchor: c.anchor, gist: c.gist, + startLine: findLineForAnchor(content, c.anchor), bullets: c.bullets || [], })); resolved.sort((a, b) => { if (a.startLine < 0 && b.startLine < 0) return 0; @@ -1105,80 +468,49 @@ class ParallelReaderPlugin extends Plugin { return resolved; } + /* ---------- Scroll sync ---------- */ + async syncViewToFile(file) { if (!file || !file.path || !file.path.endsWith('.md')) return; const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL); - if (leaves.length === 0) return; // view not open, nothing to sync + if (leaves.length === 0) return; const view = leaves[0].view as ParallelReaderView; - if (!view) return; - if (!this.activeFileStillMatches(file)) return; - + if (!view || !this.activeFileStillMatches(file)) return; const entry = this.cacheGet(file.path); - if (!entry) { - // No cache for this file → clear panel - await view.loadFor(file, [], false); - view.renderEmptyWithHint(file); - return; - } - + if (!entry) { await view.loadFor(file, [], false); view.renderEmptyWithHint(file); return; } const content = await this.app.vault.read(file); if (!this.activeFileStillMatches(file)) return; const stale = !cacheEntryMatches(entry, content, this.settings); await this.cacheTouch(file.path); - const resolved = this.resolveCardAnchors(content, entry.cards); - await view.loadFor(file, resolved, stale); + await view.loadFor(file, this.resolveCardAnchors(content, entry.cards), stale); } bindScrollSync() { - // Remove previous listener reference - if (this._scrollDispose) { - this._scrollDispose(); - this._scrollDispose = null; - } - + if (this._scrollDispose) { this._scrollDispose(); this._scrollDispose = null; } const mdView = this.getActiveView(); if (!mdView) return; - - // Prefer CM6 scrollDOM when available const editor = mdView.editor; const cm = editor && (editor as any).cm; const scrollDom = cm && cm.scrollDOM ? cm.scrollDOM : mdView.contentEl.querySelector('.cm-scroller'); if (!scrollDom) return; - const handler = createRafThrottledHandler(() => this.handleEditorScroll(mdView)); scrollDom.addEventListener('scroll', handler, { passive: true }); - this._scrollDispose = () => { - handler.cancel(); - scrollDom.removeEventListener('scroll', handler); - }; + this._scrollDispose = () => { handler.cancel(); scrollDom.removeEventListener('scroll', handler); }; } handleEditorScroll(mdView) { - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL); - if (leaves.length === 0) return; - const view = leaves[0].view as ParallelReaderView; + 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; - if (!cm) return; - const scrollDom = cm.scrollDOM; - if (!scrollDom) return; - - // Find visible top line - const rect = scrollDom.getBoundingClientRect(); + if (!cm || !cm.scrollDOM) return; + const rect = cm.scrollDOM.getBoundingClientRect(); const topY = visibleTopProbeY(rect); let topLine = 0; try { const pos = cm.posAtCoords({ x: rect.left + 20, y: topY }); - if (pos != null) { - topLine = cm.state.doc.lineAt(pos).number - 1; // 0-indexed - } - } catch (e) { - return; - } - - // Find the closest anchored section whose startLine <= topLine + if (pos != null) topLine = cm.state.doc.lineAt(pos).number - 1; + } catch (_) { return; } let activeIdx = -1; for (let i = 0; i < view.sections.length; i++) { const s = view.sections[i]; @@ -1191,8 +523,7 @@ class ParallelReaderPlugin extends Plugin { findLeafForFile(file) { if (!file) return null; - const leaves = this.app.workspace.getLeavesOfType('markdown'); - for (const leaf of leaves) { + for (const leaf of this.app.workspace.getLeavesOfType('markdown')) { const v = leaf.view as any; if (v && v.file && v.file.path === file.path) return leaf; } @@ -1201,376 +532,19 @@ class ParallelReaderPlugin extends Plugin { async scrollEditorToLine(line, file) { let leaf = file ? this.findLeafForFile(file) : null; - - // Fallback: file not open anywhere → open it in a new tab in the main area - if (!leaf && file) { - leaf = this.app.workspace.getLeaf('tab'); - await leaf.openFile(file, { active: false }); - } - - // Last resort: currently active markdown view - if (!leaf) { - const active = this.getActiveView(); - if (active) leaf = active.leaf; - } - if (!leaf) { - new Notice(this.t('noEditor')); - return; - } - + if (!leaf && file) { leaf = this.app.workspace.getLeaf('tab'); await leaf.openFile(file, { active: false }); } + if (!leaf) { const active = this.getActiveView(); if (active) leaf = active.leaf; } + if (!leaf) { new Notice(this.t('noEditor')); return; } this.app.workspace.setActiveLeaf(leaf, { focus: true }); const mdView = leaf.view; if (!(mdView instanceof MarkdownView)) return; - - // Use ephemeral state — works in both source/live-preview AND reading mode. mdView.setEphemeralState({ line }); - - // Belt-and-suspenders: also scroll the editor if in source/live-preview mode. if (mdView.editor) { - try { - mdView.editor.setCursor({ line, ch: 0 }); - mdView.editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true); - } catch (_) { /* ignore */ } + try { mdView.editor.setCursor({ line, ch: 0 }); mdView.editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true); } catch (_) { /* ignore */ } } } } -/* ---------- Settings tab ---------- */ - -class ParallelReaderSettingTab extends PluginSettingTab { - plugin: ParallelReaderPlugin; - - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - - display() { - const { containerEl } = this; - const tr = (key, vars?) => this.plugin.t(key, vars); - containerEl.empty(); - containerEl.createEl('h2', { text: tr('settingsTitle') }); - - new Setting(containerEl) - .setName(tr('settingUiLanguageName')) - .setDesc(tr('settingUiLanguageDesc')) - .addDropdown(d => { - for (const [id, label] of Object.entries(UI_LANGUAGES)) { - d.addOption(id, label); - } - return d - .setValue(this.plugin.settings.uiLanguage || DEFAULT_SETTINGS.uiLanguage) - .onChange(async v => { - this.plugin.settings.uiLanguage = v; - await this.plugin.saveSettings(); - this.display(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingBackendName')) - .setDesc(tr('settingBackendDesc')) - .addDropdown(d => d - .addOption('claude-code', 'Claude Code CLI') - .addOption('codex', 'Codex CLI') - .addOption('api', 'API / Provider') - .addOption('anthropic-api', 'Anthropic API (legacy)') - .setValue(this.plugin.settings.backend) - .onChange(async v => { - this.plugin.settings.backend = v; - if (v === 'api' && !this.plugin.settings.apiBaseUrl) { - applyApiProviderPreset(this.plugin.settings, this.plugin.settings.apiProvider || 'anthropic'); - } - await this.plugin.saveSettings(); - this.display(); - })); - - const apiBackend = isApiBackend(this.plugin.settings.backend); - - if (!apiBackend) { - new Setting(containerEl) - .setName(tr('settingCliPathName')) - .setDesc(tr('settingCliPathDesc')) - .addText(t => t - .setPlaceholder(tr('settingCliPathPlaceholder')) - .setValue(this.plugin.settings.cliPath) - .onChange(async v => { - this.plugin.settings.cliPath = v.trim(); - this.plugin.saveSettingsDebounced(); - })); - - new Setting(containerEl) - .setName(tr('settingCliTimeoutName')) - .addText(t => t - .setValue(String(this.plugin.settings.cliTimeoutMs)) - .onChange(async v => { - const n = parseInt(v, 10); - if (!isNaN(n) && n > 0) { - this.plugin.settings.cliTimeoutMs = n; - this.plugin.saveSettingsDebounced(); - } - })); - } else { - containerEl.createEl('h3', { text: tr('apiProviderHeader') }); - - const preset = getApiPreset(this.plugin.settings); - new Setting(containerEl) - .setName(tr('settingProviderPresetName')) - .setDesc(tr('settingProviderPresetDesc')) - .addDropdown(d => { - for (const [id, entry] of Object.entries(API_PROVIDER_PRESETS)) { - d.addOption(id, (entry as any).label); - } - return d - .setValue(this.plugin.settings.apiProvider) - .onChange(async v => { - applyApiProviderPreset(this.plugin.settings, v); - await this.plugin.saveSettings(); - this.display(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingApiFormatName')) - .setDesc(tr('settingApiFormatDesc')) - .addDropdown(d => { - for (const [id, entry] of Object.entries(API_FORMATS)) { - d.addOption(id, (entry as any).label); - } - return d - .setValue(getApiFormat(this.plugin.settings)) - .onChange(async v => { - this.plugin.settings.apiFormat = v; - await this.plugin.saveSettings(); - this.display(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingBaseUrlName')) - .setDesc(tr('settingBaseUrlDesc')) - .addText(t => t - .setPlaceholder( - (this.plugin.settings.apiProvider || '').startsWith('custom-') - ? 'https://your-provider.example/v1' - : (preset.baseUrl || API_FORMATS[getApiFormat(this.plugin.settings)].defaultBaseUrl) - ) - .setValue(this.plugin.settings.apiBaseUrl) - .onChange(async v => { - this.plugin.settings.apiBaseUrl = v.trim(); - this.plugin.saveSettingsDebounced(); - })); - - new Setting(containerEl) - .setName(tr('settingApiKeyName')) - .setDesc(tr('settingApiKeyDesc')) - .addText(t => { - t.inputEl.type = 'password'; - return t - .setPlaceholder('sk-...') - .setValue(this.plugin.settings.apiKey) - .onChange(async v => { - this.plugin.settings.apiKey = v.trim(); - this.plugin.saveSettingsDebounced(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingApiKeyEnvName')) - .setDesc(tr('settingApiKeyEnvDesc')) - .addText(t => t - .setPlaceholder(preset.envVar || 'OPENAI_API_KEY') - .setValue(this.plugin.settings.apiKeyEnvVar) - .onChange(async v => { - this.plugin.settings.apiKeyEnvVar = v.trim(); - this.plugin.saveSettingsDebounced(); - })); - - new Setting(containerEl) - .setName(tr('settingAuthTypeName')) - .setDesc(tr('settingAuthTypeDesc')) - .addDropdown(d => { - for (const [id, label] of Object.entries(API_AUTH_TYPES)) { - d.addOption(id, label); - } - return d - .setValue(this.plugin.settings.apiAuthType || 'auto') - .onChange(async v => { - this.plugin.settings.apiAuthType = v; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingHeadersName')) - .setDesc(tr('settingHeadersDesc')) - .addTextArea(t => t - .setPlaceholder('cf-aig-authorization: Bearer ...') - .setValue(this.plugin.settings.apiHeaders) - .onChange(async v => { - this.plugin.settings.apiHeaders = v; - this.plugin.saveSettingsDebounced(); - })); - - new Setting(containerEl) - .setName(tr('settingMaxTokensName')) - .addText(t => t - .setValue(String(this.plugin.settings.apiMaxTokens)) - .onChange(async v => { - const n = parseInt(v, 10); - if (!isNaN(n) && n > 0) { - this.plugin.settings.apiMaxTokens = n; - this.plugin.saveSettingsDebounced(); - } - })); - } - - new Setting(containerEl) - .setName(tr('settingModelName')) - .setDesc(apiBackend - ? tr('settingModelDescApi') - : tr('settingModelDescCli')) - .addText(t => t - .setPlaceholder(apiBackend ? (getApiPreset(this.plugin.settings).model || 'model-id') : DEFAULT_SETTINGS.model) - .setValue(this.plugin.settings.model) - .onChange(async v => { - this.plugin.settings.model = v.trim() || (apiBackend ? '' : DEFAULT_SETTINGS.model); - this.plugin.saveSettingsDebounced(); - })); - - new Setting(containerEl) - .setName(tr('settingMaxInputName')) - .setDesc(tr('settingMaxInputDesc')) - .addText(t => t - .setValue(String(this.plugin.settings.maxDocChars || DEFAULT_SETTINGS.maxDocChars)) - .onChange(async v => { - const n = parseInt(v, 10); - if (!isNaN(n) && n >= 1000) { - this.plugin.settings.maxDocChars = n; - this.plugin.saveSettingsDebounced(); - } - })); - - containerEl.createEl('h3', { text: tr('promptHeader') }); - - new Setting(containerEl) - .setName(tr('settingPromptLanguageName')) - .setDesc(tr('settingPromptLanguageDesc')) - .addDropdown(d => { - for (const [id, label] of Object.entries(PROMPT_LANGUAGES)) { - d.addOption(id, label); - } - return d - .setValue(this.plugin.settings.promptLanguage || DEFAULT_SETTINGS.promptLanguage) - .onChange(async v => { - this.plugin.settings.promptLanguage = v; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingCardRangeName')) - .setDesc(tr('settingCardRangeDesc')) - .addText(t => t - .setPlaceholder('min') - .setValue(String(this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards)) - .onChange(async v => { - const n = parseInt(v, 10); - if (!isNaN(n) && n > 0) { - this.plugin.settings.minCards = n; - if (this.plugin.settings.maxCards < n) this.plugin.settings.maxCards = n; - this.plugin.saveSettingsDebounced(); - } - })) - .addText(t => t - .setPlaceholder('max') - .setValue(String(this.plugin.settings.maxCards || DEFAULT_SETTINGS.maxCards)) - .onChange(async v => { - const n = parseInt(v, 10); - if (!isNaN(n) && n > 0) { - this.plugin.settings.maxCards = Math.max(n, this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards); - this.plugin.saveSettingsDebounced(); - } - })); - - new Setting(containerEl) - .setName(tr('settingCustomPromptName')) - .setDesc(tr('settingCustomPromptDesc')) - .addTextArea(t => { - t.inputEl.rows = 8; - return t - .setPlaceholder(tr('settingCustomPromptPlaceholder')) - .setValue(this.plugin.settings.customSystemPrompt || '') - .onChange(async v => { - this.plugin.settings.customSystemPrompt = v; - this.plugin.saveSettingsDebounced(); - }); - }); - - new Setting(containerEl) - .setName(tr('settingTestBackendName')) - .setDesc(apiBackend ? tr('settingTestBackendDescApi') : tr('settingTestBackendDescCli')) - .addButton(b => b - .setButtonText('Test') - .onClick(async () => { - try { - const result = await testBackend(this.plugin.settings); - new Notice(`✓ ${result.slice(0, 180)}`, 8000); - } catch (e) { - new Notice(tr('backendTestFailed', { error: e.message }), 10000); - } - })); - - new Setting(containerEl) - .setName(tr('settingExportFolderName')) - .setDesc(tr('settingExportFolderDesc')) - .addText(t => t - .setValue(this.plugin.settings.exportFolder) - .onChange(async v => { - this.plugin.settings.exportFolder = v.trim() || DEFAULT_SETTINGS.exportFolder; - this.plugin.saveSettingsDebounced(); - })); - - containerEl.createEl('h3', { text: tr('cacheHeader') }); - - new Setting(containerEl) - .setName(tr('settingMaxCacheName')) - .setDesc(tr('settingMaxCacheDesc')) - .addText(t => { - t.setValue(String(this.plugin.settings.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES)); - const commit = async () => { - const n = parseInt(t.getValue(), 10); - if (Number.isFinite(n) && n > 0) { - this.plugin.settings.maxCacheEntries = n; - await this.plugin.saveSettings(); - const removed = await this.plugin.pruneCacheIfNeeded(); - if (removed.length > 0) new Notice(tr('cachePruned', { count: removed.length })); - this.display(); - } - }; - t.inputEl.addEventListener('change', commit); - t.inputEl.addEventListener('keydown', e => { - if (e.key === 'Enter') t.inputEl.blur(); - }); - return t; - }); - - const cacheCount = Object.keys(this.plugin.cache).length; - new Setting(containerEl) - .setName(tr('cachedNotesName', { count: cacheCount })) - .setDesc(tr('cachedNotesDesc')) - .addButton(b => b - .setButtonText(tr('clearAllCacheButton')) - .setWarning() - .onClick(async () => { - const n = Object.keys(this.plugin.cache).length; - await this.plugin.cacheClear(); - new Notice(tr('cacheClearedAll', { count: n })); - this.display(); - })); - } -} - export default ParallelReaderPlugin; export const __test = { CACHE_SCHEMA_VERSION, diff --git a/src/modal.ts b/src/modal.ts new file mode 100644 index 0000000..14b9f48 --- /dev/null +++ b/src/modal.ts @@ -0,0 +1,56 @@ +'use strict'; + +import { Modal } from 'obsidian'; +import type { ResolvedCard, CardPatch, PluginHost } from './types'; +import { addTextButton } from './ui-helpers'; + +export class CardEditModal extends Modal { + plugin: PluginHost; + card: ResolvedCard; + onSave: (patch: CardPatch) => void | Promise; + + constructor(app, plugin: PluginHost, card: ResolvedCard, onSave: (patch: CardPatch) => void | Promise) { + super(app); + this.plugin = plugin; + this.card = card || ({} as ResolvedCard); + this.onSave = onSave; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl('h2', { text: this.plugin.t('editCardTitle') }); + + const titleInput = this.createLabeledInput(contentEl, this.plugin.t('editCardTitleField'), this.card.title || ''); + const gistInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardGistField'), this.card.gist || '', 3); + const bulletsInput = this.createLabeledTextarea(contentEl, this.plugin.t('editCardBulletsField'), (this.card.bullets || []).join('\n'), 8); + + const actions = contentEl.createDiv({ cls: 'parallel-reader-modal-actions' }); + addTextButton(actions, null, this.plugin.t('editCardCancel'), () => this.close(), 'parallel-reader-text-button'); + addTextButton(actions, null, this.plugin.t('editCardSave'), async () => { + await this.onSave({ + title: titleInput.value.trim() || this.card.title || '', + gist: gistInput.value.trim(), + bullets: bulletsInput.value.split(/\r?\n/).map(line => line.trim()).filter(Boolean), + }); + this.close(); + }, 'parallel-reader-text-button'); + } + + createLabeledInput(parent, label: string, value: string) { + const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' }); + wrapper.createEl('label', { text: label }); + const input = wrapper.createEl('input', { attr: { type: 'text' } }); + input.value = value; + return input; + } + + createLabeledTextarea(parent, label: string, value: string, rows: number) { + const wrapper = parent.createDiv({ cls: 'parallel-reader-modal-field' }); + wrapper.createEl('label', { text: label }); + const textarea = wrapper.createEl('textarea'); + textarea.rows = rows; + textarea.value = value; + return textarea; + } +} diff --git a/src/settings-tab.ts b/src/settings-tab.ts new file mode 100644 index 0000000..504cdae --- /dev/null +++ b/src/settings-tab.ts @@ -0,0 +1,370 @@ +'use strict'; + +import { PluginSettingTab, Setting, Notice, requestUrl } from 'obsidian'; +import type { PluginHost } from './types'; +import { resolveCliPath, runCli } from './cli'; +import { testApiBackend } from './providers'; +import { + API_AUTH_TYPES, + API_FORMATS, + API_PROVIDER_PRESETS, + DEFAULT_SETTINGS, + DEFAULT_MAX_CACHE_ENTRIES, + PROMPT_LANGUAGES, + UI_LANGUAGES, + applyApiProviderPreset, + getApiFormat, + getApiPreset, + isApiBackend, +} from './settings'; + +async function testBackend(settings) { + if (settings.backend === 'codex') { + const cmd = resolveCliPath('codex', settings.cliPath); + const { stdout } = await runCli(cmd, ['--version'], '', 10000); + return `codex @ ${cmd}\n${stdout.trim()}`; + } + if (settings.backend === 'claude-code') { + const cmd = resolveCliPath('claude', settings.cliPath); + const { stdout } = await runCli(cmd, ['--version'], '', 10000); + return `claude @ ${cmd}\n${stdout.trim()}`; + } + if (isApiBackend(settings.backend)) { + return testApiBackend(requestUrl, settings); + } + throw new Error('Unknown backend: ' + settings.backend); +} + +export class ParallelReaderSettingTab extends PluginSettingTab { + plugin: PluginHost; + + constructor(app, plugin: PluginHost) { + super(app, plugin as any); + this.plugin = plugin; + } + + display() { + const { containerEl } = this; + const tr = (key, vars?) => this.plugin.t(key, vars); + containerEl.empty(); + containerEl.createEl('h2', { text: tr('settingsTitle') }); + + new Setting(containerEl) + .setName(tr('settingUiLanguageName')) + .setDesc(tr('settingUiLanguageDesc')) + .addDropdown(d => { + for (const [id, label] of Object.entries(UI_LANGUAGES)) { + d.addOption(id, label); + } + return d + .setValue(this.plugin.settings.uiLanguage || DEFAULT_SETTINGS.uiLanguage) + .onChange(async v => { + this.plugin.settings.uiLanguage = v; + await this.plugin.saveSettings(); + this.display(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingBackendName')) + .setDesc(tr('settingBackendDesc')) + .addDropdown(d => d + .addOption('claude-code', 'Claude Code CLI') + .addOption('codex', 'Codex CLI') + .addOption('api', 'API / Provider') + .addOption('anthropic-api', 'Anthropic API (legacy)') + .setValue(this.plugin.settings.backend) + .onChange(async v => { + this.plugin.settings.backend = v; + if (v === 'api' && !this.plugin.settings.apiBaseUrl) { + applyApiProviderPreset(this.plugin.settings, this.plugin.settings.apiProvider || 'anthropic'); + } + await this.plugin.saveSettings(); + this.display(); + })); + + const apiBackend = isApiBackend(this.plugin.settings.backend); + + if (!apiBackend) { + new Setting(containerEl) + .setName(tr('settingCliPathName')) + .setDesc(tr('settingCliPathDesc')) + .addText(t => t + .setPlaceholder(tr('settingCliPathPlaceholder')) + .setValue(this.plugin.settings.cliPath) + .onChange(async v => { + this.plugin.settings.cliPath = v.trim(); + this.plugin.saveSettingsDebounced(); + })); + + new Setting(containerEl) + .setName(tr('settingCliTimeoutName')) + .addText(t => t + .setValue(String(this.plugin.settings.cliTimeoutMs)) + .onChange(async v => { + const n = parseInt(v, 10); + if (!isNaN(n) && n > 0) { + this.plugin.settings.cliTimeoutMs = n; + this.plugin.saveSettingsDebounced(); + } + })); + } else { + containerEl.createEl('h3', { text: tr('apiProviderHeader') }); + + const preset = getApiPreset(this.plugin.settings); + new Setting(containerEl) + .setName(tr('settingProviderPresetName')) + .setDesc(tr('settingProviderPresetDesc')) + .addDropdown(d => { + for (const [id, entry] of Object.entries(API_PROVIDER_PRESETS)) { + d.addOption(id, entry.label); + } + return d + .setValue(this.plugin.settings.apiProvider) + .onChange(async v => { + applyApiProviderPreset(this.plugin.settings, v); + await this.plugin.saveSettings(); + this.display(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingApiFormatName')) + .setDesc(tr('settingApiFormatDesc')) + .addDropdown(d => { + for (const [id, entry] of Object.entries(API_FORMATS)) { + d.addOption(id, entry.label); + } + return d + .setValue(getApiFormat(this.plugin.settings)) + .onChange(async v => { + this.plugin.settings.apiFormat = v; + await this.plugin.saveSettings(); + this.display(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingBaseUrlName')) + .setDesc(tr('settingBaseUrlDesc')) + .addText(t => t + .setPlaceholder( + (this.plugin.settings.apiProvider || '').startsWith('custom-') + ? 'https://your-provider.example/v1' + : (preset.baseUrl || API_FORMATS[getApiFormat(this.plugin.settings)].defaultBaseUrl) + ) + .setValue(this.plugin.settings.apiBaseUrl) + .onChange(async v => { + this.plugin.settings.apiBaseUrl = v.trim(); + this.plugin.saveSettingsDebounced(); + })); + + new Setting(containerEl) + .setName(tr('settingApiKeyName')) + .setDesc(tr('settingApiKeyDesc')) + .addText(t => { + t.inputEl.type = 'password'; + return t + .setPlaceholder('sk-...') + .setValue(this.plugin.settings.apiKey) + .onChange(async v => { + this.plugin.settings.apiKey = v.trim(); + this.plugin.saveSettingsDebounced(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingApiKeyEnvName')) + .setDesc(tr('settingApiKeyEnvDesc')) + .addText(t => t + .setPlaceholder(preset.envVar || 'OPENAI_API_KEY') + .setValue(this.plugin.settings.apiKeyEnvVar) + .onChange(async v => { + this.plugin.settings.apiKeyEnvVar = v.trim(); + this.plugin.saveSettingsDebounced(); + })); + + new Setting(containerEl) + .setName(tr('settingAuthTypeName')) + .setDesc(tr('settingAuthTypeDesc')) + .addDropdown(d => { + for (const [id, label] of Object.entries(API_AUTH_TYPES)) { + d.addOption(id, label); + } + return d + .setValue(this.plugin.settings.apiAuthType || 'auto') + .onChange(async v => { + this.plugin.settings.apiAuthType = v; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingHeadersName')) + .setDesc(tr('settingHeadersDesc')) + .addTextArea(t => t + .setPlaceholder('cf-aig-authorization: Bearer ...') + .setValue(this.plugin.settings.apiHeaders) + .onChange(async v => { + this.plugin.settings.apiHeaders = v; + this.plugin.saveSettingsDebounced(); + })); + + new Setting(containerEl) + .setName(tr('settingMaxTokensName')) + .addText(t => t + .setValue(String(this.plugin.settings.apiMaxTokens)) + .onChange(async v => { + const n = parseInt(v, 10); + if (!isNaN(n) && n > 0) { + this.plugin.settings.apiMaxTokens = n; + this.plugin.saveSettingsDebounced(); + } + })); + } + + new Setting(containerEl) + .setName(tr('settingModelName')) + .setDesc(apiBackend + ? tr('settingModelDescApi') + : tr('settingModelDescCli')) + .addText(t => t + .setPlaceholder(apiBackend ? (getApiPreset(this.plugin.settings).model || 'model-id') : DEFAULT_SETTINGS.model) + .setValue(this.plugin.settings.model) + .onChange(async v => { + this.plugin.settings.model = v.trim() || (apiBackend ? '' : DEFAULT_SETTINGS.model); + this.plugin.saveSettingsDebounced(); + })); + + new Setting(containerEl) + .setName(tr('settingMaxInputName')) + .setDesc(tr('settingMaxInputDesc')) + .addText(t => t + .setValue(String(this.plugin.settings.maxDocChars || DEFAULT_SETTINGS.maxDocChars)) + .onChange(async v => { + const n = parseInt(v, 10); + if (!isNaN(n) && n >= 1000) { + this.plugin.settings.maxDocChars = n; + this.plugin.saveSettingsDebounced(); + } + })); + + containerEl.createEl('h3', { text: tr('promptHeader') }); + + new Setting(containerEl) + .setName(tr('settingPromptLanguageName')) + .setDesc(tr('settingPromptLanguageDesc')) + .addDropdown(d => { + for (const [id, label] of Object.entries(PROMPT_LANGUAGES)) { + d.addOption(id, label); + } + return d + .setValue(this.plugin.settings.promptLanguage || DEFAULT_SETTINGS.promptLanguage) + .onChange(async v => { + this.plugin.settings.promptLanguage = v; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingCardRangeName')) + .setDesc(tr('settingCardRangeDesc')) + .addText(t => t + .setPlaceholder('min') + .setValue(String(this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards)) + .onChange(async v => { + const n = parseInt(v, 10); + if (!isNaN(n) && n > 0) { + this.plugin.settings.minCards = n; + if (this.plugin.settings.maxCards < n) this.plugin.settings.maxCards = n; + this.plugin.saveSettingsDebounced(); + } + })) + .addText(t => t + .setPlaceholder('max') + .setValue(String(this.plugin.settings.maxCards || DEFAULT_SETTINGS.maxCards)) + .onChange(async v => { + const n = parseInt(v, 10); + if (!isNaN(n) && n > 0) { + this.plugin.settings.maxCards = Math.max(n, this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards); + this.plugin.saveSettingsDebounced(); + } + })); + + new Setting(containerEl) + .setName(tr('settingCustomPromptName')) + .setDesc(tr('settingCustomPromptDesc')) + .addTextArea(t => { + t.inputEl.rows = 8; + return t + .setPlaceholder(tr('settingCustomPromptPlaceholder')) + .setValue(this.plugin.settings.customSystemPrompt || '') + .onChange(async v => { + this.plugin.settings.customSystemPrompt = v; + this.plugin.saveSettingsDebounced(); + }); + }); + + new Setting(containerEl) + .setName(tr('settingTestBackendName')) + .setDesc(apiBackend ? tr('settingTestBackendDescApi') : tr('settingTestBackendDescCli')) + .addButton(b => b + .setButtonText('Test') + .onClick(async () => { + try { + const result = await testBackend(this.plugin.settings); + new Notice(`✓ ${result.slice(0, 180)}`, 8000); + } catch (e) { + new Notice(tr('backendTestFailed', { error: e.message }), 10000); + } + })); + + new Setting(containerEl) + .setName(tr('settingExportFolderName')) + .setDesc(tr('settingExportFolderDesc')) + .addText(t => t + .setValue(this.plugin.settings.exportFolder) + .onChange(async v => { + this.plugin.settings.exportFolder = v.trim() || DEFAULT_SETTINGS.exportFolder; + this.plugin.saveSettingsDebounced(); + })); + + containerEl.createEl('h3', { text: tr('cacheHeader') }); + + new Setting(containerEl) + .setName(tr('settingMaxCacheName')) + .setDesc(tr('settingMaxCacheDesc')) + .addText(t => { + t.setValue(String(this.plugin.settings.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES)); + const commit = async () => { + const n = parseInt(t.getValue(), 10); + if (Number.isFinite(n) && n > 0) { + this.plugin.settings.maxCacheEntries = n; + await this.plugin.saveSettings(); + const removed = await this.plugin.pruneCacheIfNeeded(); + if (removed.length > 0) new Notice(tr('cachePruned', { count: removed.length })); + this.display(); + } + }; + t.inputEl.addEventListener('change', commit); + t.inputEl.addEventListener('keydown', e => { + if (e.key === 'Enter') t.inputEl.blur(); + }); + return t; + }); + + const cacheCount = Object.keys(this.plugin.cache).length; + new Setting(containerEl) + .setName(tr('cachedNotesName', { count: cacheCount })) + .setDesc(tr('cachedNotesDesc')) + .addButton(b => b + .setButtonText(tr('clearAllCacheButton')) + .setWarning() + .onClick(async () => { + const n = Object.keys(this.plugin.cache).length; + await this.plugin.cacheClear(); + new Notice(tr('cacheClearedAll', { count: n })); + this.display(); + })); + } +} diff --git a/src/types.ts b/src/types.ts index 38c53c7..26be8a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,3 +112,28 @@ export interface PromptPair { system: string; user: string; } + +/* ---------- Plugin host interface ---------- */ + +/** + * Minimal interface that extracted UI classes (View, Modal, SettingsTab) + * use to call back into the plugin. Avoids circular imports between + * main.ts and the extracted modules. + */ +export interface PluginHost { + app: { vault: any; workspace: any }; + settings: PluginSettings; + cache: Record; + manifest?: { id: string }; + t(key: string, vars?: Record): string; + isGeneratingFile(file: any): boolean; + cancelGenerationForFile(file: any): boolean; + runForFile(file: any, force: boolean): Promise; + copyCurrentViewMarkdown(): Promise; + scrollEditorToLine(line: number, file: any): Promise; + cacheReplaceCards(filePath: string, cards: ResolvedCard[]): Promise; + saveSettings(): Promise; + saveSettingsDebounced(delayMs?: number): void; + cacheClear(): Promise; + pruneCacheIfNeeded(): Promise; +} diff --git a/src/view.ts b/src/view.ts new file mode 100644 index 0000000..4136881 --- /dev/null +++ b/src/view.ts @@ -0,0 +1,341 @@ +'use strict'; + +import { ItemView, Notice, TFile, Menu, MarkdownRenderer } from 'obsidian'; +import type { ResolvedCard, CardPatch, PluginHost } from './types'; +import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './cards'; +import { cardToMarkdown, cardToPlain, cardsToMarkdown } from './markdown'; +import { activeSectionLine, nextCardIndex } from './navigation'; +import { normalizeVaultPath } from './vault'; +import { ensureVaultFolder } from './vault'; +import { addIconButton, addTextButton, copyToClipboard } from './ui-helpers'; +import { CardEditModal } from './modal'; + +export const VIEW_TYPE_PARALLEL = 'parallel-reader-view'; + +export class ParallelReaderView extends ItemView { + plugin: PluginHost; + sections: ResolvedCard[]; + sourceFile: TFile | null; + cards: HTMLElement[]; + activeIdx: number; + stale: boolean; + loadingMessage: string; + errorMessage: string; + + constructor(leaf, plugin: PluginHost) { + super(leaf); + this.plugin = plugin; + this.sections = []; + this.sourceFile = null; + this.cards = []; + this.activeIdx = -1; + this.loadingMessage = ''; + this.errorMessage = ''; + } + + getViewType() { return VIEW_TYPE_PARALLEL; } + getDisplayText() { return this.plugin.t('displayName'); } + getIcon() { return 'book-open'; } + + onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + container.addClass('parallel-reader-container'); + container.setAttr('tabindex', '0'); + container.addEventListener('keydown', e => this.handleKeydown(e as KeyboardEvent)); + this.renderEmpty(); + this.focusSummaryPane(); + return Promise.resolve(); + } + + onClose() { + return Promise.resolve(); + } + + renderEmpty() { + this.sourceFile = null; + this.sections = []; + this.stale = false; + this.loadingMessage = ''; + this.errorMessage = ''; + const container = this.containerEl.children[1]; + container.empty(); + const hint = container.createDiv({ cls: 'parallel-reader-empty' }); + hint.createEl('h3', { text: this.plugin.t('appTitle') }); + hint.createEl('p', { text: this.plugin.t('emptyOpenNote') }); + hint.createEl('code', { text: this.plugin.t('commandGenerate') }); + } + + focusSummaryPane() { + const container = this.containerEl.children[1] as HTMLElement; + if (!container || typeof container.focus !== 'function') return false; + container.focus({ preventScroll: true }); + return true; + } + + async loadFor(file, sections, stale) { + this.sourceFile = file; + this.sections = sections; + this.stale = !!stale; + this.loadingMessage = ''; + this.errorMessage = ''; + this.render(); + } + + async renderLoading(file, message) { + this.sourceFile = file; + this.sections = []; + this.stale = false; + this.loadingMessage = message || this.plugin.t('loadingDefault'); + this.errorMessage = ''; + this.render(); + } + + async renderError(file, message) { + this.sourceFile = file; + this.sections = []; + this.stale = false; + this.loadingMessage = ''; + this.errorMessage = message || this.plugin.t('errorTitle'); + this.render(); + } + + renderEmptyWithHint(file) { + this.sourceFile = file; + this.sections = []; + this.stale = false; + this.loadingMessage = ''; + this.errorMessage = ''; + const container = this.containerEl.children[1]; + container.empty(); + const hint = container.createDiv({ cls: 'parallel-reader-empty' }); + hint.createEl('h3', { text: file.basename }); + hint.createEl('p', { text: this.plugin.t('emptyNoCache') }); + hint.createEl('code', { text: this.plugin.t('commandGenerate') }); + } + + render() { + const container = this.containerEl.children[1]; + container.empty(); + + const header = container.createDiv({ cls: 'parallel-reader-header' }); + const headerRow = header.createDiv({ cls: 'parallel-reader-header-row' }); + headerRow.createEl('div', { text: this.sourceFile?.basename || '', cls: 'parallel-reader-title' }); + const actions = headerRow.createDiv({ cls: 'parallel-reader-actions' }); + if (this.sourceFile) { + if (this.plugin.isGeneratingFile(this.sourceFile)) { + addIconButton(actions, 'square', this.plugin.t('actionCancel'), () => this.plugin.cancelGenerationForFile(this.sourceFile)); + } else { + addIconButton(actions, 'refresh-cw', this.plugin.t('actionRegenerate'), () => this.plugin.runForFile(this.sourceFile, true)); + } + addIconButton(actions, 'copy', this.plugin.t('actionCopyAll'), () => this.plugin.copyCurrentViewMarkdown()); + addIconButton(actions, 'download', this.plugin.t('actionExport'), () => this.exportToVault()); + } + + if (this.stale) { + const banner = container.createDiv({ cls: 'parallel-reader-stale-banner' }); + banner.createSpan({ text: this.plugin.t('staleBanner') }); + addTextButton( + banner, + 'refresh-cw', + this.plugin.t('actionRegenerate'), + () => this.plugin.runForFile(this.sourceFile, true), + 'parallel-reader-stale-button' + ); + } + + if (this.loadingMessage) { + const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-loading' }); + state.createDiv({ cls: 'parallel-reader-spinner' }); + state.createEl('div', { text: this.loadingMessage, cls: 'parallel-reader-state-title' }); + state.createEl('div', { text: this.plugin.t('loadingSubtitle'), cls: 'parallel-reader-state-subtitle' }); + return; + } + + if (this.errorMessage) { + const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-error' }); + state.createEl('div', { text: this.plugin.t('errorTitle'), cls: 'parallel-reader-state-title' }); + state.createEl('div', { text: this.errorMessage, cls: 'parallel-reader-state-subtitle' }); + addTextButton( + state, + 'refresh-cw', + this.plugin.t('actionRegenerate'), + () => this.plugin.runForFile(this.sourceFile, true), + 'parallel-reader-text-button' + ); + return; + } + + const list = container.createDiv({ cls: 'parallel-reader-cards' }); + this.cards = []; + const sourcePath = this.sourceFile?.path || ''; + this.sections.forEach((s, i) => { + const card = list.createDiv({ cls: 'parallel-reader-card' }); + card.dataset.idx = String(i); + if (s.startLine < 0) card.addClass('parallel-reader-card-unanchored'); + + const title = card.createEl('div', { cls: 'parallel-reader-card-title' }); + title.createSpan({ text: s.title }); + if (s.startLine < 0) { + title.createEl('span', { text: ' ⚠', cls: 'parallel-reader-warn', title: this.plugin.t('anchorMismatch') }); + } + + if (s.gist) { + const gistEl = card.createEl('div', { cls: 'parallel-reader-gist' }); + MarkdownRenderer.render(this.app, s.gist, gistEl, sourcePath, this).catch(() => { + gistEl.setText(s.gist); + }); + } + + const bs = s.bullets || []; + if (bs.length > 0) { + const bulletsEl = card.createEl('div', { cls: 'parallel-reader-bullets-md' }); + const md = bs.map(b => `- ${b}`).join('\n'); + MarkdownRenderer.render(this.app, md, bulletsEl, sourcePath, this).catch(() => { + bulletsEl.setText(md); + }); + } else if (!s.gist) { + card.createEl('div', { cls: 'parallel-reader-empty-li', text: this.plugin.t('emptyCard') }); + } + + card.addEventListener('click', (e) => { + const sel = window.getSelection(); + if (sel && sel.toString().length > 0) return; + const target = e.target as HTMLElement | null; + if (target && target.tagName === 'A') return; + if (s.startLine >= 0) this.plugin.scrollEditorToLine(s.startLine, this.sourceFile); + }); + + card.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const menu = new Menu(); + menu.addItem(it => it.setTitle(this.plugin.t('menuCopyMarkdown')).setIcon('copy') + .onClick(() => copyToClipboard(cardToMarkdown(s), this.plugin.t('copiedMarkdown')))); + menu.addItem(it => it.setTitle(this.plugin.t('menuCopyPlain')).setIcon('clipboard-copy') + .onClick(() => copyToClipboard(cardToPlain(s), this.plugin.t('copiedPlain')))); + if (s.anchor) { + menu.addItem(it => it.setTitle(this.plugin.t('menuCopyAnchor')).setIcon('quote-glyph') + .onClick(() => copyToClipboard(s.anchor, this.plugin.t('copiedAnchor')))); + } + menu.addSeparator(); + if (s.startLine >= 0) { + menu.addItem(it => it.setTitle(this.plugin.t('menuJumpSource')).setIcon('arrow-right') + .onClick(() => this.plugin.scrollEditorToLine(s.startLine, this.sourceFile))); + } + menu.addSeparator(); + menu.addItem(it => it.setTitle(this.plugin.t('menuEditCard')).setIcon('pencil') + .onClick(() => this.openEditCardModal(i))); + menu.addItem(it => it.setTitle(this.plugin.t('menuDeleteCard')).setIcon('trash') + .onClick(() => this.deleteCard(i))); + menu.showAtMouseEvent(e); + }); + + this.cards.push(card); + }); + if (this.activeIdx >= 0 && this.cards[this.activeIdx]) { + this.cards[this.activeIdx].addClass('is-active'); + } + } + + setActiveSection(idx) { + if (idx === this.activeIdx) return; + if (this.activeIdx >= 0 && this.cards[this.activeIdx]) { + this.cards[this.activeIdx].removeClass('is-active'); + } + this.activeIdx = idx; + if (idx >= 0 && this.cards[idx]) { + this.cards[idx].addClass('is-active'); + this.cards[idx].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + + moveActiveSection(delta) { + const nextIdx = nextCardIndex(this.activeIdx, this.sections.length, delta); + this.setActiveSection(nextIdx); + this.focusSummaryPane(); + return nextIdx; + } + + jumpToActiveSection() { + const line = activeSectionLine(this.sections, this.activeIdx); + if (line < 0 || !this.sourceFile) return -1; + this.plugin.scrollEditorToLine(line, this.sourceFile); + return line; + } + + handleKeydown(e: KeyboardEvent) { + if (e.altKey && e.key === 'ArrowUp') { + e.preventDefault(); + this.moveActiveSection(-1); + return; + } + if (e.altKey && e.key === 'ArrowDown') { + e.preventDefault(); + this.moveActiveSection(1); + return; + } + if (!e.altKey && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.key === 'Enter') { + const line = this.jumpToActiveSection(); + if (line >= 0) e.preventDefault(); + } + } + + async deleteCard(index) { + if (!this.sourceFile) return false; + const nextSections = removeCardAt(this.sections, index); + if (nextSections.length === this.sections.length) return false; + const previousLength = this.sections.length; + this.sections = nextSections; + this.activeIdx = activeIndexAfterCardDelete(index, previousLength, this.activeIdx); + await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections); + this.render(); + new Notice(this.plugin.t('cardDeleted')); + return true; + } + + openEditCardModal(index) { + if (!this.sourceFile || !this.sections[index]) return false; + new CardEditModal(this.app, this.plugin, this.sections[index], async patch => { await this.updateCard(index, patch); }).open(); + return true; + } + + async updateCard(index, patch: CardPatch) { + if (!this.sourceFile) return false; + const nextSections = updateCardAt(this.sections, index, patch); + if (nextSections.length !== this.sections.length) return false; + this.sections = nextSections; + await this.plugin.cacheReplaceCards(this.sourceFile.path, nextSections); + this.render(); + new Notice(this.plugin.t('cardSaved')); + return true; + } + + async exportToVault() { + if (!this.sourceFile) return; + const folder = normalizeVaultPath(this.plugin.settings.exportFolder); + const name = `${this.sourceFile.basename} - ${this.plugin.t('displayName')}.md`; + const targetPath = `${folder}/${name}`; + + const markdown = [ + '---', + `source: [[${this.sourceFile.basename}]]`, + `generated: ${new Date().toISOString().slice(0, 10)}`, + 'tool: parallel-reader', + '---', + '', + cardsToMarkdown(`${this.sourceFile.basename} · ${this.plugin.t('displayName')}`, this.sections), + '', + ].join('\n'); + + const app = this.plugin.app; + await ensureVaultFolder(app, folder); + + const existing = app.vault.getAbstractFileByPath(targetPath); + if (existing instanceof TFile) { + await app.vault.modify(existing, markdown); + } else { + await app.vault.create(targetPath, markdown); + } + new Notice(this.plugin.t('exported', { path: targetPath })); + } +} diff --git a/tests/main.test.js b/tests/main.test.js index 6717a3a..84fd7cd 100644 --- a/tests/main.test.js +++ b/tests/main.test.js @@ -44,11 +44,12 @@ const t = plugin.__test; assert.ok(t, 'test helpers should be exported'); const mainSource = fs.readFileSync(path.join(__dirname, '..', 'main.ts'), 'utf8'); -assert.ok(!/\basync\s+onOpen\s*\(/.test(mainSource), 'ParallelReaderView.onOpen should not be async without await'); -assert.ok(!/\basync\s+onClose\s*\(\)\s*\{\s*\}/.test(mainSource), 'empty onClose should not be async'); -assert.ok(/focusSummaryPane\s*\(\)/.test(mainSource), 'summary pane should expose a focus helper'); -assert.ok(/\.focus\(\{\s*preventScroll:\s*true\s*\}\)/.test(mainSource), 'summary pane focus should not scroll the page'); -assert.ok(/moveActiveSection[\s\S]*focusSummaryPane/.test(mainSource), 'card navigation should focus the summary pane'); +const viewSource = fs.readFileSync(path.join(__dirname, '..', 'src', 'view.ts'), 'utf8'); +assert.ok(!/\basync\s+onOpen\s*\(/.test(viewSource), 'ParallelReaderView.onOpen should not be async without await'); +assert.ok(!/\basync\s+onClose\s*\(\)\s*\{\s*\}/.test(viewSource), 'empty onClose should not be async'); +assert.ok(/focusSummaryPane\s*\(\)/.test(viewSource), 'summary pane should expose a focus helper'); +assert.ok(/\.focus\(\{\s*preventScroll:\s*true\s*\}\)/.test(viewSource), 'summary pane focus should not scroll the page'); +assert.ok(/moveActiveSection[\s\S]*focusSummaryPane/.test(viewSource), 'card navigation should focus the summary pane'); assert.ok(/scheduleCacheSave\s*\(/.test(mainSource), 'cache touch should use a debounced cache save path'); assert.ok(/flushCacheSave\s*\(/.test(mainSource), 'pending cache touches should be flushable'); assert.ok(/onunload[\s\S]*flushCacheSave/.test(mainSource), 'plugin unload should flush pending cache touches');