import { invoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; export type PaneId = number; /** What to spawn into a fresh PTY. Mirrors the Rust `SpawnSpec` enum. */ export type SpawnSpec = | { kind: "wsl"; distro?: string; cwd?: string } | { kind: "powershell" } | { kind: "ssh"; host: string; user?: string; port?: number; identityFile?: string; jumpHost?: string; extraArgs?: string[]; /** Backend uses this to look up a saved password from keyring at * spawn time. Never echoed back to the frontend. */ hostId?: string; }; /** One saved SSH host. Mirrors the Rust `SshHost` struct (plus the * `hasPassword` flag that the backend sets when listing). */ export interface SshHost { id: string; label: string; hostname: string; user?: string; port?: number; identityFile?: string; jumpHost?: string; extraArgs?: string[]; /** True iff a credential is stored under this host's id in the system * keyring. Set by the backend on `list_ssh_hosts`; the field is * ignored on `save_ssh_hosts` (use the password commands below). */ hasPassword?: boolean; } export const listDistros = (): Promise => invoke("list_distros"); export const spawnPane = (args: { spec: SpawnSpec; cols: number; rows: number; }): Promise => invoke("spawn_pane", args); export const writeToPane = (id: PaneId, dataB64: string): Promise => invoke("write_to_pane", { id, dataB64 }); export const resizePane = (id: PaneId, cols: number, rows: number): Promise => invoke("resize_pane", { id, cols, rows }); export const killPane = (id: PaneId): Promise => invoke("kill_pane", { id }); /** Increment the "do not kill" transfer refcount for a pane. Source window * calls this BEFORE removing the leaf from its tree so the unmount-driven * kill_pane on the source becomes a no-op until the target window's * XtermPane has claimed it. */ export const markPaneTransferring = (id: PaneId): Promise => invoke("mark_pane_transferring", { id }); /** Decrement the transfer refcount. Target window's XtermPane calls this * after subscribing to pane://{id}/data and replaying the ring snapshot. */ export const claimPane = (id: PaneId): Promise => invoke("claim_pane", { id }); /** Snapshot of the per-pane scrollback ring as base64. Target window's * XtermPane writes it into xterm.js before attaching the live data * listener so a transferred pane doesn't open blank. */ export const getPaneRing = (id: PaneId): Promise => invoke("get_pane_ring", { id }); // ---- multi-window pane transfer ------------------------------------------- export interface PendingInit { leafJson: string; paneId: PaneId; workspaceName: string; } /** Open a new window and stash the pending-init payload keyed by the new * window's label. Returns the new label. */ export const createPaneWindow = (payload: PendingInit): Promise => invoke("create_pane_window", { payload }); /** Read and remove the pending-init for the current window. Null when there * is no pending payload (main window startup, or this call already * consumed it). */ export const takePendingWindowInit = ( label: string, ): Promise => invoke("take_pending_window_init", { label }); /** Push this window's workspaces snapshot to the backend aggregator. The * backend debounces and writes the merged envelope to workspace.json. */ export const pushWindowWorkspaces = ( label: string, workspacesJson: string, ): Promise => invoke("push_window_workspaces", { label, workspacesJson }); export const onPaneData = ( id: PaneId, cb: (b64: string) => void, ): Promise => listen<{ b64: string }>(`pane://${id}/data`, (e) => cb(e.payload.b64)); export const onPaneExit = ( id: PaneId, cb: () => void, ): Promise => listen(`pane://${id}/exit`, () => cb()); // ---- workspace persistence ------------------------------------------------- export const saveWorkspace = (json: string): Promise => invoke("save_workspace", { json }); export const loadWorkspace = (): Promise => invoke("load_workspace"); // ---- SSH hosts ------------------------------------------------------------- export const listSshHosts = (): Promise => invoke("list_ssh_hosts"); export const saveSshHosts = (hosts: SshHost[]): Promise => invoke("save_ssh_hosts", { hosts }); /** Store / replace the saved password for this host id. Plaintext is * IPC'd to the Rust side (in-process, no disk hop) and immediately * written to Windows Credential Manager (DPAPI). */ export const setHostPassword = (hostId: string, password: string): Promise => invoke("set_host_password", { hostId, password }); export const deleteHostPassword = (hostId: string): Promise => invoke("delete_host_password", { hostId }); export const hasHostPassword = (hostId: string): Promise => invoke("has_host_password", { hostId }); // ---- MCP server ----------------------------------------------------------- export interface McpStatus { running: boolean; url: string | null; token: string | null; } /** Shape of the cached mirror we push to the backend on every workspace * change. Mirrors src-tauri/src/mcp.rs `McpMirror`. */ export interface McpMirror { layoutJson: string; /** Only includes leaves with mcpAllow === true. */ leaves: Record; hosts: McpMirroredHost[]; } export interface McpMirroredLeaf { paneId: number | null; label?: string; shellKind: "wsl" | "powershell" | "ssh"; distro?: string; sshHostId?: string; broadcast: boolean; active: boolean; } export interface McpMirroredHost { id: string; label: string; hostname: string; user?: string; port?: number; hasPassword: boolean; } export const mcpStart = (): Promise => invoke("mcp_start"); export const mcpStop = (): Promise => invoke("mcp_stop"); export const mcpStatus = (): Promise => invoke("mcp_status"); export const mcpRegenerateToken = (): Promise => invoke("mcp_regenerate_token"); export const mcpUpdateState = (mirror: McpMirror): Promise => invoke("mcp_update_state", { mirror }); // ---- MCP audit log (events) --------------------------------------------- export interface McpAuditEntry { tsMs: number; tool: string; argsSummary: string; // already truncated to 80 chars by backend result: | { kind: "ok" } | { kind: "denied"; reason: string; hard: boolean } | { kind: "failed"; msg: string }; durationMs: number; } export interface McpActionRequest { requestId: string; tool: string; args: unknown; needsConfirm: boolean; reason: string | null; } // ---- MCP policy --------------------------------------------------------- export interface McpPolicy { version: number; permissions: { deny: string[]; ask: string[]; allow: string[]; }; /** SSH-specific capability switches; mirrors Rust SshSafeguards. All * default to false on first load. */ sshSafeguards: { allowOpenSsh: boolean; autoAllowSpawnedSsh: boolean; allowAddHost: boolean; }; } export const mcpPolicyLoad = (): Promise => invoke("mcp_policy_load"); export const mcpPolicySave = (policy: McpPolicy): Promise => invoke("mcp_policy_save", { policy }); /** Compiled-in hard-deny rule labels (the patterns the user CANNOT * override). Loaded once at PolicyTab mount; backend is the SoT. */ export const mcpHardDenyLabels = (): Promise => invoke("mcp_hard_deny_labels"); /** Subscribe to MCP action requests from the backend. Each request is a * tool call the frontend must handle (mutate state) and reply to via * {@link mcpActionReply}. */ export const onMcpRequest = ( cb: (req: McpActionRequest) => void, ): Promise => listen("mcp://request", (e) => cb(e.payload)); /** Reply to an MCP action request. The Rust side expects an externally- * tagged Result — `{ Ok: }` on success, `{ Err: }` on * failure or user rejection. */ export const mcpActionReply = ( requestId: string, result: { Ok: unknown } | { Err: string }, ): Promise => invoke("mcp_action_reply", { requestId, result });