apex-dev 2.1.3 → 3.0.1
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/.local/share/amp/history.jsonl +32 -0
- package/.local/share/amp/session.json +2 -2
- package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +1541 -0
- package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +7 -0
- package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +111 -0
- package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +7 -0
- package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +111 -0
- package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +71 -0
- package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +1611 -0
- package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +7 -0
- package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +1341 -0
- package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +163 -0
- package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +124 -0
- package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +1260 -0
- package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +403 -0
- package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +3422 -0
- package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +1830 -0
- package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +4061 -0
- package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +509 -0
- package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +2075 -0
- package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +7 -0
- package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +7 -0
- package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +1637 -0
- package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +3893 -0
- package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +7 -0
- package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +7 -0
- package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +7 -0
- package/.local/share/amp/threads/T-019c9c56-5715-72e2-b8b4-87711a842dd1.json +1799 -0
- package/.local/share/opencode/opencode.db +0 -0
- package/.local/share/opencode/opencode.db-shm +0 -0
- package/.local/share/opencode/opencode.db-wal +0 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +6 -0
- package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +1 -0
- package/.local/state/replit/log-query.db +0 -0
- package/.local/state/replit/log-query.db-shm +0 -0
- package/.local/state/replit/log-query.db-wal +0 -0
- package/.upm/store.json +1 -1
- package/AGENTS.md +32 -0
- package/bun.lock +137 -103
- package/index.jsx +24 -0
- package/package.json +12 -9
- package/src/agent.js +252 -169
- package/src/app.jsx +96 -0
- package/src/commands.js +66 -38
- package/src/components/AssistantMessage.jsx +83 -0
- package/src/components/ChatArea.jsx +84 -0
- package/src/components/DiffView.jsx +26 -0
- package/src/components/Divider.jsx +8 -0
- package/src/components/Header.jsx +44 -0
- package/src/components/HelpModal.jsx +81 -0
- package/src/components/InputBar.jsx +32 -0
- package/src/components/Spinner.jsx +23 -0
- package/src/components/StatusBar.jsx +44 -0
- package/src/components/SystemMessage.jsx +31 -0
- package/src/components/ThinkBlock.jsx +29 -0
- package/src/components/ToolCallItem.jsx +43 -0
- package/src/components/UserMessage.jsx +11 -0
- package/src/components/Welcome.jsx +14 -0
- package/src/config.js +118 -2
- package/src/hooks/useLayout.js +15 -0
- package/src/hooks/useStore.js +6 -0
- package/src/prompt.js +67 -48
- package/src/store.js +99 -0
- package/src/theme.js +19 -0
- package/src/thinking.js +0 -24
- package/src/toolExecutors.js +521 -23
- package/src/tools.js +124 -4
- package/src/utils.js +32 -0
- package/tsconfig.json +10 -0
- package/index.js +0 -92
- package/src/ui.js +0 -269
package/src/agent.js
CHANGED
|
@@ -1,58 +1,121 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const ora = require('ora');
|
|
4
3
|
const {
|
|
5
4
|
NVIDIA_MODEL,
|
|
6
5
|
MAX_TOOL_ITERATIONS,
|
|
7
6
|
nvidiaClient,
|
|
8
7
|
session,
|
|
9
8
|
sleep,
|
|
9
|
+
getMode,
|
|
10
10
|
} = require('./config');
|
|
11
11
|
const { buildSystemPrompt } = require('./prompt');
|
|
12
12
|
const { toolDefs } = require('./tools');
|
|
13
13
|
const { executeTool } = require('./toolExecutors');
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
indent,
|
|
17
|
-
hr,
|
|
18
|
-
showUserMessage,
|
|
19
|
-
showAssistantHeader,
|
|
20
|
-
toolDetail,
|
|
21
|
-
showToolCall,
|
|
22
|
-
finishToolCall,
|
|
23
|
-
showDiff,
|
|
24
|
-
} = require('./ui');
|
|
14
|
+
const { toolDetailStr } = require('./utils');
|
|
15
|
+
const store = require('./store');
|
|
25
16
|
const {
|
|
26
17
|
parseThinkBlocks,
|
|
27
18
|
findThinkClose,
|
|
28
19
|
stripStrayCloseTag,
|
|
29
20
|
splitAtPartialTag,
|
|
30
|
-
showThinkingBlock,
|
|
31
21
|
} = require('./thinking');
|
|
32
22
|
|
|
33
23
|
// ===== State =====
|
|
34
24
|
let isProcessing = false;
|
|
35
|
-
let rlClosed = false;
|
|
36
25
|
|
|
37
26
|
function getIsProcessing() { return isProcessing; }
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
|
|
28
|
+
// ===== Exploration Detection =====
|
|
29
|
+
const EXPLORATION_KEYWORDS = [
|
|
30
|
+
'explore', 'find files', 'where is', 'search for', 'locate',
|
|
31
|
+
'what files', 'which files', 'show me all', 'list all',
|
|
32
|
+
'find code', 'search code', 'find function', 'find class',
|
|
33
|
+
'find module', 'find component', 'find endpoint', 'find route',
|
|
34
|
+
'find api', 'find service', 'find model', 'find controller',
|
|
35
|
+
'where can i find', 'how do i find', 'look for',
|
|
36
|
+
'codebase structure', 'project structure', 'directory structure'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function isExplorationTask(text) {
|
|
40
|
+
const lower = text.toLowerCase();
|
|
41
|
+
return EXPLORATION_KEYWORDS.some(keyword => lower.includes(keyword));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ===== Complexity Detection (for MAX mode auto-thinker) =====
|
|
45
|
+
const COMPLEX_TASK_KEYWORDS = [
|
|
46
|
+
'refactor', 'redesign', 'architect', 'migrate', 'overhaul',
|
|
47
|
+
'implement', 'build', 'create a system', 'design pattern',
|
|
48
|
+
'add feature', 'new feature', 'integrate', 'convert',
|
|
49
|
+
'multiple files', 'across files', 'full stack',
|
|
50
|
+
'database schema', 'api endpoint', 'authentication',
|
|
51
|
+
'complex', 'tricky', 'challenging', 'difficult',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function isComplexTask(text) {
|
|
55
|
+
const lower = text.toLowerCase();
|
|
56
|
+
const wordCount = text.split(/\s+/).length;
|
|
57
|
+
const keywordMatch = COMPLEX_TASK_KEYWORDS.some(kw => lower.includes(kw));
|
|
58
|
+
return keywordMatch || wordCount > 40;
|
|
59
|
+
}
|
|
40
60
|
|
|
41
61
|
// ===== AI Conversation — Agentic Loop =====
|
|
42
|
-
async function handleUserInput(userInput
|
|
62
|
+
async function handleUserInput(userInput) {
|
|
43
63
|
isProcessing = true;
|
|
64
|
+
store.setState({ isProcessing: true });
|
|
44
65
|
session.turnCount++;
|
|
45
|
-
showUserMessage(userInput);
|
|
46
66
|
|
|
67
|
+
store.addMessage({ role: 'user', content: userInput });
|
|
47
68
|
session.conversationHistory.push({ role: 'user', content: userInput });
|
|
48
69
|
|
|
70
|
+
// Auto-delegate to FilePickerMax for exploration tasks
|
|
71
|
+
if (isExplorationTask(userInput)) {
|
|
72
|
+
const exploreId = store.addMessage({ role: 'system', content: 'Exploring codebase...', label: '🔍 Exploring' });
|
|
73
|
+
try {
|
|
74
|
+
const pickerResult = await executeTool('FilePickerMax', { prompt: userInput }, (partial) => {
|
|
75
|
+
store.updateMessage(exploreId, { content: partial, label: '🔍 Exploring' });
|
|
76
|
+
});
|
|
77
|
+
store.updateMessage(exploreId, { content: pickerResult, label: '🔍 Codebase Exploration Results' });
|
|
78
|
+
session.conversationHistory.push({
|
|
79
|
+
role: 'assistant',
|
|
80
|
+
content: `I've explored the codebase and found the following relevant files:\n\n${pickerResult}`,
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
store.updateMessage(exploreId, { content: `Exploration failed: ${err.message}` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MAX mode: Auto-think before complex tasks
|
|
88
|
+
const mode = getMode();
|
|
89
|
+
if (mode === 'max' && isComplexTask(userInput)) {
|
|
90
|
+
const thinkId = store.addMessage({ role: 'system', content: 'Deep thinking...', label: '🧠 Thinker' });
|
|
91
|
+
try {
|
|
92
|
+
const thinkerResult = await executeTool('Thinker', { prompt: userInput }, (partial) => {
|
|
93
|
+
store.updateMessage(thinkId, { content: partial, label: '🧠 Thinker' });
|
|
94
|
+
});
|
|
95
|
+
store.updateMessage(thinkId, { content: thinkerResult, label: '🧠 Thinker' });
|
|
96
|
+
session.conversationHistory.push({
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
content: `[Thinker analysis]\n${thinkerResult}`,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
store.updateMessage(thinkId, { content: `Thinker failed: ${err.message}` });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MAX mode: Auto-prune context when history is long
|
|
106
|
+
if (mode === 'max' && session.conversationHistory.length > 20) {
|
|
107
|
+
try {
|
|
108
|
+
await executeTool('ContextPruner', {}, null);
|
|
109
|
+
} catch { /* ignore pruning failures */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
49
112
|
const startTime = Date.now();
|
|
50
113
|
let turnTokens = 0;
|
|
51
114
|
let turnToolCalls = 0;
|
|
52
115
|
|
|
53
116
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
117
|
+
// Add assistant header message
|
|
118
|
+
store.addMessage({ role: 'divider' });
|
|
56
119
|
|
|
57
120
|
const systemPrompt = buildSystemPrompt();
|
|
58
121
|
let messages = [
|
|
@@ -62,18 +125,9 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
62
125
|
|
|
63
126
|
let iterations = 0;
|
|
64
127
|
|
|
65
|
-
// Agentic tool-use loop
|
|
66
128
|
while (iterations < MAX_TOOL_ITERATIONS) {
|
|
67
129
|
iterations++;
|
|
68
130
|
|
|
69
|
-
// Show thinking spinner while waiting for first token
|
|
70
|
-
const thinkSpinner = ora({
|
|
71
|
-
text: t.dim(iterations === 1 ? 'Reasoning deeply...' : 'Thinking further...'),
|
|
72
|
-
prefixText: ' ',
|
|
73
|
-
spinner: 'dots',
|
|
74
|
-
color: 'magenta',
|
|
75
|
-
}).start();
|
|
76
|
-
|
|
77
131
|
let stream;
|
|
78
132
|
const maxRetries = 3;
|
|
79
133
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -81,7 +135,6 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
81
135
|
stream = await nvidiaClient.chat.completions.create({
|
|
82
136
|
model: NVIDIA_MODEL,
|
|
83
137
|
messages: messages.map(m => {
|
|
84
|
-
// Strip any extra fields the API might reject
|
|
85
138
|
const clean = { role: m.role, content: m.content };
|
|
86
139
|
if (m.tool_calls) clean.tool_calls = m.tool_calls.map(tc => ({
|
|
87
140
|
id: tc.id, type: 'function',
|
|
@@ -101,45 +154,29 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
101
154
|
break;
|
|
102
155
|
} catch (apiErr) {
|
|
103
156
|
if (attempt < maxRetries && apiErr.status >= 400 && apiErr.status < 500) {
|
|
104
|
-
|
|
105
|
-
thinkSpinner.text = t.dim(`Retrying (${attempt + 1}/${maxRetries})...`);
|
|
106
|
-
await sleep(delay);
|
|
157
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
107
158
|
continue;
|
|
108
159
|
}
|
|
109
|
-
thinkSpinner.stop();
|
|
110
160
|
throw apiErr;
|
|
111
161
|
}
|
|
112
162
|
}
|
|
113
163
|
|
|
114
164
|
// Accumulate streamed response
|
|
115
165
|
let fullContent = '';
|
|
116
|
-
const toolCallDeltas = {};
|
|
166
|
+
const toolCallDeltas = {};
|
|
167
|
+
const toolCallMsgIds = {};
|
|
168
|
+
const seenToolCalls = new Set();
|
|
117
169
|
let finishReason = null;
|
|
118
170
|
let streamUsage = null;
|
|
119
|
-
let reasoningText = '';
|
|
171
|
+
let reasoningText = '';
|
|
120
172
|
|
|
121
|
-
// Display state
|
|
122
|
-
// 'buffering' — accumulating, watching for <think> or safe-to-stream point
|
|
123
|
-
// 'thinking' — inside <think>...</think>, accumulating reasoning
|
|
124
|
-
// 'streaming' — think block done, streaming content live to terminal
|
|
173
|
+
// Display state
|
|
125
174
|
let displayState = 'buffering';
|
|
126
|
-
let contentAccum = '';
|
|
127
|
-
let thinkAccum = '';
|
|
128
|
-
let
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
function stopThinkSpinner() {
|
|
132
|
-
if (!thinkSpinnerStopped) { thinkSpinner.stop(); thinkSpinnerStopped = true; }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function writeToTerminal(text) {
|
|
136
|
-
if (!text) return;
|
|
137
|
-
if (!streamingStarted) {
|
|
138
|
-
streamingStarted = true;
|
|
139
|
-
process.stdout.write(' ');
|
|
140
|
-
}
|
|
141
|
-
process.stdout.write(t.text(text));
|
|
142
|
-
}
|
|
175
|
+
let contentAccum = '';
|
|
176
|
+
let thinkAccum = '';
|
|
177
|
+
let displayContent = '';
|
|
178
|
+
let thinkContent = '';
|
|
179
|
+
let lastFlushTime = Date.now();
|
|
143
180
|
|
|
144
181
|
for await (const chunk of stream) {
|
|
145
182
|
if (chunk.usage) streamUsage = chunk.usage;
|
|
@@ -151,7 +188,7 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
151
188
|
}
|
|
152
189
|
if (chunk.choices[0].finish_reason) finishReason = chunk.choices[0].finish_reason;
|
|
153
190
|
|
|
154
|
-
//
|
|
191
|
+
// Tool call deltas
|
|
155
192
|
if (delta.tool_calls) {
|
|
156
193
|
for (const tc of delta.tool_calls) {
|
|
157
194
|
const idx = tc.index;
|
|
@@ -159,14 +196,27 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
159
196
|
toolCallDeltas[idx] = { id: tc.id || '', name: tc.function?.name || '', arguments: '' };
|
|
160
197
|
}
|
|
161
198
|
if (tc.id) toolCallDeltas[idx].id = tc.id;
|
|
162
|
-
if (tc.function?.name)
|
|
163
|
-
|
|
199
|
+
if (tc.function?.name) {
|
|
200
|
+
toolCallDeltas[idx].name = tc.function.name;
|
|
201
|
+
if (!seenToolCalls.has(idx)) {
|
|
202
|
+
seenToolCalls.add(idx);
|
|
203
|
+
toolCallMsgIds[idx] = store.addMessage({
|
|
204
|
+
role: 'tool',
|
|
205
|
+
name: tc.function.name,
|
|
206
|
+
detail: '...',
|
|
207
|
+
status: 'pending',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (tc.function?.arguments) {
|
|
212
|
+
toolCallDeltas[idx].arguments += tc.function.arguments;
|
|
213
|
+
}
|
|
164
214
|
}
|
|
165
215
|
}
|
|
166
216
|
|
|
167
|
-
// Handle reasoning_content field (some APIs send reasoning separately)
|
|
168
217
|
if (delta.reasoning_content) {
|
|
169
218
|
reasoningText += delta.reasoning_content;
|
|
219
|
+
store.updateStreaming(displayContent, reasoningText);
|
|
170
220
|
}
|
|
171
221
|
|
|
172
222
|
// Handle content tokens
|
|
@@ -175,129 +225,127 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
175
225
|
const hasTool = Object.keys(toolCallDeltas).length > 0;
|
|
176
226
|
|
|
177
227
|
if (displayState === 'streaming') {
|
|
178
|
-
// Live-stream, but watch for a new <think> block
|
|
179
228
|
contentAccum += delta.content;
|
|
180
|
-
// Strip any stray closing tags
|
|
181
229
|
contentAccum = stripStrayCloseTag(contentAccum);
|
|
182
|
-
// Find and handle any <think> in the accumulated buffer
|
|
183
230
|
const openIdx = contentAccum.indexOf('<think>');
|
|
184
231
|
if (openIdx !== -1) {
|
|
185
|
-
|
|
186
|
-
if (openIdx > 0) writeToTerminal(contentAccum.slice(0, openIdx));
|
|
232
|
+
if (openIdx > 0) displayContent += contentAccum.slice(0, openIdx);
|
|
187
233
|
thinkAccum = contentAccum.slice(openIdx + 7);
|
|
188
234
|
contentAccum = '';
|
|
189
235
|
displayState = 'thinking';
|
|
190
|
-
// Check if closing tag already in this buffer
|
|
191
236
|
const closeMatch = findThinkClose(thinkAccum);
|
|
192
237
|
if (closeMatch) {
|
|
193
238
|
const thought = thinkAccum.slice(0, closeMatch.pos).trim();
|
|
194
239
|
const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
|
|
195
240
|
thinkAccum = '';
|
|
196
|
-
if (thought)
|
|
241
|
+
if (thought) store.addMessage({ role: 'thinking', content: thought });
|
|
197
242
|
displayState = 'streaming';
|
|
198
243
|
contentAccum = after;
|
|
199
|
-
if (!hasTool)
|
|
244
|
+
if (!hasTool && after) displayContent += after;
|
|
200
245
|
contentAccum = '';
|
|
246
|
+
thinkContent = '';
|
|
247
|
+
} else {
|
|
248
|
+
thinkContent = thinkAccum;
|
|
201
249
|
}
|
|
202
250
|
} else {
|
|
203
|
-
// No <think> — stream it, but hold back possible partial tag at end
|
|
204
251
|
const { safe, pending } = splitAtPartialTag(contentAccum);
|
|
205
252
|
contentAccum = pending;
|
|
206
|
-
if (!hasTool && safe)
|
|
253
|
+
if (!hasTool && safe) displayContent += safe;
|
|
207
254
|
}
|
|
255
|
+
store.updateStreaming(displayContent, thinkContent || reasoningText);
|
|
208
256
|
|
|
209
257
|
} else if (displayState === 'thinking') {
|
|
210
|
-
// Accumulate inside think block
|
|
211
258
|
thinkAccum += delta.content;
|
|
212
259
|
const closeMatch = findThinkClose(thinkAccum);
|
|
213
260
|
if (closeMatch) {
|
|
214
261
|
const thought = thinkAccum.slice(0, closeMatch.pos).trim();
|
|
215
262
|
const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
|
|
216
263
|
thinkAccum = '';
|
|
217
|
-
|
|
218
|
-
if (thought) showThinkingBlock([thought]);
|
|
264
|
+
if (thought) store.addMessage({ role: 'thinking', content: thought });
|
|
219
265
|
displayState = 'streaming';
|
|
220
266
|
contentAccum = after;
|
|
221
|
-
if (!hasTool && after)
|
|
267
|
+
if (!hasTool && after) displayContent += after;
|
|
222
268
|
contentAccum = '';
|
|
269
|
+
thinkContent = '';
|
|
270
|
+
store.updateStreaming(displayContent, reasoningText);
|
|
271
|
+
} else {
|
|
272
|
+
thinkContent = thinkAccum;
|
|
273
|
+
store.updateStreaming(displayContent, thinkContent || reasoningText);
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
} else {
|
|
226
|
-
//
|
|
277
|
+
// buffering
|
|
227
278
|
contentAccum += delta.content;
|
|
228
|
-
// Strip stray closing tags
|
|
229
279
|
contentAccum = stripStrayCloseTag(contentAccum);
|
|
230
280
|
const openIdx = contentAccum.indexOf('<think>');
|
|
231
281
|
if (openIdx !== -1) {
|
|
232
|
-
// Text before <think>: stream it if non-empty
|
|
233
282
|
const before = contentAccum.slice(0, openIdx);
|
|
234
283
|
thinkAccum = contentAccum.slice(openIdx + 7);
|
|
235
284
|
contentAccum = '';
|
|
236
|
-
if (!hasTool && before.trim())
|
|
285
|
+
if (!hasTool && before.trim()) displayContent += before;
|
|
237
286
|
displayState = 'thinking';
|
|
238
|
-
// Check if closing tag already present
|
|
239
287
|
const closeMatch = findThinkClose(thinkAccum);
|
|
240
288
|
if (closeMatch) {
|
|
241
289
|
const thought = thinkAccum.slice(0, closeMatch.pos).trim();
|
|
242
290
|
const after = thinkAccum.slice(closeMatch.pos + closeMatch.len);
|
|
243
291
|
thinkAccum = '';
|
|
244
|
-
|
|
245
|
-
if (thought) showThinkingBlock([thought]);
|
|
292
|
+
if (thought) store.addMessage({ role: 'thinking', content: thought });
|
|
246
293
|
displayState = 'streaming';
|
|
247
294
|
contentAccum = after;
|
|
248
|
-
if (!hasTool && after)
|
|
295
|
+
if (!hasTool && after) displayContent += after;
|
|
249
296
|
contentAccum = '';
|
|
297
|
+
thinkContent = '';
|
|
298
|
+
} else {
|
|
299
|
+
thinkContent = thinkAccum;
|
|
300
|
+
}
|
|
301
|
+
store.updateStreaming(displayContent, thinkContent || reasoningText);
|
|
302
|
+
} else {
|
|
303
|
+
const { safe, pending } = splitAtPartialTag(contentAccum);
|
|
304
|
+
if (safe.length > 0) {
|
|
305
|
+
displayState = 'streaming';
|
|
306
|
+
if (!hasTool) displayContent += safe;
|
|
307
|
+
contentAccum = pending;
|
|
308
|
+
store.updateStreaming(displayContent, reasoningText);
|
|
250
309
|
}
|
|
251
|
-
} else if (!contentAccum.includes('<') && contentAccum.length > 20) {
|
|
252
|
-
// No think tag coming — start streaming immediately
|
|
253
|
-
stopThinkSpinner();
|
|
254
|
-
displayState = 'streaming';
|
|
255
|
-
if (!hasTool) writeToTerminal(contentAccum);
|
|
256
|
-
contentAccum = '';
|
|
257
310
|
}
|
|
258
|
-
// else keep buffering (might be start of <think>)
|
|
259
311
|
}
|
|
260
312
|
}
|
|
261
|
-
}
|
|
262
313
|
|
|
263
|
-
|
|
264
|
-
|
|
314
|
+
// Yield to the event loop periodically so the terminal renderer can paint
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
if (now - lastFlushTime > 16) {
|
|
317
|
+
lastFlushTime = now;
|
|
318
|
+
await new Promise(r => setTimeout(r, 1));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
265
321
|
|
|
322
|
+
// Stream ended — flush remaining buffers
|
|
266
323
|
if (displayState === 'thinking') {
|
|
267
|
-
// Unclosed <think> block — show what we accumulated
|
|
268
324
|
const thought = (thinkAccum + contentAccum).trim();
|
|
269
|
-
if (thought)
|
|
325
|
+
if (thought) store.addMessage({ role: 'thinking', content: thought });
|
|
270
326
|
thinkAccum = '';
|
|
271
327
|
contentAccum = '';
|
|
272
|
-
displayState = 'streaming';
|
|
273
328
|
} else if (displayState === 'buffering') {
|
|
274
|
-
// Never left buffering — flush as plain content
|
|
275
329
|
const hasTool = Object.keys(toolCallDeltas).length > 0;
|
|
276
|
-
if (!hasTool && contentAccum.trim())
|
|
330
|
+
if (!hasTool && contentAccum.trim()) displayContent += contentAccum;
|
|
277
331
|
contentAccum = '';
|
|
278
332
|
} else if (contentAccum) {
|
|
279
|
-
// Flush any pending partial-tag buffer
|
|
280
333
|
const hasTool = Object.keys(toolCallDeltas).length > 0;
|
|
281
|
-
if (!hasTool)
|
|
334
|
+
if (!hasTool) displayContent += contentAccum;
|
|
282
335
|
contentAccum = '';
|
|
283
336
|
}
|
|
284
337
|
|
|
285
|
-
// Show reasoning from reasoning_content field
|
|
286
|
-
if (reasoningText.trim()
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (streamingStarted) {
|
|
291
|
-
process.stdout.write('\n\n');
|
|
338
|
+
// Show reasoning from reasoning_content field
|
|
339
|
+
if (reasoningText.trim()) {
|
|
340
|
+
store.addMessage({ role: 'thinking', content: reasoningText.trim() });
|
|
292
341
|
}
|
|
293
342
|
|
|
294
|
-
|
|
295
|
-
const { content: displayContent } = parseThinkBlocks(fullContent);
|
|
296
|
-
|
|
343
|
+
const { content: parsedContent } = parseThinkBlocks(fullContent);
|
|
297
344
|
turnTokens += streamUsage?.total_tokens || 0;
|
|
298
345
|
|
|
299
|
-
// Reconstruct
|
|
300
|
-
const
|
|
346
|
+
// Reconstruct tool calls
|
|
347
|
+
const sortedIndices = Object.keys(toolCallDeltas).sort((a, b) => a - b);
|
|
348
|
+
const toolCalls = sortedIndices.map(idx => ({
|
|
301
349
|
id: toolCallDeltas[idx].id,
|
|
302
350
|
type: 'function',
|
|
303
351
|
function: { name: toolCallDeltas[idx].name, arguments: toolCallDeltas[idx].arguments },
|
|
@@ -311,49 +359,54 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
311
359
|
|
|
312
360
|
// If the model wants to call tools
|
|
313
361
|
if (toolCalls.length > 0) {
|
|
362
|
+
store.clearStreaming();
|
|
314
363
|
messages.push(msg);
|
|
315
364
|
|
|
316
|
-
// Show intermediate
|
|
317
|
-
if (displayContent
|
|
318
|
-
|
|
319
|
-
console.log();
|
|
365
|
+
// Show intermediate text if any
|
|
366
|
+
if (displayContent.trim()) {
|
|
367
|
+
store.addMessage({ role: 'assistant', content: displayContent.trim() });
|
|
320
368
|
}
|
|
321
369
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
const
|
|
370
|
+
// Update tool messages with full args and execute
|
|
371
|
+
sortedIndices.forEach((idx, i) => {
|
|
372
|
+
const tc = toolCalls[i];
|
|
325
373
|
let toolArgs;
|
|
326
|
-
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
374
|
+
try { toolArgs = JSON.parse(tc.function.arguments); } catch { toolArgs = {}; }
|
|
375
|
+
const detail = toolDetailStr(tc.function.name, toolArgs);
|
|
376
|
+
const msgId = toolCallMsgIds[idx];
|
|
377
|
+
if (msgId) store.updateMessage(msgId, { detail, status: 'running' });
|
|
378
|
+
});
|
|
331
379
|
|
|
332
|
-
|
|
380
|
+
const toolPromises = toolCalls.map(async (toolCall, i) => {
|
|
381
|
+
const toolName = toolCall.function.name;
|
|
382
|
+
let toolArgs;
|
|
383
|
+
try { toolArgs = JSON.parse(toolCall.function.arguments); } catch { toolArgs = {}; }
|
|
384
|
+
const detail = toolDetailStr(toolName, toolArgs);
|
|
333
385
|
const callStart = Date.now();
|
|
334
|
-
const
|
|
386
|
+
const msgId = toolCallMsgIds[sortedIndices[i]];
|
|
335
387
|
|
|
336
|
-
|
|
337
|
-
|
|
388
|
+
const result = await executeTool(toolName, toolArgs, (partial) => {
|
|
389
|
+
if (msgId) store.updateMessage(msgId, { output: partial });
|
|
390
|
+
});
|
|
338
391
|
const success = !result.startsWith('Error');
|
|
392
|
+
const elapsed = Date.now() - callStart;
|
|
339
393
|
|
|
340
|
-
finishToolCall(spinner, toolName, detail, callStart, success);
|
|
341
394
|
session.toolCallCount++;
|
|
342
395
|
turnToolCalls++;
|
|
343
396
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
397
|
+
if (msgId) {
|
|
398
|
+
store.updateMessage(msgId, {
|
|
399
|
+
detail,
|
|
400
|
+
status: success ? 'done' : 'error',
|
|
401
|
+
success,
|
|
402
|
+
elapsed,
|
|
403
|
+
output: result,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Show diff for edit operations
|
|
408
|
+
if ((toolName === 'Edit' || toolName === 'Patch') && success) {
|
|
409
|
+
store.addMessage({ role: 'diff', filename: toolArgs.path, content: result });
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
return { id: toolCall.id, result };
|
|
@@ -362,60 +415,90 @@ async function handleUserInput(userInput, { setupInputLoop, askQuestion }) {
|
|
|
362
415
|
const toolResults = await Promise.all(toolPromises);
|
|
363
416
|
|
|
364
417
|
for (const { id, result } of toolResults) {
|
|
365
|
-
messages.push({
|
|
366
|
-
role: 'tool',
|
|
367
|
-
tool_call_id: id,
|
|
368
|
-
content: result,
|
|
369
|
-
});
|
|
418
|
+
messages.push({ role: 'tool', tool_call_id: id, content: result });
|
|
370
419
|
}
|
|
371
420
|
|
|
372
|
-
// Check for stop condition
|
|
373
421
|
if (finishReason === 'stop') break;
|
|
422
|
+
displayContent = '';
|
|
374
423
|
continue;
|
|
375
424
|
}
|
|
376
425
|
|
|
377
|
-
// No tool calls — final text response
|
|
426
|
+
// No tool calls — final text response
|
|
378
427
|
if (fullContent) {
|
|
379
|
-
const cleanedContent =
|
|
428
|
+
const cleanedContent = parsedContent.trim() || fullContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
429
|
+
if (cleanedContent) {
|
|
430
|
+
store.finishStreaming({ role: 'assistant', content: cleanedContent });
|
|
431
|
+
} else {
|
|
432
|
+
store.clearStreaming();
|
|
433
|
+
}
|
|
380
434
|
session.conversationHistory.push({ role: 'assistant', content: cleanedContent || fullContent });
|
|
435
|
+
} else {
|
|
436
|
+
store.clearStreaming();
|
|
381
437
|
}
|
|
382
438
|
break;
|
|
383
439
|
}
|
|
384
440
|
|
|
385
441
|
if (iterations >= MAX_TOOL_ITERATIONS) {
|
|
386
|
-
|
|
387
|
-
console.log();
|
|
442
|
+
store.addMessage({ role: 'system', content: `⚠ Reached maximum tool iterations (${MAX_TOOL_ITERATIONS}). Stopping.` });
|
|
388
443
|
}
|
|
389
444
|
|
|
390
445
|
session.totalTokens += turnTokens;
|
|
446
|
+
|
|
447
|
+
// Auto-delegate to CodeReview for code changes
|
|
448
|
+
if (session.filesModified.size > 0) {
|
|
449
|
+
const reviewPrompt = `User request: ${userInput}\n\n${turnTokens > 0 ? `Processed with ${turnTokens} tokens and ${turnToolCalls} tool calls.` : ''}`;
|
|
450
|
+
|
|
451
|
+
if (mode === 'max') {
|
|
452
|
+
// MAX mode: Multi-perspective code review
|
|
453
|
+
const reviewId = store.addMessage({ role: 'system', content: 'Multi-perspective code review...', label: '📋 Code Review (MAX)' });
|
|
454
|
+
try {
|
|
455
|
+
const reviewResult = await executeTool('CodeReviewMulti', { prompt: reviewPrompt }, (partial) => {
|
|
456
|
+
store.updateMessage(reviewId, { content: partial, label: '📋 Code Review (MAX)' });
|
|
457
|
+
});
|
|
458
|
+
store.updateMessage(reviewId, { content: reviewResult, label: '📋 Code Review (MAX)' });
|
|
459
|
+
session.conversationHistory.push({
|
|
460
|
+
role: 'assistant',
|
|
461
|
+
content: `\n\n--- Multi-Perspective Code Review ---\n${reviewResult}`,
|
|
462
|
+
});
|
|
463
|
+
} catch (err) {
|
|
464
|
+
store.updateMessage(reviewId, { content: `Multi-review failed: ${err.message}` });
|
|
465
|
+
}
|
|
466
|
+
} else if (mode !== 'lite') {
|
|
467
|
+
// Default mode: Single code review
|
|
468
|
+
const reviewId = store.addMessage({ role: 'system', content: 'Reviewing code changes...', label: '📋 Code Review' });
|
|
469
|
+
try {
|
|
470
|
+
const reviewResult = await executeTool('CodeReview', { prompt: reviewPrompt }, (partial) => {
|
|
471
|
+
store.updateMessage(reviewId, { content: partial, label: '📋 Code Review' });
|
|
472
|
+
});
|
|
473
|
+
store.updateMessage(reviewId, { content: reviewResult, label: '📋 Code Review' });
|
|
474
|
+
session.conversationHistory.push({
|
|
475
|
+
role: 'assistant',
|
|
476
|
+
content: `\n\n--- Code Review ---\n${reviewResult}`,
|
|
477
|
+
});
|
|
478
|
+
} catch (err) {
|
|
479
|
+
store.updateMessage(reviewId, { content: `Code review failed: ${err.message}` });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Lite mode: skip code review entirely
|
|
483
|
+
}
|
|
391
484
|
} catch (err) {
|
|
392
|
-
|
|
393
|
-
|
|
485
|
+
store.clearStreaming();
|
|
486
|
+
let errorMsg = `Error: ${err.message}`;
|
|
394
487
|
if (err.status) {
|
|
395
|
-
|
|
488
|
+
errorMsg += `\nStatus: ${err.status}`;
|
|
396
489
|
}
|
|
397
490
|
if (!process.env.NVIDIA_API_KEY) {
|
|
398
|
-
|
|
491
|
+
errorMsg += '\nSet the NVIDIA_API_KEY environment variable with your API key from build.nvidia.com';
|
|
399
492
|
}
|
|
400
|
-
|
|
493
|
+
store.addMessage({ role: 'system', content: errorMsg });
|
|
401
494
|
}
|
|
402
495
|
|
|
403
|
-
|
|
496
|
+
store.addMessage({ role: 'divider' });
|
|
404
497
|
isProcessing = false;
|
|
405
|
-
|
|
406
|
-
// Readline closed during processing — re-create it if stdin is still usable
|
|
407
|
-
if (!process.stdin.destroyed && process.stdin.readable) {
|
|
408
|
-
setupInputLoop();
|
|
409
|
-
askQuestion();
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
process.exit(0);
|
|
413
|
-
}
|
|
498
|
+
store.setState({ isProcessing: false });
|
|
414
499
|
}
|
|
415
500
|
|
|
416
501
|
module.exports = {
|
|
417
502
|
handleUserInput,
|
|
418
503
|
getIsProcessing,
|
|
419
|
-
setRlClosed,
|
|
420
|
-
getRlClosed,
|
|
421
504
|
};
|