cmdr-agent 1.0.1 → 1.1.0

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.
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Interactive REPL — the primary cmdr interface.
3
3
  *
4
+ * Uses Ink (React for CLI) for robust terminal management,
5
+ * matching the approach of Claude Code and Gemini CLI.
6
+ *
4
7
  * Streaming output, tool execution display, markdown rendering,
5
8
  * AMOLED black + green/purple aesthetic.
6
9
  */
7
- import * as readline from 'readline';
10
+ import React from 'react';
11
+ import { render } from 'ink';
8
12
  import { Agent } from '../core/agent.js';
9
13
  import { OllamaAdapter } from '../llm/ollama.js';
10
14
  import { ToolRegistry } from '../tools/registry.js';
@@ -14,9 +18,7 @@ import { Orchestrator } from '../core/orchestrator.js';
14
18
  import { SessionManager } from '../session/session-manager.js';
15
19
  import { discoverProject } from '../session/project-context.js';
16
20
  import { buildSystemPrompt } from '../session/prompt-builder.js';
17
- import { startThinking, startToolExec, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary } from './spinner.js';
18
- import { renderWelcome, renderToolExec, renderError, PROMPT_SYMBOL, GREEN, PURPLE, DIM, WHITE, CYAN, renderInfo, GREEN_DIM, YELLOW, RED, SUCCESS_SYMBOL, ERROR_SYMBOL, } from './theme.js';
19
- import { isSlashCommand, parseSlashCommand, getCommand, } from './commands.js';
21
+ import { renderWelcome, renderError, GREEN, PURPLE, DIM, WHITE, renderInfo, YELLOW, RED, } from './theme.js';
20
22
  import { PermissionManager } from '../core/permissions.js';
21
23
  import { saveSession, loadSession, findRecentSession, DebouncedSaver } from '../session/session-persistence.js';
22
24
  import { PluginManager } from '../plugins/plugin-manager.js';
@@ -24,6 +26,8 @@ import { McpClient } from '../plugins/mcp-client.js';
24
26
  import { loadConfig } from '../config/config-loader.js';
25
27
  import { CostTracker } from '../session/cost-tracker.js';
26
28
  import { UndoManager } from '../session/undo-manager.js';
29
+ import { startThinking, stopSpinner, spinnerSuccess, spinnerFail, getCompletionSummary, startToolExec } from './spinner.js';
30
+ import App from './ink/App.js';
27
31
  export async function startRepl(options) {
28
32
  const cwd = process.cwd();
29
33
  const verbose = options.verbose ?? false;
@@ -86,7 +90,6 @@ export async function startRepl(options) {
86
90
  // Permission manager
87
91
  const permissionManager = new PermissionManager(options.dangerouslySkipPermissions ? 'yolo' : 'normal');
88
92
  await permissionManager.loadSettings();
89
- // CLI flag overrides persisted mode
90
93
  if (options.dangerouslySkipPermissions) {
91
94
  permissionManager.setMode('yolo');
92
95
  }
@@ -104,21 +107,20 @@ export async function startRepl(options) {
104
107
  }
105
108
  }
106
109
  // Create agent
107
- let currentModel = options.model;
110
+ const currentModel = options.model;
108
111
  const agent = new Agent({ ...SOLO_CODER, model: currentModel, systemPrompt }, adapter, toolRegistry, cwd, permissionManager);
109
- // --- Welcome ---
112
+ // --- Welcome banner (prints to normal terminal before Ink takes over) ---
110
113
  const modeLabel = permissionManager.getMode() === 'yolo'
111
114
  ? YELLOW('⚠ yolo (all tools auto-approved)')
112
115
  : permissionManager.getMode() === 'strict'
113
116
  ? RED('strict (all tools require approval)')
114
117
  : GREEN('normal (write tools require approval)');
115
- console.log(renderWelcome(currentModel, projectInfo));
118
+ console.log(renderWelcome(currentModel, projectInfo, options.version));
116
119
  console.log(` ${DIM('Permissions:')} ${modeLabel}`);
117
120
  if (activeTeamConfig) {
118
121
  const teamAgents = activeTeamConfig.agents.map(a => a.name).join(', ');
119
122
  console.log(` ${DIM('Team:')} ${PURPLE(activeTeamConfig.name)} ${DIM(`(${teamAgents})`)}`);
120
123
  }
121
- // Show CMDR.md loading status
122
124
  if (projectContext.cmdrInstructions) {
123
125
  const lineCount = projectContext.cmdrInstructions.split('\n').length;
124
126
  console.log(` ${DIM(`CMDR.md loaded (${lineCount} lines)`)}`);
@@ -155,438 +157,49 @@ export async function startRepl(options) {
155
157
  }
156
158
  };
157
159
  console.log('');
158
- // --- Handle initial prompt if provided ---
160
+ // --- Handle one-shot prompt (non-interactive) ---
159
161
  if (options.initialPrompt) {
160
- await handleUserMessage(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
162
+ await handleOneShot(options.initialPrompt, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager);
161
163
  await doSave();
162
164
  return;
163
165
  }
164
- // --- Interactive REPL ---
165
- const rl = readline.createInterface({
166
- input: process.stdin,
167
- output: process.stdout,
168
- prompt: PROMPT_SYMBOL,
169
- terminal: true,
170
- });
171
- rl.prompt();
172
- // --- Paste detection ---
173
- // Buffer lines arriving within 50ms of each other and join as single input
174
- let pasteBuffer = [];
175
- let pasteTimer = null;
176
- const PASTE_THRESHOLD_MS = 50;
177
- // Async queue — ensures commands are processed sequentially
178
- let processing = false;
179
- let closed = false;
180
- async function processInput(input) {
181
- processing = true;
182
- try {
183
- await processLine(input);
184
- }
185
- catch (err) {
186
- const msg = err instanceof Error ? err.message : String(err);
187
- console.error(renderError(msg));
188
- }
189
- finally {
190
- processing = false;
191
- if (closed) {
192
- process.exit(0);
193
- }
194
- else {
195
- rl.prompt();
196
- }
197
- }
198
- }
199
- function flushPasteBuffer() {
200
- pasteTimer = null;
201
- if (pasteBuffer.length === 0)
202
- return;
203
- const lines = pasteBuffer.slice();
204
- pasteBuffer = [];
205
- if (lines.length > 1) {
206
- console.log(` ${DIM(`+${lines.length} lines pasted`)}`);
207
- }
208
- const merged = lines.join('\n').trim();
209
- if (!merged) {
210
- rl.prompt();
211
- return;
212
- }
213
- processInput(merged);
214
- }
215
- async function processLine(input) {
216
- // Slash commands
217
- if (isSlashCommand(input)) {
218
- const { name, args } = parseSlashCommand(input);
219
- const cmd = getCommand(name);
220
- if (!cmd) {
221
- console.log(renderError(`Unknown command: /${name}. Type /help for available commands.`));
222
- return;
223
- }
224
- const result = await cmd.execute(args, {
225
- session: session.getState(),
226
- switchModel: (model) => {
227
- currentModel = model;
228
- },
229
- clearHistory: () => {
230
- session.clear();
231
- agent.reset();
232
- permissionManager.resetSession();
233
- },
234
- ollamaUrl: options.ollamaUrl,
235
- adapter,
236
- model: currentModel,
237
- agentTokenUsage: agent.getState().tokenUsage,
238
- permissionManager,
239
- });
240
- if (result === '__QUIT__') {
241
- autoSaver.cancel();
242
- session.syncFromAgent(agent.getHistory());
243
- if (session.messages.length > 0) {
244
- const sid = await saveSession(session.getState(), currentModel);
245
- console.log(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
246
- }
247
- console.log(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
248
- closed = true;
249
- rl.close();
250
- return;
251
- }
252
- if (result === '__COMPACT__') {
253
- session.syncFromAgent(agent.getHistory());
254
- const beforeTokens = session.tokenCount;
255
- const stats = await session.compact(adapter, currentModel);
256
- agent.replaceMessages(session.messages);
257
- const afterTokens = session.tokenCount;
258
- console.log(renderInfo(`${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`));
259
- return;
260
- }
261
- if (result === '__DIFF__') {
262
- const gitTool = toolRegistry.get('git_diff');
263
- if (gitTool) {
264
- const diffResult = await gitTool.execute({ staged: false }, {
265
- agent: { name: 'cmdr', role: 'assistant', model: currentModel },
266
- cwd,
267
- });
268
- console.log(`\n${WHITE(diffResult.data)}\n`);
269
- }
270
- return;
271
- }
272
- if (result === '__SESSION_SAVE__') {
273
- session.syncFromAgent(agent.getHistory());
274
- if (session.messages.length > 0) {
275
- const sid = await saveSession(session.getState(), currentModel);
276
- console.log(renderInfo(`Session saved: ${DIM(sid)}`));
277
- }
278
- else {
279
- console.log(renderInfo('No messages to save.'));
280
- }
281
- return;
282
- }
283
- if (typeof result === 'string' && result.startsWith('__SESSION_RESUME__:')) {
284
- const sessionId = result.slice('__SESSION_RESUME__:'.length);
285
- const saved = await loadSession(sessionId);
286
- if (saved) {
287
- agent.replaceMessages(saved.messages);
288
- session.syncFromAgent(saved.messages);
289
- console.log(renderInfo(`Resumed session ${DIM(saved.id)} (${saved.messages.length} messages)`));
290
- }
291
- else {
292
- console.log(renderError(`Session not found: ${sessionId}`));
293
- }
294
- return;
295
- }
296
- if (typeof result === 'string' && result.startsWith('__TEAM_SWITCH__:')) {
297
- const preset = result.slice('__TEAM_SWITCH__:'.length);
298
- const teamCfg = getTeamPreset(preset);
299
- if (teamCfg) {
300
- activeTeamConfig = teamCfg;
301
- const teamAgents = teamCfg.agents.map(a => a.name).join(', ');
302
- console.log(renderInfo(`Switched to team: ${PURPLE(teamCfg.name)} (${teamAgents})`));
303
- }
304
- else {
305
- console.log(renderError(`Unknown team: ${preset}. Use: review, fullstack, security`));
306
- }
307
- return;
308
- }
309
- if (result === '__AGENTS_STATUS__') {
310
- if (!activeTeamConfig) {
311
- console.log(renderInfo(`Solo mode (agent: ${GREEN('cmdr')}). Use /team <preset> for multi-agent.`));
312
- }
313
- else {
314
- const status = orchestrator.getStatus();
315
- const lines = ['', ` ${PURPLE.bold(`Team: ${activeTeamConfig.name}`)}`, ''];
316
- for (const agentCfg of activeTeamConfig.agents) {
317
- const agentStatus = status?.agents.find(a => a.name === agentCfg.name);
318
- const statusLabel = agentStatus ? DIM(agentStatus.status) : DIM('idle');
319
- lines.push(` ${GREEN('•')} ${WHITE(agentCfg.name.padEnd(12))} ${statusLabel}`);
320
- }
321
- lines.push('');
322
- console.log(lines.join('\n'));
323
- }
324
- return;
325
- }
326
- if (result === '__TASKS_STATUS__') {
327
- const status = orchestrator.getStatus();
328
- if (!status) {
329
- console.log(renderInfo('No active team or tasks.'));
330
- }
331
- else {
332
- const s = status.tasks;
333
- if (s) {
334
- console.log(renderInfo(`Tasks: ${GREEN(`${s.completed} done`)} · ${YELLOW(`${s.in_progress} running`)} · ${DIM(`${s.pending} pending`)} · ${s.failed > 0 ? RED(`${s.failed} failed`) : DIM('0 failed')}`));
335
- }
336
- }
337
- return;
338
- }
339
- if (result === '__COST__') {
340
- const summary = costTracker.getSummary();
341
- const elapsed = costTracker.formatElapsed();
342
- const lines = [
343
- '',
344
- ` ${PURPLE.bold('Token usage')}`,
345
- '',
346
- ` ${DIM('Model:')} ${WHITE(summary.model)}`,
347
- ` ${DIM('Turns:')} ${WHITE(String(summary.turns))}`,
348
- ` ${DIM('Input tokens:')} ${WHITE(String(summary.totalInputTokens))}`,
349
- ` ${DIM('Output tokens:')} ${WHITE(String(summary.totalOutputTokens))}`,
350
- ` ${DIM('Total tokens:')} ${GREEN(String(summary.totalTokens))}`,
351
- ` ${DIM('Tool calls:')} ${WHITE(String(summary.totalToolCalls))}`,
352
- ` ${DIM('Avg/turn:')} ${WHITE(String(summary.avgTokensPerTurn))}`,
353
- ` ${DIM('Session time:')} ${WHITE(elapsed)}`,
354
- '',
355
- ];
356
- console.log(lines.join('\n'));
357
- return;
358
- }
359
- if (result === '__UNDO__') {
360
- if (undoManager.count === 0) {
361
- console.log(renderInfo('Nothing to undo.'));
362
- }
363
- else {
364
- const change = await undoManager.undoLast();
365
- if (change) {
366
- const action = change.originalContent === null ? 'deleted' : 'restored';
367
- const fname = change.path.split('/').pop() ?? change.path;
368
- console.log(renderInfo(`Undid ${change.type} on ${GREEN(fname)} (${action})`));
369
- }
370
- }
371
- return;
372
- }
373
- if (typeof result === 'string' && result.startsWith('__PLUGIN__:')) {
374
- const sub = result.slice('__PLUGIN__:'.length).trim();
375
- if (sub === 'list' || !sub) {
376
- const plugins = pluginManager.list();
377
- if (plugins.length === 0) {
378
- console.log(renderInfo('No plugins loaded. Add plugins to ~/.cmdr/config.toml'));
379
- }
380
- else {
381
- const lines = ['', ` ${PURPLE.bold('Loaded plugins')}`, ''];
382
- for (const p of plugins) {
383
- const hooks = p.hooks ? Object.keys(p.hooks).length : 0;
384
- const tools = p.tools?.length ?? 0;
385
- console.log(` ${GREEN('•')} ${WHITE(p.name)} v${p.version} ${DIM(`(${hooks} hooks, ${tools} tools)`)}`);
386
- }
387
- lines.push('');
388
- console.log(lines.join('\n'));
389
- }
390
- }
391
- return;
392
- }
393
- if (typeof result === 'string' && result.startsWith('__MCP__:')) {
394
- const sub = result.slice('__MCP__:'.length).trim().split(/\s+/);
395
- const action = sub[0];
396
- if (action === 'list' || !action) {
397
- const conns = mcpClient.listConnections();
398
- if (conns.length === 0) {
399
- console.log(renderInfo('No MCP servers connected. Add to ~/.cmdr/config.toml or use /mcp connect <name> <url>'));
400
- }
401
- else {
402
- const lines = ['', ` ${PURPLE.bold('MCP servers')}`, ''];
403
- for (const c of conns) {
404
- const status = c.connected ? GREEN('connected') : RED('disconnected');
405
- lines.push(` ${GREEN('•')} ${WHITE(c.name)} ${DIM(c.url)} ${status} ${DIM(`(${c.tools} tools)`)}`);
406
- }
407
- lines.push('');
408
- console.log(lines.join('\n'));
409
- }
410
- }
411
- else if (action === 'connect') {
412
- const name = sub[1];
413
- const url = sub[2];
414
- if (!name || !url) {
415
- console.log(renderInfo('Usage: /mcp connect <name> <url>'));
416
- }
417
- else {
418
- try {
419
- const tools = await mcpClient.connect({ name, url });
420
- mcpClient.registerTools(toolRegistry);
421
- console.log(renderInfo(`Connected to ${name}: ${tools.length} tools discovered`));
422
- }
423
- catch (err) {
424
- const msg = err instanceof Error ? err.message : String(err);
425
- console.log(renderError(msg));
426
- }
427
- }
428
- }
429
- else if (action === 'disconnect') {
430
- const name = sub[1];
431
- if (name && mcpClient.disconnect(name)) {
432
- console.log(renderInfo(`Disconnected from ${name}`));
433
- }
434
- else {
435
- console.log(renderError(`MCP server "${name}" not found`));
436
- }
437
- }
438
- return;
439
- }
440
- if (result)
441
- console.log(result);
442
- return;
443
- }
444
- // Regular message — team mode or solo mode
445
- if (activeTeamConfig) {
446
- await handleTeamMessage(input, orchestrator, activeTeamConfig, currentModel, verbose);
447
- }
448
- else {
449
- await handleUserMessage(input, agent, session, currentModel, permissionManager, verbose, adapter, costTracker, undoManager, () => {
450
- autoSaver.schedule(doSave);
451
- });
452
- }
453
- }
454
- rl.on('line', (line) => {
455
- pasteBuffer.push(line);
456
- if (pasteTimer)
457
- clearTimeout(pasteTimer);
458
- pasteTimer = setTimeout(flushPasteBuffer, PASTE_THRESHOLD_MS);
166
+ // --- Interactive REPL via Ink ---
167
+ const app = render(React.createElement(App, {
168
+ agent,
169
+ session,
170
+ model: currentModel,
171
+ permissionManager,
172
+ adapter,
173
+ orchestrator,
174
+ activeTeamConfig,
175
+ costTracker,
176
+ undoManager,
177
+ pluginManager,
178
+ mcpClient,
179
+ toolRegistry,
180
+ ollamaUrl: options.ollamaUrl,
181
+ verbose,
182
+ doSave,
183
+ autoSaver,
184
+ }), {
185
+ exitOnCtrlC: false, // We handle Ctrl+C ourselves
186
+ patchConsole: false, // Ban console logs from being intercepted during banner
459
187
  });
460
- rl.on('close', async () => {
461
- autoSaver.cancel();
462
- if (!closed) {
463
- session.syncFromAgent(agent.getHistory());
464
- if (session.messages.length > 0) {
465
- try {
466
- const sid = await saveSession(session.getState(), currentModel);
467
- console.log(`\n ${DIM('Session saved:')} ${DIM(sid)}`);
468
- }
469
- catch {
470
- // best effort
471
- }
472
- }
473
- console.log(`\n ${PURPLE('Goodbye.')} ${DIM('Session ended.')}\n`);
474
- }
475
- if (processing) {
476
- closed = true;
477
- }
478
- else {
479
- process.exit(0);
480
- }
481
- });
482
- }
483
- // ---------------------------------------------------------------------------
484
- // Approval prompt — asks the user to approve a tool call
485
- // ---------------------------------------------------------------------------
486
- function promptApproval(toolName, input, riskLevel) {
487
- return new Promise((resolve) => {
488
- const riskColor = riskLevel === 'dangerous' ? RED : YELLOW;
489
- const riskLabel = riskColor(riskLevel.toUpperCase());
490
- // Show the tool call details
491
- console.log('');
492
- console.log(` ${YELLOW('⚠')} ${WHITE('Tool approval required')} ${DIM('[')}${riskLabel}${DIM(']')}`);
493
- console.log(` ${DIM('Tool:')} ${CYAN(toolName)}`);
494
- // Show a summary of key arguments
495
- for (const [key, val] of Object.entries(input)) {
496
- const display = typeof val === 'string'
497
- ? val.length > 120 ? val.slice(0, 120) + DIM('...') : val
498
- : JSON.stringify(val).slice(0, 120);
499
- console.log(` ${DIM(key + ':')} ${WHITE(display)}`);
500
- }
501
- console.log('');
502
- console.log(` ${GREEN('y')}${DIM('es')} ${DIM('/')} ${RED('n')}${DIM('o')} ${DIM('/')} ${PURPLE('a')}${DIM('lways allow this tool')}`);
503
- // Create a one-shot readline for the approval prompt
504
- const approvalRl = readline.createInterface({
505
- input: process.stdin,
506
- output: process.stdout,
507
- terminal: true,
508
- });
509
- approvalRl.question(` ${YELLOW('?')} `, (answer) => {
510
- approvalRl.close();
511
- const trimmed = answer.trim().toLowerCase();
512
- if (trimmed === 'y' || trimmed === 'yes' || trimmed === '') {
513
- resolve('allow');
514
- }
515
- else if (trimmed === 'a' || trimmed === 'always') {
516
- resolve('allow-always');
517
- }
518
- else {
519
- resolve('deny');
520
- }
521
- });
522
- });
523
- }
524
- // ---------------------------------------------------------------------------
525
- // Tool result summary — collapsed single-line display
526
- // ---------------------------------------------------------------------------
527
- function summarizeToolResult(toolName, input, content, isError) {
528
- const lineCount = content.split('\n').length;
529
- const prefix = isError ? ERROR_SYMBOL : SUCCESS_SYMBOL;
530
- let summary;
531
- switch (toolName) {
532
- case 'file_read': {
533
- const file = input.path ?? 'unknown';
534
- const fname = file.split('/').pop() ?? file;
535
- summary = `${fname} (${lineCount} lines)`;
536
- break;
537
- }
538
- case 'glob': {
539
- const pattern = input.pattern ?? '*';
540
- const matches = content === '(no matches)' ? 0 : lineCount;
541
- summary = `${pattern} (${matches} matches)`;
542
- break;
543
- }
544
- case 'bash': {
545
- const cmd = input.command ?? '';
546
- const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
547
- // Extract exit code from result if present
548
- const exitMatch = content.match(/\[exit code: (\d+)\]/);
549
- const exitCode = exitMatch ? exitMatch[1] : '0';
550
- summary = `\`${truncCmd}\` exit=${exitCode} (${lineCount} lines)`;
551
- break;
552
- }
553
- case 'grep': {
554
- const pattern = input.pattern ?? '';
555
- const matches = content === '(no matches)' ? 0 : lineCount;
556
- summary = `/${pattern}/ (${matches} matches)`;
557
- break;
558
- }
559
- case 'think': {
560
- const thought = input.thought ?? '';
561
- const preview = thought.length > 60 ? thought.slice(0, 57) + '...' : thought;
562
- summary = preview;
563
- break;
564
- }
565
- default: {
566
- const bytes = Buffer.byteLength(content, 'utf-8');
567
- summary = `${bytes > 1024 ? Math.round(bytes / 1024) + ' KB' : bytes + ' B'}`;
568
- break;
569
- }
570
- }
571
- return ` ${prefix} ${DIM(toolName)} ${DIM(summary)}`;
188
+ await app.waitUntilExit();
572
189
  }
573
190
  // ---------------------------------------------------------------------------
574
- // Message handler streaming output with tool execution display
191
+ // One-shot handler (non-interactive --prompt mode)
575
192
  // ---------------------------------------------------------------------------
576
- async function handleUserMessage(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager, onAfterResponse) {
577
- console.log(''); // spacing
193
+ async function handleOneShot(message, agent, session, model, permissionManager, verbose, adapter, costTracker, undoManager) {
194
+ console.log('');
578
195
  startThinking();
579
196
  let fullOutput = '';
580
197
  let firstText = true;
581
198
  let currentTool = '';
582
199
  let currentToolInput = {};
583
200
  let toolCallCount = 0;
584
- // Build callbacks with the approval gate
585
- const callbacks = {
586
- onToolApproval: (toolName, input, riskLevel) => promptApproval(toolName, input, riskLevel),
587
- };
588
201
  try {
589
- for await (const event of agent.stream(message, callbacks)) {
202
+ for await (const event of agent.stream(message)) {
590
203
  switch (event.type) {
591
204
  case 'text': {
592
205
  if (firstText) {
@@ -596,8 +209,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
596
209
  }
597
210
  const chunk = event.data;
598
211
  fullOutput += chunk;
599
- // Stream raw text token-by-token (no markdown on partial chunks)
600
- // Handle newlines by adding the prefix
601
212
  const formatted = chunk.replace(/\n/g, `\n ${PURPLE('│')} `);
602
213
  process.stdout.write(formatted);
603
214
  break;
@@ -605,7 +216,6 @@ async function handleUserMessage(message, agent, session, model, permissionManag
605
216
  case 'tool_use': {
606
217
  stopSpinner();
607
218
  if (!firstText) {
608
- // Terminate previous text stream line
609
219
  process.stdout.write('\n');
610
220
  firstText = true;
611
221
  }
@@ -613,46 +223,36 @@ async function handleUserMessage(message, agent, session, model, permissionManag
613
223
  currentTool = block.name;
614
224
  currentToolInput = block.input;
615
225
  toolCallCount++;
616
- // Record file state for undo before write/edit tools
617
- if (undoManager && (block.name === 'file_write' || block.name === 'file_edit')) {
226
+ if (block.name === 'file_write' || block.name === 'file_edit') {
618
227
  const filePath = (block.input.path ?? block.input.file_path);
619
- if (filePath) {
228
+ if (filePath)
620
229
  await undoManager.recordBefore(filePath, block.name === 'file_write' ? 'write' : 'edit');
621
- }
622
230
  }
623
- console.log(renderToolExec(block.name, block.input));
231
+ const toolSummary = Object.entries(block.input)
232
+ .map(([k, v]) => {
233
+ const val = typeof v === 'string' ? v.slice(0, 80) : JSON.stringify(v).slice(0, 80);
234
+ return `${DIM(k + ':')} ${WHITE(val)}`;
235
+ })
236
+ .join(' ');
237
+ console.log(` ${GREEN('⚡')} ${GREEN.bold(block.name)} ${toolSummary}`);
624
238
  startToolExec(block.name);
625
239
  break;
626
240
  }
627
241
  case 'tool_result': {
628
242
  const block = event.data;
629
- if (block.is_error) {
243
+ if (block.is_error)
630
244
  spinnerFail(currentTool);
631
- }
632
- else {
245
+ else
633
246
  spinnerSuccess(currentTool);
634
- }
635
- if (verbose) {
636
- // Full output in verbose mode
637
- const truncated = block.content.length > 2000
638
- ? block.content.slice(0, 2000) + DIM('\n... (truncated)')
639
- : block.content;
640
- const prefix = block.is_error ? ERROR_SYMBOL : SUCCESS_SYMBOL;
641
- console.log(` ${prefix} ${DIM(currentTool + ':')} ${block.is_error ? RED(truncated) : DIM(truncated)}`);
642
- }
643
- else {
644
- console.log(summarizeToolResult(currentTool, currentToolInput, block.content, block.is_error));
645
- }
646
247
  currentTool = '';
647
248
  currentToolInput = {};
648
249
  startThinking();
649
250
  firstText = true;
650
251
  break;
651
252
  }
652
- case 'done': {
253
+ case 'done':
653
254
  stopSpinner();
654
255
  break;
655
- }
656
256
  case 'error': {
657
257
  stopSpinner();
658
258
  const err = event.data;
@@ -665,22 +265,12 @@ async function handleUserMessage(message, agent, session, model, permissionManag
665
265
  catch (err) {
666
266
  stopSpinner();
667
267
  const msg = err instanceof Error ? err.message : String(err);
668
- if (msg.includes('not found') || msg.includes('404') || (msg.includes('model') && msg.includes('pull'))) {
669
- console.error(renderError(`Model '${model}' not found. Run ${GREEN('/models')} to see available models or ${GREEN('/model <name>')} to switch.`));
670
- }
671
- else {
672
- console.error(renderError(msg));
673
- }
268
+ console.error(renderError(msg));
674
269
  }
675
- // Ensure we end the text block with a newline
676
- if (!firstText) {
270
+ if (!firstText)
677
271
  process.stdout.write('\n');
678
- }
679
- // Add spacing after response
680
- if (fullOutput) {
272
+ if (fullOutput)
681
273
  console.log('');
682
- }
683
- // Show turn summary with whimsical verb
684
274
  const state = agent.getState();
685
275
  const tokens = state.tokenUsage;
686
276
  const summary = getCompletionSummary();
@@ -688,72 +278,7 @@ async function handleUserMessage(message, agent, session, model, permissionManag
688
278
  ? ` ${DIM('·')} ${DIM(`${tokens.input_tokens} in / ${tokens.output_tokens} out`)}`
689
279
  : '';
690
280
  console.log(` ${DIM(summary)}${tokenInfo}`);
691
- // Record cost data
692
- costTracker?.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
693
- // Sync agent messages into session for compaction tracking
281
+ costTracker.record(model, tokens.input_tokens, tokens.output_tokens, toolCallCount);
694
282
  session.syncFromAgent(agent.getHistory());
695
- // Auto-compact if context is getting full
696
- if (session.shouldCompact()) {
697
- try {
698
- const stats = await session.compact(adapter, model);
699
- agent.replaceMessages(session.messages);
700
- console.log(` ${DIM(`◇ compacted: ${stats.before} messages → ${stats.after} messages (saved ~${stats.tokensSaved} tokens)`)}`);
701
- }
702
- catch {
703
- // best effort — don't break the REPL
704
- }
705
- }
706
- // Trigger debounced auto-save
707
- onAfterResponse?.();
708
- console.log(GREEN_DIM('─'.repeat(60)));
709
- console.log('');
710
- }
711
- // ---------------------------------------------------------------------------
712
- // Team message handler — runs goal through the orchestrator
713
- // ---------------------------------------------------------------------------
714
- async function handleTeamMessage(goal, orchestrator, teamConfig, model, verbose) {
715
- console.log('');
716
- console.log(` ${PURPLE('◈')} Running team ${PURPLE.bold(teamConfig.name)} with ${teamConfig.agents.length} agents...`);
717
- console.log('');
718
- startThinking();
719
- try {
720
- const result = await orchestrator.runTeam(teamConfig, goal);
721
- stopSpinner();
722
- // Display results from each agent
723
- for (const [agentName, agentResult] of result.agentResults) {
724
- const status = agentResult.success ? GREEN('✓') : RED('✗');
725
- console.log(` ${status} ${CYAN(agentName)}`);
726
- if (agentResult.output) {
727
- const lines = agentResult.output.split('\n');
728
- const displayLines = verbose ? lines : lines.slice(0, 20);
729
- for (const line of displayLines) {
730
- console.log(` ${PURPLE('│')} ${line}`);
731
- }
732
- if (!verbose && lines.length > 20) {
733
- console.log(` ${PURPLE('│')} ${DIM(`... ${lines.length - 20} more lines (use --verbose)`)}`);
734
- }
735
- console.log('');
736
- }
737
- // Tool call summary
738
- if (agentResult.toolCalls.length > 0) {
739
- const tools = agentResult.toolCalls.map(t => t.toolName);
740
- const unique = [...new Set(tools)];
741
- console.log(` ${DIM(` tools: ${unique.join(', ')} (${tools.length} calls)`)}`);
742
- }
743
- }
744
- // Summary
745
- const usage = result.totalTokenUsage;
746
- const summary = getCompletionSummary();
747
- const tokenInfo = `${usage.input_tokens} in / ${usage.output_tokens} out`;
748
- console.log(` ${DIM(summary)} ${DIM('·')} ${DIM(tokenInfo)}`);
749
- console.log(` ${result.success ? GREEN('Team completed successfully') : RED('Team had failures')}`);
750
- }
751
- catch (err) {
752
- stopSpinner();
753
- const msg = err instanceof Error ? err.message : String(err);
754
- console.error(renderError(msg));
755
- }
756
- console.log(GREEN_DIM('─'.repeat(60)));
757
- console.log('');
758
283
  }
759
284
  //# sourceMappingURL=repl.js.map