aiexecode 1.0.157

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 (188) hide show
  1. package/LICENSE +68 -0
  2. package/README.md +347 -0
  3. package/config_template/mcp_config.json +3 -0
  4. package/config_template/package_name_store.json +5 -0
  5. package/config_template/settings.json +5 -0
  6. package/index.js +879 -0
  7. package/mcp-agent-lib/example/01-basic-usage.js +82 -0
  8. package/mcp-agent-lib/example/02-quick-start.js +52 -0
  9. package/mcp-agent-lib/example/03-http-server.js +76 -0
  10. package/mcp-agent-lib/example/04-multiple-servers.js +117 -0
  11. package/mcp-agent-lib/example/05-error-handling.js +116 -0
  12. package/mcp-agent-lib/example/06-resources-and-prompts.js +174 -0
  13. package/mcp-agent-lib/example/07-advanced-configuration.js +191 -0
  14. package/mcp-agent-lib/example/08-real-world-chatbot.js +331 -0
  15. package/mcp-agent-lib/example/README.md +346 -0
  16. package/mcp-agent-lib/index.js +19 -0
  17. package/mcp-agent-lib/init.sh +3 -0
  18. package/mcp-agent-lib/package-lock.json +1216 -0
  19. package/mcp-agent-lib/package.json +53 -0
  20. package/mcp-agent-lib/sampleFastMCPClient/client.py +25 -0
  21. package/mcp-agent-lib/sampleFastMCPClient/run.sh +3 -0
  22. package/mcp-agent-lib/sampleFastMCPServer/run.sh +3 -0
  23. package/mcp-agent-lib/sampleFastMCPServer/server.py +12 -0
  24. package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/run.sh +3 -0
  25. package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/server.py +43 -0
  26. package/mcp-agent-lib/sampleFastMCPServerRootsRequest/server.py +63 -0
  27. package/mcp-agent-lib/sampleMCPHost/index.js +386 -0
  28. package/mcp-agent-lib/sampleMCPHost/mcp_config.json +24 -0
  29. package/mcp-agent-lib/sampleMCPHostFeatures/elicitation.js +151 -0
  30. package/mcp-agent-lib/sampleMCPHostFeatures/index.js +166 -0
  31. package/mcp-agent-lib/sampleMCPHostFeatures/roots.js +197 -0
  32. package/mcp-agent-lib/src/mcp_client.js +1860 -0
  33. package/mcp-agent-lib/src/mcp_message_logger.js +517 -0
  34. package/package.json +72 -0
  35. package/payload_viewer/out/404/index.html +1 -0
  36. package/payload_viewer/out/404.html +1 -0
  37. package/payload_viewer/out/_next/static/chunks/060f9a97930f3d04.js +1 -0
  38. package/payload_viewer/out/_next/static/chunks/103c802c8f4a5ea1.js +1 -0
  39. package/payload_viewer/out/_next/static/chunks/16474fd6c6910c45.js +1 -0
  40. package/payload_viewer/out/_next/static/chunks/17722e3ac4e00587.js +1 -0
  41. package/payload_viewer/out/_next/static/chunks/305b077a9873cf54.js +1 -0
  42. package/payload_viewer/out/_next/static/chunks/4c1d05c6741c2bdd.js +5 -0
  43. package/payload_viewer/out/_next/static/chunks/538cc02e54714b23.js +1 -0
  44. package/payload_viewer/out/_next/static/chunks/6251fa5907d2b226.js +5 -0
  45. package/payload_viewer/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  46. package/payload_viewer/out/_next/static/chunks/b6c0459f3789d25c.js +1 -0
  47. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  48. package/payload_viewer/out/_next/static/chunks/bd2dcf98c9b362f6.js +1 -0
  49. package/payload_viewer/out/_next/static/chunks/c8a542ae21335479.js +1 -0
  50. package/payload_viewer/out/_next/static/chunks/cdd12d5c1a5a6064.js +1 -0
  51. package/payload_viewer/out/_next/static/chunks/e411019f55d87c42.js +1 -0
  52. package/payload_viewer/out/_next/static/chunks/e60ef129113f6e24.js +1 -0
  53. package/payload_viewer/out/_next/static/chunks/f1ac9047ac4a3fde.js +1 -0
  54. package/payload_viewer/out/_next/static/chunks/turbopack-0ac29803ce3c3c7a.js +3 -0
  55. package/payload_viewer/out/_next/static/chunks/turbopack-89db4c64206a73e4.js +3 -0
  56. package/payload_viewer/out/_next/static/chunks/turbopack-a5b8235fa59d7119.js +3 -0
  57. package/payload_viewer/out/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  58. package/payload_viewer/out/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  59. package/payload_viewer/out/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  60. package/payload_viewer/out/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  61. package/payload_viewer/out/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  62. package/payload_viewer/out/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  63. package/payload_viewer/out/_next/static/media/favicon.0b3bf435.ico +0 -0
  64. package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_buildManifest.js +14 -0
  65. package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_clientMiddlewareManifest.json +1 -0
  66. package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_ssgManifest.js +1 -0
  67. package/payload_viewer/out/favicon.ico +0 -0
  68. package/payload_viewer/out/file.svg +1 -0
  69. package/payload_viewer/out/globe.svg +1 -0
  70. package/payload_viewer/out/index.html +1 -0
  71. package/payload_viewer/out/index.txt +23 -0
  72. package/payload_viewer/out/next.svg +1 -0
  73. package/payload_viewer/out/vercel.svg +1 -0
  74. package/payload_viewer/out/window.svg +1 -0
  75. package/payload_viewer/web_server.js +861 -0
  76. package/prompts/completion_judge.txt +128 -0
  77. package/prompts/orchestrator.txt +1213 -0
  78. package/src/LLMClient/client.js +1375 -0
  79. package/src/LLMClient/converters/input-normalizer.js +238 -0
  80. package/src/LLMClient/converters/responses-to-claude.js +503 -0
  81. package/src/LLMClient/converters/responses-to-gemini.js +648 -0
  82. package/src/LLMClient/converters/responses-to-ollama.js +348 -0
  83. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  84. package/src/LLMClient/errors.js +398 -0
  85. package/src/LLMClient/index.js +36 -0
  86. package/src/ai_based/completion_judge.js +421 -0
  87. package/src/ai_based/orchestrator.js +527 -0
  88. package/src/ai_based/pip_package_installer.js +173 -0
  89. package/src/ai_based/pip_package_lookup.js +197 -0
  90. package/src/cli/mcp_cli.js +70 -0
  91. package/src/cli/mcp_commands.js +255 -0
  92. package/src/commands/agents.js +18 -0
  93. package/src/commands/apikey.js +55 -0
  94. package/src/commands/bg.js +140 -0
  95. package/src/commands/commands.js +56 -0
  96. package/src/commands/debug.js +54 -0
  97. package/src/commands/exit.js +19 -0
  98. package/src/commands/help.js +35 -0
  99. package/src/commands/mcp.js +128 -0
  100. package/src/commands/model.js +176 -0
  101. package/src/commands/setup.js +13 -0
  102. package/src/commands/skills.js +51 -0
  103. package/src/commands/tools.js +165 -0
  104. package/src/commands/viewer.js +147 -0
  105. package/src/config/ai_models.js +312 -0
  106. package/src/config/config.js +10 -0
  107. package/src/config/constants.js +71 -0
  108. package/src/config/feature_flags.js +15 -0
  109. package/src/frontend/App.js +1263 -0
  110. package/src/frontend/README.md +81 -0
  111. package/src/frontend/components/AutocompleteMenu.js +47 -0
  112. package/src/frontend/components/BackgroundProcessList.js +175 -0
  113. package/src/frontend/components/BlankLine.js +62 -0
  114. package/src/frontend/components/ConversationItem.js +893 -0
  115. package/src/frontend/components/CurrentModelView.js +43 -0
  116. package/src/frontend/components/FileDiffViewer.js +616 -0
  117. package/src/frontend/components/Footer.js +25 -0
  118. package/src/frontend/components/Header.js +42 -0
  119. package/src/frontend/components/HelpView.js +154 -0
  120. package/src/frontend/components/Input.js +344 -0
  121. package/src/frontend/components/LoadingIndicator.js +31 -0
  122. package/src/frontend/components/ModelListView.js +49 -0
  123. package/src/frontend/components/ModelUpdatedView.js +22 -0
  124. package/src/frontend/components/SessionSpinner.js +66 -0
  125. package/src/frontend/components/SetupWizard.js +242 -0
  126. package/src/frontend/components/StreamOutput.js +34 -0
  127. package/src/frontend/components/TodoList.js +56 -0
  128. package/src/frontend/components/ToolApprovalPrompt.js +452 -0
  129. package/src/frontend/design/themeColors.js +42 -0
  130. package/src/frontend/hooks/useCompletion.js +84 -0
  131. package/src/frontend/hooks/useFileCompletion.js +467 -0
  132. package/src/frontend/hooks/useKeypress.js +145 -0
  133. package/src/frontend/index.js +65 -0
  134. package/src/frontend/utils/GridRenderer.js +140 -0
  135. package/src/frontend/utils/InlineFormatter.js +156 -0
  136. package/src/frontend/utils/diffUtils.js +235 -0
  137. package/src/frontend/utils/inputBuffer.js +441 -0
  138. package/src/frontend/utils/markdownParser.js +377 -0
  139. package/src/frontend/utils/outputRedirector.js +47 -0
  140. package/src/frontend/utils/renderInkComponent.js +42 -0
  141. package/src/frontend/utils/syntaxHighlighter.js +149 -0
  142. package/src/frontend/utils/toolUIFormatter.js +261 -0
  143. package/src/system/agents_loader.js +170 -0
  144. package/src/system/ai_request.js +737 -0
  145. package/src/system/background_process.js +317 -0
  146. package/src/system/code_executer.js +1233 -0
  147. package/src/system/command_loader.js +40 -0
  148. package/src/system/command_parser.js +133 -0
  149. package/src/system/conversation_state.js +265 -0
  150. package/src/system/conversation_trimmer.js +265 -0
  151. package/src/system/custom_command_loader.js +395 -0
  152. package/src/system/file_integrity.js +466 -0
  153. package/src/system/import_analyzer.py +174 -0
  154. package/src/system/log.js +82 -0
  155. package/src/system/mcp_integration.js +304 -0
  156. package/src/system/output_helper.js +89 -0
  157. package/src/system/session.js +1393 -0
  158. package/src/system/session_memory.js +481 -0
  159. package/src/system/skill_loader.js +324 -0
  160. package/src/system/system_info.js +483 -0
  161. package/src/system/tool_approval.js +160 -0
  162. package/src/system/tool_registry.js +184 -0
  163. package/src/system/ui_events.js +279 -0
  164. package/src/tools/code_editor.js +792 -0
  165. package/src/tools/file_reader.js +385 -0
  166. package/src/tools/glob.js +263 -0
  167. package/src/tools/response_message.js +30 -0
  168. package/src/tools/ripgrep.js +554 -0
  169. package/src/tools/skill_tool.js +122 -0
  170. package/src/tools/todo_write.js +182 -0
  171. package/src/tools/web_download.py +74 -0
  172. package/src/tools/web_downloader.js +83 -0
  173. package/src/util/clone.js +174 -0
  174. package/src/util/config.js +203 -0
  175. package/src/util/config_migration.js +174 -0
  176. package/src/util/debug_log.js +49 -0
  177. package/src/util/exit_handler.js +53 -0
  178. package/src/util/file_reference_parser.js +132 -0
  179. package/src/util/mcp_config_manager.js +159 -0
  180. package/src/util/output_formatter.js +50 -0
  181. package/src/util/path_helper.js +27 -0
  182. package/src/util/path_validator.js +178 -0
  183. package/src/util/prompt_loader.js +184 -0
  184. package/src/util/rag_helper.js +101 -0
  185. package/src/util/safe_fs.js +645 -0
  186. package/src/util/setup_wizard.js +62 -0
  187. package/src/util/text_formatter.js +33 -0
  188. package/src/util/version_check.js +116 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * File Completion Hook
3
+ *
4
+ * @ 문자 감지 시 파일/디렉토리 자동완성을 제공합니다.
5
+ * - 경로 모드: @src/ → src 디렉토리 내용 표시
6
+ * - 검색 모드: @glob → 프로젝트 전체에서 "glob" 포함 파일 검색
7
+ */
8
+
9
+ import { useState, useEffect, useCallback, useRef } from 'react';
10
+ import { resolve, dirname, basename, join, relative } from 'path';
11
+ import { safeReaddir, safeStat, safeExists } from '../../util/safe_fs.js';
12
+
13
+ // 검색에서 제외할 디렉토리 (성능 최적화)
14
+ const EXCLUDED_DIRS = new Set([
15
+ 'node_modules',
16
+ '.git',
17
+ '.svn',
18
+ '.hg',
19
+ 'dist',
20
+ 'build',
21
+ 'out',
22
+ 'output',
23
+ 'target',
24
+ '.next',
25
+ '.nuxt',
26
+ '.svelte-kit',
27
+ '__pycache__',
28
+ '.pytest_cache',
29
+ '.mypy_cache',
30
+ '.tox',
31
+ 'venv',
32
+ '.venv',
33
+ 'env',
34
+ '.virtualenv',
35
+ '.cache',
36
+ '.temp',
37
+ '.tmp',
38
+ 'coverage',
39
+ '.nyc_output',
40
+ '.turbo',
41
+ '.parcel-cache',
42
+ '.sass-cache',
43
+ '.idea',
44
+ '.vscode',
45
+ '.vs',
46
+ 'vendor',
47
+ 'Pods',
48
+ '.bundle',
49
+ 'bower_components',
50
+ 'jspm_packages',
51
+ 'bin',
52
+ 'obj',
53
+ 'logs'
54
+ ]);
55
+
56
+ // 검색 최대 깊이
57
+ const MAX_SEARCH_DEPTH = 5;
58
+
59
+ // 검색 최대 결과 수
60
+ const MAX_SEARCH_RESULTS = 20;
61
+
62
+ // 디바운스 시간 (ms)
63
+ const SEARCH_DEBOUNCE_MS = 150;
64
+
65
+ /**
66
+ * 텍스트에서 @ 참조를 파싱합니다.
67
+ * @param {string} text - 전체 텍스트
68
+ * @param {number} cursorPosition - 커서 위치
69
+ * @returns {Object} { isActive, prefix, startIndex, isSearchMode }
70
+ */
71
+ function parseAtReference(text, cursorPosition) {
72
+ const textBeforeCursor = text.slice(0, cursorPosition);
73
+ const match = textBeforeCursor.match(/@([^\s@]*)$/);
74
+
75
+ if (!match) {
76
+ return { isActive: false, prefix: '', startIndex: -1, isSearchMode: false };
77
+ }
78
+
79
+ const prefix = match[1];
80
+
81
+ // 검색 모드 판단: /가 없고 2글자 이상이면 검색 모드
82
+ const isSearchMode = prefix.length >= 2 && !prefix.includes('/');
83
+
84
+ return {
85
+ isActive: true,
86
+ prefix,
87
+ startIndex: match.index,
88
+ isSearchMode
89
+ };
90
+ }
91
+
92
+ /**
93
+ * 재귀적으로 파일을 검색합니다.
94
+ * @param {string} dirPath - 검색할 디렉토리
95
+ * @param {string} searchTerm - 검색어 (소문자)
96
+ * @param {string} basePath - 기준 경로 (상대 경로 계산용)
97
+ * @param {number} depth - 현재 깊이
98
+ * @param {Array} results - 결과 배열 (mutation)
99
+ * @param {number} maxResults - 최대 결과 수
100
+ */
101
+ async function searchFilesRecursive(dirPath, searchTerm, basePath, depth, results, maxResults) {
102
+ // 깊이 제한 또는 결과 수 제한 도달
103
+ if (depth > MAX_SEARCH_DEPTH || results.length >= maxResults) {
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const entries = await safeReaddir(dirPath, { withFileTypes: true });
109
+
110
+ for (const entry of entries) {
111
+ if (results.length >= maxResults) break;
112
+
113
+ const name = entry.name;
114
+
115
+ // 숨김 파일/폴더 제외
116
+ if (name.startsWith('.')) continue;
117
+
118
+ // 제외 디렉토리 체크
119
+ if (entry.isDirectory() && EXCLUDED_DIRS.has(name)) continue;
120
+
121
+ const fullPath = join(dirPath, name);
122
+ const relativePath = relative(basePath, fullPath);
123
+ const nameLower = name.toLowerCase();
124
+
125
+ // 이름에 검색어가 포함되면 결과에 추가
126
+ if (nameLower.includes(searchTerm)) {
127
+ const isDirectory = entry.isDirectory();
128
+ results.push({
129
+ value: '@' + relativePath + (isDirectory ? '/' : ''),
130
+ displayValue: relativePath,
131
+ icon: isDirectory ? '📁' : '📄',
132
+ isDirectory,
133
+ description: isDirectory ? 'Directory' : 'File',
134
+ // 정렬용 점수: 이름이 검색어로 시작하면 높은 점수
135
+ score: nameLower.startsWith(searchTerm) ? 2 : (nameLower === searchTerm ? 3 : 1)
136
+ });
137
+ }
138
+
139
+ // 디렉토리면 재귀 탐색
140
+ if (entry.isDirectory() && results.length < maxResults) {
141
+ await searchFilesRecursive(fullPath, searchTerm, basePath, depth + 1, results, maxResults);
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // 접근 불가능한 디렉토리는 무시
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 프로젝트 전체에서 파일을 검색합니다.
151
+ * @param {string} searchTerm - 검색어
152
+ * @returns {Promise<Array>} 검색 결과
153
+ */
154
+ async function searchFiles(searchTerm) {
155
+ const basePath = process.cwd();
156
+ const searchTermLower = searchTerm.toLowerCase();
157
+ const results = [];
158
+
159
+ await searchFilesRecursive(basePath, searchTermLower, basePath, 0, results, MAX_SEARCH_RESULTS);
160
+
161
+ // 점수순 정렬 (높은 점수 우선), 같은 점수면 경로 길이 짧은 것 우선
162
+ results.sort((a, b) => {
163
+ if (b.score !== a.score) return b.score - a.score;
164
+ if (a.displayValue.length !== b.displayValue.length) {
165
+ return a.displayValue.length - b.displayValue.length;
166
+ }
167
+ return a.displayValue.localeCompare(b.displayValue);
168
+ });
169
+
170
+ return results.slice(0, MAX_SEARCH_RESULTS);
171
+ }
172
+
173
+ /**
174
+ * 디렉토리 내용을 가져와 필터링합니다.
175
+ * @param {string} prefix - @ 뒤의 경로 prefix
176
+ * @returns {Promise<Array>} 자동완성 항목 목록
177
+ */
178
+ async function fetchDirectoryContents(prefix) {
179
+ try {
180
+ let dirPath;
181
+ let filterPrefix;
182
+
183
+ if (!prefix) {
184
+ dirPath = process.cwd();
185
+ filterPrefix = '';
186
+ } else if (prefix.endsWith('/')) {
187
+ dirPath = resolve(process.cwd(), prefix);
188
+ filterPrefix = '';
189
+ } else {
190
+ const parentDir = dirname(prefix);
191
+ filterPrefix = basename(prefix).toLowerCase();
192
+
193
+ if (parentDir === '.' || parentDir === '') {
194
+ dirPath = process.cwd();
195
+ } else {
196
+ dirPath = resolve(process.cwd(), parentDir);
197
+ }
198
+ }
199
+
200
+ const exists = await safeExists(dirPath);
201
+ if (!exists) return [];
202
+
203
+ const stat = await safeStat(dirPath);
204
+ if (!stat.isDirectory()) return [];
205
+
206
+ const entries = await safeReaddir(dirPath, { withFileTypes: true });
207
+ const results = [];
208
+
209
+ for (const entry of entries) {
210
+ const name = entry.name;
211
+
212
+ if (name.startsWith('.')) continue;
213
+
214
+ if (filterPrefix && !name.toLowerCase().startsWith(filterPrefix)) continue;
215
+
216
+ const isDirectory = entry.isDirectory();
217
+
218
+ let completePath;
219
+ if (!prefix) {
220
+ completePath = name;
221
+ } else if (prefix.endsWith('/')) {
222
+ completePath = prefix + name;
223
+ } else {
224
+ const parentDir = dirname(prefix);
225
+ if (parentDir === '.' || parentDir === '') {
226
+ completePath = name;
227
+ } else {
228
+ completePath = join(parentDir, name);
229
+ }
230
+ }
231
+
232
+ results.push({
233
+ value: '@' + completePath + (isDirectory ? '/' : ''),
234
+ displayValue: name,
235
+ icon: isDirectory ? '📁' : '📄',
236
+ isDirectory,
237
+ description: isDirectory ? 'Directory' : 'File'
238
+ });
239
+ }
240
+
241
+ results.sort((a, b) => {
242
+ if (a.isDirectory && !b.isDirectory) return -1;
243
+ if (!a.isDirectory && b.isDirectory) return 1;
244
+ return a.displayValue.localeCompare(b.displayValue);
245
+ });
246
+
247
+ return results.slice(0, 20);
248
+
249
+ } catch (error) {
250
+ return [];
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 파일 자동완성 훅
256
+ * @param {Object} buffer - Input buffer 객체
257
+ * @returns {Object} 자동완성 상태 및 핸들러
258
+ */
259
+ export function useFileCompletion(buffer) {
260
+ const [suggestions, setSuggestions] = useState([]);
261
+ const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1);
262
+ const [showSuggestions, setShowSuggestions] = useState(false);
263
+ const [atReference, setAtReference] = useState({ isActive: false, prefix: '', startIndex: -1, isSearchMode: false });
264
+ const lastTextRef = useRef('');
265
+ const fetchingRef = useRef(false);
266
+ const debounceTimerRef = useRef(null);
267
+ const lastSearchTermRef = useRef('');
268
+
269
+ useEffect(() => {
270
+ const text = buffer.text;
271
+
272
+ if (text === lastTextRef.current) {
273
+ return;
274
+ }
275
+ lastTextRef.current = text;
276
+
277
+ // 커서 위치 계산
278
+ const [row, col] = buffer.cursor;
279
+ const lines = buffer.lines;
280
+ let cursorPosition = 0;
281
+ for (let i = 0; i < row; i++) {
282
+ cursorPosition += lines[i].length + 1;
283
+ }
284
+ cursorPosition += col;
285
+
286
+ const ref = parseAtReference(text, cursorPosition);
287
+ setAtReference(ref);
288
+
289
+ if (!ref.isActive) {
290
+ if (showSuggestions) {
291
+ setSuggestions([]);
292
+ setShowSuggestions(false);
293
+ setActiveSuggestionIndex(-1);
294
+ }
295
+ // 디바운스 타이머 정리
296
+ if (debounceTimerRef.current) {
297
+ clearTimeout(debounceTimerRef.current);
298
+ debounceTimerRef.current = null;
299
+ }
300
+ return;
301
+ }
302
+
303
+ // 검색 모드
304
+ if (ref.isSearchMode) {
305
+ // 같은 검색어면 스킵
306
+ if (ref.prefix === lastSearchTermRef.current) {
307
+ return;
308
+ }
309
+
310
+ // 기존 타이머 취소
311
+ if (debounceTimerRef.current) {
312
+ clearTimeout(debounceTimerRef.current);
313
+ }
314
+
315
+ // 디바운스 적용
316
+ debounceTimerRef.current = setTimeout(async () => {
317
+ if (fetchingRef.current) return;
318
+
319
+ fetchingRef.current = true;
320
+ lastSearchTermRef.current = ref.prefix;
321
+
322
+ try {
323
+ const results = await searchFiles(ref.prefix);
324
+
325
+ // 텍스트가 변경되었으면 무시
326
+ if (buffer.text !== lastTextRef.current) {
327
+ fetchingRef.current = false;
328
+ return;
329
+ }
330
+
331
+ setSuggestions(results);
332
+ setShowSuggestions(results.length > 0);
333
+ setActiveSuggestionIndex(results.length > 0 ? 0 : -1);
334
+ } catch (error) {
335
+ // 에러 무시
336
+ } finally {
337
+ fetchingRef.current = false;
338
+ }
339
+ }, SEARCH_DEBOUNCE_MS);
340
+
341
+ return;
342
+ }
343
+
344
+ // 경로 모드 (기존 로직)
345
+ lastSearchTermRef.current = '';
346
+
347
+ if (fetchingRef.current) {
348
+ return;
349
+ }
350
+
351
+ fetchingRef.current = true;
352
+ fetchDirectoryContents(ref.prefix).then(results => {
353
+ fetchingRef.current = false;
354
+
355
+ if (buffer.text !== lastTextRef.current) {
356
+ return;
357
+ }
358
+
359
+ setSuggestions(results);
360
+ setShowSuggestions(results.length > 0);
361
+ setActiveSuggestionIndex(results.length > 0 ? 0 : -1);
362
+ }).catch(() => {
363
+ fetchingRef.current = false;
364
+ });
365
+
366
+ }, [buffer.text, buffer.cursor, buffer.lines, showSuggestions]);
367
+
368
+ // 컴포넌트 언마운트 시 타이머 정리
369
+ useEffect(() => {
370
+ return () => {
371
+ if (debounceTimerRef.current) {
372
+ clearTimeout(debounceTimerRef.current);
373
+ }
374
+ };
375
+ }, []);
376
+
377
+ const navigateUp = useCallback(() => {
378
+ setActiveSuggestionIndex(prev =>
379
+ prev > 0 ? prev - 1 : suggestions.length - 1
380
+ );
381
+ }, [suggestions.length]);
382
+
383
+ const navigateDown = useCallback(() => {
384
+ setActiveSuggestionIndex(prev =>
385
+ prev < suggestions.length - 1 ? prev + 1 : 0
386
+ );
387
+ }, [suggestions.length]);
388
+
389
+ const handleAutocomplete = useCallback((index) => {
390
+ if (index >= 0 && index < suggestions.length) {
391
+ const suggestion = suggestions[index];
392
+ const text = buffer.text;
393
+
394
+ const beforeAt = text.slice(0, atReference.startIndex);
395
+
396
+ const [row, col] = buffer.cursor;
397
+ const lines = buffer.lines;
398
+ let cursorPosition = 0;
399
+ for (let i = 0; i < row; i++) {
400
+ cursorPosition += lines[i].length + 1;
401
+ }
402
+ cursorPosition += col;
403
+ const afterCursor = text.slice(cursorPosition);
404
+
405
+ let newValue = suggestion.value;
406
+
407
+ if (!suggestion.isDirectory) {
408
+ newValue += ' ';
409
+ }
410
+
411
+ const newText = beforeAt + newValue + afterCursor;
412
+ buffer.setText(newText);
413
+
414
+ const newCursorPos = beforeAt.length + newValue.length;
415
+
416
+ let pos = 0;
417
+ let targetRow = 0;
418
+ let targetCol = 0;
419
+ const newLines = newText.split('\n');
420
+ for (let i = 0; i < newLines.length; i++) {
421
+ if (pos + newLines[i].length >= newCursorPos) {
422
+ targetRow = i;
423
+ targetCol = newCursorPos - pos;
424
+ break;
425
+ }
426
+ pos += newLines[i].length + 1;
427
+ }
428
+
429
+ if (typeof buffer.setCursor === 'function') {
430
+ buffer.setCursor(targetRow, targetCol);
431
+ } else {
432
+ buffer.move('end');
433
+ }
434
+
435
+ if (suggestion.isDirectory) {
436
+ // 디렉토리 선택 시 검색 모드 해제
437
+ lastSearchTermRef.current = '';
438
+ } else {
439
+ setSuggestions([]);
440
+ setShowSuggestions(false);
441
+ setActiveSuggestionIndex(-1);
442
+ }
443
+ }
444
+ }, [buffer, suggestions, atReference]);
445
+
446
+ const resetCompletionState = useCallback(() => {
447
+ setSuggestions([]);
448
+ setShowSuggestions(false);
449
+ setActiveSuggestionIndex(-1);
450
+ setAtReference({ isActive: false, prefix: '', startIndex: -1, isSearchMode: false });
451
+ lastSearchTermRef.current = '';
452
+ if (debounceTimerRef.current) {
453
+ clearTimeout(debounceTimerRef.current);
454
+ debounceTimerRef.current = null;
455
+ }
456
+ }, []);
457
+
458
+ return {
459
+ suggestions,
460
+ activeSuggestionIndex,
461
+ showSuggestions,
462
+ navigateUp,
463
+ navigateDown,
464
+ handleAutocomplete,
465
+ resetCompletionState
466
+ };
467
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Keypress hook for handling keyboard input
3
+ */
4
+
5
+ import { useInput } from 'ink';
6
+
7
+ export function useKeypress(handler, options = {}) {
8
+ const { isActive = true } = options;
9
+
10
+ useInput((input, key) => {
11
+ if (!isActive) return;
12
+
13
+ // Fix backspace detection - Ink has serious bugs
14
+ // Workaround: Ignore empty sequence events and wait for valid ones
15
+ let actualBackspace = false;
16
+ let actualDelete = false;
17
+
18
+ // Check key.name first (most reliable when present)
19
+ if (key.name === 'backspace') {
20
+ actualBackspace = true;
21
+ } else if (key.name === 'delete') {
22
+ actualDelete = true;
23
+ }
24
+ // If key.name is undefined, check the original flags
25
+ else if (key.backspace === true) {
26
+ // For backspace, only accept if there's sequence data (Ink bug workaround)
27
+ if (input && input.length > 0) {
28
+ actualBackspace = true;
29
+ } else {
30
+ // Ignore spurious empty backspace events
31
+ return;
32
+ }
33
+ } else if (key.delete === true) {
34
+ // WORKAROUND: In some terminals, backspace is mapped to delete with empty input
35
+ // If delete flag is set but there's no input sequence, treat it as backspace
36
+ if (!input || input.length === 0) {
37
+ actualBackspace = true;
38
+ } else {
39
+ actualDelete = true;
40
+ }
41
+ }
42
+ // Last resort: check input sequence
43
+ else if (input && input.length > 0) {
44
+ const charCode = input.charCodeAt(0);
45
+ // Backspace: \x7F (127) or \x08 (8)
46
+ if (charCode === 127 || charCode === 8) {
47
+ actualBackspace = true;
48
+ }
49
+ // Delete: ESC[3~
50
+ else if (input === '\x1B[3~' || input.startsWith('\x1B[3')) {
51
+ actualDelete = true;
52
+ }
53
+ }
54
+
55
+ // Count multiple backspaces/deletes in the sequence (for fast deletion)
56
+ let repeatCount = 1;
57
+ if ((actualBackspace || actualDelete) && input && input.length > 1) {
58
+ // Count how many backspace/delete characters are in the sequence
59
+ if (actualBackspace) {
60
+ repeatCount = Array.from(input).filter(c =>
61
+ c.charCodeAt(0) === 127 || c.charCodeAt(0) === 8
62
+ ).length;
63
+ }
64
+ // For delete, it's always 1 (escape sequences don't batch)
65
+ }
66
+
67
+ // Detect Ctrl+C/D when name is undefined
68
+ let detectedName = key.name;
69
+ if (key.ctrl && !detectedName && input) {
70
+ // In Ink's raw mode, Ctrl+C comes as input="c" with ctrl=true
71
+ // not as charCode 3
72
+ if (input.toLowerCase() === 'c') {
73
+ detectedName = 'c';
74
+ } else if (input.toLowerCase() === 'd') {
75
+ detectedName = 'd';
76
+ } else {
77
+ // Fallback: check for actual control characters
78
+ const charCode = input.charCodeAt(0);
79
+ if (charCode === 3) {
80
+ detectedName = 'c';
81
+ } else if (charCode === 4) {
82
+ detectedName = 'd';
83
+ }
84
+ }
85
+ }
86
+
87
+ // Normalize key object to prevent undefined issues
88
+ const normalizedKey = {
89
+ ...key,
90
+ sequence: input,
91
+ name: key.return ? 'return' : detectedName,
92
+ ctrl: key.ctrl ?? false,
93
+ meta: key.meta ?? false,
94
+ shift: key.shift ?? false,
95
+ escape: key.escape ?? false,
96
+ return: key.return ?? false,
97
+ upArrow: key.upArrow ?? false,
98
+ downArrow: key.downArrow ?? false,
99
+ leftArrow: key.leftArrow ?? false,
100
+ rightArrow: key.rightArrow ?? false,
101
+ backspace: actualBackspace, // Use corrected value
102
+ delete: actualDelete, // Use corrected value
103
+ repeatCount: repeatCount, // NEW: How many times to repeat
104
+ tab: key.tab ?? false,
105
+ home: key.home ?? false,
106
+ end: key.end ?? false,
107
+ pageUp: key.pageUp ?? false,
108
+ pageDown: key.pageDown ?? false,
109
+ paste: key.paste ?? false, // Important for paste detection
110
+ };
111
+
112
+ handler(normalizedKey);
113
+ }, { isActive });
114
+ }
115
+
116
+ /**
117
+ * Key matcher functions
118
+ */
119
+ export const keyMatchers = {
120
+ SUBMIT: (key) => key.return && !key.shift && !key.ctrl && !key.meta,
121
+ NEWLINE: (key) => key.return && key.shift,
122
+ ESCAPE: (key) => key.escape,
123
+ NAVIGATION_UP: (key) => key.upArrow && !key.ctrl,
124
+ NAVIGATION_DOWN: (key) => key.downArrow && !key.ctrl,
125
+ HISTORY_UP: (key) => key.upArrow && !key.ctrl,
126
+ HISTORY_DOWN: (key) => key.downArrow && !key.ctrl,
127
+ COMPLETION_UP: (key) => key.upArrow && key.ctrl,
128
+ COMPLETION_DOWN: (key) => key.downArrow && key.ctrl,
129
+ ACCEPT_SUGGESTION: (key) => key.tab,
130
+ HOME: (key) => (key.name === 'a' && key.ctrl) || key.home,
131
+ END: (key) => (key.name === 'e' && key.ctrl) || key.end,
132
+ CLEAR_INPUT: (key) => key.name === 'c' && key.ctrl && !key.meta,
133
+ CLEAR_SCREEN: (key) => key.name === 'l' && key.ctrl,
134
+ KILL_LINE_RIGHT: (key) => key.name === 'k' && key.ctrl,
135
+ KILL_LINE_LEFT: (key) => key.name === 'u' && key.ctrl,
136
+ DELETE_WORD_BACKWARD: (key) => key.name === 'w' && key.ctrl,
137
+ QUIT: (key) => key.name === 'c' && key.ctrl,
138
+ EXIT: (key) => key.name === 'd' && key.ctrl,
139
+ REVERSE_SEARCH: (key) => key.name === 'r' && key.ctrl,
140
+ };
141
+
142
+ export const Command = Object.keys(keyMatchers).reduce((acc, key) => {
143
+ acc[key] = key;
144
+ return acc;
145
+ }, {});
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Ink UI entry point
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import { App } from './App.js';
8
+ import { OutputRedirector } from './utils/outputRedirector.js';
9
+ import { createDebugLogger } from '../util/debug_log.js';
10
+
11
+ const debugLog = createDebugLogger('ui.log', 'index');
12
+
13
+ export function startUI({ onSubmit, onClearScreen, onExit, commands = [], model = 'gpt-4', version = '1.0.0', initialHistory = [], reasoningEffort = null }) {
14
+ debugLog(`startUI called - model: ${model}, version: ${version}, commands: ${commands.length}, initialHistory: ${initialHistory.length}`);
15
+
16
+ // Patch console methods to prevent interference with Ink rendering
17
+ const outputRedirector = new OutputRedirector();
18
+ outputRedirector.patch();
19
+ debugLog('OutputRedirector patched');
20
+
21
+ // Disable terminal line wrapping to reduce rendering artifacts
22
+ // This prevents the terminal from wrapping lines itself, letting Ink handle all wrapping
23
+ process.stdout.write('\x1b[?7l');
24
+ debugLog('Terminal line wrapping disabled');
25
+
26
+ const { unmount, waitUntilExit } = render(
27
+ React.createElement(App, {
28
+ onSubmit,
29
+ onClearScreen,
30
+ onExit,
31
+ commands,
32
+ model,
33
+ version,
34
+ initialHistory,
35
+ reasoningEffort
36
+ }),
37
+ {
38
+ // Use patchConsole option to prevent console output from interfering
39
+ patchConsole: false, // We're handling this manually with OutputRedirector
40
+ // Don't exit on Ctrl+C - let the app handle it
41
+ exitOnCtrlC: false,
42
+ // Debug mode ON - prevents clearing terminal and preserves previous content
43
+ debug: false
44
+ }
45
+ );
46
+
47
+ debugLog('UI render completed successfully');
48
+
49
+ // Re-enable line wrapping and restore console on cleanup
50
+ const originalUnmount = unmount;
51
+ const wrappedUnmount = () => {
52
+ debugLog('wrappedUnmount called - cleaning up UI');
53
+ // Re-enable line wrapping
54
+ process.stdout.write('\x1b[?7h');
55
+ outputRedirector.cleanup();
56
+ debugLog('OutputRedirector cleanup completed');
57
+ originalUnmount();
58
+ debugLog('UI unmounted');
59
+ };
60
+
61
+ return {
62
+ unmount: wrappedUnmount,
63
+ waitUntilExit
64
+ };
65
+ }