@anionex/dsh-computer-use 0.1.0
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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +307 -0
- package/README.zh.md +307 -0
- package/assets/computer-use-fixture.png +0 -0
- package/assets/cursor.png +0 -0
- package/cordis.patch.yml +13 -0
- package/docs/interaction-policy.i18n.yaml +4 -0
- package/docs/interaction-policy.md +140 -0
- package/docs/interaction-policy.zh.md +140 -0
- package/lib/approval-policy.js +12 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/artifacts.js +79 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/backend.js +3 -0
- package/lib/backend.js.map +1 -0
- package/lib/client.js +349 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +120 -0
- package/lib/config.js.map +1 -0
- package/lib/confirmations.js +99 -0
- package/lib/confirmations.js.map +1 -0
- package/lib/diff.js +58 -0
- package/lib/diff.js.map +1 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +231 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/leases.js +259 -0
- package/lib/leases.js.map +1 -0
- package/lib/providers/macos.js +148 -0
- package/lib/providers/macos.js.map +1 -0
- package/lib/providers/native-helper.js +356 -0
- package/lib/providers/native-helper.js.map +1 -0
- package/lib/service.js +480 -0
- package/lib/service.js.map +1 -0
- package/lib/skill.js +119 -0
- package/lib/skill.js.map +1 -0
- package/lib/target-resolver.js +180 -0
- package/lib/target-resolver.js.map +1 -0
- package/lib/tools.js +493 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/approval-policy.d.ts +13 -0
- package/lib/types/approval-policy.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +10 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/backend.d.ts +94 -0
- package/lib/types/backend.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +78 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/config.d.ts +75 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/confirmations.d.ts +23 -0
- package/lib/types/confirmations.d.ts.map +1 -0
- package/lib/types/diff.d.ts +5 -0
- package/lib/types/diff.d.ts.map +1 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +30 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/leases.d.ts +80 -0
- package/lib/types/leases.d.ts.map +1 -0
- package/lib/types/providers/macos.d.ts +15 -0
- package/lib/types/providers/macos.d.ts.map +1 -0
- package/lib/types/providers/native-helper.d.ts +36 -0
- package/lib/types/providers/native-helper.d.ts.map +1 -0
- package/lib/types/service.d.ts +52 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/skill.d.ts +9 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/target-resolver.d.ts +38 -0
- package/lib/types/target-resolver.d.ts.map +1 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +246 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/web.d.ts +32 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/web.js +163 -0
- package/lib/web.js.map +1 -0
- package/native/macos/Sources/Fixture/main.swift +366 -0
- package/native/macos/Sources/Helper/CursorImage.swift +11 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
- package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
- package/native/macos/Sources/Helper/main.swift +1140 -0
- package/native/macos/Sources/Monitor/main.swift +139 -0
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +14 -0
- package/package.json +128 -0
- package/scripts/build-client.mjs +24 -0
- package/scripts/build-native.mjs +158 -0
- package/scripts/check-native.mjs +92 -0
- package/scripts/model-e2e.mjs +291 -0
- package/scripts/session-transcript.mjs +117 -0
- package/scripts/validate.mjs +256 -0
- package/src/approval-policy.ts +16 -0
- package/src/artifacts.ts +93 -0
- package/src/backend.ts +103 -0
- package/src/client/index.tsx +539 -0
- package/src/config.ts +196 -0
- package/src/confirmations.ts +128 -0
- package/src/diff.ts +52 -0
- package/src/errors.ts +39 -0
- package/src/exposure.ts +236 -0
- package/src/index.ts +66 -0
- package/src/leases.ts +337 -0
- package/src/providers/macos.ts +185 -0
- package/src/providers/native-helper.ts +393 -0
- package/src/service.ts +583 -0
- package/src/skill.ts +123 -0
- package/src/target-resolver.ts +237 -0
- package/src/tools.ts +528 -0
- package/src/types.ts +299 -0
- package/src/web.ts +180 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/** DSH Computer Use browser plugin: provider health, permissions, limits, and app policy. */
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useSyncExternalStore, type ReactNode } from 'react'
|
|
4
|
+
import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
5
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
6
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
7
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
8
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
9
|
+
import type {} from '@deepseek-ai/dsh-settings/types'
|
|
10
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
|
+
|
|
12
|
+
const NS = 'computer-use'
|
|
13
|
+
const ROUTE = '/_dsh/computer-use/settings'
|
|
14
|
+
|
|
15
|
+
const en = {
|
|
16
|
+
nav: 'Computer Use',
|
|
17
|
+
title: 'macOS Computer Use',
|
|
18
|
+
intro: 'Inspect the native helper, macOS privacy permissions, foreground/targeted-input policy, observation limits, and exact per-app read/control grants.',
|
|
19
|
+
pluginKind: 'DSH native plugin',
|
|
20
|
+
privacy: 'macOS privacy',
|
|
21
|
+
access: 'Application access',
|
|
22
|
+
accessHint: 'Choose whether Computer Use may work with every app. Exact per-app rules remain available under Advanced settings.',
|
|
23
|
+
advanced: 'Advanced options',
|
|
24
|
+
advancedHint: 'Limits, helper path, cursor timing, and application grants.',
|
|
25
|
+
cursorTiming: 'Cursor timing',
|
|
26
|
+
helper: 'Native helper',
|
|
27
|
+
helperUnknown: 'Unknown',
|
|
28
|
+
ready: 'Ready',
|
|
29
|
+
unavailable: 'Unavailable',
|
|
30
|
+
generation: 'Applied in this run',
|
|
31
|
+
generationValue: '{generation} times',
|
|
32
|
+
accessibility: 'Accessibility',
|
|
33
|
+
screenRecording: 'Screen Recording',
|
|
34
|
+
granted: 'Granted',
|
|
35
|
+
denied: 'Needs permission',
|
|
36
|
+
openSettings: 'Open macOS Settings',
|
|
37
|
+
refresh: 'Refresh health',
|
|
38
|
+
limits: 'Observation and action limits',
|
|
39
|
+
ttl: 'Observation TTL (ms; 0 = no expiry)',
|
|
40
|
+
confirmationTtl: 'Confirmation TTL (ms)',
|
|
41
|
+
actionTimeout: 'Action timeout (ms)',
|
|
42
|
+
settle: 'Settlement interval (ms)',
|
|
43
|
+
maxSettle: 'Maximum settlement (ms)',
|
|
44
|
+
maxNodes: 'Maximum AX nodes',
|
|
45
|
+
maxDepth: 'Maximum AX depth',
|
|
46
|
+
maxText: 'Maximum AX text bytes',
|
|
47
|
+
maxScreenshot: 'Maximum screenshot bytes',
|
|
48
|
+
artifactRoot: 'Artifact root',
|
|
49
|
+
helperPath: 'External helper path',
|
|
50
|
+
sourceBuild: 'Allow explicit source-build fallback',
|
|
51
|
+
interaction: 'Foreground and targeted input',
|
|
52
|
+
interactionHint: 'The default route sends pointer and keyboard events only to the selected process. It does not move the system cursor or activate the app.',
|
|
53
|
+
focusPolicy: 'Foreground policy',
|
|
54
|
+
focusPreserve: 'Preserve current foreground app',
|
|
55
|
+
focusActivate: 'Allow activating the target app',
|
|
56
|
+
keyboardPolicy: 'Keyboard policy',
|
|
57
|
+
keyboardPreserve: 'Preserve foreground; typing compatibility varies',
|
|
58
|
+
keyboardActivate: 'Activate the target app before typing',
|
|
59
|
+
pointerInputPolicy: 'Target-process pointer input',
|
|
60
|
+
pointerDeny: 'Deny mouse, drag, and wheel events',
|
|
61
|
+
pointerAllow: 'Route events only to the target process',
|
|
62
|
+
cursorVisualization: 'Agent cursor',
|
|
63
|
+
cursorVisible: 'Show a separate click-through Agent cursor',
|
|
64
|
+
cursorHidden: 'Hide the Agent cursor',
|
|
65
|
+
cursorMotion: 'Cursor travel (ms)',
|
|
66
|
+
cursorAutoHide: 'Cursor auto-hide (ms; 0 = stay visible)',
|
|
67
|
+
grants: 'Application grants',
|
|
68
|
+
grantsHint: 'One exact bundle id per line, followed by read or read,control. Wildcards are rejected.',
|
|
69
|
+
allowAllApps: 'Allow read and control for every app',
|
|
70
|
+
allowAllAppsHint: 'When enabled, exact grants are ignored and every running app is readable and controllable.',
|
|
71
|
+
save: 'Save and apply',
|
|
72
|
+
saving: 'Applying...',
|
|
73
|
+
saved: 'Settings applied.',
|
|
74
|
+
readOnly: 'The current Settings provider is read-only.',
|
|
75
|
+
loading: 'Loading Computer Use settings...',
|
|
76
|
+
retry: 'Retry',
|
|
77
|
+
} as const
|
|
78
|
+
|
|
79
|
+
type LocaleKey = keyof typeof en
|
|
80
|
+
const zh: Record<LocaleKey, string> = {
|
|
81
|
+
nav: '电脑操作',
|
|
82
|
+
title: '电脑操作',
|
|
83
|
+
intro: '让智能体读取并操作 macOS 应用。这里可以检查系统权限、设置应用范围,并按需调整操作方式。',
|
|
84
|
+
pluginKind: 'DSH 原生插件',
|
|
85
|
+
privacy: '系统权限检查',
|
|
86
|
+
access: '应用访问范围',
|
|
87
|
+
accessHint: '日常使用只需决定是否允许操作所有应用;指定应用规则可在高级设置中配置。',
|
|
88
|
+
advanced: '高级设置',
|
|
89
|
+
advancedHint: '操作方式、性能限制、光标效果、指定应用规则和运行组件。一般无需修改。',
|
|
90
|
+
cursorTiming: '光标效果',
|
|
91
|
+
helper: '运行组件版本',
|
|
92
|
+
helperUnknown: '未提供',
|
|
93
|
+
ready: '已就绪',
|
|
94
|
+
unavailable: '不可用',
|
|
95
|
+
generation: '本次运行已应用',
|
|
96
|
+
generationValue: '{generation} 次',
|
|
97
|
+
accessibility: '辅助功能',
|
|
98
|
+
screenRecording: '屏幕录制',
|
|
99
|
+
granted: '已授权',
|
|
100
|
+
denied: '需要授权',
|
|
101
|
+
openSettings: '打开 macOS 设置',
|
|
102
|
+
refresh: '重新检查',
|
|
103
|
+
limits: '性能与安全限制',
|
|
104
|
+
ttl: '界面识别结果有效期(毫秒;0 表示一直有效)',
|
|
105
|
+
confirmationTtl: '操作确认有效期(毫秒)',
|
|
106
|
+
actionTimeout: '动作超时(毫秒)',
|
|
107
|
+
settle: '操作完成后的检查间隔(毫秒)',
|
|
108
|
+
maxSettle: '等待界面稳定的最长时间(毫秒)',
|
|
109
|
+
maxNodes: '最多读取的界面元素数',
|
|
110
|
+
maxDepth: '界面结构层级上限',
|
|
111
|
+
maxText: '界面文字大小上限(字节)',
|
|
112
|
+
maxScreenshot: '截图大小上限(字节)',
|
|
113
|
+
artifactRoot: '生成文件目录',
|
|
114
|
+
helperPath: '自定义运行组件路径',
|
|
115
|
+
sourceBuild: '找不到运行组件时允许从源码构建',
|
|
116
|
+
interaction: '操作方式',
|
|
117
|
+
interactionHint: '默认只操作选定的应用,不移动你的系统光标,也不会主动切换当前应用。',
|
|
118
|
+
focusPolicy: '需要操作窗口时',
|
|
119
|
+
focusPreserve: '不主动切换当前应用',
|
|
120
|
+
focusActivate: '允许切换到目标应用',
|
|
121
|
+
keyboardPolicy: '输入文字前',
|
|
122
|
+
keyboardPreserve: '保持当前应用(部分应用可能无法输入)',
|
|
123
|
+
keyboardActivate: '先切换到目标应用',
|
|
124
|
+
pointerInputPolicy: '鼠标操作',
|
|
125
|
+
pointerDeny: '不允许点击、拖动和滚动',
|
|
126
|
+
pointerAllow: '只操作选定的应用',
|
|
127
|
+
cursorVisualization: '智能体光标',
|
|
128
|
+
cursorVisible: '显示单独的智能体光标',
|
|
129
|
+
cursorHidden: '隐藏智能体光标',
|
|
130
|
+
cursorMotion: '光标移动时长(毫秒)',
|
|
131
|
+
cursorAutoHide: '光标自动隐藏(毫秒;0 = 保持显示)',
|
|
132
|
+
grants: '指定应用规则',
|
|
133
|
+
grantsHint: '每行填写一个应用标识,后接 read 或 read,control。应用标识必须完整,不能使用通配符。',
|
|
134
|
+
allowAllApps: '允许读取和操作所有应用',
|
|
135
|
+
allowAllAppsHint: '开启后无需逐个添加应用。关闭后,可在高级设置中填写允许访问的应用。',
|
|
136
|
+
save: '保存设置',
|
|
137
|
+
saving: '正在保存...',
|
|
138
|
+
saved: '设置已生效。',
|
|
139
|
+
readOnly: '当前配置为只读,无法在这里修改。',
|
|
140
|
+
loading: '正在加载电脑操作设置...',
|
|
141
|
+
retry: '重试',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type Translate = (key: LocaleKey, params?: Record<string, string | number>) => string
|
|
145
|
+
|
|
146
|
+
interface ConfigValue {
|
|
147
|
+
observationTtlMs?: number
|
|
148
|
+
confirmationTtlMs?: number
|
|
149
|
+
actionTimeoutMs?: number
|
|
150
|
+
settleMs?: number
|
|
151
|
+
maxSettleMs?: number
|
|
152
|
+
maxNodes?: number
|
|
153
|
+
maxDepth?: number
|
|
154
|
+
maxTextBytes?: number
|
|
155
|
+
maxScreenshotBytes?: number
|
|
156
|
+
artifactRoot?: string
|
|
157
|
+
helper?: { path?: string; allowSourceBuild?: boolean }
|
|
158
|
+
interaction?: {
|
|
159
|
+
focusPolicy?: 'preserve' | 'activate'
|
|
160
|
+
keyboardPolicy?: 'preserve' | 'activate'
|
|
161
|
+
pointerInputPolicy?: 'deny' | 'targeted'
|
|
162
|
+
cursorVisualization?: 'hidden' | 'visible'
|
|
163
|
+
cursorMotionMs?: number
|
|
164
|
+
cursorAutoHideMs?: number
|
|
165
|
+
}
|
|
166
|
+
allowAllApps?: boolean
|
|
167
|
+
grants?: Array<{ bundleId: string; read?: boolean; control?: boolean }>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface Snapshot {
|
|
171
|
+
schemaVersion: 1
|
|
172
|
+
writable: boolean
|
|
173
|
+
settings: { value: ConfigValue; revision: number; applies: 'live' }
|
|
174
|
+
provider: {
|
|
175
|
+
platform: string
|
|
176
|
+
provider: string
|
|
177
|
+
generation: number
|
|
178
|
+
ready: boolean
|
|
179
|
+
helperPath: string
|
|
180
|
+
helperVersion?: string
|
|
181
|
+
helperSha256?: string
|
|
182
|
+
accessibility: string
|
|
183
|
+
screenRecording: string
|
|
184
|
+
lastError?: string
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface ControllerState {
|
|
189
|
+
status: 'idle' | 'loading' | 'ready' | 'error'
|
|
190
|
+
action?: 'save' | 'health' | 'open'
|
|
191
|
+
snapshot?: Snapshot
|
|
192
|
+
error?: string
|
|
193
|
+
notice?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
197
|
+
interface LocaleNamespaceMap {
|
|
198
|
+
/** DSH Computer Use Settings copy. */
|
|
199
|
+
'computer-use': LocaleKey
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class ComputerUseSettingsController {
|
|
204
|
+
private state: ControllerState = { status: 'idle' }
|
|
205
|
+
private readonly listeners = new Set<() => void>()
|
|
206
|
+
readonly subscribe = (listener: () => void): (() => void) => {
|
|
207
|
+
this.listeners.add(listener)
|
|
208
|
+
return () => { this.listeners.delete(listener) }
|
|
209
|
+
}
|
|
210
|
+
readonly snapshot = (): ControllerState => this.state
|
|
211
|
+
|
|
212
|
+
private publish(next: ControllerState): void {
|
|
213
|
+
this.state = next
|
|
214
|
+
for (const listener of this.listeners) listener()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async load(): Promise<void> {
|
|
218
|
+
this.publish({
|
|
219
|
+
status: 'loading',
|
|
220
|
+
...(this.state.snapshot === undefined ? {} : { snapshot: this.state.snapshot }),
|
|
221
|
+
})
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(ROUTE, { credentials: 'same-origin', cache: 'no-store' })
|
|
224
|
+
const body = await response.json() as { ok: boolean; value?: Snapshot; error?: { message?: string } }
|
|
225
|
+
if (!response.ok || body.ok !== true || body.value === undefined) throw new Error(body.error?.message ?? `HTTP ${response.status}`)
|
|
226
|
+
this.publish({ status: 'ready', snapshot: body.value })
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.publish({ status: 'error', error: error instanceof Error ? error.message : String(error) })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async action(
|
|
233
|
+
action: 'save' | 'health' | 'open-settings',
|
|
234
|
+
payload: Record<string, unknown>,
|
|
235
|
+
marker: NonNullable<ControllerState['action']>,
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
this.publish({
|
|
238
|
+
status: this.state.status,
|
|
239
|
+
action: marker,
|
|
240
|
+
...(this.state.snapshot === undefined ? {} : { snapshot: this.state.snapshot }),
|
|
241
|
+
})
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(ROUTE, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
credentials: 'same-origin',
|
|
246
|
+
headers: { 'content-type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ action, ...payload }),
|
|
248
|
+
})
|
|
249
|
+
const body = await response.json() as { ok: boolean; value?: Snapshot; error?: { message?: string } }
|
|
250
|
+
if (!response.ok || body.ok !== true || body.value === undefined) throw new Error(body.error?.message ?? `HTTP ${response.status}`)
|
|
251
|
+
this.publish({
|
|
252
|
+
status: 'ready',
|
|
253
|
+
snapshot: body.value,
|
|
254
|
+
...(action === 'save' ? { notice: 'saved' } : {}),
|
|
255
|
+
})
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.publish({
|
|
258
|
+
status: 'ready',
|
|
259
|
+
...(this.state.snapshot === undefined ? {} : { snapshot: this.state.snapshot }),
|
|
260
|
+
error: error instanceof Error ? error.message : String(error),
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
refreshIfLoaded(): void {
|
|
266
|
+
if (this.state.status !== 'idle') void this.load()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface Draft {
|
|
271
|
+
observationTtlMs: string
|
|
272
|
+
confirmationTtlMs: string
|
|
273
|
+
actionTimeoutMs: string
|
|
274
|
+
settleMs: string
|
|
275
|
+
maxSettleMs: string
|
|
276
|
+
maxNodes: string
|
|
277
|
+
maxDepth: string
|
|
278
|
+
maxTextBytes: string
|
|
279
|
+
maxScreenshotBytes: string
|
|
280
|
+
artifactRoot: string
|
|
281
|
+
helperPath: string
|
|
282
|
+
allowSourceBuild: boolean
|
|
283
|
+
focusPolicy: 'preserve' | 'activate'
|
|
284
|
+
keyboardPolicy: 'preserve' | 'activate'
|
|
285
|
+
pointerInputPolicy: 'deny' | 'targeted'
|
|
286
|
+
cursorVisualization: 'hidden' | 'visible'
|
|
287
|
+
cursorMotionMs: string
|
|
288
|
+
cursorAutoHideMs: string
|
|
289
|
+
allowAllApps: boolean
|
|
290
|
+
grants: string
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function draftOf(value: ConfigValue): Draft {
|
|
294
|
+
return {
|
|
295
|
+
observationTtlMs: String(value.observationTtlMs ?? 0),
|
|
296
|
+
confirmationTtlMs: String(value.confirmationTtlMs ?? 300000),
|
|
297
|
+
actionTimeoutMs: String(value.actionTimeoutMs ?? 15000),
|
|
298
|
+
settleMs: String(value.settleMs ?? 250),
|
|
299
|
+
maxSettleMs: String(value.maxSettleMs ?? 5000),
|
|
300
|
+
maxNodes: String(value.maxNodes ?? 500),
|
|
301
|
+
maxDepth: String(value.maxDepth ?? 14),
|
|
302
|
+
maxTextBytes: String(value.maxTextBytes ?? 64000),
|
|
303
|
+
maxScreenshotBytes: String(value.maxScreenshotBytes ?? 33554432),
|
|
304
|
+
artifactRoot: value.artifactRoot ?? '.dsh-computer-use/artifacts',
|
|
305
|
+
helperPath: value.helper?.path ?? '',
|
|
306
|
+
allowSourceBuild: value.helper?.allowSourceBuild ?? false,
|
|
307
|
+
focusPolicy: value.interaction?.focusPolicy ?? 'preserve',
|
|
308
|
+
keyboardPolicy: value.interaction?.keyboardPolicy ?? 'preserve',
|
|
309
|
+
pointerInputPolicy: value.interaction?.pointerInputPolicy ?? 'targeted',
|
|
310
|
+
cursorVisualization: value.interaction?.cursorVisualization ?? 'visible',
|
|
311
|
+
cursorMotionMs: String(value.interaction?.cursorMotionMs ?? 180),
|
|
312
|
+
cursorAutoHideMs: String(value.interaction?.cursorAutoHideMs ?? 0),
|
|
313
|
+
allowAllApps: value.allowAllApps ?? false,
|
|
314
|
+
grants: (value.grants ?? []).map(grant => `${grant.bundleId} ${grant.control === true ? 'read,control' : 'read'}`).join('\n'),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function integer(value: string, name: string): number {
|
|
319
|
+
const parsed = Number(value)
|
|
320
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error(`${name} must be a non-negative integer`)
|
|
321
|
+
return parsed
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function configOf(draft: Draft): ConfigValue {
|
|
325
|
+
const grants = draft.grants.split(/\r?\n/u).map(line => line.trim()).filter(Boolean).map((line) => {
|
|
326
|
+
const [bundleId, rawScopes, ...extra] = line.split(/\s+/u)
|
|
327
|
+
if (bundleId === undefined || rawScopes === undefined || extra.length > 0) throw new Error(`invalid grant line: ${line}`)
|
|
328
|
+
const scopes = new Set(rawScopes.split(','))
|
|
329
|
+
if (![...scopes].every(scope => scope === 'read' || scope === 'control')) throw new Error(`invalid grant scope: ${line}`)
|
|
330
|
+
return { bundleId, read: true, control: scopes.has('control') }
|
|
331
|
+
})
|
|
332
|
+
return {
|
|
333
|
+
observationTtlMs: integer(draft.observationTtlMs, 'observationTtlMs'),
|
|
334
|
+
confirmationTtlMs: integer(draft.confirmationTtlMs, 'confirmationTtlMs'),
|
|
335
|
+
actionTimeoutMs: integer(draft.actionTimeoutMs, 'actionTimeoutMs'),
|
|
336
|
+
settleMs: integer(draft.settleMs, 'settleMs'),
|
|
337
|
+
maxSettleMs: integer(draft.maxSettleMs, 'maxSettleMs'),
|
|
338
|
+
maxNodes: integer(draft.maxNodes, 'maxNodes'),
|
|
339
|
+
maxDepth: integer(draft.maxDepth, 'maxDepth'),
|
|
340
|
+
maxTextBytes: integer(draft.maxTextBytes, 'maxTextBytes'),
|
|
341
|
+
maxScreenshotBytes: integer(draft.maxScreenshotBytes, 'maxScreenshotBytes'),
|
|
342
|
+
artifactRoot: draft.artifactRoot.trim(),
|
|
343
|
+
helper: {
|
|
344
|
+
...(draft.helperPath.trim().length === 0 ? {} : { path: draft.helperPath.trim() }),
|
|
345
|
+
allowSourceBuild: draft.allowSourceBuild,
|
|
346
|
+
},
|
|
347
|
+
interaction: {
|
|
348
|
+
focusPolicy: draft.focusPolicy,
|
|
349
|
+
keyboardPolicy: draft.keyboardPolicy,
|
|
350
|
+
pointerInputPolicy: draft.pointerInputPolicy,
|
|
351
|
+
cursorVisualization: draft.cursorVisualization,
|
|
352
|
+
cursorMotionMs: integer(draft.cursorMotionMs, 'interaction.cursorMotionMs'),
|
|
353
|
+
cursorAutoHideMs: integer(draft.cursorAutoHideMs, 'interaction.cursorAutoHideMs'),
|
|
354
|
+
},
|
|
355
|
+
allowAllApps: draft.allowAllApps,
|
|
356
|
+
grants,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
361
|
+
return <label className="dcu-field"><span>{label}</span>{children}</label>
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
type SettingsProps = PropsRuntime<'settings.section'> & { controller?: ComputerUseSettingsController; t?: Translate }
|
|
365
|
+
|
|
366
|
+
function SettingsSection({ controller, t }: SettingsProps) {
|
|
367
|
+
if (controller === undefined || t === undefined) return null
|
|
368
|
+
return <LoadedSettings controller={controller} t={t} />
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function PermissionCard({ label, state, kind, controller, t }: {
|
|
372
|
+
label: string
|
|
373
|
+
state: string
|
|
374
|
+
kind: 'accessibility' | 'screen-recording'
|
|
375
|
+
controller: ComputerUseSettingsController
|
|
376
|
+
t: Translate
|
|
377
|
+
}) {
|
|
378
|
+
const granted = state === 'granted'
|
|
379
|
+
return <article className="dcu-permission" data-granted={granted || undefined}>
|
|
380
|
+
<div><span>{label}</span><strong>{granted ? t('granted') : t('denied')}</strong></div>
|
|
381
|
+
{!granted ? <Button variant="outline" onClick={() => { void controller.action('open-settings', { kind }, 'open') }}>{t('openSettings')}</Button> : null}
|
|
382
|
+
</article>
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function LoadedSettings({ controller, t }: { controller: ComputerUseSettingsController; t: Translate }) {
|
|
386
|
+
const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot)
|
|
387
|
+
const [draft, setDraft] = useState<Draft>()
|
|
388
|
+
const [draftError, setDraftError] = useState<string>()
|
|
389
|
+
useEffect(() => { if (state.status === 'idle') void controller.load() }, [controller, state.status])
|
|
390
|
+
useEffect(() => { if (state.snapshot !== undefined) setDraft(draftOf(state.snapshot.settings.value)) }, [state.snapshot])
|
|
391
|
+
if (state.snapshot === undefined || draft === undefined) {
|
|
392
|
+
return <div className="dcu-settings"><div className="dcu-panel">{state.error ?? t('loading')}</div>{state.status === 'error' ? <Button variant="outline" onClick={() => { void controller.load() }}>{t('retry')}</Button> : null}</div>
|
|
393
|
+
}
|
|
394
|
+
const snapshot = state.snapshot
|
|
395
|
+
const update = <K extends keyof Draft>(key: K, value: Draft[K]): void => setDraft(current => current === undefined ? current : { ...current, [key]: value })
|
|
396
|
+
const save = (): void => {
|
|
397
|
+
try {
|
|
398
|
+
setDraftError(undefined)
|
|
399
|
+
void controller.action('save', { expectedRevision: snapshot.settings.revision, value: configOf(draft) }, 'save')
|
|
400
|
+
} catch (error) {
|
|
401
|
+
setDraftError(error instanceof Error ? error.message : String(error))
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const numberField = (key: keyof Draft, label: string) => <Field label={label}><Input value={String(draft[key])} onChange={event => update(key, event.target.value as never)} /></Field>
|
|
405
|
+
return <div className="dcu-settings">
|
|
406
|
+
{snapshot.provider.lastError === undefined ? null : <div className="dcu-alert error">{snapshot.provider.lastError}</div>}
|
|
407
|
+
{!snapshot.writable ? <div className="dcu-alert warning">{t('readOnly')}</div> : null}
|
|
408
|
+
{state.error === undefined && draftError === undefined ? null : <div className="dcu-alert error">{draftError ?? state.error}</div>}
|
|
409
|
+
{state.notice === 'saved' ? <div className="dcu-alert ok">{t('saved')}</div> : null}
|
|
410
|
+
<section className="dcu-panel">
|
|
411
|
+
<div className="dcu-panel-title"><h3>{t('privacy')}</h3><Button variant="outline" onClick={() => { void controller.action('health', {}, 'health') }}>{t('refresh')}</Button></div>
|
|
412
|
+
<div className="dcu-permissions">
|
|
413
|
+
<PermissionCard label={t('accessibility')} state={snapshot.provider.accessibility} kind="accessibility" controller={controller} t={t} />
|
|
414
|
+
<PermissionCard label={t('screenRecording')} state={snapshot.provider.screenRecording} kind="screen-recording" controller={controller} t={t} />
|
|
415
|
+
</div>
|
|
416
|
+
</section>
|
|
417
|
+
<section className="dcu-panel dcu-essential">
|
|
418
|
+
<div className="dcu-panel-title"><div><h3>{t('access')}</h3><p>{t('accessHint')}</p></div></div>
|
|
419
|
+
<label className="dcu-check dcu-check-primary"><input type="checkbox" checked={draft.allowAllApps} onChange={event => update('allowAllApps', event.target.checked)} /><span><strong>{t('allowAllApps')}</strong><small>{t('allowAllAppsHint')}</small></span></label>
|
|
420
|
+
</section>
|
|
421
|
+
<div className="dcu-actions"><Button variant="primary" disabled={!snapshot.writable || state.action !== undefined} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button></div>
|
|
422
|
+
<details className="dcu-advanced">
|
|
423
|
+
<summary><span>{t('advanced')}</span><small>{t('advancedHint')}</small></summary>
|
|
424
|
+
<div className="dcu-advanced-body">
|
|
425
|
+
<section className="dcu-panel">
|
|
426
|
+
<div className="dcu-panel-title"><div><h3>{t('interaction')}</h3><p>{t('interactionHint')}</p></div></div>
|
|
427
|
+
<div className="dcu-grid">
|
|
428
|
+
<Field label={t('focusPolicy')}>
|
|
429
|
+
<select value={draft.focusPolicy} onChange={event => update('focusPolicy', event.target.value as Draft['focusPolicy'])}>
|
|
430
|
+
<option value="preserve">{t('focusPreserve')}</option>
|
|
431
|
+
<option value="activate">{t('focusActivate')}</option>
|
|
432
|
+
</select>
|
|
433
|
+
</Field>
|
|
434
|
+
<Field label={t('keyboardPolicy')}>
|
|
435
|
+
<select value={draft.keyboardPolicy} onChange={event => update('keyboardPolicy', event.target.value as Draft['keyboardPolicy'])}>
|
|
436
|
+
<option value="preserve">{t('keyboardPreserve')}</option>
|
|
437
|
+
<option value="activate">{t('keyboardActivate')}</option>
|
|
438
|
+
</select>
|
|
439
|
+
</Field>
|
|
440
|
+
<Field label={t('pointerInputPolicy')}>
|
|
441
|
+
<select value={draft.pointerInputPolicy} onChange={event => update('pointerInputPolicy', event.target.value as Draft['pointerInputPolicy'])}>
|
|
442
|
+
<option value="deny">{t('pointerDeny')}</option>
|
|
443
|
+
<option value="targeted">{t('pointerAllow')}</option>
|
|
444
|
+
</select>
|
|
445
|
+
</Field>
|
|
446
|
+
<Field label={t('cursorVisualization')}>
|
|
447
|
+
<select value={draft.cursorVisualization} onChange={event => update('cursorVisualization', event.target.value as Draft['cursorVisualization'])}>
|
|
448
|
+
<option value="visible">{t('cursorVisible')}</option>
|
|
449
|
+
<option value="hidden">{t('cursorHidden')}</option>
|
|
450
|
+
</select>
|
|
451
|
+
</Field>
|
|
452
|
+
</div>
|
|
453
|
+
</section>
|
|
454
|
+
<section className="dcu-panel">
|
|
455
|
+
<div className="dcu-panel-title"><h3>{t('limits')}</h3></div>
|
|
456
|
+
<div className="dcu-grid">
|
|
457
|
+
{numberField('observationTtlMs', t('ttl'))}
|
|
458
|
+
{numberField('confirmationTtlMs', t('confirmationTtl'))}
|
|
459
|
+
{numberField('actionTimeoutMs', t('actionTimeout'))}
|
|
460
|
+
{numberField('settleMs', t('settle'))}
|
|
461
|
+
{numberField('maxSettleMs', t('maxSettle'))}
|
|
462
|
+
{numberField('maxNodes', t('maxNodes'))}
|
|
463
|
+
{numberField('maxDepth', t('maxDepth'))}
|
|
464
|
+
{numberField('maxTextBytes', t('maxText'))}
|
|
465
|
+
{numberField('maxScreenshotBytes', t('maxScreenshot'))}
|
|
466
|
+
<Field label={t('artifactRoot')}><Input value={draft.artifactRoot} onChange={event => update('artifactRoot', event.target.value)} /></Field>
|
|
467
|
+
</div>
|
|
468
|
+
</section>
|
|
469
|
+
<section className="dcu-panel">
|
|
470
|
+
<div className="dcu-panel-title"><h3>{t('cursorTiming')}</h3></div>
|
|
471
|
+
<div className="dcu-grid">
|
|
472
|
+
{numberField('cursorMotionMs', t('cursorMotion'))}
|
|
473
|
+
{numberField('cursorAutoHideMs', t('cursorAutoHide'))}
|
|
474
|
+
</div>
|
|
475
|
+
</section>
|
|
476
|
+
<section className="dcu-panel">
|
|
477
|
+
<div className="dcu-panel-title"><h3>{t('helper')}</h3></div>
|
|
478
|
+
<code className="dcu-path">{snapshot.provider.helperPath}</code>
|
|
479
|
+
{snapshot.provider.helperSha256 === undefined ? null : <code className="dcu-path">sha256 {snapshot.provider.helperSha256}</code>}
|
|
480
|
+
<div className="dcu-grid">
|
|
481
|
+
<Field label={t('helperPath')}><Input value={draft.helperPath} placeholder="managed" onChange={event => update('helperPath', event.target.value)} /></Field>
|
|
482
|
+
<label className="dcu-check"><input type="checkbox" checked={draft.allowSourceBuild} onChange={event => update('allowSourceBuild', event.target.checked)} /><span>{t('sourceBuild')}</span></label>
|
|
483
|
+
</div>
|
|
484
|
+
</section>
|
|
485
|
+
<section className="dcu-panel">
|
|
486
|
+
<div className="dcu-panel-title"><div><h3>{t('grants')}</h3><p>{t('grantsHint')}</p></div></div>
|
|
487
|
+
<textarea value={draft.grants} disabled={draft.allowAllApps} onChange={event => update('grants', event.target.value)} placeholder={'com.example.App read\ncom.example.Editor read,control'} />
|
|
488
|
+
</section>
|
|
489
|
+
</div>
|
|
490
|
+
</details>
|
|
491
|
+
<footer className="dcu-footer">
|
|
492
|
+
<div><span className="dcu-kicker">{t('pluginKind')}</span><h2>{t('title')}</h2><p>{t('intro')}</p></div>
|
|
493
|
+
<div className="dcu-release"><span>{t('helper')} <strong>{snapshot.provider.helperVersion ?? t('helperUnknown')}</strong></span><span>{t('generation')} <strong>{t('generationValue', { generation: snapshot.provider.generation })}</strong></span><span className={snapshot.provider.ready ? 'ok' : 'bad'}>{snapshot.provider.ready ? t('ready') : t('unavailable')}</span></div>
|
|
494
|
+
</footer>
|
|
495
|
+
</div>
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const CSS = `
|
|
499
|
+
.dcu-settings{display:grid;gap:14px;max-width:920px;padding:2px 0 24px}.dcu-footer{display:flex;justify-content:space-between;gap:22px;align-items:flex-start;margin-top:8px;padding:20px 2px 4px;border-top:1px solid var(--dsw-alias-border-subtle,#dedbd5);opacity:.82}.dcu-footer h2{margin:4px 0 6px;font-size:18px}.dcu-footer p{margin:0;max-width:610px;font-size:11px;line-height:1.55;color:var(--dsw-alias-fg-muted,#706d67)}.dcu-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:#687f74;font-weight:700}.dcu-release{display:grid;gap:5px;min-width:220px;padding:10px 12px;border-radius:11px;background:var(--dsw-alias-bg-layer-2,#f7f5f1);font-size:10px}.dcu-release span{display:flex;justify-content:space-between;gap:12px;white-space:nowrap}.dcu-release .ok{color:#277d52}.dcu-release .bad{color:#aa3939}.dcu-panel{display:grid;gap:13px;padding:16px;border:1px solid var(--dsw-alias-border-subtle,#dedbd5);border-radius:14px;background:var(--dsw-alias-bg-layer-1,#fff)}.dcu-essential{border-color:color-mix(in srgb,#687f74 35%,var(--dsw-alias-border-subtle,#dedbd5));box-shadow:0 0 0 3px rgba(104,127,116,.05)}.dcu-panel-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.dcu-panel-title h3{margin:0;font-size:14px}.dcu-panel-title p{margin:4px 0 0;font-size:10px;line-height:1.45;color:var(--dsw-alias-fg-muted,#706d67)}.dcu-permissions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.dcu-permission{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:11px;border-radius:11px;background:var(--dsw-alias-bg-layer-2,#f7f5f1);border-left:3px solid #cc5555}.dcu-permission[data-granted]{border-left-color:#3ca26b}.dcu-permission div{display:grid;gap:3px}.dcu-permission span{font-size:10px}.dcu-permission strong{font-size:11px}.dcu-path{display:block;overflow:auto;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f7f5f1);font-size:10px;color:var(--dsw-alias-fg-muted,#706d67)}.dcu-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.dcu-field{display:grid;gap:5px}.dcu-field>span{font-size:10px;font-weight:650}.dcu-field select,.dcu-panel textarea{border:1px solid var(--dsw-alias-border-subtle,#dedbd5);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit}.dcu-field select{min-height:34px;padding:6px 9px;font:11px inherit}.dcu-check{display:flex;align-items:center;gap:8px;padding-top:19px;font-size:11px}.dcu-check-primary{align-items:flex-start;padding:10px 0}.dcu-check-primary input{margin-top:2px}.dcu-check-primary span{display:grid;gap:3px}.dcu-check-primary strong{font-size:12px}.dcu-check-primary small{font-size:10px;line-height:1.45;color:var(--dsw-alias-fg-muted,#706d67)}.dcu-panel textarea{min-height:110px;resize:vertical;padding:10px;font:12px ui-monospace,SFMono-Regular,Menlo,monospace}.dcu-alert{padding:10px 12px;border-radius:10px;font-size:11px}.dcu-alert.error{background:rgba(205,72,72,.1);color:#a13b3b}.dcu-alert.warning{background:rgba(211,151,49,.12);color:#8b651f}.dcu-alert.ok{background:rgba(48,154,100,.12);color:#267d52}.dcu-actions{display:flex;justify-content:flex-start;padding:2px 0}@media(max-width:720px){.dcu-footer{display:grid}.dcu-release{width:auto}.dcu-grid,.dcu-permissions{grid-template-columns:1fr}}
|
|
500
|
+
`
|
|
501
|
+
|
|
502
|
+
const ADVANCED_CSS = `.dcu-advanced{border:1px solid var(--dsw-alias-border-subtle,#dedbd5);border-radius:14px;background:var(--dsw-alias-bg-layer-1,#fff);overflow:hidden}.dcu-advanced>summary{display:flex;align-items:center;gap:10px;padding:14px 16px;cursor:pointer;list-style:none;font-size:13px;font-weight:650}.dcu-advanced>summary::-webkit-details-marker{display:none}.dcu-advanced>summary small{margin-left:auto;font-size:10px;font-weight:400;color:var(--dsw-alias-fg-muted,#706d67)}.dcu-advanced>summary::after{content:"▸";margin-left:2px;font-size:12px;color:var(--dsw-alias-fg-muted,#706d67);transform:rotate(0deg);transition:transform .15s ease}.dcu-advanced[open]>summary::after{transform:rotate(90deg)}.dcu-advanced-body{display:grid;gap:14px;padding:2px 16px 16px}`
|
|
503
|
+
|
|
504
|
+
function installStyles(): () => void {
|
|
505
|
+
const id = '@anionex/dsh-computer-use/client'
|
|
506
|
+
const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
|
|
507
|
+
if (existing !== null) return () => {}
|
|
508
|
+
const style = document.createElement('style')
|
|
509
|
+
style.dataset.plugin = '@anionex/dsh-computer-use'
|
|
510
|
+
style.dataset.pluginCss = id
|
|
511
|
+
style.textContent = CSS + ADVANCED_CSS
|
|
512
|
+
document.head.appendChild(style)
|
|
513
|
+
return () => { style.remove() }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Required browser services. */
|
|
517
|
+
export const inject = ['slots', 'locale', 'remote']
|
|
518
|
+
|
|
519
|
+
/** Register the Computer Use Settings section. */
|
|
520
|
+
export function apply(ctx: ClientContext): void {
|
|
521
|
+
ctx.effect(installStyles, 'dsh-computer-use: styles')
|
|
522
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-computer-use: locale')
|
|
523
|
+
const t = ctx.locale.bind(NS)
|
|
524
|
+
const controller = new ComputerUseSettingsController()
|
|
525
|
+
ctx.effect(() => {
|
|
526
|
+
const disposers = [
|
|
527
|
+
ctx.remote.$on('settings/document-updated', ns => { if (ns === NS) controller.refreshIfLoaded() }),
|
|
528
|
+
ctx.on('connection/reset', () => { controller.refreshIfLoaded() }),
|
|
529
|
+
]
|
|
530
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
531
|
+
}, 'dsh-computer-use: Settings invalidation')
|
|
532
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
533
|
+
name: 'settings.section',
|
|
534
|
+
id: 'computer-use',
|
|
535
|
+
order: 35,
|
|
536
|
+
label: () => t('nav'),
|
|
537
|
+
inject: () => ({ controller, t }),
|
|
538
|
+
}, SettingsSection))
|
|
539
|
+
}
|