mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
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
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
'use strict';
|
|
|
|
import { type App, Modal } from 'obsidian';
|
|
import { cacheEntryMatches } from './settings';
|
|
import type { CacheEntry, PluginSettings } from './types';
|
|
|
|
export interface BatchFileLike {
|
|
path: string;
|
|
parent?: { path: string } | null;
|
|
}
|
|
|
|
export interface BatchStats {
|
|
total: number;
|
|
processed: number;
|
|
skipped: number;
|
|
errors: number;
|
|
}
|
|
|
|
export interface BatchRunState {
|
|
cancelled: boolean;
|
|
currentPath: string;
|
|
}
|
|
|
|
export type BatchFolderValidationReason = 'ok' | 'unsafe' | 'missing';
|
|
|
|
export interface BatchFolderValidation {
|
|
valid: boolean;
|
|
reason: BatchFolderValidationReason;
|
|
folderPath: string;
|
|
}
|
|
|
|
export function normalizeBatchFolderInput(input: string): string {
|
|
return (input || '')
|
|
.trim()
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.filter((part) => part && part !== '.' && part !== '..')
|
|
.join('/');
|
|
}
|
|
|
|
export function hasUnsafeBatchFolderSegments(input: string): boolean {
|
|
return (input || '')
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.some((part) => part === '.' || part === '..');
|
|
}
|
|
|
|
export function validateBatchFolderInput(
|
|
input: string,
|
|
folderExists: (folderPath: string) => boolean,
|
|
): BatchFolderValidation {
|
|
const folderPath = normalizeBatchFolderInput(input);
|
|
if (hasUnsafeBatchFolderSegments(input)) return { valid: false, reason: 'unsafe', folderPath };
|
|
if (folderPath && !folderExists(folderPath)) return { valid: false, reason: 'missing', folderPath };
|
|
return { valid: true, reason: 'ok', folderPath };
|
|
}
|
|
|
|
export function isFileInBatchFolder(file: BatchFileLike, folderPath: string): boolean {
|
|
if (!folderPath) return !file.path.includes('/');
|
|
return file.parent?.path === folderPath;
|
|
}
|
|
|
|
export function selectBatchFiles<T extends BatchFileLike>(files: T[], folderPath: string): T[] {
|
|
const normalized = normalizeBatchFolderInput(folderPath);
|
|
return files.filter((file) => isFileInBatchFolder(file, normalized));
|
|
}
|
|
|
|
export function batchProgressVars(index: number, total: number): { current: number; total: number } {
|
|
return { current: index + 1, total };
|
|
}
|
|
|
|
export function createBatchStats(total: number): BatchStats {
|
|
return { total, processed: 0, skipped: 0, errors: 0 };
|
|
}
|
|
|
|
export function recordBatchProcessed(stats: BatchStats): BatchStats {
|
|
return { ...stats, processed: stats.processed + 1 };
|
|
}
|
|
|
|
export function recordBatchSkip(stats: BatchStats): BatchStats {
|
|
return { ...stats, processed: stats.processed + 1, skipped: stats.skipped + 1 };
|
|
}
|
|
|
|
export function recordBatchError(stats: BatchStats): BatchStats {
|
|
return { ...stats, processed: stats.processed + 1, errors: stats.errors + 1 };
|
|
}
|
|
|
|
export function shouldSkipBatchFile(entry: CacheEntry | null, content: string, settings: PluginSettings): boolean {
|
|
return !!entry && cacheEntryMatches(entry, content, settings);
|
|
}
|
|
|
|
export function createBatchRunState(): BatchRunState {
|
|
return { cancelled: false, currentPath: '' };
|
|
}
|
|
|
|
export function markBatchFileRunning(state: BatchRunState, filePath: string): BatchRunState {
|
|
state.currentPath = filePath;
|
|
return state;
|
|
}
|
|
|
|
export function requestBatchCancel(state: BatchRunState | null): boolean {
|
|
if (!state || state.cancelled) return false;
|
|
state.cancelled = true;
|
|
return true;
|
|
}
|
|
|
|
export function promptForBatchFolder(
|
|
app: App,
|
|
selectText: string,
|
|
promptText: string,
|
|
confirmText: string,
|
|
cancelText: string,
|
|
): Promise<string | null> {
|
|
return new Promise<string | null>((resolve) => {
|
|
let settled = false;
|
|
const settle = (value: string | null) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
resolve(value);
|
|
};
|
|
const modal = new Modal(app);
|
|
modal.onOpen = () => {
|
|
modal.contentEl.createEl('p', { text: selectText });
|
|
const field = modal.contentEl.createDiv({ cls: 'parallel-reader-modal-field' });
|
|
const input = field.createEl('input', { type: 'text' });
|
|
input.placeholder = promptText;
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
settle(input.value.trim());
|
|
modal.close();
|
|
}
|
|
});
|
|
const actions = modal.contentEl.createDiv({ cls: 'modal-button-container' });
|
|
actions.createEl('button', { text: cancelText }).addEventListener('click', () => {
|
|
settle(null);
|
|
modal.close();
|
|
});
|
|
actions.createEl('button', { text: confirmText, cls: 'mod-cta' }).addEventListener('click', () => {
|
|
settle(input.value.trim());
|
|
modal.close();
|
|
});
|
|
};
|
|
modal.onClose = () => settle(null);
|
|
modal.open();
|
|
});
|
|
}
|