@shareai-lab/kode 1.0.69 → 1.0.71

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 (253) hide show
  1. package/README.md +205 -72
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,938 @@
1
+ import { randomUUID, UUID } from 'crypto'
2
+ import { Box } from 'ink'
3
+ import {
4
+ AssistantMessage,
5
+ Message,
6
+ ProgressMessage,
7
+ UserMessage,
8
+ } from '../query.js'
9
+ import { getCommand, hasCommand } from '../commands'
10
+ import { MalformedCommandError } from './errors'
11
+ import { logError } from './log'
12
+ import { resolve } from 'path'
13
+ import { last, memoize } from 'lodash-es'
14
+ import { logEvent } from '../services/statsig'
15
+ import type { SetToolJSXFn, Tool, ToolUseContext } from '../Tool'
16
+ import { lastX } from '../utils/generators'
17
+ import { NO_CONTENT_MESSAGE } from '../services/claude'
18
+ import {
19
+ ImageBlockParam,
20
+ TextBlockParam,
21
+ ToolResultBlockParam,
22
+ ToolUseBlockParam,
23
+ Message as APIMessage,
24
+ ContentBlockParam,
25
+ ContentBlock,
26
+ } from '@anthropic-ai/sdk/resources/index.mjs'
27
+ import { setCwd } from './state'
28
+ import { getCwd } from './state'
29
+ import chalk from 'chalk'
30
+ import * as React from 'react'
31
+ import { UserBashInputMessage } from '../components/messages/UserBashInputMessage'
32
+ import { Spinner } from '../components/Spinner'
33
+ import { BashTool } from '../tools/BashTool/BashTool'
34
+ import { ToolUseBlock } from '@anthropic-ai/sdk/resources/index.mjs'
35
+
36
+ // NOTE: Dynamic content processing for custom commands has been moved to
37
+ // src/services/customCommands.ts for better organization and reusability.
38
+ // The functions executeBashCommands and resolveFileReferences are no longer
39
+ // duplicated here but are imported when needed for custom command processing.
40
+
41
+ export const INTERRUPT_MESSAGE = '[Request interrupted by user]'
42
+ export const INTERRUPT_MESSAGE_FOR_TOOL_USE =
43
+ '[Request interrupted by user for tool use]'
44
+ export const CANCEL_MESSAGE =
45
+ "The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed."
46
+ export const REJECT_MESSAGE =
47
+ "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed."
48
+ export const NO_RESPONSE_REQUESTED = 'No response requested.'
49
+
50
+ export const SYNTHETIC_ASSISTANT_MESSAGES = new Set([
51
+ INTERRUPT_MESSAGE,
52
+ INTERRUPT_MESSAGE_FOR_TOOL_USE,
53
+ CANCEL_MESSAGE,
54
+ REJECT_MESSAGE,
55
+ NO_RESPONSE_REQUESTED,
56
+ ])
57
+
58
+ function baseCreateAssistantMessage(
59
+ content: ContentBlock[],
60
+ extra?: Partial<AssistantMessage>,
61
+ ): AssistantMessage {
62
+ return {
63
+ type: 'assistant',
64
+ costUSD: 0,
65
+ durationMs: 0,
66
+ uuid: randomUUID(),
67
+ message: {
68
+ id: randomUUID(),
69
+ model: '<synthetic>',
70
+ role: 'assistant',
71
+ stop_reason: 'stop_sequence',
72
+ stop_sequence: '',
73
+ type: 'message',
74
+ usage: {
75
+ input_tokens: 0,
76
+ output_tokens: 0,
77
+ cache_creation_input_tokens: 0,
78
+ cache_read_input_tokens: 0,
79
+ },
80
+ content,
81
+ },
82
+ ...extra,
83
+ }
84
+ }
85
+
86
+ export function createAssistantMessage(content: string): AssistantMessage {
87
+ return baseCreateAssistantMessage([
88
+ {
89
+ type: 'text' as const,
90
+ text: content === '' ? NO_CONTENT_MESSAGE : content,
91
+ citations: [],
92
+ },
93
+ ])
94
+ }
95
+
96
+ export function createAssistantAPIErrorMessage(
97
+ content: string,
98
+ ): AssistantMessage {
99
+ return baseCreateAssistantMessage(
100
+ [
101
+ {
102
+ type: 'text' as const,
103
+ text: content === '' ? NO_CONTENT_MESSAGE : content,
104
+ citations: [],
105
+ },
106
+ ],
107
+ { isApiErrorMessage: true },
108
+ )
109
+ }
110
+
111
+ export type FullToolUseResult = {
112
+ data: unknown // Matches tool's `Output` type
113
+ resultForAssistant: ToolResultBlockParam['content']
114
+ }
115
+
116
+ export function createUserMessage(
117
+ content: string | ContentBlockParam[],
118
+ toolUseResult?: FullToolUseResult,
119
+ ): UserMessage {
120
+ const m: UserMessage = {
121
+ type: 'user',
122
+ message: {
123
+ role: 'user',
124
+ content,
125
+ },
126
+ uuid: randomUUID(),
127
+ toolUseResult,
128
+ }
129
+ return m
130
+ }
131
+
132
+ export function createProgressMessage(
133
+ toolUseID: string,
134
+ siblingToolUseIDs: Set<string>,
135
+ content: AssistantMessage,
136
+ normalizedMessages: NormalizedMessage[],
137
+ tools: Tool[],
138
+ ): ProgressMessage {
139
+ return {
140
+ type: 'progress',
141
+ content,
142
+ normalizedMessages,
143
+ siblingToolUseIDs,
144
+ tools,
145
+ toolUseID,
146
+ uuid: randomUUID(),
147
+ }
148
+ }
149
+
150
+ export function createToolResultStopMessage(
151
+ toolUseID: string,
152
+ ): ToolResultBlockParam {
153
+ return {
154
+ type: 'tool_result',
155
+ content: CANCEL_MESSAGE,
156
+ is_error: true,
157
+ tool_use_id: toolUseID,
158
+ }
159
+ }
160
+
161
+ export async function processUserInput(
162
+ input: string,
163
+ mode: 'bash' | 'prompt' | 'koding',
164
+ setToolJSX: SetToolJSXFn,
165
+ context: ToolUseContext & {
166
+ setForkConvoWithMessagesOnTheNextRender: (
167
+ forkConvoWithMessages: Message[],
168
+ ) => void
169
+ options?: {
170
+ isKodingRequest?: boolean
171
+ kodingContext?: string
172
+ }
173
+ },
174
+ pastedImage: string | null,
175
+ ): Promise<Message[]> {
176
+ // Bash commands
177
+ if (mode === 'bash') {
178
+ logEvent('tengu_input_bash', {})
179
+
180
+ const userMessage = createUserMessage(`<bash-input>${input}</bash-input>`)
181
+
182
+ // Special case: cd
183
+ if (input.startsWith('cd ')) {
184
+ const oldCwd = getCwd()
185
+ const newCwd = resolve(oldCwd, input.slice(3))
186
+ try {
187
+ await setCwd(newCwd)
188
+ return [
189
+ userMessage,
190
+ createAssistantMessage(
191
+ `<bash-stdout>Changed directory to ${chalk.bold(`${newCwd}/`)}</bash-stdout>`,
192
+ ),
193
+ ]
194
+ } catch (e) {
195
+ logError(e)
196
+ return [
197
+ userMessage,
198
+ createAssistantMessage(
199
+ `<bash-stderr>cwd error: ${e instanceof Error ? e.message : String(e)}</bash-stderr>`,
200
+ ),
201
+ ]
202
+ }
203
+ }
204
+
205
+ // All other bash commands
206
+ setToolJSX({
207
+ jsx: (
208
+ <Box flexDirection="column" marginTop={1}>
209
+ <UserBashInputMessage
210
+ addMargin={false}
211
+ param={{ text: `<bash-input>${input}</bash-input>`, type: 'text' }}
212
+ />
213
+ <Spinner />
214
+ </Box>
215
+ ),
216
+ shouldHidePromptInput: false,
217
+ })
218
+ try {
219
+ const validationResult = await BashTool.validateInput({
220
+ command: input,
221
+ })
222
+ if (!validationResult.result) {
223
+ return [userMessage, createAssistantMessage(validationResult.message)]
224
+ }
225
+ const { data } = await lastX(BashTool.call({ command: input }, context))
226
+ return [
227
+ userMessage,
228
+ createAssistantMessage(
229
+ `<bash-stdout>${data.stdout}</bash-stdout><bash-stderr>${data.stderr}</bash-stderr>`,
230
+ ),
231
+ ]
232
+ } catch (e) {
233
+ return [
234
+ userMessage,
235
+ createAssistantMessage(
236
+ `<bash-stderr>Command failed: ${e instanceof Error ? e.message : String(e)}</bash-stderr>`,
237
+ ),
238
+ ]
239
+ } finally {
240
+ setToolJSX(null)
241
+ }
242
+ }
243
+ // Koding mode - special wrapper for display
244
+ else if (mode === 'koding') {
245
+ logEvent('tengu_input_koding', {})
246
+
247
+ const userMessage = createUserMessage(
248
+ `<koding-input>${input}</koding-input>`,
249
+ )
250
+ // Add the Koding flag to the message
251
+ userMessage.options = {
252
+ ...userMessage.options,
253
+ isKodingRequest: true,
254
+ }
255
+
256
+ // Rest of koding processing is handled separately to capture assistant response
257
+ return [userMessage]
258
+ }
259
+
260
+ // Slash commands
261
+ if (input.startsWith('/')) {
262
+ const words = input.slice(1).split(' ')
263
+ let commandName = words[0]
264
+ if (words.length > 1 && words[1] === '(MCP)') {
265
+ commandName = commandName + ' (MCP)'
266
+ }
267
+ if (!commandName) {
268
+ logEvent('tengu_input_slash_missing', { input })
269
+ return [
270
+ createAssistantMessage('Commands are in the form `/command [args]`'),
271
+ ]
272
+ }
273
+
274
+ // Check if it's a real command before processing
275
+ if (!hasCommand(commandName, context.options.commands)) {
276
+ // If not a real command, treat it as a regular user input
277
+ logEvent('tengu_input_prompt', {})
278
+ return [createUserMessage(input)]
279
+ }
280
+
281
+ const args = input.slice(commandName.length + 2)
282
+ const newMessages = await getMessagesForSlashCommand(
283
+ commandName,
284
+ args,
285
+ setToolJSX,
286
+ context,
287
+ )
288
+
289
+ // Local JSX commands
290
+ if (newMessages.length === 0) {
291
+ logEvent('tengu_input_command', { input })
292
+ return []
293
+ }
294
+
295
+ // For invalid commands, preserve both the user message and error
296
+ if (
297
+ newMessages.length === 2 &&
298
+ newMessages[0]!.type === 'user' &&
299
+ newMessages[1]!.type === 'assistant' &&
300
+ typeof newMessages[1]!.message.content === 'string' &&
301
+ // @ts-expect-error: TODO: this is probably a bug
302
+ newMessages[1]!.message.content.startsWith('Unknown command:')
303
+ ) {
304
+ logEvent('tengu_input_slash_invalid', { input })
305
+ return newMessages
306
+ }
307
+
308
+ // User-Assistant pair (eg. local commands)
309
+ if (newMessages.length === 2) {
310
+ logEvent('tengu_input_command', { input })
311
+ return newMessages
312
+ }
313
+
314
+ // A valid command
315
+ logEvent('tengu_input_command', { input })
316
+ return newMessages
317
+ }
318
+
319
+ // Regular user prompt
320
+ logEvent('tengu_input_prompt', {})
321
+
322
+ // Check if this is a Koding request that needs special handling
323
+ const isKodingRequest = context.options?.isKodingRequest === true
324
+ const kodingContextInfo = context.options?.kodingContext
325
+
326
+ // Create base message
327
+ let userMessage: UserMessage
328
+
329
+ if (pastedImage) {
330
+ userMessage = createUserMessage([
331
+ {
332
+ type: 'image',
333
+ source: {
334
+ type: 'base64',
335
+ media_type: 'image/png',
336
+ data: pastedImage,
337
+ },
338
+ },
339
+ {
340
+ type: 'text',
341
+ text:
342
+ isKodingRequest && kodingContextInfo
343
+ ? `${kodingContextInfo}\n\n${input}`
344
+ : input,
345
+ },
346
+ ])
347
+ } else {
348
+ let processedInput =
349
+ isKodingRequest && kodingContextInfo
350
+ ? `${kodingContextInfo}\n\n${input}`
351
+ : input
352
+
353
+ // Process dynamic content for custom commands with ! and @ prefixes
354
+ // This uses the same processing functions as custom commands to maintain consistency
355
+ if (input.includes('!`') || input.includes('@')) {
356
+ try {
357
+ // Import functions from customCommands service to avoid code duplication
358
+ const { executeBashCommands, resolveFileReferences } = await import(
359
+ '../services/customCommands'
360
+ )
361
+
362
+ // Execute bash commands if present
363
+ if (input.includes('!`')) {
364
+ // Note: This function is not exported from customCommands.ts, so we need to expose it
365
+ // For now, we'll keep the local implementation until we refactor the service
366
+ processedInput = await executeBashCommands(processedInput)
367
+ }
368
+
369
+ // Resolve file references if present
370
+ if (input.includes('@')) {
371
+ // Note: This function is not exported from customCommands.ts, so we need to expose it
372
+ // For now, we'll keep the local implementation until we refactor the service
373
+ processedInput = await resolveFileReferences(processedInput)
374
+ }
375
+ } catch (error) {
376
+ console.warn('Dynamic content processing failed:', error)
377
+ // Continue with original input if processing fails
378
+ }
379
+ }
380
+
381
+ userMessage = createUserMessage(processedInput)
382
+ }
383
+
384
+ // Add the Koding flag to the message if needed
385
+ if (isKodingRequest) {
386
+ userMessage.options = {
387
+ ...userMessage.options,
388
+ isKodingRequest: true,
389
+ }
390
+ }
391
+
392
+ return [userMessage]
393
+ }
394
+
395
+ async function getMessagesForSlashCommand(
396
+ commandName: string,
397
+ args: string,
398
+ setToolJSX: SetToolJSXFn,
399
+ context: ToolUseContext & {
400
+ setForkConvoWithMessagesOnTheNextRender: (
401
+ forkConvoWithMessages: Message[],
402
+ ) => void
403
+ },
404
+ ): Promise<Message[]> {
405
+ try {
406
+ const command = getCommand(commandName, context.options.commands)
407
+ switch (command.type) {
408
+ case 'local-jsx': {
409
+ return new Promise(resolve => {
410
+ command
411
+ .call(r => {
412
+ setToolJSX(null)
413
+ resolve([
414
+ createUserMessage(`<command-name>${command.userFacingName()}</command-name>
415
+ <command-message>${command.userFacingName()}</command-message>
416
+ <command-args>${args}</command-args>`),
417
+ r
418
+ ? createAssistantMessage(r)
419
+ : createAssistantMessage(NO_RESPONSE_REQUESTED),
420
+ ])
421
+ }, context)
422
+ .then(jsx => {
423
+ setToolJSX({
424
+ jsx,
425
+ shouldHidePromptInput: true,
426
+ })
427
+ })
428
+ })
429
+ }
430
+ case 'local': {
431
+ const userMessage =
432
+ createUserMessage(`<command-name>${command.userFacingName()}</command-name>
433
+ <command-message>${command.userFacingName()}</command-message>
434
+ <command-args>${args}</command-args>`)
435
+
436
+ try {
437
+ // Use the context's abortController for local commands
438
+ const result = await command.call(args, context)
439
+
440
+ return [
441
+ userMessage,
442
+ createAssistantMessage(
443
+ `<local-command-stdout>${result}</local-command-stdout>`,
444
+ ),
445
+ ]
446
+ } catch (e) {
447
+ logError(e)
448
+ return [
449
+ userMessage,
450
+ createAssistantMessage(
451
+ `<local-command-stderr>${String(e)}</local-command-stderr>`,
452
+ ),
453
+ ]
454
+ }
455
+ }
456
+ case 'prompt': {
457
+ // For custom commands, process them naturally instead of wrapping in command-contents
458
+ const prompt = await command.getPromptForCommand(args)
459
+ return prompt.map(msg => {
460
+ // Create a normal user message from the custom command content
461
+ const userMessage = createUserMessage(
462
+ typeof msg.content === 'string'
463
+ ? msg.content
464
+ : msg.content
465
+ .map(block => (block.type === 'text' ? block.text : ''))
466
+ .join('\n'),
467
+ )
468
+
469
+ // Add metadata for tracking but don't wrap in special tags
470
+ userMessage.options = {
471
+ ...userMessage.options,
472
+ isCustomCommand: true,
473
+ commandName: command.userFacingName(),
474
+ commandArgs: args,
475
+ }
476
+
477
+ return userMessage
478
+ })
479
+ }
480
+ }
481
+ } catch (e) {
482
+ if (e instanceof MalformedCommandError) {
483
+ return [createAssistantMessage(e.message)]
484
+ }
485
+ throw e
486
+ }
487
+ }
488
+
489
+ export function extractTagFromMessage(
490
+ message: Message,
491
+ tagName: string,
492
+ ): string | null {
493
+ if (message.type === 'progress') {
494
+ return null
495
+ }
496
+ if (typeof message.message.content !== 'string') {
497
+ return null
498
+ }
499
+ return extractTag(message.message.content, tagName)
500
+ }
501
+
502
+ export function extractTag(html: string, tagName: string): string | null {
503
+ if (!html.trim() || !tagName.trim()) {
504
+ return null
505
+ }
506
+
507
+ // Escape special characters in the tag name
508
+ const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
509
+
510
+ // Create regex pattern that handles:
511
+ // 1. Self-closing tags
512
+ // 2. Tags with attributes
513
+ // 3. Nested tags of the same type
514
+ // 4. Multiline content
515
+ const pattern = new RegExp(
516
+ `<${escapedTag}(?:\\s+[^>]*)?>` + // Opening tag with optional attributes
517
+ '([\\s\\S]*?)' + // Content (non-greedy match)
518
+ `<\\/${escapedTag}>`, // Closing tag
519
+ 'gi',
520
+ )
521
+
522
+ let match
523
+ let depth = 0
524
+ let lastIndex = 0
525
+ const openingTag = new RegExp(`<${escapedTag}(?:\\s+[^>]*?)?>`, 'gi')
526
+ const closingTag = new RegExp(`<\\/${escapedTag}>`, 'gi')
527
+
528
+ while ((match = pattern.exec(html)) !== null) {
529
+ // Check for nested tags
530
+ const content = match[1]
531
+ const beforeMatch = html.slice(lastIndex, match.index)
532
+
533
+ // Reset depth counter
534
+ depth = 0
535
+
536
+ // Count opening tags before this match
537
+ openingTag.lastIndex = 0
538
+ while (openingTag.exec(beforeMatch) !== null) {
539
+ depth++
540
+ }
541
+
542
+ // Count closing tags before this match
543
+ closingTag.lastIndex = 0
544
+ while (closingTag.exec(beforeMatch) !== null) {
545
+ depth--
546
+ }
547
+
548
+ // Only include content if we're at the correct nesting level
549
+ if (depth === 0 && content) {
550
+ return content
551
+ }
552
+
553
+ lastIndex = match.index + match[0].length
554
+ }
555
+
556
+ return null
557
+ }
558
+
559
+ export function isNotEmptyMessage(message: Message): boolean {
560
+ if (message.type === 'progress') {
561
+ return true
562
+ }
563
+
564
+ if (typeof message.message.content === 'string') {
565
+ return message.message.content.trim().length > 0
566
+ }
567
+
568
+ if (message.message.content.length === 0) {
569
+ return false
570
+ }
571
+
572
+ // Skip multi-block messages for now
573
+ if (message.message.content.length > 1) {
574
+ return true
575
+ }
576
+
577
+ if (message.message.content[0]!.type !== 'text') {
578
+ return true
579
+ }
580
+
581
+ return (
582
+ message.message.content[0]!.text.trim().length > 0 &&
583
+ message.message.content[0]!.text !== NO_CONTENT_MESSAGE &&
584
+ message.message.content[0]!.text !== INTERRUPT_MESSAGE_FOR_TOOL_USE
585
+ )
586
+ }
587
+
588
+ // TODO: replace this with plain UserMessage if/when PR #405 lands
589
+ type NormalizedUserMessage = {
590
+ message: {
591
+ content: [
592
+ | TextBlockParam
593
+ | ImageBlockParam
594
+ | ToolUseBlockParam
595
+ | ToolResultBlockParam,
596
+ ]
597
+ role: 'user'
598
+ }
599
+ type: 'user'
600
+ uuid: UUID
601
+ }
602
+
603
+ export type NormalizedMessage =
604
+ | NormalizedUserMessage
605
+ | AssistantMessage
606
+ | ProgressMessage
607
+
608
+ // Split messages, so each content block gets its own message
609
+ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
610
+ return messages.flatMap(message => {
611
+ if (message.type === 'progress') {
612
+ return [message] as NormalizedMessage[]
613
+ }
614
+ if (typeof message.message.content === 'string') {
615
+ return [message] as NormalizedMessage[]
616
+ }
617
+ return message.message.content.map(_ => {
618
+ switch (message.type) {
619
+ case 'assistant':
620
+ return {
621
+ type: 'assistant',
622
+ uuid: randomUUID(),
623
+ message: {
624
+ ...message.message,
625
+ content: [_],
626
+ },
627
+ costUSD:
628
+ (message as AssistantMessage).costUSD /
629
+ message.message.content.length,
630
+ durationMs: (message as AssistantMessage).durationMs,
631
+ } as NormalizedMessage
632
+ case 'user':
633
+ // It seems like the line below was a no-op before, but I'm not sure.
634
+ // To check, we could throw an error if any of the following are true:
635
+ // - message `role` does isn't `user` -- this possibility is allowed by MCP tools,
636
+ // though isn't supposed to happen in practice (we should fix this)
637
+ // - message `content` is not an array -- this one is more concerning because it's
638
+ // not allowed by the `NormalizedUserMessage` type, but if it's happening that was
639
+ // probably a bug before.
640
+ // Maybe I'm missing something? -(ab)
641
+ // return createUserMessage([_]) as NormalizedMessage
642
+ return message as NormalizedUserMessage
643
+ }
644
+ })
645
+ })
646
+ }
647
+
648
+ type ToolUseRequestMessage = AssistantMessage & {
649
+ message: { content: ToolUseBlock[] }
650
+ }
651
+
652
+ function isToolUseRequestMessage(
653
+ message: Message,
654
+ ): message is ToolUseRequestMessage {
655
+ return (
656
+ message.type === 'assistant' &&
657
+ 'costUSD' in message &&
658
+ // Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly
659
+ message.message.content.some(_ => _.type === 'tool_use')
660
+ )
661
+ }
662
+
663
+ // Re-order, to move result messages to be after their tool use messages
664
+ export function reorderMessages(
665
+ messages: NormalizedMessage[],
666
+ ): NormalizedMessage[] {
667
+ const ms: NormalizedMessage[] = []
668
+ const toolUseMessages: ToolUseRequestMessage[] = []
669
+
670
+ for (const message of messages) {
671
+ // track tool use messages we've seen
672
+ if (isToolUseRequestMessage(message)) {
673
+ toolUseMessages.push(message)
674
+ }
675
+
676
+ // if it's a tool progress message...
677
+ if (message.type === 'progress') {
678
+ // replace any existing progress messages with this one
679
+ const existingProgressMessage = ms.find(
680
+ _ => _.type === 'progress' && _.toolUseID === message.toolUseID,
681
+ )
682
+ if (existingProgressMessage) {
683
+ ms[ms.indexOf(existingProgressMessage)] = message
684
+ continue
685
+ }
686
+ // otherwise, insert it after its tool use
687
+ const toolUseMessage = toolUseMessages.find(
688
+ _ => _.message.content[0]?.id === message.toolUseID,
689
+ )
690
+ if (toolUseMessage) {
691
+ ms.splice(ms.indexOf(toolUseMessage) + 1, 0, message)
692
+ continue
693
+ }
694
+ }
695
+
696
+ // if it's a tool result, insert it after its tool use and progress messages
697
+ if (
698
+ message.type === 'user' &&
699
+ Array.isArray(message.message.content) &&
700
+ message.message.content[0]?.type === 'tool_result'
701
+ ) {
702
+ const toolUseID = (message.message.content[0] as ToolResultBlockParam)
703
+ ?.tool_use_id
704
+
705
+ // First check for progress messages
706
+ const lastProgressMessage = ms.find(
707
+ _ => _.type === 'progress' && _.toolUseID === toolUseID,
708
+ )
709
+ if (lastProgressMessage) {
710
+ ms.splice(ms.indexOf(lastProgressMessage) + 1, 0, message)
711
+ continue
712
+ }
713
+
714
+ // If no progress messages, check for tool use messages
715
+ const toolUseMessage = toolUseMessages.find(
716
+ _ => _.message.content[0]?.id === toolUseID,
717
+ )
718
+ if (toolUseMessage) {
719
+ ms.splice(ms.indexOf(toolUseMessage) + 1, 0, message)
720
+ continue
721
+ }
722
+ }
723
+
724
+ // otherwise, just add it to the list
725
+ else {
726
+ ms.push(message)
727
+ }
728
+ }
729
+
730
+ return ms
731
+ }
732
+
733
+ const getToolResultIDs = memoize(
734
+ (normalizedMessages: NormalizedMessage[]): { [toolUseID: string]: boolean } =>
735
+ Object.fromEntries(
736
+ normalizedMessages.flatMap(_ =>
737
+ _.type === 'user' && _.message.content[0]?.type === 'tool_result'
738
+ ? [
739
+ [
740
+ _.message.content[0]!.tool_use_id,
741
+ _.message.content[0]!.is_error ?? false,
742
+ ],
743
+ ]
744
+ : ([] as [string, boolean][]),
745
+ ),
746
+ ),
747
+ )
748
+
749
+ export function getUnresolvedToolUseIDs(
750
+ normalizedMessages: NormalizedMessage[],
751
+ ): Set<string> {
752
+ const toolResults = getToolResultIDs(normalizedMessages)
753
+ return new Set(
754
+ normalizedMessages
755
+ .filter(
756
+ (
757
+ _,
758
+ ): _ is AssistantMessage & {
759
+ message: { content: [ToolUseBlockParam] }
760
+ } =>
761
+ _.type === 'assistant' &&
762
+ Array.isArray(_.message.content) &&
763
+ _.message.content[0]?.type === 'tool_use' &&
764
+ !(_.message.content[0]?.id in toolResults),
765
+ )
766
+ .map(_ => _.message.content[0].id),
767
+ )
768
+ }
769
+
770
+ /**
771
+ * Tool uses are in flight if either:
772
+ * 1. They have a corresponding progress message and no result message
773
+ * 2. They are the first unresoved tool use
774
+ *
775
+ * TODO: Find a way to harden this logic to make it more explicit
776
+ */
777
+ export function getInProgressToolUseIDs(
778
+ normalizedMessages: NormalizedMessage[],
779
+ ): Set<string> {
780
+ const unresolvedToolUseIDs = getUnresolvedToolUseIDs(normalizedMessages)
781
+ const toolUseIDsThatHaveProgressMessages = new Set(
782
+ normalizedMessages.filter(_ => _.type === 'progress').map(_ => _.toolUseID),
783
+ )
784
+ return new Set(
785
+ (
786
+ normalizedMessages.filter(_ => {
787
+ if (_.type !== 'assistant') {
788
+ return false
789
+ }
790
+ if (_.message.content[0]?.type !== 'tool_use') {
791
+ return false
792
+ }
793
+ const toolUseID = _.message.content[0].id
794
+ if (toolUseID === unresolvedToolUseIDs.values().next().value) {
795
+ return true
796
+ }
797
+
798
+ if (
799
+ toolUseIDsThatHaveProgressMessages.has(toolUseID) &&
800
+ unresolvedToolUseIDs.has(toolUseID)
801
+ ) {
802
+ return true
803
+ }
804
+
805
+ return false
806
+ }) as AssistantMessage[]
807
+ ).map(_ => (_.message.content[0]! as ToolUseBlockParam).id),
808
+ )
809
+ }
810
+
811
+ export function getErroredToolUseMessages(
812
+ normalizedMessages: NormalizedMessage[],
813
+ ): AssistantMessage[] {
814
+ const toolResults = getToolResultIDs(normalizedMessages)
815
+ return normalizedMessages.filter(
816
+ _ =>
817
+ _.type === 'assistant' &&
818
+ Array.isArray(_.message.content) &&
819
+ _.message.content[0]?.type === 'tool_use' &&
820
+ _.message.content[0]?.id in toolResults &&
821
+ toolResults[_.message.content[0]?.id],
822
+ ) as AssistantMessage[]
823
+ }
824
+
825
+ export function normalizeMessagesForAPI(
826
+ messages: Message[],
827
+ ): (UserMessage | AssistantMessage)[] {
828
+ const result: (UserMessage | AssistantMessage)[] = []
829
+ messages
830
+ .filter(_ => _.type !== 'progress')
831
+ .forEach(message => {
832
+ switch (message.type) {
833
+ case 'user': {
834
+ // If the current message is not a tool result, add it to the result
835
+ if (
836
+ !Array.isArray(message.message.content) ||
837
+ message.message.content[0]?.type !== 'tool_result'
838
+ ) {
839
+ result.push(message)
840
+ return
841
+ }
842
+
843
+ // If the last message is not a tool result, add it to the result
844
+ const lastMessage = last(result)
845
+ if (
846
+ !lastMessage ||
847
+ lastMessage?.type === 'assistant' ||
848
+ !Array.isArray(lastMessage.message.content) ||
849
+ lastMessage.message.content[0]?.type !== 'tool_result'
850
+ ) {
851
+ result.push(message)
852
+ return
853
+ }
854
+
855
+ // Otherwise, merge the current message with the last message
856
+ result[result.indexOf(lastMessage)] = {
857
+ ...lastMessage,
858
+ message: {
859
+ ...lastMessage.message,
860
+ content: [
861
+ ...lastMessage.message.content,
862
+ ...message.message.content,
863
+ ],
864
+ },
865
+ }
866
+ return
867
+ }
868
+ case 'assistant':
869
+ result.push(message)
870
+ return
871
+ }
872
+ })
873
+ return result
874
+ }
875
+
876
+ // Sometimes the API returns empty messages (eg. "\n\n"). We need to filter these out,
877
+ // otherwise they will give an API error when we send them to the API next time we call query().
878
+ export function normalizeContentFromAPI(
879
+ content: APIMessage['content'],
880
+ ): APIMessage['content'] {
881
+ const filteredContent = content.filter(
882
+ _ => _.type !== 'text' || _.text.trim().length > 0,
883
+ )
884
+
885
+ if (filteredContent.length === 0) {
886
+ return [{ type: 'text', text: NO_CONTENT_MESSAGE, citations: [] }]
887
+ }
888
+
889
+ return filteredContent
890
+ }
891
+
892
+ export function isEmptyMessageText(text: string): boolean {
893
+ return (
894
+ stripSystemMessages(text).trim() === '' ||
895
+ text.trim() === NO_CONTENT_MESSAGE
896
+ )
897
+ }
898
+ const STRIPPED_TAGS = [
899
+ 'commit_analysis',
900
+ 'context',
901
+ 'function_analysis',
902
+ 'pr_analysis',
903
+ ]
904
+
905
+ export function stripSystemMessages(content: string): string {
906
+ const regex = new RegExp(`<(${STRIPPED_TAGS.join('|')})>.*?</\\1>\n?`, 'gs')
907
+ return content.replace(regex, '').trim()
908
+ }
909
+
910
+ export function getToolUseID(message: NormalizedMessage): string | null {
911
+ switch (message.type) {
912
+ case 'assistant':
913
+ if (message.message.content[0]?.type !== 'tool_use') {
914
+ return null
915
+ }
916
+ return message.message.content[0].id
917
+ case 'user':
918
+ if (message.message.content[0]?.type !== 'tool_result') {
919
+ return null
920
+ }
921
+ return message.message.content[0].tool_use_id
922
+ case 'progress':
923
+ return message.toolUseID
924
+ }
925
+ }
926
+
927
+ export function getLastAssistantMessageId(
928
+ messages: Message[],
929
+ ): string | undefined {
930
+ // Iterate from the end of the array to find the last assistant message
931
+ for (let i = messages.length - 1; i >= 0; i--) {
932
+ const message = messages[i]
933
+ if (message && message.type === 'assistant') {
934
+ return message.message.id
935
+ }
936
+ }
937
+ return undefined
938
+ }