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.
Files changed (75) hide show
  1. package/.local/share/amp/history.jsonl +32 -0
  2. package/.local/share/amp/session.json +2 -2
  3. package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +1541 -0
  4. package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +7 -0
  5. package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +111 -0
  6. package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +7 -0
  7. package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +111 -0
  8. package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +71 -0
  9. package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +1611 -0
  10. package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +7 -0
  11. package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +1341 -0
  12. package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +163 -0
  13. package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +124 -0
  14. package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +1260 -0
  15. package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +403 -0
  16. package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +3422 -0
  17. package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +1830 -0
  18. package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +4061 -0
  19. package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +509 -0
  20. package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +2075 -0
  21. package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +7 -0
  22. package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +7 -0
  23. package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +1637 -0
  24. package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +3893 -0
  25. package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +7 -0
  26. package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +7 -0
  27. package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +7 -0
  28. package/.local/share/amp/threads/T-019c9c56-5715-72e2-b8b4-87711a842dd1.json +1799 -0
  29. package/.local/share/opencode/opencode.db +0 -0
  30. package/.local/share/opencode/opencode.db-shm +0 -0
  31. package/.local/share/opencode/opencode.db-wal +0 -0
  32. package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +6 -0
  33. package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +6 -0
  34. package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +6 -0
  35. package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +1 -0
  36. package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +1 -0
  37. package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +1 -0
  38. package/.local/state/replit/log-query.db +0 -0
  39. package/.local/state/replit/log-query.db-shm +0 -0
  40. package/.local/state/replit/log-query.db-wal +0 -0
  41. package/.upm/store.json +1 -1
  42. package/AGENTS.md +32 -0
  43. package/bun.lock +137 -103
  44. package/index.jsx +24 -0
  45. package/package.json +12 -9
  46. package/src/agent.js +252 -169
  47. package/src/app.jsx +96 -0
  48. package/src/commands.js +66 -38
  49. package/src/components/AssistantMessage.jsx +83 -0
  50. package/src/components/ChatArea.jsx +84 -0
  51. package/src/components/DiffView.jsx +26 -0
  52. package/src/components/Divider.jsx +8 -0
  53. package/src/components/Header.jsx +44 -0
  54. package/src/components/HelpModal.jsx +81 -0
  55. package/src/components/InputBar.jsx +32 -0
  56. package/src/components/Spinner.jsx +23 -0
  57. package/src/components/StatusBar.jsx +44 -0
  58. package/src/components/SystemMessage.jsx +31 -0
  59. package/src/components/ThinkBlock.jsx +29 -0
  60. package/src/components/ToolCallItem.jsx +43 -0
  61. package/src/components/UserMessage.jsx +11 -0
  62. package/src/components/Welcome.jsx +14 -0
  63. package/src/config.js +118 -2
  64. package/src/hooks/useLayout.js +15 -0
  65. package/src/hooks/useStore.js +6 -0
  66. package/src/prompt.js +67 -48
  67. package/src/store.js +99 -0
  68. package/src/theme.js +19 -0
  69. package/src/thinking.js +0 -24
  70. package/src/toolExecutors.js +521 -23
  71. package/src/tools.js +124 -4
  72. package/src/utils.js +32 -0
  73. package/tsconfig.json +10 -0
  74. package/index.js +0 -92
  75. 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
- t,
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
- function setRlClosed(val) { rlClosed = val; }
39
- function getRlClosed() { return rlClosed; }
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, { setupInputLoop, askQuestion }) {
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
- showAssistantHeader();
55
- console.log();
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
- const delay = 1000 * Math.pow(2, attempt);
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 = {}; // index -> { id, name, arguments }
166
+ const toolCallDeltas = {};
167
+ const toolCallMsgIds = {};
168
+ const seenToolCalls = new Set();
117
169
  let finishReason = null;
118
170
  let streamUsage = null;
119
- let reasoningText = ''; // collected reasoning from reasoning_content field
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 = ''; // buffer for buffering/thinking states
127
- let thinkAccum = ''; // accumulates text inside <think>...</think>
128
- let streamingStarted = false;
129
- let thinkSpinnerStopped = false;
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
- // Accumulate tool call deltas
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) toolCallDeltas[idx].name = tc.function.name;
163
- if (tc.function?.arguments) toolCallDeltas[idx].arguments += tc.function.arguments;
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
- // Write everything before <think>
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) showThinkingBlock([thought]);
241
+ if (thought) store.addMessage({ role: 'thinking', content: thought });
197
242
  displayState = 'streaming';
198
243
  contentAccum = after;
199
- if (!hasTool) writeToTerminal(after);
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) writeToTerminal(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
- stopThinkSpinner();
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) writeToTerminal(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
- // 'buffering' — accumulate until we know what's coming
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()) writeToTerminal(before);
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
- stopThinkSpinner();
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) writeToTerminal(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
- // Stream ended flush any remaining buffers
264
- stopThinkSpinner();
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) showThinkingBlock([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()) writeToTerminal(contentAccum);
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) writeToTerminal(contentAccum);
334
+ if (!hasTool) displayContent += contentAccum;
282
335
  contentAccum = '';
283
336
  }
284
337
 
285
- // Show reasoning from reasoning_content field if not already shown via tags
286
- if (reasoningText.trim() && displayState !== 'thinking') {
287
- showThinkingBlock([reasoningText.trim()]);
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
- // Final parsed content for history/display (tags stripped)
295
- const { content: displayContent } = parseThinkBlocks(fullContent);
296
-
343
+ const { content: parsedContent } = parseThinkBlocks(fullContent);
297
344
  turnTokens += streamUsage?.total_tokens || 0;
298
345
 
299
- // Reconstruct the full message object
300
- const toolCalls = Object.keys(toolCallDeltas).sort((a, b) => a - b).map(idx => ({
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 reasoning that wasn't streamed (e.g. with tool calls)
317
- if (displayContent && displayContent.trim()) {
318
- console.log(indent(t.dim.italic(displayContent.trim()), 6));
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
- // Execute tool calls (parallel for independent calls)
323
- const toolPromises = toolCalls.map(async (toolCall) => {
324
- const toolName = toolCall.function.name;
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
- toolArgs = JSON.parse(toolCall.function.arguments);
328
- } catch {
329
- toolArgs = {};
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
- const detail = toolDetail(toolName, toolArgs);
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 spinner = await showToolCall(toolName, detail, callStart);
386
+ const msgId = toolCallMsgIds[sortedIndices[i]];
335
387
 
336
- // Execute
337
- const result = await executeTool(toolName, toolArgs);
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
- // Show result preview
345
- const resultLines = result.split('\n');
346
- let preview;
347
- if (toolName === 'Edit' || toolName === 'Patch') {
348
- // Show diff
349
- showDiff(toolArgs.path, result);
350
- } else if (resultLines.length > 5) {
351
- preview = resultLines.slice(0, 4).join('\n') + `\n... (${resultLines.length} lines)`;
352
- console.log(indent(t.dim(preview), 8));
353
- console.log();
354
- } else if (result.trim() && result !== '(no output)') {
355
- console.log(indent(t.dim(result.length > 300 ? result.slice(0, 297) + '...' : result), 8));
356
- console.log();
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 (already displayed above)
426
+ // No tool calls — final text response
378
427
  if (fullContent) {
379
- const cleanedContent = displayContent.trim() || fullContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
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
- console.log(indent(t.yellow(`⚠ Reached maximum tool iterations (${MAX_TOOL_ITERATIONS}). Stopping.`)));
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
- console.log();
393
- console.log(indent(t.red('✗ Error: ') + t.text(err.message)));
485
+ store.clearStreaming();
486
+ let errorMsg = `Error: ${err.message}`;
394
487
  if (err.status) {
395
- console.log(indent(t.dim(`Status: ${err.status}`) + (err.error ? t.dim(` | Detail: ${JSON.stringify(err.error).slice(0, 200)}`) : '')));
488
+ errorMsg += `\nStatus: ${err.status}`;
396
489
  }
397
490
  if (!process.env.NVIDIA_API_KEY) {
398
- console.log(indent(t.yellow('Set the NVIDIA_API_KEY environment variable with your API key from build.nvidia.com')));
491
+ errorMsg += '\nSet the NVIDIA_API_KEY environment variable with your API key from build.nvidia.com';
399
492
  }
400
- console.log();
493
+ store.addMessage({ role: 'system', content: errorMsg });
401
494
  }
402
495
 
403
- console.log(indent(hr()));
496
+ store.addMessage({ role: 'divider' });
404
497
  isProcessing = false;
405
- if (rlClosed) {
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
  };