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>
249 lines
8.2 KiB
TypeScript
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 });
|