/** * `@ai-matrx/agents/sources` — THE ONE SOURCE PAYLOAD (contract v1, frozen 2026-09-27). * * Every request that is built from a person's Sources carries them as a * `SourceSet`: a list of `SourceRef` pointers (never blobs) plus how the model * should be grounded in them. The server answers with a `SourceManifest` * (sizes and states, never bodies — POST /sources/manifest) or a * `ResolvedSourceSet` (grounded text for generators — POST /sources/resolve). * * Contract of record: common-docs `projects/unified-source-input/DESIGN.md` * § "Contract v1 — FROZEN". A shape change here is an AMENDMENT there first, * with its Pydantic twin beside aidream's `parse_resource_reference`. * * `SourceRef` IS the existing `resource_ref` pointer already on the wire * (aidream `services/conversation_context/resource_context.py`), extended with * optional fields only — never coin a second pointer type. * * Pure module: no React, no I/O, no "use client". */ declare const RESOURCE_REF_KIND: "resource_ref"; declare const SOURCE_SET_KIND: "source_set"; declare const SOURCE_MANIFEST_KIND: "source_manifest"; declare const RESOLVED_SOURCE_SET_KIND: "resolved_source_set"; /** The wire version every `SourceSet` carries. */ declare const SOURCE_SET_VERSION: 1; /** A bounded preview of one representation promoted into the prompt. */ interface ResourcePromotion { representation: string; max_chars?: number; } type SourceDelivery = "direct" | "context"; type SourceGrounding = "whole" | "selected" | "retrieve"; /** The pointer — the existing `resource_ref` envelope, extended (all new fields optional). */ interface SourceRef { envelope: typeof RESOURCE_REF_KIND; /** Server resource token: "file" (A2; "cld_file" is a server alias) | "processed_document" | "note" | "fc_set" | … */ resource_type: string; resource_id: string; /** "clean" | "raw" | "pdf" | a family representation key. Omitted = server's Clean → Raw → original fallback. */ representation?: string; /** Hand-picked Segment ids (rag.kg_chunks ids or ":"). */ include_segments?: string[]; /** Include the text (default "direct") | the AI fetches it on demand ("context"). */ delivery?: SourceDelivery; /** Per-Source cap chosen on the review page. */ max_chars?: number; promote?: ResourcePromotion | ResourcePromotion[]; exclude?: string[]; } /** * A TYPE, not an interface (2026-10-23): it IS a JSON object, so it is assignable wherever the server * contract types the field as a JSON object (@ai-matrx/api's request bodies carry `source_set` as * `{ [key: string]: JsonValue }` — the podcast generate request). An interface has no implicit index * signature and would need a cast at every send. */ type SourceSet = { envelope: typeof SOURCE_SET_KIND; version: typeof SOURCE_SET_VERSION; sources: SourceRef[]; /** Only for "Just a topic". */ topic?: string; /** Default "whole". */ grounding?: SourceGrounding; /** Required in spirit when grounding = "retrieve". */ retrieve_query?: string; /** The model whose context window sets the budget. */ target_model_id?: string; }; type SourceState = "ready" | "processing" | "failed" | "unavailable"; interface SourceManifestForm { form: string; label: string; chars: number; available: boolean; } interface SourceManifestSegment { id: string; /** "Page 7", "Pages 22–24" (a part spanning pages), "0:04–2:31", a heading path, "Part 3". */ label: string; page?: number; chars: number; /** * Amendment A5: the part's opening words (about 160 characters, whitespace * collapsed) — never its body. Lets a person see what a part says and find * a part by its words. */ preview?: string; } interface SourceManifestEntry { ref: SourceRef; label: string; resource_type: string; state: SourceState; state_detail?: string; /** Why it cannot be used — the same words as `DroppedSource.reason`; "archived" = show Restore. */ reason?: DroppedSourceReason | null; forms: SourceManifestForm[]; default_form: string; segments?: SourceManifestSegment[]; } /** POST /sources/manifest { source_set } → sizes and states, never bodies. */ interface SourceManifest { __kind: typeof SOURCE_MANIFEST_KIND; sources: SourceManifestEntry[]; total_chars: number; estimated_tokens: number; /** null = unknown model; the UI says so. */ model_context_tokens: number | null; } interface ResolvedSourceSegment { id: string; page?: number; chars: number; } interface ResolvedSource { ref: SourceRef; label: string; form_used: string; /** Grounded: "### Chunk (page N)" blocks; never re-chunked. */ text: string; segments: ResolvedSourceSegment[]; file_id?: string; processed_document_id?: string; /** "processing" = the raw fallback was used. */ state: "ready" | "processing"; truncated: boolean; /** Every stand-in announces itself. */ notes: string[]; } /** * A1 (2026-09-27): "not_ready" = a file not yet read. "archived" (2026-10-01) = the Source is * archived and this person may know it (its owner, or anyone who can read it anyway): restore it * to use it. Anyone else hears "no_access". One server rule for every kind (`archived_reason.py`). */ type DroppedSourceReason = "no_access" | "archived" | "missing" | "failed" | "over_budget" | "not_ready"; interface DroppedSource { ref: SourceRef; reason: DroppedSourceReason; /** A1: what happened and what to do. */ detail?: string; } /** POST /sources/resolve { source_set } → grounded text for generators. */ interface ResolvedSourceSet { envelope: typeof RESOLVED_SOURCE_SET_KIND; sources: ResolvedSource[]; dropped: DroppedSource[]; total_chars: number; } /** Builder options. `undefined` is accepted everywhere so callers can forward an existing ref's optional fields. */ interface SourceRefOptions { representation?: string | undefined; include_segments?: readonly string[] | undefined; delivery?: SourceDelivery | undefined; max_chars?: number | undefined; promote?: ResourcePromotion | ResourcePromotion[] | undefined; exclude?: readonly string[] | undefined; } /** * The Source input's runtime types — what a list of picked Sources IS while a * person is still choosing, on top of the frozen v1 wire contract * (`@ai-matrx/agents/sources`), which is never re-declared here. * * UI-free: no React, no framework, no store library. Every client (the web * app, the browser extension, the desktop app, mobile) holds this same shape * in its own store through a `SourceSetStore` adapter. * * Contract of record: common-docs `projects/unified-source-input/DESIGN.md` * § "One core, many screens". */ /** Every tile a host can allow: the Add new doors, plus "existing" (Use existing + search). */ type SourceTileId = "upload" | "paste" | "web" | "youtube" | "audio" | "image" | "topic" | "existing"; /** * What a picked Source is while the person chooses: the tile it came through, * or — for something they already had — "files", "notes", "your_sources" * (a Source screen) or "records" (any other registry kind). */ type SourceKindId = SourceTileId | "your_sources" | "files" | "notes" | "records"; /** The input behind a Source that is still landing — enough to land it again. */ interface SourceIntakeInput { /** Pasted text (paste). */ text?: string | undefined; /** The name the person typed for pasted text. */ name?: string | undefined; /** A web page or YouTube link (web, youtube). */ url?: string | undefined; /** A recording that already finished uploading (audio) — transcribe from here. */ fileId?: string | undefined; } /** * One picked Source while the person is still choosing. JSON only — it is * persisted as a draft so a refresh never loses a pick. * * `ref` is null only while a NEW Source is still landing (reading a page, * transcribing, uploading); it becomes the pointer the moment the door * answers. Nothing is ever sent to the server as a blob. */ interface SourceDraft { kind: SourceKindId; /** What the person sees: a file name, a page title, the first line of a paste. */ label: string; ref: SourceRef | null; /** Where it came from, in plain words (a web address, "Pasted text"). */ origin?: string | undefined; /** The Source screen id when known (a landed Source) — every card opens. */ processedDocumentId?: string | undefined; /** * How a reused Source was captured (`processed_documents.source_kind`), so * the card and the review say "Transcript" or "Web page" — never "Document". */ sourceKind?: string | undefined; /** The stored file behind it, when it is one (the form chooser reads its family). */ fileId?: string | undefined; /** Every stand-in announces itself: a reader fallback, a door notice. */ notes?: string[] | undefined; /** The person asked to wait for the clean version before anything runs. */ waitForClean?: boolean | undefined; /** * Where the picked parts sit in the Source (label + pages), remembered when * they are picked. A Source still being cleaned lists its raw pages; when the * clean version lands its parts have NEW ids, and the pick is carried over by * page from here (verify-3: "0 characters · 4 of 21 parts"). Persisted with * the draft, so it survives a reload. */ pickedParts?: SourcePickedPart[] | undefined; /** * What the person handed over, kept ONLY while the Source is still landing * (or failed) so a reload or a failure never loses it: the pasted text, the * link, or an already-uploaded recording's file. Never file bytes. Cleared * the moment the Source settles. */ input?: SourceIntakeInput | undefined; } /** One picked part as the person saw it: its id, its label, and the pages it covers. */ interface SourcePickedPart { id: string; label: string; /** First and last page it covers, when the Source has pages. */ pages?: [number, number] | undefined; } type SourceCardStatus = "pending" | "resolving" | "ready" | "error"; /** One card: the draft, its lifecycle, and what the server measured. */ interface SourceCardModel { id: string; draft: SourceDraft; status: SourceCardStatus; /** A sentence with its remedy — never a code. */ error: string | null; /** The server's measurement (sizes, forms, parts, state). Null until read. */ manifest: SourceManifestEntry | null; } /** The entity the Sources are being used for — passed to the door as `attach_to`. */ interface SourceAttachTo { entityType: string; entityId: string; label?: string | undefined; } /** The whole state of one Source input: its cards (in the order picked) and the topic. */ interface SourceSetState { cards: readonly SourceCardModel[]; topic: string; } /** What a draft store keeps for a reload: the cards (without measurements) and the topic. */ interface PersistedSourceCard { id: string; draft: SourceDraft; status: SourceCardStatus; error: string | null; } interface PersistedSourceInput { sources: PersistedSourceCard[]; topic: string; } /** * Finding a part of a Source — THE one matcher, used by the Source card's * "Choose parts" and by "Review what goes in". * * A query is one of: * - a page number ("12") — the part that covers that page; * - a page range ("3-10", "3–10", "3 to 10") — every part inside it; * - words — every word must appear in the part's label or its preview (the * opening words the manifest carries, contract amendment A5), matched here; * or the part is one the server found holding every word in its full text * (`POST /sources/parts/search`, via `useSourcePartsSearch`). * * A part spanning several pages ("Pages 22–24") covers each of them: the * manifest's numeric `page` is its first page and the label names the span. * * V1-A: the search promised "words" but matched only labels, so * "photosynthesis" found nothing in a biology course. */ /** The ids of parts the server found holding every word (`useSourcePartsSearch`). */ type PartMatchIds = ReadonlySet; /** * fileSource — how a stored file on a Source card becomes its Source, decided * from the SERVER's state alone (USI-3e). Pure: the one read * (`/files/{id}/rag-status`) and the file's own organization are INJECTED by * the host (`SourceFileState` below), never imported. * * The server owns reading a file: every upload's finalize already starts the * one processing run (the file adapters → a `processed_documents` row), and * `/files/{id}/rag-status` merges that run's lifecycle row with the Source it * made. So a card never starts its own run while one is live, a reload simply * reads the state again (re-attach, never a second job), and a file someone * already had ("use the one you already have", or picked from Files) is kept * the moment its existing Source is seen. */ /** The part of the server's `/files/{id}/rag-status` answer the decision reads. */ interface FileSourceStatus { state: string; processed_document_id?: string | null | undefined; error?: { message?: string | null | undefined; [detail: string]: unknown; } | null | undefined; } /** * THE one client for the Source doors on the server (aidream * `api/routers/source_sets.py`): * * - `POST /sources/manifest` — sizes, states, forms and parts (never bodies); * - `POST /sources/resolve` — the full grounded text a generator reads; * - `POST /sources/parts/search` — the ids of one Source's parts that hold * every word of a query (the manifest's own part ids; never a body). * * All three are READS decided by access, never by the selected organization: * the server names them in `BODY_CARRIED_READS` (aidream `api/read_by_access.py`), * so every call goes out marked `bodyCarriedRead` — with an organization it is * still named, with none it is sent naming none instead of being refused on * the client (V1-A: "Select an organization before sending this request" was a * dead end the server never asked for). * * The TRANSPORT is injected: auth, base URL and error shape belong to the * host. A web app binds its typed API client; any other client can use * `createFetchSourcesTransport` below. */ /** Options every Source door call carries. */ interface SourceCallOptions { organizationId?: string | undefined; signal?: AbortSignal | undefined; } /** The server's answer to a part search. */ interface SourcePartsSearchResult { segment_ids?: string[] | null | undefined; truncated?: boolean | null | undefined; /** Set when the Source cannot be searched ("no_access", …). */ unavailable?: string | null | undefined; /** What happened and what to do, in the server's words. */ detail?: string | null | undefined; } interface SourcesClient { manifest(sourceSet: SourceSet, options?: SourceCallOptions): Promise; resolve(sourceSet: SourceSet, options?: SourceCallOptions): Promise; /** The ids of `ref`'s parts whose label or text holds every word of `query`. */ searchParts(ref: SourceRef, query: string, options?: SourceCallOptions): Promise; } /** * The Source-set controller — the imperative face of the state machine, with * every outside dependency injected through one adapter: * * - `store` where the cards live (a Redux slice, a Zustand store, the * in-memory store below — the host's choice); * - `persistence` the draft store a reload reads back (optional); * - `client` the Source doors (`createSourcesClient(transport)`); * - `config` the host's choices: default form, organization, the * deliveries it can use, the most Sources it takes. * * Every screen (the Studio grid, a compact strip, chat's "+", the review) * drives the SAME controller; `@ai-matrx/agents/sources/react` binds it to * React with `useSourceSet(adapter)`. */ /** Where the cards live. `getState` must return the SAME object until something changes. */ interface SourceSetStore { getState(): SourceSetState; setState(next: SourceSetState): void; subscribe(listener: () => void): () => void; /** Called once when a screen mounts (a store that registers entries lazily). */ init?: (() => void) | undefined; } /** The draft store a reload reads back. `load` returns the raw saved value (stable until it changes). */ interface SourceDraftStorage { load(): unknown; save(value: PersistedSourceInput): void; subscribe?: ((listener: () => void) => () => void) | undefined; } interface SourceSetConfig { /** The form each new Source starts on ("clean" | "raw"). Omitted = the server's choice. */ defaultForm?: string | undefined; /** The organization named on the door calls (they answer without one too). */ organizationId?: string | null | undefined; /** How the AI may get the Sources on this surface. Omitted = both. */ deliveries?: readonly SourceDelivery[] | undefined; /** Most Sources the person may pick. Omitted = no limit. */ max?: number | undefined; } interface SourceSetAdapter { store: SourceSetStore; persistence?: SourceDraftStorage | undefined; /** Needed for `manifest()` and `resolve()`. */ client?: SourcesClient | undefined; config?: SourceSetConfig | undefined; /** A new card's id (default: a random UUID). */ createId?: (() => string) | undefined; /** A failed call as one sentence with its remedy (default: the error's message). */ describeError?: ((error: unknown) => string) | undefined; } /** The measurement's own state (not persisted, not shared across controllers). */ interface SourceSetMeta { measuring: boolean; /** Why the measurement failed, with its remedy. */ manifestError: string | null; } /** The mutations every intake and screen drives. */ interface SourceSetActions { /** Add a Source that is already a pointer (a stored record, a landed Source). */ addReady(draft: SourceDraft): string; /** Add a Source that is still landing; finish it with `settle` or `fail`. */ addPending(draft: Omit): string; settle(id: string, patch: Partial & { ref: SourceRef; }): void; fail(id: string, sentence: string): void; /** Back to landing. False when the card is gone (removed) — nothing to land. */ restart(id: string): boolean; /** Change what a card that is still landing says or keeps (label, input, fileId, notes). */ updateDraft(id: string, patch: Partial>): void; /** Mark a landing card as waiting for the person to choose an organization (or clear it). */ setAsking(id: string, asking: boolean): void; remove(id: string): void; /** Change the pointer's choices (form, parts, cap, delivery). */ updateRef(id: string, options: SourceRefOptions): void; setWaitForClean(id: string, wait: boolean): void; setTopic(topic: string): void; /** True when this pointer is already picked. */ hasRef(resourceType: string, resourceId: string): boolean; /** How many Sources are picked RIGHT NOW (read from the store, never a stale render). */ liveCount(): number; /** POST /sources/manifest for the ready Sources; cards update with it. */ manifest(): Promise; } interface SourceSetController extends SourceSetActions { getState(): SourceSetState; subscribe(listener: () => void): () => void; getMeta(): SourceSetMeta; subscribeMeta(listener: () => void): () => void; /** Replace the host's choices (called on every render by a React binding). */ configure(config: SourceSetConfig): void; readonly config: SourceSetConfig; /** Bring back a saved draft once — only into an empty input. True when it restored. */ restore(saved: unknown): boolean; /** The frozen v1 payload, built from the ready Sources. */ toSourceSet(options?: { targetModelId?: string | undefined; }): SourceSet; /** * Apply a set the review returned (forms, parts, caps, delivery). Pass * `reviewed` — the set sent INTO the review — and every Source removed there * leaves the list too (only those; one that landed meanwhile stays). */ applySourceSet(set: SourceSet, options?: { reviewed?: SourceSet | undefined; }): void; /** POST /sources/resolve — the grounded text for a generator, named as the cards name them. */ resolve(options?: { targetModelId?: string | undefined; }): Promise; /** Switch back any Source set to a delivery the host cannot use (with a note). */ fitDeliveries(deliveries?: readonly SourceDelivery[]): void; /** How many more Sources fit under the host's max. */ roomLeft(): number; totalChars(): number; settled(): boolean; } /** * Source intake — every way NEW material becomes a Source, through the doors * that already exist on the server (DESIGN.md amendment A3). Each call adds a * card at once (so the person sees it working), then settles it to a pointer * or fails it with a sentence and a remedy. Nothing is ever sent onward as a * blob. * * pasted text → `POST /sources/land`, kept. * a web page → the host's scraper lands it (`addScrapedPage`), or a link * typed before a reload is scraped again (`addWebPage`); * `POST /sources/{id}/keep` files it against the thing being made. * a file/image → the host's uploader hands over file ids (`addUploaded`); the * file is filed against `attachTo` in the direction the * association registry declares, and its Source is kept once * reading makes one (`fileLanded`, driven by the recovery). * existing → a registry item (`addExisting`). * YouTube/audio → the transcript comes from the host's readers and lands * through `POST /sources/land`. * * Never lose input: every landing keeps what the person handed over in the * draft (`SourceDraft.input` — text, link, uploaded recording; never bytes) * until it settles, so `resume` can land it again after a reload, a failure, * or once an organization is chosen (the recovery replays every card waiting * for one — from state, never an in-memory queue). Landing twice is safe: the * door dedupes by (organization, identity, content hash). * * Every outside dependency is INJECTED (`SourceIntakeDeps`): the doors, the * organization gate, the signed-in person, the failure wording. */ /** A door's notice: a sentence, and sometimes a remedy code ("save_it…"). */ interface SourceLandingNotice { message: string; remedy?: string | null | undefined; } /** The target a door files a Source against (`attach_to`). */ interface SourceAttachTarget { entity_type: string; entity_id: string; label: string | null; signal: boolean; } /** The part of a landing body the intake reads or sets — the rest is the host's door shape. */ interface SourceLandingBodyBase { name: string; canonical_identity?: string | null | undefined; provenance?: Record | null | undefined; } /** A landed (or kept) Source's answer. */ interface SourceLandingAnswer { processed_document_id: string; notices?: SourceLandingNotice[] | null | undefined; } /** What the host's web reader returns for a link. Null = the page could not be read. */ interface ScrapedSourcePage { processedDocumentId: string | null | undefined; sourceNotices: SourceLandingNotice[]; pageTitle?: string | null | undefined; } /** What the host's YouTube reader returns. `source` = the server landed a timed transcript Source. */ interface YouTubeTranscriptResult { text: string | null | undefined; note?: string | null | undefined; source?: { processed_document_id: string; title?: string | null | undefined; } | null | undefined; } /** One file the host's uploader already uploaded. */ interface UploadedSourceFile { fileId: string; name: string; } /** The association write (the host's one chokepoint). */ interface SourceAssociationEdge { sourceType: string; sourceId: string; targetType: string; targetId: string; orgId: string; } type SourceAssociationResult = { ok: true; } | { ok: false; error: { message: string; }; }; interface SourceIntakeDoors { /** `POST /sources/land` with the body as given (keep + attach_to set by the intake). */ land(body: Body & { keep: true; attach_to: SourceAttachTarget[]; }): Promise; /** `POST /sources/{id}/keep`, filed against the targets. */ keep(processedDocumentId: string, options: { attachTo: SourceAttachTarget[]; organizationId: string; }): Promise<{ notices?: SourceLandingNotice[] | null | undefined; }>; /** Build the landing body for text the person handed over. */ buildTextLanding(input: { text: string; name?: string | undefined; organizationId: string; userId: string; }): Promise; /** Read a web page (the scraper lands it). Null = it could not be read. */ scrapeUrl(url: string): Promise; /** Read a YouTube video's transcript. */ fetchYouTubeTranscript(url: string): Promise; /** Write out a recording that is already stored. */ transcribeFile(input: { fileId: string; organizationId: string; }): Promise<{ text?: string | null | undefined; }>; /** File one edge through the host's association chokepoint. */ associate(edge: SourceAssociationEdge): Promise; } interface SourceIntakeDeps { doors: SourceIntakeDoors; /** The organization to land in — asks the person when none is chosen (the host's gate). */ ensureOrganization(): Promise; /** Run a landing the person asked for so the gate can carry the click intent. Default: run it. */ holdIntent?: ((run: () => Promise) => Promise) | undefined; /** True when an organization is already chosen (asking would not open the picker). Omitted = unknown. */ hasOrganization?(): boolean; /** True when a landing stopped only because no organization is chosen yet. */ waitsForOrganization(error: unknown): boolean; /** A failed landing as one sentence with its remedy. */ failureSentence(error: unknown): string; /** The signed-in person (null while sign-in is still loading). */ userId(): string | null | undefined; /** The YouTube video id in a link, or null. */ youtubeId(url: string): string | null | undefined; /** The largest pasted text kept in the draft for a reload (null = unknown — nothing kept). */ maxKeptChars(): number | null; /** The thing being made — new Sources are filed against it and kept. */ attachTo?: SourceAttachTo | undefined; } interface SourceIntake { addPastedText(text: string, name?: string): Promise; addWebPage(url: string): Promise; /** * A page the host's web picker already read (and the scraper landed); null id * = land its text instead. `pendingId` = the card `beginWebPage` made when the * read started — it is settled instead of a new card being added. */ addScrapedPage(page: { url: string; title: string; text: string; processedDocumentId: string | null; }, pendingId?: string | null): Promise; /** * The host's web picker STARTED reading a link: a card with the link kept * appears at once, so a reload mid-read brings it back and lands it (verify-3 * 12a/12b: a reload while a page was read lost it, link and all). Finish it * with `addScrapedPage(page, id)`; if the person drops the preview, remove it; * if the picker goes away mid-read, `resume(card)` lands it through the web door. */ beginWebPage(url: string): string; /** Files the host's uploader already uploaded. */ addUploaded(files: readonly UploadedSourceFile[], kind: SourceKindId): Promise; /** * A recording made in place (the Voice Pad) that is already written out: it * lands as text, but it IS a recording — its card and review say "Recording", * never "Pasted text" — and it keeps its name ("Recording 3:41 PM"). */ addRecordedText(text: string, name?: string): Promise; /** A recording already uploaded — written out, then landed. */ addUploadedRecording(file: UploadedSourceFile): Promise; /** Something the person already has (Use existing / search). Returns false when already picked. */ addExisting(item: { token: string; id: string; title: string; }): boolean; addYouTube(url: string): Promise; /** Land a card's kept input again (after a reload or a failure). False when nothing was kept. */ resume(card: SourceCardModel): boolean; /** The person chose the file again (uploaded) for a card whose upload was cut off. */ retryFile(card: SourceCardModel, file: UploadedSourceFile): Promise; /** Reading an uploaded file made its Source: keep it and file it against `attachTo`. */ fileLanded(card: SourceCardModel, processedDocumentId: string): Promise; } /** * Recovery — the UI-free half of "never lose input" (USI-3b), as decisions and * one step function. A React binding (`@ai-matrx/agents/sources/react` * `useSourceRecovery`) schedules them; any other client can call them itself. * * 1. A Source a reload cut off while it was landing, whose input the draft * kept (`interrupted.ts`), is handed back to the door once — the door * dedupes by content hash, so a second landing reuses the same Source. * 2. A landing that waited for an organization is landed again the moment one * is set — from the card's kept input, or, for a read Source whose keep * waited, by keeping it again. State, never an in-memory queue. * 3. A stored file's Source (new upload, reused copy, or picked file) is read * from the SERVER's state (`fileSource.ts`): kept and filed once it exists, * the one run started only when nothing is reading it, re-attached after a * reload — never a duplicate run. * * The once-per-card guards live on `globalThis` (one registry however many * loader graphs import this module), so two inputs on one surface, or a * re-mount, never land the same card twice. */ /** The host's processing runner, as far as the recovery needs it. */ interface SourceFileRunner { jobs: ReadonlyArray<{ cldFileId?: string | null | undefined; status: string; processedDocumentId?: string | null | undefined; }>; runForCldFile(fileId: string, label: string, reason: string): Promise; } interface FileRecoveryDeps { /** The organization the person picked (null = none yet). */ organizationId: string | null | undefined; /** The server's `/files/{id}/rag-status` read. */ readFileState(fileId: string, signal?: AbortSignal): Promise; /** The host's lookup of a file's own organization (omitted = unknown). */ fileOrganizationId?: ((fileId: string) => string | null | undefined) | undefined; } interface UseSourceSetOptions { /** * The draft store has been read back (a device store loads a moment after * the page). While false, "nothing picked" is not known — `restoring` stays * true. Default true. */ persistenceReady?: boolean | undefined; /** The host's choices, re-read on every render. Default: the adapter's config. */ config?: SourceSetConfig | undefined; } interface UseSourceSetResult extends SourceSetActions { /** Every picked Source, in the order picked. */ sources: SourceCardModel[]; topic: string; /** The frozen v1 payload, built from the ready Sources. */ toSourceSet: (options?: { targetModelId?: string | undefined; }) => SourceSet; /** Apply a set the review page returned (forms, parts, caps, delivery). */ applySourceSet: (set: SourceSet, options?: { reviewed?: SourceSet | undefined; }) => void; /** POST /sources/resolve — the grounded text for a generator. */ resolve: (options?: { targetModelId?: string | undefined; }) => Promise; /** Switch back any Source set to a delivery the host cannot use (with a note). */ fitDeliveries: (deliveries?: readonly SourceDelivery[]) => void; /** How many more Sources fit under the host's max. */ roomLeft: () => number; /** Characters that will go in (the server's measurement; 0 until measured). */ totalChars: number; measuring: boolean; /** Why the measurement failed, with its remedy. */ manifestError: string | null; /** Every Source finished landing (nothing pending) and none failed. */ settled: boolean; /** The saved draft is not read back yet — show a loading state, never an empty one. */ restoring: boolean; /** The controller itself, for non-React code on the same surface. */ controller: SourceSetController; } /** * THE hook for one Source input. `adapter` must be stable for a surface * (memoize it); a new adapter builds a new controller. */ declare function useSourceSet(adapter: SourceSetAdapter, options?: UseSourceSetOptions): UseSourceSetResult; /** Every landing door, over the set. Rebuilt each render (closures only — nothing to keep). */ declare function useSourceIntake(set: SourceSetActions, deps: SourceIntakeDeps): SourceIntake; interface UseSourceRecoveryOptions extends FileRecoveryDeps { } /** * Never lose input, and follow stored files' Sources — call once per screen * that sits on `useSourceSet` + `useSourceIntake`. */ declare function useSourceRecovery(set: Pick, intake: Pick, runner: SourceFileRunner, options: UseSourceRecoveryOptions): void; interface SourcePartsSearchState { /** Ids of the parts whose text holds every word (undefined until answered). */ matches: PartMatchIds | undefined; /** The server is being asked about the current words ("Searching inside the text…"). */ reading: boolean; /** Said under the search when the text could not be searched (titles and previews still match). */ error: string | null; /** More parts matched than the server returns; the list shown is the first of them. */ truncated: boolean; } /** * Which of one Source's parts hold every word a person typed, answered by the * server (`POST /sources/parts/search` — ids only; the whole text never comes * to the client). Asked only for words (never a page or a range), after the * person pauses typing. */ declare function useSourcePartsSearch(client: Pick, ref: SourceRef | null, query: string): SourcePartsSearchState; export { useSourceIntake, useSourcePartsSearch, useSourceRecovery, useSourceSet }; export type { SourcePartsSearchState, UseSourceRecoveryOptions, UseSourceSetOptions, UseSourceSetResult };