/** * What a session does, as events a browser can render. * * The existing /run route shelled out to the CLI and forwarded stdout line by line, so the web side * received text that had already been formatted for a terminal — no way to tell a tool call from a * sentence, no timings, no plan, nothing to lay out. Everything needed was already there: the * engine takes twenty-one callbacks, and each one is an event with a shape. * * Typed as a discriminated union so the client can switch on `t` and the compiler checks both ends. * Every event carries `at` because when things happened is most of what anyone wants to know * afterwards, and `seq` so a client that reconnects can say what it already has. */ export type WebEvent = /** A session was created or adopted. */ { t: 'session'; at: number; seq: number; sessionId: string; cwd: string; provider: string; model: string; } /** The user's message, echoed back so every client sees the same transcript. */ | { t: 'user'; at: number; seq: number; text: string; images?: number; /** * What an `@` in the message actually attached. * * Reported rather than assumed, because attaching a file spends tokens and a spend nobody can * see is a spend nobody can question. The transcript shows the sentence as typed; this says * what was added beneath it, and what could not be. */ refs?: Array<{ ref: string; kind: string; lines: number | null; note?: string; }>; } /** * Take back the partial reply on screen: the turn is being attempted again. * * A retry re-asks from the same messages, so the model starts its reply over. Without this the * half sentence the failed attempt managed stays where it is and the new one is appended to it. */ | { t: 'reply-reset'; at: number; seq: number; } /** A piece of the reply as it streams. */ | { t: 'chunk'; at: number; seq: number; text: string; } /** Reasoning, where the model exposes it. */ | { t: 'thinking'; at: number; seq: number; text: string; chars: number; } /** A tool is about to run. */ | { t: 'tool'; at: number; seq: number; callId: string; name: string; detail: string; /** The full arguments, for the card's body. `detail` is the one-line version. */ args?: string; } /** Output from a tool while it runs — a dev server's log, a test's progress. */ | { t: 'tool-output'; at: number; seq: number; name: string; line: string; } /** * A write as it arrives, so it can be watched rather than announced. * * The fields rather than the JSON fragment they came from: extracted where it can be tested, and * one less place for a half-finished escape sequence to reach the page. */ | { t: 'tool-args'; at: number; seq: number; name: string; path: string; content: string; } /** A tool finished. */ | { t: 'tool-done'; at: number; seq: number; callId: string; name: string; ok: boolean; ms: number; detail: string; /** * The call's arguments, as the start event carried them. * * The panel decides what a call was about by parsing these, so a finished write without them * had no path: the header said "write_file" instead of the filename, and the diff that should * replace the streamed text never opened. */ args?: string; } /** The plan, whenever it changes. */ | { t: 'plan'; at: number; seq: number; steps: Array<{ text: string; state: string; }>; } /** * Sub-agents, whenever their progress changes. * * A fan-out was a silent gap in the transcript: the parent said it was spawning agents and then * nothing until they finished. */ | { t: 'agents'; at: number; seq: number; agents: Array<{ index: number; task: string; status: string; tokens: number; turns: number; startedAt: number; endedAt?: number; branch?: string; }>; } /** Token counts, as the provider reports them. */ | { t: 'tokens'; at: number; seq: number; prompt: number; completion: number; total: number; cost: number; } /** Permission is needed before a tool runs. Answered by POST /api/answer. */ | { t: 'ask'; at: number; seq: number; askId: string; tool: string; args: string; waiting: number; } /** A permission request was resolved, by anyone. */ | { t: 'ask-done'; at: number; seq: number; askId: string; allowed: boolean; } /** Something worth telling the user that is not the model talking. */ | { t: 'notice'; at: number; seq: number; level: 'info' | 'warn'; text: string; } /** * A turn changed files on disk. * * Emitted so a badge can appear without the client polling for it, and so the Changes view knows * a new turn's worth of diff exists. */ | { t: 'changed'; at: number; seq: number; turn: number; files: number; added: number; removed: number; } /** The turn ended. */ | { t: 'turn-end'; at: number; seq: number; ms: number; completed: boolean; } /** The run failed. */ | { t: 'error'; at: number; seq: number; text: string; } /** Sent periodically so a proxy does not close an idle stream. */ | { t: 'ping'; at: number; seq: number; }; /** * A bounded log of everything one session has emitted, and the clients watching it. * * Kept so a browser that arrives late — or reloads, which is the common case — sees the whole * conversation rather than whatever happens next. Bounded because a session that runs for an hour * should not be able to fill memory with its own history; the transcript on disk is the permanent * record and this is only what the live view needs. */ export declare const MAX_BUFFERED = 5000; /** * One event minus the fields the bus fills in. * * Written to distribute over the union. A plain `Omit` collapses a union to * the keys every member shares — which is `t` alone — so every field of every event was rejected * as unknown. The conditional forces TypeScript to apply the Omit to each member separately. */ export type Emitted = T extends unknown ? Omit & { at?: number; } : never; export declare class EventBus { private readonly log; private readonly clients; private seq; /** Stamps and records an event, then hands it to every listener. */ emit(event: Emitted): WebEvent; /** * How much history there is to replay. * * Asked before opening a session, because the live view is built entirely from this log — so a * session whose events belong to a previous server process replays nothing and shows a blank * transcript, however many turns it has actually taken. Knowing the log is empty is what lets the * client fall back to the copy on disk instead. */ get length(): number; /** Everything since `after`, for a client that is catching up. */ since(after?: number): WebEvent[]; subscribe(fn: (e: WebEvent) => void): () => void; get watchers(): number; get lastSeq(): number; } /** * A backlog with its streamed text run together. * * Replaying a session is not the same act as watching one. A reply arrives live as sixty or a * hundred `chunk` events because that is how a model speaks, and each one is a DOM write and a * re-render of the reply so far — which is the right cost to pay while it is happening and a * ridiculous one to pay again for a conversation that finished an hour ago. Measured: a 39-turn * session held 2,458 events, and opening it made 2,419 DOM writes and took 3.5 seconds, while the * same conversation read from its file took under 300ms. * * So consecutive pieces of the same thing are run together before they are sent. The merged event * keeps the LAST sequence number of the run, because that number is what a client sends back as * Last-Event-ID — keeping the first would make it ask for text it already has on every reconnect. */ export declare function coalesce(events: readonly WebEvent[]): WebEvent[]; //# sourceMappingURL=events.d.ts.map