mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
Merge branch 'feat/agents-new' of https://github.com/CherryHQ/cherry-studio into feat/agents-new
This commit is contained in:
commit
f65149af19
@ -1,5 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
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 { ListAgentsResponse, type ReplaceAgentRequest, type UpdateAgentRequest } from '@types'
|
||||||
import { Request, Response } from 'express'
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
@ -20,7 +20,8 @@ const modelValidationErrorBody = (error: AgentModelValidationError) => ({
|
|||||||
* /v1/agents:
|
* /v1/agents:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create a new agent
|
* 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]
|
* tags: [Agents]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
@ -55,8 +56,37 @@ export const createAgent = async (req: Request, res: Response): Promise<Response
|
|||||||
|
|
||||||
const agent = await agentService.createAgent(req.body)
|
const agent = await agentService.createAgent(req.body)
|
||||||
|
|
||||||
|
try {
|
||||||
logger.info(`Agent created successfully: ${agent.id}`)
|
logger.info(`Agent created successfully: ${agent.id}`)
|
||||||
|
logger.info(`Creating default session for new agent: ${agent.id}`)
|
||||||
|
|
||||||
|
await sessionService.createSession(agent.id, {})
|
||||||
|
|
||||||
|
logger.info(`Default session created for agent: ${agent.id}`)
|
||||||
return res.status(201).json(agent)
|
return res.status(201).json(agent)
|
||||||
|
} catch (sessionError: any) {
|
||||||
|
logger.error('Failed to create default session for new agent, rolling back agent creation', {
|
||||||
|
agentId: agent.id,
|
||||||
|
error: sessionError
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await agentService.deleteAgent(agent.id)
|
||||||
|
} catch (rollbackError: any) {
|
||||||
|
logger.error('Failed to roll back agent after session creation failure', {
|
||||||
|
agentId: agent.id,
|
||||||
|
error: rollbackError
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: `Failed to create default session for agent: ${sessionError.message}`,
|
||||||
|
type: 'internal_error',
|
||||||
|
code: 'agent_session_creation_failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error instanceof AgentModelValidationError) {
|
if (error instanceof AgentModelValidationError) {
|
||||||
logger.warn('Agent model validation error during create:', {
|
logger.warn('Agent model validation error during create:', {
|
||||||
|
|||||||
@ -285,6 +285,32 @@ export const deleteSession = async (req: Request, res: Response): Promise<Respon
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Session deleted successfully: ${sessionId}`)
|
logger.info(`Session deleted successfully: ${sessionId}`)
|
||||||
|
|
||||||
|
const { total } = await sessionService.listSessions(agentId, { limit: 1 })
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
logger.info(`No remaining sessions for agent ${agentId}, creating default session`)
|
||||||
|
try {
|
||||||
|
const fallbackSession = await sessionService.createSession(agentId, {})
|
||||||
|
logger.info('Default session created after deleting last session', {
|
||||||
|
agentId,
|
||||||
|
sessionId: fallbackSession?.id
|
||||||
|
})
|
||||||
|
} catch (recoveryError: any) {
|
||||||
|
logger.error('Failed to recreate session after deleting last session', {
|
||||||
|
agentId,
|
||||||
|
error: recoveryError
|
||||||
|
})
|
||||||
|
return res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: `Failed to recreate session after deletion: ${recoveryError.message}`,
|
||||||
|
type: 'internal_error',
|
||||||
|
code: 'session_recovery_failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(204).send()
|
return res.status(204).send()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Error deleting session:', error)
|
logger.error('Error deleting session:', error)
|
||||||
|
|||||||
@ -68,15 +68,23 @@ class TextStreamAccumulator {
|
|||||||
}
|
}
|
||||||
case 'tool-call':
|
case 'tool-call':
|
||||||
if (part.toolCallId) {
|
if (part.toolCallId) {
|
||||||
|
const legacyPart = part as typeof part & {
|
||||||
|
args?: unknown
|
||||||
|
providerMetadata?: { raw?: { input?: unknown } }
|
||||||
|
}
|
||||||
this.toolCalls.set(part.toolCallId, {
|
this.toolCalls.set(part.toolCallId, {
|
||||||
toolName: part.toolName,
|
toolName: part.toolName,
|
||||||
input: part.input ?? part.providerMetadata?.raw?.input
|
input: part.input ?? legacyPart.args ?? legacyPart.providerMetadata?.raw?.input
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'tool-result':
|
case 'tool-result':
|
||||||
if (part.toolCallId) {
|
if (part.toolCallId) {
|
||||||
this.toolResults.set(part.toolCallId, part.output)
|
const legacyPart = part as typeof part & {
|
||||||
|
result?: unknown
|
||||||
|
providerMetadata?: { raw?: unknown }
|
||||||
|
}
|
||||||
|
this.toolResults.set(part.toolCallId, part.output ?? legacyPart.result ?? legacyPart.providerMetadata?.raw)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -29,7 +29,10 @@ export class SessionService extends BaseService {
|
|||||||
await BaseService.initialize()
|
await BaseService.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(agentId: string, req: CreateSessionRequest): Promise<GetAgentSessionResponse | null> {
|
async createSession(
|
||||||
|
agentId: string,
|
||||||
|
req: Partial<CreateSessionRequest> = {}
|
||||||
|
): Promise<GetAgentSessionResponse | null> {
|
||||||
this.ensureInitialized()
|
this.ensureInitialized()
|
||||||
|
|
||||||
// Validate agent exists - we'll need to import AgentService for this check
|
// Validate agent exists - we'll need to import AgentService for this check
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
cn,
|
cn,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@ -10,17 +11,27 @@ import {
|
|||||||
ModalHeader,
|
ModalHeader,
|
||||||
Select,
|
Select,
|
||||||
SelectedItemProps,
|
SelectedItemProps,
|
||||||
|
SelectedItems,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Textarea,
|
Textarea,
|
||||||
useDisclosure
|
useDisclosure
|
||||||
} from '@heroui/react'
|
} from '@heroui/react'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import type { Selection } from '@react-types/shared'
|
||||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||||
import { getModelLogo } from '@renderer/config/models'
|
import { getModelLogo } from '@renderer/config/models'
|
||||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
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 { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -37,17 +48,20 @@ interface AgentTypeOption extends BaseOption {
|
|||||||
|
|
||||||
type Option = AgentTypeOption | ModelOption
|
type Option = AgentTypeOption | ModelOption
|
||||||
|
|
||||||
const buildAgentForm = (existing?: AgentEntity): BaseAgentForm => ({
|
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||||
|
|
||||||
|
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||||
type: existing?.type ?? 'claude-code',
|
type: existing?.type ?? 'claude-code',
|
||||||
name: existing?.name ?? 'Claude Code',
|
name: existing?.name ?? 'Claude Code',
|
||||||
description: existing?.description,
|
description: existing?.description,
|
||||||
instructions: existing?.instructions,
|
instructions: existing?.instructions,
|
||||||
model: existing?.model ?? 'claude-4-sonnet',
|
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 {
|
interface BaseProps {
|
||||||
agent?: AgentEntity
|
agent?: AgentWithTools
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TriggerProps extends BaseProps {
|
interface TriggerProps extends BaseProps {
|
||||||
@ -83,7 +97,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
const updateAgent = useUpdateAgent()
|
const updateAgent = useUpdateAgent()
|
||||||
// hard-coded. We only support anthropic for now.
|
// hard-coded. We only support anthropic for now.
|
||||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||||
const isEditing = (agent?: AgentEntity) => agent !== undefined
|
const isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||||
|
|
||||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||||
|
|
||||||
@ -93,6 +107,26 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
}
|
}
|
||||||
}, [agent, isOpen])
|
}, [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.
|
// add supported agents type here.
|
||||||
const agentConfig = useMemo(
|
const agentConfig = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -160,6 +194,45 @@ export const AgentModal: React.FC<Props> = ({ 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<Tool>) => {
|
||||||
|
if (!items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||||
|
{item.data?.name ?? item.textValue ?? item.key}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const addAccessiblePath = useCallback(async () => {
|
const addAccessiblePath = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const selected = await window.api.file.selectFolder()
|
const selected = await window.api.file.selectFolder()
|
||||||
@ -246,7 +319,8 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
description: form.description,
|
description: form.description,
|
||||||
instructions: form.instructions,
|
instructions: form.instructions,
|
||||||
model: form.model,
|
model: form.model,
|
||||||
accessible_paths: [...form.accessible_paths]
|
accessible_paths: [...form.accessible_paths],
|
||||||
|
allowed_tools: [...form.allowed_tools]
|
||||||
} satisfies UpdateAgentForm
|
} satisfies UpdateAgentForm
|
||||||
|
|
||||||
updateAgent(updatePayload)
|
updateAgent(updatePayload)
|
||||||
@ -258,7 +332,8 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
description: form.description,
|
description: form.description,
|
||||||
instructions: form.instructions,
|
instructions: form.instructions,
|
||||||
model: form.model,
|
model: form.model,
|
||||||
accessible_paths: [...form.accessible_paths]
|
accessible_paths: [...form.accessible_paths],
|
||||||
|
allowed_tools: [...form.allowed_tools]
|
||||||
} satisfies AddAgentForm
|
} satisfies AddAgentForm
|
||||||
addAgent(newAgent)
|
addAgent(newAgent)
|
||||||
logger.debug('Added agent', newAgent)
|
logger.debug('Added agent', newAgent)
|
||||||
@ -276,6 +351,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
form.description,
|
form.description,
|
||||||
form.instructions,
|
form.instructions,
|
||||||
form.accessible_paths,
|
form.accessible_paths,
|
||||||
|
form.allowed_tools,
|
||||||
agent,
|
agent,
|
||||||
onClose,
|
onClose,
|
||||||
t,
|
t,
|
||||||
@ -347,6 +423,31 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
|||||||
value={form.description ?? ''}
|
value={form.description ?? ''}
|
||||||
onValueChange={onDescChange}
|
onValueChange={onDescChange}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={selectedToolKeys}
|
||||||
|
onSelectionChange={onAllowedToolsChange}
|
||||||
|
label={t('agent.session.allowed_tools.label')}
|
||||||
|
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||||
|
description={
|
||||||
|
availableTools.length
|
||||||
|
? t('agent.session.allowed_tools.helper')
|
||||||
|
: t('agent.session.allowed_tools.empty')
|
||||||
|
}
|
||||||
|
isDisabled={!availableTools.length}
|
||||||
|
items={availableTools}
|
||||||
|
renderValue={renderSelectedTools}>
|
||||||
|
{(tool) => (
|
||||||
|
<SelectItem key={tool.id} textValue={tool.name}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">{tool.name}</span>
|
||||||
|
{tool.description ? (
|
||||||
|
<span className="text-xs text-foreground-500">{tool.description}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-medium text-foreground text-sm">
|
<span className="font-medium text-foreground text-sm">
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
cn,
|
cn,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@ -16,12 +17,20 @@ import {
|
|||||||
useDisclosure
|
useDisclosure
|
||||||
} from '@heroui/react'
|
} from '@heroui/react'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import type { Selection } from '@react-types/shared'
|
||||||
import { getModelLogo } from '@renderer/config/models'
|
import { getModelLogo } from '@renderer/config/models'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
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 { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@ -32,7 +41,10 @@ const logger = loggerService.withContext('SessionAgentPopup')
|
|||||||
|
|
||||||
type Option = ModelOption
|
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',
|
name: existing?.name ?? agent?.name ?? 'Claude Code',
|
||||||
description: existing?.description ?? agent?.description,
|
description: existing?.description ?? agent?.description,
|
||||||
instructions: existing?.instructions ?? agent?.instructions,
|
instructions: existing?.instructions ?? agent?.instructions,
|
||||||
@ -41,12 +53,17 @@ const buildSessionForm = (existing?: AgentSessionEntity, agent?: AgentEntity): B
|
|||||||
? [...existing.accessible_paths]
|
? [...existing.accessible_paths]
|
||||||
: agent?.accessible_paths
|
: agent?.accessible_paths
|
||||||
? [...agent.accessible_paths]
|
? [...agent.accessible_paths]
|
||||||
|
: [],
|
||||||
|
allowed_tools: existing?.allowed_tools
|
||||||
|
? [...existing.allowed_tools]
|
||||||
|
: agent?.allowed_tools
|
||||||
|
? [...agent.allowed_tools]
|
||||||
: []
|
: []
|
||||||
})
|
})
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
agentId: string
|
agentId: string
|
||||||
session?: AgentSessionEntity
|
session?: SessionWithTools
|
||||||
onSessionCreated?: (session: AgentSessionEntity) => void
|
onSessionCreated?: (session: AgentSessionEntity) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +119,27 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [session, agent, isOpen])
|
}, [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<BaseOption> }) => <Option option={item.data} />, [])
|
const Item = useCallback(({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />, [])
|
||||||
|
|
||||||
const renderOption = useCallback(
|
const renderOption = useCallback(
|
||||||
@ -130,6 +168,50 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
}))
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onAllowedToolsChange = useCallback(
|
||||||
|
(keys: Selection) => {
|
||||||
|
setForm((prev) => {
|
||||||
|
const existing = prev.allowed_tools ?? []
|
||||||
|
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
|
||||||
|
|
||||||
|
if (existing.length === filtered.length && existing.every((id) => filtered.includes(id))) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
allowed_tools: filtered
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[availableTools]
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||||
|
if (!items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||||
|
{item.data?.name ?? item.textValue ?? item.key}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
// mocked data. not final version
|
// mocked data. not final version
|
||||||
return (models ?? []).map((model) => ({
|
return (models ?? []).map((model) => ({
|
||||||
@ -183,7 +265,8 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
description: form.description,
|
description: form.description,
|
||||||
instructions: form.instructions,
|
instructions: form.instructions,
|
||||||
model: form.model,
|
model: form.model,
|
||||||
accessible_paths: [...form.accessible_paths]
|
accessible_paths: [...form.accessible_paths],
|
||||||
|
allowed_tools: [...(form.allowed_tools ?? [])]
|
||||||
} satisfies UpdateSessionForm
|
} satisfies UpdateSessionForm
|
||||||
|
|
||||||
updateSession(updatePayload)
|
updateSession(updatePayload)
|
||||||
@ -194,7 +277,8 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
description: form.description,
|
description: form.description,
|
||||||
instructions: form.instructions,
|
instructions: form.instructions,
|
||||||
model: form.model,
|
model: form.model,
|
||||||
accessible_paths: [...form.accessible_paths]
|
accessible_paths: [...form.accessible_paths],
|
||||||
|
allowed_tools: [...(form.allowed_tools ?? [])]
|
||||||
} satisfies CreateSessionForm
|
} satisfies CreateSessionForm
|
||||||
const createdSession = await createSession(newSession)
|
const createdSession = await createSession(newSession)
|
||||||
if (createdSession) {
|
if (createdSession) {
|
||||||
@ -215,6 +299,7 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
form.description,
|
form.description,
|
||||||
form.instructions,
|
form.instructions,
|
||||||
form.accessible_paths,
|
form.accessible_paths,
|
||||||
|
form.allowed_tools,
|
||||||
session,
|
session,
|
||||||
onClose,
|
onClose,
|
||||||
onSessionCreated,
|
onSessionCreated,
|
||||||
@ -274,6 +359,31 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
value={form.description ?? ''}
|
value={form.description ?? ''}
|
||||||
onValueChange={onDescChange}
|
onValueChange={onDescChange}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={selectedToolKeys}
|
||||||
|
onSelectionChange={onAllowedToolsChange}
|
||||||
|
label={t('agent.session.allowed_tools.label')}
|
||||||
|
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||||
|
description={
|
||||||
|
availableTools.length
|
||||||
|
? t('agent.session.allowed_tools.helper')
|
||||||
|
: t('agent.session.allowed_tools.empty')
|
||||||
|
}
|
||||||
|
isDisabled={!availableTools.length}
|
||||||
|
items={availableTools}
|
||||||
|
renderValue={renderSelectedTools}>
|
||||||
|
{(tool) => (
|
||||||
|
<SelectItem key={tool.id} textValue={tool.name}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">{tool.name}</span>
|
||||||
|
{tool.description ? (
|
||||||
|
<span className="text-xs text-foreground-500">{tool.description}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="w-full">
|
<ModalFooter className="w-full">
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Add a session"
|
"title": "Add a session"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "No tools available for this agent.",
|
||||||
|
"helper": "Choose which tools are pre-approved. Unselected tools require approval before use.",
|
||||||
|
"label": "Allowed tools",
|
||||||
|
"placeholder": "Select allowed tools"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Failed to add a session"
|
"failed": "Failed to add a session"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "添加会话"
|
"title": "添加会话"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "当前智能体暂无可用工具。",
|
||||||
|
"helper": "选择预先授权的工具,未选中的工具在使用时需要手动审批。",
|
||||||
|
"label": "允许的工具",
|
||||||
|
"placeholder": "选择允许使用的工具"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "添加会话失败"
|
"failed": "添加会话失败"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "新增會議"
|
"title": "新增會議"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "目前此代理沒有可用的工具。",
|
||||||
|
"helper": "選擇預先授權的工具,未選取的工具在使用時需要手動審批。",
|
||||||
|
"label": "允許的工具",
|
||||||
|
"placeholder": "選擇允許使用的工具"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "無法新增工作階段"
|
"failed": "無法新增工作階段"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Προσθήκη συνεδρίας"
|
"title": "Προσθήκη συνεδρίας"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "Δεν υπάρχουν διαθέσιμα εργαλεία για αυτόν τον agent.",
|
||||||
|
"helper": "Επιλέξτε ποια εργαλεία είναι προεγκεκριμένα. Τα μη επιλεγμένα θα χρειαστούν έγκριση πριν από τη χρήση.",
|
||||||
|
"label": "Επιτρεπόμενα εργαλεία",
|
||||||
|
"placeholder": "Επιλέξτε επιτρεπόμενα εργαλεία"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Αποτυχία προσθήκης συνεδρίας"
|
"failed": "Αποτυχία προσθήκης συνεδρίας"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Agregar una sesión"
|
"title": "Agregar una sesión"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "No hay herramientas disponibles para este agente.",
|
||||||
|
"helper": "Elige qué herramientas quedan preaprobadas. Las no seleccionadas requerirán aprobación manual antes de usarse.",
|
||||||
|
"label": "Herramientas permitidas",
|
||||||
|
"placeholder": "Selecciona las herramientas permitidas"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Error al añadir una sesión"
|
"failed": "Error al añadir una sesión"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Ajouter une session"
|
"title": "Ajouter une session"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "Aucun outil disponible pour cet agent.",
|
||||||
|
"helper": "Choisissez les outils préapprouvés. Les outils non sélectionnés nécessiteront une approbation avant utilisation.",
|
||||||
|
"label": "Outils autorisés",
|
||||||
|
"placeholder": "Sélectionner les outils autorisés"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Échec de l'ajout d'une session"
|
"failed": "Échec de l'ajout d'une session"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "セッションを追加"
|
"title": "セッションを追加"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "このエージェントが利用できるツールはありません。",
|
||||||
|
"helper": "事前承認済みのツールを選択します。未選択のツールは使用時に承認が必要になります。",
|
||||||
|
"label": "許可されたツール",
|
||||||
|
"placeholder": "許可するツールを選択"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "セッションの追加に失敗しました"
|
"failed": "セッションの追加に失敗しました"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Adicionar uma sessão"
|
"title": "Adicionar uma sessão"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "Não há ferramentas disponíveis para este agente.",
|
||||||
|
"helper": "Escolha quais ferramentas ficam pré-autorizadas. As não selecionadas exigirão aprovação antes do uso.",
|
||||||
|
"label": "Ferramentas permitidas",
|
||||||
|
"placeholder": "Selecione as ferramentas permitidas"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Falha ao adicionar uma sessão"
|
"failed": "Falha ao adicionar uma sessão"
|
||||||
|
|||||||
@ -44,6 +44,12 @@
|
|||||||
"add": {
|
"add": {
|
||||||
"title": "Добавить сеанс"
|
"title": "Добавить сеанс"
|
||||||
},
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"empty": "Для этого агента нет доступных инструментов.",
|
||||||
|
"helper": "Выберите инструменты с предварительным допуском. Неотмеченные инструменты потребуют подтверждения перед использованием.",
|
||||||
|
"label": "Разрешённые инструменты",
|
||||||
|
"placeholder": "Выберите разрешённые инструменты"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Не удалось добавить сеанс"
|
"failed": "Не удалось добавить сеанс"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { FileText } from 'lucide-react'
|
|||||||
import { ToolTitle } from './GenericTools'
|
import { ToolTitle } from './GenericTools'
|
||||||
import type { WriteToolInput, WriteToolOutput } from './types'
|
import type { WriteToolInput, WriteToolOutput } from './types'
|
||||||
|
|
||||||
export function WriteTool({ input, output }: { input: WriteToolInput; output?: WriteToolOutput }) {
|
export function WriteTool({ input }: { input: WriteToolInput; output?: WriteToolOutput }) {
|
||||||
return (
|
return (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key="tool"
|
key="tool"
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Alert, Button, Spinner } from '@heroui/react'
|
import { Alert, Button, Spinner } from '@heroui/react'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
@ -27,7 +26,6 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agent } = useAgent(agentId)
|
const { agent } = useAgent(agentId)
|
||||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
||||||
const updateSession = useUpdateSession(agentId)
|
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeSessionId, sessionWaiting } = chat
|
const { activeSessionId, sessionWaiting } = chat
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -70,7 +68,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
}
|
}
|
||||||
dispatch(setSessionWaitingAction({ id, value: false }))
|
dispatch(setSessionWaitingAction({ id, value: false }))
|
||||||
},
|
},
|
||||||
[agentId, deleteSession, dispatch, sessions]
|
[agentId, deleteSession, dispatch, sessions, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentActiveSessionId = activeSessionId[agentId]
|
const currentActiveSessionId = activeSessionId[agentId]
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Button, Tooltip } from '@heroui/react'
|
import { Button, Chip, Select as HeroSelect, SelectedItems, SelectItem, Tooltip } from '@heroui/react'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import type { Selection } from '@react-types/shared'
|
||||||
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||||
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
import { GetAgentResponse, Tool, UpdateAgentForm } from '@renderer/types'
|
||||||
import { Input, Select } from 'antd'
|
import { Input, Select } from 'antd'
|
||||||
import { DefaultOptionType } from 'antd/es/select'
|
import { DefaultOptionType } from 'antd/es/select'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { FC, useCallback, useMemo, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||||
@ -15,7 +16,7 @@ import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './sh
|
|||||||
const logger = loggerService.withContext('AgentEssentialSettings')
|
const logger = loggerService.withContext('AgentEssentialSettings')
|
||||||
|
|
||||||
interface AgentEssentialSettingsProps {
|
interface AgentEssentialSettingsProps {
|
||||||
agent: AgentEntity | undefined | null
|
agent: GetAgentResponse | undefined | null
|
||||||
update: ReturnType<typeof useUpdateAgent>
|
update: ReturnType<typeof useUpdateAgent>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +24,9 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
||||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||||
|
const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools])
|
||||||
|
const [allowedToolIds, setAllowedToolIds] = useState<string[]>([])
|
||||||
|
const selectedToolKeys = useMemo<Selection>(() => new Set<string>(allowedToolIds), [allowedToolIds])
|
||||||
|
|
||||||
const updateName = (name: string) => {
|
const updateName = (name: string) => {
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
@ -42,6 +46,14 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
[agent, update]
|
[agent, update]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updateAllowedTools = useCallback(
|
||||||
|
(allowed_tools: UpdateAgentForm['allowed_tools']) => {
|
||||||
|
if (!agent) return
|
||||||
|
update({ id: agent.id, allowed_tools })
|
||||||
|
},
|
||||||
|
[agent, update]
|
||||||
|
)
|
||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
return models.map((model) => ({
|
return models.map((model) => ({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
@ -49,6 +61,27 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
})) satisfies DefaultOptionType[]
|
})) satisfies DefaultOptionType[]
|
||||||
}, [models])
|
}, [models])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agent) {
|
||||||
|
setAllowedToolIds((prev) => (prev.length === 0 ? prev : []))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = agent.allowed_tools ?? []
|
||||||
|
const filtered = availableTools.length
|
||||||
|
? allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||||
|
: allowed
|
||||||
|
|
||||||
|
setAllowedToolIds((prev) => {
|
||||||
|
const prevSet = new Set(prev)
|
||||||
|
const isSame = filtered.length === prevSet.size && filtered.every((id) => prevSet.has(id))
|
||||||
|
if (isSame) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
}, [agent, availableTools])
|
||||||
|
|
||||||
const addAccessiblePath = useCallback(async () => {
|
const addAccessiblePath = useCallback(async () => {
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
|
|
||||||
@ -83,6 +116,53 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
[agent, t, updateAccessiblePaths]
|
[agent, t, updateAccessiblePaths]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||||
|
if (!items.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||||
|
{item.data?.name ?? item.textValue ?? item.key}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onAllowedToolsChange = useCallback(
|
||||||
|
(keys: Selection) => {
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
const nextIds = keys === 'all' ? availableTools.map((tool) => tool.id) : Array.from(keys).map(String)
|
||||||
|
const filtered = availableTools.length
|
||||||
|
? nextIds.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||||
|
: nextIds
|
||||||
|
|
||||||
|
setAllowedToolIds((prev) => {
|
||||||
|
const prevSet = new Set(prev)
|
||||||
|
const isSame = filtered.length === prevSet.size && filtered.every((id) => prevSet.has(id))
|
||||||
|
if (isSame) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
const previous = agent.allowed_tools ?? []
|
||||||
|
const previousSet = new Set(previous)
|
||||||
|
const isSameSelection = filtered.length === previousSet.size && filtered.every((id) => previousSet.has(id))
|
||||||
|
|
||||||
|
if (isSameSelection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllowedTools(filtered)
|
||||||
|
},
|
||||||
|
[agent, availableTools, updateAllowedTools]
|
||||||
|
)
|
||||||
|
|
||||||
if (!agent) return null
|
if (!agent) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -117,6 +197,31 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
|||||||
placeholder={t('common.placeholders.select.model')}
|
placeholder={t('common.placeholders.select.model')}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem>
|
||||||
|
<SettingsTitle>{t('agent.session.allowed_tools.label')}</SettingsTitle>
|
||||||
|
<HeroSelect
|
||||||
|
aria-label={t('agent.session.allowed_tools.label')}
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={selectedToolKeys}
|
||||||
|
onSelectionChange={onAllowedToolsChange}
|
||||||
|
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||||
|
description={
|
||||||
|
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||||
|
}
|
||||||
|
isDisabled={!availableTools.length}
|
||||||
|
items={availableTools}
|
||||||
|
renderValue={renderSelectedTools}
|
||||||
|
className="max-w-xl">
|
||||||
|
{(tool) => (
|
||||||
|
<SelectItem key={tool.id} textValue={tool.name}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">{tool.name}</span>
|
||||||
|
{tool.description ? <span className="text-xs text-foreground-500">{tool.description}</span> : null}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</HeroSelect>
|
||||||
|
</SettingsItem>
|
||||||
<SettingsItem>
|
<SettingsItem>
|
||||||
<SettingsTitle
|
<SettingsTitle
|
||||||
actions={
|
actions={
|
||||||
|
|||||||
@ -169,6 +169,7 @@ export type BaseAgentForm = {
|
|||||||
instructions?: string
|
instructions?: string
|
||||||
model: string
|
model: string
|
||||||
accessible_paths: string[]
|
accessible_paths: string[]
|
||||||
|
allowed_tools: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddAgentForm = Omit<BaseAgentForm, 'id'> & { id?: never }
|
export type AddAgentForm = Omit<BaseAgentForm, 'id'> & { id?: never }
|
||||||
|
|||||||
@ -108,7 +108,12 @@ export function openAIToolsToMcpTool(
|
|||||||
export async function callBuiltInTool(toolResponse: MCPToolResponse): Promise<MCPCallToolResponse | undefined> {
|
export async function callBuiltInTool(toolResponse: MCPToolResponse): Promise<MCPCallToolResponse | undefined> {
|
||||||
logger.info(`[BuiltIn] Calling Built-in Tool: ${toolResponse.tool.name}`, toolResponse.tool)
|
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
|
const thought = toolResponse.arguments?.thought
|
||||||
return {
|
return {
|
||||||
isError: false,
|
isError: false,
|
||||||
|
|||||||
@ -14,13 +14,21 @@ vi.mock('@logger', async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('axios', () => ({
|
vi.mock('axios', () => {
|
||||||
default: {
|
const defaultAxiosMock = {
|
||||||
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
|
get: vi.fn().mockResolvedValue({ data: {} }), // Mocking axios GET request
|
||||||
post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request
|
post: vi.fn().mockResolvedValue({ data: {} }) // Mocking axios POST request
|
||||||
// You can add other axios methods like put, delete etc. as needed
|
// 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', {
|
vi.stubGlobal('electron', {
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user