@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.
- package/dist/agterm/agent-detection.d.ts +35 -0
- package/dist/agterm/agent-detection.d.ts.map +1 -0
- package/dist/agterm/agent-detection.js +94 -0
- package/dist/agterm/agent-detection.js.map +1 -0
- package/dist/agterm/cli-backend.d.ts +32 -0
- package/dist/agterm/cli-backend.d.ts.map +1 -0
- package/dist/agterm/cli-backend.js +193 -0
- package/dist/agterm/cli-backend.js.map +1 -0
- package/dist/agterm/index.d.ts +12 -0
- package/dist/agterm/index.d.ts.map +1 -0
- package/dist/agterm/index.js +15 -0
- package/dist/agterm/index.js.map +1 -0
- package/dist/agterm/orchestrator.d.ts +94 -0
- package/dist/agterm/orchestrator.d.ts.map +1 -0
- package/dist/agterm/orchestrator.js +302 -0
- package/dist/agterm/orchestrator.js.map +1 -0
- package/dist/agterm/tools.d.ts +24 -0
- package/dist/agterm/tools.d.ts.map +1 -0
- package/dist/agterm/tools.js +277 -0
- package/dist/agterm/tools.js.map +1 -0
- package/dist/agterm/types.d.ts +95 -0
- package/dist/agterm/types.d.ts.map +1 -0
- package/dist/agterm/types.js +38 -0
- package/dist/agterm/types.js.map +1 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +18 -3
- package/dist/bin.js.map +1 -1
- package/dist/errors.d.ts +5 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +107 -4
- package/dist/errors.js.map +1 -1
- package/dist/plugins.d.ts +120 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +356 -0
- package/dist/plugins.js.map +1 -0
- package/dist/providers.js +208 -58
- package/dist/providers.js.map +1 -1
- package/dist/scope.d.ts +89 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +253 -0
- package/dist/scope.js.map +1 -0
- package/dist/storage.d.ts +23 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +83 -0
- package/dist/storage.js.map +1 -1
- package/dist/styles.d.ts +49 -83
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +140 -126
- package/dist/styles.js.map +1 -1
- package/dist/tools.d.ts +5 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +24 -10
- package/dist/tools.js.map +1 -1
- package/dist/ui-cli.d.ts +1 -0
- package/dist/ui-cli.d.ts.map +1 -1
- package/dist/ui-cli.js +704 -106
- package/dist/ui-cli.js.map +1 -1
- 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 {
|
|
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
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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,
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1524
|
-
//
|
|
1525
|
-
|
|
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:
|
|
1963
|
+
content: preCheck.blockContent,
|
|
1534
1964
|
toolCallId: toolCall.id,
|
|
1535
1965
|
});
|
|
1536
|
-
continue;
|
|
1537
1966
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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
|
|
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 /
|
|
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
|
-
|
|
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 })),
|
|
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
|
-
|
|
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, {}), {
|