tiletopia/src/ipc.ts
megaproxy f51033a142 Idle filter: suppress when watched process (claude) is running in distro
Probes wsl.exe -d <distro> -- pgrep -x claude before flagging a WSL pane
idle, with a 3s per-distro cache on the Rust side. If claude is running
anywhere in the distro, all panes in that distro stay out of the idle set
(per-pane granularity is out of scope — PIDs aren't observable from
Windows). PowerShell + SSH panes skip the probe and keep the legacy
always-notify behaviour.
2026-05-26 17:33:10 +01:00

210 lines
6.8 KiB
TypeScript

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<string[]> => invoke("list_distros");
/** Ask the backend whether any built-in "watched" process (currently just
* `claude`) is running in the given WSL distro. Cached per-distro for ~3s
* on the Rust side. Fail-safe: probe failures resolve to `true` so the
* caller suppresses the idle indicator. Only meaningful for WSL panes —
* PowerShell + SSH should skip this and fall back to always-notify. */
export const isWatchProcessRunning = (distro: string): Promise<boolean> =>
invoke("is_watch_process_running", { distro });
export const spawnPane = (args: {
spec: SpawnSpec;
cols: number;
rows: number;
}): Promise<PaneId> => invoke("spawn_pane", args);
export const writeToPane = (id: PaneId, dataB64: string): Promise<void> =>
invoke("write_to_pane", { id, dataB64 });
export const resizePane = (id: PaneId, cols: number, rows: number): Promise<void> =>
invoke("resize_pane", { id, cols, rows });
export const killPane = (id: PaneId): Promise<void> => invoke("kill_pane", { id });
export const onPaneData = (
id: PaneId,
cb: (b64: string) => void,
): Promise<UnlistenFn> =>
listen<{ b64: string }>(`pane://${id}/data`, (e) => cb(e.payload.b64));
export const onPaneExit = (
id: PaneId,
cb: () => void,
): Promise<UnlistenFn> => listen(`pane://${id}/exit`, () => cb());
// ---- workspace persistence -------------------------------------------------
export const saveWorkspace = (json: string): Promise<void> =>
invoke("save_workspace", { json });
export const loadWorkspace = (): Promise<string | null> =>
invoke("load_workspace");
// ---- SSH hosts -------------------------------------------------------------
export const listSshHosts = (): Promise<SshHost[]> => invoke("list_ssh_hosts");
export const saveSshHosts = (hosts: SshHost[]): Promise<void> =>
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<void> =>
invoke("set_host_password", { hostId, password });
export const deleteHostPassword = (hostId: string): Promise<void> =>
invoke("delete_host_password", { hostId });
export const hasHostPassword = (hostId: string): Promise<boolean> =>
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<string, McpMirroredLeaf>;
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<McpStatus> => invoke("mcp_start");
export const mcpStop = (): Promise<McpStatus> => invoke("mcp_stop");
export const mcpStatus = (): Promise<McpStatus> => invoke("mcp_status");
export const mcpRegenerateToken = (): Promise<McpStatus> =>
invoke("mcp_regenerate_token");
export const mcpUpdateState = (mirror: McpMirror): Promise<void> =>
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<McpPolicy> =>
invoke("mcp_policy_load");
export const mcpPolicySave = (policy: McpPolicy): Promise<void> =>
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<string[]> =>
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<UnlistenFn> =>
listen<McpActionRequest>("mcp://request", (e) => cb(e.payload));
/** Reply to an MCP action request. The Rust side expects an externally-
* tagged Result — `{ Ok: <value> }` on success, `{ Err: <msg> }` on
* failure or user rejection. */
export const mcpActionReply = (
requestId: string,
result: { Ok: unknown } | { Err: string },
): Promise<void> => invoke("mcp_action_reply", { requestId, result });