/** * DSH Vision Toolkit browser plugin: dedicated Tool cards plus the Settings, * health, connection-test, and safe Artifact preview experience. */ import { useEffect, useState, useSyncExternalStore, type ReactNode, } from 'react' import { Button, Input } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context as ClientContext } from '@deepseek-ai/cordis' /** * `ToolCallBlock` is the one DSH type this bundle consumes by name. DSH 0.1.5 * publishes it from `@deepseek-ai/dsh-client-ui-conversation/client`; its * earlier home, `@deepseek-ai/dsh-client-runtime/client`, stopped publishing * after 0.1.1-rc.2 and has no member of the 0.1.5 family at all, so importing * the old specifier would leave an unresolvable peer on a real host. The * `paths` row in tsconfig.client.json and the peerDependency in package.json * name the same package, and both must move together. A type-only import costs * nothing at runtime and emits no `require()` in lib/client.js. */ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client' import type {} from '@deepseek-ai/dsh-api-remotes/client' import type {} from '@deepseek-ai/dsh-client-connection/client' import type {} from '@deepseek-ai/dsh-credentials/types' import type {} from '@deepseek-ai/dsh-settings/types' import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import { installPasteImages } from './paste-images.tsx' import { installModelVariantsHider } from './model-variants-hider.ts' import { resetDisplayConfigCache } from './display-config.ts' const NS = 'vision-toolkit' const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings' const PRESENTATION_META_KEY = '$dshVisionToolkit' const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' // Keep these browser defaults aligned with src/defaults.ts without importing server-side config. const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1' const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION' const BUILT_IN_FREE_VISION_MODEL = 'gemini-3.7-flash' const AIHUBMIX_TUTORIAL_URL_EN = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.md' const AIHUBMIX_TUTORIAL_URL_ZH = 'https://github.com/Anionex/dsh-vision-toolkit/blob/main/docs/aihubmix-gemini-vision.zh.md' const en = { nav: 'Vision', settingsTitle: 'Vision Toolkit', settingsIntro: 'Configure the pinned visual engineering runtime, its external vision endpoint, and local safety limits.', externalNotice: 'Remote tools send the selected image bytes to the configured external vision API. Local crop, trace, pixel diff, palette, foreground extraction, and HTML rendering do not upload images.', provider: 'Vision service', providerHint: 'Choose the API protocol, then provide the service address, model, and API key used by online vision features.', aihubmixTutorial: 'Need an AIHubMix key for free Gemini 3.7 Flash vision? Follow the signup guide →', baseUrl: 'Base URL', apiKey: 'API key', apiKeyPlaceholderMissing: 'Paste the API key', apiKeyPlaceholderConfigured: 'Saved; leave blank to keep it', apiKeyHint: 'The key is stored in DSH Credentials and is never shown again after saving.', apiKeyLocked: 'The current key comes from a read-only source and cannot be replaced here.', apiKeyBlank: 'The API key cannot contain only spaces.', apiKeyInvalid: 'Paste only the key, without a variable name, quotes, spaces, or line breaks.', credential: 'Credential name', credentialHint: 'The built-in free provider needs no user key. For a custom provider, this is the DSH credential reference used to store its key.', model: 'Model', protocol: 'API protocol', anthropicThinking: 'Anthropic thinking', anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.', userAgent: 'User-Agent', language: 'Output language', limits: 'Limits', timeout: 'Request timeout (ms)', maxBytes: 'Maximum image bytes', maxPixels: 'Maximum image pixels', concurrency: 'Concurrent calls per session', runtime: 'Runtime', runtimeMode: 'Runtime mode', toolkitPath: 'Pinned checkout path', python: 'Python override', storage: 'Local files', storageDir: 'Default save directory', storageDirHint: 'Leave blank to keep .dsh-vision-toolkit in each workspace. On POSIX systems, set an absolute shared root such as /tmp/dsh-vision-toolkit to store artifacts, pasted images, and caches in an automatically generated per-user, workspace-specific child directory. Windows shared roots remain disabled until their ACLs can be verified safely.', allowedDirs: 'Additional allowed directories', allowedDirsHint: 'One path per line. The session workspace is always allowed.', save: 'Save and apply', saving: 'Validating runtime…', reload: 'Reload', saved: 'Settings validated and applied.', readOnly: 'Service settings are read-only. A writable API key can still be saved.', configured: 'Configured', missing: 'Missing', source: 'Source', sourceHint: '{source}: {value}', sourceEnv: 'Environment variable', sourceFile: 'Credential file', health: 'Health', runHealth: 'Run health check', testConnection: 'Test API connection', testModel: 'Test vision model', testing: 'Checking…', testingModel: 'Testing model…', connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.', saveBeforeTesting: 'Save service changes before testing the connection.', advanced: 'Advanced settings', advancedHint: 'Credential name, provider compatibility, output language, resource limits, default save directory, runtime source, Python, and additional readable directories.', imageInput: 'Image input', hiddenVariants: 'Transparent variant routing', hiddenVariantsLabel: 'Keep the original model names and enable images automatically', hiddenVariantsHint: 'Text-only models keep one model-selector entry with the original name while the session runs on the image-capable variant. Pasted images, image history, and the built-in read_image tool keep working; disable to restore the explicit (Vision Toolkit) entries.', pluginVersion: 'Plugin', upstreamVersion: 'Upstream', activeGeneration: 'Runtime generation', activeGenerationValue: 'Generation {generation}', updates: 'Plugin updates', updatesHint: 'Check npm for a newer release, install it into this DSH profile, and restart DSH Web automatically.', manualUpdate: 'Manual update', manualUpdateHint: 'Run this command in your terminal to install the latest release into this DSH profile.', copy: 'Copy', copied: 'Copied', checkUpdate: 'Check for updates', checkingUpdate: 'Checking for updates…', updateAvailable: 'Update available', updateAvailableDetail: 'Version {version} is available. It will restart DSH Web automatically when safe; otherwise you will be asked to restart it manually.', upToDate: 'Up to date', upToDateDetail: 'Version {version} is the latest release.', updateNow: 'Install update', updatingPlugin: 'Installing update…', updateConfirm: 'Install Vision Toolkit {version} now? DSH Web will restart automatically when supported; otherwise a manual restart will be required.', restarting: 'Version {version} was installed. Waiting for DSH Web to restart…', manualRestartRequired: 'Version {version} was installed. Restart DSH Web through your usual command or process manager to activate it.', updateProfile: 'Profile', updateInstalled: 'Installed', updateLatest: 'Latest', updateUnsupported: 'In-app updates are unavailable for this installation.', updateReasonProfileNotFound: 'The running plugin could not be matched to a DSH profile installation.', updateReasonNotDependency: 'The plugin is not a direct dependency of this DSH profile.', updateReasonLocalSource: 'This profile uses a local, workspace, URL, or git installation; update that source manually so local work is not overwritten.', updateReasonReadOnly: 'The profile package manifest is read-only.', updateReasonPnpm: 'pnpm is unavailable in the DSH execution environment.', updateReasonPlatform: 'Automatic restart is unavailable on this operating system.', updateReasonRestartUnmanaged: 'Detached self-restart is disabled. Use a supported process manager, or explicitly opt in with DSH_VISION_TOOLKIT_ALLOW_DETACHED_RESTART=1 for an unsupervised Web process.', updateReasonRestartAddress: 'Automatic restart is unavailable when DSH Web uses an unknown or dynamically allocated port. Start it with a fixed --port value.', updateSaveFirst: 'Save or discard the current Settings and API key changes before updating the plugin.', restartTimedOut: 'DSH Web did not return with the target plugin version. Check the restart log and restart the Web profile through its original process manager.', restartRolledBack: 'The new plugin did not become ready, so the previous version was restored. Check the restart log before trying again.', pluginKind: 'DSH native plugin', runtimeUnavailable: 'Runtime unavailable', runtimeCandidateRejected: 'Last runtime candidate was rejected; the active generation remains available.', runtimeReady: 'Ready', runtimeManaged: 'Managed', runtimeExternal: 'External checkout', retry: 'Retry', open: 'Open file', download: 'Download', previewUnavailable: 'HTTP preview is unavailable in this host; use Open file.', running: 'Running…', failed: 'Failed', matches: 'matches', elements: 'elements', dimensions: 'Dimensions', coordinates: 'Coordinates', artifact: 'Artifact', artifacts: 'Artifacts', difference: 'Overall difference', worstRegions: 'Worst regions', colors: 'Dominant colors', noResult: 'Structured result unavailable; inspect the raw Tool result.', healthy: 'Healthy', degraded: 'Needs attention', notTested: 'Not tested', groundTitle: 'Ground', detectTitle: 'Detect', traceTitle: 'Trace SVG', pixelDiffTitle: 'Pixel Diff', cropTitle: 'Crop', longOcrTitle: 'Long OCR', extractForegroundTitle: 'Extract Foreground', htmlScreenshotTitle: 'HTML Screenshot', artifactTitle: 'Vision Artifact', dominantColorsTitle: 'Dominant Colors', artifactGroundPreview: 'Grounding bounding-box preview', artifactDetectPreview: 'Detected-element bounding-box preview', artifactCrop: 'Cropped image region', artifactTrace: 'Traced vector geometry', artifactDiffHeatmap: 'Pixel-difference heatmap', artifactDiffReport: 'Structured pixel-difference report', artifactLongManifest: 'Long-screenshot split and merge manifest', artifactLongTranscript: 'Merged long-screenshot OCR transcript', artifactLongAudit: 'Long-screenshot OCR boundary audit', artifactLongChunk: 'Long-screenshot OCR chunk {index}', artifactOcrSidecar: 'OCR sidecar for chunk {index}', artifactForeground: 'Extracted transparent foreground', artifactHtmlScreenshot: 'Headless browser screenshot of local HTML', label: 'Label', paths: 'paths', healthPython: 'Python', healthDependencies: 'Dependencies', healthChrome: 'Browser', healthCredential: 'Credential', healthArtifactDirectory: 'Artifact directory', healthTempDirectory: 'Temporary directory', healthService: 'Vision service', healthModel: 'Vision model', statusOk: 'OK', statusWarning: 'Warning', statusError: 'Error', statusNotTested: 'Not tested', positiveInteger: '{field} must be a positive integer.', healthPythonDetail: '{version} via {path}', healthChromeMissing: 'Chrome, Chromium, or Edge was not found; HTML Screenshot is unavailable.', healthChromeProbeFailed: 'Could not check whether a supported browser is available.', healthCredentialMissing: 'Credential {credential} is not configured.', healthCredentialReady: 'Credential {credential} is available.', healthCredentialFailed: 'Could not read credential {credential}.', healthDirectoryWritable: '{directory} is writable: {path}', healthDirectoryNotWritable: '{directory} is not writable: {path}', healthArtifactDirectoryFailed: 'Could not prepare the artifact directory.', healthConnectionNotTested: 'API connection not tested. Use Test API connection to query /models.', healthConnectionCredentialMissing: 'Connection test skipped because the credential is unavailable.', healthServiceResponded: 'Service responded at {endpoint} (HTTP {status}).', healthServiceRejectedCredential: 'Service rejected the configured credential (HTTP {status}).', healthServiceForbidden: 'Service is reachable, but GET /models is restricted (HTTP {status}). This is often an account or model-list permission limit, not an invalid key; you can ignore this warning when the vision-model test reports success.', healthServiceNoModels: 'Service is reachable but does not support GET /models (HTTP {status}).', healthServiceRateLimited: 'Service is reachable, but the connection test was rate-limited (HTTP 429).', healthServiceHttpFailed: 'Connection test failed with HTTP {status}.', healthServiceUnreachable: 'Could not reach {endpoint}.', healthModelNotTested: 'Vision model not tested. Run Test vision model to make one real multimodal request.', healthModelCredentialMissing: 'Vision model test skipped because the credential is unavailable.', healthModelReady: 'Model {model} completed a real multimodal request.', healthModelFailed: 'Real multimodal request failed: {detail}', modelTestVerifiedTag: 'Verified', modelTestNotRunTag: 'Not tested', modelTestFailedTag: 'Test failed', } as const type LocaleKey = keyof typeof en const zh: Record = { nav: '视觉工具', settingsTitle: '视觉工具箱', settingsIntro: '在这里设置视觉模型服务、工具运行环境,以及图片和文件的本地访问范围。', externalNotice: '使用图像理解、目标定位、界面检测或文字识别等在线功能时,所选图片会发送到下方配置的视觉服务。图片裁剪、轮廓描摹、像素对比、主色提取、前景提取和网页截图均在本机完成,不会上传图片。', provider: '在线视觉服务', providerHint: '选择接口协议后,填写在线视觉功能使用的 API 地址、模型名称和 API 密钥。', aihubmixTutorial: '想申请 AIHubMix Key,并用免费 Gemini 3.7 Flash 识图?看这篇图文教程 →', baseUrl: 'API 地址', apiKey: 'API 密钥', apiKeyPlaceholderMissing: '粘贴 API 密钥', apiKeyPlaceholderConfigured: '已保存;留空表示不修改', apiKeyHint: '密钥会保存到 DSH 凭据存储,保存后不会在页面中回显。', apiKeyLocked: '当前密钥来自只读配置,无法在此替换。', apiKeyBlank: 'API 密钥不能只包含空格。', apiKeyInvalid: '请只粘贴密钥本身,不要包含变量名、引号、空格或换行。', credential: '凭据名称', credentialHint: '内置免费视觉服务无需用户密钥;切换到自定义服务时,此处是保存其密钥的 DSH 凭据名称。', model: '模型名称', protocol: 'API 协议', anthropicThinking: 'Anthropic thinking', anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。', userAgent: 'User-Agent', language: '结果语言', limits: '资源与并发限制', timeout: '单次请求超时(毫秒)', maxBytes: '单张图片大小上限(字节)', maxPixels: '单张图片最大像素数', concurrency: '单个会话最多并发任务数', runtime: '工具运行环境', runtimeMode: '环境来源', toolkitPath: 'agent-vision-toolkit 目录', python: 'Python 解释器(可选)', storage: '本地文件', storageDir: '默认保存目录', storageDirHint: '留空时继续在各工作区创建 .dsh-vision-toolkit。在 POSIX 系统上填写绝对路径共享根目录(例如 /tmp/dsh-vision-toolkit)后,产物、粘贴图片和缓存会保存到自动生成的当前用户、工作区专属子目录中。Windows 共享目录会保持禁用,直到能够安全校验 ACL 权限。', allowedDirs: '允许读取的其他目录', allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。', save: '保存设置', saving: '正在检查并应用…', reload: '重新加载', saved: '设置已保存并生效。', readOnly: '服务设置来自只读配置;如果 API 密钥可写,仍可在此保存密钥。', configured: '已就绪', missing: '未配置', source: '配置来源', sourceHint: '{source}:{value}', sourceEnv: '环境变量', sourceFile: '凭据文件', health: '运行检查', runHealth: '检查本地环境', testConnection: '测试 API 连接', testModel: '测试视觉模型', testing: '检查中…', testingModel: '正在测试模型…', connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。', saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。', advanced: '高级设置', advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、默认保存目录、运行环境来源、Python 和额外可读目录。一般无需修改。', imageInput: '图片输入', hiddenVariants: '透明变体路由', hiddenVariantsLabel: '保留原模型名并自动启用图片能力', hiddenVariantsHint: '文本模型在模型列表中只显示原名称,会话实际运行在支持图片的变体路由上:粘贴图片、历史图片和内置 read_image 工具均可正常使用。关闭后恢复显示显式的(Vision Toolkit)条目。', pluginVersion: '插件版本', upstreamVersion: '工具包版本', activeGeneration: '本次运行已应用', activeGenerationValue: '{generation} 次', updates: '插件更新', updatesHint: '检查 npm 新版本,自动更新当前 DSH Profile 中的插件,然后重启 DSH Web。', manualUpdate: '手动更新', manualUpdateHint: '在终端运行以下命令,将当前 DSH Profile 更新到最新版本。', copy: '复制', copied: '已复制', checkUpdate: '检查更新', checkingUpdate: '正在检查更新…', updateAvailable: '发现新版本', updateAvailableDetail: '可更新到 {version}。能安全自重启时会自动重启,否则安装完成后会提示你手动重启。', upToDate: '已是最新版', upToDateDetail: '当前 {version} 已是最新正式版本。', updateNow: '安装更新', updatingPlugin: '正在安装更新…', updateConfirm: '现在安装 Vision Toolkit {version} 吗?支持安全自重启时会自动重启,否则需要你手动重启 DSH Web。', restarting: '已安装 {version},正在等待 DSH Web 重启…', manualRestartRequired: '已安装 {version}。请按你平时的方式手动重启 DSH Web,重启后新版本生效。', updateProfile: 'Profile', updateInstalled: '当前版本', updateLatest: '最新版本', updateUnsupported: '当前安装方式不支持页面内更新。', updateReasonProfileNotFound: '无法把正在运行的插件匹配到某个 DSH Profile 安装。', updateReasonNotDependency: '该插件不是当前 DSH Profile 的直接依赖。', updateReasonLocalSource: '当前使用本地、workspace、URL 或 git 安装;为避免覆盖本地修改,请手动更新对应来源。', updateReasonReadOnly: '当前 Profile 的 package.json 不可写。', updateReasonPnpm: 'DSH 运行环境中找不到 pnpm。', updateReasonPlatform: '当前操作系统不支持安全的自动重启。', updateReasonRestartUnmanaged: '默认禁用脱离原进程管理器的自重启。仅对无人监管的 Web 进程明确设置 DSH_VISION_TOOLKIT_ALLOW_DETACHED_RESTART=1 后开放。', updateReasonRestartAddress: 'DSH Web 使用未知端口或动态端口时无法安全自动重启。请用固定的 --port 值启动。', updateSaveFirst: '更新插件前,请先保存或放弃当前 Settings 和 API 密钥修改。', restartTimedOut: 'DSH Web 未能以目标插件版本恢复。请检查重启日志,并通过原进程管理器重启 Web Profile。', restartRolledBack: '新插件未能就绪,系统已恢复上一版本。再次尝试前请检查重启日志。', pluginKind: 'DSH 原生插件', runtimeUnavailable: '运行环境尚未就绪', runtimeCandidateRejected: '新设置未能生效,仍在使用上一次可用的设置。', runtimeReady: '已就绪', runtimeManaged: '自动安装', runtimeExternal: '本地源码', retry: '重试', open: '在工作区中打开', download: '下载', previewUnavailable: '此页面无法直接预览该文件,请在工作区中打开。', running: '运行中…', failed: '运行失败', matches: '处匹配', elements: '个元素', dimensions: '图片尺寸', coordinates: '坐标', artifact: '生成文件', artifacts: '个生成文件', difference: '像素差异', worstRegions: '差异最大的区域', colors: '种颜色', noResult: '未能读取结果,请查看工具的原始输出。', healthy: '一切正常', degraded: '有项目需要处理', notTested: '尚未检查', groundTitle: '目标定位', detectTitle: '界面元素识别', traceTitle: '描摹为 SVG', pixelDiffTitle: '像素对比', cropTitle: '裁剪图片', longOcrTitle: '长图文字识别', extractForegroundTitle: '提取前景', htmlScreenshotTitle: '网页截图', artifactTitle: '视觉处理结果', dominantColorsTitle: '主色提取', artifactGroundPreview: '目标定位框预览', artifactDetectPreview: '界面元素标注预览', artifactCrop: '裁剪后的图片', artifactTrace: '描摹得到的矢量图', artifactDiffHeatmap: '像素差异热力图', artifactDiffReport: '像素差异详细报告', artifactLongManifest: '长图切分与合并记录', artifactLongTranscript: '长图文字识别结果', artifactLongAudit: '长图分块边界检查记录', artifactLongChunk: '长图文字识别分块 {index}', artifactOcrSidecar: '分块 {index} 的文字识别记录', artifactForeground: '提取后的透明背景前景图', artifactHtmlScreenshot: '本地网页截图', label: '名称', paths: '条路径', healthPython: 'Python', healthDependencies: 'Python 依赖', healthChrome: '浏览器', healthCredential: 'API 密钥', healthArtifactDirectory: '输出目录', healthTempDirectory: '临时目录', healthService: '视觉服务', healthModel: '视觉模型', statusOk: '正常', statusWarning: '注意', statusError: '异常', statusNotTested: '未检查', positiveInteger: '{field}必须填写正整数。', healthPythonDetail: '版本 {version};解释器:{path}', healthChromeMissing: '未找到 Chrome、Chromium 或 Edge,网页截图功能暂不可用。', healthChromeProbeFailed: '无法检查浏览器是否可用。', healthCredentialMissing: '尚未配置凭据 {credential}。', healthCredentialReady: '已找到凭据 {credential}。', healthCredentialFailed: '无法读取凭据 {credential}。', healthDirectoryWritable: '{directory}可写:{path}', healthDirectoryNotWritable: '{directory}不可写:{path}', healthArtifactDirectoryFailed: '无法准备输出目录。', healthConnectionNotTested: '尚未测试 API 连接。点击“测试 API 连接”可请求 /models。', healthConnectionCredentialMissing: 'API 密钥不可用,未执行连接测试。', healthServiceResponded: '服务已响应:{endpoint}(HTTP {status})。', healthServiceRejectedCredential: '服务拒绝了当前 API 密钥(HTTP {status})。', healthServiceForbidden: '服务可以访问,但对 GET /models 的访问被限制(HTTP {status})。这通常是账号或模型列表权限限制,不代表密钥无效;若“视觉模型已实测正常”,此警告可忽略。', healthServiceNoModels: '服务可以访问,但不支持 GET /models(HTTP {status})。', healthServiceRateLimited: '服务可以访问,但本次连接测试触发了限流(HTTP 429)。', healthServiceHttpFailed: '连接测试失败(HTTP {status})。', healthServiceUnreachable: '无法连接到 {endpoint}。', healthModelNotTested: '尚未测试视觉模型。点击“测试视觉模型”可执行一次真实多模态请求。', healthModelCredentialMissing: '视觉模型测试已跳过,因为当前 API 密钥不可用。', healthModelReady: '模型 {model} 已完成一次真实多模态请求。', healthModelFailed: '真实多模态请求失败:{detail}', modelTestVerifiedTag: '已实测', modelTestNotRunTag: '未测试', modelTestFailedTag: '测试失败', } type Translate = (key: LocaleKey, params?: Record) => string interface ToolCallOwnerProps { callId: string toolName: string block: ToolCallBlock cwd?: string | undefined openFile: (path: string) => void inspect?: (() => void) | undefined } declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { /** Keyed atomic Tool call view, dispatched by wire Tool name. */ 'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ToolCallOwnerProps } } interface LocaleNamespaceMap { /** DSH Vision Toolkit Tool cards and Settings copy. */ 'vision-toolkit': LocaleKey } } type ToolCallViewProps = PropsRuntime<'tool.call.toolview'> interface ArtifactDescriptor { path: string filename: string mimeType: string kind: 'image' | 'svg' | 'markdown' | 'json' description: string sourceTool: string previewIntent: 'image' | 'svg' | 'text' | 'download' bytes: number } interface ArtifactGrant { path: string previewUrl: string downloadUrl: string } interface HealthCheck { status: 'ok' | 'warning' | 'error' | 'not_tested' detail: string } interface HealthResult { pluginVersion: string checks: Record healthy: boolean connectionTested: boolean modelTested: boolean } interface SettingsValue { provider?: { baseUrl?: string credential?: string model?: string protocol?: 'openai' | 'anthropic' anthropicThinking?: 'omit' | 'disabled' | 'adaptive' userAgent?: string } language?: 'zh' | 'en' timeoutMs?: number maxImageBytes?: number maxImagePixels?: number concurrency?: number runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string } storageDir?: string storageHistory?: string[] allowedDirs?: string[] imageInputVariants?: { enabled?: boolean providers?: string[] autoSwitch?: boolean hidden?: boolean } } type PluginUpdateUnavailableReason = | 'profile-not-found' | 'not-direct-dependency' | 'unsupported-install-source' | 'profile-read-only' | 'pnpm-unavailable' | 'unsupported-platform' | 'restart-unmanaged' | 'restart-address-unavailable' interface PluginUpdateCapability { supported: boolean checkSupported?: boolean profile?: string dependencySpec?: string reason?: PluginUpdateUnavailableReason } interface PluginUpdateCheck extends PluginUpdateCapability { currentVersion: string latestVersion?: string updateAvailable: boolean checkedAt: string } type PluginUpdateResult = { fromVersion: string toVersion: string profile: string restarting: true retryAfterMs: number manualRestartRequired?: false } | { fromVersion: string toVersion: string profile: string restarting: false manualRestartRequired: true retryAfterMs?: undefined } interface SettingsSnapshot { schemaVersion: 1 writable: boolean settings: { value: SettingsValue; revision: number; applies: 'live' } credential: { ref: string; configured: boolean; source?: string; writable: boolean } runtime: { ready: boolean generation: number activeConfig?: SettingsValue upstream?: { source: 'managed' | 'external' path: string runtimeHome: string python: string pythonVersion: string } lastError?: string } release: { pluginVersion: string upstreamRepository: string upstreamVersion: string upstreamCommit: string update: PluginUpdateCapability } artifactRouteAvailable: boolean } interface ApiSuccess { ok: true; value: T } interface ApiFailure { ok: false; error: { code: string; message: string } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function textOfContent(block: ToolCallBlock): string { if (!('kind' in block)) return '' return block.content .filter((entry): entry is Extract => entry.type === 'text') .map(entry => entry.text) .join('\n') } /** Decode canonical presentation metadata with a JSON-text fallback. */ export function decodeVisionResult(block: ToolCallBlock): Record | undefined { if (!('kind' in block) || block.isError) return undefined if (isRecord(block.meta)) return block.meta const text = textOfContent(block).trim() if (text.length === 0) return undefined try { const parsed = JSON.parse(text) as unknown return isRecord(parsed) ? parsed : undefined } catch { return undefined } } function accessMap(value: Record | undefined): Map { const map = new Map() if (value === undefined) return map const envelope = value[PRESENTATION_META_KEY] if (!isRecord(envelope) || envelope.schemaVersion !== 1 || !Array.isArray(envelope.artifacts)) return map for (const entry of envelope.artifacts) { if (!isRecord(entry) || typeof entry.path !== 'string' || typeof entry.previewUrl !== 'string' || typeof entry.downloadUrl !== 'string') continue map.set(entry.path, entry as unknown as ArtifactGrant) } return map } function artifactFrom(value: unknown): ArtifactDescriptor | undefined { if (!isRecord(value)) return undefined if ( typeof value.path !== 'string' || typeof value.filename !== 'string' || typeof value.mimeType !== 'string' || (value.kind !== 'image' && value.kind !== 'svg' && value.kind !== 'markdown' && value.kind !== 'json') || typeof value.description !== 'string' || typeof value.sourceTool !== 'string' || (value.previewIntent !== 'image' && value.previewIntent !== 'svg' && value.previewIntent !== 'text' && value.previewIntent !== 'download') || typeof value.bytes !== 'number' ) return undefined return value as unknown as ArtifactDescriptor } function collectArtifacts(value: unknown, found = new Map(), depth = 0): ArtifactDescriptor[] { if (depth > 16) return [...found.values()] const artifact = artifactFrom(value) if (artifact !== undefined) { found.set(artifact.path, artifact) return [...found.values()] } if (Array.isArray(value)) { for (const entry of value) collectArtifacts(entry, found, depth + 1) } else if (isRecord(value)) { for (const entry of Object.values(value)) collectArtifacts(entry, found, depth + 1) } return [...found.values()] } function numberOf(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined } function stringOf(value: unknown): string | undefined { return typeof value === 'string' ? value : undefined } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } function boxText(value: unknown): string { if (!isRecord(value)) return '—' const parts = ['x1', 'y1', 'x2', 'y2'].map(key => numberOf(value[key])) return parts.every(part => part !== undefined) ? parts.join(', ') : '—' } function statusText(block: ToolCallBlock, t: Translate): string | undefined { if (!('kind' in block)) return t('running') if (block.isError) return textOfContent(block).split('\n')[0] || t('failed') return undefined } function VisionIcon({ kind = 'scan' }: { kind?: 'scan' | 'target' | 'layers' | 'shape' | 'diff' | 'palette' }) { const path = kind === 'target' ? 'M8 2v2m0 8v2M2 8h2m8 0h2M5 5h6v6H5z' : kind === 'layers' ? 'm3 6 5-3 5 3-5 3-5-3Zm0 3 5 3 5-3M3 12l5 3 5-3' : kind === 'shape' ? 'M3 12 6 4l7-1-1 7-9 2Zm3-8 6 6' : kind === 'diff' ? 'M3 3h4v4H3V3Zm6 6h4v4H9V9Zm0-6h4M3 11h4' : kind === 'palette' ? 'M8 2a6 6 0 1 0 0 12h1.2a1.3 1.3 0 0 0 0-2.6H8a1.5 1.5 0 0 1 0-3h3.5A2.5 2.5 0 0 0 14 5.9C13.2 3.6 10.9 2 8 2Z' : 'M3 5V3h2M11 3h2v2M13 11v2h-2M5 13H3v-2M5 8h6' return ( ) } function ToolShell({ block, title, summary, icon, children, t, }: { block: ToolCallBlock title: string summary?: string | undefined icon: ReactNode children?: ReactNode | undefined t: Translate }) { const [open, setOpen] = useState(true) const status = statusText(block, t) const expandable = children !== undefined && children !== null return (
{expandable && open ?
{children}
: null}
) } function ArtifactActions({ artifact, grant, openFile, t }: { artifact: ArtifactDescriptor grant?: ArtifactGrant | undefined openFile: (path: string) => void t: Translate }) { return (
{grant === undefined ? null : {t('download')}}
) } function ArtifactPreview({ artifact, grant, openFile, t }: { artifact: ArtifactDescriptor grant?: ArtifactGrant | undefined openFile: (path: string) => void t: Translate }) { const canPreview = grant !== undefined && (artifact.kind === 'image' || artifact.kind === 'svg') const description = artifactDescription(artifact.description, t) return (
{canPreview ? artifact.kind === 'svg' ?