import { AssistantModeConfig } from './conversation'; import { AssistantMode, BuiltInAssistantMode } from './types'; /** * What a mode DOES, as far as the panel is concerned: whether its turns produce * files, how long one turn may run, and whether an unfinished turn keeps going * server-side. * * ★★ Capabilities, not mode ids. Each of these was a `mode === "vibe-plugins"` * test scattered across the panel, which meant no other mode could ever list * artifacts or run a long build — however it was configured — without another * edit to shared code naming it. A product now says what its mode does on its * `AssistantModeConfig`, and this is the one place that decides. * * ★ Built-in ids keep their built-in behaviour when the product says nothing. * That is what keeps an existing product byte-identical: VibeControls' `Build` * mode is `vibe-plugins` with none of these fields, and resolves exactly as the * hard-coded checks did (artifacts, 600s, "still building"). */ export interface AssistantModeCapabilities { /** List `/sessions/:id/artifacts` after each turn and render them. */ readonly producesArtifacts: boolean; /** How long the client polls one turn before closing it as unfinished. */ readonly streamBudgetMs: number; /** An unfinished turn keeps building server-side — selects the "still building" note. */ readonly continuesInBackground: boolean; /** * Report this run's narration for the WHOLE run, joined oldest first, instead * of only its newest message (BC1, BOFF-7334). * * ★★ Off for every mode by default, and deliberately so. Latest-only is what * both host panels and every product wrapping the shared transport render * today, so this cannot be switched on globally. It also changes what is * STORED — the completed turn persists this content as its answer — so only a * mode whose answer IS the account of what it built should ask for it. */ readonly accumulatesNarration: boolean; /** * May the panel provision a sandbox for this mode at all. * * ★ `false` only for a mode that is neither built in nor declared by the * product — a persisted id from another build, or a malformed one. It fails * CLOSED: no sandbox, no artifacts, the default budget. */ readonly provisionable: boolean; } /** The capability fields a product may set, plus the id they belong to. */ export type AssistantModeCapabilityConfig = Pick; /** * The largest `streamBudgetMs` a product may configure: one hour. * * The sandbox TTL is extended every 30s while a turn is polled, so a long budget * never outlives the sandbox — but a value like `Infinity` or a seconds/ms mix-up * (`600_000_000`) would leave a spinner up for days. Out-of-range values fall * back to the default rather than throwing, for the same reason a `NaN` must. */ export declare const MAX_ASSISTANT_STREAM_BUDGET_MS = 3600000; /** * The built-in modes' behaviour. The ONLY table in shared code that knows * `vibe-plugins` builds things. */ export declare const BUILT_IN_MODE_CAPABILITIES: Readonly>; /** Fail-closed result for a mode nobody declared. */ export declare const UNDECLARED_MODE_CAPABILITIES: AssistantModeCapabilities; /** * The capabilities of `mode`, given the product's config for it. * * - Built-in id: its built-in behaviour, with any field the product set applied * over it. * - Product-declared id WITH a config: chat defaults with the product's fields * applied. Declared means "the product offers a config for exactly this id". * - Anything else — an unoffered product id, a malformed id, no mode at all: * `UNDECLARED_MODE_CAPABILITIES`. * * ★ A config whose `id` is not `mode` is IGNORED. The caller looked it up, and a * lookup that fell back to another mode (as `resolveModeConfig` does, by design, * for display) would otherwise lend one mode's 10-minute budget and artifact * listing to another. * * ★ `continuesInBackground` follows `producesArtifacts` when only the latter is * set: the "still building" note promises "any artifacts below will finish * shortly", which is only ever true of a mode that produces them. */ export declare function resolveAssistantModeCapabilities(mode: AssistantMode | null | undefined, config?: AssistantModeCapabilityConfig | null): AssistantModeCapabilities; /** * Why a send in an admin-only mode must not go out yet, or null when it may. * * ★★ The mode list hides an admin-only mode from a non-admin, and an effect * resets a persisted one back to Ask — but only AFTER the role check resolves, * so an admin's own persisted choice is not reset during that window. A send in * the window used to go straight through: the mode was provisioned while the * answer was still unknown, and the proxy-token modes have no server-side admin * check behind it. So the send asks the same question the list does. * * ★★ `loading` is NOT the send's normal path through here. The send WAITS for * the check (`adminOnlyModeSendRefusalAfterAccess`), because refusing during it * broke every product whose help button opens the panel and sends on the next * tick. Reaching this branch means that wait timed out, so the copy says the * check has not finished — it must never read as a refusal of the person. * * Only a config that says `requiresAdmin` is gated — Ask carries no such flag * and is never held. Truthiness is read exactly as the mode list and the reset * effect read it, so for any config the three agree. * * ★ The one case where they do not is a mode with NO config at all: the reset * effect treats a missing config as blocked, while `requiresAdmin` is simply * absent here, so nothing is gated. That is reachable only for a built-in id a * build has stopped declaring — where there is no admin check to consult * anyway, and `createApp` is RBAC-gated server-side regardless. */ export declare function adminOnlyModeSendRefusal(config: { readonly requiresAdmin?: boolean; } | null | undefined, access: { readonly loading: boolean; readonly canUseAdminModes: boolean; }): string | null; /** * Why a send in a mode the product does not offer must not go out, or null * when it may (BOFF-7308). * * ★★ The gap `adminOnlyModeSendRefusal` cannot see. That check reads the mode's * config — and a BUILT-IN id the product never listed has none, so there is no * `requiresAdmin` to read and nothing is held. A built-in id is also always * provisionable (`resolveAssistantModeCapabilities`), so a persisted `api-calls` * or `vibe-plugins` in a product whose list lacks it was provisioned on the * first send. The reset effect does move it to Ask, but only after the role * check answers, which is later than a help button that sends on the next tick. * * Refused rather than held: waiting on the role check cannot help, because no * answer from it makes an unlisted mode listed. The copy matches the refusal for * an undeclared product id, so the person sees one message for one situation. * * ★ `assistant` (Ask) is exempt even when unlisted. It is the mode every person * starts in and where the reset effect lands for everyone, including in a panel * mounted with no product modes at all; refusing it would leave such a panel * with no mode that can send. */ export declare const UNAVAILABLE_MODE_REFUSAL = "This assistant mode is not available here. Switch to another mode and send again."; export declare function unofferedModeSendRefusal(mode: AssistantMode, config: { readonly id: AssistantMode; } | null | undefined): string | null; /** * The product's config for EXACTLY this mode, or undefined. * * ★ Deliberately not `resolveModeConfig` (product.tsx), which falls back to the * first mode so a stale id still renders suggestions. Behaviour must never be * borrowed that way — see `resolveAssistantModeCapabilities`. */ export declare function findAssistantModeConfig(modes: readonly T[] | null | undefined, mode: AssistantMode | null | undefined): T | undefined; //# sourceMappingURL=modeCapabilities.d.ts.map