From 04428950de91a828a3ffa6b4a3694bc07e11a33e Mon Sep 17 00:00:00 2001 From: wujunchen Date: Sat, 9 May 2026 10:48:36 +0800 Subject: [PATCH] chore: prepare 1.0.19 release Change-Id: I86c01c3d6a2c13a193636ac08ba562d54a2e9a20 --- esbuild.config.mjs | 7 +- eslint.obsidian-review.config.mjs | 47 +++++++++++++ manifest.json | 2 +- package.json | 2 + src/cli.ts | 113 +++++++++++------------------- src/error-ui.ts | 55 +++++++++++---- src/generation-job-manager.ts | 1 - src/i18n-strings.ts | 28 ++++---- src/settings-tab.ts | 32 +-------- src/settings.ts | 13 ---- src/test-exports.ts | 1 - src/types.ts | 4 +- tests/cli.test.js | 41 ----------- tests/settings.test.js | 13 ---- versions.json | 3 +- 15 files changed, 151 insertions(+), 211 deletions(-) create mode 100644 eslint.obsidian-review.config.mjs diff --git a/esbuild.config.mjs b/esbuild.config.mjs index da556d7..b9031bb 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -4,12 +4,7 @@ import { builtinModules } from 'module'; const production = process.argv[2] === 'production'; const watch = process.argv[2] === 'watch'; -const external = [ - 'obsidian', - 'electron', - ...builtinModules, - ...builtinModules.map(m => `node:${m}`), -]; +const external = ['obsidian', 'electron', ...builtinModules, ...builtinModules.map((m) => `node:${m}`)]; const context = await esbuild.context({ entryPoints: ['main.ts'], diff --git a/eslint.obsidian-review.config.mjs b/eslint.obsidian-review.config.mjs new file mode 100644 index 0000000..9a67a53 --- /dev/null +++ b/eslint.obsidian-review.config.mjs @@ -0,0 +1,47 @@ +import tsparser from "@typescript-eslint/parser"; +import { defineConfig } from "eslint/config"; +import obsidianmd from "eslint-plugin-obsidianmd"; + +export default defineConfig([ + ...obsidianmd.configs.recommended, + { + files: ["main.ts", "src/**/*.ts"], + languageOptions: { + parser: tsparser, + parserOptions: { project: "./tsconfig.json" }, + }, + rules: { + // Keep this review check close to Obsidian policy rules while avoiding + // project-specific dynamic-type noise that the ReviewBot did not report. + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-return": "off", + + // The ReviewBot currently reports underscore-only unused bindings as + // Optional findings, while the published recommended config ignores them. + "@typescript-eslint/no-unused-vars": [ + "warn", + { + args: "all", + caughtErrors: "all", + ignoreRestSiblings: true, + }, + ], + }, + }, + { + ignores: [ + "node_modules/", + "main.js", + "main.js.map", + "tests/", + "scripts/", + ".e2e/", + ".orchestration/", + ".agent/", + ".codex-tmp/", + ], + }, +]); diff --git a/manifest.json b/manifest.json index ca66634..427a71d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "parallel-reader", "name": "Parallel Reader", - "version": "1.0.18", + "version": "1.0.19", "minAppVersion": "1.8.7", "description": "AI-powered split-view reading: source note on the left, LLM-generated summary cards on the right with scroll-sync highlighting.", "author": "lancivez", diff --git a/package.json b/package.json index 93d7467..04c02e3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "lint": "biome check main.ts src/ tests/ scripts/ .e2e/scripts/", "lint:fix": "biome check --write main.ts src/ tests/ scripts/ .e2e/scripts/", "lint:obsidian": "eslint main.ts src/", + "lint:obsidian:review": "eslint --config eslint.obsidian-review.config.mjs --no-config-lookup main.ts src/", + "lint:obsidian:review:all": "npm run lint:obsidian:review -- --max-warnings=0", "version": "node scripts/bump-version.mjs", "test": "npm run build && npm run typecheck && node scripts/run-tests.mjs", "test:only": "node scripts/run-tests.mjs", diff --git a/src/cli.ts b/src/cli.ts index beecfa4..f683a4f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -52,16 +52,8 @@ export function resolveCliPath(name: string, override: string): string { return name; } -export interface RunCliOptions { - /** Idle timeout in ms — fail if no stdout/stderr data arrives for this long. 0 disables. */ - idleTimeoutMs?: number; - /** When true, log spawn/settle lifecycle to console for diagnostics. */ - debug?: boolean; -} - export type CliFailureReason = | 'wall-timeout' - | 'idle-timeout' | 'exit-nonzero' | 'streams-unavailable' | 'spawn-failure' @@ -72,6 +64,7 @@ export interface CliErrorDetails { cmd: string; pid: number | null; elapsedMs: number; + /** ms since the last byte of stdout/stderr — useful to tell hung-mid-call from hung-from-start. */ idleMs: number | null; stdoutBytes: number; stderrBytes: number; @@ -82,7 +75,6 @@ export interface CliErrorDetails { exitCode: number | null; signal: NodeJS.Signals | null; timeoutMs: number; - idleTimeoutMs: number; } export class CliProcessError extends Error { @@ -95,8 +87,8 @@ export class CliProcessError extends Error { } } -const DIAG_STDERR_TAIL_CHARS = 800; -const DIAG_STDOUT_TAIL_CHARS = 200; +const DIAG_STDERR_TAIL_CHARS = 1500; +const DIAG_STDOUT_TAIL_CHARS = 1500; function tail(text: string, max: number): string { if (text.length <= max) return text; @@ -123,15 +115,11 @@ export function runCli( timeoutMs: number, job?: GenerationJob, spawnImpl: typeof spawn = spawn, - options: RunCliOptions = {}, ): Promise<{ stdout: string; stderr: string }> { - const idleTimeoutMs = Math.max(0, Math.floor(options.idleTimeoutMs ?? 0)); - const debug = !!options.debug; return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { let child: ReturnType; let settled = false; let timer: number | null = null; - let idleTimer: number | null = null; const startedAt = Date.now(); let lastActivityAt = startedAt; const clearTimers = () => { @@ -139,42 +127,40 @@ export function runCli( activeWindow.clearTimeout(timer); timer = null; } - if (idleTimer) { - activeWindow.clearTimeout(idleTimer); - idleTimer = null; - } }; const fail = (err: Error) => { if (settled) return; settled = true; clearTimers(); - if (debug) { - console.warn('[parallel-reader] cli failed', { - cmd, - pid: child?.pid, - elapsedMs: Date.now() - startedAt, - error: err.message, - }); - } + console.warn('[parallel-reader] cli failed', { + cmd, + pid: child?.pid, + elapsedMs: Date.now() - startedAt, + error: err.message, + }); reject(err); }; const succeed = (value: { stdout: string; stderr: string }) => { if (settled) return; settled = true; clearTimers(); - if (debug) { - console.debug('[parallel-reader] cli ok', { - cmd, - pid: child?.pid, - elapsedMs: Date.now() - startedAt, - stdoutBytes: utf8ByteLength(value.stdout), - stderrBytes: utf8ByteLength(value.stderr), - }); - } + console.debug('[parallel-reader] cli ok', { + cmd, + pid: child?.pid, + elapsedMs: Date.now() - startedAt, + stdoutBytes: utf8ByteLength(value.stdout), + stderrBytes: utf8ByteLength(value.stderr), + }); resolve(value); }; try { child = spawnImpl(cmd, args, { + // Lock cwd to the user's home dir. Inheriting Obsidian's cwd (often the Vault path) + // can break Claude CLI's per-project memory writes — iCloud-synced Vault paths + // contain characters like `~md~` that produce malformed `~/.claude/projects/...` + // slugs and an immediate exit-1 with empty modelUsage. Home dir is stable and + // writable; the LLM call itself is independent of cwd. + cwd: os.homedir(), stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, @@ -191,7 +177,7 @@ export function runCli( }); } catch (e: unknown) { const message = `Failed to start ${cmd}: ${e instanceof Error ? e.message : String(e)}`; - if (debug) console.warn('[parallel-reader] cli spawn failed', { cmd, error: message }); + console.warn('[parallel-reader] cli spawn failed', { cmd, error: message }); return reject( new CliProcessError(message, { reason: 'spawn-failure', @@ -206,20 +192,16 @@ export function runCli( exitCode: null, signal: null, timeoutMs, - idleTimeoutMs, }), ); } - if (debug) { - console.debug('[parallel-reader] cli spawn', { - cmd, - argCount: args.length, - pid: child?.pid, - timeoutMs, - idleTimeoutMs, - }); - } + console.debug('[parallel-reader] cli spawn', { + cmd, + argCount: args.length, + pid: child?.pid, + timeoutMs, + }); let stdout = ''; let stderr = ''; @@ -241,7 +223,6 @@ export function runCli( exitCode: extras.exitCode ?? null, signal: extras.signal ?? null, timeoutMs, - idleTimeoutMs, }); const buildDiagSuffix = (details: CliErrorDetails): string => { @@ -264,22 +245,6 @@ export function runCli( fail(new CliProcessError(`CLI timed out (${timeoutMs}ms)${buildDiagSuffix(details)}`, details)); }, timeoutMs); - const armIdleTimer = () => { - if (settled || !idleTimeoutMs) return; - if (idleTimer) activeWindow.clearTimeout(idleTimer); - idleTimer = activeWindow.setTimeout(() => { - const idleMs = Date.now() - lastActivityAt; - try { - child.kill('SIGKILL'); - } catch (e: unknown) { - console.warn('[parallel-reader] failed to kill idle-timed-out CLI process', e); - } - const details = collectDetails('idle-timeout', idleMs, { signal: 'SIGKILL' }); - fail(new CliProcessError(`CLI idle timeout (${idleTimeoutMs}ms)${buildDiagSuffix(details)}`, details)); - }, idleTimeoutMs); - }; - armIdleTimer(); - if (job) { job.onCancel(() => { try { @@ -303,7 +268,6 @@ export function runCli( const noteActivity = () => { if (settled) return; lastActivityAt = Date.now(); - armIdleTimer(); }; childStdout.on('data', (d) => { @@ -328,7 +292,14 @@ export function runCli( if (code !== 0) { const idleMs = Date.now() - lastActivityAt; const details = collectDetails('exit-nonzero', idleMs, { exitCode: code, signal }); - return fail(new CliProcessError(`CLI exited with code ${code}\nstderr:\n${stderr.slice(0, 1000)}`, details)); + // Some CLIs (notably `claude --output-format json`) emit error events to stdout + // instead of stderr. Always include both tails so the failure mode is debuggable + // even when stderr is empty. + let suffix = ''; + if (details.stderrTail) suffix += `\nstderr:\n${details.stderrTail}`; + if (details.stdoutTail) suffix += `\nstdout:\n${details.stdoutTail}`; + if (!suffix) suffix = '\n(no output on either stream)'; + return fail(new CliProcessError(`CLI exited with code ${code} (signal=${signal ?? 'none'})${suffix}`, details)); } succeed({ stdout, stderr }); }); @@ -369,10 +340,7 @@ export async function summarizeViaClaudeCode( ]; const claudeModel = (settings.model || '').trim(); if (claudeModel) args.push('--model', claudeModel); - const { stdout } = await runCli(cmd, args, user, settings.cliTimeoutMs, job, spawnImpl, { - idleTimeoutMs: settings.cliIdleTimeoutMs, - debug: settings.debugLogging, - }); + const { stdout } = await runCli(cmd, args, user, settings.cliTimeoutMs, job, spawnImpl); // --output-format json produces a JSON array of event objects. // Find the "result" entry and extract its .result text field. @@ -424,9 +392,6 @@ export async function summarizeViaCodex( // (claude-sonnet-4-6), which would break Codex if passed verbatim. Codex // uses its own config.toml profile for model selection. const args = ['exec', '--skip-git-repo-check', '--sandbox', 'read-only', '-']; - const { stdout } = await runCli(cmd, args, combined, settings.cliTimeoutMs, job, spawnImpl, { - idleTimeoutMs: settings.cliIdleTimeoutMs, - debug: settings.debugLogging, - }); + const { stdout } = await runCli(cmd, args, combined, settings.cliTimeoutMs, job, spawnImpl); return parseCardsJson(stdout, settings); } diff --git a/src/error-ui.ts b/src/error-ui.ts index 035e5ec..12979d6 100644 --- a/src/error-ui.ts +++ b/src/error-ui.ts @@ -48,7 +48,21 @@ function showActionableNotice(message: string, actions: ActionableNoticeAction[] return notice; } -class TimeoutDiagnosticsModal extends Modal { +function reasonCopyKeys(reason: CliErrorDetails['reason']): { titleKey: string; reasonKey: string } { + switch (reason) { + case 'wall-timeout': + return { titleKey: 'errorModalTimeoutTitle', reasonKey: 'errorModalReasonWall' }; + case 'exit-nonzero': + return { titleKey: 'errorModalExitTitle', reasonKey: 'errorModalReasonExit' }; + case 'spawn-failure': + case 'startup-error': + return { titleKey: 'errorModalStartupTitle', reasonKey: 'errorModalReasonStartup' }; + default: + return { titleKey: 'errorModalTimeoutTitle', reasonKey: 'errorModalReasonWall' }; + } +} + +class CliDiagnosticsModal extends Modal { private readonly settings: PluginSettings; private readonly details: CliErrorDetails; private readonly fullMessage: string; @@ -72,10 +86,18 @@ class TimeoutDiagnosticsModal extends Modal { const { contentEl } = this; contentEl.empty(); contentEl.addClass('parallel-reader-error-modal'); - contentEl.createEl('h2', { text: translate(this.settings, 'errorModalTimeoutTitle') }); - - const reasonKey = this.details.reason === 'idle-timeout' ? 'errorModalReasonIdle' : 'errorModalReasonWall'; + const { titleKey, reasonKey } = reasonCopyKeys(this.details.reason); + contentEl.createEl('h2', { text: translate(this.settings, titleKey) }); contentEl.createEl('p', { text: translate(this.settings, reasonKey) }); + if (this.details.exitCode != null) { + contentEl.createEl('p', { + cls: 'parallel-reader-error-modal-exit', + text: translate(this.settings, 'errorModalFieldExit', { + code: String(this.details.exitCode), + signal: this.details.signal ?? 'none', + }), + }); + } const grid = contentEl.createDiv({ cls: 'parallel-reader-error-modal-grid' }); const addRow = (label: string, value: string) => { @@ -95,13 +117,18 @@ class TimeoutDiagnosticsModal extends Modal { `stdout ${this.details.stdoutBytes}B / stderr ${this.details.stderrBytes}B`, ); + let hasOutput = false; if (this.details.stderrTail) { contentEl.createEl('h3', { text: translate(this.settings, 'errorModalStderrTail') }); contentEl.createEl('pre', { cls: 'parallel-reader-error-modal-tail', text: this.details.stderrTail }); - } else if (this.details.stdoutTail) { + hasOutput = true; + } + if (this.details.stdoutTail) { contentEl.createEl('h3', { text: translate(this.settings, 'errorModalStdoutTail') }); contentEl.createEl('pre', { cls: 'parallel-reader-error-modal-tail', text: this.details.stdoutTail }); - } else { + hasOutput = true; + } + if (!hasOutput) { contentEl.createEl('p', { cls: 'parallel-reader-error-modal-empty', text: translate(this.settings, 'errorModalNoOutput'), @@ -139,13 +166,17 @@ export function showGenerationError( ): void { const tr = (k: string, vars?: Record) => translate(ctx.settings, k, vars); + // Any structured CLI error opens the diagnostics modal — even when classify falls + // through to 'unknown' (e.g. exit-nonzero with empty stderr) we want the stdout tail + // surfaced so the failure is debuggable. + const cliDetails = (error as { details?: CliErrorDetails }).details; + const isStructuredCliError = !!cliDetails && typeof cliDetails.reason === 'string'; + if (isStructuredCliError) { + new CliDiagnosticsModal(ctx.app, ctx.settings, cliDetails, message, ctx.openSettings).open(); + return; + } + if (kind === 'timeout') { - const details = (error as { details?: CliErrorDetails }).details; - if (details) { - // Open a modal so users can read the stderr tail at leisure and copy it. - new TimeoutDiagnosticsModal(ctx.app, ctx.settings, details, message, ctx.openSettings).open(); - return; - } // API streaming timeout: no structured details, show actionable notice with raw message tail. showActionableNotice(tr('errorNoticeTimeout'), [ { diff --git a/src/generation-job-manager.ts b/src/generation-job-manager.ts index 65d0891..0b272e0 100644 --- a/src/generation-job-manager.ts +++ b/src/generation-job-manager.ts @@ -202,7 +202,6 @@ export function classifyGenerationError(error: unknown): ErrorKind { if (details && typeof details.reason === 'string') { switch (details.reason) { case 'wall-timeout': - case 'idle-timeout': return 'timeout'; case 'spawn-failure': case 'startup-error': diff --git a/src/i18n-strings.ts b/src/i18n-strings.ts index 1c5027e..e902437 100644 --- a/src/i18n-strings.ts +++ b/src/i18n-strings.ts @@ -93,10 +93,15 @@ export const STRINGS: Record> = { errorActionCopyDetails: '复制详情', errorActionCopyRaw: '复制原始输出', errorModalTimeoutTitle: 'CLI 超时诊断', + errorModalExitTitle: 'CLI 异常退出诊断', + errorModalStartupTitle: 'CLI 启动失败诊断', errorModalReasonWall: 'CLI 在墙钟超时之前没有完成。建议先看 stderr 是否有线索,再考虑增大 CLI 超时或换用 API 后端。', - errorModalReasonIdle: - 'CLI 在静默超时窗口内没有任何输出,已被终止。常见原因:网络挂死、模型在思考但 CLI 不流式输出。', + errorModalReasonExit: + 'CLI 进程返回了非零退出码。下方是 stdout 和 stderr 的末尾(已脱敏)—— claude CLI 在 --output-format json 模式下会把错误事件写到 stdout,请优先看那里。', + errorModalReasonStartup: + 'CLI 进程未能启动。常见原因:路径错误、二进制不可执行、Obsidian GUI 没继承 shell PATH。请在设置里填绝对路径再试。', + errorModalFieldExit: '退出码 {code}(信号 {signal})', errorModalFieldCmd: '命令', errorModalFieldPid: '进程 PID', errorModalFieldElapsed: '已耗时', @@ -139,10 +144,6 @@ export const STRINGS: Record> = { settingCliPathPlaceholder: '例:/Users/you/bin/codex', settingCliTimeoutName: 'CLI 超时 (ms)', settingCliTimeoutDesc: 'CLI 调用的最大等待时间(毫秒),最小 1000,默认 120000', - settingCliIdleTimeoutName: 'CLI 静默超时 (ms)', - settingCliIdleTimeoutDesc: '若 CLI 持续 X 毫秒没有任何输出则视为卡死并终止;0 表示关闭,建议 30000–60000', - settingDebugLoggingName: '调试日志', - settingDebugLoggingDesc: '在浏览器控制台打印 CLI spawn / 完成 / 失败的诊断信息(含 PID、耗时、字节数)', sectionQuickSetup: '快速配置', sectionReadingOutput: '阅读输出', sectionAdvancedConnection: '高级连接设置', @@ -306,10 +307,15 @@ export const STRINGS: Record> = { errorActionCopyDetails: 'Copy details', errorActionCopyRaw: 'Copy raw output', errorModalTimeoutTitle: 'CLI timeout diagnostics', + errorModalExitTitle: 'CLI non-zero exit', + errorModalStartupTitle: 'CLI startup failure', errorModalReasonWall: 'The CLI did not finish before the wall-clock timeout. Inspect stderr below, then consider raising the CLI timeout or switching to the API backend.', - errorModalReasonIdle: - 'The CLI produced no output during the idle-timeout window and was terminated. Common causes: network hang, or model thinking with a non-streaming CLI.', + errorModalReasonExit: + 'The CLI process exited with a non-zero status. stdout and stderr tails are below (redacted). Note that `claude --output-format json` writes error events to stdout, so check there first.', + errorModalReasonStartup: + 'The CLI process failed to launch. Common causes: wrong path, non-executable binary, or Obsidian GUI not inheriting shell PATH. Try setting an absolute CLI path in Settings.', + errorModalFieldExit: 'exit {code} (signal {signal})', errorModalFieldCmd: 'Command', errorModalFieldPid: 'PID', errorModalFieldElapsed: 'Elapsed', @@ -355,12 +361,6 @@ export const STRINGS: Record> = { settingCliPathPlaceholder: 'Example: /Users/you/bin/codex', settingCliTimeoutName: 'CLI timeout (ms)', settingCliTimeoutDesc: 'Max wait time for CLI calls (ms). Minimum 1000, default 120000.', - settingCliIdleTimeoutName: 'CLI idle timeout (ms)', - settingCliIdleTimeoutDesc: - 'Kill the CLI if it produces no output for this long. 0 disables. Recommended 30000–60000.', - settingDebugLoggingName: 'Debug logging', - settingDebugLoggingDesc: - 'Log CLI spawn / completion / failure to the browser console (PID, elapsed time, byte counts).', sectionQuickSetup: 'Quick setup', sectionReadingOutput: 'Reading output', sectionAdvancedConnection: 'Advanced connection', diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 94b6b15..01e7a2a 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -13,7 +13,6 @@ import { getApiFormat, getApiPreset, normalizeCardCount, - normalizeCliIdleTimeoutMs, normalizeCliTimeoutMs, normalizeStreamingTimeoutMs, PROMPT_LANGUAGES, @@ -425,13 +424,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab { private renderAdvancedConnectionCli(containerEl: HTMLElement) { const settings = this.plugin.settings; const userChangedTimeout = !!(settings.cliTimeoutMs && settings.cliTimeoutMs !== DEFAULT_SETTINGS.cliTimeoutMs); - const userChangedIdleTimeout = !!settings.cliIdleTimeoutMs; - const userEnabledDebug = !!settings.debugLogging; - const details = this.openCollapsibleSection( - containerEl, - 'sectionAdvancedConnection', - userChangedTimeout || userChangedIdleTimeout || userEnabledDebug, - ); + const details = this.openCollapsibleSection(containerEl, 'sectionAdvancedConnection', userChangedTimeout); new Setting(details) .setName(this.tr('settingCliTimeoutName')) .setDesc(this.tr('settingCliTimeoutDesc')) @@ -444,29 +437,6 @@ export class ParallelReaderSettingTab extends PluginSettingTab { this.plugin.saveSettingsDebounced(); }), ); - - new Setting(details) - .setName(this.tr('settingCliIdleTimeoutName')) - .setDesc(this.tr('settingCliIdleTimeoutDesc')) - .addText((t) => - t - .setPlaceholder('0') - .setValue(String(this.plugin.settings.cliIdleTimeoutMs ?? 0)) - .onChange((v) => { - this.plugin.settings.cliIdleTimeoutMs = normalizeCliIdleTimeoutMs(v); - this.plugin.saveSettingsDebounced(); - }), - ); - - new Setting(details) - .setName(this.tr('settingDebugLoggingName')) - .setDesc(this.tr('settingDebugLoggingDesc')) - .addToggle((toggle) => - toggle.setValue(!!this.plugin.settings.debugLogging).onChange(async (v) => { - this.plugin.settings.debugLogging = v; - await this.plugin.saveSettings(); - }), - ); } /* ---------- 4. Advanced prompt (collapsed by default) ---------- */ diff --git a/src/settings.ts b/src/settings.ts index ed754ea..23047e2 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -13,7 +13,6 @@ export const CACHE_SCHEMA_VERSION = 2; export const DEFAULT_MAX_CACHE_ENTRIES = 100; export const MIN_STREAMING_TIMEOUT_MS = 1000; export const MIN_CLI_TIMEOUT_MS = 1000; -export const MIN_CLI_IDLE_TIMEOUT_MS = 1000; export const PROMPT_LANGUAGES = { zh: '中文', en: 'English', @@ -46,10 +45,8 @@ export const DEFAULT_SETTINGS: PluginSettings = { model: 'claude-sonnet-4-6', exportFolder: 'Reading/Articles', cliTimeoutMs: 120000, - cliIdleTimeoutMs: 0, streaming: true, streamingTimeoutMs: 120000, - debugLogging: false, }; export const API_FORMATS: Record = { @@ -218,9 +215,7 @@ export function normalizeSettings(settings: Readonly): PluginSet if (out.maxCards < out.minCards) out.maxCards = out.minCards; out.streamingTimeoutMs = normalizeStreamingTimeoutMs(out.streamingTimeoutMs); out.cliTimeoutMs = normalizeCliTimeoutMs(out.cliTimeoutMs); - out.cliIdleTimeoutMs = normalizeCliIdleTimeoutMs(out.cliIdleTimeoutMs); if (typeof out.customSystemPrompt !== 'string') out.customSystemPrompt = ''; - out.debugLogging = !!out.debugLogging; return out; } @@ -248,14 +243,6 @@ export function normalizeCliTimeoutMs(value: unknown): number { return n; } -export function normalizeCliIdleTimeoutMs(value: unknown): number { - const n = Math.floor(Number(value)); - if (!Number.isFinite(n) || n < 0) return 0; - if (n === 0) return 0; - if (n < MIN_CLI_IDLE_TIMEOUT_MS) return 0; - return n; -} - function cacheEntryTime(entry: CacheEntry): number { const value = entry && (entry.lastAccessedAt || entry.generatedAt || entry.updatedAt); const timestamp = Date.parse(value || ''); diff --git a/src/test-exports.ts b/src/test-exports.ts index 9c40e45..6c73b7d 100644 --- a/src/test-exports.ts +++ b/src/test-exports.ts @@ -52,7 +52,6 @@ export { getApiBaseUrl, modelForApi, normalizeCardCount, - normalizeCliIdleTimeoutMs, normalizeCliTimeoutMs, normalizeSettings, normalizeStreamingTimeoutMs, diff --git a/src/types.ts b/src/types.ts index 5e8e6c4..c14f29a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -65,10 +65,8 @@ export interface PluginSettings { model: string; exportFolder: string; cliTimeoutMs: number; - cliIdleTimeoutMs: number; streaming: boolean; streamingTimeoutMs: number; - debugLogging: boolean; } /* ---------- Provider types ---------- */ @@ -173,7 +171,7 @@ export interface PluginHost { force: boolean, options?: RunForFileOptions, preloadedContent?: string, - ): Promise; + ): Promise; copyCurrentViewMarkdown(): Promise; scrollEditorToLine(line: number, file: TFile | null): Promise; cacheReplaceCards(filePath: string, cards: ResolvedCard[]): Promise; diff --git a/tests/cli.test.js b/tests/cli.test.js index eb09228..eb8a5f6 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -79,44 +79,6 @@ async function testRunCliEdgeCases() { ); assert.strictEqual(timeoutChild.killedWith, 'SIGKILL', 'runCli kills timed out processes'); - // Idle timeout fires when no data within idle window even if wall clock is generous. - const idleChild = createFakeChild(); - idleChild.pid = 5252; - await assert.rejects( - () => t.runCli('fake', [], '', 60000, undefined, () => idleChild, { idleTimeoutMs: 15 }), - (err) => { - assert.match(err.message, /CLI idle timeout \(15ms\)/, 'idle timeout error message'); - assert.match(err.message, /pid=5252/, 'idle timeout carries pid'); - assert.ok(err instanceof t.CliProcessError, 'idle-timeout throws CliProcessError'); - assert.strictEqual(err.details.reason, 'idle-timeout', 'idle-timeout reason set'); - assert.strictEqual(err.details.idleTimeoutMs, 15, 'details.idleTimeoutMs reflects idle window'); - return true; - }, - 'runCli idle timeout fires when no output', - ); - assert.strictEqual(idleChild.killedWith, 'SIGKILL', 'idle timeout kills the process'); - - // Idle timer must reset on data — child keeps emitting and only wall clock should fire. - const heartbeatChild = createFakeChild(); - let cancelHeartbeat = false; - const heartbeatPromise = t.runCli('fake', [], '', 80, undefined, () => heartbeatChild, { idleTimeoutMs: 30 }); - const beat = () => { - if (cancelHeartbeat) return; - heartbeatChild.stdout.emit('data', Buffer.from('.')); - setTimeout(beat, 10); - }; - beat(); - await assert.rejects( - () => heartbeatPromise, - (err) => { - cancelHeartbeat = true; - assert.match(err.message, /CLI timed out \(80ms\)/, 'wall-clock fires when idle resets keep happening'); - assert.doesNotMatch(err.message, /CLI idle timeout/, 'idle timeout must not fire while data flows'); - return true; - }, - 'runCli idle timer resets on incoming data', - ); - const cancelChild = createFakeChild(); let cancelHandler = null; const cancelPromise = t.runCli( @@ -483,7 +445,6 @@ const wallTimeout = new t.CliProcessError('whatever message text', { exitCode: null, signal: null, timeoutMs: 0, - idleTimeoutMs: 0, }); assert.strictEqual(t.classifyGenerationError(wallTimeout), 'timeout', 'wall-timeout details → timeout kind'); const spawnFailure = new t.CliProcessError('Failed to start claude', { @@ -499,7 +460,6 @@ const spawnFailure = new t.CliProcessError('Failed to start claude', { exitCode: null, signal: null, timeoutMs: 0, - idleTimeoutMs: 0, }); assert.strictEqual(t.classifyGenerationError(spawnFailure), 'config', 'spawn-failure details → config kind'); // exit-nonzero falls through so stderr-derived auth/rate-limit messages can still classify. @@ -516,7 +476,6 @@ const exitWithAuthStderr = new t.CliProcessError('CLI exited with code 1\nstderr exitCode: 1, signal: null, timeoutMs: 0, - idleTimeoutMs: 0, }); assert.strictEqual( t.classifyGenerationError(exitWithAuthStderr), diff --git a/tests/settings.test.js b/tests/settings.test.js index a9deb45..c2e9e79 100644 --- a/tests/settings.test.js +++ b/tests/settings.test.js @@ -111,19 +111,6 @@ assert.strictEqual( 'normalizeSettings protects invalid cli timeout values', ); -// normalizeCliIdleTimeoutMs: 0 means disabled, anything below MIN coerces to 0, valid passes through -assert.strictEqual(t.normalizeCliIdleTimeoutMs(0), 0, 'idle timeout 0 stays disabled'); -assert.strictEqual(t.normalizeCliIdleTimeoutMs(45000), 45000, 'idle timeout valid value passes through'); -assert.strictEqual(t.normalizeCliIdleTimeoutMs('30000'), 30000, 'idle timeout numeric string accepted'); -assert.strictEqual(t.normalizeCliIdleTimeoutMs(500), 0, 'idle timeout below 1000ms disables (treats as off)'); -assert.strictEqual(t.normalizeCliIdleTimeoutMs(-1), 0, 'idle timeout negative coerces to 0'); -assert.strictEqual(t.normalizeCliIdleTimeoutMs('bad'), 0, 'idle timeout non-numeric coerces to 0'); -assert.strictEqual( - t.normalizeSettings({ ...baseSettings, cliIdleTimeoutMs: 250 }).cliIdleTimeoutMs, - 0, - 'normalizeSettings disables sub-minimum idle timeout', -); - // applyApiProviderPreset clears credentials but keeps preset baseUrl (security: prevent cross-provider key leak) { const previous = { diff --git a/versions.json b/versions.json index a00ac61..f37353c 100644 --- a/versions.json +++ b/versions.json @@ -17,5 +17,6 @@ "1.0.15": "1.7.2", "1.0.16": "1.7.2", "1.0.17": "1.7.2", - "1.0.18": "1.8.7" + "1.0.18": "1.8.7", + "1.0.19": "1.8.7" }