Replaces the single long-Notice failure UX with a kind-aware dispatcher
so users get actionable buttons instead of a wall of text.
- CliProcessError extends Error with a typed CliErrorDetails payload
(reason / pid / elapsed / idle / bytes / tails / exitCode / signal /
timeoutMs / idleTimeoutMs). All five runCli failure paths now throw
the typed error; message format is preserved so existing classifier
regex and tests still match.
- classifyGenerationError short-circuits on details.reason for the
deterministic cases (wall/idle-timeout → timeout, spawn/startup →
config, streams-unavailable → unknown). exit-nonzero falls through
so stderr-derived auth/rate-limit hints still classify correctly.
Duck-typed check avoids cli ↔ generation-job-manager circular import.
- New error-ui.ts dispatches by ErrorKind:
timeout (with details) → TimeoutDiagnosticsModal showing cmd, pid,
elapsed, idle, bytes, redacted stderr/stdout tail, plus copy /
open-settings / close buttons.
timeout (no details, e.g. API streaming) → actionable Notice.
auth/config → Notice + "Open Settings" CTA.
rate-limit → Notice + "Copy details".
schema → Notice + "Copy raw output" CTA.
unknown → unchanged legacy short Notice.
- main.ts handleGenerationError delegates to dispatcher; new
openPluginSettings() helper uses Obsidian's app.setting.openTabById.
- 21 i18n strings (zh + en) for all new UI labels and the modal copy.
- styles.css adds error notice + diagnostics modal styling.
- 7 new test assertions cover typed details on every failure path and
classifyGenerationError respecting structured reasons.
Change-Id: I8af46d375d92ba26d14180ffbeb6c3a6dedd89f5
The CLI provider previously surfaced only "CLI timed out (Xms)" on hang,
discarding accumulated stderr/stdout, pid, and timing — leaving users
unable to tell network hangs from auth failures from a slow model.
- runCli timeout/idle errors now include pid, elapsed, idle duration,
utf-8 byte counts, plus stderr/stdout tails (with secret redaction
for Bearer tokens, sk-* keys, and Authorization-style headers).
- Add optional cliIdleTimeoutMs (default 0/disabled): kill the CLI if
no output for N ms, independent of the wall-clock cliTimeoutMs.
- Add optional debugLogging: console hooks at spawn/ok/fail with pid
and byte counts for live diagnosis.
- Settled-guard on noteActivity/armIdleTimer prevents stale timers
after first-wins settle (cancel / wall / idle / child-error / close).
- New unit coverage: rich diagnostic suffix, idle timeout firing, idle
reset on heartbeat, secret redaction, idle normalization edge cases.
Change-Id: I9191f8d348ef43ff9605dc5169f0951f5af85d26
Add eslint v9 + @typescript-eslint/parser + eslint-plugin-obsidianmd as
a dedicated `npm run lint:obsidian` track, separate from the existing
biome lint. Apply autofixes from the recommended config and resolve the
follow-on type/test issues.
- prefer-create-el: createEl('div'/'span') -> createDiv/createSpan
- prefer-active-window-timers: setTimeout/clearTimeout -> activeWindow.*
+ retype timer holders from ReturnType<typeof setTimeout> to number
- prefer-active-doc: drop globalThis.fetch in streaming
- no-useless-catch: drop no-op rethrow wrapper
- no-restricted-globals: streaming.ts is exempted because requestUrl
does not support streaming responses
- @typescript-eslint/no-unsafe-*: disabled (out of scope for an Obsidian
plugin lint pass; tsc + biome already cover type safety)
- tests: polyfill globalThis.activeWindow so unit tests still run in Node
Change-Id: I43cc652e29a66d490e2834e940843c39b211b80b
The settings tab listed ~23 entries on first paint (API mode), most of
which the typical user never touches. Restructured into 5 sections,
of which only the first two are expanded by default.
Sections (default visibility):
- Quick setup (open): backend / provider preset / credential / model / test
- Reading output (open): prompt language / card range / export folder / UI lang
- Advanced connection: api format / base URL / auth type / headers /
max tokens / streaming + timeout merged
- Advanced prompt: max input chars / custom system prompt
- Cache & maintenance: max entries / clear-all
First-paint count drops from ~23 to ~9 rows.
Smart auto-expand: Advanced connection opens automatically when the
user has departed from preset defaults — custom-* provider, custom
headers, non-default base URL, non-auto auth type, streaming=false,
non-default max tokens, or api format mismatching preset. Same idea
for Advanced prompt (custom prompt or non-default max input chars)
and CLI advanced (non-default cli timeout). Old users find their
configured values without manual digging.
Other simplifications:
- API key + env var fallback combined into one row. The env var input
lives behind a tiny details/summary disclosure and is built with
raw DOM (label + input) instead of a nested `new Setting` to avoid
Obsidian theme CSS conflicts on .setting-item.
- Provider preset row gains an inline read-only summary of the
derived API format and base URL, replacing the standalone rows
for those two fields under built-in presets.
- Streaming toggle and streaming timeout are now one row; the
timeout input is wrapped in its own div and toggled visible only
when streaming is on.
No data.json changes — every settings key keeps its existing name and
default value. Existing installs upgrade with zero migration.
styles.css: add scoped classes for the collapsible sections, env-var
inline row, timeout inline input, and a generic .parallel-reader-hidden.
i18n: 7 new keys × zh+en for section headings + summary fallbacks.
Codex review pass:
- shouldOpenAdvancedConnection now also reacts to streaming=false /
non-default apiMaxTokens / apiFormat ≠ preset.format.
- streaming + timeout merged row hides a dedicated wrapper instead of
inputEl.parentElement (which on some themes shares the controlEl
with the toggle).
- env-var fallback no longer nests `new Setting` inside a controlEl.
- "(no base url)" placeholder in the preset summary is now i18n'd.
Change-Id: I8ef11c56a89121df13a630b1c724652e78b4069f
UX change:
- The "open Parallel Reader" command now toggles the right sidebar's
collapsed state instead of detaching the leaf. The tab and its content
are preserved across hide/show, matching standard Obsidian behaviour
for File Explorer and friends. Detaching the tab on every second
press destroyed cached cards and forced re-generation.
Behaviour:
- No leaf yet → create + reveal
- Leaf exists, right sidebar collapsed → reveal (expands sidebar)
- Leaf exists, right sidebar expanded → collapse(); tab is preserved
- No right sidebar (mobile / unusual layout) → reveal as fallback
Tests:
- New tests/view-render.test.js (18 cases) covering the component layer
that was previously a gap. The 1.0.12 → 1.0.13 sidebar regression
could not have been caught by any existing pure-function test; this
file fills that gap by mocking app.workspace + view spies and running
real runForFile / refreshViewAfterCache* / toggleParallelView.
- shouldRender matrix (4 cases): silentView × sourceFile state — the
exact regression introduced in PR2 where guards were over-applied.
- toggleParallelView (4 cases): no-leaf opens, expanded → collapse,
collapsed → reveal, no-rightSplit fallback.
- refreshViewAfterCacheDelete + Clear (4 cases): matching file vs
different file vs no-view, plus clear-all always renderEmpty.
- runForFile outcome enum mapping (6 cases): already-running early
return, already-running from catch, cancelled, generic error,
regenerate-confirm cancellation, skipEditConfirm bypass — these
drive batch statistics and were untested before.
- view.deleteCard / updateCard cardPersistFailed path (3 cases):
exposes ParallelReaderView via test-exports for instantiation;
mocks cacheReplaceCards returning false to verify the failure
notice path lands and the public method returns false.
src/test-exports.ts: export ParallelReaderView for component tests.
tests/catalog.json: register view-render.test.js under component.
Change-Id: I439b5fca07b0928e6fe44d9025c2421c71c6cb28
Findings from the original review (P2):
- streaming: flush unterminated final SSE event at EOF (some providers
close the stream without a trailing blank line, dropping the last delta)
- cache-manager: validate entry shape on load — drop entries where
cards is not an array, anchor is not a string, or bullets is not an
array. Tolerates missing optional fields (treated as cache miss).
- view: card edit/delete now check cacheReplaceCards return and surface
failures via a localized Notice instead of pretending success
- main: clear-current / clear-all / file-menu-clear refresh open view
via renderEmpty so stale UI does not display deleted data
- prompt + settings-tab: card count is normalized via a single helper
used by buildPrompts and onChange. Prompt and fingerprint stay in sync.
UI value is written back to the textbox after clamping.
- generation-job-manager: global concurrency limit (default 3) with a
cancellable wait queue. Race-safe slot accounting via a reserved
counter so resolved-but-not-yet-set waiters are visible to fast-path
start() callers.
- runForFile: now returns RunForFileResult; accepts preloadedContent +
silentView + skipEditConfirm options used by batch (and reflected on
the PluginHost interface).
- batch: avoid double file read, do not steal UI focus, classify
results correctly (generated / cached / already-running / empty / ...)
Follow-up from the 1.0.11 review:
- generationFingerprint: codex backend excludes settings.model from the
hash. Codex ignores --model; previously editing model spuriously
invalidated all codex cache.
Codex review pass:
- generation-job-manager: race fix — releaseSlot's resolve microtask
and a synchronous start() fast-path could briefly overshoot
maxConcurrent. Reserve the slot synchronously inside the wrapped
resolve to close the window.
- cache-manager: anchor type validation in addition to bullets.
NOTE: Codex backend users will see a one-time "stale cache" banner on
existing notes due to the fingerprint change; regenerate to refresh.
Change-Id: I7721c7dfe51dea3f51b0215764f721523c2f6806
P1:
- cli: pin codex --sandbox read-only (defeat prompt injection)
- settings: clear apiKey/apiHeaders on provider preset switch
- main/view: render error state when LLM returns empty cards
- schema/cli: do not log raw model output; UI errors expose length only
P2:
- cli: pass --model to Claude Code when settings.model set
- settings-tab: add CLI timeout input + normalizeCliTimeoutMs (min 1s)
- view: export source link uses [[path|basename]]
- view: export failures surface a localized Notice
P3:
- schema: cardUntitled fallback now respects uiLanguage
- generation-job-manager: error codes localized via main.ts
- batch: settled guard on promptForBatchFolder modal
- README: drop stale e2e validator install note
Codex review pass:
- provider-parsers: forward settings to normalizeCardsPayload
- generation-job-manager: classifyGenerationError regex covers new EN/zh
schema-error messages
- cli: do NOT pass --model to codex (DEFAULT_SETTINGS.model is a Claude
name and would break codex; codex relies on its own config)
Tests: cli (codex sandbox + claude --model + classify zh/en), settings
(applyApiProviderPreset isolation + normalizeCliTimeoutMs), schema
(language-aware fallback title), test-exports (new symbols).
Change-Id: I9f4c21f9299e1a9bf166f69f712859854482fe92
The Claude Code CLI doesn't support --max-tokens. This caused the CLI
backend to always fail with exit code 1. Also adds args validation tests
for both claude and codex CLI backends.
Change-Id: I57c5cc672ed08de291e3941511ce68ba56e5b4ff
- Create scripts/run-tests.mjs to auto-discover test files, replacing
the 900-char inline test command in package.json (P3-1)
- Add test:only script for CI to avoid duplicate build+typecheck (P2-1)
- Preserve original SyntaxError as cause in responseJson (P2-3)
Change-Id: I51d0c8c51d3b0b648ed5ff4ea282bda070b0d457
Extracts renderCliBackendSettings() and renderApiBackendSettings() from
the 144-line renderBackendSection(). The parent method is now a 5-line
dispatcher. CLI settings (14 lines) and API settings (128 lines) are
cleanly separated.
Change-Id: Ic866ba78d3b572f5f564760b248460584b0819c5
Replaces the 64-line __test manual export block in main.ts with a
single import from src/test-exports.ts. Also removes 30+ imports that
were only needed for __test re-export. main.ts reduced from 788 to
693 lines.
Change-Id: I029f127faf053135d04dba17b8c39c8c39ca8540
Moves RequestUrlFunction type, endpointUrl, responseJson, requestJsonBody,
shouldRetryWithoutStructuredOutput, and requestJsonBodyWithStructuredFallback
to a dedicated provider-request.ts module (103 lines). providers.ts reduced
from 377 to 277 lines.
Change-Id: I013d6a0b9565770c00e48eb416541fcf08d1c5e8
Decomposes the 76-line render() method into renderHeader(),
renderStaleBanner(), renderLoadingState(), renderErrorState(), and
renderCardList(). Each helper is under 22 lines. render() is now a
15-line orchestrator that delegates to the appropriate helper.
Change-Id: Iffcf31131fcf0bce882c1e9ee35aaf58d002b5fa
Moves 18 provider preset definitions (148 lines) from settings.ts into
a dedicated provider-presets.ts file. settings.ts reduced from 443 to
297 lines, now contains only utility functions and non-preset constants.
Change-Id: Ia9a1ef48854ab79cb405df9cd1f88d19826317d6
Moves the STRINGS object (330 lines of zh/en translations) from i18n.ts
into a separate i18n-strings.ts file. i18n.ts now contains only the
resolveUiLanguage and translate logic (28 lines).
Change-Id: I9ddea79bddb4e8313dc16aae2a7c8d4eeff96fe1
Add DEFAULT_MAX_OFFSET_PX (80), DEFAULT_PROBE_RATIO (0.1), and
FALLBACK_FRAME_MS (16) to document the purpose of each value.
Change-Id: Ie34d61c290384de81ae001f292e89f17b410e701
Fix 4 locations in providers.ts, cli.ts, and settings-tab.ts where error
objects were cast to Error without checking. Now uses proper instanceof
narrowing with String fallback for non-Error throwables.
Change-Id: Ib324380f5d56945672e32b9d3621855f6179186a
Move buildAnthropicMessagesBody, buildOpenAiChatBody, buildOpenAiResponsesBody,
buildGeminiBody, tokenLimitFieldForOpenAiChat and their type interfaces to a
new module. Reduces providers.ts from 494 to 367 lines (under 400 target).
Change-Id: Ib124d5cf5f2f66bac03acbf83e1a224cb6325d12
Replace fragile double-casts with a branded ScheduleId wrapper that
unifies RAF (returns number) and setTimeout (returns Timeout) IDs
without losing type safety. The wrapper is opaque to callers.
Change-Id: I304d68b562ff803d0b20ebcbfb71a3c3d9e1d881
Set noImplicitAnyLet to error in biome.json. Fix the violation in
parseCardsJson by typing the parsed variable as unknown. Widen
normalizeCardsPayload to accept unknown (it already handles non-object
input defensively). Remove now-unnecessary cast in provider-parsers.ts.
Change-Id: I57ee46d3179775d5d7c34f35803a44c45061b0ec
Split the 160-line render() method into three focused methods:
- render(): orchestrates header, state, and card list (now ~60 lines)
- renderCard(): handles single card DOM creation with content and events
- showCardContextMenu(): builds the right-click context menu
Change-Id: I46f94e75d6aef1043ac905f1dc33c13cf4ae8447
Move the inline Modal dialog for batch folder selection into a reusable
promptForBatchFolder function. Removes Modal import from main.ts.
main.ts now 775 lines.
Change-Id: I7a5c62b7df95635dcbe0ac814007838ff83eded4
Move resolveCardAnchors to src/cards.ts and confirmRegenerateEditedCards
to src/modal.ts to reduce main.ts from 835 to 796 lines (under 800 target).
Change-Id: If38ed4e45c61658b7cc52a6283f8e0e24458761e
The constructor always receives a validated ResolvedCard from the call
site (which guards with !this.sections[index]). The fallback to an
empty object cast as ResolvedCard was dead code that hid type safety.
Change-Id: I35c9731d9079b9aeea5eb0fda7eda46a740bc964
The previous code checked signal.aborted before adding the listener,
creating a timing window where the signal could abort between the check
and addEventListener. Now the listener is always registered first, then
signal.aborted is checked after, ensuring no abort event is missed.
Change-Id: I1abc3ad21e5bb93559d66922edf7c325a977bc65
normalizeSettings previously modified its input parameter in-place.
While callers already created copies before calling it, the function
signature was misleading. Now it creates a shallow copy internally and
operates on that, with a Readonly<PluginSettings> parameter type to
signal intent. Added a test verifying the original object is unchanged.
Change-Id: I7600ac37930bbcebb02aa7c84d8988cb7aa71196
The CLI path was not forwarding settings.apiMaxTokens, causing the
model to use its own default which is too small for long documents
with many CJK cards. This was the root cause of repeated "LLM 返回
非 JSON" errors — the JSON was being cut off mid-card.
Change-Id: Ia738418e2e6cf7f919752cb3128a82768d95cedf
- Add repairTruncatedCardsJson to recover complete cards when output
is cut off mid-card (e.g. token limit reached)
- Log raw LLM response to console on parse failure for debugging
- Make error panel text selectable and add "copy error" button
Change-Id: I4e76121138888234d42f84f3695cbbfd6beba886
- Fix CI to run all 4 test files (was missing direct-modules.test.js)
- Remove unused ESLint dependencies (eslint, @typescript-eslint/*, eslint-plugin-obsidianmd)
- Enable noImplicitReturns in tsconfig.json for stricter type safety
- Make applyApiProviderPreset return new object instead of mutating input
- Add per-file error handling in batch processing loop with error count in summary
- Narrow all catch clause types to unknown with proper instanceof narrowing
Change-Id: I76975dbd5ebdb5645d18c5858c8a52fd6da3405e
When a note has no cached parallel notes, the empty state now shows a
clickable "Generate" button so users can trigger generation directly
without going through the command palette. Closes#2.
Change-Id: I879897168ef77c44e0ab570e9c094fa9af9f66d4
- Remove unnecessary TFile cast (instanceof already narrows)
- Remove await on non-Promise view methods (loadFor, renderLoading, renderError)
- Remove async from CacheManager.touch() (no await expression)
- Replace this-aliasing with captured locals in batch modal
- Use CSS class instead of inline style for modal input width
- Fix sentence case in settings UI text
Change-Id: I2a222c6429eb7e4d69761cca8369956fef510054