import { PendingWrite } from './pendingWrite'; import { AssistantMessageAttachment } from './attachments'; import { AssistantMode } from './types'; export type AssistantSandboxStatus = "none" | "provisioning" | "running" | "stopping" | "error"; export type AssistantConversationStatus = "idle" | "provisioning" | "connecting" | "streaming" | "complete" | "error"; /** * ★ `AssistantPanelArtifact`, not `AssistantArtifact`. The transport already * exports an `AssistantArtifact` in `./types` and the two genuinely differ — * this one narrows `type` to a union and makes `downloadPath` optional. They * were never reconciled, and reconciling them here would be a silent behaviour * change smuggled into a move. Two identically-named exports would also make * `export *` drop the name from the barrel with no error at all. * * The product re-exports this under its historical name, so no call site changes. */ export interface AssistantPanelArtifact { name: string; path: string; type: "file" | "directory"; size: number; downloadPath?: string; } export interface AssistantMessageMetadata { artifacts?: AssistantPanelArtifact[]; mode?: AssistantMode; sandboxId?: string; sessionId?: string; /** * Files the user attached to this turn. Deliberately the SLIM descriptor — * conversations are persisted to localStorage, so preview data URLs and * inlined text content must never land here (see AssistantMessageAttachment). */ attachments?: AssistantMessageAttachment[]; /** * The language this turn was generated under (BOFF-7108), as an assistant * language code. * * Fixed at send time, because it is a property of the TURN and not of the * current preference. `resolveSpeechLocale` cannot tell English from Spanish * or French — they share a script — so it falls back to whatever language is * selected NOW. Without this, a French reply starts being read by a Spanish * voice the moment the user switches, even with the right voice installed. */ lang?: string; /** * On a "continued from a voice conversation" note: how many spoken turns the * conversation held when it was written. * * ★★ The marker lives on the MESSAGE, not in component state, and that is the * whole point. Tracked in state it reset every time `PanelContainer` * unmounted the panel — minimise and reopen and the same note could be * appended again — and it could not say WHICH turns were already carried * over, so a later note repeated questions the thread already showed. A * message persists with the conversation and answers both. */ continuedVoiceTurns?: number; /** * This turn happened inside a live VOICE session, not in the chat panel. * * ★ The two surfaces share one conversation and one backend session — voice * was built as an overlay over the chat thread, so `submitVoiceTurn` calls * the same `handleSend` the composer does. Without a marker there is no way * to tell a spoken turn from a typed one once it is in the store, and every * voice exchange therefore appeared in the normal chat: the operator's own * report, "I see the same chat in the normal conversation tab, both are * separate, don't confuse". * * Stamped at send time and persisted, so the distinction survives a reload — * the chat transcript is never rehydrated from the backend, so the store is * the only source of truth for what the panel shows. */ voice?: boolean; /** * WHICH live voice session produced this turn. * * ★ Not the same thing as `sessionId` above, which is the BACKEND session and * is shared with the typed thread. This one identifies a single * microphone-on-to-End, so the transcript can say where one spoken exchange * stopped and the next began. * * `voice: true` alone could never do that: it is a boolean, so a second voice * session was indistinguishable from the first and the transcript silently ran * them together. Two comments claimed the transcript was scoped to "this * session" while it was in fact showing the conversation's entire spoken * history. * * ★ OPTIONAL, and it has to stay optional. Every spoken turn recorded before * this field existed has `voice: true` and no id, and those conversations are * in users' localStorage right now. Anything that REQUIRED an id would erase * their history — so an absent id means "some earlier session", which is * exactly what it is, and never "not a voice turn". */ voiceSessionId?: string; /** * A write this turn asked permission for, read from the refusal the sandbox * returned rather than from the model's description of it. * * ★ Never contains the confirmation token. The token is the capability — a * write goes through if it is echoed back — so it must not be persisted to * localStorage or rendered. See `utils/pendingWrite`. */ pendingWrite?: PendingWrite; [key: string]: unknown; } export interface Message { id: string; role: "user" | "assistant" | "system"; content: string; timestamp: string; tokens?: { input: number; output: number; }; metadata?: AssistantMessageMetadata; } export interface Conversation { id: string; title: string; mode: AssistantMode; /** * EVERY turn, both surfaces — typed and spoken. * * ★ `readonly` on purpose, and it is load-bearing rather than hygiene. Voice * and chat share this one array; what separates them is `metadata.voice` plus * a filter at every reader, so the failure mode is a new reader that renders * the array as-is and silently spills spoken content into the typed thread. * Nothing about that is a type error, and it stays green in CI. * * Being readonly makes it one: components take `Message[]`, so handing them * this field directly no longer compiles. The way through is `chatTurns()` or * `voiceTurnsOnly()` (see `utils/voiceTurnVisibility`), both of which return a * fresh mutable array — which means picking a surface is now a step you cannot * skip by accident. * * Counting is still fine: `.length` answers "is this conversation used at * all", which is a question about the conversation and not about either * surface. */ messages: readonly Message[]; createdAt: string; updatedAt: string; /** Backend session ID for this conversation */ backendSessionId?: string | null; } export interface AssistantSandboxRuntime { sandboxId: string; status: AssistantSandboxStatus; } /** * Which step an in-flight turn is on, so the bubble can say what is happening. * * ★★ TRANSIENT BY CONSTRUCTION. It lives on `AssistantConversationProcessing`, * which the store never persists. This copy used to be WRITTEN into the * assistant message's `content` (BOFF-7277), and every consumer of content — * the transcript, the history preview, Copy, Export, the preview, speech — then * had to guess whether it was holding an answer or our own loading copy. A * reload mid-turn froze it there as a permanent "answer". * * ★ Status alone cannot carry this: `provisioning` spans three different * environment steps, and `connecting` is only ever shown as "Preparing * session...". The names are `STAGE_COPY`'s keys, which `stageCopy.ts` enforces. */ export type AssistantTurnStage = "reusingEnvironment" | "lookingForEnvironment" | "startingEnvironment" | "preparingSession" | "connecting" | "thinking"; export interface AssistantConversationProcessing { status: AssistantConversationStatus; messageId: string | null; error?: string | null; updatedAt: string; /** * The step an in-flight turn is on. Absent when the runtime has not said yet * (the synchronous `provisioning` write before any await) and on every settled * status. */ stage?: AssistantTurnStage; /** * Epoch ms the current `stage` began: the rotation origin while `thinking`. * * ★ Stored, not taken from a component clock. The panel is unmounted when the * host closes or minimises it, and a mount-relative clock would restart the * phrases from the top on every reopen of a turn that was still running. */ stageStartedAt?: number; } export interface AssistantModeConfig { id: AssistantMode; label: string; description: string; suggestions: readonly string[]; /** Only offered to a global Owner/Admin (see useAssistantApiAccess). */ requiresAdmin?: boolean; /** Executes mutations on live data → triggers the confirm-before-mutate guard. */ writeCapable?: boolean; /** * The mode's turns produce downloadable files: after each turn the panel lists * the session's artifacts and renders them as tiles. * * Default: `true` for the built-in `vibe-plugins`, `false` for every other * mode. Resolved by `resolveAssistantModeCapabilities`. */ producesArtifacts?: boolean; /** * How long (ms) the panel polls one turn before closing it with the * incomplete-turn note. Must be finite, positive and at most * `MAX_ASSISTANT_STREAM_BUDGET_MS`; anything else is ignored and the default * applies. * * Default: 600_000 for the built-in `vibe-plugins`, 180_000 otherwise. */ streamBudgetMs?: number; /** * The turn keeps running server-side after the client stops waiting, so an * unfinished turn says "still building — re-open this conversation" * (`assistant.turnStillBuilding`) instead of "send it again" * (`assistant.turnIncomplete`). * * Default: the value of `producesArtifacts` when that is set; otherwise `true` * only for the built-in `vibe-plugins`. */ continuesInBackground?: boolean; /** * Show this mode's narration for the WHOLE run, joined oldest first, rather * than only the newest sandbox message (BC1, BOFF-7334). * * The agent grows text parts inside one message, so the answer grows while it * stays in that message — and the instant it opens the next one, everything it * narrated before vanishes from the bubble on the following poll. A mode whose * turns build things (and whose narration carries the links to what it built) * needs the whole account; a conversational mode does not. * * ★★ This changes what is STORED, not merely what is displayed: the completed * turn persists this content as its answer, so Copy, Export and read-aloud all * carry the running account too. Set it only where the build log IS the answer. * * Default: `false` for every mode, built-in or product-declared. */ accumulatesNarration?: boolean; /** * How many characters of ONE attached file's text this mode sends inline * (BOFF-7334). For a mode that builds from what it is handed — BigConsole's * Build designs a data sink from a CSV. * * Default: `MAX_INLINE_CHARS_PER_FILE` (8,000). A positive number; anything * else is ignored, and anything above `MAX_CONFIGURABLE_INLINE_CHARS` * (200,000) is held at it. Set alone, the turn total rises to match it. * Resolved by `resolveAssistantInlineLimits`. * * ★ A ceiling, not a promise: however large, a turn still has to fit the * gateway's 256 KB body, and text that would not fit is cut — documents first, * then text files — with the cut said in the prompt. */ maxInlineCharsPerFile?: number; /** * How many characters of attached text, across every file in one turn, this * mode sends inline. Default: `MAX_INLINE_CHARS_TOTAL` (20,000), or the * per-file limit if that is larger; bounded like `maxInlineCharsPerFile`. */ maxInlineCharsTotal?: number; } //# sourceMappingURL=conversation.d.ts.map