Skip to content

Commit 962a318

Browse files
quanruclaude
andcommitted
feat(playground): add device options configuration for Android/iOS
This commit implements device-specific configuration options in the playground UI, allowing users to customize device behavior such as keyboard handling and IME strategy. Changes: - Add device options state management with localStorage persistence - Create UI controls for Android-specific options (imeStrategy, autoDismissKeyboard, keyboardDismissStrategy, alwaysRefreshScreenInfo) - Create UI controls for iOS-specific options (autoDismissKeyboard) - Extend execution pipeline to pass deviceOptions from frontend to backend - Update agent.interface.options on the server side when deviceOptions are received - Optimize parameter flattening to avoid delete operator performance issues Technical implementation: - Frontend: Store device options in Zustand with localStorage sync - SDK: Include deviceOptions in remote execution adapter payload - Server: Update agent.interface.options to apply settings globally - This ensures all actions (including those called by aiAct) use the updated options Fixes #1282 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 8b2ff8b commit 962a318

File tree

12 files changed

+266
-8
lines changed

12 files changed

+266
-8
lines changed

apps/android-playground/src/components/playground-panel/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export default function PlaygroundPanel() {
7777
enableScrollToBottom: true,
7878
serverMode: true,
7979
showEnvConfigReminder: true,
80+
deviceType: 'android',
8081
}}
8182
branding={{
8283
title: 'Android Playground',

packages/playground/src/adapters/remote-execution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export class RemoteExecutionAdapter extends BasePlaygroundAdapter {
191191
{ key: 'deepThink', value: options.deepThink },
192192
{ key: 'screenshotIncluded', value: options.screenshotIncluded },
193193
{ key: 'domIncluded', value: options.domIncluded },
194+
{ key: 'deviceOptions', value: options.deviceOptions },
194195
{ key: 'params', value: value.params },
195196
] as const;
196197

packages/playground/src/common.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ export async function parseStructuredParams(
5151
? Object.keys((schema as { shape: Record<string, unknown> }).shape)
5252
: [];
5353

54-
const paramObj: Record<string, unknown> = { ...options };
54+
// Start with options and merge deviceOptions into the same level
55+
// Destructure to exclude deviceOptions from the final object
56+
const { deviceOptions: _, ...optionsWithoutDeviceOptions } = options;
57+
const paramObj: Record<string, unknown> = {
58+
...optionsWithoutDeviceOptions,
59+
...(options.deviceOptions || {}),
60+
};
5561

5662
keys.forEach((key) => {
5763
if (
@@ -186,10 +192,19 @@ export async function executeAction(
186192
})
187193
: undefined;
188194

189-
return await activeAgent.callActionInActionSpace(action.name, {
195+
// Flatten deviceOptions into the params
196+
// Destructure to exclude deviceOptions from the final object
197+
const { deviceOptions: _, ...optionsWithoutDeviceOptions } = options;
198+
const actionParams = {
190199
locate: detailedLocateParam,
191-
...options,
192-
});
200+
...optionsWithoutDeviceOptions,
201+
...(options.deviceOptions || {}),
202+
};
203+
204+
return await activeAgent.callActionInActionSpace(
205+
action.name,
206+
actionParams,
207+
);
193208
}
194209
} else {
195210
const prompt = value.prompt;

packages/playground/src/server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ class PlaygroundServer {
315315
deepThink,
316316
screenshotIncluded,
317317
domIncluded,
318+
deviceOptions,
318319
} = req.body;
319320

320321
if (!type) {
@@ -323,6 +324,18 @@ class PlaygroundServer {
323324
});
324325
}
325326

327+
// Update device options if provided
328+
if (
329+
deviceOptions &&
330+
this.agent.interface &&
331+
'options' in this.agent.interface
332+
) {
333+
this.agent.interface.options = {
334+
...(this.agent.interface.options || {}),
335+
...deviceOptions,
336+
};
337+
}
338+
326339
// Check if another task is running
327340
if (this.currentTaskId) {
328341
return res.status(409).json({
@@ -376,6 +389,7 @@ class PlaygroundServer {
376389
deepThink,
377390
screenshotIncluded,
378391
domIncluded,
392+
deviceOptions,
379393
},
380394
);
381395
} catch (error: unknown) {

packages/playground/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ export interface ServerResponse {
2323
error?: string;
2424
}
2525

26+
export interface DeviceOptions {
27+
imeStrategy?: 'always-yadb' | 'yadb-for-non-ascii';
28+
autoDismissKeyboard?: boolean;
29+
keyboardDismissStrategy?: 'esc-first' | 'back-first';
30+
alwaysRefreshScreenInfo?: boolean;
31+
}
32+
2633
export interface ExecutionOptions {
2734
deepThink?: boolean;
2835
screenshotIncluded?: boolean;
2936
domIncluded?: boolean | 'visible-only';
3037
context?: any;
3138
requestId?: string;
39+
deviceOptions?: DeviceOptions;
3240
}
3341

3442
// Extended web types for playground

packages/visualizer/src/component/config-selector/index.tsx

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { Checkbox, Dropdown, type MenuProps, Radio } from 'antd';
22
import type React from 'react';
33
import SettingOutlined from '../../icons/setting.svg';
44
import { useEnvConfig } from '../../store/store';
5+
import type { DeviceType } from '../../types';
56
import {
7+
alwaysRefreshScreenInfoTip,
8+
autoDismissKeyboardTip,
69
deepThinkTip,
710
domIncludedTip,
11+
imeStrategyTip,
12+
keyboardDismissStrategyTip,
813
screenshotIncludedTip,
914
trackingTip,
1015
} from '../../utils/constants';
@@ -14,13 +19,15 @@ interface ConfigSelectorProps {
1419
enableTracking: boolean;
1520
showDataExtractionOptions: boolean;
1621
hideDomAndScreenshotOptions?: boolean; // Hide domIncluded and screenshotIncluded options
22+
deviceType?: DeviceType;
1723
}
1824

1925
export const ConfigSelector: React.FC<ConfigSelectorProps> = ({
2026
showDeepThinkOption = false,
2127
enableTracking = false,
2228
showDataExtractionOptions = false,
2329
hideDomAndScreenshotOptions = false,
30+
deviceType,
2431
}) => {
2532
const forceSameTabNavigation = useEnvConfig(
2633
(state) => state.forceSameTabNavigation,
@@ -37,7 +44,36 @@ export const ConfigSelector: React.FC<ConfigSelectorProps> = ({
3744
const domIncluded = useEnvConfig((state) => state.domIncluded);
3845
const setDomIncluded = useEnvConfig((state) => state.setDomIncluded);
3946

40-
if (!enableTracking && !showDeepThinkOption && !showDataExtractionOptions) {
47+
// Device-specific configuration
48+
const imeStrategy = useEnvConfig((state) => state.imeStrategy);
49+
const setImeStrategy = useEnvConfig((state) => state.setImeStrategy);
50+
const autoDismissKeyboard = useEnvConfig(
51+
(state) => state.autoDismissKeyboard,
52+
);
53+
const setAutoDismissKeyboard = useEnvConfig(
54+
(state) => state.setAutoDismissKeyboard,
55+
);
56+
const keyboardDismissStrategy = useEnvConfig(
57+
(state) => state.keyboardDismissStrategy,
58+
);
59+
const setKeyboardDismissStrategy = useEnvConfig(
60+
(state) => state.setKeyboardDismissStrategy,
61+
);
62+
const alwaysRefreshScreenInfo = useEnvConfig(
63+
(state) => state.alwaysRefreshScreenInfo,
64+
);
65+
const setAlwaysRefreshScreenInfo = useEnvConfig(
66+
(state) => state.setAlwaysRefreshScreenInfo,
67+
);
68+
69+
const hasDeviceOptions = deviceType === 'android' || deviceType === 'ios';
70+
71+
if (
72+
!enableTracking &&
73+
!showDeepThinkOption &&
74+
!showDataExtractionOptions &&
75+
!hasDeviceOptions
76+
) {
4177
return null;
4278
}
4379

@@ -120,6 +156,86 @@ export const ConfigSelector: React.FC<ConfigSelectorProps> = ({
120156
});
121157
}
122158

159+
// Android-specific options
160+
if (deviceType === 'android') {
161+
items.push({
162+
label: (
163+
<div style={{ padding: '4px 0' }}>
164+
<div style={{ marginBottom: '4px', fontSize: '14px' }}>
165+
{imeStrategyTip}
166+
</div>
167+
<Radio.Group
168+
size="small"
169+
value={imeStrategy}
170+
onChange={(e) => setImeStrategy(e.target.value)}
171+
>
172+
<Radio value="always-yadb">Always YADB</Radio>
173+
<Radio value="yadb-for-non-ascii">YADB for non-ASCII</Radio>
174+
</Radio.Group>
175+
</div>
176+
),
177+
key: 'ime-strategy-config',
178+
});
179+
180+
items.push({
181+
label: (
182+
<Checkbox
183+
onChange={(e) => setAutoDismissKeyboard(e.target.checked)}
184+
checked={autoDismissKeyboard}
185+
>
186+
{autoDismissKeyboardTip}
187+
</Checkbox>
188+
),
189+
key: 'auto-dismiss-keyboard-config',
190+
});
191+
192+
items.push({
193+
label: (
194+
<div style={{ padding: '4px 0' }}>
195+
<div style={{ marginBottom: '4px', fontSize: '14px' }}>
196+
{keyboardDismissStrategyTip}
197+
</div>
198+
<Radio.Group
199+
size="small"
200+
value={keyboardDismissStrategy}
201+
onChange={(e) => setKeyboardDismissStrategy(e.target.value)}
202+
>
203+
<Radio value="esc-first">ESC first</Radio>
204+
<Radio value="back-first">Back first</Radio>
205+
</Radio.Group>
206+
</div>
207+
),
208+
key: 'keyboard-dismiss-strategy-config',
209+
});
210+
211+
items.push({
212+
label: (
213+
<Checkbox
214+
onChange={(e) => setAlwaysRefreshScreenInfo(e.target.checked)}
215+
checked={alwaysRefreshScreenInfo}
216+
>
217+
{alwaysRefreshScreenInfoTip}
218+
</Checkbox>
219+
),
220+
key: 'always-refresh-screen-info-config',
221+
});
222+
}
223+
224+
// iOS-specific options
225+
if (deviceType === 'ios') {
226+
items.push({
227+
label: (
228+
<Checkbox
229+
onChange={(e) => setAutoDismissKeyboard(e.target.checked)}
230+
checked={autoDismissKeyboard}
231+
>
232+
{autoDismissKeyboardTip}
233+
</Checkbox>
234+
),
235+
key: 'auto-dismiss-keyboard-config',
236+
});
237+
}
238+
123239
return items;
124240
}
125241
};

packages/visualizer/src/component/prompt-input/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import React, {
1212
} from 'react';
1313
import type { HistoryItem } from '../../store/history';
1414
import { useHistoryStore } from '../../store/history';
15-
import type { RunType } from '../../types';
15+
import type { DeviceType, RunType } from '../../types';
1616
import type { ServiceModeType } from '../../types';
1717
import {
1818
type FormParams,
@@ -57,6 +57,7 @@ interface PromptInputProps {
5757
clearPromptAfterRun?: boolean;
5858
hideDomAndScreenshotOptions?: boolean; // Hide domIncluded and screenshotIncluded options
5959
actionSpace: DeviceAction<any>[]; // Required actionSpace for dynamic parameter detection
60+
deviceType?: DeviceType;
6061
}
6162

6263
export const PromptInput: React.FC<PromptInputProps> = ({
@@ -72,6 +73,7 @@ export const PromptInput: React.FC<PromptInputProps> = ({
7273
clearPromptAfterRun = true,
7374
actionSpace,
7475
hideDomAndScreenshotOptions = false,
76+
deviceType,
7577
}) => {
7678
const [hoveringSettings, setHoveringSettings] = useState(false);
7779
const [promptValue, setPromptValue] = useState('');
@@ -202,12 +204,14 @@ export const PromptInput: React.FC<PromptInputProps> = ({
202204
const hasDeepThink = showDeepThinkOption;
203205
const hasDataExtraction =
204206
showDataExtractionOptions && !hideDomAndScreenshotOptions;
205-
return hasTracking || hasDeepThink || hasDataExtraction;
207+
const hasDeviceOptions = deviceType === 'android' || deviceType === 'ios';
208+
return hasTracking || hasDeepThink || hasDataExtraction || hasDeviceOptions;
206209
}, [
207210
serviceMode,
208211
showDeepThinkOption,
209212
showDataExtractionOptions,
210213
hideDomAndScreenshotOptions,
214+
deviceType,
211215
]);
212216

213217
// Get available methods for dropdown (filtered by actionSpace)
@@ -1026,6 +1030,7 @@ export const PromptInput: React.FC<PromptInputProps> = ({
10261030
showDeepThinkOption={showDeepThinkOption}
10271031
showDataExtractionOptions={showDataExtractionOptions}
10281032
hideDomAndScreenshotOptions={hideDomAndScreenshotOptions}
1033+
deviceType={deviceType}
10291034
/>
10301035
</div>
10311036
)}

packages/visualizer/src/component/universal-playground/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export function UniversalPlayground({
184184
showContextPreview && componentConfig.showContextPreview !== false;
185185
const layout = componentConfig.layout || 'vertical';
186186
const showVersionInfo = componentConfig.showVersionInfo !== false;
187+
const deviceType = componentConfig.deviceType;
187188

188189
return (
189190
<div className={`playground-container ${layout}-mode ${className}`.trim()}>
@@ -374,6 +375,7 @@ export function UniversalPlayground({
374375
onRun={handleFormRun}
375376
onStop={handleStop}
376377
actionSpace={actionSpace}
378+
deviceType={deviceType}
377379
/>
378380
</div>
379381

packages/visualizer/src/hooks/usePlaygroundExecution.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ export function usePlaygroundExecution(
3030
interruptedFlagRef: React.MutableRefObject<Record<number, boolean>>,
3131
) {
3232
// Get execution options from environment config
33-
const { deepThink, screenshotIncluded, domIncluded } = useEnvConfig();
33+
const {
34+
deepThink,
35+
screenshotIncluded,
36+
domIncluded,
37+
imeStrategy,
38+
autoDismissKeyboard,
39+
keyboardDismissStrategy,
40+
alwaysRefreshScreenInfo,
41+
} = useEnvConfig();
3442
// Handle form submission and execution
3543
const handleRun = useCallback(
3644
async (value: FormValue) => {
@@ -109,11 +117,19 @@ export function usePlaygroundExecution(
109117
}
110118

111119
// Execute the action using the SDK
120+
const deviceOptionsToSend = {
121+
imeStrategy,
122+
autoDismissKeyboard,
123+
keyboardDismissStrategy,
124+
alwaysRefreshScreenInfo,
125+
};
126+
112127
result.result = await playgroundSDK.executeAction(actionType, value, {
113128
requestId: thisRunningId.toString(),
114129
deepThink,
115130
screenshotIncluded,
116131
domIncluded,
132+
deviceOptions: deviceOptionsToSend,
117133
});
118134

119135
// For some adapters, result might already include dump and reportHTML
@@ -213,6 +229,10 @@ export function usePlaygroundExecution(
213229
deepThink,
214230
screenshotIncluded,
215231
domIncluded,
232+
imeStrategy,
233+
autoDismissKeyboard,
234+
keyboardDismissStrategy,
235+
alwaysRefreshScreenInfo,
216236
],
217237
);
218238

0 commit comments

Comments
 (0)