import { EditorBridge } from "./bridge.js"; import { ProjectContext } from "./project.js"; import { GuardedBridge } from "./flow/guarded-bridge.js"; import { GuardRegistry } from "./flow/guard.js"; /** Key used for the session that has no project bound. */ export declare const DEFAULT_SESSION_KEY = ""; /** Name of the session that has no project bound. */ export declare const DEFAULT_SESSION_NAME = "default"; /** One editor's reported state, as `project(list_editors)` returns it. */ export interface EditorSessionInfo { name: string; projectName: string | null; projectPath: string | null; /** Port this session will connect on (lockfile value once the editor published one). */ port: number; connected: boolean; /** True for the session untargeted calls fall through to. */ active: boolean; /** Other session names resolving to the same port. Targeting is ambiguous while non-empty. */ portSharedWith?: string[]; } /** * Normalize a .uproject path or a project directory to the registry key. * Accepts either form because every caller has one or the other, and a * session addressed by directory must find the session registered by file. */ export declare function sessionKeyFor(projectPathOrDir: string): string; export declare class EditorSession { name: string; /** Resolved project root. Moves when the session's project moves. */ key: string; readonly project: ProjectContext; /** This session's own guard pipeline. Guards from one project's plugins * must not veto another project's calls, so each session has its own. */ readonly guards: GuardRegistry; /** Raw bridge: connection lifecycle, port, lockfile. */ readonly bridge: EditorBridge; /** What tools and tasks see - the guard pipeline wrapped around `bridge`. */ readonly guarded: GuardedBridge; /** Names of other sessions that resolved to the same port. */ portSharedWith: string[]; /** * Who this editor's asset locks belong to (#817). * * The lock registry lives in the bridge, which is per editor, so the holder * has to be per editor as well. One id shared across sessions would make a * lock taken in one editor read as re-entrant in another, which defeats the * point of taking it. */ readonly lockOwnerId: string; constructor(name: string, /** Resolved project root. Moves when the session's project moves. */ key: string, project: ProjectContext, /** This session's own guard pipeline. Guards from one project's plugins * must not veto another project's calls, so each session has its own. */ guards: GuardRegistry); get projectDir(): string | null; get hasProject(): boolean; info(active: boolean): EditorSessionInfo; } export interface RegisterSessionInput { /** .uproject file or the directory holding one. Omit for the project-less default session. */ projectPath?: string | null; /** Addressable handle. Defaults to the project name, de-duplicated. */ name?: string; /** Make this the session untargeted calls fall through to. */ makeActive?: boolean; } /** * The set of editors this server drives. Ordered: the first registered * session is the default target, and stays so until `use` moves it. */ export declare class SessionRegistry { private readonly guards; private readonly byKey; private activeKey; /** Fired whenever the session set changes, so the caller can re-advertise. */ onCountChanged?: (count: number) => void; /** * Builds one session's dispatch surface: its own tool graph, plugins, task * registry and guards. Installed by the server, which owns all of those. * * A session registered at runtime has none of it until this has run, and * until then it must not be dispatched to: every lookup that misses used to * fall back to the FIRST project's load, so a second editor ran the first * project's flows and plugin tasks inside itself (D1). */ prepareSession?: (session: EditorSession) => Promise; /** Held while a compound edit is mid-flight, so observers see one change. */ private suppressNotify; /** * `guards` is the pipeline the FIRST session gets, so a caller that already * built one (the server, which populates it after the task registries exist) * keeps working unchanged at one editor. Every session after the first gets * its own, because a guard declared by one project's plugins has no business * running on another project's calls. */ constructor(guards?: GuardRegistry); private guardsForNewSession; get size(): number; list(): EditorSession[]; /** The session untargeted calls fall through to. Throws only when nothing is registered. */ get active(): EditorSession; /** * Register a project as a session. Idempotent per project root: registering * a path that is already bound returns the existing session rather than a * second one competing for the same editor. */ register(input?: RegisterSessionInput): EditorSession; /** * Resolve a target to a session. An empty target is the active session, so * every existing single-editor caller keeps landing where it always did. * Accepts a session name, a project name, a .uproject path, or a project * directory - whichever the caller happens to hold. */ resolve(target?: unknown): EditorSession; /** * Re-file a session after its project moved (`project(set_project)`). * * switchProject moves the ProjectContext and the socket together; this moves * the registry entry with them, since sessions are keyed by project root. A * session named after the project it just left is renamed too, so the handle * a caller sees still describes what it addresses. * * The editor of the project being left is never touched: this detaches, it * does not stop anything. */ rekey(session: EditorSession): EditorSession; /** * Recompute which sessions share a bridge port (S2). * * register() and rekey() note this from `bridge.port` as it stands BEFORE * any lockfile has been read, and connect() can move the port afterwards - * that is the whole point of the lockfile. So the collision record computed * at registration describes ports that may no longer be the ones in use, in * both directions: a clash that has since resolved is still reported, and * one that only appeared after both editors published their real ports is * not. Anything displaying or acting on the record asks for it fresh. */ refreshSharedPorts(): void; /** * Wait for a session's dispatch surface to exist. * * A no-op when no builder is installed, which is every embedder and test * that drives the registry without the server around it. */ prepare(session: EditorSession): Promise; /** Resolve without throwing. */ find(target: string): EditorSession | undefined; /** Move the default target. Returns the newly active session. */ use(target: string): EditorSession; /** * Forget a session and close its socket. This NEVER touches the editor * process: dropping a session detaches this server from that editor and * leaves it running. Stopping an editor is editor(stop_editor), which is a * separate, explicitly targeted action. */ drop(target: string): { name: string; projectPath: string | null; }; /** Close a session's socket and remove it. Never touches the editor process. */ private forget; /** * Two sessions on one port cannot be told apart: whichever editor answers * serves both, so a call targeted at one can execute in the other. That is * the exact failure multi-editor exists to prevent, so it is recorded on * both sessions, reported by list_editors, and refused by the lifecycle * actions rather than being left to surface as a mystery later. */ private noteSharedPorts; private uniqueName; }