tiletopia/src/ipc.ts
megaproxy 00a1e24ecf Shelve the per-pane context indicator (keep narrow-toolbar fix)
Reliable per-pane context tracking isn't achievable from transcripts: we
can't distinguish 'claude is live in this pane' from 'a shell sitting in
a directory that recently had a claude session' (claude renders inline,
not alt-screen; no WSL foreground-process access), and the 200k-vs-1M
window isn't recorded so % is unreliable. Removed the context indicator,
its OSC 7 cwd injection (pty.rs), the get_pane_context backend
(usage.rs), src/lib/usage.ts, the orchestration paneContext map, and the
App poll. The narrow-pane toolbar reflow (leaf--narrow/xnarrow tiers,
label shrink, close × pinned) is KEPT — it's verified and independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:47:06 +01:00

249 lines
8.2 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");
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 });
/** 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<void> =>
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<void> =>
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<string> =>
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<string> =>
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<PendingInit | null> =>
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<void> =>
invoke("push_window_workspaces", { label, workspacesJson });
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 });