feat: add save_image tool and list image MCP tools

Expose save_image across direct model, MCP gateway, DeepSeek Harness and remote Agents (written on the remote host via chunked reads), and show the image MCP tool list in settings.
This commit is contained in:
mesalogo
2026-10-02 10:08:42 +08:00
parent b02ce31886
commit dd29f309f1
20 changed files with 479 additions and 33 deletions
@@ -148,6 +148,22 @@ it('preserves the Obsidian draft after failures and folder-picker cancellation',
})
})
it('lists the image generation and save tools on the managed image card', async () => {
const getSnapshot = vi.fn().mockResolvedValue({ mcpServers: [], skills: [] })
vi.stubGlobal('goodbuddy', { capabilities: { getSnapshot } })
render(<McpSettingsSection onOpenImageModelSettings={vi.fn()} />)
await waitFor(() => expect(getSnapshot).toHaveBeenCalledOnce())
const name = i18n.t('integrations:conversationImages.title')
const toggle = screen.getByRole('button', { name: i18n.t('integrations:mcp.builtin.expandServer', { name }) })
expect(toggle).toHaveTextContent(i18n.t('integrations:mcp.builtin.toolCount', { count: 2 }))
expect(screen.queryByText('save_image')).not.toBeInTheDocument()
fireEvent.click(toggle)
const tools = screen.getByRole('region', { name: i18n.t('integrations:mcp.builtin.toolsAriaLabel', { name }) })
expect(tools).toHaveTextContent('generate_image')
expect(tools).toHaveTextContent('save_image')
expect(tools).toHaveTextContent(i18n.t('integrations:conversationImages.tools.save_image'))
})
it('removes the redundant MCP title and places the custom action in content', async () => {
const getSnapshot = vi.fn().mockResolvedValue({ mcpServers: [], skills: [] })
vi.stubGlobal('goodbuddy', { capabilities: { getSnapshot } })
+65 -1
View File
@@ -16,7 +16,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { builtinMcpServers } from '../../shared/builtin-mcp-servers'
import { builtinModelToolGroups } from '../../shared/builtin-model-tools'
import {
builtinModelToolGroups,
builtinModelTools
} from '../../shared/builtin-model-tools'
import type {
BuiltinMcpServerId,
CapabilityDiagnosticReport,
@@ -40,6 +43,8 @@ const configurableMcpTargets: RuntimeTarget[] = [
'continue',
'deepseek-harness'
]
const imageTools = builtinModelTools.filter((tool) => tool.group === 'image')
const imageExpansionId = 'builtin:image-generation'
type McpSettingsTab =
| 'builtin'
| 'custom'
@@ -647,6 +652,65 @@ export function McpSettingsSection({
>
{t('conversationImages.openSettings')}
</button>
<button
aria-controls="mcp-server-tools-image-generation"
aria-expanded={expandedItemIds.has(imageExpansionId)}
aria-label={t(
expandedItemIds.has(imageExpansionId)
? 'mcp.builtin.collapseServer'
: 'mcp.builtin.expandServer',
{ name: t('conversationImages.title') }
)}
className="secondary-button builtin-mcp-card__details"
onClick={() => toggleItem(imageExpansionId)}
type="button"
>
{t('mcp.builtin.toolCount', { count: imageTools.length })}
<ChevronDown
aria-hidden="true"
className={
expandedItemIds.has(imageExpansionId)
? 'mcp-server-card__chevron mcp-server-card__chevron--expanded'
: 'mcp-server-card__chevron'
}
size={15}
/>
</button>
{expandedItemIds.has(imageExpansionId) && (
<div
className="builtin-mcp-card__tools"
id="mcp-server-tools-image-generation"
>
<section
aria-label={t('mcp.builtin.toolsAriaLabel', {
name: t('conversationImages.title')
})}
className="mcp-server-tools"
>
<div className="mcp-server-tools__heading">
<strong>{t('mcp.builtin.tools')}</strong>
<small>
{t('mcp.profiles.count', { count: imageTools.length })}
</small>
</div>
<ul>
{imageTools.map((tool) => (
<li key={tool.name}>
<div>
<code>{tool.name}</code>
<span className="builtin-tool-badge">
{tool.access === 'write'
? t('mcp.builtin.write')
: t('mcp.builtin.readOnly')}
</span>
</div>
<p>{t(`conversationImages.tools.${tool.name}`)}</p>
</li>
))}
</ul>
</section>
</div>
)}
</article>
{builtinMcpServers.map((server) => {
const expansionId = `builtin:${server.id}`
@@ -7,7 +7,11 @@ export const integrations = {
managed: 'Builtin · Automatically managed',
description: 'Automatically managed by the image models’ “Allow AI to invoke in conversations” setting.',
runtimeScope: 'Assignments include applicable remote Runtimes through GoodBuddy Agent. Generation requires Execute and a chat model with tool calling; assignments do not indicate runtime readiness.',
openSettings: 'Go to image model settings'
openSettings: 'Go to image model settings',
tools: {
generate_image: 'Generate a new image with an image model allowed in conversations, or edit images from this conversation. Results are saved in the conversation.',
save_image: 'Save a generated or uploaded image from this conversation to a PNG, JPEG or WebP file. Requires Execute mode; in remote projects the file is written on the remote host.'
}
},
channels: {
tabs: {
@@ -4,7 +4,11 @@ export const integrations = {
managed: '内置 · 自动管理',
description: '由图片模型的「允许 AI 在会话中调用」设置自动管理。',
runtimeScope: '分配包含 GoodBuddy Agent 远程路径上的适用 Runtime。生成需要 Execute 模式及支持工具调用的聊天模型;勾选不代表 Runtime 已就绪。',
openSettings: '前往图片模型设置'
openSettings: '前往图片模型设置',
tools: {
generate_image: '使用已允许会话调用的图片模型生成新图片,或基于会话中的图片进行编辑,结果保存在当前会话。',
save_image: '将当前会话中已生成或已上传的图片保存为 PNG、JPEG 或 WebP 文件;需要 Execute 模式,远程项目中文件写入远程主机。'
}
},
channels: {
tabs: {