aiexecode 1.0.96 → 1.0.100
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +8 -21
- package/index.js +10 -8
- package/package.json +2 -2
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/src/LLMClient/converters/responses-to-zai.js +61 -2
- package/src/LLMClient/errors.js +12 -0
- package/src/commands/apikey.js +1 -1
- package/src/commands/model.js +1 -2
- package/src/commands/reasoning_effort.js +2 -2
- package/src/config/ai_models.js +17 -8
- package/src/frontend/App.js +8 -0
- package/src/frontend/components/AutocompleteMenu.js +7 -1
- package/src/frontend/components/CurrentModelView.js +2 -2
- package/src/frontend/components/Input.js +33 -11
- package/src/frontend/components/ModelListView.js +1 -1
- package/src/frontend/components/SetupWizard.js +4 -6
- package/src/frontend/hooks/useFileCompletion.js +467 -0
- package/src/system/ai_request.js +28 -3
- package/src/util/file_reference_parser.js +132 -0
- /package/payload_viewer/out/_next/static/{lHmNygVpv4N1VR0LdnwkJ → FMDjqqcDAb3kUlfyFCuUn}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{lHmNygVpv4N1VR0LdnwkJ → FMDjqqcDAb3kUlfyFCuUn}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{lHmNygVpv4N1VR0LdnwkJ → FMDjqqcDAb3kUlfyFCuUn}/_ssgManifest.js +0 -0
|
@@ -8,11 +8,11 @@ import { theme } from '../design/themeColors.js';
|
|
|
8
8
|
|
|
9
9
|
export function CurrentModelView({ provider, modelId, modelInfo }) {
|
|
10
10
|
const providerColors = {
|
|
11
|
-
|
|
11
|
+
zai: 'cyan'
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const providerEmojis = {
|
|
15
|
-
|
|
15
|
+
zai: '🤖'
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
return React.createElement(Box, {
|
|
@@ -7,6 +7,7 @@ import { Box, Text } from 'ink';
|
|
|
7
7
|
import { theme } from '../design/themeColors.js';
|
|
8
8
|
import { useKeypress, keyMatchers, Command } from '../hooks/useKeypress.js';
|
|
9
9
|
import { useCompletion } from '../hooks/useCompletion.js';
|
|
10
|
+
import { useFileCompletion } from '../hooks/useFileCompletion.js';
|
|
10
11
|
import { AutocompleteMenu } from './AutocompleteMenu.js';
|
|
11
12
|
import { cpSlice, cpLen } from '../utils/inputBuffer.js';
|
|
12
13
|
import { uiEvents } from '../../system/ui_events.js';
|
|
@@ -48,18 +49,31 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
|
|
|
48
49
|
// placeholder가 제공되지 않으면 무작위로 선택 (컴포넌트 마운트 시 한 번만)
|
|
49
50
|
const defaultPlaceholder = useMemo(() => placeholder || getRandomPlaceholder(), []);
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
+
const commandCompletionRaw = useCompletion(buffer, commands);
|
|
53
|
+
const fileCompletionRaw = useFileCompletion(buffer);
|
|
54
|
+
|
|
55
|
+
// Stabilize completion objects to prevent handleInput from being recreated
|
|
56
|
+
const commandCompletion = useMemo(() => commandCompletionRaw, [
|
|
57
|
+
commandCompletionRaw.showSuggestions,
|
|
58
|
+
commandCompletionRaw.suggestions.length,
|
|
59
|
+
commandCompletionRaw.activeSuggestionIndex,
|
|
60
|
+
commandCompletionRaw.handleAutocomplete,
|
|
61
|
+
commandCompletionRaw.navigateUp,
|
|
62
|
+
commandCompletionRaw.navigateDown
|
|
63
|
+
]);
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
completionRaw.navigateDown
|
|
65
|
+
const fileCompletion = useMemo(() => fileCompletionRaw, [
|
|
66
|
+
fileCompletionRaw.showSuggestions,
|
|
67
|
+
fileCompletionRaw.suggestions.length,
|
|
68
|
+
fileCompletionRaw.activeSuggestionIndex,
|
|
69
|
+
fileCompletionRaw.handleAutocomplete,
|
|
70
|
+
fileCompletionRaw.navigateUp,
|
|
71
|
+
fileCompletionRaw.navigateDown
|
|
61
72
|
]);
|
|
62
73
|
|
|
74
|
+
// 파일 자동완성 우선, 그 다음 명령어 자동완성
|
|
75
|
+
const completion = fileCompletion.showSuggestions ? fileCompletion : commandCompletion;
|
|
76
|
+
|
|
63
77
|
|
|
64
78
|
const handleSubmitAndClear = useCallback((submittedValue) => {
|
|
65
79
|
buffer.setText('');
|
|
@@ -139,9 +153,17 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
|
|
|
139
153
|
// If suggestions are shown, accept the active suggestion
|
|
140
154
|
if (completion.showSuggestions && completion.suggestions.length > 0) {
|
|
141
155
|
const targetIndex = completion.activeSuggestionIndex === -1 ? 0 : completion.activeSuggestionIndex;
|
|
156
|
+
const suggestion = completion.suggestions[targetIndex];
|
|
157
|
+
|
|
158
|
+
// 파일 자동완성인 경우 (@로 시작) 완성만 하고 submit하지 않음
|
|
159
|
+
if (suggestion.value.startsWith('@')) {
|
|
160
|
+
completion.handleAutocomplete(targetIndex);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 명령어 자동완성인 경우 완성 후 바로 실행
|
|
142
165
|
completion.handleAutocomplete(targetIndex);
|
|
143
|
-
|
|
144
|
-
const completedCommand = completion.suggestions[targetIndex].value;
|
|
166
|
+
const completedCommand = suggestion.value;
|
|
145
167
|
handleSubmitAndClear(completedCommand);
|
|
146
168
|
return;
|
|
147
169
|
}
|
|
@@ -40,7 +40,7 @@ export function ModelListView({ modelsByProvider }) {
|
|
|
40
40
|
React.createElement(Text, { key: 'spacer2' }, null),
|
|
41
41
|
React.createElement(Text, { key: 'usage', bold: true }, 'Usage:'),
|
|
42
42
|
React.createElement(Text, { key: 'usage-cmd' }, ' /model <model-id>'),
|
|
43
|
-
React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model
|
|
43
|
+
React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model glm-4.5')
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
return React.createElement(Box, {
|
|
@@ -15,10 +15,8 @@ import { AI_MODELS, getAllModelIds, getModelsByProvider, DEFAULT_MODEL } from '.
|
|
|
15
15
|
function detectProviderFromApiKey(apiKey) {
|
|
16
16
|
if (!apiKey || typeof apiKey !== 'string') return null;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} else if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
|
|
21
|
-
// Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
|
|
18
|
+
// Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
|
|
19
|
+
if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
|
|
22
20
|
return 'zai';
|
|
23
21
|
}
|
|
24
22
|
return null;
|
|
@@ -73,7 +71,7 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
73
71
|
const provider = detectProviderFromApiKey(apiKey);
|
|
74
72
|
if (!provider) {
|
|
75
73
|
// 유효하지 않은 API 키 형식
|
|
76
|
-
setErrorMessage('Invalid API key. Please use
|
|
74
|
+
setErrorMessage('Invalid API key format. Please use Z.AI API key.');
|
|
77
75
|
setTextInput('');
|
|
78
76
|
return;
|
|
79
77
|
}
|
|
@@ -188,7 +186,7 @@ export function SetupWizard({ onComplete, onCancel }) {
|
|
|
188
186
|
case STEPS.API_KEY:
|
|
189
187
|
return React.createElement(Box, { flexDirection: 'column' },
|
|
190
188
|
React.createElement(Text, { bold: true, color: theme.text.accent }, '1. API Key:'),
|
|
191
|
-
React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://
|
|
189
|
+
React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://z.ai/manage-apikey/apikey-list'),
|
|
192
190
|
React.createElement(Text, null),
|
|
193
191
|
React.createElement(Box, {
|
|
194
192
|
borderStyle: 'round',
|
|
@@ -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
|
+
}
|
package/src/system/ai_request.js
CHANGED
|
@@ -704,21 +704,46 @@ export async function request(taskName, requestPayload) {
|
|
|
704
704
|
*
|
|
705
705
|
* @see https://platform.openai.com/docs/guides/error-codes
|
|
706
706
|
*/
|
|
707
|
+
/*
|
|
708
|
+
* Z.AI/GLM 컨텍스트 초과 에러 샘플:
|
|
709
|
+
* {
|
|
710
|
+
* "error": {
|
|
711
|
+
* "code": "1210",
|
|
712
|
+
* "message": "Invalid API parameter, please check the documentation.Request 320006 input tokens exceeds the model's maximum context length 202750"
|
|
713
|
+
* },
|
|
714
|
+
* "request_id": "20260124000100026f0914d2e744f5"
|
|
715
|
+
* }
|
|
716
|
+
* HTTP_STATUS: 400
|
|
717
|
+
*/
|
|
707
718
|
export function shouldRetryWithTrim(error) {
|
|
708
719
|
if (!error) return false;
|
|
709
720
|
|
|
710
|
-
//
|
|
721
|
+
// 에러 코드 및 메시지 추출
|
|
711
722
|
const errorCode = error?.code || error?.error?.code;
|
|
723
|
+
const errorMessage = error?.message || error?.error?.message || '';
|
|
712
724
|
|
|
725
|
+
// OpenAI: context_length_exceeded
|
|
713
726
|
if (errorCode === 'context_length_exceeded') {
|
|
714
|
-
debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded');
|
|
727
|
+
debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded (OpenAI)');
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Z.AI/GLM: code "1210" (context length exceeded)
|
|
732
|
+
if (errorCode === '1210' || errorCode === 1210) {
|
|
733
|
+
debugLog('[shouldRetryWithTrim] Detected: code 1210 (GLM context exceeded)');
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 메시지에서 컨텍스트 초과 패턴 감지 (다양한 API 대응)
|
|
738
|
+
const contextExceededPattern = /exceeds.*(?:context|token).*(?:length|limit|maximum)/i;
|
|
739
|
+
if (contextExceededPattern.test(errorMessage)) {
|
|
740
|
+
debugLog(`[shouldRetryWithTrim] Detected context exceeded in message: ${errorMessage.substring(0, 100)}`);
|
|
715
741
|
return true;
|
|
716
742
|
}
|
|
717
743
|
|
|
718
744
|
// 400 에러도 trim 후 재시도 대상
|
|
719
745
|
if (error?.status === 400 || error?.response?.status === 400) {
|
|
720
746
|
const errorType = error?.type || error?.error?.type;
|
|
721
|
-
const errorMessage = error?.message || error?.error?.message || '';
|
|
722
747
|
debugLog(
|
|
723
748
|
`[shouldRetryWithTrim] Detected 400 error - ` +
|
|
724
749
|
`Type: ${errorType}, Code: ${errorCode}, ` +
|