@renxqoo/renx-code 0.0.3 → 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 (306) hide show
  1. package/README.md +58 -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 -12
  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 -48
  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 -6
  262. package/dist/index.js.map +0 -1
  263. package/dist/run-cli-app.d.ts +0 -2
  264. package/dist/run-cli-app.d.ts.map +0 -1
  265. package/dist/run-cli-app.js +0 -41
  266. package/dist/run-cli-app.js.map +0 -1
  267. package/dist/runtime/clipboard.d.ts +0 -10
  268. package/dist/runtime/clipboard.d.ts.map +0 -1
  269. package/dist/runtime/clipboard.js +0 -64
  270. package/dist/runtime/clipboard.js.map +0 -1
  271. package/dist/runtime/exit.d.ts +0 -7
  272. package/dist/runtime/exit.d.ts.map +0 -1
  273. package/dist/runtime/exit.js +0 -85
  274. package/dist/runtime/exit.js.map +0 -1
  275. package/dist/runtime/runtime-support.d.ts +0 -4
  276. package/dist/runtime/runtime-support.d.ts.map +0 -1
  277. package/dist/runtime/runtime-support.js +0 -19
  278. package/dist/runtime/runtime-support.js.map +0 -1
  279. package/dist/runtime/terminal-theme.d.ts +0 -25
  280. package/dist/runtime/terminal-theme.d.ts.map +0 -1
  281. package/dist/runtime/terminal-theme.js +0 -148
  282. package/dist/runtime/terminal-theme.js.map +0 -1
  283. package/dist/types/chat.d.ts +0 -29
  284. package/dist/types/chat.d.ts.map +0 -1
  285. package/dist/types/chat.js +0 -1
  286. package/dist/types/chat.js.map +0 -1
  287. package/dist/types/message-content.d.ts +0 -38
  288. package/dist/types/message-content.d.ts.map +0 -1
  289. package/dist/types/message-content.js +0 -1
  290. package/dist/types/message-content.js.map +0 -1
  291. package/dist/ui/open-code-theme.d.ts +0 -58
  292. package/dist/ui/open-code-theme.d.ts.map +0 -1
  293. package/dist/ui/open-code-theme.js +0 -113
  294. package/dist/ui/open-code-theme.js.map +0 -1
  295. package/dist/ui/opencode-markdown.d.ts +0 -7
  296. package/dist/ui/opencode-markdown.d.ts.map +0 -1
  297. package/dist/ui/opencode-markdown.js +0 -169
  298. package/dist/ui/opencode-markdown.js.map +0 -1
  299. package/dist/ui/theme.d.ts +0 -68
  300. package/dist/ui/theme.d.ts.map +0 -1
  301. package/dist/ui/theme.js +0 -80
  302. package/dist/ui/theme.js.map +0 -1
  303. package/dist/utils/time.d.ts +0 -2
  304. package/dist/utils/time.d.ts.map +0 -1
  305. package/dist/utils/time.js +0 -7
  306. package/dist/utils/time.js.map +0 -1
@@ -0,0 +1,196 @@
1
+ import type { KeyEvent, TextareaRenderable } from '@opentui/core';
2
+ import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
3
+
4
+ import { findTrailingFileMention } from '../files/file-mention-query';
5
+ import type { PromptFileSelection } from '../files/types';
6
+ import { isMediaSelection } from '../files/attachment-capabilities';
7
+ import { listWorkspaceFiles } from '../files/workspace-files';
8
+
9
+ type UseFileMentionMenuParams = {
10
+ value: string;
11
+ textareaRef: RefObject<TextareaRenderable | null>;
12
+ selectedFiles: PromptFileSelection[];
13
+ onFilesSelected: (files: PromptFileSelection[]) => void;
14
+ onValueChange: (value: string) => void;
15
+ disabled?: boolean;
16
+ };
17
+
18
+ type UseFileMentionMenuResult = {
19
+ visible: boolean;
20
+ loading: boolean;
21
+ error: string | null;
22
+ options: PromptFileSelection[];
23
+ selectedIndex: number;
24
+ handleKeyDown: (event: KeyEvent) => boolean;
25
+ };
26
+
27
+ const normalize = (value: string) => value.trim().toLowerCase();
28
+
29
+ export const useFileMentionMenu = ({
30
+ value,
31
+ textareaRef,
32
+ selectedFiles,
33
+ onFilesSelected,
34
+ onValueChange,
35
+ disabled = false,
36
+ }: UseFileMentionMenuParams): UseFileMentionMenuResult => {
37
+ const [selectedIndex, setSelectedIndex] = useState(0);
38
+ const [dismissedToken, setDismissedToken] = useState<string | null>(null);
39
+ const [loading, setLoading] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [allOptions, setAllOptions] = useState<PromptFileSelection[]>([]);
42
+ const hasLoadedRef = useRef(false);
43
+ const requestIdRef = useRef(0);
44
+
45
+ const mention = useMemo(() => findTrailingFileMention(value), [value]);
46
+
47
+ useEffect(() => {
48
+ if (!mention || mention.token !== dismissedToken) {
49
+ return;
50
+ }
51
+ setDismissedToken(null);
52
+ }, [dismissedToken, mention]);
53
+
54
+ useEffect(() => {
55
+ if (disabled || !mention || hasLoadedRef.current) {
56
+ return;
57
+ }
58
+
59
+ hasLoadedRef.current = true;
60
+ setLoading(true);
61
+ setError(null);
62
+ requestIdRef.current += 1;
63
+ const requestId = requestIdRef.current;
64
+
65
+ void listWorkspaceFiles()
66
+ .then(files => {
67
+ if (requestId !== requestIdRef.current) {
68
+ return;
69
+ }
70
+ setAllOptions(files);
71
+ })
72
+ .catch(loadError => {
73
+ if (requestId !== requestIdRef.current) {
74
+ return;
75
+ }
76
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
77
+ })
78
+ .finally(() => {
79
+ if (requestId !== requestIdRef.current) {
80
+ return;
81
+ }
82
+ setLoading(false);
83
+ });
84
+ }, [disabled, mention]);
85
+
86
+ const options = useMemo(() => {
87
+ if (!mention) {
88
+ return [];
89
+ }
90
+ const selectedPaths = new Set(selectedFiles.map(file => file.absolutePath));
91
+ const query = normalize(mention.query);
92
+ return allOptions.filter(item => {
93
+ if (selectedPaths.has(item.absolutePath)) {
94
+ return false;
95
+ }
96
+ return query.length === 0 || item.relativePath.toLowerCase().includes(query);
97
+ });
98
+ }, [allOptions, mention, selectedFiles]);
99
+
100
+ const visible = !disabled && !!mention && mention.token !== dismissedToken;
101
+
102
+ useEffect(() => {
103
+ setSelectedIndex(0);
104
+ }, [mention?.token]);
105
+
106
+ useEffect(() => {
107
+ if (selectedIndex < options.length) {
108
+ return;
109
+ }
110
+ setSelectedIndex(0);
111
+ }, [options.length, selectedIndex]);
112
+
113
+ const applySelection = useCallback(
114
+ (index: number) => {
115
+ const selected = options[index];
116
+ if (!selected || !mention) {
117
+ return false;
118
+ }
119
+
120
+ onFilesSelected([selected]);
121
+ const nextValue = isMediaSelection(selected)
122
+ ? value.slice(0, mention.start)
123
+ : `${value.slice(0, mention.start)}@/${selected.relativePath} `;
124
+ onValueChange(nextValue);
125
+
126
+ const textarea = textareaRef.current;
127
+ if (textarea) {
128
+ textarea.setText(nextValue);
129
+ textarea.cursorOffset = nextValue.length;
130
+ }
131
+ return true;
132
+ },
133
+ [mention, onFilesSelected, onValueChange, options, textareaRef, value]
134
+ );
135
+
136
+ const moveSelection = useCallback(
137
+ (step: number) => {
138
+ if (!visible || options.length === 0) {
139
+ return;
140
+ }
141
+ setSelectedIndex(current => (current + step + options.length) % options.length);
142
+ },
143
+ [options.length, visible]
144
+ );
145
+
146
+ const handleKeyDown = useCallback(
147
+ (event: KeyEvent): boolean => {
148
+ if (!visible) {
149
+ return false;
150
+ }
151
+
152
+ const name = (event.name ?? '').toLowerCase();
153
+ const ctrlOnly = !!event.ctrl && !event.shift && !event.meta;
154
+ const isUp = name === 'up' || (ctrlOnly && name === 'p');
155
+ const isDown = name === 'down' || (ctrlOnly && name === 'n');
156
+
157
+ if (isUp) {
158
+ moveSelection(-1);
159
+ event.preventDefault();
160
+ return true;
161
+ }
162
+
163
+ if (isDown) {
164
+ moveSelection(1);
165
+ event.preventDefault();
166
+ return true;
167
+ }
168
+
169
+ if (name === 'escape') {
170
+ setDismissedToken(mention?.token ?? null);
171
+ event.preventDefault();
172
+ return true;
173
+ }
174
+
175
+ if (name === 'return' || name === 'enter' || name === 'tab') {
176
+ const applied = applySelection(selectedIndex);
177
+ if (applied || options.length === 0) {
178
+ event.preventDefault();
179
+ return true;
180
+ }
181
+ }
182
+
183
+ return false;
184
+ },
185
+ [applySelection, mention?.token, moveSelection, options.length, selectedIndex, visible]
186
+ );
187
+
188
+ return {
189
+ visible,
190
+ loading,
191
+ error,
192
+ options,
193
+ selectedIndex,
194
+ handleKeyDown,
195
+ };
196
+ };
@@ -0,0 +1,185 @@
1
+ import type { KeyEvent } from '@opentui/core';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import type { PromptFileSelection } from '../files/types';
5
+ import { listWorkspaceFiles } from '../files/workspace-files';
6
+
7
+ type UseFilePickerResult = {
8
+ visible: boolean;
9
+ loading: boolean;
10
+ error: string | null;
11
+ search: string;
12
+ options: PromptFileSelection[];
13
+ selectedIndex: number;
14
+ selectedPaths: Set<string>;
15
+ open: (initialSelection?: PromptFileSelection[]) => void;
16
+ close: () => void;
17
+ setSearch: (value: string) => void;
18
+ toggleSelectedIndex: () => void;
19
+ setSelectedIndex: (value: number) => void;
20
+ handleListKeyDown: (event: KeyEvent) => boolean;
21
+ confirmSelected: () => PromptFileSelection[];
22
+ };
23
+
24
+ const normalize = (value: string) => value.trim().toLowerCase();
25
+
26
+ export const useFilePicker = (): UseFilePickerResult => {
27
+ const [visible, setVisible] = useState(false);
28
+ const [loading, setLoading] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [search, setSearch] = useState('');
31
+ const [selectedIndex, setSelectedIndex] = useState(0);
32
+ const [allOptions, setAllOptions] = useState<PromptFileSelection[]>([]);
33
+ const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
34
+ const requestIdRef = useRef(0);
35
+
36
+ const options = useMemo(() => {
37
+ const query = normalize(search);
38
+ if (!query) {
39
+ return allOptions;
40
+ }
41
+ return allOptions.filter(item => item.relativePath.toLowerCase().includes(query));
42
+ }, [allOptions, search]);
43
+
44
+ useEffect(() => {
45
+ setSelectedIndex(0);
46
+ }, [search]);
47
+
48
+ useEffect(() => {
49
+ if (selectedIndex < options.length) {
50
+ return;
51
+ }
52
+ setSelectedIndex(0);
53
+ }, [options.length, selectedIndex]);
54
+
55
+ const close = useCallback(() => {
56
+ requestIdRef.current += 1;
57
+ setVisible(false);
58
+ setLoading(false);
59
+ setError(null);
60
+ setSearch('');
61
+ setSelectedIndex(0);
62
+ setAllOptions([]);
63
+ setSelectedPaths(new Set());
64
+ }, []);
65
+
66
+ const open = useCallback((initialSelection: PromptFileSelection[] = []) => {
67
+ setVisible(true);
68
+ setLoading(true);
69
+ setError(null);
70
+ setSearch('');
71
+ setSelectedIndex(0);
72
+ setAllOptions([]);
73
+ setSelectedPaths(new Set(initialSelection.map(item => item.absolutePath)));
74
+ requestIdRef.current += 1;
75
+ const requestId = requestIdRef.current;
76
+
77
+ void listWorkspaceFiles()
78
+ .then(files => {
79
+ if (requestId !== requestIdRef.current) {
80
+ return;
81
+ }
82
+ setAllOptions(files);
83
+ })
84
+ .catch(loadError => {
85
+ if (requestId !== requestIdRef.current) {
86
+ return;
87
+ }
88
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
89
+ })
90
+ .finally(() => {
91
+ if (requestId !== requestIdRef.current) {
92
+ return;
93
+ }
94
+ setLoading(false);
95
+ });
96
+ }, []);
97
+
98
+ const toggleSelectedIndex = useCallback(() => {
99
+ const selected = options[selectedIndex];
100
+ if (!selected) {
101
+ return;
102
+ }
103
+ setSelectedPaths(current => {
104
+ const next = new Set(current);
105
+ if (next.has(selected.absolutePath)) {
106
+ next.delete(selected.absolutePath);
107
+ } else {
108
+ next.add(selected.absolutePath);
109
+ }
110
+ return next;
111
+ });
112
+ }, [options, selectedIndex]);
113
+
114
+ const confirmSelected = useCallback(() => {
115
+ const result = allOptions.filter(item => selectedPaths.has(item.absolutePath));
116
+ close();
117
+ return result;
118
+ }, [allOptions, close, selectedPaths]);
119
+
120
+ const handleListKeyDown = useCallback(
121
+ (event: KeyEvent): boolean => {
122
+ if (!visible) {
123
+ return false;
124
+ }
125
+
126
+ const name = (event.name ?? '').toLowerCase();
127
+ const ctrlOnly = !!event.ctrl && !event.shift && !event.meta;
128
+ const isUp = name === 'up' || (ctrlOnly && name === 'p');
129
+ const isDown = name === 'down' || (ctrlOnly && name === 'n');
130
+
131
+ if (isUp) {
132
+ if (options.length > 0) {
133
+ setSelectedIndex(current => (current - 1 + options.length) % options.length);
134
+ }
135
+ event.preventDefault();
136
+ return true;
137
+ }
138
+
139
+ if (isDown) {
140
+ if (options.length > 0) {
141
+ setSelectedIndex(current => (current + 1) % options.length);
142
+ }
143
+ event.preventDefault();
144
+ return true;
145
+ }
146
+
147
+ if (name === 'space') {
148
+ toggleSelectedIndex();
149
+ event.preventDefault();
150
+ return true;
151
+ }
152
+
153
+ if (name === 'escape') {
154
+ close();
155
+ event.preventDefault();
156
+ return true;
157
+ }
158
+
159
+ if (name === 'return' || name === 'enter') {
160
+ event.preventDefault();
161
+ return true;
162
+ }
163
+
164
+ return false;
165
+ },
166
+ [close, options.length, toggleSelectedIndex, visible]
167
+ );
168
+
169
+ return {
170
+ visible,
171
+ loading,
172
+ error,
173
+ search,
174
+ options,
175
+ selectedIndex,
176
+ selectedPaths,
177
+ open,
178
+ close,
179
+ setSearch,
180
+ toggleSelectedIndex,
181
+ setSelectedIndex,
182
+ handleListKeyDown,
183
+ confirmSelected,
184
+ };
185
+ };
@@ -0,0 +1,196 @@
1
+ import type { KeyEvent } from '@opentui/core';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import { listAgentModels, switchAgentModel } from '../agent/runtime/runtime';
5
+ import type { AgentModelOption } from '../agent/runtime/model-types';
6
+
7
+ type UseModelPickerParams = {
8
+ onModelChanged: (label: string) => void;
9
+ };
10
+
11
+ type UseModelPickerResult = {
12
+ visible: boolean;
13
+ loading: boolean;
14
+ switching: boolean;
15
+ error: string | null;
16
+ search: string;
17
+ options: AgentModelOption[];
18
+ selectedIndex: number;
19
+ open: () => void;
20
+ close: () => void;
21
+ setSearch: (value: string) => void;
22
+ setSelectedIndex: (value: number) => void;
23
+ handleListKeyDown: (event: KeyEvent) => boolean;
24
+ confirmSelected: () => Promise<boolean>;
25
+ };
26
+
27
+ const normalize = (value: string) => value.trim().toLowerCase();
28
+
29
+ export const useModelPicker = ({ onModelChanged }: UseModelPickerParams): UseModelPickerResult => {
30
+ const [visible, setVisible] = useState(false);
31
+ const [loading, setLoading] = useState(false);
32
+ const [switching, setSwitching] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [search, setSearch] = useState('');
35
+ const [selectedIndex, setSelectedIndex] = useState(0);
36
+ const [allOptions, setAllOptions] = useState<AgentModelOption[]>([]);
37
+ const requestIdRef = useRef(0);
38
+
39
+ const options = useMemo(() => {
40
+ const query = normalize(search);
41
+ if (!query) {
42
+ return allOptions;
43
+ }
44
+
45
+ return allOptions.filter(item => {
46
+ const provider = item.provider.toLowerCase();
47
+ const id = item.id.toLowerCase();
48
+ const name = item.name.toLowerCase();
49
+ return provider.includes(query) || id.includes(query) || name.includes(query);
50
+ });
51
+ }, [allOptions, search]);
52
+
53
+ useEffect(() => {
54
+ setSelectedIndex(0);
55
+ }, [search]);
56
+
57
+ useEffect(() => {
58
+ if (selectedIndex < options.length) {
59
+ return;
60
+ }
61
+ setSelectedIndex(0);
62
+ }, [options.length, selectedIndex]);
63
+
64
+ const close = useCallback(() => {
65
+ requestIdRef.current += 1;
66
+ setVisible(false);
67
+ setLoading(false);
68
+ setSwitching(false);
69
+ setError(null);
70
+ setSearch('');
71
+ setSelectedIndex(0);
72
+ }, []);
73
+
74
+ const open = useCallback(() => {
75
+ setVisible(true);
76
+ setLoading(true);
77
+ setError(null);
78
+ setSearch('');
79
+ setSelectedIndex(0);
80
+ setAllOptions([]);
81
+ requestIdRef.current += 1;
82
+ const requestId = requestIdRef.current;
83
+
84
+ void listAgentModels()
85
+ .then(models => {
86
+ if (requestId !== requestIdRef.current) {
87
+ return;
88
+ }
89
+ setAllOptions(models);
90
+ })
91
+ .catch(loadError => {
92
+ if (requestId !== requestIdRef.current) {
93
+ return;
94
+ }
95
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
96
+ })
97
+ .finally(() => {
98
+ if (requestId !== requestIdRef.current) {
99
+ return;
100
+ }
101
+ setLoading(false);
102
+ });
103
+ }, []);
104
+
105
+ const confirmSelected = useCallback(async (): Promise<boolean> => {
106
+ const selected = options[selectedIndex];
107
+ if (!selected || !visible || loading || switching) {
108
+ return false;
109
+ }
110
+
111
+ if (!selected.configured) {
112
+ setError(`Missing env ${selected.apiKeyEnv} for ${selected.id}.`);
113
+ return true;
114
+ }
115
+
116
+ try {
117
+ setSwitching(true);
118
+ setError(null);
119
+ const changed = await switchAgentModel(selected.id);
120
+ setAllOptions(prev =>
121
+ prev.map(item => ({
122
+ ...item,
123
+ current: item.id === changed.modelId,
124
+ }))
125
+ );
126
+ onModelChanged(changed.modelLabel);
127
+ close();
128
+ return true;
129
+ } catch (switchError) {
130
+ setError(switchError instanceof Error ? switchError.message : String(switchError));
131
+ return true;
132
+ } finally {
133
+ setSwitching(false);
134
+ }
135
+ }, [close, loading, onModelChanged, options, selectedIndex, switching, visible]);
136
+
137
+ const handleListKeyDown = useCallback(
138
+ (event: KeyEvent): boolean => {
139
+ if (!visible) {
140
+ return false;
141
+ }
142
+
143
+ const name = (event.name ?? '').toLowerCase();
144
+ const ctrlOnly = !!event.ctrl && !event.shift && !event.meta;
145
+ const isUp = name === 'up' || (ctrlOnly && name === 'p');
146
+ const isDown = name === 'down' || (ctrlOnly && name === 'n');
147
+
148
+ if (isUp) {
149
+ if (options.length > 0) {
150
+ setSelectedIndex(current => (current - 1 + options.length) % options.length);
151
+ }
152
+ event.preventDefault();
153
+ return true;
154
+ }
155
+
156
+ if (isDown) {
157
+ if (options.length > 0) {
158
+ setSelectedIndex(current => (current + 1) % options.length);
159
+ }
160
+ event.preventDefault();
161
+ return true;
162
+ }
163
+
164
+ if (name === 'escape') {
165
+ close();
166
+ event.preventDefault();
167
+ return true;
168
+ }
169
+
170
+ if (name === 'return' || name === 'enter') {
171
+ void confirmSelected();
172
+ event.preventDefault();
173
+ return true;
174
+ }
175
+
176
+ return false;
177
+ },
178
+ [close, confirmSelected, options.length, visible]
179
+ );
180
+
181
+ return {
182
+ visible,
183
+ loading,
184
+ switching,
185
+ error,
186
+ search,
187
+ options,
188
+ selectedIndex,
189
+ open,
190
+ close,
191
+ setSearch,
192
+ setSelectedIndex,
193
+ handleListKeyDown,
194
+ confirmSelected,
195
+ };
196
+ };