fix: address all required issues from Obsidian plugin review bot

- Remove `parallel-reader-` prefix from command IDs (Obsidian auto-namespaces)
- Remove default hotkeys to avoid conflicts with user keybindings
- Replace `as TFile` casts with `instanceof TFile` checks
- Make onunload sync, remove detachLeavesOfType (preserves leaf positions)
- Use `vault.configDir` instead of hardcoded `.obsidian`
- Replace `window.confirm` with Obsidian Modal dialog
- Fix unhandled promises with `void` or `await`
- Remove unnecessary `async` from methods without `await`
- Use `new Setting().setHeading()` instead of raw HTML headings
- Fix promise-returning callbacks in event listeners
- Fix error type narrowing with `instanceof Error`
- Apply sentence case to UI text labels

Change-Id: I6e850d8a13b458d5cf1ad91227bf1772636b56b9
This commit is contained in:
wujunchen 2026-04-26 15:57:16 +08:00
parent e0acce7f6d
commit 89d28d1113
7 changed files with 103 additions and 92 deletions

48
main.js

File diff suppressed because one or more lines are too long

75
main.ts
View file

@ -139,41 +139,41 @@ class ParallelReaderPlugin extends Plugin {
this.registerView(VIEW_TYPE_PARALLEL, (leaf) => new ParallelReaderView(leaf, this));
this.addCommand({
id: 'parallel-reader-run',
id: 'run',
name: this.t('cmdRun'),
callback: () => this.runForActiveFile(false),
});
this.addCommand({
id: 'parallel-reader-regen',
id: 'regen',
name: this.t('cmdRegen'),
callback: () => this.runForActiveFile(true),
});
this.addCommand({
id: 'parallel-reader-open-view',
id: 'open-view',
name: this.t('cmdOpenView'),
callback: async () => {
const active = this.getActiveView();
await this.ensureView();
if (active?.file) this.syncViewToFile(active.file);
if (active?.file) void this.syncViewToFile(active.file);
},
});
this.addCommand({
id: 'parallel-reader-export-current',
id: 'export-current',
name: this.t('cmdExport'),
callback: () => this.exportCurrentView(),
});
this.addCommand({
id: 'parallel-reader-copy-current-markdown',
id: 'copy-current-markdown',
name: this.t('cmdCopyMarkdown'),
callback: () => this.copyCurrentViewMarkdown(),
});
this.addCommand({
id: 'parallel-reader-cancel-current',
id: 'cancel-current',
name: this.t('cmdCancel'),
callback: () => this.cancelActiveGeneration(),
});
this.addCommand({
id: 'parallel-reader-clear-current',
id: 'clear-current',
name: this.t('cmdClearCurrent'),
callback: async () => {
const active = this.getActiveView();
@ -183,7 +183,7 @@ class ParallelReaderPlugin extends Plugin {
},
});
this.addCommand({
id: 'parallel-reader-clear-all',
id: 'clear-all',
name: this.t('cmdClearAll'),
callback: async () => {
const n = Object.keys(this.cacheManager.cache).length;
@ -192,24 +192,22 @@ class ParallelReaderPlugin extends Plugin {
},
});
this.addCommand({
id: 'parallel-reader-card-prev',
id: 'card-prev',
name: this.t('cmdCardPrev'),
hotkeys: [{ modifiers: ['Alt'], key: 'ArrowUp' }],
callback: () => this.moveActiveCard(-1),
});
this.addCommand({
id: 'parallel-reader-card-next',
id: 'card-next',
name: this.t('cmdCardNext'),
hotkeys: [{ modifiers: ['Alt'], key: 'ArrowDown' }],
callback: () => this.moveActiveCard(1),
});
this.addCommand({
id: 'parallel-reader-card-jump',
id: 'card-jump',
name: this.t('cmdCardJump'),
callback: () => this.jumpActiveCard(),
});
this.addCommand({
id: 'parallel-reader-batch-generate',
id: 'batch-generate',
name: this.t('cmdBatchGenerate'),
callback: () => this.runBatchForFolder(),
});
@ -219,19 +217,22 @@ class ParallelReaderPlugin extends Plugin {
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);
if (file) void 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 as TFile, oldPath)));
this.registerEvent(this.app.vault.on('delete', (file) => this.handleFileDelete(file as TFile)));
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => {
if (file instanceof TFile) void this.handleFileRename(file, oldPath);
}));
this.registerEvent(this.app.vault.on('delete', (file) => {
if (file instanceof TFile) void this.handleFileDelete(file);
}));
this.bindScrollSync();
}
async onunload() {
await this.flushSettingsSave();
await this.flushCacheSave();
this.app.workspace.detachLeavesOfType(VIEW_TYPE_PARALLEL);
onunload() {
void this.flushSettingsSave();
void this.flushCacheSave();
}
/* ---------- Settings persistence ---------- */
@ -242,7 +243,7 @@ class ParallelReaderPlugin extends Plugin {
this.settings = normalizeSettings(Object.assign({}, DEFAULT_SETTINGS, settingsBlob));
this.cacheManager = new CacheManager(
this.app.vault.adapter,
this.app.vault.configDir || '.obsidian',
this.app.vault.configDir,
this.manifest?.id || 'parallel-reader',
() => this.settings,
);
@ -274,7 +275,7 @@ class ParallelReaderPlugin extends Plugin {
/* ---------- Cache delegation ---------- */
async cacheTouch(filePath: string) {
cacheTouch(filePath: string) {
return this.cacheManager.touch(filePath);
}
scheduleCacheSave(delayMs = 5000) {
@ -302,7 +303,7 @@ class ParallelReaderPlugin extends Plugin {
leaf = workspace.getRightLeaf(false)!;
await leaf.setViewState({ type: VIEW_TYPE_PARALLEL, active: true });
}
workspace.revealLeaf(leaf);
await workspace.revealLeaf(leaf);
return leaf.view as ParallelReaderView;
}
@ -363,10 +364,24 @@ class ParallelReaderPlugin extends Plugin {
else new Notice(this.t('noCancelableJob'));
}
confirmRegenerateEditedCards() {
const message = this.t('confirmRegenerateEditedCards');
if (typeof window !== 'undefined' && typeof window.confirm === 'function') return window.confirm(message);
return true;
confirmRegenerateEditedCards(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const message = this.t('confirmRegenerateEditedCards');
const modal = new Modal(this.app);
modal.titleEl.setText(this.t('displayName'));
modal.contentEl.createEl('p', { text: message });
const btnRow = modal.contentEl.createDiv({ cls: 'modal-button-container' });
btnRow.createEl('button', { text: 'Cancel' }).addEventListener('click', () => {
modal.close();
resolve(false);
});
btnRow.createEl('button', { text: 'OK', cls: 'mod-cta' }).addEventListener('click', () => {
modal.close();
resolve(true);
});
modal.onClose = () => resolve(false);
modal.open();
});
}
addFileMenuItems(menu: ObsidianMenu, file: unknown) {
@ -477,7 +492,7 @@ class ParallelReaderPlugin extends Plugin {
new Notice(this.t('alreadyGenerating'));
return;
}
if (shouldConfirmRegenerate(this.cacheManager.get(file.path), force) && !this.confirmRegenerateEditedCards()) {
if (shouldConfirmRegenerate(this.cacheManager.get(file.path), force) && !(await this.confirmRegenerateEditedCards())) {
new Notice(this.t('regenerateCancelled'));
return;
}

View file

@ -44,7 +44,7 @@ export class CacheManager {
if (parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object')
return parsed.entries;
} catch (e: unknown) {
const message = String((e as Error)?.message || e || '');
const message = e instanceof Error ? e.message : String(e);
if (!/not found|does not exist|ENOENT/i.test(message))
console.warn('[parallel-reader] failed to read cache.json', e);
}

View file

@ -64,7 +64,7 @@ export class GenerationJob {
for (const handler of this._cancelHandlers.splice(0)) {
try {
handler();
} catch (_) {}
} catch { /* handler error ignored */ }
}
return true;
}

View file

@ -43,7 +43,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
const { containerEl } = this;
const tr = (key: string, vars?: Record<string, string | number>) => this.plugin.t(key, vars);
containerEl.empty();
containerEl.createEl('h2', { text: tr('settingsTitle') });
new Setting(containerEl).setName(tr('settingsTitle')).setHeading();
new Setting(containerEl)
.setName(tr('settingUiLanguageName'))
@ -64,7 +64,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
.setDesc(tr('settingBackendDesc'))
.addDropdown((d) =>
d
.addOption('api', 'API / Provider')
.addOption('api', 'API / provider')
.addOption('claude-code', 'Claude Code CLI')
.addOption('codex', 'Codex CLI')
.setValue(this.plugin.settings.backend)
@ -88,7 +88,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder(tr('settingCliPathPlaceholder'))
.setValue(this.plugin.settings.cliPath)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.cliPath = v.trim();
this.plugin.saveSettingsDebounced();
}),
@ -96,7 +96,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
}
if (!isCliBacked) {
containerEl.createEl('h3', { text: tr('apiProviderHeader') });
new Setting(containerEl).setName(tr('apiProviderHeader')).setHeading();
}
if (!isCliBacked) {
@ -140,7 +140,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
: preset.baseUrl || API_FORMATS[getApiFormat(this.plugin.settings)].defaultBaseUrl,
)
.setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.apiBaseUrl = v.trim();
this.plugin.saveSettingsDebounced();
}),
@ -154,7 +154,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
return t
.setPlaceholder('sk-...')
.setValue(this.plugin.settings.apiKey)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.apiKey = v.trim();
this.plugin.saveSettingsDebounced();
});
@ -167,7 +167,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder(preset.envVar || 'OPENAI_API_KEY')
.setValue(this.plugin.settings.apiKeyEnvVar)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.apiKeyEnvVar = v.trim();
this.plugin.saveSettingsDebounced();
}),
@ -193,14 +193,14 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder('cf-aig-authorization: Bearer ...')
.setValue(this.plugin.settings.apiHeaders)
.onChange(async (v) => {
.onChange((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) => {
t.setValue(String(this.plugin.settings.apiMaxTokens)).onChange((v) => {
const n = parseInt(v, 10);
if (!Number.isNaN(n) && n > 0) {
this.plugin.settings.apiMaxTokens = n;
@ -227,7 +227,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder(isCliBacked ? DEFAULT_SETTINGS.model : getApiPreset(this.plugin.settings).model || 'model-id')
.setValue(this.plugin.settings.model)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.model = v.trim() || (isCliBacked ? DEFAULT_SETTINGS.model : '');
this.plugin.saveSettingsDebounced();
}),
@ -237,7 +237,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
.setName(tr('settingMaxInputName'))
.setDesc(tr('settingMaxInputDesc'))
.addText((t) =>
t.setValue(String(this.plugin.settings.maxDocChars || DEFAULT_SETTINGS.maxDocChars)).onChange(async (v) => {
t.setValue(String(this.plugin.settings.maxDocChars || DEFAULT_SETTINGS.maxDocChars)).onChange((v) => {
const n = parseInt(v, 10);
if (!Number.isNaN(n) && n >= 1000) {
this.plugin.settings.maxDocChars = n;
@ -246,7 +246,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
}),
);
containerEl.createEl('h3', { text: tr('promptHeader') });
new Setting(containerEl).setName(tr('promptHeader')).setHeading();
new Setting(containerEl)
.setName(tr('settingPromptLanguageName'))
@ -270,7 +270,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder('min')
.setValue(String(this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards))
.onChange(async (v) => {
.onChange((v) => {
const n = parseInt(v, 10);
if (!Number.isNaN(n) && n > 0) {
this.plugin.settings.minCards = n;
@ -283,7 +283,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
t
.setPlaceholder('max')
.setValue(String(this.plugin.settings.maxCards || DEFAULT_SETTINGS.maxCards))
.onChange(async (v) => {
.onChange((v) => {
const n = parseInt(v, 10);
if (!Number.isNaN(n) && n > 0) {
this.plugin.settings.maxCards = Math.max(n, this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards);
@ -300,7 +300,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
return t
.setPlaceholder(tr('settingCustomPromptPlaceholder'))
.setValue(this.plugin.settings.customSystemPrompt || '')
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.customSystemPrompt = v;
this.plugin.saveSettingsDebounced();
});
@ -324,13 +324,13 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
.setName(tr('settingExportFolderName'))
.setDesc(tr('settingExportFolderDesc'))
.addText((t) =>
t.setValue(this.plugin.settings.exportFolder).onChange(async (v) => {
t.setValue(this.plugin.settings.exportFolder).onChange((v) => {
this.plugin.settings.exportFolder = v.trim() || DEFAULT_SETTINGS.exportFolder;
this.plugin.saveSettingsDebounced();
}),
);
containerEl.createEl('h3', { text: tr('cacheHeader') });
new Setting(containerEl).setName(tr('cacheHeader')).setHeading();
new Setting(containerEl)
.setName(tr('settingMaxCacheName'))
@ -347,7 +347,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
this.display();
}
};
t.inputEl.addEventListener('change', commit);
t.inputEl.addEventListener('change', () => { void commit(); });
t.inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') t.inputEl.blur();
});

View file

@ -13,15 +13,13 @@ export function addIconButton(parent: HTMLElement, icon: string, title: string,
} else {
button.textContent = title;
}
button.addEventListener('click', async (e) => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
try {
await onClick();
} catch (err: unknown) {
Promise.resolve(onClick()).catch((err: unknown) => {
console.error(err);
new Notice(`${title} failed: ` + ((err as Error).message || err));
}
new Notice(`${title} failed: ${err instanceof Error ? err.message : String(err)}`);
});
});
return button;
}
@ -39,15 +37,13 @@ export function addTextButton(
});
if (icon && typeof setIcon === 'function') setIcon(button, icon);
button.createSpan({ text: label });
button.addEventListener('click', async (e) => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
try {
await onClick();
} catch (err: unknown) {
Promise.resolve(onClick()).catch((err: unknown) => {
console.error(err);
new Notice(`${label} failed: ` + ((err as Error).message || err));
}
new Notice(`${label} failed: ${err instanceof Error ? err.message : String(err)}`);
});
});
return button;
}
@ -57,6 +53,6 @@ export async function copyToClipboard(text: string, successMsg: string) {
await navigator.clipboard.writeText(text);
new Notice(successMsg);
} catch (e: unknown) {
new Notice('Copy failed: ' + ((e as Error).message || e));
new Notice(`Copy failed: ${e instanceof Error ? e.message : String(e)}`);
}
}

View file

@ -76,7 +76,7 @@ export class ParallelReaderView extends ItemView {
return true;
}
async loadFor(file: TFile, sections: ResolvedCard[], stale: boolean) {
loadFor(file: TFile, sections: ResolvedCard[], stale: boolean) {
this.sourceFile = file;
this.sections = sections;
this.stale = !!stale;
@ -85,7 +85,7 @@ export class ParallelReaderView extends ItemView {
this.render();
}
async renderLoading(file: TFile, message: string) {
renderLoading(file: TFile, message: string) {
this.sourceFile = file;
this.sections = [];
this.stale = false;
@ -127,7 +127,7 @@ export class ParallelReaderView extends ItemView {
pre.textContent = text.slice(-2000);
}
async renderError(file: TFile, message: string) {
renderError(file: TFile, message: string) {
this.sourceFile = file;
this.sections = [];
this.stale = false;
@ -243,7 +243,7 @@ export class ParallelReaderView extends ItemView {
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);
if (s.startLine >= 0) void this.plugin.scrollEditorToLine(s.startLine, this.sourceFile);
});
card.addEventListener('contextmenu', (e) => {
@ -323,7 +323,7 @@ export class ParallelReaderView extends ItemView {
jumpToActiveSection() {
const line = activeSectionLine(this.sections, this.activeIdx);
if (line < 0 || !this.sourceFile) return -1;
this.plugin.scrollEditorToLine(line, this.sourceFile);
void this.plugin.scrollEditorToLine(line, this.sourceFile);
return line;
}