@renxqoo/renx-code 0.0.2 → 0.0.4

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 (298) hide show
  1. package/README.md +59 -223
  2. package/bin/renx.cjs +34 -0
  3. package/package.json +27 -83
  4. package/src/App.tsx +297 -0
  5. package/src/agent/runtime/event-format.ts +258 -0
  6. package/src/agent/runtime/model-types.ts +13 -0
  7. package/src/agent/runtime/runtime.context-usage.test.ts +193 -0
  8. package/src/agent/runtime/runtime.error-handling.test.ts +236 -0
  9. package/src/agent/runtime/runtime.simple.test.ts +16 -0
  10. package/src/agent/runtime/runtime.test.ts +293 -0
  11. package/src/agent/runtime/runtime.ts +881 -0
  12. package/src/agent/runtime/runtime.usage-forwarding.test.ts +229 -0
  13. package/src/agent/runtime/source-modules.test.ts +57 -0
  14. package/src/agent/runtime/source-modules.ts +353 -0
  15. package/src/agent/runtime/tool-call-buffer.test.ts +65 -0
  16. package/src/agent/runtime/tool-call-buffer.ts +60 -0
  17. package/src/agent/runtime/tool-confirmation.test.ts +56 -0
  18. package/src/agent/runtime/tool-confirmation.ts +15 -0
  19. package/src/agent/runtime/types.ts +99 -0
  20. package/src/commands/slash-commands.test.ts +216 -0
  21. package/src/commands/slash-commands.ts +64 -0
  22. package/src/components/chat/assistant-reply.test.tsx +47 -0
  23. package/src/components/chat/assistant-reply.tsx +136 -0
  24. package/src/components/chat/assistant-segment.test.ts +99 -0
  25. package/src/components/chat/assistant-segment.tsx +125 -0
  26. package/src/components/chat/assistant-tool-group.tsx +900 -0
  27. package/src/components/chat/code-block.test.tsx +206 -0
  28. package/src/components/chat/code-block.tsx +313 -0
  29. package/src/components/chat/prompt-card.tsx +81 -0
  30. package/src/components/chat/segment-groups.test.ts +52 -0
  31. package/src/components/chat/segment-groups.ts +106 -0
  32. package/src/components/chat/turn-item.tsx +39 -0
  33. package/src/components/conversation-panel.tsx +43 -0
  34. package/src/components/file-mention-menu.tsx +77 -0
  35. package/src/components/file-picker-dialog.tsx +206 -0
  36. package/src/components/footer-hints.tsx +75 -0
  37. package/src/components/model-picker-dialog.tsx +248 -0
  38. package/src/components/prompt.tsx +233 -0
  39. package/src/components/slash-command-menu.tsx +65 -0
  40. package/src/components/tool-confirm-dialog-content.test.ts +103 -0
  41. package/src/components/tool-confirm-dialog-content.ts +186 -0
  42. package/src/components/tool-confirm-dialog.tsx +187 -0
  43. package/src/components/tool-display-config.ts +119 -0
  44. package/src/context-usage-regressions.test.ts +26 -0
  45. package/src/files/attachment-capabilities.test.ts +30 -0
  46. package/src/files/attachment-capabilities.ts +50 -0
  47. package/src/files/attachment-content.ts +153 -0
  48. package/src/files/file-mention-query.test.ts +34 -0
  49. package/src/files/file-mention-query.ts +32 -0
  50. package/src/files/prompt-display.ts +13 -0
  51. package/src/files/types.ts +5 -0
  52. package/src/files/workspace-files.ts +63 -0
  53. package/src/hooks/agent-event-handlers.test.ts +207 -0
  54. package/src/hooks/agent-event-handlers.ts +196 -0
  55. package/src/hooks/chat-local-replies.fixed.test.ts +119 -0
  56. package/src/hooks/chat-local-replies.test.ts +153 -0
  57. package/src/hooks/chat-local-replies.ts +63 -0
  58. package/src/hooks/turn-updater.test.ts +70 -0
  59. package/src/hooks/turn-updater.ts +166 -0
  60. package/src/hooks/use-agent-chat.context.test.ts +10 -0
  61. package/src/hooks/use-agent-chat.status.test.ts +14 -0
  62. package/src/hooks/use-agent-chat.test.ts +80 -0
  63. package/src/hooks/use-agent-chat.ts +621 -0
  64. package/src/hooks/use-file-mention-menu.ts +196 -0
  65. package/src/hooks/use-file-picker.ts +185 -0
  66. package/src/hooks/use-model-picker.ts +196 -0
  67. package/src/hooks/use-slash-command-menu.ts +154 -0
  68. package/src/index.tsx +55 -0
  69. package/src/runtime/clipboard.test.ts +43 -0
  70. package/src/runtime/clipboard.ts +89 -0
  71. package/src/runtime/exit.test.ts +177 -0
  72. package/src/runtime/exit.ts +98 -0
  73. package/src/runtime/runtime-support.test.ts +31 -0
  74. package/src/runtime/terminal-theme.test.ts +55 -0
  75. package/src/runtime/terminal-theme.ts +196 -0
  76. package/src/types/chat.ts +32 -0
  77. package/src/types/message-content.ts +48 -0
  78. package/src/ui/open-code-theme.ts +176 -0
  79. package/src/ui/opencode-markdown.ts +211 -0
  80. package/src/ui/theme.simple.test.ts +52 -0
  81. package/src/ui/theme.test.ts +151 -0
  82. package/src/ui/theme.ts +152 -0
  83. package/src/utils/time.test.ts +144 -0
  84. package/src/utils/time.ts +7 -0
  85. package/tsconfig.json +30 -0
  86. package/LICENSE +0 -21
  87. package/dist/App.d.ts +0 -2
  88. package/dist/App.d.ts.map +0 -1
  89. package/dist/App.js +0 -170
  90. package/dist/App.js.map +0 -1
  91. package/dist/agent/prompts/system.d.ts +0 -24
  92. package/dist/agent/prompts/system.d.ts.map +0 -1
  93. package/dist/agent/prompts/system.js +0 -222
  94. package/dist/agent/prompts/system.js.map +0 -1
  95. package/dist/agent/runtime/event-format.d.ts +0 -17
  96. package/dist/agent/runtime/event-format.d.ts.map +0 -1
  97. package/dist/agent/runtime/event-format.js +0 -194
  98. package/dist/agent/runtime/event-format.js.map +0 -1
  99. package/dist/agent/runtime/model-types.d.ts +0 -13
  100. package/dist/agent/runtime/model-types.d.ts.map +0 -1
  101. package/dist/agent/runtime/model-types.js +0 -1
  102. package/dist/agent/runtime/model-types.js.map +0 -1
  103. package/dist/agent/runtime/runtime.d.ts +0 -16
  104. package/dist/agent/runtime/runtime.d.ts.map +0 -1
  105. package/dist/agent/runtime/runtime.js +0 -691
  106. package/dist/agent/runtime/runtime.js.map +0 -1
  107. package/dist/agent/runtime/source-modules.d.ts +0 -176
  108. package/dist/agent/runtime/source-modules.d.ts.map +0 -1
  109. package/dist/agent/runtime/source-modules.js +0 -110
  110. package/dist/agent/runtime/source-modules.js.map +0 -1
  111. package/dist/agent/runtime/tool-call-buffer.d.ts +0 -12
  112. package/dist/agent/runtime/tool-call-buffer.d.ts.map +0 -1
  113. package/dist/agent/runtime/tool-call-buffer.js +0 -48
  114. package/dist/agent/runtime/tool-call-buffer.js.map +0 -1
  115. package/dist/agent/runtime/tool-confirmation.d.ts +0 -3
  116. package/dist/agent/runtime/tool-confirmation.d.ts.map +0 -1
  117. package/dist/agent/runtime/tool-confirmation.js +0 -9
  118. package/dist/agent/runtime/tool-confirmation.js.map +0 -1
  119. package/dist/agent/runtime/types.d.ts +0 -86
  120. package/dist/agent/runtime/types.d.ts.map +0 -1
  121. package/dist/agent/runtime/types.js +0 -1
  122. package/dist/agent/runtime/types.js.map +0 -1
  123. package/dist/cli.d.ts +0 -3
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -43
  126. package/dist/cli.js.map +0 -1
  127. package/dist/commands/slash-commands.d.ts +0 -11
  128. package/dist/commands/slash-commands.d.ts.map +0 -1
  129. package/dist/commands/slash-commands.js +0 -48
  130. package/dist/commands/slash-commands.js.map +0 -1
  131. package/dist/components/chat/assistant-reply.d.ts +0 -13
  132. package/dist/components/chat/assistant-reply.d.ts.map +0 -1
  133. package/dist/components/chat/assistant-reply.js +0 -78
  134. package/dist/components/chat/assistant-reply.js.map +0 -1
  135. package/dist/components/chat/assistant-segment.d.ts +0 -8
  136. package/dist/components/chat/assistant-segment.d.ts.map +0 -1
  137. package/dist/components/chat/assistant-segment.js +0 -54
  138. package/dist/components/chat/assistant-segment.js.map +0 -1
  139. package/dist/components/chat/assistant-tool-group.d.ts +0 -7
  140. package/dist/components/chat/assistant-tool-group.d.ts.map +0 -1
  141. package/dist/components/chat/assistant-tool-group.js +0 -695
  142. package/dist/components/chat/assistant-tool-group.js.map +0 -1
  143. package/dist/components/chat/code-block.d.ts +0 -16
  144. package/dist/components/chat/code-block.d.ts.map +0 -1
  145. package/dist/components/chat/code-block.js +0 -194
  146. package/dist/components/chat/code-block.js.map +0 -1
  147. package/dist/components/chat/prompt-card.d.ts +0 -9
  148. package/dist/components/chat/prompt-card.d.ts.map +0 -1
  149. package/dist/components/chat/prompt-card.js +0 -18
  150. package/dist/components/chat/prompt-card.js.map +0 -1
  151. package/dist/components/chat/segment-groups.d.ts +0 -24
  152. package/dist/components/chat/segment-groups.d.ts.map +0 -1
  153. package/dist/components/chat/segment-groups.js +0 -69
  154. package/dist/components/chat/segment-groups.js.map +0 -1
  155. package/dist/components/chat/turn-item.d.ts +0 -9
  156. package/dist/components/chat/turn-item.d.ts.map +0 -1
  157. package/dist/components/chat/turn-item.js +0 -11
  158. package/dist/components/chat/turn-item.js.map +0 -1
  159. package/dist/components/conversation-panel.d.ts +0 -8
  160. package/dist/components/conversation-panel.d.ts.map +0 -1
  161. package/dist/components/conversation-panel.js +0 -8
  162. package/dist/components/conversation-panel.js.map +0 -1
  163. package/dist/components/file-mention-menu.d.ts +0 -11
  164. package/dist/components/file-mention-menu.d.ts.map +0 -1
  165. package/dist/components/file-mention-menu.js +0 -15
  166. package/dist/components/file-mention-menu.js.map +0 -1
  167. package/dist/components/file-picker-dialog.d.ts +0 -21
  168. package/dist/components/file-picker-dialog.d.ts.map +0 -1
  169. package/dist/components/file-picker-dialog.js +0 -48
  170. package/dist/components/file-picker-dialog.js.map +0 -1
  171. package/dist/components/footer-hints.d.ts +0 -7
  172. package/dist/components/footer-hints.d.ts.map +0 -1
  173. package/dist/components/footer-hints.js +0 -29
  174. package/dist/components/footer-hints.js.map +0 -1
  175. package/dist/components/model-picker-dialog.d.ts +0 -20
  176. package/dist/components/model-picker-dialog.d.ts.map +0 -1
  177. package/dist/components/model-picker-dialog.js +0 -72
  178. package/dist/components/model-picker-dialog.js.map +0 -1
  179. package/dist/components/prompt.d.ts +0 -18
  180. package/dist/components/prompt.d.ts.map +0 -1
  181. package/dist/components/prompt.js +0 -96
  182. package/dist/components/prompt.js.map +0 -1
  183. package/dist/components/slash-command-menu.d.ts +0 -9
  184. package/dist/components/slash-command-menu.d.ts.map +0 -1
  185. package/dist/components/slash-command-menu.js +0 -20
  186. package/dist/components/slash-command-menu.js.map +0 -1
  187. package/dist/components/tool-confirm-dialog-content.d.ts +0 -15
  188. package/dist/components/tool-confirm-dialog-content.d.ts.map +0 -1
  189. package/dist/components/tool-confirm-dialog-content.js +0 -143
  190. package/dist/components/tool-confirm-dialog-content.js.map +0 -1
  191. package/dist/components/tool-confirm-dialog.d.ts +0 -12
  192. package/dist/components/tool-confirm-dialog.d.ts.map +0 -1
  193. package/dist/components/tool-confirm-dialog.js +0 -21
  194. package/dist/components/tool-confirm-dialog.js.map +0 -1
  195. package/dist/components/tool-display-config.d.ts +0 -11
  196. package/dist/components/tool-display-config.d.ts.map +0 -1
  197. package/dist/components/tool-display-config.js +0 -94
  198. package/dist/components/tool-display-config.js.map +0 -1
  199. package/dist/config/paths.d.ts +0 -7
  200. package/dist/config/paths.d.ts.map +0 -1
  201. package/dist/config/paths.js +0 -24
  202. package/dist/config/paths.js.map +0 -1
  203. package/dist/files/attachment-capabilities.d.ts +0 -19
  204. package/dist/files/attachment-capabilities.d.ts.map +0 -1
  205. package/dist/files/attachment-capabilities.js +0 -26
  206. package/dist/files/attachment-capabilities.js.map +0 -1
  207. package/dist/files/attachment-content.d.ts +0 -5
  208. package/dist/files/attachment-content.d.ts.map +0 -1
  209. package/dist/files/attachment-content.js +0 -117
  210. package/dist/files/attachment-content.js.map +0 -1
  211. package/dist/files/file-mention-query.d.ts +0 -9
  212. package/dist/files/file-mention-query.d.ts.map +0 -1
  213. package/dist/files/file-mention-query.js +0 -23
  214. package/dist/files/file-mention-query.js.map +0 -1
  215. package/dist/files/prompt-display.d.ts +0 -3
  216. package/dist/files/prompt-display.d.ts.map +0 -1
  217. package/dist/files/prompt-display.js +0 -11
  218. package/dist/files/prompt-display.js.map +0 -1
  219. package/dist/files/types.d.ts +0 -6
  220. package/dist/files/types.d.ts.map +0 -1
  221. package/dist/files/types.js +0 -1
  222. package/dist/files/types.js.map +0 -1
  223. package/dist/files/workspace-files.d.ts +0 -3
  224. package/dist/files/workspace-files.d.ts.map +0 -1
  225. package/dist/files/workspace-files.js +0 -50
  226. package/dist/files/workspace-files.js.map +0 -1
  227. package/dist/hooks/agent-event-handlers.d.ts +0 -11
  228. package/dist/hooks/agent-event-handlers.d.ts.map +0 -1
  229. package/dist/hooks/agent-event-handlers.js +0 -137
  230. package/dist/hooks/agent-event-handlers.js.map +0 -1
  231. package/dist/hooks/chat-local-replies.d.ts +0 -9
  232. package/dist/hooks/chat-local-replies.d.ts.map +0 -1
  233. package/dist/hooks/chat-local-replies.js +0 -54
  234. package/dist/hooks/chat-local-replies.js.map +0 -1
  235. package/dist/hooks/turn-updater.d.ts +0 -9
  236. package/dist/hooks/turn-updater.d.ts.map +0 -1
  237. package/dist/hooks/turn-updater.js +0 -103
  238. package/dist/hooks/turn-updater.js.map +0 -1
  239. package/dist/hooks/use-agent-chat.d.ts +0 -29
  240. package/dist/hooks/use-agent-chat.d.ts.map +0 -1
  241. package/dist/hooks/use-agent-chat.js +0 -455
  242. package/dist/hooks/use-agent-chat.js.map +0 -1
  243. package/dist/hooks/use-file-mention-menu.d.ts +0 -22
  244. package/dist/hooks/use-file-mention-menu.d.ts.map +0 -1
  245. package/dist/hooks/use-file-mention-menu.js +0 -137
  246. package/dist/hooks/use-file-mention-menu.js.map +0 -1
  247. package/dist/hooks/use-file-picker.d.ts +0 -21
  248. package/dist/hooks/use-file-picker.d.ts.map +0 -1
  249. package/dist/hooks/use-file-picker.js +0 -145
  250. package/dist/hooks/use-file-picker.js.map +0 -1
  251. package/dist/hooks/use-model-picker.d.ts +0 -23
  252. package/dist/hooks/use-model-picker.d.ts.map +0 -1
  253. package/dist/hooks/use-model-picker.js +0 -151
  254. package/dist/hooks/use-model-picker.js.map +0 -1
  255. package/dist/hooks/use-slash-command-menu.d.ts +0 -19
  256. package/dist/hooks/use-slash-command-menu.d.ts.map +0 -1
  257. package/dist/hooks/use-slash-command-menu.js +0 -101
  258. package/dist/hooks/use-slash-command-menu.js.map +0 -1
  259. package/dist/index.d.ts +0 -2
  260. package/dist/index.d.ts.map +0 -1
  261. package/dist/index.js +0 -39
  262. package/dist/index.js.map +0 -1
  263. package/dist/runtime/clipboard.d.ts +0 -10
  264. package/dist/runtime/clipboard.d.ts.map +0 -1
  265. package/dist/runtime/clipboard.js +0 -64
  266. package/dist/runtime/clipboard.js.map +0 -1
  267. package/dist/runtime/exit.d.ts +0 -7
  268. package/dist/runtime/exit.d.ts.map +0 -1
  269. package/dist/runtime/exit.js +0 -85
  270. package/dist/runtime/exit.js.map +0 -1
  271. package/dist/runtime/terminal-theme.d.ts +0 -25
  272. package/dist/runtime/terminal-theme.d.ts.map +0 -1
  273. package/dist/runtime/terminal-theme.js +0 -148
  274. package/dist/runtime/terminal-theme.js.map +0 -1
  275. package/dist/types/chat.d.ts +0 -29
  276. package/dist/types/chat.d.ts.map +0 -1
  277. package/dist/types/chat.js +0 -1
  278. package/dist/types/chat.js.map +0 -1
  279. package/dist/types/message-content.d.ts +0 -38
  280. package/dist/types/message-content.d.ts.map +0 -1
  281. package/dist/types/message-content.js +0 -1
  282. package/dist/types/message-content.js.map +0 -1
  283. package/dist/ui/open-code-theme.d.ts +0 -58
  284. package/dist/ui/open-code-theme.d.ts.map +0 -1
  285. package/dist/ui/open-code-theme.js +0 -113
  286. package/dist/ui/open-code-theme.js.map +0 -1
  287. package/dist/ui/opencode-markdown.d.ts +0 -7
  288. package/dist/ui/opencode-markdown.d.ts.map +0 -1
  289. package/dist/ui/opencode-markdown.js +0 -169
  290. package/dist/ui/opencode-markdown.js.map +0 -1
  291. package/dist/ui/theme.d.ts +0 -68
  292. package/dist/ui/theme.d.ts.map +0 -1
  293. package/dist/ui/theme.js +0 -80
  294. package/dist/ui/theme.js.map +0 -1
  295. package/dist/utils/time.d.ts +0 -2
  296. package/dist/utils/time.d.ts.map +0 -1
  297. package/dist/utils/time.js +0 -7
  298. package/dist/utils/time.js.map +0 -1
@@ -0,0 +1,153 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+
4
+ import type { InputContentPart, MessageContent } from '../types/message-content';
5
+ import type { PromptFileSelection } from './types';
6
+ import {
7
+ type AttachmentModelCapabilities,
8
+ isAudioSelection,
9
+ isImageSelection,
10
+ isVideoSelection,
11
+ } from './attachment-capabilities';
12
+
13
+ const IMAGE_MIME_BY_EXTENSION: Record<string, string> = {
14
+ '.gif': 'image/gif',
15
+ '.jpeg': 'image/jpeg',
16
+ '.jpg': 'image/jpeg',
17
+ '.png': 'image/png',
18
+ '.webp': 'image/webp',
19
+ };
20
+
21
+ const VIDEO_MIME_BY_EXTENSION: Record<string, string> = {
22
+ '.avi': 'video/x-msvideo',
23
+ '.m4v': 'video/mp4',
24
+ '.mkv': 'video/x-matroska',
25
+ '.mov': 'video/quicktime',
26
+ '.mp4': 'video/mp4',
27
+ '.webm': 'video/webm',
28
+ };
29
+
30
+ const AUDIO_MIME_BY_EXTENSION: Record<string, string> = {
31
+ '.aac': 'audio/aac',
32
+ '.flac': 'audio/flac',
33
+ '.m4a': 'audio/mp4',
34
+ '.mp3': 'audio/mpeg',
35
+ '.ogg': 'audio/ogg',
36
+ '.wav': 'audio/wav',
37
+ };
38
+
39
+ const FALLBACK_FILE_MIME = 'application/octet-stream';
40
+ const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
41
+ const MAX_TEXT_ATTACHMENT_CHARS = 80_000;
42
+ const TEXT_DECODER = new TextDecoder('utf-8', { fatal: false });
43
+
44
+ const formatFileFence = (path: string, content: string, truncated: boolean) => {
45
+ const suffix = truncated ? '\n\n[truncated for prompt size]' : '';
46
+ return `Attached file: ${path}\n\n\`\`\`\n${content}\n\`\`\`${suffix}`;
47
+ };
48
+
49
+ const inferMimeType = (path: string) => {
50
+ const extension = extname(path).toLowerCase();
51
+ return (
52
+ IMAGE_MIME_BY_EXTENSION[extension] ??
53
+ VIDEO_MIME_BY_EXTENSION[extension] ??
54
+ AUDIO_MIME_BY_EXTENSION[extension] ??
55
+ FALLBACK_FILE_MIME
56
+ );
57
+ };
58
+
59
+ const isImageMimeType = (mimeType: string) => mimeType.startsWith('image/');
60
+ const isAudioMimeType = (mimeType: string) => mimeType.startsWith('audio/');
61
+ const isVideoMimeType = (mimeType: string) => mimeType.startsWith('video/');
62
+
63
+ const toImagePart = (file: PromptFileSelection, buffer: Uint8Array): InputContentPart => {
64
+ const mimeType = inferMimeType(file.relativePath);
65
+ const dataUrl = `data:${mimeType};base64,${Buffer.from(buffer).toString('base64')}`;
66
+ return {
67
+ type: 'image_url',
68
+ image_url: {
69
+ url: dataUrl,
70
+ detail: 'auto',
71
+ },
72
+ };
73
+ };
74
+
75
+ const toTextPart = (file: PromptFileSelection, buffer: Uint8Array): InputContentPart => {
76
+ const rawText = TEXT_DECODER.decode(buffer);
77
+ const normalized = rawText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
78
+ const truncated = normalized.length > MAX_TEXT_ATTACHMENT_CHARS;
79
+ const content = truncated ? normalized.slice(0, MAX_TEXT_ATTACHMENT_CHARS) : normalized;
80
+ return {
81
+ type: 'text',
82
+ text: formatFileFence(file.relativePath, content, truncated),
83
+ };
84
+ };
85
+
86
+ const toAudioTextPart = (file: PromptFileSelection): InputContentPart => {
87
+ return {
88
+ type: 'text',
89
+ text: `Attached audio: ${file.relativePath}`,
90
+ };
91
+ };
92
+
93
+ const toVideoTextPart = (file: PromptFileSelection): InputContentPart => {
94
+ return {
95
+ type: 'text',
96
+ text: `Attached video: ${file.relativePath}`,
97
+ };
98
+ };
99
+
100
+ const toFileParts = async (
101
+ file: PromptFileSelection,
102
+ capabilities: AttachmentModelCapabilities
103
+ ): Promise<InputContentPart[]> => {
104
+ const buffer = await readFile(file.absolutePath);
105
+ if (buffer.byteLength > MAX_ATTACHMENT_BYTES) {
106
+ throw new Error(`Attachment too large: ${file.relativePath}`);
107
+ }
108
+
109
+ const mimeType = inferMimeType(file.relativePath);
110
+ if (isImageMimeType(mimeType) && capabilities.image && isImageSelection(file)) {
111
+ return [
112
+ {
113
+ type: 'text',
114
+ text: `Attached image: ${file.relativePath}`,
115
+ },
116
+ toImagePart(file, buffer),
117
+ ];
118
+ }
119
+
120
+ if (isAudioMimeType(mimeType) && capabilities.audio && isAudioSelection(file)) {
121
+ return [toAudioTextPart(file)];
122
+ }
123
+
124
+ if (isVideoMimeType(mimeType) && capabilities.video && isVideoSelection(file)) {
125
+ return [toVideoTextPart(file)];
126
+ }
127
+
128
+ return [toTextPart(file, buffer)];
129
+ };
130
+
131
+ export const buildPromptContent = async (
132
+ prompt: string,
133
+ files: PromptFileSelection[],
134
+ capabilities: AttachmentModelCapabilities
135
+ ): Promise<MessageContent> => {
136
+ if (files.length === 0) {
137
+ return prompt;
138
+ }
139
+
140
+ const parts: InputContentPart[] = [];
141
+ if (prompt.trim().length > 0) {
142
+ parts.push({
143
+ type: 'text',
144
+ text: prompt,
145
+ });
146
+ }
147
+
148
+ for (const file of files) {
149
+ parts.push(...(await toFileParts(file, capabilities)));
150
+ }
151
+
152
+ return parts;
153
+ };
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { findTrailingFileMention, removeTrailingFileMention } from './file-mention-query';
4
+
5
+ describe('file-mention-query', () => {
6
+ it('finds trailing @/ mention at input start', () => {
7
+ expect(findTrailingFileMention('@/src')).toEqual({
8
+ token: '@/src',
9
+ query: 'src',
10
+ start: 0,
11
+ end: 5,
12
+ });
13
+ });
14
+
15
+ it('finds trailing @/ mention after text', () => {
16
+ expect(findTrailingFileMention('check @/src/app')).toEqual({
17
+ token: '@/src/app',
18
+ query: 'src/app',
19
+ start: 6,
20
+ end: 15,
21
+ });
22
+ });
23
+
24
+ it('returns null when mention is not trailing token', () => {
25
+ expect(findTrailingFileMention('use @/src and continue')).toBeNull();
26
+ expect(findTrailingFileMention('plain text')).toBeNull();
27
+ });
28
+
29
+ it('removes trailing mention token and keeps prior text', () => {
30
+ expect(removeTrailingFileMention('@/src')).toBe('');
31
+ expect(removeTrailingFileMention('check @/src/app')).toBe('check ');
32
+ expect(removeTrailingFileMention('plain text')).toBe('plain text');
33
+ });
34
+ });
@@ -0,0 +1,32 @@
1
+ export type FileMentionMatch = {
2
+ token: string;
3
+ query: string;
4
+ start: number;
5
+ end: number;
6
+ };
7
+
8
+ const FILE_MENTION_PATTERN = /(^|\s)(@\/[^\s]*)$/;
9
+
10
+ export const findTrailingFileMention = (value: string): FileMentionMatch | null => {
11
+ const match = value.match(FILE_MENTION_PATTERN);
12
+ const token = match?.[2];
13
+ if (!token) {
14
+ return null;
15
+ }
16
+
17
+ const start = value.length - token.length;
18
+ return {
19
+ token,
20
+ query: token.slice(2),
21
+ start,
22
+ end: value.length,
23
+ };
24
+ };
25
+
26
+ export const removeTrailingFileMention = (value: string): string => {
27
+ const match = findTrailingFileMention(value);
28
+ if (!match) {
29
+ return value;
30
+ }
31
+ return value.slice(0, match.start);
32
+ };
@@ -0,0 +1,13 @@
1
+ import type { PromptFileSelection } from './types';
2
+
3
+ const formatSelectedFiles = (files: PromptFileSelection[]) => {
4
+ return files.map(file => `@${file.relativePath}`).join(' ');
5
+ };
6
+
7
+ export const buildPromptDisplay = (prompt: string, files: PromptFileSelection[]): string => {
8
+ const normalizedPrompt = prompt.trim();
9
+ if (normalizedPrompt.length > 0) {
10
+ return normalizedPrompt;
11
+ }
12
+ return files.length > 0 ? formatSelectedFiles(files) : '';
13
+ };
@@ -0,0 +1,5 @@
1
+ export type PromptFileSelection = {
2
+ relativePath: string;
3
+ absolutePath: string;
4
+ size: number;
5
+ };
@@ -0,0 +1,63 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+
4
+ import { resolveWorkspaceRoot } from '../agent/runtime/source-modules';
5
+ import type { PromptFileSelection } from './types';
6
+
7
+ const IGNORED_DIR_NAMES = new Set([
8
+ '.git',
9
+ 'node_modules',
10
+ 'dist',
11
+ 'build',
12
+ 'coverage',
13
+ '.next',
14
+ '.turbo',
15
+ ]);
16
+
17
+ const comparePaths = (a: PromptFileSelection, b: PromptFileSelection) => {
18
+ return a.relativePath.localeCompare(b.relativePath);
19
+ };
20
+
21
+ const visitDirectory = async (
22
+ root: string,
23
+ directory: string,
24
+ output: PromptFileSelection[]
25
+ ): Promise<void> => {
26
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
27
+
28
+ const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name));
29
+ for (const entry of sortedEntries) {
30
+ const absolutePath = join(directory, entry.name);
31
+ if (entry.isDirectory()) {
32
+ if (IGNORED_DIR_NAMES.has(entry.name)) {
33
+ continue;
34
+ }
35
+ await visitDirectory(root, absolutePath, output);
36
+ continue;
37
+ }
38
+
39
+ if (!entry.isFile()) {
40
+ continue;
41
+ }
42
+
43
+ const stat = await Bun.file(absolutePath)
44
+ .stat()
45
+ .catch(() => undefined);
46
+ if (!stat || !stat.isFile()) {
47
+ continue;
48
+ }
49
+
50
+ output.push({
51
+ relativePath: relative(root, absolutePath),
52
+ absolutePath,
53
+ size: stat.size,
54
+ });
55
+ }
56
+ };
57
+
58
+ export const listWorkspaceFiles = async (): Promise<PromptFileSelection[]> => {
59
+ const workspaceRoot = resolveWorkspaceRoot();
60
+ const files: PromptFileSelection[] = [];
61
+ await visitDirectory(workspaceRoot, workspaceRoot, files);
62
+ return files.sort(comparePaths);
63
+ };
@@ -0,0 +1,207 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type {
4
+ AgentToolResultEvent,
5
+ AgentToolStreamEvent,
6
+ AgentToolUseEvent,
7
+ } from '../agent/runtime/types';
8
+ import { buildAgentEventHandlers } from './agent-event-handlers';
9
+ import { appendToSegment } from './turn-updater';
10
+ import type { ReplySegment } from '../types/chat';
11
+
12
+ const buildHarness = () => {
13
+ const turnId = 1;
14
+ let segments: ReplySegment[] = [];
15
+ const notes: string[] = [];
16
+
17
+ const handlers = buildAgentEventHandlers({
18
+ turnId,
19
+ isCurrentRequest: () => true,
20
+ appendSegment: (_turnId, segmentId, type, chunk, data) => {
21
+ segments = appendToSegment(segments, segmentId, type, chunk, data);
22
+ },
23
+ appendEventLine: (_turnId, text) => {
24
+ notes.push(text);
25
+ },
26
+ });
27
+
28
+ return {
29
+ turnId,
30
+ handlers,
31
+ readSegments: () => segments,
32
+ notes,
33
+ };
34
+ };
35
+
36
+ const createToolUseEvent = (): AgentToolUseEvent => ({
37
+ id: 'call_1',
38
+ function: {
39
+ name: 'bash',
40
+ arguments: JSON.stringify({ command: 'ls -la' }),
41
+ },
42
+ });
43
+
44
+ const createStdoutStreamEvent = (): AgentToolStreamEvent => ({
45
+ toolCallId: 'call_1',
46
+ toolName: 'bash',
47
+ type: 'stdout',
48
+ sequence: 1,
49
+ timestamp: Date.now(),
50
+ content: 'total 80',
51
+ });
52
+
53
+ const createToolResultEvent = (): AgentToolResultEvent => ({
54
+ toolCall: {
55
+ id: 'call_1',
56
+ function: {
57
+ name: 'bash',
58
+ arguments: JSON.stringify({ command: 'ls -la' }),
59
+ },
60
+ },
61
+ result: {
62
+ success: true,
63
+ data: {
64
+ output: 'total 80',
65
+ },
66
+ },
67
+ });
68
+
69
+ const createEmptyToolResultEvent = (): AgentToolResultEvent => ({
70
+ toolCall: {
71
+ id: 'call_2',
72
+ function: {
73
+ name: 'bash',
74
+ arguments: JSON.stringify({ command: 'find /tmp -name missing' }),
75
+ },
76
+ },
77
+ result: {
78
+ success: true,
79
+ data: {
80
+ summary: 'Command completed successfully with no output.',
81
+ },
82
+ },
83
+ });
84
+
85
+ describe('buildAgentEventHandlers', () => {
86
+ it('keeps ordered stream segments as thinking -> text -> thinking -> tool -> tool result', () => {
87
+ const { handlers, readSegments, turnId } = buildHarness();
88
+
89
+ handlers.onTextDelta?.({
90
+ text: '先想一下要做什么。',
91
+ isReasoning: true,
92
+ });
93
+ handlers.onTextDelta?.({
94
+ text: '当前目录看起来是一个 TypeScript 项目。',
95
+ isReasoning: false,
96
+ });
97
+ handlers.onTextDelta?.({
98
+ text: '我会先执行 ls -la 看目录结构。',
99
+ isReasoning: true,
100
+ });
101
+ handlers.onToolUse?.(createToolUseEvent());
102
+ handlers.onToolStream?.(createStdoutStreamEvent());
103
+ handlers.onToolResult?.(createToolResultEvent());
104
+ handlers.onTextDelta?.({
105
+ text: '当前目录包含以下内容。',
106
+ isReasoning: false,
107
+ });
108
+ handlers.onTextComplete?.('');
109
+
110
+ const segments = readSegments();
111
+ expect(segments.map(segment => segment.id)).toEqual([
112
+ `${turnId}:thinking:1`,
113
+ `${turnId}:text:2`,
114
+ `${turnId}:thinking:3`,
115
+ `${turnId}:tool-use:call_1`,
116
+ `${turnId}:tool:call_1:stdout`,
117
+ `${turnId}:tool-result:call_1`,
118
+ `${turnId}:text:4`,
119
+ ]);
120
+
121
+ const firstThinkingIndex = segments.findIndex(segment => segment.id === `${turnId}:thinking:1`);
122
+ const toolUseIndex = segments.findIndex(segment => segment.id === `${turnId}:tool-use:call_1`);
123
+ const toolResultIndex = segments.findIndex(
124
+ segment => segment.id === `${turnId}:tool-result:call_1`
125
+ );
126
+ expect(firstThinkingIndex).toBeGreaterThanOrEqual(0);
127
+ expect(toolUseIndex).toBeGreaterThan(firstThinkingIndex);
128
+ expect(toolResultIndex).toBeGreaterThan(toolUseIndex);
129
+ });
130
+
131
+ it('suppresses duplicated tool output in tool-result when stdout/stderr stream already exists', () => {
132
+ const { handlers, readSegments, turnId } = buildHarness();
133
+
134
+ handlers.onToolUse?.(createToolUseEvent());
135
+ handlers.onToolStream?.(createStdoutStreamEvent());
136
+ handlers.onToolResult?.(createToolResultEvent());
137
+
138
+ const toolResult = readSegments().find(
139
+ segment => segment.id === `${turnId}:tool-result:call_1`
140
+ );
141
+ expect(toolResult?.content).toContain('# Result: bash (call_1) success');
142
+ expect(toolResult?.content).not.toContain('total 80');
143
+ });
144
+
145
+ it('keeps tool-result output when no stdout/stderr stream was emitted', () => {
146
+ const { handlers, readSegments, turnId } = buildHarness();
147
+
148
+ handlers.onToolUse?.(createToolUseEvent());
149
+ handlers.onToolResult?.(createToolResultEvent());
150
+
151
+ const toolResult = readSegments().find(
152
+ segment => segment.id === `${turnId}:tool-result:call_1`
153
+ );
154
+ expect(toolResult?.content).toContain('# Result: bash (call_1) success');
155
+ expect(toolResult?.content).toContain('total 80');
156
+ });
157
+
158
+ it('renders a summary instead of an output wrapper when tool succeeds without output', () => {
159
+ const { handlers, readSegments, turnId } = buildHarness();
160
+
161
+ handlers.onToolUse?.({
162
+ id: 'call_2',
163
+ function: {
164
+ name: 'bash',
165
+ arguments: JSON.stringify({ command: 'find /tmp -name missing' }),
166
+ },
167
+ });
168
+ handlers.onToolResult?.(createEmptyToolResultEvent());
169
+
170
+ const toolResult = readSegments().find(
171
+ segment => segment.id === `${turnId}:tool-result:call_2`
172
+ );
173
+ expect(toolResult?.content).toContain('# Result: bash (call_2) success');
174
+ expect(toolResult?.content).toContain('Command completed successfully with no output.');
175
+ expect(toolResult?.content).not.toContain('{"output":""}');
176
+ });
177
+
178
+ it('stores structured tool event data on tool-use and tool-result segments', () => {
179
+ const { handlers, readSegments, turnId } = buildHarness();
180
+
181
+ const toolUseEvent = createToolUseEvent();
182
+ const toolResultEvent = createToolResultEvent();
183
+ handlers.onToolUse?.(toolUseEvent);
184
+ handlers.onToolResult?.(toolResultEvent);
185
+
186
+ const toolUse = readSegments().find(segment => segment.id === `${turnId}:tool-use:call_1`);
187
+ const toolResult = readSegments().find(
188
+ segment => segment.id === `${turnId}:tool-result:call_1`
189
+ );
190
+
191
+ expect(toolUse?.data).toEqual(toolUseEvent);
192
+ expect(toolResult?.data).toEqual(toolResultEvent);
193
+ });
194
+
195
+ it('deduplicates repeated tool-use events for the same toolCallId', () => {
196
+ const { handlers, readSegments, turnId } = buildHarness();
197
+
198
+ handlers.onToolUse?.(createToolUseEvent());
199
+ handlers.onToolUse?.(createToolUseEvent());
200
+ handlers.onToolUse?.(createToolUseEvent());
201
+
202
+ const toolUseSegments = readSegments().filter(
203
+ segment => segment.id === `${turnId}:tool-use:call_1`
204
+ );
205
+ expect(toolUseSegments.length).toBe(1);
206
+ });
207
+ });
@@ -0,0 +1,196 @@
1
+ import {
2
+ formatLoopEvent,
3
+ formatStepEvent,
4
+ formatStopEvent,
5
+ formatToolConfirmEvent,
6
+ formatToolResultEvent,
7
+ formatToolResultEventCode,
8
+ formatToolStreamEvent,
9
+ formatToolUseEvent,
10
+ formatToolUseEventCode,
11
+ } from '../agent/runtime/event-format';
12
+ import type {
13
+ AgentEventHandlers,
14
+ AgentToolResultEvent,
15
+ AgentToolUseEvent,
16
+ } from '../agent/runtime/types';
17
+ import type { ReplySegmentType } from '../types/chat';
18
+
19
+ type BuildAgentEventHandlersParams = {
20
+ turnId: number;
21
+ isCurrentRequest: () => boolean;
22
+ appendSegment: (
23
+ turnId: number,
24
+ segmentId: string,
25
+ type: ReplySegmentType,
26
+ chunk: string,
27
+ data?: unknown
28
+ ) => void;
29
+ appendEventLine: (turnId: number, text: string) => void;
30
+ };
31
+
32
+ const shouldShowEventLog = () => {
33
+ const value = process.env.AGENT_SHOW_EVENTS?.trim().toLowerCase();
34
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
35
+ };
36
+
37
+ export const buildAgentEventHandlers = ({
38
+ turnId,
39
+ isCurrentRequest,
40
+ appendSegment,
41
+ appendEventLine,
42
+ }: BuildAgentEventHandlersParams): AgentEventHandlers => {
43
+ const showEvents = shouldShowEventLog();
44
+ const streamedToolCallIds = new Set<string>();
45
+ const renderedToolUseIds = new Set<string>();
46
+ let anonymousToolUseCounter = 0;
47
+ let anonymousToolResultCounter = 0;
48
+ let streamSegmentCursor = 0;
49
+ let activeTextSegment: {
50
+ id: string;
51
+ type: 'thinking' | 'text';
52
+ } | null = null;
53
+
54
+ const createStreamSegmentId = (type: 'thinking' | 'text') => {
55
+ streamSegmentCursor += 1;
56
+ return `${turnId}:${type}:${streamSegmentCursor}`;
57
+ };
58
+
59
+ const appendTextDeltaInOrder = (text: string, isReasoning: boolean) => {
60
+ const type: 'thinking' | 'text' = isReasoning ? 'thinking' : 'text';
61
+ if (!activeTextSegment || activeTextSegment.type !== type) {
62
+ activeTextSegment = {
63
+ id: createStreamSegmentId(type),
64
+ type,
65
+ };
66
+ }
67
+ appendSegment(turnId, activeTextSegment.id, type, text);
68
+ };
69
+
70
+ const breakTextDeltaContinuation = () => {
71
+ activeTextSegment = null;
72
+ };
73
+
74
+ const readToolCallIdFromResult = (event: AgentToolResultEvent): string | undefined => {
75
+ if (!event.toolCall || typeof event.toolCall !== 'object') {
76
+ return undefined;
77
+ }
78
+ const maybeId = (event.toolCall as { id?: unknown }).id;
79
+ return typeof maybeId === 'string' ? maybeId : undefined;
80
+ };
81
+
82
+ const readToolCallIdFromUse = (event: AgentToolUseEvent): string | undefined => {
83
+ if (!event || typeof event !== 'object') {
84
+ return undefined;
85
+ }
86
+ const maybeId = (event as { id?: unknown }).id;
87
+ return typeof maybeId === 'string' ? maybeId : undefined;
88
+ };
89
+
90
+ const logEvent = (text: string) => {
91
+ if (!showEvents) {
92
+ return;
93
+ }
94
+ appendEventLine(turnId, text);
95
+ };
96
+
97
+ return {
98
+ onTextDelta: event => {
99
+ if (!isCurrentRequest() || !event.text) {
100
+ return;
101
+ }
102
+ appendTextDeltaInOrder(event.text, Boolean(event.isReasoning));
103
+ },
104
+ onTextComplete: () => {
105
+ if (!isCurrentRequest()) {
106
+ return;
107
+ }
108
+ breakTextDeltaContinuation();
109
+ logEvent('[text-complete]');
110
+ },
111
+ onToolStream: event => {
112
+ if (!isCurrentRequest()) {
113
+ return;
114
+ }
115
+ breakTextDeltaContinuation();
116
+ const mapped = formatToolStreamEvent(event);
117
+ if (mapped.codeChunk && mapped.segmentKey) {
118
+ appendSegment(turnId, `${turnId}:tool:${mapped.segmentKey}`, 'code', mapped.codeChunk);
119
+ }
120
+ if (
121
+ (event.type === 'stdout' || event.type === 'stderr') &&
122
+ typeof event.toolCallId === 'string' &&
123
+ event.toolCallId.length > 0
124
+ ) {
125
+ streamedToolCallIds.add(event.toolCallId);
126
+ }
127
+ if (mapped.note) {
128
+ logEvent(mapped.note);
129
+ }
130
+ },
131
+ onToolConfirm: event => {
132
+ if (!isCurrentRequest()) {
133
+ return;
134
+ }
135
+ logEvent(formatToolConfirmEvent(event));
136
+ },
137
+ onToolUse: event => {
138
+ if (!isCurrentRequest()) {
139
+ return;
140
+ }
141
+ breakTextDeltaContinuation();
142
+ const toolCallId = readToolCallIdFromUse(event);
143
+ if (toolCallId && renderedToolUseIds.has(toolCallId)) {
144
+ return;
145
+ }
146
+ if (toolCallId) {
147
+ renderedToolUseIds.add(toolCallId);
148
+ }
149
+ const segmentSuffix = toolCallId ?? `anonymous_${++anonymousToolUseCounter}`;
150
+ appendSegment(
151
+ turnId,
152
+ `${turnId}:tool-use:${segmentSuffix}`,
153
+ 'code',
154
+ `${formatToolUseEventCode(event)}\n`,
155
+ event
156
+ );
157
+ logEvent(formatToolUseEvent(event));
158
+ },
159
+ onToolResult: event => {
160
+ if (!isCurrentRequest()) {
161
+ return;
162
+ }
163
+ breakTextDeltaContinuation();
164
+ const toolCallId = readToolCallIdFromResult(event);
165
+ const suppressOutput = Boolean(toolCallId && streamedToolCallIds.has(toolCallId));
166
+ const segmentSuffix = toolCallId ?? `anonymous_${++anonymousToolResultCounter}`;
167
+ appendSegment(
168
+ turnId,
169
+ `${turnId}:tool-result:${segmentSuffix}`,
170
+ 'code',
171
+ `${formatToolResultEventCode(event, { suppressOutput })}\n`,
172
+ event
173
+ );
174
+ logEvent(formatToolResultEvent(event));
175
+ },
176
+ onStep: event => {
177
+ if (!isCurrentRequest()) {
178
+ return;
179
+ }
180
+ logEvent(formatStepEvent(event));
181
+ },
182
+ onLoop: event => {
183
+ if (!isCurrentRequest()) {
184
+ return;
185
+ }
186
+ logEvent(formatLoopEvent(event));
187
+ },
188
+ onStop: event => {
189
+ if (!isCurrentRequest()) {
190
+ return;
191
+ }
192
+ breakTextDeltaContinuation();
193
+ logEvent(formatStopEvent(event));
194
+ },
195
+ };
196
+ };