From 5d26bf15a320be758e58fffbe3162be17b843cad Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 22 Sep 2025 22:42:43 +0800 Subject: [PATCH 1/4] fix: stabilize tool streaming typings --- .../agents/services/SessionMessageService.ts | 15 +++++++++++++-- tests/renderer.setup.ts | 14 +++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/services/agents/services/SessionMessageService.ts b/src/main/services/agents/services/SessionMessageService.ts index 786c9b72b..09e34bf02 100644 --- a/src/main/services/agents/services/SessionMessageService.ts +++ b/src/main/services/agents/services/SessionMessageService.ts @@ -68,15 +68,26 @@ class TextStreamAccumulator { } case 'tool-call': if (part.toolCallId) { + const legacyPart = part as typeof part & { + args?: unknown + providerMetadata?: { raw?: { input?: unknown } } + } this.toolCalls.set(part.toolCallId, { toolName: part.toolName, - input: part.input ?? part.args ?? part.providerMetadata?.raw?.input + input: part.input ?? legacyPart.args ?? legacyPart.providerMetadata?.raw?.input }) } break case 'tool-result': if (part.toolCallId) { - this.toolResults.set(part.toolCallId, part.output ?? part.result ?? part.providerMetadata?.raw) + const legacyPart = part as typeof part & { + result?: unknown + providerMetadata?: { raw?: unknown } + } + this.toolResults.set( + part.toolCallId, + part.output ?? legacyPart.result ?? legacyPart.providerMetadata?.raw + ) } break default: diff --git a/tests/renderer.setup.ts b/tests/renderer.setup.ts index ea4057eca..fab761fae 100644 --- a/tests/renderer.setup.ts +++ b/tests/renderer.setup.ts @@ -14,13 +14,21 @@ vi.mock('@logger', async () => { } }) -vi.mock('axios', () => ({ - default: { +vi.mock('axios', () => { + const defaultAxiosMock = { get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request // You can add other axios methods like put, delete etc. as needed } -})) + + const isAxiosError = (error: unknown): error is { isAxiosError?: boolean } => + Boolean((error as { isAxiosError?: boolean } | undefined)?.isAxiosError) + + return { + default: defaultAxiosMock, + isAxiosError + } +}) vi.stubGlobal('electron', { ipcRenderer: { From 49e4667410364f47472d044167cb2ca57d015f4e Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 22 Sep 2025 22:48:15 +0800 Subject: [PATCH 2/4] fix: resolve lint findings in renderer --- .../home/Messages/Tools/MessageAgentTools/WriteTool.tsx | 2 +- src/renderer/src/pages/home/Tabs/components/Sessions.tsx | 4 +--- src/renderer/src/utils/mcp-tools.ts | 7 ++++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/WriteTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/WriteTool.tsx index 8b5e116ef..08644c1c5 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/WriteTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/WriteTool.tsx @@ -4,7 +4,7 @@ import { FileText } from 'lucide-react' import { ToolTitle } from './GenericTools' import type { WriteToolInput, WriteToolOutput } from './types' -export function WriteTool({ input, output }: { input: WriteToolInput; output?: WriteToolOutput }) { +export function WriteTool({ input }: { input: WriteToolInput; output?: WriteToolOutput }) { return ( = ({ agentId }) => { const { t } = useTranslation() const { agent } = useAgent(agentId) const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId) - const updateSession = useUpdateSession(agentId) const { chat } = useRuntime() const { activeSessionId, sessionWaiting } = chat const dispatch = useAppDispatch() @@ -70,7 +68,7 @@ const Sessions: React.FC = ({ agentId }) => { } dispatch(setSessionWaitingAction({ id, value: false })) }, - [agentId, deleteSession, dispatch, sessions] + [agentId, deleteSession, dispatch, sessions, t] ) const currentActiveSessionId = activeSessionId[agentId] diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index 417509434..6914868d4 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -108,7 +108,12 @@ export function openAIToolsToMcpTool( export async function callBuiltInTool(toolResponse: MCPToolResponse): Promise { logger.info(`[BuiltIn] Calling Built-in Tool: ${toolResponse.tool.name}`, toolResponse.tool) - if (toolResponse.tool.name === 'think') { + if ( + toolResponse.tool.name === 'think' && + typeof toolResponse.arguments === 'object' && + toolResponse.arguments !== null && + !Array.isArray(toolResponse.arguments) + ) { const thought = toolResponse.arguments?.thought return { isError: false, From b4a92cecc88e716952f89dd915d889c576d2119b Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 22 Sep 2025 23:11:04 +0800 Subject: [PATCH 3/4] feat: enhance agent creation and session management with automatic session provisioning --- .../routes/agents/handlers/agents.ts | 38 +++++++++++++++++-- .../routes/agents/handlers/sessions.ts | 26 +++++++++++++ .../agents/services/SessionService.ts | 5 ++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/main/apiServer/routes/agents/handlers/agents.ts b/src/main/apiServer/routes/agents/handlers/agents.ts index 6e35f12c8..dc9e4ecf5 100644 --- a/src/main/apiServer/routes/agents/handlers/agents.ts +++ b/src/main/apiServer/routes/agents/handlers/agents.ts @@ -1,5 +1,5 @@ import { loggerService } from '@logger' -import { AgentModelValidationError, agentService } from '@main/services/agents' +import { AgentModelValidationError, agentService, sessionService } from '@main/services/agents' import { ListAgentsResponse, type ReplaceAgentRequest, type UpdateAgentRequest } from '@types' import { Request, Response } from 'express' @@ -20,7 +20,8 @@ const modelValidationErrorBody = (error: AgentModelValidationError) => ({ * /v1/agents: * post: * summary: Create a new agent - * description: Creates a new autonomous agent with the specified configuration + * description: Creates a new autonomous agent with the specified configuration and automatically + * provisions an initial session that mirrors the agent's settings. * tags: [Agents] * requestBody: * required: true @@ -55,8 +56,37 @@ export const createAgent = async (req: Request, res: Response): Promise { + async createSession( + agentId: string, + req: Partial = {} + ): Promise { this.ensureInitialized() // Validate agent exists - we'll need to import AgentService for this check From 14638b7470fb13206178fb2fa5ae70d7f8146f9f Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 23 Sep 2025 00:21:04 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20tool=20selection?= =?UTF-8?q?=20functionality=20for=20agents=20and=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive tool management UI allowing users to select which tools are pre-approved for agents and sessions. Includes multi-select dropdowns with tool descriptions, proper validation, and internationalization support across 10+ languages. - Add tool selection UI to AgentModal, SessionModal, and AgentEssentialSettings - Extend BaseAgentForm and related types with allowed_tools field - Implement tool validation and filtering logic - Add i18n support for tool selection labels and descriptions - Include visual chip-based display for selected tools --- .../components/Popups/agent/AgentModal.tsx | 110 +++++++++++++++- .../components/Popups/agent/SessionModal.tsx | 123 +++++++++++++++++- src/renderer/src/i18n/locales/en-us.json | 6 + src/renderer/src/i18n/locales/zh-cn.json | 6 + src/renderer/src/i18n/locales/zh-tw.json | 6 + src/renderer/src/i18n/translate/el-gr.json | 6 + src/renderer/src/i18n/translate/es-es.json | 6 + src/renderer/src/i18n/translate/fr-fr.json | 6 + src/renderer/src/i18n/translate/ja-jp.json | 6 + src/renderer/src/i18n/translate/pt-pt.json | 6 + src/renderer/src/i18n/translate/ru-ru.json | 6 + .../AgentSettings/AgentEssentialSettings.tsx | 117 ++++++++++++++++- src/renderer/src/types/agent.ts | 1 + 13 files changed, 389 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 76ddfa2ee..d4fd396f8 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -1,5 +1,6 @@ import { Button, + Chip, cn, Form, Input, @@ -10,17 +11,19 @@ import { ModalHeader, Select, SelectedItemProps, + SelectedItems, SelectItem, Textarea, useDisclosure } from '@heroui/react' import { loggerService } from '@logger' +import type { Selection } from '@react-types/shared' import ClaudeIcon from '@renderer/assets/images/models/claude.png' import { getModelLogo } from '@renderer/config/models' import { useAgents } from '@renderer/hooks/agents/useAgents' import { useApiModels } from '@renderer/hooks/agents/useModels' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' -import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, UpdateAgentForm } from '@renderer/types' +import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, Tool, UpdateAgentForm } from '@renderer/types' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -37,17 +40,20 @@ interface AgentTypeOption extends BaseOption { type Option = AgentTypeOption | ModelOption -const buildAgentForm = (existing?: AgentEntity): BaseAgentForm => ({ +type AgentWithTools = AgentEntity & { tools?: Tool[] } + +const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({ type: existing?.type ?? 'claude-code', name: existing?.name ?? 'Claude Code', description: existing?.description, instructions: existing?.instructions, model: existing?.model ?? 'claude-4-sonnet', - accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [] + accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [], + allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [] }) interface BaseProps { - agent?: AgentEntity + agent?: AgentWithTools } interface TriggerProps extends BaseProps { @@ -83,7 +89,7 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o const updateAgent = useUpdateAgent() // hard-coded. We only support anthropic for now. const { models } = useApiModels({ providerType: 'anthropic' }) - const isEditing = (agent?: AgentEntity) => agent !== undefined + const isEditing = (agent?: AgentWithTools) => agent !== undefined const [form, setForm] = useState(() => buildAgentForm(agent)) @@ -93,6 +99,26 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o } }, [agent, isOpen]) + const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools]) + const selectedToolKeys = useMemo(() => new Set(form.allowed_tools), [form.allowed_tools]) + + useEffect(() => { + if (!availableTools.length) { + return + } + + setForm((prev) => { + const validTools = prev.allowed_tools.filter((id) => availableTools.some((tool) => tool.id === id)) + if (validTools.length === prev.allowed_tools.length) { + return prev + } + return { + ...prev, + allowed_tools: validTools + } + }) + }, [availableTools]) + // add supported agents type here. const agentConfig = useMemo( () => @@ -160,6 +186,48 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o })) }, []) + const onAllowedToolsChange = useCallback( + (keys: Selection) => { + setForm((prev) => { + if (keys === 'all') { + return { + ...prev, + allowed_tools: availableTools.map((tool) => tool.id) + } + } + + const next = Array.from(keys).map(String) + const filtered = availableTools.length + ? next.filter((id) => availableTools.some((tool) => tool.id === id)) + : next + + return { + ...prev, + allowed_tools: filtered + } + }) + }, + [availableTools] + ) + + const renderSelectedTools = useCallback( + (items: SelectedItems) => { + if (!items.length) { + return null + } + return ( +
+ {items.map((item) => ( + + {item.data?.name ?? item.textValue ?? item.key} + + ))} +
+ ) + }, + [] + ) + const addAccessiblePath = useCallback(async () => { try { const selected = await window.api.file.selectFolder() @@ -246,7 +314,8 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o description: form.description, instructions: form.instructions, model: form.model, - accessible_paths: [...form.accessible_paths] + accessible_paths: [...form.accessible_paths], + allowed_tools: [...form.allowed_tools] } satisfies UpdateAgentForm updateAgent(updatePayload) @@ -258,7 +327,8 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o description: form.description, instructions: form.instructions, model: form.model, - accessible_paths: [...form.accessible_paths] + accessible_paths: [...form.accessible_paths], + allowed_tools: [...form.allowed_tools] } satisfies AddAgentForm addAgent(newAgent) logger.debug('Added agent', newAgent) @@ -276,6 +346,7 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o form.description, form.instructions, form.accessible_paths, + form.allowed_tools, agent, onClose, t, @@ -347,6 +418,31 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o value={form.description ?? ''} onValueChange={onDescChange} /> +
diff --git a/src/renderer/src/components/Popups/agent/SessionModal.tsx b/src/renderer/src/components/Popups/agent/SessionModal.tsx index 64f8b1e62..4ce7196ca 100644 --- a/src/renderer/src/components/Popups/agent/SessionModal.tsx +++ b/src/renderer/src/components/Popups/agent/SessionModal.tsx @@ -1,5 +1,6 @@ import { Button, + Chip, cn, Form, Input, @@ -16,12 +17,20 @@ import { useDisclosure } from '@heroui/react' import { loggerService } from '@logger' +import type { Selection } from '@react-types/shared' import { getModelLogo } from '@renderer/config/models' import { useAgent } from '@renderer/hooks/agents/useAgent' import { useApiModels } from '@renderer/hooks/agents/useModels' import { useSessions } from '@renderer/hooks/agents/useSessions' import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' -import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types' +import { + AgentEntity, + AgentSessionEntity, + BaseSessionForm, + CreateSessionForm, + Tool, + UpdateSessionForm +} from '@renderer/types' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -32,7 +41,10 @@ const logger = loggerService.withContext('SessionAgentPopup') type Option = ModelOption -const buildSessionForm = (existing?: AgentSessionEntity, agent?: AgentEntity): BaseSessionForm => ({ +type AgentWithTools = AgentEntity & { tools?: Tool[] } +type SessionWithTools = AgentSessionEntity & { tools?: Tool[] } + +const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): BaseSessionForm => ({ name: existing?.name ?? agent?.name ?? 'Claude Code', description: existing?.description ?? agent?.description, instructions: existing?.instructions ?? agent?.instructions, @@ -41,12 +53,17 @@ const buildSessionForm = (existing?: AgentSessionEntity, agent?: AgentEntity): B ? [...existing.accessible_paths] : agent?.accessible_paths ? [...agent.accessible_paths] + : [], + allowed_tools: existing?.allowed_tools + ? [...existing.allowed_tools] + : agent?.allowed_tools + ? [...agent.allowed_tools] : [] }) interface BaseProps { agentId: string - session?: AgentSessionEntity + session?: SessionWithTools onSessionCreated?: (session: AgentSessionEntity) => void } @@ -102,6 +119,27 @@ export const SessionModal: React.FC = ({ } }, [session, agent, isOpen]) + const availableTools = useMemo(() => session?.tools ?? agent?.tools ?? [], [agent?.tools, session?.tools]) + const selectedToolKeys = useMemo(() => new Set(form.allowed_tools ?? []), [form.allowed_tools]) + + useEffect(() => { + if (!availableTools.length) { + return + } + + setForm((prev) => { + const allowed = prev.allowed_tools ?? [] + const validTools = allowed.filter((id) => availableTools.some((tool) => tool.id === id)) + if (validTools.length === allowed.length) { + return prev + } + return { + ...prev, + allowed_tools: validTools + } + }) + }, [availableTools]) + const Item = useCallback(({ item }: { item: SelectedItemProps }) =>