@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.
- package/README.i18n.yaml +2 -2
- package/README.md +34 -17
- package/README.zh.md +32 -7
- package/assets/community-group-qr.png +0 -0
- package/assets/vision-model-test.png +0 -0
- package/docs/requirements-traceability/README.i18n.yaml +2 -2
- package/docs/requirements-traceability/README.md +2 -2
- package/docs/requirements-traceability/README.zh.md +2 -2
- package/lib/client.js +361 -28
- package/lib/client.js.map +1 -1
- package/lib/config.js +14 -0
- package/lib/config.js.map +1 -1
- package/lib/image-input-variants.js +615 -0
- package/lib/image-input-variants.js.map +1 -0
- package/lib/index.js +8 -1
- package/lib/index.js.map +1 -1
- package/lib/paste-images.js +6 -0
- package/lib/paste-images.js.map +1 -1
- package/lib/runtime.js +37 -3
- package/lib/runtime.js.map +1 -1
- package/lib/types/client/index.d.ts +16 -5
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paste-images.d.ts +69 -0
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/config.d.ts +26 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +153 -0
- package/lib/types/image-input-variants.d.ts.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/paste-images.d.ts +35 -0
- package/lib/types/paste-images.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +5 -2
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/web-request.d.ts +7 -0
- package/lib/types/web-request.d.ts.map +1 -1
- package/lib/types/web.d.ts +17 -2
- package/lib/types/web.d.ts.map +1 -1
- package/lib/web-request.js +11 -2
- package/lib/web-request.js.map +1 -1
- package/lib/web.js +88 -5
- package/lib/web.js.map +1 -1
- package/package.json +2 -1
- package/src/client/index.tsx +53 -14
- package/src/client/paste-images.tsx +331 -15
- package/src/config.ts +40 -0
- package/src/image-input-variants.ts +688 -0
- package/src/index.ts +18 -1
- package/src/paste-images.ts +39 -0
- package/src/runtime.ts +42 -3
- package/src/web-request.ts +12 -2
- 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
|
-
|
|
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: '
|
|
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
|
-
|
|
208
|
-
|
|
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: '
|
|
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(
|
|
605
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1085
|
-
event.
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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',
|