@calliopelabs/cli 0.6.7 → 0.6.8

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 (59) hide show
  1. package/dist/agterm/agent-detection.d.ts +35 -0
  2. package/dist/agterm/agent-detection.d.ts.map +1 -0
  3. package/dist/agterm/agent-detection.js +94 -0
  4. package/dist/agterm/agent-detection.js.map +1 -0
  5. package/dist/agterm/cli-backend.d.ts +32 -0
  6. package/dist/agterm/cli-backend.d.ts.map +1 -0
  7. package/dist/agterm/cli-backend.js +193 -0
  8. package/dist/agterm/cli-backend.js.map +1 -0
  9. package/dist/agterm/index.d.ts +12 -0
  10. package/dist/agterm/index.d.ts.map +1 -0
  11. package/dist/agterm/index.js +15 -0
  12. package/dist/agterm/index.js.map +1 -0
  13. package/dist/agterm/orchestrator.d.ts +94 -0
  14. package/dist/agterm/orchestrator.d.ts.map +1 -0
  15. package/dist/agterm/orchestrator.js +302 -0
  16. package/dist/agterm/orchestrator.js.map +1 -0
  17. package/dist/agterm/tools.d.ts +24 -0
  18. package/dist/agterm/tools.d.ts.map +1 -0
  19. package/dist/agterm/tools.js +277 -0
  20. package/dist/agterm/tools.js.map +1 -0
  21. package/dist/agterm/types.d.ts +95 -0
  22. package/dist/agterm/types.d.ts.map +1 -0
  23. package/dist/agterm/types.js +38 -0
  24. package/dist/agterm/types.js.map +1 -0
  25. package/dist/bin.d.ts +2 -1
  26. package/dist/bin.d.ts.map +1 -1
  27. package/dist/bin.js +18 -3
  28. package/dist/bin.js.map +1 -1
  29. package/dist/errors.d.ts +5 -2
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +107 -4
  32. package/dist/errors.js.map +1 -1
  33. package/dist/plugins.d.ts +120 -0
  34. package/dist/plugins.d.ts.map +1 -0
  35. package/dist/plugins.js +356 -0
  36. package/dist/plugins.js.map +1 -0
  37. package/dist/providers.js +208 -58
  38. package/dist/providers.js.map +1 -1
  39. package/dist/scope.d.ts +89 -0
  40. package/dist/scope.d.ts.map +1 -0
  41. package/dist/scope.js +253 -0
  42. package/dist/scope.js.map +1 -0
  43. package/dist/storage.d.ts +23 -0
  44. package/dist/storage.d.ts.map +1 -1
  45. package/dist/storage.js +83 -0
  46. package/dist/storage.js.map +1 -1
  47. package/dist/styles.d.ts +49 -83
  48. package/dist/styles.d.ts.map +1 -1
  49. package/dist/styles.js +140 -126
  50. package/dist/styles.js.map +1 -1
  51. package/dist/tools.d.ts +5 -0
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +24 -10
  54. package/dist/tools.js.map +1 -1
  55. package/dist/ui-cli.d.ts +1 -0
  56. package/dist/ui-cli.d.ts.map +1 -1
  57. package/dist/ui-cli.js +704 -106
  58. package/dist/ui-cli.js.map +1 -1
  59. package/package.json +1 -1
package/dist/ui-cli.js CHANGED
@@ -11,16 +11,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * ├── ChatInput (input line)
12
12
  * └── StatusBar (footer)
13
13
  */
14
- import { useState, useCallback, useRef, useEffect } from 'react';
14
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
15
15
  import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
16
16
  import * as fs from 'fs';
17
17
  import * as config from './config.js';
18
18
  import { chat, getAvailableProviders, selectProvider } from './providers.js';
19
- import { TOOLS, executeTool } from './tools.js';
19
+ import { executeTool, getTools } from './tools.js';
20
20
  import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
21
21
  import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
22
22
  import { getAvailableModels } from './model-detection.js';
23
- import { assessToolRisk } from './risk.js';
23
+ import { assessToolRisk, detectComplexity } from './risk.js';
24
24
  import { formatError } from './errors.js';
25
25
  import * as storage from './storage.js';
26
26
  import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
@@ -32,6 +32,50 @@ import * as hooks from './hooks.js';
32
32
  import * as modelRouter from './model-router.js';
33
33
  import * as summarization from './summarization.js';
34
34
  import { requiresConfirmation } from './risk.js';
35
+ import { executeParallel, getParallelizationStats } from './parallel-tools.js';
36
+ import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetScope } from './scope.js';
37
+ import { getAgentStatusReport } from './agterm/index.js';
38
+ // Module-level state for agterm mode
39
+ let moduleAgtermEnabled = false;
40
+ class ErrorBoundary extends React.Component {
41
+ constructor(props) {
42
+ super(props);
43
+ this.state = { hasError: false, error: null, errorInfo: '' };
44
+ }
45
+ static getDerivedStateFromError(error) {
46
+ return { hasError: true, error };
47
+ }
48
+ componentDidCatch(error, errorInfo) {
49
+ // Log error details
50
+ const info = errorInfo.componentStack || '';
51
+ this.setState({ errorInfo: info });
52
+ // Could also log to file or external service
53
+ console.error('Calliope Error:', error);
54
+ console.error('Component Stack:', info);
55
+ }
56
+ handleRetry = () => {
57
+ this.setState({ hasError: false, error: null, errorInfo: '' });
58
+ this.props.onReset?.();
59
+ };
60
+ render() {
61
+ if (this.state.hasError) {
62
+ return _jsx(ErrorFallback, { error: this.state.error, errorInfo: this.state.errorInfo, onRetry: this.handleRetry });
63
+ }
64
+ return this.props.children;
65
+ }
66
+ }
67
+ function ErrorFallback({ error, errorInfo, onRetry }) {
68
+ const { exit } = useApp();
69
+ useInput((input, key) => {
70
+ if (input === 'r' || input === 'R') {
71
+ onRetry();
72
+ }
73
+ else if (input === 'q' || input === 'Q' || key.escape) {
74
+ exit();
75
+ }
76
+ });
77
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: "\u26A0\uFE0F Calliope encountered an error" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "red", padding: 1, children: [_jsx(Text, { color: "red", children: error?.message || 'Unknown error' }), error?.name && error.name !== 'Error' && (_jsxs(Text, { dimColor: true, children: ["Type: ", error.name] }))] }), errorInfo && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Component trace:" }), _jsx(Text, { dimColor: true, children: errorInfo.split('\n').slice(0, 5).join('\n') })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), _jsx(Text, { children: "etry " }), _jsx(Text, { color: "cyan", children: "[Q]" }), _jsx(Text, { children: "uit" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If this persists, try: calliope --legacy" }) })] }));
78
+ }
35
79
  // ============================================================================
36
80
  // Constants
37
81
  // ============================================================================
@@ -54,6 +98,11 @@ const TOOL_ICONS = {
54
98
  web_search: '🔍',
55
99
  git: '🔀',
56
100
  mermaid: '📊',
101
+ // AGTerm tools
102
+ spawn_agent: '🤖',
103
+ check_agent: '📋',
104
+ list_agents: '📊',
105
+ cancel_agent: '🛑',
57
106
  };
58
107
  // ============================================================================
59
108
  // Utility Components
@@ -129,11 +178,33 @@ function MessageItem({ msg }) {
129
178
  return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] })] }, i));
130
179
  }), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: "green", children: "\u2713" }), " ", _jsx(Text, { dimColor: true, children: filePath })] })] }));
131
180
  }
132
- // Regular tool result
133
- const lines = msg.content.split('\n').slice(0, 5);
134
- const hasMore = msg.content.split('\n').length > 5;
135
- const hasError = msg.content.toLowerCase().includes('error');
136
- return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: line.substring(0, 100) })] }, i))), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", hasError ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" })] })] }));
181
+ // Regular tool result with enhanced status detection
182
+ const allLines = msg.content.split('\n');
183
+ const lines = allLines.slice(0, 5);
184
+ const totalLines = allLines.length;
185
+ const hasMore = totalLines > 5;
186
+ // Enhanced status detection
187
+ const lowerContent = msg.content.toLowerCase();
188
+ const hasError = lowerContent.includes('error') ||
189
+ lowerContent.includes('failed') ||
190
+ lowerContent.includes('permission denied') ||
191
+ lowerContent.includes('not found') ||
192
+ lowerContent.includes('exception');
193
+ const hasWarning = lowerContent.includes('warning') ||
194
+ lowerContent.includes('deprecated') ||
195
+ lowerContent.includes('caution');
196
+ // Determine status icon and color
197
+ let statusIcon = '✓';
198
+ let statusColor = 'green';
199
+ if (hasError) {
200
+ statusIcon = '✗';
201
+ statusColor = 'red';
202
+ }
203
+ else if (hasWarning) {
204
+ statusIcon = '⚠';
205
+ statusColor = 'yellow';
206
+ }
207
+ return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: line.substring(0, 100) })] }, i))), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsxs(Text, { dimColor: true, children: ["... (", totalLines - 5, " more lines)"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: statusColor, children: statusIcon })] })] }));
137
208
  }
138
209
  case 'system':
139
210
  return _jsx(Text, { color: "yellow", children: msg.content });
@@ -181,6 +252,37 @@ function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
181
252
  });
182
253
  return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: "yellow", children: ["Update available: v", currentVersion, " \u2192 ", _jsxs(Text, { color: "green", children: ["v", latestVersion] })] }), _jsxs(Text, { children: ["Upgrade now? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
183
254
  }
255
+ function ComplexityWarning({ reason, onProceed, onPlan, onCancel, }) {
256
+ useInput((input, key) => {
257
+ if (input === 'p' || input === 'P')
258
+ onProceed();
259
+ else if (input === 'l' || input === 'L')
260
+ onPlan();
261
+ else if (key.escape || input === 'c' || input === 'C')
262
+ onCancel();
263
+ });
264
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\uD83D\uDD0D Complex Operation Detected" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: reason }), _jsx(Text, { children: " " }), _jsx(Text, { children: "This operation may affect multiple files or require careful planning." }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "How would you like to proceed?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "[P]" }), _jsx(Text, { children: "roceed directly " }), _jsx(Text, { color: "yellow", children: "[L]" }), _jsx(Text, { children: "et me plan first " }), _jsx(Text, { color: "red", children: "[C]" }), _jsx(Text, { children: "ancel" })] })] }));
265
+ }
266
+ function SessionResumePrompt({ session, onResume, onNew, }) {
267
+ useInput((input, key) => {
268
+ if (input === 'r' || input === 'R')
269
+ onResume();
270
+ else if (input === 'n' || input === 'N' || key.escape)
271
+ onNew();
272
+ });
273
+ const timeAgo = (() => {
274
+ const diff = Date.now() - new Date(session.lastAccessedAt).getTime();
275
+ const hours = Math.floor(diff / (1000 * 60 * 60));
276
+ const days = Math.floor(hours / 24);
277
+ if (days > 0)
278
+ return `${days} day${days > 1 ? 's' : ''} ago`;
279
+ if (hours > 0)
280
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`;
281
+ const minutes = Math.floor(diff / (1000 * 60));
282
+ return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
283
+ })();
284
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\uD83D\uDCC2 Previous Session Found" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Project: ", _jsx(Text, { color: "yellow", children: session.projectName })] }), _jsxs(Text, { children: ["Last active: ", _jsx(Text, { dimColor: true, children: timeAgo })] }), _jsxs(Text, { children: ["Messages: ", _jsx(Text, { dimColor: true, children: session.messageCount })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), "esume session ", _jsx(Text, { color: "cyan", children: "[N]" }), "ew session"] })] }));
285
+ }
184
286
  function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
185
287
  useInput((input, key) => {
186
288
  if (input === 'y' || input === 'Y')
@@ -197,7 +299,7 @@ function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
197
299
  // ============================================================================
198
300
  // Input Components
199
301
  // ============================================================================
200
- function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, }) {
302
+ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, isProcessing, queuedCount, onQueueMessage, }) {
201
303
  // Handle ALL keyboard input here - single source of input handling
202
304
  useInput((input, key) => {
203
305
  // ESC to exit (always works)
@@ -210,9 +312,32 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
210
312
  onEscape();
211
313
  return;
212
314
  }
213
- // When disabled, ignore all other input
315
+ // When fully disabled (modal), ignore all input
214
316
  if (disabled)
215
317
  return;
318
+ // When processing, queue messages instead of submitting directly
319
+ if (isProcessing) {
320
+ // Allow typing
321
+ if (key.backspace || key.delete) {
322
+ onChange(value.slice(0, -1));
323
+ return;
324
+ }
325
+ if (key.ctrl && input === 'u') {
326
+ onChange('');
327
+ return;
328
+ }
329
+ // Enter queues the message
330
+ if (key.return && value.trim() && onQueueMessage) {
331
+ onQueueMessage(value.trim());
332
+ onChange('');
333
+ return;
334
+ }
335
+ // Regular input
336
+ if (input && !key.ctrl && !key.meta && !key.tab) {
337
+ onChange(value + input);
338
+ }
339
+ return;
340
+ }
216
341
  // Shift+Tab to cycle mode
217
342
  if (key.shift && key.tab) {
218
343
  onCycleMode();
@@ -244,7 +369,10 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
244
369
  onChange(value + input);
245
370
  }
246
371
  });
247
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Box, { children: [_jsx(Text, { color: disabled ? 'gray' : 'cyan', children: "calliope> " }), _jsx(Text, { children: value }), _jsx(Text, { color: "cyan", children: "\u258C" })] })] }));
372
+ // Determine prompt style based on state
373
+ const promptColor = disabled ? 'gray' : isProcessing ? 'yellow' : 'cyan';
374
+ const promptText = isProcessing ? 'queue>' : 'calliope>';
375
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), queuedCount && queuedCount > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\uD83D\uDCE8 ", queuedCount, " message", queuedCount > 1 ? 's' : '', " queued (will be sent after current task)"] }) })), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptText, " "] }), _jsx(Text, { children: value }), _jsx(Text, { color: promptColor, children: "\u258C" })] })] }));
248
376
  }
249
377
  // Context window limits by model (approximate)
250
378
  const CONTEXT_LIMITS = {
@@ -302,6 +430,8 @@ function TerminalChat() {
302
430
  const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
303
431
  // Modal state
304
432
  const [modalMode, setModalMode] = useState('none');
433
+ const [pendingComplexPrompt, setPendingComplexPrompt] = useState(null);
434
+ const [previousSession, setPreviousSession] = useState(null);
305
435
  const [pendingToolCall, setPendingToolCall] = useState(null);
306
436
  const [availableModels, setAvailableModels] = useState([]);
307
437
  const [latestVersion, setLatestVersion] = useState(null);
@@ -313,6 +443,28 @@ function TerminalChat() {
313
443
  messageCount: 0,
314
444
  });
315
445
  const [contextTokens, setContextTokens] = useState(0);
446
+ // Message queue for human-in-the-loop feedback during processing
447
+ const [queuedMessages, setQueuedMessages] = useState([]);
448
+ const [queueInput, setQueueInput] = useState('');
449
+ const undoStack = useRef([]);
450
+ const redoStack = useRef([]);
451
+ const MAX_UNDO_HISTORY = 10;
452
+ const [bookmarks, setBookmarks] = useState([]);
453
+ const [templates, setTemplates] = useState([]);
454
+ // Save state before changes (call before modifying messages)
455
+ const saveUndoState = useCallback(() => {
456
+ undoStack.current.push({
457
+ messages: [...messages],
458
+ llmMessages: [...llmMessages.current],
459
+ timestamp: new Date(),
460
+ });
461
+ // Limit stack size
462
+ if (undoStack.current.length > MAX_UNDO_HISTORY) {
463
+ undoStack.current.shift();
464
+ }
465
+ // Clear redo stack on new action
466
+ redoStack.current = [];
467
+ }, [messages]);
316
468
  // LLM conversation history
317
469
  const llmMessages = useRef([
318
470
  { role: 'system', content: getSystemPrompt(persona) }
@@ -350,7 +502,23 @@ function TerminalChat() {
350
502
  const loopCancelledRef = useRef(false);
351
503
  // Initialize session and load memory on mount
352
504
  useEffect(() => {
353
- const session = storage.getOrCreateSession(process.cwd());
505
+ const cwd = process.cwd();
506
+ // Check for existing session with messages
507
+ const existingSessions = storage.listSessions(5);
508
+ const recentSession = existingSessions.find(s => s.projectPath === cwd &&
509
+ s.messageCount > 0 &&
510
+ Date.now() - new Date(s.lastAccessedAt).getTime() < 24 * 60 * 60 * 1000 // Within 24 hours
511
+ );
512
+ if (recentSession && !sessionRef.current) {
513
+ // Offer to resume
514
+ setPreviousSession({
515
+ projectName: recentSession.projectName,
516
+ lastAccessedAt: recentSession.lastAccessedAt,
517
+ messageCount: recentSession.messageCount,
518
+ });
519
+ setModalMode('session-resume');
520
+ }
521
+ const session = storage.getOrCreateSession(cwd);
354
522
  sessionRef.current = session;
355
523
  // Load memory context into system prompt
356
524
  if (!memoryLoaded) {
@@ -409,7 +577,8 @@ function TerminalChat() {
409
577
  /copy - Copy last response to clipboard
410
578
  /export [file.md] - Export conversation to markdown
411
579
  /edit - Edit and resend last message
412
- /undo - Remove last exchange
580
+ /undo - Undo last action (up to 10 steps)
581
+ /redo - Redo undone action
413
582
  /confirm [on|off] - Toggle risky op confirmation
414
583
  /profile [name|save|del] - Switch/save/delete profiles
415
584
  /mcp [add|remove|tools] - Manage MCP servers
@@ -424,11 +593,22 @@ function TerminalChat() {
424
593
  /status - Show status
425
594
  /config - Show config
426
595
  /upgrade - Check for updates
596
+ /agents - Show sub-agent status (--agterm mode)
597
+ /scope [details|reset] - Show/manage file access scope
598
+ /add-dir <path> - Add directory to allowed scope
599
+ /remove-dir <path> - Remove directory from scope
600
+ /template [save|use|del] - Manage prompt templates
601
+ /cost - Show cost tracking summary
602
+ /bookmark [name] - Create bookmark at current point
603
+ /bookmark list - List all bookmarks
604
+ /bookmark goto <n> - Jump to bookmark
605
+ /queue [show|clear] - Manage queued messages
606
+ /resume [n] - Resume previous session (load n messages)
427
607
  /exit - Exit
428
608
 
429
609
  File references: @filename, ./path, /absolute/path
430
610
  Modes: 📋 Plan | 🔄 Hybrid | 🔧 Work
431
- Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
611
+ Auto-route: ${autoRoute ? 'ON' : 'OFF'}${moduleAgtermEnabled ? '\nAGTerm: ON (spawn_agent, check_agent tools available)' : ''}`);
432
612
  break;
433
613
  case '/provider':
434
614
  case '/p':
@@ -592,27 +772,41 @@ Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
592
772
  break;
593
773
  }
594
774
  case '/undo': {
595
- // Remove last exchange (user message + assistant response)
596
- let removed = 0;
597
- const newMessages = [...messages];
598
- // Remove from the end until we've removed a user message
599
- while (newMessages.length > 0 && removed < 10) {
600
- const last = newMessages.pop();
601
- removed++;
602
- if (last?.type === 'user')
603
- break;
775
+ if (undoStack.current.length === 0) {
776
+ addMessage('system', 'Nothing to undo.');
777
+ break;
604
778
  }
605
- // Also remove from LLM context
606
- while (llmMessages.current.length > 1) {
607
- const last = llmMessages.current[llmMessages.current.length - 1];
608
- if (last.role === 'user') {
609
- llmMessages.current.pop();
610
- break;
611
- }
612
- llmMessages.current.pop();
779
+ // Save current state to redo stack
780
+ redoStack.current.push({
781
+ messages: [...messages],
782
+ llmMessages: [...llmMessages.current],
783
+ timestamp: new Date(),
784
+ });
785
+ // Restore previous state
786
+ const prevState = undoStack.current.pop();
787
+ setMessages(prevState.messages);
788
+ llmMessages.current = prevState.llmMessages;
789
+ setContextTokens(estimateContextTokens());
790
+ addMessage('system', `✓ Undone (${undoStack.current.length} more available)`);
791
+ break;
792
+ }
793
+ case '/redo': {
794
+ if (redoStack.current.length === 0) {
795
+ addMessage('system', 'Nothing to redo.');
796
+ break;
613
797
  }
614
- setMessages(newMessages);
615
- addMessage('system', `✓ Removed last ${removed} message(s)`);
798
+ // Save current state to undo stack
799
+ undoStack.current.push({
800
+ messages: [...messages],
801
+ llmMessages: [...llmMessages.current],
802
+ timestamp: new Date(),
803
+ });
804
+ // Restore redo state
805
+ const redoState = redoStack.current.pop();
806
+ setMessages(redoState.messages);
807
+ llmMessages.current = redoState.llmMessages;
808
+ setContextTokens(estimateContextTokens());
809
+ addMessage('system', `✓ Redone (${redoStack.current.length} more available)`);
616
810
  break;
617
811
  }
618
812
  case '/status':
@@ -622,6 +816,14 @@ Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
622
816
  case '/config':
623
817
  addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}`);
624
818
  break;
819
+ case '/agents':
820
+ if (!moduleAgtermEnabled) {
821
+ addMessage('system', 'AGTerm mode not enabled. Start with --agterm flag to unlock multi-agent features.');
822
+ }
823
+ else {
824
+ addMessage('system', getAgentStatusReport());
825
+ }
826
+ break;
625
827
  case '/set': {
626
828
  // /set <key> <value>
627
829
  const key = parts[1];
@@ -1402,9 +1604,229 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1402
1604
  }
1403
1605
  break;
1404
1606
  }
1607
+ case '/scope':
1608
+ case '/dirs': {
1609
+ const subCmd = parts[1];
1610
+ if (subCmd === 'details' || subCmd === 'full') {
1611
+ addMessage('system', getScopeDetails());
1612
+ }
1613
+ else if (subCmd === 'reset') {
1614
+ resetScope(process.cwd());
1615
+ addMessage('system', '✓ Scope reset to current directory only');
1616
+ }
1617
+ else {
1618
+ addMessage('system', getScopeSummary());
1619
+ }
1620
+ break;
1621
+ }
1622
+ case '/add-dir': {
1623
+ const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
1624
+ if (!dirPath) {
1625
+ addMessage('system', 'Usage: /add-dir <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
1626
+ }
1627
+ else {
1628
+ const result = addToScope(dirPath);
1629
+ if (result.success) {
1630
+ addMessage('system', `✓ ${result.message}`);
1631
+ }
1632
+ else {
1633
+ addMessage('error', result.message);
1634
+ }
1635
+ }
1636
+ break;
1637
+ }
1638
+ case '/remove-dir': {
1639
+ const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
1640
+ if (!dirPath) {
1641
+ addMessage('system', 'Usage: /remove-dir <path>\n\nRemove a directory from the allowed scope.');
1642
+ }
1643
+ else {
1644
+ const result = removeFromScope(dirPath);
1645
+ if (result.success) {
1646
+ addMessage('system', `✓ ${result.message}`);
1647
+ }
1648
+ else {
1649
+ addMessage('error', result.message);
1650
+ }
1651
+ }
1652
+ break;
1653
+ }
1654
+ case '/template':
1655
+ case '/t': {
1656
+ const subCmd = parts[1];
1657
+ if (subCmd === 'list' || !subCmd) {
1658
+ if (templates.length === 0) {
1659
+ addMessage('system', 'No templates saved.\n\nUsage:\n /template save <name> <prompt>\n /template use <name>\n /template delete <name>');
1660
+ }
1661
+ else {
1662
+ const list = templates.map((t, i) => ` ${i + 1}. ${t.name}: "${t.prompt.substring(0, 50)}${t.prompt.length > 50 ? '...' : ''}"`).join('\n');
1663
+ addMessage('system', `Templates:\n${list}`);
1664
+ }
1665
+ }
1666
+ else if (subCmd === 'save' && parts[2]) {
1667
+ const name = parts[2];
1668
+ const prompt = parts.slice(3).join(' ').replace(/^["']|["']$/g, '');
1669
+ if (!prompt) {
1670
+ addMessage('error', 'Usage: /template save <name> "<prompt>"');
1671
+ }
1672
+ else {
1673
+ setTemplates(prev => {
1674
+ const filtered = prev.filter(t => t.name !== name);
1675
+ return [...filtered, { name, prompt, createdAt: new Date() }];
1676
+ });
1677
+ addMessage('system', `✓ Template saved: ${name}`);
1678
+ }
1679
+ }
1680
+ else if (subCmd === 'use' && parts[2]) {
1681
+ const name = parts[2];
1682
+ const template = templates.find(t => t.name === name);
1683
+ if (template) {
1684
+ setInput(template.prompt);
1685
+ addMessage('system', `✓ Template loaded: ${name} (press Enter to send)`);
1686
+ }
1687
+ else {
1688
+ addMessage('error', `Template not found: ${name}`);
1689
+ }
1690
+ }
1691
+ else if (subCmd === 'delete' && parts[2]) {
1692
+ const name = parts[2];
1693
+ const found = templates.find(t => t.name === name);
1694
+ if (found) {
1695
+ setTemplates(prev => prev.filter(t => t.name !== name));
1696
+ addMessage('system', `✓ Template deleted: ${name}`);
1697
+ }
1698
+ else {
1699
+ addMessage('error', `Template not found: ${name}`);
1700
+ }
1701
+ }
1702
+ else {
1703
+ addMessage('system', 'Usage: /template [list|save <name> <prompt>|use <name>|delete <name>]');
1704
+ }
1705
+ break;
1706
+ }
1707
+ case '/cost':
1708
+ case '/costs': {
1709
+ const subCmd = parts[1];
1710
+ if (subCmd === 'reset') {
1711
+ storage.resetCosts();
1712
+ addMessage('system', '✓ Cost tracking reset');
1713
+ }
1714
+ else {
1715
+ addMessage('system', storage.getCostSummary());
1716
+ }
1717
+ break;
1718
+ }
1719
+ case '/bookmark':
1720
+ case '/bm': {
1721
+ const subCmd = parts[1];
1722
+ if (!subCmd || subCmd === 'list') {
1723
+ // List bookmarks
1724
+ if (bookmarks.length === 0) {
1725
+ addMessage('system', 'No bookmarks. Use /bookmark "name" to create one.');
1726
+ }
1727
+ else {
1728
+ const list = bookmarks.map((b, i) => ` ${i + 1}. 🔖 ${b.name} (message #${b.messageIndex})`).join('\n');
1729
+ addMessage('system', `Bookmarks:\n${list}\n\nUse /bookmark goto <number> to jump.`);
1730
+ }
1731
+ }
1732
+ else if (subCmd === 'goto' && parts[2]) {
1733
+ const idx = parseInt(parts[2]) - 1;
1734
+ if (idx >= 0 && idx < bookmarks.length) {
1735
+ const bm = bookmarks[idx];
1736
+ // Save current state for undo
1737
+ saveUndoState();
1738
+ // Restore to bookmark point
1739
+ setMessages(messages.slice(0, bm.messageIndex + 1));
1740
+ llmMessages.current = llmMessages.current.slice(0, bm.llmMessageIndex + 1);
1741
+ setContextTokens(estimateContextTokens());
1742
+ addMessage('system', `✓ Jumped to bookmark: ${bm.name}`);
1743
+ }
1744
+ else {
1745
+ addMessage('error', `Invalid bookmark number. Use /bookmark list to see available.`);
1746
+ }
1747
+ }
1748
+ else if (subCmd === 'delete' && parts[2]) {
1749
+ const idx = parseInt(parts[2]) - 1;
1750
+ if (idx >= 0 && idx < bookmarks.length) {
1751
+ const removed = bookmarks[idx];
1752
+ setBookmarks(prev => prev.filter((_, i) => i !== idx));
1753
+ addMessage('system', `✓ Deleted bookmark: ${removed.name}`);
1754
+ }
1755
+ else {
1756
+ addMessage('error', 'Invalid bookmark number.');
1757
+ }
1758
+ }
1759
+ else {
1760
+ // Create bookmark with given name
1761
+ const name = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
1762
+ const bm = {
1763
+ id: `bm_${Date.now()}`,
1764
+ name,
1765
+ messageIndex: messages.length - 1,
1766
+ llmMessageIndex: llmMessages.current.length - 1,
1767
+ timestamp: new Date(),
1768
+ };
1769
+ setBookmarks(prev => [...prev, bm]);
1770
+ addMessage('system', `🔖 Bookmark created: "${name}"`);
1771
+ }
1772
+ break;
1773
+ }
1774
+ case '/queue':
1775
+ case '/q': {
1776
+ // /q is now queue, use /exit to quit
1777
+ if (command === '/q' && !parts[1]) {
1778
+ // Just /q with no args shows queue
1779
+ if (queuedMessages.length === 0) {
1780
+ addMessage('system', 'No messages queued. Type while agent is processing to queue feedback.');
1781
+ }
1782
+ else {
1783
+ const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m.substring(0, 60)}${m.length > 60 ? '...' : ''}`).join('\n');
1784
+ addMessage('system', `📨 Queued messages (${queuedMessages.length}):\n${list}\n\nUse /queue clear to remove all.`);
1785
+ }
1786
+ break;
1787
+ }
1788
+ const subCmd = parts[1];
1789
+ if (subCmd === 'clear') {
1790
+ const count = queuedMessages.length;
1791
+ setQueuedMessages([]);
1792
+ addMessage('system', `✓ Cleared ${count} queued message${count !== 1 ? 's' : ''}`);
1793
+ }
1794
+ else if (subCmd === 'show' || !subCmd) {
1795
+ if (queuedMessages.length === 0) {
1796
+ addMessage('system', 'No messages queued.');
1797
+ }
1798
+ else {
1799
+ const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m}`).join('\n');
1800
+ addMessage('system', `📨 Queued messages:\n${list}`);
1801
+ }
1802
+ }
1803
+ else {
1804
+ addMessage('system', 'Usage: /queue [show|clear]\n\nTip: Type while agent is processing to queue follow-up messages.');
1805
+ }
1806
+ break;
1807
+ }
1808
+ case '/resume': {
1809
+ // Resume previous session manually
1810
+ const history = storage.getChatHistory(parseInt(parts[1]) || 20);
1811
+ if (history.length === 0) {
1812
+ addMessage('system', 'No previous messages to resume.');
1813
+ }
1814
+ else {
1815
+ for (const msg of history) {
1816
+ if (msg.role === 'user' || msg.role === 'assistant') {
1817
+ llmMessages.current.push({
1818
+ role: msg.role,
1819
+ content: msg.content,
1820
+ });
1821
+ }
1822
+ }
1823
+ addMessage('system', `✓ Loaded ${history.length} messages from previous session`);
1824
+ setContextTokens(estimateContextTokens());
1825
+ }
1826
+ break;
1827
+ }
1405
1828
  case '/exit':
1406
1829
  case '/quit':
1407
- case '/q':
1408
1830
  exit();
1409
1831
  break;
1410
1832
  default:
@@ -1430,6 +1852,17 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1430
1852
  }
1431
1853
  const maxIterations = config.get('maxIterations');
1432
1854
  let completedNaturally = false;
1855
+ // Check context limit and warn if approaching capacity
1856
+ const currentContextTokens = estimateContextTokens();
1857
+ const modelLimit = getContextLimit(effectiveModel || actualModel);
1858
+ const contextPercentage = (currentContextTokens / modelLimit) * 100;
1859
+ if (contextPercentage > 90) {
1860
+ addMessage('system', `🔴 Context at ${Math.round(contextPercentage)}% capacity (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K tokens)
1861
+ Consider: /summarize compact | /clear | shorter messages`);
1862
+ }
1863
+ else if (contextPercentage > 80) {
1864
+ addMessage('system', `⚠️ Context at ${Math.round(contextPercentage)}% capacity - consider /summarize compact soon`);
1865
+ }
1433
1866
  for (let i = 0; i < maxIterations; i++) {
1434
1867
  try {
1435
1868
  // Update thinking state for LLM call
@@ -1453,7 +1886,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1453
1886
  maxIterations,
1454
1887
  });
1455
1888
  };
1456
- const response = await chat(provider, llmMessages.current, TOOLS, effectiveModel, onToken, onRetry);
1889
+ const response = await chat(provider, llmMessages.current, getTools(moduleAgtermEnabled), effectiveModel, onToken, onRetry);
1457
1890
  // Update token stats and cost
1458
1891
  if (response.usage) {
1459
1892
  const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
@@ -1463,99 +1896,178 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1463
1896
  outputTokens: s.outputTokens + response.usage.outputTokens,
1464
1897
  cost: s.cost + usageCost,
1465
1898
  }));
1899
+ // Persist cost to storage
1900
+ storage.recordCost(usageCost, actualProvider, sessionRef.current?.id);
1466
1901
  }
1467
- // Handle tool calls
1902
+ // Handle tool calls with parallel execution support
1468
1903
  if (response.toolCalls?.length) {
1469
1904
  llmMessages.current.push({
1470
1905
  role: 'assistant',
1471
1906
  content: response.content,
1472
1907
  toolCalls: response.toolCalls,
1473
1908
  });
1909
+ const preChecks = [];
1910
+ const executableTools = [];
1474
1911
  for (const toolCall of response.toolCalls) {
1475
1912
  const args = toolCall.arguments;
1476
1913
  const toolPreview = String(args.command || args.path || '...');
1477
- // Assess risk
1478
1914
  const risk = assessToolRisk(toolCall);
1479
1915
  const riskConfig = RISK_CONFIG[risk.level];
1480
1916
  const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
1481
- // Special handling for think tool
1482
- if (toolCall.name === 'think') {
1483
- const thought = String(args.thought || '');
1484
- setThinkingState({
1485
- status: 'Reasoning...',
1486
- detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
1487
- thinking: thought,
1488
- iteration: i + 1,
1489
- maxIterations,
1490
- });
1491
- }
1492
- else {
1493
- setThinkingState({
1494
- status: `Executing ${toolCall.name}...`,
1495
- detail: toolPreview.substring(0, 60),
1496
- thinking: undefined,
1497
- iteration: i + 1,
1498
- maxIterations,
1499
- });
1500
- }
1501
- // In plan mode, don't execute tools (except think)
1917
+ const preCheck = {
1918
+ toolCall,
1919
+ args,
1920
+ preview: toolPreview,
1921
+ risk,
1922
+ riskDisplay,
1923
+ blocked: false,
1924
+ };
1925
+ // Check blocking conditions
1502
1926
  if (mode === 'plan' && toolCall.name !== 'think') {
1927
+ preCheck.blocked = true;
1928
+ preCheck.blockReason = 'plan mode';
1929
+ preCheck.blockContent = '[Plan mode: Tool not executed. Describe what this would do.]';
1503
1930
  addMessage('tool', `📋 ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
1504
- llmMessages.current.push({
1505
- role: 'tool',
1506
- content: '[Plan mode: Tool not executed. Describe what this would do.]',
1507
- toolCallId: toolCall.id,
1508
- });
1509
- continue;
1510
1931
  }
1511
- // Check if confirmation is required for risky operations
1512
- if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
1513
- // Show warning and skip execution
1932
+ else if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
1933
+ preCheck.blocked = true;
1934
+ preCheck.blockReason = 'confirmation required';
1935
+ preCheck.blockContent = `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`;
1514
1936
  const riskIcon = risk.level === 'critical' ? '🛑' : '⚠️';
1515
1937
  addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
1516
- llmMessages.current.push({
1517
- role: 'tool',
1518
- content: `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`,
1519
- toolCallId: toolCall.id,
1938
+ }
1939
+ else {
1940
+ // Check pre-tool hooks
1941
+ const preHookResult = await hooks.checkHooksAllow('pre-tool', {
1942
+ tool: toolCall.name,
1943
+ toolArgs: args,
1520
1944
  });
1521
- continue;
1945
+ if (!preHookResult.allowed) {
1946
+ preCheck.blocked = true;
1947
+ preCheck.blockReason = 'blocked by hook';
1948
+ preCheck.blockContent = `[Blocked by hook: ${preHookResult.reason}]`;
1949
+ addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
1950
+ addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
1951
+ }
1952
+ else {
1953
+ // Tool can be executed
1954
+ executableTools.push(toolCall);
1955
+ addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
1956
+ }
1522
1957
  }
1523
- addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
1524
- // Execute pre-tool hooks
1525
- const preHookResult = await hooks.checkHooksAllow('pre-tool', {
1526
- tool: toolCall.name,
1527
- toolArgs: args,
1528
- });
1529
- if (!preHookResult.allowed) {
1530
- addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
1958
+ preChecks.push(preCheck);
1959
+ // Add blocked tool results to LLM messages
1960
+ if (preCheck.blocked) {
1531
1961
  llmMessages.current.push({
1532
1962
  role: 'tool',
1533
- content: `[Blocked by hook: ${preHookResult.reason}]`,
1963
+ content: preCheck.blockContent,
1534
1964
  toolCallId: toolCall.id,
1535
1965
  });
1536
- continue;
1537
1966
  }
1538
- const result = await executeTool(toolCall, process.cwd());
1539
- // Execute post-tool hooks
1540
- hooks.executeHooks('post-tool', {
1541
- tool: toolCall.name,
1542
- toolArgs: args,
1543
- toolResult: result.result,
1544
- }).catch(() => { });
1545
- // For think tool, show the actual thought content
1546
- if (toolCall.name === 'think') {
1547
- const thought = String(args.thought || '');
1548
- addMessage('tool', thought);
1967
+ }
1968
+ // ============================================================
1969
+ // Phase 2: Execute tools (parallel when beneficial)
1970
+ // ============================================================
1971
+ if (executableTools.length > 0) {
1972
+ const parallelStats = getParallelizationStats(executableTools);
1973
+ const useParallel = parallelStats.maxParallel > 1 && executableTools.length > 1;
1974
+ if (useParallel) {
1975
+ // Show parallelization info
1976
+ setThinkingState({
1977
+ status: `Executing ${executableTools.length} tools in parallel...`,
1978
+ detail: `${parallelStats.stages} stages, up to ${parallelStats.maxParallel}x speedup`,
1979
+ iteration: i + 1,
1980
+ maxIterations,
1981
+ });
1982
+ // Execute in parallel using dependency-aware staging
1983
+ const results = await executeParallel(executableTools, async (call) => {
1984
+ const result = await executeTool(call, process.cwd());
1985
+ return result.result;
1986
+ }, (completed, total, current) => {
1987
+ setThinkingState({
1988
+ status: `Executing tools... (${completed + 1}/${total})`,
1989
+ detail: current.name,
1990
+ iteration: i + 1,
1991
+ maxIterations,
1992
+ });
1993
+ });
1994
+ // Process results sequentially for UI and LLM messages
1995
+ for (const result of results) {
1996
+ const toolCall = result.toolCall;
1997
+ const args = toolCall.arguments;
1998
+ // Execute post-tool hooks
1999
+ hooks.executeHooks('post-tool', {
2000
+ tool: toolCall.name,
2001
+ toolArgs: args,
2002
+ toolResult: result.result,
2003
+ }).catch(() => { });
2004
+ // Display result
2005
+ if (toolCall.name === 'think') {
2006
+ const thought = String(args.thought || '');
2007
+ addMessage('tool', thought);
2008
+ }
2009
+ else if (result.error) {
2010
+ addMessage('tool', `Error: ${result.error}`);
2011
+ }
2012
+ else {
2013
+ const preview = result.result.split('\n').slice(0, 3).join('\n');
2014
+ addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
2015
+ }
2016
+ llmMessages.current.push({
2017
+ role: 'tool',
2018
+ content: result.error ? `Error: ${result.error}` : result.result,
2019
+ toolCallId: toolCall.id,
2020
+ });
2021
+ }
1549
2022
  }
1550
2023
  else {
1551
- const preview = result.result.split('\n').slice(0, 3).join('\n');
1552
- addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
2024
+ // Sequential execution (single tool or dependencies prevent parallelization)
2025
+ for (const toolCall of executableTools) {
2026
+ const args = toolCall.arguments;
2027
+ const toolPreview = String(args.command || args.path || '...');
2028
+ // Special handling for think tool UI
2029
+ if (toolCall.name === 'think') {
2030
+ const thought = String(args.thought || '');
2031
+ setThinkingState({
2032
+ status: 'Reasoning...',
2033
+ detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
2034
+ thinking: thought,
2035
+ iteration: i + 1,
2036
+ maxIterations,
2037
+ });
2038
+ }
2039
+ else {
2040
+ setThinkingState({
2041
+ status: `Executing ${toolCall.name}...`,
2042
+ detail: toolPreview.substring(0, 60),
2043
+ thinking: undefined,
2044
+ iteration: i + 1,
2045
+ maxIterations,
2046
+ });
2047
+ }
2048
+ const result = await executeTool(toolCall, process.cwd());
2049
+ // Execute post-tool hooks
2050
+ hooks.executeHooks('post-tool', {
2051
+ tool: toolCall.name,
2052
+ toolArgs: args,
2053
+ toolResult: result.result,
2054
+ }).catch(() => { });
2055
+ // Display result
2056
+ if (toolCall.name === 'think') {
2057
+ const thought = String(args.thought || '');
2058
+ addMessage('tool', thought);
2059
+ }
2060
+ else {
2061
+ const preview = result.result.split('\n').slice(0, 3).join('\n');
2062
+ addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
2063
+ }
2064
+ llmMessages.current.push({
2065
+ role: 'tool',
2066
+ content: result.result,
2067
+ toolCallId: toolCall.id,
2068
+ });
2069
+ }
1553
2070
  }
1554
- llmMessages.current.push({
1555
- role: 'tool',
1556
- content: result.result,
1557
- toolCallId: toolCall.id,
1558
- });
1559
2071
  }
1560
2072
  continue;
1561
2073
  }
@@ -1578,16 +2090,32 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1578
2090
  setThinkingState(null);
1579
2091
  setStreamingResponse('');
1580
2092
  addMessage('error', formatError(error));
2093
+ completedNaturally = true; // Error counts as "done" - don't show iteration warning
1581
2094
  break;
1582
2095
  }
1583
2096
  }
1584
- // Only show warning if we hit the limit without completing naturally
2097
+ // Only show warning if we actually hit the iteration limit (not errors or natural completion)
1585
2098
  if (!completedNaturally) {
1586
- addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /config.`);
2099
+ addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /set maxIterations <number>.`);
1587
2100
  }
1588
2101
  // Update context tokens after agent run
1589
2102
  setContextTokens(estimateContextTokens());
1590
- }, [provider, model, addMessage, mode, estimateContextTokens]);
2103
+ // Process any queued messages (human-in-the-loop feedback)
2104
+ if (queuedMessages.length > 0) {
2105
+ const queued = [...queuedMessages];
2106
+ setQueuedMessages([]); // Clear the queue
2107
+ // Combine queued messages into a single follow-up
2108
+ const followUp = queued.length === 1
2109
+ ? queued[0]
2110
+ : `[Multiple follow-up messages from user:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2111
+ addMessage('system', `📨 Processing ${queued.length} queued message${queued.length > 1 ? 's' : ''}...`);
2112
+ // Recursively run agent with follow-up
2113
+ // Use setTimeout to avoid stack overflow and allow UI to update
2114
+ setTimeout(() => {
2115
+ runAgent(followUp);
2116
+ }, 100);
2117
+ }
2118
+ }, [provider, model, addMessage, mode, estimateContextTokens, queuedMessages]);
1591
2119
  // Ralph Wiggum loop - runs prompt repeatedly until completion promise or max iterations
1592
2120
  const runLoop = useCallback(async (prompt, maxIter, completionPromise) => {
1593
2121
  setIsProcessing(true);
@@ -1647,6 +2175,17 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1647
2175
  await handleCommand(trimmed);
1648
2176
  return;
1649
2177
  }
2178
+ // In hybrid mode, check for complex operations
2179
+ if (mode === 'hybrid') {
2180
+ const complexity = detectComplexity(trimmed);
2181
+ if (complexity.isComplex) {
2182
+ setPendingComplexPrompt({ prompt: trimmed, complexity });
2183
+ setModalMode('complexity-warning');
2184
+ return;
2185
+ }
2186
+ }
2187
+ // Save state for undo before modifying conversation
2188
+ saveUndoState();
1650
2189
  // Parse file references from input
1651
2190
  const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
1652
2191
  // Show user message (with file info if any)
@@ -1680,7 +2219,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1680
2219
  setThinkingState(null);
1681
2220
  setStreamingResponse('');
1682
2221
  }
1683
- }, [isProcessing, handleCommand, runAgent, addMessage, provider, model]);
2222
+ }, [isProcessing, handleCommand, runAgent, addMessage, provider, model, saveUndoState]);
1684
2223
  // Modal handlers
1685
2224
  const handleModelSelect = useCallback((selectedModel) => {
1686
2225
  setModel(selectedModel);
@@ -1727,13 +2266,70 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1727
2266
  });
1728
2267
  }, []);
1729
2268
  // Render
1730
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), streamingResponse && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "blue", children: "\u2502" }), _jsx(Text, { color: "cyan", children: "\u258C" })] })), isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Processing..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, onEscape: exit, onCycleMode: cycleMode, disabled: isModalActive || isProcessing }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
2269
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), streamingResponse && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "blue", children: "\u2502" }), _jsx(Text, { color: "cyan", children: "\u258C" })] })), isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Processing..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), modalMode === 'session-resume' && previousSession && (_jsx(SessionResumePrompt, { session: previousSession, onResume: () => {
2270
+ // Load chat history into context
2271
+ const history = storage.getChatHistory(20);
2272
+ if (history.length > 0) {
2273
+ for (const msg of history) {
2274
+ if (msg.role === 'user' || msg.role === 'assistant') {
2275
+ llmMessages.current.push({
2276
+ role: msg.role,
2277
+ content: msg.content,
2278
+ });
2279
+ }
2280
+ }
2281
+ addMessage('system', `✓ Resumed session with ${history.length} messages loaded`);
2282
+ setContextTokens(estimateContextTokens());
2283
+ }
2284
+ setModalMode('none');
2285
+ setPreviousSession(null);
2286
+ }, onNew: () => {
2287
+ addMessage('system', '✓ Starting fresh session');
2288
+ setModalMode('none');
2289
+ setPreviousSession(null);
2290
+ } })), modalMode === 'complexity-warning' && pendingComplexPrompt && (_jsx(ComplexityWarning, { reason: pendingComplexPrompt.complexity.reason || 'Complex operation detected', onProceed: async () => {
2291
+ setModalMode('none');
2292
+ const prompt = pendingComplexPrompt.prompt;
2293
+ setPendingComplexPrompt(null);
2294
+ // Proceed with execution
2295
+ saveUndoState();
2296
+ addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
2297
+ setIsProcessing(true);
2298
+ try {
2299
+ await runAgent(prompt);
2300
+ }
2301
+ finally {
2302
+ setIsProcessing(false);
2303
+ }
2304
+ }, onPlan: () => {
2305
+ setModalMode('none');
2306
+ const prompt = pendingComplexPrompt.prompt;
2307
+ setPendingComplexPrompt(null);
2308
+ // Switch to plan mode and proceed
2309
+ setMode('plan');
2310
+ addMessage('system', '📋 Switched to Plan mode - I\'ll describe what I would do without executing.');
2311
+ saveUndoState();
2312
+ addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
2313
+ setIsProcessing(true);
2314
+ runAgent(prompt).finally(() => setIsProcessing(false));
2315
+ }, onCancel: () => {
2316
+ setModalMode('none');
2317
+ setPendingComplexPrompt(null);
2318
+ addMessage('system', 'Operation cancelled.');
2319
+ } })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, onEscape: exit, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, onQueueMessage: (msg) => {
2320
+ setQueuedMessages(prev => [...prev, msg]);
2321
+ addMessage('system', `📨 Queued: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}"`);
2322
+ } }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
1731
2323
  }
1732
2324
  // ============================================================================
1733
2325
  // App Wrapper & Entry Point
1734
2326
  // ============================================================================
1735
2327
  function App() {
1736
- return _jsx(TerminalChat, {});
2328
+ const [resetKey, setResetKey] = React.useState(0);
2329
+ const handleReset = React.useCallback(() => {
2330
+ setResetKey(k => k + 1);
2331
+ }, []);
2332
+ return (_jsx(ErrorBoundary, { onReset: handleReset, children: _jsx(TerminalChat, {}, resetKey) }));
1737
2333
  }
1738
2334
  // Print banner before Ink takes over (stays fixed at top)
1739
2335
  function printBanner() {
@@ -1758,6 +2354,8 @@ function printBanner() {
1758
2354
  console.log();
1759
2355
  }
1760
2356
  export async function startInkCLI(options = {}) {
2357
+ // Set module-level agterm state
2358
+ moduleAgtermEnabled = options.agtermEnabled ?? false;
1761
2359
  // Print banner BEFORE Ink starts - it stays fixed at the top
1762
2360
  printBanner();
1763
2361
  const { waitUntilExit } = render(_jsx(App, {}), {