diff --git a/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch b/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch deleted file mode 100644 index 75c418e591..0000000000 --- a/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index ff305b112779b718f21a636a27b1196125a332d9..cf32ff5086d4d9e56f8fe90c98724559083bafc3 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { - - // src/get-model-path.ts - function getModelPath(modelId) { -- return modelId.includes("/") ? modelId : `models/${modelId}`; -+ return modelId.includes("models/") ? modelId : `models/${modelId}`; - } - - // src/google-generative-ai-options.ts -diff --git a/dist/index.mjs b/dist/index.mjs -index 57659290f1cec74878a385626ad75b2a4d5cd3fc..d04e5927ec3725b6ffdb80868bfa1b5a48849537 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { - - // src/get-model-path.ts - function getModelPath(modelId) { -- return modelId.includes("/") ? modelId : `models/${modelId}`; -+ return modelId.includes("models/") ? modelId : `models/${modelId}`; - } - - // src/google-generative-ai-options.ts diff --git a/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch b/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch new file mode 100644 index 0000000000..18570d5ced --- /dev/null +++ b/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch @@ -0,0 +1,152 @@ +diff --git a/dist/index.js b/dist/index.js +index c2ef089c42e13a8ee4a833899a415564130e5d79..75efa7baafb0f019fb44dd50dec1641eee8879e7 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { + + // src/get-model-path.ts + function getModelPath(modelId) { +- return modelId.includes("/") ? modelId : `models/${modelId}`; ++ return modelId.includes("models/") ? modelId : `models/${modelId}`; + } + + // src/google-generative-ai-options.ts +diff --git a/dist/index.mjs b/dist/index.mjs +index d75c0cc13c41192408c1f3f2d29d76a7bffa6268..ada730b8cb97d9b7d4cb32883a1d1ff416404d9b 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { + + // src/get-model-path.ts + function getModelPath(modelId) { +- return modelId.includes("/") ? modelId : `models/${modelId}`; ++ return modelId.includes("models/") ? modelId : `models/${modelId}`; + } + + // src/google-generative-ai-options.ts +diff --git a/dist/internal/index.js b/dist/internal/index.js +index 277cac8dc734bea2fb4f3e9a225986b402b24f48..bb704cd79e602eb8b0cee1889e42497d59ccdb7a 100644 +--- a/dist/internal/index.js ++++ b/dist/internal/index.js +@@ -432,7 +432,15 @@ function prepareTools({ + var _a; + tools = (tools == null ? void 0 : tools.length) ? tools : void 0; + const toolWarnings = []; +- const isGemini2 = modelId.includes("gemini-2"); ++ // These changes could be safely removed when @ai-sdk/google v3 released. ++ const isLatest = ( ++ [ ++ 'gemini-flash-latest', ++ 'gemini-flash-lite-latest', ++ 'gemini-pro-latest', ++ ] ++ ).some(id => id === modelId); ++ const isGemini2OrNewer = modelId.includes("gemini-2") || modelId.includes("gemini-3") || isLatest; + const supportsDynamicRetrieval = modelId.includes("gemini-1.5-flash") && !modelId.includes("-8b"); + const supportsFileSearch = modelId.includes("gemini-2.5"); + if (tools == null) { +@@ -458,7 +466,7 @@ function prepareTools({ + providerDefinedTools.forEach((tool) => { + switch (tool.id) { + case "google.google_search": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ googleSearch: {} }); + } else if (supportsDynamicRetrieval) { + googleTools2.push({ +@@ -474,7 +482,7 @@ function prepareTools({ + } + break; + case "google.url_context": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ urlContext: {} }); + } else { + toolWarnings.push({ +@@ -485,7 +493,7 @@ function prepareTools({ + } + break; + case "google.code_execution": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ codeExecution: {} }); + } else { + toolWarnings.push({ +@@ -507,7 +515,7 @@ function prepareTools({ + } + break; + case "google.vertex_rag_store": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ + retrieval: { + vertex_rag_store: { +diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs +index 03b7cc591be9b58bcc2e775a96740d9f98862a10..347d2c12e1cee79f0f8bb258f3844fb0522a6485 100644 +--- a/dist/internal/index.mjs ++++ b/dist/internal/index.mjs +@@ -424,7 +424,15 @@ function prepareTools({ + var _a; + tools = (tools == null ? void 0 : tools.length) ? tools : void 0; + const toolWarnings = []; +- const isGemini2 = modelId.includes("gemini-2"); ++ // These changes could be safely removed when @ai-sdk/google v3 released. ++ const isLatest = ( ++ [ ++ 'gemini-flash-latest', ++ 'gemini-flash-lite-latest', ++ 'gemini-pro-latest', ++ ] ++ ).some(id => id === modelId); ++ const isGemini2OrNewer = modelId.includes("gemini-2") || modelId.includes("gemini-3") || isLatest; + const supportsDynamicRetrieval = modelId.includes("gemini-1.5-flash") && !modelId.includes("-8b"); + const supportsFileSearch = modelId.includes("gemini-2.5"); + if (tools == null) { +@@ -450,7 +458,7 @@ function prepareTools({ + providerDefinedTools.forEach((tool) => { + switch (tool.id) { + case "google.google_search": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ googleSearch: {} }); + } else if (supportsDynamicRetrieval) { + googleTools2.push({ +@@ -466,7 +474,7 @@ function prepareTools({ + } + break; + case "google.url_context": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ urlContext: {} }); + } else { + toolWarnings.push({ +@@ -477,7 +485,7 @@ function prepareTools({ + } + break; + case "google.code_execution": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ codeExecution: {} }); + } else { + toolWarnings.push({ +@@ -499,7 +507,7 @@ function prepareTools({ + } + break; + case "google.vertex_rag_store": +- if (isGemini2) { ++ if (isGemini2OrNewer) { + googleTools2.push({ + retrieval: { + vertex_rag_store: { +@@ -1434,9 +1442,7 @@ var googleTools = { + vertexRagStore + }; + export { +- GoogleGenerativeAILanguageModel, + getGroundingMetadataSchema, +- getUrlContextMetadataSchema, +- googleTools ++ getUrlContextMetadataSchema, GoogleGenerativeAILanguageModel, googleTools + }; + //# sourceMappingURL=index.mjs.map +\ No newline at end of file diff --git a/package.json b/package.json index ea14d7d5f5..ceb0cbf3ac 100644 --- a/package.json +++ b/package.json @@ -74,9 +74,10 @@ "format:check": "biome format && biome lint", "prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky", "claude": "dotenv -e .env -- claude", - "release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public", - "release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public", - "release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public" + "release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --preid alpha --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public", + "release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --preid beta --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public", + "release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --access public", + "release:ai-sdk-provider": "yarn workspace @cherrystudio/ai-sdk-provider version patch --immediate && yarn workspace @cherrystudio/ai-sdk-provider build && yarn workspace @cherrystudio/ai-sdk-provider npm publish --access public" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch", @@ -85,6 +86,7 @@ "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch", "@paymoapp/electron-shutdown-handler": "^1.1.2", "@strongtz/win32-arm64-msvc": "^0.4.7", + "emoji-picker-element-data": "^1", "express": "^5.1.0", "font-list": "^2.0.0", "graceful-fs": "^4.2.11", @@ -111,10 +113,11 @@ "@ai-sdk/anthropic": "^2.0.44", "@ai-sdk/cerebras": "^1.0.31", "@ai-sdk/gateway": "^2.0.9", - "@ai-sdk/google": "^2.0.32", - "@ai-sdk/google-vertex": "^3.0.62", + "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch", + "@ai-sdk/google-vertex": "^3.0.68", "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch", "@ai-sdk/mistral": "^2.0.23", + "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", "@ai-sdk/perplexity": "^2.0.17", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@anthropic-ai/sdk": "^0.41.0", @@ -123,7 +126,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.910.0", "@aws-sdk/client-s3": "^3.910.0", "@biomejs/biome": "2.2.4", - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18", + "@cherrystudio/ai-core": "workspace:^1.0.9", "@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31", @@ -410,7 +413,7 @@ "@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", "@ai-sdk/openai@npm:2.0.64": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", "@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", - "@ai-sdk/google@npm:2.0.31": "patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch" + "@ai-sdk/google@npm:2.0.36": "patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/ai-sdk-provider/package.json b/packages/ai-sdk-provider/package.json index fd0aac2643..bf509ee963 100644 --- a/packages/ai-sdk-provider/package.json +++ b/packages/ai-sdk-provider/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-sdk-provider", - "version": "0.1.0", + "version": "0.1.2", "description": "Cherry Studio AI SDK provider bundle with CherryIN routing.", "keywords": [ "ai-sdk", diff --git a/packages/aiCore/README.md b/packages/aiCore/README.md index 4ca5ea6640..1380019094 100644 --- a/packages/aiCore/README.md +++ b/packages/aiCore/README.md @@ -71,7 +71,7 @@ Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口 ## 安装 ```bash -npm install @cherrystudio/ai-core ai +npm install @cherrystudio/ai-core ai @ai-sdk/google @ai-sdk/openai ``` ### React Native diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index bb673392a2..fbbea52d40 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-core", - "version": "1.0.1", + "version": "1.0.9", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "main": "dist/index.js", "module": "dist/index.mjs", @@ -33,19 +33,19 @@ }, "homepage": "https://github.com/CherryHQ/cherry-studio#readme", "peerDependencies": { + "@ai-sdk/google": "^2.0.36", + "@ai-sdk/openai": "^2.0.64", + "@cherrystudio/ai-sdk-provider": "^0.1.2", "ai": "^5.0.26" }, "dependencies": { "@ai-sdk/anthropic": "^2.0.43", "@ai-sdk/azure": "^2.0.66", "@ai-sdk/deepseek": "^1.0.27", - "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch", - "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", "@ai-sdk/openai-compatible": "^1.0.26", "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.16", "@ai-sdk/xai": "^2.0.31", - "@cherrystudio/ai-sdk-provider": "workspace:*", "zod": "^4.1.5" }, "devDependencies": { diff --git a/packages/aiCore/src/core/plugins/built-in/index.ts b/packages/aiCore/src/core/plugins/built-in/index.ts index 1f8916b09a..d7f35d0cd1 100644 --- a/packages/aiCore/src/core/plugins/built-in/index.ts +++ b/packages/aiCore/src/core/plugins/built-in/index.ts @@ -4,12 +4,7 @@ */ export const BUILT_IN_PLUGIN_PREFIX = 'built-in:' -export { googleToolsPlugin } from './googleToolsPlugin' -export { createLoggingPlugin } from './logging' -export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin' -export type { - PromptToolUseConfig, - ToolUseRequestContext, - ToolUseResult -} from './toolUsePlugin/type' -export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin' +export * from './googleToolsPlugin' +export * from './toolUsePlugin/promptToolUsePlugin' +export * from './toolUsePlugin/type' +export * from './webSearchPlugin' diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts index 23ea952323..75692cdf36 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts @@ -32,7 +32,7 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR }) // 导出类型定义供开发者使用 -export type { WebSearchPluginConfig, WebSearchToolOutputSchema } from './helper' +export * from './helper' // 默认导出 export default webSearchPlugin diff --git a/packages/aiCore/src/core/providers/index.ts b/packages/aiCore/src/core/providers/index.ts index 3ac445cb22..b9ebd6f682 100644 --- a/packages/aiCore/src/core/providers/index.ts +++ b/packages/aiCore/src/core/providers/index.ts @@ -44,7 +44,7 @@ export { // ==================== 基础数据和类型 ==================== // 基础Provider数据源 -export { baseProviderIds, baseProviders } from './schemas' +export { baseProviderIds, baseProviders, isBaseProvider } from './schemas' // 类型定义和Schema export type { diff --git a/packages/aiCore/src/core/providers/schemas.ts b/packages/aiCore/src/core/providers/schemas.ts index 778b1b705a..43a370af9b 100644 --- a/packages/aiCore/src/core/providers/schemas.ts +++ b/packages/aiCore/src/core/providers/schemas.ts @@ -7,7 +7,6 @@ import { createAzure } from '@ai-sdk/azure' import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure' import { createDeepSeek } from '@ai-sdk/deepseek' import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { createHuggingFace } from '@ai-sdk/huggingface' import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import type { LanguageModelV2 } from '@ai-sdk/provider' @@ -33,8 +32,7 @@ export const baseProviderIds = [ 'deepseek', 'openrouter', 'cherryin', - 'cherryin-chat', - 'huggingface' + 'cherryin-chat' ] as const /** @@ -158,12 +156,6 @@ export const baseProviders = [ }) }, supportsImageGeneration: true - }, - { - id: 'huggingface', - name: 'HuggingFace', - creator: createHuggingFace, - supportsImageGeneration: true } ] as const satisfies BaseProvider[] diff --git a/src/main/apiServer/routes/models.ts b/src/main/apiServer/routes/models.ts index 8481e1ea59..d776d5ea91 100644 --- a/src/main/apiServer/routes/models.ts +++ b/src/main/apiServer/routes/models.ts @@ -104,12 +104,6 @@ const router = express logger.warn('No models available from providers', { filter }) } - logger.info('Models response ready', { - filter, - total: response.total, - modelIds: response.data.map((m) => m.id) - }) - return res.json(response satisfies ApiModelsResponse) } catch (error: any) { logger.error('Error fetching models', { error }) diff --git a/src/main/apiServer/services/models.ts b/src/main/apiServer/services/models.ts index a32d6d37dc..52f0db857f 100644 --- a/src/main/apiServer/services/models.ts +++ b/src/main/apiServer/services/models.ts @@ -32,7 +32,7 @@ export class ModelsService { for (const model of models) { const provider = providers.find((p) => p.id === model.provider) - logger.debug(`Processing model ${model.id}`) + // logger.debug(`Processing model ${model.id}`) if (!provider) { logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`) continue diff --git a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts index 8f8c1df038..2565f5e605 100644 --- a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts +++ b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts @@ -21,11 +21,16 @@ describe('stripLocalCommandTags', () => { 'line1\nkeep\nError' expect(stripLocalCommandTags(input)).toBe('line1\nkeep\nError') }) + + it('if no tags present, returns original string', () => { + const input = 'just some normal text' + expect(stripLocalCommandTags(input)).toBe(input) + }) }) describe('Claude → AiSDK transform', () => { it('handles tool call streaming lifecycle', () => { - const state = new ClaudeStreamState() + const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id }) const parts: ReturnType[number][] = [] const messages: SDKMessage[] = [ @@ -182,14 +187,119 @@ describe('Claude → AiSDK transform', () => { (typeof parts)[number], { type: 'tool-result' } > - expect(toolResult.toolCallId).toBe('tool-1') + expect(toolResult.toolCallId).toBe('session-123:tool-1') expect(toolResult.toolName).toBe('Bash') expect(toolResult.input).toEqual({ command: 'ls' }) expect(toolResult.output).toBe('ok') }) + it('handles tool calls without streaming events (no content_block_start/stop)', () => { + const state = new ClaudeStreamState({ agentSessionId: '12344' }) + const parts: ReturnType[number][] = [] + + const messages: SDKMessage[] = [ + { + ...baseStreamMetadata, + type: 'assistant', + uuid: uuid(20), + message: { + id: 'msg-tool-no-stream', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [ + { + type: 'tool_use', + id: 'tool-read', + name: 'Read', + input: { file_path: '/test.txt' } + }, + { + type: 'tool_use', + id: 'tool-bash', + name: 'Bash', + input: { command: 'ls -la' } + } + ], + stop_reason: 'tool_use', + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20 + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'user', + uuid: uuid(21), + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-read', + content: 'file contents', + is_error: false + } + ] + } + } as SDKMessage, + { + ...baseStreamMetadata, + type: 'user', + uuid: uuid(22), + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-bash', + content: 'total 42\n...', + is_error: false + } + ] + } + } as SDKMessage + ] + + for (const message of messages) { + const transformed = transformSDKMessageToStreamParts(message, state) + parts.push(...transformed) + } + + const types = parts.map((part) => part.type) + expect(types).toEqual(['tool-call', 'tool-call', 'tool-result', 'tool-result']) + + const toolCalls = parts.filter((part) => part.type === 'tool-call') as Extract< + (typeof parts)[number], + { type: 'tool-call' } + >[] + expect(toolCalls).toHaveLength(2) + expect(toolCalls[0].toolName).toBe('Read') + expect(toolCalls[0].toolCallId).toBe('12344:tool-read') + expect(toolCalls[1].toolName).toBe('Bash') + expect(toolCalls[1].toolCallId).toBe('12344:tool-bash') + + const toolResults = parts.filter((part) => part.type === 'tool-result') as Extract< + (typeof parts)[number], + { type: 'tool-result' } + >[] + expect(toolResults).toHaveLength(2) + // This is the key assertion - toolName should NOT be 'unknown' + expect(toolResults[0].toolName).toBe('Read') + expect(toolResults[0].toolCallId).toBe('12344:tool-read') + expect(toolResults[0].input).toEqual({ file_path: '/test.txt' }) + expect(toolResults[0].output).toBe('file contents') + + expect(toolResults[1].toolName).toBe('Bash') + expect(toolResults[1].toolCallId).toBe('12344:tool-bash') + expect(toolResults[1].input).toEqual({ command: 'ls -la' }) + expect(toolResults[1].output).toBe('total 42\n...') + }) + it('handles streaming text completion', () => { - const state = new ClaudeStreamState() + const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id }) const parts: ReturnType[number][] = [] const messages: SDKMessage[] = [ @@ -300,4 +410,87 @@ describe('Claude → AiSDK transform', () => { expect(finishStep.finishReason).toBe('stop') expect(finishStep.usage).toEqual({ inputTokens: 2, outputTokens: 4, totalTokens: 6 }) }) + + it('emits fallback text when Claude sends a snapshot instead of deltas', () => { + const state = new ClaudeStreamState({ agentSessionId: '12344' }) + const parts: ReturnType[number][] = [] + + const messages: SDKMessage[] = [ + { + ...baseStreamMetadata, + type: 'stream_event', + uuid: uuid(30), + event: { + type: 'message_start', + message: { + id: 'msg-fallback', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [], + stop_reason: null, + stop_sequence: null, + usage: {} + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'stream_event', + uuid: uuid(31), + event: { + type: 'content_block_start', + index: 0, + content_block: { + type: 'text', + text: '' + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'assistant', + uuid: uuid(32), + message: { + id: 'msg-fallback-content', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [ + { + type: 'text', + text: 'Final answer without streaming deltas.' + } + ], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 3, + output_tokens: 7 + } + } + } as unknown as SDKMessage + ] + + for (const message of messages) { + const transformed = transformSDKMessageToStreamParts(message, state) + parts.push(...transformed) + } + + const types = parts.map((part) => part.type) + expect(types).toEqual(['start-step', 'text-start', 'text-delta', 'text-end', 'finish-step']) + + const delta = parts.find((part) => part.type === 'text-delta') as Extract< + (typeof parts)[number], + { type: 'text-delta' } + > + expect(delta.text).toBe('Final answer without streaming deltas.') + + const finish = parts.find((part) => part.type === 'finish-step') as Extract< + (typeof parts)[number], + { type: 'finish-step' } + > + expect(finish.usage).toEqual({ inputTokens: 3, outputTokens: 7, totalTokens: 10 }) + expect(finish.finishReason).toBe('stop') + }) }) diff --git a/src/main/services/agents/services/claudecode/claude-stream-state.ts b/src/main/services/agents/services/claudecode/claude-stream-state.ts index 078f048ce8..30b5790c82 100644 --- a/src/main/services/agents/services/claudecode/claude-stream-state.ts +++ b/src/main/services/agents/services/claudecode/claude-stream-state.ts @@ -10,8 +10,21 @@ * Every Claude turn gets its own instance. `resetStep` should be invoked once the finish event has * been emitted to avoid leaking state into the next turn. */ +import { loggerService } from '@logger' import type { FinishReason, LanguageModelUsage, ProviderMetadata } from 'ai' +/** + * Builds a namespaced tool call ID by combining session ID with raw tool call ID. + * This ensures tool calls from different sessions don't conflict even if they have + * the same raw ID from the SDK. + * + * @param sessionId - The agent session ID + * @param rawToolCallId - The raw tool call ID from SDK (e.g., "WebFetch_0") + */ +export function buildNamespacedToolCallId(sessionId: string, rawToolCallId: string): string { + return `${sessionId}:${rawToolCallId}` +} + /** * Shared fields for every block that Claude can stream (text, reasoning, tool). */ @@ -34,6 +47,7 @@ type ReasoningBlockState = BaseBlockState & { type ToolBlockState = BaseBlockState & { kind: 'tool' toolCallId: string + rawToolCallId: string toolName: string inputBuffer: string providerMetadata?: ProviderMetadata @@ -48,12 +62,17 @@ type PendingUsageState = { } type PendingToolCall = { + rawToolCallId: string toolCallId: string toolName: string input: unknown providerMetadata?: ProviderMetadata } +type ClaudeStreamStateOptions = { + agentSessionId: string +} + /** * Tracks the lifecycle of Claude streaming blocks (text, thinking, tool calls) * across individual websocket events. The transformer relies on this class to @@ -61,12 +80,20 @@ type PendingToolCall = { * usage/finish metadata once Anthropic closes a message. */ export class ClaudeStreamState { + private logger + private readonly agentSessionId: string private blocksByIndex = new Map() - private toolIndexById = new Map() + private toolIndexByNamespacedId = new Map() private pendingUsage: PendingUsageState = {} private pendingToolCalls = new Map() private stepActive = false + constructor(options: ClaudeStreamStateOptions) { + this.logger = loggerService.withContext('ClaudeStreamState') + this.agentSessionId = options.agentSessionId + this.logger.silly('ClaudeStreamState', options) + } + /** Marks the beginning of a new AiSDK step. */ beginStep(): void { this.stepActive = true @@ -104,19 +131,21 @@ export class ClaudeStreamState { /** Caches tool metadata so subsequent input deltas and results can find it. */ openToolBlock( index: number, - params: { toolCallId: string; toolName: string; providerMetadata?: ProviderMetadata } + params: { rawToolCallId: string; toolName: string; providerMetadata?: ProviderMetadata } ): ToolBlockState { + const toolCallId = buildNamespacedToolCallId(this.agentSessionId, params.rawToolCallId) const block: ToolBlockState = { kind: 'tool', - id: params.toolCallId, + id: toolCallId, index, - toolCallId: params.toolCallId, + toolCallId, + rawToolCallId: params.rawToolCallId, toolName: params.toolName, inputBuffer: '', providerMetadata: params.providerMetadata } this.blocksByIndex.set(index, block) - this.toolIndexById.set(params.toolCallId, index) + this.toolIndexByNamespacedId.set(toolCallId, index) return block } @@ -124,14 +153,32 @@ export class ClaudeStreamState { return this.blocksByIndex.get(index) } + getFirstOpenTextBlock(): TextBlockState | undefined { + const candidates: TextBlockState[] = [] + for (const block of this.blocksByIndex.values()) { + if (block.kind === 'text') { + candidates.push(block) + } + } + if (candidates.length === 0) { + return undefined + } + candidates.sort((a, b) => a.index - b.index) + return candidates[0] + } + getToolBlockById(toolCallId: string): ToolBlockState | undefined { - const index = this.toolIndexById.get(toolCallId) + const index = this.toolIndexByNamespacedId.get(toolCallId) if (index === undefined) return undefined const block = this.blocksByIndex.get(index) if (!block || block.kind !== 'tool') return undefined return block } + getToolBlockByRawId(rawToolCallId: string): ToolBlockState | undefined { + return this.getToolBlockById(buildNamespacedToolCallId(this.agentSessionId, rawToolCallId)) + } + /** Appends streamed text to a text block, returning the updated state when present. */ appendTextDelta(index: number, text: string): TextBlockState | undefined { const block = this.blocksByIndex.get(index) @@ -158,10 +205,12 @@ export class ClaudeStreamState { /** Records a tool call to be consumed once its result arrives from the user. */ registerToolCall( - toolCallId: string, + rawToolCallId: string, payload: { toolName: string; input: unknown; providerMetadata?: ProviderMetadata } ): void { - this.pendingToolCalls.set(toolCallId, { + const toolCallId = buildNamespacedToolCallId(this.agentSessionId, rawToolCallId) + this.pendingToolCalls.set(rawToolCallId, { + rawToolCallId, toolCallId, toolName: payload.toolName, input: payload.input, @@ -170,10 +219,10 @@ export class ClaudeStreamState { } /** Retrieves and clears the buffered tool call metadata for the given id. */ - consumePendingToolCall(toolCallId: string): PendingToolCall | undefined { - const entry = this.pendingToolCalls.get(toolCallId) + consumePendingToolCall(rawToolCallId: string): PendingToolCall | undefined { + const entry = this.pendingToolCalls.get(rawToolCallId) if (entry) { - this.pendingToolCalls.delete(toolCallId) + this.pendingToolCalls.delete(rawToolCallId) } return entry } @@ -182,13 +231,13 @@ export class ClaudeStreamState { * Persists the final input payload for a tool block once the provider signals * completion so that downstream tool results can reference the original call. */ - completeToolBlock(toolCallId: string, input: unknown, providerMetadata?: ProviderMetadata): void { + completeToolBlock(toolCallId: string, toolName: string, input: unknown, providerMetadata?: ProviderMetadata): void { + const block = this.getToolBlockByRawId(toolCallId) this.registerToolCall(toolCallId, { - toolName: this.getToolBlockById(toolCallId)?.toolName ?? 'unknown', + toolName, input, providerMetadata }) - const block = this.getToolBlockById(toolCallId) if (block) { block.resolvedInput = input } @@ -200,7 +249,7 @@ export class ClaudeStreamState { if (!block) return undefined this.blocksByIndex.delete(index) if (block.kind === 'tool') { - this.toolIndexById.delete(block.toolCallId) + this.toolIndexByNamespacedId.delete(block.toolCallId) } return block } @@ -227,7 +276,7 @@ export class ClaudeStreamState { /** Drops cached block metadata for the currently active message. */ resetBlocks(): void { this.blocksByIndex.clear() - this.toolIndexById.clear() + this.toolIndexByNamespacedId.clear() } /** Resets the entire step lifecycle after emitting a terminal frame. */ @@ -236,6 +285,10 @@ export class ClaudeStreamState { this.resetPendingUsage() this.stepActive = false } + + getNamespacedToolCallId(rawToolCallId: string): string { + return buildNamespacedToolCallId(this.agentSessionId, rawToolCallId) + } } export type { PendingToolCall } diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 6405ae94ef..f912539550 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -19,6 +19,7 @@ import { app } from 'electron' import { agentService, type GetAgentSessionResponse } from '../..' import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface' import { sessionService } from '../SessionService' +import { buildNamespacedToolCallId } from './claude-stream-state' import { promptForToolApproval } from './tool-permissions' import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform' @@ -156,7 +157,10 @@ class ClaudeCodeService implements AgentServiceInterface { return { behavior: 'allow', updatedInput: input } } - return promptForToolApproval(toolName, input, options) + return promptForToolApproval(toolName, input, { + ...options, + toolCallId: buildNamespacedToolCallId(session.id, options.toolUseID) + }) } const subAgents: Record = {} @@ -378,7 +382,7 @@ class ClaudeCodeService implements AgentServiceInterface { const jsonOutput: SDKMessage[] = [] let hasCompleted = false const startTime = Date.now() - const streamState = new ClaudeStreamState() + const streamState = new ClaudeStreamState({ agentSessionId: sessionId }) try { for await (const message of query({ prompt: promptStream, options })) { @@ -442,23 +446,6 @@ class ClaudeCodeService implements AgentServiceInterface { } } - if (message.type === 'assistant' || message.type === 'user') { - logger.silly('claude response', { - message, - content: JSON.stringify(message.message.content) - }) - } else if (message.type === 'stream_event') { - // logger.silly('Claude stream event', { - // message, - // event: JSON.stringify(message.event) - // }) - } else { - logger.silly('Claude response', { - message, - event: JSON.stringify(message) - }) - } - const chunks = transformSDKMessageToStreamParts(message, streamState) for (const chunk of chunks) { stream.emit('data', { diff --git a/src/main/services/agents/services/claudecode/tool-permissions.ts b/src/main/services/agents/services/claudecode/tool-permissions.ts index c95f4c679e..5b50f4567e 100644 --- a/src/main/services/agents/services/claudecode/tool-permissions.ts +++ b/src/main/services/agents/services/claudecode/tool-permissions.ts @@ -37,6 +37,7 @@ type RendererPermissionRequestPayload = { requestId: string toolName: string toolId: string + toolCallId: string description?: string requiresPermissions: boolean input: Record @@ -206,10 +207,19 @@ const ensureIpcHandlersRegistered = () => { }) } +type PromptForToolApprovalOptions = { + signal: AbortSignal + suggestions?: PermissionUpdate[] + + // NOTICE: This ID is namespaced with session ID, not the raw SDK tool call ID. + // Format: `${sessionId}:${rawToolCallId}`, e.g., `session_123:WebFetch_0` + toolCallId: string +} + export async function promptForToolApproval( toolName: string, input: Record, - options?: { signal: AbortSignal; suggestions?: PermissionUpdate[] } + options: PromptForToolApprovalOptions ): Promise { if (shouldAutoApproveTools) { logger.debug('promptForToolApproval auto-approving tool for test', { @@ -245,6 +255,7 @@ export async function promptForToolApproval( logger.info('Requesting user approval for tool usage', { requestId, toolName, + toolCallId: options.toolCallId, description: toolMetadata?.description }) @@ -252,6 +263,7 @@ export async function promptForToolApproval( requestId, toolName, toolId: toolMetadata?.id ?? toolName, + toolCallId: options.toolCallId, description: toolMetadata?.description, requiresPermissions: toolMetadata?.requirePermissions ?? false, input: sanitizedInput, @@ -266,6 +278,7 @@ export async function promptForToolApproval( logger.debug('Registering tool permission request', { requestId, toolName, + toolCallId: options.toolCallId, requiresPermissions: requestPayload.requiresPermissions, timeoutMs: TOOL_APPROVAL_TIMEOUT_MS, suggestionCount: sanitizedSuggestions.length @@ -273,7 +286,11 @@ export async function promptForToolApproval( return new Promise((resolve) => { const timeout = setTimeout(() => { - logger.info('User tool permission request timed out', { requestId, toolName }) + logger.info('User tool permission request timed out', { + requestId, + toolName, + toolCallId: options.toolCallId + }) finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout') }, TOOL_APPROVAL_TIMEOUT_MS) @@ -287,7 +304,11 @@ export async function promptForToolApproval( if (options?.signal) { const abortListener = () => { - logger.info('Tool permission request aborted before user responded', { requestId, toolName }) + logger.info('Tool permission request aborted before user responded', { + requestId, + toolName, + toolCallId: options.toolCallId + }) finalizeRequest(requestId, defaultDenyUpdate, 'aborted') } diff --git a/src/main/services/agents/services/claudecode/transform.ts b/src/main/services/agents/services/claudecode/transform.ts index 41285175b4..00be683ba8 100644 --- a/src/main/services/agents/services/claudecode/transform.ts +++ b/src/main/services/agents/services/claudecode/transform.ts @@ -110,7 +110,7 @@ const sdkMessageToProviderMetadata = (message: SDKMessage): ProviderMetadata => * blocks across calls so that incremental deltas can be correlated correctly. */ export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage, state: ClaudeStreamState): AgentStreamPart[] { - logger.silly('Transforming SDKMessage', { message: sdkMessage }) + logger.silly('Transforming SDKMessage', { message: JSON.stringify(sdkMessage) }) switch (sdkMessage.type) { case 'assistant': return handleAssistantMessage(sdkMessage, state) @@ -186,14 +186,13 @@ function handleAssistantMessage( for (const block of content) { switch (block.type) { - case 'text': - if (!isStreamingActive) { - const sanitizedText = stripLocalCommandTags(block.text) - if (sanitizedText) { - textBlocks.push(sanitizedText) - } + case 'text': { + const sanitizedText = stripLocalCommandTags(block.text) + if (sanitizedText) { + textBlocks.push(sanitizedText) } break + } case 'tool_use': handleAssistantToolUse(block as ToolUseContent, providerMetadata, state, chunks) break @@ -203,7 +202,16 @@ function handleAssistantMessage( } } - if (!isStreamingActive && textBlocks.length > 0) { + if (textBlocks.length === 0) { + return chunks + } + + const combinedText = textBlocks.join('') + if (!combinedText) { + return chunks + } + + if (!isStreamingActive) { const id = message.uuid?.toString() || generateMessageId() state.beginStep() chunks.push({ @@ -219,7 +227,7 @@ function handleAssistantMessage( chunks.push({ type: 'text-delta', id, - text: textBlocks.join(''), + text: combinedText, providerMetadata }) chunks.push({ @@ -230,7 +238,27 @@ function handleAssistantMessage( return finalizeNonStreamingStep(message, state, chunks) } - return chunks + const existingTextBlock = state.getFirstOpenTextBlock() + const fallbackId = existingTextBlock?.id || message.uuid?.toString() || generateMessageId() + if (!existingTextBlock) { + chunks.push({ + type: 'text-start', + id: fallbackId, + providerMetadata + }) + } + chunks.push({ + type: 'text-delta', + id: fallbackId, + text: combinedText, + providerMetadata + }) + chunks.push({ + type: 'text-end', + id: fallbackId, + providerMetadata + }) + return finalizeNonStreamingStep(message, state, chunks) } /** @@ -243,15 +271,16 @@ function handleAssistantToolUse( state: ClaudeStreamState, chunks: AgentStreamPart[] ): void { + const toolCallId = state.getNamespacedToolCallId(block.id) chunks.push({ type: 'tool-call', - toolCallId: block.id, + toolCallId, toolName: block.name, input: block.input, providerExecuted: true, providerMetadata }) - state.completeToolBlock(block.id, block.input, providerMetadata) + state.completeToolBlock(block.id, block.name, block.input, providerMetadata) } /** @@ -331,10 +360,11 @@ function handleUserMessage( if (block.type === 'tool_result') { const toolResult = block as ToolResultContent const pendingCall = state.consumePendingToolCall(toolResult.tool_use_id) + const toolCallId = pendingCall?.toolCallId ?? state.getNamespacedToolCallId(toolResult.tool_use_id) if (toolResult.is_error) { chunks.push({ type: 'tool-error', - toolCallId: toolResult.tool_use_id, + toolCallId, toolName: pendingCall?.toolName ?? 'unknown', input: pendingCall?.input, error: toolResult.content, @@ -343,7 +373,7 @@ function handleUserMessage( } else { chunks.push({ type: 'tool-result', - toolCallId: toolResult.tool_use_id, + toolCallId, toolName: pendingCall?.toolName ?? 'unknown', input: pendingCall?.input, output: toolResult.content, @@ -457,6 +487,9 @@ function handleStreamEvent( } case 'message_stop': { + if (!state.hasActiveStep()) { + break + } const pending = state.getPendingUsage() chunks.push({ type: 'finish-step', @@ -514,7 +547,7 @@ function handleContentBlockStart( } case 'tool_use': { const block = state.openToolBlock(index, { - toolCallId: contentBlock.id, + rawToolCallId: contentBlock.id, toolName: contentBlock.name, providerMetadata }) diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 128a0f5269..7f4cd33608 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -99,9 +99,6 @@ export function buildProviderOptions( serviceTier: serviceTierSetting } break - case 'huggingface': - providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities) - break case 'anthropic': providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities) break @@ -144,6 +141,9 @@ export function buildProviderOptions( case 'bedrock': providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities) break + case 'huggingface': + providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities) + break default: // 对于其他 provider,使用通用的构建逻辑 providerSpecificOptions = { @@ -162,13 +162,17 @@ export function buildProviderOptions( ...getCustomParameters(assistant) } - const rawProviderKey = + let rawProviderKey = { 'google-vertex': 'google', 'google-vertex-anthropic': 'anthropic', 'ai-gateway': 'gateway' }[rawProviderId] || rawProviderId + if (rawProviderKey === 'cherryin') { + rawProviderKey = { gemini: 'google' }[actualProvider.type] || actualProvider.type + } + // 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions } return { [rawProviderKey]: providerSpecificOptions diff --git a/src/renderer/src/components/EmojiPicker/index.tsx b/src/renderer/src/components/EmojiPicker/index.tsx index 8ba9d3e967..9a4158d469 100644 --- a/src/renderer/src/components/EmojiPicker/index.tsx +++ b/src/renderer/src/components/EmojiPicker/index.tsx @@ -1,35 +1,120 @@ +import 'emoji-picker-element' + import TwemojiCountryFlagsWoff2 from '@renderer/assets/fonts/country-flag-fonts/TwemojiCountryFlags.woff2?url' import { useTheme } from '@renderer/context/ThemeProvider' +import type { LanguageVarious } from '@renderer/types' import { polyfillCountryFlagEmojis } from 'country-flag-emoji-polyfill' +// i18n translations from emoji-picker-element +import de from 'emoji-picker-element/i18n/de' +import en from 'emoji-picker-element/i18n/en' +import es from 'emoji-picker-element/i18n/es' +import fr from 'emoji-picker-element/i18n/fr' +import ja from 'emoji-picker-element/i18n/ja' +import pt_PT from 'emoji-picker-element/i18n/pt_PT' +import ru_RU from 'emoji-picker-element/i18n/ru_RU' +import zh_CN from 'emoji-picker-element/i18n/zh_CN' +import type Picker from 'emoji-picker-element/picker' +import type { EmojiClickEvent, NativeEmoji } from 'emoji-picker-element/shared' +// Emoji data from emoji-picker-element-data (local, no CDN) +// Using CLDR format for full multi-language search support (28 languages) +import dataDE from 'emoji-picker-element-data/de/cldr/data.json?url' +import dataEN from 'emoji-picker-element-data/en/cldr/data.json?url' +import dataES from 'emoji-picker-element-data/es/cldr/data.json?url' +import dataFR from 'emoji-picker-element-data/fr/cldr/data.json?url' +import dataJA from 'emoji-picker-element-data/ja/cldr/data.json?url' +import dataPT from 'emoji-picker-element-data/pt/cldr/data.json?url' +import dataRU from 'emoji-picker-element-data/ru/cldr/data.json?url' +import dataZH from 'emoji-picker-element-data/zh/cldr/data.json?url' +import dataZH_HANT from 'emoji-picker-element-data/zh-hant/cldr/data.json?url' import type { FC } from 'react' import { useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' interface Props { onEmojiClick: (emoji: string) => void } +// Mapping from app locale to emoji-picker-element i18n +const i18nMap: Record = { + 'en-US': en, + 'zh-CN': zh_CN, + 'zh-TW': zh_CN, // Closest available + 'de-DE': de, + 'el-GR': en, // No Greek available, fallback to English + 'es-ES': es, + 'fr-FR': fr, + 'ja-JP': ja, + 'pt-PT': pt_PT, + 'ru-RU': ru_RU +} + +// Mapping from app locale to emoji data URL +// Using CLDR format provides native language search support for all locales +const dataSourceMap: Record = { + 'en-US': dataEN, + 'zh-CN': dataZH, + 'zh-TW': dataZH_HANT, + 'de-DE': dataDE, + 'el-GR': dataEN, // No Greek CLDR available, fallback to English + 'es-ES': dataES, + 'fr-FR': dataFR, + 'ja-JP': dataJA, + 'pt-PT': dataPT, + 'ru-RU': dataRU +} + +// Mapping from app locale to emoji-picker-element locale string +// Must match the data source locale for proper IndexedDB caching +const localeMap: Record = { + 'en-US': 'en', + 'zh-CN': 'zh', + 'zh-TW': 'zh-hant', + 'de-DE': 'de', + 'el-GR': 'en', + 'es-ES': 'es', + 'fr-FR': 'fr', + 'ja-JP': 'ja', + 'pt-PT': 'pt', + 'ru-RU': 'ru' +} + const EmojiPicker: FC = ({ onEmojiClick }) => { const { theme } = useTheme() - const ref = useRef(null) + const { i18n } = useTranslation() + const ref = useRef(null) + const currentLocale = i18n.language as LanguageVarious useEffect(() => { polyfillCountryFlagEmojis('Twemoji Mozilla', TwemojiCountryFlagsWoff2) }, []) + // Configure picker with i18n and dataSource useEffect(() => { - const refValue = ref.current + const picker = ref.current + if (picker) { + picker.i18n = i18nMap[currentLocale] || en + picker.dataSource = dataSourceMap[currentLocale] || dataEN + picker.locale = localeMap[currentLocale] || 'en' + } + }, [currentLocale]) - if (refValue) { - const handleEmojiClick = (event: any) => { + useEffect(() => { + const picker = ref.current + + if (picker) { + const handleEmojiClick = (event: EmojiClickEvent) => { event.stopPropagation() - onEmojiClick(event.detail.unicode || event.detail.emoji.unicode) + const { detail } = event + // Use detail.unicode (processed with skin tone) or fallback to emoji's unicode for native emoji + const unicode = detail.unicode || ('unicode' in detail.emoji ? (detail.emoji as NativeEmoji).unicode : '') + onEmojiClick(unicode) } // 添加事件监听器 - refValue.addEventListener('emoji-click', handleEmojiClick) + picker.addEventListener('emoji-click', handleEmojiClick) // 清理事件监听器 return () => { - refValue.removeEventListener('emoji-click', handleEmojiClick) + picker.removeEventListener('emoji-click', handleEmojiClick) } } return diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index d504699399..2574cbe669 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -1,5 +1,4 @@ import { loggerService } from '@logger' -import ClaudeIcon from '@renderer/assets/images/models/claude.png' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { TopView } from '@renderer/components/TopView' import { permissionModeCards } from '@renderer/config/agent' @@ -9,7 +8,6 @@ import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAg import type { AddAgentForm, AgentEntity, - AgentType, ApiModel, BaseAgentForm, PermissionMode, @@ -17,30 +15,22 @@ import type { UpdateAgentForm } from '@renderer/types' import { AgentConfigurationSchema, isAgentType } from '@renderer/types' -import { Avatar, Button, Input, Modal, Select } from 'antd' +import { Button, Input, Modal, Select } from 'antd' import { AlertTriangleIcon } from 'lucide-react' import type { ChangeEvent, FormEvent } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import type { BaseOption } from './shared' - const { TextArea } = Input const logger = loggerService.withContext('AddAgentPopup') -interface AgentTypeOption extends BaseOption { - type: 'type' - key: AgentEntity['type'] - name: AgentEntity['name'] -} - type AgentWithTools = AgentEntity & { tools?: Tool[] } const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({ type: existing?.type ?? 'claude-code', - name: existing?.name ?? 'Claude Code', + name: existing?.name ?? 'Agent', description: existing?.description, instructions: existing?.instructions, model: existing?.model ?? '', @@ -100,54 +90,6 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { }) }, []) - // add supported agents type here. - const agentConfig = useMemo( - () => - [ - { - type: 'type', - key: 'claude-code', - label: 'Claude Code', - name: 'Claude Code', - avatar: ClaudeIcon - } - ] as const satisfies AgentTypeOption[], - [] - ) - - const agentOptions = useMemo( - () => - agentConfig.map((option) => ({ - value: option.key, - label: ( - - - {option.label} - - ) - })), - [agentConfig] - ) - - const onAgentTypeChange = useCallback( - (value: AgentType) => { - const prevConfig = agentConfig.find((config) => config.key === form.type) - let newName: string | undefined = form.name - if (prevConfig && prevConfig.name === form.name) { - const newConfig = agentConfig.find((config) => config.key === value) - if (newConfig) { - newName = newConfig.name - } - } - setForm((prev) => ({ - ...prev, - type: value, - name: newName - })) - }, - [agentConfig, form.name, form.type] - ) - const onNameChange = useCallback((e: ChangeEvent) => { setForm((prev) => ({ ...prev, @@ -155,12 +97,12 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { })) }, []) - const onDescChange = useCallback((e: ChangeEvent) => { - setForm((prev) => ({ - ...prev, - description: e.target.value - })) - }, []) + // const onDescChange = useCallback((e: ChangeEvent) => { + // setForm((prev) => ({ + // ...prev, + // description: e.target.value + // })) + // }, []) const onInstChange = useCallback((e: ChangeEvent) => { setForm((prev) => ({ @@ -334,16 +276,6 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { - - {t('agent.type.label')} - - {t('common.name')} * @@ -363,7 +295,7 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { avatarSize={24} iconSize={16} buttonStyle={{ - padding: '8px 12px', + padding: '3px 8px', width: '100%', border: '1px solid var(--color-border)', borderRadius: 6, @@ -382,7 +314,6 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { onChange={onPermissionModeChange} style={{ width: '100%' }} placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')} - dropdownStyle={{ minWidth: '500px' }} optionLabelProp="label"> {permissionModeCards.map((item) => ( @@ -438,10 +369,10 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { - + {/* {t('common.description')} - - + + */} @@ -575,14 +506,7 @@ const FormFooter = styled.div` display: flex; justify-content: flex-end; gap: 8px; - padding-top: 16px; - border-top: 1px solid var(--color-border); -` - -const OptionWrapper = styled.div` - display: flex; - align-items: center; - gap: 8px; + padding: 10px; ` const PermissionOptionWrapper = styled.div` diff --git a/src/renderer/src/config/__test__/reasoning.test.ts b/src/renderer/src/config/__test__/reasoning.test.ts index ff66e76b6e..006fc79d49 100644 --- a/src/renderer/src/config/__test__/reasoning.test.ts +++ b/src/renderer/src/config/__test__/reasoning.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it, vi } from 'vitest' -import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning' +import { + isDoubaoSeedAfter251015, + isDoubaoThinkingAutoModel, + isGeminiReasoningModel, + isLingReasoningModel, + isSupportedThinkingTokenGeminiModel +} from '../models/reasoning' vi.mock('@renderer/store', () => ({ default: { @@ -231,3 +237,284 @@ describe('Ling Models', () => { }) }) }) + +describe('Gemini Models', () => { + describe('isSupportedThinkingTokenGeminiModel', () => { + it('should return true for gemini 2.5 models', () => { + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-flash-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-pro-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini latest models', () => { + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-flash-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-pro-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-flash-lite-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini 3 models', () => { + // Preview versions + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-pro-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'google/gemini-3-pro-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + // Future stable versions + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'google/gemini-3-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'google/gemini-3-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return false for image and tts models', () => { + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-flash-image', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-2.5-flash-preview-tts', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + }) + + it('should return false for older gemini models', () => { + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-1.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-1.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-1.0-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + }) + }) + + describe('isGeminiReasoningModel', () => { + it('should return true for gemini thinking models', () => { + expect( + isGeminiReasoningModel({ + id: 'gemini-2.0-flash-thinking', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'gemini-thinking-exp', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for supported thinking token gemini models', () => { + expect( + isGeminiReasoningModel({ + id: 'gemini-2.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'gemini-2.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini-3 models', () => { + // Preview versions + expect( + isGeminiReasoningModel({ + id: 'gemini-3-pro-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'google/gemini-3-pro-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + // Future stable versions + expect( + isGeminiReasoningModel({ + id: 'gemini-3-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'gemini-3-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'google/gemini-3-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isGeminiReasoningModel({ + id: 'google/gemini-3-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return false for older gemini models without thinking', () => { + expect( + isGeminiReasoningModel({ + id: 'gemini-1.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + expect( + isGeminiReasoningModel({ + id: 'gemini-1.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + }) + + it('should return false for undefined model', () => { + expect(isGeminiReasoningModel(undefined)).toBe(false) + }) + }) +}) diff --git a/src/renderer/src/config/__test__/vision.test.ts b/src/renderer/src/config/__test__/vision.test.ts new file mode 100644 index 0000000000..79bcd629c7 --- /dev/null +++ b/src/renderer/src/config/__test__/vision.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it, vi } from 'vitest' + +import { isVisionModel } from '../models/vision' + +vi.mock('@renderer/store', () => ({ + default: { + getState: () => ({ + llm: { + settings: {} + } + }) + } +})) + +// FIXME: Idk why it's imported. Maybe circular dependency somewhere +vi.mock('@renderer/services/AssistantService.ts', () => ({ + getDefaultAssistant: () => { + return { + id: 'default', + name: 'default', + emoji: '😀', + prompt: '', + topics: [], + messages: [], + type: 'assistant', + regularPhrases: [], + settings: {} + } + }, + getProviderByModel: () => null +})) + +describe('isVisionModel', () => { + describe('Gemini Models', () => { + it('should return true for gemini 1.5 models', () => { + expect( + isVisionModel({ + id: 'gemini-1.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-1.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini 2.x models', () => { + expect( + isVisionModel({ + id: 'gemini-2.0-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-2.0-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-2.5-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-2.5-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini latest models', () => { + expect( + isVisionModel({ + id: 'gemini-flash-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-pro-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-flash-lite-latest', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini 3 models', () => { + // Preview versions + expect( + isVisionModel({ + id: 'gemini-3-pro-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + // Future stable versions + expect( + isVisionModel({ + id: 'gemini-3-flash', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isVisionModel({ + id: 'gemini-3-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return true for gemini exp models', () => { + expect( + isVisionModel({ + id: 'gemini-exp-1206', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return false for gemini 1.0 models', () => { + expect( + isVisionModel({ + id: 'gemini-1.0-pro', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + }) + }) +}) diff --git a/src/renderer/src/config/__test__/websearch.test.ts b/src/renderer/src/config/__test__/websearch.test.ts new file mode 100644 index 0000000000..be18505a4c --- /dev/null +++ b/src/renderer/src/config/__test__/websearch.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest' + +import { GEMINI_SEARCH_REGEX } from '../models/websearch' + +vi.mock('@renderer/store', () => ({ + default: { + getState: () => ({ + llm: { + settings: {} + } + }) + } +})) + +// FIXME: Idk why it's imported. Maybe circular dependency somewhere +vi.mock('@renderer/services/AssistantService.ts', () => ({ + getDefaultAssistant: () => { + return { + id: 'default', + name: 'default', + emoji: '😀', + prompt: '', + topics: [], + messages: [], + type: 'assistant', + regularPhrases: [], + settings: {} + } + }, + getProviderByModel: () => null +})) + +describe('Gemini Search Models', () => { + describe('GEMINI_SEARCH_REGEX', () => { + it('should match gemini 2.x models', () => { + expect(GEMINI_SEARCH_REGEX.test('gemini-2.0-flash')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-2.0-pro')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-2.5-flash')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-2.5-pro')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-2.5-flash-latest')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-2.5-pro-latest')).toBe(true) + }) + + it('should match gemini latest models', () => { + expect(GEMINI_SEARCH_REGEX.test('gemini-flash-latest')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-pro-latest')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-flash-lite-latest')).toBe(true) + }) + + it('should match gemini 3 models', () => { + // Preview versions + expect(GEMINI_SEARCH_REGEX.test('gemini-3-pro-preview')).toBe(true) + // Future stable versions + expect(GEMINI_SEARCH_REGEX.test('gemini-3-flash')).toBe(true) + expect(GEMINI_SEARCH_REGEX.test('gemini-3-pro')).toBe(true) + }) + + it('should not match older gemini models', () => { + expect(GEMINI_SEARCH_REGEX.test('gemini-1.5-flash')).toBe(false) + expect(GEMINI_SEARCH_REGEX.test('gemini-1.5-pro')).toBe(false) + expect(GEMINI_SEARCH_REGEX.test('gemini-1.0-pro')).toBe(false) + }) + }) +}) diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index cc5449f819..0d4c652848 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -254,7 +254,7 @@ export function isGeminiReasoningModel(model?: Model): boolean { // Gemini 支持思考模式的模型正则 export const GEMINI_THINKING_MODEL_REGEX = - /gemini-(?:2\.5.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\w-]+)*$/i + /gemini-(?:2\.5.*(?:-latest)?|3-(?:flash|pro)(?:-preview)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\w-]+)*$/i export const isSupportedThinkingTokenGeminiModel = (model: Model): boolean => { const modelId = getLowerBaseModelName(model.id, '/') diff --git a/src/renderer/src/config/models/vision.ts b/src/renderer/src/config/models/vision.ts index 18b3480710..21d553d249 100644 --- a/src/renderer/src/config/models/vision.ts +++ b/src/renderer/src/config/models/vision.ts @@ -12,6 +12,7 @@ const visionAllowedModels = [ 'gemini-1\\.5', 'gemini-2\\.0', 'gemini-2\\.5', + 'gemini-3-(?:flash|pro)(?:-preview)?', 'gemini-(flash|pro|flash-lite)-latest', 'gemini-exp', 'claude-3', @@ -64,13 +65,13 @@ const visionExcludedModels = [ 'o1-preview', 'AIDC-AI/Marco-o1' ] -export const VISION_REGEX = new RegExp( +const VISION_REGEX = new RegExp( `\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`, 'i' ) // For middleware to identify models that must use the dedicated Image API -export const DEDICATED_IMAGE_MODELS = [ +const DEDICATED_IMAGE_MODELS = [ 'grok-2-image', 'grok-2-image-1212', 'grok-2-image-latest', @@ -79,7 +80,7 @@ export const DEDICATED_IMAGE_MODELS = [ 'gpt-image-1' ] -export const IMAGE_ENHANCEMENT_MODELS = [ +const IMAGE_ENHANCEMENT_MODELS = [ 'grok-2-image(?:-[\\w-]+)?', 'qwen-image-edit', 'gpt-image-1', @@ -90,9 +91,9 @@ export const IMAGE_ENHANCEMENT_MODELS = [ const IMAGE_ENHANCEMENT_MODELS_REGEX = new RegExp(IMAGE_ENHANCEMENT_MODELS.join('|'), 'i') // Models that should auto-enable image generation button when selected -export const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image', ...DEDICATED_IMAGE_MODELS] +const AUTO_ENABLE_IMAGE_MODELS = ['gemini-2.5-flash-image', ...DEDICATED_IMAGE_MODELS] -export const OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS = [ +const OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS = [ 'o3', 'gpt-4o', 'gpt-4o-mini', @@ -102,9 +103,9 @@ export const OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS = [ 'gpt-5' ] -export const OPENAI_IMAGE_GENERATION_MODELS = [...OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS, 'gpt-image-1'] +const OPENAI_IMAGE_GENERATION_MODELS = [...OPENAI_TOOL_USE_IMAGE_GENERATION_MODELS, 'gpt-image-1'] -export const GENERATE_IMAGE_MODELS = [ +const GENERATE_IMAGE_MODELS = [ 'gemini-2.0-flash-exp', 'gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-preview-image-generation', @@ -169,22 +170,23 @@ export function isPureGenerateImageModel(model: Model): boolean { } // Text to image models -export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus|midjourney|mj-|image|gpt-image/i +const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus|midjourney|mj-|image|gpt-image/i export function isTextToImageModel(model: Model): boolean { const modelId = getLowerBaseModelName(model.id) return TEXT_TO_IMAGE_REGEX.test(modelId) } -export function isNotSupportedImageSizeModel(model?: Model): boolean { - if (!model) { - return false - } +// It's not used now +// export function isNotSupportedImageSizeModel(model?: Model): boolean { +// if (!model) { +// return false +// } - const baseName = getLowerBaseModelName(model.id, '/') +// const baseName = getLowerBaseModelName(model.id, '/') - return baseName.includes('grok-2-image') -} +// return baseName.includes('grok-2-image') +// } /** * 判断模型是否支持图片增强(包括编辑、增强、修复等) diff --git a/src/renderer/src/config/models/websearch.ts b/src/renderer/src/config/models/websearch.ts index f012be7cfa..f7bca774b8 100644 --- a/src/renderer/src/config/models/websearch.ts +++ b/src/renderer/src/config/models/websearch.ts @@ -3,7 +3,13 @@ import type { Model } from '@renderer/types' import { SystemProviderIds } from '@renderer/types' import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils' -import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers' +import { + isGeminiProvider, + isNewApiProvider, + isOpenAICompatibleProvider, + isOpenAIProvider, + isVertexAiProvider +} from '../providers' import { isEmbeddingModel, isRerankModel } from './embedding' import { isAnthropicModel } from './utils' import { isPureGenerateImageModel, isTextToImageModel } from './vision' @@ -16,7 +22,7 @@ export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp( export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini.*-flash.*$') export const GEMINI_SEARCH_REGEX = new RegExp( - 'gemini-(?:2.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\\w-]+)*$', + 'gemini-(?:2.*(?:-latest)?|3-(?:flash|pro)(?:-preview)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\\w-]+)*$', 'i' ) @@ -107,7 +113,7 @@ export function isWebSearchModel(model: Model): boolean { } } - if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) { + if (isGeminiProvider(provider) || isVertexAiProvider(provider)) { return GEMINI_SEARCH_REGEX.test(modelId) } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 92ac37e9f1..b21721e719 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -686,7 +686,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = name: 'AI Gateway', type: 'ai-gateway', apiKey: '', - apiHost: 'https://ai-gateway.vercel.sh/v1', + apiHost: 'https://ai-gateway.vercel.sh/v1/ai', models: [], isSystem: true, enabled: false @@ -1571,6 +1571,10 @@ export function isGeminiProvider(provider: Provider): boolean { return provider.type === 'gemini' } +export function isVertexAiProvider(provider: Provider): boolean { + return provider.type === 'vertexai' +} + export function isAIGatewayProvider(provider: Provider): boolean { return provider.type === 'ai-gateway' } diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx index 71fa6307d2..b47bb3f64a 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx @@ -1,7 +1,6 @@ import type { CollapseProps } from 'antd' import { Tag } from 'antd' import { CheckCircle, Terminal, XCircle } from 'lucide-react' -import { useMemo } from 'react' import { ToolTitle } from './GenericTools' import type { BashOutputToolInput, BashOutputToolOutput } from './types' @@ -16,6 +15,63 @@ interface ParsedBashOutput { tool_use_error?: string } +const parseBashOutput = (output?: BashOutputToolOutput): ParsedBashOutput | null => { + if (!output) return null + + try { + const parser = new DOMParser() + const hasToolError = output.includes('') + const xmlStr = output.includes('') || hasToolError ? `${output}` : output + const xmlDoc = parser.parseFromString(xmlStr, 'application/xml') + const parserError = xmlDoc.querySelector('parsererror') + if (parserError) return null + + const getElementText = (tagName: string): string | undefined => { + const element = xmlDoc.getElementsByTagName(tagName)[0] + return element?.textContent?.trim() + } + + return { + status: getElementText('status'), + exit_code: getElementText('exit_code') ? parseInt(getElementText('exit_code')!) : undefined, + stdout: getElementText('stdout'), + stderr: getElementText('stderr'), + timestamp: getElementText('timestamp'), + tool_use_error: getElementText('tool_use_error') + } + } catch { + return null + } +} + +const getStatusConfig = (parsedOutput: ParsedBashOutput | null) => { + if (!parsedOutput) return null + + if (parsedOutput.tool_use_error) { + return { + color: 'danger', + icon: , + text: 'Error' + } as const + } + + const isCompleted = parsedOutput.status === 'completed' + const isSuccess = parsedOutput.exit_code === 0 + + return { + color: isCompleted && isSuccess ? 'success' : isCompleted && !isSuccess ? 'danger' : 'warning', + icon: + isCompleted && isSuccess ? ( + + ) : isCompleted && !isSuccess ? ( + + ) : ( + + ), + text: isCompleted ? (isSuccess ? 'Success' : 'Failed') : 'Running' + } as const +} + export function BashOutputTool({ input, output @@ -23,73 +79,8 @@ export function BashOutputTool({ input: BashOutputToolInput output?: BashOutputToolOutput }): NonNullable[number] { - // 解析 XML 输出 - const parsedOutput = useMemo(() => { - if (!output) return null - - try { - const parser = new DOMParser() - // 检查是否包含 tool_use_error 标签 - const hasToolError = output.includes('') - // 包装成有效的 XML(如果还没有根元素) - const xmlStr = output.includes('') || hasToolError ? `${output}` : output - const xmlDoc = parser.parseFromString(xmlStr, 'application/xml') - - // 检查是否有解析错误 - const parserError = xmlDoc.querySelector('parsererror') - if (parserError) { - return null - } - - const getElementText = (tagName: string): string | undefined => { - const element = xmlDoc.getElementsByTagName(tagName)[0] - return element?.textContent?.trim() - } - - const result: ParsedBashOutput = { - status: getElementText('status'), - exit_code: getElementText('exit_code') ? parseInt(getElementText('exit_code')!) : undefined, - stdout: getElementText('stdout'), - stderr: getElementText('stderr'), - timestamp: getElementText('timestamp'), - tool_use_error: getElementText('tool_use_error') - } - - return result - } catch { - return null - } - }, [output]) - - // 获取状态配置 - const statusConfig = useMemo(() => { - if (!parsedOutput) return null - - // 如果有 tool_use_error,直接显示错误状态 - if (parsedOutput.tool_use_error) { - return { - color: 'danger', - icon: , - text: 'Error' - } as const - } - - const isCompleted = parsedOutput.status === 'completed' - const isSuccess = parsedOutput.exit_code === 0 - - return { - color: isCompleted && isSuccess ? 'success' : isCompleted && !isSuccess ? 'danger' : 'warning', - icon: - isCompleted && isSuccess ? ( - - ) : isCompleted && !isSuccess ? ( - - ) : ( - - ), - text: isCompleted ? (isSuccess ? 'Success' : 'Failed') : 'Running' - } as const - }, [parsedOutput]) + const parsedOutput = parseBashOutput(output) + const statusConfig = getStatusConfig(parsedOutput) const children = parsedOutput ? ( diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx index 0b665a9fd4..043d8a94c4 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx @@ -1,12 +1,47 @@ import type { CollapseProps } from 'antd' import { FileText } from 'lucide-react' -import { useMemo } from 'react' import ReactMarkdown from 'react-markdown' import { ToolTitle } from './GenericTools' import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types' import { AgentToolsType } from './types' +const removeSystemReminderTags = (text: string): string => { + return text.replace(/[\s\S]*?<\/system-reminder>/gi, '') +} + +const normalizeOutputString = (output?: ReadToolOutputType): string | null => { + if (!output) return null + + const toText = (item: TextOutput) => removeSystemReminderTags(item.text) + + if (Array.isArray(output)) { + return output + .filter((item): item is TextOutput => item.type === 'text') + .map(toText) + .join('') + } + + return removeSystemReminderTags(output) +} + +const getOutputStats = (outputString: string | null) => { + if (!outputString) return null + + const bytes = new Blob([outputString]).size + const formatSize = (size: number) => { + if (size < 1024) return `${size} B` + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB` + return `${(size / (1024 * 1024)).toFixed(1)} MB` + } + + return { + lineCount: outputString.split('\n').length, + fileSize: bytes, + formatSize + } +} + export function ReadTool({ input, output @@ -14,50 +49,8 @@ export function ReadTool({ input: ReadToolInputType output?: ReadToolOutputType }): NonNullable[number] { - // 移除 system-reminder 标签及其内容的辅助函数 - const removeSystemReminderTags = (text: string): string => { - // 使用正则表达式匹配 标签及其内容,包括换行符 - return text.replace(/[\s\S]*?<\/system-reminder>/gi, '') - } - - // 将 output 统一转换为字符串 - const outputString = useMemo(() => { - if (!output) return null - - let processedOutput: string - - // 如果是 TextOutput[] 类型,提取所有 text 内容 - if (Array.isArray(output)) { - processedOutput = output - .filter((item): item is TextOutput => item.type === 'text') - .map((item) => removeSystemReminderTags(item.text)) - .join('') - } else { - // 如果是字符串,直接使用 - processedOutput = output - } - - // 移除 system-reminder 标签及其内容 - return removeSystemReminderTags(processedOutput) - }, [output]) - - // 如果有输出,计算统计信息 - const stats = useMemo(() => { - if (!outputString) return null - - const bytes = new Blob([outputString]).size - const formatSize = (bytes: number) => { - if (bytes < 1024) return `${bytes} B` - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` - return `${(bytes / (1024 * 1024)).toFixed(1)} MB` - } - - return { - lineCount: outputString.split('\n').length, - fileSize: bytes, - formatSize - } - }, [outputString]) + const outputString = normalizeOutputString(output) + const stats = getOutputStats(outputString) return { key: AgentToolsType.Read, diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx index 969eda9507..8a6965b6f6 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx @@ -11,11 +11,24 @@ interface UnknownToolProps { output?: unknown } -export function UnknownToolRenderer({ - toolName = '', - input, - output -}: UnknownToolProps): NonNullable[number] { +const getToolDisplayName = (name: string) => { + if (name.startsWith('mcp__')) { + const parts = name.substring(5).split('__') + if (parts.length >= 2) { + return `${parts[0]}:${parts.slice(1).join(':')}` + } + } + return name +} + +const getToolDescription = (toolName: string) => { + if (toolName.startsWith('mcp__')) { + return 'MCP Server Tool' + } + return 'Tool' +} + +const UnknownToolContent = ({ input, output }: { input?: unknown; output?: unknown }) => { const { highlightCode } = useCodeStyle() const [inputHtml, setInputHtml] = useState('') const [outputHtml, setOutputHtml] = useState('') @@ -34,58 +47,49 @@ export function UnknownToolRenderer({ } }, [output, highlightCode]) - const getToolDisplayName = (name: string) => { - if (name.startsWith('mcp__')) { - const parts = name.substring(5).split('__') - if (parts.length >= 2) { - return `${parts[0]}:${parts.slice(1).join(':')}` - } - } - return name + if (input === undefined && output === undefined) { + return No data available for this tool } - const getToolDescription = () => { - if (toolName.startsWith('mcp__')) { - return 'MCP Server Tool' - } - return 'Tool' - } + return ( + + {input !== undefined && ( + + Input: + + + )} + {output !== undefined && ( + + Output: + + + )} + + ) +} + +export function UnknownToolRenderer({ + toolName = '', + input, + output +}: UnknownToolProps): NonNullable[number] { return { key: 'unknown-tool', label: ( } label={getToolDisplayName(toolName)} - params={getToolDescription()} + params={getToolDescription(toolName)} /> ), - children: ( - - {input !== undefined && ( - - Input: - - - )} - - {output !== undefined && ( - - Output: - - - )} - - {input === undefined && output === undefined && ( - No data available for this tool - )} - - ) + children: } } diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx index 27abf7426c..42a1cf403b 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx @@ -6,8 +6,6 @@ import { Collapse } from 'antd' // 导出所有类型 export * from './types' -import { useMemo } from 'react' - // 导入所有渲染器 import ToolPermissionRequestCard from '../ToolPermissionRequestCard' import { BashOutputTool } from './BashOutputTool' @@ -57,22 +55,19 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools return typeof toolName === 'string' && Object.values(AgentToolsType).includes(toolName as AgentToolsType) } -// 统一的渲染函数 -function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) { +// 统一的渲染组件 +function ToolContent({ toolName, input, output }: { toolName: AgentToolsType; input: ToolInput; output?: ToolOutput }) { const Renderer = toolRenderers[toolName] + const renderedItem = Renderer + ? Renderer({ input: input as any, output: output as any }) + : UnknownToolRenderer({ input: input as any, output: output as any, toolName }) - // eslint-disable-next-line react-hooks/rules-of-hooks - const toolContentItem = useMemo(() => { - const rendered = Renderer - ? Renderer({ input: input as any, output: output as any }) - : UnknownToolRenderer({ input: input as any, output: output as any, toolName }) - return { - ...rendered, - classNames: { - body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll' - } as NonNullable[number]['classNames'] - } as NonNullable[number] - }, [Renderer, input, output, toolName]) + const toolContentItem: NonNullable[number] = { + ...renderedItem, + classNames: { + body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll' + } + } return ( } - return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput) + return ( + + ) } diff --git a/src/renderer/src/pages/home/Messages/Tools/ToolPermissionRequestCard.tsx b/src/renderer/src/pages/home/Messages/Tools/ToolPermissionRequestCard.tsx index 7df8544a6f..1fd2023b38 100644 --- a/src/renderer/src/pages/home/Messages/Tools/ToolPermissionRequestCard.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/ToolPermissionRequestCard.tsx @@ -1,7 +1,7 @@ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk' import { loggerService } from '@logger' import { useAppDispatch, useAppSelector } from '@renderer/store' -import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions' +import { selectPendingPermission, toolPermissionsActions } from '@renderer/store/toolPermissions' import type { NormalToolResponse } from '@renderer/types' import { Button } from 'antd' import { ChevronDown, CirclePlay, CircleX } from 'lucide-react' @@ -17,9 +17,7 @@ interface Props { export function ToolPermissionRequestCard({ toolResponse }: Props) { const { t } = useTranslation() const dispatch = useAppDispatch() - const request = useAppSelector((state) => - selectPendingPermissionByToolName(state.toolPermissions, toolResponse.tool.name) - ) + const request = useAppSelector((state) => selectPendingPermission(state.toolPermissions, toolResponse.toolCallId)) const [now, setNow] = useState(() => Date.now()) const [showDetails, setShowDetails] = useState(false) diff --git a/src/renderer/src/pages/settings/AgentSettings/EssentialSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/EssentialSettings.tsx index 846912dc64..78a9313b4f 100644 --- a/src/renderer/src/pages/settings/AgentSettings/EssentialSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/EssentialSettings.tsx @@ -1,21 +1,13 @@ -import { getAgentTypeAvatar } from '@renderer/config/agent' import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' -import { getAgentTypeLabel } from '@renderer/i18n/label' import type { GetAgentResponse, GetAgentSessionResponse } from '@renderer/types' -import { isAgentEntity } from '@renderer/types' -import { Avatar } from 'antd' import type { FC } from 'react' -import { useTranslation } from 'react-i18next' import { AccessibleDirsSetting } from './AccessibleDirsSetting' -import { AvatarSetting } from './AvatarSetting' import { DescriptionSetting } from './DescriptionSetting' import { ModelSetting } from './ModelSetting' import { NameSetting } from './NameSetting' -import { SettingsContainer, SettingsItem, SettingsTitle } from './shared' - -// const logger = loggerService.withContext('AgentEssentialSettings') +import { SettingsContainer } from './shared' type EssentialSettingsProps = | { @@ -30,26 +22,10 @@ type EssentialSettingsProps = } const EssentialSettings: FC = ({ agentBase, update, showModelSetting = true }) => { - const { t } = useTranslation() - if (!agentBase) return null - const isAgent = isAgentEntity(agentBase) - return ( - {isAgent && ( - - {t('agent.type.label')} - - - {(agentBase?.name ?? agentBase?.type) ? getAgentTypeLabel(agentBase.type) : ''} - - - )} - {isAgent && ( - ['updateAgent']} /> - )} {showModelSetting && } diff --git a/src/renderer/src/pages/settings/AgentSettings/NameSetting.tsx b/src/renderer/src/pages/settings/AgentSettings/NameSetting.tsx index e6e0b9190b..6e1f89ed50 100644 --- a/src/renderer/src/pages/settings/AgentSettings/NameSetting.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/NameSetting.tsx @@ -1,6 +1,8 @@ +import { EmojiAvatarWithPicker } from '@renderer/components/Avatar/EmojiAvatarWithPicker' import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types' +import { AgentConfigurationSchema, isAgentEntity, isAgentType } from '@renderer/types' import { Input } from 'antd' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { SettingsItem, SettingsTitle } from './shared' @@ -13,26 +15,61 @@ export interface NameSettingsProps { export const NameSetting = ({ base, update }: NameSettingsProps) => { const { t } = useTranslation() const [name, setName] = useState(base?.name?.trim()) + const updateName = async (name: UpdateAgentBaseForm['name']) => { if (!base) return return update({ id: base.id, name: name?.trim() }) } + + // Avatar logic + const isAgent = isAgentEntity(base) + const isDefault = isAgent ? isAgentType(base.configuration?.avatar) : false + const [emoji, setEmoji] = useState(isAgent && !isDefault ? (base.configuration?.avatar ?? '⭐️') : '⭐️') + + const updateAvatar = useCallback( + (avatar: string) => { + if (!isAgent || !base) return + const parsedConfiguration = AgentConfigurationSchema.parse(base.configuration ?? {}) + const payload = { + id: base.id, + configuration: { + ...parsedConfiguration, + avatar + } + } + update(payload) + }, + [base, update, isAgent] + ) + if (!base) return null return ( {t('common.name')} - setName(e.target.value)} - onBlur={() => { - if (name !== base.name) { - updateName(name) - } - }} - className="max-w-70 flex-1" - /> + + {isAgent && ( + { + setEmoji(emoji) + if (isAgent && emoji === base?.configuration?.avatar) return + updateAvatar(emoji) + }} + /> + )} + setName(e.target.value)} + onBlur={() => { + if (name !== base.name) { + updateName(name) + } + }} + className="flex-1" + /> + ) } diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx index b30ba5e7d7..cbaa57bf0a 100644 --- a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx @@ -109,7 +109,6 @@ const InstallNpxUv: FC = ({ mini = false }) => { @@ -140,7 +139,6 @@ const InstallNpxUv: FC = ({ mini = false }) => { /> diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 987b6cd0d6..4637cf2e89 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -140,7 +140,7 @@ const MCPSettings: FC = () => { + } diff --git a/src/renderer/src/pages/store/assistants/presets/components/AddAssistantPresetPopup.tsx b/src/renderer/src/pages/store/assistants/presets/components/AddAssistantPresetPopup.tsx index d5558b8e88..a6f733dfef 100644 --- a/src/renderer/src/pages/store/assistants/presets/components/AddAssistantPresetPopup.tsx +++ b/src/renderer/src/pages/store/assistants/presets/components/AddAssistantPresetPopup.tsx @@ -1,5 +1,3 @@ -import 'emoji-picker-element' - import { CheckOutlined, LoadingOutlined, RollbackOutlined, ThunderboltOutlined } from '@ant-design/icons' import { loggerService } from '@logger' import EmojiPicker from '@renderer/components/EmojiPicker' diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index c9b33eea8e..a70fdf572d 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -585,9 +585,11 @@ const fetchAndProcessAgentResponseImpl = async ( return } + // Only mark as cleared if there was a previous session ID (not initial assignment) + sessionWasCleared = !!latestAgentSessionId + latestAgentSessionId = sessionId agentSession.agentSessionId = sessionId - sessionWasCleared = true logger.debug(`Agent session ID updated`, { topicId, diff --git a/src/renderer/src/store/toolPermissions.ts b/src/renderer/src/store/toolPermissions.ts index a7ac87482e..59ff971329 100644 --- a/src/renderer/src/store/toolPermissions.ts +++ b/src/renderer/src/store/toolPermissions.ts @@ -6,6 +6,7 @@ export type ToolPermissionRequestPayload = { requestId: string toolName: string toolId: string + toolCallId: string description?: string requiresPermissions: boolean input: Record @@ -82,12 +83,12 @@ export const selectActiveToolPermission = (state: ToolPermissionsState): ToolPer return activeEntries[0] } -export const selectPendingPermissionByToolName = ( +export const selectPendingPermission = ( state: ToolPermissionsState, - toolName: string + toolCallId: string ): ToolPermissionEntry | undefined => { const activeEntries = Object.values(state.requests) - .filter((entry) => entry.toolName === toolName) + .filter((entry) => entry.toolCallId === toolCallId) .filter( (entry) => entry.status === 'pending' || entry.status === 'submitting-allow' || entry.status === 'submitting-deny' ) diff --git a/yarn.lock b/yarn.lock index dc6f25823e..240532dfb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,7 +102,19 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.44, @ai-sdk/anthropic@npm:^2.0.44": +"@ai-sdk/anthropic@npm:2.0.45": + version: 2.0.45 + resolution: "@ai-sdk/anthropic@npm:2.0.45" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.17" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/ef0e54f032e3b8324c278f3b25d9b388308204d753404c49fd880709a796c2343aee36d335c99f50e683edd39d5b8b6f42b2e9034e1725d8e0db514e2233d104 + languageName: node + linkType: hard + +"@ai-sdk/anthropic@npm:^2.0.44": version: 2.0.44 resolution: "@ai-sdk/anthropic@npm:2.0.44" dependencies: @@ -179,54 +191,42 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/google-vertex@npm:^3.0.62": - version: 3.0.62 - resolution: "@ai-sdk/google-vertex@npm:3.0.62" +"@ai-sdk/google-vertex@npm:^3.0.68": + version: 3.0.68 + resolution: "@ai-sdk/google-vertex@npm:3.0.68" dependencies: - "@ai-sdk/anthropic": "npm:2.0.44" - "@ai-sdk/google": "npm:2.0.31" + "@ai-sdk/anthropic": "npm:2.0.45" + "@ai-sdk/google": "npm:2.0.36" "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.17" google-auth-library: "npm:^9.15.0" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/673bb51e3e0cbe5235ad5e65379b1cb8f099dbc690ab8552e208553a9f1cc6026d2588e956e73468bc6d267066be276e7a9aba98e32e905809dfbeab4ac0e352 + checksum: 10c0/6a3f4cb1e649313b46a0c349c717757071f8b012b0a28e59ab7a55fd35d9600f0043f0a4f57417c4cc49e0d3734e89a1e4fb248fc88795b5286c83395d3f617a languageName: node linkType: hard -"@ai-sdk/google@npm:2.0.31": - version: 2.0.31 - resolution: "@ai-sdk/google@npm:2.0.31" +"@ai-sdk/google@npm:2.0.36": + version: 2.0.36 + resolution: "@ai-sdk/google@npm:2.0.36" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.17" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/d8f143f058fb62e6e67e30564ec92530d7389c22ad91b1e4bbe781c8570bf718cd417e44dcd4855e347e85c4174538a9a884eac666109e17f20d21467ab3e749 + checksum: 10c0/2c6de5e1cf0703b6b932a3f313bf4bc9439897af39c805169ab04bba397185d99b2b1306f3b817f991ca41fdced0365b072ee39e76382c045930256bce47e0e4 languageName: node linkType: hard -"@ai-sdk/google@npm:^2.0.32": - version: 2.0.32 - resolution: "@ai-sdk/google@npm:2.0.32" +"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch": + version: 2.0.36 + resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch::version=2.0.36&hash=2da8c3" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider-utils": "npm:3.0.17" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/052de16f1f66188e126168c8a9cc903448104528c7e44d6867bbf555c9067b9d6d44a4c4e0e014838156ba39095cb417f1b76363eb65212ca4d005f3651e58d2 - languageName: node - linkType: hard - -"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch": - version: 2.0.31 - resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch::version=2.0.31&hash=9f3835" - dependencies: - "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.17" - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/dd37dfb7abf402caaae3edb2f1a8dab018fddad6ba3190376723e03a2a0c352329c8e41e60df3fb8436b717d9c2ee4b82dff091848f50d026f62565cbdb158f8 + checksum: 10c0/ce99a497360377d2917cf3a48278eb6f4337623ce3738ba743cf048c8c2a7731ec4fc27605a50e461e716ed49b3690206ca8e4078f27cb7be162b684bfc2fc22 languageName: node linkType: hard @@ -1891,30 +1891,30 @@ __metadata: languageName: node linkType: hard -"@cherrystudio/ai-core@workspace:^1.0.0-alpha.18, @cherrystudio/ai-core@workspace:packages/aiCore": +"@cherrystudio/ai-core@workspace:^1.0.9, @cherrystudio/ai-core@workspace:packages/aiCore": version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: "@ai-sdk/anthropic": "npm:^2.0.43" "@ai-sdk/azure": "npm:^2.0.66" "@ai-sdk/deepseek": "npm:^1.0.27" - "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch" - "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch" "@ai-sdk/openai-compatible": "npm:^1.0.26" "@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider-utils": "npm:^3.0.16" "@ai-sdk/xai": "npm:^2.0.31" - "@cherrystudio/ai-sdk-provider": "workspace:*" tsdown: "npm:^0.12.9" typescript: "npm:^5.0.0" vitest: "npm:^3.2.4" zod: "npm:^4.1.5" peerDependencies: + "@ai-sdk/google": ^2.0.36 + "@ai-sdk/openai": ^2.0.64 + "@cherrystudio/ai-sdk-provider": ^0.1.2 ai: ^5.0.26 languageName: unknown linkType: soft -"@cherrystudio/ai-sdk-provider@workspace:*, @cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider": +"@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider": version: 0.0.0-use.local resolution: "@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider" dependencies: @@ -9906,10 +9906,11 @@ __metadata: "@ai-sdk/anthropic": "npm:^2.0.44" "@ai-sdk/cerebras": "npm:^1.0.31" "@ai-sdk/gateway": "npm:^2.0.9" - "@ai-sdk/google": "npm:^2.0.32" - "@ai-sdk/google-vertex": "npm:^3.0.62" + "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch" + "@ai-sdk/google-vertex": "npm:^3.0.68" "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch" "@ai-sdk/mistral": "npm:^2.0.23" + "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch" "@ai-sdk/perplexity": "npm:^2.0.17" "@ant-design/v5-patch-for-react-19": "npm:^1.0.3" "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch" @@ -9919,7 +9920,7 @@ __metadata: "@aws-sdk/client-bedrock-runtime": "npm:^3.910.0" "@aws-sdk/client-s3": "npm:^3.910.0" "@biomejs/biome": "npm:2.2.4" - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18" + "@cherrystudio/ai-core": "workspace:^1.0.9" "@cherrystudio/embedjs": "npm:^0.1.31" "@cherrystudio/embedjs-libsql": "npm:^0.1.31" "@cherrystudio/embedjs-loader-csv": "npm:^0.1.31" @@ -10073,6 +10074,7 @@ __metadata: electron-window-state: "npm:^5.0.3" emittery: "npm:^1.0.3" emoji-picker-element: "npm:^1.22.1" + emoji-picker-element-data: "npm:^1" epub: "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch" eslint: "npm:^9.22.0" eslint-plugin-import-zod: "npm:^1.2.0" @@ -13654,6 +13656,13 @@ __metadata: languageName: node linkType: hard +"emoji-picker-element-data@npm:^1": + version: 1.8.0 + resolution: "emoji-picker-element-data@npm:1.8.0" + checksum: 10c0/c8976b636205a0cc90d2690859a1193add71a948dadf743962b47c338a4c3715768404d0ccbc02156608b44abf41f3e1d51756e06f1bbed9d164dd4cb1752103 + languageName: node + linkType: hard + "emoji-picker-element@npm:^1.22.1": version: 1.26.3 resolution: "emoji-picker-element@npm:1.26.3"