@ai-matrx/desktop-protocol 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -681,7 +681,7 @@ type OpSpec = {
681
681
  readonly params: z.ZodType;
682
682
  readonly result: z.ZodType;
683
683
  readonly kind: "unary" | "stream" | "resource";
684
- readonly since: "1.0" | "1.2" | "1.3" | "1.4" | "1.5";
684
+ readonly since: "1.0" | "1.2" | "1.3" | "1.4" | "1.5" | "1.6";
685
685
  readonly errors: readonly z.infer<typeof ErrorCode>[];
686
686
  };
687
687
  declare const OPS: {
@@ -2202,6 +2202,52 @@ declare const OPS: {
2202
2202
  readonly since: "1.4";
2203
2203
  readonly errors: readonly ["INTERNAL"];
2204
2204
  };
2205
+ readonly "audio.devices": {
2206
+ readonly params: z.ZodObject<{}, z.core.$strict>;
2207
+ readonly result: z.ZodObject<{
2208
+ inputs: z.ZodArray<z.ZodObject<{
2209
+ id: z.ZodString;
2210
+ name: z.ZodString;
2211
+ default: z.ZodBoolean;
2212
+ }, z.core.$strict>>;
2213
+ outputs: z.ZodArray<z.ZodObject<{
2214
+ id: z.ZodString;
2215
+ name: z.ZodString;
2216
+ default: z.ZodBoolean;
2217
+ }, z.core.$strict>>;
2218
+ }, z.core.$strict>;
2219
+ readonly kind: "unary";
2220
+ readonly since: "1.6";
2221
+ readonly errors: readonly ["PLATFORM_UNSUPPORTED", "INTERNAL"];
2222
+ };
2223
+ readonly "audio.record": {
2224
+ readonly params: z.ZodObject<{
2225
+ seconds: z.ZodNumber;
2226
+ device_id: z.ZodOptional<z.ZodString>;
2227
+ path: z.ZodOptional<z.ZodString>;
2228
+ }, z.core.$strict>;
2229
+ readonly result: z.ZodObject<{
2230
+ path: z.ZodString;
2231
+ seconds: z.ZodNumber;
2232
+ bytes: z.ZodInt;
2233
+ peak: z.ZodNumber;
2234
+ }, z.core.$strict>;
2235
+ readonly kind: "unary";
2236
+ readonly since: "1.6";
2237
+ readonly errors: readonly ["NOT_FOUND", "PERMISSION_DENIED_OS", "PLATFORM_UNSUPPORTED", "RESOURCE_BUSY", "INTERNAL"];
2238
+ };
2239
+ readonly "audio.play": {
2240
+ readonly params: z.ZodObject<{
2241
+ path: z.ZodString;
2242
+ }, z.core.$strict>;
2243
+ readonly result: z.ZodObject<{
2244
+ path: z.ZodString;
2245
+ seconds: z.ZodNumber;
2246
+ }, z.core.$strict>;
2247
+ readonly kind: "unary";
2248
+ readonly since: "1.6";
2249
+ readonly errors: readonly ["NOT_FOUND", "INVALID_PARAMS", "PLATFORM_UNSUPPORTED", "INTERNAL"];
2250
+ };
2205
2251
  readonly "coding.history.import_missing": {
2206
2252
  readonly params: z.ZodObject<{}, z.core.$strict>;
2207
2253
  readonly result: z.ZodObject<{
@@ -4948,6 +4994,42 @@ declare const NAMED_SCHEMAS: {
4948
4994
  }, z.core.$strict>>;
4949
4995
  uptime_seconds: z.ZodInt;
4950
4996
  }, z.core.$strict>;
4997
+ readonly AudioDevice: z.ZodObject<{
4998
+ id: z.ZodString;
4999
+ name: z.ZodString;
5000
+ default: z.ZodBoolean;
5001
+ }, z.core.$strict>;
5002
+ readonly AudioDevicesParams: z.ZodObject<{}, z.core.$strict>;
5003
+ readonly AudioDevicesResult: z.ZodObject<{
5004
+ inputs: z.ZodArray<z.ZodObject<{
5005
+ id: z.ZodString;
5006
+ name: z.ZodString;
5007
+ default: z.ZodBoolean;
5008
+ }, z.core.$strict>>;
5009
+ outputs: z.ZodArray<z.ZodObject<{
5010
+ id: z.ZodString;
5011
+ name: z.ZodString;
5012
+ default: z.ZodBoolean;
5013
+ }, z.core.$strict>>;
5014
+ }, z.core.$strict>;
5015
+ readonly AudioRecordParams: z.ZodObject<{
5016
+ seconds: z.ZodNumber;
5017
+ device_id: z.ZodOptional<z.ZodString>;
5018
+ path: z.ZodOptional<z.ZodString>;
5019
+ }, z.core.$strict>;
5020
+ readonly AudioRecordResult: z.ZodObject<{
5021
+ path: z.ZodString;
5022
+ seconds: z.ZodNumber;
5023
+ bytes: z.ZodInt;
5024
+ peak: z.ZodNumber;
5025
+ }, z.core.$strict>;
5026
+ readonly AudioPlayParams: z.ZodObject<{
5027
+ path: z.ZodString;
5028
+ }, z.core.$strict>;
5029
+ readonly AudioPlayResult: z.ZodObject<{
5030
+ path: z.ZodString;
5031
+ seconds: z.ZodNumber;
5032
+ }, z.core.$strict>;
4951
5033
  readonly FsWatchId: z.ZodString;
4952
5034
  readonly FsWatchStartParams: z.ZodObject<{
4953
5035
  path: z.ZodString;
@@ -681,7 +681,7 @@ type OpSpec = {
681
681
  readonly params: z.ZodType;
682
682
  readonly result: z.ZodType;
683
683
  readonly kind: "unary" | "stream" | "resource";
684
- readonly since: "1.0" | "1.2" | "1.3" | "1.4" | "1.5";
684
+ readonly since: "1.0" | "1.2" | "1.3" | "1.4" | "1.5" | "1.6";
685
685
  readonly errors: readonly z.infer<typeof ErrorCode>[];
686
686
  };
687
687
  declare const OPS: {
@@ -2202,6 +2202,52 @@ declare const OPS: {
2202
2202
  readonly since: "1.4";
2203
2203
  readonly errors: readonly ["INTERNAL"];
2204
2204
  };
2205
+ readonly "audio.devices": {
2206
+ readonly params: z.ZodObject<{}, z.core.$strict>;
2207
+ readonly result: z.ZodObject<{
2208
+ inputs: z.ZodArray<z.ZodObject<{
2209
+ id: z.ZodString;
2210
+ name: z.ZodString;
2211
+ default: z.ZodBoolean;
2212
+ }, z.core.$strict>>;
2213
+ outputs: z.ZodArray<z.ZodObject<{
2214
+ id: z.ZodString;
2215
+ name: z.ZodString;
2216
+ default: z.ZodBoolean;
2217
+ }, z.core.$strict>>;
2218
+ }, z.core.$strict>;
2219
+ readonly kind: "unary";
2220
+ readonly since: "1.6";
2221
+ readonly errors: readonly ["PLATFORM_UNSUPPORTED", "INTERNAL"];
2222
+ };
2223
+ readonly "audio.record": {
2224
+ readonly params: z.ZodObject<{
2225
+ seconds: z.ZodNumber;
2226
+ device_id: z.ZodOptional<z.ZodString>;
2227
+ path: z.ZodOptional<z.ZodString>;
2228
+ }, z.core.$strict>;
2229
+ readonly result: z.ZodObject<{
2230
+ path: z.ZodString;
2231
+ seconds: z.ZodNumber;
2232
+ bytes: z.ZodInt;
2233
+ peak: z.ZodNumber;
2234
+ }, z.core.$strict>;
2235
+ readonly kind: "unary";
2236
+ readonly since: "1.6";
2237
+ readonly errors: readonly ["NOT_FOUND", "PERMISSION_DENIED_OS", "PLATFORM_UNSUPPORTED", "RESOURCE_BUSY", "INTERNAL"];
2238
+ };
2239
+ readonly "audio.play": {
2240
+ readonly params: z.ZodObject<{
2241
+ path: z.ZodString;
2242
+ }, z.core.$strict>;
2243
+ readonly result: z.ZodObject<{
2244
+ path: z.ZodString;
2245
+ seconds: z.ZodNumber;
2246
+ }, z.core.$strict>;
2247
+ readonly kind: "unary";
2248
+ readonly since: "1.6";
2249
+ readonly errors: readonly ["NOT_FOUND", "INVALID_PARAMS", "PLATFORM_UNSUPPORTED", "INTERNAL"];
2250
+ };
2205
2251
  readonly "coding.history.import_missing": {
2206
2252
  readonly params: z.ZodObject<{}, z.core.$strict>;
2207
2253
  readonly result: z.ZodObject<{
@@ -4948,6 +4994,42 @@ declare const NAMED_SCHEMAS: {
4948
4994
  }, z.core.$strict>>;
4949
4995
  uptime_seconds: z.ZodInt;
4950
4996
  }, z.core.$strict>;
4997
+ readonly AudioDevice: z.ZodObject<{
4998
+ id: z.ZodString;
4999
+ name: z.ZodString;
5000
+ default: z.ZodBoolean;
5001
+ }, z.core.$strict>;
5002
+ readonly AudioDevicesParams: z.ZodObject<{}, z.core.$strict>;
5003
+ readonly AudioDevicesResult: z.ZodObject<{
5004
+ inputs: z.ZodArray<z.ZodObject<{
5005
+ id: z.ZodString;
5006
+ name: z.ZodString;
5007
+ default: z.ZodBoolean;
5008
+ }, z.core.$strict>>;
5009
+ outputs: z.ZodArray<z.ZodObject<{
5010
+ id: z.ZodString;
5011
+ name: z.ZodString;
5012
+ default: z.ZodBoolean;
5013
+ }, z.core.$strict>>;
5014
+ }, z.core.$strict>;
5015
+ readonly AudioRecordParams: z.ZodObject<{
5016
+ seconds: z.ZodNumber;
5017
+ device_id: z.ZodOptional<z.ZodString>;
5018
+ path: z.ZodOptional<z.ZodString>;
5019
+ }, z.core.$strict>;
5020
+ readonly AudioRecordResult: z.ZodObject<{
5021
+ path: z.ZodString;
5022
+ seconds: z.ZodNumber;
5023
+ bytes: z.ZodInt;
5024
+ peak: z.ZodNumber;
5025
+ }, z.core.$strict>;
5026
+ readonly AudioPlayParams: z.ZodObject<{
5027
+ path: z.ZodString;
5028
+ }, z.core.$strict>;
5029
+ readonly AudioPlayResult: z.ZodObject<{
5030
+ path: z.ZodString;
5031
+ seconds: z.ZodNumber;
5032
+ }, z.core.$strict>;
4951
5033
  readonly FsWatchId: z.ZodString;
4952
5034
  readonly FsWatchStartParams: z.ZodObject<{
4953
5035
  path: z.ZodString;
package/dist/react.cjs CHANGED
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var react_exports = {};
23
23
  __export(react_exports, {
24
24
  DesktopClientProvider: () => DesktopClientProvider,
25
+ useDesktopAction: () => useDesktopAction,
25
26
  useDesktopClient: () => useDesktopClient,
26
27
  useDesktopConnection: () => useDesktopConnection,
27
28
  useDesktopEvent: () => useDesktopEvent,
@@ -136,12 +137,62 @@ function useDesktopRequest(op, params, options = {}) {
136
137
  abortRef.current?.abort();
137
138
  };
138
139
  }, [enabled, run]);
140
+ const refreshMs = options.refreshMs;
141
+ (0, import_react.useEffect)(() => {
142
+ if (!enabled || !refreshMs || typeof document === "undefined") return;
143
+ let timer = null;
144
+ const start = () => {
145
+ if (timer === null) timer = setInterval(() => void run(), refreshMs);
146
+ };
147
+ const stop = () => {
148
+ if (timer !== null) clearInterval(timer);
149
+ timer = null;
150
+ };
151
+ const onVisibility = () => {
152
+ if (document.visibilityState === "visible") {
153
+ void run();
154
+ start();
155
+ } else stop();
156
+ };
157
+ if (document.visibilityState === "visible") start();
158
+ document.addEventListener("visibilitychange", onVisibility);
159
+ return () => {
160
+ stop();
161
+ document.removeEventListener("visibilitychange", onVisibility);
162
+ };
163
+ }, [enabled, refreshMs, run]);
139
164
  const status = (0, import_react.useSyncExternalStore)(c.subscribe, () => c.getState().status, () => c.getState().status);
140
165
  (0, import_react.useEffect)(() => {
141
166
  if (enabled && status === "open" && error?.retryable) void run();
142
167
  }, [status]);
143
168
  return { data, error, loading, refetch: run };
144
169
  }
170
+ function useDesktopAction(options = {}) {
171
+ const c = useDesktopClient(options.client);
172
+ const [busy, setBusy] = (0, import_react.useState)(false);
173
+ const [error, setError] = (0, import_react.useState)(void 0);
174
+ const onDoneRef = (0, import_react.useRef)(options.onDone);
175
+ onDoneRef.current = options.onDone;
176
+ const run = (0, import_react.useCallback)(
177
+ async (work) => {
178
+ setBusy(true);
179
+ setError(void 0);
180
+ try {
181
+ await work(c);
182
+ await onDoneRef.current?.();
183
+ return true;
184
+ } catch (caught) {
185
+ setError(caught instanceof DesktopProtocolError ? caught : new DesktopProtocolError("INTERNAL", caught instanceof Error ? caught.message : String(caught), { cause: caught }));
186
+ return false;
187
+ } finally {
188
+ setBusy(false);
189
+ }
190
+ },
191
+ [c]
192
+ );
193
+ const clearError = (0, import_react.useCallback)(() => setError(void 0), []);
194
+ return { run, busy, error, clearError };
195
+ }
145
196
  function useDesktopEvent(name, handler, options = {}) {
146
197
  const c = useDesktopClient(options.client);
147
198
  const handlerRef = (0, import_react.useRef)(handler);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.ts","../src/client/wake.ts","../src/error.ts"],"sourcesContent":["/**\n * `@ai-matrx/desktop-protocol/react` — hooks over the client: provider, connection state, typed\n * one-shot requests, events. The client owns every hard part (reconnect, reattach, credits); these\n * hooks only bind it to React's lifecycle.\n */\nimport { createContext, createElement, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { Context, ReactNode } from \"react\";\nimport type { DesktopClient, DesktopClientState, EventHandler, EventName, RequestOptions } from \"./client/types\";\nimport { bindDesktopWake } from \"./client/wake\";\nimport { DesktopProtocolError } from \"./error\";\nimport type { OpName, OpParams, OpResult } from \"./schema/registry\";\n\n// The context lives on globalThis: an ESM and a CJS copy of this entry in one app (Next.js\n// does this) would otherwise create two contexts and a provider the hooks cannot see.\nconst CONTEXT_SLOT = Symbol.for(\"ai-matrx.desktop-protocol.client-context\");\nfunction clientContext(): Context<DesktopClient | null> {\n const slot = globalThis as { [CONTEXT_SLOT]?: Context<DesktopClient | null> };\n return (slot[CONTEXT_SLOT] ??= createContext<DesktopClient | null>(null));\n}\n\nexport function DesktopClientProvider(props: { client: DesktopClient; children?: ReactNode }): ReactNode {\n return createElement(clientContext().Provider, { value: props.client }, props.children);\n}\n\n/** The client from the nearest provider (or the one passed in). Throws when there is neither. */\nexport function useDesktopClient(client?: DesktopClient): DesktopClient {\n const fromContext = useContext(clientContext());\n const resolved = client ?? fromContext;\n if (!resolved) throw new Error(\"useDesktopClient: wrap the tree in <DesktopClientProvider client={…}> or pass a client\");\n return resolved;\n}\n\n/** Live connection state (status, welcome, attempts, retryInMs, lastError). Connects on mount when idle. */\nexport function useDesktopConnection(client?: DesktopClient): DesktopClientState {\n const c = useDesktopClient(client);\n const state = useSyncExternalStore(c.subscribe, c.getState, c.getState);\n useEffect(() => {\n if (c.getState().status === \"idle\") c.connect();\n }, [c]);\n return state;\n}\n\nexport interface DesktopRequestState<N extends OpName> {\n data: OpResult<N> | undefined;\n error: DesktopProtocolError | undefined;\n loading: boolean;\n /** Re-run now; resolves with the fresh result. */\n refetch: () => Promise<OpResult<N> | undefined>;\n}\n\nexport interface UseDesktopRequestOptions extends Omit<RequestOptions, \"signal\"> {\n /** Default true. False = do not run until refetch() or enabled flips true. */\n enabled?: boolean;\n client?: DesktopClient;\n}\n\n/**\n * Run a unary op and keep its latest result. Re-runs when `op` or the params' JSON changes, and\n * after a reconnect if the last attempt failed with a retryable error. Aborts on unmount.\n */\nexport function useDesktopRequest<N extends OpName>(op: N, params: OpParams<N>, options: UseDesktopRequestOptions = {}): DesktopRequestState<N> {\n const c = useDesktopClient(options.client);\n const enabled = options.enabled ?? true;\n const key = JSON.stringify(params);\n const [data, setData] = useState<OpResult<N> | undefined>(undefined);\n const [error, setError] = useState<DesktopProtocolError | undefined>(undefined);\n const [loading, setLoading] = useState<boolean>(enabled);\n const paramsRef = useRef(params);\n paramsRef.current = params;\n const runRef = useRef(0);\n const abortRef = useRef<AbortController | null>(null);\n const timeoutMs = options.timeoutMs;\n\n const run = useCallback(async (): Promise<OpResult<N> | undefined> => {\n const runId = ++runRef.current;\n abortRef.current?.abort();\n const abort = new AbortController();\n abortRef.current = abort;\n setLoading(true);\n try {\n const result = await c.request(op, paramsRef.current, timeoutMs === undefined ? { signal: abort.signal } : { signal: abort.signal, timeoutMs });\n if (runId === runRef.current) {\n setData(result);\n setError(undefined);\n }\n return result;\n } catch (caught) {\n const err = caught instanceof DesktopProtocolError ? caught : new DesktopProtocolError(\"INTERNAL\", String(caught), { cause: caught });\n if (runId === runRef.current && err.code !== \"CANCELLED\") setError(err);\n return undefined;\n } finally {\n if (runId === runRef.current) setLoading(false);\n }\n // `key` stands for the params' content: a new object with equal JSON is not a new request.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [c, op, key, timeoutMs]);\n\n useEffect(() => {\n if (!enabled) {\n setLoading(false);\n return;\n }\n void run();\n return () => {\n abortRef.current?.abort();\n };\n }, [enabled, run]);\n\n // A retryable failure (device offline, connection lost) re-runs once the client is open again.\n const status = useSyncExternalStore(c.subscribe, () => c.getState().status, () => c.getState().status);\n useEffect(() => {\n if (enabled && status === \"open\" && error?.retryable) void run();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n\n return { data, error, loading, refetch: run };\n}\n\n/** Subscribe to one protocol event (fs.changed, exec.exited, relay.device_status, …) for the component's life. */\nexport function useDesktopEvent<E extends EventName>(name: E, handler: EventHandler<E>, options: { client?: DesktopClient } = {}): void {\n const c = useDesktopClient(options.client);\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => c.on(name, ((payload, resourceId) => handlerRef.current(payload, resourceId)) as EventHandler<E>), [c, name]);\n}\n\n/**\n * Reconnect the moment the person is back: page visible again, a bfcache restore, the network\n * returning, window focus. Each calls `client.wake()` — dial now if reconnecting, probe if open.\n */\nexport function useDesktopWake(client?: DesktopClient, options: { probeMs?: number } = {}): void {\n const c = useDesktopClient(client);\n const probeMs = options.probeMs;\n useEffect(() => bindDesktopWake(c, probeMs === undefined ? {} : { probeMs }), [c, probeMs]);\n}\n","/**\n * Wire a client's wake() to the browser events that mean \"the person is back\": the page becomes\n * visible again (iOS Safari suspends a background tab's sockets), a back/forward-cache restore\n * (`pageshow` with persisted), and the network returning (`online`). Framework-free; the React\n * binding is `useDesktopWake` in `./react`.\n */\nimport type { DesktopClient, WakeOptions } from \"./types\";\n\n/** The subset of window/document the binding listens on — injectable for tests and non-DOM hosts. */\nexport interface WakeTargets {\n window: Pick<Window, \"addEventListener\" | \"removeEventListener\">;\n document: Pick<Document, \"addEventListener\" | \"removeEventListener\" | \"visibilityState\">;\n}\n\nexport function bindDesktopWake(client: Pick<DesktopClient, \"wake\">, options: WakeOptions & { targets?: WakeTargets } = {}): () => void {\n const targets: WakeTargets | null =\n options.targets ?? (typeof window !== \"undefined\" && typeof document !== \"undefined\" ? { window, document } : null);\n if (!targets) return () => undefined;\n const wakeOptions: WakeOptions = options.probeMs === undefined ? {} : { probeMs: options.probeMs };\n const onVisibility = () => {\n if (targets.document.visibilityState === \"visible\") client.wake(wakeOptions);\n };\n const onWake = () => client.wake(wakeOptions);\n targets.document.addEventListener(\"visibilitychange\", onVisibility);\n targets.window.addEventListener(\"pageshow\", onWake);\n targets.window.addEventListener(\"online\", onWake);\n targets.window.addEventListener(\"focus\", onWake);\n return () => {\n targets.document.removeEventListener(\"visibilitychange\", onVisibility);\n targets.window.removeEventListener(\"pageshow\", onWake);\n targets.window.removeEventListener(\"online\", onWake);\n targets.window.removeEventListener(\"focus\", onWake);\n };\n}\n","/**\n * The one error type every package entry throws. It carries the wire ErrorCode (SPEC §5), so a\n * caller branches on `code`/`retryable`/`data.reason` and never parses a message. Zod-free on\n * purpose: `./frame` (the relay hot path) throws it without pulling the schema graph.\n */\nimport type { ErrorCodeName, ErrorData } from \"./types\";\n\nexport class DesktopProtocolError extends Error {\n readonly code: ErrorCodeName;\n readonly retryable: boolean;\n readonly data: ErrorData | undefined;\n\n constructor(code: ErrorCodeName, message: string, options: { retryable?: boolean; data?: ErrorData; cause?: unknown } = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"DesktopProtocolError\";\n this.code = code;\n this.retryable = options.retryable ?? false;\n this.data = options.data;\n }\n\n /** The wire body (ProtocolErrorBody) — what a core or relay sends for this error. */\n toBody(): { code: ErrorCodeName; message: string; retryable: boolean; data?: ErrorData } {\n return this.data === undefined\n ? { code: this.code, message: this.message, retryable: this.retryable }\n : { code: this.code, message: this.message, retryable: this.retryable, data: this.data };\n }\n}\n\nexport function isDesktopProtocolError(value: unknown): value is DesktopProtocolError {\n return value instanceof DesktopProtocolError || (value instanceof Error && value.name === \"DesktopProtocolError\" && \"code\" in value);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react.ts","../src/client/wake.ts","../src/error.ts"],"sourcesContent":["/**\n * `@ai-matrx/desktop-protocol/react` — hooks over the client: provider, connection state, typed\n * one-shot requests, events. The client owns every hard part (reconnect, reattach, credits); these\n * hooks only bind it to React's lifecycle.\n */\nimport { createContext, createElement, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { Context, ReactNode } from \"react\";\nimport type { DesktopClient, DesktopClientState, EventHandler, EventName, RequestOptions } from \"./client/types\";\nimport { bindDesktopWake } from \"./client/wake\";\nimport { DesktopProtocolError } from \"./error\";\nimport type { OpName, OpParams, OpResult } from \"./schema/registry\";\n\n// The context lives on globalThis: an ESM and a CJS copy of this entry in one app (Next.js\n// does this) would otherwise create two contexts and a provider the hooks cannot see.\nconst CONTEXT_SLOT = Symbol.for(\"ai-matrx.desktop-protocol.client-context\");\nfunction clientContext(): Context<DesktopClient | null> {\n const slot = globalThis as { [CONTEXT_SLOT]?: Context<DesktopClient | null> };\n return (slot[CONTEXT_SLOT] ??= createContext<DesktopClient | null>(null));\n}\n\nexport function DesktopClientProvider(props: { client: DesktopClient; children?: ReactNode }): ReactNode {\n return createElement(clientContext().Provider, { value: props.client }, props.children);\n}\n\n/** The client from the nearest provider (or the one passed in). Throws when there is neither. */\nexport function useDesktopClient(client?: DesktopClient): DesktopClient {\n const fromContext = useContext(clientContext());\n const resolved = client ?? fromContext;\n if (!resolved) throw new Error(\"useDesktopClient: wrap the tree in <DesktopClientProvider client={…}> or pass a client\");\n return resolved;\n}\n\n/** Live connection state (status, welcome, attempts, retryInMs, lastError). Connects on mount when idle. */\nexport function useDesktopConnection(client?: DesktopClient): DesktopClientState {\n const c = useDesktopClient(client);\n const state = useSyncExternalStore(c.subscribe, c.getState, c.getState);\n useEffect(() => {\n if (c.getState().status === \"idle\") c.connect();\n }, [c]);\n return state;\n}\n\nexport interface DesktopRequestState<N extends OpName> {\n data: OpResult<N> | undefined;\n error: DesktopProtocolError | undefined;\n loading: boolean;\n /** Re-run now; resolves with the fresh result. */\n refetch: () => Promise<OpResult<N> | undefined>;\n}\n\nexport interface UseDesktopRequestOptions extends Omit<RequestOptions, \"signal\"> {\n /** Default true. False = do not run until refetch() or enabled flips true. */\n enabled?: boolean;\n /** Re-run every `refreshMs` while the page is visible, and at once when it becomes visible\n * again. A hidden window never polls. Default: no refresh. */\n refreshMs?: number;\n client?: DesktopClient;\n}\n\n/**\n * Run a unary op and keep its latest result. Re-runs when `op` or the params' JSON changes, and\n * after a reconnect if the last attempt failed with a retryable error. Aborts on unmount.\n */\nexport function useDesktopRequest<N extends OpName>(op: N, params: OpParams<N>, options: UseDesktopRequestOptions = {}): DesktopRequestState<N> {\n const c = useDesktopClient(options.client);\n const enabled = options.enabled ?? true;\n const key = JSON.stringify(params);\n const [data, setData] = useState<OpResult<N> | undefined>(undefined);\n const [error, setError] = useState<DesktopProtocolError | undefined>(undefined);\n const [loading, setLoading] = useState<boolean>(enabled);\n const paramsRef = useRef(params);\n paramsRef.current = params;\n const runRef = useRef(0);\n const abortRef = useRef<AbortController | null>(null);\n const timeoutMs = options.timeoutMs;\n\n const run = useCallback(async (): Promise<OpResult<N> | undefined> => {\n const runId = ++runRef.current;\n abortRef.current?.abort();\n const abort = new AbortController();\n abortRef.current = abort;\n setLoading(true);\n try {\n const result = await c.request(op, paramsRef.current, timeoutMs === undefined ? { signal: abort.signal } : { signal: abort.signal, timeoutMs });\n if (runId === runRef.current) {\n setData(result);\n setError(undefined);\n }\n return result;\n } catch (caught) {\n const err = caught instanceof DesktopProtocolError ? caught : new DesktopProtocolError(\"INTERNAL\", String(caught), { cause: caught });\n if (runId === runRef.current && err.code !== \"CANCELLED\") setError(err);\n return undefined;\n } finally {\n if (runId === runRef.current) setLoading(false);\n }\n // `key` stands for the params' content: a new object with equal JSON is not a new request.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [c, op, key, timeoutMs]);\n\n useEffect(() => {\n if (!enabled) {\n setLoading(false);\n return;\n }\n void run();\n return () => {\n abortRef.current?.abort();\n };\n }, [enabled, run]);\n\n // Live data: poll while visible, catch up the moment the window is visible again.\n const refreshMs = options.refreshMs;\n useEffect(() => {\n if (!enabled || !refreshMs || typeof document === \"undefined\") return;\n let timer: ReturnType<typeof setInterval> | null = null;\n const start = () => {\n if (timer === null) timer = setInterval(() => void run(), refreshMs);\n };\n const stop = () => {\n if (timer !== null) clearInterval(timer);\n timer = null;\n };\n const onVisibility = () => {\n if (document.visibilityState === \"visible\") {\n void run();\n start();\n } else stop();\n };\n if (document.visibilityState === \"visible\") start();\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => {\n stop();\n document.removeEventListener(\"visibilitychange\", onVisibility);\n };\n }, [enabled, refreshMs, run]);\n\n // A retryable failure (device offline, connection lost) re-runs once the client is open again.\n const status = useSyncExternalStore(c.subscribe, () => c.getState().status, () => c.getState().status);\n useEffect(() => {\n if (enabled && status === \"open\" && error?.retryable) void run();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [status]);\n\n return { data, error, loading, refetch: run };\n}\n\nexport interface DesktopActionState {\n /** Run one or more ops as one action; resolves true when it finished without an error. */\n run: (work: (client: DesktopClient) => Promise<unknown>) => Promise<boolean>;\n /** An action is in flight (disable its controls). */\n busy: boolean;\n /** The last action's error, until the next action or `clearError()`. */\n error: DesktopProtocolError | undefined;\n clearError: () => void;\n}\n\n/**\n * Actions (create, save, remove, open): one busy flag and one error for a screen's writes, with the\n * same error type as reads. `onDone` runs after every successful action (refetch what it changed).\n */\nexport function useDesktopAction(options: { client?: DesktopClient; onDone?: () => unknown } = {}): DesktopActionState {\n const c = useDesktopClient(options.client);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<DesktopProtocolError | undefined>(undefined);\n const onDoneRef = useRef(options.onDone);\n onDoneRef.current = options.onDone;\n const run = useCallback(\n async (work: (client: DesktopClient) => Promise<unknown>): Promise<boolean> => {\n setBusy(true);\n setError(undefined);\n try {\n await work(c);\n await onDoneRef.current?.();\n return true;\n } catch (caught) {\n setError(caught instanceof DesktopProtocolError ? caught : new DesktopProtocolError(\"INTERNAL\", caught instanceof Error ? caught.message : String(caught), { cause: caught }));\n return false;\n } finally {\n setBusy(false);\n }\n },\n [c],\n );\n const clearError = useCallback(() => setError(undefined), []);\n return { run, busy, error, clearError };\n}\n\n/** Subscribe to one protocol event (fs.changed, exec.exited, relay.device_status, …) for the component's life. */\nexport function useDesktopEvent<E extends EventName>(name: E, handler: EventHandler<E>, options: { client?: DesktopClient } = {}): void {\n const c = useDesktopClient(options.client);\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => c.on(name, ((payload, resourceId) => handlerRef.current(payload, resourceId)) as EventHandler<E>), [c, name]);\n}\n\n/**\n * Reconnect the moment the person is back: page visible again, a bfcache restore, the network\n * returning, window focus. Each calls `client.wake()` — dial now if reconnecting, probe if open.\n */\nexport function useDesktopWake(client?: DesktopClient, options: { probeMs?: number } = {}): void {\n const c = useDesktopClient(client);\n const probeMs = options.probeMs;\n useEffect(() => bindDesktopWake(c, probeMs === undefined ? {} : { probeMs }), [c, probeMs]);\n}\n","/**\n * Wire a client's wake() to the browser events that mean \"the person is back\": the page becomes\n * visible again (iOS Safari suspends a background tab's sockets), a back/forward-cache restore\n * (`pageshow` with persisted), and the network returning (`online`). Framework-free; the React\n * binding is `useDesktopWake` in `./react`.\n */\nimport type { DesktopClient, WakeOptions } from \"./types\";\n\n/** The subset of window/document the binding listens on — injectable for tests and non-DOM hosts. */\nexport interface WakeTargets {\n window: Pick<Window, \"addEventListener\" | \"removeEventListener\">;\n document: Pick<Document, \"addEventListener\" | \"removeEventListener\" | \"visibilityState\">;\n}\n\nexport function bindDesktopWake(client: Pick<DesktopClient, \"wake\">, options: WakeOptions & { targets?: WakeTargets } = {}): () => void {\n const targets: WakeTargets | null =\n options.targets ?? (typeof window !== \"undefined\" && typeof document !== \"undefined\" ? { window, document } : null);\n if (!targets) return () => undefined;\n const wakeOptions: WakeOptions = options.probeMs === undefined ? {} : { probeMs: options.probeMs };\n const onVisibility = () => {\n if (targets.document.visibilityState === \"visible\") client.wake(wakeOptions);\n };\n const onWake = () => client.wake(wakeOptions);\n targets.document.addEventListener(\"visibilitychange\", onVisibility);\n targets.window.addEventListener(\"pageshow\", onWake);\n targets.window.addEventListener(\"online\", onWake);\n targets.window.addEventListener(\"focus\", onWake);\n return () => {\n targets.document.removeEventListener(\"visibilitychange\", onVisibility);\n targets.window.removeEventListener(\"pageshow\", onWake);\n targets.window.removeEventListener(\"online\", onWake);\n targets.window.removeEventListener(\"focus\", onWake);\n };\n}\n","/**\n * The one error type every package entry throws. It carries the wire ErrorCode (SPEC §5), so a\n * caller branches on `code`/`retryable`/`data.reason` and never parses a message. Zod-free on\n * purpose: `./frame` (the relay hot path) throws it without pulling the schema graph.\n */\nimport type { ErrorCodeName, ErrorData } from \"./types\";\n\nexport class DesktopProtocolError extends Error {\n readonly code: ErrorCodeName;\n readonly retryable: boolean;\n readonly data: ErrorData | undefined;\n\n constructor(code: ErrorCodeName, message: string, options: { retryable?: boolean; data?: ErrorData; cause?: unknown } = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"DesktopProtocolError\";\n this.code = code;\n this.retryable = options.retryable ?? false;\n this.data = options.data;\n }\n\n /** The wire body (ProtocolErrorBody) — what a core or relay sends for this error. */\n toBody(): { code: ErrorCodeName; message: string; retryable: boolean; data?: ErrorData } {\n return this.data === undefined\n ? { code: this.code, message: this.message, retryable: this.retryable }\n : { code: this.code, message: this.message, retryable: this.retryable, data: this.data };\n }\n}\n\nexport function isDesktopProtocolError(value: unknown): value is DesktopProtocolError {\n return value instanceof DesktopProtocolError || (value instanceof Error && value.name === \"DesktopProtocolError\" && \"code\" in value);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,mBAAyH;;;ACSlH,SAAS,gBAAgB,QAAqC,UAAmD,CAAC,GAAe;AACtI,QAAM,UACJ,QAAQ,YAAY,OAAO,WAAW,eAAe,OAAO,aAAa,cAAc,EAAE,QAAQ,SAAS,IAAI;AAChH,MAAI,CAAC,QAAS,QAAO,MAAM;AAC3B,QAAM,cAA2B,QAAQ,YAAY,SAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;AACjG,QAAM,eAAe,MAAM;AACzB,QAAI,QAAQ,SAAS,oBAAoB,UAAW,QAAO,KAAK,WAAW;AAAA,EAC7E;AACA,QAAM,SAAS,MAAM,OAAO,KAAK,WAAW;AAC5C,UAAQ,SAAS,iBAAiB,oBAAoB,YAAY;AAClE,UAAQ,OAAO,iBAAiB,YAAY,MAAM;AAClD,UAAQ,OAAO,iBAAiB,UAAU,MAAM;AAChD,UAAQ,OAAO,iBAAiB,SAAS,MAAM;AAC/C,SAAO,MAAM;AACX,YAAQ,SAAS,oBAAoB,oBAAoB,YAAY;AACrE,YAAQ,OAAO,oBAAoB,YAAY,MAAM;AACrD,YAAQ,OAAO,oBAAoB,UAAU,MAAM;AACnD,YAAQ,OAAO,oBAAoB,SAAS,MAAM;AAAA,EACpD;AACF;;;AC1BO,IAAM,uBAAN,cAAmC,MAAM;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAAqB,SAAiB,UAAsE,CAAC,GAAG;AAC1H,UAAM,SAAS,QAAQ,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;AACjF,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,YAAY,QAAQ,aAAa;AACtC,SAAK,OAAO,QAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,SAAyF;AACvF,WAAO,KAAK,SAAS,SACjB,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,SAAS,WAAW,KAAK,UAAU,IACpE,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,SAAS,WAAW,KAAK,WAAW,MAAM,KAAK,KAAK;AAAA,EAC3F;AACF;;;AFZA,IAAM,eAAe,uBAAO,IAAI,0CAA0C;AAC1E,SAAS,gBAA+C;AACtD,QAAM,OAAO;AACb,SAAQ,KAAK,YAAY,UAAM,4BAAoC,IAAI;AACzE;AAEO,SAAS,sBAAsB,OAAmE;AACvG,aAAO,4BAAc,cAAc,EAAE,UAAU,EAAE,OAAO,MAAM,OAAO,GAAG,MAAM,QAAQ;AACxF;AAGO,SAAS,iBAAiB,QAAuC;AACtE,QAAM,kBAAc,yBAAW,cAAc,CAAC;AAC9C,QAAM,WAAW,UAAU;AAC3B,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,6FAAwF;AACvH,SAAO;AACT;AAGO,SAAS,qBAAqB,QAA4C;AAC/E,QAAM,IAAI,iBAAiB,MAAM;AACjC,QAAM,YAAQ,mCAAqB,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ;AACtE,8BAAU,MAAM;AACd,QAAI,EAAE,SAAS,EAAE,WAAW,OAAQ,GAAE,QAAQ;AAAA,EAChD,GAAG,CAAC,CAAC,CAAC;AACN,SAAO;AACT;AAuBO,SAAS,kBAAoC,IAAO,QAAqB,UAAoC,CAAC,GAA2B;AAC9I,QAAM,IAAI,iBAAiB,QAAQ,MAAM;AACzC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,MAAM,KAAK,UAAU,MAAM;AACjC,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAkC,MAAS;AACnE,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA2C,MAAS;AAC9E,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAkB,OAAO;AACvD,QAAM,gBAAY,qBAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,QAAM,aAAS,qBAAO,CAAC;AACvB,QAAM,eAAW,qBAA+B,IAAI;AACpD,QAAM,YAAY,QAAQ;AAE1B,QAAM,UAAM,0BAAY,YAA8C;AACpE,UAAM,QAAQ,EAAE,OAAO;AACvB,aAAS,SAAS,MAAM;AACxB,UAAM,QAAQ,IAAI,gBAAgB;AAClC,aAAS,UAAU;AACnB,eAAW,IAAI;AACf,QAAI;AACF,YAAM,SAAS,MAAM,EAAE,QAAQ,IAAI,UAAU,SAAS,cAAc,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,EAAE,QAAQ,MAAM,QAAQ,UAAU,CAAC;AAC9I,UAAI,UAAU,OAAO,SAAS;AAC5B,gBAAQ,MAAM;AACd,iBAAS,MAAS;AAAA,MACpB;AACA,aAAO;AAAA,IACT,SAAS,QAAQ;AACf,YAAM,MAAM,kBAAkB,uBAAuB,SAAS,IAAI,qBAAqB,YAAY,OAAO,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;AACpI,UAAI,UAAU,OAAO,WAAW,IAAI,SAAS,YAAa,UAAS,GAAG;AACtE,aAAO;AAAA,IACT,UAAE;AACA,UAAI,UAAU,OAAO,QAAS,YAAW,KAAK;AAAA,IAChD;AAAA,EAGF,GAAG,CAAC,GAAG,IAAI,KAAK,SAAS,CAAC;AAE1B,8BAAU,MAAM;AACd,QAAI,CAAC,SAAS;AACZ,iBAAW,KAAK;AAChB;AAAA,IACF;AACA,SAAK,IAAI;AACT,WAAO,MAAM;AACX,eAAS,SAAS,MAAM;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,SAAS,GAAG,CAAC;AAGjB,QAAM,YAAY,QAAQ;AAC1B,8BAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,aAAa,OAAO,aAAa,YAAa;AAC/D,QAAI,QAA+C;AACnD,UAAM,QAAQ,MAAM;AAClB,UAAI,UAAU,KAAM,SAAQ,YAAY,MAAM,KAAK,IAAI,GAAG,SAAS;AAAA,IACrE;AACA,UAAM,OAAO,MAAM;AACjB,UAAI,UAAU,KAAM,eAAc,KAAK;AACvC,cAAQ;AAAA,IACV;AACA,UAAM,eAAe,MAAM;AACzB,UAAI,SAAS,oBAAoB,WAAW;AAC1C,aAAK,IAAI;AACT,cAAM;AAAA,MACR,MAAO,MAAK;AAAA,IACd;AACA,QAAI,SAAS,oBAAoB,UAAW,OAAM;AAClD,aAAS,iBAAiB,oBAAoB,YAAY;AAC1D,WAAO,MAAM;AACX,WAAK;AACL,eAAS,oBAAoB,oBAAoB,YAAY;AAAA,IAC/D;AAAA,EACF,GAAG,CAAC,SAAS,WAAW,GAAG,CAAC;AAG5B,QAAM,aAAS,mCAAqB,EAAE,WAAW,MAAM,EAAE,SAAS,EAAE,QAAQ,MAAM,EAAE,SAAS,EAAE,MAAM;AACrG,8BAAU,MAAM;AACd,QAAI,WAAW,WAAW,UAAU,OAAO,UAAW,MAAK,IAAI;AAAA,EAEjE,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO,EAAE,MAAM,OAAO,SAAS,SAAS,IAAI;AAC9C;AAgBO,SAAS,iBAAiB,UAA8D,CAAC,GAAuB;AACrH,QAAM,IAAI,iBAAiB,QAAQ,MAAM;AACzC,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA2C,MAAS;AAC9E,QAAM,gBAAY,qBAAO,QAAQ,MAAM;AACvC,YAAU,UAAU,QAAQ;AAC5B,QAAM,UAAM;AAAA,IACV,OAAO,SAAwE;AAC7E,cAAQ,IAAI;AACZ,eAAS,MAAS;AAClB,UAAI;AACF,cAAM,KAAK,CAAC;AACZ,cAAM,UAAU,UAAU;AAC1B,eAAO;AAAA,MACT,SAAS,QAAQ;AACf,iBAAS,kBAAkB,uBAAuB,SAAS,IAAI,qBAAqB,YAAY,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC,CAAC;AAC7K,eAAO;AAAA,MACT,UAAE;AACA,gBAAQ,KAAK;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AACA,QAAM,iBAAa,0BAAY,MAAM,SAAS,MAAS,GAAG,CAAC,CAAC;AAC5D,SAAO,EAAE,KAAK,MAAM,OAAO,WAAW;AACxC;AAGO,SAAS,gBAAqC,MAAS,SAA0B,UAAsC,CAAC,GAAS;AACtI,QAAM,IAAI,iBAAiB,QAAQ,MAAM;AACzC,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AACrB,8BAAU,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,eAAe,WAAW,QAAQ,SAAS,UAAU,EAAqB,GAAG,CAAC,GAAG,IAAI,CAAC;AAC9H;AAMO,SAAS,eAAe,QAAwB,UAAgC,CAAC,GAAS;AAC/F,QAAM,IAAI,iBAAiB,MAAM;AACjC,QAAM,UAAU,QAAQ;AACxB,8BAAU,MAAM,gBAAgB,GAAG,YAAY,SAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;AAC5F;","names":[]}
package/dist/react.d.cts CHANGED
@@ -1662,6 +1662,52 @@ declare const OPS: {
1662
1662
  readonly since: "1.4";
1663
1663
  readonly errors: readonly ["INTERNAL"];
1664
1664
  };
1665
+ readonly "audio.devices": {
1666
+ readonly params: z.ZodObject<{}, z.core.$strict>;
1667
+ readonly result: z.ZodObject<{
1668
+ inputs: z.ZodArray<z.ZodObject<{
1669
+ id: z.ZodString;
1670
+ name: z.ZodString;
1671
+ default: z.ZodBoolean;
1672
+ }, z.core.$strict>>;
1673
+ outputs: z.ZodArray<z.ZodObject<{
1674
+ id: z.ZodString;
1675
+ name: z.ZodString;
1676
+ default: z.ZodBoolean;
1677
+ }, z.core.$strict>>;
1678
+ }, z.core.$strict>;
1679
+ readonly kind: "unary";
1680
+ readonly since: "1.6";
1681
+ readonly errors: readonly ["PLATFORM_UNSUPPORTED", "INTERNAL"];
1682
+ };
1683
+ readonly "audio.record": {
1684
+ readonly params: z.ZodObject<{
1685
+ seconds: z.ZodNumber;
1686
+ device_id: z.ZodOptional<z.ZodString>;
1687
+ path: z.ZodOptional<z.ZodString>;
1688
+ }, z.core.$strict>;
1689
+ readonly result: z.ZodObject<{
1690
+ path: z.ZodString;
1691
+ seconds: z.ZodNumber;
1692
+ bytes: z.ZodInt;
1693
+ peak: z.ZodNumber;
1694
+ }, z.core.$strict>;
1695
+ readonly kind: "unary";
1696
+ readonly since: "1.6";
1697
+ readonly errors: readonly ["NOT_FOUND", "PERMISSION_DENIED_OS", "PLATFORM_UNSUPPORTED", "RESOURCE_BUSY", "INTERNAL"];
1698
+ };
1699
+ readonly "audio.play": {
1700
+ readonly params: z.ZodObject<{
1701
+ path: z.ZodString;
1702
+ }, z.core.$strict>;
1703
+ readonly result: z.ZodObject<{
1704
+ path: z.ZodString;
1705
+ seconds: z.ZodNumber;
1706
+ }, z.core.$strict>;
1707
+ readonly kind: "unary";
1708
+ readonly since: "1.6";
1709
+ readonly errors: readonly ["NOT_FOUND", "INVALID_PARAMS", "PLATFORM_UNSUPPORTED", "INTERNAL"];
1710
+ };
1665
1711
  readonly "coding.history.import_missing": {
1666
1712
  readonly params: z.ZodObject<{}, z.core.$strict>;
1667
1713
  readonly result: z.ZodObject<{
@@ -1918,6 +1964,9 @@ interface DesktopRequestState<N extends OpName> {
1918
1964
  interface UseDesktopRequestOptions extends Omit<RequestOptions, "signal"> {
1919
1965
  /** Default true. False = do not run until refetch() or enabled flips true. */
1920
1966
  enabled?: boolean;
1967
+ /** Re-run every `refreshMs` while the page is visible, and at once when it becomes visible
1968
+ * again. A hidden window never polls. Default: no refresh. */
1969
+ refreshMs?: number;
1921
1970
  client?: DesktopClient;
1922
1971
  }
1923
1972
  /**
@@ -1925,6 +1974,23 @@ interface UseDesktopRequestOptions extends Omit<RequestOptions, "signal"> {
1925
1974
  * after a reconnect if the last attempt failed with a retryable error. Aborts on unmount.
1926
1975
  */
1927
1976
  declare function useDesktopRequest<N extends OpName>(op: N, params: OpParams<N>, options?: UseDesktopRequestOptions): DesktopRequestState<N>;
1977
+ interface DesktopActionState {
1978
+ /** Run one or more ops as one action; resolves true when it finished without an error. */
1979
+ run: (work: (client: DesktopClient) => Promise<unknown>) => Promise<boolean>;
1980
+ /** An action is in flight (disable its controls). */
1981
+ busy: boolean;
1982
+ /** The last action's error, until the next action or `clearError()`. */
1983
+ error: DesktopProtocolError | undefined;
1984
+ clearError: () => void;
1985
+ }
1986
+ /**
1987
+ * Actions (create, save, remove, open): one busy flag and one error for a screen's writes, with the
1988
+ * same error type as reads. `onDone` runs after every successful action (refetch what it changed).
1989
+ */
1990
+ declare function useDesktopAction(options?: {
1991
+ client?: DesktopClient;
1992
+ onDone?: () => unknown;
1993
+ }): DesktopActionState;
1928
1994
  /** Subscribe to one protocol event (fs.changed, exec.exited, relay.device_status, …) for the component's life. */
1929
1995
  declare function useDesktopEvent<E extends EventName>(name: E, handler: EventHandler<E>, options?: {
1930
1996
  client?: DesktopClient;
@@ -1937,4 +2003,4 @@ declare function useDesktopWake(client?: DesktopClient, options?: {
1937
2003
  probeMs?: number;
1938
2004
  }): void;
1939
2005
 
1940
- export { DesktopClientProvider, type DesktopRequestState, type UseDesktopRequestOptions, useDesktopClient, useDesktopConnection, useDesktopEvent, useDesktopRequest, useDesktopWake };
2006
+ export { type DesktopActionState, DesktopClientProvider, type DesktopRequestState, type UseDesktopRequestOptions, useDesktopAction, useDesktopClient, useDesktopConnection, useDesktopEvent, useDesktopRequest, useDesktopWake };
package/dist/react.d.ts CHANGED
@@ -1662,6 +1662,52 @@ declare const OPS: {
1662
1662
  readonly since: "1.4";
1663
1663
  readonly errors: readonly ["INTERNAL"];
1664
1664
  };
1665
+ readonly "audio.devices": {
1666
+ readonly params: z.ZodObject<{}, z.core.$strict>;
1667
+ readonly result: z.ZodObject<{
1668
+ inputs: z.ZodArray<z.ZodObject<{
1669
+ id: z.ZodString;
1670
+ name: z.ZodString;
1671
+ default: z.ZodBoolean;
1672
+ }, z.core.$strict>>;
1673
+ outputs: z.ZodArray<z.ZodObject<{
1674
+ id: z.ZodString;
1675
+ name: z.ZodString;
1676
+ default: z.ZodBoolean;
1677
+ }, z.core.$strict>>;
1678
+ }, z.core.$strict>;
1679
+ readonly kind: "unary";
1680
+ readonly since: "1.6";
1681
+ readonly errors: readonly ["PLATFORM_UNSUPPORTED", "INTERNAL"];
1682
+ };
1683
+ readonly "audio.record": {
1684
+ readonly params: z.ZodObject<{
1685
+ seconds: z.ZodNumber;
1686
+ device_id: z.ZodOptional<z.ZodString>;
1687
+ path: z.ZodOptional<z.ZodString>;
1688
+ }, z.core.$strict>;
1689
+ readonly result: z.ZodObject<{
1690
+ path: z.ZodString;
1691
+ seconds: z.ZodNumber;
1692
+ bytes: z.ZodInt;
1693
+ peak: z.ZodNumber;
1694
+ }, z.core.$strict>;
1695
+ readonly kind: "unary";
1696
+ readonly since: "1.6";
1697
+ readonly errors: readonly ["NOT_FOUND", "PERMISSION_DENIED_OS", "PLATFORM_UNSUPPORTED", "RESOURCE_BUSY", "INTERNAL"];
1698
+ };
1699
+ readonly "audio.play": {
1700
+ readonly params: z.ZodObject<{
1701
+ path: z.ZodString;
1702
+ }, z.core.$strict>;
1703
+ readonly result: z.ZodObject<{
1704
+ path: z.ZodString;
1705
+ seconds: z.ZodNumber;
1706
+ }, z.core.$strict>;
1707
+ readonly kind: "unary";
1708
+ readonly since: "1.6";
1709
+ readonly errors: readonly ["NOT_FOUND", "INVALID_PARAMS", "PLATFORM_UNSUPPORTED", "INTERNAL"];
1710
+ };
1665
1711
  readonly "coding.history.import_missing": {
1666
1712
  readonly params: z.ZodObject<{}, z.core.$strict>;
1667
1713
  readonly result: z.ZodObject<{
@@ -1918,6 +1964,9 @@ interface DesktopRequestState<N extends OpName> {
1918
1964
  interface UseDesktopRequestOptions extends Omit<RequestOptions, "signal"> {
1919
1965
  /** Default true. False = do not run until refetch() or enabled flips true. */
1920
1966
  enabled?: boolean;
1967
+ /** Re-run every `refreshMs` while the page is visible, and at once when it becomes visible
1968
+ * again. A hidden window never polls. Default: no refresh. */
1969
+ refreshMs?: number;
1921
1970
  client?: DesktopClient;
1922
1971
  }
1923
1972
  /**
@@ -1925,6 +1974,23 @@ interface UseDesktopRequestOptions extends Omit<RequestOptions, "signal"> {
1925
1974
  * after a reconnect if the last attempt failed with a retryable error. Aborts on unmount.
1926
1975
  */
1927
1976
  declare function useDesktopRequest<N extends OpName>(op: N, params: OpParams<N>, options?: UseDesktopRequestOptions): DesktopRequestState<N>;
1977
+ interface DesktopActionState {
1978
+ /** Run one or more ops as one action; resolves true when it finished without an error. */
1979
+ run: (work: (client: DesktopClient) => Promise<unknown>) => Promise<boolean>;
1980
+ /** An action is in flight (disable its controls). */
1981
+ busy: boolean;
1982
+ /** The last action's error, until the next action or `clearError()`. */
1983
+ error: DesktopProtocolError | undefined;
1984
+ clearError: () => void;
1985
+ }
1986
+ /**
1987
+ * Actions (create, save, remove, open): one busy flag and one error for a screen's writes, with the
1988
+ * same error type as reads. `onDone` runs after every successful action (refetch what it changed).
1989
+ */
1990
+ declare function useDesktopAction(options?: {
1991
+ client?: DesktopClient;
1992
+ onDone?: () => unknown;
1993
+ }): DesktopActionState;
1928
1994
  /** Subscribe to one protocol event (fs.changed, exec.exited, relay.device_status, …) for the component's life. */
1929
1995
  declare function useDesktopEvent<E extends EventName>(name: E, handler: EventHandler<E>, options?: {
1930
1996
  client?: DesktopClient;
@@ -1937,4 +2003,4 @@ declare function useDesktopWake(client?: DesktopClient, options?: {
1937
2003
  probeMs?: number;
1938
2004
  }): void;
1939
2005
 
1940
- export { DesktopClientProvider, type DesktopRequestState, type UseDesktopRequestOptions, useDesktopClient, useDesktopConnection, useDesktopEvent, useDesktopRequest, useDesktopWake };
2006
+ export { type DesktopActionState, DesktopClientProvider, type DesktopRequestState, type UseDesktopRequestOptions, useDesktopAction, useDesktopClient, useDesktopConnection, useDesktopEvent, useDesktopRequest, useDesktopWake };
package/dist/react.js CHANGED
@@ -108,12 +108,62 @@ function useDesktopRequest(op, params, options = {}) {
108
108
  abortRef.current?.abort();
109
109
  };
110
110
  }, [enabled, run]);
111
+ const refreshMs = options.refreshMs;
112
+ useEffect(() => {
113
+ if (!enabled || !refreshMs || typeof document === "undefined") return;
114
+ let timer = null;
115
+ const start = () => {
116
+ if (timer === null) timer = setInterval(() => void run(), refreshMs);
117
+ };
118
+ const stop = () => {
119
+ if (timer !== null) clearInterval(timer);
120
+ timer = null;
121
+ };
122
+ const onVisibility = () => {
123
+ if (document.visibilityState === "visible") {
124
+ void run();
125
+ start();
126
+ } else stop();
127
+ };
128
+ if (document.visibilityState === "visible") start();
129
+ document.addEventListener("visibilitychange", onVisibility);
130
+ return () => {
131
+ stop();
132
+ document.removeEventListener("visibilitychange", onVisibility);
133
+ };
134
+ }, [enabled, refreshMs, run]);
111
135
  const status = useSyncExternalStore(c.subscribe, () => c.getState().status, () => c.getState().status);
112
136
  useEffect(() => {
113
137
  if (enabled && status === "open" && error?.retryable) void run();
114
138
  }, [status]);
115
139
  return { data, error, loading, refetch: run };
116
140
  }
141
+ function useDesktopAction(options = {}) {
142
+ const c = useDesktopClient(options.client);
143
+ const [busy, setBusy] = useState(false);
144
+ const [error, setError] = useState(void 0);
145
+ const onDoneRef = useRef(options.onDone);
146
+ onDoneRef.current = options.onDone;
147
+ const run = useCallback(
148
+ async (work) => {
149
+ setBusy(true);
150
+ setError(void 0);
151
+ try {
152
+ await work(c);
153
+ await onDoneRef.current?.();
154
+ return true;
155
+ } catch (caught) {
156
+ setError(caught instanceof DesktopProtocolError ? caught : new DesktopProtocolError("INTERNAL", caught instanceof Error ? caught.message : String(caught), { cause: caught }));
157
+ return false;
158
+ } finally {
159
+ setBusy(false);
160
+ }
161
+ },
162
+ [c]
163
+ );
164
+ const clearError = useCallback(() => setError(void 0), []);
165
+ return { run, busy, error, clearError };
166
+ }
117
167
  function useDesktopEvent(name, handler, options = {}) {
118
168
  const c = useDesktopClient(options.client);
119
169
  const handlerRef = useRef(handler);
@@ -127,6 +177,7 @@ function useDesktopWake(client, options = {}) {
127
177
  }
128
178
  export {
129
179
  DesktopClientProvider,
180
+ useDesktopAction,
130
181
  useDesktopClient,
131
182
  useDesktopConnection,
132
183
  useDesktopEvent,