/** * core/hybrid.ts — HybridSessionManager: unified API + visual session management. * * Maintains a flat, creation-ordered list of sessions that can be either: * - "api": headless Claude subprocess (managed by APIBackend) * - "visual": iTerm2 terminal tab (managed by the transport's iTerm2 adapter) * * Transports (Whazaa, Telex) use this to provide a single /s list and /N switch * that seamlessly mixes both session types. Message delivery routing is based on * the active session's kind — the transport decides how to deliver. */ import type { APIBackend } from "../backend/api.js"; export type SessionKind = "api" | "visual"; export interface HybridSession { /** Hybrid session ID: "h-1", "h-2", ... */ id: string; /** Human-readable name */ name: string; /** Working directory */ cwd: string; /** Session kind */ kind: SessionKind; /** Creation timestamp */ createdAt: number; /** Backend-specific ID: "api-N" for API sessions, iTerm2 UUID for visual */ backendSessionId: string; } export declare class HybridSessionManager { readonly apiBackend: APIBackend; private readonly sessions; private _activeIndex; private nextNum; private discover?; /** True when the last attempt to look could not complete, so the list is last-known. */ private lastDiscoveryFailed; /** When discovery last ran, so a burst of readers costs one enumeration. */ private lastSyncAt; private coalesceMs; constructor(apiBackend: APIBackend); /** * Where live sessions come from. Injected rather than imported so this stays * a registry rather than growing a dependency on a particular terminal. */ setDiscovery(fn: () => Array<{ id: string; name: string; paiName?: string | null; tabTitle?: string | null; }>): void; /** * Change how long one enumeration is reused for. * * Exists so tests can assert the two properties separately: that a burst * costs one enumeration, and that a fresh look reflects what changed. With a * fixed window the second is only observable by waiting, which makes the * suite slow and the failure mode ambiguous. */ setCoalesceWindow(ms: number): void; /** Create a new headless (API) session. Delegates to APIBackend. */ createApiSession(name: string, cwd: string): HybridSession; /** Register a visual (iTerm2) session. The transport creates the tab and passes the ID. */ registerVisualSession(name: string, cwd: string, itermSessionId: string): HybridSession; /** Switch to session by 1-based display index. Returns the session or undefined. */ switchToIndex(index: number): HybridSession | undefined; /** Remove session by 1-based display index. For API sessions, also ends in APIBackend. */ removeByIndex(index: number): HybridSession | undefined; /** Clear the active session's conversation (API only — no-op for visual). */ clearActiveSession(): void; /** The currently active session, or undefined if none. */ get activeSession(): HybridSession | undefined; /** * Remove visual sessions whose iTerm2 tab no longer exists. * Call with the set of live iTerm2 session IDs from snapshotAllSessions(). */ pruneDeadVisualSessions(liveIds: Set): number; /** Update the name of a session identified by its backend ID. */ updateName(backendSessionId: string, newName: string): void; /** * All sessions in creation order, after checking what is actually running. * * For anything a person asked for. Discovery is an AppleScript round trip, so * this is the wrong call on a path that runs per message — use * `knownSessions()` there. */ listSessions(): HybridSession[]; /** * What the registry already knows, without going out to look. * * The read for hot paths: resolving a name for a push, checking that an id * still exists on the way through. Those run per message, and an enumeration * per message would put a terminal round trip in the delivery loop. Callers * that need certainty rather than speed should fall back to `listSessions()` * when this misses — cheap in the common case, correct in the rare one. */ knownSessions(): HybridSession[]; /** * Bring the registry in line with what is actually running. * * Both read paths call this, which is the point: a caller cannot forget to * populate, because populating is not the caller's job. Registration by the * gateway and by the command that spawns a tab still works and is still * useful — it names a session at the moment it is created, before discovery * would have anything to go on — this only guarantees the floor. * * TWO FAILURES THAT ARE NOT THE SAME FACT. Discovery throwing means we could * not look; discovery returning nothing means we looked and there is nothing. * Folding them together would trade a false "none" for a false "current" — * a list that reads as freshly enumerated while being last-known, which is * the same lie as a Funnel reporting itself on while refusing connections. * * So: a throw keeps the known list AND is surfaced to the reader. An honest * empty prunes, because a registry that keeps dead rows after looking is * showing sessions that do not exist. */ private syncFromLive; /** Get session by 1-based display index. */ getByIndex(index: number): HybridSession | undefined; /** * Format the unified session list for display. * * Discovers first, so "No sessions." can only ever mean there are none — * not that nobody had populated the registry yet. Before this, the answer * depended on whether a PAILot client had happened to connect since the last * daemon restart: ask over a channel before that and you were told the * machine was empty, in the same words it would use if it were. */ formatSessionList(): string; /** * Get a status string for the active session. * Returns formatted text for API sessions, null for visual sessions * (signals transport to take a screenshot instead). */ formatActiveStatus(): string | null; } /** Singleton hybrid manager (set at startup by transport's watch()). */ export declare let hybridManager: HybridSessionManager | null; export declare function setHybridManager(m: HybridSessionManager | null): void; //# sourceMappingURL=hybrid.d.ts.map