/** * Browser-local object layer behind the research panel: one controller per * client runtime backs both the project list and the selected project's * outline and compile status. Every read goes through the generated `research` * Remote; the generated face wraps each call in {@link RemoteResult}, so a * carrier failure arrives as the `ok: false` branch rather than a rejection. * Failures surface as `{ code, message }` pairs — the component maps the codes * it knows to localized copy and falls back to the Host-supplied message. * @module dsh-client-ui-mimir/client/controller */ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'; import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'; import type { ResearchKey } from './locales.ts'; import type { MetricChartRow } from './view-common.ts'; import { type ResearchToast, type ResearchToastKind } from './toasts.ts'; import type { EvidenceGraphRequest } from './evidence-graph-view.ts'; import type { ArxivEntry, AddEvidenceEdgeRequest, ResearchAddEvidenceEdgeResult, ResearchEvidenceGraphView, ResearchGetEvidenceGraphResult, ResearchRetractEvidenceEdgeResult, RetractEvidenceEdgeRequest, ArxivSubscriptionView, BibEntry, EventRecord, ExperimentRecord, ExperimentInput, FigureEntry, JobRecord, OutlineNode, PaperRecord, PaperSnapshotView, ResearchArtifactResult, ResearchArxivSubscriptionsResult, ResearchAddJournalEntryResult, ResearchCloseIdeaResult, ResearchAdoptIdeaResult, ResearchGetForagingResult, ResearchGetWorktreeResult, ResearchGetMomentIndexResult, ResearchGetEurekaViewResult, ResearchMomentIndexView, ResearchEurekaView, ResearchForagingView, ResearchSetIdeaParentResult, ResearchSetMainlineResult, ResearchWorktreeView, ResearchBriefQuestion, ResearchBackupStatusView, ResearchBibliographyResult, ResearchCheckArxivSubscriptionsResult, ResearchCheckServerResult, ResearchCheckZoteroResult, ResearchCompileResult, ResearchCompileStatusResult, ResearchCompileStatusView, ResearchConvertFigureResult, ResearchCreateProjectResult, ResearchDeleteArxivSubscriptionResult, ResearchDeleteExperimentResult, ResearchDeleteFigureResult, ResearchDeleteJobResult, ResearchDeleteProjectResult, ResearchDeleteServerResult, ResearchEventFilter, ResearchExperimentsResult, ResearchExportWikiResult, ResearchFetchPaperPdfResult, ResearchFiguresResult, ResearchGenerateBriefOptions, ResearchGenerateBriefResult, ResearchGenerateDigestResult, ResearchDigestView, ResearchDigestTier, ResearchSetEurekaResult, ResearchPinMomentResult, ResearchJournalQuestionRef, ResearchImportBibResult, ResearchImportPaperResult, ResearchImportProjectRequest, ResearchImportProjectResult, ResearchImportedProject, ResearchImportWikiMode, ResearchImportWikiResult, ResearchListBackupsResult, ResearchListEventsResult, ResearchListJobsResult, ResearchListProjectsResult, ResearchListServersResult, MetricDirection, ResearchScheduledTaskView, ResearchTaskHealthResult, ResearchVenueTemplatesResult, ResearchApplyVenueResult, ResearchClearVenueResult, ResearchDeleteMeetingDeckResult, ResearchGenerateMeetingResult, ResearchGetImageGenConfigResult, ResearchGetDigdigConfigResult, ResearchMeetingDecksResult, MeetingDeckView, MeetingInclude, VenueTemplateView, ResearchOutlineResult, ResearchPaperSnapshotResult, ResearchPaperSnapshotsResult, ResearchPaperSourceResult, ResearchPapersResult, ResearchProjectView, ResearchProgressReportOptions, ResearchProgressReportResult, ResearchRemovePaperResult, ResearchRenameFigureResult, ResearchRenameProjectResult, ResearchRevertPaperSnapshotResult, ResearchSaveArxivSubscriptionResult, ResearchSaveBibliographyResult, ResearchSaveExperimentResult, ResearchSavePaperSourceResult, ResearchSaveFigureResult, ResearchSaveServerResult, ResearchSearchArxivResult, ResearchSearchWebResult, ResearchSetImageGenConfigResult, ResearchSetDigdigConfigResult, ResearchSubmitJobResult, ResearchUpdateExperimentResult, ResearchUpdateFigureResult, ResearchUpdatePaperResult, ResearchRefreshVenueDeadlinesResult, ResearchSetVenueWatchResult, ResearchVenueDeadlinesResult, ResearchWikiSnapshot, VenueDeadlineView, VenueJournalView, ResearchZoteroCollectionsResult, ResearchZoteroExportResult, ResearchZoteroImportResult, ResearchZoteroSearchResult, SectionMove, SectionOutlineTitles, ServerInput, ServerStatusView, ServerView, SubsectionMove, WebSearchEntry, ZoteroCollectionView, ZoteroItemView, ResearchWikiChangeEvent } from 'dsh-mimir/types'; export type { ResearchDigestView, ResearchDigestTier, ResearchExperienceCapsule, ResearchCapsulePerspective, } from 'dsh-mimir/types'; /** * The Remote calls this controller needs, exactly as the * generated `research` namespace types them. The ledger remotes keep * `actorKind`/`order` as widened `string` (the generated face elides the * literal unions across the boundary). */ export interface ResearchRemote { listProjects: () => Promise>; importProject: (request: ResearchImportProjectRequest) => Promise>; createProject: (request: { title: string; }) => Promise>; renameProject: (request: { projectId: string; title: string; }) => Promise>; deleteProject: (request: { projectId: string; confirm: true; }) => Promise>; getPaperOutline: (request: { projectId: string; dir?: string | undefined; }) => Promise>; compile: (request: { projectId?: string; dir?: string | undefined; }, signal?: AbortSignal) => Promise>; getCompileStatus: (request: { projectId?: string; }) => Promise>; getPaperSource: (request: { projectId: string; dir?: string | undefined; }) => Promise>; savePaperSource: (request: { projectId: string; content: string; baseMtimeMs: number; dir?: string | undefined; }) => Promise>; listPapers: () => Promise>; searchArxiv: (request: { query: string; maxResults?: number; }) => Promise>; searchWeb: (request: { query: string; maxResults?: number; categories?: string | undefined; lang?: string | undefined; }) => Promise>; importPaper: (request: { entry: ArxivEntry; projectId?: string | undefined; }) => Promise>; removePaper: (request: { arxivId: string; }) => Promise>; updatePaper: (request: { arxivId: string; tags?: string[] | undefined; projectIds?: string[] | undefined; notes?: string | undefined; relevance?: { projectId: string; score: number; reason: string; } | undefined; }) => Promise>; fetchPaperPdf: (request: { arxivId: string; }) => Promise>; checkZotero: () => Promise>; listZoteroCollections: () => Promise>; searchZotero: (request: { query: string; maxResults?: number; }) => Promise>; importZoteroItem: (request: { key: string; projectId?: string | undefined; }) => Promise>; exportZoteroCollectionToBib: (request: { projectId: string; collectionKey: string; dir?: string | undefined; }) => Promise>; listArxivSubscriptions: () => Promise>; saveArxivSubscription: (request: { query: string; }) => Promise>; deleteArxivSubscription: (request: { id: string; }) => Promise>; checkArxivSubscriptions: (request: { id?: string; }) => Promise>; listExperiments: (request: { projectId?: string; }) => Promise>; deleteExperiment: (request: { id: string; }) => Promise>; updateExperiment: (request: { id: string; serverId?: string | null | undefined; }) => Promise>; saveExperiment: (request: { experiment: ExperimentInput; }) => Promise>; readArtifact: (request: { projectId: string; name: string; }) => Promise>; listFigures: (request: { projectId: string; dir?: string | undefined; }) => Promise>; deleteFigure: (request: { projectId: string; relPath: string; dir?: string | undefined; }) => Promise>; renameFigure: (request: { projectId: string; relPath: string; newName: string; dir?: string | undefined; }) => Promise>; updateFigure: (request: { projectId: string; relPath: string; caption: string; }) => Promise>; convertFigure: (request: { projectId: string; relPath: string; dir?: string | undefined; }) => Promise>; saveFigure: (request: { projectId: string; name: string; content: string; caption?: string | undefined; dir?: string | undefined; }) => Promise>; listServers: () => Promise>; listVenueTemplates: () => Promise>; applyVenueTemplate: (request: { projectId: string; dir?: string | undefined; templateId?: string | undefined; customName?: string | undefined; }) => Promise>; clearVenueTemplate: (request: { projectId: string; }) => Promise>; listVenueDeadlines: (request: { projectId?: string | undefined; }) => Promise>; setVenueWatch: (request: { projectId: string; series: string; watched: boolean; }) => Promise>; refreshVenueDeadlines: () => Promise>; generateMeetingDeck: (request: { projectId: string; title?: string | undefined; presenter?: string | undefined; date?: string | undefined; paperIds?: readonly string[] | undefined; figureRelPaths?: readonly string[] | undefined; include?: Partial | undefined; aiIllustrations?: boolean | undefined; }) => Promise>; listMeetingDecks: (request: { projectId: string; }) => Promise>; deleteMeetingDeck: (request: { projectId: string; file: string; }) => Promise>; getImageGenConfig: () => Promise>; setImageGenConfig: (request: { baseUrl?: string | undefined; apiKey?: string | undefined; model?: string | undefined; size?: string | undefined; }) => Promise>; getDigdigConfig: () => Promise>; setDigdigConfig: (request: { baseUrl?: string | undefined; defaultEngine?: string | undefined; allowedEngines?: readonly string[] | undefined; defaultLimit?: number | undefined; defaultFormat?: 'md' | 'json' | undefined; useProxy?: boolean | undefined; proxyUrl?: string | undefined; timeout?: number | undefined; ollamaApiKey?: string | undefined; redundancyThreshold?: number | undefined; redundancyBigramThreshold?: number | undefined; }) => Promise>; saveServer: (request: { server: ServerInput; }) => Promise>; deleteServer: (request: { id: string; }) => Promise>; checkServer: (request: { id: string; }) => Promise>; submitJob: (request: { serverId: string; command: string; experimentId?: string | undefined; }) => Promise>; listJobs: (request: { serverId?: string; }) => Promise>; deleteJob: (request: { id: string; }) => Promise>; getBibliography: (request: { projectId: string; dir?: string | undefined; }) => Promise>; saveBibliography: (request: { projectId: string; entries: BibEntry[]; baseMtimeMs: number | null; dir?: string | undefined; }) => Promise>; importPapersToBib: (request: { projectId: string; arxivIds: string[]; dir?: string | undefined; }) => Promise>; reorderPaperSections: (request: { projectId: string; moves: SectionMove[]; baseOutline: string[]; dir?: string | undefined; }) => Promise>; reorderPaperSubsections: (request: { projectId: string; moves: SubsectionMove[]; baseOutline: SectionOutlineTitles[]; dir?: string | undefined; }) => Promise>; listPaperSnapshots: (request: { projectId: string; }) => Promise>; getPaperSnapshot: (request: { projectId: string; id: string; }) => Promise>; revertPaperSnapshot: (request: { projectId: string; id: string; baseMtimeMs: number; dir?: string | undefined; }) => Promise>; exportWiki: () => Promise>; importWiki: (request: { snapshot: ResearchWikiSnapshot; mode: ResearchImportWikiMode; confirmReplace?: boolean; }) => Promise>; listBackups: () => Promise>; getTaskHealth: () => Promise>; listEvents: (request: { projectId?: string | undefined; actorKind?: string | undefined; actionPrefix?: string | undefined; since?: string | undefined; until?: string | undefined; limit?: number | undefined; order?: string | undefined; }) => Promise>; generateProgressReport: (request: { projectId?: string | undefined; since?: string | undefined; until?: string | undefined; }) => Promise>; generateBrief: (request: { projectId?: string | undefined; since?: string | undefined; until?: string | undefined; }) => Promise>; addJournalEntry: (request: { text: string; projectId?: string | undefined; ideaId?: string | undefined; valence?: number | undefined; arousal?: number | undefined; question?: ResearchJournalQuestionRef | undefined; }) => Promise>; getForaging: () => Promise>; getWorktree: () => Promise>; setMainline: (request: { ideaId?: string | undefined; projectId?: string | undefined; }) => Promise>; setIdeaParent: (request: { ideaId: string; parentIdeaId: string | null; }) => Promise>; adoptIdea: (request: { ideaId: string; }) => Promise>; closeIdea: (request: { ideaId: string; reason: string; }) => Promise>; /** Assemble one digest (weekly / monthly / project) as a pure fold, rendered as MMS. */ generateDigest: (request: { tier?: string | undefined; since?: string | undefined; until?: string | undefined; lang?: string | undefined; }) => Promise>; /** Declare one Eureka — the researcher's own named milestone (never system-declared). */ setEureka: (request: { ideaId?: string | undefined; projectId?: string | undefined; title: string; }) => Promise>; /** Pin (or unpin/decline) one moment — the explicit bookmark of a curated instant. */ pinMoment: (request: { targetEventId: string; note?: string | undefined; pinned?: boolean | undefined; }) => Promise>; /** Read the unified moment timeline (S9b): pull-only, five sources, zero verbs. */ getMomentIndex: (request: { since?: string | undefined; until?: string | undefined; }) => Promise>; /** Read the retrospective eureka view (S8c): declarations × measured roads. */ getEurekaView: () => Promise>; /** Read the evidence graph (v1): one pure fold over one ledger window (L1). */ getEvidenceGraph: (request: { projectId?: string | undefined; since?: string | undefined; until?: string | undefined; }) => Promise>; /** Declare one evidence edge — the panel channel of the graph's writes. */ addEvidenceEdge: (request: AddEvidenceEdgeRequest) => Promise>; /** Retract one evidence edge by dedupKey (panel: any edge — human final say). */ retractEvidenceEdge: (request: RetractEvidenceEdgeRequest) => Promise>; } /** Quiet period after the last keystroke before the draft autosaves. */ export declare const AUTOSAVE_DEBOUNCE_MS = 800; /** Quiet period after a successful save before the auto-compile fires. */ export declare const COMPILE_DEBOUNCE_MS = 1500; /** Quiet window that collapses an agent's burst of wiki writes into one refresh. */ export declare const LIVE_REFRESH_DEBOUNCE_MS = 400; /** Hard cap on the burst window: a sustained write stream never postpones the flush past this. */ export declare const LIVE_REFRESH_MAX_WAIT_MS = 2000; /** One open `/research/events` stream handle (EventSource or a test double). */ export interface WikiEventStream { close(): void; } /** * Opener for the wiki events stream; the default uses the page's EventSource. * @param url - the events route. * @param onEvent - parsed change-frame consumer. * @param onReconnect - fired on every open AFTER the first: the stream gave * no replay guarantee, so writes landed during the outage are unknown and * the consumer must re-read from scratch. * @returns the stream handle, or null where EventSource is unavailable. */ export type WikiEventStreamFactory = (url: string, onEvent: (event: ResearchWikiChangeEvent) => void, onReconnect?: () => void) => WikiEventStream | null; /** Load lifecycle of one fetched view. */ export type ResearchLoadStatus = 'cold' | 'loading' | 'ready' | 'error'; /** One settled failure: a known code for localized copy plus the raw message. */ export interface ResearchFailureView { readonly code: string; readonly message: string; } /** The selected project's outline load. */ export interface ResearchOutlineView { readonly projectId: string; readonly status: 'loading' | 'ready' | 'error'; readonly nodes: readonly OutlineNode[]; readonly failure: ResearchFailureView | null; } /** Compile status annotated with the project it belongs to. */ export interface ResearchCompileView extends ResearchCompileStatusView { readonly projectId: string | null; } /** Autosave lifecycle of the editor draft. */ export type ResearchSaveState = 'clean' | 'dirty' | 'saving' | 'saved' | 'conflict' | 'save-error'; /** The selected project's source-editor view. */ export interface ResearchSourceView { readonly projectId: string; readonly status: 'loading' | 'ready' | 'error'; /** The draft: the file's content, with local edits not yet saved. */ readonly content: string; /** mtime the current draft is based on; null until the first load settles. */ readonly mtimeMs: number | null; readonly saveState: ResearchSaveState; readonly failure: ResearchFailureView | null; } /** * A pending editor jump the paper view applies once its draft shows the * target line (the figures view's insert-into-paper outcome). The monotonic * `seq` re-fires the paper view's effect when two jumps land on the same line. */ export interface ResearchPaperJump { readonly projectId: string; /** 1-based target line in the current draft. */ readonly line: number; readonly seq: number; } /** The literature view: every remembered paper. */ export interface ResearchPapersView { readonly status: ResearchLoadStatus; readonly list: readonly PaperRecord[]; readonly failure: ResearchFailureView | null; } /** The venue picker's built-in template registry slice. */ export interface ResearchVenueTemplatesView { readonly status: ResearchLoadStatus; readonly list: readonly VenueTemplateView[]; readonly failure: ResearchFailureView | null; } /** The arXiv search panel: the last query's outcome (null before any search). */ export interface ResearchArxivSearchView { readonly query: string; readonly status: 'loading' | 'ready' | 'error'; readonly list: readonly ArxivEntry[]; readonly failure: ResearchFailureView | null; } /** The web search panel: the last query's outcome (null before any search). */ export interface ResearchWebSearchView { readonly query: string; readonly status: 'loading' | 'ready' | 'error'; readonly list: readonly WebSearchEntry[]; readonly failure: ResearchFailureView | null; } /** The papers view's Zotero section: connection status plus the collection list. */ export interface ResearchZoteroView { readonly status: ResearchLoadStatus; /** The settled probe outcome; null while the first check is still cold. */ readonly state: 'unconfigured' | 'ok' | 'failed' | null; /** The probe's failure reason (state `failed`); null otherwise. */ readonly message: string | null; /** The configured library's collections; empty until a successful check loads them. */ readonly collections: readonly ZoteroCollectionView[]; readonly failure: ResearchFailureView | null; } /** The Zotero search panel: the last query's outcome (null before any search). */ export interface ResearchZoteroSearchView { readonly query: string; readonly status: 'loading' | 'ready' | 'error'; readonly list: readonly ZoteroItemView[]; readonly failure: ResearchFailureView | null; } /** The arXiv subscription bar of the papers view. */ export interface ResearchSubscriptionsView { readonly status: ResearchLoadStatus; readonly list: readonly ArxivSubscriptionView[]; /** True while a check run is in flight (manual or open-triggered). */ readonly checking: boolean; /** Whole-call failure of the list load; per-subscription check failures live in `checkErrors`. */ readonly failure: ResearchFailureView | null; /** The last run's per-subscription fetch failures, keyed by subscription id. */ readonly checkErrors: Readonly>; } /** One per-project fetched view (experiments, artifact, figures). */ export interface ResearchProjectSlice { readonly projectId: string; readonly status: 'loading' | 'ready' | 'error'; readonly list: T; readonly failure: ResearchFailureView | null; } /** The meetings view's image-generation config (the panel-safe masked view). */ export interface ResearchImageGenView { readonly status: ResearchLoadStatus; /** Whether an API key is stored host-side. */ readonly configured: boolean; readonly baseUrl: string; readonly model: string; readonly size: string; /** The stored key's mask (e.g. `sk-ab…34`); '' when unconfigured. */ readonly apiKeyPreview: string; readonly failure: ResearchFailureView | null; } /** The Papers view's native digdig configuration. */ export interface ResearchDigdigConfigView { readonly status: ResearchLoadStatus; readonly configured: boolean; readonly baseUrl: string; readonly defaultEngine: string; readonly allowedEngines: readonly string[]; readonly defaultLimit: number; readonly defaultFormat: 'md' | 'json'; readonly useProxy: boolean; readonly proxyUrl: string; readonly timeout: number; readonly ollamaApiKeyPreview: string; readonly redundancyThreshold: number; readonly redundancyBigramThreshold: number; readonly failure: ResearchFailureView | null; } /** The markdown artifact viewer's load. */ export interface ResearchArtifactView { readonly projectId: string; readonly name: string; readonly status: 'loading' | 'ready' | 'error'; readonly content: string; readonly mtimeMs: number | null; readonly failure: ResearchFailureView | null; } /** The servers view: every remembered compute server. */ export interface ResearchServersView { readonly status: ResearchLoadStatus; readonly list: readonly ServerView[]; readonly failure: ResearchFailureView | null; } /** * The venues view: the cached ccfddl conference catalog (every series with * its current edition resolved), the static CCF-A journal directory, and the * watch list of the project the list was loaded for (`watchedProjectId`, null * = no project addressed). `fetchedAt` is the upstream snapshot's age; null * means the cache has never been fetched (first-ever start, offline). */ export interface ResearchVenuesView { readonly status: ResearchLoadStatus; readonly list: readonly VenueDeadlineView[]; readonly journals: readonly VenueJournalView[]; readonly watched: readonly string[]; readonly watchedProjectId: string | null; readonly fetchedAt: string | null; readonly failure: ResearchFailureView | null; } /** The remote-jobs view: every submitted job, most recently submitted first. */ export interface ResearchJobsView { readonly status: ResearchLoadStatus; readonly list: readonly JobRecord[]; readonly failure: ResearchFailureView | null; } /** Settled counts of one `importPapersToBib` run (appended vs already-present keys). */ export interface ResearchImportCounts { readonly added: readonly string[]; readonly skipped: readonly string[]; } /** The ledger (growth record) view: one time window's events, newest first. */ export interface ResearchLedgerView { readonly status: ResearchLoadStatus; readonly list: readonly EventRecord[]; readonly failure: ResearchFailureView | null; } /** The last progress report the ledger view generated (null fields while none). */ export interface ResearchReportView { readonly status: 'idle' | 'loading' | 'ready' | 'error'; readonly markdown: string; readonly generatedAt: string | null; readonly eventCount: number | null; readonly failure: ResearchFailureView | null; } /** The last cognitive brief the ledger view generated (null fields while none). */ export interface ResearchBriefView { readonly status: 'idle' | 'loading' | 'ready' | 'error'; readonly markdown: string; readonly generatedAt: string | null; readonly eventCount: number | null; /** The derivation version the brief was rendered under (I5). */ readonly derivationVersion: number | null; /** True when this version differs from the last one seen (I5: the map re-calibrated). */ readonly recalibrated: boolean; /** Label-resolved boundary questions (the confirmation cards' data). */ readonly questions: readonly ResearchBriefQuestion[]; readonly failure: ResearchFailureView | null; } /** The worktree (S2) slice: the derived working tree, cold until first opened. */ export interface ResearchWorktreeSlice { readonly status: ResearchLoadStatus; readonly view: ResearchWorktreeView | null; readonly failure: ResearchFailureView | null; } /** * The digest (B–F) slice: the weekly / monthly / project report as a * generated model. It is generation-driven (like the brief) but pushed * proactively when the ledger view first opens. The model is a plain data * object — no prediction, no "you are near an insight" prompt. */ export interface ResearchDigestSlice { readonly status: 'idle' | 'loading' | 'ready' | 'error'; /** The depth last requested (or `weekly` before the first generation). */ readonly tier: ResearchDigestTier; /** The language the report model + MMS were rendered in. */ readonly lang: 'zh' | 'en'; /** The assembled report model (rendered directly by DigestView); null until ready. */ readonly report: ResearchDigestView | null; /** The MMS string for copy / download into a wiki, Obsidian, or GitHub. */ readonly markdown: string; readonly generatedAt: string | null; readonly failure: ResearchFailureView | null; } /** The foraging (S4) slice: the territory ledger + GUT baseline, cold until opened. */ export interface ResearchForagingSlice { readonly status: ResearchLoadStatus; readonly view: ResearchForagingView | null; readonly failure: ResearchFailureView | null; } /** The moments (S9b) slice: the unified timeline, cold until first opened. */ export interface ResearchMomentsSlice { readonly status: ResearchLoadStatus; readonly view: ResearchMomentIndexView | null; readonly failure: ResearchFailureView | null; } /** The evidence graph (v1) slice: the pure fold's product (L1), cold until opened. */ export interface ResearchEvidenceGraphSlice { readonly status: ResearchLoadStatus; readonly view: ResearchEvidenceGraphView | null; readonly failure: ResearchFailureView | null; } /** The eureka (S8c) slice: declarations × measured roads, cold until opened. */ export interface ResearchEurekaSlice { readonly status: ResearchLoadStatus; readonly view: ResearchEurekaView | null; readonly failure: ResearchFailureView | null; } /** The selected project's `references.bib` view, edited entry-wise through the panel. */ export interface ResearchBibView { readonly projectId: string; readonly status: 'loading' | 'ready' | 'error'; readonly entries: readonly BibEntry[]; /** Optimistic-concurrency base for entry deletes; null while the file is absent. */ readonly mtimeMs: number | null; readonly saveState: ResearchSaveState; readonly failure: ResearchFailureView | null; /** The last import's counts, surfaced as the panel's confirmation line. */ readonly lastImport: ResearchImportCounts | null; } /** One server's probe lifecycle: in flight, or the last settled view. */ export type ServerCheckState = ServerStatusView | 'checking'; /** One snapshot's fetched content (the diff source of the snapshots panel). */ export interface ResearchSnapshotDetailView { readonly projectId: string; readonly id: string; readonly status: 'loading' | 'ready' | 'error'; readonly files: readonly { readonly path: string; readonly content: string; }[]; readonly failure: ResearchFailureView | null; } /** Immutable view published to the panel. */ export interface ResearchView { readonly projects: readonly ResearchProjectView[]; readonly projectsStatus: ResearchLoadStatus; readonly projectsFailure: ResearchFailureView | null; readonly outline: ResearchOutlineView | null; readonly compile: ResearchCompileView; readonly source: ResearchSourceView | null; readonly papers: ResearchPapersView; /** The papers view's arXiv search outcome; null before the first search. */ readonly arxivSearch: ResearchArxivSearchView | null; /** The papers view's web search outcome; null before the first search. */ readonly webSearch: ResearchWebSearchView | null; /** The papers view's arXiv subscription bar. */ readonly arxivSubscriptions: ResearchSubscriptionsView; /** The papers view's Zotero section (connection status plus collections). */ readonly zotero: ResearchZoteroView; /** The Zotero section's search outcome; null before the first search. */ readonly zoteroSearch: ResearchZoteroSearchView | null; readonly experiments: ResearchProjectSlice | null; readonly artifact: ResearchArtifactView | null; readonly figures: ResearchProjectSlice | null; /** The meetings view's generated decks of the selected project; null until first opened. */ readonly meetings: ResearchProjectSlice | null; /** The meetings view's image-generation config; `cold` until first fetched. */ readonly imageGen: ResearchImageGenView; /** The Papers view's digdig config; `cold` until its Web tab opens. */ readonly digdigConfig: ResearchDigdigConfigView; readonly servers: ResearchServersView; /** Per-server probe state, keyed by server id; absent means never probed. */ readonly serverChecks: Readonly>; /** Submitted remote jobs (the servers view's jobs section). */ readonly jobs: ResearchJobsView; /** The selected project's bibliography; null until the bib panel first opens. */ readonly bib: ResearchBibView | null; /** The selected project's paper snapshots; null until the snapshots panel first opens. */ readonly snapshots: ResearchProjectSlice | null; /** The venue picker's built-in template registry; loads once, lazily. */ readonly venueTemplates: ResearchVenueTemplatesView; /** The venues view's ccfddl catalog + the selected project's watch list; cold until first opened. */ readonly venues: ResearchVenuesView; /** The snapshot the snapshots panel expanded for diffing; null when closed. */ readonly snapshotDetail: ResearchSnapshotDetailView | null; /** The ledger (growth record) view's events for its selected window. */ readonly ledger: ResearchLedgerView; /** The ledger view's progress report; `idle` before the first generation. */ readonly report: ResearchReportView; /** The ledger view's cognitive brief (CBE roadbook); `idle` before the first generation. */ readonly brief: ResearchBriefView; /** The ledger view's worktree (S2): the process as branches, dead ends, and the mainline ref. */ readonly worktree: ResearchWorktreeSlice; /** The ledger view's foraging layer (S4): territories, the GUT baseline, the GUT cards. */ readonly foraging: ResearchForagingSlice; /** The ledger view's moments (S9b): the unified timeline, five sources, zero verbs. */ readonly moments: ResearchMomentsSlice; /** The ledger view's eureka (S8c): declarations with their measured roads. */ readonly eureka: ResearchEurekaSlice; /** The ledger view's evidence graph (v1): the pure fold over one window. */ readonly evidence: ResearchEvidenceGraphSlice; /** The ledger view's digest (B–F): six-perspective capsules + Eureka EWS, pushed on open. */ readonly digest: ResearchDigestSlice; /** The corner toast queue (oldest first); the host component sweeps expiries. */ readonly toasts: readonly ResearchToast[]; /** Scheduled-backup status for the overview; null until loaded (or on failure). */ readonly backup: ResearchBackupStatusView | null; /** Scheduled-task health snapshot (#223); null until loaded (or on failure). */ readonly taskHealth: readonly ResearchScheduledTaskView[] | null; /** The pending paper-editor jump of a figure insert; null once consumed. */ readonly paperJump: ResearchPaperJump | null; } /** * The panel's object layer. The paper directory is shared across projects, so * compile status is tracked per addressed project id but describes the same * physical compile; the Host owns the authoritative record. */ export declare class ResearchController implements HostObservable { private readonly remote; private view; private readonly listeners; private loadPromise; private backupPromise; private taskHealthPromise; private papersPromise; private subscriptionsPromise; private zoteroPromise; private zoteroGeneration; private serversPromise; private jobsPromise; /** Cancels stale outline replies after project changes or outline refreshes. */ private outlineGeneration; /** Cancels stale source replies independently of the outline lifecycle. */ private sourceGeneration; /** Cancels stale experiment replies independently of paper reads. */ private experimentsGeneration; /** Cancels stale compile-status replies after project changes. */ private compileGeneration; private artifactGeneration; private figuresGeneration; private arxivGeneration; private webGeneration; private bibGeneration; private snapshotsGeneration; private snapshotDetailGeneration; private ledgerGeneration; private reportGeneration; private briefGeneration; /** In-flight digest generation counter (the supersede contract). */ private digestGeneration; /** In-flight digest load guard (the ensure contract). */ private digestPromise; /** In-flight worktree load guard (the ensure/refresh contract). */ private worktreePromise; /** In-flight foraging load guard (the ensure/refresh contract). */ private foragingPromise; private momentsPromise; private eurekaPromise; private evidencePromise; /** The evidence graph's last window request (PRD §56); null before the first load. */ private evidenceRequest; /** Monotonic evidence generation: newer window requests supersede stale reads. */ private evidenceGeneration; private figuresInFlight; private figuresRefreshPending; private meetingsGeneration; private meetingsInFlight; private meetingsRefreshProject; private imageGenInFlight; private digdigConfigInFlight; /** A venue-registry load already in flight is left alone. */ private venueTemplatesInFlight; /** A venue-catalog load already in flight is left alone. */ private venuesInFlight; /** Cancels a stale venue-catalog reply when a newer request queued behind it. */ private venuesGeneration; /** The newest venue load requested while another was in flight. */ private venuesPending; private snapshotsInFlight; private compileAbort; private compileProject; private compileQueued; private saveTimer; private compileTimer; private saveInFlight; private saveAgain; /** Identifies the save currently holding {@link saveInFlight}; a stale save's finally must not clear it. */ private saveSeq; /** * The last dirty draft of a project the user navigated away from while its * save was still in flight; persisted by a trailing save chained off the * in-flight save's settle. */ private pendingTrailingSave; /** Live-refresh: supersedes an older overlapping source re-read; a late reply loses. */ private liveSourceReadSeq; /** Live-refresh: the open `/research/events` stream, or null. */ private eventStream; /** Live-refresh: the trailing-debounce aggregator behind pushed changes. */ private readonly liveAggregator; /** The filter of the last ledger load, replayed by the live refresh. */ private ledgerFilter; private disposed; private toastSeq; private paperJumpSeq; /** * @param remote - the research Remote namespace. */ constructor(remote: ResearchRemote); /** Return the cached immutable view. */ getSnapshot: () => ResearchView; /** Subscribe to view replacement. */ subscribe: (listener: () => void) => (() => void); /** Load the project list once; a failed load stays retryable. */ ensure(): void; /** Re-read the project list (the retry entry and the reconnect resync). */ resync(): void; /** * Fetch the scheduled-backup status for the overview's data section. * Informational only: any failure leaves the slice null, hiding the line * instead of surfacing an error. */ private loadBackup; /** * Fetch the scheduled-task health snapshot (#223) for the overview's data * section. Informational only: any failure leaves the slice null, hiding * the block instead of surfacing an error. */ private loadTaskHealth; /** * Push one toast into the corner stack: same copy+detail dedupes to a * refresh, the queue caps at {@link TOAST_LIMIT} (oldest drops first). * Only user-initiated, slow, or asynchronous completions call this — never * high-frequency editor state like the autosave pill. * @param kind - toast severity. * @param copy - locale copy key (the controller stays locale-free). * @param detail - optional verbatim suffix (counts, the failure message). */ notify(kind: ResearchToastKind, copy: ResearchKey, detail?: string | null): void; /** Remove one toast (the × button). @param id - toast id. */ dismissToast(id: number): void; /** Sweep expired toasts (the host component's expiry timer). */ pruneToasts(): void; /** * Export the whole wiki as one snapshot (the overview data section's * download button). * @returns the snapshot, or the settled failure view. */ exportWiki(): Promise; /** * Import one parsed snapshot in the given mode, then re-fetch every loaded * slice so the panel reflects the new wiki without a reopen. * @param snapshot - the parsed export JSON (revalidated host-side). * @param mode - `merge` skips existing keys; `replace` wipes first. * @param confirmReplace - must be true for `replace`. * @returns the per-table counts, or the settled failure view. */ importWiki(snapshot: unknown, mode: ResearchImportWikiMode, confirmReplace: boolean): Promise<{ imported: Record; skipped: Record; } | ResearchFailureView>; /** Re-fetch every loaded slice (the post-import repaint). */ reloadAll(): void; /** * Load one window of ledger (growth record) events for the ledger view. A * newer window supersedes an in-flight one, whose late reply is discarded by * generation; the previous window's events stay on screen while the refresh * runs (no blank flash on a window switch). * @param filter - the window/scope/order/limit filter the view assembled. */ loadLedger(filter: ResearchEventFilter): void; /** * Generate the progress report of one window (the ledger view's button): * publish `loading`, then the rendered Markdown or the settled failure. A * newer generation supersedes an in-flight one; the returned failure (when * any) is what the button surfaces — a success toasts once. * @param options - the window/scope options the view assembled. * @returns null on success, the settled failure view otherwise. */ generateReport(options: ResearchProgressReportOptions): Promise; /** localStorage key of the last-seen derivation version (I5 notice). */ private static readonly DERIVATION_STORAGE_KEY; /** legacy colon key, kept for one-cycle migration */ private static readonly DERIVATION_STORAGE_KEY_LEGACY; /** * I5: remember the derivation version the user last saw; a changed * version returns true so the brief view can show the re-calibration * notice. Reads the dotted key first, falling back to the legacy colon * key with an immediate migration (write new + remove old) so users who * stored a version before the key rename are not re-prompted. Writes go * to the dotted key only. Persistence is best-effort — a blocked storage * just drops the cross-session comparison, never the brief. */ private derivationRecalibrated; /** * Generate the cognitive brief (CBE roadbook) of one window (the brief * card's button): the same publish/supersede contract as * {@link generateReport}. A success toasts once. * @param options - the window/scope options the view assembled. * @returns null on success, the settled failure view otherwise. */ generateBrief(options: ResearchGenerateBriefOptions): Promise; /** * Generate the digest (B–F) of one tier (the digest card's tier switch, or * the proactive first-open push): publish `loading`, then the assembled * report model + MMS string, or the settled failure. A newer generation * supersedes an in-flight one. * @param tier - weekly / monthly / project. * @param lang - zh / en. * @returns null on success, the settled failure view otherwise. */ generateDigest(tier: ResearchDigestTier, lang: 'zh' | 'en'): Promise; /** Load the digest once, on the ledger view's first open (active push). */ ensureDigest(): void; /** Re-fetch the digest at its current tier/lang (the card's refresh button). */ refreshDigest(): void; /** * Declare one Eureka (the digest card's "name a milestone" form) — the * researcher's own words; the system never declares one and never prompts * "you are near an insight". A success toasts once and refreshes the digest * so the new milestone's lead-in EWS appears. * @returns null on success, the settled failure view otherwise. */ setEureka(ideaId: string | undefined, projectId: string | undefined, title: string): Promise; /** * Pin one moment (a capsule's pin affordance) — the explicit bookmark of a * curated instant. A success toasts once and refreshes the digest so the * pinned moment's capsule updates. * @returns null on success, the settled failure view otherwise. */ pinMoment(targetEventId: string, note?: string | undefined): Promise; /** * Decline one moment candidate (the timeline's decline affordance): a * `pinned:false` declaration on a never-canonical candidate — "seen and * refused", distinct from unpin. The row stays (append-only readability) * and moves to the declined group. * @returns null on success, the settled failure view otherwise. */ declineMoment(targetEventId: string): Promise; /** * Write one L2 journal line (the journal box's submit): the text plus the * project scope the view assembled, and optional refs — an `ideaId` writes * the entry against one line (the boundary-question cards' answer path), * `valence`/`arousal` are 1–5 self-report ratings that ride the payload. * A success toasts once and returns null; a failure returns the failure * view WITHOUT a toast — the journal box surfaces it inline. * @param text - the user's words (validated client-side, capped server-side). * @param projectId - the project scope, or null for an unscoped entry. * @param refs - optional line ref and self-reported mood ratings. * @returns null on success, the settled failure view otherwise. */ addJournal(text: string, projectId: string | null, refs?: { ideaId?: string | undefined; valence?: number | undefined; arousal?: number | undefined; question?: ResearchJournalQuestionRef | undefined; }): Promise; /** * Load the worktree (S2) slice: the derived working tree over the full * ledger — lanes with their statuses and documented-No numbers, the * declared parent edges, the mainline ref and its reflog. Publishing * contract mirrors {@link generateBrief}: loading → ready/error, the * previous view kept while loading. */ private loadWorktree; /** * Load the foraging (S4) slice: the territory ledger, the GUT baseline, * and the GUT cards — the same publish contract as the worktree slice. */ private loadForaging; /** Load the foraging layer once, on the ledger view's first open. */ ensureForaging(): void; /** * Load the moments (S9b) slice: the unified timeline over the five * deterministic sources, unified with pins and declines. Pull-only; the * same publish contract as the worktree/foraging slices. */ private loadMoments; /** Load the moments timeline once, on the ledger view's first open. */ ensureMoments(): void; /** * Reload the moments timeline after a pin/decline/unpin write, so the * timeline reflects the researcher's latest declarations. */ refreshMoments(): Promise; /** Load the eureka (S8c) slice: declarations × lead/control features + profile. */ private loadEureka; /** Load the eureka view once, on the ledger view's first open. */ ensureEureka(): void; /** * Load the evidence graph (v1) slice: one pure fold over the requested * ledger window (L1 — the product lives only in this view and the fold, * never persisted). The ledger's window/project scope is authoritative; * the graph never grows its own filter state (PRD §57). An identical * in-flight request is single-flighted; a new request supersedes the old * generation and its stale response is dropped. */ loadEvidenceGraph(request: EvidenceGraphRequest): void; /** Re-fetch with the last window request (the card's refresh, or after a write). */ refreshEvidenceGraph(): void; private fetchEvidenceGraph; /** * Retract one evidence edge by dedupKey — the panel's human-final-say * retraction (any edge; agents are gated host-side). A success toasts once * and re-folds the graph so the strikethrough appears immediately. * @returns null on success, the settled failure view otherwise. */ retractEvidence(dedupKey: string, reason?: string | undefined): Promise; /** Re-fetch the foraging layer (the card's refresh button). */ refreshForaging(): void; /** Refresh the foraging layer behind an in-flight guard (write paths). */ private requeueForaging; /** Load the worktree once, on the ledger view's first open. */ ensureWorktree(): void; /** Re-fetch the worktree (after a structural write, or the refresh button). */ refreshWorktree(): void; /** Refresh the worktree behind an in-flight guard, used by the write verbs. */ private requeueWorktree; /** * Move the mainline ref (one explicit user declaration; the system never * ranks lines into it). A success toasts once and refreshes the tree so * the new ref renders immediately. * @param lineId - the lane to declare (idea id or `project:`). * @returns null on success, the settled failure view otherwise. */ setMainline(lineId: string): Promise; /** * Declare (or clear, with null) one derivation edge — a branch point in * the surveyor's own words; never inferred. A success toasts once and * refreshes the tree. * @param ideaId - the child idea lane. * @param parentIdeaId - the parent idea lane, or null to clear. * @returns null on success, the settled failure view otherwise. */ setIdeaParent(ideaId: string, parentIdeaId: string | null): Promise; /** * Close one idea lane as a dead end — a documented No: the reason is * required, capped at {@link WORKTREE_REASON_MAX_CHARS} characters, and * re-validated server-side. A success toasts once and refreshes the tree; * the caller refreshes the ledger timeline so the event lands there too. * @param ideaId - the idea lane to close. * @param reason - the one-line lesson (what this No taught). * @returns null on success, the settled failure view otherwise. */ closeIdea(ideaId: string, reason: string): Promise; /** * Declare the merge: one idea line adopted (✓). A merge refreshes the * worktree; it is NOT a GUT departure, so the foraging baseline stays * put (giving-up time is measured on documented closes only). */ adoptIdea(ideaId: string): Promise; /** Load the literature list once, on the papers view's first open. */ ensurePapers(): void; /** * Re-fetch the literature list without flipping the view to loading (the * papers view's poll after handing a scoring request to the agent). */ refreshPapers(): void; /** * Load the arXiv subscription list once, on the papers view's first open; * once the list settles, a stale subscription (never checked, or checked * over the auto-check gap ago) triggers one open-time check — the host's * scheduled daily check is the steady-state path, this only freshens. */ ensureSubscriptions(): void; /** * Add one arXiv subscription, then refresh the list. The failure view of a * rejected save (empty or duplicate query) is returned so the bar surfaces it. * @param query - the free-text query. * @returns null on success, the settled failure otherwise. */ saveArxivSubscription(query: string): Promise; /** * Delete one arXiv subscription, then refresh the list. * @param id - the subscription id. * @returns null on success, the settled failure otherwise. */ deleteArxivSubscription(id: string): Promise; /** * Check every subscription for new papers (the bar's manual button and the * open-triggered refresh). The run republishes the list with each checked * subscription's post-check view; per-subscription fetch failures land in * `checkErrors`, a whole-call failure returns as the failure view. * @returns null on success, the settled failure otherwise. */ checkArxivSubscriptions(): Promise; /** Fetch the arXiv subscription list and publish it. */ private loadSubscriptions; /** * Load one whitelisted markdown artifact for the artifact viewer. Skips a * refetch of an already-ready same project+name unless forced. * @param projectId - wiki project id. * @param name - a whitelisted artifact name (e.g. `EXPERIMENT_LOG.md`). * @param force - bypass the fresh-view skip. */ loadArtifact(projectId: string, name: string, force?: boolean): void; /** * Scan one project's paper directory for figures. Skips a rescan of an * already-ready same project unless forced (the refresh button forces). * @param projectId - wiki project id. * @param force - bypass the fresh-view skip. */ loadFigures(projectId: string, force?: boolean, quiet?: boolean): void; /** * Rename one figure file of one project and force a rescan. The host moves * the wiki metadata row along and rewrites the paper's `.tex` references; * when any reference was rewritten and the editor draft is clean, the paper * view re-reads the file (a dirty draft is left alone — its next save takes * the conflict path). Failures surface as the returned failure view. * @param projectId - wiki project id. * @param relPath - figure path relative to the paper directory. * @param newName - the new bare file name (same extension). * @returns null on success, the settled failure otherwise. */ renameFigure(projectId: string, relPath: string, newName: string): Promise; /** * Replace one figure's wiki-recorded caption, then quietly rescan so the * card repaints without a loading flash. Failures surface as the returned * failure view. * @param projectId - wiki project id. * @param relPath - figure path relative to the paper directory. * @param caption - the replacement caption. * @returns null on success, the settled failure otherwise. */ updateFigure(projectId: string, relPath: string, caption: string): Promise; /** * Delete one figure of one project and force a rescan. The failure view of * a rejected delete is returned so the card can surface it; a successful * delete republishes the figures slice. * @param projectId - wiki project id. * @param relPath - figure path relative to the paper directory. * @returns null on success, the settled failure otherwise. */ deleteFigure(projectId: string, relPath: string): Promise; /** * List one project's generated meeting decks. Skips a reload of an * already-ready same project unless forced (generation and deletion force). * @param projectId - wiki project id. * @param force - bypass the fresh-view skip. */ loadMeetings(projectId: string, force?: boolean): void; /** * Generate one project's meeting deck, then force a deck-list reload. * Failures surface as the returned failure view; success toasts the slide * count. * @param projectId - wiki project id. * @param request - the deck options (title/presenter/date/selections). * @returns null on success, the settled failure otherwise. */ generateMeetingDeck(projectId: string, request: { title?: string | undefined; presenter?: string | undefined; date?: string | undefined; paperIds?: readonly string[] | undefined; figureRelPaths?: readonly string[] | undefined; include?: Partial | undefined; aiIllustrations?: boolean | undefined; }): Promise; /** * Delete one generated deck and force a deck-list reload. * @param projectId - wiki project id. * @param file - the deck file name within the project's meetings directory. * @returns null on success, the settled failure otherwise. */ deleteMeetingDeck(projectId: string, file: string): Promise; /** * Fetch the image-generation config (the masked panel view) once; a ready * slice or an in-flight load is left alone. Saving publishes the fresh view * directly, so this stays a cold-start warm-up. */ getImageGenConfig(): void; /** Fetch the masked image-gen config and publish it. */ private loadImageGenConfig; /** * Save the image-generation config, then publish the returned fresh masked * view. An omitted `apiKey` keeps the stored key; '' clears it. * @param input - the editable fields (empty strings are dropped host-side). * @returns null on success, the settled failure otherwise. */ saveImageGenConfig(input: { baseUrl?: string | undefined; apiKey?: string | undefined; model?: string | undefined; size?: string | undefined; }): Promise; getDigdigConfig(): void; private loadDigdigConfig; saveDigdigConfig(input: { baseUrl?: string | undefined; defaultEngine?: string | undefined; allowedEngines?: readonly string[] | undefined; defaultLimit?: number | undefined; defaultFormat?: 'md' | 'json' | undefined; useProxy?: boolean | undefined; proxyUrl?: string | undefined; timeout?: number | undefined; ollamaApiKey?: string | undefined; redundancyThreshold?: number | undefined; redundancyBigramThreshold?: number | undefined; }): Promise; /** * Load the venue picker's built-in registry, once and lazily: a ready (or * in-flight) registry is left alone. */ ensureVenueTemplates(): void; /** Fetch the venue registry and publish it. */ private loadVenueTemplates; /** * Apply one venue (built-in `templateId` or uploaded-kit `customName`) to * one project and refresh the project list so the header chip updates. * @param projectId - wiki project id. * @param options - built-in template id, or a custom kit display name. * @returns null on success, the settled failure otherwise. */ applyVenueTemplate(projectId: string, options: { templateId?: string | undefined; customName?: string | undefined; }): Promise; /** * Clear one project's target venue and refresh the project list. * @param projectId - wiki project id. * @returns null on success, the settled failure otherwise. */ clearVenueTemplate(projectId: string): Promise; /** * Load the venue catalog once and lazily: a ready catalog is left alone, * but a watch list loaded for a DIFFERENT project refetches (the watched * flags ride the catalog read). * @param projectId - the project whose watch list rides along, or null. */ ensureVenues(projectId: string | null): void; /** * Re-fetch the catalog read (the view's retry); no loading flash when a * previous list is on screen. * @param projectId - the project whose watch list rides along, or null. */ refreshVenues(projectId: string | null): void; /** * Ask the host to fetch the upstream catalog NOW, then reload the view. A * failure toasts and keeps the last good snapshot (the host never drops it). * @param projectId - the project whose watch list rides the reload, or null. */ refreshVenueCatalog(projectId: string | null): Promise; /** Fetch the catalog plus one project's watch list and publish the slice. */ private loadVenueDeadlines; /** * Flip one series in the selected project's watch list. Optimistic: the * star toggles immediately; a settled failure rolls the flag back and * toasts. * @param seriesKey - the ccfddl series key (lowercased title). */ toggleVenueWatch(seriesKey: string): Promise; /** * List one project's paper snapshots (the snapshots panel's open and its * refresh). Skips a refetch of an already-ready same project unless forced; * a list load already in flight is left alone. * @param projectId - wiki project id. * @param force - bypass the fresh-view skip. */ loadSnapshots(projectId: string, force?: boolean): void; /** * Fetch one snapshot's files for the panel's diff view. A newer open * supersedes an in-flight older read, whose late reply is discarded by * generation. * @param projectId - wiki project id. * @param id - the snapshot id. */ loadSnapshotDetail(projectId: string, id: string): void; /** Close the snapshots panel's diff view (superseding any in-flight read). */ closeSnapshotDetail(): void; /** * Revert the paper to one snapshot: the snapshot's files land on disk under * the same optimistic concurrency as `savePaperSource` (the base is the * current draft's mtime). Both a success and a conflict re-read the outline * and the source from the Host, because the file on disk is newer than * either view; a success also re-reads the snapshot list. * @param projectId - wiki project id. * @param id - the snapshot id. * @returns null on success, the settled failure otherwise. */ revertSnapshot(projectId: string, id: string): Promise; /** * Insert one figure's standard LaTeX block into the selected project's * `main.tex` (the figures view's "insert into paper" button). The figure * file already lives in the paper directory — the figures view lists exactly * those files — so the insert only edits the source: the block lands right * before `\end{document}` and rides the normal draft/autosave path, so an * unsaved draft survives and the optimistic-concurrency save still guards * the write. A figure the draft already references is never inserted twice: * the existing `\includegraphics` line becomes the jump target instead. An * SVG figure is converted on the host first (`convertFigure` — vector PDF * preferred, raster PNG as the fallback) and the block references the * product; a machine with no usable converter keeps the failure toast with * the reason attached. Either way the paper view jumps to the block. * @param projectId - wiki project id. * @param entry - the figure card's entry. * @returns the 1-based target line for the paper view to jump to, or null * when the insert failed (a toast already carries the reason). */ insertFigureIntoPaper(projectId: string, entry: FigureEntry): Promise; /** * Generate one metric's comparison chart as a paper figure (the experiments * view's per-chart button): render the rows as a standalone SVG document, * save it through the host (`saveFigure` — writes `figures/metric-.svg`, * registers the caption in the wiki's figures table, and runs the SVG * conversion pipeline), then insert the figure block exactly like the * figures view's insert button (the SVG branch of `insertFigureIntoPaper` * reuses the just-converted product, references it, jumps, and toasts). A * rejected save toasts the reason and touches nothing. * @param projectId - wiki project id. * @param metricKey - the metric the chart compares. * @param rows - the chart's rows (runs carrying a finite value, oldest first). * @param direction - the metric's preference direction (#220): the exported * SVG shades the same best run the panel chart does. * @returns the 1-based target line for the paper view to jump to, or null * when the save or insert failed (a toast already carries the reason). */ generateMetricFigure(projectId: string, metricKey: string, rows: readonly MetricChartRow[], direction: MetricDirection): Promise; /** Clear the consumed paper-editor jump ticket (the paper view's callback). */ consumePaperJump(): void; /** Publish the paper view's next jump target. */ private jumpPaper; /** * Guarantee a ready source draft for one project, loading it from the Host * when the current slice is absent, stale, or another project's. A failed * load publishes the error slice, toasts the insert failure, and reads as * null. * @param projectId - wiki project id. * @returns the ready source view, or null. */ private ensureSourceReady; /** * Reorder the top-level sections of one project's `main.tex`. The failure * view of a rejected reorder is returned so the outline rail can surface it; * both a success and a conflict re-read the outline and the source from the * Host, because the file on disk is newer than either view. * @param projectId - wiki project id. * @param moves - the drops, applied in order. * @param baseOutline - the top-level titles the drag started from. * @returns null on success, the settled failure otherwise. */ reorderPaperSections(projectId: string, moves: readonly SectionMove[], baseOutline: readonly string[]): Promise; /** * Reorder the subsections of one project's `main.tex`, inside their own * section or across sections. Same settlement contract as * {@link ResearchController.reorderPaperSections}: the failure view is * returned for the rail, and both a success and a conflict re-read the * outline and the source from the Host. * @param projectId - wiki project id. * @param moves - the drops, applied in order. * @param baseOutline - the section/subsection title tree the drag started from. * @returns null on success, the settled failure otherwise. */ reorderPaperSubsections(projectId: string, moves: readonly SubsectionMove[], baseOutline: readonly SectionOutlineTitles[]): Promise; /** * Search arXiv for one query and publish the outcome to the papers view. * A newer search supersedes an in-flight one, whose late reply is discarded * by generation; an empty query never leaves the client. * @param query - the free-text query. */ searchArxiv(query: string): void; /** * Search the web (SearXNG through the digdig CLI) for one query and publish * the outcome to the papers view. Mirrors {@link searchArxiv}'s supersede * semantics: a newer search discards an in-flight one's late reply, and an * empty query never leaves the client. * @param query - the free-text query. */ searchWeb(query: string): void; /** * Import one arXiv entry into the wiki, then refresh the literature list so * both the library grid and the result card's imported state repaint. The * selected project rides along as the paper's initial project link, so each * project's literature view fills up on its own. The failure view of a * rejected import is returned so the card can surface it. * @param entry - the parsed arXiv entry. * @param projectId - the selected project to link, when any. * @returns null on success, the settled failure otherwise. */ importPaper(entry: ArxivEntry, projectId?: string): Promise; /** * Import one existing local LaTeX project into the workspace (the sidebar's * "import existing project" dialog): the host copies the tree into * `imported//` and registers the project. On success the project list * refreshes BEFORE the summary resolves, so the caller can select the new * project against an up-to-date list. * @param path - absolute or `~`-prefixed source directory. * @param title - the optional explicit title. * @returns the import summary on success, the settled failure otherwise. */ importProject(path: string, title?: string): Promise; /** * Create one empty project (the sidebar's "new project" row): the host * registers it at the `idea` stage. On success the project list refreshes * BEFORE the view resolves, so the caller can select the new project * against an up-to-date list. * @param title - the project title (trimmed, non-blank, capped host-side). * @returns the created project on success, the settled failure otherwise. */ createProject(title: string): Promise; /** * Rename one project (the sidebar row's inline editor), then refresh the * project list so the sidebar and the overview repaint. * @param projectId - wiki project id. * @param title - the new title (trimmed, non-blank, capped host-side). * @returns the updated project on success, the settled failure otherwise. */ renameProject(projectId: string, title: string): Promise; /** * Delete one project and its project-scoped data (the sidebar row's delete * button; the component confirms first — the verb always sends the * confirmed request), then refresh the project list. The selection hand-off * is the caller's: it knows whether the deleted project was selected. * @param projectId - wiki project id. * @returns null on success, the settled failure otherwise. */ deleteProject(projectId: string): Promise; /** * Remove one remembered paper, then refresh the literature list so the * library grid and any matching search result repaint. * @param arxivId - the bare arXiv id. * @returns null on success, the settled failure otherwise. */ removePaper(arxivId: string): Promise; /** * Partially update one paper's organization fields (tags, project links, * notes), then refresh the literature list so the grid, the filter bar, * and any matching search result repaint. * @param arxivId - the bare arXiv id. * @param patch - the fields to replace; omitted fields stay untouched. * @returns null on success, the settled failure otherwise. */ updatePaper(arxivId: string, patch: { tags?: string[]; projectIds?: string[]; notes?: string; }): Promise; /** * Download one remembered paper's arXiv PDF into the workspace, then refresh * the literature list so the card's read/fetch buttons repaint. The failure * view of a rejected fetch is returned so the card can surface it. * @param arxivId - the bare arXiv id. * @returns null on success, the settled failure otherwise. */ fetchPaperPdf(arxivId: string): Promise; /** * Probe the Zotero connection once, on the papers view's first open; a * successful probe also loads the collection list. A failed or unconfigured * probe stays retryable through {@link recheckZotero}. */ ensureZotero(): void; /** Re-probe the Zotero connection (the section's retry entry). */ recheckZotero(): void; /** Run the connection probe; on `ok` follow it with the collection list. */ private loadZotero; /** * Search the configured Zotero library from the papers view; the outcome * lands in the view's `zoteroSearch` slice. A superseded query never * publishes. * @param query - the free-text query; an empty one never leaves the client. */ searchZotero(query: string): void; /** * Import one Zotero item into the wiki, then refresh the literature list so * the library grid and the item's imported state repaint. The selected * project rides along as the paper's initial project link, matching the * arXiv import. The failure view of a rejected import is returned so the * row can surface it. * @param key - the Zotero item key. * @param projectId - the selected project to link, when any. * @returns null on success, the settled failure otherwise. */ importZoteroItem(key: string, projectId?: string): Promise; /** * Export one Zotero collection into one project's `references.bib`, then * repaint the open bib panel from the Host's authoritative file. The * settled counts are returned so the invoking button shows its own feedback. * @param projectId - wiki project id. * @param collectionKey - the Zotero collection to export. * @returns the settled counts on success, the failure view otherwise. */ exportZoteroCollectionToBib(projectId: string, collectionKey: string): Promise; /** * Load one project's `references.bib` on the bib panel's first open. A * ready (or in-flight) view of the same project is kept; a project switch * or an error view reloads. * @param projectId - wiki project id. */ ensureBibliography(projectId: string): void; /** Re-read the open bibliography from the Host (the conflict recovery path). */ reloadBibliography(): void; /** * Delete one entry from the open bibliography and commit the file under * optimistic concurrency. The failure view of a rejected save is returned * so the row can surface it; a conflict freezes the panel until reloaded. * @param key - the citation key to drop. * @returns null on success, the settled failure otherwise. */ deleteBibEntry(key: string): Promise; /** * Replace one entry of the open bibliography (the field editor's save) and * commit the file under the same optimistic concurrency as the delete. The * edited entry keeps its list position; a rename to another entry's key is * rejected client-side (`invalid-input`), as are an empty key or type and * an edit addressed to a key no longer listed. The failure view is returned * so the editor stays open with the rejection surfaced. * @param originalKey - the citation key the editor opened on. * @param entry - the edited entry (its key may differ from `originalKey`). * @returns null on success, the settled failure otherwise. */ updateBibEntry(originalKey: string, entry: BibEntry): Promise; /** * Commit one next entry list of the open bibliography through * `saveBibliography`'s optimistic concurrency: publish `saving`, land the * new mtime and entries on success, freeze the panel on a conflict. Shared * by the entry delete and the field editor's save. * @param entries - the complete next entry list. * @param toast - the success toast's copy key. * @returns null on success, the settled failure otherwise. */ private commitBibEntries; /** * Append library papers to one project's `references.bib`, then repaint the * open bib panel from the Host's authoritative file. The settled counts are * returned so the invoking button shows its own feedback. * @param projectId - wiki project id. * @param arxivIds - the papers to append. * @returns the settled counts on success, the failure view otherwise. */ importPapersToBib(projectId: string, arxivIds: string[]): Promise; /** Fetch one project's `references.bib`; a superseded generation never publishes. */ private loadBibliography; /** * Delete one experiment record and drop it from the loaded slice (the Host * already removed its record, so a local filter repaints without a refetch). * The failure view of a rejected delete is returned so the row surfaces it. * @param id - experiment record id. * @returns null on success, the settled failure otherwise. */ deleteExperiment(id: string): Promise; /** * Relink one experiment to a server (null clears the link) and patch the * loaded experiments slice with the returned record. The failure view of a * rejected update is returned so the view can surface it. * @param id - experiment record id. * @param serverId - the server to link, or null to clear. * @returns null on success, the settled failure otherwise. */ updateExperiment(id: string, serverId: string | null): Promise; /** * Create or update one experiment (the inline form's save), patch the * loaded experiments slice with the returned record (replace when the id * was already listed, append otherwise), and toast the success. The * failure view of a rejected save is returned so the form surfaces it. * @param experiment - the full-field upsert payload; `id` present updates. * @returns null on success, the settled failure otherwise. */ saveExperiment(experiment: ExperimentInput): Promise; /** Load the server list once, on the servers view's first open. */ ensureServers(): void; /** * Create or update one server, then refresh the list. * @param server - the upsert payload; `id` present updates, absent creates. * @returns null on success, the settled failure otherwise. */ saveServer(server: ServerInput): Promise; /** * Delete one server, drop its probe state, and refresh the list. * @param id - server record id. * @returns null on success, the settled failure otherwise. */ deleteServer(id: string): Promise; /** * Probe one server: publish `checking`, then the settled view. A probe * already in flight for the same server is left alone. * @param id - server record id. */ checkServer(id: string): Promise; /** * Probe every listed server that is not already being probed, then toast * when the batch settles (a user-initiated, potentially slow operation). */ checkAllServers(): Promise; /** Run one probe, translating every failure mode into a settled offline view. */ private runServerCheck; /** Fetch the server list and publish it. */ private loadServers; /** Load the job list once, on the servers view's jobs section first open. */ ensureJobs(): void; /** * Re-poll the job list (the jobs section's interval while any job is * active, and the post-submit repaint). A poll already in flight is left * alone; a ready list stays ready while the refresh runs. */ refreshJobs(): void; /** * Submit one remote command to a server, then repaint the job list. The * failure view of a rejected submit is returned so the form can surface it. * @param serverId - the target server record id. * @param command - the remote command line. * @param experimentId - the experiment to link, when given. * @returns null on success, the settled failure otherwise. */ submitJob(serverId: string, command: string, experimentId?: string): Promise; /** * Delete one job record, dropping it from the loaded list. * @param id - job record id. * @returns null on success, the settled failure otherwise. */ deleteJob(id: string): Promise; /** Fetch the job list and publish it, toasting terminal flips observed between polls. */ private loadJobs; /** * Toast each job whose poll-observed status newly flipped terminal, then * refresh the loaded experiments slice when a LINKED job settled: the * Host's write-back flipped that experiment's status and recorded the * outcome as `lastJob`, and the row should show both without a reselect. */ private notifyJobTransitions; /** * Select one project — or clear the selection with null (the deleted-last- * project path): load its paper outline, last compile status, and source. * A newer selection supersedes in-flight older reads, whose late replies * are discarded by generation; pending autosave/auto-compile timers of the * previous selection are cancelled. Null flushes a dirty draft exactly like * a switch (the same losing-it-is-not-an-option rule), then empties every * per-project slice so no later write can aim at the removed project. * @param projectId - wiki project id, or null to deselect. */ select(projectId: string | null): void; /** * Apply one keystroke batch to the draft and schedule the autosave. Only a * ready editor accepts edits; a conflicted or failed draft is frozen until * reloaded. * @param content - the textarea's full next value. */ edit(content: string): void; /** * Discard the draft and re-read the file from the Host. The conflict * recovery path: the agent's version wins and the editor snaps back to it. */ reloadSource(): void; /** * Compile the paper for one project. A compile requested while another run * is in flight is queued and fired when the in-flight run settles, so an * autosave-triggered compile never interrupts the one already running. * @param projectId - wiki project id. */ compile(projectId: string): Promise; /** Abort an in-flight compile, cancel pending timers, and drop subscribers. */ dispose(): void; /** * Open the wiki events stream (the panel's live refresh). Idempotent; the * stream auto-reconnects, and every frame rides the debounced aggregation * into {@link applyLiveSlices}. * @param factory - stream opener override (tests); defaults to EventSource. */ connectWikiEvents(factory?: WikiEventStreamFactory): void; /** * Reconnect compensation: the events stream gives no replay, so writes that * landed while it was down are unknown. Re-read every warm slice at once — * one pseudo change per mappable table rides the normal aggregation path, * and the cold-slice gates in {@link applyLiveSlices} keep untouched views * untouched. */ private resyncLiveSlices; /** Close the events stream and drop any pending live refresh. */ disconnectWikiEvents(): void; /** Fold one pushed wiki change into the debounced refresh. */ enqueueWikiChange(event: ResearchWikiChangeEvent): void; /** Flush pending live-refresh slices immediately (the visible-again catch-up). */ flushWikiChanges(): void; /** * Re-read exactly the slices one settled change burst dirtied. Every reload * goes through the slice's own loader, so the generation/pending guards of * the refresh paths apply unchanged; a cold slice (its view never opened) * is skipped. The paper slice never disturbs a draft: the outline re-read * is always safe, the source re-read only runs for a settled draft and * only publishes when the file on disk actually moved. */ private applyLiveSlices; /** * Re-read one project's `main.tex` for the live refresh and publish ONLY * when the file moved: a same-content same-mtime reply (the echo of the * panel's own save) keeps the current view untouched. No generation bump — * the publish re-checks the view identity so a draft started mid-flight is * never overwritten. */ private reloadSourceIfChanged; /** Publish a compile failure as an error state carrying the message as one synthetic issue. */ private publishCompileError; /** Fetch the project list and publish it. */ /** Fetch the project list and publish it. A refresh over a ready list keeps the list on screen (no sidebar flash); only the first load shows the loading state. */ private loadProjects; /** Fetch the literature list and publish it. A quiet poll keeps a ready list on screen. */ private loadPapers; /** Fetch one project's experiment runs; a superseded generation never publishes. */ private loadExperiments; /** The selected project's paper directory override, from the loaded list row. */ private dirOf; /** * Re-read one project's outline and source after the file changed on disk * (a section reorder, or the conflict that rejected one). */ private refreshPaper; /** Fetch one project's outline; a superseded generation never publishes. */ private loadOutline; /** Fetch one project's last compile status without touching an in-flight run. */ private loadCompileStatus; /** Fetch one project's `main.tex`; a superseded generation never publishes. */ private loadSource; /** * Save the current draft under optimistic concurrency. Only one save is in * flight at a time; an edit landing mid-flight re-runs the save after it * settles. A successful save of an untouched draft schedules the auto-compile. */ private flushSave; /** * Persist the draft tail of a project the user navigated away from mid-save. * Deliberately outside the view state machine — the loading slice of the new * selection already replaced the view, so only the file side effect matters. * A conflict is fine and silent (a concurrent agent/host write won; selecting * the project back re-reads the file). * @param draft - the snapshot select() took of the dirty view. * @param baseMtimeMs - the mtime to build on: the in-flight save's landed * mtime when it succeeded, else the snapshot's original base. */ private saveTrailingDraft; /** Debounce the auto-compile that follows a successful save. */ private scheduleCompile; /** Cancel pending autosave and auto-compile timers. */ private clearTimers; /** Replace part of the view and contain subscriber failures at the boundary. */ private publish; } //# sourceMappingURL=controller.d.ts.map