@shareai-lab/kode 1.0.71 → 1.0.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +142 -1
  2. package/README.zh-CN.md +47 -1
  3. package/package.json +5 -1
  4. package/src/ProjectOnboarding.tsx +47 -29
  5. package/src/Tool.ts +33 -4
  6. package/src/commands/agents.tsx +3401 -0
  7. package/src/commands/help.tsx +2 -2
  8. package/src/commands/resume.tsx +2 -1
  9. package/src/commands/terminalSetup.ts +4 -4
  10. package/src/commands.ts +3 -0
  11. package/src/components/ApproveApiKey.tsx +1 -1
  12. package/src/components/Config.tsx +10 -6
  13. package/src/components/ConsoleOAuthFlow.tsx +5 -4
  14. package/src/components/CustomSelect/select-option.tsx +28 -2
  15. package/src/components/CustomSelect/select.tsx +14 -5
  16. package/src/components/CustomSelect/theme.ts +45 -0
  17. package/src/components/Help.tsx +4 -4
  18. package/src/components/InvalidConfigDialog.tsx +1 -1
  19. package/src/components/LogSelector.tsx +1 -1
  20. package/src/components/MCPServerApprovalDialog.tsx +1 -1
  21. package/src/components/Message.tsx +2 -0
  22. package/src/components/ModelListManager.tsx +10 -6
  23. package/src/components/ModelSelector.tsx +201 -23
  24. package/src/components/ModelStatusDisplay.tsx +7 -5
  25. package/src/components/PromptInput.tsx +117 -87
  26. package/src/components/SentryErrorBoundary.ts +3 -3
  27. package/src/components/StickerRequestForm.tsx +16 -0
  28. package/src/components/StructuredDiff.tsx +36 -29
  29. package/src/components/TextInput.tsx +13 -0
  30. package/src/components/TodoItem.tsx +11 -0
  31. package/src/components/TrustDialog.tsx +1 -1
  32. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +5 -1
  33. package/src/components/messages/AssistantToolUseMessage.tsx +14 -4
  34. package/src/components/messages/TaskProgressMessage.tsx +32 -0
  35. package/src/components/messages/TaskToolMessage.tsx +58 -0
  36. package/src/components/permissions/FallbackPermissionRequest.tsx +2 -4
  37. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +1 -1
  38. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +5 -3
  39. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +1 -1
  40. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +5 -3
  41. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +2 -4
  42. package/src/components/permissions/PermissionRequest.tsx +3 -5
  43. package/src/constants/macros.ts +2 -0
  44. package/src/constants/modelCapabilities.ts +179 -0
  45. package/src/constants/models.ts +90 -0
  46. package/src/constants/product.ts +1 -1
  47. package/src/context.ts +7 -7
  48. package/src/entrypoints/cli.tsx +23 -3
  49. package/src/entrypoints/mcp.ts +10 -10
  50. package/src/hooks/useCanUseTool.ts +1 -1
  51. package/src/hooks/useTextInput.ts +5 -2
  52. package/src/hooks/useUnifiedCompletion.ts +1404 -0
  53. package/src/messages.ts +1 -0
  54. package/src/query.ts +3 -0
  55. package/src/screens/ConfigureNpmPrefix.tsx +1 -1
  56. package/src/screens/Doctor.tsx +1 -1
  57. package/src/screens/REPL.tsx +15 -9
  58. package/src/services/adapters/base.ts +38 -0
  59. package/src/services/adapters/chatCompletions.ts +90 -0
  60. package/src/services/adapters/responsesAPI.ts +170 -0
  61. package/src/services/claude.ts +198 -62
  62. package/src/services/customCommands.ts +43 -22
  63. package/src/services/gpt5ConnectionTest.ts +340 -0
  64. package/src/services/mcpClient.ts +1 -1
  65. package/src/services/mentionProcessor.ts +273 -0
  66. package/src/services/modelAdapterFactory.ts +69 -0
  67. package/src/services/openai.ts +521 -12
  68. package/src/services/responseStateManager.ts +90 -0
  69. package/src/services/systemReminder.ts +113 -12
  70. package/src/test/testAdapters.ts +96 -0
  71. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +120 -56
  72. package/src/tools/BashTool/BashTool.tsx +4 -31
  73. package/src/tools/BashTool/BashToolResultMessage.tsx +1 -1
  74. package/src/tools/BashTool/OutputLine.tsx +1 -0
  75. package/src/tools/FileEditTool/FileEditTool.tsx +4 -5
  76. package/src/tools/FileReadTool/FileReadTool.tsx +43 -10
  77. package/src/tools/MCPTool/MCPTool.tsx +2 -1
  78. package/src/tools/MultiEditTool/MultiEditTool.tsx +2 -2
  79. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +15 -23
  80. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +1 -1
  81. package/src/tools/TaskTool/TaskTool.tsx +170 -86
  82. package/src/tools/TaskTool/prompt.ts +61 -25
  83. package/src/tools/ThinkTool/ThinkTool.tsx +1 -3
  84. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +11 -10
  85. package/src/tools/lsTool/lsTool.tsx +5 -2
  86. package/src/tools.ts +16 -16
  87. package/src/types/conversation.ts +51 -0
  88. package/src/types/logs.ts +58 -0
  89. package/src/types/modelCapabilities.ts +64 -0
  90. package/src/types/notebook.ts +87 -0
  91. package/src/utils/advancedFuzzyMatcher.ts +290 -0
  92. package/src/utils/agentLoader.ts +284 -0
  93. package/src/utils/ask.tsx +1 -0
  94. package/src/utils/commands.ts +1 -1
  95. package/src/utils/commonUnixCommands.ts +161 -0
  96. package/src/utils/config.ts +173 -2
  97. package/src/utils/conversationRecovery.ts +1 -0
  98. package/src/utils/debugLogger.ts +13 -13
  99. package/src/utils/exampleCommands.ts +1 -0
  100. package/src/utils/fuzzyMatcher.ts +328 -0
  101. package/src/utils/messages.tsx +6 -5
  102. package/src/utils/responseState.ts +23 -0
  103. package/src/utils/secureFile.ts +559 -0
  104. package/src/utils/terminal.ts +1 -0
  105. package/src/utils/theme.ts +11 -0
  106. package/src/hooks/useSlashCommandTypeahead.ts +0 -137
@@ -0,0 +1,3401 @@
1
+ import React, { useState, useEffect, useMemo, useCallback, useReducer, Fragment } from 'react'
2
+ import { Box, Text, useInput } from 'ink'
3
+ import InkTextInput from 'ink-text-input'
4
+ import { getActiveAgents, clearAgentCache } from '../utils/agentLoader'
5
+ import { AgentConfig } from '../utils/agentLoader'
6
+ import { writeFileSync, unlinkSync, mkdirSync, existsSync, readFileSync, renameSync } from 'fs'
7
+ import { join } from 'path'
8
+ import * as path from 'path'
9
+ import { homedir } from 'os'
10
+ import * as os from 'os'
11
+ import { getCwd } from '../utils/state'
12
+ import { getTheme } from '../utils/theme'
13
+ import matter from 'gray-matter'
14
+ import { exec, spawn } from 'child_process'
15
+ import { promisify } from 'util'
16
+ import { watch, FSWatcher } from 'fs'
17
+ import { getMCPTools } from '../services/mcpClient'
18
+ import { getModelManager } from '../utils/model'
19
+ import { randomUUID } from 'crypto'
20
+
21
+ const execAsync = promisify(exec)
22
+
23
+ // Core constants aligned with Claude Code architecture
24
+ const AGENT_LOCATIONS = {
25
+ USER: "user",
26
+ PROJECT: "project",
27
+ BUILT_IN: "built-in",
28
+ ALL: "all"
29
+ } as const
30
+
31
+ const UI_ICONS = {
32
+ pointer: "❯",
33
+ checkboxOn: "☑",
34
+ checkboxOff: "☐",
35
+ warning: "⚠",
36
+ separator: "─",
37
+ loading: "◐◑◒◓"
38
+ } as const
39
+
40
+ const FOLDER_CONFIG = {
41
+ FOLDER_NAME: ".claude",
42
+ AGENTS_DIR: "agents"
43
+ } as const
44
+
45
+ // Tool categories for sophisticated selection
46
+ const TOOL_CATEGORIES = {
47
+ read: ['Read', 'Glob', 'Grep', 'LS'],
48
+ edit: ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'],
49
+ execution: ['Bash', 'BashOutput', 'KillBash'],
50
+ web: ['WebFetch', 'WebSearch'],
51
+ other: ['TodoWrite', 'ExitPlanMode', 'Task']
52
+ } as const
53
+
54
+ type AgentLocation = typeof AGENT_LOCATIONS[keyof typeof AGENT_LOCATIONS]
55
+
56
+ // Models will be listed dynamically from ModelManager
57
+
58
+ // Comprehensive mode state for complete UI flow
59
+ type ModeState = {
60
+ mode: 'list-agents' | 'create-location' | 'create-method' | 'create-generate' | 'create-type' |
61
+ 'create-description' | 'create-tools' | 'create-model' | 'create-color' | 'create-prompt' | 'create-confirm' |
62
+ 'agent-menu' | 'view-agent' | 'edit-agent' | 'edit-tools' | 'edit-model' | 'edit-color' | 'delete-confirm'
63
+ location?: AgentLocation
64
+ selectedAgent?: AgentConfig
65
+ previousMode?: ModeState
66
+ [key: string]: any
67
+ }
68
+
69
+ // State for agent creation flow
70
+ type CreateState = {
71
+ location: AgentLocation | null
72
+ agentType: string
73
+ method: 'generate' | 'manual' | null
74
+ generationPrompt: string
75
+ whenToUse: string
76
+ selectedTools: string[]
77
+ selectedModel: string | null // null for inherit, or model profile modelName
78
+ selectedColor: string | null
79
+ systemPrompt: string
80
+ isGenerating: boolean
81
+ wasGenerated: boolean
82
+ isAIGenerated: boolean
83
+ error: string | null
84
+ warnings: string[]
85
+ // Cursor positions for text inputs
86
+ agentTypeCursor: number
87
+ whenToUseCursor: number
88
+ promptCursor: number
89
+ generationPromptCursor: number
90
+ }
91
+
92
+ type Tool = {
93
+ name: string
94
+ description?: string | (() => Promise<string>)
95
+ }
96
+
97
+ // Map a stored model identifier to a display name via ModelManager
98
+ function getDisplayModelName(modelId?: string | null): string {
99
+ // null/undefined means inherit from parent (task model)
100
+ if (!modelId) return 'Inherit'
101
+
102
+ try {
103
+ const profiles = getModelManager().getActiveModelProfiles()
104
+ const profile = profiles.find((p: any) => p.modelName === modelId || p.name === modelId)
105
+ return profile ? profile.name : `Custom (${modelId})`
106
+ } catch (error) {
107
+ console.warn('Failed to get model profiles:', error)
108
+ return modelId ? `Custom (${modelId})` : 'Inherit'
109
+ }
110
+ }
111
+
112
+ // AI Generation response type
113
+ type GeneratedAgent = {
114
+ identifier: string
115
+ whenToUse: string
116
+ systemPrompt: string
117
+ }
118
+
119
+ // AI generation function (use main pointer model)
120
+ async function generateAgentWithClaude(prompt: string): Promise<GeneratedAgent> {
121
+ // Import Claude service dynamically to avoid circular dependencies
122
+ const { queryModel } = await import('../services/claude')
123
+
124
+ const systemPrompt = `You are an expert at creating AI agent configurations. Based on the user's description, generate a specialized agent configuration.
125
+
126
+ Return your response as a JSON object with exactly these fields:
127
+ - identifier: A short, kebab-case identifier for the agent (e.g., "code-reviewer", "security-auditor")
128
+ - whenToUse: A clear description of when this agent should be used (50-200 words)
129
+ - systemPrompt: A comprehensive system prompt that defines the agent's role, capabilities, and behavior (200-500 words)
130
+
131
+ Make the agent highly specialized and effective for the described use case.`
132
+
133
+ try {
134
+ const messages = [
135
+ {
136
+ type: 'user',
137
+ uuid: randomUUID(),
138
+ message: { role: 'user', content: prompt },
139
+ },
140
+ ] as any
141
+ const response = await queryModel('main', messages, [systemPrompt])
142
+
143
+ // Get the text content from the response - handle both string and object responses
144
+ let responseText = ''
145
+ if (typeof response.message?.content === 'string') {
146
+ responseText = response.message.content
147
+ } else if (Array.isArray(response.message?.content)) {
148
+ const textContent = response.message.content.find((c: any) => c.type === 'text')
149
+ responseText = textContent?.text || ''
150
+ } else if (response.message?.content?.[0]?.text) {
151
+ responseText = response.message.content[0].text
152
+ }
153
+
154
+ if (!responseText) {
155
+ throw new Error('No text content in Claude response')
156
+ }
157
+
158
+ // 安全限制
159
+ const MAX_JSON_SIZE = 100_000 // 100KB
160
+ const MAX_FIELD_LENGTH = 10_000
161
+
162
+ if (responseText.length > MAX_JSON_SIZE) {
163
+ throw new Error('Response too large')
164
+ }
165
+
166
+ // 安全的JSON提取和解析
167
+ let parsed: any
168
+ try {
169
+ // 首先尝试直接解析整个响应
170
+ parsed = JSON.parse(responseText.trim())
171
+ } catch {
172
+ // 如果失败,提取第一个JSON对象,限制搜索范围
173
+ const startIdx = responseText.indexOf('{')
174
+ const endIdx = responseText.lastIndexOf('}')
175
+
176
+ if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) {
177
+ throw new Error('No valid JSON found in Claude response')
178
+ }
179
+
180
+ const jsonStr = responseText.substring(startIdx, endIdx + 1)
181
+ if (jsonStr.length > MAX_JSON_SIZE) {
182
+ throw new Error('JSON content too large')
183
+ }
184
+
185
+ try {
186
+ parsed = JSON.parse(jsonStr)
187
+ } catch (parseError) {
188
+ throw new Error(`Invalid JSON format: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`)
189
+ }
190
+ }
191
+
192
+ // 深度验证和安全清理
193
+ const identifier = String(parsed.identifier || '').slice(0, 100).trim()
194
+ const whenToUse = String(parsed.whenToUse || '').slice(0, MAX_FIELD_LENGTH).trim()
195
+ const agentSystemPrompt = String(parsed.systemPrompt || '').slice(0, MAX_FIELD_LENGTH).trim()
196
+
197
+ // 验证必填字段
198
+ if (!identifier || !whenToUse || !agentSystemPrompt) {
199
+ throw new Error('Invalid response structure: missing required fields (identifier, whenToUse, systemPrompt)')
200
+ }
201
+
202
+ // 清理危险字符(控制字符和非打印字符)
203
+ const sanitize = (str: string) => str.replace(/[\x00-\x1F\x7F-\x9F]/g, '')
204
+
205
+ // 验证identifier格式(只允许字母、数字、连字符)
206
+ const cleanIdentifier = sanitize(identifier)
207
+ if (!/^[a-zA-Z0-9-]+$/.test(cleanIdentifier)) {
208
+ throw new Error('Invalid identifier format: only letters, numbers, and hyphens allowed')
209
+ }
210
+
211
+ return {
212
+ identifier: cleanIdentifier,
213
+ whenToUse: sanitize(whenToUse),
214
+ systemPrompt: sanitize(agentSystemPrompt)
215
+ }
216
+ } catch (error) {
217
+ console.error('AI generation failed:', error)
218
+ // Fallback to a reasonable default based on the prompt
219
+ const fallbackId = prompt.toLowerCase()
220
+ .replace(/[^a-z0-9\s-]/g, '')
221
+ .replace(/\s+/g, '-')
222
+ .slice(0, 30)
223
+
224
+ return {
225
+ identifier: fallbackId || 'custom-agent',
226
+ whenToUse: `Use this agent when you need assistance with: ${prompt}`,
227
+ systemPrompt: `You are a specialized assistant focused on helping with ${prompt}. Provide expert-level assistance in this domain.`
228
+ }
229
+ }
230
+ }
231
+
232
+ // Comprehensive validation system
233
+ function validateAgentType(agentType: string, existingAgents: AgentConfig[] = []): {
234
+ isValid: boolean
235
+ errors: string[]
236
+ warnings: string[]
237
+ } {
238
+ const errors: string[] = []
239
+ const warnings: string[] = []
240
+
241
+ if (!agentType) {
242
+ errors.push("Agent type is required")
243
+ return { isValid: false, errors, warnings }
244
+ }
245
+
246
+ if (!/^[a-zA-Z]/.test(agentType)) {
247
+ errors.push("Agent type must start with a letter")
248
+ }
249
+
250
+ if (!/^[a-zA-Z0-9-]+$/.test(agentType)) {
251
+ errors.push("Agent type can only contain letters, numbers, and hyphens")
252
+ }
253
+
254
+ if (agentType.length < 3) {
255
+ errors.push("Agent type must be at least 3 characters long")
256
+ }
257
+
258
+ if (agentType.length > 50) {
259
+ errors.push("Agent type must be less than 50 characters")
260
+ }
261
+
262
+ // Check for reserved names
263
+ const reserved = ['help', 'exit', 'quit', 'agents', 'task']
264
+ if (reserved.includes(agentType.toLowerCase())) {
265
+ errors.push("This name is reserved")
266
+ }
267
+
268
+ // Check for duplicates
269
+ const duplicate = existingAgents.find(a => a.agentType === agentType)
270
+ if (duplicate) {
271
+ errors.push(`An agent with this name already exists in ${duplicate.location}`)
272
+ }
273
+
274
+ // Warnings
275
+ if (agentType.includes('--')) {
276
+ warnings.push("Consider avoiding consecutive hyphens")
277
+ }
278
+
279
+ return {
280
+ isValid: errors.length === 0,
281
+ errors,
282
+ warnings
283
+ }
284
+ }
285
+
286
+ function validateAgentConfig(config: Partial<CreateState>, existingAgents: AgentConfig[] = []): {
287
+ isValid: boolean
288
+ errors: string[]
289
+ warnings: string[]
290
+ } {
291
+ const errors: string[] = []
292
+ const warnings: string[] = []
293
+
294
+ // Validate agent type
295
+ if (config.agentType) {
296
+ const typeValidation = validateAgentType(config.agentType, existingAgents)
297
+ errors.push(...typeValidation.errors)
298
+ warnings.push(...typeValidation.warnings)
299
+ }
300
+
301
+ // Validate description
302
+ if (!config.whenToUse) {
303
+ errors.push("Description is required")
304
+ } else if (config.whenToUse.length < 10) {
305
+ warnings.push("Description should be more descriptive (at least 10 characters)")
306
+ }
307
+
308
+ // Validate system prompt
309
+ if (!config.systemPrompt) {
310
+ errors.push("System prompt is required")
311
+ } else if (config.systemPrompt.length < 20) {
312
+ warnings.push("System prompt might be too short for effective agent behavior")
313
+ }
314
+
315
+ // Validate tools
316
+ if (!config.selectedTools || config.selectedTools.length === 0) {
317
+ warnings.push("No tools selected - agent will have limited capabilities")
318
+ }
319
+
320
+ return {
321
+ isValid: errors.length === 0,
322
+ errors,
323
+ warnings
324
+ }
325
+ }
326
+
327
+ // File system operations with Claude Code alignment
328
+ function getAgentDirectory(location: AgentLocation): string {
329
+ if (location === AGENT_LOCATIONS.BUILT_IN || location === AGENT_LOCATIONS.ALL) {
330
+ throw new Error(`Cannot get directory path for ${location} agents`)
331
+ }
332
+
333
+ if (location === AGENT_LOCATIONS.USER) {
334
+ return join(homedir(), FOLDER_CONFIG.FOLDER_NAME, FOLDER_CONFIG.AGENTS_DIR)
335
+ } else {
336
+ return join(getCwd(), FOLDER_CONFIG.FOLDER_NAME, FOLDER_CONFIG.AGENTS_DIR)
337
+ }
338
+ }
339
+
340
+ function getAgentFilePath(agent: AgentConfig): string {
341
+ if (agent.location === 'built-in') {
342
+ throw new Error('Cannot get file path for built-in agents')
343
+ }
344
+ const dir = getAgentDirectory(agent.location as AgentLocation)
345
+ return join(dir, `${agent.agentType}.md`)
346
+ }
347
+
348
+ function ensureDirectoryExists(location: AgentLocation): string {
349
+ const dir = getAgentDirectory(location)
350
+ if (!existsSync(dir)) {
351
+ mkdirSync(dir, { recursive: true })
352
+ }
353
+ return dir
354
+ }
355
+
356
+ // Generate agent file content
357
+ function generateAgentFileContent(
358
+ agentType: string,
359
+ description: string,
360
+ tools: string[] | '*',
361
+ systemPrompt: string,
362
+ model?: string,
363
+ color?: string
364
+ ): string {
365
+ // Use YAML multi-line string for description to avoid escaping issues
366
+ const descriptionLines = description.split('\n')
367
+ const formattedDescription = descriptionLines.length > 1
368
+ ? `|\n ${descriptionLines.join('\n ')}`
369
+ : JSON.stringify(description)
370
+
371
+ const lines = [
372
+ '---',
373
+ `name: ${agentType}`,
374
+ `description: ${formattedDescription}`
375
+ ]
376
+
377
+ if (tools) {
378
+ if (tools === '*') {
379
+ lines.push(`tools: "*"`)
380
+ } else if (Array.isArray(tools) && tools.length > 0) {
381
+ lines.push(`tools: [${tools.map(t => `"${t}"`).join(', ')}]`)
382
+ }
383
+ }
384
+
385
+ if (model) {
386
+ lines.push(`model: ${model}`)
387
+ }
388
+
389
+ if (color) {
390
+ lines.push(`color: ${color}`)
391
+ }
392
+
393
+ lines.push('---', '', systemPrompt)
394
+ return lines.join('\n')
395
+ }
396
+
397
+ // Save agent to file
398
+ async function saveAgent(
399
+ location: AgentLocation,
400
+ agentType: string,
401
+ description: string,
402
+ tools: string[],
403
+ systemPrompt: string,
404
+ model?: string,
405
+ color?: string,
406
+ throwIfExists: boolean = true
407
+ ): Promise<void> {
408
+ if (location === AGENT_LOCATIONS.BUILT_IN) {
409
+ throw new Error('Cannot save built-in agents')
410
+ }
411
+
412
+ ensureDirectoryExists(location)
413
+
414
+ const filePath = join(getAgentDirectory(location), `${agentType}.md`)
415
+ const tempFile = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substr(2, 9)}`
416
+
417
+ // Ensure tools is properly typed for file saving
418
+ const toolsForFile: string[] | '*' = Array.isArray(tools) && tools.length === 1 && tools[0] === '*' ? '*' : tools
419
+ const content = generateAgentFileContent(agentType, description, toolsForFile, systemPrompt, model, color)
420
+
421
+ try {
422
+ // 先写入临时文件,使用 'wx' 确保不覆盖现有文件
423
+ writeFileSync(tempFile, content, { encoding: 'utf-8', flag: 'wx' })
424
+
425
+ // 检查目标文件是否存在(原子性检查)
426
+ if (throwIfExists && existsSync(filePath)) {
427
+ // 清理临时文件
428
+ try { unlinkSync(tempFile) } catch {}
429
+ throw new Error(`Agent file already exists: ${filePath}`)
430
+ }
431
+
432
+ // 原子性重命名(在大多数文件系统上,rename是原子操作)
433
+ renameSync(tempFile, filePath)
434
+
435
+ } catch (error) {
436
+ // 确保清理临时文件
437
+ try {
438
+ if (existsSync(tempFile)) {
439
+ unlinkSync(tempFile)
440
+ }
441
+ } catch (cleanupError) {
442
+ console.warn('Failed to cleanup temp file:', cleanupError)
443
+ }
444
+ throw error
445
+ }
446
+ }
447
+
448
+ // Delete agent file
449
+ async function deleteAgent(agent: AgentConfig): Promise<void> {
450
+ if (agent.location === 'built-in') {
451
+ throw new Error('Cannot delete built-in agents')
452
+ }
453
+
454
+ const filePath = getAgentFilePath(agent)
455
+ unlinkSync(filePath)
456
+ }
457
+
458
+ // Open file in system editor - 安全版本,防止命令注入
459
+ async function openInEditor(filePath: string): Promise<void> {
460
+ // 安全验证:确保路径在允许的目录内
461
+ const resolvedPath = path.resolve(filePath)
462
+ const projectDir = process.cwd()
463
+ const homeDir = os.homedir()
464
+
465
+ if (!resolvedPath.startsWith(projectDir) && !resolvedPath.startsWith(homeDir)) {
466
+ throw new Error('Access denied: File path outside allowed directories')
467
+ }
468
+
469
+ // 验证文件扩展名
470
+ if (!resolvedPath.endsWith('.md')) {
471
+ throw new Error('Invalid file type: Only .md files are allowed')
472
+ }
473
+
474
+ return new Promise((resolve, reject) => {
475
+ const platform = process.platform
476
+ let command: string
477
+ let args: string[]
478
+
479
+ // 使用spawn而不是exec,避免shell注入
480
+ switch (platform) {
481
+ case 'darwin': // macOS
482
+ command = 'open'
483
+ args = [resolvedPath]
484
+ break
485
+ case 'win32': // Windows
486
+ command = 'cmd'
487
+ args = ['/c', 'start', '', resolvedPath]
488
+ break
489
+ default: // Linux and others
490
+ command = 'xdg-open'
491
+ args = [resolvedPath]
492
+ break
493
+ }
494
+
495
+ // 使用spawn替代exec,避免shell解释
496
+ const child = spawn(command, args, {
497
+ detached: true,
498
+ stdio: 'ignore',
499
+ // 确保没有shell解释
500
+ shell: false
501
+ })
502
+
503
+ child.unref() // 允许父进程退出
504
+
505
+ child.on('error', (error) => {
506
+ reject(new Error(`Failed to open editor: ${error.message}`))
507
+ })
508
+
509
+ child.on('exit', (code) => {
510
+ if (code === 0) {
511
+ resolve()
512
+ } else {
513
+ reject(new Error(`Editor exited with code ${code}`))
514
+ }
515
+ })
516
+ })
517
+ }
518
+
519
+ // Update existing agent
520
+ async function updateAgent(
521
+ agent: AgentConfig,
522
+ description: string,
523
+ tools: string[] | '*',
524
+ systemPrompt: string,
525
+ color?: string,
526
+ model?: string
527
+ ): Promise<void> {
528
+ if (agent.location === 'built-in') {
529
+ throw new Error('Cannot update built-in agents')
530
+ }
531
+
532
+ const toolsForFile = tools.length === 1 && tools[0] === '*' ? '*' : tools
533
+ const content = generateAgentFileContent(agent.agentType, description, toolsForFile, systemPrompt, model, color)
534
+ const filePath = getAgentFilePath(agent)
535
+
536
+ writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'w' })
537
+ }
538
+
539
+ // Enhanced UI Components with Claude Code alignment
540
+
541
+ interface HeaderProps {
542
+ title: string
543
+ subtitle?: string
544
+ step?: number
545
+ totalSteps?: number
546
+ children?: React.ReactNode
547
+ }
548
+
549
+ function Header({ title, subtitle, step, totalSteps, children }: HeaderProps) {
550
+ const theme = getTheme()
551
+ return (
552
+ <Box flexDirection="column">
553
+ <Text bold color={theme.primary}>{title}</Text>
554
+ {subtitle && (
555
+ <Text color={theme.secondary}>
556
+ {step && totalSteps ? `Step ${step}/${totalSteps}: ` : ''}{subtitle}
557
+ </Text>
558
+ )}
559
+ {children}
560
+ </Box>
561
+ )
562
+ }
563
+
564
+ interface InstructionBarProps {
565
+ instructions?: string
566
+ }
567
+
568
+ function InstructionBar({ instructions = "Press ↑↓ to navigate · Enter to select · Esc to go back" }: InstructionBarProps) {
569
+ const theme = getTheme()
570
+ return (
571
+ <Box marginTop={2}>
572
+ <Box borderStyle="round" borderColor={theme.secondary} paddingX={1}>
573
+ <Text color={theme.secondary}>{instructions}</Text>
574
+ </Box>
575
+ </Box>
576
+ )
577
+ }
578
+
579
+ interface SelectListProps {
580
+ options: Array<{ label: string; value: string }>
581
+ selectedIndex: number
582
+ onChange: (value: string) => void
583
+ onCancel?: () => void
584
+ numbered?: boolean
585
+ }
586
+
587
+ function SelectList({ options, selectedIndex, onChange, onCancel, numbered = true }: SelectListProps) {
588
+ const theme = getTheme()
589
+
590
+ useInput((input, key) => {
591
+ if (key.escape && onCancel) {
592
+ onCancel()
593
+ } else if (key.return) {
594
+ onChange(options[selectedIndex].value)
595
+ }
596
+ })
597
+
598
+ return (
599
+ <Box flexDirection="column">
600
+ {options.map((option, idx) => (
601
+ <Box key={option.value}>
602
+ <Text color={idx === selectedIndex ? theme.primary : undefined}>
603
+ {idx === selectedIndex ? `${UI_ICONS.pointer} ` : " "}
604
+ {numbered ? `${idx + 1}. ` : ''}{option.label}
605
+ </Text>
606
+ </Box>
607
+ ))}
608
+ </Box>
609
+ )
610
+ }
611
+
612
+
613
+ // Multiline text input component with better UX
614
+ interface MultilineTextInputProps {
615
+ value: string
616
+ onChange: (value: string) => void
617
+ placeholder?: string
618
+ onSubmit?: () => void
619
+ focus?: boolean
620
+ rows?: number
621
+ error?: string | null
622
+ }
623
+
624
+ function MultilineTextInput({
625
+ value,
626
+ onChange,
627
+ placeholder = '',
628
+ onSubmit,
629
+ focus = true,
630
+ rows = 5,
631
+ error
632
+ }: MultilineTextInputProps) {
633
+ const theme = getTheme()
634
+ const [internalValue, setInternalValue] = useState(value)
635
+ const [cursorBlink, setCursorBlink] = useState(true)
636
+
637
+ // Sync with external value changes
638
+ useEffect(() => {
639
+ setInternalValue(value)
640
+ }, [value])
641
+
642
+ // Cursor blink animation
643
+ useEffect(() => {
644
+ if (!focus) return
645
+ const timer = setInterval(() => {
646
+ setCursorBlink(prev => !prev)
647
+ }, 500)
648
+ return () => clearInterval(timer)
649
+ }, [focus])
650
+
651
+ // Calculate display metrics
652
+ const lines = internalValue.split('\n')
653
+ const lineCount = lines.length
654
+ const charCount = internalValue.length
655
+ const isEmpty = !internalValue.trim()
656
+ const hasContent = !isEmpty
657
+
658
+ // Format lines for display with word wrapping
659
+ const formatLines = (text: string): string[] => {
660
+ if (!text && placeholder) {
661
+ return [placeholder]
662
+ }
663
+ const maxWidth = 70 // Maximum characters per line
664
+ const result: string[] = []
665
+ const textLines = text.split('\n')
666
+
667
+ textLines.forEach(line => {
668
+ if (line.length <= maxWidth) {
669
+ result.push(line)
670
+ } else {
671
+ // Word wrap long lines
672
+ let remaining = line
673
+ while (remaining.length > 0) {
674
+ result.push(remaining.slice(0, maxWidth))
675
+ remaining = remaining.slice(maxWidth)
676
+ }
677
+ }
678
+ })
679
+
680
+ return result.length > 0 ? result : ['']
681
+ }
682
+
683
+ const displayLines = formatLines(internalValue)
684
+ const visibleLines = displayLines.slice(Math.max(0, displayLines.length - rows))
685
+
686
+ // Handle submit
687
+ const handleSubmit = () => {
688
+ if (internalValue.trim() && onSubmit) {
689
+ onSubmit()
690
+ }
691
+ }
692
+
693
+ return (
694
+ <Box flexDirection="column" width="100%">
695
+ {/* Modern card-style input container */}
696
+ <Box flexDirection="column">
697
+ {/* Input area */}
698
+ <Box
699
+ borderStyle="round"
700
+ borderColor={focus ? theme.primary : 'gray'}
701
+ paddingX={2}
702
+ paddingY={1}
703
+ minHeight={rows + 2}
704
+ >
705
+ <Box flexDirection="column">
706
+ {/* Use ink-text-input for better input handling */}
707
+ <InkTextInput
708
+ value={internalValue}
709
+ onChange={(val) => {
710
+ setInternalValue(val)
711
+ onChange(val)
712
+ }}
713
+ onSubmit={handleSubmit}
714
+ focus={focus}
715
+ placeholder={placeholder}
716
+ />
717
+
718
+ {/* Show cursor indicator when focused */}
719
+ {focus && cursorBlink && hasContent && (
720
+ <Text color={theme.primary}>_</Text>
721
+ )}
722
+ </Box>
723
+ </Box>
724
+
725
+ {/* Status bar */}
726
+ <Box marginTop={1} flexDirection="row" justifyContent="space-between">
727
+ <Box>
728
+ {hasContent ? (
729
+ <Text color={theme.success}>
730
+ ✓ {charCount} chars • {lineCount} line{lineCount !== 1 ? 's' : ''}
731
+ </Text>
732
+ ) : (
733
+ <Text dimColor>○ Type to begin...</Text>
734
+ )}
735
+ </Box>
736
+ <Box>
737
+ {error ? (
738
+ <Text color={theme.error}>⚠ {error}</Text>
739
+ ) : (
740
+ <Text dimColor>
741
+ {hasContent ? 'Ready' : 'Waiting'}
742
+ </Text>
743
+ )}
744
+ </Box>
745
+ </Box>
746
+ </Box>
747
+
748
+ {/* Instructions */}
749
+ <Box marginTop={1}>
750
+ <Text dimColor>
751
+ Press Enter to submit · Shift+Enter for new line
752
+ </Text>
753
+ </Box>
754
+ </Box>
755
+ )
756
+ }
757
+
758
+ // Loading spinner component
759
+ interface LoadingSpinnerProps {
760
+ text?: string
761
+ }
762
+
763
+ function LoadingSpinner({ text }: LoadingSpinnerProps) {
764
+ const theme = getTheme()
765
+ const [frame, setFrame] = useState(0)
766
+
767
+ useEffect(() => {
768
+ const interval = setInterval(() => {
769
+ setFrame(prev => (prev + 1) % UI_ICONS.loading.length)
770
+ }, 100)
771
+ return () => clearInterval(interval)
772
+ }, [])
773
+
774
+ return (
775
+ <Box>
776
+ <Text color={theme.primary}>{UI_ICONS.loading[frame]}</Text>
777
+ {text && <Text color={theme.secondary}> {text}</Text>}
778
+ </Box>
779
+ )
780
+ }
781
+
782
+ // Complete agents UI with comprehensive state management
783
+ interface AgentsUIProps {
784
+ onExit: (message?: string) => void
785
+ }
786
+
787
+ function AgentsUI({ onExit }: AgentsUIProps) {
788
+ const theme = getTheme()
789
+
790
+ // Core state management
791
+ const [modeState, setModeState] = useState<ModeState>({
792
+ mode: "list-agents",
793
+ location: "all" as AgentLocation
794
+ })
795
+
796
+ const [agents, setAgents] = useState<AgentConfig[]>([])
797
+ const [changes, setChanges] = useState<string[]>([])
798
+ const [refreshKey, setRefreshKey] = useState(0)
799
+ const [loading, setLoading] = useState(true)
800
+ const [tools, setTools] = useState<Tool[]>([])
801
+
802
+ // Creation state using reducer for complex flow management
803
+ const [createState, setCreateState] = useReducer(
804
+ (state: CreateState, action: any) => {
805
+ switch (action.type) {
806
+ case 'RESET':
807
+ return {
808
+ location: null,
809
+ agentType: '',
810
+ method: null,
811
+ generationPrompt: '',
812
+ whenToUse: '',
813
+ selectedTools: [],
814
+ selectedModel: null,
815
+ selectedColor: null,
816
+ systemPrompt: '',
817
+ isGenerating: false,
818
+ wasGenerated: false,
819
+ isAIGenerated: false,
820
+ error: null,
821
+ warnings: [],
822
+ agentTypeCursor: 0,
823
+ whenToUseCursor: 0,
824
+ promptCursor: 0,
825
+ generationPromptCursor: 0
826
+ }
827
+ case 'SET_LOCATION':
828
+ return { ...state, location: action.value }
829
+ case 'SET_METHOD':
830
+ return { ...state, method: action.value }
831
+ case 'SET_AGENT_TYPE':
832
+ return { ...state, agentType: action.value, error: null }
833
+ case 'SET_GENERATION_PROMPT':
834
+ return { ...state, generationPrompt: action.value }
835
+ case 'SET_WHEN_TO_USE':
836
+ return { ...state, whenToUse: action.value, error: null }
837
+ case 'SET_SELECTED_TOOLS':
838
+ return { ...state, selectedTools: action.value }
839
+ case 'SET_SELECTED_MODEL':
840
+ return { ...state, selectedModel: action.value }
841
+ case 'SET_SELECTED_COLOR':
842
+ return { ...state, selectedColor: action.value }
843
+ case 'SET_SYSTEM_PROMPT':
844
+ return { ...state, systemPrompt: action.value }
845
+ case 'SET_IS_GENERATING':
846
+ return { ...state, isGenerating: action.value }
847
+ case 'SET_WAS_GENERATED':
848
+ return { ...state, wasGenerated: action.value }
849
+ case 'SET_IS_AI_GENERATED':
850
+ return { ...state, isAIGenerated: action.value }
851
+ case 'SET_ERROR':
852
+ return { ...state, error: action.value }
853
+ case 'SET_WARNINGS':
854
+ return { ...state, warnings: action.value }
855
+ case 'SET_CURSOR':
856
+ return { ...state, [action.field]: action.value }
857
+ default:
858
+ return state
859
+ }
860
+ },
861
+ {
862
+ location: null,
863
+ agentType: '',
864
+ method: null,
865
+ generationPrompt: '',
866
+ whenToUse: '',
867
+ selectedTools: [],
868
+ selectedModel: null,
869
+ selectedColor: null,
870
+ systemPrompt: '',
871
+ isGenerating: false,
872
+ wasGenerated: false,
873
+ isAIGenerated: false,
874
+ error: null,
875
+ warnings: [],
876
+ agentTypeCursor: 0,
877
+ whenToUseCursor: 0,
878
+ promptCursor: 0,
879
+ generationPromptCursor: 0
880
+ }
881
+ )
882
+
883
+ // Load agents and tools dynamically
884
+ const loadAgents = useCallback(async () => {
885
+ setLoading(true)
886
+ clearAgentCache()
887
+
888
+ // 创建取消令牌以防止竞态条件
889
+ const abortController = new AbortController()
890
+ const loadingId = Date.now() // 用于标识这次加载
891
+
892
+ try {
893
+ const result = await getActiveAgents()
894
+
895
+ // 检查是否仍然是当前的加载请求
896
+ if (abortController.signal.aborted) {
897
+ return // 组件已卸载或新的加载已开始
898
+ }
899
+
900
+ setAgents(result)
901
+
902
+ // Update selectedAgent if there's one currently selected (for live reload)
903
+ if (modeState.selectedAgent) {
904
+ const freshSelectedAgent = result.find(a => a.agentType === modeState.selectedAgent!.agentType)
905
+ if (freshSelectedAgent) {
906
+ setModeState(prev => ({ ...prev, selectedAgent: freshSelectedAgent }))
907
+ }
908
+ }
909
+
910
+ // Load available tools dynamically from tool registry
911
+ const availableTools: Tool[] = []
912
+
913
+ // Core built-in tools
914
+ let coreTools = [
915
+ { name: 'Read', description: 'Read files from filesystem' },
916
+ { name: 'Write', description: 'Write files to filesystem' },
917
+ { name: 'Edit', description: 'Edit existing files' },
918
+ { name: 'MultiEdit', description: 'Make multiple edits to files' },
919
+ { name: 'NotebookEdit', description: 'Edit Jupyter notebooks' },
920
+ { name: 'Bash', description: 'Execute bash commands' },
921
+ { name: 'Glob', description: 'Find files matching patterns' },
922
+ { name: 'Grep', description: 'Search file contents' },
923
+ { name: 'LS', description: 'List directory contents' },
924
+ { name: 'WebFetch', description: 'Fetch web content' },
925
+ { name: 'WebSearch', description: 'Search the web' },
926
+ { name: 'TodoWrite', description: 'Manage task lists' }
927
+ ]
928
+ // Hide agent orchestration/self-control tools for subagent configs
929
+ coreTools = coreTools.filter(t => t.name !== 'Task' && t.name !== 'ExitPlanMode')
930
+
931
+ availableTools.push(...coreTools)
932
+
933
+ // Try to load MCP tools dynamically
934
+ try {
935
+ const mcpTools = await getMCPTools()
936
+ if (Array.isArray(mcpTools) && mcpTools.length > 0) {
937
+ availableTools.push(...mcpTools)
938
+ }
939
+ } catch (error) {
940
+ console.warn('Failed to load MCP tools:', error)
941
+ }
942
+
943
+ if (!abortController.signal.aborted) {
944
+ setTools(availableTools)
945
+ }
946
+ } catch (error) {
947
+ if (!abortController.signal.aborted) {
948
+ console.error('Failed to load agents:', error)
949
+ }
950
+ } finally {
951
+ if (!abortController.signal.aborted) {
952
+ setLoading(false)
953
+ }
954
+ }
955
+
956
+ // 返回取消函数供useEffect使用
957
+ return () => abortController.abort()
958
+ }, [])
959
+
960
+ // Remove mock MCP loader; real MCP tools are loaded via getMCPTools()
961
+
962
+ useEffect(() => {
963
+ let cleanup: (() => void) | undefined
964
+
965
+ const load = async () => {
966
+ cleanup = await loadAgents()
967
+ }
968
+
969
+ load()
970
+
971
+ return () => {
972
+ if (cleanup) {
973
+ cleanup()
974
+ }
975
+ }
976
+ }, [refreshKey, loadAgents])
977
+
978
+ // Local file watcher removed; rely on global watcher started in CLI.
979
+
980
+ // Global keyboard handling: ESC 逐级返回
981
+ useInput((input, key) => {
982
+ if (!key.escape) return
983
+
984
+ const changesSummary = changes.length > 0 ?
985
+ `Agent changes:\n${changes.join('\n')}` : undefined
986
+
987
+ const current = modeState.mode
988
+
989
+ if (current === 'list-agents') {
990
+ onExit(changesSummary)
991
+ return
992
+ }
993
+
994
+ // Hierarchical back navigation
995
+ switch (current) {
996
+ case 'create-location':
997
+ setModeState({ mode: 'list-agents', location: 'all' as AgentLocation })
998
+ break
999
+ case 'create-method':
1000
+ setModeState({ mode: 'create-location', location: modeState.location })
1001
+ break
1002
+ case 'create-generate':
1003
+ setModeState({ mode: 'create-location', location: modeState.location })
1004
+ break
1005
+ case 'create-type':
1006
+ setModeState({ mode: 'create-generate', location: modeState.location })
1007
+ break
1008
+ case 'create-prompt':
1009
+ setModeState({ mode: 'create-type', location: modeState.location })
1010
+ break
1011
+ case 'create-description':
1012
+ setModeState({ mode: 'create-prompt', location: modeState.location })
1013
+ break
1014
+ case 'create-tools':
1015
+ setModeState({ mode: 'create-description', location: modeState.location })
1016
+ break
1017
+ case 'create-model':
1018
+ setModeState({ mode: 'create-tools', location: modeState.location })
1019
+ break
1020
+ case 'create-color':
1021
+ setModeState({ mode: 'create-model', location: modeState.location })
1022
+ break
1023
+ case 'create-confirm':
1024
+ setModeState({ mode: 'create-color', location: modeState.location })
1025
+ break
1026
+ case 'agent-menu':
1027
+ setModeState({ mode: 'list-agents', location: 'all' as AgentLocation })
1028
+ break
1029
+ case 'view-agent':
1030
+ setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent })
1031
+ break
1032
+ case 'edit-agent':
1033
+ setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent })
1034
+ break
1035
+ case 'edit-tools':
1036
+ case 'edit-model':
1037
+ case 'edit-color':
1038
+ setModeState({ mode: 'edit-agent', selectedAgent: modeState.selectedAgent })
1039
+ break
1040
+ case 'delete-confirm':
1041
+ setModeState({ mode: 'agent-menu', selectedAgent: modeState.selectedAgent })
1042
+ break
1043
+ default:
1044
+ setModeState({ mode: 'list-agents', location: 'all' as AgentLocation })
1045
+ break
1046
+ }
1047
+ })
1048
+
1049
+ // Event handlers
1050
+ const handleAgentSelect = useCallback((agent: AgentConfig) => {
1051
+ setModeState({
1052
+ mode: "agent-menu",
1053
+ location: modeState.location,
1054
+ selectedAgent: agent
1055
+ })
1056
+ }, [modeState])
1057
+
1058
+ const handleCreateNew = useCallback(() => {
1059
+ console.log('=== STARTING AGENT CREATION FLOW ===')
1060
+ console.log('Current mode state:', modeState)
1061
+ setCreateState({ type: 'RESET' })
1062
+ console.log('Reset create state')
1063
+ setModeState({ mode: "create-location" })
1064
+ console.log('Set mode to create-location')
1065
+ console.log('=== CREATE NEW HANDLER COMPLETED ===')
1066
+ }, [modeState])
1067
+
1068
+ const handleAgentCreated = useCallback((message: string) => {
1069
+ setChanges(prev => [...prev, message])
1070
+ setRefreshKey(prev => prev + 1)
1071
+ setModeState({ mode: "list-agents", location: "all" as AgentLocation })
1072
+ }, [])
1073
+
1074
+ const handleAgentDeleted = useCallback((message: string) => {
1075
+ setChanges(prev => [...prev, message])
1076
+ setRefreshKey(prev => prev + 1)
1077
+ setModeState({ mode: "list-agents", location: "all" as AgentLocation })
1078
+ }, [])
1079
+
1080
+ if (loading) {
1081
+ return (
1082
+ <Box flexDirection="column">
1083
+ <Header title="Agents">
1084
+ <Box marginTop={1}>
1085
+ <LoadingSpinner text="Loading agents..." />
1086
+ </Box>
1087
+ </Header>
1088
+ <InstructionBar />
1089
+ </Box>
1090
+ )
1091
+ }
1092
+
1093
+ // Render based on current mode
1094
+ switch (modeState.mode) {
1095
+ case "list-agents":
1096
+ return (
1097
+ <AgentListView
1098
+ location={modeState.location || "all"}
1099
+ agents={agents}
1100
+ allAgents={agents}
1101
+ onBack={() => onExit()}
1102
+ onSelect={handleAgentSelect}
1103
+ onCreateNew={handleCreateNew}
1104
+ changes={changes}
1105
+ />
1106
+ )
1107
+
1108
+ case "create-location":
1109
+ return (
1110
+ <LocationSelect
1111
+ createState={createState}
1112
+ setCreateState={setCreateState}
1113
+ setModeState={setModeState}
1114
+ />
1115
+ )
1116
+
1117
+ case "create-method":
1118
+ return (
1119
+ <MethodSelect
1120
+ createState={createState}
1121
+ setCreateState={setCreateState}
1122
+ setModeState={setModeState}
1123
+ />
1124
+ )
1125
+
1126
+ case "create-generate":
1127
+ return (
1128
+ <GenerateStep
1129
+ createState={createState}
1130
+ setCreateState={setCreateState}
1131
+ setModeState={setModeState}
1132
+ existingAgents={agents}
1133
+ />
1134
+ )
1135
+
1136
+ case "create-type":
1137
+ return (
1138
+ <TypeStep
1139
+ createState={createState}
1140
+ setCreateState={setCreateState}
1141
+ setModeState={setModeState}
1142
+ existingAgents={agents}
1143
+ />
1144
+ )
1145
+
1146
+ case "create-description":
1147
+ return (
1148
+ <DescriptionStep
1149
+ createState={createState}
1150
+ setCreateState={setCreateState}
1151
+ setModeState={setModeState}
1152
+ />
1153
+ )
1154
+
1155
+ case "create-tools":
1156
+ return (
1157
+ <ToolsStep
1158
+ createState={createState}
1159
+ setCreateState={setCreateState}
1160
+ setModeState={setModeState}
1161
+ tools={tools}
1162
+ />
1163
+ )
1164
+
1165
+ case "create-model":
1166
+ return (
1167
+ <ModelStep
1168
+ createState={createState}
1169
+ setCreateState={setCreateState}
1170
+ setModeState={setModeState}
1171
+ />
1172
+ )
1173
+
1174
+ case "create-color":
1175
+ return (
1176
+ <ColorStep
1177
+ createState={createState}
1178
+ setCreateState={setCreateState}
1179
+ setModeState={setModeState}
1180
+ />
1181
+ )
1182
+
1183
+ case "create-prompt":
1184
+ return (
1185
+ <PromptStep
1186
+ createState={createState}
1187
+ setCreateState={setCreateState}
1188
+ setModeState={setModeState}
1189
+ />
1190
+ )
1191
+
1192
+ case "create-confirm":
1193
+ return (
1194
+ <ConfirmStep
1195
+ createState={createState}
1196
+ setCreateState={setCreateState}
1197
+ setModeState={setModeState}
1198
+ tools={tools}
1199
+ onAgentCreated={handleAgentCreated}
1200
+ />
1201
+ )
1202
+
1203
+ case "agent-menu":
1204
+ return (
1205
+ <AgentMenu
1206
+ agent={modeState.selectedAgent!}
1207
+ setModeState={setModeState}
1208
+ />
1209
+ )
1210
+
1211
+ case "view-agent":
1212
+ return (
1213
+ <ViewAgent
1214
+ agent={modeState.selectedAgent!}
1215
+ tools={tools}
1216
+ setModeState={setModeState}
1217
+ />
1218
+ )
1219
+
1220
+ case "edit-agent":
1221
+ return (
1222
+ <EditMenu
1223
+ agent={modeState.selectedAgent!}
1224
+ setModeState={setModeState}
1225
+ />
1226
+ )
1227
+
1228
+ case "edit-tools":
1229
+ return (
1230
+ <EditToolsStep
1231
+ agent={modeState.selectedAgent!}
1232
+ tools={tools}
1233
+ setModeState={setModeState}
1234
+ onAgentUpdated={(message, updated) => {
1235
+ setChanges(prev => [...prev, message])
1236
+ setRefreshKey(prev => prev + 1)
1237
+ setModeState({ mode: "agent-menu", selectedAgent: updated })
1238
+ }}
1239
+ />
1240
+ )
1241
+
1242
+ case "edit-model":
1243
+ return (
1244
+ <EditModelStep
1245
+ agent={modeState.selectedAgent!}
1246
+ setModeState={setModeState}
1247
+ onAgentUpdated={(message, updated) => {
1248
+ setChanges(prev => [...prev, message])
1249
+ setRefreshKey(prev => prev + 1)
1250
+ setModeState({ mode: "agent-menu", selectedAgent: updated })
1251
+ }}
1252
+ />
1253
+ )
1254
+
1255
+ case "edit-color":
1256
+ return (
1257
+ <EditColorStep
1258
+ agent={modeState.selectedAgent!}
1259
+ setModeState={setModeState}
1260
+ onAgentUpdated={(message, updated) => {
1261
+ setChanges(prev => [...prev, message])
1262
+ setRefreshKey(prev => prev + 1)
1263
+ setModeState({ mode: "agent-menu", selectedAgent: updated })
1264
+ }}
1265
+ />
1266
+ )
1267
+
1268
+ case "delete-confirm":
1269
+ return (
1270
+ <DeleteConfirm
1271
+ agent={modeState.selectedAgent!}
1272
+ setModeState={setModeState}
1273
+ onAgentDeleted={handleAgentDeleted}
1274
+ />
1275
+ )
1276
+
1277
+ default:
1278
+ return (
1279
+ <Box flexDirection="column">
1280
+ <Header title="Agents">
1281
+ <Text>Mode: {modeState.mode} (Not implemented yet)</Text>
1282
+ <Box marginTop={1}>
1283
+ <Text>Press Esc to go back</Text>
1284
+ </Box>
1285
+ </Header>
1286
+ <InstructionBar instructions="Esc to go back" />
1287
+ </Box>
1288
+ )
1289
+ }
1290
+ }
1291
+
1292
+ interface AgentListProps {
1293
+ location: AgentLocation
1294
+ agents: AgentConfig[]
1295
+ allAgents: AgentConfig[]
1296
+ onBack: () => void
1297
+ onSelect: (agent: AgentConfig) => void
1298
+ onCreateNew?: () => void
1299
+ changes: string[]
1300
+ }
1301
+
1302
+ function AgentListView({
1303
+ location,
1304
+ agents,
1305
+ allAgents,
1306
+ onBack,
1307
+ onSelect,
1308
+ onCreateNew,
1309
+ changes
1310
+ }: AgentListProps) {
1311
+ const theme = getTheme()
1312
+ const allAgentsList = allAgents || agents
1313
+ const customAgents = allAgentsList.filter(a => a.location !== "built-in")
1314
+ const builtInAgents = allAgentsList.filter(a => a.location === "built-in")
1315
+
1316
+ const [selectedAgent, setSelectedAgent] = useState<AgentConfig | null>(null)
1317
+ const [onCreateOption, setOnCreateOption] = useState(true)
1318
+ const [currentLocation, setCurrentLocation] = useState<AgentLocation>(location)
1319
+ const [inLocationTabs, setInLocationTabs] = useState(false)
1320
+ const [selectedLocationTab, setSelectedLocationTab] = useState(0)
1321
+
1322
+ const locationTabs = [
1323
+ { label: "All", value: "all" as AgentLocation },
1324
+ { label: "Personal", value: "user" as AgentLocation },
1325
+ { label: "Project", value: "project" as AgentLocation }
1326
+ ]
1327
+
1328
+ const activeMap = useMemo(() => {
1329
+ const map = new Map<string, AgentConfig>()
1330
+ agents.forEach(a => map.set(a.agentType, a))
1331
+ return map
1332
+ }, [agents])
1333
+
1334
+ const checkOverride = (agent: AgentConfig) => {
1335
+ const active = activeMap.get(agent.agentType)
1336
+ const isOverridden = !!(active && active.location !== agent.location)
1337
+ return {
1338
+ isOverridden,
1339
+ overriddenBy: isOverridden ? active.location : null
1340
+ }
1341
+ }
1342
+
1343
+ const renderCreateOption = () => (
1344
+ <Box flexDirection="row" gap={1}>
1345
+ <Text color={onCreateOption ? theme.primary : undefined}>
1346
+ {onCreateOption ? `${UI_ICONS.pointer} ` : " "}
1347
+ </Text>
1348
+ <Text bold color={onCreateOption ? theme.primary : undefined}>
1349
+ ✨ Create new agent
1350
+ </Text>
1351
+ </Box>
1352
+ )
1353
+
1354
+ const renderAgent = (agent: AgentConfig, isBuiltIn = false) => {
1355
+ const isSelected = !isBuiltIn && !onCreateOption &&
1356
+ selectedAgent?.agentType === agent.agentType &&
1357
+ selectedAgent?.location === agent.location
1358
+ const { isOverridden, overriddenBy } = checkOverride(agent)
1359
+ const dimmed = isBuiltIn || isOverridden
1360
+ const color = !isBuiltIn && isSelected ? theme.primary : undefined
1361
+
1362
+ // Extract model from agent metadata
1363
+ const agentModel = (agent as any).model || null
1364
+ const modelDisplay = getDisplayModelName(agentModel)
1365
+
1366
+ return (
1367
+ <Box key={`${agent.agentType}-${agent.location}`} flexDirection="row" alignItems="center">
1368
+ <Box flexDirection="row" alignItems="center" minWidth={3}>
1369
+ <Text dimColor={dimmed && !isSelected} color={color}>
1370
+ {isBuiltIn ? "" : isSelected ? `${UI_ICONS.pointer} ` : " "}
1371
+ </Text>
1372
+ </Box>
1373
+ <Box flexDirection="row" alignItems="center" flexGrow={1}>
1374
+ <Text dimColor={dimmed && !isSelected} color={color}>
1375
+ {agent.agentType}
1376
+ </Text>
1377
+ <Text dimColor={true} color={dimmed ? undefined : 'gray'}>
1378
+ {" · "}{modelDisplay}
1379
+ </Text>
1380
+ </Box>
1381
+ {overriddenBy && (
1382
+ <Box marginLeft={1}>
1383
+ <Text dimColor={!isSelected} color={isSelected ? 'yellow' : 'gray'}>
1384
+ {UI_ICONS.warning} overridden by {overriddenBy}
1385
+ </Text>
1386
+ </Box>
1387
+ )}
1388
+ </Box>
1389
+ )
1390
+ }
1391
+
1392
+ const displayAgents = useMemo(() => {
1393
+ if (currentLocation === "all") {
1394
+ return [
1395
+ ...customAgents.filter(a => a.location === "user"),
1396
+ ...customAgents.filter(a => a.location === "project")
1397
+ ]
1398
+ } else if (currentLocation === "user" || currentLocation === "project") {
1399
+ return customAgents.filter(a => a.location === currentLocation)
1400
+ }
1401
+ return customAgents
1402
+ }, [customAgents, currentLocation])
1403
+
1404
+ // 更新当前选中的标签索引
1405
+ useEffect(() => {
1406
+ const tabIndex = locationTabs.findIndex(tab => tab.value === currentLocation)
1407
+ if (tabIndex !== -1) {
1408
+ setSelectedLocationTab(tabIndex)
1409
+ }
1410
+ }, [currentLocation, locationTabs])
1411
+
1412
+ // 确保当有agents时,初始化选择状态
1413
+ useEffect(() => {
1414
+ if (displayAgents.length > 0 && !selectedAgent && !onCreateOption) {
1415
+ setOnCreateOption(true) // 默认选择创建选项
1416
+ }
1417
+ }, [displayAgents.length, selectedAgent, onCreateOption])
1418
+
1419
+ useInput((input, key) => {
1420
+ if (key.escape) {
1421
+ if (inLocationTabs) {
1422
+ setInLocationTabs(false)
1423
+ return
1424
+ }
1425
+ onBack()
1426
+ return
1427
+ }
1428
+
1429
+ if (key.return) {
1430
+ if (inLocationTabs) {
1431
+ setCurrentLocation(locationTabs[selectedLocationTab].value)
1432
+ setInLocationTabs(false)
1433
+ return
1434
+ }
1435
+ if (onCreateOption && onCreateNew) {
1436
+ onCreateNew()
1437
+ } else if (selectedAgent) {
1438
+ onSelect(selectedAgent)
1439
+ }
1440
+ return
1441
+ }
1442
+
1443
+ // Tab键进入/退出标签导航
1444
+ if (key.tab) {
1445
+ setInLocationTabs(!inLocationTabs)
1446
+ return
1447
+ }
1448
+
1449
+ // 在标签导航模式
1450
+ if (inLocationTabs) {
1451
+ if (key.leftArrow) {
1452
+ setSelectedLocationTab(prev => prev > 0 ? prev - 1 : locationTabs.length - 1)
1453
+ } else if (key.rightArrow) {
1454
+ setSelectedLocationTab(prev => prev < locationTabs.length - 1 ? prev + 1 : 0)
1455
+ }
1456
+ return
1457
+ }
1458
+
1459
+ // 键盘导航 - 这是关键缺失的功能
1460
+ if (key.upArrow || key.downArrow) {
1461
+ const allNavigableItems = []
1462
+
1463
+ // 添加创建选项
1464
+ if (onCreateNew) {
1465
+ allNavigableItems.push({ type: 'create', agent: null })
1466
+ }
1467
+
1468
+ // 添加可导航的agents
1469
+ displayAgents.forEach(agent => {
1470
+ const { isOverridden } = checkOverride(agent)
1471
+ if (!isOverridden) { // 只显示未被覆盖的agents
1472
+ allNavigableItems.push({ type: 'agent', agent })
1473
+ }
1474
+ })
1475
+
1476
+ if (allNavigableItems.length === 0) return
1477
+
1478
+ if (key.upArrow) {
1479
+ if (onCreateOption) {
1480
+ // 从创建选项向上到最后一个agent
1481
+ const lastAgent = allNavigableItems[allNavigableItems.length - 1]
1482
+ if (lastAgent.type === 'agent') {
1483
+ setSelectedAgent(lastAgent.agent)
1484
+ setOnCreateOption(false)
1485
+ }
1486
+ } else if (selectedAgent) {
1487
+ const currentIndex = allNavigableItems.findIndex(
1488
+ item => item.type === 'agent' &&
1489
+ item.agent?.agentType === selectedAgent.agentType &&
1490
+ item.agent?.location === selectedAgent.location
1491
+ )
1492
+ if (currentIndex > 0) {
1493
+ const prevItem = allNavigableItems[currentIndex - 1]
1494
+ if (prevItem.type === 'create') {
1495
+ setOnCreateOption(true)
1496
+ setSelectedAgent(null)
1497
+ } else {
1498
+ setSelectedAgent(prevItem.agent)
1499
+ }
1500
+ } else {
1501
+ // 到达顶部,回到创建选项
1502
+ if (onCreateNew) {
1503
+ setOnCreateOption(true)
1504
+ setSelectedAgent(null)
1505
+ }
1506
+ }
1507
+ }
1508
+ } else if (key.downArrow) {
1509
+ if (onCreateOption) {
1510
+ // 从创建选项向下到第一个agent
1511
+ const firstAgent = allNavigableItems.find(item => item.type === 'agent')
1512
+ if (firstAgent) {
1513
+ setSelectedAgent(firstAgent.agent)
1514
+ setOnCreateOption(false)
1515
+ }
1516
+ } else if (selectedAgent) {
1517
+ const currentIndex = allNavigableItems.findIndex(
1518
+ item => item.type === 'agent' &&
1519
+ item.agent?.agentType === selectedAgent.agentType &&
1520
+ item.agent?.location === selectedAgent.location
1521
+ )
1522
+ if (currentIndex < allNavigableItems.length - 1) {
1523
+ const nextItem = allNavigableItems[currentIndex + 1]
1524
+ if (nextItem.type === 'agent') {
1525
+ setSelectedAgent(nextItem.agent)
1526
+ }
1527
+ } else {
1528
+ // 到达底部,回到创建选项
1529
+ if (onCreateNew) {
1530
+ setOnCreateOption(true)
1531
+ setSelectedAgent(null)
1532
+ }
1533
+ }
1534
+ }
1535
+ }
1536
+ }
1537
+ })
1538
+
1539
+ // 特殊的键盘输入处理组件用于空状态
1540
+ const EmptyStateInput = () => {
1541
+ useInput((input, key) => {
1542
+ if (key.escape) {
1543
+ onBack()
1544
+ return
1545
+ }
1546
+ if (key.return && onCreateNew) {
1547
+ onCreateNew()
1548
+ return
1549
+ }
1550
+ })
1551
+ return null
1552
+ }
1553
+
1554
+ if (!agents.length || (currentLocation !== "built-in" && !customAgents.length)) {
1555
+ return (
1556
+ <Box flexDirection="column">
1557
+ <EmptyStateInput />
1558
+ <Header title="🤖 Agents" subtitle="">
1559
+ {onCreateNew && (
1560
+ <Box marginY={1}>
1561
+ {renderCreateOption()}
1562
+ </Box>
1563
+ )}
1564
+ <Box marginTop={1} flexDirection="column">
1565
+ <Box marginBottom={1}>
1566
+ <Text bold color={theme.primary}>💭 What are agents?</Text>
1567
+ </Box>
1568
+ <Text>Specialized AI assistants that Claude can delegate to for specific tasks.</Text>
1569
+ <Text>Each agent has its own context, prompt, and tools.</Text>
1570
+
1571
+ <Box marginTop={1} marginBottom={1}>
1572
+ <Text bold color={theme.primary}>💡 Popular agent ideas:</Text>
1573
+ </Box>
1574
+ <Box paddingLeft={2} flexDirection="column">
1575
+ <Text>• 🔍 Code Reviewer - Reviews PRs for best practices</Text>
1576
+ <Text>• 🔒 Security Auditor - Finds vulnerabilities</Text>
1577
+ <Text>• ⚡ Performance Optimizer - Improves code speed</Text>
1578
+ <Text>• 🧑‍💼 Tech Lead - Makes architecture decisions</Text>
1579
+ <Text>• 🎨 UX Expert - Improves user experience</Text>
1580
+ </Box>
1581
+ </Box>
1582
+
1583
+ {currentLocation !== "built-in" && builtInAgents.length > 0 && (
1584
+ <>
1585
+ <Box marginTop={1}><Text>{UI_ICONS.separator.repeat(40)}</Text></Box>
1586
+ <Box flexDirection="column" marginBottom={1} paddingLeft={2}>
1587
+ <Text bold color={theme.secondary}>Built-in (always available):</Text>
1588
+ {builtInAgents.map(a => renderAgent(a, true))}
1589
+ </Box>
1590
+ </>
1591
+ )}
1592
+ </Header>
1593
+ <InstructionBar instructions="Press Enter to create new agent · Esc to go back" />
1594
+ </Box>
1595
+ )
1596
+ }
1597
+
1598
+ return (
1599
+ <Box flexDirection="column">
1600
+ <Header title="🤖 Agents" subtitle="">
1601
+ {changes.length > 0 && (
1602
+ <Box marginTop={1}>
1603
+ <Text dimColor>{changes[changes.length - 1]}</Text>
1604
+ </Box>
1605
+ )}
1606
+
1607
+ {/* Fancy location tabs */}
1608
+ <Box marginTop={1} flexDirection="column">
1609
+ <Box flexDirection="row" gap={2}>
1610
+ {locationTabs.map((tab, idx) => {
1611
+ const isActive = currentLocation === tab.value
1612
+ const isSelected = inLocationTabs && idx === selectedLocationTab
1613
+ return (
1614
+ <Box key={tab.value} flexDirection="row">
1615
+ <Text
1616
+ color={isSelected || isActive ? theme.primary : undefined}
1617
+ bold={isActive}
1618
+ dimColor={!isActive && !isSelected}
1619
+ >
1620
+ {isSelected ? '▶ ' : isActive ? '◉ ' : '○ '}
1621
+ {tab.label}
1622
+ </Text>
1623
+ {idx < locationTabs.length - 1 && <Text dimColor> | </Text>}
1624
+ </Box>
1625
+ )
1626
+ })}
1627
+ </Box>
1628
+ <Box marginTop={0}>
1629
+ <Text dimColor>
1630
+ {currentLocation === 'all' ? 'Showing all agents' :
1631
+ currentLocation === 'user' ? 'Personal agents (~/.claude/agents)' :
1632
+ 'Project agents (.claude/agents)'}
1633
+ </Text>
1634
+ </Box>
1635
+ </Box>
1636
+
1637
+ <Box flexDirection="column" marginTop={1}>
1638
+ {onCreateNew && (
1639
+ <Box marginBottom={1}>
1640
+ {renderCreateOption()}
1641
+ </Box>
1642
+ )}
1643
+
1644
+ {currentLocation === "all" ? (
1645
+ <>
1646
+ {customAgents.filter(a => a.location === "user").length > 0 && (
1647
+ <>
1648
+ <Text bold color={theme.secondary}>Personal:</Text>
1649
+ {customAgents.filter(a => a.location === "user").map(a => renderAgent(a))}
1650
+ </>
1651
+ )}
1652
+
1653
+ {customAgents.filter(a => a.location === "project").length > 0 && (
1654
+ <>
1655
+ <Box marginTop={customAgents.filter(a => a.location === "user").length > 0 ? 1 : 0}>
1656
+ <Text bold color={theme.secondary}>Project:</Text>
1657
+ </Box>
1658
+ {customAgents.filter(a => a.location === "project").map(a => renderAgent(a))}
1659
+ </>
1660
+ )}
1661
+
1662
+ {builtInAgents.length > 0 && (
1663
+ <>
1664
+ <Box marginTop={customAgents.length > 0 ? 1 : 0}>
1665
+ <Text>{UI_ICONS.separator.repeat(40)}</Text>
1666
+ </Box>
1667
+ <Box flexDirection="column">
1668
+ <Text bold color={theme.secondary}>Built-in:</Text>
1669
+ {builtInAgents.map(a => renderAgent(a, true))}
1670
+ </Box>
1671
+ </>
1672
+ )}
1673
+ </>
1674
+ ) : (
1675
+ <>
1676
+ {displayAgents.map(a => renderAgent(a))}
1677
+ {currentLocation !== "built-in" && builtInAgents.length > 0 && (
1678
+ <>
1679
+ <Box marginTop={1}><Text>{UI_ICONS.separator.repeat(40)}</Text></Box>
1680
+ <Box flexDirection="column">
1681
+ <Text bold color={theme.secondary}>Built-in:</Text>
1682
+ {builtInAgents.map(a => renderAgent(a, true))}
1683
+ </Box>
1684
+ </>
1685
+ )}
1686
+ </>
1687
+ )}
1688
+ </Box>
1689
+ </Header>
1690
+ <InstructionBar
1691
+ instructions={inLocationTabs ?
1692
+ "←→ Switch tabs • Enter Select • Tab Exit tabs" :
1693
+ "↑↓ Navigate • Tab Location • Enter Select"
1694
+ }
1695
+ />
1696
+ </Box>
1697
+ )
1698
+ }
1699
+
1700
+ // Common interface for creation step props
1701
+ interface StepProps {
1702
+ createState: CreateState
1703
+ setCreateState: React.Dispatch<any>
1704
+ setModeState: (state: ModeState) => void
1705
+ }
1706
+
1707
+ // Step 3: AI Generation
1708
+ interface GenerateStepProps extends StepProps {
1709
+ existingAgents: AgentConfig[]
1710
+ }
1711
+
1712
+ function GenerateStep({ createState, setCreateState, setModeState, existingAgents }: GenerateStepProps) {
1713
+ const handleSubmit = async () => {
1714
+ if (createState.generationPrompt.trim()) {
1715
+ setCreateState({ type: 'SET_IS_GENERATING', value: true })
1716
+ setCreateState({ type: 'SET_ERROR', value: null })
1717
+
1718
+ try {
1719
+ const generated = await generateAgentWithClaude(createState.generationPrompt)
1720
+
1721
+ // Validate the generated identifier doesn't conflict
1722
+ const validation = validateAgentType(generated.identifier, existingAgents)
1723
+ let finalIdentifier = generated.identifier
1724
+
1725
+ if (!validation.isValid) {
1726
+ // Add a suffix to make it unique
1727
+ let counter = 1
1728
+ while (true) {
1729
+ const testId = `${generated.identifier}-${counter}`
1730
+ const testValidation = validateAgentType(testId, existingAgents)
1731
+ if (testValidation.isValid) {
1732
+ finalIdentifier = testId
1733
+ break
1734
+ }
1735
+ counter++
1736
+ if (counter > 10) {
1737
+ finalIdentifier = `custom-agent-${Date.now()}`
1738
+ break
1739
+ }
1740
+ }
1741
+ }
1742
+
1743
+ setCreateState({ type: 'SET_AGENT_TYPE', value: finalIdentifier })
1744
+ setCreateState({ type: 'SET_WHEN_TO_USE', value: generated.whenToUse })
1745
+ setCreateState({ type: 'SET_SYSTEM_PROMPT', value: generated.systemPrompt })
1746
+ setCreateState({ type: 'SET_WAS_GENERATED', value: true })
1747
+ setCreateState({ type: 'SET_IS_GENERATING', value: false })
1748
+ setModeState({ mode: 'create-tools', location: createState.location })
1749
+ } catch (error) {
1750
+ console.error('Generation failed:', error)
1751
+ setCreateState({ type: 'SET_ERROR', value: 'Failed to generate agent. Please try again or use manual configuration.' })
1752
+ setCreateState({ type: 'SET_IS_GENERATING', value: false })
1753
+ }
1754
+ }
1755
+ }
1756
+
1757
+ return (
1758
+ <Box flexDirection="column">
1759
+ <Header title="✨ New Agent" subtitle="What should it do?" step={2} totalSteps={8}>
1760
+ <Box marginTop={1}>
1761
+ {createState.isGenerating ? (
1762
+ <Box flexDirection="column">
1763
+ <Text dimColor>{createState.generationPrompt}</Text>
1764
+ <Box marginTop={1}>
1765
+ <LoadingSpinner text="Generating agent configuration..." />
1766
+ </Box>
1767
+ </Box>
1768
+ ) : (
1769
+ <MultilineTextInput
1770
+ value={createState.generationPrompt}
1771
+ onChange={(value) => setCreateState({ type: 'SET_GENERATION_PROMPT', value })}
1772
+ placeholder="An expert that reviews pull requests for best practices, security issues, and suggests improvements..."
1773
+ onSubmit={handleSubmit}
1774
+ error={createState.error}
1775
+ rows={3}
1776
+ />
1777
+ )}
1778
+ </Box>
1779
+ </Header>
1780
+ <InstructionBar />
1781
+ </Box>
1782
+ )
1783
+ }
1784
+
1785
+ // Step 4: Manual type input (for manual method)
1786
+ interface TypeStepProps extends StepProps {
1787
+ existingAgents: AgentConfig[]
1788
+ }
1789
+
1790
+ function TypeStep({ createState, setCreateState, setModeState, existingAgents }: TypeStepProps) {
1791
+ const handleSubmit = () => {
1792
+ const validation = validateAgentType(createState.agentType, existingAgents)
1793
+ if (validation.isValid) {
1794
+ setModeState({ mode: 'create-prompt', location: createState.location })
1795
+ } else {
1796
+ setCreateState({ type: 'SET_ERROR', value: validation.errors[0] })
1797
+ }
1798
+ }
1799
+
1800
+ return (
1801
+ <Box flexDirection="column">
1802
+ <Header title="Create new agent" subtitle="Enter agent identifier" step={3} totalSteps={8}>
1803
+ <Box marginTop={1}>
1804
+ <InkTextInput
1805
+ value={createState.agentType}
1806
+ onChange={(value) => setCreateState({ type: 'SET_AGENT_TYPE', value })}
1807
+ placeholder="e.g. code-reviewer, tech-lead"
1808
+ onSubmit={handleSubmit}
1809
+ />
1810
+ {createState.error && (
1811
+ <Box marginTop={1}>
1812
+ <Text color="red">⚠ {createState.error}</Text>
1813
+ </Box>
1814
+ )}
1815
+ </Box>
1816
+ </Header>
1817
+ <InstructionBar />
1818
+ </Box>
1819
+ )
1820
+ }
1821
+
1822
+ // Step 5: Description input
1823
+ function DescriptionStep({ createState, setCreateState, setModeState }: StepProps) {
1824
+ const handleSubmit = () => {
1825
+ if (createState.whenToUse.trim()) {
1826
+ setModeState({ mode: 'create-tools', location: createState.location })
1827
+ }
1828
+ }
1829
+
1830
+ return (
1831
+ <Box flexDirection="column">
1832
+ <Header title="Create new agent" subtitle="Describe when to use this agent" step={5} totalSteps={8}>
1833
+ <Box marginTop={1}>
1834
+ <MultilineTextInput
1835
+ value={createState.whenToUse}
1836
+ onChange={(value) => setCreateState({ type: 'SET_WHEN_TO_USE', value })}
1837
+ placeholder="Use this agent when you need to review code for best practices, security issues..."
1838
+ onSubmit={handleSubmit}
1839
+ error={createState.error}
1840
+ rows={4}
1841
+ />
1842
+ </Box>
1843
+ </Header>
1844
+ <InstructionBar />
1845
+ </Box>
1846
+ )
1847
+ }
1848
+
1849
+ // Step 6: Tools selection
1850
+ interface ToolsStepProps extends StepProps {
1851
+ tools: Tool[]
1852
+ }
1853
+
1854
+ function ToolsStep({ createState, setCreateState, setModeState, tools }: ToolsStepProps) {
1855
+ const [selectedIndex, setSelectedIndex] = useState(0)
1856
+ // Default to all tools selected initially
1857
+ const initialSelection = createState.selectedTools.length > 0 ?
1858
+ new Set(createState.selectedTools) :
1859
+ new Set(tools.map(t => t.name)) // Select all tools by default
1860
+ const [selectedTools, setSelectedTools] = useState<Set<string>>(initialSelection)
1861
+ const [showAdvanced, setShowAdvanced] = useState(false)
1862
+ const [selectedCategory, setSelectedCategory] = useState<keyof typeof TOOL_CATEGORIES | 'mcp' | 'all'>('all')
1863
+
1864
+ // Categorize tools
1865
+ const categorizedTools = useMemo(() => {
1866
+ const categories: Record<string, Tool[]> = {
1867
+ read: [],
1868
+ edit: [],
1869
+ execution: [],
1870
+ web: [],
1871
+ mcp: [],
1872
+ other: []
1873
+ }
1874
+
1875
+ tools.forEach(tool => {
1876
+ let categorized = false
1877
+
1878
+ // Check MCP tools first
1879
+ if (tool.name.startsWith('mcp__')) {
1880
+ categories.mcp.push(tool)
1881
+ categorized = true
1882
+ } else {
1883
+ // Check built-in categories
1884
+ for (const [category, toolNames] of Object.entries(TOOL_CATEGORIES)) {
1885
+ if (Array.isArray(toolNames) && toolNames.includes(tool.name)) {
1886
+ categories[category as keyof typeof categories]?.push(tool)
1887
+ categorized = true
1888
+ break
1889
+ }
1890
+ }
1891
+ }
1892
+
1893
+ if (!categorized) {
1894
+ categories.other.push(tool)
1895
+ }
1896
+ })
1897
+
1898
+ return categories
1899
+ }, [tools])
1900
+
1901
+ const displayTools = useMemo(() => {
1902
+ if (selectedCategory === 'all') {
1903
+ return tools
1904
+ }
1905
+ return categorizedTools[selectedCategory] || []
1906
+ }, [selectedCategory, tools, categorizedTools])
1907
+
1908
+ const allSelected = selectedTools.size === tools.length && tools.length > 0
1909
+ const categoryOptions = [
1910
+ { id: 'all', label: `All (${tools.length})` },
1911
+ { id: 'read', label: `Read (${categorizedTools.read.length})` },
1912
+ { id: 'edit', label: `Edit (${categorizedTools.edit.length})` },
1913
+ { id: 'execution', label: `Execution (${categorizedTools.execution.length})` },
1914
+ { id: 'web', label: `Web (${categorizedTools.web.length})` },
1915
+ { id: 'mcp', label: `MCP (${categorizedTools.mcp.length})` },
1916
+ { id: 'other', label: `Other (${categorizedTools.other.length})` }
1917
+ ].filter(cat => cat.id === 'all' || categorizedTools[cat.id]?.length > 0)
1918
+
1919
+ // Calculate category selections
1920
+ const readSelected = categorizedTools.read.every(tool => selectedTools.has(tool.name))
1921
+ const editSelected = categorizedTools.edit.every(tool => selectedTools.has(tool.name))
1922
+ const execSelected = categorizedTools.execution.every(tool => selectedTools.has(tool.name))
1923
+ const webSelected = categorizedTools.web.every(tool => selectedTools.has(tool.name))
1924
+
1925
+ const options: Array<{
1926
+ id: string
1927
+ label: string
1928
+ isContinue?: boolean
1929
+ isAll?: boolean
1930
+ isTool?: boolean
1931
+ isCategory?: boolean
1932
+ isAdvancedToggle?: boolean
1933
+ isSeparator?: boolean
1934
+ }> = [
1935
+ { id: 'continue', label: 'Save', isContinue: true },
1936
+ { id: 'separator1', label: '────────────────────────────────────', isSeparator: true },
1937
+ { id: 'all', label: `${allSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} All tools`, isAll: true },
1938
+ { id: 'read', label: `${readSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Read-only tools`, isCategory: true },
1939
+ { id: 'edit', label: `${editSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Edit tools`, isCategory: true },
1940
+ { id: 'execution', label: `${execSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Execution tools`, isCategory: true },
1941
+ { id: 'separator2', label: '────────────────────────────────────', isSeparator: true },
1942
+ { id: 'advanced', label: `[ ${showAdvanced ? 'Hide' : 'Show'} advanced options ]`, isAdvancedToggle: true },
1943
+ ...(showAdvanced ? displayTools.map(tool => ({
1944
+ id: tool.name,
1945
+ label: `${selectedTools.has(tool.name) ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} ${tool.name}`,
1946
+ isTool: true
1947
+ })) : [])
1948
+ ]
1949
+
1950
+ const handleSelect = () => {
1951
+ const option = options[selectedIndex] as any // Type assertion for union type
1952
+ if (!option) return
1953
+ if (option.isSeparator) return
1954
+
1955
+ if (option.isContinue) {
1956
+ const result = allSelected ? ['*'] : Array.from(selectedTools)
1957
+ setCreateState({ type: 'SET_SELECTED_TOOLS', value: result })
1958
+ setModeState({ mode: 'create-model', location: createState.location })
1959
+ } else if (option.isAdvancedToggle) {
1960
+ setShowAdvanced(!showAdvanced)
1961
+ } else if (option.isAll) {
1962
+ if (allSelected) {
1963
+ setSelectedTools(new Set())
1964
+ } else {
1965
+ setSelectedTools(new Set(tools.map(t => t.name)))
1966
+ }
1967
+ } else if (option.isCategory) {
1968
+ const categoryName = option.id as keyof typeof categorizedTools
1969
+ const categoryTools = categorizedTools[categoryName] || []
1970
+ const newSelected = new Set(selectedTools)
1971
+
1972
+ const categorySelected = categoryTools.every(tool => selectedTools.has(tool.name))
1973
+ if (categorySelected) {
1974
+ // Unselect all tools in this category
1975
+ categoryTools.forEach(tool => newSelected.delete(tool.name))
1976
+ } else {
1977
+ // Select all tools in this category
1978
+ categoryTools.forEach(tool => newSelected.add(tool.name))
1979
+ }
1980
+ setSelectedTools(newSelected)
1981
+ } else if (option.isTool) {
1982
+ const newSelected = new Set(selectedTools)
1983
+ if (newSelected.has(option.id)) {
1984
+ newSelected.delete(option.id)
1985
+ } else {
1986
+ newSelected.add(option.id)
1987
+ }
1988
+ setSelectedTools(newSelected)
1989
+ }
1990
+ }
1991
+
1992
+ useInput((input, key) => {
1993
+ if (key.return) {
1994
+ handleSelect()
1995
+ } else if (key.upArrow) {
1996
+ setSelectedIndex(prev => {
1997
+ let newIndex = prev > 0 ? prev - 1 : options.length - 1
1998
+ // Skip separators when going up
1999
+ while (options[newIndex] && (options[newIndex] as any).isSeparator) {
2000
+ newIndex = newIndex > 0 ? newIndex - 1 : options.length - 1
2001
+ }
2002
+ return newIndex
2003
+ })
2004
+ } else if (key.downArrow) {
2005
+ setSelectedIndex(prev => {
2006
+ let newIndex = prev < options.length - 1 ? prev + 1 : 0
2007
+ // Skip separators when going down
2008
+ while (options[newIndex] && (options[newIndex] as any).isSeparator) {
2009
+ newIndex = newIndex < options.length - 1 ? newIndex + 1 : 0
2010
+ }
2011
+ return newIndex
2012
+ })
2013
+ }
2014
+ })
2015
+
2016
+ return (
2017
+ <Box flexDirection="column">
2018
+ <Header title="🔧 Tool Permissions" subtitle="" step={3} totalSteps={5}>
2019
+ <Box flexDirection="column" marginTop={1}>
2020
+ {options.map((option, idx) => {
2021
+ const isSelected = idx === selectedIndex
2022
+ const isContinue = option.isContinue
2023
+ const isAdvancedToggle = option.isAdvancedToggle
2024
+ const isSeparator = option.isSeparator
2025
+
2026
+ return (
2027
+ <Box key={option.id}>
2028
+ <Text
2029
+ color={isSelected && !isSeparator ? 'cyan' : isSeparator ? 'gray' : undefined}
2030
+ bold={isContinue}
2031
+ dimColor={isSeparator}
2032
+ >
2033
+ {isSeparator ?
2034
+ option.label :
2035
+ `${isSelected ? `${UI_ICONS.pointer} ` : ' '}${isContinue || isAdvancedToggle ? `${option.label}` : option.label}`
2036
+ }
2037
+ </Text>
2038
+ {option.isTool && isSelected && tools.find(t => t.name === option.id)?.description && (
2039
+ <Box marginLeft={4}>
2040
+ <Text dimColor>{tools.find(t => t.name === option.id)?.description}</Text>
2041
+ </Box>
2042
+ )}
2043
+ </Box>
2044
+ )
2045
+ })}
2046
+
2047
+ <Box marginTop={1}>
2048
+ <Text dimColor>
2049
+ {allSelected ?
2050
+ 'All tools selected' :
2051
+ `${selectedTools.size} of ${tools.length} tools selected`}
2052
+ </Text>
2053
+ {selectedCategory !== 'all' && (
2054
+ <Text dimColor>Filtering: {selectedCategory} tools</Text>
2055
+ )}
2056
+ </Box>
2057
+ </Box>
2058
+ </Header>
2059
+ <InstructionBar instructions="↑↓ Navigate • Enter Toggle • Esc Back" />
2060
+ </Box>
2061
+ )
2062
+ }
2063
+
2064
+ // Step 6: Model selection (clean design like /models)
2065
+ function ModelStep({ createState, setCreateState, setModeState }: StepProps) {
2066
+ const theme = getTheme()
2067
+ const manager = getModelManager()
2068
+ const profiles = manager.getActiveModelProfiles()
2069
+
2070
+ // Group models by provider
2071
+ const groupedModels = profiles.reduce((acc: any, profile: any) => {
2072
+ const provider = profile.provider || 'Default'
2073
+ if (!acc[provider]) acc[provider] = []
2074
+ acc[provider].push(profile)
2075
+ return acc
2076
+ }, {})
2077
+
2078
+ // Flatten with inherit option
2079
+ const modelOptions = [
2080
+ { id: null, name: '◈ Inherit from parent', provider: 'System', modelName: 'default' },
2081
+ ...Object.entries(groupedModels).flatMap(([provider, models]: any) =>
2082
+ models.map((p: any) => ({
2083
+ id: p.modelName,
2084
+ name: p.name,
2085
+ provider: provider,
2086
+ modelName: p.modelName
2087
+ }))
2088
+ )
2089
+ ]
2090
+
2091
+ const [selectedIndex, setSelectedIndex] = useState(() => {
2092
+ const idx = modelOptions.findIndex(m => m.id === createState.selectedModel)
2093
+ return idx >= 0 ? idx : 0
2094
+ })
2095
+
2096
+ const handleSelect = (modelId: string | null) => {
2097
+ setCreateState({ type: 'SET_SELECTED_MODEL', value: modelId })
2098
+ setModeState({ mode: 'create-color', location: createState.location })
2099
+ }
2100
+
2101
+ useInput((input, key) => {
2102
+ if (key.return) {
2103
+ handleSelect(modelOptions[selectedIndex].id)
2104
+ } else if (key.upArrow) {
2105
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : modelOptions.length - 1))
2106
+ } else if (key.downArrow) {
2107
+ setSelectedIndex(prev => (prev < modelOptions.length - 1 ? prev + 1 : 0))
2108
+ }
2109
+ })
2110
+
2111
+ return (
2112
+ <Box flexDirection="column">
2113
+ <Header title="🤖 Select Model" subtitle="" step={4} totalSteps={5}>
2114
+ <Box marginTop={1} flexDirection="column">
2115
+ {modelOptions.map((model, index) => {
2116
+ const isSelected = index === selectedIndex
2117
+ const isInherit = model.id === null
2118
+
2119
+ return (
2120
+ <Box key={model.id || 'inherit'} marginBottom={0}>
2121
+ <Box flexDirection="row" gap={1}>
2122
+ <Text color={isSelected ? theme.primary : undefined}>
2123
+ {isSelected ? UI_ICONS.pointer : ' '}
2124
+ </Text>
2125
+ <Box flexDirection="column" flexGrow={1}>
2126
+ <Box flexDirection="row" gap={1}>
2127
+ <Text
2128
+ bold={isInherit}
2129
+ color={isSelected ? theme.primary : undefined}
2130
+ >
2131
+ {model.name}
2132
+ </Text>
2133
+ {!isInherit && (
2134
+ <Text dimColor>
2135
+ {model.provider} • {model.modelName}
2136
+ </Text>
2137
+ )}
2138
+ </Box>
2139
+ </Box>
2140
+ </Box>
2141
+ </Box>
2142
+ )
2143
+ })}
2144
+ </Box>
2145
+ </Header>
2146
+ <InstructionBar instructions="↑↓ Navigate • Enter Select" />
2147
+ </Box>
2148
+ )
2149
+ }
2150
+
2151
+ // Step 7: Color selection (using hex colors for display)
2152
+ function ColorStep({ createState, setCreateState, setModeState }: StepProps) {
2153
+ const theme = getTheme()
2154
+ const [selectedIndex, setSelectedIndex] = useState(0)
2155
+
2156
+ // Color options without red/green due to display issues
2157
+ const colors = [
2158
+ { label: 'Default', value: null, displayColor: null },
2159
+ { label: 'Yellow', value: 'yellow', displayColor: 'yellow' },
2160
+ { label: 'Blue', value: 'blue', displayColor: 'blue' },
2161
+ { label: 'Magenta', value: 'magenta', displayColor: 'magenta' },
2162
+ { label: 'Cyan', value: 'cyan', displayColor: 'cyan' },
2163
+ { label: 'Gray', value: 'gray', displayColor: 'gray' },
2164
+ { label: 'White', value: 'white', displayColor: 'white' }
2165
+ ]
2166
+
2167
+ const handleSelect = (value: string | null) => {
2168
+ setCreateState({ type: 'SET_SELECTED_COLOR', value: value })
2169
+ setModeState({ mode: 'create-confirm', location: createState.location })
2170
+ }
2171
+
2172
+ useInput((input, key) => {
2173
+ if (key.return) {
2174
+ handleSelect(colors[selectedIndex].value)
2175
+ } else if (key.upArrow) {
2176
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : colors.length - 1)
2177
+ } else if (key.downArrow) {
2178
+ setSelectedIndex(prev => prev < colors.length - 1 ? prev + 1 : 0)
2179
+ }
2180
+ })
2181
+
2182
+ return (
2183
+ <Box flexDirection="column">
2184
+ <Header title="🎨 Color Theme" subtitle="" step={5} totalSteps={5}>
2185
+ <Box marginTop={1} flexDirection="column">
2186
+ <Box marginBottom={1}>
2187
+ <Text dimColor>Choose how your agent appears in the list:</Text>
2188
+ </Box>
2189
+ {colors.map((color, idx) => {
2190
+ const isSelected = idx === selectedIndex
2191
+ return (
2192
+ <Box key={idx} flexDirection="row">
2193
+ <Text color={isSelected ? theme.primary : undefined}>
2194
+ {isSelected ? '❯ ' : ' '}
2195
+ </Text>
2196
+ <Box minWidth={12}>
2197
+ <Text bold={isSelected} color={color.displayColor || undefined}>
2198
+ {color.label}
2199
+ </Text>
2200
+ </Box>
2201
+ </Box>
2202
+ )
2203
+ })}
2204
+ <Box marginTop={1} paddingLeft={2}>
2205
+ <Text>Preview: </Text>
2206
+ <Text bold color={colors[selectedIndex].displayColor || undefined}>
2207
+ {createState.agentType || 'your-agent'}
2208
+ </Text>
2209
+ </Box>
2210
+ </Box>
2211
+ </Header>
2212
+ <InstructionBar instructions="↑↓ Navigate • Enter Select" />
2213
+ </Box>
2214
+ )
2215
+ }
2216
+
2217
+ // Step 8: System prompt
2218
+ function PromptStep({ createState, setCreateState, setModeState }: StepProps) {
2219
+ const handleSubmit = () => {
2220
+ if (createState.systemPrompt.trim()) {
2221
+ setModeState({ mode: 'create-description', location: createState.location })
2222
+ }
2223
+ }
2224
+
2225
+ return (
2226
+ <Box flexDirection="column">
2227
+ <Header title="Create new agent" subtitle="System prompt" step={4} totalSteps={8}>
2228
+ <Box marginTop={1}>
2229
+ <MultilineTextInput
2230
+ value={createState.systemPrompt}
2231
+ onChange={(value) => setCreateState({ type: 'SET_SYSTEM_PROMPT', value })}
2232
+ placeholder="You are a helpful assistant that specializes in..."
2233
+ onSubmit={handleSubmit}
2234
+ error={createState.error}
2235
+ rows={5}
2236
+ />
2237
+ </Box>
2238
+ </Header>
2239
+ <InstructionBar />
2240
+ </Box>
2241
+ )
2242
+ }
2243
+
2244
+ // Step 9: Confirmation
2245
+ interface ConfirmStepProps extends StepProps {
2246
+ tools: Tool[]
2247
+ onAgentCreated: (message: string) => void
2248
+ }
2249
+
2250
+ function ConfirmStep({ createState, setCreateState, setModeState, tools, onAgentCreated }: ConfirmStepProps) {
2251
+ const [isCreating, setIsCreating] = useState(false)
2252
+ const theme = getTheme()
2253
+
2254
+ const handleConfirm = async () => {
2255
+ setIsCreating(true)
2256
+ try {
2257
+ await saveAgent(
2258
+ createState.location!,
2259
+ createState.agentType,
2260
+ createState.whenToUse,
2261
+ createState.selectedTools,
2262
+ createState.systemPrompt,
2263
+ createState.selectedModel,
2264
+ createState.selectedColor || undefined
2265
+ )
2266
+ onAgentCreated(`Created agent: ${createState.agentType}`)
2267
+ } catch (error) {
2268
+ setCreateState({ type: 'SET_ERROR', value: (error as Error).message })
2269
+ setIsCreating(false)
2270
+ }
2271
+ }
2272
+
2273
+ const validation = validateAgentConfig(createState)
2274
+ const toolNames = createState.selectedTools.includes('*') ?
2275
+ 'All tools' :
2276
+ createState.selectedTools.length > 0 ?
2277
+ createState.selectedTools.join(', ') :
2278
+ 'No tools'
2279
+
2280
+ const handleEditInEditor = async () => {
2281
+ const filePath = createState.location === 'project'
2282
+ ? path.join(process.cwd(), '.claude', 'agents', `${createState.agentType}.md`)
2283
+ : path.join(os.homedir(), '.claude', 'agents', `${createState.agentType}.md`)
2284
+
2285
+ try {
2286
+ // First, save the agent file
2287
+ await saveAgent(
2288
+ createState.location!,
2289
+ createState.agentType,
2290
+ createState.whenToUse,
2291
+ createState.selectedTools,
2292
+ createState.systemPrompt,
2293
+ createState.selectedModel,
2294
+ createState.selectedColor || undefined
2295
+ )
2296
+
2297
+ // Then open it in editor
2298
+ const command = process.platform === 'win32' ? 'start' :
2299
+ process.platform === 'darwin' ? 'open' : 'xdg-open'
2300
+ await execAsync(`${command} "${filePath}"`)
2301
+ onAgentCreated(`Created agent: ${createState.agentType}`)
2302
+ } catch (error) {
2303
+ setCreateState({ type: 'SET_ERROR', value: (error as Error).message })
2304
+ }
2305
+ }
2306
+
2307
+ useInput((input, key) => {
2308
+ if (isCreating) return
2309
+
2310
+ if ((key.return || input === 's') && !isCreating) {
2311
+ handleConfirm()
2312
+ } else if (input === 'e') {
2313
+ handleEditInEditor()
2314
+ } else if (key.escape) {
2315
+ setModeState({ mode: "create-color", location: createState.location! })
2316
+ }
2317
+ })
2318
+
2319
+ return (
2320
+ <Box flexDirection="column">
2321
+ <Header title="✅ Review & Create" subtitle="">
2322
+ <Box flexDirection="column" marginTop={1}>
2323
+ <Box marginBottom={1}>
2324
+ <Text bold color={theme.primary}>📋 Configuration</Text>
2325
+ </Box>
2326
+
2327
+ <Box flexDirection="column" gap={0}>
2328
+ <Text>• <Text bold>Agent ID:</Text> {createState.agentType}</Text>
2329
+ <Text>• <Text bold>Location:</Text> {createState.location === 'project' ? 'Project' : 'Personal'}</Text>
2330
+ <Text>• <Text bold>Tools:</Text> {toolNames.length > 50 ? toolNames.slice(0, 50) + '...' : toolNames}</Text>
2331
+ <Text>• <Text bold>Model:</Text> {getDisplayModelName(createState.selectedModel)}</Text>
2332
+ {createState.selectedColor && (
2333
+ <Text>• <Text bold>Color:</Text> <Text color={createState.selectedColor}>{createState.selectedColor}</Text></Text>
2334
+ )}
2335
+ </Box>
2336
+
2337
+ <Box marginTop={1} marginBottom={1}>
2338
+ <Text bold color={theme.primary}>📝 Purpose</Text>
2339
+ </Box>
2340
+ <Box paddingLeft={1}>
2341
+ <Text>{createState.whenToUse}</Text>
2342
+ </Box>
2343
+
2344
+ {validation.warnings.length > 0 && (
2345
+ <Box marginTop={1}>
2346
+ <Text><Text bold>Warnings:</Text></Text>
2347
+ {validation.warnings.map((warning, idx) => (
2348
+ <Text key={idx} color={theme.warning}> • {warning}</Text>
2349
+ ))}
2350
+ </Box>
2351
+ )}
2352
+
2353
+ {createState.error && (
2354
+ <Box marginTop={1}>
2355
+ <Text color={theme.error}>✗ {createState.error}</Text>
2356
+ </Box>
2357
+ )}
2358
+
2359
+ <Box marginTop={2}>
2360
+ {isCreating ? (
2361
+ <LoadingSpinner text="Creating agent..." />
2362
+ ) : null}
2363
+ </Box>
2364
+ </Box>
2365
+ </Header>
2366
+ <InstructionBar instructions="Enter Save • E Edit • Esc Back" />
2367
+ </Box>
2368
+ )
2369
+ }
2370
+
2371
+ // Step 1: Location selection
2372
+ interface LocationSelectProps {
2373
+ createState: CreateState
2374
+ setCreateState: React.Dispatch<any>
2375
+ setModeState: (state: ModeState) => void
2376
+ }
2377
+
2378
+ function LocationSelect({ createState, setCreateState, setModeState }: LocationSelectProps) {
2379
+ const theme = getTheme()
2380
+ const [selectedIndex, setSelectedIndex] = useState(0)
2381
+
2382
+ const options = [
2383
+ { label: "📁 Project", value: "project", desc: ".claude/agents/" },
2384
+ { label: "🏠 Personal", value: "user", desc: "~/.claude/agents/" }
2385
+ ]
2386
+
2387
+ const handleChange = (value: string) => {
2388
+ setCreateState({ type: 'SET_LOCATION', value: value as AgentLocation })
2389
+ setCreateState({ type: 'SET_METHOD', value: 'generate' }) // Always use generate method
2390
+ setModeState({ mode: "create-generate", location: value as AgentLocation })
2391
+ }
2392
+
2393
+ const handleCancel = () => {
2394
+ setModeState({ mode: "list-agents", location: "all" as AgentLocation })
2395
+ }
2396
+
2397
+ useInput((input, key) => {
2398
+ if (key.escape) {
2399
+ handleCancel()
2400
+ } else if (key.return) {
2401
+ handleChange(options[selectedIndex].value)
2402
+ } else if (key.upArrow) {
2403
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1)
2404
+ } else if (key.downArrow) {
2405
+ setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0)
2406
+ }
2407
+ })
2408
+
2409
+ return (
2410
+ <Box flexDirection="column">
2411
+ <Header title="📦 Save Location" subtitle="" step={1} totalSteps={5}>
2412
+ <Box marginTop={1} flexDirection="column">
2413
+ {options.map((opt, idx) => (
2414
+ <Box key={opt.value} flexDirection="column" marginBottom={1}>
2415
+ <Text color={idx === selectedIndex ? theme.primary : undefined}>
2416
+ {idx === selectedIndex ? '❯ ' : ' '}{opt.label}
2417
+ </Text>
2418
+ <Box marginLeft={3}>
2419
+ <Text dimColor>{opt.desc}</Text>
2420
+ </Box>
2421
+ </Box>
2422
+ ))}
2423
+ </Box>
2424
+ </Header>
2425
+ <InstructionBar instructions="↑↓ Navigate • Enter Select" />
2426
+ </Box>
2427
+ )
2428
+ }
2429
+
2430
+ // Step 2: Method selection
2431
+ interface MethodSelectProps {
2432
+ createState: CreateState
2433
+ setCreateState: React.Dispatch<any>
2434
+ setModeState: (state: ModeState) => void
2435
+ }
2436
+
2437
+ function MethodSelect({ createState, setCreateState, setModeState }: MethodSelectProps) {
2438
+ const [selectedIndex, setSelectedIndex] = useState(0)
2439
+
2440
+ const options = [
2441
+ { label: "Generate with Claude (recommended)", value: "generate" },
2442
+ { label: "Manual configuration", value: "manual" }
2443
+ ]
2444
+
2445
+ const handleChange = (value: string) => {
2446
+ setCreateState({ type: 'SET_METHOD', value: value as 'generate' | 'manual' })
2447
+ if (value === "generate") {
2448
+ setCreateState({ type: 'SET_IS_AI_GENERATED', value: true })
2449
+ setModeState({ mode: "create-generate", location: createState.location })
2450
+ } else {
2451
+ setCreateState({ type: 'SET_IS_AI_GENERATED', value: false })
2452
+ setModeState({ mode: "create-type", location: createState.location })
2453
+ }
2454
+ }
2455
+
2456
+ const handleCancel = () => {
2457
+ setModeState({ mode: "create-location" })
2458
+ }
2459
+
2460
+ useInput((input, key) => {
2461
+ if (key.escape) {
2462
+ handleCancel()
2463
+ } else if (key.return) {
2464
+ handleChange(options[selectedIndex].value)
2465
+ } else if (key.upArrow) {
2466
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1)
2467
+ } else if (key.downArrow) {
2468
+ setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0)
2469
+ }
2470
+ })
2471
+
2472
+ return (
2473
+ <Box flexDirection="column">
2474
+ <Header title="Create new agent" subtitle="Creation method" step={2} totalSteps={9}>
2475
+ <Box marginTop={1}>
2476
+ <SelectList
2477
+ options={options}
2478
+ selectedIndex={selectedIndex}
2479
+ onChange={handleChange}
2480
+ onCancel={handleCancel}
2481
+ />
2482
+ </Box>
2483
+ </Header>
2484
+ <InstructionBar />
2485
+ </Box>
2486
+ )
2487
+ }
2488
+
2489
+ // Agent menu for agent operations
2490
+ interface AgentMenuProps {
2491
+ agent: AgentConfig
2492
+ setModeState: (state: ModeState) => void
2493
+ }
2494
+
2495
+ function AgentMenu({ agent, setModeState }: AgentMenuProps) {
2496
+ const [selectedIndex, setSelectedIndex] = useState(0)
2497
+
2498
+ const options = [
2499
+ { label: "View details", value: "view" },
2500
+ { label: "Edit agent", value: "edit", disabled: agent.location === 'built-in' },
2501
+ { label: "Delete agent", value: "delete", disabled: agent.location === 'built-in' }
2502
+ ]
2503
+
2504
+ const availableOptions = options.filter(opt => !opt.disabled)
2505
+
2506
+ const handleSelect = (value: string) => {
2507
+ switch (value) {
2508
+ case "view":
2509
+ setModeState({ mode: "view-agent", selectedAgent: agent })
2510
+ break
2511
+ case "edit":
2512
+ setModeState({ mode: "edit-agent", selectedAgent: agent })
2513
+ break
2514
+ case "delete":
2515
+ setModeState({ mode: "delete-confirm", selectedAgent: agent })
2516
+ break
2517
+ }
2518
+ }
2519
+
2520
+ useInput((input, key) => {
2521
+ if (key.return) {
2522
+ handleSelect(availableOptions[selectedIndex].value)
2523
+ } else if (key.upArrow) {
2524
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : availableOptions.length - 1)
2525
+ } else if (key.downArrow) {
2526
+ setSelectedIndex(prev => prev < availableOptions.length - 1 ? prev + 1 : 0)
2527
+ }
2528
+ })
2529
+
2530
+ return (
2531
+ <Box flexDirection="column">
2532
+ <Header title={`Agent: ${agent.agentType}`} subtitle={`${agent.location}`}>
2533
+ <Box marginTop={1}>
2534
+ <SelectList
2535
+ options={availableOptions}
2536
+ selectedIndex={selectedIndex}
2537
+ onChange={handleSelect}
2538
+ numbered={false}
2539
+ />
2540
+ </Box>
2541
+ </Header>
2542
+ <InstructionBar />
2543
+ </Box>
2544
+ )
2545
+ }
2546
+
2547
+ // Edit menu for agent editing options
2548
+ interface EditMenuProps {
2549
+ agent: AgentConfig
2550
+ setModeState: (state: ModeState) => void
2551
+ }
2552
+
2553
+ function EditMenu({ agent, setModeState }: EditMenuProps) {
2554
+ const [selectedIndex, setSelectedIndex] = useState(0)
2555
+ const [isOpening, setIsOpening] = useState(false)
2556
+ const theme = getTheme()
2557
+
2558
+ const options = [
2559
+ { label: "Open in editor", value: "open-editor" },
2560
+ { label: "Edit tools", value: "edit-tools" },
2561
+ { label: "Edit model", value: "edit-model" },
2562
+ { label: "Edit color", value: "edit-color" }
2563
+ ]
2564
+
2565
+ const handleSelect = async (value: string) => {
2566
+ switch (value) {
2567
+ case "open-editor":
2568
+ setIsOpening(true)
2569
+ try {
2570
+ const filePath = getAgentFilePath(agent)
2571
+ await openInEditor(filePath)
2572
+ setModeState({ mode: "agent-menu", selectedAgent: agent })
2573
+ } catch (error) {
2574
+ console.error('Failed to open editor:', error)
2575
+ // TODO: Show error to user
2576
+ } finally {
2577
+ setIsOpening(false)
2578
+ }
2579
+ break
2580
+ case "edit-tools":
2581
+ setModeState({ mode: "edit-tools", selectedAgent: agent })
2582
+ break
2583
+ case "edit-model":
2584
+ setModeState({ mode: "edit-model", selectedAgent: agent })
2585
+ break
2586
+ case "edit-color":
2587
+ setModeState({ mode: "edit-color", selectedAgent: agent })
2588
+ break
2589
+ }
2590
+ }
2591
+
2592
+ const handleBack = () => {
2593
+ setModeState({ mode: "agent-menu", selectedAgent: agent })
2594
+ }
2595
+
2596
+ useInput((input, key) => {
2597
+ if (key.escape) {
2598
+ handleBack()
2599
+ } else if (key.return && !isOpening) {
2600
+ handleSelect(options[selectedIndex].value)
2601
+ } else if (key.upArrow) {
2602
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : options.length - 1)
2603
+ } else if (key.downArrow) {
2604
+ setSelectedIndex(prev => prev < options.length - 1 ? prev + 1 : 0)
2605
+ }
2606
+ })
2607
+
2608
+ if (isOpening) {
2609
+ return (
2610
+ <Box flexDirection="column">
2611
+ <Header title={`Edit agent: ${agent.agentType}`} subtitle="Opening in editor...">
2612
+ <Box marginTop={1}>
2613
+ <LoadingSpinner text="Opening file in editor..." />
2614
+ </Box>
2615
+ </Header>
2616
+ <InstructionBar />
2617
+ </Box>
2618
+ )
2619
+ }
2620
+
2621
+ return (
2622
+ <Box flexDirection="column">
2623
+ <Header title={`Edit agent: ${agent.agentType}`} subtitle={`Location: ${agent.location}`}>
2624
+ <Box marginTop={1}>
2625
+ <SelectList
2626
+ options={options}
2627
+ selectedIndex={selectedIndex}
2628
+ onChange={handleSelect}
2629
+ numbered={false}
2630
+ />
2631
+ </Box>
2632
+ </Header>
2633
+ <InstructionBar instructions="↑↓ navigate · Enter select · Esc back" />
2634
+ </Box>
2635
+ )
2636
+ }
2637
+
2638
+ // Edit tools step
2639
+ interface EditToolsStepProps {
2640
+ agent: AgentConfig
2641
+ tools: Tool[]
2642
+ setModeState: (state: ModeState) => void
2643
+ onAgentUpdated: (message: string, updated: AgentConfig) => void
2644
+ }
2645
+
2646
+ function EditToolsStep({ agent, tools, setModeState, onAgentUpdated }: EditToolsStepProps) {
2647
+ const [selectedIndex, setSelectedIndex] = useState(0)
2648
+
2649
+ // Initialize selected tools based on agent.tools
2650
+ const initialTools = Array.isArray(agent.tools) ? agent.tools :
2651
+ agent.tools === '*' ? tools.map(t => t.name) : []
2652
+ const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set(initialTools))
2653
+ const [showAdvanced, setShowAdvanced] = useState(false)
2654
+ const [isUpdating, setIsUpdating] = useState(false)
2655
+
2656
+ // Categorize tools
2657
+ const categorizedTools = useMemo(() => {
2658
+ const categories: Record<string, Tool[]> = {
2659
+ read: [],
2660
+ edit: [],
2661
+ execution: [],
2662
+ web: [],
2663
+ other: []
2664
+ }
2665
+
2666
+ tools.forEach(tool => {
2667
+ let categorized = false
2668
+
2669
+ // Check built-in categories
2670
+ for (const [category, toolNames] of Object.entries(TOOL_CATEGORIES)) {
2671
+ if (Array.isArray(toolNames) && toolNames.includes(tool.name)) {
2672
+ categories[category as keyof typeof categories]?.push(tool)
2673
+ categorized = true
2674
+ break
2675
+ }
2676
+ }
2677
+
2678
+ if (!categorized) {
2679
+ categories.other.push(tool)
2680
+ }
2681
+ })
2682
+
2683
+ return categories
2684
+ }, [tools])
2685
+
2686
+ const allSelected = selectedTools.size === tools.length && tools.length > 0
2687
+ const readSelected = categorizedTools.read.every(tool => selectedTools.has(tool.name)) && categorizedTools.read.length > 0
2688
+ const editSelected = categorizedTools.edit.every(tool => selectedTools.has(tool.name)) && categorizedTools.edit.length > 0
2689
+ const execSelected = categorizedTools.execution.every(tool => selectedTools.has(tool.name)) && categorizedTools.execution.length > 0
2690
+
2691
+ const options = [
2692
+ { id: 'continue', label: 'Save', isContinue: true },
2693
+ { id: 'separator1', label: '────────────────────────────────────', isSeparator: true },
2694
+ { id: 'all', label: `${allSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} All tools`, isAll: true },
2695
+ { id: 'read', label: `${readSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Read-only tools`, isCategory: true },
2696
+ { id: 'edit', label: `${editSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Edit tools`, isCategory: true },
2697
+ { id: 'execution', label: `${execSelected ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} Execution tools`, isCategory: true },
2698
+ { id: 'separator2', label: '────────────────────────────────────', isSeparator: true },
2699
+ { id: 'advanced', label: `[ ${showAdvanced ? 'Hide' : 'Show'} advanced options ]`, isAdvancedToggle: true },
2700
+ ...(showAdvanced ? tools.map(tool => ({
2701
+ id: tool.name,
2702
+ label: `${selectedTools.has(tool.name) ? UI_ICONS.checkboxOn : UI_ICONS.checkboxOff} ${tool.name}`,
2703
+ isTool: true
2704
+ })) : [])
2705
+ ]
2706
+
2707
+ const handleSave = async () => {
2708
+ setIsUpdating(true)
2709
+ try {
2710
+ // Type-safe tools conversion for updateAgent
2711
+ const toolsArray: string[] | '*' = allSelected ? '*' : Array.from(selectedTools)
2712
+ await updateAgent(agent, agent.whenToUse, toolsArray, agent.systemPrompt, agent.color, (agent as any).model)
2713
+
2714
+ // Clear cache and reload fresh agent data from file system
2715
+ clearAgentCache()
2716
+ const freshAgents = await getActiveAgents()
2717
+ const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType)
2718
+
2719
+ if (updatedAgent) {
2720
+ onAgentUpdated(`Updated tools for agent: ${agent.agentType}`, updatedAgent)
2721
+ setModeState({ mode: "edit-agent", selectedAgent: updatedAgent })
2722
+ } else {
2723
+ console.error('Failed to find updated agent after save')
2724
+ // Fallback to manual update
2725
+ const fallbackAgent: AgentConfig = {
2726
+ ...agent,
2727
+ tools: toolsArray.length === 1 && toolsArray[0] === '*' ? '*' : toolsArray,
2728
+ }
2729
+ onAgentUpdated(`Updated tools for agent: ${agent.agentType}`, fallbackAgent)
2730
+ setModeState({ mode: "edit-agent", selectedAgent: fallbackAgent })
2731
+ }
2732
+ } catch (error) {
2733
+ console.error('Failed to update agent tools:', error)
2734
+ // TODO: Show error to user
2735
+ } finally {
2736
+ setIsUpdating(false)
2737
+ }
2738
+ }
2739
+
2740
+ const handleSelect = () => {
2741
+ const option = options[selectedIndex] as any // Type assertion for union type
2742
+ if (!option) return
2743
+ if (option.isSeparator) return
2744
+
2745
+ if (option.isContinue) {
2746
+ handleSave()
2747
+ } else if (option.isAdvancedToggle) {
2748
+ setShowAdvanced(!showAdvanced)
2749
+ } else if (option.isAll) {
2750
+ if (allSelected) {
2751
+ setSelectedTools(new Set())
2752
+ } else {
2753
+ setSelectedTools(new Set(tools.map(t => t.name)))
2754
+ }
2755
+ } else if (option.isCategory) {
2756
+ const categoryName = option.id as keyof typeof categorizedTools
2757
+ const categoryTools = categorizedTools[categoryName] || []
2758
+ const newSelected = new Set(selectedTools)
2759
+
2760
+ const categorySelected = categoryTools.every(tool => selectedTools.has(tool.name))
2761
+ if (categorySelected) {
2762
+ categoryTools.forEach(tool => newSelected.delete(tool.name))
2763
+ } else {
2764
+ categoryTools.forEach(tool => newSelected.add(tool.name))
2765
+ }
2766
+ setSelectedTools(newSelected)
2767
+ } else if (option.isTool) {
2768
+ const newSelected = new Set(selectedTools)
2769
+ if (newSelected.has(option.id)) {
2770
+ newSelected.delete(option.id)
2771
+ } else {
2772
+ newSelected.add(option.id)
2773
+ }
2774
+ setSelectedTools(newSelected)
2775
+ }
2776
+ }
2777
+
2778
+ useInput((input, key) => {
2779
+ if (key.escape) {
2780
+ setModeState({ mode: "edit-agent", selectedAgent: agent })
2781
+ } else if (key.return && !isUpdating) {
2782
+ handleSelect()
2783
+ } else if (key.upArrow) {
2784
+ setSelectedIndex(prev => {
2785
+ let newIndex = prev > 0 ? prev - 1 : options.length - 1
2786
+ // Skip separators when going up
2787
+ while (options[newIndex] && (options[newIndex] as any).isSeparator) {
2788
+ newIndex = newIndex > 0 ? newIndex - 1 : options.length - 1
2789
+ }
2790
+ return newIndex
2791
+ })
2792
+ } else if (key.downArrow) {
2793
+ setSelectedIndex(prev => {
2794
+ let newIndex = prev < options.length - 1 ? prev + 1 : 0
2795
+ // Skip separators when going down
2796
+ while (options[newIndex] && (options[newIndex] as any).isSeparator) {
2797
+ newIndex = newIndex < options.length - 1 ? newIndex + 1 : 0
2798
+ }
2799
+ return newIndex
2800
+ })
2801
+ }
2802
+ })
2803
+
2804
+ if (isUpdating) {
2805
+ return (
2806
+ <Box flexDirection="column">
2807
+ <Header title={`Edit agent: ${agent.agentType}`}>
2808
+ <Box marginTop={1}>
2809
+ <LoadingSpinner text="Updating agent tools..." />
2810
+ </Box>
2811
+ </Header>
2812
+ <InstructionBar />
2813
+ </Box>
2814
+ )
2815
+ }
2816
+
2817
+ return (
2818
+ <Box flexDirection="column">
2819
+ <Header title={`Edit agent: ${agent.agentType}`}>
2820
+ <Box flexDirection="column" marginTop={1}>
2821
+ {options.map((option, idx) => {
2822
+ const isSelected = idx === selectedIndex
2823
+ const isContinue = option.isContinue
2824
+ const isAdvancedToggle = (option as any).isAdvancedToggle
2825
+ const isSeparator = (option as any).isSeparator
2826
+
2827
+ return (
2828
+ <Box key={option.id}>
2829
+ <Text
2830
+ color={isSelected && !isSeparator ? 'cyan' : isSeparator ? 'gray' : undefined}
2831
+ bold={isContinue}
2832
+ dimColor={isSeparator}
2833
+ >
2834
+ {isSeparator ?
2835
+ option.label :
2836
+ `${isSelected ? `${UI_ICONS.pointer} ` : ' '}${isContinue || isAdvancedToggle ? option.label : option.label}`
2837
+ }
2838
+ </Text>
2839
+ {(option as any).isTool && isSelected && tools.find(t => t.name === option.id)?.description && (
2840
+ <Box marginLeft={4}>
2841
+ <Text dimColor>{tools.find(t => t.name === option.id)?.description}</Text>
2842
+ </Box>
2843
+ )}
2844
+ </Box>
2845
+ )
2846
+ })}
2847
+
2848
+ <Box marginTop={1}>
2849
+ <Text dimColor>
2850
+ {allSelected ?
2851
+ 'All tools selected' :
2852
+ `${selectedTools.size} of ${tools.length} tools selected`}
2853
+ </Text>
2854
+ </Box>
2855
+ </Box>
2856
+ </Header>
2857
+ <InstructionBar instructions="Enter toggle selection · ↑↓ navigate · Esc back" />
2858
+ </Box>
2859
+ )
2860
+ }
2861
+
2862
+ // Edit model step
2863
+ interface EditModelStepProps {
2864
+ agent: AgentConfig
2865
+ setModeState: (state: ModeState) => void
2866
+ onAgentUpdated: (message: string, updated: AgentConfig) => void
2867
+ }
2868
+
2869
+ function EditModelStep({ agent, setModeState, onAgentUpdated }: EditModelStepProps) {
2870
+ const manager = getModelManager()
2871
+ const profiles = manager.getActiveModelProfiles()
2872
+ const currentModel = (agent as any).model || null
2873
+
2874
+ // Build model options array
2875
+ const modelOptions = [
2876
+ { id: null, name: 'Inherit from parent', description: 'Use the model from task configuration' },
2877
+ ...profiles.map((p: any) => ({ id: p.modelName, name: p.name, description: `${p.provider || 'provider'} · ${p.modelName}` }))
2878
+ ]
2879
+
2880
+ // Find the index of current model
2881
+ const defaultIndex = modelOptions.findIndex(m => m.id === currentModel)
2882
+ const [selectedIndex, setSelectedIndex] = useState(defaultIndex >= 0 ? defaultIndex : 0)
2883
+ const [isUpdating, setIsUpdating] = useState(false)
2884
+
2885
+ const handleSave = async (modelId: string | null) => {
2886
+ setIsUpdating(true)
2887
+ try {
2888
+ const modelValue = modelId === null ? undefined : modelId
2889
+ await updateAgent(agent, agent.whenToUse, agent.tools, agent.systemPrompt, agent.color, modelValue)
2890
+
2891
+ // Clear cache and reload fresh agent data from file system
2892
+ clearAgentCache()
2893
+ const freshAgents = await getActiveAgents()
2894
+ const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType)
2895
+
2896
+ if (updatedAgent) {
2897
+ onAgentUpdated(`Updated model for agent: ${agent.agentType}`, updatedAgent)
2898
+ setModeState({ mode: 'edit-agent', selectedAgent: updatedAgent })
2899
+ } else {
2900
+ console.error('Failed to find updated agent after save')
2901
+ // Fallback to manual update
2902
+ const fallbackAgent: AgentConfig = { ...agent }
2903
+ if (modelValue) {
2904
+ (fallbackAgent as any).model = modelValue
2905
+ } else {
2906
+ delete (fallbackAgent as any).model
2907
+ }
2908
+ onAgentUpdated(`Updated model for agent: ${agent.agentType}`, fallbackAgent)
2909
+ setModeState({ mode: 'edit-agent', selectedAgent: fallbackAgent })
2910
+ }
2911
+ } catch (error) {
2912
+ console.error('Failed to update agent model:', error)
2913
+ } finally {
2914
+ setIsUpdating(false)
2915
+ }
2916
+ }
2917
+
2918
+ useInput((input, key) => {
2919
+ if (key.escape) {
2920
+ setModeState({ mode: 'edit-agent', selectedAgent: agent })
2921
+ } else if (key.return && !isUpdating) {
2922
+ handleSave(modelOptions[selectedIndex].id)
2923
+ } else if (key.upArrow) {
2924
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : modelOptions.length - 1))
2925
+ } else if (key.downArrow) {
2926
+ setSelectedIndex(prev => (prev < modelOptions.length - 1 ? prev + 1 : 0))
2927
+ }
2928
+ })
2929
+
2930
+ if (isUpdating) {
2931
+ return (
2932
+ <Box flexDirection="column">
2933
+ <Header title={`Edit agent: ${agent.agentType}`}>
2934
+ <Box marginTop={1}>
2935
+ <LoadingSpinner text="Updating agent model..." />
2936
+ </Box>
2937
+ </Header>
2938
+ <InstructionBar />
2939
+ </Box>
2940
+ )
2941
+ }
2942
+
2943
+ return (
2944
+ <Box flexDirection="column">
2945
+ <Header title={`Edit agent: ${agent.agentType}`} subtitle="Model determines the agent's reasoning capabilities and speed.">
2946
+ <Box marginTop={2}>
2947
+ <SelectList
2948
+ options={modelOptions.map((m, i) => ({ label: `${i + 1}. ${m.name}${m.description ? `\n${m.description}` : ''}`, value: m.id }))}
2949
+ selectedIndex={selectedIndex}
2950
+ onChange={(val) => handleSave(val)}
2951
+ numbered={false}
2952
+ />
2953
+ </Box>
2954
+ </Header>
2955
+ <InstructionBar instructions="↑↓ navigate · Enter select · Esc back" />
2956
+ </Box>
2957
+ )
2958
+ }
2959
+
2960
+ // Edit color step
2961
+ interface EditColorStepProps {
2962
+ agent: AgentConfig
2963
+ setModeState: (state: ModeState) => void
2964
+ onAgentUpdated: (message: string, updated: AgentConfig) => void
2965
+ }
2966
+
2967
+ function EditColorStep({ agent, setModeState, onAgentUpdated }: EditColorStepProps) {
2968
+ const currentColor = agent.color || null
2969
+
2970
+ // Define color options (removed red/green due to display issues)
2971
+ const colors = [
2972
+ { label: 'Automatic color', value: null },
2973
+ { label: 'Yellow', value: 'yellow' },
2974
+ { label: 'Blue', value: 'blue' },
2975
+ { label: 'Magenta', value: 'magenta' },
2976
+ { label: 'Cyan', value: 'cyan' },
2977
+ { label: 'Gray', value: 'gray' },
2978
+ { label: 'White', value: 'white' }
2979
+ ]
2980
+
2981
+ // Find current color index
2982
+ const defaultIndex = colors.findIndex(color => color.value === currentColor)
2983
+ const [selectedIndex, setSelectedIndex] = useState(defaultIndex >= 0 ? defaultIndex : 0)
2984
+ const [isUpdating, setIsUpdating] = useState(false)
2985
+
2986
+ const handleSave = async (color: string | null) => {
2987
+ setIsUpdating(true)
2988
+ try {
2989
+ const colorValue = color === null ? undefined : color
2990
+ await updateAgent(agent, agent.whenToUse, agent.tools, agent.systemPrompt, colorValue, (agent as any).model)
2991
+
2992
+ // Clear cache and reload fresh agent data from file system
2993
+ clearAgentCache()
2994
+ const freshAgents = await getActiveAgents()
2995
+ const updatedAgent = freshAgents.find(a => a.agentType === agent.agentType)
2996
+
2997
+ if (updatedAgent) {
2998
+ onAgentUpdated(`Updated color for agent: ${agent.agentType}`, updatedAgent)
2999
+ setModeState({ mode: "edit-agent", selectedAgent: updatedAgent })
3000
+ } else {
3001
+ console.error('Failed to find updated agent after save')
3002
+ // Fallback to manual update
3003
+ const fallbackAgent: AgentConfig = { ...agent, ...(colorValue ? { color: colorValue } : { color: undefined }) }
3004
+ onAgentUpdated(`Updated color for agent: ${agent.agentType}`, fallbackAgent)
3005
+ setModeState({ mode: "edit-agent", selectedAgent: fallbackAgent })
3006
+ }
3007
+ } catch (error) {
3008
+ console.error('Failed to update agent color:', error)
3009
+ // TODO: Show error to user
3010
+ } finally {
3011
+ setIsUpdating(false)
3012
+ }
3013
+ }
3014
+
3015
+ useInput((input, key) => {
3016
+ if (key.escape) {
3017
+ setModeState({ mode: "edit-agent", selectedAgent: agent })
3018
+ } else if (key.return && !isUpdating) {
3019
+ handleSave(colors[selectedIndex].value)
3020
+ } else if (key.upArrow) {
3021
+ setSelectedIndex(prev => prev > 0 ? prev - 1 : colors.length - 1)
3022
+ } else if (key.downArrow) {
3023
+ setSelectedIndex(prev => prev < colors.length - 1 ? prev + 1 : 0)
3024
+ }
3025
+ })
3026
+
3027
+ if (isUpdating) {
3028
+ return (
3029
+ <Box flexDirection="column">
3030
+ <Header title={`Edit agent: ${agent.agentType}`}>
3031
+ <Box marginTop={1}>
3032
+ <LoadingSpinner text="Updating agent color..." />
3033
+ </Box>
3034
+ </Header>
3035
+ <InstructionBar />
3036
+ </Box>
3037
+ )
3038
+ }
3039
+
3040
+ const selectedColor = colors[selectedIndex]
3041
+ const previewColor = selectedColor.value || undefined
3042
+
3043
+ return (
3044
+ <Box flexDirection="column">
3045
+ <Header title={`Edit agent: ${agent.agentType}`} subtitle="Choose background color">
3046
+ <Box flexDirection="column" marginTop={1}>
3047
+ {colors.map((color, index) => {
3048
+ const isSelected = index === selectedIndex
3049
+ const isCurrent = color.value === currentColor
3050
+
3051
+ return (
3052
+ <Box key={color.value || 'automatic'}>
3053
+ <Text color={isSelected ? 'cyan' : undefined}>
3054
+ {isSelected ? '❯ ' : ' '}
3055
+ </Text>
3056
+ <Text color={color.value || undefined}>●</Text>
3057
+ <Text>
3058
+ {' '}{color.label}
3059
+ {isCurrent && (
3060
+ <Text color="green"> ✔</Text>
3061
+ )}
3062
+ </Text>
3063
+ </Box>
3064
+ )
3065
+ })}
3066
+
3067
+ <Box marginTop={2}>
3068
+ <Text>Preview: </Text>
3069
+ <Text color={previewColor}>{agent.agentType}</Text>
3070
+ </Box>
3071
+ </Box>
3072
+ </Header>
3073
+ <InstructionBar instructions="↑↓ navigate · Enter select · Esc back" />
3074
+ </Box>
3075
+ )
3076
+ }
3077
+
3078
+ // View agent details
3079
+ interface ViewAgentProps {
3080
+ agent: AgentConfig
3081
+ tools: Tool[]
3082
+ setModeState: (state: ModeState) => void
3083
+ }
3084
+
3085
+ function ViewAgent({ agent, tools, setModeState }: ViewAgentProps) {
3086
+ const theme = getTheme()
3087
+ const agentTools = Array.isArray(agent.tools) ? agent.tools : []
3088
+ const hasAllTools = agent.tools === "*" || agentTools.includes("*")
3089
+ const locationPath = agent.location === 'user'
3090
+ ? `~/.claude/agents/${agent.agentType}.md`
3091
+ : agent.location === 'project'
3092
+ ? `.claude/agents/${agent.agentType}.md`
3093
+ : '(built-in)'
3094
+ const displayModel = getDisplayModelName((agent as any).model || null)
3095
+
3096
+ const allowedTools = useMemo(() => {
3097
+ if (hasAllTools) return tools
3098
+
3099
+ return tools.filter(tool =>
3100
+ agentTools.some(allowedTool => {
3101
+ if (allowedTool.includes("*")) {
3102
+ const prefix = allowedTool.replace("*", "")
3103
+ return tool.name.startsWith(prefix)
3104
+ }
3105
+ return tool.name === allowedTool
3106
+ })
3107
+ )
3108
+ }, [tools, agentTools, hasAllTools])
3109
+
3110
+ return (
3111
+ <Box flexDirection="column">
3112
+ <Header title={`Agent: ${agent.agentType}`} subtitle="Details">
3113
+ <Box flexDirection="column" marginTop={1}>
3114
+ <Text><Text bold>Type:</Text> {agent.agentType}</Text>
3115
+ <Text><Text bold>Location:</Text> {agent.location} {locationPath !== '(built-in)' ? `· ${locationPath}` : ''}</Text>
3116
+ <Text><Text bold>Description:</Text> {agent.whenToUse}</Text>
3117
+ <Text><Text bold>Model:</Text> {displayModel}</Text>
3118
+ <Text><Text bold>Color:</Text> {agent.color || 'auto'}</Text>
3119
+
3120
+ <Box marginTop={1}>
3121
+ <Text bold>Tools:</Text>
3122
+ </Box>
3123
+ {hasAllTools ? (
3124
+ <Text color={theme.secondary}>All tools ({tools.length} available)</Text>
3125
+ ) : (
3126
+ <Box flexDirection="column" paddingLeft={2}>
3127
+ {allowedTools.map(tool => (
3128
+ <Text key={tool.name} color={theme.secondary}>• {tool.name}</Text>
3129
+ ))}
3130
+ </Box>
3131
+ )}
3132
+
3133
+ <Box marginTop={1}>
3134
+ <Text bold>System Prompt:</Text>
3135
+ </Box>
3136
+ <Box paddingLeft={2}>
3137
+ <Text>{agent.systemPrompt}</Text>
3138
+ </Box>
3139
+ </Box>
3140
+ </Header>
3141
+ <InstructionBar />
3142
+ </Box>
3143
+ )
3144
+ }
3145
+
3146
+ // Edit agent component
3147
+ interface EditAgentProps {
3148
+ agent: AgentConfig
3149
+ tools: Tool[]
3150
+ setModeState: (state: ModeState) => void
3151
+ onAgentUpdated: (message: string) => void
3152
+ }
3153
+
3154
+ function EditAgent({ agent, tools, setModeState, onAgentUpdated }: EditAgentProps) {
3155
+ const theme = getTheme()
3156
+ const [currentStep, setCurrentStep] = useState<'description' | 'tools' | 'prompt' | 'confirm'>('description')
3157
+ const [isUpdating, setIsUpdating] = useState(false)
3158
+
3159
+ // 编辑状态
3160
+ const [editedDescription, setEditedDescription] = useState(agent.whenToUse)
3161
+ const [editedTools, setEditedTools] = useState<string[]>(
3162
+ Array.isArray(agent.tools) ? agent.tools : agent.tools === '*' ? ['*'] : []
3163
+ )
3164
+ const [editedPrompt, setEditedPrompt] = useState(agent.systemPrompt)
3165
+ const [error, setError] = useState<string | null>(null)
3166
+
3167
+ const handleSave = async () => {
3168
+ setIsUpdating(true)
3169
+ try {
3170
+ await updateAgent(agent, editedDescription, editedTools, editedPrompt, agent.color)
3171
+ clearAgentCache()
3172
+ onAgentUpdated(`Updated agent: ${agent.agentType}`)
3173
+ } catch (error) {
3174
+ setError((error as Error).message)
3175
+ setIsUpdating(false)
3176
+ }
3177
+ }
3178
+
3179
+ const renderStepContent = () => {
3180
+ switch (currentStep) {
3181
+ case 'description':
3182
+ return (
3183
+ <Box flexDirection="column">
3184
+ <Text bold>Edit Description:</Text>
3185
+ <Box marginTop={1}>
3186
+ <MultilineTextInput
3187
+ value={editedDescription}
3188
+ onChange={setEditedDescription}
3189
+ placeholder="Describe when to use this agent..."
3190
+ onSubmit={() => setCurrentStep('tools')}
3191
+ error={error}
3192
+ rows={4}
3193
+ />
3194
+ </Box>
3195
+ </Box>
3196
+ )
3197
+
3198
+ case 'tools':
3199
+ return (
3200
+ <Box flexDirection="column">
3201
+ <Text bold>Edit Tools:</Text>
3202
+ <Box marginTop={1}>
3203
+ <ToolsStep
3204
+ createState={{
3205
+ selectedTools: editedTools,
3206
+ } as CreateState}
3207
+ setCreateState={(action) => {
3208
+ if (action.type === 'SET_SELECTED_TOOLS') {
3209
+ setEditedTools(action.value)
3210
+ setCurrentStep('prompt')
3211
+ }
3212
+ }}
3213
+ setModeState={() => {}}
3214
+ tools={tools}
3215
+ />
3216
+ </Box>
3217
+ </Box>
3218
+ )
3219
+
3220
+ case 'prompt':
3221
+ return (
3222
+ <Box flexDirection="column">
3223
+ <Text bold>Edit System Prompt:</Text>
3224
+ <Box marginTop={1}>
3225
+ <MultilineTextInput
3226
+ value={editedPrompt}
3227
+ onChange={setEditedPrompt}
3228
+ placeholder="System prompt for the agent..."
3229
+ onSubmit={() => setCurrentStep('confirm')}
3230
+ error={error}
3231
+ rows={5}
3232
+ />
3233
+ </Box>
3234
+ </Box>
3235
+ )
3236
+
3237
+ case 'confirm':
3238
+ const validation = validateAgentConfig({
3239
+ agentType: agent.agentType,
3240
+ whenToUse: editedDescription,
3241
+ systemPrompt: editedPrompt,
3242
+ selectedTools: editedTools
3243
+ })
3244
+
3245
+ return (
3246
+ <Box flexDirection="column">
3247
+ <Text bold>Confirm Changes:</Text>
3248
+ <Box flexDirection="column" marginTop={1}>
3249
+ <Text><Text bold>Agent:</Text> {agent.agentType}</Text>
3250
+ <Text><Text bold>Description:</Text> {editedDescription}</Text>
3251
+ <Text><Text bold>Tools:</Text> {editedTools.includes('*') ? 'All tools' : editedTools.join(', ')}</Text>
3252
+ <Text><Text bold>System Prompt:</Text> {editedPrompt.slice(0, 100)}{editedPrompt.length > 100 ? '...' : ''}</Text>
3253
+
3254
+ {validation.warnings.length > 0 && (
3255
+ <Box marginTop={1}>
3256
+ {validation.warnings.map((warning, idx) => (
3257
+ <Text key={idx} color={theme.warning}>⚠ {warning}</Text>
3258
+ ))}
3259
+ </Box>
3260
+ )}
3261
+
3262
+ {error && (
3263
+ <Box marginTop={1}>
3264
+ <Text color={theme.error}>✗ {error}</Text>
3265
+ </Box>
3266
+ )}
3267
+
3268
+ <Box marginTop={2}>
3269
+ {isUpdating ? (
3270
+ <LoadingSpinner text="Updating agent..." />
3271
+ ) : (
3272
+ <Text>Press Enter to save changes</Text>
3273
+ )}
3274
+ </Box>
3275
+ </Box>
3276
+ </Box>
3277
+ )
3278
+ }
3279
+ }
3280
+
3281
+ useInput((input, key) => {
3282
+ if (key.escape) {
3283
+ if (currentStep === 'description') {
3284
+ setModeState({ mode: "agent-menu", selectedAgent: agent })
3285
+ } else {
3286
+ // 返回上一步
3287
+ const steps: Array<typeof currentStep> = ['description', 'tools', 'prompt', 'confirm']
3288
+ const currentIndex = steps.indexOf(currentStep)
3289
+ if (currentIndex > 0) {
3290
+ setCurrentStep(steps[currentIndex - 1])
3291
+ }
3292
+ }
3293
+ return
3294
+ }
3295
+
3296
+ if (key.return && currentStep === 'confirm' && !isUpdating) {
3297
+ handleSave()
3298
+ }
3299
+ })
3300
+
3301
+ return (
3302
+ <Box flexDirection="column">
3303
+ <Header title={`Edit Agent: ${agent.agentType}`} subtitle={`Step ${['description', 'tools', 'prompt', 'confirm'].indexOf(currentStep) + 1}/4`}>
3304
+ <Box marginTop={1}>
3305
+ {renderStepContent()}
3306
+ </Box>
3307
+ </Header>
3308
+ <InstructionBar
3309
+ instructions={currentStep === 'confirm' ?
3310
+ "Press Enter to save · Esc to go back" :
3311
+ "Enter to continue · Esc to go back"
3312
+ }
3313
+ />
3314
+ </Box>
3315
+ )
3316
+ }
3317
+
3318
+ // Delete confirmation
3319
+ interface DeleteConfirmProps {
3320
+ agent: AgentConfig
3321
+ setModeState: (state: ModeState) => void
3322
+ onAgentDeleted: (message: string) => void
3323
+ }
3324
+
3325
+ function DeleteConfirm({ agent, setModeState, onAgentDeleted }: DeleteConfirmProps) {
3326
+ const [isDeleting, setIsDeleting] = useState(false)
3327
+ const [selected, setSelected] = useState(false) // false = No, true = Yes
3328
+
3329
+ const handleConfirm = async () => {
3330
+ if (selected) {
3331
+ setIsDeleting(true)
3332
+ try {
3333
+ await deleteAgent(agent)
3334
+ clearAgentCache()
3335
+ onAgentDeleted(`Deleted agent: ${agent.agentType}`)
3336
+ } catch (error) {
3337
+ console.error('Failed to delete agent:', error)
3338
+ setIsDeleting(false)
3339
+ // TODO: Show error to user
3340
+ }
3341
+ } else {
3342
+ setModeState({ mode: "agent-menu", selectedAgent: agent })
3343
+ }
3344
+ }
3345
+
3346
+ useInput((input, key) => {
3347
+ if (key.return) {
3348
+ handleConfirm()
3349
+ } else if (key.leftArrow || key.rightArrow || key.tab) {
3350
+ setSelected(!selected)
3351
+ }
3352
+ })
3353
+
3354
+ if (isDeleting) {
3355
+ return (
3356
+ <Box flexDirection="column">
3357
+ <Header title="Delete agent" subtitle="Deleting...">
3358
+ <Box marginTop={1}>
3359
+ <LoadingSpinner text="Deleting agent..." />
3360
+ </Box>
3361
+ </Header>
3362
+ <InstructionBar />
3363
+ </Box>
3364
+ )
3365
+ }
3366
+
3367
+ return (
3368
+ <Box flexDirection="column">
3369
+ <Header title="Delete agent" subtitle={`Delete "${agent.agentType}"?`}>
3370
+ <Box marginTop={1}>
3371
+ <Text>This action cannot be undone. The agent file will be permanently deleted.</Text>
3372
+ <Box marginTop={2} gap={3}>
3373
+ <Text color={!selected ? 'cyan' : undefined}>
3374
+ {!selected ? `${UI_ICONS.pointer} ` : ' '}No
3375
+ </Text>
3376
+ <Text color={selected ? 'red' : undefined}>
3377
+ {selected ? `${UI_ICONS.pointer} ` : ' '}Yes, delete
3378
+ </Text>
3379
+ </Box>
3380
+ </Box>
3381
+ </Header>
3382
+ <InstructionBar />
3383
+ </Box>
3384
+ )
3385
+ }
3386
+
3387
+ export default {
3388
+ name: 'agents',
3389
+ description: 'Manage agent configurations',
3390
+ type: 'local-jsx' as const,
3391
+ isEnabled: true,
3392
+ isHidden: false,
3393
+
3394
+ async call(onExit: (message?: string) => void) {
3395
+ return <AgentsUI onExit={onExit} />
3396
+ },
3397
+
3398
+ userFacingName() {
3399
+ return 'agents'
3400
+ }
3401
+ }