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.
- package/LICENSE +68 -0
- package/README.md +347 -0
- package/config_template/mcp_config.json +3 -0
- package/config_template/package_name_store.json +5 -0
- package/config_template/settings.json +5 -0
- package/index.js +879 -0
- package/mcp-agent-lib/example/01-basic-usage.js +82 -0
- package/mcp-agent-lib/example/02-quick-start.js +52 -0
- package/mcp-agent-lib/example/03-http-server.js +76 -0
- package/mcp-agent-lib/example/04-multiple-servers.js +117 -0
- package/mcp-agent-lib/example/05-error-handling.js +116 -0
- package/mcp-agent-lib/example/06-resources-and-prompts.js +174 -0
- package/mcp-agent-lib/example/07-advanced-configuration.js +191 -0
- package/mcp-agent-lib/example/08-real-world-chatbot.js +331 -0
- package/mcp-agent-lib/example/README.md +346 -0
- package/mcp-agent-lib/index.js +19 -0
- package/mcp-agent-lib/init.sh +3 -0
- package/mcp-agent-lib/package-lock.json +1216 -0
- package/mcp-agent-lib/package.json +53 -0
- package/mcp-agent-lib/sampleFastMCPClient/client.py +25 -0
- package/mcp-agent-lib/sampleFastMCPClient/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/server.py +12 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/server.py +43 -0
- package/mcp-agent-lib/sampleFastMCPServerRootsRequest/server.py +63 -0
- package/mcp-agent-lib/sampleMCPHost/index.js +386 -0
- package/mcp-agent-lib/sampleMCPHost/mcp_config.json +24 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/elicitation.js +151 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/index.js +166 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/roots.js +197 -0
- package/mcp-agent-lib/src/mcp_client.js +1860 -0
- package/mcp-agent-lib/src/mcp_message_logger.js +517 -0
- package/package.json +72 -0
- package/payload_viewer/out/404/index.html +1 -0
- package/payload_viewer/out/404.html +1 -0
- package/payload_viewer/out/_next/static/chunks/060f9a97930f3d04.js +1 -0
- package/payload_viewer/out/_next/static/chunks/103c802c8f4a5ea1.js +1 -0
- package/payload_viewer/out/_next/static/chunks/16474fd6c6910c45.js +1 -0
- package/payload_viewer/out/_next/static/chunks/17722e3ac4e00587.js +1 -0
- package/payload_viewer/out/_next/static/chunks/305b077a9873cf54.js +1 -0
- package/payload_viewer/out/_next/static/chunks/4c1d05c6741c2bdd.js +5 -0
- package/payload_viewer/out/_next/static/chunks/538cc02e54714b23.js +1 -0
- package/payload_viewer/out/_next/static/chunks/6251fa5907d2b226.js +5 -0
- package/payload_viewer/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- package/payload_viewer/out/_next/static/chunks/b6c0459f3789d25c.js +1 -0
- package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
- package/payload_viewer/out/_next/static/chunks/bd2dcf98c9b362f6.js +1 -0
- package/payload_viewer/out/_next/static/chunks/c8a542ae21335479.js +1 -0
- package/payload_viewer/out/_next/static/chunks/cdd12d5c1a5a6064.js +1 -0
- package/payload_viewer/out/_next/static/chunks/e411019f55d87c42.js +1 -0
- package/payload_viewer/out/_next/static/chunks/e60ef129113f6e24.js +1 -0
- package/payload_viewer/out/_next/static/chunks/f1ac9047ac4a3fde.js +1 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-0ac29803ce3c3c7a.js +3 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-89db4c64206a73e4.js +3 -0
- package/payload_viewer/out/_next/static/chunks/turbopack-a5b8235fa59d7119.js +3 -0
- package/payload_viewer/out/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- package/payload_viewer/out/_next/static/media/favicon.0b3bf435.ico +0 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_buildManifest.js +14 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_clientMiddlewareManifest.json +1 -0
- package/payload_viewer/out/_next/static/uqQYtKpKV7kCSkUbLgdfJ/_ssgManifest.js +1 -0
- package/payload_viewer/out/favicon.ico +0 -0
- package/payload_viewer/out/file.svg +1 -0
- package/payload_viewer/out/globe.svg +1 -0
- package/payload_viewer/out/index.html +1 -0
- package/payload_viewer/out/index.txt +23 -0
- package/payload_viewer/out/next.svg +1 -0
- package/payload_viewer/out/vercel.svg +1 -0
- package/payload_viewer/out/window.svg +1 -0
- package/payload_viewer/web_server.js +861 -0
- package/prompts/completion_judge.txt +128 -0
- package/prompts/orchestrator.txt +1213 -0
- package/src/LLMClient/client.js +1375 -0
- package/src/LLMClient/converters/input-normalizer.js +238 -0
- package/src/LLMClient/converters/responses-to-claude.js +503 -0
- package/src/LLMClient/converters/responses-to-gemini.js +648 -0
- package/src/LLMClient/converters/responses-to-ollama.js +348 -0
- package/src/LLMClient/converters/responses-to-zai.js +667 -0
- package/src/LLMClient/errors.js +398 -0
- package/src/LLMClient/index.js +36 -0
- package/src/ai_based/completion_judge.js +421 -0
- package/src/ai_based/orchestrator.js +527 -0
- package/src/ai_based/pip_package_installer.js +173 -0
- package/src/ai_based/pip_package_lookup.js +197 -0
- package/src/cli/mcp_cli.js +70 -0
- package/src/cli/mcp_commands.js +255 -0
- package/src/commands/agents.js +18 -0
- package/src/commands/apikey.js +55 -0
- package/src/commands/bg.js +140 -0
- package/src/commands/commands.js +56 -0
- package/src/commands/debug.js +54 -0
- package/src/commands/exit.js +19 -0
- package/src/commands/help.js +35 -0
- package/src/commands/mcp.js +128 -0
- package/src/commands/model.js +176 -0
- package/src/commands/setup.js +13 -0
- package/src/commands/skills.js +51 -0
- package/src/commands/tools.js +165 -0
- package/src/commands/viewer.js +147 -0
- package/src/config/ai_models.js +312 -0
- package/src/config/config.js +10 -0
- package/src/config/constants.js +71 -0
- package/src/config/feature_flags.js +15 -0
- package/src/frontend/App.js +1263 -0
- package/src/frontend/README.md +81 -0
- package/src/frontend/components/AutocompleteMenu.js +47 -0
- package/src/frontend/components/BackgroundProcessList.js +175 -0
- package/src/frontend/components/BlankLine.js +62 -0
- package/src/frontend/components/ConversationItem.js +893 -0
- package/src/frontend/components/CurrentModelView.js +43 -0
- package/src/frontend/components/FileDiffViewer.js +616 -0
- package/src/frontend/components/Footer.js +25 -0
- package/src/frontend/components/Header.js +42 -0
- package/src/frontend/components/HelpView.js +154 -0
- package/src/frontend/components/Input.js +344 -0
- package/src/frontend/components/LoadingIndicator.js +31 -0
- package/src/frontend/components/ModelListView.js +49 -0
- package/src/frontend/components/ModelUpdatedView.js +22 -0
- package/src/frontend/components/SessionSpinner.js +66 -0
- package/src/frontend/components/SetupWizard.js +242 -0
- package/src/frontend/components/StreamOutput.js +34 -0
- package/src/frontend/components/TodoList.js +56 -0
- package/src/frontend/components/ToolApprovalPrompt.js +452 -0
- package/src/frontend/design/themeColors.js +42 -0
- package/src/frontend/hooks/useCompletion.js +84 -0
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/frontend/hooks/useKeypress.js +145 -0
- package/src/frontend/index.js +65 -0
- package/src/frontend/utils/GridRenderer.js +140 -0
- package/src/frontend/utils/InlineFormatter.js +156 -0
- package/src/frontend/utils/diffUtils.js +235 -0
- package/src/frontend/utils/inputBuffer.js +441 -0
- package/src/frontend/utils/markdownParser.js +377 -0
- package/src/frontend/utils/outputRedirector.js +47 -0
- package/src/frontend/utils/renderInkComponent.js +42 -0
- package/src/frontend/utils/syntaxHighlighter.js +149 -0
- package/src/frontend/utils/toolUIFormatter.js +261 -0
- package/src/system/agents_loader.js +170 -0
- package/src/system/ai_request.js +737 -0
- package/src/system/background_process.js +317 -0
- package/src/system/code_executer.js +1233 -0
- package/src/system/command_loader.js +40 -0
- package/src/system/command_parser.js +133 -0
- package/src/system/conversation_state.js +265 -0
- package/src/system/conversation_trimmer.js +265 -0
- package/src/system/custom_command_loader.js +395 -0
- package/src/system/file_integrity.js +466 -0
- package/src/system/import_analyzer.py +174 -0
- package/src/system/log.js +82 -0
- package/src/system/mcp_integration.js +304 -0
- package/src/system/output_helper.js +89 -0
- package/src/system/session.js +1393 -0
- package/src/system/session_memory.js +481 -0
- package/src/system/skill_loader.js +324 -0
- package/src/system/system_info.js +483 -0
- package/src/system/tool_approval.js +160 -0
- package/src/system/tool_registry.js +184 -0
- package/src/system/ui_events.js +279 -0
- package/src/tools/code_editor.js +792 -0
- package/src/tools/file_reader.js +385 -0
- package/src/tools/glob.js +263 -0
- package/src/tools/response_message.js +30 -0
- package/src/tools/ripgrep.js +554 -0
- package/src/tools/skill_tool.js +122 -0
- package/src/tools/todo_write.js +182 -0
- package/src/tools/web_download.py +74 -0
- package/src/tools/web_downloader.js +83 -0
- package/src/util/clone.js +174 -0
- package/src/util/config.js +203 -0
- package/src/util/config_migration.js +174 -0
- package/src/util/debug_log.js +49 -0
- package/src/util/exit_handler.js +53 -0
- package/src/util/file_reference_parser.js +132 -0
- package/src/util/mcp_config_manager.js +159 -0
- package/src/util/output_formatter.js +50 -0
- package/src/util/path_helper.js +27 -0
- package/src/util/path_validator.js +178 -0
- package/src/util/prompt_loader.js +184 -0
- package/src/util/rag_helper.js +101 -0
- package/src/util/safe_fs.js +645 -0
- package/src/util/setup_wizard.js +62 -0
- package/src/util/text_formatter.js +33 -0
- 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
|
+
}
|