@calliopelabs/cli 0.6.10 → 0.7.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.
package/dist/ui-cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * Calliope CLI - Ink UI
4
4
  *
@@ -21,7 +21,7 @@ import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVisi
21
21
  import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
22
22
  import { getAvailableModels } from './model-detection.js';
23
23
  import { assessToolRisk, detectComplexity } from './risk.js';
24
- import { formatError } from './errors.js';
24
+ import { formatError, classifyError } from './errors.js';
25
25
  import * as storage from './storage.js';
26
26
  import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
27
27
  import { renderMarkdown } from './markdown.js';
@@ -37,6 +37,14 @@ import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetSco
37
37
  import { getAgentStatusReport } from './agterm/index.js';
38
38
  // Module-level state for agterm mode
39
39
  let moduleAgtermEnabled = false;
40
+ // Debug logging for flow control issues
41
+ let debugEnabled = process.env.CALLIOPE_DEBUG === '1';
42
+ const debugLog = (label, ...args) => {
43
+ if (debugEnabled) {
44
+ const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
45
+ console.error(`[${timestamp}] ${label}:`, ...args);
46
+ }
47
+ };
40
48
  class ErrorBoundary extends React.Component {
41
49
  constructor(props) {
42
50
  super(props);
@@ -120,7 +128,7 @@ function ThinkingDisplay({ state }) {
120
128
  }, 80);
121
129
  return () => clearInterval(timer);
122
130
  }, []);
123
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
131
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration != null && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
124
132
  }
125
133
  // Legacy simple indicator for non-agent operations
126
134
  function ProcessingIndicator({ label }) {
@@ -133,6 +141,18 @@ function ProcessingIndicator({ label }) {
133
141
  }, []);
134
142
  return (_jsxs(Box, { marginY: 1, children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
135
143
  }
144
+ // Minimal indicator shown during streaming to show we're still receiving
145
+ function StreamingIndicator() {
146
+ const [frame, setFrame] = useState(0);
147
+ const pulseFrames = ['·', '•', '●', '•'];
148
+ useEffect(() => {
149
+ const timer = setInterval(() => {
150
+ setFrame(f => (f + 1) % pulseFrames.length);
151
+ }, 200);
152
+ return () => clearInterval(timer);
153
+ }, []);
154
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: pulseFrames[frame] }), _jsx(Text, { dimColor: true, children: " receiving..." })] }));
155
+ }
136
156
  // ============================================================================
137
157
  // Message Components
138
158
  // ============================================================================
@@ -252,7 +272,7 @@ function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
252
272
  });
253
273
  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)" })] })] }));
254
274
  }
255
- function ComplexityWarning({ reason, onProceed, onPlan, onCancel, }) {
275
+ function ComplexityWarning({ reason, prompt, onProceed, onPlan, onCancel, }) {
256
276
  useInput((input, key) => {
257
277
  if (input === 'p' || input === 'P')
258
278
  onProceed();
@@ -261,7 +281,53 @@ function ComplexityWarning({ reason, onProceed, onPlan, onCancel, }) {
261
281
  else if (key.escape || input === 'c' || input === 'C')
262
282
  onCancel();
263
283
  });
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" })] })] }));
284
+ // Analyze the prompt for operation preview
285
+ const analysis = React.useMemo(() => {
286
+ if (!prompt)
287
+ return null;
288
+ const lower = prompt.toLowerCase();
289
+ const cwd = process.cwd();
290
+ // Parse file references
291
+ const fileRefs = parseFileReferences(prompt, cwd);
292
+ // Detect operation types
293
+ const operations = [];
294
+ if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) {
295
+ operations.push('Delete files');
296
+ }
297
+ if (lower.includes('create') || lower.includes('add') || lower.includes('new ')) {
298
+ operations.push('Create files');
299
+ }
300
+ if (lower.includes('modify') || lower.includes('change') || lower.includes('update') || lower.includes('edit')) {
301
+ operations.push('Modify files');
302
+ }
303
+ if (lower.includes('refactor') || lower.includes('restructure') || lower.includes('reorganize')) {
304
+ operations.push('Refactor code');
305
+ }
306
+ if (lower.includes('install') || lower.includes('npm') || lower.includes('yarn') || lower.includes('pip')) {
307
+ operations.push('Install packages');
308
+ }
309
+ if (lower.includes('git ') || lower.includes('commit') || lower.includes('push') || lower.includes('merge')) {
310
+ operations.push('Git operations');
311
+ }
312
+ if (lower.includes('test') || lower.includes('build') || lower.includes('compile')) {
313
+ operations.push('Build/Test');
314
+ }
315
+ // Estimate risk level based on keywords
316
+ let riskLevel = 'medium';
317
+ if (lower.includes('delete') || lower.includes('remove') || lower.includes('force') || lower.includes('--hard')) {
318
+ riskLevel = 'high';
319
+ }
320
+ else if (lower.includes('read') || lower.includes('show') || lower.includes('list') || lower.includes('find')) {
321
+ riskLevel = 'low';
322
+ }
323
+ return {
324
+ files: fileRefs.files,
325
+ operations,
326
+ riskLevel,
327
+ };
328
+ }, [prompt]);
329
+ const riskColors = { low: 'green', medium: 'yellow', high: 'red' };
330
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\uD83D\uDD0D Operation Preview" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: reason }), analysis && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), analysis.operations.length > 0 && (_jsxs(Text, { children: ["Operations: ", _jsx(Text, { color: "cyan", children: analysis.operations.join(', ') })] })), analysis.files.length > 0 && (_jsxs(Text, { children: ["Files referenced: ", _jsx(Text, { color: "cyan", children: analysis.files.length }), analysis.files.length <= 3 && (_jsxs(Text, { dimColor: true, children: [" (", analysis.files.map(f => f.split('/').pop()).join(', '), ")"] }))] })), _jsxs(Text, { children: ["Risk level: ", _jsx(Text, { color: riskColors[analysis.riskLevel], children: analysis.riskLevel.toUpperCase() })] })] })), _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
331
  }
266
332
  function SessionResumePrompt({ session, onResume, onNew, }) {
267
333
  useInput((input, key) => {
@@ -296,6 +362,14 @@ function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
296
362
  const riskIcon = riskLevel === 'critical' ? '⚠️' : '⚡';
297
363
  return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: riskColor, paddingX: 1, children: [_jsxs(Text, { color: riskColor, bold: true, children: [riskIcon, " ", riskLevel.toUpperCase(), " RISK OPERATION"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Tool: ", _jsx(Text, { color: "cyan", children: toolCall.name })] }), _jsxs(Text, { children: ["Command: ", _jsx(Text, { dimColor: true, children: preview.substring(0, 60) })] }), _jsxs(Text, { children: ["Reason: ", _jsx(Text, { dimColor: true, children: reason })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Execute this operation? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
298
364
  }
365
+ // Keybindings modal component
366
+ function KeybindingsModal({ onClose }) {
367
+ useInput((input, key) => {
368
+ if (key.escape || key.return || input === 'q')
369
+ onClose();
370
+ });
371
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2328\uFE0F Keyboard Shortcuts" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "General:" }), _jsx(Text, { children: " Enter Submit message" }), _jsx(Text, { children: " Alt/Ctrl+Enter Insert newline (multiline)" }), _jsx(Text, { children: " Shift+Tab Cycle modes (plan/hybrid/work)" }), _jsx(Text, { children: " Esc Cancel operation / show hint" }), _jsx(Text, { children: " Ctrl+C Exit" }), _jsx(Text, { children: " \u2191/\u2193 Navigate input history" }), _jsx(Text, { children: " Tab Auto-complete commands/paths" }), _jsx(Text, { children: " Ctrl+U Clear input line" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "During Processing (queue mode):" }), _jsx(Text, { children: " Enter Queue message for later" }), _jsx(Text, { children: " Shift+Enter Send directly (interrupt)" }), _jsx(Text, { children: " \u2191/\u2193 Edit queued messages" }), _jsx(Text, { children: " Ctrl+D Delete queued message" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "Quick Commands:" }), _jsx(Text, { children: " /keys This help" }), _jsx(Text, { children: " /work Switch to work mode" }), _jsx(Text, { children: " /plan Switch to plan mode" }), _jsx(Text, { children: " /flush Force-process queue" }), _jsx(Text, { children: " /unstick Reset stuck state" }), _jsx(Text, { children: " /debug on/off Toggle debug mode" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press any key to close..." })] }));
372
+ }
299
373
  // ============================================================================
300
374
  // Slash Commands (for tab completion)
301
375
  // ============================================================================
@@ -341,6 +415,15 @@ const SLASH_COMMANDS = [
341
415
  '/loop',
342
416
  '/cancel-loop',
343
417
  '/exit',
418
+ '/keys',
419
+ '/?',
420
+ '/queue',
421
+ '/flush',
422
+ '/debug',
423
+ '/unstick',
424
+ '/work',
425
+ '/plan',
426
+ '/resume',
344
427
  ];
345
428
  // Commands that take a path argument (for file tab completion)
346
429
  const PATH_COMMANDS = ['/add-dir', '/remove-dir', '/export', '/find'];
@@ -410,7 +493,9 @@ function getPathCompletions(partial, cwd) {
410
493
  // ============================================================================
411
494
  // Input Components
412
495
  // ============================================================================
413
- function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, isProcessing, queuedCount, onQueueMessage, cwd, suggestions, onSuggestionsChange, onNavigateHistory, }) {
496
+ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, isProcessing, queuedCount, queuedMessages, editingQueueIndex, onQueueMessage, onEditQueuedMessage, onSetEditingQueueIndex, onDirectSend, cwd, suggestions, onSuggestionsChange, onNavigateHistory,
497
+ // Smart suggestion context
498
+ currentMode, contextPercentage, recentCommands, hasGitRepo, }) {
414
499
  const workingDir = cwd || process.cwd();
415
500
  // Handle ALL keyboard input here - single source of input handling
416
501
  useInput((input, key) => {
@@ -436,11 +521,70 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
436
521
  }
437
522
  if (key.ctrl && input === 'u') {
438
523
  onChange('');
524
+ onSetEditingQueueIndex?.(null); // Clear editing state
525
+ return;
526
+ }
527
+ // Up/Down arrows to navigate queued messages for editing
528
+ if (key.upArrow && queuedMessages && queuedMessages.length > 0) {
529
+ if (editingQueueIndex === null || editingQueueIndex === undefined) {
530
+ // Start editing the last queued message
531
+ const idx = queuedMessages.length - 1;
532
+ onSetEditingQueueIndex?.(idx);
533
+ onChange(queuedMessages[idx]);
534
+ }
535
+ else if (editingQueueIndex > 0) {
536
+ // Move to previous message
537
+ const idx = editingQueueIndex - 1;
538
+ onSetEditingQueueIndex?.(idx);
539
+ onChange(queuedMessages[idx]);
540
+ }
541
+ return;
542
+ }
543
+ if (key.downArrow && queuedMessages && editingQueueIndex !== null && editingQueueIndex !== undefined) {
544
+ if (editingQueueIndex < queuedMessages.length - 1) {
545
+ // Move to next message
546
+ const idx = editingQueueIndex + 1;
547
+ onSetEditingQueueIndex?.(idx);
548
+ onChange(queuedMessages[idx]);
549
+ }
550
+ else {
551
+ // At the end, clear to new input
552
+ onSetEditingQueueIndex?.(null);
553
+ onChange('');
554
+ }
439
555
  return;
440
556
  }
441
- // Enter queues the message
442
- if (key.return && value.trim() && onQueueMessage) {
443
- onQueueMessage(value.trim());
557
+ // Alt+Enter or Ctrl+Enter to insert newline (multiline input)
558
+ if (key.return && (key.meta || key.ctrl)) {
559
+ onChange(value + '\n');
560
+ return;
561
+ }
562
+ // Shift+Enter sends directly (interrupts current operation)
563
+ if (key.return && key.shift && value.trim() && onDirectSend) {
564
+ onDirectSend(value.trim());
565
+ onSetEditingQueueIndex?.(null);
566
+ onChange('');
567
+ return;
568
+ }
569
+ // Enter queues or updates the message
570
+ if (key.return && value.trim()) {
571
+ if (editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
572
+ // Update existing queued message
573
+ onEditQueuedMessage(editingQueueIndex, value.trim());
574
+ onSetEditingQueueIndex?.(null);
575
+ onChange('');
576
+ }
577
+ else if (onQueueMessage) {
578
+ // Add new queued message
579
+ onQueueMessage(value.trim());
580
+ onChange('');
581
+ }
582
+ return;
583
+ }
584
+ // Ctrl+D to delete currently editing queued message
585
+ if (key.ctrl && input === 'd' && editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
586
+ onEditQueuedMessage(editingQueueIndex, ''); // Empty string signals deletion
587
+ onSetEditingQueueIndex?.(null);
444
588
  onChange('');
445
589
  return;
446
590
  }
@@ -455,6 +599,11 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
455
599
  onCycleMode();
456
600
  return;
457
601
  }
602
+ // Alt+Enter or Ctrl+Enter to insert newline (multiline input)
603
+ if (key.return && (key.meta || key.ctrl)) {
604
+ onChange(value + '\n');
605
+ return;
606
+ }
458
607
  // Enter to submit
459
608
  if (key.return) {
460
609
  if (value.trim()) {
@@ -500,10 +649,20 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
500
649
  }
501
650
  return;
502
651
  }
503
- // Slash command completion
652
+ // Slash command completion with smart suggestions
504
653
  if (value.startsWith('/')) {
654
+ // Use smart suggestions if context is available
655
+ const smartMatches = getSmartCommandSuggestions({
656
+ input: value,
657
+ hasGitRepo: hasGitRepo ?? false,
658
+ contextPercentage: contextPercentage ?? 0,
659
+ currentMode: currentMode ?? 'hybrid',
660
+ recentCommands: recentCommands ?? [],
661
+ isProcessing: isProcessing ?? false,
662
+ });
663
+ // Fall back to basic matching if smart suggestions didn't find anything
505
664
  const partial = value.toLowerCase();
506
- const matches = SLASH_COMMANDS.filter(cmdName => cmdName.startsWith(partial) && cmdName !== partial);
665
+ const matches = smartMatches.length > 0 ? smartMatches : SLASH_COMMANDS.filter(cmdName => cmdName.startsWith(partial) && cmdName !== partial);
507
666
  if (matches.length === 1) {
508
667
  onChange(matches[0] + ' ');
509
668
  onSuggestionsChange?.([]);
@@ -543,7 +702,10 @@ function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled,
543
702
  });
544
703
  // Determine prompt style based on state
545
704
  const promptColor = disabled ? 'gray' : isProcessing ? 'yellow' : 'cyan';
546
- const promptText = isProcessing ? 'queue>' : 'calliope>';
705
+ const isEditing = editingQueueIndex !== null && editingQueueIndex !== undefined;
706
+ const promptText = isProcessing
707
+ ? (isEditing ? `edit[${editingQueueIndex + 1}]>` : 'queue>')
708
+ : 'calliope>';
547
709
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), suggestions && suggestions.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Tab: " }), _jsx(Text, { color: "cyan", children: suggestions.slice(0, 5).join(' ') }), suggestions.length > 5 && _jsxs(Text, { dimColor: true, children: [" (+", suggestions.length - 5, " more)"] })] })), (queuedCount ?? 0) > 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" })] })] }));
548
710
  }
549
711
  // Context window limits by model (approximate)
@@ -570,6 +732,142 @@ function getContextLimit(model) {
570
732
  }
571
733
  return CONTEXT_LIMITS.default;
572
734
  }
735
+ // Module-level state for context tracking (persists across renders)
736
+ const contextState = {
737
+ lastLevel: 'healthy',
738
+ warningCounts: { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 },
739
+ lastWarningTime: 0,
740
+ };
741
+ function getContextLevel(percentage) {
742
+ if (percentage >= 98)
743
+ return 'emergency';
744
+ if (percentage >= 95)
745
+ return 'critical';
746
+ if (percentage >= 85)
747
+ return 'warning';
748
+ if (percentage >= 70)
749
+ return 'caution';
750
+ return 'healthy';
751
+ }
752
+ function getContextLevelIndex(level) {
753
+ const order = ['healthy', 'caution', 'warning', 'critical', 'emergency'];
754
+ return order.indexOf(level);
755
+ }
756
+ function shouldShowContextWarning(level) {
757
+ if (level === 'healthy')
758
+ return false;
759
+ const now = Date.now();
760
+ const timeSinceLastWarning = now - contextState.lastWarningTime;
761
+ const minInterval = level === 'emergency' ? 30000 : 60000; // 30s for emergency, 60s otherwise
762
+ // Always warn on level increase
763
+ if (getContextLevelIndex(level) > getContextLevelIndex(contextState.lastLevel)) {
764
+ return true;
765
+ }
766
+ // Warn again if enough time has passed and we're at critical/emergency
767
+ if ((level === 'critical' || level === 'emergency') && timeSinceLastWarning > minInterval) {
768
+ return true;
769
+ }
770
+ return false;
771
+ }
772
+ function checkAndWarnContextLimit(model, tokens, addMessage) {
773
+ const limit = getContextLimit(model);
774
+ const percentage = (tokens / limit) * 100;
775
+ const level = getContextLevel(percentage);
776
+ const used = Math.round(tokens / 1000);
777
+ const limitK = Math.round(limit / 1000);
778
+ if (!shouldShowContextWarning(level))
779
+ return;
780
+ // Update state
781
+ contextState.lastLevel = level;
782
+ contextState.warningCounts[level]++;
783
+ contextState.lastWarningTime = Date.now();
784
+ // Generate warning message based on level
785
+ let message;
786
+ switch (level) {
787
+ case 'emergency':
788
+ message = `\x1b[31m\x1b[1m🚨 EMERGENCY: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
789
+ \x1b[31m Responses WILL be truncated. Take action NOW:\x1b[0m
790
+ \x1b[2m /summarize compact - Auto-compress (recommended)
791
+ /clear - Fresh start
792
+ /branch new "save" - Save and branch\x1b[0m`;
793
+ break;
794
+ case 'critical':
795
+ message = `\x1b[31m🔴 CRITICAL: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
796
+ \x1b[2m Approaching limits. Action recommended:
797
+ /summarize compact | /clear | shorter messages\x1b[0m`;
798
+ break;
799
+ case 'warning':
800
+ message = `\x1b[33m⚠️ WARNING: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
801
+ \x1b[2m Consider: /summarize compact | /clear\x1b[0m`;
802
+ break;
803
+ case 'caution':
804
+ message = `\x1b[36m💡 Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
805
+ \x1b[2m Monitor usage. /context summary for details\x1b[0m`;
806
+ break;
807
+ default:
808
+ return;
809
+ }
810
+ console.log(message + '\n');
811
+ // Also add to UI messages if callback provided (for critical+)
812
+ if (addMessage && (level === 'critical' || level === 'emergency')) {
813
+ const uiMessage = level === 'emergency'
814
+ ? `🚨 EMERGENCY: Context at ${Math.round(percentage)}% - responses will be truncated! Use /summarize compact NOW`
815
+ : `🔴 Context at ${Math.round(percentage)}% - consider /summarize compact`;
816
+ addMessage('system', uiMessage);
817
+ }
818
+ }
819
+ function resetContextWarnings() {
820
+ contextState.lastLevel = 'healthy';
821
+ contextState.warningCounts = { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 };
822
+ contextState.lastWarningTime = 0;
823
+ }
824
+ function getSmartCommandSuggestions(ctx) {
825
+ const { input, hasGitRepo, contextPercentage, currentMode, recentCommands } = ctx;
826
+ if (!input.startsWith('/'))
827
+ return [];
828
+ const suggestions = [];
829
+ const inputLower = input.toLowerCase();
830
+ // All available commands for matching
831
+ const allCommands = [
832
+ '/help', '/clear', '/exit', '/quit',
833
+ '/mode', '/work', '/plan',
834
+ '/provider', '/model', '/models', '/config',
835
+ '/scope', '/add-dir', '/remove-dir', '/find',
836
+ '/summarize', '/context', '/cost', '/session',
837
+ '/debug', '/keys', '/unstick', '/flush',
838
+ '/branch', '/branches', '/switch',
839
+ '/save', '/load', '/sessions',
840
+ '/git', '/run', '/set', '/confirm',
841
+ ];
842
+ // Context-aware prioritization
843
+ const prioritized = [];
844
+ // High context? Suggest compaction commands first
845
+ if (contextPercentage > 70) {
846
+ prioritized.push('/summarize compact', '/clear', '/branch new');
847
+ }
848
+ // Mode-specific suggestions
849
+ if (currentMode === 'plan') {
850
+ prioritized.push('/mode hybrid', '/work');
851
+ }
852
+ else if (currentMode === 'work') {
853
+ prioritized.push('/mode hybrid', '/plan');
854
+ }
855
+ // Git repo? Suggest git commands
856
+ if (hasGitRepo) {
857
+ prioritized.push('/git status', '/git diff', '/git add', '/git commit');
858
+ }
859
+ // Add recent commands (deduplicated)
860
+ for (const cmd of recentCommands.slice(-5)) {
861
+ if (cmd.startsWith('/') && !prioritized.includes(cmd)) {
862
+ prioritized.push(cmd);
863
+ }
864
+ }
865
+ // Filter by what user is typing
866
+ const matchingPrioritized = prioritized.filter(cmd => cmd.toLowerCase().startsWith(inputLower));
867
+ const matchingAll = allCommands.filter(cmd => cmd.toLowerCase().startsWith(inputLower) && !matchingPrioritized.includes(cmd));
868
+ suggestions.push(...matchingPrioritized, ...matchingAll);
869
+ return suggestions.slice(0, 6);
870
+ }
573
871
  function StatusBar({ provider, model, stats, mode, contextTokens, }) {
574
872
  const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
575
873
  const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
@@ -599,6 +897,16 @@ function TerminalChat() {
599
897
  const [inputHistory, setInputHistory] = useState([]);
600
898
  const [historyIndex, setHistoryIndex] = useState(-1);
601
899
  const [savedInput, setSavedInput] = useState(''); // Save current input when navigating
900
+ // Smart suggestions context
901
+ const [hasGitRepo] = useState(() => {
902
+ try {
903
+ return fs.existsSync('.git') || fs.existsSync('../.git');
904
+ }
905
+ catch {
906
+ return false;
907
+ }
908
+ });
909
+ const recentCommands = React.useMemo(() => inputHistory.filter(cmd => cmd.startsWith('/')).slice(-10), [inputHistory]);
602
910
  // Clear suggestions when input changes significantly
603
911
  const handleInputChange = useCallback((newValue) => {
604
912
  setInput(newValue);
@@ -671,6 +979,7 @@ function TerminalChat() {
671
979
  // Message queue for human-in-the-loop feedback during processing
672
980
  const [queuedMessages, setQueuedMessages] = useState([]);
673
981
  const [queueInput, setQueueInput] = useState('');
982
+ const [editingQueueIndex, setEditingQueueIndex] = useState(null);
674
983
  const undoStack = useRef([]);
675
984
  const redoStack = useRef([]);
676
985
  const MAX_UNDO_HISTORY = 10;
@@ -765,6 +1074,15 @@ function TerminalChat() {
765
1074
  setMemoryLoaded(true);
766
1075
  // Execute session start hooks
767
1076
  hooks.executeHooks('session-start', {}).catch(() => { });
1077
+ // Load templates from storage
1078
+ const savedTemplates = storage.getTemplates();
1079
+ if (savedTemplates.length > 0) {
1080
+ setTemplates(savedTemplates.map(t => ({
1081
+ name: t.name,
1082
+ prompt: t.prompt,
1083
+ createdAt: new Date(t.createdAt),
1084
+ })));
1085
+ }
768
1086
  }
769
1087
  }, [memoryLoaded]);
770
1088
  // Derived values
@@ -779,6 +1097,19 @@ function TerminalChat() {
779
1097
  content
780
1098
  }]);
781
1099
  }, []);
1100
+ // Handler to edit or delete a queued message
1101
+ const handleEditQueuedMessage = useCallback((index, newMsg) => {
1102
+ if (newMsg === '') {
1103
+ // Delete the message
1104
+ setQueuedMessages(prev => prev.filter((_, i) => i !== index));
1105
+ addMessage('system', `🗑️ Deleted queued message #${index + 1}`);
1106
+ }
1107
+ else {
1108
+ // Update the message
1109
+ setQueuedMessages(prev => prev.map((msg, i) => i === index ? newMsg : msg));
1110
+ addMessage('system', `✏️ Updated queued message #${index + 1}`);
1111
+ }
1112
+ }, [addMessage]);
782
1113
  // Handle slash commands
783
1114
  const handleCommand = useCallback(async (cmd) => {
784
1115
  const parts = cmd.split(/\s+/);
@@ -827,12 +1158,19 @@ function TerminalChat() {
827
1158
  /bookmark [name] - Create bookmark at current point
828
1159
  /bookmark list - List all bookmarks
829
1160
  /bookmark goto <n> - Jump to bookmark
830
- /queue [show|clear] - Manage queued messages
1161
+ /queue [show|clear|flush] - Manage queued messages
1162
+ /flush - Force-process queued msgs (unstick)
1163
+ /debug [on|off] - Show state / toggle debug logging
1164
+ /unstick - Emergency reset of processing state
1165
+ /work - Quick switch to work mode
1166
+ /plan - Quick switch to plan mode
1167
+ /keys or /? - Show keyboard shortcuts
831
1168
  /resume [n] - Resume previous session (load n messages)
832
1169
  /exit - Exit
833
1170
 
834
1171
  File references: @filename, ./path, /absolute/path
835
1172
  Modes: 📋 Plan | 🔄 Hybrid | 🔧 Work
1173
+ Queue: ↑/↓ edit, Ctrl+D delete, Shift+Enter send directly
836
1174
  Auto-route: ${autoRoute ? 'ON' : 'OFF'}${moduleAgtermEnabled ? '\nAGTerm: ON (spawn_agent, check_agent tools available)' : ''}`);
837
1175
  break;
838
1176
  case '/provider':
@@ -912,6 +1250,7 @@ Auto-route: ${autoRoute ? 'ON' : 'OFF'}${moduleAgtermEnabled ? '\nAGTerm: ON (sp
912
1250
  setMessages([]);
913
1251
  llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
914
1252
  setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
1253
+ resetContextWarnings(); // Reset context warning state
915
1254
  break;
916
1255
  case '/copy': {
917
1256
  // Copy last assistant response to clipboard
@@ -1737,8 +2076,25 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1737
2076
  addMessage('system', output);
1738
2077
  }
1739
2078
  }
2079
+ else if (subCommand === 'work' && parts[2]) {
2080
+ const id = parts[2];
2081
+ const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
2082
+ const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
2083
+ if (todo) {
2084
+ storage.setActiveTodo(todo.id);
2085
+ storage.updateTodo(todo.id, { status: 'in_progress' });
2086
+ addMessage('system', `✓ Working on: ${todo.content}\n\nTip: I'll help you complete this task. Describe what you need.`);
2087
+ }
2088
+ else {
2089
+ addMessage('error', `TODO #${id} not found`);
2090
+ }
2091
+ }
2092
+ else if (subCommand === 'clear') {
2093
+ storage.setActiveTodo(null);
2094
+ addMessage('system', '✓ Active TODO cleared');
2095
+ }
1740
2096
  else {
1741
- addMessage('system', 'Usage: /todo [add <task>|done <id>|list]');
2097
+ addMessage('system', 'Usage: /todo [add <task>|done <id>|work <id>|clear|list]');
1742
2098
  }
1743
2099
  break;
1744
2100
  }
@@ -1765,8 +2121,27 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1765
2121
  addMessage('error', `Plan #${parts[2]} not found`);
1766
2122
  }
1767
2123
  }
2124
+ else if (subCommand === 'rerun' && parts[2]) {
2125
+ const plans = storage.getPlans();
2126
+ const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
2127
+ if (plan) {
2128
+ // Reset plan status and activate
2129
+ plan.status = 'in_progress';
2130
+ plan.phases.forEach(ph => ph.status = 'pending');
2131
+ storage.savePlan(plan);
2132
+ storage.setActivePlan(plan);
2133
+ // Generate prompt for re-execution
2134
+ const phaseList = plan.phases.map(ph => `- ${ph.name}`).join('\n');
2135
+ const prompt = `Please help me execute this plan:\n\n**${plan.title}**\n\nPhases:\n${phaseList}\n\nStart with the first phase.`;
2136
+ setInput(prompt);
2137
+ addMessage('system', `✓ Plan loaded: ${plan.title}\nPress Enter to start execution.`);
2138
+ }
2139
+ else {
2140
+ addMessage('error', `Plan #${parts[2]} not found`);
2141
+ }
2142
+ }
1768
2143
  else {
1769
- addMessage('system', 'Usage: /plans [list|view <id>]');
2144
+ addMessage('system', 'Usage: /plans [list|view <id>|rerun <id>]');
1770
2145
  }
1771
2146
  break;
1772
2147
  }
@@ -1895,6 +2270,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1895
2270
  addMessage('error', 'Usage: /template save <name> "<prompt>"');
1896
2271
  }
1897
2272
  else {
2273
+ storage.saveTemplate(name, prompt);
1898
2274
  setTemplates(prev => {
1899
2275
  const filtered = prev.filter(t => t.name !== name);
1900
2276
  return [...filtered, { name, prompt, createdAt: new Date() }];
@@ -1917,6 +2293,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
1917
2293
  const name = parts[2];
1918
2294
  const found = templates.find(t => t.name === name);
1919
2295
  if (found) {
2296
+ storage.deleteTemplate(name);
1920
2297
  setTemplates(prev => prev.filter(t => t.name !== name));
1921
2298
  addMessage('system', `✓ Template deleted: ${name}`);
1922
2299
  }
@@ -2025,11 +2402,126 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2025
2402
  addMessage('system', `📨 Queued messages:\n${list}`);
2026
2403
  }
2027
2404
  }
2405
+ else if (subCmd === 'flush') {
2406
+ // Force-process queued messages even if stuck
2407
+ if (queuedMessages.length === 0) {
2408
+ addMessage('system', 'No messages to flush.');
2409
+ }
2410
+ else {
2411
+ const queued = [...queuedMessages];
2412
+ setQueuedMessages([]);
2413
+ setIsProcessing(false); // Force reset processing state
2414
+ setThinkingState(null);
2415
+ setStreamingResponse('');
2416
+ addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
2417
+ const followUp = queued.length === 1
2418
+ ? queued[0]
2419
+ : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2420
+ setTimeout(() => {
2421
+ setIsProcessing(true);
2422
+ runAgent(followUp).finally(() => {
2423
+ setIsProcessing(false);
2424
+ setThinkingState(null);
2425
+ setStreamingResponse('');
2426
+ });
2427
+ }, 50);
2428
+ }
2429
+ }
2430
+ else {
2431
+ addMessage('system', 'Usage: /queue [show|clear|flush]\n\nTip: Type while agent is processing to queue follow-up messages.');
2432
+ }
2433
+ break;
2434
+ }
2435
+ case '/flush': {
2436
+ // Shortcut for /queue flush - force-process queued messages
2437
+ if (queuedMessages.length === 0) {
2438
+ addMessage('system', 'No messages to flush. Use /debug to see current state.');
2439
+ }
2440
+ else {
2441
+ const queued = [...queuedMessages];
2442
+ setQueuedMessages([]);
2443
+ setIsProcessing(false); // Force reset processing state
2444
+ setThinkingState(null);
2445
+ setStreamingResponse('');
2446
+ addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
2447
+ const followUp = queued.length === 1
2448
+ ? queued[0]
2449
+ : `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
2450
+ setTimeout(() => {
2451
+ setIsProcessing(true);
2452
+ runAgent(followUp).finally(() => {
2453
+ setIsProcessing(false);
2454
+ setThinkingState(null);
2455
+ setStreamingResponse('');
2456
+ });
2457
+ }, 50);
2458
+ }
2459
+ break;
2460
+ }
2461
+ case '/debug': {
2462
+ const subCmd = parts[1];
2463
+ if (subCmd === 'on') {
2464
+ debugEnabled = true;
2465
+ addMessage('system', '🔍 Debug logging ON (output to stderr). Use /debug off to disable.');
2466
+ }
2467
+ else if (subCmd === 'off') {
2468
+ debugEnabled = false;
2469
+ addMessage('system', '🔍 Debug logging OFF');
2470
+ }
2471
+ else {
2472
+ // Show internal state for debugging stuck issues
2473
+ const debugInfo = [
2474
+ `isProcessing: ${isProcessing}`,
2475
+ `queuedMessages: ${queuedMessages.length}`,
2476
+ `modalMode: ${modalMode}`,
2477
+ `confirmMode: ${confirmMode}`,
2478
+ `loopActive: ${loopActive}`,
2479
+ `thinkingState: ${thinkingState ? JSON.stringify(thinkingState) : 'null'}`,
2480
+ `streamingResponse length: ${streamingResponse.length}`,
2481
+ `llmMessages count: ${llmMessages.current.length}`,
2482
+ `mode: ${mode}`,
2483
+ `debugEnabled: ${debugEnabled}`,
2484
+ ];
2485
+ addMessage('system', `🔍 Debug State:\n${debugInfo.join('\n')}\n\nUse /debug on|off to toggle logging.`);
2486
+ }
2487
+ break;
2488
+ }
2489
+ case '/unstick': {
2490
+ // Emergency reset of processing state
2491
+ setIsProcessing(false);
2492
+ setThinkingState(null);
2493
+ setStreamingResponse('');
2494
+ setLoopActive(false);
2495
+ setModalMode('none');
2496
+ setPendingComplexPrompt(null);
2497
+ // Also reset to hybrid mode if stuck in plan mode
2498
+ if (mode === 'plan') {
2499
+ setMode('hybrid');
2500
+ addMessage('system', '🔧 Reset processing state + switched from plan to hybrid mode.');
2501
+ }
2028
2502
  else {
2029
- addMessage('system', 'Usage: /queue [show|clear]\n\nTip: Type while agent is processing to queue follow-up messages.');
2503
+ addMessage('system', '🔧 Reset processing state. You can now submit new messages.');
2030
2504
  }
2031
2505
  break;
2032
2506
  }
2507
+ case '/keys':
2508
+ case '/?': {
2509
+ // Show keybindings modal
2510
+ setModalMode('keys');
2511
+ break;
2512
+ }
2513
+ case '/work': {
2514
+ // Quick shortcut to enter work mode
2515
+ setMode('work');
2516
+ addMessage('system', `Mode: ${MODE_CONFIG['work'].icon} ${MODE_CONFIG['work'].label} - ${MODE_CONFIG['work'].description}`);
2517
+ break;
2518
+ }
2519
+ case '/plan': {
2520
+ // Quick shortcut to enter plan mode
2521
+ setMode('plan');
2522
+ addMessage('system', `Mode: ${MODE_CONFIG['plan'].icon} ${MODE_CONFIG['plan'].label} - ${MODE_CONFIG['plan'].description}`);
2523
+ break;
2524
+ }
2033
2525
  case '/resume': {
2034
2526
  // Resume previous session manually
2035
2527
  const history = storage.getChatHistory(parseInt(parts[1]) || 20);
@@ -2058,8 +2550,44 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2058
2550
  addMessage('error', `Unknown command: ${command}. Type /help for help.`);
2059
2551
  }
2060
2552
  }, [actualProvider, actualModel, persona, stats, addMessage, exit]);
2553
+ // Validate and repair message history to ensure tool_use always has tool_result
2554
+ const validateAndRepairMessages = useCallback(() => {
2555
+ const messages = llmMessages.current;
2556
+ let repaired = false;
2557
+ for (let i = 0; i < messages.length; i++) {
2558
+ const msg = messages[i];
2559
+ if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
2560
+ // Check that each tool_use has a corresponding tool_result
2561
+ for (const toolCall of msg.toolCalls) {
2562
+ const hasResult = messages.slice(i + 1).some(m => m.role === 'tool' && m.toolCallId === toolCall.id);
2563
+ if (!hasResult) {
2564
+ // Add a placeholder tool_result for the missing tool call
2565
+ debugLog('repair', 'Adding missing tool_result for', toolCall.id);
2566
+ // Find the right position to insert (right after this assistant message or after existing tool results)
2567
+ let insertPos = i + 1;
2568
+ while (insertPos < messages.length && messages[insertPos].role === 'tool') {
2569
+ insertPos++;
2570
+ }
2571
+ messages.splice(insertPos, 0, {
2572
+ role: 'tool',
2573
+ content: '[Error: Tool execution was interrupted. Please retry.]',
2574
+ toolCallId: toolCall.id,
2575
+ });
2576
+ repaired = true;
2577
+ }
2578
+ }
2579
+ }
2580
+ }
2581
+ if (repaired) {
2582
+ addMessage('system', '🔧 Repaired corrupted message history (missing tool results).');
2583
+ }
2584
+ return repaired;
2585
+ }, [addMessage]);
2061
2586
  // Run agent with user prompt
2062
2587
  const runAgent = useCallback(async (content) => {
2588
+ debugLog('runAgent', 'ENTER', typeof content === 'string' ? content.substring(0, 50) : '[complex]');
2589
+ // Validate message history before adding new content
2590
+ validateAndRepairMessages();
2063
2591
  llmMessages.current.push({ role: 'user', content });
2064
2592
  setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
2065
2593
  setStreamingResponse('');
@@ -2111,7 +2639,9 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2111
2639
  maxIterations,
2112
2640
  });
2113
2641
  };
2642
+ debugLog('chat', 'WAITING for LLM response', `iteration=${i + 1}`);
2114
2643
  const response = await chat(provider, llmMessages.current, getTools(moduleAgtermEnabled), effectiveModel, onToken, onRetry);
2644
+ debugLog('chat', 'GOT response', `toolCalls=${response.toolCalls?.length ?? 0}`);
2115
2645
  // Update token stats and cost
2116
2646
  if (response.usage) {
2117
2647
  const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
@@ -2205,6 +2735,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2205
2735
  maxIterations,
2206
2736
  });
2207
2737
  // Execute in parallel using dependency-aware staging
2738
+ debugLog('tools', 'PARALLEL exec start', `count=${executableTools.length}`);
2208
2739
  const results = await executeParallel(executableTools, async (call) => {
2209
2740
  const result = await executeTool(call, process.cwd());
2210
2741
  return result.result;
@@ -2216,6 +2747,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2216
2747
  maxIterations,
2217
2748
  });
2218
2749
  });
2750
+ debugLog('tools', 'PARALLEL exec done', `results=${results.length}`);
2219
2751
  // Process results sequentially for UI and LLM messages
2220
2752
  for (const result of results) {
2221
2753
  const toolCall = result.toolCall;
@@ -2247,6 +2779,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2247
2779
  }
2248
2780
  else {
2249
2781
  // Sequential execution (single tool or dependencies prevent parallelization)
2782
+ debugLog('tools', 'SEQUENTIAL exec start', `count=${executableTools.length}`);
2250
2783
  for (const toolCall of executableTools) {
2251
2784
  const args = toolCall.arguments;
2252
2785
  const toolPreview = String(args.command || args.path || '...');
@@ -2270,7 +2803,9 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2270
2803
  maxIterations,
2271
2804
  });
2272
2805
  }
2806
+ debugLog('tools', 'EXEC', toolCall.name, toolPreview.substring(0, 30));
2273
2807
  const result = await executeTool(toolCall, process.cwd());
2808
+ debugLog('tools', 'DONE', toolCall.name);
2274
2809
  // Execute post-tool hooks
2275
2810
  hooks.executeHooks('post-tool', {
2276
2811
  tool: toolCall.name,
@@ -2302,6 +2837,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2302
2837
  addMessage('assistant', response.content);
2303
2838
  setStreamingResponse('');
2304
2839
  setContextTokens(estimateContextTokens());
2840
+ checkAndWarnContextLimit(actualModel, estimateContextTokens(), addMessage);
2305
2841
  // Auto-continue if response was truncated due to length
2306
2842
  if (response.finishReason === 'length') {
2307
2843
  addMessage('system', '(auto-continuing...)');
@@ -2314,9 +2850,32 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2314
2850
  catch (error) {
2315
2851
  setThinkingState(null);
2316
2852
  setStreamingResponse('');
2317
- addMessage('error', formatError(error));
2853
+ // Format error with provider context for better suggestions
2854
+ const errorMsg = formatError(error, { provider: actualProvider });
2855
+ addMessage('error', errorMsg);
2856
+ // Classify error to provide additional recovery suggestions
2857
+ const classified = classifyError(error);
2858
+ const availableProviders = getAvailableProviders();
2859
+ const otherProviders = availableProviders.filter(p => p !== actualProvider);
2860
+ // Suggest alternatives based on error type
2861
+ if (classified.category === 'rate_limit' || classified.category === 'server') {
2862
+ if (otherProviders.length > 0) {
2863
+ addMessage('system', `💡 Try switching providers: /provider ${otherProviders[0]} or /models to see alternatives`);
2864
+ }
2865
+ }
2866
+ else if (classified.category === 'timeout' || classified.category === 'network') {
2867
+ addMessage('system', `💡 Network issue detected. Check connection and try again, or use /provider to switch.`);
2868
+ }
2869
+ else if (classified.category === 'auth') {
2870
+ addMessage('system', `💡 Run 'calliope --setup' to reconfigure API keys.`);
2871
+ }
2318
2872
  completedNaturally = true; // Error counts as "done" - don't show iteration warning
2319
- break;
2873
+ // On error, clear queued messages to prevent infinite retry loop
2874
+ if (queuedMessages.length > 0) {
2875
+ addMessage('system', `⚠️ Cleared ${queuedMessages.length} queued message(s) due to error. Use /clear to reset conversation.`);
2876
+ setQueuedMessages([]);
2877
+ }
2878
+ return; // Exit early on error - don't process queued messages
2320
2879
  }
2321
2880
  }
2322
2881
  // Only show warning if we actually hit the iteration limit (not errors or natural completion)
@@ -2326,6 +2885,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2326
2885
  // Update context tokens after agent run
2327
2886
  setContextTokens(estimateContextTokens());
2328
2887
  // Process any queued messages (human-in-the-loop feedback)
2888
+ debugLog('runAgent', 'EXIT loop', `queued=${queuedMessages.length}`);
2329
2889
  if (queuedMessages.length > 0) {
2330
2890
  const queued = [...queuedMessages];
2331
2891
  setQueuedMessages([]); // Clear the queue
@@ -2336,10 +2896,20 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2336
2896
  addMessage('system', `📨 Processing ${queued.length} queued message${queued.length > 1 ? 's' : ''}...`);
2337
2897
  // Recursively run agent with follow-up
2338
2898
  // Use setTimeout to avoid stack overflow and allow UI to update
2899
+ // Note: handleSubmit's finally will set isProcessing=false, so we need to re-enable it
2900
+ debugLog('runAgent', 'SCHEDULING recursive call for queued messages');
2339
2901
  setTimeout(() => {
2340
- runAgent(followUp);
2902
+ debugLog('runAgent', 'RECURSIVE call starting');
2903
+ setIsProcessing(true);
2904
+ runAgent(followUp).finally(() => {
2905
+ setIsProcessing(false);
2906
+ setThinkingState(null);
2907
+ setStreamingResponse('');
2908
+ setEditingQueueIndex(null);
2909
+ });
2341
2910
  }, 100);
2342
2911
  }
2912
+ debugLog('runAgent', 'RETURN');
2343
2913
  }, [provider, model, addMessage, mode, estimateContextTokens, queuedMessages]);
2344
2914
  // Ralph Wiggum loop - runs prompt repeatedly until completion promise or max iterations
2345
2915
  const runLoop = useCallback(async (prompt, maxIter, completionPromise) => {
@@ -2492,8 +3062,48 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2492
3062
  return next;
2493
3063
  });
2494
3064
  }, []);
3065
+ // Handle Escape key - cancel operation if processing, otherwise show hint
3066
+ const handleEscape = useCallback(() => {
3067
+ if (isProcessing) {
3068
+ // Cancel current operation
3069
+ setIsProcessing(false);
3070
+ setThinkingState(null);
3071
+ setStreamingResponse('');
3072
+ setLoopActive(false);
3073
+ setEditingQueueIndex(null);
3074
+ addMessage('system', '⏹ Operation cancelled. Use /exit to quit.');
3075
+ }
3076
+ else if (modalMode !== 'none') {
3077
+ // Close any open modal
3078
+ setModalMode('none');
3079
+ setPendingComplexPrompt(null);
3080
+ }
3081
+ else {
3082
+ // Not processing - show hint instead of exiting
3083
+ addMessage('system', '💡 Use /exit to quit, or Ctrl+C.');
3084
+ }
3085
+ }, [isProcessing, modalMode, addMessage]);
3086
+ // Handle direct send (Shift+Enter) - interrupts current operation and sends immediately
3087
+ const handleDirectSend = useCallback((msg) => {
3088
+ // Stop current processing
3089
+ setIsProcessing(false);
3090
+ setThinkingState(null);
3091
+ setStreamingResponse('');
3092
+ setEditingQueueIndex(null);
3093
+ // Show what happened
3094
+ addMessage('system', '⚡ Direct send - interrupting current operation');
3095
+ addMessage('user', msg);
3096
+ // Start new agent run with this message
3097
+ setIsProcessing(true);
3098
+ runAgent(msg).finally(() => {
3099
+ setIsProcessing(false);
3100
+ setThinkingState(null);
3101
+ setStreamingResponse('');
3102
+ setEditingQueueIndex(null);
3103
+ });
3104
+ }, [addMessage, runAgent]);
2495
3105
  // Render
2496
- 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: () => {
3106
+ 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: "Waiting for response..." }), isProcessing && streamingResponse && _jsx(StreamingIndicator, {}), debugEnabled && (_jsx(Box, { marginY: 0, children: _jsxs(Text, { dimColor: true, children: ["[dbg] proc=", isProcessing ? 'Y' : 'N', " think=", thinkingState ? 'Y' : 'N', " stream=", streamingResponse.length, " mode=", mode, " queue=", queuedMessages.length] }) })), 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: () => {
2497
3107
  // Load chat history into context
2498
3108
  const history = storage.getChatHistory(20);
2499
3109
  if (history.length > 0) {
@@ -2514,7 +3124,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2514
3124
  addMessage('system', '✓ Starting fresh session');
2515
3125
  setModalMode('none');
2516
3126
  setPreviousSession(null);
2517
- } })), modalMode === 'complexity-warning' && pendingComplexPrompt && (_jsx(ComplexityWarning, { reason: pendingComplexPrompt.complexity.reason || 'Complex operation detected', onProceed: async () => {
3127
+ } })), modalMode === 'complexity-warning' && pendingComplexPrompt && (_jsx(ComplexityWarning, { reason: pendingComplexPrompt.complexity.reason || 'Complex operation detected', prompt: typeof pendingComplexPrompt.prompt === 'string' ? pendingComplexPrompt.prompt : undefined, onProceed: async () => {
2518
3128
  setModalMode('none');
2519
3129
  const prompt = pendingComplexPrompt.prompt;
2520
3130
  setPendingComplexPrompt(null);
@@ -2543,10 +3153,12 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
2543
3153
  setModalMode('none');
2544
3154
  setPendingComplexPrompt(null);
2545
3155
  addMessage('system', 'Operation cancelled.');
2546
- } })), _jsx(ChatInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: exit, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, onQueueMessage: (msg) => {
3156
+ } })), modalMode === 'keys' && (_jsx(KeybindingsModal, { onClose: () => setModalMode('none') })), _jsx(ChatInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: handleEscape, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, editingQueueIndex: editingQueueIndex, onQueueMessage: (msg) => {
2547
3157
  setQueuedMessages(prev => [...prev, msg]);
2548
3158
  addMessage('system', `📨 Queued: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}"`);
2549
- }, cwd: process.cwd(), suggestions: suggestions, onSuggestionsChange: setSuggestions, onNavigateHistory: navigateHistory }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
3159
+ }, onEditQueuedMessage: handleEditQueuedMessage, onSetEditingQueueIndex: setEditingQueueIndex, onDirectSend: handleDirectSend, cwd: process.cwd(), suggestions: suggestions, onSuggestionsChange: setSuggestions, onNavigateHistory: navigateHistory,
3160
+ // Smart suggestions context
3161
+ currentMode: mode, contextPercentage: Math.round((contextTokens / getContextLimit(actualModel)) * 100), recentCommands: recentCommands, hasGitRepo: hasGitRepo }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
2550
3162
  }
2551
3163
  // ============================================================================
2552
3164
  // App Wrapper & Entry Point