diff --git a/CLAUDE.md b/CLAUDE.md index 88ab1a116f..c96fc0e403 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,15 @@ This file provides guidance to AI coding assistants when working with code in th - **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications. - **Lint, test, and format before completion**: Coding tasks are only complete after running `yarn lint`, `yarn test`, and `yarn format` successfully. - **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`). -- **Follow PR template**: When submitting pull requests, follow the template in `.github/pull_request_template.md` to ensure complete context and documentation. + +## Pull Request Workflow (CRITICAL) + +When creating a Pull Request, you MUST: + +1. **Read the PR template first**: Always read `.github/pull_request_template.md` before creating the PR +2. **Follow ALL template sections**: Structure the `--body` parameter to include every section from the template +3. **Never skip sections**: Include all sections even if marking them as N/A or "None" +4. **Use proper formatting**: Match the template's markdown structure exactly (headings, checkboxes, code blocks) ## Development Commands diff --git a/electron-builder.yml b/electron-builder.yml index d75cd5855d..5e63e7231d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -134,9 +134,9 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - A New Era of Intelligence with Cherry Studio 1.7.0 + A New Era of Intelligence with Cherry Studio 1.7.1 - Today we're releasing Cherry Studio 1.7.0 — our most ambitious update yet, introducing Agent: autonomous AI that thinks, plans, and acts. + Today we're releasing Cherry Studio 1.7.1 — our most ambitious update yet, introducing Agent: autonomous AI that thinks, plans, and acts. For years, AI assistants have been reactive — waiting for your commands, responding to your questions. With Agent, we're changing that. Now, AI can truly work alongside you: understanding complex goals, breaking them into steps, and executing them independently. @@ -187,9 +187,9 @@ releaseInfo: The Agent Era is here. We can't wait to see what you'll create. - Cherry Studio 1.7.0:开启智能新纪元 + Cherry Studio 1.7.1:开启智能新纪元 - 今天,我们正式发布 Cherry Studio 1.7.0 —— 迄今最具雄心的版本,带来全新的 Agent:能够自主思考、规划和行动的 AI。 + 今天,我们正式发布 Cherry Studio 1.7.1 —— 迄今最具雄心的版本,带来全新的 Agent:能够自主思考、规划和行动的 AI。 多年来,AI 助手一直是被动的——等待你的指令,回应你的问题。Agent 改变了这一切。现在,AI 能够真正与你并肩工作:理解复杂目标,将其拆解为步骤,并独立执行。 diff --git a/package.json b/package.json index 52c57b886f..17be71ee59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.7.0", + "version": "1.7.1", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 00a395c751..864c3f82eb 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -1,6 +1,7 @@ // src/main/services/agents/services/claudecode/index.ts import { EventEmitter } from 'node:events' import { createRequire } from 'node:module' +import path from 'node:path' import type { CanUseTool, @@ -116,7 +117,11 @@ class ClaudeCodeService implements AgentServiceInterface { // TODO: support set small model in UI ANTHROPIC_DEFAULT_HAIKU_MODEL: modelInfo.modelId, ELECTRON_RUN_AS_NODE: '1', - ELECTRON_NO_ATTACH_CONSOLE: '1' + ELECTRON_NO_ATTACH_CONSOLE: '1', + // Set CLAUDE_CONFIG_DIR to app's userData directory to avoid path encoding issues + // on Windows when the username contains non-ASCII characters (e.g., Chinese characters) + // This prevents the SDK from using the user's home directory which may have encoding problems + CLAUDE_CONFIG_DIR: path.join(app.getPath('userData'), '.claude') } const errorChunks: string[] = [] diff --git a/src/renderer/src/assets/styles/ant.css b/src/renderer/src/assets/styles/ant.css index 30005ff738..7d651a6a6a 100644 --- a/src/renderer/src/assets/styles/ant.css +++ b/src/renderer/src/assets/styles/ant.css @@ -215,6 +215,10 @@ border-top: none !important; } +.ant-collapse-header-text { + overflow-x: hidden; +} + .ant-slider .ant-slider-handle::after { box-shadow: 0 1px 4px 0px rgb(128 128 128 / 50%) !important; } diff --git a/src/renderer/src/hooks/useScrollPosition.ts b/src/renderer/src/hooks/useScrollPosition.ts index acb1bd851b..c0f09300d8 100644 --- a/src/renderer/src/hooks/useScrollPosition.ts +++ b/src/renderer/src/hooks/useScrollPosition.ts @@ -1,5 +1,5 @@ import { throttle } from 'lodash' -import { useEffect, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import { useTimer } from './useTimer' @@ -12,13 +12,18 @@ import { useTimer } from './useTimer' */ export default function useScrollPosition(key: string, throttleWait?: number) { const containerRef = useRef(null) - const scrollKey = `scroll:${key}` + const scrollKey = useMemo(() => `scroll:${key}`, [key]) + const scrollKeyRef = useRef(scrollKey) const { setTimeoutTimer } = useTimer() + useEffect(() => { + scrollKeyRef.current = scrollKey + }, [scrollKey]) + const handleScroll = throttle(() => { const position = containerRef.current?.scrollTop ?? 0 window.requestAnimationFrame(() => { - window.keyv.set(scrollKey, position) + window.keyv.set(scrollKeyRef.current, position) }) }, throttleWait ?? 100) @@ -28,5 +33,9 @@ export default function useScrollPosition(key: string, throttleWait?: number) { setTimeoutTimer('scrollEffect', scroll, 50) }, [scrollKey, setTimeoutTimer]) + useEffect(() => { + return () => handleScroll.cancel() + }, [handleScroll]) + return { containerRef, handleScroll } } diff --git a/src/renderer/src/hooks/useTimer.ts b/src/renderer/src/hooks/useTimer.ts index af4df045cf..69fa89cdf9 100644 --- a/src/renderer/src/hooks/useTimer.ts +++ b/src/renderer/src/hooks/useTimer.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' /** * 定时器管理 Hook,用于管理 setTimeout 和 setInterval 定时器,支持通过 key 来标识不同的定时器 @@ -43,10 +43,38 @@ export const useTimer = () => { const timeoutMapRef = useRef(new Map()) const intervalMapRef = useRef(new Map()) + /** + * 清除指定 key 的 setTimeout 定时器 + * @param key - 定时器标识符 + */ + const clearTimeoutTimer = useCallback((key: string) => { + clearTimeout(timeoutMapRef.current.get(key)) + timeoutMapRef.current.delete(key) + }, []) + + /** + * 清除指定 key 的 setInterval 定时器 + * @param key - 定时器标识符 + */ + const clearIntervalTimer = useCallback((key: string) => { + clearInterval(intervalMapRef.current.get(key)) + intervalMapRef.current.delete(key) + }, []) + + /** + * 清除所有定时器,包括 setTimeout 和 setInterval + */ + const clearAllTimers = useCallback(() => { + timeoutMapRef.current.forEach((timer) => clearTimeout(timer)) + intervalMapRef.current.forEach((timer) => clearInterval(timer)) + timeoutMapRef.current.clear() + intervalMapRef.current.clear() + }, []) + // 组件卸载时自动清理所有定时器 useEffect(() => { return () => clearAllTimers() - }, []) + }, [clearAllTimers]) /** * 设置一个 setTimeout 定时器 @@ -65,12 +93,15 @@ export const useTimer = () => { * cleanup(); * ``` */ - const setTimeoutTimer = (key: string, ...args: Parameters) => { - clearTimeout(timeoutMapRef.current.get(key)) - const timer = setTimeout(...args) - timeoutMapRef.current.set(key, timer) - return () => clearTimeoutTimer(key) - } + const setTimeoutTimer = useCallback( + (key: string, ...args: Parameters) => { + clearTimeout(timeoutMapRef.current.get(key)) + const timer = setTimeout(...args) + timeoutMapRef.current.set(key, timer) + return () => clearTimeoutTimer(key) + }, + [clearTimeoutTimer] + ) /** * 设置一个 setInterval 定时器 @@ -89,56 +120,31 @@ export const useTimer = () => { * cleanup(); * ``` */ - const setIntervalTimer = (key: string, ...args: Parameters) => { - clearInterval(intervalMapRef.current.get(key)) - const timer = setInterval(...args) - intervalMapRef.current.set(key, timer) - return () => clearIntervalTimer(key) - } - - /** - * 清除指定 key 的 setTimeout 定时器 - * @param key - 定时器标识符 - */ - const clearTimeoutTimer = (key: string) => { - clearTimeout(timeoutMapRef.current.get(key)) - timeoutMapRef.current.delete(key) - } - - /** - * 清除指定 key 的 setInterval 定时器 - * @param key - 定时器标识符 - */ - const clearIntervalTimer = (key: string) => { - clearInterval(intervalMapRef.current.get(key)) - intervalMapRef.current.delete(key) - } + const setIntervalTimer = useCallback( + (key: string, ...args: Parameters) => { + clearInterval(intervalMapRef.current.get(key)) + const timer = setInterval(...args) + intervalMapRef.current.set(key, timer) + return () => clearIntervalTimer(key) + }, + [clearIntervalTimer] + ) /** * 清除所有 setTimeout 定时器 */ - const clearAllTimeoutTimers = () => { + const clearAllTimeoutTimers = useCallback(() => { timeoutMapRef.current.forEach((timer) => clearTimeout(timer)) timeoutMapRef.current.clear() - } + }, []) /** * 清除所有 setInterval 定时器 */ - const clearAllIntervalTimers = () => { + const clearAllIntervalTimers = useCallback(() => { intervalMapRef.current.forEach((timer) => clearInterval(timer)) intervalMapRef.current.clear() - } - - /** - * 清除所有定时器,包括 setTimeout 和 setInterval - */ - const clearAllTimers = () => { - timeoutMapRef.current.forEach((timer) => clearTimeout(timer)) - intervalMapRef.current.forEach((timer) => clearInterval(timer)) - timeoutMapRef.current.clear() - intervalMapRef.current.clear() - } + }, []) return { setTimeoutTimer, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 008f1721e8..782340e011 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -280,6 +280,7 @@ "denied": "Tool request was denied.", "timeout": "Tool request timed out before receiving approval." }, + "toolPendingFallback": "Tool", "waiting": "Waiting for tool permission decision..." }, "type": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 60b4782739..c1874f7fb8 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -280,6 +280,7 @@ "denied": "工具请求已被拒绝。", "timeout": "工具请求在收到批准前超时。" }, + "toolPendingFallback": "工具", "waiting": "等待工具权限决定..." }, "type": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8068b666f0..db81e30006 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -280,6 +280,7 @@ "denied": "工具請求已被拒絕。", "timeout": "工具請求在收到核准前逾時。" }, + "toolPendingFallback": "工具", "waiting": "等待工具權限決定..." }, "type": { diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 61446bc794..e7314482a3 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -280,6 +280,7 @@ "denied": "Tool-Anfrage wurde abgelehnt.", "timeout": "Tool-Anfrage ist abgelaufen, bevor eine Genehmigung eingegangen ist." }, + "toolPendingFallback": "Werkzeug", "waiting": "Warten auf Entscheidung über Tool-Berechtigung..." }, "type": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 39830d9c51..bc825ec688 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -280,6 +280,7 @@ "denied": "Το αίτημα για εργαλείο απορρίφθηκε.", "timeout": "Το αίτημα για το εργαλείο έληξε πριν λάβει έγκριση." }, + "toolPendingFallback": "Εργαλείο", "waiting": "Αναμονή για απόφαση άδειας εργαλείου..." }, "type": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 8573824525..2da83ad229 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -280,6 +280,7 @@ "denied": "La solicitud de herramienta fue denegada.", "timeout": "La solicitud de herramienta expiró antes de recibir la aprobación." }, + "toolPendingFallback": "Herramienta", "waiting": "Esperando la decisión de permiso de la herramienta..." }, "type": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 16b2da7596..3f3e9108c5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -280,6 +280,7 @@ "denied": "La demande d'outil a été refusée.", "timeout": "La demande d'outil a expiré avant d'obtenir l'approbation." }, + "toolPendingFallback": "Outil", "waiting": "En attente de la décision d'autorisation de l'outil..." }, "type": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 03a077a7b8..e2591fb20d 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -280,6 +280,7 @@ "denied": "ツールリクエストは拒否されました。", "timeout": "ツールリクエストは承認を受ける前にタイムアウトしました。" }, + "toolPendingFallback": "ツール", "waiting": "ツールの許可決定を待っています..." }, "type": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index e8704d318d..cae6ebd38d 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -280,6 +280,7 @@ "denied": "Solicitação de ferramenta foi negada.", "timeout": "A solicitação da ferramenta expirou antes de receber aprovação." }, + "toolPendingFallback": "Ferramenta", "waiting": "Aguardando decisão de permissão da ferramenta..." }, "type": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 1114ae9544..fe5ebbcb25 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -280,6 +280,7 @@ "denied": "Запрос на инструмент был отклонён.", "timeout": "Запрос на инструмент превысил время ожидания до получения подтверждения." }, + "toolPendingFallback": "Инструмент", "waiting": "Ожидание решения о разрешении на использование инструмента..." }, "type": { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 0624bfed81..0985023ead 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -14,7 +14,6 @@ import { useInputText } from '@renderer/hooks/useInputText' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' -import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useTextareaResize } from '@renderer/hooks/useTextareaResize' import { useTimer } from '@renderer/hooks/useTimer' import { @@ -150,7 +149,6 @@ const InputbarInner: FC = ({ assistant: initialAssistant, se minHeight: 30 }) - const showKnowledgeIcon = useSidebarIconShow('knowledge') const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(initialAssistant.id) const { sendMessageShortcut, showInputEstimatedTokens, enableQuickPanelTriggers } = useSettings() const [estimateTokenCount, setEstimateTokenCount] = useState(0) @@ -407,9 +405,10 @@ const InputbarInner: FC = ({ assistant: initialAssistant, se focusTextarea ]) + // TODO: Just use assistant.knowledge_bases as selectedKnowledgeBases. context state is overdesigned. useEffect(() => { - setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : []) - }, [assistant.knowledge_bases, setSelectedKnowledgeBases, showKnowledgeIcon]) + setSelectedKnowledgeBases(assistant.knowledge_bases ?? []) + }, [assistant.knowledge_bases, setSelectedKnowledgeBases]) useEffect(() => { // Disable web search if model doesn't support it diff --git a/src/renderer/src/pages/home/Inputbar/tools/knowledgeBaseTool.tsx b/src/renderer/src/pages/home/Inputbar/tools/knowledgeBaseTool.tsx index 5e3ffcf864..787d98eb34 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/knowledgeBaseTool.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/knowledgeBaseTool.tsx @@ -1,5 +1,4 @@ import { useAssistant } from '@renderer/hooks/useAssistant' -import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { defineTool, registerTool, TopicType } from '@renderer/pages/home/Inputbar/types' import type { KnowledgeBase } from '@renderer/types' import { isPromptToolUse, isSupportedToolUse } from '@renderer/utils/mcp-tools' @@ -30,7 +29,6 @@ const knowledgeBaseTool = defineTool({ render: function KnowledgeBaseToolRender(context) { const { assistant, state, actions, quickPanel } = context - const knowledgeSidebarEnabled = useSidebarIconShow('knowledge') const { updateAssistant } = useAssistant(assistant.id) const handleSelect = useCallback( @@ -41,10 +39,6 @@ const knowledgeBaseTool = defineTool({ [updateAssistant, actions] ) - if (!knowledgeSidebarEnabled) { - return null - } - return ( = ({ containerId }) => { } const scrollToMessage = (element: HTMLElement) => { - element.scrollIntoView({ behavior: 'smooth', block: 'start' }) + // Use container: 'nearest' to keep scroll within the chat pane (Chromium-only, see #11565, #11567) + scrollIntoView(element, { behavior: 'smooth', block: 'start', container: 'nearest' }) } const scrollToTop = () => { diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 798559f8d9..ddbcd9cf25 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -15,6 +15,7 @@ import { estimateMessageUsage } from '@renderer/services/TokenService' import type { Assistant, Topic } from '@renderer/types' import type { Message, MessageBlock } from '@renderer/types/newMessage' import { classNames, cn } from '@renderer/utils' +import { scrollIntoView } from '@renderer/utils/dom' import { isMessageProcessing } from '@renderer/utils/messageUtils/is' import { Divider } from 'antd' import type { Dispatch, FC, SetStateAction } from 'react' @@ -79,9 +80,10 @@ const MessageItem: FC = ({ useEffect(() => { if (isEditing && messageContainerRef.current) { - messageContainerRef.current.scrollIntoView({ + scrollIntoView(messageContainerRef.current, { behavior: 'smooth', - block: 'center' + block: 'center', + container: 'nearest' }) } }, [isEditing]) @@ -124,7 +126,7 @@ const MessageItem: FC = ({ const messageHighlightHandler = useCallback( (highlight: boolean = true) => { if (messageContainerRef.current) { - messageContainerRef.current.scrollIntoView({ behavior: 'smooth' }) + scrollIntoView(messageContainerRef.current, { behavior: 'smooth', block: 'center', container: 'nearest' }) if (highlight) { setTimeoutTimer( 'messageHighlightHandler', diff --git a/src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx b/src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx index d36448913f..ab489dd700 100644 --- a/src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx +++ b/src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx @@ -12,6 +12,7 @@ import { newMessagesActions } from '@renderer/store/newMessage' // import { updateMessageThunk } from '@renderer/store/thunk/messageThunk' import type { Message } from '@renderer/types/newMessage' import { isEmoji, removeLeadingEmoji } from '@renderer/utils' +import { scrollIntoView } from '@renderer/utils/dom' import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { Avatar } from 'antd' import { CircleChevronDown } from 'lucide-react' @@ -119,7 +120,7 @@ const MessageAnchorLine: FC = ({ messages }) => { () => { const messageElement = document.getElementById(`message-${message.id}`) if (messageElement) { - messageElement.scrollIntoView({ behavior: 'auto', block: 'start' }) + scrollIntoView(messageElement, { behavior: 'auto', block: 'start', container: 'nearest' }) } }, 100 @@ -141,7 +142,7 @@ const MessageAnchorLine: FC = ({ messages }) => { return } - messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) + scrollIntoView(messageElement, { behavior: 'smooth', block: 'start', container: 'nearest' }) }, [setSelectedMessage] ) diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 1e1eca27a1..849e4b1c76 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -10,6 +10,7 @@ import type { MultiModelMessageStyle } from '@renderer/store/settings' import type { Topic } from '@renderer/types' import type { Message } from '@renderer/types/newMessage' import { classNames } from '@renderer/utils' +import { scrollIntoView } from '@renderer/utils/dom' import { Popover } from 'antd' import type { ComponentProps } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react' @@ -73,7 +74,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { () => { const messageElement = document.getElementById(`message-${message.id}`) if (messageElement) { - messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) + scrollIntoView(messageElement, { behavior: 'smooth', block: 'start', container: 'nearest' }) } }, 200 @@ -132,7 +133,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { setSelectedMessage(message) } else { // 直接滚动 - element.scrollIntoView({ behavior: 'smooth', block: 'start' }) + scrollIntoView(element, { behavior: 'smooth', block: 'start', container: 'nearest' }) } } } diff --git a/src/renderer/src/pages/home/Messages/MessageOutline.tsx b/src/renderer/src/pages/home/Messages/MessageOutline.tsx index 1327dd4a89..0fb372841f 100644 --- a/src/renderer/src/pages/home/Messages/MessageOutline.tsx +++ b/src/renderer/src/pages/home/Messages/MessageOutline.tsx @@ -3,6 +3,7 @@ import type { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' import type { Message } from '@renderer/types/newMessage' import { MessageBlockType } from '@renderer/types/newMessage' +import { scrollIntoView } from '@renderer/utils/dom' import type { FC } from 'react' import React, { useMemo, useRef } from 'react' import { useSelector } from 'react-redux' @@ -72,10 +73,10 @@ const MessageOutline: FC = ({ message }) => { const parent = messageOutlineContainerRef.current?.parentElement const messageContentContainer = parent?.querySelector('.message-content-container') if (messageContentContainer) { - const headingElement = messageContentContainer.querySelector(`#${id}`) + const headingElement = messageContentContainer.querySelector(`#${id}`) if (headingElement) { const scrollBlock = ['horizontal', 'grid'].includes(message.multiModelMessageStyle ?? '') ? 'nearest' : 'start' - headingElement.scrollIntoView({ behavior: 'smooth', block: scrollBlock }) + scrollIntoView(headingElement, { behavior: 'smooth', block: scrollBlock, container: 'nearest' }) } } } diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx index 7d627c3455..798807d4d6 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx @@ -5,8 +5,6 @@ import { Terminal } from 'lucide-react' import { ToolTitle } from './GenericTools' import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types' -const MAX_TAG_LENGTH = 100 - export function BashTool({ input, output @@ -17,12 +15,10 @@ export function BashTool({ // 如果有输出,计算输出行数 const outputLines = output ? output.split('\n').length : 0 - // 处理命令字符串的截断,添加空值检查 + // 处理命令字符串,添加空值检查 const command = input?.command ?? '' - const needsTruncate = command.length > MAX_TAG_LENGTH - const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command - const tagContent = {displayCommand} + const tagContent = {command} return { key: 'tool', @@ -34,16 +30,12 @@ export function BashTool({ params={input?.description} stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} /> -
- {needsTruncate ? ( - {command}
} - trigger="hover"> - {tagContent} - - ) : ( - tagContent - )} +
+ {command}
} + trigger="hover"> + {tagContent} + ), diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/GenericTools.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/GenericTools.tsx index 9eaaf76f2c..2245730ce7 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/GenericTools.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/GenericTools.tsx @@ -18,9 +18,9 @@ export function ToolTitle({ }) { return (
- {icon} - {label && {label}} - {params && {params}} + {icon && {icon}} + {label && {label}} + {params && {params}} {stats && {stats}}
) 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 42a1cf403b..e523305277 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx @@ -1,7 +1,10 @@ import { loggerService } from '@logger' +import { useAppSelector } from '@renderer/store' +import { selectPendingPermission } from '@renderer/store/toolPermissions' import type { NormalToolResponse } from '@renderer/types' import type { CollapseProps } from 'antd' -import { Collapse } from 'antd' +import { Collapse, Spin } from 'antd' +import { useTranslation } from 'react-i18next' // 导出所有类型 export * from './types' @@ -83,17 +86,41 @@ function ToolContent({ toolName, input, output }: { toolName: AgentToolsType; in // 统一的组件渲染入口 export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolResponse }) { const { arguments: args, response, tool, status } = toolResponse - logger.info('Rendering agent tool response', { + logger.debug('Rendering agent tool response', { tool: tool, arguments: args, + status, response }) + const pendingPermission = useAppSelector((state) => + selectPendingPermission(state.toolPermissions, toolResponse.toolCallId) + ) + if (status === 'pending') { - return + if (pendingPermission) { + return + } + return } return ( ) } + +function ToolPendingIndicator({ toolName, description }: { toolName?: string; description?: string }) { + const { t } = useTranslation() + const label = toolName || t('agent.toolPermission.toolPendingFallback', 'Tool') + const detail = description?.trim() || t('agent.toolPermission.executing') + + return ( +
+ +
+ {label} + {detail} +
+
+ ) +} diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 1b2bf0433f..d265d5cb48 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -12,7 +12,7 @@ import type { FetchChatCompletionParams } from '@renderer/types' import type { Assistant, MCPServer, MCPTool, Model, Provider } from '@renderer/types' import type { StreamTextParams } from '@renderer/types/aiCoreTypes' import { type Chunk, ChunkType } from '@renderer/types/chunk' -import type { Message } from '@renderer/types/newMessage' +import type { Message, ResponseError } from '@renderer/types/newMessage' import type { SdkModel } from '@renderer/types/sdk' import { removeSpecialCharactersForTopicName, uuid } from '@renderer/utils' import { abortCompletion, readyToAbort } from '@renderer/utils/abortController' @@ -476,7 +476,7 @@ export async function checkApi(provider: Provider, model: Model, timeout = 15000 } else { const abortId = uuid() const signal = readyToAbort(abortId) - let chunkError + let streamError: ResponseError | undefined const params: StreamTextParams = { system: assistant.prompt, prompt: 'hi', @@ -495,19 +495,18 @@ export async function checkApi(provider: Provider, model: Model, timeout = 15000 callType: 'check', onChunk: (chunk: Chunk) => { if (chunk.type === ChunkType.ERROR) { - chunkError = chunk.error + streamError = chunk.error } else { abortCompletion(abortId) } } } - // Try streaming check try { await ai.completions(model.id, params, config) } catch (e) { - if (!isAbortError(e) && !isAbortError(chunkError)) { - throw e + if (!isAbortError(e) && !isAbortError(streamError)) { + throw streamError ?? e } } } diff --git a/src/renderer/src/types/newMessage.ts b/src/renderer/src/types/newMessage.ts index 5ce96e4ec7..ef7179527d 100644 --- a/src/renderer/src/types/newMessage.ts +++ b/src/renderer/src/types/newMessage.ts @@ -234,6 +234,7 @@ export interface Response { error?: ResponseError } +// FIXME: Weak type safety. It may be a specific class instance which inherits Error in runtime. export type ResponseError = Record export interface MessageInputBaseParams { diff --git a/src/renderer/src/utils/dom.ts b/src/renderer/src/utils/dom.ts index 6dd09cda5e..15161ea86d 100644 --- a/src/renderer/src/utils/dom.ts +++ b/src/renderer/src/utils/dom.ts @@ -1,3 +1,15 @@ +import { loggerService } from '@logger' + +const logger = loggerService.withContext('utils/dom') + +interface ChromiumScrollIntoViewOptions extends ScrollIntoViewOptions { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#container + * @see https://github.com/microsoft/TypeScript/issues/62803 + */ + container?: 'all' | 'nearest' +} + /** * Simple wrapper for scrollIntoView with common default options. * Provides a unified interface with sensible defaults. @@ -5,7 +17,12 @@ * @param element - The target element to scroll into view * @param options - Scroll options. If not provided, uses { behavior: 'smooth', block: 'center', inline: 'nearest' } */ -export function scrollIntoView(element: HTMLElement, options?: ScrollIntoViewOptions): void { +export function scrollIntoView(element: HTMLElement, options?: ChromiumScrollIntoViewOptions): void { + if (!element) { + logger.warn('[scrollIntoView] Unexpected falsy element. Do nothing as fallback.') + return + } + const defaultOptions: ScrollIntoViewOptions = { behavior: 'smooth', block: 'center',