@mengruo/dsh-vision-toolkit 0.1.4 → 0.1.6-beta.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.
Files changed (74) hide show
  1. package/README.md +11 -70
  2. package/README.zh.md +11 -69
  3. package/assets/1.mp4 +0 -0
  4. package/assets/skill/SKILL.md +37 -5
  5. package/docs/plan-per-tool-visibility.md +82 -0
  6. package/lib/client.js +157 -6
  7. package/lib/client.js.map +1 -1
  8. package/lib/config.js +52 -0
  9. package/lib/config.js.map +1 -1
  10. package/lib/exposure.js +35 -8
  11. package/lib/exposure.js.map +1 -1
  12. package/lib/index.js +1 -1
  13. package/lib/index.js.map +1 -1
  14. package/lib/object-storage.js +141 -0
  15. package/lib/object-storage.js.map +1 -0
  16. package/lib/paths.js +9 -0
  17. package/lib/paths.js.map +1 -1
  18. package/lib/runtime.js +314 -27
  19. package/lib/runtime.js.map +1 -1
  20. package/lib/tools.js +116 -4
  21. package/lib/tools.js.map +1 -1
  22. package/lib/types/client/index.d.ts +62 -1
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/config.d.ts +59 -0
  25. package/lib/types/config.d.ts.map +1 -1
  26. package/lib/types/exposure.d.ts +14 -2
  27. package/lib/types/exposure.d.ts.map +1 -1
  28. package/lib/types/index.d.ts.map +1 -1
  29. package/lib/types/object-storage.d.ts +54 -0
  30. package/lib/types/object-storage.d.ts.map +1 -0
  31. package/lib/types/paths.d.ts +7 -0
  32. package/lib/types/paths.d.ts.map +1 -1
  33. package/lib/types/runtime.d.ts +50 -0
  34. package/lib/types/runtime.d.ts.map +1 -1
  35. package/lib/types/tools.d.ts +22 -1
  36. package/lib/types/tools.d.ts.map +1 -1
  37. package/lib/types/upstream.d.ts +1 -0
  38. package/lib/types/upstream.d.ts.map +1 -1
  39. package/lib/types/video.d.ts +78 -0
  40. package/lib/types/video.d.ts.map +1 -0
  41. package/lib/types/web.d.ts +8 -0
  42. package/lib/types/web.d.ts.map +1 -1
  43. package/lib/upstream.js +3 -0
  44. package/lib/upstream.js.map +1 -1
  45. package/lib/video.js +169 -0
  46. package/lib/video.js.map +1 -0
  47. package/lib/web.js +78 -6
  48. package/lib/web.js.map +1 -1
  49. package/package.json +4 -1
  50. package/src/client/index.tsx +222 -6
  51. package/src/config.ts +110 -0
  52. package/src/exposure.ts +34 -9
  53. package/src/index.ts +10 -5
  54. package/src/object-storage.ts +174 -0
  55. package/src/paths.ts +11 -0
  56. package/src/runtime.ts +332 -25
  57. package/src/tools.ts +144 -3
  58. package/src/upstream.ts +4 -0
  59. package/src/video.ts +222 -0
  60. package/src/web.ts +93 -7
  61. package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +11 -11
  62. package/vendor/agent-vision-toolkit/__pycache__/detect.cpython-314.pyc +0 -0
  63. package/vendor/agent-vision-toolkit/__pycache__/ground.cpython-314.pyc +0 -0
  64. package/vendor/agent-vision-toolkit/__pycache__/vision_client.cpython-314.pyc +0 -0
  65. package/vendor/agent-vision-toolkit/bin/__pycache__/glancecpython-314.pyc +0 -0
  66. package/vendor/agent-vision-toolkit/bin/glance +8 -1
  67. package/vendor/agent-vision-toolkit/detect.py +13 -7
  68. package/vendor/agent-vision-toolkit/ground.py +43 -18
  69. package/vendor/agent-vision-toolkit/tests/test_vision_client.py +88 -0
  70. package/vendor/agent-vision-toolkit/vision_client.py +84 -6
  71. package/assets/community-group-qr.png +0 -0
  72. package/assets/logo_aihubmix.png +0 -0
  73. package/assets/logo_eapi_dark.png +0 -0
  74. package/assets/wechat-reward.png +0 -0
@@ -31,6 +31,8 @@ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKi
31
31
  const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1'
32
32
  const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION'
33
33
  const BUILT_IN_FREE_VISION_MODEL = 'gemini-3.7-flash'
34
+ // Fixed credential reference holding the combined "accessKeyId:secretAccessKey".
35
+ const OBJECT_STORAGE_CREDENTIAL = 'VISION_OBJECT_STORAGE'
34
36
 
35
37
  const en = {
36
38
  nav: 'Vision',
@@ -42,6 +44,7 @@ const en = {
42
44
  providers: 'Vision providers',
43
45
  name: 'Label',
44
46
  enabled: 'Enabled',
47
+ disabled: 'Disabled',
45
48
  priority: 'Priority {index}',
46
49
  attempts: 'Attempts',
47
50
  attemptsHint: 'Total attempts against this provider before failing over to the next one.',
@@ -70,6 +73,12 @@ const en = {
70
73
  anthropicThinking: 'Anthropic thinking',
71
74
  anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
72
75
  userAgent: 'User-Agent',
76
+ stream: 'Streaming',
77
+ streamHint: 'Request a streamed (SSE) completion instead of one JSON response. Use it for endpoints with weak non-streaming support or that time out on long outputs.',
78
+ uploadViaUrl: 'Transfer images via URL',
79
+ uploadViaUrlHint: 'Upload each image to the configured object storage and send the model a URL instead of base64. Requires object storage (below); region crops fall back to base64.',
80
+ videoSupport: 'Enable video support',
81
+ videoSupportHint: 'OpenAI-compatible video understanding. This setting is hidden for Anthropic providers and is currently a placeholder with no effect yet.',
73
82
  language: 'Output language',
74
83
  limits: 'Timeout and concurrency',
75
84
  timeout: 'Request timeout (ms)',
@@ -95,10 +104,24 @@ const en = {
95
104
  storageDirHint: 'Leave blank to keep .dsh-vision-toolkit in each workspace. On POSIX systems, set an absolute shared root such as /tmp/dsh-vision-toolkit to store artifacts, pasted images, and caches in an automatically generated per-user, workspace-specific child directory. Windows shared roots remain disabled until their ACLs can be verified safely.',
96
105
  allowedDirs: 'Additional allowed directories',
97
106
  allowedDirsHint: 'One path per line. The session workspace is always allowed.',
107
+ objectStorage: 'Object storage',
108
+ objectStorageHint: 'S3-compatible storage used when a provider enables URL image transfer. The access key is stored as a DSH credential and never shown again after saving.',
109
+ storageEndpoint: 'Endpoint',
110
+ storageBucket: 'Bucket',
111
+ storageAccessKeyId: 'Access Key ID',
112
+ storageSecretAccessKey: 'Secret Access Key',
113
+ storagePublicBase: 'Public base URL (optional)',
114
+ storagePublicBaseHint: 'Custom domain or r2.dev URL for permanent public links. Leave blank to use temporary presigned URLs.',
115
+ testStorage: 'Test object storage',
116
+ testingStorage: 'Testing…',
117
+ storageKeyPair: 'Access Key ID and Secret Access Key must both be filled together.',
98
118
  save: 'Save and apply',
99
119
  saving: 'Validating runtime…',
100
120
  reload: 'Reload',
101
121
  saved: 'Settings validated and applied.',
122
+ unsavedChanges: 'Changed but not saved',
123
+ discardChanges: 'Discard changes',
124
+ noUnsavedChanges: 'Settings unchanged',
102
125
  readOnly: 'Service settings are read-only. A writable API key can still be saved.',
103
126
  configured: 'Configured',
104
127
  missing: 'Missing',
@@ -110,8 +133,10 @@ const en = {
110
133
  runHealth: 'Run health check',
111
134
  testConnection: 'Test API connection',
112
135
  testModel: 'Test vision model',
136
+ testVideo: 'Test video call',
113
137
  testing: 'Checking…',
114
138
  testingModel: 'Testing model…',
139
+ testingVideo: 'Testing video…',
115
140
  connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
116
141
  saveBeforeTesting: 'Save service changes before testing the connection.',
117
142
  advanced: 'Advanced settings',
@@ -120,6 +145,14 @@ const en = {
120
145
  hiddenVariants: 'Transparent variant routing',
121
146
  hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
122
147
  hiddenVariantsHint: 'Text-only models keep one model-selector entry with the original name while the session runs on the image-capable variant. Pasted images, image history, and the built-in read_image tool keep working; disable to restore the explicit (Vision Toolkit) entries.',
148
+ toolVisibility: 'Tool visibility',
149
+ toolVisibilityHint: 'Which groups of vision tools an Agent sees. Applies to the next Agent conversation: an already-active Agent keeps its activation-time set until it is reloaded.',
150
+ toolVisibilityLocal: 'Local tools',
151
+ toolVisibilityLocalHint: 'trace / crop / pixel diff / foreground / colors / HTML screenshot / video info. Local processing, no on-line parallel limits.',
152
+ toolVisibilityOnline: 'Online image tools',
153
+ toolVisibilityOnlineHint: 'glance / ground / detect / long-screenshot OCR, plus the concurrency/status probe. On-line service calls share the session concurrency budget.',
154
+ toolVisibilityVideo: 'Video tools',
155
+ toolVisibilityVideoHint: 'video understanding (experimental). Calling it without an enabled video-capable service returns "video understanding unavailable".',
123
156
  pluginVersion: 'Plugin',
124
157
  upstreamVersion: 'Upstream',
125
158
  activeGeneration: 'Runtime generation',
@@ -258,6 +291,7 @@ const zh: Record<LocaleKey, string> = {
258
291
  providers: '视觉服务列表',
259
292
  name: '名称',
260
293
  enabled: '启用',
294
+ disabled: '禁用',
261
295
  priority: '优先级 {index}',
262
296
  attempts: '尝试次数',
263
297
  attemptsHint: '该服务在故障转移到下一个之前最多尝试的次数。',
@@ -286,6 +320,12 @@ const zh: Record<LocaleKey, string> = {
286
320
  anthropicThinking: 'Anthropic thinking',
287
321
  anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
288
322
  userAgent: 'User-Agent',
323
+ stream: '流式请求',
324
+ streamHint: '以流式(SSE)方式请求补全,而非一次性返回 JSON。适合对非流式支持不佳、长输出易连接超时的端点。',
325
+ uploadViaUrl: '通过 URL 传输图片',
326
+ uploadViaUrlHint: '把每张图片上传到下方配置的对象存储,将 URL 传给模型而不是 base64。需要先配置对象存储;使用 region 裁剪时回退为 base64。',
327
+ videoSupport: '启用视频支持',
328
+ videoSupportHint: 'OpenAI 兼容接口的视频理解能力。Anthropic 服务不显示此项;当前为占位设置,尚未生效。',
289
329
  language: '结果语言',
290
330
  limits: '超时与并发限制',
291
331
  timeout: '单次请求超时(毫秒)',
@@ -311,10 +351,24 @@ const zh: Record<LocaleKey, string> = {
311
351
  storageDirHint: '留空时继续在各工作区创建 .dsh-vision-toolkit。在 POSIX 系统上填写绝对路径共享根目录(例如 /tmp/dsh-vision-toolkit)后,产物、粘贴图片和缓存会保存到自动生成的当前用户、工作区专属子目录中。Windows 共享目录会保持禁用,直到能够安全校验 ACL 权限。',
312
352
  allowedDirs: '允许读取的其他目录',
313
353
  allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
354
+ objectStorage: '对象存储',
355
+ objectStorageHint: '用于「通过 URL 传输图片」的 S3 兼容存储。访问密钥以 DSH 凭据保存,保存后不再显示。',
356
+ storageEndpoint: 'Endpoint',
357
+ storageBucket: 'Bucket',
358
+ storageAccessKeyId: 'Access Key ID',
359
+ storageSecretAccessKey: 'Secret Access Key',
360
+ storagePublicBase: '公网 URL 前缀(可选)',
361
+ storagePublicBaseHint: '自定义域名或 r2.dev 地址,用于生成永久公网链接。留空则使用临时预签名 URL。',
362
+ testStorage: '测试对象存储',
363
+ testingStorage: '测试中…',
364
+ storageKeyPair: 'Access Key ID 与 Secret Access Key 必须同时填写。',
314
365
  save: '保存设置',
315
366
  saving: '正在检查并应用…',
316
367
  reload: '重新加载',
317
368
  saved: '设置已保存并生效。',
369
+ unsavedChanges: '已更改但尚未保存',
370
+ discardChanges: '丢弃更改',
371
+ noUnsavedChanges: '设置项未变更',
318
372
  readOnly: '服务设置来自只读配置;如果 API 密钥可写,仍可在此保存密钥。',
319
373
  configured: '已就绪',
320
374
  missing: '未配置',
@@ -326,8 +380,10 @@ const zh: Record<LocaleKey, string> = {
326
380
  runHealth: '检查本地环境',
327
381
  testConnection: '测试 API 连接',
328
382
  testModel: '测试视觉模型',
383
+ testVideo: '测试视频调用',
329
384
  testing: '检查中…',
330
385
  testingModel: '正在测试模型…',
386
+ testingVideo: '正在测试视频…',
331
387
  connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
332
388
  saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
333
389
  advanced: '高级设置',
@@ -336,6 +392,14 @@ const zh: Record<LocaleKey, string> = {
336
392
  hiddenVariants: '透明变体路由',
337
393
  hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
338
394
  hiddenVariantsHint: '文本模型在模型列表中只显示原名称,会话实际运行在支持图片的变体路由上:粘贴图片、历史图片和内置 read_image 工具均可正常使用。关闭后恢复显示显式的(Vision Toolkit)条目。',
395
+ toolVisibility: '工具可见性',
396
+ toolVisibilityHint: 'Agent 可见的视觉工具分组。仅在下一个 agent 对话中生效:已激活的 agent 保持激活时的工具集,直到重载。',
397
+ toolVisibilityLocal: '本地工具',
398
+ toolVisibilityLocalHint: 'trace / crop / 像素对比 / 前景提取 / 取色 / HTML 截图 / 视频信息。本地处理,不受联机并发限制。',
399
+ toolVisibilityOnline: '联机图片工具',
400
+ toolVisibilityOnlineHint: 'glance / ground / detect / 长截图 OCR,以及并发/状态查询。联机调用共享会话并发额度。',
401
+ toolVisibilityVideo: '视频工具',
402
+ toolVisibilityVideoHint: '视频理解(测试)。调用时若没有已启用且支持视频的服务,将返回“视频理解不可用”。',
339
403
  pluginVersion: '插件版本',
340
404
  upstreamVersion: '工具包版本',
341
405
  activeGeneration: '本次运行已应用',
@@ -527,6 +591,9 @@ interface ProviderValue {
527
591
  protocol?: 'openai' | 'anthropic'
528
592
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
529
593
  userAgent?: string
594
+ stream?: boolean
595
+ uploadViaUrl?: boolean
596
+ videoSupport?: boolean
530
597
  t1Seconds?: number
531
598
  t2Seconds?: number
532
599
  maxImageBytes?: number
@@ -543,6 +610,9 @@ interface SettingsValue {
543
610
  protocol?: 'openai' | 'anthropic'
544
611
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
545
612
  userAgent?: string
613
+ stream?: boolean
614
+ uploadViaUrl?: boolean
615
+ videoSupport?: boolean
546
616
  }
547
617
  providers?: ProviderValue[]
548
618
  language?: 'zh' | 'en'
@@ -552,6 +622,12 @@ interface SettingsValue {
552
622
  maxImageBytes?: number
553
623
  maxImagePixels?: number
554
624
  concurrency?: number
625
+ objectStorage?: {
626
+ endpoint?: string
627
+ bucket?: string
628
+ credential?: string
629
+ publicBase?: string
630
+ }
555
631
  runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
556
632
  storageDir?: string
557
633
  storageHistory?: string[]
@@ -562,6 +638,11 @@ interface SettingsValue {
562
638
  autoSwitch?: boolean
563
639
  hidden?: boolean
564
640
  }
641
+ toolVisibility?: {
642
+ local?: boolean
643
+ online?: boolean
644
+ video?: boolean
645
+ }
565
646
  }
566
647
 
567
648
  type PluginUpdateUnavailableReason =
@@ -611,6 +692,7 @@ interface SettingsSnapshot {
611
692
  settings: { value: SettingsValue; revision: number; applies: 'live' }
612
693
  credential: { ref: string; configured: boolean; source?: string; writable: boolean }
613
694
  credentials: Array<{ ref: string; configured: boolean; source?: string; writable: boolean }>
695
+ objectStorageCredential: { ref: string; configured: boolean; writable: boolean }
614
696
  runtime: {
615
697
  ready: boolean
616
698
  generation: number
@@ -750,6 +832,15 @@ function VisionIcon({ kind = 'scan' }: { kind?: 'scan' | 'target' | 'layers' | '
750
832
  )
751
833
  }
752
834
 
835
+ function ReloadIcon() {
836
+ return (
837
+ <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
838
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
839
+ <path d="M21 3v5h-5" />
840
+ </svg>
841
+ )
842
+ }
843
+
753
844
  function ToolShell({
754
845
  block, title, summary, icon, children, t,
755
846
  }: {
@@ -988,7 +1079,9 @@ interface SettingsState {
988
1079
  providerHealth?: Record<number, HealthResult> | undefined
989
1080
  update?: PluginUpdateCheck | undefined
990
1081
  restart?: PluginUpdateResult | undefined
991
- action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | undefined
1082
+ storageTest?: { detail: string } | undefined
1083
+ videoTest?: { detail: string } | undefined
1084
+ action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | 'test-storage' | 'test-video' | undefined
992
1085
  message?: string | undefined
993
1086
  error?: string | undefined
994
1087
  }
@@ -1123,6 +1216,34 @@ export class VisionSettingsController {
1123
1216
  }
1124
1217
  }
1125
1218
 
1219
+ async testStorage(): Promise<void> {
1220
+ this.set({ ...this.state, action: 'test-storage', error: undefined, message: undefined })
1221
+ try {
1222
+ const result = await apiRequest<{ detail: string }>({
1223
+ method: 'POST',
1224
+ headers: { 'Content-Type': 'application/json' },
1225
+ body: JSON.stringify({ action: 'test-storage' }),
1226
+ })
1227
+ this.set({ ...this.state, action: undefined, storageTest: result })
1228
+ } catch (error) {
1229
+ this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
1230
+ }
1231
+ }
1232
+
1233
+ async runVideoTest(providerIndex?: number): Promise<void> {
1234
+ this.set({ ...this.state, action: 'test-video', error: undefined, message: undefined })
1235
+ try {
1236
+ const result = await apiRequest<{ detail: string }>({
1237
+ method: 'POST',
1238
+ headers: { 'Content-Type': 'application/json' },
1239
+ body: JSON.stringify({ action: 'test-video', ...(providerIndex === undefined ? {} : { providerIndex }) }),
1240
+ })
1241
+ this.set({ ...this.state, action: undefined, videoTest: result })
1242
+ } catch (error) {
1243
+ this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
1244
+ }
1245
+ }
1246
+
1126
1247
  async checkUpdate(): Promise<void> {
1127
1248
  this.set({ ...this.state, action: 'check-update', error: undefined, message: undefined })
1128
1249
  try {
@@ -1171,6 +1292,9 @@ interface ProviderDraft {
1171
1292
  protocol: 'openai' | 'anthropic'
1172
1293
  anthropicThinking: 'omit' | 'disabled' | 'adaptive'
1173
1294
  userAgent: string
1295
+ stream: boolean
1296
+ uploadViaUrl: boolean
1297
+ videoSupport: boolean
1174
1298
  t1Seconds: string
1175
1299
  t2Seconds: string
1176
1300
  maxImageBytes: string
@@ -1188,6 +1312,9 @@ interface Draft {
1188
1312
  maxImageBytes: string
1189
1313
  maxImagePixels: string
1190
1314
  concurrency: string
1315
+ storageEndpoint: string
1316
+ storageBucket: string
1317
+ storagePublicBase: string
1191
1318
  runtimeMode: 'managed' | 'external'
1192
1319
  toolkitPath: string
1193
1320
  python: string
@@ -1197,6 +1324,9 @@ interface Draft {
1197
1324
  variantEnabled: boolean
1198
1325
  variantProviders: string
1199
1326
  variantAutoSwitch: boolean
1327
+ toolVisibilityLocal: boolean
1328
+ toolVisibilityOnline: boolean
1329
+ toolVisibilityVideo: boolean
1200
1330
  }
1201
1331
 
1202
1332
  function randomProviderId(): string {
@@ -1231,6 +1361,9 @@ function emptyProviderDraft(defaults: ProviderDefaults): ProviderDraft {
1231
1361
  protocol: 'openai',
1232
1362
  anthropicThinking: 'omit',
1233
1363
  userAgent: DEFAULT_USER_AGENT,
1364
+ stream: false,
1365
+ uploadViaUrl: false,
1366
+ videoSupport: false,
1234
1367
  t1Seconds: '90',
1235
1368
  t2Seconds: '90',
1236
1369
  maxImageBytes: String(defaults.maxImageBytes),
@@ -1253,6 +1386,9 @@ function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderVal
1253
1386
  protocol: source?.protocol ?? 'openai',
1254
1387
  anthropicThinking: source?.anthropicThinking ?? 'omit',
1255
1388
  userAgent: source?.userAgent ?? DEFAULT_USER_AGENT,
1389
+ stream: source?.stream === true,
1390
+ uploadViaUrl: source?.uploadViaUrl === true,
1391
+ videoSupport: source?.videoSupport === true,
1256
1392
  t1Seconds: String(source?.t1Seconds ?? 90),
1257
1393
  t2Seconds: String(source?.t2Seconds ?? 90),
1258
1394
  maxImageBytes: String(source?.maxImageBytes ?? defaults.maxImageBytes),
@@ -1281,6 +1417,9 @@ function draftOf(value: SettingsValue): Draft {
1281
1417
  maxImageBytes: String(globalDefaults.maxImageBytes),
1282
1418
  maxImagePixels: String(globalDefaults.maxImagePixels),
1283
1419
  concurrency: String(globalDefaults.concurrency),
1420
+ storageEndpoint: value.objectStorage?.endpoint ?? '',
1421
+ storageBucket: value.objectStorage?.bucket ?? '',
1422
+ storagePublicBase: value.objectStorage?.publicBase ?? '',
1284
1423
  runtimeMode: value.runtime?.mode ?? 'managed',
1285
1424
  toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
1286
1425
  python: value.runtime?.python ?? '',
@@ -1290,6 +1429,9 @@ function draftOf(value: SettingsValue): Draft {
1290
1429
  variantEnabled: value.imageInputVariants?.enabled ?? true,
1291
1430
  variantProviders: (value.imageInputVariants?.providers ?? []).join('\n'),
1292
1431
  variantAutoSwitch: value.imageInputVariants?.autoSwitch ?? true,
1432
+ toolVisibilityLocal: value.toolVisibility?.local ?? true,
1433
+ toolVisibilityOnline: value.toolVisibility?.online ?? true,
1434
+ toolVisibilityVideo: value.toolVisibility?.video ?? false,
1293
1435
  }
1294
1436
  }
1295
1437
 
@@ -1320,6 +1462,9 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1320
1462
  protocol: provider.protocol,
1321
1463
  anthropicThinking: provider.anthropicThinking,
1322
1464
  ...(provider.userAgent.trim() === DEFAULT_USER_AGENT ? {} : { userAgent: provider.userAgent.trim() }),
1465
+ ...(provider.stream ? { stream: true } : {}),
1466
+ ...(provider.uploadViaUrl ? { uploadViaUrl: true } : {}),
1467
+ ...(provider.videoSupport ? { videoSupport: true } : {}),
1323
1468
  ...(provider.t1Seconds.trim().length === 0 ? {} : { t1Seconds: positiveInteger(provider.t1Seconds, t('t1'), t) }),
1324
1469
  ...(provider.t2Seconds.trim().length === 0 ? {} : { t2Seconds: positiveInteger(provider.t2Seconds, t('t2'), t) }),
1325
1470
  ...(provider.maxImageBytes.trim().length === 0 ? {} : { maxImageBytes: positiveInteger(provider.maxImageBytes, t('maxBytes'), t) }),
@@ -1336,6 +1481,9 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1336
1481
  protocol: primary?.protocol ?? 'openai',
1337
1482
  anthropicThinking: primary?.anthropicThinking ?? 'omit',
1338
1483
  userAgent: primary?.userAgent.trim() || DEFAULT_USER_AGENT,
1484
+ stream: primary?.stream === true,
1485
+ uploadViaUrl: primary?.uploadViaUrl === true,
1486
+ videoSupport: primary?.videoSupport === true,
1339
1487
  },
1340
1488
  providers,
1341
1489
  language: draft.language,
@@ -1345,6 +1493,16 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1345
1493
  maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
1346
1494
  maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
1347
1495
  concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
1496
+ ...(draft.storageEndpoint.trim().length === 0 && draft.storageBucket.trim().length === 0
1497
+ ? {}
1498
+ : {
1499
+ objectStorage: {
1500
+ endpoint: draft.storageEndpoint.trim(),
1501
+ bucket: draft.storageBucket.trim(),
1502
+ credential: OBJECT_STORAGE_CREDENTIAL,
1503
+ ...(draft.storagePublicBase.trim().length === 0 ? {} : { publicBase: draft.storagePublicBase.trim() }),
1504
+ },
1505
+ }),
1348
1506
  runtime: {
1349
1507
  mode: draft.runtimeMode,
1350
1508
  ...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
@@ -1360,6 +1518,11 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1360
1518
  ...(draft.variantAutoSwitch ? {} : { autoSwitch: false }),
1361
1519
  hidden: draft.hiddenVariants,
1362
1520
  },
1521
+ toolVisibility: {
1522
+ local: draft.toolVisibilityLocal,
1523
+ online: draft.toolVisibilityOnline,
1524
+ video: draft.toolVisibilityVideo,
1525
+ },
1363
1526
  }
1364
1527
  }
1365
1528
 
@@ -1474,6 +1637,7 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1474
1637
  const [draft, setDraft] = useState<Draft | undefined>(undefined)
1475
1638
  const [selectedIndex, setSelectedIndex] = useState(0)
1476
1639
  const [keys, setKeys] = useState<Record<string, string>>({})
1640
+ const [storageKeys, setStorageKeys] = useState<{ accessKeyId: string; secretAccessKey: string }>({ accessKeyId: '', secretAccessKey: '' })
1477
1641
  const [draftError, setDraftError] = useState<string | undefined>(undefined)
1478
1642
  const [copiedCommand, setCopiedCommand] = useState(false)
1479
1643
 
@@ -1582,9 +1746,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1582
1746
  return
1583
1747
  }
1584
1748
  }
1749
+ const accessKeyId = storageKeys.accessKeyId.trim()
1750
+ const secretAccessKey = storageKeys.secretAccessKey.trim()
1751
+ if ((accessKeyId.length === 0) !== (secretAccessKey.length === 0)) {
1752
+ setDraftError(t('storageKeyPair'))
1753
+ return
1754
+ }
1585
1755
  const credentials = Object.entries(keys)
1586
1756
  .filter(([, value]) => value.trim().length > 0)
1587
1757
  .map(([ref, value]) => ({ ref, value: value.trim() }))
1758
+ if (accessKeyId.length > 0 && (draft.storageEndpoint.trim().length > 0 || draft.storageBucket.trim().length > 0)) {
1759
+ credentials.push({ ref: OBJECT_STORAGE_CREDENTIAL, value: `${accessKeyId}:${secretAccessKey}` })
1760
+ }
1588
1761
  const savedRefs = new Set(credentials.map(credential => credential.ref))
1589
1762
  setDraftError(undefined)
1590
1763
  void controller.save(
@@ -1595,12 +1768,19 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1595
1768
  ).then(saved => {
1596
1769
  if (saved) {
1597
1770
  setKeys(current => Object.fromEntries(Object.entries(current).filter(([ref]) => !savedRefs.has(ref))))
1771
+ if (savedRefs.has(OBJECT_STORAGE_CREDENTIAL)) setStorageKeys({ accessKeyId: '', secretAccessKey: '' })
1598
1772
  }
1599
1773
  })
1600
1774
  } catch (error) {
1601
1775
  setDraftError(error instanceof Error ? error.message : String(error))
1602
1776
  }
1603
1777
  }
1778
+ const discard = (): void => {
1779
+ setDraft(draftOf(snapshot.settings.value))
1780
+ setKeys({})
1781
+ setStorageKeys({ accessKeyId: '', secretAccessKey: '' })
1782
+ setDraftError(undefined)
1783
+ }
1604
1784
  const busy = state.action !== undefined
1605
1785
  const selectedProvider = draft.providers[selectedIndex]
1606
1786
  const providerCredentials = snapshot.credentials ?? []
@@ -1608,14 +1788,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1608
1788
  const selectedKey = selectedProvider === undefined ? '' : (keys[selectedProvider.credential] ?? '')
1609
1789
  const selectedKeyLocked = selectedCredential !== undefined && !selectedCredential.writable
1610
1790
  const selectedBuiltInFree = selectedCredential?.source === 'built-in-free'
1611
- const canSave = snapshot.writable || Object.values(keys).some(value => value.trim().length > 0)
1791
+ const objectStorageCredential = snapshot.objectStorageCredential ?? { ref: '', configured: false, writable: false }
1792
+ const canSave = snapshot.writable || Object.values(keys).some(value => value.trim().length > 0) || storageKeys.accessKeyId.trim().length > 0 || storageKeys.secretAccessKey.trim().length > 0
1612
1793
  const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable')
1613
1794
  const pluginUpdate = state.update
1614
1795
  const updateCapability = pluginUpdate ?? snapshot.release.update
1615
1796
  const latestVersion = pluginUpdate?.latestVersion
1616
1797
  const updateReason = updateCapability.reason === undefined ? undefined : t(UPDATE_REASON_KEYS[updateCapability.reason])
1617
1798
  const updateCheckSupported = updateCapability.checkSupported ?? updateCapability.supported
1618
- const updateHasUnsavedChanges = Object.values(keys).some(value => value.trim().length > 0) || settingsDraftChanged(draft, snapshot.settings.value, t)
1799
+ const updateHasUnsavedChanges = Object.values(keys).some(value => value.length > 0)
1800
+ || storageKeys.accessKeyId.length > 0
1801
+ || storageKeys.secretAccessKey.length > 0
1802
+ || settingsDraftChanged(draft, snapshot.settings.value, t)
1619
1803
  const manualUpdateProfile = updateCapability.profile ?? 'web'
1620
1804
  const manualUpdateCommand = `dsh plugin --profile ${manualUpdateProfile} add @mengruo/dsh-vision-toolkit@latest --registry=https://registry.npmjs.org/`
1621
1805
  const copyManualUpdate = (): void => {
@@ -1635,6 +1819,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1635
1819
 
1636
1820
  return (
1637
1821
  <div className="dvt-settings">
1822
+ <div className="dvt-save-dock" data-dirty={updateHasUnsavedChanges ? 'true' : undefined}>
1823
+ <button type="button" className="dvt-save-dock-reload" aria-label={t('reload')} title={t('reload')} disabled={busy} onClick={() => { void controller.load() }}><ReloadIcon /></button>
1824
+ {updateHasUnsavedChanges ? (
1825
+ <>
1826
+ <span className="dvt-save-dock-text">{t('unsavedChanges')}</span>
1827
+ <Button size="sm" variant="outline" disabled={busy} onClick={discard}>{t('discardChanges')}</Button>
1828
+ <Button size="sm" variant="primary" disabled={!canSave || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button>
1829
+ </>
1830
+ ) : (
1831
+ <span className="dvt-save-dock-text">{t('noUnsavedChanges')}</span>
1832
+ )}
1833
+ </div>
1638
1834
  <div className="dvt-alert notice">{t('externalNotice')}</div>
1639
1835
  {!snapshot.writable ? <div className="dvt-alert warning">{t('readOnly')}</div> : null}
1640
1836
  {draftError === undefined ? null : <div className="dvt-alert error">{draftError}</div>}
@@ -1667,6 +1863,9 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1667
1863
  <Field label={t('protocol')}><select aria-label={t('protocol')} disabled={!snapshot.writable || busy} value={selectedProvider.protocol} onChange={(event) => { updateProvider(selectedIndex, 'protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
1668
1864
  {selectedProvider.protocol === 'anthropic' ? <Field label={t('anthropicThinking')} hint={t('anthropicThinkingHint')}><select aria-label={t('anthropicThinking')} value={selectedProvider.anthropicThinking} onChange={(event) => { updateProvider(selectedIndex, '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}
1669
1865
  <Field label={t('userAgent')}><Input aria-label={t('userAgent')} value={selectedProvider.userAgent} onChange={(event) => { updateProvider(selectedIndex, 'userAgent', event.target.value) }} /></Field>
1866
+ <Field label={t('stream')} hint={t('streamHint')}><select aria-label={t('stream')} disabled={!snapshot.writable || busy} value={selectedProvider.stream ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'stream', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
1867
+ <Field label={t('uploadViaUrl')} hint={t('uploadViaUrlHint')}><select aria-label={t('uploadViaUrl')} disabled={!snapshot.writable || busy} value={selectedProvider.uploadViaUrl ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'uploadViaUrl', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
1868
+ {selectedProvider.protocol === 'openai' ? <Field label={t('videoSupport')} hint={t('videoSupportHint')}><select aria-label={t('videoSupport')} disabled={!snapshot.writable || busy} value={selectedProvider.videoSupport ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'videoSupport', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field> : null}
1670
1869
  <Field label={t('t1')} hint={t('t1Hint')}><Input aria-label={t('t1')} inputMode="numeric" value={selectedProvider.t1Seconds} onChange={(event) => { updateProvider(selectedIndex, 't1Seconds', event.target.value) }} /></Field>
1671
1870
  <Field label={t('t2')} hint={t('t2Hint')}><Input aria-label={t('t2')} inputMode="numeric" value={selectedProvider.t2Seconds} onChange={(event) => { updateProvider(selectedIndex, 't2Seconds', event.target.value) }} /></Field>
1672
1871
  <Field label={t('maxBytes')}><Input aria-label={t('maxBytes')} inputMode="numeric" value={selectedProvider.maxImageBytes} onChange={(event) => { updateProvider(selectedIndex, 'maxImageBytes', event.target.value) }} /></Field>
@@ -1678,18 +1877,18 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1678
1877
  <div className="dvt-provider-tests">
1679
1878
  <Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('connection', selectedIndex) }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button>
1680
1879
  <Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('model', selectedIndex) }}>{state.action === 'model' ? t('testingModel') : t('testModel')}</Button>
1880
+ {selectedProvider.protocol === 'openai' ? <Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runVideoTest(selectedIndex) }}>{state.action === 'test-video' ? t('testingVideo') : t('testVideo')}</Button> : null}
1681
1881
  </div>
1682
1882
  {providerHealth === undefined ? null : <div className="dvt-provider-test-result">{(['service', 'model'] as const).map(name => {
1683
1883
  const check = providerHealth.checks[name]
1684
1884
  if (check === undefined) return null
1685
1885
  return <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>
1686
1886
  })}</div>}
1887
+ {state.videoTest === undefined ? null : <div className="dvt-provider-test-result"><div data-status="ok"><span>{t('videoSupport')}</span><strong>{t('statusOk')}</strong><p>{state.videoTest.detail}</p></div></div>}
1687
1888
  </div>
1688
1889
  )}
1689
1890
  </section>
1690
1891
 
1691
- <div className="dvt-save-row"><Button variant="primary" disabled={!canSave || busy} onClick={save}>{state.action === 'save' ? t('saving') : t('save')}</Button><Button variant="outline" disabled={busy} onClick={() => { void controller.load() }}>{t('reload')}</Button></div>
1692
-
1693
1892
  <section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('prioritySort')}</h3><p>{t('prioritySortHint')}</p></div></div>
1694
1893
  <div className="dvt-sorter">
1695
1894
  {draft.providers.map((provider, index) => (
@@ -1753,11 +1952,27 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1753
1952
 
1754
1953
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('imageInput')}</h3></div><label className="dvt-check"><input type="checkbox" checked={draft.hiddenVariants} disabled={!snapshot.writable || busy} onChange={(event) => { update('hiddenVariants', event.target.checked) }} /><span>{t('hiddenVariantsLabel')}</span><small>{t('hiddenVariantsHint')}</small></label></section>
1755
1954
 
1955
+ <section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('toolVisibility')}</h3><p>{t('toolVisibilityHint')}</p></div></div>
1956
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityLocal} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityLocal', event.target.checked) }} /><span>{t('toolVisibilityLocal')}</span><small>{t('toolVisibilityLocalHint')}</small></label>
1957
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityOnline} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityOnline', event.target.checked) }} /><span>{t('toolVisibilityOnline')}</span><small>{t('toolVisibilityOnlineHint')}</small></label>
1958
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityVideo} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityVideo', event.target.checked) }} /><span>{t('toolVisibilityVideo')} <em>(测试)</em></span><small>{t('toolVisibilityVideoHint')}</small></label>
1959
+ </section>
1960
+
1756
1961
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('storage')}</h3></div><div className="dvt-form-grid">
1757
1962
  <Field label={t('storageDir')} hint={t('storageDirHint')}><Input aria-label={t('storageDir')} placeholder="/tmp/dsh-vision-toolkit" value={draft.storageDir} onChange={(event) => { update('storageDir', event.target.value) }} /></Field>
1758
1963
  <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
1759
1964
  </div></section>
1760
1965
 
1966
+ <section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('objectStorage')}</h3><p>{t('objectStorageHint')}</p></div><div className="dvt-actions"><Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.testStorage() }}>{state.action === 'test-storage' ? t('testingStorage') : t('testStorage')}</Button></div></div><div className="dvt-form-grid">
1967
+ <Field label={t('storageEndpoint')}><Input aria-label={t('storageEndpoint')} disabled={!snapshot.writable || busy} value={draft.storageEndpoint} onChange={(event) => { update('storageEndpoint', event.target.value) }} placeholder="https://<account>.r2.cloudflarestorage.com" /></Field>
1968
+ <Field label={t('storageBucket')}><Input aria-label={t('storageBucket')} disabled={!snapshot.writable || busy} value={draft.storageBucket} onChange={(event) => { update('storageBucket', event.target.value) }} /></Field>
1969
+ <Field label={t('storageAccessKeyId')}><Input aria-label={t('storageAccessKeyId')} type="password" autoComplete="new-password" disabled={busy || !snapshot.writable} placeholder={objectStorageCredential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={storageKeys.accessKeyId} onChange={(event) => { setStorageKeys(current => ({ ...current, accessKeyId: event.target.value })); setDraftError(undefined) }} /></Field>
1970
+ <Field label={t('storageSecretAccessKey')}><Input aria-label={t('storageSecretAccessKey')} type="password" autoComplete="new-password" disabled={busy || !snapshot.writable} placeholder={objectStorageCredential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing')} value={storageKeys.secretAccessKey} onChange={(event) => { setStorageKeys(current => ({ ...current, secretAccessKey: event.target.value })); setDraftError(undefined) }} /></Field>
1971
+ <Field label={t('storagePublicBase')} hint={t('storagePublicBaseHint')}><Input aria-label={t('storagePublicBase')} disabled={!snapshot.writable || busy} value={draft.storagePublicBase} onChange={(event) => { update('storagePublicBase', event.target.value) }} placeholder="https://cdn.example.com" /></Field>
1972
+ </div>
1973
+ {state.storageTest === undefined ? null : <div className="dvt-provider-test-result"><div data-status="ok"><span>{t('objectStorage')}</span><strong>{t('statusOk')}</strong><p>{state.storageTest.detail}</p></div></div>}
1974
+ </section>
1975
+
1761
1976
  <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">
1762
1977
  <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>
1763
1978
  {draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
@@ -1782,11 +1997,12 @@ const CSS = `
1782
1997
  .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}
1783
1998
  .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}
1784
1999
  .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)}
1785
- .dvt-tutorial-link{margin:0;font-size:12px;line-height:1.5}.dvt-tutorial-link a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-weight:600}.dvt-tutorial-link a:hover{text-decoration:underline}.dvt-manual-update{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-manual-update code{flex:1;min-width:0;overflow:auto;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-primary)}.dvt-settings{display:grid;grid-template-columns:minmax(0,1fr);width:100%;max-width:900px;min-width:0;box-sizing:border-box;gap:14px;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;grid-template-columns:minmax(0,1fr);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;flex-wrap:wrap;gap:12px}.dvt-panel-title>div:first-child{flex:1 1 320px;min-width:0}.dvt-panel-title>.dvt-actions{margin-left:auto;justify-content:flex-end}.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.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.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;min-width:0;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-check{display:grid;gap:6px;cursor:pointer}.dvt-check input{width:auto}.dvt-check>span{font-size:12px;font-weight:600}.dvt-check>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.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}.dvt-update-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dvt-update-grid>div{display:grid;gap:3px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-update-grid span{font-size:9px;text-transform:uppercase;letter-spacing:.04em;color:var(--dsw-alias-label-caption)}.dvt-update-grid strong{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2000
+ .dvt-tutorial-link{margin:0;font-size:12px;line-height:1.5}.dvt-tutorial-link a{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-weight:600}.dvt-tutorial-link a:hover{text-decoration:underline}.dvt-manual-update{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-manual-update code{flex:1;min-width:0;overflow:auto;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-primary)}.dvt-settings{display:flex;flex-direction:column;width:100%;max-width:900px;min-width:0;box-sizing:border-box;gap:14px;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;grid-template-columns:minmax(0,1fr);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;flex-wrap:wrap;gap:12px}.dvt-panel-title>div:first-child{flex:1 1 320px;min-width:0}.dvt-panel-title>.dvt-actions{margin-left:auto;justify-content:flex-end}.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.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.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;min-width:0;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-check{display:grid;gap:6px;cursor:pointer}.dvt-check input{width:auto}.dvt-check>span{font-size:12px;font-weight:600}.dvt-check>small{font-size:10px;color:var(--dsw-alias-label-secondary);line-height:1.4}.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-update-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dvt-update-grid>div{display:grid;gap:3px;padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2)}.dvt-update-grid span{font-size:9px;text-transform:uppercase;letter-spacing:.04em;color:var(--dsw-alias-label-caption)}.dvt-update-grid strong{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1786
2001
  .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;grid-template-columns:minmax(0,1fr);gap:12px;padding:0 12px 12px}.dvt-advanced-body>.dvt-panel{box-shadow:none}
1787
2002
  .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-test-tag{display:inline-flex;margin-left:6px;padding:1px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);font-size:9px;font-style:normal;font-weight:600;color:var(--dsw-alias-label-secondary)}.dvt-health-test-tag[data-status=ok]{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}.dvt-health-test-tag[data-status=warning]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}.dvt-health-test-tag[data-status=error]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary)}.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)}
1788
2003
  .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}
1789
2004
  .dvt-provider-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:9px 10px;display:grid;gap:8px;background:var(--dsw-alias-bg-layer-2)}.dvt-provider-head{display:flex;align-items:center;gap:10px}.dvt-provider-priority{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap}.dvt-provider-actions{display:flex;gap:4px}.dvt-provider-actions button{width:26px;height:26px;display:grid;place-items:center;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:14px;line-height:1}.dvt-provider-actions button:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvt-provider-actions button:disabled{opacity:.35;cursor:default}.dvt-provider-select{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dvt-provider-select select{flex:1;min-width:180px;height:36px;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:0 10px}.dvt-provider-tests{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dvt-provider-test-result{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.dvt-provider-test-result>div{padding:9px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-2);border-left:3px solid var(--dsw-alias-border-l4)}.dvt-provider-test-result>div[data-status=ok]{border-left-color:var(--dsw-alias-state-success-primary)}.dvt-provider-test-result>div[data-status=warning],.dvt-provider-test-result>div[data-status=not_tested]{border-left-color:var(--dsw-alias-state-warn-primary)}.dvt-provider-test-result>div[data-status=error]{border-left-color:var(--dsw-alias-state-error-primary)}.dvt-provider-test-result span{font-size:10px;text-transform:capitalize}.dvt-provider-test-result strong{float:right;font-size:9px;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}.dvt-provider-test-result p{clear:both;margin:5px 0 0;font-size:10px;line-height:1.4;color:var(--dsw-alias-label-secondary)}.dvt-sorter{display:grid;gap:6px}.dvt-sorter-row{display:flex;align-items:center;gap:10px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}.dvt-sorter-index{flex:none;width:24px;height:24px;display:grid;place-items:center;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-size:11px;font-weight:700}.dvt-sorter-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
2005
+ .dvt-save-dock{position:sticky;top:8px;align-self:flex-end;z-index:50;display:flex;align-items:center;gap:10px;max-width:calc(100% - 16px);padding:11px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1)}.dvt-save-dock[data-dirty]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,var(--dsw-alias-border-l1))}.dvt-save-dock-text{font-size:12px;color:var(--dsw-alias-label-secondary)}.dvt-save-dock[data-dirty] .dvt-save-dock-text{color:var(--dsw-alias-state-warn-label);font-weight:600}.dvt-save-dock-reload{flex:none;display:grid;place-items:center;width:26px;height:26px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0}.dvt-save-dock-reload:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dvt-save-dock-reload:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.dvt-save-dock-reload:disabled{opacity:.4;cursor:default}
1790
2006
  @media(max-width:720px){.dvt-settings-footer{display:grid}.dvt-release{width:auto}.dvt-form-grid,.dvt-update-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}}
1791
2007
  `
1792
2008