mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
Merge branch 'feat/agents-new' of https://github.com/CherryHQ/cherry-studio into feat/agents-new
This commit is contained in:
commit
34ab01e0a1
@ -228,3 +228,53 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteMessage = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId, sessionId, messageId: messageIdParam } = req.params
|
||||
const messageId = Number(messageIdParam)
|
||||
|
||||
await verifyAgentAndSession(agentId, sessionId)
|
||||
|
||||
const deleted = await sessionMessageService.deleteSessionMessage(sessionId, messageId)
|
||||
|
||||
if (!deleted) {
|
||||
logger.warn(`Message ${messageId} not found for session ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Message not found for this session',
|
||||
type: 'not_found',
|
||||
code: 'session_message_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Message ${messageId} deleted successfully for session ${sessionId}`)
|
||||
return res.status(204).send()
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) {
|
||||
logger.warn('Delete message failed - missing resource', {
|
||||
agentId: req.params.agentId,
|
||||
sessionId: req.params.sessionId,
|
||||
messageId: req.params.messageId,
|
||||
error
|
||||
})
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: error.message,
|
||||
type: 'not_found',
|
||||
code: error.code ?? 'session_message_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.error('Error deleting session message:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to delete session message',
|
||||
type: 'internal_error',
|
||||
code: 'session_message_delete_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
validateSession,
|
||||
validateSessionId,
|
||||
validateSessionMessage,
|
||||
validateSessionMessageId,
|
||||
validateSessionReplace,
|
||||
validateSessionUpdate
|
||||
} from './validators'
|
||||
@ -362,7 +363,7 @@ const agentsRouter = express.Router()
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents:
|
||||
* /agents:
|
||||
* post:
|
||||
* summary: Create a new agent
|
||||
* tags: [Agents]
|
||||
@ -391,7 +392,7 @@ agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.crea
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents:
|
||||
* /agents:
|
||||
* get:
|
||||
* summary: List all agents with pagination
|
||||
* tags: [Agents]
|
||||
@ -429,7 +430,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* /agents/{agentId}:
|
||||
* get:
|
||||
* summary: Get agent by ID
|
||||
* tags: [Agents]
|
||||
@ -457,7 +458,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.
|
||||
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* /agents/{agentId}:
|
||||
* put:
|
||||
* summary: Replace agent (full update)
|
||||
* tags: [Agents]
|
||||
@ -497,7 +498,7 @@ agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHand
|
||||
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* /agents/{agentId}:
|
||||
* patch:
|
||||
* summary: Update agent (partial update)
|
||||
* tags: [Agents]
|
||||
@ -537,7 +538,7 @@ agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValid
|
||||
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* /agents/{agentId}:
|
||||
* delete:
|
||||
* summary: Delete agent
|
||||
* tags: [Agents]
|
||||
@ -567,7 +568,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
// Session CRUD routes (nested under agent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions:
|
||||
* /agents/{agentId}/sessions:
|
||||
* post:
|
||||
* summary: Create a new session for an agent
|
||||
* tags: [Sessions]
|
||||
@ -608,7 +609,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions:
|
||||
* /agents/{agentId}/sessions:
|
||||
* get:
|
||||
* summary: List sessions for an agent
|
||||
* tags: [Sessions]
|
||||
@ -657,7 +658,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* /agents/{agentId}/sessions/{sessionId}:
|
||||
* get:
|
||||
* summary: Get session by ID
|
||||
* tags: [Sessions]
|
||||
@ -691,7 +692,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* /agents/{agentId}/sessions/{sessionId}:
|
||||
* put:
|
||||
* summary: Replace session (full update)
|
||||
* tags: [Sessions]
|
||||
@ -743,7 +744,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* /agents/{agentId}/sessions/{sessionId}:
|
||||
* patch:
|
||||
* summary: Update session (partial update)
|
||||
* tags: [Sessions]
|
||||
@ -795,7 +796,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* /agents/{agentId}/sessions/{sessionId}:
|
||||
* delete:
|
||||
* summary: Delete session
|
||||
* tags: [Sessions]
|
||||
@ -834,7 +835,7 @@ const createMessagesRouter = (): express.Router => {
|
||||
// Message CRUD routes (nested under agent/session)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}/messages:
|
||||
* /agents/{agentId}/sessions/{sessionId}/messages:
|
||||
* post:
|
||||
* summary: Create a new message in a session
|
||||
* tags: [Messages]
|
||||
@ -902,8 +903,45 @@ const createMessagesRouter = (): express.Router => {
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
*/
|
||||
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /agents/{agentId}/sessions/{sessionId}/messages/{messageId}:
|
||||
* delete:
|
||||
* summary: Delete a message from a session
|
||||
* tags: [Messages]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* - in: path
|
||||
* name: messageId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Message ID
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Message deleted successfully
|
||||
* 404:
|
||||
* description: Agent, session, or message not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
messagesRouter.delete('/:messageId', validateSessionMessageId, handleValidationErrors, messageHandlers.deleteMessage)
|
||||
return messagesRouter
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { CreateSessionMessageRequestSchema } from '@types'
|
||||
import { CreateSessionMessageRequestSchema, SessionMessageIdParamSchema } from '@types'
|
||||
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionMessage = createZodValidator({
|
||||
body: CreateSessionMessageRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionMessageId = createZodValidator({
|
||||
params: SessionMessageIdParamSchema
|
||||
})
|
||||
|
||||
@ -102,19 +102,14 @@ class MCPApiService extends EventEmitter {
|
||||
|
||||
async getServerInfo(id: string): Promise<any> {
|
||||
try {
|
||||
logger.silly(`getServerInfo called with id: ${id}`)
|
||||
const server = await this.getServerById(id)
|
||||
if (!server) {
|
||||
logger.warn(`Server with id ${id} not found`)
|
||||
return null
|
||||
}
|
||||
logger.silly(`Returning server info for id ${id}`)
|
||||
|
||||
const client = await mcpService.initClient(server)
|
||||
const tools = await client.listTools()
|
||||
|
||||
logger.silly(`Server with id ${id} info:`, { tools: JSON.stringify(tools.tools) })
|
||||
|
||||
return {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
|
||||
@ -11,7 +11,16 @@ import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { FileMetadata, Notification, OcrProvider, Provider, Shortcut, SupportedOcrFile, ThemeMode } from '@types'
|
||||
import {
|
||||
AgentPersistedMessage,
|
||||
FileMetadata,
|
||||
Notification,
|
||||
OcrProvider,
|
||||
Provider,
|
||||
Shortcut,
|
||||
SupportedOcrFile,
|
||||
ThemeMode,
|
||||
} from "@types";
|
||||
import checkDiskSpace from 'check-disk-space'
|
||||
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
||||
import fontList from 'font-list'
|
||||
@ -209,14 +218,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.AgentMessage_GetHistory, async (_event, { sessionId }: { sessionId: string }) => {
|
||||
try {
|
||||
return await agentMessageRepository.getSessionHistory(sessionId)
|
||||
} catch (error) {
|
||||
logger.error('Failed to get agent session history', error as Error)
|
||||
throw error
|
||||
ipcMain.handle(
|
||||
IpcChannel.AgentMessage_GetHistory,
|
||||
async (
|
||||
_event,
|
||||
{ sessionId }: { sessionId: string }
|
||||
): Promise<AgentPersistedMessage[]> => {
|
||||
try {
|
||||
return await agentMessageRepository.getSessionHistory(sessionId);
|
||||
} catch (error) {
|
||||
logger.error("Failed to get agent session history", error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
//only for mac
|
||||
if (isMac) {
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ListOptions
|
||||
} from '@types'
|
||||
import { TextStreamPart } from 'ai'
|
||||
import { desc, eq } from 'drizzle-orm'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import { sessionMessagesTable } from '../database/schema'
|
||||
@ -145,6 +145,16 @@ export class SessionMessageService extends BaseService {
|
||||
return { messages }
|
||||
}
|
||||
|
||||
async deleteSessionMessage(sessionId: string, messageId: number): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database
|
||||
.delete(sessionMessagesTable)
|
||||
.where(and(eq(sessionMessagesTable.id, messageId), eq(sessionMessagesTable.session_id, sessionId)))
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async createSessionMessage(
|
||||
session: GetAgentSessionResponse,
|
||||
messageData: CreateSessionMessageRequest,
|
||||
@ -270,6 +280,7 @@ export class SessionMessageService extends BaseService {
|
||||
.orderBy(desc(sessionMessagesTable.created_at))
|
||||
.limit(1)
|
||||
|
||||
logger.silly('Last agent session ID result:', { agentSessionId: result[0]?.agent_session_id, sessionId })
|
||||
return result[0]?.agent_session_id || ''
|
||||
} catch (error) {
|
||||
logger.error('Failed to get last agent session ID', {
|
||||
|
||||
@ -74,6 +74,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
ELECTRON_RUN_AS_NODE: '1'
|
||||
}
|
||||
|
||||
const errorChunks: string[] = []
|
||||
|
||||
// Build SDK options from parameters
|
||||
const options: Options = {
|
||||
abortController,
|
||||
@ -82,7 +84,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
model: modelInfo.modelId,
|
||||
pathToClaudeCodeExecutable: this.claudeExecutablePath,
|
||||
stderr: (chunk: string) => {
|
||||
logger.info('claude stderr', { chunk })
|
||||
logger.warn('claude stderr', { chunk })
|
||||
errorChunks.push(chunk)
|
||||
},
|
||||
appendSystemPrompt: session.instructions,
|
||||
permissionMode: session.configuration?.permission_mode,
|
||||
@ -116,11 +119,16 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
logger.silly('Starting Claude Code SDK query', {
|
||||
prompt,
|
||||
options
|
||||
cwd: options.cwd,
|
||||
model: options.model,
|
||||
permissionMode: options.permissionMode,
|
||||
maxTurns: options.maxTurns,
|
||||
allowedTools: options.allowedTools,
|
||||
resume: options.resume
|
||||
})
|
||||
|
||||
// Start async processing
|
||||
this.processSDKQuery(prompt, options, aiStream)
|
||||
this.processSDKQuery(prompt, options, aiStream, errorChunks)
|
||||
|
||||
return aiStream
|
||||
}
|
||||
@ -142,7 +150,12 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
/**
|
||||
* Process SDK query and emit stream events
|
||||
*/
|
||||
private async processSDKQuery(prompt: string, options: Options, stream: ClaudeCodeStream): Promise<void> {
|
||||
private async processSDKQuery(
|
||||
prompt: string,
|
||||
options: Options,
|
||||
stream: ClaudeCodeStream,
|
||||
errorChunks: string[]
|
||||
): Promise<void> {
|
||||
const jsonOutput: SDKMessage[] = []
|
||||
let hasCompleted = false
|
||||
const startTime = Date.now()
|
||||
@ -209,17 +222,12 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
return
|
||||
}
|
||||
|
||||
// Original error handling for non-abort errors
|
||||
logger.error('SDK query error:', {
|
||||
error: errorObj instanceof Error ? errorObj.message : String(errorObj),
|
||||
duration,
|
||||
messageCount: jsonOutput.length
|
||||
})
|
||||
|
||||
errorChunks.push(errorObj instanceof Error ? errorObj.message : String(errorObj))
|
||||
const errorMessage = errorChunks.join('\n\n')
|
||||
// Emit error event
|
||||
stream.emit('data', {
|
||||
type: 'error',
|
||||
error: errorObj instanceof Error ? errorObj : new Error(String(errorObj))
|
||||
error: new Error(errorMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,6 +206,16 @@ export class AgentApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteSessionMessage(agentId: string, sessionId: string, messageId: number): Promise<void> {
|
||||
const base = this.getSessionMessagesPath(agentId, sessionId)
|
||||
const url = `${base}/${messageId}`
|
||||
try {
|
||||
await this.axios.delete(url)
|
||||
} catch (error) {
|
||||
throw processError(error, 'Failed to delete session message.')
|
||||
}
|
||||
}
|
||||
|
||||
public async updateSession(agentId: string, session: UpdateSessionForm): Promise<GetAgentSessionResponse> {
|
||||
const url = this.getSessionPaths(agentId).withId(session.id)
|
||||
try {
|
||||
|
||||
@ -36,7 +36,13 @@ export const DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [
|
||||
'more-menu'
|
||||
]
|
||||
|
||||
export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = ['copy', 'translate', 'notes', 'more-menu']
|
||||
export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [
|
||||
'copy',
|
||||
'translate',
|
||||
'notes',
|
||||
'delete',
|
||||
'more-menu'
|
||||
]
|
||||
|
||||
const messageMenubarRegistry = new Map<MessageMenubarScope, MessageMenubarScopeConfig>([
|
||||
[DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }],
|
||||
|
||||
@ -133,7 +133,7 @@
|
||||
"plan": {
|
||||
"behavior": "Solo herramientas de solo lectura. La ejecución está deshabilitada.",
|
||||
"description": "Claude solo puede usar herramientas de solo lectura y presenta un plan antes de ejecutarlo.",
|
||||
"title": "[to be translated]:Planning mode (coming soon)"
|
||||
"title": "Modo de planificación (próximamente)"
|
||||
}
|
||||
},
|
||||
"preapproved": {
|
||||
|
||||
@ -0,0 +1,190 @@
|
||||
import { AccordionItem, Card, CardBody, Chip, Code } from '@heroui/react'
|
||||
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { BashOutputToolInput, BashOutputToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
interface ParsedBashOutput {
|
||||
status?: string
|
||||
exit_code?: number
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
timestamp?: string
|
||||
tool_use_error?: string
|
||||
}
|
||||
|
||||
export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) {
|
||||
// 解析 XML 输出
|
||||
const parsedOutput = useMemo(() => {
|
||||
if (!output) return null
|
||||
|
||||
try {
|
||||
const parser = new DOMParser()
|
||||
// 检查是否包含 tool_use_error 标签
|
||||
const hasToolError = output.includes('<tool_use_error>')
|
||||
// 包装成有效的 XML(如果还没有根元素)
|
||||
const xmlStr = output.includes('<status>') || hasToolError ? `<root>${output}</root>` : output
|
||||
const xmlDoc = parser.parseFromString(xmlStr, 'application/xml')
|
||||
|
||||
// 检查是否有解析错误
|
||||
const parserError = xmlDoc.querySelector('parsererror')
|
||||
if (parserError) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getElementText = (tagName: string): string | undefined => {
|
||||
const element = xmlDoc.getElementsByTagName(tagName)[0]
|
||||
return element?.textContent?.trim()
|
||||
}
|
||||
|
||||
const result: ParsedBashOutput = {
|
||||
status: getElementText('status'),
|
||||
exit_code: getElementText('exit_code') ? parseInt(getElementText('exit_code')!) : undefined,
|
||||
stdout: getElementText('stdout'),
|
||||
stderr: getElementText('stderr'),
|
||||
timestamp: getElementText('timestamp'),
|
||||
tool_use_error: getElementText('tool_use_error')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [output])
|
||||
|
||||
// 获取状态配置
|
||||
const statusConfig = useMemo(() => {
|
||||
if (!parsedOutput) return null
|
||||
|
||||
// 如果有 tool_use_error,直接显示错误状态
|
||||
if (parsedOutput.tool_use_error) {
|
||||
return {
|
||||
color: 'danger',
|
||||
icon: <XCircle className="h-3.5 w-3.5" />,
|
||||
text: 'Error'
|
||||
} as const
|
||||
}
|
||||
|
||||
const isCompleted = parsedOutput.status === 'completed'
|
||||
const isSuccess = parsedOutput.exit_code === 0
|
||||
|
||||
return {
|
||||
color: isCompleted && isSuccess ? 'success' : isCompleted && !isSuccess ? 'danger' : 'warning',
|
||||
icon:
|
||||
isCompleted && isSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
) : isCompleted && !isSuccess ? (
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
),
|
||||
text: isCompleted ? (isSuccess ? 'Success' : 'Failed') : 'Running'
|
||||
} as const
|
||||
}, [parsedOutput])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.BashOutput}
|
||||
aria-label="BashOutput Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash Output"
|
||||
params={
|
||||
<div className="flex items-center gap-2">
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{input.bash_id}
|
||||
</Code>
|
||||
{statusConfig && (
|
||||
<Chip
|
||||
size="sm"
|
||||
color={statusConfig.color}
|
||||
variant="flat"
|
||||
startContent={statusConfig.icon}
|
||||
className="h-5">
|
||||
{statusConfig.text}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
classNames={{
|
||||
content: 'space-y-3 px-1'
|
||||
}}>
|
||||
{parsedOutput ? (
|
||||
<>
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Chip size="sm" color={parsedOutput.exit_code === 0 ? 'success' : 'danger'} variant="flat">
|
||||
Exit Code: {parsedOutput.exit_code}
|
||||
</Chip>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{new Date(parsedOutput.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<Card
|
||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
||||
shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<Card
|
||||
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
|
||||
shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
|
||||
<CardBody className="p-3">
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{output}
|
||||
</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -21,7 +21,7 @@ export function BashTool({ input, output }: { input: BashToolInputType; output?:
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis text-xs">
|
||||
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis py-0 text-xs">
|
||||
{input.command}
|
||||
</Code>
|
||||
}>
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileEdit } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { EditToolInput, EditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
// 处理多行文本显示
|
||||
export const renderCodeBlock = (content: string, variant: 'old' | 'new') => {
|
||||
const lines = content.split('\n')
|
||||
const textColorClass =
|
||||
variant === 'old' ? 'text-danger-600 dark:text-danger-400' : 'text-success-600 dark:text-success-400'
|
||||
|
||||
return (
|
||||
// 删除线
|
||||
<pre className={`whitespace-pre-wrap font-mono text-xs ${textColorClass}`}>
|
||||
{lines.map((line, idx) => (
|
||||
<div key={idx} className="flex hover:bg-default-100/50 dark:hover:bg-default-900/50">
|
||||
<span className="mr-3 min-w-[2rem] select-none text-right opacity-50">
|
||||
{variant === 'old' && '-'}
|
||||
{variant === 'new' && '+'}
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className={`flex-1 ${variant === 'old' && 'line-through'}`}>{line || ' '}</span>
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Edit}
|
||||
aria-label="Edit Tool"
|
||||
title={<ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />}>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { DoorOpen } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.ExitPlanMode}
|
||||
aria-label="ExitPlanMode Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
}>
|
||||
{<ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { renderCodeBlock } from './EditTool'
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { MultiEditToolInput, MultiEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function MultiEditTool({ input, output }: { input: MultiEditToolInput; output?: MultiEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.MultiEdit}
|
||||
aria-label="MultiEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />}>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileText } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { NotebookEditToolInput, NotebookEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function NotebookEditTool({ input, output }: { input: NotebookEditToolInput; output?: NotebookEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.NotebookEdit}
|
||||
aria-label="NotebookEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />}
|
||||
subtitle={input.notebook_path}>
|
||||
<ReactMarkdown>{output}</ReactMarkdown>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@ -1,24 +1,46 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { FileText } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType } from './types'
|
||||
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
||||
// 将 output 统一转换为字符串
|
||||
const outputString = useMemo(() => {
|
||||
if (!output) return null
|
||||
|
||||
// 如果是 TextOutput[] 类型,提取所有 text 内容
|
||||
if (Array.isArray(output)) {
|
||||
return output
|
||||
.filter((item): item is TextOutput => item.type === 'text')
|
||||
.map((item) => item.text)
|
||||
.join('')
|
||||
}
|
||||
|
||||
// 如果是字符串,直接返回
|
||||
return output
|
||||
}, [output])
|
||||
|
||||
// 如果有输出,计算统计信息
|
||||
const stats = output
|
||||
? {
|
||||
lineCount: output.split('\n').length,
|
||||
fileSize: new Blob([output]).size,
|
||||
formatSize: (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
}
|
||||
: null
|
||||
const stats = useMemo(() => {
|
||||
if (!outputString) return null
|
||||
|
||||
const bytes = new Blob([outputString]).size
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return {
|
||||
lineCount: outputString.split('\n').length,
|
||||
fileSize: bytes,
|
||||
formatSize
|
||||
}
|
||||
}, [outputString])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
@ -29,14 +51,10 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={output && stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
}>
|
||||
{output ? (
|
||||
// <div className="h-full scroll-auto">
|
||||
<ReactMarkdown>{output}</ReactMarkdown>
|
||||
// </div>
|
||||
) : null}
|
||||
{outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,31 +14,28 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
||||
case 'completed':
|
||||
return {
|
||||
color: 'success' as const,
|
||||
icon: <CheckCircle className="h-3 w-3" />,
|
||||
label: '已完成'
|
||||
icon: <CheckCircle className="h-3 w-3" />
|
||||
}
|
||||
case 'in_progress':
|
||||
return {
|
||||
color: 'primary' as const,
|
||||
icon: <Clock className="h-3 w-3" />,
|
||||
label: '进行中'
|
||||
icon: <Clock className="h-3 w-3" />
|
||||
}
|
||||
case 'pending':
|
||||
return {
|
||||
color: 'default' as const,
|
||||
icon: <Circle className="h-3 w-3" />,
|
||||
label: '待处理'
|
||||
icon: <Circle className="h-3 w-3" />
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'default' as const,
|
||||
icon: <Circle className="h-3 w-3" />,
|
||||
label: '待处理'
|
||||
icon: <Circle className="h-3 w-3" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
|
||||
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.TodoWrite}
|
||||
@ -46,7 +43,8 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Update"
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
}>
|
||||
@ -55,15 +53,10 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType
|
||||
const statusConfig = getStatusConfig(todo.status)
|
||||
return (
|
||||
<Card key={index} className="shadow-sm">
|
||||
<CardBody>
|
||||
<CardBody className="p-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Chip
|
||||
color={statusConfig.color}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={statusConfig.icon}
|
||||
className="flex-shrink-0">
|
||||
{statusConfig.label}
|
||||
<Chip color={statusConfig.color} variant="flat" size="sm" className="flex-shrink-0">
|
||||
{statusConfig.icon}
|
||||
</Chip>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||
|
||||
@ -6,9 +6,14 @@ import { NormalToolResponse } from '@renderer/types'
|
||||
export * from './types'
|
||||
|
||||
// 导入所有渲染器
|
||||
import { BashOutputTool } from './BashOutputTool'
|
||||
import { BashTool } from './BashTool'
|
||||
import { EditTool } from './EditTool'
|
||||
import { ExitPlanModeTool } from './ExitPlanModeTool'
|
||||
import { GlobTool } from './GlobTool'
|
||||
import { GrepTool } from './GrepTool'
|
||||
import { MultiEditTool } from './MultiEditTool'
|
||||
import { NotebookEditTool } from './NotebookEditTool'
|
||||
import { ReadTool } from './ReadTool'
|
||||
import { SearchTool } from './SearchTool'
|
||||
import { TaskTool } from './TaskTool'
|
||||
@ -31,7 +36,12 @@ export const toolRenderers = {
|
||||
[AgentToolsType.WebSearch]: WebSearchTool,
|
||||
[AgentToolsType.Grep]: GrepTool,
|
||||
[AgentToolsType.Write]: WriteTool,
|
||||
[AgentToolsType.WebFetch]: WebFetchTool
|
||||
[AgentToolsType.WebFetch]: WebFetchTool,
|
||||
[AgentToolsType.Edit]: EditTool,
|
||||
[AgentToolsType.MultiEdit]: MultiEditTool,
|
||||
[AgentToolsType.BashOutput]: BashOutputTool,
|
||||
[AgentToolsType.NotebookEdit]: NotebookEditTool,
|
||||
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool
|
||||
} as const
|
||||
|
||||
// 类型守卫函数
|
||||
@ -51,7 +61,8 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?:
|
||||
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
|
||||
indicator: 'flex-shrink-0',
|
||||
subtitle: 'text-xs',
|
||||
content: 'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 h-fit max-h-96 scroll-auto'
|
||||
content:
|
||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
|
||||
}}
|
||||
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||
{/* <Renderer input={input as any} output={output as any} /> */}
|
||||
|
||||
@ -8,7 +8,17 @@ export enum AgentToolsType {
|
||||
WebSearch = 'WebSearch',
|
||||
Grep = 'Grep',
|
||||
Write = 'Write',
|
||||
WebFetch = 'WebFetch'
|
||||
WebFetch = 'WebFetch',
|
||||
Edit = 'Edit',
|
||||
MultiEdit = 'MultiEdit',
|
||||
BashOutput = 'BashOutput',
|
||||
NotebookEdit = 'NotebookEdit',
|
||||
ExitPlanMode = 'ExitPlanMode'
|
||||
}
|
||||
|
||||
export type TextOutput = {
|
||||
type: 'text'
|
||||
text: string
|
||||
}
|
||||
|
||||
// Read 工具的类型定义
|
||||
@ -16,7 +26,7 @@ export interface ReadToolInput {
|
||||
file_path: string
|
||||
}
|
||||
|
||||
export type ReadToolOutput = string
|
||||
export type ReadToolOutput = string | TextOutput[]
|
||||
|
||||
// Task 工具的类型定义
|
||||
export type TaskToolInput = {
|
||||
@ -25,10 +35,7 @@ export type TaskToolInput = {
|
||||
subagent_type: string
|
||||
}
|
||||
|
||||
export type TaskToolOutput = {
|
||||
type: 'text'
|
||||
text: string
|
||||
}[]
|
||||
export type TaskToolOutput = TextOutput[]
|
||||
|
||||
// Bash 工具的类型定义
|
||||
export type BashToolInput = {
|
||||
@ -84,6 +91,7 @@ export interface GrepToolInput {
|
||||
|
||||
export type GrepToolOutput = string
|
||||
|
||||
// Write 工具的类型定义
|
||||
export type WriteToolInput = {
|
||||
content: string
|
||||
file_path: string
|
||||
@ -91,6 +99,44 @@ export type WriteToolInput = {
|
||||
|
||||
export type WriteToolOutput = string
|
||||
|
||||
// Edit 工具的类型定义
|
||||
export type EditToolInput = {
|
||||
file_path: string
|
||||
old_string: string
|
||||
new_string: string
|
||||
}
|
||||
export type EditToolOutput = string
|
||||
|
||||
// MultiEdit 工具的类型定义
|
||||
export type MultiEditToolInput = {
|
||||
file_path: string
|
||||
edits: {
|
||||
old_string: string
|
||||
new_string: string
|
||||
}[]
|
||||
}
|
||||
export type MultiEditToolOutput = string
|
||||
|
||||
// BashOutput 工具的类型定义
|
||||
export type BashOutputToolInput = {
|
||||
bash_id: string
|
||||
}
|
||||
export type BashOutputToolOutput = string
|
||||
|
||||
// NotebookEdit 工具的类型定义
|
||||
export type NotebookEditToolInput = {
|
||||
notebook_path: string
|
||||
edit_mode: string
|
||||
cell_type: string
|
||||
new_source: string
|
||||
}
|
||||
export type NotebookEditToolOutput = string
|
||||
|
||||
export type ExitPlanModeToolInput = {
|
||||
plan: string
|
||||
}
|
||||
export type ExitPlanModeToolOutput = string
|
||||
|
||||
// 联合类型
|
||||
export type ToolInput =
|
||||
| ReadToolInput
|
||||
@ -103,6 +149,11 @@ export type ToolInput =
|
||||
| WebFetchToolInput
|
||||
| GrepToolInput
|
||||
| WriteToolInput
|
||||
| EditToolInput
|
||||
| MultiEditToolInput
|
||||
| BashOutputToolInput
|
||||
| NotebookEditToolInput
|
||||
| ExitPlanModeToolInput
|
||||
export type ToolOutput =
|
||||
| ReadToolOutput
|
||||
| TaskToolOutput
|
||||
@ -114,6 +165,11 @@ export type ToolOutput =
|
||||
| GrepToolOutput
|
||||
| WebFetchToolOutput
|
||||
| WriteToolOutput
|
||||
| EditToolOutput
|
||||
| MultiEditToolOutput
|
||||
| BashOutputToolOutput
|
||||
| NotebookEditToolOutput
|
||||
| ExitPlanModeToolOutput
|
||||
// 工具渲染器接口
|
||||
export interface ToolRenderer {
|
||||
render: (props: { input: ToolInput; output?: ToolOutput }) => React.ReactElement
|
||||
|
||||
@ -35,6 +35,11 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
|
||||
case 'Grep':
|
||||
case 'Write':
|
||||
case 'WebFetch':
|
||||
case 'Edit':
|
||||
case 'MultiEdit':
|
||||
case 'BashOutput':
|
||||
case 'NotebookEdit':
|
||||
case 'ExitPlanMode':
|
||||
return <MessageAgentTools toolResponse={toolResponse} />
|
||||
default:
|
||||
return null
|
||||
|
||||
@ -108,6 +108,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const { mcpServers: allServers } = useMCPServers()
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
|
||||
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
|
||||
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode)
|
||||
@ -192,8 +193,12 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
}
|
||||
|
||||
if (removedDefaults.length > 0) {
|
||||
Modal.confirm({
|
||||
title: t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?'),
|
||||
modal.confirm({
|
||||
title: (
|
||||
<span className="text-foreground">
|
||||
{t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?')}
|
||||
</span>
|
||||
),
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-foreground-500 text-sm">
|
||||
@ -207,7 +212,11 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
<ul className="mt-1 list-disc pl-4">
|
||||
{removedDefaults.map((id) => {
|
||||
const tool = availableTools.find((item) => item.id === id)
|
||||
return <li key={id}>{tool?.name ?? id}</li>
|
||||
return (
|
||||
<li className="text-foreground" key={id}>
|
||||
{tool?.name ?? id}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
@ -215,13 +224,27 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: applyChange
|
||||
onOk: applyChange,
|
||||
classNames: {
|
||||
content: 'bg-background! border! border-solid! rounded border-grey border-default-200!'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
void applyChange()
|
||||
}
|
||||
},
|
||||
[agent, selectedMode, isUpdatingMode, availableTools, userAddedIds, autoToolIds, configuration, updateAgent, t]
|
||||
[
|
||||
agent,
|
||||
selectedMode,
|
||||
isUpdatingMode,
|
||||
availableTools,
|
||||
userAddedIds,
|
||||
autoToolIds,
|
||||
configuration,
|
||||
updateAgent,
|
||||
modal,
|
||||
t
|
||||
]
|
||||
)
|
||||
|
||||
const handleToggleTool = useCallback(
|
||||
@ -300,6 +323,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{contextHolder}
|
||||
<SettingsItem>
|
||||
<SettingsTitle>
|
||||
{t('agent.settings.tooling.steps.permissionMode.title', 'Step 1 · Permission mode')}
|
||||
@ -309,6 +333,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
const isSelected = card.mode === selectedMode
|
||||
const disabled = card.unsupported
|
||||
const showCaution = card.caution
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={card.mode}
|
||||
@ -338,13 +363,15 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
<CardBody className="gap-2 text-left text-xs">
|
||||
<span className="text-foreground-600">{t(card.behaviorKey, card.behaviorFallback)}</span>
|
||||
{showCaution ? (
|
||||
<span className="flex items-center gap-1 text-danger-600">
|
||||
<ShieldAlert size={14} />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<ShieldAlert size={24} />
|
||||
<span className="text-danger-600">
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</CardBody>
|
||||
</Card>
|
||||
@ -375,7 +402,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
||||
onValueChange={setSearchTerm}
|
||||
placeholder={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
||||
aria-label={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
||||
className="max-w-md"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
{filteredTools.length === 0 ? (
|
||||
|
||||
@ -267,6 +267,10 @@ export const SessionIdParamSchema = z.object({
|
||||
sessionId: z.string().min(1, 'Session ID is required')
|
||||
})
|
||||
|
||||
export const SessionMessageIdParamSchema = z.object({
|
||||
messageId: z.coerce.number().int().positive('Message ID must be a positive integer')
|
||||
})
|
||||
|
||||
// Query validation schemas
|
||||
export const PaginationQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user