@anionex/dsh-vision-toolkit 0.1.5 → 0.1.6

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.
@@ -24,6 +24,7 @@ import { installPasteImages } from './paste-images.tsx'
24
24
  const NS = 'vision-toolkit'
25
25
  const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings'
26
26
  const PRESENTATION_META_KEY = '$dshVisionToolkit'
27
+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
27
28
 
28
29
  const en = {
29
30
  nav: 'Vision',
@@ -31,9 +32,14 @@ const en = {
31
32
  settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.',
32
33
  externalNotice: 'Remote tools send the selected image bytes to the configured external vision API. Local crop, trace, pixel diff, palette, foreground extraction, and HTML rendering do not upload images.',
33
34
  provider: 'Vision service',
35
+ providerHint: 'Choose the API protocol, then provide the service address, model, and credential used by online vision features.',
34
36
  baseUrl: 'Base URL',
35
37
  credential: 'Credential reference',
36
38
  model: 'Model',
39
+ protocol: 'API protocol',
40
+ anthropicThinking: 'Anthropic thinking',
41
+ anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
42
+ userAgent: 'User-Agent',
37
43
  language: 'Output language',
38
44
  limits: 'Limits',
39
45
  timeout: 'Request timeout (ms)',
@@ -54,16 +60,27 @@ const en = {
54
60
  configured: 'Configured',
55
61
  missing: 'Missing',
56
62
  source: 'Source',
63
+ sourceHint: '{source}: {value}',
64
+ sourceEnv: 'Environment variable',
65
+ sourceFile: 'Credential file',
57
66
  health: 'Health',
58
67
  runHealth: 'Run health check',
59
68
  testConnection: 'Test connection',
60
69
  testing: 'Checking…',
61
70
  connectionHint: 'Connection testing explicitly sends the configured credential to GET /models. It uploads no image and creates no completion.',
71
+ saveBeforeTesting: 'Save service changes before testing the connection.',
72
+ advanced: 'Advanced settings',
73
+ advancedHint: 'Provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
62
74
  pluginVersion: 'Plugin',
63
75
  upstreamVersion: 'Upstream',
64
76
  activeGeneration: 'Runtime generation',
77
+ activeGenerationValue: 'Generation {generation}',
78
+ pluginKind: 'DSH native plugin',
65
79
  runtimeUnavailable: 'Runtime unavailable',
66
80
  runtimeCandidateRejected: 'Last runtime candidate was rejected; the active generation remains available.',
81
+ runtimeReady: 'Ready',
82
+ runtimeManaged: 'Managed',
83
+ runtimeExternal: 'External checkout',
67
84
  retry: 'Retry',
68
85
  open: 'Open file',
69
86
  download: 'Download',
@@ -83,71 +100,195 @@ const en = {
83
100
  healthy: 'Healthy',
84
101
  degraded: 'Needs attention',
85
102
  notTested: 'Not tested',
103
+ groundTitle: 'Ground',
104
+ detectTitle: 'Detect',
105
+ traceTitle: 'Trace SVG',
106
+ pixelDiffTitle: 'Pixel Diff',
107
+ cropTitle: 'Crop',
108
+ longOcrTitle: 'Long OCR',
109
+ extractForegroundTitle: 'Extract Foreground',
110
+ htmlScreenshotTitle: 'HTML Screenshot',
111
+ artifactTitle: 'Vision Artifact',
112
+ dominantColorsTitle: 'Dominant Colors',
113
+ artifactGroundPreview: 'Grounding bounding-box preview',
114
+ artifactDetectPreview: 'Detected-element bounding-box preview',
115
+ artifactCrop: 'Cropped image region',
116
+ artifactTrace: 'Traced vector geometry',
117
+ artifactDiffHeatmap: 'Pixel-difference heatmap',
118
+ artifactDiffReport: 'Structured pixel-difference report',
119
+ artifactLongManifest: 'Long-screenshot split and merge manifest',
120
+ artifactLongTranscript: 'Merged long-screenshot OCR transcript',
121
+ artifactLongAudit: 'Long-screenshot OCR boundary audit',
122
+ artifactLongChunk: 'Long-screenshot OCR chunk {index}',
123
+ artifactOcrSidecar: 'OCR sidecar for chunk {index}',
124
+ artifactForeground: 'Extracted transparent foreground',
125
+ artifactHtmlScreenshot: 'Headless browser screenshot of local HTML',
126
+ label: 'Label',
127
+ paths: 'paths',
128
+ healthPython: 'Python',
129
+ healthDependencies: 'Dependencies',
130
+ healthChrome: 'Browser',
131
+ healthCredential: 'Credential',
132
+ healthArtifactDirectory: 'Artifact directory',
133
+ healthTempDirectory: 'Temporary directory',
134
+ healthService: 'Vision service',
135
+ statusOk: 'OK',
136
+ statusWarning: 'Warning',
137
+ statusError: 'Error',
138
+ statusNotTested: 'Not tested',
139
+ positiveInteger: '{field} must be a positive integer.',
140
+ healthPythonDetail: '{version} via {path}',
141
+ healthChromeMissing: 'Chrome, Chromium, or Edge was not found; HTML Screenshot is unavailable.',
142
+ healthChromeProbeFailed: 'Could not check whether a supported browser is available.',
143
+ healthCredentialMissing: 'Credential {credential} is not configured.',
144
+ healthCredentialReady: 'Credential {credential} is available.',
145
+ healthCredentialFailed: 'Could not read credential {credential}.',
146
+ healthDirectoryWritable: '{directory} is writable: {path}',
147
+ healthDirectoryNotWritable: '{directory} is not writable: {path}',
148
+ healthArtifactDirectoryFailed: 'Could not prepare the artifact directory.',
149
+ healthConnectionNotTested: 'Connection not tested. Use Test connection to query /models.',
150
+ healthConnectionCredentialMissing: 'Connection test skipped because the credential is unavailable.',
151
+ healthServiceResponded: 'Service responded at {endpoint} (HTTP {status}).',
152
+ healthServiceRejectedCredential: 'Service rejected the configured credential (HTTP {status}).',
153
+ healthServiceNoModels: 'Service is reachable but does not support GET /models (HTTP {status}).',
154
+ healthServiceRateLimited: 'Service is reachable, but the connection test was rate-limited (HTTP 429).',
155
+ healthServiceHttpFailed: 'Connection test failed with HTTP {status}.',
156
+ healthServiceUnreachable: 'Could not reach {endpoint}.',
86
157
  } as const
87
158
 
88
159
  type LocaleKey = keyof typeof en
89
160
 
90
161
  const zh: Record<LocaleKey, string> = {
91
162
  nav: '视觉工具',
92
- settingsTitle: 'Vision Toolkit',
93
- settingsIntro: '配置固定版本的视觉工程运行时、外部视觉服务与本地安全限制。',
94
- externalNotice: '远程工具会把选中的图片字节发送到已配置的外部视觉 API。本地裁剪、几何恢复、像素对比、调色板、前景提取和 HTML 渲染不会上传图片。',
95
- provider: '视觉服务',
96
- baseUrl: '服务地址',
97
- credential: 'Credential 引用',
98
- model: '模型',
99
- language: '输出语言',
100
- limits: '限制',
101
- timeout: '请求超时(毫秒)',
102
- maxBytes: '最大图片字节数',
103
- maxPixels: '最大图片像素数',
104
- concurrency: '每个 Session 并发数',
105
- runtime: '运行时',
106
- runtimeMode: '运行模式',
107
- toolkitPath: '固定上游 checkout 路径',
108
- python: 'Python 覆盖',
109
- allowedDirs: '额外允许目录',
110
- allowedDirsHint: '每行一个路径;Session 工作区始终允许。',
111
- save: '保存并应用',
112
- saving: '正在验证运行时…',
163
+ settingsTitle: '视觉工具箱',
164
+ settingsIntro: '在这里设置视觉模型服务、工具运行环境,以及图片和文件的本地访问范围。',
165
+ externalNotice: '使用图像理解、目标定位、界面检测或文字识别等在线功能时,所选图片会发送到下方配置的视觉服务。图片裁剪、轮廓描摹、像素对比、主色提取、前景提取和网页截图均在本机完成,不会上传图片。',
166
+ provider: '在线视觉服务',
167
+ providerHint: '选择接口协议后,填写在线视觉功能使用的 API 地址、模型名称和密钥名称。',
168
+ baseUrl: 'API 地址',
169
+ credential: 'API 密钥名称',
170
+ model: '模型名称',
171
+ protocol: 'API 协议',
172
+ anthropicThinking: 'Anthropic thinking',
173
+ anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
174
+ userAgent: 'User-Agent',
175
+ language: '结果语言',
176
+ limits: '资源与并发限制',
177
+ timeout: '单次请求超时(毫秒)',
178
+ maxBytes: '单张图片大小上限(字节)',
179
+ maxPixels: '单张图片最大像素数',
180
+ concurrency: '单个会话最多并发任务数',
181
+ runtime: '工具运行环境',
182
+ runtimeMode: '环境来源',
183
+ toolkitPath: 'agent-vision-toolkit 目录',
184
+ python: 'Python 解释器(可选)',
185
+ allowedDirs: '允许读取的其他目录',
186
+ allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
187
+ save: '保存设置',
188
+ saving: '正在检查并应用…',
113
189
  reload: '重新加载',
114
- saved: '设置已验证并生效。',
115
- readOnly: '当前 Settings 提供方为只读。',
116
- configured: '已配置',
117
- missing: '缺失',
118
- source: '来源',
119
- health: '健康检查',
120
- runHealth: '运行健康检查',
121
- testConnection: '测试连接',
190
+ saved: '设置已保存并生效。',
191
+ readOnly: '当前设置由只读配置提供,无法在此修改。',
192
+ configured: '已就绪',
193
+ missing: '未配置',
194
+ source: '配置来源',
195
+ sourceHint: '{source}:{value}',
196
+ sourceEnv: '环境变量',
197
+ sourceFile: '凭据文件',
198
+ health: '运行检查',
199
+ runHealth: '检查本地环境',
200
+ testConnection: '测试服务连接',
122
201
  testing: '检查中…',
123
- connectionHint: '连接测试会显式把已配置 Credential 发送到 GET /models;不会上传图片,也不会创建 completion。',
124
- pluginVersion: '插件',
125
- upstreamVersion: '上游',
126
- activeGeneration: '运行时世代',
127
- runtimeUnavailable: '运行时不可用',
128
- runtimeCandidateRejected: '最近的运行时候选已被拒绝;当前世代仍可用。',
202
+ connectionHint: '“检查本地环境”只检查本机依赖和目录;“测试服务连接”会携带已配置的 API 密钥请求 /models,但不会上传图片或调用模型。',
203
+ saveBeforeTesting: '修改服务配置后,请先保存,再测试连接。',
204
+ advanced: '高级设置',
205
+ advancedHint: '服务兼容参数、结果语言、资源限制、运行环境来源、Python 和额外可读目录。一般无需修改。',
206
+ pluginVersion: '插件版本',
207
+ upstreamVersion: '工具包版本',
208
+ activeGeneration: '本次运行已应用',
209
+ activeGenerationValue: '{generation} 次',
210
+ pluginKind: 'DSH 原生插件',
211
+ runtimeUnavailable: '运行环境尚未就绪',
212
+ runtimeCandidateRejected: '新设置未能生效,仍在使用上一次可用的设置。',
213
+ runtimeReady: '已就绪',
214
+ runtimeManaged: '自动安装',
215
+ runtimeExternal: '本地源码',
129
216
  retry: '重试',
130
- open: '打开文件',
217
+ open: '在工作区中打开',
131
218
  download: '下载',
132
- previewUnavailable: '当前宿主不提供 HTTP 预览,请使用“打开文件”。',
219
+ previewUnavailable: '此页面无法直接预览该文件,请在工作区中打开。',
133
220
  running: '运行中…',
134
- failed: '失败',
135
- matches: '个匹配',
221
+ failed: '运行失败',
222
+ matches: '处匹配',
136
223
  elements: '个元素',
137
- dimensions: '尺寸',
224
+ dimensions: '图片尺寸',
138
225
  coordinates: '坐标',
139
- artifact: '交付文件',
140
- artifacts: '交付文件',
141
- difference: '总体差异',
142
- worstRegions: '差异最大区域',
143
- colors: '主色',
144
- noResult: '结构化结果不可用,请检查原始 Tool 结果。',
145
- healthy: '健康',
146
- degraded: '需要处理',
147
- notTested: '未测试',
226
+ artifact: '生成文件',
227
+ artifacts: '个生成文件',
228
+ difference: '像素差异',
229
+ worstRegions: '差异最大的区域',
230
+ colors: '种颜色',
231
+ noResult: '未能读取结果,请查看工具的原始输出。',
232
+ healthy: '一切正常',
233
+ degraded: '有项目需要处理',
234
+ notTested: '尚未检查',
235
+ groundTitle: '目标定位',
236
+ detectTitle: '界面元素识别',
237
+ traceTitle: '描摹为 SVG',
238
+ pixelDiffTitle: '像素对比',
239
+ cropTitle: '裁剪图片',
240
+ longOcrTitle: '长图文字识别',
241
+ extractForegroundTitle: '提取前景',
242
+ htmlScreenshotTitle: '网页截图',
243
+ artifactTitle: '视觉处理结果',
244
+ dominantColorsTitle: '主色提取',
245
+ artifactGroundPreview: '目标定位框预览',
246
+ artifactDetectPreview: '界面元素标注预览',
247
+ artifactCrop: '裁剪后的图片',
248
+ artifactTrace: '描摹得到的矢量图',
249
+ artifactDiffHeatmap: '像素差异热力图',
250
+ artifactDiffReport: '像素差异详细报告',
251
+ artifactLongManifest: '长图切分与合并记录',
252
+ artifactLongTranscript: '长图文字识别结果',
253
+ artifactLongAudit: '长图分块边界检查记录',
254
+ artifactLongChunk: '长图文字识别分块 {index}',
255
+ artifactOcrSidecar: '分块 {index} 的文字识别记录',
256
+ artifactForeground: '提取后的透明背景前景图',
257
+ artifactHtmlScreenshot: '本地网页截图',
258
+ label: '名称',
259
+ paths: '条路径',
260
+ healthPython: 'Python',
261
+ healthDependencies: 'Python 依赖',
262
+ healthChrome: '浏览器',
263
+ healthCredential: 'API 密钥',
264
+ healthArtifactDirectory: '输出目录',
265
+ healthTempDirectory: '临时目录',
266
+ healthService: '视觉服务',
267
+ statusOk: '正常',
268
+ statusWarning: '注意',
269
+ statusError: '异常',
270
+ statusNotTested: '未检查',
271
+ positiveInteger: '{field}必须填写正整数。',
272
+ healthPythonDetail: '版本 {version};解释器:{path}',
273
+ healthChromeMissing: '未找到 Chrome、Chromium 或 Edge,网页截图功能暂不可用。',
274
+ healthChromeProbeFailed: '无法检查浏览器是否可用。',
275
+ healthCredentialMissing: '尚未配置凭据 {credential}。',
276
+ healthCredentialReady: '已找到凭据 {credential}。',
277
+ healthCredentialFailed: '无法读取凭据 {credential}。',
278
+ healthDirectoryWritable: '{directory}可写:{path}',
279
+ healthDirectoryNotWritable: '{directory}不可写:{path}',
280
+ healthArtifactDirectoryFailed: '无法准备输出目录。',
281
+ healthConnectionNotTested: '尚未测试服务连接。点击“测试服务连接”可请求 /models。',
282
+ healthConnectionCredentialMissing: 'API 密钥不可用,未执行连接测试。',
283
+ healthServiceResponded: '服务已响应:{endpoint}(HTTP {status})。',
284
+ healthServiceRejectedCredential: '服务拒绝了当前 API 密钥(HTTP {status})。',
285
+ healthServiceNoModels: '服务可以访问,但不支持 GET /models(HTTP {status})。',
286
+ healthServiceRateLimited: '服务可以访问,但本次连接测试触发了限流(HTTP 429)。',
287
+ healthServiceHttpFailed: '连接测试失败(HTTP {status})。',
288
+ healthServiceUnreachable: '无法连接到 {endpoint}。',
148
289
  }
149
290
 
150
- type Translate = (key: LocaleKey) => string
291
+ type Translate = (key: LocaleKey, params?: Record<string, unknown>) => string
151
292
 
152
293
  interface ToolCallOwnerProps {
153
294
  callId: string
@@ -202,7 +343,14 @@ interface HealthResult {
202
343
  }
203
344
 
204
345
  interface SettingsValue {
205
- provider?: { baseUrl?: string; credential?: string; model?: string }
346
+ provider?: {
347
+ baseUrl?: string
348
+ credential?: string
349
+ model?: string
350
+ protocol?: 'openai' | 'anthropic'
351
+ anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
352
+ userAgent?: string
353
+ }
206
354
  language?: 'zh' | 'en'
207
355
  timeoutMs?: number
208
356
  maxImageBytes?: number
@@ -404,17 +552,18 @@ function ArtifactPreview({ artifact, grant, openFile, t }: {
404
552
  t: Translate
405
553
  }) {
406
554
  const canPreview = grant !== undefined && (artifact.kind === 'image' || artifact.kind === 'svg')
555
+ const description = artifactDescription(artifact.description, t)
407
556
  return (
408
557
  <article className="dvt-artifact">
409
558
  {canPreview
410
559
  ? artifact.kind === 'svg'
411
- ? <iframe className="dvt-preview dvt-svg" sandbox="" src={grant.previewUrl} title={artifact.description} />
412
- : <img className="dvt-preview" src={grant.previewUrl} alt={artifact.description} loading="lazy" />
560
+ ? <iframe className="dvt-preview dvt-svg" sandbox="" src={grant.previewUrl} title={description} />
561
+ : <img className="dvt-preview" src={grant.previewUrl} alt={description} loading="lazy" />
413
562
  : null}
414
563
  <div className="dvt-artifact-meta">
415
564
  <div>
416
565
  <strong>{artifact.filename}</strong>
417
- <span>{artifact.description}</span>
566
+ <span>{description}</span>
418
567
  <small>{artifact.mimeType} · {formatBytes(artifact.bytes)}</small>
419
568
  </div>
420
569
  <ArtifactActions artifact={artifact} grant={grant} openFile={openFile} t={t} />
@@ -424,18 +573,51 @@ function ArtifactPreview({ artifact, grant, openFile, t }: {
424
573
  )
425
574
  }
426
575
 
576
+ const ARTIFACT_DESCRIPTION_KEYS: Record<string, LocaleKey> = {
577
+ 'Grounding bounding-box preview': 'artifactGroundPreview',
578
+ 'Detected-element bounding-box preview': 'artifactDetectPreview',
579
+ 'Cropped image region': 'artifactCrop',
580
+ 'Traced vector geometry': 'artifactTrace',
581
+ 'Pixel-difference heatmap': 'artifactDiffHeatmap',
582
+ 'Structured pixel-difference report': 'artifactDiffReport',
583
+ 'Long-screenshot split and merge manifest': 'artifactLongManifest',
584
+ 'Merged long-screenshot OCR transcript': 'artifactLongTranscript',
585
+ 'Long-screenshot OCR boundary audit': 'artifactLongAudit',
586
+ 'Extracted transparent foreground': 'artifactForeground',
587
+ 'Headless browser screenshot of local HTML': 'artifactHtmlScreenshot',
588
+ }
589
+
590
+ function artifactDescription(description: string, t: Translate): string {
591
+ const key = ARTIFACT_DESCRIPTION_KEYS[description]
592
+ if (key !== undefined) {
593
+ const translated = t(key)
594
+ return translated === key ? description : translated
595
+ }
596
+ let match = /^Long-screenshot OCR chunk (\d+)$/u.exec(description)
597
+ if (match !== null) {
598
+ const translated = t('artifactLongChunk', { index: match[1] })
599
+ return translated === 'artifactLongChunk' ? description : translated
600
+ }
601
+ match = /^OCR sidecar for chunk (\d+)$/u.exec(description)
602
+ if (match !== null) {
603
+ const translated = t('artifactOcrSidecar', { index: match[1] })
604
+ return translated === 'artifactOcrSidecar' ? description : translated
605
+ }
606
+ return description
607
+ }
608
+
427
609
  type ViewProps = ToolCallViewProps & { t?: Translate }
428
610
 
429
611
  function GroundView({ block, openFile, t = key => en[key] }: ViewProps) {
430
612
  const value = decodeVisionResult(block)
431
613
  const matches = Array.isArray(value?.matches) ? value.matches.filter(isRecord) : []
432
- const target = stringOf(value?.target) ?? 'Ground'
614
+ const target = stringOf(value?.target) ?? t('groundTitle')
433
615
  const width = numberOf(value?.imageWidth)
434
616
  const height = numberOf(value?.imageHeight)
435
617
  const preview = artifactFrom(value?.preview)
436
618
  const grants = accessMap(value)
437
619
  return (
438
- <ToolShell block={block} title="Ground" summary={matches.length > 0 ? `${target} · ${matches.length} ${t('matches')}` : target} icon={<VisionIcon kind="target" />} t={t}>
620
+ <ToolShell block={block} title={t('groundTitle')} summary={matches.length > 0 ? `${target} · ${matches.length} ${t('matches')}` : target} icon={<VisionIcon kind="target" />} t={t}>
439
621
  {value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
440
622
  <div className="dvt-stack">
441
623
  <div className="dvt-metrics">
@@ -460,14 +642,14 @@ function DetectView({ block, openFile, t = key => en[key] }: ViewProps) {
460
642
  const preview = artifactFrom(value?.preview)
461
643
  const grants = accessMap(value)
462
644
  return (
463
- <ToolShell block={block} title="Detect" summary={`${elements.length} ${t('elements')}`} icon={<VisionIcon kind="layers" />} t={t}>
645
+ <ToolShell block={block} title={t('detectTitle')} summary={`${elements.length} ${t('elements')}`} icon={<VisionIcon kind="layers" />} t={t}>
464
646
  {value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
465
647
  <div className="dvt-stack">
466
648
  <div className="dvt-metrics">
467
649
  <div><span>{t('dimensions')}</span><strong>{width ?? '—'} × {height ?? '—'}</strong></div>
468
650
  <div><span>{t('elements')}</span><strong>{elements.length}</strong></div>
469
651
  </div>
470
- <div className="dvt-table-wrap"><table className="dvt-table"><thead><tr><th>#</th><th>Label</th><th>{t('coordinates')}</th></tr></thead><tbody>
652
+ <div className="dvt-table-wrap"><table className="dvt-table"><thead><tr><th>#</th><th>{t('label')}</th><th>{t('coordinates')}</th></tr></thead><tbody>
471
653
  {elements.map((element, index) => <tr key={index}><td>{numberOf(element.index) ?? index + 1}</td><td>{stringOf(element.label) ?? '—'}</td><td><code>{boxText(element.box)}</code></td></tr>)}
472
654
  </tbody></table></div>
473
655
  {preview === undefined ? null : <ArtifactPreview artifact={preview} grant={grants.get(preview.path)} openFile={openFile} t={t} />}
@@ -481,10 +663,10 @@ function TraceView({ block, openFile, t = key => en[key] }: ViewProps) {
481
663
  const value = decodeVisionResult(block)
482
664
  const artifact = artifactFrom(value?.artifact)
483
665
  const geometry = isRecord(value?.geometry) ? value.geometry : undefined
484
- const summary = geometry === undefined ? undefined : `${numberOf(geometry.pathCount) ?? 0} paths · ${formatBytes(numberOf(geometry.bytes) ?? 0)}`
666
+ const summary = geometry === undefined ? undefined : `${numberOf(geometry.pathCount) ?? 0} ${t('paths')} · ${formatBytes(numberOf(geometry.bytes) ?? 0)}`
485
667
  const grants = accessMap(value)
486
668
  return (
487
- <ToolShell block={block} title="Trace SVG" summary={summary} icon={<VisionIcon kind="shape" />} t={t}>
669
+ <ToolShell block={block} title={t('traceTitle')} summary={summary} icon={<VisionIcon kind="shape" />} t={t}>
488
670
  {artifact === undefined ? <p className="dvt-muted">{t('noResult')}</p> : <ArtifactPreview artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />}
489
671
  </ToolShell>
490
672
  )
@@ -498,7 +680,7 @@ function PixelDiffView({ block, openFile, t = key => en[key] }: ViewProps) {
498
680
  const report = artifactFrom(value?.report)
499
681
  const grants = accessMap(value)
500
682
  return (
501
- <ToolShell block={block} title="Pixel Diff" summary={pct === undefined ? undefined : `${pct.toFixed(3)}%`} icon={<VisionIcon kind="diff" />} t={t}>
683
+ <ToolShell block={block} title={t('pixelDiffTitle')} summary={pct === undefined ? undefined : `${pct.toFixed(3)}%`} icon={<VisionIcon kind="diff" />} t={t}>
502
684
  {value === undefined ? <p className="dvt-muted">{t('noResult')}</p> : (
503
685
  <div className="dvt-stack">
504
686
  <div className="dvt-diff-score"><span>{t('difference')}</span><strong>{pct?.toFixed(4) ?? '—'}%</strong><div><i style={{ width: `${Math.min(100, Math.max(0, pct ?? 0))}%` }} /></div></div>
@@ -515,11 +697,11 @@ function ArtifactView({ block, openFile, toolName, t = key => en[key] }: ViewPro
515
697
  const value = decodeVisionResult(block)
516
698
  const artifacts = collectArtifacts(value)
517
699
  const grants = accessMap(value)
518
- const title = toolName === 'vision_crop' ? 'Crop'
519
- : toolName === 'vision_long_screenshot_ocr' ? 'Long OCR'
520
- : toolName === 'vision_extract_foreground' ? 'Extract Foreground'
521
- : toolName === 'vision_html_screenshot' ? 'HTML Screenshot'
522
- : 'Vision Artifact'
700
+ const title = toolName === 'vision_crop' ? t('cropTitle')
701
+ : toolName === 'vision_long_screenshot_ocr' ? t('longOcrTitle')
702
+ : toolName === 'vision_extract_foreground' ? t('extractForegroundTitle')
703
+ : toolName === 'vision_html_screenshot' ? t('htmlScreenshotTitle')
704
+ : t('artifactTitle')
523
705
  return (
524
706
  <ToolShell block={block} title={title} summary={artifacts.length > 0 ? `${artifacts.length} ${t('artifacts')}` : undefined} icon={<VisionIcon />} t={t}>
525
707
  {artifacts.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-stack">{artifacts.map(artifact => <ArtifactPreview key={artifact.path} artifact={artifact} grant={grants.get(artifact.path)} openFile={openFile} t={t} />)}</div>}
@@ -532,7 +714,7 @@ function PaletteView({ block, t = key => en[key] }: ViewProps) {
532
714
  const analysis = isRecord(value?.analysis) ? value.analysis : undefined
533
715
  const colors = Array.isArray(analysis?.colors) ? analysis.colors.filter(isRecord) : []
534
716
  return (
535
- <ToolShell block={block} title="Dominant Colors" summary={`${colors.length} ${t('colors')}`} icon={<VisionIcon kind="palette" />} t={t}>
717
+ <ToolShell block={block} title={t('dominantColorsTitle')} summary={`${colors.length} ${t('colors')}`} icon={<VisionIcon kind="palette" />} t={t}>
536
718
  {colors.length === 0 ? <p className="dvt-muted">{t('noResult')}</p> : <div className="dvt-palette">{colors.map((color, index) => {
537
719
  const hex = stringOf(color.color) ?? '#000000'
538
720
  const share = numberOf(color.sharePct)
@@ -630,6 +812,9 @@ interface Draft {
630
812
  baseUrl: string
631
813
  credential: string
632
814
  model: string
815
+ protocol: 'openai' | 'anthropic'
816
+ anthropicThinking: 'omit' | 'disabled' | 'adaptive'
817
+ userAgent: string
633
818
  language: 'zh' | 'en'
634
819
  timeoutMs: string
635
820
  maxImageBytes: string
@@ -646,6 +831,9 @@ function draftOf(value: SettingsValue): Draft {
646
831
  baseUrl: value.provider?.baseUrl ?? 'https://api.inferera.com/v1',
647
832
  credential: value.provider?.credential ?? 'VISION_API_KEY',
648
833
  model: value.provider?.model ?? 'gemini-3.6-flash',
834
+ protocol: value.provider?.protocol ?? 'openai',
835
+ anthropicThinking: value.provider?.anthropicThinking ?? 'omit',
836
+ userAgent: value.provider?.userAgent ?? DEFAULT_USER_AGENT,
649
837
  language: value.language ?? 'zh',
650
838
  timeoutMs: String(value.timeoutMs ?? 60000),
651
839
  maxImageBytes: String(value.maxImageBytes ?? 10485760),
@@ -658,20 +846,27 @@ function draftOf(value: SettingsValue): Draft {
658
846
  }
659
847
  }
660
848
 
661
- function positiveInteger(raw: string, label: string): number {
849
+ function positiveInteger(raw: string, label: string, t: Translate): number {
662
850
  const value = Number(raw)
663
- if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`)
851
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error(t('positiveInteger', { field: label }))
664
852
  return value
665
853
  }
666
854
 
667
- function valueOf(draft: Draft): SettingsValue {
855
+ function valueOf(draft: Draft, t: Translate): SettingsValue {
668
856
  return {
669
- provider: { baseUrl: draft.baseUrl.trim(), credential: draft.credential.trim(), model: draft.model.trim() },
857
+ provider: {
858
+ baseUrl: draft.baseUrl.trim(),
859
+ credential: draft.credential.trim(),
860
+ model: draft.model.trim(),
861
+ protocol: draft.protocol,
862
+ anthropicThinking: draft.anthropicThinking,
863
+ userAgent: draft.userAgent.trim(),
864
+ },
670
865
  language: draft.language,
671
- timeoutMs: positiveInteger(draft.timeoutMs, 'timeoutMs'),
672
- maxImageBytes: positiveInteger(draft.maxImageBytes, 'maxImageBytes'),
673
- maxImagePixels: positiveInteger(draft.maxImagePixels, 'maxImagePixels'),
674
- concurrency: positiveInteger(draft.concurrency, 'concurrency'),
866
+ timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
867
+ maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
868
+ maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
869
+ concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
675
870
  runtime: {
676
871
  mode: draft.runtimeMode,
677
872
  ...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
@@ -697,6 +892,69 @@ function SettingsSection({ controller, t }: SettingsProps) {
697
892
  return <LoadedSettings controller={controller} t={t} />
698
893
  }
699
894
 
895
+ const HEALTH_NAME_KEYS: Record<string, LocaleKey> = {
896
+ python: 'healthPython',
897
+ dependencies: 'healthDependencies',
898
+ chrome: 'healthChrome',
899
+ credential: 'healthCredential',
900
+ artifactDirectory: 'healthArtifactDirectory',
901
+ tempDirectory: 'healthTempDirectory',
902
+ service: 'healthService',
903
+ }
904
+
905
+ const HEALTH_STATUS_KEYS: Record<HealthCheck['status'], LocaleKey> = {
906
+ ok: 'statusOk',
907
+ warning: 'statusWarning',
908
+ error: 'statusError',
909
+ not_tested: 'statusNotTested',
910
+ }
911
+
912
+ function healthDetail(name: string, detail: string, t: Translate): string {
913
+ if (name === 'python') {
914
+ const match = /^(.+) via (.+)$/u.exec(detail)
915
+ if (match !== null) return t('healthPythonDetail', { version: match[1], path: match[2] })
916
+ }
917
+ if (detail === 'Chrome/Chromium/Edge was not found; vision_html_screenshot is unavailable') return t('healthChromeMissing')
918
+ if (detail === 'Chrome availability probe failed') return t('healthChromeProbeFailed')
919
+ let match = /^credential (.+) is not configured$/u.exec(detail)
920
+ if (match !== null) return t('healthCredentialMissing', { credential: match[1] })
921
+ match = /^credential (.+) is resolvable$/u.exec(detail)
922
+ if (match !== null) return t('healthCredentialReady', { credential: match[1] })
923
+ match = /^credential (.+) could not be resolved$/u.exec(detail)
924
+ if (match !== null) return t('healthCredentialFailed', { credential: match[1] })
925
+ match = /^(Artifact directory|Runtime temp directory) is writable: (.+)$/u.exec(detail)
926
+ if (match !== null) return t('healthDirectoryWritable', {
927
+ directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
928
+ path: match[2],
929
+ })
930
+ match = /^(Artifact directory|Runtime temp directory) is not writable: (.+)$/u.exec(detail)
931
+ if (match !== null) return t('healthDirectoryNotWritable', {
932
+ directory: match[1] === 'Artifact directory' ? t('healthArtifactDirectory') : t('healthTempDirectory'),
933
+ path: match[2],
934
+ })
935
+ if (detail === 'Artifact directory could not be prepared') return t('healthArtifactDirectoryFailed')
936
+ if (detail === 'Connection was not tested; pass testConnection=true to query the configured /models endpoint') return t('healthConnectionNotTested')
937
+ if (detail === 'Connection test skipped because the configured credential is unavailable') return t('healthConnectionCredentialMissing')
938
+ match = /^Service responded at (.+) \(HTTP (\d+)\)$/u.exec(detail)
939
+ if (match !== null) return t('healthServiceResponded', { endpoint: match[1], status: match[2] })
940
+ match = /^Service rejected the configured credential \(HTTP (\d+)\)$/u.exec(detail)
941
+ if (match !== null) return t('healthServiceRejectedCredential', { status: match[1] })
942
+ match = /^Service is reachable but does not expose GET \/models \(HTTP (\d+)\)$/u.exec(detail)
943
+ if (match !== null) return t('healthServiceNoModels', { status: match[1] })
944
+ if (detail === 'Service is reachable but rate-limited the connection test (HTTP 429)') return t('healthServiceRateLimited')
945
+ match = /^Service connection test failed with HTTP (\d+)$/u.exec(detail)
946
+ if (match !== null) return t('healthServiceHttpFailed', { status: match[1] })
947
+ match = /^Service could not be reached at (.+)$/u.exec(detail)
948
+ if (match !== null) return t('healthServiceUnreachable', { endpoint: match[1] })
949
+ return detail
950
+ }
951
+
952
+ function credentialSource(source: string, t: Translate): string {
953
+ if (source === 'env') return t('sourceEnv')
954
+ if (source === 'file') return t('sourceFile')
955
+ return source
956
+ }
957
+
700
958
  function LoadedSettings({ controller, t }: SettingsInjected) {
701
959
  const state = useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot)
702
960
  const snapshot = state.snapshot
@@ -719,7 +977,7 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
719
977
  const save = (): void => {
720
978
  try {
721
979
  setDraftError(undefined)
722
- void controller.save(valueOf(draft), snapshot.settings.revision)
980
+ void controller.save(valueOf(draft, t), snapshot.settings.revision)
723
981
  } catch (error) {
724
982
  setDraftError(error instanceof Error ? error.message : String(error))
725
983
  }
@@ -729,10 +987,6 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
729
987
 
730
988
  return (
731
989
  <div className="dvt-settings">
732
- <header className="dvt-settings-header">
733
- <div><span className="dvt-kicker">DSH native plugin</span><h2>{t('settingsTitle')}</h2><p>{t('settingsIntro')}</p></div>
734
- <div className="dvt-release"><span>{t('pluginVersion')} <strong>{snapshot.release.pluginVersion}</strong></span><span>{t('upstreamVersion')} <strong>{snapshot.release.upstreamVersion}</strong></span><span>{t('activeGeneration')} <strong>{snapshot.runtime.generation}</strong></span></div>
735
- </header>
736
990
  <div className="dvt-alert notice">{t('externalNotice')}</div>
737
991
  {!snapshot.writable ? <div className="dvt-alert warning">{t('readOnly')}</div> : null}
738
992
  {draftError === undefined ? null : <div className="dvt-alert error">{draftError}</div>}
@@ -740,36 +994,54 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
740
994
  {state.message === 'saved' ? <div className="dvt-alert success">{t('saved')}</div> : null}
741
995
  {snapshot.runtime.lastError === undefined ? null : <div className="dvt-alert error"><strong>{runtimeErrorTitle}</strong><span>{snapshot.runtime.lastError}</span></div>}
742
996
 
743
- <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('provider')}</h3><span className={`dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`}>{snapshot.credential.configured ? t('configured') : t('missing')}</span></div>
997
+ <section className="dvt-panel dvt-essential"><div className="dvt-panel-title"><div><h3>{t('provider')}</h3><p>{t('providerHint')}</p></div><span className={`dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`}>{snapshot.credential.configured ? t('configured') : t('missing')}</span></div>
744
998
  <div className="dvt-form-grid">
999
+ <Field label={t('protocol')}><select value={draft.protocol} onChange={(event) => { update('protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
745
1000
  <Field label={t('baseUrl')}><Input value={draft.baseUrl} onChange={(event) => { update('baseUrl', event.target.value) }} /></Field>
746
1001
  <Field label={t('model')}><Input value={draft.model} onChange={(event) => { update('model', event.target.value) }} /></Field>
747
- <Field label={t('credential')} hint={snapshot.credential.source === undefined ? undefined : `${t('source')}: ${snapshot.credential.source}`}><Input value={draft.credential} onChange={(event) => { update('credential', event.target.value) }} /></Field>
748
- <Field label={t('language')}><select value={draft.language} onChange={(event) => { update('language', event.target.value as 'zh' | 'en') }}><option value="zh">中文</option><option value="en">English</option></select></Field>
1002
+ <Field label={t('credential')} hint={snapshot.credential.source === undefined ? undefined : t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}><Input value={draft.credential} onChange={(event) => { update('credential', event.target.value) }} /></Field>
749
1003
  </div>
750
1004
  </section>
751
1005
 
752
- <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('limits')}</h3></div><div className="dvt-form-grid">
753
- <Field label={t('timeout')}><Input inputMode="numeric" value={draft.timeoutMs} onChange={(event) => { update('timeoutMs', event.target.value) }} /></Field>
754
- <Field label={t('maxBytes')}><Input inputMode="numeric" value={draft.maxImageBytes} onChange={(event) => { update('maxImageBytes', event.target.value) }} /></Field>
755
- <Field label={t('maxPixels')}><Input inputMode="numeric" value={draft.maxImagePixels} onChange={(event) => { update('maxImagePixels', event.target.value) }} /></Field>
756
- <Field label={t('concurrency')}><Input inputMode="numeric" value={draft.concurrency} onChange={(event) => { update('concurrency', event.target.value) }} /></Field>
757
- </div></section>
758
-
759
- <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source ?? 'ready' : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
760
- <Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">managed</option><option value="external">external</option></select></Field>
761
- {draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
762
- <Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
763
- <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
764
- </div>
765
- {snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
766
- </section>
767
-
768
1006
  <div className="dvt-save-row"><Button variant="primary" disabled={!snapshot.writable || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button><Button variant="outline" disabled={busy} onClick={() => { void controller.load() }}>{t('reload')}</Button></div>
769
1007
 
770
1008
  <section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('health')}</h3><p>{t('connectionHint')}</p></div><div className="dvt-actions"><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth(false) }}>{state.action === 'health' ? t('testing') : t('runHealth')}</Button><Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth(true) }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button></div></div>
771
- {state.health === undefined ? <p className="dvt-muted">{t('notTested')}</p> : <div className="dvt-health-grid">{Object.entries(state.health.checks).map(([name, check]) => <div key={name} data-status={check.status}><span>{name}</span><strong>{check.status}</strong><p>{check.detail}</p></div>)}</div>}
1009
+ <p className="dvt-muted">{t('saveBeforeTesting')}</p>
1010
+ {state.health === undefined ? <p className="dvt-muted">{t('notTested')}</p> : <div className="dvt-health-grid">{Object.entries(state.health.checks).map(([name, check]) => <div key={name} data-status={check.status}><span>{t(HEALTH_NAME_KEYS[name] ?? 'health')}</span><strong>{t(HEALTH_STATUS_KEYS[check.status])}</strong><p>{healthDetail(name, check.detail, t)}</p></div>)}</div>}
772
1011
  </section>
1012
+
1013
+ <details className="dvt-advanced">
1014
+ <summary><span><strong>{t('advanced')}</strong><small>{t('advancedHint')}</small></span><span className="dvt-details-chevron" aria-hidden="true">⌄</span></summary>
1015
+ <div className="dvt-advanced-body">
1016
+ <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('provider')}</h3></div><div className="dvt-form-grid">
1017
+ {draft.protocol === 'anthropic' ? <Field label={t('anthropicThinking')} hint={t('anthropicThinkingHint')}><select aria-label={t('anthropicThinking')} value={draft.anthropicThinking} onChange={(event) => { update('anthropicThinking', event.target.value as 'omit' | 'disabled' | 'adaptive') }}><option value="omit">omit (widest compatibility)</option><option value="disabled">disabled (model support required)</option><option value="adaptive">adaptive (model support required)</option></select></Field> : null}
1018
+ <Field label={t('userAgent')}><Input value={draft.userAgent} onChange={(event) => { update('userAgent', event.target.value) }} /></Field>
1019
+ </div></section>
1020
+
1021
+ <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('limits')}</h3></div><div className="dvt-form-grid">
1022
+ <Field label={t('language')}><select value={draft.language} onChange={(event) => { update('language', event.target.value as 'zh' | 'en') }}><option value="zh">中文</option><option value="en">English</option></select></Field>
1023
+ <Field label={t('timeout')}><Input inputMode="numeric" value={draft.timeoutMs} onChange={(event) => { update('timeoutMs', event.target.value) }} /></Field>
1024
+ <Field label={t('maxBytes')}><Input inputMode="numeric" value={draft.maxImageBytes} onChange={(event) => { update('maxImageBytes', event.target.value) }} /></Field>
1025
+ <Field label={t('maxPixels')}><Input inputMode="numeric" value={draft.maxImagePixels} onChange={(event) => { update('maxImagePixels', event.target.value) }} /></Field>
1026
+ <Field label={t('concurrency')}><Input inputMode="numeric" value={draft.concurrency} onChange={(event) => { update('concurrency', event.target.value) }} /></Field>
1027
+ </div></section>
1028
+
1029
+ <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
1030
+ <Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">{t('runtimeManaged')}</option><option value="external">{t('runtimeExternal')}</option></select></Field>
1031
+ {draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
1032
+ <Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
1033
+ <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
1034
+ </div>
1035
+ {snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
1036
+ </section>
1037
+
1038
+ </div>
1039
+ </details>
1040
+
1041
+ <footer className="dvt-settings-footer">
1042
+ <div><span className="dvt-kicker">{t('pluginKind')}</span><h2>{t('settingsTitle')}</h2><p>{t('settingsIntro')}</p></div>
1043
+ <div className="dvt-release"><span>{t('pluginVersion')} <strong>{snapshot.release.pluginVersion}</strong></span><span>{t('upstreamVersion')} <strong>{snapshot.release.upstreamVersion}</strong></span><span>{t('activeGeneration')} <strong>{t('activeGenerationValue', { generation: snapshot.runtime.generation })}</strong></span></div>
1044
+ </footer>
773
1045
  </div>
774
1046
  )
775
1047
  }
@@ -779,10 +1051,11 @@ const CSS = `
779
1051
  .dvt-tool-head{width:100%;min-height:38px;display:flex;align-items:center;gap:7px;padding:8px 10px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;font:inherit}.dvt-tool-head:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}.dvt-tool-icon{width:20px;height:20px;display:grid;place-items:center;border-radius:6px;color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);flex:none}.dvt-tool-title{font-size:12px;font-weight:650;white-space:nowrap}.dvt-tool-sep{opacity:.35}.dvt-tool-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-secondary)}.dvt-tool-status{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-tool[data-state=error] .dvt-tool-status{color:var(--dsw-alias-state-error-primary)}.dvt-chevron{margin-left:auto;transition:transform .16s ease;opacity:.55}.dvt-chevron[data-open=true]{transform:rotate(180deg)}.dvt-tool-body{padding:0 10px 10px}.dvt-stack{display:grid;gap:10px}.dvt-muted{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
780
1052
  .dvt-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dvt-metrics>div,.dvt-diff-score{padding:10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);display:grid;gap:4px}.dvt-metrics span,.dvt-diff-score span{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--dsw-alias-label-secondary)}.dvt-metrics strong,.dvt-diff-score strong{font-size:13px}.dvt-list{list-style:none;margin:0;padding:0;display:grid;gap:4px;max-height:160px;overflow:auto}.dvt-list li{display:flex;justify-content:space-between;gap:12px;padding:6px 8px;border-radius:7px;background:var(--dsw-alias-bg-layer-2);font-size:11px}.dvt-list code{color:var(--dsw-alias-state-business-primary)}.dvt-table-wrap{max-height:220px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-table{width:100%;border-collapse:collapse;font-size:11px}.dvt-table th,.dvt-table td{padding:7px 8px;text-align:left;border-bottom:1px solid var(--dsw-alias-border-l1)}.dvt-table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em}.dvt-table tr:last-child td{border-bottom:0}
781
1053
  .dvt-artifact{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}.dvt-preview{display:block;width:100%;max-height:360px;object-fit:contain;background:repeating-conic-gradient(var(--dsw-alias-bg-module-platform) 0 25%,var(--dsw-alias-bg-layer-1) 0 50%) 50%/18px 18px;border:0}.dvt-svg{height:280px}.dvt-artifact-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px}.dvt-artifact-meta>div:first-child{min-width:0;display:grid;gap:2px}.dvt-artifact-meta strong{font-size:12px;overflow:hidden;text-overflow:ellipsis}.dvt-artifact-meta span,.dvt-artifact-meta small{font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dvt-download{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);text-decoration:none;font-size:12px;font-weight:600}.dvt-download:hover{background:var(--dsw-alias-button-primary-hover)}.dvt-download:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.dvt-artifact>.dvt-muted{padding:0 10px 10px}.dvt-diff-score>div{height:5px;border-radius:99px;background:var(--dsw-alias-border-l2);overflow:hidden}.dvt-diff-score i{display:block;height:100%;min-width:2px;background:linear-gradient(90deg,var(--dsw-alias-state-warn-primary),var(--dsw-alias-state-error-primary));border-radius:99px}.dvt-tool h4{font-size:11px;margin:0 0 6px}.dvt-palette{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:7px}.dvt-palette>div{display:flex;align-items:center;gap:8px;padding:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px}.dvt-palette i{width:28px;height:28px;border-radius:7px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}.dvt-palette span{display:grid}.dvt-palette strong{font-size:11px}.dvt-palette small{font-size:10px;color:var(--dsw-alias-label-secondary)}
782
- .dvt-settings{display:grid;gap:14px;max-width:900px;padding:8px 2px 32px;color:var(--dsw-alias-label-primary)}.dvt-settings-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:8px 2px}.dvt-settings-header h2{font-size:25px;letter-spacing:-.025em;margin:3px 0 6px}.dvt-settings-header p{max-width:620px;margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.55}.dvt-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dsw-alias-state-business-primary);font-weight:700}.dvt-release{display:grid;gap:4px;min-width:170px;padding:9px 11px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-release span{display:flex;justify-content:space-between;gap:12px}.dvt-release strong{color:var(--dsw-alias-label-primary)}.dvt-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5;display:grid;gap:3px}.dvt-alert.notice{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}.dvt-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-alert.success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-panel{display:grid;gap:12px;padding:15px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-panel-title{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.dvt-panel-title h3{font-size:14px;margin:0}.dvt-panel-title p{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary);margin:4px 0 0;max-width:620px}.dvt-badge{font-size:10px;padding:3px 7px;border-radius:999px;font-weight:650}.dvt-badge.ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-badge.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.dvt-field{display:grid;gap:6px;align-content:start}.dvt-field>span{font-size:11px;font-weight:600}.dvt-field>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dvt-field select,.dvt-field textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:8px 10px}.dvt-field select{height:36px}.dvt-field textarea{resize:vertical;min-height:76px}.dvt-runtime-facts{display:grid;gap:4px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:auto}.dvt-runtime-facts code{font-size:10px;white-space:nowrap;color:var(--dsw-alias-label-secondary)}.dvt-save-row{display:flex;gap:8px;padding:2px 0}
1054
+ .dvt-settings{display:grid;gap:14px;max-width:900px;padding:8px 2px 32px;color:var(--dsw-alias-label-primary)}.dvt-settings-footer{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding:8px 2px}.dvt-settings-footer h2{font-size:25px;letter-spacing:-.025em;margin:3px 0 6px}.dvt-settings-footer p{max-width:620px;margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.55}.dvt-kicker{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--dsw-alias-state-business-primary);font-weight:700}.dvt-release{display:grid;gap:4px;min-width:170px;padding:9px 11px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:10px;color:var(--dsw-alias-label-secondary)}.dvt-release span{display:flex;justify-content:space-between;gap:12px}.dvt-release strong{color:var(--dsw-alias-label-primary)}.dvt-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5;display:grid;gap:3px}.dvt-alert.notice{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}.dvt-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-alert.success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-panel{display:grid;gap:12px;padding:15px;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-panel-title{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.dvt-panel-title h3{font-size:14px;margin:0}.dvt-panel-title p{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary);margin:4px 0 0;max-width:620px}.dvt-badge{font-size:10px;padding:3px 7px;border-radius:999px;font-weight:650}.dvt-badge.ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-badge.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}.dvt-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.dvt-field{display:grid;gap:6px;align-content:start}.dvt-field>span{font-size:11px;font-weight:600}.dvt-field>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dvt-field select,.dvt-field textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:8px 10px}.dvt-field select{height:36px}.dvt-field textarea{resize:vertical;min-height:76px}.dvt-runtime-facts{display:grid;gap:4px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:auto}.dvt-runtime-facts code{font-size:10px;white-space:nowrap;color:var(--dsw-alias-label-secondary)}.dvt-save-row{display:flex;gap:8px;padding:2px 0}
1055
+ .dvt-settings-footer{margin-top:8px;padding:20px 2px 4px;border-top:1px solid var(--dsw-alias-border-l1);opacity:.82}.dvt-settings-footer h2{font-size:18px;letter-spacing:-.015em;margin:3px 0 5px}.dvt-settings-footer p{font-size:11px;line-height:1.5}.dvt-release{min-width:220px}.dvt-release span{white-space:nowrap}.dvt-essential{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,var(--dsw-alias-border-l1));box-shadow:var(--dsw-shadow-lv1),0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 5%,transparent)}.dvt-advanced{border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dvt-advanced>summary{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 15px;cursor:pointer;list-style:none}.dvt-advanced>summary::-webkit-details-marker{display:none}.dvt-advanced>summary>span:first-child{display:grid;gap:3px}.dvt-advanced>summary strong{font-size:13px}.dvt-advanced>summary small{font-size:10px;line-height:1.45;color:var(--dsw-alias-label-secondary);font-weight:400}.dvt-details-chevron{font-size:15px;opacity:.55;transition:transform .16s ease}.dvt-advanced[open] .dvt-details-chevron{transform:rotate(180deg)}.dvt-advanced-body{display:grid;gap:12px;padding:0 12px 12px}.dvt-advanced-body>.dvt-panel{box-shadow:none}
783
1056
  .dvt-health-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.dvt-health-grid>div{padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-border-l4)}.dvt-health-grid>div[data-status=ok]{border-left-color:var(--dsw-alias-state-success-primary)}.dvt-health-grid>div[data-status=warning],.dvt-health-grid>div[data-status=not_tested]{border-left-color:var(--dsw-alias-state-warn-primary)}.dvt-health-grid>div[data-status=error]{border-left-color:var(--dsw-alias-state-error-primary)}.dvt-health-grid span{font-size:10px;text-transform:capitalize}.dvt-health-grid strong{float:right;font-size:9px;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}.dvt-health-grid p{clear:both;margin:5px 0 0;font-size:10px;line-height:1.4;color:var(--dsw-alias-label-secondary)}.dvt-loading{padding:24px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-secondary)}
784
1057
  .dvt-paste-dock{box-sizing:border-box;width:calc(100% - 32px);max-width:var(--dsh-composer-card-max-width,960px);margin:0 auto;display:flex;flex-wrap:wrap;gap:6px;padding:0 2px 6px}.dvt-paste-chip{max-width:100%;height:32px;box-sizing:border-box;display:flex;align-items:center;gap:7px;padding:0 6px 0 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-specific-tip);font-size:12px}.dvt-paste-chip[data-status=copying]{border-color:var(--dsw-alias-state-business-primary)}.dvt-paste-chip[data-status=error]{border-color:var(--dsw-alias-state-error-primary)}.dvt-paste-name{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-detail{color:var(--dsw-alias-label-caption);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvt-paste-chip[data-status=error] .dvt-paste-detail{color:var(--dsw-alias-state-error-primary)}.dvt-paste-chip button{width:20px;height:20px;display:grid;place-items:center;border:0;border-radius:50%;padding:0;background:transparent;color:var(--dsw-alias-label-caption);font:inherit;font-size:16px;cursor:pointer}.dvt-paste-chip button:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dvt-paste-chip button:disabled{opacity:.4;cursor:default}
785
- @media(max-width:720px){.dvt-settings-header{display:grid}.dvt-release{width:auto}.dvt-form-grid{grid-template-columns:1fr}.dvt-metrics{grid-template-columns:1fr}.dvt-artifact-meta{align-items:flex-start;flex-direction:column}.dvt-panel-title{flex-direction:column}}
1058
+ @media(max-width:720px){.dvt-settings-footer{display:grid}.dvt-release{width:auto}.dvt-form-grid{grid-template-columns:1fr}.dvt-metrics{grid-template-columns:1fr}.dvt-artifact-meta{align-items:flex-start;flex-direction:column}.dvt-panel-title{flex-direction:column}}
786
1059
  `
787
1060
 
788
1061
  function installStyles(): () => void {