mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +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()
|
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,
|
validateSession,
|
||||||
validateSessionId,
|
validateSessionId,
|
||||||
validateSessionMessage,
|
validateSessionMessage,
|
||||||
|
validateSessionMessageId,
|
||||||
validateSessionReplace,
|
validateSessionReplace,
|
||||||
validateSessionUpdate
|
validateSessionUpdate
|
||||||
} from './validators'
|
} from './validators'
|
||||||
@ -362,7 +363,7 @@ const agentsRouter = express.Router()
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents:
|
* /agents:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create a new agent
|
* summary: Create a new agent
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -391,7 +392,7 @@ agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.crea
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents:
|
* /agents:
|
||||||
* get:
|
* get:
|
||||||
* summary: List all agents with pagination
|
* summary: List all agents with pagination
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -429,7 +430,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}:
|
* /agents/{agentId}:
|
||||||
* get:
|
* get:
|
||||||
* summary: Get agent by ID
|
* summary: Get agent by ID
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -457,7 +458,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.
|
|||||||
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
|
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}:
|
* /agents/{agentId}:
|
||||||
* put:
|
* put:
|
||||||
* summary: Replace agent (full update)
|
* summary: Replace agent (full update)
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -497,7 +498,7 @@ agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHand
|
|||||||
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
|
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}:
|
* /agents/{agentId}:
|
||||||
* patch:
|
* patch:
|
||||||
* summary: Update agent (partial update)
|
* summary: Update agent (partial update)
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -537,7 +538,7 @@ agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValid
|
|||||||
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
|
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}:
|
* /agents/{agentId}:
|
||||||
* delete:
|
* delete:
|
||||||
* summary: Delete agent
|
* summary: Delete agent
|
||||||
* tags: [Agents]
|
* tags: [Agents]
|
||||||
@ -567,7 +568,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
// Session CRUD routes (nested under agent)
|
// Session CRUD routes (nested under agent)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions:
|
* /agents/{agentId}/sessions:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create a new session for an agent
|
* summary: Create a new session for an agent
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -608,7 +609,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions:
|
* /agents/{agentId}/sessions:
|
||||||
* get:
|
* get:
|
||||||
* summary: List sessions for an agent
|
* summary: List sessions for an agent
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -657,7 +658,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
|
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
* /agents/{agentId}/sessions/{sessionId}:
|
||||||
* get:
|
* get:
|
||||||
* summary: Get session by ID
|
* summary: Get session by ID
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -691,7 +692,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
|
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
* /agents/{agentId}/sessions/{sessionId}:
|
||||||
* put:
|
* put:
|
||||||
* summary: Replace session (full update)
|
* summary: Replace session (full update)
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -743,7 +744,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
* /agents/{agentId}/sessions/{sessionId}:
|
||||||
* patch:
|
* patch:
|
||||||
* summary: Update session (partial update)
|
* summary: Update session (partial update)
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -795,7 +796,7 @@ const createSessionsRouter = (): express.Router => {
|
|||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
* /agents/{agentId}/sessions/{sessionId}:
|
||||||
* delete:
|
* delete:
|
||||||
* summary: Delete session
|
* summary: Delete session
|
||||||
* tags: [Sessions]
|
* tags: [Sessions]
|
||||||
@ -834,7 +835,7 @@ const createMessagesRouter = (): express.Router => {
|
|||||||
// Message CRUD routes (nested under agent/session)
|
// Message CRUD routes (nested under agent/session)
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/agents/{agentId}/sessions/{sessionId}/messages:
|
* /agents/{agentId}/sessions/{sessionId}/messages:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create a new message in a session
|
* summary: Create a new message in a session
|
||||||
* tags: [Messages]
|
* tags: [Messages]
|
||||||
@ -902,8 +903,45 @@ const createMessagesRouter = (): express.Router => {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/ErrorResponse'
|
* $ref: '#/components/schemas/ErrorResponse'
|
||||||
*/
|
*/
|
||||||
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
|
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
|
return messagesRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { CreateSessionMessageRequestSchema } from '@types'
|
import { CreateSessionMessageRequestSchema, SessionMessageIdParamSchema } from '@types'
|
||||||
|
|
||||||
import { createZodValidator } from './zodValidator'
|
import { createZodValidator } from './zodValidator'
|
||||||
|
|
||||||
export const validateSessionMessage = createZodValidator({
|
export const validateSessionMessage = createZodValidator({
|
||||||
body: CreateSessionMessageRequestSchema
|
body: CreateSessionMessageRequestSchema
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const validateSessionMessageId = createZodValidator({
|
||||||
|
params: SessionMessageIdParamSchema
|
||||||
|
})
|
||||||
|
|||||||
@ -102,19 +102,14 @@ class MCPApiService extends EventEmitter {
|
|||||||
|
|
||||||
async getServerInfo(id: string): Promise<any> {
|
async getServerInfo(id: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
logger.silly(`getServerInfo called with id: ${id}`)
|
|
||||||
const server = await this.getServerById(id)
|
const server = await this.getServerById(id)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
logger.warn(`Server with id ${id} not found`)
|
logger.warn(`Server with id ${id} not found`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
logger.silly(`Returning server info for id ${id}`)
|
|
||||||
|
|
||||||
const client = await mcpService.initClient(server)
|
const client = await mcpService.initClient(server)
|
||||||
const tools = await client.listTools()
|
const tools = await client.listTools()
|
||||||
|
|
||||||
logger.silly(`Server with id ${id} info:`, { tools: JSON.stringify(tools.tools) })
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: server.id,
|
id: server.id,
|
||||||
name: server.name,
|
name: server.name,
|
||||||
|
|||||||
@ -11,7 +11,16 @@ import { handleZoomFactor } from '@main/utils/zoom'
|
|||||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||||
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
|
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
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 checkDiskSpace from 'check-disk-space'
|
||||||
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
||||||
import fontList from 'font-list'
|
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 }) => {
|
ipcMain.handle(
|
||||||
try {
|
IpcChannel.AgentMessage_GetHistory,
|
||||||
return await agentMessageRepository.getSessionHistory(sessionId)
|
async (
|
||||||
} catch (error) {
|
_event,
|
||||||
logger.error('Failed to get agent session history', error as Error)
|
{ sessionId }: { sessionId: string }
|
||||||
throw error
|
): 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
|
//only for mac
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type {
|
|||||||
ListOptions
|
ListOptions
|
||||||
} from '@types'
|
} from '@types'
|
||||||
import { TextStreamPart } from 'ai'
|
import { TextStreamPart } from 'ai'
|
||||||
import { desc, eq } from 'drizzle-orm'
|
import { and, desc, eq } from 'drizzle-orm'
|
||||||
|
|
||||||
import { BaseService } from '../BaseService'
|
import { BaseService } from '../BaseService'
|
||||||
import { sessionMessagesTable } from '../database/schema'
|
import { sessionMessagesTable } from '../database/schema'
|
||||||
@ -145,6 +145,16 @@ export class SessionMessageService extends BaseService {
|
|||||||
return { messages }
|
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(
|
async createSessionMessage(
|
||||||
session: GetAgentSessionResponse,
|
session: GetAgentSessionResponse,
|
||||||
messageData: CreateSessionMessageRequest,
|
messageData: CreateSessionMessageRequest,
|
||||||
@ -270,6 +280,7 @@ export class SessionMessageService extends BaseService {
|
|||||||
.orderBy(desc(sessionMessagesTable.created_at))
|
.orderBy(desc(sessionMessagesTable.created_at))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
|
logger.silly('Last agent session ID result:', { agentSessionId: result[0]?.agent_session_id, sessionId })
|
||||||
return result[0]?.agent_session_id || ''
|
return result[0]?.agent_session_id || ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get last agent session ID', {
|
logger.error('Failed to get last agent session ID', {
|
||||||
|
|||||||
@ -74,6 +74,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
ELECTRON_RUN_AS_NODE: '1'
|
ELECTRON_RUN_AS_NODE: '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorChunks: string[] = []
|
||||||
|
|
||||||
// Build SDK options from parameters
|
// Build SDK options from parameters
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
abortController,
|
abortController,
|
||||||
@ -82,7 +84,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
model: modelInfo.modelId,
|
model: modelInfo.modelId,
|
||||||
pathToClaudeCodeExecutable: this.claudeExecutablePath,
|
pathToClaudeCodeExecutable: this.claudeExecutablePath,
|
||||||
stderr: (chunk: string) => {
|
stderr: (chunk: string) => {
|
||||||
logger.info('claude stderr', { chunk })
|
logger.warn('claude stderr', { chunk })
|
||||||
|
errorChunks.push(chunk)
|
||||||
},
|
},
|
||||||
appendSystemPrompt: session.instructions,
|
appendSystemPrompt: session.instructions,
|
||||||
permissionMode: session.configuration?.permission_mode,
|
permissionMode: session.configuration?.permission_mode,
|
||||||
@ -116,11 +119,16 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
|
|
||||||
logger.silly('Starting Claude Code SDK query', {
|
logger.silly('Starting Claude Code SDK query', {
|
||||||
prompt,
|
prompt,
|
||||||
options
|
cwd: options.cwd,
|
||||||
|
model: options.model,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
maxTurns: options.maxTurns,
|
||||||
|
allowedTools: options.allowedTools,
|
||||||
|
resume: options.resume
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start async processing
|
// Start async processing
|
||||||
this.processSDKQuery(prompt, options, aiStream)
|
this.processSDKQuery(prompt, options, aiStream, errorChunks)
|
||||||
|
|
||||||
return aiStream
|
return aiStream
|
||||||
}
|
}
|
||||||
@ -142,7 +150,12 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
/**
|
/**
|
||||||
* Process SDK query and emit stream events
|
* 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[] = []
|
const jsonOutput: SDKMessage[] = []
|
||||||
let hasCompleted = false
|
let hasCompleted = false
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
@ -209,17 +222,12 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original error handling for non-abort errors
|
errorChunks.push(errorObj instanceof Error ? errorObj.message : String(errorObj))
|
||||||
logger.error('SDK query error:', {
|
const errorMessage = errorChunks.join('\n\n')
|
||||||
error: errorObj instanceof Error ? errorObj.message : String(errorObj),
|
|
||||||
duration,
|
|
||||||
messageCount: jsonOutput.length
|
|
||||||
})
|
|
||||||
|
|
||||||
// Emit error event
|
// Emit error event
|
||||||
stream.emit('data', {
|
stream.emit('data', {
|
||||||
type: 'error',
|
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> {
|
public async updateSession(agentId: string, session: UpdateSessionForm): Promise<GetAgentSessionResponse> {
|
||||||
const url = this.getSessionPaths(agentId).withId(session.id)
|
const url = this.getSessionPaths(agentId).withId(session.id)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -36,7 +36,13 @@ export const DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [
|
|||||||
'more-menu'
|
'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>([
|
const messageMenubarRegistry = new Map<MessageMenubarScope, MessageMenubarScopeConfig>([
|
||||||
[DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }],
|
[DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }],
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
"plan": {
|
"plan": {
|
||||||
"behavior": "Solo herramientas de solo lectura. La ejecución está deshabilitada.",
|
"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.",
|
"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": {
|
"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={
|
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}
|
{input.command}
|
||||||
</Code>
|
</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 { AccordionItem } from '@heroui/react'
|
||||||
import { FileText } from 'lucide-react'
|
import { FileText } from 'lucide-react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
|
||||||
import { ToolTitle } from './GenericTools'
|
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'
|
import { AgentToolsType } from './types'
|
||||||
|
|
||||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
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
|
const stats = useMemo(() => {
|
||||||
? {
|
if (!outputString) return null
|
||||||
lineCount: output.split('\n').length,
|
|
||||||
fileSize: new Blob([output]).size,
|
const bytes = new Blob([outputString]).size
|
||||||
formatSize: (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
if (bytes < 1024) return `${bytes} B`
|
if (bytes < 1024) return `${bytes} B`
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
}
|
}
|
||||||
}
|
|
||||||
: null
|
return {
|
||||||
|
lineCount: outputString.split('\n').length,
|
||||||
|
fileSize: bytes,
|
||||||
|
formatSize
|
||||||
|
}
|
||||||
|
}, [outputString])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
@ -29,14 +51,10 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
|
|||||||
icon={<FileText className="h-4 w-4" />}
|
icon={<FileText className="h-4 w-4" />}
|
||||||
label="Read File"
|
label="Read File"
|
||||||
params={input.file_path.split('/').pop()}
|
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 ? (
|
{outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null}
|
||||||
// <div className="h-full scroll-auto">
|
|
||||||
<ReactMarkdown>{output}</ReactMarkdown>
|
|
||||||
// </div>
|
|
||||||
) : null}
|
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,31 +14,28 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
|||||||
case 'completed':
|
case 'completed':
|
||||||
return {
|
return {
|
||||||
color: 'success' as const,
|
color: 'success' as const,
|
||||||
icon: <CheckCircle className="h-3 w-3" />,
|
icon: <CheckCircle className="h-3 w-3" />
|
||||||
label: '已完成'
|
|
||||||
}
|
}
|
||||||
case 'in_progress':
|
case 'in_progress':
|
||||||
return {
|
return {
|
||||||
color: 'primary' as const,
|
color: 'primary' as const,
|
||||||
icon: <Clock className="h-3 w-3" />,
|
icon: <Clock className="h-3 w-3" />
|
||||||
label: '进行中'
|
|
||||||
}
|
}
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return {
|
return {
|
||||||
color: 'default' as const,
|
color: 'default' as const,
|
||||||
icon: <Circle className="h-3 w-3" />,
|
icon: <Circle className="h-3 w-3" />
|
||||||
label: '待处理'
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
color: 'default' as const,
|
color: 'default' as const,
|
||||||
icon: <Circle className="h-3 w-3" />,
|
icon: <Circle className="h-3 w-3" />
|
||||||
label: '待处理'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
|
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
|
||||||
|
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||||
return (
|
return (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key={AgentToolsType.TodoWrite}
|
key={AgentToolsType.TodoWrite}
|
||||||
@ -46,7 +43,8 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType
|
|||||||
title={
|
title={
|
||||||
<ToolTitle
|
<ToolTitle
|
||||||
icon={<ListTodo className="h-4 w-4" />}
|
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'}`}
|
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)
|
const statusConfig = getStatusConfig(todo.status)
|
||||||
return (
|
return (
|
||||||
<Card key={index} className="shadow-sm">
|
<Card key={index} className="shadow-sm">
|
||||||
<CardBody>
|
<CardBody className="p-2">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Chip
|
<Chip color={statusConfig.color} variant="flat" size="sm" className="flex-shrink-0">
|
||||||
color={statusConfig.color}
|
{statusConfig.icon}
|
||||||
variant="flat"
|
|
||||||
size="sm"
|
|
||||||
startContent={statusConfig.icon}
|
|
||||||
className="flex-shrink-0">
|
|
||||||
{statusConfig.label}
|
|
||||||
</Chip>
|
</Chip>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||||
|
|||||||
@ -6,9 +6,14 @@ import { NormalToolResponse } from '@renderer/types'
|
|||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
// 导入所有渲染器
|
// 导入所有渲染器
|
||||||
|
import { BashOutputTool } from './BashOutputTool'
|
||||||
import { BashTool } from './BashTool'
|
import { BashTool } from './BashTool'
|
||||||
|
import { EditTool } from './EditTool'
|
||||||
|
import { ExitPlanModeTool } from './ExitPlanModeTool'
|
||||||
import { GlobTool } from './GlobTool'
|
import { GlobTool } from './GlobTool'
|
||||||
import { GrepTool } from './GrepTool'
|
import { GrepTool } from './GrepTool'
|
||||||
|
import { MultiEditTool } from './MultiEditTool'
|
||||||
|
import { NotebookEditTool } from './NotebookEditTool'
|
||||||
import { ReadTool } from './ReadTool'
|
import { ReadTool } from './ReadTool'
|
||||||
import { SearchTool } from './SearchTool'
|
import { SearchTool } from './SearchTool'
|
||||||
import { TaskTool } from './TaskTool'
|
import { TaskTool } from './TaskTool'
|
||||||
@ -31,7 +36,12 @@ export const toolRenderers = {
|
|||||||
[AgentToolsType.WebSearch]: WebSearchTool,
|
[AgentToolsType.WebSearch]: WebSearchTool,
|
||||||
[AgentToolsType.Grep]: GrepTool,
|
[AgentToolsType.Grep]: GrepTool,
|
||||||
[AgentToolsType.Write]: WriteTool,
|
[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
|
} 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',
|
'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',
|
indicator: 'flex-shrink-0',
|
||||||
subtitle: 'text-xs',
|
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] : []}>
|
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||||
{/* <Renderer input={input as any} output={output as any} /> */}
|
{/* <Renderer input={input as any} output={output as any} /> */}
|
||||||
|
|||||||
@ -8,7 +8,17 @@ export enum AgentToolsType {
|
|||||||
WebSearch = 'WebSearch',
|
WebSearch = 'WebSearch',
|
||||||
Grep = 'Grep',
|
Grep = 'Grep',
|
||||||
Write = 'Write',
|
Write = 'Write',
|
||||||
WebFetch = 'WebFetch'
|
WebFetch = 'WebFetch',
|
||||||
|
Edit = 'Edit',
|
||||||
|
MultiEdit = 'MultiEdit',
|
||||||
|
BashOutput = 'BashOutput',
|
||||||
|
NotebookEdit = 'NotebookEdit',
|
||||||
|
ExitPlanMode = 'ExitPlanMode'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextOutput = {
|
||||||
|
type: 'text'
|
||||||
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 工具的类型定义
|
// Read 工具的类型定义
|
||||||
@ -16,7 +26,7 @@ export interface ReadToolInput {
|
|||||||
file_path: string
|
file_path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReadToolOutput = string
|
export type ReadToolOutput = string | TextOutput[]
|
||||||
|
|
||||||
// Task 工具的类型定义
|
// Task 工具的类型定义
|
||||||
export type TaskToolInput = {
|
export type TaskToolInput = {
|
||||||
@ -25,10 +35,7 @@ export type TaskToolInput = {
|
|||||||
subagent_type: string
|
subagent_type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskToolOutput = {
|
export type TaskToolOutput = TextOutput[]
|
||||||
type: 'text'
|
|
||||||
text: string
|
|
||||||
}[]
|
|
||||||
|
|
||||||
// Bash 工具的类型定义
|
// Bash 工具的类型定义
|
||||||
export type BashToolInput = {
|
export type BashToolInput = {
|
||||||
@ -84,6 +91,7 @@ export interface GrepToolInput {
|
|||||||
|
|
||||||
export type GrepToolOutput = string
|
export type GrepToolOutput = string
|
||||||
|
|
||||||
|
// Write 工具的类型定义
|
||||||
export type WriteToolInput = {
|
export type WriteToolInput = {
|
||||||
content: string
|
content: string
|
||||||
file_path: string
|
file_path: string
|
||||||
@ -91,6 +99,44 @@ export type WriteToolInput = {
|
|||||||
|
|
||||||
export type WriteToolOutput = string
|
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 =
|
export type ToolInput =
|
||||||
| ReadToolInput
|
| ReadToolInput
|
||||||
@ -103,6 +149,11 @@ export type ToolInput =
|
|||||||
| WebFetchToolInput
|
| WebFetchToolInput
|
||||||
| GrepToolInput
|
| GrepToolInput
|
||||||
| WriteToolInput
|
| WriteToolInput
|
||||||
|
| EditToolInput
|
||||||
|
| MultiEditToolInput
|
||||||
|
| BashOutputToolInput
|
||||||
|
| NotebookEditToolInput
|
||||||
|
| ExitPlanModeToolInput
|
||||||
export type ToolOutput =
|
export type ToolOutput =
|
||||||
| ReadToolOutput
|
| ReadToolOutput
|
||||||
| TaskToolOutput
|
| TaskToolOutput
|
||||||
@ -114,6 +165,11 @@ export type ToolOutput =
|
|||||||
| GrepToolOutput
|
| GrepToolOutput
|
||||||
| WebFetchToolOutput
|
| WebFetchToolOutput
|
||||||
| WriteToolOutput
|
| WriteToolOutput
|
||||||
|
| EditToolOutput
|
||||||
|
| MultiEditToolOutput
|
||||||
|
| BashOutputToolOutput
|
||||||
|
| NotebookEditToolOutput
|
||||||
|
| ExitPlanModeToolOutput
|
||||||
// 工具渲染器接口
|
// 工具渲染器接口
|
||||||
export interface ToolRenderer {
|
export interface ToolRenderer {
|
||||||
render: (props: { input: ToolInput; output?: ToolOutput }) => React.ReactElement
|
render: (props: { input: ToolInput; output?: ToolOutput }) => React.ReactElement
|
||||||
|
|||||||
@ -35,6 +35,11 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
|
|||||||
case 'Grep':
|
case 'Grep':
|
||||||
case 'Write':
|
case 'Write':
|
||||||
case 'WebFetch':
|
case 'WebFetch':
|
||||||
|
case 'Edit':
|
||||||
|
case 'MultiEdit':
|
||||||
|
case 'BashOutput':
|
||||||
|
case 'NotebookEdit':
|
||||||
|
case 'ExitPlanMode':
|
||||||
return <MessageAgentTools toolResponse={toolResponse} />
|
return <MessageAgentTools toolResponse={toolResponse} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const client = useAgentClient()
|
const client = useAgentClient()
|
||||||
const { mcpServers: allServers } = useMCPServers()
|
const { mcpServers: allServers } = useMCPServers()
|
||||||
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
|
|
||||||
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
|
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
|
||||||
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode)
|
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode)
|
||||||
@ -192,8 +193,12 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (removedDefaults.length > 0) {
|
if (removedDefaults.length > 0) {
|
||||||
Modal.confirm({
|
modal.confirm({
|
||||||
title: t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?'),
|
title: (
|
||||||
|
<span className="text-foreground">
|
||||||
|
{t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
content: (
|
content: (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-foreground-500 text-sm">
|
<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">
|
<ul className="mt-1 list-disc pl-4">
|
||||||
{removedDefaults.map((id) => {
|
{removedDefaults.map((id) => {
|
||||||
const tool = availableTools.find((item) => item.id === 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -215,13 +224,27 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
),
|
),
|
||||||
okText: t('common.confirm'),
|
okText: t('common.confirm'),
|
||||||
cancelText: t('common.cancel'),
|
cancelText: t('common.cancel'),
|
||||||
onOk: applyChange
|
onOk: applyChange,
|
||||||
|
classNames: {
|
||||||
|
content: 'bg-background! border! border-solid! rounded border-grey border-default-200!'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
void applyChange()
|
void applyChange()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[agent, selectedMode, isUpdatingMode, availableTools, userAddedIds, autoToolIds, configuration, updateAgent, t]
|
[
|
||||||
|
agent,
|
||||||
|
selectedMode,
|
||||||
|
isUpdatingMode,
|
||||||
|
availableTools,
|
||||||
|
userAddedIds,
|
||||||
|
autoToolIds,
|
||||||
|
configuration,
|
||||||
|
updateAgent,
|
||||||
|
modal,
|
||||||
|
t
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleToggleTool = useCallback(
|
const handleToggleTool = useCallback(
|
||||||
@ -300,6 +323,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
{contextHolder}
|
||||||
<SettingsItem>
|
<SettingsItem>
|
||||||
<SettingsTitle>
|
<SettingsTitle>
|
||||||
{t('agent.settings.tooling.steps.permissionMode.title', 'Step 1 · Permission mode')}
|
{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 isSelected = card.mode === selectedMode
|
||||||
const disabled = card.unsupported
|
const disabled = card.unsupported
|
||||||
const showCaution = card.caution
|
const showCaution = card.caution
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={card.mode}
|
key={card.mode}
|
||||||
@ -338,13 +363,15 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
<CardBody className="gap-2 text-left text-xs">
|
<CardBody className="gap-2 text-left text-xs">
|
||||||
<span className="text-foreground-600">{t(card.behaviorKey, card.behaviorFallback)}</span>
|
<span className="text-foreground-600">{t(card.behaviorKey, card.behaviorFallback)}</span>
|
||||||
{showCaution ? (
|
{showCaution ? (
|
||||||
<span className="flex items-center gap-1 text-danger-600">
|
<div className="flex items-center gap-1">
|
||||||
<ShieldAlert size={14} />
|
<ShieldAlert size={24} />
|
||||||
{t(
|
<span className="text-danger-600">
|
||||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
{t(
|
||||||
'Use with caution — all tools will run without asking for approval.'
|
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||||
)}
|
'Use with caution — all tools will run without asking for approval.'
|
||||||
</span>
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
@ -375,7 +402,7 @@ export const AgentToolingSettings: FC<AgentToolingSettingsProps> = ({ agent, upd
|
|||||||
onValueChange={setSearchTerm}
|
onValueChange={setSearchTerm}
|
||||||
placeholder={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
placeholder={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
||||||
aria-label={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">
|
<div className="flex flex-col gap-3">
|
||||||
{filteredTools.length === 0 ? (
|
{filteredTools.length === 0 ? (
|
||||||
|
|||||||
@ -267,6 +267,10 @@ export const SessionIdParamSchema = z.object({
|
|||||||
sessionId: z.string().min(1, 'Session ID is required')
|
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
|
// Query validation schemas
|
||||||
export const PaginationQuerySchema = z.object({
|
export const PaginationQuerySchema = z.object({
|
||||||
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user