@anionex/dsh-vision-toolkit 0.1.7 → 0.1.9

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 (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -17
  3. package/README.zh.md +32 -7
  4. package/assets/community-group-qr.png +0 -0
  5. package/assets/vision-model-test.png +0 -0
  6. package/docs/requirements-traceability/README.i18n.yaml +2 -2
  7. package/docs/requirements-traceability/README.md +2 -2
  8. package/docs/requirements-traceability/README.zh.md +2 -2
  9. package/lib/client.js +361 -28
  10. package/lib/client.js.map +1 -1
  11. package/lib/config.js +14 -0
  12. package/lib/config.js.map +1 -1
  13. package/lib/image-input-variants.js +615 -0
  14. package/lib/image-input-variants.js.map +1 -0
  15. package/lib/index.js +8 -1
  16. package/lib/index.js.map +1 -1
  17. package/lib/paste-images.js +6 -0
  18. package/lib/paste-images.js.map +1 -1
  19. package/lib/runtime.js +37 -3
  20. package/lib/runtime.js.map +1 -1
  21. package/lib/types/client/index.d.ts +16 -5
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/client/paste-images.d.ts +69 -0
  24. package/lib/types/client/paste-images.d.ts.map +1 -1
  25. package/lib/types/config.d.ts +26 -0
  26. package/lib/types/config.d.ts.map +1 -1
  27. package/lib/types/image-input-variants.d.ts +153 -0
  28. package/lib/types/image-input-variants.d.ts.map +1 -0
  29. package/lib/types/index.d.ts.map +1 -1
  30. package/lib/types/paste-images.d.ts +35 -0
  31. package/lib/types/paste-images.d.ts.map +1 -1
  32. package/lib/types/runtime.d.ts +5 -2
  33. package/lib/types/runtime.d.ts.map +1 -1
  34. package/lib/types/web-request.d.ts +7 -0
  35. package/lib/types/web-request.d.ts.map +1 -1
  36. package/lib/types/web.d.ts +17 -2
  37. package/lib/types/web.d.ts.map +1 -1
  38. package/lib/web-request.js +11 -2
  39. package/lib/web-request.js.map +1 -1
  40. package/lib/web.js +88 -5
  41. package/lib/web.js.map +1 -1
  42. package/package.json +2 -1
  43. package/src/client/index.tsx +53 -14
  44. package/src/client/paste-images.tsx +331 -15
  45. package/src/config.ts +40 -0
  46. package/src/image-input-variants.ts +688 -0
  47. package/src/index.ts +18 -1
  48. package/src/paste-images.ts +39 -0
  49. package/src/runtime.ts +42 -3
  50. package/src/web-request.ts +12 -2
  51. package/src/web.ts +90 -4
package/lib/client.js CHANGED
@@ -65,9 +65,11 @@ const en = {
65
65
  sourceFile: 'Credential file',
66
66
  health: 'Health',
67
67
  runHealth: 'Run health check',
68
- testConnection: 'Test connection',
68
+ testConnection: 'Test API connection',
69
+ testModel: 'Test vision model',
69
70
  testing: 'Checking…',
70
- connectionHint: 'Connection testing explicitly sends the configured credential to GET /models. It uploads no image and creates no completion.',
71
+ testingModel: 'Testing model…',
72
+ connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
71
73
  saveBeforeTesting: 'Save service changes before testing the connection.',
72
74
  advanced: 'Advanced settings',
73
75
  advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
@@ -132,6 +134,7 @@ const en = {
132
134
  healthArtifactDirectory: 'Artifact directory',
133
135
  healthTempDirectory: 'Temporary directory',
134
136
  healthService: 'Vision service',
137
+ healthModel: 'Vision model',
135
138
  statusOk: 'OK',
136
139
  statusWarning: 'Warning',
137
140
  statusError: 'Error',
@@ -146,7 +149,7 @@ const en = {
146
149
  healthDirectoryWritable: '{directory} is writable: {path}',
147
150
  healthDirectoryNotWritable: '{directory} is not writable: {path}',
148
151
  healthArtifactDirectoryFailed: 'Could not prepare the artifact directory.',
149
- healthConnectionNotTested: 'Connection not tested. Use Test connection to query /models.',
152
+ healthConnectionNotTested: 'API connection not tested. Use Test API connection to query /models.',
150
153
  healthConnectionCredentialMissing: 'Connection test skipped because the credential is unavailable.',
151
154
  healthServiceResponded: 'Service responded at {endpoint} (HTTP {status}).',
152
155
  healthServiceRejectedCredential: 'Service rejected the configured credential (HTTP {status}).',
@@ -154,6 +157,13 @@ const en = {
154
157
  healthServiceRateLimited: 'Service is reachable, but the connection test was rate-limited (HTTP 429).',
155
158
  healthServiceHttpFailed: 'Connection test failed with HTTP {status}.',
156
159
  healthServiceUnreachable: 'Could not reach {endpoint}.',
160
+ healthModelNotTested: 'Vision model not tested. Run Test vision model to make one real multimodal request.',
161
+ healthModelCredentialMissing: 'Vision model test skipped because the credential is unavailable.',
162
+ healthModelReady: 'Model {model} completed a real multimodal request.',
163
+ healthModelFailed: 'Real multimodal request failed: {detail}',
164
+ modelTestVerifiedTag: 'Verified',
165
+ modelTestNotRunTag: 'Not tested',
166
+ modelTestFailedTag: 'Test failed',
157
167
  };
158
168
  const zh = {
159
169
  nav: '视觉工具',
@@ -202,10 +212,12 @@ const zh = {
202
212
  sourceFile: '凭据文件',
203
213
  health: '运行检查',
204
214
  runHealth: '检查本地环境',
205
- testConnection: '测试服务连接',
215
+ testConnection: '测试 API 连接',
216
+ testModel: '测试视觉模型',
206
217
  testing: '检查中…',
207
- connectionHint: '“检查本地环境”只检查本机依赖和目录;“测试服务连接”会携带已配置的 API 密钥请求 /models,但不会上传图片或调用模型。',
208
- saveBeforeTesting: '修改服务配置后,请先保存,再测试连接。',
218
+ testingModel: '正在测试模型…',
219
+ connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
220
+ saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
209
221
  advanced: '高级设置',
210
222
  advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、运行环境来源、Python 和额外可读目录。一般无需修改。',
211
223
  pluginVersion: '插件版本',
@@ -269,6 +281,7 @@ const zh = {
269
281
  healthArtifactDirectory: '输出目录',
270
282
  healthTempDirectory: '临时目录',
271
283
  healthService: '视觉服务',
284
+ healthModel: '视觉模型',
272
285
  statusOk: '正常',
273
286
  statusWarning: '注意',
274
287
  statusError: '异常',
@@ -283,7 +296,7 @@ const zh = {
283
296
  healthDirectoryWritable: '{directory}可写:{path}',
284
297
  healthDirectoryNotWritable: '{directory}不可写:{path}',
285
298
  healthArtifactDirectoryFailed: '无法准备输出目录。',
286
- healthConnectionNotTested: '尚未测试服务连接。点击“测试服务连接”可请求 /models。',
299
+ healthConnectionNotTested: '尚未测试 API 连接。点击“测试 API 连接”可请求 /models。',
287
300
  healthConnectionCredentialMissing: 'API 密钥不可用,未执行连接测试。',
288
301
  healthServiceResponded: '服务已响应:{endpoint}(HTTP {status})。',
289
302
  healthServiceRejectedCredential: '服务拒绝了当前 API 密钥(HTTP {status})。',
@@ -291,6 +304,13 @@ const zh = {
291
304
  healthServiceRateLimited: '服务可以访问,但本次连接测试触发了限流(HTTP 429)。',
292
305
  healthServiceHttpFailed: '连接测试失败(HTTP {status})。',
293
306
  healthServiceUnreachable: '无法连接到 {endpoint}。',
307
+ healthModelNotTested: '尚未测试视觉模型。点击“测试视觉模型”可执行一次真实多模态请求。',
308
+ healthModelCredentialMissing: '视觉模型测试已跳过,因为当前 API 密钥不可用。',
309
+ healthModelReady: '模型 {model} 已完成一次真实多模态请求。',
310
+ healthModelFailed: '真实多模态请求失败:{detail}',
311
+ modelTestVerifiedTag: '已实测',
312
+ modelTestNotRunTag: '未测试',
313
+ modelTestFailedTag: '测试失败',
294
314
  };
295
315
  function isRecord(value) {
296
316
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -601,13 +621,15 @@ class VisionSettingsController {
601
621
  return false;
602
622
  }
603
623
  }
604
- async runHealth(testConnection) {
605
- this.set({ ...this.state, action: testConnection ? 'connection' : 'health', error: undefined, message: undefined });
624
+ async runHealth(mode) {
625
+ const testConnection = mode !== 'health';
626
+ const testModel = mode === 'model';
627
+ this.set({ ...this.state, action: mode, error: undefined, message: undefined });
606
628
  try {
607
629
  const health = await apiRequest({
608
630
  method: 'POST',
609
631
  headers: { 'Content-Type': 'application/json' },
610
- body: JSON.stringify({ action: 'health', testConnection }),
632
+ body: JSON.stringify({ action: 'health', testConnection, testModel }),
611
633
  });
612
634
  this.set({ ...this.state, action: undefined, health });
613
635
  }
@@ -693,6 +715,7 @@ const HEALTH_NAME_KEYS = {
693
715
  artifactDirectory: 'healthArtifactDirectory',
694
716
  tempDirectory: 'healthTempDirectory',
695
717
  service: 'healthService',
718
+ model: 'healthModel',
696
719
  };
697
720
  const HEALTH_STATUS_KEYS = {
698
721
  ok: 'statusOk',
@@ -754,8 +777,25 @@ function healthDetail(name, detail, t) {
754
777
  match = /^Service could not be reached at (.+)$/u.exec(detail);
755
778
  if (match !== null)
756
779
  return t('healthServiceUnreachable', { endpoint: match[1] });
780
+ if (detail === 'Vision model was not tested; run an explicit model test to send the bundled diagnostic image')
781
+ return t('healthModelNotTested');
782
+ if (detail === 'Vision model test skipped because the configured credential is unavailable')
783
+ return t('healthModelCredentialMissing');
784
+ match = /^Vision model (.+) completed a multimodal request$/u.exec(detail);
785
+ if (match !== null)
786
+ return t('healthModelReady', { model: match[1] });
787
+ match = /^Vision model test failed: (.+)$/u.exec(detail);
788
+ if (match !== null)
789
+ return t('healthModelFailed', { detail: match[1] });
757
790
  return detail;
758
791
  }
792
+ function modelTestTag(health, check) {
793
+ if (!health.modelTested)
794
+ return { status: 'warning', label: 'modelTestNotRunTag' };
795
+ if (check.status === 'ok')
796
+ return { status: 'ok', label: 'modelTestVerifiedTag' };
797
+ return { status: 'error', label: 'modelTestFailedTag' };
798
+ }
759
799
  function credentialSource(source, t) {
760
800
  if (source === 'env')
761
801
  return t('sourceEnv');
@@ -803,7 +843,10 @@ function LoadedSettings({ controller, t }) {
803
843
  const keyLocked = credentialMatchesSnapshot && !snapshot.credential.writable;
804
844
  const canSave = snapshot.writable || (apiKey.length > 0 && !keyLocked);
805
845
  const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable');
806
- return ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-settings", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-alert notice", children: t('externalNotice') }), !snapshot.writable ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert warning", children: t('readOnly') }) : null, draftError === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: draftError }), state.error === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: state.error }), state.message === 'saved' ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('saved') }) : null, snapshot.runtime.lastError === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-alert error", children: [(0, jsx_runtime_1.jsx)("strong", { children: runtimeErrorTitle }), (0, jsx_runtime_1.jsx)("span", { children: snapshot.runtime.lastError })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel dvt-essential", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('provider') }), (0, jsx_runtime_1.jsx)("p", { children: t('providerHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`, children: snapshot.credential.configured ? t('configured') : t('missing') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('protocol'), children: (0, jsx_runtime_1.jsxs)("select", { disabled: !snapshot.writable || busy, value: draft.protocol, onChange: (event) => { update('protocol', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "openai", children: "OpenAI Chat Completions" }), (0, jsx_runtime_1.jsx)("option", { value: "anthropic", children: "Anthropic Messages" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('baseUrl'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.baseUrl, onChange: (event) => { update('baseUrl', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('model'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.model, onChange: (event) => { update('model', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('apiKey'), hint: keyLocked ? t('apiKeyLocked') : snapshot.credential.source === undefined ? t('apiKeyHint') : `${t('apiKeyHint')} ${t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}`, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('apiKey'), type: "password", autoComplete: "new-password", disabled: busy || keyLocked, placeholder: snapshot.credential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing'), value: apiKey, onChange: (event) => { setApiKey(event.target.value); setDraftError(undefined); } }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-save-row", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", disabled: !canSave || busy, onClick: save, children: state.action === 'save' ? t('saving') : t('save') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", disabled: busy, onClick: () => { void controller.load(); }, children: t('reload') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('health') }), (0, jsx_runtime_1.jsx)("p", { children: t('connectionHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth(false); }, children: state.action === 'health' ? t('testing') : t('runHealth') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "primary", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth(true); }, children: state.action === 'connection' ? t('testing') : t('testConnection') })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('saveBeforeTesting') }), state.health === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('notTested') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-health-grid", children: Object.entries(state.health.checks).map(([name, check]) => (0, jsx_runtime_1.jsxs)("div", { "data-status": check.status, children: [(0, jsx_runtime_1.jsx)("span", { children: t(HEALTH_NAME_KEYS[name] ?? 'health') }), (0, jsx_runtime_1.jsx)("strong", { children: t(HEALTH_STATUS_KEYS[check.status]) }), (0, jsx_runtime_1.jsx)("p", { children: healthDetail(name, check.detail, t) })] }, name)) })] }), (0, jsx_runtime_1.jsxs)("details", { className: "dvt-advanced", children: [(0, jsx_runtime_1.jsxs)("summary", { children: [(0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("strong", { children: t('advanced') }), (0, jsx_runtime_1.jsx)("small", { children: t('advancedHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: "dvt-details-chevron", "aria-hidden": "true", children: "\u2304" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-advanced-body", children: [(0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('provider') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('credential'), hint: t('credentialHint'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('credential'), disabled: !snapshot.writable || busy, value: draft.credential, onChange: (event) => { update('credential', event.target.value); } }) }), draft.protocol === 'anthropic' ? (0, jsx_runtime_1.jsx)(Field, { label: t('anthropicThinking'), hint: t('anthropicThinkingHint'), children: (0, jsx_runtime_1.jsxs)("select", { "aria-label": t('anthropicThinking'), value: draft.anthropicThinking, onChange: (event) => { update('anthropicThinking', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "omit", children: "omit (widest compatibility)" }), (0, jsx_runtime_1.jsx)("option", { value: "disabled", children: "disabled (model support required)" }), (0, jsx_runtime_1.jsx)("option", { value: "adaptive", children: "adaptive (model support required)" })] }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('userAgent'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.userAgent, onChange: (event) => { update('userAgent', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('limits') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('language'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.language, onChange: (event) => { update('language', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "zh", children: "\u4E2D\u6587" }), (0, jsx_runtime_1.jsx)("option", { value: "en", children: "English" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('timeout'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.timeoutMs, onChange: (event) => { update('timeoutMs', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxBytes'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImageBytes, onChange: (event) => { update('maxImageBytes', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxPixels'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImagePixels, onChange: (event) => { update('maxImagePixels', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('concurrency'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.concurrency, onChange: (event) => { update('concurrency', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsx)("h3", { children: t('runtime') }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`, children: snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('runtimeMode'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.runtimeMode, onChange: (event) => { update('runtimeMode', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "managed", children: t('runtimeManaged') }), (0, jsx_runtime_1.jsx)("option", { value: "external", children: t('runtimeExternal') })] }) }), draft.runtimeMode === 'external' ? (0, jsx_runtime_1.jsx)(Field, { label: t('toolkitPath'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.toolkitPath, onChange: (event) => { update('toolkitPath', event.target.value); } }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('python'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { placeholder: "python3", value: draft.python, onChange: (event) => { update('python', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('allowedDirs'), hint: t('allowedDirsHint'), children: (0, jsx_runtime_1.jsx)("textarea", { rows: 3, value: draft.allowedDirs, onChange: (event) => { update('allowedDirs', event.target.value); } }) })] }), snapshot.runtime.upstream === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-runtime-facts", children: [(0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.path }), (0, jsx_runtime_1.jsxs)("code", { children: [snapshot.runtime.upstream.python, " \u00B7 ", snapshot.runtime.upstream.pythonVersion] }), (0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.runtimeHome })] })] })] })] }), (0, jsx_runtime_1.jsxs)("footer", { className: "dvt-settings-footer", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "dvt-kicker", children: t('pluginKind') }), (0, jsx_runtime_1.jsx)("h2", { children: t('settingsTitle') }), (0, jsx_runtime_1.jsx)("p", { children: t('settingsIntro') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-release", children: [(0, jsx_runtime_1.jsxs)("span", { children: [t('pluginVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.pluginVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('upstreamVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.upstreamVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('activeGeneration'), " ", (0, jsx_runtime_1.jsx)("strong", { children: t('activeGenerationValue', { generation: snapshot.runtime.generation }) })] })] })] })] }));
846
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-settings", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-alert notice", children: t('externalNotice') }), !snapshot.writable ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert warning", children: t('readOnly') }) : null, draftError === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: draftError }), state.error === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: state.error }), state.message === 'saved' ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('saved') }) : null, snapshot.runtime.lastError === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-alert error", children: [(0, jsx_runtime_1.jsx)("strong", { children: runtimeErrorTitle }), (0, jsx_runtime_1.jsx)("span", { children: snapshot.runtime.lastError })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel dvt-essential", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('provider') }), (0, jsx_runtime_1.jsx)("p", { children: t('providerHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`, children: snapshot.credential.configured ? t('configured') : t('missing') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('protocol'), children: (0, jsx_runtime_1.jsxs)("select", { disabled: !snapshot.writable || busy, value: draft.protocol, onChange: (event) => { update('protocol', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "openai", children: "OpenAI Chat Completions" }), (0, jsx_runtime_1.jsx)("option", { value: "anthropic", children: "Anthropic Messages" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('baseUrl'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.baseUrl, onChange: (event) => { update('baseUrl', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('model'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.model, onChange: (event) => { update('model', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('apiKey'), hint: keyLocked ? t('apiKeyLocked') : snapshot.credential.source === undefined ? t('apiKeyHint') : `${t('apiKeyHint')} ${t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}`, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('apiKey'), type: "password", autoComplete: "new-password", disabled: busy || keyLocked, placeholder: snapshot.credential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing'), value: apiKey, onChange: (event) => { setApiKey(event.target.value); setDraftError(undefined); } }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-save-row", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", disabled: !canSave || busy, onClick: save, children: state.action === 'save' ? t('saving') : t('save') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", disabled: busy, onClick: () => { void controller.load(); }, children: t('reload') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('health') }), (0, jsx_runtime_1.jsx)("p", { children: t('connectionHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('health'); }, children: state.action === 'health' ? t('testing') : t('runHealth') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('connection'); }, children: state.action === 'connection' ? t('testing') : t('testConnection') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "primary", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('model'); }, children: state.action === 'model' ? t('testingModel') : t('testModel') })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('saveBeforeTesting') }), state.health === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('notTested') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-health-grid", children: Object.entries(state.health.checks).map(([name, check]) => {
847
+ const testTag = name === 'model' ? modelTestTag(state.health, check) : undefined;
848
+ return (0, jsx_runtime_1.jsxs)("div", { "data-status": check.status, children: [(0, jsx_runtime_1.jsx)("span", { children: t(HEALTH_NAME_KEYS[name] ?? 'health') }), testTag === undefined ? null : (0, jsx_runtime_1.jsx)("em", { className: "dvt-health-test-tag", "data-status": testTag.status, children: t(testTag.label) }), (0, jsx_runtime_1.jsx)("strong", { children: t(HEALTH_STATUS_KEYS[check.status]) }), (0, jsx_runtime_1.jsx)("p", { children: healthDetail(name, check.detail, t) })] }, name);
849
+ }) })] }), (0, jsx_runtime_1.jsxs)("details", { className: "dvt-advanced", children: [(0, jsx_runtime_1.jsxs)("summary", { children: [(0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("strong", { children: t('advanced') }), (0, jsx_runtime_1.jsx)("small", { children: t('advancedHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: "dvt-details-chevron", "aria-hidden": "true", children: "\u2304" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-advanced-body", children: [(0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('provider') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('credential'), hint: t('credentialHint'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('credential'), disabled: !snapshot.writable || busy, value: draft.credential, onChange: (event) => { update('credential', event.target.value); } }) }), draft.protocol === 'anthropic' ? (0, jsx_runtime_1.jsx)(Field, { label: t('anthropicThinking'), hint: t('anthropicThinkingHint'), children: (0, jsx_runtime_1.jsxs)("select", { "aria-label": t('anthropicThinking'), value: draft.anthropicThinking, onChange: (event) => { update('anthropicThinking', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "omit", children: "omit (widest compatibility)" }), (0, jsx_runtime_1.jsx)("option", { value: "disabled", children: "disabled (model support required)" }), (0, jsx_runtime_1.jsx)("option", { value: "adaptive", children: "adaptive (model support required)" })] }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('userAgent'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.userAgent, onChange: (event) => { update('userAgent', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-panel-title", children: (0, jsx_runtime_1.jsx)("h3", { children: t('limits') }) }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('language'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.language, onChange: (event) => { update('language', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "zh", children: "\u4E2D\u6587" }), (0, jsx_runtime_1.jsx)("option", { value: "en", children: "English" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('timeout'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.timeoutMs, onChange: (event) => { update('timeoutMs', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxBytes'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImageBytes, onChange: (event) => { update('maxImageBytes', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('maxPixels'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.maxImagePixels, onChange: (event) => { update('maxImagePixels', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('concurrency'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { inputMode: "numeric", value: draft.concurrency, onChange: (event) => { update('concurrency', event.target.value); } }) })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsx)("h3", { children: t('runtime') }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`, children: snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('runtimeMode'), children: (0, jsx_runtime_1.jsxs)("select", { value: draft.runtimeMode, onChange: (event) => { update('runtimeMode', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "managed", children: t('runtimeManaged') }), (0, jsx_runtime_1.jsx)("option", { value: "external", children: t('runtimeExternal') })] }) }), draft.runtimeMode === 'external' ? (0, jsx_runtime_1.jsx)(Field, { label: t('toolkitPath'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { value: draft.toolkitPath, onChange: (event) => { update('toolkitPath', event.target.value); } }) }) : null, (0, jsx_runtime_1.jsx)(Field, { label: t('python'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { placeholder: "python3", value: draft.python, onChange: (event) => { update('python', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('allowedDirs'), hint: t('allowedDirsHint'), children: (0, jsx_runtime_1.jsx)("textarea", { rows: 3, value: draft.allowedDirs, onChange: (event) => { update('allowedDirs', event.target.value); } }) })] }), snapshot.runtime.upstream === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-runtime-facts", children: [(0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.path }), (0, jsx_runtime_1.jsxs)("code", { children: [snapshot.runtime.upstream.python, " \u00B7 ", snapshot.runtime.upstream.pythonVersion] }), (0, jsx_runtime_1.jsx)("code", { children: snapshot.runtime.upstream.runtimeHome })] })] })] })] }), (0, jsx_runtime_1.jsxs)("footer", { className: "dvt-settings-footer", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "dvt-kicker", children: t('pluginKind') }), (0, jsx_runtime_1.jsx)("h2", { children: t('settingsTitle') }), (0, jsx_runtime_1.jsx)("p", { children: t('settingsIntro') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-release", children: [(0, jsx_runtime_1.jsxs)("span", { children: [t('pluginVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.pluginVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('upstreamVersion'), " ", (0, jsx_runtime_1.jsx)("strong", { children: snapshot.release.upstreamVersion })] }), (0, jsx_runtime_1.jsxs)("span", { children: [t('activeGeneration'), " ", (0, jsx_runtime_1.jsx)("strong", { children: t('activeGenerationValue', { generation: snapshot.runtime.generation }) })] })] })] })] }));
807
850
  }
808
851
  const CSS = `
809
852
  .dvt-tool{margin:4px 0;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;box-shadow:var(--dsw-shadow-lv1)}
@@ -812,7 +855,7 @@ const CSS = `
812
855
  .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)}
813
856
  .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}
814
857
  .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}
815
- .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)}
858
+ .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)}
816
859
  .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}
817
860
  @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}}
818
861
  `;
@@ -895,7 +938,7 @@ function apply(ctx) {
895
938
  __modules["./paste-images.js"] = function(module, exports, require, __load_) {
896
939
  "use strict";
897
940
  Object.defineProperty(exports, "__esModule", { value: true });
898
- exports.PasteImageController = exports.PASTE_IMAGES_ROUTE = void 0;
941
+ exports.PasteImageController = exports.PASTE_POLICY_ROUTE = exports.PASTE_IMAGES_ROUTE = void 0;
899
942
  exports.PasteImageDock = PasteImageDock;
900
943
  exports.installPasteImages = installPasteImages;
901
944
  const jsx_runtime_1 = require("react/jsx-runtime");
@@ -903,9 +946,12 @@ const jsx_runtime_1 = require("react/jsx-runtime");
903
946
  const react_1 = require("react");
904
947
  const SOURCE = 'vision-toolkit-pasted-image';
905
948
  exports.PASTE_IMAGES_ROUTE = '/_dsh/vision-toolkit/paste-images';
949
+ exports.PASTE_POLICY_ROUTE = '/_dsh/vision-toolkit/paste-policy';
906
950
  const MAX_IMAGES = 20;
907
951
  const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
908
952
  const MAX_BATCH_BYTES = 80 * 1024 * 1024;
953
+ /** A confirmed paste verdict older than this is unknown again, even while a refresh is in flight. */
954
+ const VERDICT_MAX_AGE_MS = 15000;
909
955
  const CORDIS_ORIGINAL = Symbol.for('cordis.original');
910
956
  function registryIdentity(registry) {
911
957
  let current = registry;
@@ -944,6 +990,25 @@ function imageFiles(data) {
944
990
  const candidates = itemFiles.length > 0 ? itemFiles : Array.from(data.files);
945
991
  return candidates.filter(file => file.type.toLowerCase().startsWith('image/'));
946
992
  }
993
+ /**
994
+ * The selector label the model picker currently shows, or '' when none is
995
+ * readable. Matches the host ModelSelect trigger aria-labels ("Select model,
996
+ * current …" / "选择模型,当前 …"); any other label wording falls back to the
997
+ * session-header verdict, which is stale until the next request.
998
+ */
999
+ function currentModelLabel() {
1000
+ const buttons = document.querySelectorAll('button[aria-label]');
1001
+ for (const button of buttons) {
1002
+ const label = button.getAttribute('aria-label') ?? '';
1003
+ if (/select model|current model|选择模型/iu.test(label))
1004
+ return label;
1005
+ }
1006
+ return '';
1007
+ }
1008
+ /** Verdict cache key: the model label is part of the answer, so a switch invalidates it. */
1009
+ function verdictKey(sessionId, modelLabel) {
1010
+ return `${sessionId}|${modelLabel}`;
1011
+ }
947
1012
  function validateImages(files) {
948
1013
  if (files.length > MAX_IMAGES)
949
1014
  throw new Error(`Paste at most ${MAX_IMAGES} images at a time`);
@@ -975,6 +1040,9 @@ class PasteImageController {
975
1040
  records = new Map();
976
1041
  listeners = new Set();
977
1042
  revision = 0;
1043
+ verdicts = new Map();
1044
+ /** Guards the synthetic replay paste from re-entering capture interception. */
1045
+ replaying = false;
978
1046
  constructor(ctx) {
979
1047
  this.ctx = ctx;
980
1048
  }
@@ -1074,30 +1142,246 @@ class PasteImageController {
1074
1142
  throw error;
1075
1143
  }
1076
1144
  }
1077
- handlePaste(event) {
1078
- const files = imageFiles(event.clipboardData);
1079
- if (files.length === 0)
1145
+ /**
1146
+ * The host's verdict for one Session and selector label, when fresh. The
1147
+ * last CONFIRMED answer is authoritative while a background refresh is in
1148
+ * flight (the paste acts on what the host last said; the refresh only
1149
+ * covers the next paste). A label that changed since the confirmation
1150
+ * answers undefined, so the native attachment flow stays the default.
1151
+ * @param sessionId - the live Session the paste belongs to.
1152
+ * @param modelLabel - the model-selector label currently shown.
1153
+ * @returns the fresh confirmed verdict, or undefined when unconfirmed.
1154
+ */
1155
+ verdictFor(sessionId, modelLabel) {
1156
+ const entry = this.verdicts.get(verdictKey(sessionId, modelLabel));
1157
+ if (entry === undefined || entry.at === 0)
1158
+ return undefined;
1159
+ if (Date.now() - entry.at > VERDICT_MAX_AGE_MS)
1160
+ return undefined;
1161
+ return { takeOver: entry.takeOver, ...(entry.autoSwitch === undefined ? {} : { autoSwitch: entry.autoSwitch }) };
1162
+ }
1163
+ /**
1164
+ * The exact model route the live model catalog reports for one Session.
1165
+ * Unreadable routes answer undefined, so the verdict falls back to the
1166
+ * selector label alone.
1167
+ * @param sessionId - the live Session id.
1168
+ * @returns the current provider/model selection, when readable.
1169
+ */
1170
+ async readSelection(sessionId) {
1171
+ const connection = this.ctx.get('connection');
1172
+ if (connection === undefined)
1173
+ return undefined;
1174
+ try {
1175
+ const { result } = await connection.api.sessions.models({ sessionId });
1176
+ if (!result.ok)
1177
+ return undefined;
1178
+ const current = result.value.current;
1179
+ if (current === undefined || current === null || current.provider === '' || current.model === '')
1180
+ return undefined;
1181
+ return {
1182
+ provider: current.provider,
1183
+ model: current.model,
1184
+ ...(current.reasoningEffort === undefined ? {} : { reasoningEffort: current.reasoningEffort }),
1185
+ };
1186
+ }
1187
+ catch {
1188
+ return undefined;
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Ask the host what to do with a paste for the current model, and cache the
1193
+ * answer per Session and selector label. A model switch changes the label,
1194
+ * which changes the cache key, so a stale verdict never outlives the model
1195
+ * it described. The exact selection rides along when the live model catalog
1196
+ * is readable, so the host can answer with an auto-switch route; a 404
1197
+ * simply leaves the verdict unconfirmed; the next focus or paste retries.
1198
+ * @param sessionId - the live Session to ask about.
1199
+ * @param modelLabel - the model-selector label currently shown.
1200
+ */
1201
+ refreshVerdict(sessionId, modelLabel) {
1202
+ const key = verdictKey(sessionId, modelLabel);
1203
+ const cached = this.verdicts.get(key);
1204
+ // Dedupe only on an in-flight request, never on freshness: the host's
1205
+ // model route can change under an unchanged Session id.
1206
+ if (cached?.pending)
1207
+ return;
1208
+ const entry = {
1209
+ pending: true,
1210
+ takeOver: cached ? cached.takeOver : false,
1211
+ at: cached ? cached.at : 0,
1212
+ ...(cached?.autoSwitch === undefined ? {} : { autoSwitch: cached.autoSwitch }),
1213
+ };
1214
+ this.verdicts.set(key, entry);
1215
+ void (async () => {
1216
+ const selection = await this.readSelection(sessionId);
1217
+ const query = new URLSearchParams({ sessionId });
1218
+ if (modelLabel !== '')
1219
+ query.set('model', modelLabel);
1220
+ if (selection !== undefined) {
1221
+ query.set('provider', selection.provider);
1222
+ query.set('modelId', selection.model);
1223
+ if (selection.reasoningEffort !== undefined)
1224
+ query.set('reasoningEffort', selection.reasoningEffort);
1225
+ }
1226
+ let request;
1227
+ try {
1228
+ request = fetch(`${exports.PASTE_POLICY_ROUTE}?${query.toString()}`);
1229
+ }
1230
+ catch {
1231
+ // No fetch surface (test runtime, pre-fetch bootstrap): leave the
1232
+ // verdict unconfirmed rather than letting the paste listener die.
1233
+ entry.pending = false;
1234
+ return;
1235
+ }
1236
+ request
1237
+ .then((response) => {
1238
+ if (response.status === 404) {
1239
+ // Route not mounted yet (plugin load race, hot reload): forget every
1240
+ // verdict and retry on the next focus or paste instead of standing
1241
+ // down for the page lifetime.
1242
+ this.verdicts.clear();
1243
+ return null;
1244
+ }
1245
+ if (!response.ok)
1246
+ throw new Error(`paste policy ${response.status}`);
1247
+ return response.json();
1248
+ })
1249
+ .then((body) => {
1250
+ entry.pending = false;
1251
+ if (body !== null) {
1252
+ entry.takeOver = body.value.takeOver === true;
1253
+ if (body.value.autoSwitch !== undefined)
1254
+ entry.autoSwitch = body.value.autoSwitch;
1255
+ else
1256
+ delete entry.autoSwitch;
1257
+ entry.at = Date.now();
1258
+ }
1259
+ })
1260
+ .catch(() => {
1261
+ entry.pending = false;
1262
+ });
1263
+ })();
1264
+ }
1265
+ /**
1266
+ * Switch one Session to the route the host validated, through the same
1267
+ * model-directory seat the selector uses when present (so the shared UI
1268
+ * state moves with the session), falling back to the raw RPC.
1269
+ * @param sessionId - the live Session id.
1270
+ * @param route - the validated variant route.
1271
+ */
1272
+ async switchModel(sessionId, route) {
1273
+ const directories = this.ctx.get('modelDirectories');
1274
+ if (directories !== undefined) {
1275
+ // The label is a display hint; the seat only needs the exact route.
1276
+ await directories.directoryFor(sessionId).select({
1277
+ provider: route.provider,
1278
+ model: route.model,
1279
+ ...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort }),
1280
+ });
1281
+ return;
1282
+ }
1283
+ const connection = this.ctx.get('connection');
1284
+ if (connection === undefined)
1285
+ throw new Error('No model switch channel is available in this Web application');
1286
+ const { result } = await connection.api.sessions.selectModel({
1287
+ sessionId,
1288
+ provider: route.provider,
1289
+ model: route.model,
1290
+ ...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort }),
1291
+ });
1292
+ if (!result.ok)
1293
+ throw new Error(`${result.error?.code ?? 'select-model-failed'}: ${result.error?.message ?? 'unknown error'}`);
1294
+ }
1295
+ /**
1296
+ * Replay a swallowed paste as a synthetic clipboard event so the composer's
1297
+ * own intake (limits, thumbnails, keyboard) runs with the captured files.
1298
+ * @returns false when the environment cannot construct a clipboard payload.
1299
+ */
1300
+ replayPaste(target, files, text) {
1301
+ let data;
1302
+ try {
1303
+ data = new DataTransfer();
1304
+ for (const file of files)
1305
+ data.items.add(file);
1306
+ if (text !== '')
1307
+ data.setData('text/plain', text);
1308
+ }
1309
+ catch {
1080
1310
  return false;
1081
- const target = event.target;
1082
- if (!(target instanceof HTMLTextAreaElement) || target.closest('[data-composer-card]') === null)
1311
+ }
1312
+ let event;
1313
+ try {
1314
+ event = new ClipboardEvent('paste', {
1315
+ clipboardData: data,
1316
+ bubbles: true,
1317
+ cancelable: true,
1318
+ });
1319
+ }
1320
+ catch {
1083
1321
  return false;
1084
- event.preventDefault();
1085
- event.stopPropagation();
1086
- event.stopImmediatePropagation();
1087
- const sessionId = this.ctx.sessions.list.getSnapshot().current;
1088
- if (sessionId === undefined)
1089
- return true;
1322
+ }
1323
+ if (event.clipboardData === null || event.clipboardData.files.length === 0)
1324
+ return false;
1325
+ this.replaying = true;
1326
+ try {
1327
+ target.dispatchEvent(event);
1328
+ }
1329
+ finally {
1330
+ this.replaying = false;
1331
+ }
1332
+ return true;
1333
+ }
1334
+ /**
1335
+ * Auto-switch flow: switch the Session to the image-input variant, announce
1336
+ * it, then replay the paste into the composer's native intake. A failed
1337
+ * switch, or an environment that cannot replay clipboard bytes, degrades to
1338
+ * the path takeover with the same files.
1339
+ * @param sessionId - the live Session id.
1340
+ * @param target - the composer textarea the paste landed on.
1341
+ * @param files - the captured image files.
1342
+ * @param text - same-paste text, replayed alongside the files.
1343
+ * @param route - the validated variant route to switch to.
1344
+ */
1345
+ async autoSwitchPaste(sessionId, target, files, text, route) {
1346
+ const input = this.inputFor(sessionId);
1347
+ try {
1348
+ await this.switchModel(sessionId, route);
1349
+ input.notify('info', `Switched to ${route.label || `${route.model} (Vision Toolkit)`}; pasted images now keep the native attachment flow`);
1350
+ }
1351
+ catch (error) {
1352
+ input.notify('error', `Model switch failed; images will be sent as workspace paths: ${message(error)}`);
1353
+ this.takeoverPaste(sessionId, target, files, text);
1354
+ return;
1355
+ }
1356
+ // Replaying lets the composer's own intake run (thumbnail, limits,
1357
+ // keyboard); if the environment cannot replay clipboard bytes, the
1358
+ // images still land as workspace paths.
1359
+ const before = input.state.getSnapshot().imageIds.length;
1360
+ const replayed = this.replayPaste(target, files, text);
1361
+ const after = input.state.getSnapshot().imageIds.length;
1362
+ if (!replayed || after <= before) {
1363
+ this.takeoverPaste(sessionId, target, files, text);
1364
+ }
1365
+ }
1366
+ /**
1367
+ * Path-takeover flow: insert the same-paste text and every image as a text
1368
+ * reference that serializes to the image's workspace path on send.
1369
+ * @param sessionId - the live Session id.
1370
+ * @param target - the composer textarea the paste landed on.
1371
+ * @param files - the captured image files.
1372
+ * @param text - same-paste text.
1373
+ */
1374
+ takeoverPaste(sessionId, target, files, text) {
1090
1375
  const input = this.inputFor(sessionId);
1091
1376
  const snapshot = input.state.getSnapshot();
1092
1377
  if (snapshot.phase !== 'plain')
1093
- return true;
1378
+ return;
1094
1379
  const start = Math.max(0, Math.min(target.selectionStart ?? snapshot.draft.length, snapshot.draft.length));
1095
1380
  const end = Math.max(start, Math.min(target.selectionEnd ?? start, snapshot.draft.length));
1096
- const text = (event.clipboardData?.getData('text/plain') ?? '').replaceAll('\uFFFC', '');
1097
1381
  try {
1098
1382
  let cursor = this.insertText(input, text, start, end);
1099
1383
  validateImages(files);
1100
- cursor = this.insertRecords(String(sessionId), input, files, cursor);
1384
+ cursor = this.insertRecords(sessionId, input, files, cursor);
1101
1385
  requestAnimationFrame(() => {
1102
1386
  target.focus({ preventScroll: true });
1103
1387
  target.setSelectionRange(cursor, cursor);
@@ -1106,6 +1390,45 @@ class PasteImageController {
1106
1390
  catch (error) {
1107
1391
  input.notify('error', message(error));
1108
1392
  }
1393
+ }
1394
+ handlePaste(event) {
1395
+ if (this.replaying)
1396
+ return false;
1397
+ const files = imageFiles(event.clipboardData);
1398
+ if (files.length === 0)
1399
+ return false;
1400
+ const target = event.target;
1401
+ if (!(target instanceof HTMLTextAreaElement) || target.closest('[data-composer-card]') === null)
1402
+ return false;
1403
+ const sessionId = this.ctx.sessions.list.getSnapshot().current;
1404
+ if (sessionId === undefined)
1405
+ return false;
1406
+ const modelLabel = currentModelLabel();
1407
+ this.refreshVerdict(sessionId, modelLabel);
1408
+ // Only a fresh host verdict acts; the native attachment flow stays the
1409
+ // default while the host is unconfirmed.
1410
+ const verdict = this.verdictFor(sessionId, modelLabel);
1411
+ if (verdict === undefined)
1412
+ return false;
1413
+ // An image-capable model (the variant routes included) keeps its native
1414
+ // paste: no switch, no takeover.
1415
+ if (verdict.takeOver === false && verdict.autoSwitch === undefined)
1416
+ return false;
1417
+ event.preventDefault();
1418
+ event.stopPropagation();
1419
+ event.stopImmediatePropagation();
1420
+ const input = this.inputFor(sessionId);
1421
+ if (input.state.getSnapshot().phase !== 'plain')
1422
+ return true;
1423
+ const text = (event.clipboardData?.getData('text/plain') ?? '').replaceAll('\uFFFC', '');
1424
+ if (verdict.autoSwitch !== undefined) {
1425
+ // The text-only model has an image-input variant: switch first, then
1426
+ // let the paste flow natively so the thumbnail and durable session
1427
+ // image are preserved.
1428
+ void this.autoSwitchPaste(sessionId, target, files, text, verdict.autoSwitch);
1429
+ return true;
1430
+ }
1431
+ this.takeoverPaste(sessionId, target, files, text);
1109
1432
  return true;
1110
1433
  }
1111
1434
  remove(sessionId, occurrence) {
@@ -1252,8 +1575,18 @@ function installPasteImages(ctx) {
1252
1575
  });
1253
1576
  ctx.effect(() => {
1254
1577
  const listener = (event) => { controller.handlePaste(event); };
1578
+ // A focus-time prefetch has the verdict ready before the first paste can land.
1579
+ const onFocusIn = () => {
1580
+ const sessionId = ctx.sessions.list.getSnapshot().current;
1581
+ if (sessionId !== undefined)
1582
+ controller.refreshVerdict(String(sessionId), currentModelLabel());
1583
+ };
1255
1584
  document.addEventListener('paste', listener, true);
1256
- return () => { document.removeEventListener('paste', listener, true); };
1585
+ document.addEventListener('focusin', onFocusIn, true);
1586
+ return () => {
1587
+ document.removeEventListener('paste', listener, true);
1588
+ document.removeEventListener('focusin', onFocusIn, true);
1589
+ };
1257
1590
  }, 'dsh-vision-toolkit: clipboard image capture');
1258
1591
  ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1259
1592
  name: 'conversation.input.dock',