Compare commits

..

4 Commits

Author SHA1 Message Date
suyao
3da8b63673 Merge remote-tracking branch 'origin/main' into feat/sub_agents 2025-11-21 14:57:03 +08:00
SuYao
dcdd1bf852 refactor: replace renderToolContent function with ToolContent component for improved readability (#11300)
* refactor: replace renderToolContent function with ToolContent component for improved readability

* fix

* fix test
2025-11-21 09:55:46 +08:00
beyondkmp
a12b6bfeca feat: enable native language emoji search with CLDR data format (#11381)
* feat: add i18n support and local data to emoji picker

- Add emoji-picker-element-data package for offline-first emoji data
- Implement i18n translations for emoji picker UI (de, en, es, fr, ja, pt, ru, zh)
- Switch from CDN to local emoji data to improve performance and reliability
- Add locale mapping to match app language with emoji picker data
- Move emoji-picker-element import to EmojiPicker component for better encapsulation
- Use proper TypeScript types instead of 'any' for type safety

This improves user experience by providing localized emoji picker interface
and eliminating dependency on external CDN, ensuring the picker works offline.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: enable native language emoji search with CLDR data format

Switch from emojibase to CLDR format for emoji-picker-element data to support full multi-language search functionality. Users can now search for emojis in their native language (e.g., German users can search "Herz" for ❤️, Spanish users can search "corazón"). Also improves type safety by using the LanguageVarious type for locale mappings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 19:23:27 +08:00
suyao
1175823ab8 feat: add support for sub-agents in agent and session management
- Added 'sub_agents' field to the BaseService, agents schema, and sessions schema.
- Implemented methods in AgentService and SessionService to handle sub-agent configurations.
- Updated ClaudeCodeService to load and manage sub-agents.
- Enhanced UI components to display and select sub-agents in the agent settings and activity directory.
- Added translations for sub-agent related UI elements in multiple languages.
- Created SubAgentsSettings component for managing sub-agent associations in agent settings.
2025-11-19 15:58:15 +08:00
43 changed files with 1303 additions and 317 deletions

View File

@@ -81,11 +81,12 @@
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch", "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch",
"@libsql/client": "0.15.15", "@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.5.22", "@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch", "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@paymoapp/electron-shutdown-handler": "^1.1.2", "@paymoapp/electron-shutdown-handler": "^1.1.2",
"@strongtz/win32-arm64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7",
"emoji-picker-element-data": "^1",
"express": "^5.1.0", "express": "^5.1.0",
"font-list": "^2.0.0", "font-list": "^2.0.0",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
@@ -386,10 +387,10 @@
"@codemirror/lint": "6.8.5", "@codemirror/lint": "6.8.5",
"@codemirror/view": "6.38.1", "@codemirror/view": "6.38.1",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", "@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
"@libsql/client": "0.15.15",
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch", "atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch", "file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
"node-abi": "4.24.0", "node-abi": "4.24.0",
"openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0", "openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0",
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0", "openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0",

View File

@@ -0,0 +1,2 @@
ALTER TABLE `agents` ADD `sub_agents` text;--> statement-breakpoint
ALTER TABLE `sessions` ADD `sub_agents` text;

View File

@@ -0,0 +1,360 @@
{
"version": "6",
"dialect": "sqlite",
"id": "9aeb5f21-fed7-4dbf-973d-c344681b71c2",
"prevId": "0cf3d79e-69bf-4dba-8df4-996b9b67d2e8",
"tables": {
"agents": {
"name": "agents",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"accessible_paths": {
"name": "accessible_paths",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"instructions": {
"name": "instructions",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"plan_model": {
"name": "plan_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"small_model": {
"name": "small_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"mcps": {
"name": "mcps",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_tools": {
"name": "allowed_tools",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sub_agents": {
"name": "sub_agents",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"configuration": {
"name": "configuration",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session_messages": {
"name": "session_messages",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_session_id": {
"name": "agent_session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"migrations": {
"name": "migrations",
"columns": {
"version": {
"name": "version",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"tag": {
"name": "tag",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"executed_at": {
"name": "executed_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"agent_type": {
"name": "agent_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_id": {
"name": "agent_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"accessible_paths": {
"name": "accessible_paths",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"instructions": {
"name": "instructions",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"plan_model": {
"name": "plan_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"small_model": {
"name": "small_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"mcps": {
"name": "mcps",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_tools": {
"name": "allowed_tools",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sub_agents": {
"name": "sub_agents",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"slash_commands": {
"name": "slash_commands",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"configuration": {
"name": "configuration",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1762526423527, "when": 1762526423527,
"tag": "0002_wealthy_naoko", "tag": "0002_wealthy_naoko",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1763500397620,
"tag": "0003_smooth_talkback",
"breakpoints": true
} }
] ]
} }

View File

@@ -104,12 +104,6 @@ const router = express
logger.warn('No models available from providers', { filter }) logger.warn('No models available from providers', { filter })
} }
logger.info('Models response ready', {
filter,
total: response.total,
modelIds: response.data.map((m) => m.id)
})
return res.json(response satisfies ApiModelsResponse) return res.json(response satisfies ApiModelsResponse)
} catch (error: any) { } catch (error: any) {
logger.error('Error fetching models', { error }) logger.error('Error fetching models', { error })

View File

@@ -32,7 +32,7 @@ export class ModelsService {
for (const model of models) { for (const model of models) {
const provider = providers.find((p) => p.id === model.provider) const provider = providers.find((p) => p.id === model.provider)
logger.debug(`Processing model ${model.id}`) // logger.debug(`Processing model ${model.id}`)
if (!provider) { if (!provider) {
logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`) logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`)
continue continue

View File

@@ -849,7 +849,7 @@ class FileStorage {
const resolvedPath = path.resolve(dirPath) const resolvedPath = path.resolve(dirPath)
const stat = await fs.promises.stat(resolvedPath).catch((error) => { const stat = await fs.promises.stat(resolvedPath).catch((error) => {
logger.error(`[IPC - Error] Failed to access directory: ${resolvedPath}`, error as Error) logger.error(`Failed to access directory: ${resolvedPath}`, error as Error)
throw error throw error
}) })

View File

@@ -42,6 +42,7 @@ export abstract class BaseService {
'configuration', 'configuration',
'accessible_paths', 'accessible_paths',
'allowed_tools', 'allowed_tools',
'sub_agents',
'slash_commands' 'slash_commands'
] ]

View File

@@ -19,6 +19,7 @@ export const agentsTable = sqliteTable('agents', {
mcps: text('mcps'), // JSON array of MCP tool IDs mcps: text('mcps'), // JSON array of MCP tool IDs
allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist) allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist)
sub_agents: text('sub_agents'), // JSON array of sub-agent IDs
configuration: text('configuration'), // JSON, extensible settings configuration: text('configuration'), // JSON, extensible settings

View File

@@ -22,6 +22,7 @@ export const sessionsTable = sqliteTable('sessions', {
mcps: text('mcps'), // JSON array of MCP tool IDs mcps: text('mcps'), // JSON array of MCP tool IDs
allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist) allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist)
sub_agents: text('sub_agents'), // JSON array of sub-agent IDs
slash_commands: text('slash_commands'), // JSON array of slash command objects from SDK init slash_commands: text('slash_commands'), // JSON array of slash command objects from SDK init
configuration: text('configuration'), // JSON, extensible settings configuration: text('configuration'), // JSON, extensible settings

View File

@@ -117,6 +117,19 @@ export class AgentService extends BaseService {
return agent return agent
} }
async getAgentConfigForSDK(id: string): Promise<AgentEntity | null> {
this.ensureInitialized()
const result = await this.database.select().from(agentsTable).where(eq(agentsTable.id, id)).limit(1)
if (!result[0]) {
return null
}
const agent = this.deserializeJsonFields(result[0]) as AgentEntity
return agent
}
async listAgents(options: ListOptions = {}): Promise<{ agents: AgentEntity[]; total: number }> { async listAgents(options: ListOptions = {}): Promise<{ agents: AgentEntity[]; total: number }> {
this.ensureInitialized() // Build query with pagination this.ensureInitialized() // Build query with pagination

View File

@@ -130,6 +130,7 @@ export class SessionService extends BaseService {
small_model: serializedData.small_model || null, small_model: serializedData.small_model || null,
mcps: serializedData.mcps || null, mcps: serializedData.mcps || null,
allowed_tools: serializedData.allowed_tools || null, allowed_tools: serializedData.allowed_tools || null,
sub_agents: serializedData.sub_agents || null,
configuration: serializedData.configuration || null, configuration: serializedData.configuration || null,
created_at: now, created_at: now,
updated_at: now updated_at: now
@@ -169,6 +170,22 @@ export class SessionService extends BaseService {
session.slash_commands = await this.listSlashCommands(session.agent_type, agentId) session.slash_commands = await this.listSlashCommands(session.agent_type, agentId)
} }
// Load installed plugins from cache file
const workdir = session.accessible_paths?.[0]
if (workdir) {
try {
session.plugins = await pluginService.listInstalledFromCache(workdir)
} catch (error) {
logger.warn(`Failed to load installed plugins for session ${id}`, {
workdir,
error: error instanceof Error ? error.message : String(error)
})
session.plugins = []
}
} else {
session.plugins = []
}
return session return session
} }

View File

@@ -21,6 +21,11 @@ describe('stripLocalCommandTags', () => {
'<local-command-stdout>line1</local-command-stdout>\nkeep\n<local-command-stderr>Error</local-command-stderr>' '<local-command-stdout>line1</local-command-stdout>\nkeep\n<local-command-stderr>Error</local-command-stderr>'
expect(stripLocalCommandTags(input)).toBe('line1\nkeep\nError') expect(stripLocalCommandTags(input)).toBe('line1\nkeep\nError')
}) })
it('if no tags present, returns original string', () => {
const input = 'just some normal text'
expect(stripLocalCommandTags(input)).toBe(input)
})
}) })
describe('Claude → AiSDK transform', () => { describe('Claude → AiSDK transform', () => {
@@ -188,6 +193,111 @@ describe('Claude → AiSDK transform', () => {
expect(toolResult.output).toBe('ok') expect(toolResult.output).toBe('ok')
}) })
it('handles tool calls without streaming events (no content_block_start/stop)', () => {
const state = new ClaudeStreamState({ agentSessionId: '12344' })
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
const messages: SDKMessage[] = [
{
...baseStreamMetadata,
type: 'assistant',
uuid: uuid(20),
message: {
id: 'msg-tool-no-stream',
type: 'message',
role: 'assistant',
model: 'claude-test',
content: [
{
type: 'tool_use',
id: 'tool-read',
name: 'Read',
input: { file_path: '/test.txt' }
},
{
type: 'tool_use',
id: 'tool-bash',
name: 'Bash',
input: { command: 'ls -la' }
}
],
stop_reason: 'tool_use',
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 20
}
}
} as unknown as SDKMessage,
{
...baseStreamMetadata,
type: 'user',
uuid: uuid(21),
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-read',
content: 'file contents',
is_error: false
}
]
}
} as SDKMessage,
{
...baseStreamMetadata,
type: 'user',
uuid: uuid(22),
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-bash',
content: 'total 42\n...',
is_error: false
}
]
}
} as SDKMessage
]
for (const message of messages) {
const transformed = transformSDKMessageToStreamParts(message, state)
parts.push(...transformed)
}
const types = parts.map((part) => part.type)
expect(types).toEqual(['tool-call', 'tool-call', 'tool-result', 'tool-result'])
const toolCalls = parts.filter((part) => part.type === 'tool-call') as Extract<
(typeof parts)[number],
{ type: 'tool-call' }
>[]
expect(toolCalls).toHaveLength(2)
expect(toolCalls[0].toolName).toBe('Read')
expect(toolCalls[0].toolCallId).toBe('12344:tool-read')
expect(toolCalls[1].toolName).toBe('Bash')
expect(toolCalls[1].toolCallId).toBe('12344:tool-bash')
const toolResults = parts.filter((part) => part.type === 'tool-result') as Extract<
(typeof parts)[number],
{ type: 'tool-result' }
>[]
expect(toolResults).toHaveLength(2)
// This is the key assertion - toolName should NOT be 'unknown'
expect(toolResults[0].toolName).toBe('Read')
expect(toolResults[0].toolCallId).toBe('12344:tool-read')
expect(toolResults[0].input).toEqual({ file_path: '/test.txt' })
expect(toolResults[0].output).toBe('file contents')
expect(toolResults[1].toolName).toBe('Bash')
expect(toolResults[1].toolCallId).toBe('12344:tool-bash')
expect(toolResults[1].input).toEqual({ command: 'ls -la' })
expect(toolResults[1].output).toBe('total 42\n...')
})
it('handles streaming text completion', () => { it('handles streaming text completion', () => {
const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id }) const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id })
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = [] const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
@@ -300,4 +410,87 @@ describe('Claude → AiSDK transform', () => {
expect(finishStep.finishReason).toBe('stop') expect(finishStep.finishReason).toBe('stop')
expect(finishStep.usage).toEqual({ inputTokens: 2, outputTokens: 4, totalTokens: 6 }) expect(finishStep.usage).toEqual({ inputTokens: 2, outputTokens: 4, totalTokens: 6 })
}) })
it('emits fallback text when Claude sends a snapshot instead of deltas', () => {
const state = new ClaudeStreamState({ agentSessionId: '12344' })
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
const messages: SDKMessage[] = [
{
...baseStreamMetadata,
type: 'stream_event',
uuid: uuid(30),
event: {
type: 'message_start',
message: {
id: 'msg-fallback',
type: 'message',
role: 'assistant',
model: 'claude-test',
content: [],
stop_reason: null,
stop_sequence: null,
usage: {}
}
}
} as unknown as SDKMessage,
{
...baseStreamMetadata,
type: 'stream_event',
uuid: uuid(31),
event: {
type: 'content_block_start',
index: 0,
content_block: {
type: 'text',
text: ''
}
}
} as unknown as SDKMessage,
{
...baseStreamMetadata,
type: 'assistant',
uuid: uuid(32),
message: {
id: 'msg-fallback-content',
type: 'message',
role: 'assistant',
model: 'claude-test',
content: [
{
type: 'text',
text: 'Final answer without streaming deltas.'
}
],
stop_reason: 'end_turn',
stop_sequence: null,
usage: {
input_tokens: 3,
output_tokens: 7
}
}
} as unknown as SDKMessage
]
for (const message of messages) {
const transformed = transformSDKMessageToStreamParts(message, state)
parts.push(...transformed)
}
const types = parts.map((part) => part.type)
expect(types).toEqual(['start-step', 'text-start', 'text-delta', 'text-end', 'finish-step'])
const delta = parts.find((part) => part.type === 'text-delta') as Extract<
(typeof parts)[number],
{ type: 'text-delta' }
>
expect(delta.text).toBe('Final answer without streaming deltas.')
const finish = parts.find((part) => part.type === 'finish-step') as Extract<
(typeof parts)[number],
{ type: 'finish-step' }
>
expect(finish.usage).toEqual({ inputTokens: 3, outputTokens: 7, totalTokens: 10 })
expect(finish.finishReason).toBe('stop')
})
}) })

View File

@@ -153,6 +153,20 @@ export class ClaudeStreamState {
return this.blocksByIndex.get(index) return this.blocksByIndex.get(index)
} }
getFirstOpenTextBlock(): TextBlockState | undefined {
const candidates: TextBlockState[] = []
for (const block of this.blocksByIndex.values()) {
if (block.kind === 'text') {
candidates.push(block)
}
}
if (candidates.length === 0) {
return undefined
}
candidates.sort((a, b) => a.index - b.index)
return candidates[0]
}
getToolBlockById(toolCallId: string): ToolBlockState | undefined { getToolBlockById(toolCallId: string): ToolBlockState | undefined {
const index = this.toolIndexByNamespacedId.get(toolCallId) const index = this.toolIndexByNamespacedId.get(toolCallId)
if (index === undefined) return undefined if (index === undefined) return undefined
@@ -217,10 +231,10 @@ export class ClaudeStreamState {
* Persists the final input payload for a tool block once the provider signals * Persists the final input payload for a tool block once the provider signals
* completion so that downstream tool results can reference the original call. * completion so that downstream tool results can reference the original call.
*/ */
completeToolBlock(toolCallId: string, input: unknown, providerMetadata?: ProviderMetadata): void { completeToolBlock(toolCallId: string, toolName: string, input: unknown, providerMetadata?: ProviderMetadata): void {
const block = this.getToolBlockByRawId(toolCallId) const block = this.getToolBlockByRawId(toolCallId)
this.registerToolCall(toolCallId, { this.registerToolCall(toolCallId, {
toolName: block?.toolName ?? 'unknown', toolName,
input, input,
providerMetadata providerMetadata
}) })

View File

@@ -2,7 +2,13 @@
import { EventEmitter } from 'node:events' import { EventEmitter } from 'node:events'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import type { CanUseTool, McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk' import type {
AgentDefinition,
CanUseTool,
McpHttpServerConfig,
Options,
SDKMessage
} from '@anthropic-ai/claude-agent-sdk'
import { query } from '@anthropic-ai/claude-agent-sdk' import { query } from '@anthropic-ai/claude-agent-sdk'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { config as apiConfigService } from '@main/apiServer/config' import { config as apiConfigService } from '@main/apiServer/config'
@@ -10,7 +16,7 @@ import { validateModelId } from '@main/apiServer/utils'
import getLoginShellEnvironment from '@main/utils/shell-env' import getLoginShellEnvironment from '@main/utils/shell-env'
import { app } from 'electron' import { app } from 'electron'
import type { GetAgentSessionResponse } from '../..' import { agentService, type GetAgentSessionResponse } from '../..'
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface' import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
import { sessionService } from '../SessionService' import { sessionService } from '../SessionService'
import { buildNamespacedToolCallId } from './claude-stream-state' import { buildNamespacedToolCallId } from './claude-stream-state'
@@ -157,6 +163,32 @@ class ClaudeCodeService implements AgentServiceInterface {
}) })
} }
const subAgents: Record<string, AgentDefinition> = {}
if (session.sub_agents && session.sub_agents.length > 0) {
for (const subAgentId of session.sub_agents) {
try {
const agentConfig = await agentService.getAgentConfigForSDK(subAgentId)
if (agentConfig) {
subAgents[subAgentId] = {
// TODO: support custom model for sub-agents
model: 'inherit',
description: agentConfig.description ?? '',
prompt: agentConfig.instructions ?? '',
tools: agentConfig.allowed_tools
}
logger.info('Loaded sub-agent', { subAgentId })
} else {
logger.warn('Sub-agent not found', { subAgentId })
}
} catch (error) {
logger.error('Failed to load sub-agent config', {
subAgentId,
error: error instanceof Error ? error.message : String(error)
})
}
}
}
// Build SDK options from parameters // Build SDK options from parameters
const options: Options = { const options: Options = {
abortController, abortController,
@@ -414,23 +446,6 @@ class ClaudeCodeService implements AgentServiceInterface {
} }
} }
if (message.type === 'assistant' || message.type === 'user') {
logger.silly('claude response', {
message,
content: JSON.stringify(message.message.content)
})
} else if (message.type === 'stream_event') {
// logger.silly('Claude stream event', {
// message,
// event: JSON.stringify(message.event)
// })
} else {
logger.silly('Claude response', {
message,
event: JSON.stringify(message)
})
}
const chunks = transformSDKMessageToStreamParts(message, streamState) const chunks = transformSDKMessageToStreamParts(message, streamState)
for (const chunk of chunks) { for (const chunk of chunks) {
stream.emit('data', { stream.emit('data', {

View File

@@ -110,7 +110,7 @@ const sdkMessageToProviderMetadata = (message: SDKMessage): ProviderMetadata =>
* blocks across calls so that incremental deltas can be correlated correctly. * blocks across calls so that incremental deltas can be correlated correctly.
*/ */
export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage, state: ClaudeStreamState): AgentStreamPart[] { export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage, state: ClaudeStreamState): AgentStreamPart[] {
logger.silly('Transforming SDKMessage', { message: sdkMessage }) logger.silly('Transforming SDKMessage', { message: JSON.stringify(sdkMessage) })
switch (sdkMessage.type) { switch (sdkMessage.type) {
case 'assistant': case 'assistant':
return handleAssistantMessage(sdkMessage, state) return handleAssistantMessage(sdkMessage, state)
@@ -186,14 +186,13 @@ function handleAssistantMessage(
for (const block of content) { for (const block of content) {
switch (block.type) { switch (block.type) {
case 'text': case 'text': {
if (!isStreamingActive) { const sanitizedText = stripLocalCommandTags(block.text)
const sanitizedText = stripLocalCommandTags(block.text) if (sanitizedText) {
if (sanitizedText) { textBlocks.push(sanitizedText)
textBlocks.push(sanitizedText)
}
} }
break break
}
case 'tool_use': case 'tool_use':
handleAssistantToolUse(block as ToolUseContent, providerMetadata, state, chunks) handleAssistantToolUse(block as ToolUseContent, providerMetadata, state, chunks)
break break
@@ -203,7 +202,16 @@ function handleAssistantMessage(
} }
} }
if (!isStreamingActive && textBlocks.length > 0) { if (textBlocks.length === 0) {
return chunks
}
const combinedText = textBlocks.join('')
if (!combinedText) {
return chunks
}
if (!isStreamingActive) {
const id = message.uuid?.toString() || generateMessageId() const id = message.uuid?.toString() || generateMessageId()
state.beginStep() state.beginStep()
chunks.push({ chunks.push({
@@ -219,7 +227,7 @@ function handleAssistantMessage(
chunks.push({ chunks.push({
type: 'text-delta', type: 'text-delta',
id, id,
text: textBlocks.join(''), text: combinedText,
providerMetadata providerMetadata
}) })
chunks.push({ chunks.push({
@@ -230,7 +238,27 @@ function handleAssistantMessage(
return finalizeNonStreamingStep(message, state, chunks) return finalizeNonStreamingStep(message, state, chunks)
} }
return chunks const existingTextBlock = state.getFirstOpenTextBlock()
const fallbackId = existingTextBlock?.id || message.uuid?.toString() || generateMessageId()
if (!existingTextBlock) {
chunks.push({
type: 'text-start',
id: fallbackId,
providerMetadata
})
}
chunks.push({
type: 'text-delta',
id: fallbackId,
text: combinedText,
providerMetadata
})
chunks.push({
type: 'text-end',
id: fallbackId,
providerMetadata
})
return finalizeNonStreamingStep(message, state, chunks)
} }
/** /**
@@ -252,7 +280,7 @@ function handleAssistantToolUse(
providerExecuted: true, providerExecuted: true,
providerMetadata providerMetadata
}) })
state.completeToolBlock(block.id, block.input, providerMetadata) state.completeToolBlock(block.id, block.name, block.input, providerMetadata)
} }
/** /**
@@ -459,6 +487,9 @@ function handleStreamEvent(
} }
case 'message_stop': { case 'message_stop': {
if (!state.hasActiveStep()) {
break
}
const pending = state.getPendingUsage() const pending = state.getPendingUsage()
chunks.push({ chunks.push({
type: 'finish-step', type: 'finish-step',

View File

@@ -1,35 +1,120 @@
import 'emoji-picker-element'
import TwemojiCountryFlagsWoff2 from '@renderer/assets/fonts/country-flag-fonts/TwemojiCountryFlags.woff2?url' import TwemojiCountryFlagsWoff2 from '@renderer/assets/fonts/country-flag-fonts/TwemojiCountryFlags.woff2?url'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import type { LanguageVarious } from '@renderer/types'
import { polyfillCountryFlagEmojis } from 'country-flag-emoji-polyfill' import { polyfillCountryFlagEmojis } from 'country-flag-emoji-polyfill'
// i18n translations from emoji-picker-element
import de from 'emoji-picker-element/i18n/de'
import en from 'emoji-picker-element/i18n/en'
import es from 'emoji-picker-element/i18n/es'
import fr from 'emoji-picker-element/i18n/fr'
import ja from 'emoji-picker-element/i18n/ja'
import pt_PT from 'emoji-picker-element/i18n/pt_PT'
import ru_RU from 'emoji-picker-element/i18n/ru_RU'
import zh_CN from 'emoji-picker-element/i18n/zh_CN'
import type Picker from 'emoji-picker-element/picker'
import type { EmojiClickEvent, NativeEmoji } from 'emoji-picker-element/shared'
// Emoji data from emoji-picker-element-data (local, no CDN)
// Using CLDR format for full multi-language search support (28 languages)
import dataDE from 'emoji-picker-element-data/de/cldr/data.json?url'
import dataEN from 'emoji-picker-element-data/en/cldr/data.json?url'
import dataES from 'emoji-picker-element-data/es/cldr/data.json?url'
import dataFR from 'emoji-picker-element-data/fr/cldr/data.json?url'
import dataJA from 'emoji-picker-element-data/ja/cldr/data.json?url'
import dataPT from 'emoji-picker-element-data/pt/cldr/data.json?url'
import dataRU from 'emoji-picker-element-data/ru/cldr/data.json?url'
import dataZH from 'emoji-picker-element-data/zh/cldr/data.json?url'
import dataZH_HANT from 'emoji-picker-element-data/zh-hant/cldr/data.json?url'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
interface Props { interface Props {
onEmojiClick: (emoji: string) => void onEmojiClick: (emoji: string) => void
} }
// Mapping from app locale to emoji-picker-element i18n
const i18nMap: Record<LanguageVarious, typeof en> = {
'en-US': en,
'zh-CN': zh_CN,
'zh-TW': zh_CN, // Closest available
'de-DE': de,
'el-GR': en, // No Greek available, fallback to English
'es-ES': es,
'fr-FR': fr,
'ja-JP': ja,
'pt-PT': pt_PT,
'ru-RU': ru_RU
}
// Mapping from app locale to emoji data URL
// Using CLDR format provides native language search support for all locales
const dataSourceMap: Record<LanguageVarious, string> = {
'en-US': dataEN,
'zh-CN': dataZH,
'zh-TW': dataZH_HANT,
'de-DE': dataDE,
'el-GR': dataEN, // No Greek CLDR available, fallback to English
'es-ES': dataES,
'fr-FR': dataFR,
'ja-JP': dataJA,
'pt-PT': dataPT,
'ru-RU': dataRU
}
// Mapping from app locale to emoji-picker-element locale string
// Must match the data source locale for proper IndexedDB caching
const localeMap: Record<LanguageVarious, string> = {
'en-US': 'en',
'zh-CN': 'zh',
'zh-TW': 'zh-hant',
'de-DE': 'de',
'el-GR': 'en',
'es-ES': 'es',
'fr-FR': 'fr',
'ja-JP': 'ja',
'pt-PT': 'pt',
'ru-RU': 'ru'
}
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => { const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
const { theme } = useTheme() const { theme } = useTheme()
const ref = useRef<HTMLDivElement>(null) const { i18n } = useTranslation()
const ref = useRef<Picker>(null)
const currentLocale = i18n.language as LanguageVarious
useEffect(() => { useEffect(() => {
polyfillCountryFlagEmojis('Twemoji Mozilla', TwemojiCountryFlagsWoff2) polyfillCountryFlagEmojis('Twemoji Mozilla', TwemojiCountryFlagsWoff2)
}, []) }, [])
// Configure picker with i18n and dataSource
useEffect(() => { useEffect(() => {
const refValue = ref.current const picker = ref.current
if (picker) {
picker.i18n = i18nMap[currentLocale] || en
picker.dataSource = dataSourceMap[currentLocale] || dataEN
picker.locale = localeMap[currentLocale] || 'en'
}
}, [currentLocale])
if (refValue) { useEffect(() => {
const handleEmojiClick = (event: any) => { const picker = ref.current
if (picker) {
const handleEmojiClick = (event: EmojiClickEvent) => {
event.stopPropagation() event.stopPropagation()
onEmojiClick(event.detail.unicode || event.detail.emoji.unicode) const { detail } = event
// Use detail.unicode (processed with skin tone) or fallback to emoji's unicode for native emoji
const unicode = detail.unicode || ('unicode' in detail.emoji ? (detail.emoji as NativeEmoji).unicode : '')
onEmojiClick(unicode)
} }
// 添加事件监听器 // 添加事件监听器
refValue.addEventListener('emoji-click', handleEmojiClick) picker.addEventListener('emoji-click', handleEmojiClick)
// 清理事件监听器 // 清理事件监听器
return () => { return () => {
refValue.removeEventListener('emoji-click', handleEmojiClick) picker.removeEventListener('emoji-click', handleEmojiClick)
} }
} }
return return

View File

@@ -156,6 +156,12 @@
"uninstalling": "Uninstalling..." "uninstalling": "Uninstalling..."
}, },
"prompt": "Prompt Settings", "prompt": "Prompt Settings",
"sub_agents": {
"placeholder": "Select sub agents",
"tab": "Sub Agents",
"title": "Sub Agents",
"tooltip": "Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Connect MCP servers to unlock additional tools you can approve above.", "description": "Connect MCP servers to unlock additional tools you can approve above.",
@@ -641,6 +647,7 @@
"description": "No files available in accessible directories", "description": "No files available in accessible directories",
"label": "No File Found" "label": "No File Found"
}, },
"sub_agent": "Sub-Agent",
"title": "Activity Directory" "title": "Activity Directory"
}, },
"auto_resize": "Auto resize height", "auto_resize": "Auto resize height",

View File

@@ -156,6 +156,12 @@
"uninstalling": "卸载中..." "uninstalling": "卸载中..."
}, },
"prompt": "提示词设置", "prompt": "提示词设置",
"sub_agents": {
"placeholder": "选择子智能体",
"tab": "子智能体",
"title": "子智能体",
"tooltip": "选择可以被此智能体委派任务的其他智能体"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "连接 MCP 服务器即可解锁更多可在上方预先授权的工具。", "description": "连接 MCP 服务器即可解锁更多可在上方预先授权的工具。",
@@ -641,6 +647,7 @@
"description": "可访问目录中没有可用文件", "description": "可访问目录中没有可用文件",
"label": "未找到文件" "label": "未找到文件"
}, },
"sub_agent": "子代理",
"title": "活动目录" "title": "活动目录"
}, },
"auto_resize": "自动调整高度", "auto_resize": "自动调整高度",

View File

@@ -156,6 +156,12 @@
"uninstalling": "解除安裝中..." "uninstalling": "解除安裝中..."
}, },
"prompt": "提示設定", "prompt": "提示設定",
"sub_agents": {
"placeholder": "選擇子助手",
"tab": "子助手",
"title": "子助手",
"tooltip": "選擇可以被此助手委派任務的其他助手"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "連線 MCP 伺服器即可解鎖更多可在上方預先授權的工具。", "description": "連線 MCP 伺服器即可解鎖更多可在上方預先授權的工具。",
@@ -641,6 +647,7 @@
"description": "可存取的目錄中沒有檔案", "description": "可存取的目錄中沒有檔案",
"label": "找不到檔案" "label": "找不到檔案"
}, },
"sub_agent": "子代理",
"title": "活動目錄" "title": "活動目錄"
}, },
"auto_resize": "自動調整高度", "auto_resize": "自動調整高度",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Deinstallation läuft..." "uninstalling": "Deinstallation läuft..."
}, },
"prompt": "Prompt-Einstellungen", "prompt": "Prompt-Einstellungen",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Verbinden Sie MCP-Server, um weitere Tools freizuschalten, die oben vorab autorisiert werden können.", "description": "Verbinden Sie MCP-Server, um weitere Tools freizuschalten, die oben vorab autorisiert werden können.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Απεγκατάσταση..." "uninstalling": "Απεγκατάσταση..."
}, },
"prompt": "Ρυθμίσεις Προτροπής", "prompt": "Ρυθμίσεις Προτροπής",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Συνδέστε διακομιστές MCP για να ξεκλειδώσετε πρόσθετα εργαλεία που μπορείτε να εγκρίνετε παραπάνω.", "description": "Συνδέστε διακομιστές MCP για να ξεκλειδώσετε πρόσθετα εργαλεία που μπορείτε να εγκρίνετε παραπάνω.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Desinstalando..." "uninstalling": "Desinstalando..."
}, },
"prompt": "Configuración de indicaciones", "prompt": "Configuración de indicaciones",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Conecta servidores MCP para desbloquear herramientas adicionales que puedes aprobar arriba.", "description": "Conecta servidores MCP para desbloquear herramientas adicionales que puedes aprobar arriba.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Désinstallation en cours..." "uninstalling": "Désinstallation en cours..."
}, },
"prompt": "Paramètres de l'invite", "prompt": "Paramètres de l'invite",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Connectez des serveurs MCP pour débloquer des outils supplémentaires que vous pouvez approuver ci-dessus.", "description": "Connectez des serveurs MCP pour débloquer des outils supplémentaires que vous pouvez approuver ci-dessus.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "アンインストール中..." "uninstalling": "アンインストール中..."
}, },
"prompt": "プロンプト設定", "prompt": "プロンプト設定",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "MCPサーバーを接続して、上で承認できる追加ツールを解放します。", "description": "MCPサーバーを接続して、上で承認できる追加ツールを解放します。",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Desinstalando..." "uninstalling": "Desinstalando..."
}, },
"prompt": "Configurações de Prompt", "prompt": "Configurações de Prompt",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Conecte servidores MCP para desbloquear ferramentas adicionais que você pode aprovar acima.", "description": "Conecte servidores MCP para desbloquear ferramentas adicionais que você pode aprovar acima.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Удаление..." "uninstalling": "Удаление..."
}, },
"prompt": "Настройки подсказки", "prompt": "Настройки подсказки",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": { "tooling": {
"mcp": { "mcp": {
"description": "Подключите серверы MCP, чтобы разблокировать дополнительные инструменты, которые вы можете одобрить выше.", "description": "Подключите серверы MCP, чтобы разблокировать дополнительные инструменты, которые вы можете одобрить выше.",

View File

@@ -103,12 +103,23 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
// Prepare session data for tools // Prepare session data for tools
const sessionData = useMemo(() => { const sessionData = useMemo(() => {
if (!session) return undefined if (!session) return undefined
// Get installed agent plugins from session.plugins
const agentPlugins = (session.plugins ?? [])
.filter((plugin) => plugin.type === 'agent')
.map((plugin) => ({
id: plugin.filename,
name: plugin.metadata.name ?? plugin.filename.replace(/\.md$/i, ''),
description: plugin.metadata.description
}))
return { return {
agentId, agentId,
sessionId, sessionId,
slashCommands: session.slash_commands, slashCommands: session.slash_commands,
tools: session.tools, tools: session.tools,
accessiblePaths: session.accessible_paths ?? [] accessiblePaths: session.accessible_paths ?? [],
subAgents: agentPlugins
} }
}, [session, agentId, sessionId]) }, [session, agentId, sessionId])
@@ -158,6 +169,8 @@ interface InnerProps {
sessionId?: string sessionId?: string
slashCommands?: Array<{ command: string; description?: string }> slashCommands?: Array<{ command: string; description?: string }>
tools?: Array<{ id: string; name: string; type: string; description?: string }> tools?: Array<{ id: string; name: string; type: string; description?: string }>
accessiblePaths?: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
} }
actionsRef: React.MutableRefObject<{ actionsRef: React.MutableRefObject<{
resizeTextArea: () => void resizeTextArea: () => void

View File

@@ -25,11 +25,12 @@ const activityDirectoryTool = defineTool({
const { quickPanel, quickPanelController, actions, session } = context const { quickPanel, quickPanelController, actions, session } = context
const { onTextChange } = actions const { onTextChange } = actions
// Get accessible paths from session data // Get accessible paths and sub-agents from session data
const accessiblePaths = session?.accessiblePaths ?? [] const accessiblePaths = session?.accessiblePaths ?? []
const subAgents = session?.subAgents ?? []
// Only render if we have accessible paths // Only render if we have accessible paths or sub-agents
if (accessiblePaths.length === 0) { if (accessiblePaths.length === 0 && subAgents.length === 0) {
return null return null
} }
@@ -38,6 +39,7 @@ const activityDirectoryTool = defineTool({
quickPanel={quickPanel} quickPanel={quickPanel}
quickPanelController={quickPanelController} quickPanelController={quickPanelController}
accessiblePaths={accessiblePaths} accessiblePaths={accessiblePaths}
subAgents={subAgents}
setText={onTextChange as React.Dispatch<React.SetStateAction<string>>} setText={onTextChange as React.Dispatch<React.SetStateAction<string>>}
/> />
) )

View File

@@ -13,10 +13,17 @@ interface Props {
quickPanel: ToolQuickPanelApi quickPanel: ToolQuickPanelApi
quickPanelController: ToolQuickPanelController quickPanelController: ToolQuickPanelController
accessiblePaths: string[] accessiblePaths: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
setText: React.Dispatch<React.SetStateAction<string>> setText: React.Dispatch<React.SetStateAction<string>>
} }
const ActivityDirectoryButton: FC<Props> = ({ quickPanel, quickPanelController, accessiblePaths, setText }) => { const ActivityDirectoryButton: FC<Props> = ({
quickPanel,
quickPanelController,
accessiblePaths,
subAgents,
setText
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleOpenQuickPanel } = useActivityDirectoryPanel( const { handleOpenQuickPanel } = useActivityDirectoryPanel(
@@ -24,6 +31,7 @@ const ActivityDirectoryButton: FC<Props> = ({ quickPanel, quickPanelController,
quickPanel, quickPanel,
quickPanelController, quickPanelController,
accessiblePaths, accessiblePaths,
subAgents,
setText setText
}, },
'button' 'button'

View File

@@ -15,8 +15,9 @@ const ActivityDirectoryQuickPanelManager = ({ context }: ManagerProps) => {
session session
} = context } = context
// Get accessible paths from session data // Get accessible paths and sub-agents from session data
const accessiblePaths = session?.accessiblePaths ?? [] const accessiblePaths = session?.accessiblePaths ?? []
const subAgents = session?.subAgents ?? []
// Always call hooks unconditionally (React rules) // Always call hooks unconditionally (React rules)
useActivityDirectoryPanel( useActivityDirectoryPanel(
@@ -24,6 +25,7 @@ const ActivityDirectoryQuickPanelManager = ({ context }: ManagerProps) => {
quickPanel, quickPanel,
quickPanelController, quickPanelController,
accessiblePaths, accessiblePaths,
subAgents,
setText: onTextChange as React.Dispatch<React.SetStateAction<string>> setText: onTextChange as React.Dispatch<React.SetStateAction<string>>
}, },
'manager' 'manager'

View File

@@ -2,7 +2,7 @@ import { loggerService } from '@logger'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel' import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { QuickPanelReservedSymbol } from '@renderer/components/QuickPanel' import { QuickPanelReservedSymbol } from '@renderer/components/QuickPanel'
import type { ToolQuickPanelApi, ToolQuickPanelController } from '@renderer/pages/home/Inputbar/types' import type { ToolQuickPanelApi, ToolQuickPanelController } from '@renderer/pages/home/Inputbar/types'
import { File, Folder } from 'lucide-react' import { Bot, File, Folder } from 'lucide-react'
import type React from 'react' import type React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -25,15 +25,22 @@ export type ActivityDirectoryTriggerInfo = {
symbol?: QuickPanelReservedSymbol symbol?: QuickPanelReservedSymbol
} }
interface SubAgentInfo {
id: string
name: string
description?: string
}
interface Params { interface Params {
quickPanel: ToolQuickPanelApi quickPanel: ToolQuickPanelApi
quickPanelController: ToolQuickPanelController quickPanelController: ToolQuickPanelController
accessiblePaths: string[] accessiblePaths: string[]
subAgents?: SubAgentInfo[]
setText: React.Dispatch<React.SetStateAction<string>> setText: React.Dispatch<React.SetStateAction<string>>
} }
export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'manager' = 'button') => { export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'manager' = 'button') => {
const { quickPanel, quickPanelController, accessiblePaths, setText } = params const { quickPanel, quickPanelController, accessiblePaths, subAgents = [], setText } = params
const { registerTrigger, registerRootMenu } = quickPanel const { registerTrigger, registerRootMenu } = quickPanel
const { open, close, updateList, isVisible, symbol } = quickPanelController const { open, close, updateList, isVisible, symbol } = quickPanelController
const { t } = useTranslation() const { t } = useTranslation()
@@ -238,6 +245,68 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
[close, insertFilePath] [close, insertFilePath]
) )
/**
* Insert sub-agent name at @ position
*/
const insertSubAgentName = useCallback(
(agentName: string, triggerInfo?: ActivityDirectoryTriggerInfo) => {
setText((currentText) => {
const symbol = triggerInfo?.symbol ?? QuickPanelReservedSymbol.MentionModels
const triggerIndex =
triggerInfo?.position !== undefined
? triggerInfo.position
: symbol === QuickPanelReservedSymbol.Root
? currentText.lastIndexOf('/')
: currentText.lastIndexOf('@')
if (triggerIndex !== -1) {
let endPos = triggerIndex + 1
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
endPos++
}
return currentText.slice(0, triggerIndex) + agentName + ' ' + currentText.slice(endPos)
}
// If no trigger found, append at end
return currentText + ' ' + agentName + ' '
})
},
[setText]
)
/**
* Handle sub-agent selection
*/
const onSelectSubAgent = useCallback(
(agentName: string) => {
const trigger = triggerInfoRef.current
insertSubAgentName(agentName, trigger)
close()
},
[close, insertSubAgentName]
)
/**
* Create sub-agent list items for QuickPanel
*/
const createSubAgentItems = useCallback(
(agents: SubAgentInfo[]): QuickPanelListItem[] => {
if (agents.length === 0) {
return []
}
return agents.map((agent) => ({
label: agent.name,
description: agent.description || t('chat.input.activity_directory.sub_agent'),
icon: <Bot size={16} />,
filterText: `${agent.name} ${agent.description || ''} ${agent.id}`,
action: () => onSelectSubAgent(agent.name),
isSelected: false
}))
},
[onSelectSubAgent, t]
)
/** /**
* Create file list items for QuickPanel from a file list * Create file list items for QuickPanel from a file list
*/ */
@@ -291,12 +360,18 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
) )
/** /**
* Create file list items for QuickPanel (for current state) * Create combined list items for QuickPanel (sub-agents + files)
*/ */
const fileItems = useMemo<QuickPanelListItem[]>( const combinedItems = useMemo<QuickPanelListItem[]>(() => {
() => createFileItems(fileList, isLoading), const agentItems = createSubAgentItems(subAgents)
[createFileItems, fileList, isLoading] const files = createFileItems(fileList, isLoading)
)
// Combine: sub-agents first, then files
return [...agentItems, ...files]
}, [createSubAgentItems, subAgents, createFileItems, fileList, isLoading])
// Keep fileItems for backward compatibility
const fileItems = combinedItems
/** /**
* Handle search text change - load files and update list * Handle search text change - load files and update list
@@ -311,11 +386,13 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
const hasChanged = updateFileListState(newFiles) const hasChanged = updateFileListState(newFiles)
if (hasChanged) { if (hasChanged) {
const newItems = createFileItems(newFiles, false) // Combine sub-agents and files
updateList(newItems) const agentItems = createSubAgentItems(subAgents)
const fileItems = createFileItems(newFiles, false)
updateList([...agentItems, ...fileItems])
} }
}, },
[loadFiles, createFileItems, updateList, updateFileListState] [loadFiles, createFileItems, createSubAgentItems, subAgents, updateList, updateFileListState]
) )
/** /**
@@ -336,8 +413,10 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
const files = await loadFiles() const files = await loadFiles()
updateFileListState(files) updateFileListState(files)
// Create items from the loaded files immediately // Create items from sub-agents and loaded files immediately
const items = createFileItems(files, false) const agentItems = createSubAgentItems(subAgents)
const fileItems = createFileItems(files, false)
const items = [...agentItems, ...fileItems]
open({ open({
title: t('chat.input.activity_directory.description'), title: t('chat.input.activity_directory.description'),
@@ -377,7 +456,18 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
onSearchChange: handleSearchChange onSearchChange: handleSearchChange
}) })
}, },
[loadFiles, open, removeTriggerSymbolAndText, setText, t, handleSearchChange, createFileItems, updateFileListState] [
loadFiles,
open,
removeTriggerSymbolAndText,
setText,
t,
handleSearchChange,
createFileItems,
createSubAgentItems,
subAgents,
updateFileListState
]
) )
/** /**

View File

@@ -68,6 +68,7 @@ export interface ToolContext {
slashCommands?: Array<{ command: string; description?: string }> slashCommands?: Array<{ command: string; description?: string }>
tools?: Array<{ id: string; name: string; type: string; description?: string }> tools?: Array<{ id: string; name: string; type: string; description?: string }>
accessiblePaths?: string[] accessiblePaths?: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
} }
} }

View File

@@ -1,7 +1,6 @@
import type { CollapseProps } from 'antd' import type { CollapseProps } from 'antd'
import { Tag } from 'antd' import { Tag } from 'antd'
import { CheckCircle, Terminal, XCircle } from 'lucide-react' import { CheckCircle, Terminal, XCircle } from 'lucide-react'
import { useMemo } from 'react'
import { ToolTitle } from './GenericTools' import { ToolTitle } from './GenericTools'
import type { BashOutputToolInput, BashOutputToolOutput } from './types' import type { BashOutputToolInput, BashOutputToolOutput } from './types'
@@ -16,6 +15,63 @@ interface ParsedBashOutput {
tool_use_error?: string tool_use_error?: string
} }
const parseBashOutput = (output?: BashOutputToolOutput): ParsedBashOutput | null => {
if (!output) return null
try {
const parser = new DOMParser()
const hasToolError = output.includes('<tool_use_error>')
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()
}
return {
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')
}
} catch {
return null
}
}
const getStatusConfig = (parsedOutput: ParsedBashOutput | null) => {
if (!parsedOutput) return null
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
}
export function BashOutputTool({ export function BashOutputTool({
input, input,
output output
@@ -23,73 +79,8 @@ export function BashOutputTool({
input: BashOutputToolInput input: BashOutputToolInput
output?: BashOutputToolOutput output?: BashOutputToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 解析 XML 输出 const parsedOutput = parseBashOutput(output)
const parsedOutput = useMemo(() => { const statusConfig = getStatusConfig(parsedOutput)
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])
const children = parsedOutput ? ( const children = parsedOutput ? (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,12 +1,47 @@
import type { CollapseProps } from 'antd' import type { CollapseProps } from 'antd'
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, TextOutput } from './types' import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types'
import { AgentToolsType } from './types' import { AgentToolsType } from './types'
const removeSystemReminderTags = (text: string): string => {
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, '')
}
const normalizeOutputString = (output?: ReadToolOutputType): string | null => {
if (!output) return null
const toText = (item: TextOutput) => removeSystemReminderTags(item.text)
if (Array.isArray(output)) {
return output
.filter((item): item is TextOutput => item.type === 'text')
.map(toText)
.join('')
}
return removeSystemReminderTags(output)
}
const getOutputStats = (outputString: string | null) => {
if (!outputString) return null
const bytes = new Blob([outputString]).size
const formatSize = (size: number) => {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
return `${(size / (1024 * 1024)).toFixed(1)} MB`
}
return {
lineCount: outputString.split('\n').length,
fileSize: bytes,
formatSize
}
}
export function ReadTool({ export function ReadTool({
input, input,
output output
@@ -14,50 +49,8 @@ export function ReadTool({
input: ReadToolInputType input: ReadToolInputType
output?: ReadToolOutputType output?: ReadToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 移除 system-reminder 标签及其内容的辅助函数 const outputString = normalizeOutputString(output)
const removeSystemReminderTags = (text: string): string => { const stats = getOutputStats(outputString)
// 使用正则表达式匹配 <system-reminder> 标签及其内容,包括换行符
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, '')
}
// 将 output 统一转换为字符串
const outputString = useMemo(() => {
if (!output) return null
let processedOutput: string
// 如果是 TextOutput[] 类型,提取所有 text 内容
if (Array.isArray(output)) {
processedOutput = output
.filter((item): item is TextOutput => item.type === 'text')
.map((item) => removeSystemReminderTags(item.text))
.join('')
} else {
// 如果是字符串,直接使用
processedOutput = output
}
// 移除 system-reminder 标签及其内容
return removeSystemReminderTags(processedOutput)
}, [output])
// 如果有输出,计算统计信息
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 { return {
key: AgentToolsType.Read, key: AgentToolsType.Read,

View File

@@ -11,11 +11,24 @@ interface UnknownToolProps {
output?: unknown output?: unknown
} }
export function UnknownToolRenderer({ const getToolDisplayName = (name: string) => {
toolName = '', if (name.startsWith('mcp__')) {
input, const parts = name.substring(5).split('__')
output if (parts.length >= 2) {
}: UnknownToolProps): NonNullable<CollapseProps['items']>[number] { return `${parts[0]}:${parts.slice(1).join(':')}`
}
}
return name
}
const getToolDescription = (toolName: string) => {
if (toolName.startsWith('mcp__')) {
return 'MCP Server Tool'
}
return 'Tool'
}
const UnknownToolContent = ({ input, output }: { input?: unknown; output?: unknown }) => {
const { highlightCode } = useCodeStyle() const { highlightCode } = useCodeStyle()
const [inputHtml, setInputHtml] = useState<string>('') const [inputHtml, setInputHtml] = useState<string>('')
const [outputHtml, setOutputHtml] = useState<string>('') const [outputHtml, setOutputHtml] = useState<string>('')
@@ -34,58 +47,49 @@ export function UnknownToolRenderer({
} }
}, [output, highlightCode]) }, [output, highlightCode])
const getToolDisplayName = (name: string) => { if (input === undefined && output === undefined) {
if (name.startsWith('mcp__')) { return <div className="text-foreground-500 text-xs">No data available for this tool</div>
const parts = name.substring(5).split('__')
if (parts.length >= 2) {
return `${parts[0]}:${parts.slice(1).join(':')}`
}
}
return name
} }
const getToolDescription = () => { return (
if (toolName.startsWith('mcp__')) { <div className="space-y-3">
return 'MCP Server Tool' {input !== undefined && (
} <div>
return 'Tool' <div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Input:</div>
} <div
className="overflow-x-auto rounded bg-gray-50 dark:bg-gray-900"
dangerouslySetInnerHTML={{ __html: inputHtml }}
/>
</div>
)}
{output !== undefined && (
<div>
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Output:</div>
<div
className="rounded bg-gray-50 dark:bg-gray-900 [&>*]:whitespace-pre-line"
dangerouslySetInnerHTML={{ __html: outputHtml }}
/>
</div>
)}
</div>
)
}
export function UnknownToolRenderer({
toolName = '',
input,
output
}: UnknownToolProps): NonNullable<CollapseProps['items']>[number] {
return { return {
key: 'unknown-tool', key: 'unknown-tool',
label: ( label: (
<ToolTitle <ToolTitle
icon={<Wrench className="h-4 w-4" />} icon={<Wrench className="h-4 w-4" />}
label={getToolDisplayName(toolName)} label={getToolDisplayName(toolName)}
params={getToolDescription()} params={getToolDescription(toolName)}
/> />
), ),
children: ( children: <UnknownToolContent input={input} output={output} />
<div className="space-y-3">
{input !== undefined && (
<div>
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Input:</div>
<div
className="overflow-x-auto rounded bg-gray-50 dark:bg-gray-900"
dangerouslySetInnerHTML={{ __html: inputHtml }}
/>
</div>
)}
{output !== undefined && (
<div>
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Output:</div>
<div
className="rounded bg-gray-50 dark:bg-gray-900 [&>*]:whitespace-pre-line"
dangerouslySetInnerHTML={{ __html: outputHtml }}
/>
</div>
)}
{input === undefined && output === undefined && (
<div className="text-foreground-500 text-xs">No data available for this tool</div>
)}
</div>
)
} }
} }

View File

@@ -6,8 +6,6 @@ import { Collapse } from 'antd'
// 导出所有类型 // 导出所有类型
export * from './types' export * from './types'
import { useMemo } from 'react'
// 导入所有渲染器 // 导入所有渲染器
import ToolPermissionRequestCard from '../ToolPermissionRequestCard' import ToolPermissionRequestCard from '../ToolPermissionRequestCard'
import { BashOutputTool } from './BashOutputTool' import { BashOutputTool } from './BashOutputTool'
@@ -57,22 +55,19 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools
return typeof toolName === 'string' && Object.values(AgentToolsType).includes(toolName as AgentToolsType) return typeof toolName === 'string' && Object.values(AgentToolsType).includes(toolName as AgentToolsType)
} }
// 统一的渲染函数 // 统一的渲染组件
function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) { function ToolContent({ toolName, input, output }: { toolName: AgentToolsType; input: ToolInput; output?: ToolOutput }) {
const Renderer = toolRenderers[toolName] const Renderer = toolRenderers[toolName]
const renderedItem = Renderer
? Renderer({ input: input as any, output: output as any })
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })
// eslint-disable-next-line react-hooks/rules-of-hooks const toolContentItem: NonNullable<CollapseProps['items']>[number] = {
const toolContentItem = useMemo(() => { ...renderedItem,
const rendered = Renderer classNames: {
? Renderer({ input: input as any, output: output as any }) body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
: UnknownToolRenderer({ input: input as any, output: output as any, toolName }) }
return { }
...rendered,
classNames: {
body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
} as NonNullable<CollapseProps['items']>[number]['classNames']
} as NonNullable<CollapseProps['items']>[number]
}, [Renderer, input, output, toolName])
return ( return (
<Collapse <Collapse
@@ -98,5 +93,7 @@ export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolRe
return <ToolPermissionRequestCard toolResponse={toolResponse} /> return <ToolPermissionRequestCard toolResponse={toolResponse} />
} }
return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput) return (
<ToolContent toolName={tool.name as AgentToolsType} input={args as ToolInput} output={response as ToolOutput} />
)
} }

View File

@@ -11,6 +11,7 @@ import EssentialSettings from './EssentialSettings'
import PluginSettings from './PluginSettings' import PluginSettings from './PluginSettings'
import PromptSettings from './PromptSettings' import PromptSettings from './PromptSettings'
import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared' import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared'
import SubAgentsSettings from './SubAgentsSettings'
import ToolingSettings from './ToolingSettings' import ToolingSettings from './ToolingSettings'
interface AgentSettingPopupShowParams { interface AgentSettingPopupShowParams {
@@ -22,7 +23,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams {
resolve: () => void resolve: () => void
} }
type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'plugins' | 'session-mcps' type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'plugins' | 'sub-agents' | 'session-mcps'
const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => { const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
@@ -62,6 +63,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
key: 'plugins', key: 'plugins',
label: t('agent.settings.plugins.tab', 'Plugins') label: t('agent.settings.plugins.tab', 'Plugins')
}, },
{
key: 'sub-agents',
label: t('agent.settings.sub_agents.tab', 'Sub-agents')
},
{ {
key: 'advanced', key: 'advanced',
label: t('agent.settings.advance.title', 'Advanced Settings') label: t('agent.settings.advance.title', 'Advanced Settings')
@@ -107,6 +112,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />} {menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />} {menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
{menu === 'plugins' && <PluginSettings agentBase={agent} update={updateAgent} />} {menu === 'plugins' && <PluginSettings agentBase={agent} update={updateAgent} />}
{menu === 'sub-agents' && <SubAgentsSettings agentBase={agent} update={updateAgent} />}
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />} {menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
</Settings> </Settings>
</div> </div>

View File

@@ -0,0 +1,57 @@
import { useAgents } from '@renderer/hooks/agents/useAgents'
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types'
import { Form, Select, Spin } from 'antd'
import { useTranslation } from 'react-i18next'
interface SubAgentsSettingsProps {
agentBase: GetAgentResponse | GetAgentSessionResponse | undefined | null
update: UpdateAgentFunctionUnion
}
const SubAgentsSettings: React.FC<SubAgentsSettingsProps> = ({ agentBase, update }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const { agents, isLoading } = useAgents()
if (!agentBase) return
const handleValuesChange = (changedValues: { sub_agents: string[] }) => {
update({
id: agentBase.id,
...changedValues
})
}
if (isLoading) {
return <Spin />
}
const availableAgents = agents?.filter((agent) => agent.id !== agentBase.id) || []
return (
<Form
form={form}
layout="vertical"
initialValues={{ sub_agents: agentBase.sub_agents || [] }}
onValuesChange={handleValuesChange}
style={{ maxWidth: 600 }}>
<Form.Item
name="sub_agents"
label={t('agent.settings.sub_agents.title')}
tooltip={t('agent.settings.sub_agents.tooltip')}>
<Select
mode="multiple"
placeholder={t('agent.settings.sub_agents.placeholder')}
loading={isLoading}
options={availableAgents.map((agent) => ({
label: agent.name,
value: agent.id
}))}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
</Form>
)
}
export default SubAgentsSettings

View File

@@ -1,5 +1,3 @@
import 'emoji-picker-element'
import { CheckOutlined, LoadingOutlined, RollbackOutlined, ThunderboltOutlined } from '@ant-design/icons' import { CheckOutlined, LoadingOutlined, RollbackOutlined, ThunderboltOutlined } from '@ant-design/icons'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import EmojiPicker from '@renderer/components/EmojiPicker' import EmojiPicker from '@renderer/components/EmojiPicker'

View File

@@ -585,9 +585,11 @@ const fetchAndProcessAgentResponseImpl = async (
return return
} }
// Only mark as cleared if there was a previous session ID (not initial assignment)
sessionWasCleared = !!latestAgentSessionId
latestAgentSessionId = sessionId latestAgentSessionId = sessionId
agentSession.agentSessionId = sessionId agentSession.agentSessionId = sessionId
sessionWasCleared = true
logger.debug(`Agent session ID updated`, { logger.debug(`Agent session ID updated`, {
topicId, topicId,

View File

@@ -82,6 +82,7 @@ export const AgentBaseSchema = z.object({
// Tools // Tools
mcps: z.array(z.string()).optional(), // Array of MCP tool IDs mcps: z.array(z.string()).optional(), // Array of MCP tool IDs
allowed_tools: z.array(z.string()).optional(), // Array of allowed tool IDs (whitelist) allowed_tools: z.array(z.string()).optional(), // Array of allowed tool IDs (whitelist)
sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs
slash_commands: z.array(SlashCommandSchema).optional(), // Array of slash commands merged from builtin and SDK slash_commands: z.array(SlashCommandSchema).optional(), // Array of slash commands merged from builtin and SDK
// Configuration // Configuration
@@ -132,7 +133,7 @@ export const AgentSessionEntitySchema = AgentBaseSchema.extend({
id: z.string(), id: z.string(),
agent_id: z.string(), // Primary agent ID for the session agent_id: z.string(), // Primary agent ID for the session
agent_type: AgentTypeSchema, agent_type: AgentTypeSchema,
// sub_agent_ids?: string[] // Array of sub-agent IDs involved in the session sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs involved in the session
created_at: z.iso.datetime(), created_at: z.iso.datetime(),
updated_at: z.iso.datetime() updated_at: z.iso.datetime()
@@ -205,6 +206,7 @@ export type BaseAgentForm = {
model: string model: string
accessible_paths: string[] accessible_paths: string[]
allowed_tools: string[] allowed_tools: string[]
sub_agents?: string[]
mcps?: string[] mcps?: string[]
configuration?: AgentConfiguration configuration?: AgentConfiguration
} }
@@ -286,6 +288,7 @@ export interface UpdateSessionRequest extends Partial<AgentBase> {}
export const GetAgentSessionResponseSchema = AgentSessionEntitySchema.extend({ export const GetAgentSessionResponseSchema = AgentSessionEntitySchema.extend({
tools: z.array(ToolSchema).optional(), // All tools available to the session (including built-in and custom) tools: z.array(ToolSchema).optional(), // All tools available to the session (including built-in and custom)
sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs
messages: z.array(AgentSessionMessageEntitySchema).optional(), // Messages in the session messages: z.array(AgentSessionMessageEntitySchema).optional(), // Messages in the session
plugins: z plugins: z
.array( .array(

153
yarn.lock
View File

@@ -4558,38 +4558,38 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/client@npm:0.15.15": "@libsql/client@npm:0.14.0, @libsql/client@npm:^0.14.0":
version: 0.15.15 version: 0.14.0
resolution: "@libsql/client@npm:0.15.15" resolution: "@libsql/client@npm:0.14.0"
dependencies: dependencies:
"@libsql/core": "npm:^0.15.14" "@libsql/core": "npm:^0.14.0"
"@libsql/hrana-client": "npm:^0.7.0" "@libsql/hrana-client": "npm:^0.7.0"
js-base64: "npm:^3.7.5" js-base64: "npm:^3.7.5"
libsql: "npm:^0.5.22" libsql: "npm:^0.4.4"
promise-limit: "npm:^2.7.0" promise-limit: "npm:^2.7.0"
checksum: 10c0/1ae67280ebe27903ff142b07e2a256c22ef5ada65185286a72823e8eae8d9d2602e0d72e423d3bd64ae57494791bfffff946aa0fc7c2378b55a227ff63f8df69 checksum: 10c0/9c6bab468453df765f647422c772af3578f1e108b663a80b99063f47ed3542db26ae0fcdba2e153d72e6d5089c5caeba947a167a6c065b0191a0832621539335
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/core@npm:^0.15.14": "@libsql/core@npm:^0.14.0":
version: 0.15.15 version: 0.14.0
resolution: "@libsql/core@npm:0.15.15" resolution: "@libsql/core@npm:0.14.0"
dependencies: dependencies:
js-base64: "npm:^3.7.5" js-base64: "npm:^3.7.5"
checksum: 10c0/0a619689c9504f4239d9745882a128b81e2f6c0547352bbb0d36932261c053bbcbea4435a17f91abe61556bb791f2f1203b36c36b2d4b4f369953d7949bdc40e checksum: 10c0/327bb991cf191d5a9a9fc0cc1a17123f7ca88f222187a3bde845fbad8ceaeaa1f139882080e4b2969da57b83e576c52702572e2838d1743c6bff75f95e6f774a
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/darwin-arm64@npm:0.5.22": "@libsql/darwin-arm64@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/darwin-arm64@npm:0.5.22" resolution: "@libsql/darwin-arm64@npm:0.4.7"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/darwin-x64@npm:0.5.22": "@libsql/darwin-x64@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/darwin-x64@npm:0.5.22" resolution: "@libsql/darwin-x64@npm:0.4.7"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4623,52 +4623,38 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/linux-arm-gnueabihf@npm:0.5.22": "@libsql/linux-arm64-gnu@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/linux-arm-gnueabihf@npm:0.5.22" resolution: "@libsql/linux-arm64-gnu@npm:0.4.7"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@libsql/linux-arm-musleabihf@npm:0.5.22":
version: 0.5.22
resolution: "@libsql/linux-arm-musleabihf@npm:0.5.22"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@libsql/linux-arm64-gnu@npm:0.5.22":
version: 0.5.22
resolution: "@libsql/linux-arm64-gnu@npm:0.5.22"
conditions: os=linux & cpu=arm64 conditions: os=linux & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/linux-arm64-musl@npm:0.5.22": "@libsql/linux-arm64-musl@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/linux-arm64-musl@npm:0.5.22" resolution: "@libsql/linux-arm64-musl@npm:0.4.7"
conditions: os=linux & cpu=arm64 conditions: os=linux & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/linux-x64-gnu@npm:0.5.22": "@libsql/linux-x64-gnu@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/linux-x64-gnu@npm:0.5.22" resolution: "@libsql/linux-x64-gnu@npm:0.4.7"
conditions: os=linux & cpu=x64 conditions: os=linux & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/linux-x64-musl@npm:0.5.22": "@libsql/linux-x64-musl@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/linux-x64-musl@npm:0.5.22" resolution: "@libsql/linux-x64-musl@npm:0.4.7"
conditions: os=linux & cpu=x64 conditions: os=linux & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@libsql/win32-x64-msvc@npm:0.5.22, @libsql/win32-x64-msvc@npm:^0.5.22": "@libsql/win32-x64-msvc@npm:0.4.7, @libsql/win32-x64-msvc@npm:^0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "@libsql/win32-x64-msvc@npm:0.5.22" resolution: "@libsql/win32-x64-msvc@npm:0.4.7"
checksum: 10c0/1bb2730563c603c03a229faa352897685648659d85ba0872dda60cc02abc469fbd55539ffd8b86c81d00230d76292e5a4d2a763fe44c05694612ce6db6e929aa checksum: 10c0/2fcb8715b6f0571dec145eaaf3fd53c7c5aa5bf408fe1be9d84b10adc8a909bb6ee60b45e0d7052b0c1722c30ac212356a3f1adcdf7f57d5a59b48f36ca5bdf5
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9967,8 +9953,8 @@ __metadata:
"@langchain/community": "npm:^1.0.0" "@langchain/community": "npm:^1.0.0"
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch" "@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch"
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch" "@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
"@libsql/client": "npm:0.15.15" "@libsql/client": "npm:0.14.0"
"@libsql/win32-x64-msvc": "npm:^0.5.22" "@libsql/win32-x64-msvc": "npm:^0.4.7"
"@mistralai/mistralai": "npm:^1.7.5" "@mistralai/mistralai": "npm:^1.7.5"
"@modelcontextprotocol/sdk": "npm:^1.17.5" "@modelcontextprotocol/sdk": "npm:^1.17.5"
"@mozilla/readability": "npm:^0.6.0" "@mozilla/readability": "npm:^0.6.0"
@@ -10088,6 +10074,7 @@ __metadata:
electron-window-state: "npm:^5.0.3" electron-window-state: "npm:^5.0.3"
emittery: "npm:^1.0.3" emittery: "npm:^1.0.3"
emoji-picker-element: "npm:^1.22.1" emoji-picker-element: "npm:^1.22.1"
emoji-picker-element-data: "npm:^1"
epub: "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch" epub: "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch"
eslint: "npm:^9.22.0" eslint: "npm:^9.22.0"
eslint-plugin-import-zod: "npm:^1.2.0" eslint-plugin-import-zod: "npm:^1.2.0"
@@ -13669,6 +13656,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"emoji-picker-element-data@npm:^1":
version: 1.8.0
resolution: "emoji-picker-element-data@npm:1.8.0"
checksum: 10c0/c8976b636205a0cc90d2690859a1193add71a948dadf743962b47c338a4c3715768404d0ccbc02156608b44abf41f3e1d51756e06f1bbed9d164dd4cb1752103
languageName: node
linkType: hard
"emoji-picker-element@npm:^1.22.1": "emoji-picker-element@npm:^1.22.1":
version: 1.26.3 version: 1.26.3
resolution: "emoji-picker-element@npm:1.26.3" resolution: "emoji-picker-element@npm:1.26.3"
@@ -17281,19 +17275,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"libsql@npm:^0.5.22": "libsql@npm:0.4.7":
version: 0.5.22 version: 0.4.7
resolution: "libsql@npm:0.5.22" resolution: "libsql@npm:0.4.7"
dependencies: dependencies:
"@libsql/darwin-arm64": "npm:0.5.22" "@libsql/darwin-arm64": "npm:0.4.7"
"@libsql/darwin-x64": "npm:0.5.22" "@libsql/darwin-x64": "npm:0.4.7"
"@libsql/linux-arm-gnueabihf": "npm:0.5.22" "@libsql/linux-arm64-gnu": "npm:0.4.7"
"@libsql/linux-arm-musleabihf": "npm:0.5.22" "@libsql/linux-arm64-musl": "npm:0.4.7"
"@libsql/linux-arm64-gnu": "npm:0.5.22" "@libsql/linux-x64-gnu": "npm:0.4.7"
"@libsql/linux-arm64-musl": "npm:0.5.22" "@libsql/linux-x64-musl": "npm:0.4.7"
"@libsql/linux-x64-gnu": "npm:0.5.22" "@libsql/win32-x64-msvc": "npm:0.4.7"
"@libsql/linux-x64-musl": "npm:0.5.22"
"@libsql/win32-x64-msvc": "npm:0.5.22"
"@neon-rs/load": "npm:^0.0.4" "@neon-rs/load": "npm:^0.0.4"
detect-libc: "npm:2.0.2" detect-libc: "npm:2.0.2"
dependenciesMeta: dependenciesMeta:
@@ -17301,10 +17293,6 @@ __metadata:
optional: true optional: true
"@libsql/darwin-x64": "@libsql/darwin-x64":
optional: true optional: true
"@libsql/linux-arm-gnueabihf":
optional: true
"@libsql/linux-arm-musleabihf":
optional: true
"@libsql/linux-arm64-gnu": "@libsql/linux-arm64-gnu":
optional: true optional: true
"@libsql/linux-arm64-musl": "@libsql/linux-arm64-musl":
@@ -17315,8 +17303,41 @@ __metadata:
optional: true optional: true
"@libsql/win32-x64-msvc": "@libsql/win32-x64-msvc":
optional: true optional: true
checksum: 10c0/6c34f08fc7408ebee16708ba12e5def9d1b2a4fa166070c956a120133ba9be68ec532e2d0b76bdc7005ef9ef69bf70d2ba7208ed824c4288c2a3d881edd5eaf6 checksum: 10c0/351952440e6bad3477e5f1bb1b9d6570d16e403b894f4a13c5c7e183a1307b2fb04a2fa902728cb8594a259e1726c51c61b822d545bbc88319b126ad15468a87
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32 | cpu=arm) conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32)
languageName: node
linkType: hard
"libsql@patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch":
version: 0.4.7
resolution: "libsql@patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch::version=0.4.7&hash=972e11"
dependencies:
"@libsql/darwin-arm64": "npm:0.4.7"
"@libsql/darwin-x64": "npm:0.4.7"
"@libsql/linux-arm64-gnu": "npm:0.4.7"
"@libsql/linux-arm64-musl": "npm:0.4.7"
"@libsql/linux-x64-gnu": "npm:0.4.7"
"@libsql/linux-x64-musl": "npm:0.4.7"
"@libsql/win32-x64-msvc": "npm:0.4.7"
"@neon-rs/load": "npm:^0.0.4"
detect-libc: "npm:2.0.2"
dependenciesMeta:
"@libsql/darwin-arm64":
optional: true
"@libsql/darwin-x64":
optional: true
"@libsql/linux-arm64-gnu":
optional: true
"@libsql/linux-arm64-musl":
optional: true
"@libsql/linux-x64-gnu":
optional: true
"@libsql/linux-x64-musl":
optional: true
"@libsql/win32-x64-msvc":
optional: true
checksum: 10c0/6098770dc6c31ae0dbfe0821719d184d9bb353ac92553923096f6e3420d3786f240f0b3858f519af0aeada93beb4aa83cb9a9a1a6aa18d625511b484dcb53d07
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32)
languageName: node languageName: node
linkType: hard linkType: hard