import type { BlitConnectionSnapshot, BlitClientList, BlitSearchResult, BlitSession, BlitTransport, ConnectionId, CopyRangeResult, SessionId, TerminalPalette } from "./types"; import { type SurfaceDragItem, type SurfaceAxisEvent, type SurfaceTouchPoint } from "./protocol"; import { AudioPlayer } from "./AudioPlayer"; import { type BlitExtensionRecord, type BlitExtensionStatus } from "./extension"; import { type ChannelHandle, type ChannelNamesWatch, type ChannelOpenOptions } from "./channel"; import { SurfaceStore } from "./SurfaceStore"; import { TerminalStore, type BlitWasmModule } from "./TerminalStore"; import { type FsFileIndex, type FsReadRecord, type FsGrepResult, type FsGrepOptions, type FsSyncHandle, type FsSyncOptions } from "./fs"; import { type GitDiscoverOptions, type GitFoundRepo, type GitOpenOptions, type GitRepoHandle } from "./git"; import { type LspHandle, type LspOpenOptions } from "./lsp"; import { type KvFetchResult, type KvPutOptions, type KvWatchHandle, type KvWatchOptions } from "./kv"; import { DesktopStore } from "./desktop"; import { MediaStore, MprisStore } from "./media"; export declare const SEARCH_SOURCE_TITLE = 0; export declare const SEARCH_SOURCE_VISIBLE = 1; export declare const SEARCH_SOURCE_SCROLLBACK = 2; export declare const SEARCH_MATCH_TITLE: number; export declare const SEARCH_MATCH_VISIBLE: number; export declare const SEARCH_MATCH_SCROLLBACK: number; export interface CreateBlitConnectionOptions { id: ConnectionId; transport: BlitTransport; wasm: BlitWasmModule | Promise; autoConnect?: boolean; logger?: import("./BlitWorkspace").BlitLogger; } export interface CreateSessionOptions { rows: number; cols: number; tag?: string; /** Run this through the server's login shell. Mutually exclusive with * {@link argv}. */ command?: string; /** Exec this argv directly — no login shell, so no rc files and no shell * syntax. Rejected unless the server advertised `FEATURE_CREATE_EXEC`, * because an older one would quietly start a plain shell instead. */ argv?: readonly string[]; cwdFromSessionId?: SessionId; /** Working directory for the new session. Interpreted on the target server. */ cwd?: string; /** Environment overrides for the child, applied on top of everything the * server derives. Rejected unless the server advertised * `FEATURE_CREATE_EXEC`, because an older one would drop them silently. */ env?: Readonly>; /** Stop the terminal server-side after this many milliseconds, armed at * creation so it survives this client dying. Rejected unless the server * advertised `FEATURE_PTY_DEADLINE`. */ deadlineMs?: number; /** Whether this connection should receive terminal frame updates * immediately. Defaults to true. Setting false needs * `FEATURE_CREATE_NO_SUBSCRIBE`. */ subscribe?: boolean; } export interface AwaitSessionExitOptions { /** Reject if the session has not exited or closed within this many * milliseconds. Omit to wait without a client-side deadline. */ timeoutMs?: number; } type ResizeSessionOptions = { sessionId: SessionId; rows: number; cols: number; }; /** A fixed encode size one view wants for a surface, in pixels. */ export interface SurfaceTarget { width: number; height: number; } export declare class BlitConnection { readonly id: ConnectionId; readonly transport: BlitTransport; private readonly store; readonly surfaceStore: SurfaceStore; readonly audioPlayer: AudioPlayer; readonly desktopStore: DesktopStore; readonly mediaStore: MediaStore; readonly mprisStore: MprisStore; private readonly listeners; private readonly scrollAnchorListeners; private readonly sessionsById; private readonly currentSessionIdByPtyId; private readonly pendingCreates; private readonly pendingCloses; private readonly pendingSearches; private readonly pendingReads; private readonly pendingClientLists; private readonly pendingClientKicks; private readonly clientCatalogSubscribers; private clientCatalogWatchNonce; /** Nonce of the most recent `CLIENT_UNWATCH`. A successful unwatch draws no * reply, so without holding the nonce back it is free for reuse while a * refusal of that unwatch is still in flight — and the refusal would then * settle whichever request had since taken the nonce. */ private retiredWatchNonce; private lastClientCatalog; /** Unanswered `C2S_FS_SYNC`s by nonce; `pendingFsSyncsByKey` indexes the * same entries so wire-identical opens coalesce while in flight. */ private readonly pendingFsSyncs; private readonly pendingFsSyncsByKey; /** Live syncs by server `sync_id`; `fsSyncsByKey` indexes the same * shares for coalescing until their last consumer stops. */ private readonly fsSyncs; private readonly fsSyncsByKey; private readonly pendingFsFetches; private readonly pendingFsSearches; private readonly pendingFsIndexes; private readonly pendingFsReads; private readonly pendingFsGreps; private readonly pendingCwds; private cwdNonceCounter; /** Latest server-pushed cwd per session (`S2C_TERM_CWD_EVENT`); cleared * on reset/HELLO — pushes do not survive a server session change. */ private readonly termCwds; private readonly termCwdListeners; private readonly pendingFsWrites; /** Unanswered `C2S_FS_UPLOAD_BEGIN`s by nonce. */ private readonly pendingFsUploadBegins; /** Live chunked uploads by server `upload_id`; `ack` is driven by each * `S2C_FS_UPLOAD_CHUNK` reply. */ private readonly pendingFsUploads; /** Unanswered `C2S_FS_UPLOAD_FINISH`es by nonce. */ private readonly pendingFsUploadFinishes; private readonly pendingKvOpens; private readonly kvWatches; private readonly pendingKvPuts; private readonly pendingKvFetches; private readonly pendingGitOpens; private readonly gitRepos; /** Connection-wide blob cache: oid-addressed content is immutable * (docs/design/git.md "GIT_BLOB"), so entries outlive repo handles and * reconnects. Promises coalesce concurrent fetches of one oid; Map * order is the LRU order for the byte budget. */ private readonly gitBlobCache; private gitBlobCacheBytes; /** Byte budget for {@link gitBlobCache}; tests shrink it to exercise * eviction without moving real megabytes. */ private gitBlobCacheBudget; private readonly pendingGitRequests; /** Live log subscriptions keyed by client-assigned `log_id`. */ private readonly gitLogSubs; private readonly pendingLspOpens; private readonly lspAttachments; private readonly pendingChannelOpens; private readonly channels; /** Live channel-name watches, by the id that carries them. A NAMES packet * for an id that is not here belongs to a watch already stopped — the * server's answer and the client's UNWATCH can cross. */ private readonly channelNameWatches; /** Client-created channel ids must be even; the server owns the odd ones. */ private nextChannelId; private readonly pendingExtensionLists; private readonly pendingExtensionStatuses; private readonly pendingExtensionPuts; private readonly pendingLspRequests; private sessionCounter; private nonceCounter; private clientControlNonceCounter; private searchCounter; private fsNonceCounter; private gitLogIdCounter; private features; /** Mounted direct-touch canvases sharing this transport. The server sees * one virtual touchscreen capability while this count is non-zero. */ private surfaceTouchUsers; private disposed; /** Per-session, per-view size registry for computing minimum resize. */ private viewSizes; private viewIdCounter; private hasReceivedList; private retryCount; private generation; private lastError; /** Clipboard authority learned from the compositor. `null` means the * browser may have acquired a newer clipboard while this page was not * authoritative, so the next paste must import it before pressing V. */ private waylandClipboardOwned; /** Text mirrored from the current Wayland owner. Unlike the host * clipboard mirror, this remains usable when browser clipboard writes are * permission-gated. Null means it has not arrived or is not text. */ private waylandClipboardText; private pendingClipboardList; /** Last custom cursor artwork per surface, so identical repeats keep the * object URL the browser is already painting from. */ private lastCursorArtwork; private pendingClipboardGets; private clipboardChangeTarget; private clipboardChangeHandler; private clipboardMirrorToken; private pendingClipboardMirrors; /** Default video bandwidth for new surface subscriptions (0 = server default). */ defaultSurfaceBandwidth: number; /** Default encoder speed for new surface subscriptions (0 = server default). */ defaultSurfaceSpeed: number; /** User-selected ceiling applied after each view's own cadence request. * Zero leaves surface cadence tied to the display rate. */ private surfaceMaxFpsCap; /** Default audio bitrate in kbps for audio subscriptions (0 = server default). */ defaultAudioBitrateKbps: number; /** When false, surface subscribe messages are suppressed (ref-counts * still tracked so re-enabling restores subscriptions). */ surfaceStreamingEnabled: boolean; /** Page visibility is an effective streaming gate, separate from the * user's persistent video preference above. */ private pageVisible; private pageVisibilityHandler; private pingTimer; private readonly pingIntervalMs; private clockPingNonce; private pendingClockPings; /** * Reusable accumulator for `S2C_FRAGMENT` messages. TCP preserves order * and the server only splits one bulk message at a time, so fragments of * different messages never interleave. Incoming transport views are * borrowed; copy them into this buffer synchronously, then reuse its * capacity after dispatch instead of allocating once per fragment. * Audio frames and other small messages bypass this buffer. */ private fragmentBuffer; private fragmentBytes; private fragmentCount; private readonly surfaceAckMessages; private snapshot; private sessions; private _publicSessions; private _publicSessionsDirty; private _logger; private readonly sendDesktopMedia; private setDesktopMediaTransportStatus; constructor({ id, transport, wasm, autoConnect, logger, }: CreateBlitConnectionOptions); subscribe: (listener: () => void) => (() => void); private get publicSessions(); private invalidatePublicSessions; getSnapshot: () => BlitConnectionSnapshot; connect(): void; reconnect(): void; close(): void; dispose(): void; setVisibleSessionIds(sessionIds: Iterable): void; getSession(sessionId: SessionId): BlitSession | null; /** Wait until one session exits or closes without a check/subscribe race. * * Subscription happens before the state recheck, so an exit delivered * during listener setup is observed either by the listener or by the * recheck. The resolved session carries its final state and exit status. */ awaitSessionExit(sessionId: SessionId, options?: AwaitSessionExitOptions): Promise; getDebugStats(sessionId: SessionId | null): ReturnType & { surfaces: ReturnType; audioBuffer: AudioPlayer["bufferStats"] & { fastPath: string; }; }; createSession(options: CreateSessionOptions): Promise; copyRange(sessionId: SessionId, startTail: number, startCol: number, endTail: number, endCol: number): Promise; supportsCopyRange(): boolean; /** List other connections to this server and their active subscriptions. */ listClients(): Promise; /** * Subscribe to the live catalog of other server connections. Multiple * consumers share one wire subscription. The returned function disposes * this consumer and unwatches the server after the final consumer leaves. */ subscribeClients(listener: (catalog: BlitClientList) => void, onError?: (error: Error) => void): () => void; /** Disconnect another connection to this server. */ kickClient(clientId: bigint, reason?: string): Promise; /** Whether to ask the catalog where each connection came from. An older * server answers the flag with `INVALID` instead of a catalog, so this is * the difference between a missing column and a broken pane. */ private get wantsClientOrigin(); private clientControlAvailabilityError; private nextClientControlNonce; private startClientCatalogWatch; private stopClientCatalogWatch; private resetClientControl; closeSession(sessionId: SessionId): Promise; restartSession(sessionId: SessionId): void; /** * Signals a terminal. Reaches the child's process group by default, which * is what a "kill this terminal" affordance means and what the kernel does * for a real `^C`. `leaderOnly` addresses the session leader alone; it is * dropped against a server without {@link FEATURE_KILL_MODE}, which is * leader-only regardless. */ killSession(sessionId: SessionId, signal?: number, leaderOnly?: boolean): void; focusSession(sessionId: SessionId | null): void; sendInput(sessionId: SessionId, data: Uint8Array): void; resizeSession(sessionId: SessionId, rows: number, cols: number): void; clearSessionSize(sessionId: SessionId): void; clearSessionSizes(sessionIds: Iterable): void; resizeSessions(entries: Iterable): void; scrollSession(sessionId: SessionId, offset: number): void; /** * Move a scrolled view by `lines` rather than to a position. * * A gesture is relative — a notch, a page, a drag — and the absolute * offset it works out to only means what the user intended for as long as * the live bottom it counts from stays put. Under a chatty app the server * moves that bottom, and re-anchors this client, while the request is in * flight; the stale absolute then lands short by however many lines * scrolled in between. * * `offset` is where the caller believes the move lands, used verbatim * against a server too old to know the relative form. */ scrollSessionBy(sessionId: SessionId, offset: number, lines: number): void; sendMouse(sessionId: SessionId, type: number, button: number, col: number, row: number): void; search(query: string): Promise; /** * Mirror a server-side directory tree (docs/fs-watch.md). Resolves once * the server accepts the sync; the handle's `live` map fills as the * staged snapshot streams in and `onSync` fires when it is coherent. * Updates are applied and acknowledged automatically. * * No callback of this open runs before the opener holds its handle: the * accept echo and the first snapshot can arrive in one transport chunk, * and a chunk's frames are dispatched in one synchronous loop, so the * callbacks of a not-yet-handed-off consumer are held and released on a * task of their own (`dispatchFs`). Only the mirror and the wire acks * advance synchronously. * * Wire-identical opens (same path and options) share one server sync: * each caller gets its own handle and callbacks, and the wire stop goes * out when the last handle stops. A caller joining an established sync * has its snapshot replayed from the mirror (`onReset`/`onRecord`/ * `onSync`) on that same later task, so per-record consumers stay * coherent. */ syncFs(path: string, options?: FsSyncOptions): Promise; /** Attach a consumer to an established share, replaying the snapshot it * missed. Like a fresh open, its callbacks are held until the caller * holds the handle (`dispatchFs`); the replay runs on that same task and * re-reads the mirror, so it subsumes — and therefore drops — whatever * the wire delivered in between. The replay is always a valid restage of * everything that preceded it. */ private joinFsShare; /** Deliver one consumer callback, honoring the open contract: nothing * fires before that consumer's opener holds its handle. The opener's * promise continuation and the frames of one transport chunk are not * interleaved — a chunk is dispatched in a single synchronous loop * (webtransport / mux / unix), so an `FS_UPDATE` riding the same chunk * as the `FS_SYNCED` that resolves the open is handled first. Held * callbacks keep their order and are released together. */ private dispatchFs; /** Release a consumer's held callbacks, in order. Callbacks appended * while draining (a callback that stops the handle) stay in order; the * gate opens only once the queue has run dry. */ private releaseHeldFs; /** Drop the held callbacks a mirror restage reproduces, keeping the close * notifications. Used when the replay that supersedes them is about to * run, and when the consumer is going away. */ private dropHeldFsRecords; private makeFsSyncHandle; /** Detach one consumer; the last one releases the wire sync. */ private releaseFsConsumer; private nextFsNonce; private fsFetch; /** Resolve a session's live working directory (server reads the pty's cwd). * Resolves "" when the session/pty is gone or the cwd can't be read. */ sessionCwd(sessionId: SessionId): Promise; /** Subscribe to server-pushed cwd changes (`S2C_TERM_CWD_EVENT`, * docs/protocol.md): the server watches OSC 7 reports, so consumers * can suppress `sessionCwd` polling while pushes flow. Returns an * unsubscribe function. */ onTermCwd(listener: (sessionId: SessionId, cwd: string) => void): () => void; /** The most recent server-pushed cwd for a session, or null when none * has arrived since the session (or server connection) was * established. The `sessionCwd()` poll is independent of pushes. */ lastPushedCwd(sessionId: SessionId): string | null; /** Fuzzy file search under `root`; resolves with up to `limit` root-relative * paths, best match first. No sync — a one-shot server-side walk. */ searchFiles(root: string, query: string, limit?: number): Promise; /** Fetch the candidate file list under `root` for client-side fuzzy * search (docs/design/fs-search.md): root-relative paths, sorted, * gitignore-filtered. `truncated` means a budget clipped the list, so * callers should keep `searchFiles` for this root. Servers predating * `FS_INDEX` never answer — the promise just stays pending until the * connection resets, so callers should race it against a fallback. */ indexFiles(root: string): Promise; /** * Read whole files, or resolve which of several paths exists * (docs/design/fs-read.md). No sync session and nothing watched. * * Paths come in groups, and a group is one question: with `FS_READ_FIRST` * each group is answered by its own first readable path, one record per group * in group order. With `FS_READ_NO_CONTENT` the records name the file without * carrying it, which is how a caller that only wants to know *where* something * is pays a stat rather than a read. * * Every record carries its own `FS_FILE_*`, so a missing or oversized path is * an answer about that path rather than a failure of the batch. */ readFiles(groups: readonly (readonly string[])[], options?: { flags?: number; maxBytes?: number; }): Promise; /** Content search under `root` (docs/design/fs-grep.md). Resolves with * hits grouped by file — tracked files first, then gitignored ones, * which are ranked rather than excluded. No sync: a one-shot * server-side walk. * * Rejects on a non-OK status, carrying the server's own message where * it has one — an uncompilable regex reports the engine's wording, * which is the useful thing to show someone mid-typing. Like * `indexFiles`, a server predating `FS_GREP` drops the opcode and never * answers, so the in-flight cap keeps a repeating caller bounded. */ grep(root: string, query: string, opts?: FsGrepOptions): Promise; private kvGuard; /** CAS put: `ifHash` → compare-and-swap, `create` → create-exclusive, * neither → unconditional. Conflicts reject with {@link FsConflictError} * whose `hash` is the current value hash (rebase and retry). */ kvPut(key: string, value: Uint8Array, options?: KvPutOptions): Promise<{ hash: bigint; mtimeNs: bigint; }>; /** Delete a key: `ifHash` → delete-iff-unchanged, absent → unconditional * (idempotent on a missing key). */ kvDelete(key: string, options?: { ifHash?: bigint; }): Promise; /** Fetch one value; null when the key is absent. */ kvFetch(key: string): Promise; /** Subscribe to a literal byte prefix (empty = whole store). The handle's * mirror fills from the snapshot and tracks live changes; updates are * acknowledged automatically. Subscriptions do not survive re-establish — * `onClosed` fires and the caller re-`watchKv`s (the fs-family rule). */ watchKv(prefix: string, options?: KvWatchOptions): Promise; /** Reject pending kv requests and close watches (disconnect or * re-establish; nothing kv survives either). */ private resetKv; private fsWrite; /** * Chunked upload (the `FS_UPLOAD_*` family): BEGIN, a pipelined run of * CHUNKs acked cumulatively, FINISH. `data` may be a `Blob`, read slice * by slice so the whole file is never in memory at once. */ private fsUpload; private fsOp; /** * Open a repository on the server (docs/git.md). Resolves once the * server accepts; state snapshots (when watching) apply to the handle's * mirror and acknowledge automatically. */ openRepo(path: string, options?: GitOpenOptions): Promise; /** * Repositories under `path` (docs/design/git.md `GIT_DISCOVER`): the * answer to "what is checked out here" in one call, instead of a ladder * of candidate paths probed with an `FS_SYNC` per level. * * It hangs off the connection rather than a repo handle because it * allocates no repo id — an enumeration, not an open — so it cannot * exhaust the per-connection repo budget. * * A capped walk says where it stopped, and this follows that cursor to * the end by default: the caller asked what is under a path, not for one * page of it. `onPage` sees each page as it lands, for a caller that * wants to render progressively; `maxPages` bounds a walk over a tree * that is being written to underneath it. */ discoverRepos(path: string, options?: GitDiscoverOptions): Promise; /** One nonce-correlated git request; resolves with the raw response. */ private gitRequest; /** A nonce-correlated git request that is not scoped to an open repo — * `GIT_DISCOVER` is the only one, since it enumerates repositories * rather than using one and allocates no repo id. */ private gitCall; /** Serve one oid's bytes from the connection-wide cache, coalescing * concurrent fetches; a hit refreshes LRU recency. */ private cachedGitBlob; /** Drop least-recently-used settled blobs until back under budget. */ private evictGitBlobs; private makeGitRepoHandle; /** Start a live log subscription; the server pushes pages we auto-ack. */ private watchGitLog; /** Drop every log subscription bound to a repo (close or teardown). */ private closeGitLogSubs; /** Tear down all git repo state (reconnect or dispose). */ private resetGitRepos; /** * Attach to the workspace containing a path (docs/design/lsp.md). * Resolves once the server accepts; state snapshots and diagnostics * (when subscribed) apply to the handle's mirrors and acknowledge * automatically. */ openLsp(path: string, options?: LspOpenOptions): Promise; /** One nonce-correlated LSP query; resolves with the raw response. */ private lspRequest; private makeLspHandle; /** Tear down all LSP attachment state (reconnect or dispose). */ private resetLspAttachments; /** * Connect to a named native channel served by an extension or another * client (`docs/design/extensions.md`). * * The promise settles on the server's `OPENED`: it resolves with a handle * whose `send` is credit-checked, or rejects with the refusal detail. * Incoming DATA is acknowledged for the caller — a channel that is never * acknowledged stops receiving once its window fills. */ connectChannel(name: string, options?: ChannelOpenOptions): Promise; /** * Follow which of `names` currently have a listener. * * This is how a client asks "is this extension serving right now" without * connecting: the promise settles on the server's first answer, and `onNames` * is called every time that answer changes afterwards — an extension being * installed, restarted, disabled or removed all reach the watcher, because * all of them end with a listener claimed or released. * * A connect-and-close probe answers the same question once. It cannot say * when the answer stops being true, so anything holding a probe's result * over time is showing the viewer a stale server. */ watchChannelNames(names: readonly string[], onNames: (present: ReadonlySet) => void): Promise; /** The handle for a watch already registered. `present` is the live set the * message handler keeps up to date, so it still reads correctly after the * watch is stopped or the transport is lost. */ private makeChannelNamesWatch; private makeChannelHandle; /** Route one `CHANNEL` packet to its pending open or connected channel. */ private handleChannelMessage; private extensionGuard; /** What this server has installed, running or not. */ listExtensions(): Promise; /** Cancel, restart, enable, disable, or remove one extension. * Protocol refusals reject with the server's detail. */ controlExtension(extensionId: bigint, action: number): Promise; /** * Install or replace an extension from module bytes. * * The digest is the module's identity, so it goes first and the bytes only * if the server answers that it lacks that object — a server that already * has it transfers nothing. The server re-hashes what it receives and * refuses a mismatch, which is what lets this client name a digest it has * no way to compute. */ installExtension(request: { hash: Uint8Array; name: string; module: () => Promise; args?: readonly string[]; restart?: number; /** Replace this definition rather than create one (CAS-checked). */ expectedExtensionId?: bigint; expectedDefinitionRevision?: bigint; }): Promise; private extensionRun; /** Upload one module, lock-step: one chunk, one reply, repeat. */ private uploadExtensionModule; /** Route one extension packet to whoever is waiting on its nonce. */ private handleExtensionMessage; /** Tear down all extension bookkeeping (reconnect or dispose). */ private resetExtensions; /** Tear down all native-channel state (reconnect or dispose). */ private resetChannels; /** Tear down all fs sync state (reconnect or dispose). */ private resetFsSyncs; private ptyId; /** * The pty a `fromSessionId` open resolves its root from (fs/git/lsp * FROM_PTY, docs/ide.md Decision 3), or `undefined` for a plain * path-based open. * * A caller that asked to follow a terminal must never silently get a * path-based open instead: those opens carry a *pty-relative* path (the * dock's follow-terminal root is `""`), so dropping FROM_PTY rebases them * onto the server's own cwd — and for git, `open("")` is refused outright, * which left the commit log loading forever. SessionIds are minted fresh on * every re-establish and superseded ones are pruned, so an unresolvable id * means the caller is holding one from a past generation: fail loudly. */ private srcPtyForOpen; getTerminal(sessionId: SessionId): import("@blit-sh/browser").Terminal | null; /** Allocate a unique view ID for multi-pane size tracking. */ allocViewId(): string; /** Register/update a view's size for a session. Sends the minimum to the server. */ setViewSize(sessionId: SessionId, viewId: string, rows: number, cols: number, isActive?: () => boolean): void; /** Unregister a view. Recalculates and sends the new minimum. */ removeView(sessionId: SessionId, viewId: string): void; /** * Forget every mounted terminal view owned by this browser client. * * This is primarily an HMR recovery boundary. A hot update can replace the * UI tree without running every old surface cleanup, leaving an orphaned * (often smaller) view in {@link viewSizes}. Since the session uses the * minimum of all registered views, that stale entry pins the terminal to a * small grid until the page is refreshed. Clear the server constraints in * one batch before the replacement tree registers its live views again. */ resetViewSizes(): void; private sendMinSize; metricsGeneration(): number; bumpMetricsGeneration(): number; getRetainCount(sessionId: SessionId): number; retain(sessionId: SessionId): void; release(sessionId: SessionId): void; addDirtyListener(sessionId: SessionId, listener: () => void): () => void; /** * Called when the server re-anchors this session's scrolled-back view * (`S2C_SCROLL_OFFSET`) with the offset it now holds for us. * * Separate from the dirty listener because it isn't a frame: the content * hasn't been decided yet, only where in the scrollback it will be read * from. */ addScrollAnchorListener(sessionId: SessionId, listener: (offset: number) => void): () => void; getSharedRenderer(): { renderer: import("./gl-renderer").GlRenderer; canvas: HTMLCanvasElement; } | null; setCellSize(pw: number, ph: number): void; getCellSize(): { pw: number; ph: number; }; wasmMemory(): WebAssembly.Memory | null; noteFrameRendered(): void; invalidateAtlas(): void; setFontFamily(f: string): void; setFontSize(s: number): void; setPalette(p: TerminalPalette): void; sendSurfaceInput(surfaceId: number, keycode: number, pressed: boolean, timeMs?: number): void; sendSurfaceText(surfaceId: number, text: string): void; /** Report the composition in progress; empty text withdraws it. */ sendSurfacePreedit(surfaceId: number, text: string, cursorUtf16: number): void; sendSurfacePointer(surfaceId: number, type: number, button: number, x: number, y: number, timeMs?: number): void; get supportsSurfaceTouch(): boolean; get supportsSurfaceTextInput(): boolean; /** Keep the compositor's virtual touchscreen capability present while at * least one mounted view is configured for direct touch. */ acquireSurfaceTouch(): void; releaseSurfaceTouch(): void; private syncSurfaceTouchCapability; sendSurfaceTouch(surfaceId: number, phase: number, contacts?: readonly SurfaceTouchPoint[], timeMs?: number): void; sendSurfaceDragEnter(surfaceId: number, x: number, y: number, mimes: string[], items?: string[]): void; sendSurfaceDragMotion(surfaceId: number, x: number, y: number): void; sendSurfaceDragLeave(surfaceId: number): void; sendSurfaceDragDrop(surfaceId: number, x: number, y: number, items: SurfaceDragItem[]): void; sendSurfaceDragCancel(): void; sendSurfaceAxis(surfaceId: number, axis: number, valueX100: number): void; sendSurfaceAxis2(surfaceId: number, ev: SurfaceAxisEvent): void; /** Returns whether the message went out. Unlike the input sends, a * dropped resize is not water under the bridge: the caller records it * as the size the server knows about, so silently swallowing one on a * disconnected transport leaves the surface stuck at the previous size * until the pane happens to change size again. */ private sendSurfaceResize; private surfaceViewSizes; /** Offer one view's size for a surface. Returns whether the server now * knows the effective size (sent, or already current) — false means the * transport was down and the caller should retry. */ offerSurfaceViewSize(surfaceId: number, viewId: string, width: number, height: number, scale120?: number): boolean; /** Withdraw one view's size. Re-derives the request across the surviving * views — the departing one may have been the constraint — or sends the * unset when it was the last sized view. */ withdrawSurfaceViewSize(surfaceId: number, viewId: string): void; /** Forget the size the server was told for this surface. * * A wire UNSUBSCRIBE makes the server drop this client's view size * (`C2S_SURFACE_UNSUBSCRIBE` clears `surface_view_sizes`), so what we * last sent is no longer what it knows. Without this the offer that * follows the next subscribe dedups against a size only the previous * subscription ever carried, and the client silently drops out of the * server's size mediation: it is subscribed, it has a pane, and it has * no say in how big the surface is. With another viewer watching, the * surface then sits at *their* size forever — the pane's box never * changes again, so no new offer is ever made. Hiding the tab and * coming back is enough to trigger it. * * Mirrors what {@link resetSurfaceSubsForReconnect} does for a new * server session. */ private forgetSentSurfaceViewSize; /** Re-offer the effective view size after a subscribe goes on the wire. * No-op unless {@link forgetSentSurfaceViewSize} (or a reconnect) cleared * `lastSent`, so a steady-state resubscribe costs nothing. */ private resendSurfaceViewSize; /** * The one size this connection can ask for on behalf of every live view * of a surface. * * The wire carries one size per (client, surface), so several views have * to be reconciled into a single request — and the answer is the same * one the server computes across clients: the largest logical box that * fits in every view, at the highest density any of them will display. * Taking the most recent offer instead made the surface follow whichever * pane was measured last, so the other one was left with a surface too * big for it — the same defect the server's mediation exists to prevent, * reintroduced one layer up. * * The constraining view's own physical extent is returned verbatim when * it is already at the winning scale: the logical round trip does not * return what it was given (at 2× an odd extent comes back a pixel * *larger*, 1001 → 501 → 1002), and a surface a pixel bigger than the * pane that asked for it shows up as a letterbox bar. `Session:: * mediated_size_for_surface` takes the same escape hatch for the same * reason. */ private effectiveSurfaceViewSize; private flushSurfaceViewSize; sendSurfaceFocus(surfaceId: number): void; sendSurfaceClose(surfaceId: number): void; /** Active surface subscriptions keyed by surface id. */ private surfaceSubs; private surfaceViewIdCounter; /** Allocate a token identifying one view's subscription to a surface. * Mirrors {@link allocViewId} for PTYs. * * Prefixed with the connection id, because a view mints its token once and * keeps it for the life of its mount — including across * `BlitSurfaceCanvas.setConnectionId`, which re-points a canvas at another * server without re-minting. A bare per-connection counter is only unique * within the connection that issued it, so a canvas carrying `s3` from * connection A onto connection B collided with B's own `s3`: two views * sharing one `SurfaceSub.views` entry, where the last writer decides the * encode size and cadence for both. A live pane then inherited a dock * card's `{512x256, 15fps}` request and could not take it back * (`serverSubscribe` early-returns once `_subscribedSurface` is set), and * the card's unsubscribe deleted the pane's registration outright, taking * the pane's stream with it. * * The token never reaches the wire — it only keys {@link surfaceSubs}' * `views` and {@link surfaceViewSizes}' `views` — so the prefix costs * nothing but the string. Session ids are built the same way. */ allocSurfaceViewId(): string; /** * The subscribe this connection should be asking for, given every live * view of the surface. * * A view that wants the surface unscaled wins outright: it needs pixels * nobody can reconstruct from a downscale, and the thumbnail sharing the * stream can always shrink what it is given. Otherwise the largest * request wins, for the same reason in miniature — downscaling further is * cheap, upscaling is lossy. */ private effectiveSurfaceTarget; /** Highest cadence any live view needs, constrained by the user's global * ceiling. Zero (uncapped) wins between views, just as an unscaled target * wins the resolution derivation above, but it does not bypass that cap. */ private effectiveSurfaceMaxFps; /** Grace window before a refCount=0 subscription's wire UNSUB fires. * Chosen to comfortably cover typical Solid re-render ordering where * the old mount's `onCleanup` fires before the new mount's * `onMount`, but keeps dropped-stream latency tight if the user * really did stop watching. */ private static readonly SUB_UNSUB_GRACE_MS; /** Cancel any pending deferred unsubscribe timers and reset * `lastSent` so the next refresh fires a wire subscribe. * Called on reconnect / S2C_HELLO: the refCounts (one per live * mount) are authoritative and must survive a reconnect — wiping * the map would leave the existing mounts with no way to reclaim * their subscriptions (`refreshSurfaceSubscribe` would no-op). */ private resetSurfaceSubsForReconnect; /** Called from `dispose()` — the connection is going away permanently. * Drop everything, including ref-counts. */ private clearSurfaceSubs; /** Retire all connection-owned state for an id the server destroyed. * Do not send UNSUBSCRIBE: destruction is already authoritative, and a * delayed unsubscribe could otherwise hit a new surface reusing the id. */ private retireDestroyedSurface; private maybeSendSurfaceSubscribe; /** * Subscribe one view to a surface's frames. A single wire subscription * exists per (connection, surface); additional views share it and the * effective request is derived across them. * * `viewId` comes from {@link allocSurfaceViewId} and identifies this view * for the lifetime of its mount. `target` asks the server to encode a * fixed-size downscale for this client instead of sizing the surface to * fit — pass null to watch the surface at its mediated size. `maxFps` * limits this view's cadence; zero uses the display rate. */ sendSurfaceSubscribe(surfaceId: number, viewId: string, target?: SurfaceTarget | null, maxFps?: number): void; /** Update the fixed encode size one view wants, re-deriving the wire * request. No-op for a view that is not subscribed. */ setSurfaceViewTarget(surfaceId: number, viewId: string, target: SurfaceTarget | null, maxFps?: number): void; /** Resend the wire subscribe without bumping the ref-count. Used * after reconnect, where the server lost its subscription table but * the client still has all its mounts active — bumping the count * would leak references. */ refreshSurfaceSubscribe(surfaceId: number): void; /** Re-subscribe active subs after the codec probe resolves so the * server can switch to the optimal encoder for this client's * capabilities. Subs subscribed with codec_support=0 ("accept * anything") before the probe completed get updated. */ private resubscribeWithCodecSupport; /** Pending probation per withdrawn bit set: the timer that will re-offer * it, and how long this round's ban is (kept after the timer fires so * the next demotion of the same bits doubles it). */ private codecProbation; /** First ban after a demotion, and the point past which bans stop * expiring. A demotion is three decode failures deep — enough to get * the session unstuck, not enough to conclude the codec is broken — so * the first one is short. Each repeat doubles it, and a codec that * fails again after every reprieve has earned the permanent one. */ private static readonly CODEC_PROBATION_MS; private static readonly CODEC_PROBATION_MAX_MS; /** Arrange for demoted codec bits to be offered again after a ban. */ private scheduleCodecProbation; sendSurfaceUnsubscribe(surfaceId: number, viewId: string): void; /** Set per-surface bandwidth and speed overrides and re-send the * subscribe. The server treats a second SURFACE_SUBSCRIBE at the * same sid as a codec/bandwidth/speed update. No-op when the sid is * unknown. */ sendSurfaceResubscribe(surfaceId: number, bandwidth: number, speed: number): void; /** Apply a user-selected cadence ceiling to every active surface stream. * Existing per-view limits (such as the 15 fps thumbnail limit) still win * when they are lower. Zero disables the global ceiling. */ setSurfaceMaxFpsCap(maxFps: number): void; /** * Enable or disable surface video streaming. When disabled, per-sub * state is preserved but no subscribe messages are sent. Re-enabling * sends subscribe for every active sub. */ setSurfaceStreamingEnabled(enabled: boolean): void; /** Suspend video while the document is hidden without overwriting the * user's persistent streaming preference. The live view registry stays * intact, so becoming visible restores exactly the previous streams. */ private setPageVisible; /** * Subscribe to audio frames, optionally specifying bitrate. * Can be called repeatedly to adjust bitrate without unsubscribing first. * `bitrateKbps`: 0 = server default, otherwise desired Opus bitrate in kbps. */ /** * Ask the transport to deliver audio straight to the decoder, if it can. * * Feature-detected rather than typed: only the worker-backed mux channel * offers this, and every other transport — WebRTC shares, unix sockets, * tests — must keep working untouched. Done once, when audio is first * subscribed, since before that there is nothing to carry. */ private attachAudioFastPath; /** Which route audio takes, and when it is the slow one, why. Reported in * the debug panel: the distinction picks the next thing to fix. */ private audioFastPath; sendAudioSubscribe(bitrateKbps?: number): void; sendAudioUnsubscribe(): void; sendClipboard(mimeType: string, data: Uint8Array): void; /** True when Ctrl/Cmd+V must preserve the compositor's current selection. * A Wayland-owned selection can carry several representations and is * spliced directly from its source to the destination client. */ usesWaylandClipboard(): boolean; /** Read text from the compositor's live Wayland selection. * * A Wayland copy is mirrored to `navigator.clipboard` as a convenience, * but browsers can reject that background write. Terminal paste therefore * consumes the in-connection mirror directly and asks the compositor when * this client connected after the copy or the eager mirror did not arrive. */ readWaylandClipboardText(): Promise; private requestClipboardMimes; private requestClipboardContent; private rejectPendingClipboardRequests; /** The host clipboard may be newer (clipboardchange, window/tab loss, or a * real DOM copy/cut). The next paste probes the browser clipboard and * publishes it to the compositor before forwarding V. */ noteBrowserClipboardMayHaveChanged(): void; /** Expect the text-only clipboardchange caused by mirroring a Wayland * selection into the host clipboard. That write must not make us forget * the richer, client-owned Wayland source. */ private expectMirroredClipboardChange; private finishMirroredClipboardChange; private clearPendingClipboardMirrors; private consumeMirroredClipboardChange; /** * Take ownership of PRIMARY, the selection a middle click pastes. * * The compositor serves these bytes itself, so send them only when the * user actually asks to paste — see {@link buildPrimaryMessage}. */ sendPrimary(mimeType: string, data: Uint8Array): void; /** * Advertise client capabilities to the server: which video codecs this * browser decodes, so the server picks a compatible encoder, and the * largest frame it decodes, so the server knows whether it may composite * a surface above the H.264 ceiling for us. Called automatically when * the connection is established and codec probing completes. */ sendClientFeatures(codecSupport: number): void; /** * Re-advertise the current codec mask and re-subscribe every surface, so a * viewer's codec preference takes effect on streams that are already * running. A no-op before the probe resolves — its own advertisement * carries the preference then. */ refreshCodecSupport(): void; isReady(): boolean; onReady(listener: () => void): () => void; private emit; private noteServerResponsive; /** Drop any half-received fragment sequence (reconnect or dispose) so it * cannot bleed into the first fragmented message on the next connection. */ private resetFragmentReassembly; /** A malformed sequence desynchronizes the stream, so discard both the * allocation and the transport instead of attempting to continue. */ private abortFragmentReassembly; /** Grow the fragment accumulator geometrically. In steady state the first * large frame sizes it and later frames require no reassembly allocation. */ private ensureFragmentCapacity; private handleMessage; private handleStatusChange; private sendClockPing; private parseListMessage; private handleListMessage; /** * Remove closed sessions from `sessionsById`, `sessions`, and `viewSizes` * when a live session already exists for the same ptyId. This prevents * stale closed sessions from accumulating across reconnect cycles. */ private pruneSupersededSessions; private handleSearchResults; private syncTitleFromTerminal; private upsertLiveSession; private updateSession; private markSessionClosed; private firstLiveSessionId; private rejectPendingCreates; private rejectPendingSearches; private rejectPendingReads; private resolveAllPendingCloses; } export {}; //# sourceMappingURL=BlitConnection.d.ts.map