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 diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 76ddfa2ee..8d0a41e98 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,27 @@ 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 +48,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 +97,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 +107,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 +194,45 @@ 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 +319,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 +332,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 +351,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 +423,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..3682b7eeb 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 }) =>