@shareai-lab/kode 1.1.14 → 1.1.16-dev.1

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