import type { JupyterFrontEnd } from '@jupyterlab/application'; import type { MainAreaWidget } from '@jupyterlab/apputils'; import type { ServiceManager } from '@jupyterlab/services'; import type { ITerminal, ITerminalTracker } from '@jupyterlab/terminal'; import type { IDisposable } from '@lumino/disposable'; import { ISignal } from '@lumino/signaling'; import type { IAgentSessions } from '../agentSessions'; /** * The widget shape every entry in `ITerminalTracker` takes — kept here * so the registry's call sites read cleanly. */ export type TerminalWidget = MainAreaWidget; /** * Source-of-truth model for the running-terminals panel. Each running * terminal session known to the server is one entry; the registry caches * the last `widget.title.label` we observed so the agent name (or any * xterm-published title) survives the user closing the tab while the * session continues running on the backend. * * For sessions running a coding agent it also surfaces a *latest activity* * line — the freshest meaningful line of the terminal's output, read from the * open tab's xterm buffer on a poll (see {@link activityFor}) — so each row * shows what its agent is doing, not just its name. * * It also resolves *which agent is running* in each session, so the panel can * badge rows with the agent's logo. Two inputs feed that, reconciled by * {@link agentCommandFor}: * - an optimistic launch tag ({@link IAgentSessions}) written when we start * an agent ourselves — instant, but blind to the agent later exiting; and * - authoritative server-side process detection, polled here, which works * for hand-started agents too and clears once an agent exits. * * Upstream session signals: * - `serviceManager.terminals.runningChanged` is the authoritative * list of live sessions; everything not in it has been shut down * server-side and we must drop it. * - `tracker.widgetAdded` plus the per-widget `title.changed` / * `disposed` signals keep our label cache in sync with whatever * xterm/the launcher has set on the open tabs. * * `stateChanged` is emitted for every kind of update; the panel hooks it * through a `UseSignal` so a single subscription re-renders the list on * any change. */ export declare class SessionRegistry implements IDisposable { constructor(options: SessionRegistry.IOptions); /** * Emitted whenever the live session list, a cached label, or the detected * running agent changes. */ get stateChanged(): ISignal; get isDisposed(): boolean; /** * Names of all live sessions, ordered by their stable rank so the * rendered list keeps a consistent order (roughly creation order) as * new sessions are added and old ones shut down. */ sessionNames(): string[]; /** * True iff the named session is still on the server. Used by the panel * to skip a row during the brief window between the session's shutdown * and the next `runningChanged` arriving. */ has(name: string): boolean; /** * Display name for the session. The cache wins because it only * holds "real" labels (the launcher's agent name or an xterm escape * sequence — see `_cacheLabel` for the filter); reading it first * prevents the transient `Terminal {name}` an XTerm widget shows * during reconnect from flickering into the panel. The live widget * title is used when no cache exists, and the `Terminal {name}` * fallback covers sessions we have never seen a widget for (e.g. * surviving a lab reload). */ labelFor(name: string): string; /** * An identifier for the agent running in the session — its configured * command, or its canonical id when detection matched the spawned process by * id rather than by an aliased command — or `null` if none. Callers resolve * it to a logo through the plugin's `iconForCommand`, which accepts either. * * Server-side detection is authoritative whenever it reports a running * agent. A launch tag fills two gaps: the startup grace window right after * we launch an agent (so its logo shows before its process is detectable), * and any time detection is unavailable (older server, transient error). * Once the grace window has passed and detection has reported the session * as idle, the tag is ignored — that is how the badge clears when an agent * exits. */ agentCommandFor(name: string): string | null; /** * The agent command server-side detection currently reports for the * session, or `null` when it has not confirmed one (the session is idle, * not yet covered by a poll, or detection is unavailable). Unlike * {@link agentCommandFor} this never falls back to the optimistic launch * tags: callers about to *write into* a session use it, and text must only * ever be sent to a confirmed running agent — pasted into a shell prompt it * would be executed. */ detectedCommandFor(name: string): string | null; /** * The most recent meaningful line of output from the session's terminal, or * `null` when there is nothing to surface — no coding agent is running in the * session, its tab is closed (so there is no live buffer to read), or the * buffer holds only the agent's input box and chrome. Refreshed on a poll by * {@link _refreshActivity}; shown as a smaller line under the row's title. * Always `null` while the activity line is disabled (see * {@link setActivityEnabled}). */ activityFor(name: string): string | null; /** * Turn the per-row latest-activity line on or off, driven by the * `xtralab:terminals` `showAgentActivity` setting. When turned off the * activity poll stops reading terminal buffers and any cached lines are * dropped, so rows fall back to their title alone; when turned back on the * lines are repopulated on the spot. A no-op when the value is unchanged. */ setActivityEnabled(enabled: boolean): void; /** * Return the open widget for a session, if any. The panel uses this to * switch behavior between "activate existing tab" and "open a new tab * connected to the live session". */ widgetFor(name: string): TerminalWidget | null; /** * Session name of the terminal that is currently the active widget in the * shell's main area, or `null` when that widget is not a terminal (for * example a notebook or text editor is current). The panel uses this to * highlight the current terminal's row — mirroring how the file browser * surfaces the open document — and to leave every row unhighlighted while a * non-terminal tab is current. */ currentSessionName(): string | null; /** * Stable per-session rank assigned in observation order. Used to keep * the rendered list in a steady order so rows don't reshuffle as * sessions come and go. Cleaned up when the session is shut down so the * counter does not grow without bound across long sessions. */ rankFor(name: string): number; dispose(): void; private _refreshLive; private _onRunningChanged; /** * Adopt `next` as the live set and prune every per-session map for * sessions that have gone away — labels, ranks, first-seen timestamps, * detection results, and the shared launch tag. Without this the maps (and * the rank counter) would grow without bound as terminals come and go. */ private _reconcileLive; /** * Poll body: ask the server which agent runs in each terminal and update * the detection map. On failure we keep the previous results rather than * clearing them, so a transient error doesn't strip every badge. */ private _refreshDetection; /** * Poll body: re-read the buffer of each open coding-agent terminal and cache * its latest meaningful line of output. Only sessions that have a running * *agent* (not an editor) and an open tab qualify — a closed tab has no live * buffer, editors run full-screen UIs that are not "activity", and plain * shells would only echo their prompt. While a tab is reopening (its xterm * is not ready yet) any existing line is kept; once the buffer is readable * but has nothing worth showing the line is dropped, so a screen the agent * has cleared doesn't leave a frozen line behind. Cached lines for sessions * that no longer qualify are pruned. Emits only when something changed. */ private _refreshActivity; private _onTagChanged; private _onWidgetAdded; private _onShellCurrentChanged; /** * Recompute which terminal (if any) is the active main-area widget and emit * only when it changes. The terminal tracker tells us whether the shell's * current widget is one of our terminals; anything else — a notebook, an * editor, or nothing — clears the highlight. */ private _updateCurrent; private _trackWidget; private _untrackWidget; private _onTitleChanged; /** * Update the cached label for a session, but only when the new * label is "real" — non-empty and not one of the transient defaults * the XTerm widget cycles through during reconnect (`'...'` while * the websocket is opening, `'Terminal {name}'` once * `_initialConnection` fires). Without the filter, briefly * reopening a tab while an agent has yet to re-emit its xterm * title escape sequence would clobber the cached agent name with * `Terminal 1`. Live widget titles still display whatever the widget * currently holds because `labelFor` consults the widget first; * the filter only affects what survives a tab close. */ private _cacheLabel; private _onWidgetDisposed; private _terminals; private _tracker; private _agentSessions; private _detectCommands; private _isAgentCommand; private _shell; private _poll; private _activityPoll; private _labels; private _ranks; private _firstSeen; private _detected; private _activity; private _activityEnabled; private _live; private _currentName; private _nextRank; private _isDisposed; private _stateChanged; } /** * Construction options for {@link SessionRegistry}. */ export declare namespace SessionRegistry { interface IOptions { serviceManager: ServiceManager.IManager; tracker: ITerminalTracker; /** * The application shell, used to tell which widget is currently active so * the panel can highlight the terminal that is the current main-area * widget — and highlight nothing when that widget is not a terminal (a * notebook, an editor, …). Optional: without it, or on a shell that cannot * report `currentChanged`, the highlight stays off. */ shell?: JupyterFrontEnd.IShell | null; /** * Shared launch-tag registry. When present, its records seed each row's * agent badge until server-side detection takes over. */ agentSessions?: IAgentSessions | null; /** * Returns the names the server should look for when detecting running * agents — each agent's command together with its canonical id, so a * command pointed at an alias (e.g. `ccm` running `claude`) is still * matched by the process it spawns. Read on every poll so it tracks the * live agent list. */ detectCommands?: () => string[]; /** * Whether a detected command (or id) belongs to a coding agent rather than * an editor. Only agent sessions get a latest-activity line — editors * (Neovim/Vim) are badged too, but run full-screen UIs whose buffer is not * meaningfully "activity". Defaults to treating every detected command as an * agent. */ isAgentCommand?: (command: string) => boolean; } }