TypeScript · 9074 bytes Raw Blame History
1 // Typed wrappers around Tauri's `invoke` and `listen`. The frontend
2 // should import from here, never from `@tauri-apps/api/core` directly —
3 // that keeps the IPC surface area in one place and typed against the
4 // ts-rs generated contracts.
5
6 import { invoke } from "@tauri-apps/api/core";
7 import { listen, type UnlistenFn } from "@tauri-apps/api/event";
8 import type {
9 Message,
10 PermissionMode,
11 Project,
12 SessionDetail,
13 SessionSource,
14 SessionSummary,
15 } from "./types";
16
17 export async function listProjects(): Promise<Project[]> {
18 return invoke<Project[]>("list_projects");
19 }
20
21 /** @deprecated Projects returned by `listProjects` now carry their
22 * sessions embedded. Only kept for debugging / future callers. */
23 export async function listSessions(
24 encodedDir: string,
25 ): Promise<SessionSummary[]> {
26 return invoke<SessionSummary[]>("list_sessions", { encodedDir });
27 }
28
29 /** Read the full viewer payload for a session.
30 *
31 * For disk sessions: `encodedDir` is the session's own `projectId`
32 * field (the physical source dir), **not** the merged logical
33 * project id from `Project.id`.
34 *
35 * For archive sessions (reconstructed from `~/.claude/history.jsonl`):
36 * `encodedDir` is the absolute project path (still `session.projectId`,
37 * since archive sessions use the project path there too). The
38 * `source` tag drives the dispatch on the Rust side. */
39 export async function readSession(
40 encodedDir: string,
41 sessionId: string,
42 source: SessionSource = "disk",
43 ): Promise<SessionDetail> {
44 return invoke<SessionDetail>("read_session", {
45 encodedDir,
46 sessionId,
47 source,
48 });
49 }
50
51 export async function rescan(): Promise<Project[]> {
52 return invoke<Project[]>("rescan");
53 }
54
55 /** Sent by the Rust watcher whenever a session file is added,
56 * modified, or removed under `~/.claude/projects/`. */
57 export type SessionChangeEvent =
58 | { kind: "added"; path: string }
59 | { kind: "modified"; path: string }
60 | { kind: "removed"; path: string };
61
62 /** Subscribe to live session changes. Returns an unlisten function. */
63 export async function onSessionsChanged(
64 cb: (ev: SessionChangeEvent) => void,
65 ): Promise<UnlistenFn> {
66 return listen<SessionChangeEvent>("sessions:changed", (e) => cb(e.payload));
67 }
68
69 // ============================================================================
70 // Chat turn commands + event bus
71 // ============================================================================
72
73 /** Mirrors the Rust `ChatEventWire` enum in commands.rs. One tagged
74 * payload per `chat:event` emission. */
75 export type ChatEvent =
76 | {
77 kind: "turn_started";
78 turnId: string;
79 resumeSessionId: string | null;
80 newSessionId: string | null;
81 }
82 | { kind: "session_bound"; turnId: string; sessionId: string }
83 | { kind: "message"; turnId: string; message: Message }
84 | { kind: "stderr"; turnId: string; line: string }
85 | { kind: "turn_completed"; turnId: string; exitCode: number }
86 | { kind: "turn_failed"; turnId: string; reason: string }
87 | { kind: "turn_cancelled"; turnId: string };
88
89 export interface StartTurnArgs {
90 /** Client-generated uuid — the frontend owns turn identity so it
91 * has something to pass to cancelTurn before start_turn returns. */
92 turnId: string;
93 /** Absolute working directory. Must exist on disk. */
94 cwd: string;
95 /** If set, resumes an existing session by id. Mutually exclusive
96 * with `newSessionId`. */
97 resumeSessionId: string | null;
98 /** If set, starts a new session with the given client-generated
99 * uuid via `--session-id`. Mutually exclusive with
100 * `resumeSessionId`. */
101 newSessionId: string | null;
102 prompt: string;
103 permissionMode?: PermissionMode;
104 }
105
106 export async function startTurn(args: StartTurnArgs): Promise<void> {
107 return invoke("start_turn", {
108 turnId: args.turnId,
109 cwd: args.cwd,
110 resumeSessionId: args.resumeSessionId,
111 newSessionId: args.newSessionId,
112 prompt: args.prompt,
113 permissionMode: args.permissionMode ?? "auto",
114 });
115 }
116
117 export async function cancelTurn(turnId: string): Promise<void> {
118 return invoke("cancel_turn", { turnId });
119 }
120
121 export async function listActiveTurns(): Promise<string[]> {
122 return invoke<string[]>("list_active_turns");
123 }
124
125 /** Subscribe to `chat:event` payloads emitted while chat turns are
126 * in flight. Returns an unlisten function. */
127 export async function onChatEvent(
128 cb: (ev: ChatEvent) => void,
129 ): Promise<UnlistenFn> {
130 return listen<ChatEvent>("chat:event", (e) => cb(e.payload));
131 }
132
133 // ============================================================================
134 // PTY commands + event bus
135 // ============================================================================
136
137 /** One live PTY as reported by `list_ptys`. Mirrors the Rust
138 * `PtyInfo` struct in commands.rs. Used by the titlebar popover
139 * and on mount to reconcile the store's `ptyIds` map. */
140 export interface PtyInfo {
141 ptyId: string;
142 sessionId: string | null;
143 cwd: string;
144 startedAt: string;
145 }
146
147 /** Stdout chunk from a live PTY, base64-encoded. Decode with
148 * `atob` + `Uint8Array.from(..., c => c.charCodeAt(0))` before
149 * passing to `xterm.write`. */
150 export interface PtyDataEvent {
151 ptyId: string;
152 base64: string;
153 }
154
155 /** Fired exactly once when a PTY subprocess exits. `exitCode` is
156 * `null` when the subprocess was killed by a signal (no clean
157 * status code available). */
158 export interface PtyExitEvent {
159 ptyId: string;
160 exitCode: number | null;
161 }
162
163 export interface SpawnPtyArgs {
164 /** Client-generated uuid — the frontend owns pty identity so the
165 * TerminalPane can pass a stable id to every subsequent call. */
166 ptyId: string;
167 /** Claudex session this PTY belongs to. `null` for detached
168 * terminals (not currently exposed in the UI). */
169 sessionId: string | null;
170 /** Absolute working directory. Must exist on disk. */
171 cwd: string;
172 /** Argv for the claude subprocess. Usually
173 * `["--resume", <id>]` or `["--session-id", <uuid>]`. */
174 args: string[];
175 cols: number;
176 rows: number;
177 }
178
179 export async function spawnPty(args: SpawnPtyArgs): Promise<void> {
180 return invoke("spawn_pty", {
181 ptyId: args.ptyId,
182 sessionId: args.sessionId,
183 cwd: args.cwd,
184 args: args.args,
185 cols: args.cols,
186 rows: args.rows,
187 });
188 }
189
190 /** Send a keystroke / paste string into the PTY master. `data` is
191 * a utf-8 string; the backend passes the bytes verbatim so escape
192 * sequences round-trip correctly. */
193 export async function writePty(ptyId: string, data: string): Promise<void> {
194 return invoke("write_pty", { ptyId, data });
195 }
196
197 /** Propagate an xterm resize down to the PTY master. Triggers
198 * `SIGWINCH` in the subprocess on unix. */
199 export async function resizePty(
200 ptyId: string,
201 cols: number,
202 rows: number,
203 ): Promise<void> {
204 return invoke("resize_pty", { ptyId, cols, rows });
205 }
206
207 /** Explicit teardown — kill the subprocess and remove its entry
208 * from the backend's `active_ptys` map. No-op if the pty id is
209 * unknown. */
210 export async function closePty(ptyId: string): Promise<void> {
211 return invoke("close_pty", { ptyId });
212 }
213
214 /** Snapshot the ring buffer and return it as base64. Called on
215 * reattach to replay recent stdout into a freshly-mounted xterm. */
216 export async function getPtyBuffer(ptyId: string): Promise<string> {
217 return invoke<string>("get_pty_buffer", { ptyId });
218 }
219
220 /** List every live PTY with enough metadata to render the
221 * titlebar popover and reconcile the store's `ptyIds` map. */
222 export async function listPtys(): Promise<PtyInfo[]> {
223 return invoke<PtyInfo[]>("list_ptys");
224 }
225
226 /** Subscribe to `pty:data` payloads. The frontend filters by its
227 * own `ptyId` inside the callback — one event channel is shared
228 * across every live PTY. Returns an unlisten function. */
229 export async function onPtyData(
230 cb: (ev: PtyDataEvent) => void,
231 ): Promise<UnlistenFn> {
232 return listen<PtyDataEvent>("pty:data", (e) => cb(e.payload));
233 }
234
235 /** Subscribe to `pty:exit` payloads. Fires once per PTY when its
236 * subprocess exits (naturally, killed via `close_pty`, or on
237 * window-destroy shutdown). Returns an unlisten function. */
238 export async function onPtyExit(
239 cb: (ev: PtyExitEvent) => void,
240 ): Promise<UnlistenFn> {
241 return listen<PtyExitEvent>("pty:exit", (e) => cb(e.payload));
242 }
243
244 // ============================================================================
245 // Frontend log bridge
246 // ============================================================================
247
248 export type LogLevel = "error" | "warn" | "info" | "debug";
249
250 /** Forward a structured log entry into Rust tracing, which writes
251 * it to the daily-rotated file at
252 * `~/Library/Logs/claudex/claudex.log.<date>`. Used by the global
253 * error handlers in `src/lib/debug.ts` so a silent React crash
254 * still leaves a trace on disk. */
255 export async function logFrontend(
256 level: LogLevel,
257 source: string,
258 message: string,
259 stack?: string,
260 ): Promise<void> {
261 try {
262 await invoke("log_frontend", { level, source, message, stack });
263 } catch {
264 // Swallow — logging must never raise its own errors.
265 }
266 }