Skip to content

Commit fa4109d

Browse files
authored
refactor(contracts): drop the Limrun iOS denials its facts already make (#2815)
* refactor(contracts): make the springboard buttons optional members and drop the Limrun denials ADR 0019 keeps a stub that throws `unsupported` out of an interactor: an owner with no control says so with its facts, and the facts are what the binding consults. `home` and `appSwitcher` reached the Limrun iOS direct session's interactor only through `bindSystemButton`, which the catalog calls for exactly the buttons a binding's facts admitted — and `limrunSystemButtonOperationFacts` refuses both cells on that leg (`homeUnavailableIos`/`appSwitcherUnavailableIos`, or `liveSessionUnavailable` when no session is live). The interactor denials were therefore unreachable code pretending to be an implementation, and the fact hints already carried the identical wording. Both members become optional, following #2715's `actionButton` shape: the system-button binder already fails closed through `requireInteractorMethod`, so a fact that admits a button its interactor cannot serve names the missing button instead of answering a silent no-op. Apple, Android, HarmonyOS and the Vega TV leg keep their real mechanics. * refactor(contracts): make the clipboard halves optional members and drop the Limrun denials The Limrun iOS direct session's clipboard legs were unreachable code pretending to be an implementation. `bindClipboardRead`/`bindClipboardWrite` are reached only through the catalog, which binds a half exactly when the owner's fact admits it, and `limrunClipboardOperationFacts` refuses both halves on that leg (`clipboardUnavailableIos`, or `liveSessionUnavailable` with no live session). The Android leg stays honest: it rides the same `createAndroidInteractor` the local family binds, and its cells stay available. Both members become optional and the binders now fail closed through `requireInteractorMethod`, naming the half with the labels `CLIPBOARD_LEG_LABELS` introduces and the catalog adopts — a fact that admits a half its interactor cannot serve reports the missing method instead of resolving an absent member as a TypeError. Apple, Android, Linux and the WebDriver clipboard extension keep their real mechanics. * refactor(contracts): make the TV remote an optional member and drop the Limrun denial The Limrun iOS direct session's remote leg was unreachable code pretending to be an implementation. `bindTvRemote` is reached only through the catalog, which binds the press exactly when the owner's fact admits it, and `limrunNavigationOperationFacts` refuses the cell on that leg (`tvRemoteUnavailableIos`, or `liveSessionUnavailable` with no live session) — a phone/tablet direct session has no remote receiver. The Android leg stays honest: its cells gate on `device.target === 'tv'` and ride the real Android interactor. The member becomes optional and the binder now fails closed through `requireInteractorMethod`, naming the press with the label the catalog already used — a fact that admits a press its interactor cannot serve reports the missing method instead of resolving an absent member as a TypeError. Apple tvOS, the Android TV leaf, and Vega keep their real mechanics. * refactor(provider-limrun): drop the Limrun iOS gesture denial the facts already make The iOS direct session refused every gesture tier through `limrunGestureFacts` (`iosGestureUnavailable` on live sessions, the liveness cell otherwise), and `bindGestureOperations` binds a tier only when its cell is available — so the interactor's throwing `performGesture` could never run. The member is already optional on `Interactor`, and the binder already escalates an advertised tier with no executor to `invalidRuntimeContract` rather than a refusal (ADR 0019 §2), a path pinned by `gesture-runtime.test.ts`. What remained was unreachable code pretending to be an implementation; the fact cell is now the single source of the agent-facing wording. * test(interactors): narrow the optional members at pinned implementation call sites The Vega, Apple-runner-provider, and core interactor tests drive interactors that really do carry home/appSwitcher/tvRemote/readClipboard/writeClipboard, but the factory hands them an Interactor, and those members are optional now. Same `!` narrowing #2715 and #2807 applied at their own pinned call sites: the assertion still runs the real method, and an owner that ever stopped carrying it would fail in the factory's own test rather than silently skipping the press.
1 parent d988768 commit fa4109d

11 files changed

Lines changed: 146 additions & 58 deletions

File tree

‎packages/contracts/src/clipboard-runtime.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test, vi } from 'vitest';
2+
import { AppError } from '@agent-device/kernel/errors';
23
import {
34
bindClipboardRead,
45
bindClipboardWrite,
@@ -96,3 +97,31 @@ test('a local write binding hands the interactor the already-joined text', async
9697

9798
expect(writeClipboard).toHaveBeenCalledWith('hello world');
9899
});
100+
101+
// Facts admitted the half, so an interactor without it is an ownership bug the caller must see
102+
// rather than a refusal to degrade around. The label is the one the command already used.
103+
test('a clipboard half whose fact admitted but whose interactor cannot serve it fails closed', async () => {
104+
const served = { readClipboard: vi.fn(async () => 'copied text') } as unknown as Interactor;
105+
const signal = new AbortController().signal;
106+
const missingWrite = local(async () => ({ ...served, writeClipboard: undefined }));
107+
const missingRead = local(
108+
async () => ({ writeClipboard: vi.fn(async () => undefined) }) as unknown as Interactor,
109+
);
110+
111+
await expect(
112+
bindClipboardWrite(signal, missingWrite).writeClipboard({ text: 'hello' }),
113+
).rejects.toSatisfy(
114+
(error: unknown) =>
115+
error instanceof AppError &&
116+
error.code === 'COMMAND_FAILED' &&
117+
error.details?.['reason'] === 'interactor-method-missing' &&
118+
error.message.includes('clipboard write'),
119+
);
120+
await expect(bindClipboardRead(signal, missingRead).readClipboard({})).rejects.toSatisfy(
121+
(error: unknown) =>
122+
error instanceof AppError &&
123+
error.code === 'COMMAND_FAILED' &&
124+
error.details?.['reason'] === 'interactor-method-missing' &&
125+
error.message.includes('clipboard read'),
126+
);
127+
});

‎packages/contracts/src/clipboard-runtime.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export function clipboardRuntimeOperationFacts(
6969
});
7070
}
7171

72+
/** How a fail-closed refusal names each clipboard half to the caller. */
73+
export const CLIPBOARD_LEG_LABELS = {
74+
readClipboard: 'clipboard read',
75+
writeClipboard: 'clipboard write',
76+
} as const;
77+
7278
/**
7379
* Captures one selected owner's interactor authority for the lifetime of a request binding. The
7480
* owner is already chosen by the time a binder is called, so each entry point supplies its own
@@ -87,14 +93,28 @@ async function resolveClipboardInteractor(
8793
});
8894
}
8995

96+
/**
97+
* Resolves one clipboard half's member through the shared optional-member guard. Loaded on the
98+
* call rather than at module evaluation because this facade's eager closure is held at its
99+
* merge-base size (`eager-closure-budgets`); a static edge would grow it.
100+
*/
101+
async function requireClipboardMethod<Leg extends keyof typeof CLIPBOARD_LEG_LABELS>(
102+
interactor: Interactor,
103+
leg: Leg,
104+
): Promise<NonNullable<Interactor[Leg]>> {
105+
const { requireInteractorMethod } = await import('./interactor-operation-binding.ts');
106+
return requireInteractorMethod(interactor[leg], CLIPBOARD_LEG_LABELS[leg]);
107+
}
108+
90109
export function bindClipboardRead(
91110
signal: AbortSignal,
92111
resolveInteractor: (runner: RunnerContext) => Promise<Interactor>,
93112
): ClipboardReadRuntimeOperations {
94113
return Object.freeze({
95114
readClipboard: async (input: ClipboardReadInput) => {
96115
const interactor = await resolveClipboardInteractor(signal, resolveInteractor, input);
97-
return await interactor.readClipboard();
116+
const readClipboard = await requireClipboardMethod(interactor, 'readClipboard');
117+
return await readClipboard.call(interactor);
98118
},
99119
});
100120
}
@@ -106,7 +126,8 @@ export function bindClipboardWrite(
106126
return Object.freeze({
107127
writeClipboard: async (input: ClipboardWriteInput) => {
108128
const interactor = await resolveClipboardInteractor(signal, resolveInteractor, input);
109-
await interactor.writeClipboard(input.text);
129+
const writeClipboard = await requireClipboardMethod(interactor, 'writeClipboard');
130+
await writeClipboard.call(interactor, input.text);
110131
},
111132
});
112133
}

‎packages/contracts/src/interactor-operation-catalog.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import type { DeviceInfo } from '@agent-device/kernel/device';
22
import { ALERT_LEG_LABELS, bindAlertLeg } from './alert-runtime.ts';
33
import { bindAppEvent } from './app-event-runtime.ts';
44
import { bindBack } from './back-runtime.ts';
5-
import { bindClipboardRead, bindClipboardWrite } from './clipboard-runtime.ts';
5+
import {
6+
CLIPBOARD_LEG_LABELS,
7+
bindClipboardRead,
8+
bindClipboardWrite,
9+
} from './clipboard-runtime.ts';
610
import { KEYBOARD_ACTION_LABELS, bindKeyboardAction } from './keyboard-runtime.ts';
711
import { bindOrientation } from './orientation-runtime.ts';
812
import { bindReadSetting, bindSetSetting } from './settings-runtime.ts';
9-
import { bindTvRemote } from './tv-remote-runtime.ts';
13+
import { TV_REMOTE_LABEL, bindTvRemote } from './tv-remote-runtime.ts';
1014
import {
1115
localInteractorSource,
1216
providerInteractorSource,
@@ -58,7 +62,7 @@ export const INTERACTOR_OPERATIONS = [
5862
bind: (signal, resolve) => bindSystemButton('home', signal, resolve),
5963
},
6064
{ operation: 'setOrientation', label: 'orientation', bind: bindOrientation },
61-
{ operation: 'tvRemote', label: 'tv-remote', bind: bindTvRemote },
65+
{ operation: 'tvRemote', label: TV_REMOTE_LABEL, bind: bindTvRemote },
6266
{
6367
operation: 'keyboardStatus',
6468
label: KEYBOARD_ACTION_LABELS.keyboardStatus,
@@ -74,8 +78,16 @@ export const INTERACTOR_OPERATIONS = [
7478
label: KEYBOARD_ACTION_LABELS.keyboardEnter,
7579
bind: (signal, resolve) => bindKeyboardAction('keyboardEnter', signal, resolve),
7680
},
77-
{ operation: 'readClipboard', label: 'clipboard read', bind: bindClipboardRead },
78-
{ operation: 'writeClipboard', label: 'clipboard write', bind: bindClipboardWrite },
81+
{
82+
operation: 'readClipboard',
83+
label: CLIPBOARD_LEG_LABELS.readClipboard,
84+
bind: bindClipboardRead,
85+
},
86+
{
87+
operation: 'writeClipboard',
88+
label: CLIPBOARD_LEG_LABELS.writeClipboard,
89+
bind: bindClipboardWrite,
90+
},
7991
{
8092
operation: 'appSwitcher',
8193
label: SYSTEM_BUTTON_LABELS.appSwitcher,

‎packages/contracts/src/interactor-types.ts‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,29 @@ export type Interactor = {
356356
}>;
357357
gestureViewport?(): Promise<Rect>;
358358
back(mode?: BackMode): Promise<void>;
359-
home(): Promise<void>;
359+
/**
360+
* Optional (parity with `actionButton`): opens the springboard, a control not every owner
361+
* carries. An owner without it leaves it undefined; its fact refuses the press before binding,
362+
* and the system-button binder fails closed rather than resolving an absent member as a
363+
* successful no-op.
364+
*/
365+
home?(): Promise<void>;
360366
setOrientation(orientation: DeviceRotation): Promise<{ orientation?: DeviceRotation } | void>;
361367
performGesture?(plan: GesturePlan): Promise<Record<string, unknown> | void>;
362-
appSwitcher(): Promise<void>;
363-
tvRemote(button: TvRemoteButton, durationMs?: number): Promise<void>;
368+
/**
369+
* Optional (parity with `home`): opens the recents surface, a control not every owner carries.
370+
* An owner without it leaves it undefined; its fact refuses the press before binding, and the
371+
* system-button binder fails closed rather than resolving an absent member as a successful
372+
* no-op.
373+
*/
374+
appSwitcher?(): Promise<void>;
375+
/**
376+
* Optional (parity with `home`): presses one TV remote key, a control only TV-capable owners
377+
* carry. An owner without it leaves it undefined; its fact refuses the press before binding,
378+
* and the TV remote binder fails closed rather than resolving an absent member as a silent
379+
* no-op.
380+
*/
381+
tvRemote?(button: TvRemoteButton, durationMs?: number): Promise<void>;
364382
/**
365383
* Optional (parity with `keyboardDismiss`): presses the iPhone/iPad Action Button, hardware only
366384
* the Apple owner carries. An owner without the button leaves it undefined; its fact refuses the
@@ -374,8 +392,17 @@ export type Interactor = {
374392
keyboardDismiss?(): Promise<KeyboardDismissResult>;
375393
/** Optional: platforms with no keyboard-return concept leave it undefined. */
376394
keyboardEnter?(): Promise<KeyboardEnterResult>;
377-
readClipboard(): Promise<string>;
378-
writeClipboard(text: string): Promise<void>;
395+
/**
396+
* Optional (parity with `readSetting`): the owner's pasteboard read. An owner with no clipboard
397+
* surface leaves both halves undefined; its fact refuses the half before binding, and the
398+
* clipboard binder fails closed rather than resolving an absent member as an empty answer.
399+
*/
400+
readClipboard?(): Promise<string>;
401+
/**
402+
* Optional (parity with `readClipboard`): the owner's pasteboard write, with the same
403+
* fact-then-guard contract as the read.
404+
*/
405+
writeClipboard?(text: string): Promise<void>;
379406
setSetting(
380407
setting: string,
381408
state: string,

‎packages/contracts/src/tv-remote-runtime.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test, vi } from 'vitest';
2+
import { AppError } from '@agent-device/kernel/errors';
23
import { bindTvRemote, tvRemoteRuntimeOperationFacts } from './tv-remote-runtime.ts';
34
import type { Interactor } from './interactor-types.ts';
45
import { localInteractorSource } from './interactor-operation-binding.ts';
@@ -39,3 +40,21 @@ test('a local binding drives the interactor with the button and duration', async
3940
// durationMs is the one transposition a point-shaped assertion would not catch.
4041
expect(tvRemote).toHaveBeenCalledWith('down', 250);
4142
});
43+
44+
// Facts admitted the press, so an interactor without the member is an ownership bug the caller
45+
// must see rather than a refusal to degrade around. The label is the one the command used.
46+
test('a press whose fact admitted but whose interactor cannot serve it fails closed', async () => {
47+
const resolveInteractor = vi.fn(async () => ({}) as unknown as Interactor);
48+
49+
const operations = bindTvRemote(
50+
new AbortController().signal,
51+
localInteractorSource({ device, resolveInteractor }),
52+
);
53+
await expect(operations.tvRemote({ button: 'up' })).rejects.toSatisfy(
54+
(error: unknown) =>
55+
error instanceof AppError &&
56+
error.code === 'COMMAND_FAILED' &&
57+
error.details?.['reason'] === 'interactor-method-missing' &&
58+
error.message.includes('tv-remote'),
59+
);
60+
});

‎packages/contracts/src/tv-remote-runtime.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export function tvRemoteRuntimeOperationFacts(
3535
return Object.freeze({ tvRemote: input.tvRemote });
3636
}
3737

38+
/** How a fail-closed refusal names the TV remote to the caller, matching the command. */
39+
export const TV_REMOTE_LABEL = 'tv-remote';
40+
3841
/**
3942
* Captures one selected owner's interactor authority for the lifetime of a request binding. The
4043
* owner is already chosen by the time a binder is called, so each entry point supplies its own
@@ -52,7 +55,11 @@ export function bindTvRemote(
5255
appBundleId: input.options?.appBundleId,
5356
signal,
5457
});
55-
await interactor.tvRemote(input.button, input.durationMs);
58+
// Loaded on the call rather than at module evaluation because this facade's eager closure
59+
// is held at its merge-base size (`eager-closure-budgets`); a static edge would grow it.
60+
const { requireInteractorMethod } = await import('./interactor-operation-binding.ts');
61+
const tvRemote = requireInteractorMethod(interactor.tvRemote, TV_REMOTE_LABEL);
62+
await tvRemote.call(interactor, input.button, input.durationMs);
5663
},
5764
});
5865
}

‎packages/platform-apple/src/__tests__/interactor-runner-provider.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ const RUNNER_TRANSPORT_METHODS: Record<
7171
},
7272
findText: { invoke: (i) => i.findText!('Ready'), runnerCommand: 'findText' },
7373
back: { invoke: (i) => i.back(), runnerCommand: 'backInApp' },
74-
home: { invoke: (i) => i.home(), runnerCommand: 'home' },
74+
home: { invoke: (i) => i.home!(), runnerCommand: 'home' },
7575
setOrientation: { invoke: (i) => i.setOrientation('portrait'), runnerCommand: 'rotate' },
76-
appSwitcher: { invoke: (i) => i.appSwitcher(), runnerCommand: 'appSwitcher' },
76+
appSwitcher: { invoke: (i) => i.appSwitcher!(), runnerCommand: 'appSwitcher' },
7777
actionButton: { invoke: (i) => i.actionButton!(), runnerCommand: 'actionButton' },
78-
tvRemote: { invoke: (i) => i.tvRemote('select'), runnerCommand: 'remotePress' },
78+
tvRemote: { invoke: (i) => i.tvRemote!('select'), runnerCommand: 'remotePress' },
7979
keyboardDismiss: { invoke: (i) => i.keyboardDismiss!(), runnerCommand: 'keyboardDismiss' },
8080
keyboardEnter: { invoke: (i) => i.keyboardEnter!(), runnerCommand: 'keyboardReturn' },
8181
// R59: same reading as `readTextAtPoint` — the macOS-helper branch is reachable only for a
@@ -92,8 +92,8 @@ const LOCAL_TOOL_METHODS: Record<string, (interactor: Interactor) => Promise<unk
9292
openDevice: (i) => i.openDevice(),
9393
close: (i) => i.close('com.example.app'),
9494
screenshot: (i) => i.screenshot('/dev/null'),
95-
readClipboard: (i) => i.readClipboard(),
96-
writeClipboard: (i) => i.writeClipboard('hi'),
95+
readClipboard: (i) => i.readClipboard!(),
96+
writeClipboard: (i) => i.writeClipboard!('hi'),
9797
setSetting: (i) => i.setSetting('wifi', 'on'),
9898
// `simctl ui ... content_size` is local Apple tooling like the write leg beside it, so a provider-owned
9999
// device has no way to answer a text-size read.

‎packages/platform-vega/src/__tests__/interactor.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ beforeEach(() => {
4242
test('tvRemote, back, and home share the Vega remote primitive', async () => {
4343
const interactor = createVegaInteractor(VEGA_VVD, {});
4444

45-
await interactor.tvRemote('left', 250);
45+
await interactor.tvRemote!('left', 250);
4646
await interactor.back('system');
47-
await interactor.home();
47+
await interactor.home!();
4848

4949
assert.deepEqual(mockPressVegaTvRemote.mock.calls, [
5050
[VEGA_VVD, 'left', 250],
@@ -129,9 +129,9 @@ test('required unproven Vega operations share typed unsupported behavior', async
129129
['screenshot', () => interactor.screenshot('/tmp/vega.png')],
130130
['snapshot', () => interactor.snapshot()],
131131
['setOrientation', () => interactor.setOrientation('portrait')],
132-
['appSwitcher', () => interactor.appSwitcher()],
133-
['readClipboard', () => interactor.readClipboard()],
134-
['writeClipboard', () => interactor.writeClipboard('text')],
132+
['appSwitcher', () => interactor.appSwitcher!()],
133+
['readClipboard', () => interactor.readClipboard!()],
134+
['writeClipboard', () => interactor.writeClipboard!('text')],
135135
['setSetting', () => interactor.setSetting('wifi', 'on')],
136136
];
137137

‎packages/provider-limrun/src/interaction-operations.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ import { setTimeout as sleep } from 'node:timers/promises';
4444

4545
const available = Object.freeze({ available: true } as const);
4646
/**
47-
* Limrun's iOS direct session drives text and touch but exposes no portable gesture execution —
48-
* its interactor's own `performGesture` refuses with this wording. Stating it as a fact refuses at
49-
* admission instead of mid-execution (ADR 0019 §6), keeping the agent-facing hint identical.
47+
* Limrun's iOS direct session drives text and touch but exposes no portable gesture execution.
48+
* Stating it as a fact refuses at admission instead of mid-execution (ADR 0019 §6), and it is
49+
* the only refusal the caller sees: the leg's interactor carries no gesture member at all.
5050
*/
5151
const iosGestureUnavailable = Object.freeze({
5252
available: false,
@@ -228,8 +228,9 @@ export function bindLimrunInteractionOperations(
228228
* `back`/`orientation`/`tvRemote` differ by direct-session platform, unlike focus/type:
229229
* the Android leg rides `session.dependencies.android.createInteractor` (`android.ts`) — the
230230
* SAME factory the local Android family binds, so it carries the identical cell table (parity
231-
* with the local owner, including the `device.target === 'tv'` gate for `tvRemote`). The iOS leg
232-
* (`ios.ts`) implements `back`/`setOrientation` but explicitly refuses `tvRemote`.
231+
* with the local owner, including the `device.target === 'tv'` gate for `tvRemote`). The iOS
232+
* direct session is a phone/tablet surface with no remote receiver, so its fact refuses
233+
* `tvRemote` and its interactor carries no such member.
233234
*/
234235
export function limrunNavigationOperationFacts(
235236
device: DeviceInfo,
@@ -267,8 +268,7 @@ export function limrunNavigationOperationFacts(
267268
* `clipboard` shares the split its siblings have: the Android leg rides
268269
* `session.dependencies.android.createInteractor` — the SAME factory the local Android family
269270
* binds, so `cmd clipboard get/set text` reaches the device exactly as it does locally — while
270-
* the iOS leg's own `readClipboard`/`writeClipboard` throw, so both cells stay unavailable there
271-
* and carry the interactor's wording.
271+
* the iOS direct session has no pasteboard transport, so both cells stay unavailable there.
272272
*/
273273
export function limrunClipboardOperationFacts(
274274
device: DeviceInfo,

‎packages/provider-limrun/src/ios.ts‎

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,6 @@ class LimrunIosInteractor implements Interactor {
322322
await this.session.client.pressKey('escape');
323323
}
324324

325-
async home(): Promise<never> {
326-
throw unsupported('home', 'Limrun iOS direct sessions do not expose home yet.');
327-
}
328-
329325
async setOrientation(orientation: DeviceRotation): Promise<void> {
330326
if (orientation === 'portrait-upside-down') {
331327
throw unsupported(
@@ -336,29 +332,6 @@ class LimrunIosInteractor implements Interactor {
336332
await this.session.client.setOrientation(orientation === 'portrait' ? 'Portrait' : 'Landscape');
337333
}
338334

339-
async performGesture(): Promise<never> {
340-
throw unsupported(
341-
'gesture',
342-
'Limrun iOS direct sessions do not expose portable gesture execution yet.',
343-
);
344-
}
345-
346-
async appSwitcher(): Promise<never> {
347-
throw unsupported('app-switcher', 'Limrun iOS direct sessions do not expose app switcher yet.');
348-
}
349-
350-
async tvRemote(): Promise<never> {
351-
throw unsupported('tv-remote', 'Limrun iOS direct sessions do not expose tv remote control.');
352-
}
353-
354-
async readClipboard(): Promise<never> {
355-
throw unsupported('clipboard', 'Limrun iOS direct sessions do not expose clipboard read yet.');
356-
}
357-
358-
async writeClipboard(): Promise<never> {
359-
throw unsupported('clipboard', 'Limrun iOS direct sessions do not expose clipboard write yet.');
360-
}
361-
362335
async setSetting(): Promise<never> {
363336
throw unsupported('settings', 'Limrun iOS direct sessions do not expose settings changes yet.');
364337
}

0 commit comments

Comments
 (0)