fancive_obsidian-parallel-r.../src/types.ts
fancivez 76d2e8050a fix: address 10 P2 behavioral and concurrency findings
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
2026-05-02 13:52:49 +08:00

182 lines
4.6 KiB
TypeScript

'use strict';
import type { App, PluginManifest, TFile } from 'obsidian';
/* ---------- Card types ---------- */
/** Raw card as returned by the LLM and stored in cache (no computed fields). */
export interface RawCard {
title: string;
anchor: string;
gist: string;
bullets: string[];
}
/** Card with the computed startLine from anchor resolution. */
export interface ResolvedCard extends RawCard {
level: number;
startLine: number;
}
/** Patch payload when editing a card via the modal. */
export interface CardPatch {
title?: string;
gist?: string;
bullets?: string[];
}
/* ---------- Cache types ---------- */
export interface CacheEntry {
schemaVersion: number;
contentHash: string;
settingsHash: string;
cards: RawCard[];
generatedAt: string;
lastAccessedAt?: string;
updatedAt?: string;
}
export interface CacheFile {
version: number;
entries: Record<string, CacheEntry>;
}
/* ---------- Settings types ---------- */
export interface PluginSettings {
uiLanguage: string;
backend: string;
cliPath: string;
apiProvider: string;
apiFormat: string;
apiBaseUrl: string;
apiKey: string;
apiKeyEnvVar: string;
apiAuthType: string;
apiHeaders: string;
apiMaxTokens: number;
maxDocChars: number;
maxCacheEntries: number;
promptLanguage: string;
minCards: number;
maxCards: number;
customSystemPrompt: string;
model: string;
exportFolder: string;
cliTimeoutMs: number;
streaming: boolean;
streamingTimeoutMs: number;
}
/* ---------- Provider types ---------- */
export interface ApiProviderPreset {
label: string;
format: string;
baseUrl: string;
authType: string;
envVar: string;
model: string;
tokenLimitField?: string;
modelPrefix?: string;
}
export interface ApiFormat {
label: string;
defaultBaseUrl: string;
defaultAuthType: string;
tokenLimitField?: string;
}
/* ---------- Generation job types ---------- */
export type GenerationPhase =
| 'queued'
| 'running'
| 'reading'
| 'cache-check'
| 'generating'
| 'saving'
| 'done'
| 'cancelled';
export type ErrorKind = 'auth' | 'timeout' | 'rate-limit' | 'schema' | 'config' | 'cancelled' | 'unknown';
export interface RunForFileOptions {
rethrowErrors?: boolean;
/** Skip ensureView+revealLeaf; only update view if it's already showing this file (used by batch). */
silentView?: boolean;
/** Skip the "you have edited cards" confirm dialog (used by unattended batch). */
skipEditConfirm?: boolean;
}
export type RunForFileResult = 'generated' | 'cached' | 'cancelled' | 'already-running' | 'empty' | 'error' | 'no-view';
/* ---------- Prompt types ---------- */
export interface PromptPair {
system: string;
user: string;
}
/* ---------- Obsidian internal API types ---------- */
/** Minimal CodeMirror 6 EditorView shape used for scroll synchronization. */
export interface CmEditorView {
scrollDOM: HTMLElement;
state: {
doc: {
lineAt(pos: number): { number: number };
};
};
posAtCoords(coords: { x: number; y: number }): number | null;
}
/** Obsidian Editor with optional CodeMirror 6 view attached at `.cm`. */
export interface ObsidianEditorWithCm {
cm?: CmEditorView;
}
/** Minimal Obsidian MenuItem builder API used in file-menu callbacks. */
export interface ObsidianMenuItem {
setTitle(title: string): this;
setIcon(icon: string): this;
onClick(callback: () => unknown): this;
}
/** Minimal Obsidian Menu API used to build context-menu entries. */
export interface ObsidianMenu {
addSeparator(): void;
addItem(cb: (item: ObsidianMenuItem) => void): void;
}
/* ---------- 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: App;
settings: PluginSettings;
cache: Record<string, CacheEntry>;
manifest: PluginManifest;
t(key: string, vars?: Record<string, string | number>): string;
isGeneratingFile(file: TFile | null): boolean;
cancelGenerationForFile(file: TFile | null): boolean;
runForFile(
file: TFile | null,
force: boolean,
options?: RunForFileOptions,
preloadedContent?: string,
): Promise<RunForFileResult | void>;
copyCurrentViewMarkdown(): Promise<void>;
scrollEditorToLine(line: number, file: TFile | null): Promise<void>;
cacheReplaceCards(filePath: string, cards: ResolvedCard[]): Promise<boolean>;
saveSettings(): Promise<void>;
saveSettingsDebounced(delayMs?: number): void;
cacheClear(): Promise<void>;
pruneCacheIfNeeded(): Promise<string[]>;
}