@calliopelabs/cli 0.4.6 → 0.6.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/README.md +258 -105
- package/dist/branching.d.ts +56 -0
- package/dist/branching.d.ts.map +1 -0
- package/dist/branching.js +211 -0
- package/dist/branching.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +235 -2
- package/dist/cli.js.map +1 -1
- package/dist/completion.d.ts +75 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +234 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +73 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +317 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +199 -0
- package/dist/errors.js.map +1 -0
- package/dist/file-watcher.d.ts +91 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +269 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/files.d.ts +49 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +191 -0
- package/dist/files.js.map +1 -0
- package/dist/fuzzy-search.d.ts +75 -0
- package/dist/fuzzy-search.d.ts.map +1 -0
- package/dist/fuzzy-search.js +240 -0
- package/dist/fuzzy-search.js.map +1 -0
- package/dist/hooks.d.ts +79 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +271 -0
- package/dist/hooks.js.map +1 -0
- package/dist/keyboard.d.ts +57 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +265 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +248 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.d.ts +90 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +290 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +104 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +394 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +67 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +289 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-tools.d.ts +51 -0
- package/dist/parallel-tools.d.ts.map +1 -0
- package/dist/parallel-tools.js +278 -0
- package/dist/parallel-tools.js.map +1 -0
- package/dist/project-config.d.ts +84 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +250 -0
- package/dist/project-config.js.map +1 -0
- package/dist/providers.d.ts +10 -2
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +240 -38
- package/dist/providers.js.map +1 -1
- package/dist/risk.d.ts +31 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +367 -0
- package/dist/risk.js.map +1 -0
- package/dist/sandbox.d.ts +49 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +347 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/skills.d.ts +71 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +383 -0
- package/dist/skills.js.map +1 -0
- package/dist/storage.d.ts +139 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +508 -0
- package/dist/storage.js.map +1 -0
- package/dist/streaming.d.ts +94 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +305 -0
- package/dist/streaming.js.map +1 -0
- package/dist/summarization.d.ts +76 -0
- package/dist/summarization.d.ts.map +1 -0
- package/dist/summarization.js +242 -0
- package/dist/summarization.js.map +1 -0
- package/dist/themes.d.ts +110 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +329 -0
- package/dist/themes.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +335 -1
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +56 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +105 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-cli.d.ts +9 -2
- package/dist/ui-cli.d.ts.map +1 -1
- package/dist/ui-cli.js +1315 -220
- package/dist/ui-cli.js.map +1 -1
- package/package.json +1 -1
package/dist/ui-cli.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Calliope CLI - Ink UI
|
|
3
|
+
* Calliope CLI - Ink UI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Component hierarchy inspired by Claude Code:
|
|
6
|
+
* App
|
|
7
|
+
* └── TerminalChat (main hub)
|
|
8
|
+
* ├── MessageHistory (Static for messages)
|
|
9
|
+
* │ └── MessageItem (formatted messages)
|
|
10
|
+
* ├── ProcessingIndicator (animated spinner)
|
|
11
|
+
* ├── ChatInput (input line)
|
|
12
|
+
* └── StatusBar (footer)
|
|
6
13
|
*/
|
|
7
14
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
8
15
|
import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
|
|
9
16
|
import TextInput from 'ink-text-input';
|
|
17
|
+
import * as fs from 'fs';
|
|
10
18
|
import * as config from './config.js';
|
|
11
19
|
import { chat, getAvailableProviders, selectProvider } from './providers.js';
|
|
12
20
|
import { TOOLS, executeTool } from './tools.js';
|
|
13
|
-
import { getSystemPrompt, DEFAULT_MODELS } from './types.js';
|
|
21
|
+
import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
|
|
14
22
|
import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
|
|
15
23
|
import { getAvailableModels } from './model-detection.js';
|
|
16
|
-
|
|
24
|
+
import { assessToolRisk } from './risk.js';
|
|
25
|
+
import { formatError } from './errors.js';
|
|
26
|
+
import * as storage from './storage.js';
|
|
27
|
+
import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
|
|
28
|
+
import { renderMarkdown } from './markdown.js';
|
|
29
|
+
import * as mcp from './mcp.js';
|
|
30
|
+
import * as skills from './skills.js';
|
|
31
|
+
import * as memory from './memory.js';
|
|
32
|
+
import * as hooks from './hooks.js';
|
|
33
|
+
import * as modelRouter from './model-router.js';
|
|
34
|
+
import * as summarization from './summarization.js';
|
|
35
|
+
import { requiresConfirmation } from './risk.js';
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Constants
|
|
38
|
+
// ============================================================================
|
|
17
39
|
const BANNER_LINES = [
|
|
18
40
|
' ██████╗ █████╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗',
|
|
19
41
|
'██╔════╝██╔══██╗██║ ██║ ██║██╔═══██╗██╔══██╗██╔════╝',
|
|
@@ -22,185 +44,367 @@ const BANNER_LINES = [
|
|
|
22
44
|
'╚██████╗██║ ██║███████╗███████╗██║╚██████╔╝██║ ███████╗',
|
|
23
45
|
' ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚══════╝',
|
|
24
46
|
];
|
|
25
|
-
// Format helpers
|
|
26
|
-
const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
27
|
-
const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
|
|
28
|
-
const formatTime = (d) => {
|
|
29
|
-
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
30
|
-
if (s < 60)
|
|
31
|
-
return `${s}s`;
|
|
32
|
-
const m = Math.floor(s / 60);
|
|
33
|
-
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h${m % 60}m`;
|
|
34
|
-
};
|
|
35
|
-
// Spinner frames
|
|
36
47
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
37
|
-
// Animated Spinner component
|
|
38
|
-
function Spinner({ label }) {
|
|
39
|
-
const [frame, setFrame] = useState(0);
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
const timer = setInterval(() => {
|
|
42
|
-
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
43
|
-
}, 80);
|
|
44
|
-
return () => clearInterval(timer);
|
|
45
|
-
}, []);
|
|
46
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
47
|
-
}
|
|
48
|
-
// Tool icons
|
|
49
48
|
const TOOL_ICONS = {
|
|
50
49
|
shell: '⚡',
|
|
51
50
|
read_file: '📄',
|
|
52
51
|
write_file: '✍️',
|
|
53
52
|
list_files: '📁',
|
|
54
53
|
think: '💭',
|
|
54
|
+
execute_code: '▶️',
|
|
55
|
+
web_search: '🔍',
|
|
56
|
+
git: '🔀',
|
|
57
|
+
mermaid: '📊',
|
|
55
58
|
};
|
|
56
|
-
//
|
|
57
|
-
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Utility Components
|
|
61
|
+
// ============================================================================
|
|
62
|
+
function Separator() {
|
|
58
63
|
const { stdout } = useStdout();
|
|
59
|
-
|
|
64
|
+
const width = stdout?.columns || 80;
|
|
65
|
+
return _jsx(Text, { dimColor: true, children: '─'.repeat(width) });
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
function ThinkingDisplay({ state }) {
|
|
68
|
+
const [frame, setFrame] = useState(0);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const timer = setInterval(() => {
|
|
71
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
72
|
+
}, 80);
|
|
73
|
+
return () => clearInterval(timer);
|
|
74
|
+
}, []);
|
|
75
|
+
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: " ..." }))] }))] }));
|
|
76
|
+
}
|
|
77
|
+
// Legacy simple indicator for non-agent operations
|
|
78
|
+
function ProcessingIndicator({ label }) {
|
|
79
|
+
const [frame, setFrame] = useState(0);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const timer = setInterval(() => {
|
|
82
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
83
|
+
}, 80);
|
|
84
|
+
return () => clearInterval(timer);
|
|
85
|
+
}, []);
|
|
86
|
+
return (_jsxs(Box, { marginY: 1, children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
87
|
+
}
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Message Components
|
|
90
|
+
// ============================================================================
|
|
91
|
+
function MessageItem({ msg }) {
|
|
92
|
+
switch (msg.type) {
|
|
93
|
+
case 'user':
|
|
94
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u203A" }), " ", msg.content] }) }));
|
|
95
|
+
case 'assistant': {
|
|
96
|
+
// Render markdown with syntax highlighting
|
|
97
|
+
const rendered = renderMarkdown(msg.content);
|
|
98
|
+
const lines = rendered.split('\n');
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i)))] }));
|
|
100
|
+
}
|
|
101
|
+
case 'tool': {
|
|
102
|
+
const isToolCall = msg.content.startsWith('⚡');
|
|
103
|
+
if (isToolCall) {
|
|
104
|
+
const match = msg.content.match(/^⚡ (\w+): (.*)$/);
|
|
105
|
+
if (match) {
|
|
106
|
+
const [, toolName, preview] = match;
|
|
107
|
+
const icon = TOOL_ICONS[toolName] || '⚙️';
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), " ", icon, " ", _jsx(Text, { color: "yellow", children: toolName })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: preview })] })] }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Check for diff output from write_file
|
|
112
|
+
const isDiff = msg.content.startsWith('DIFF:');
|
|
113
|
+
if (isDiff) {
|
|
114
|
+
const lines = msg.content.split('\n');
|
|
115
|
+
const header = lines[0];
|
|
116
|
+
const isNewFile = header.includes('NEW_FILE:');
|
|
117
|
+
const filePath = isNewFile
|
|
118
|
+
? header.replace('DIFF:NEW_FILE:', '')
|
|
119
|
+
: header.replace('DIFF:', '');
|
|
120
|
+
const diffLines = lines.slice(1, 12);
|
|
121
|
+
const hasMore = lines.length > 12;
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u251C\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" ", isNewFile ? '(new file)' : '(modified)'] })] }), diffLines.map((line, i) => {
|
|
123
|
+
let color;
|
|
124
|
+
if (line.startsWith('+ '))
|
|
125
|
+
color = 'green';
|
|
126
|
+
else if (line.startsWith('- '))
|
|
127
|
+
color = 'red';
|
|
128
|
+
else if (line.startsWith('@@'))
|
|
129
|
+
color = 'cyan';
|
|
130
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] })] }, i));
|
|
131
|
+
}), 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 })] })] }));
|
|
79
132
|
}
|
|
133
|
+
// Regular tool result
|
|
134
|
+
const lines = msg.content.split('\n').slice(0, 5);
|
|
135
|
+
const hasMore = msg.content.split('\n').length > 5;
|
|
136
|
+
const hasError = msg.content.toLowerCase().includes('error');
|
|
137
|
+
return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: line.substring(0, 100) })] }, i))), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", hasError ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" })] })] }));
|
|
80
138
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
139
|
+
case 'system':
|
|
140
|
+
return _jsx(Text, { color: "yellow", children: msg.content });
|
|
141
|
+
case 'error':
|
|
142
|
+
return _jsxs(Text, { color: "red", children: ["\u2717 ", msg.content] });
|
|
143
|
+
default:
|
|
144
|
+
return _jsx(Text, { children: msg.content });
|
|
86
145
|
}
|
|
87
|
-
if (msg.type === 'system') {
|
|
88
|
-
return _jsx(Text, { color: "yellow", children: msg.content });
|
|
89
|
-
}
|
|
90
|
-
// error
|
|
91
|
-
return _jsxs(Text, { color: "red", children: ["\u2717 ", msg.content] });
|
|
92
146
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
147
|
+
function MessageHistory({ messages }) {
|
|
148
|
+
return (_jsx(Static, { items: messages, children: (msg) => (_jsx(Box, { children: _jsx(MessageItem, { msg: msg }) }, msg.id)) }));
|
|
149
|
+
}
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Modal Components
|
|
152
|
+
// ============================================================================
|
|
153
|
+
function ModelSelector({ models, onSelect, onCancel }) {
|
|
154
|
+
const [index, setIndex] = useState(0);
|
|
95
155
|
const pageSize = 10;
|
|
96
156
|
const start = Math.max(0, Math.min(index - Math.floor(pageSize / 2), models.length - pageSize));
|
|
97
157
|
const visible = models.slice(start, start + pageSize);
|
|
98
158
|
useInput((input, key) => {
|
|
99
|
-
if (key.upArrow)
|
|
159
|
+
if (key.upArrow)
|
|
100
160
|
setIndex(i => Math.max(0, i - 1));
|
|
101
|
-
|
|
102
|
-
else if (key.downArrow) {
|
|
161
|
+
else if (key.downArrow)
|
|
103
162
|
setIndex(i => Math.min(models.length - 1, i + 1));
|
|
104
|
-
|
|
105
|
-
else if (key.return) {
|
|
163
|
+
else if (key.return)
|
|
106
164
|
onSelect(models[index].id);
|
|
107
|
-
|
|
108
|
-
else if (key.escape || input === 'q') {
|
|
165
|
+
else if (key.escape || input === 'q')
|
|
109
166
|
onCancel();
|
|
110
|
-
}
|
|
111
167
|
});
|
|
112
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Select model (\u2191/\u2193 navigate, Enter select, Esc cancel):" }), visible.map((model, i) => {
|
|
168
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "yellow", children: "Select model (\u2191/\u2193 navigate, Enter select, Esc cancel):" }), visible.map((model, i) => {
|
|
113
169
|
const globalIndex = start + i;
|
|
114
170
|
const isSelected = globalIndex === index;
|
|
115
171
|
const name = model.name || model.id;
|
|
116
172
|
const displayName = name.length > 50 ? name.slice(0, 47) + '...' : name;
|
|
117
|
-
return (
|
|
173
|
+
return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', displayName] }, model.id));
|
|
118
174
|
}), models.length > pageSize && (_jsxs(Text, { dimColor: true, children: [" (", index + 1, "/", models.length, ")"] }))] }));
|
|
119
175
|
}
|
|
120
176
|
function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
|
|
121
177
|
useInput((input, key) => {
|
|
122
|
-
if (input === 'y' || input === 'Y')
|
|
178
|
+
if (input === 'y' || input === 'Y')
|
|
123
179
|
onConfirm();
|
|
124
|
-
|
|
125
|
-
else if (input === 'n' || input === 'N' || key.escape) {
|
|
180
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
126
181
|
onCancel();
|
|
127
|
-
}
|
|
128
182
|
});
|
|
129
|
-
return (_jsxs(Box, { flexDirection: "column", 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
|
+
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)" })] })] }));
|
|
184
|
+
}
|
|
185
|
+
function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
|
|
186
|
+
useInput((input, key) => {
|
|
187
|
+
if (input === 'y' || input === 'Y')
|
|
188
|
+
onConfirm();
|
|
189
|
+
else if (input === 'n' || input === 'N' || key.escape)
|
|
190
|
+
onDeny();
|
|
191
|
+
});
|
|
192
|
+
const args = toolCall.arguments;
|
|
193
|
+
const preview = String(args.command || args.path || args.operation || '...');
|
|
194
|
+
const riskColor = riskLevel === 'critical' ? 'red' : 'yellow';
|
|
195
|
+
const riskIcon = riskLevel === 'critical' ? '⚠️' : '⚡';
|
|
196
|
+
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)" })] })] }));
|
|
197
|
+
}
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Input Components
|
|
200
|
+
// ============================================================================
|
|
201
|
+
function ChatInput({ value, onChange, onSubmit, disabled, mode }) {
|
|
202
|
+
if (disabled)
|
|
203
|
+
return null;
|
|
204
|
+
const modeConfig = MODE_CONFIG[mode];
|
|
205
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "calliope " }), _jsx(Text, { children: modeConfig.icon }), _jsx(Text, { dimColor: true, children: "> " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] })] }));
|
|
206
|
+
}
|
|
207
|
+
// Context window limits by model (approximate)
|
|
208
|
+
const CONTEXT_LIMITS = {
|
|
209
|
+
'claude-sonnet-4': 200000,
|
|
210
|
+
'claude-opus-4': 200000,
|
|
211
|
+
'claude-3': 200000,
|
|
212
|
+
'gpt-4o': 128000,
|
|
213
|
+
'gpt-4-turbo': 128000,
|
|
214
|
+
'gpt-4': 8192,
|
|
215
|
+
'gemini-2': 1000000,
|
|
216
|
+
'gemini-1.5-pro': 1000000,
|
|
217
|
+
'gemini-1.5-flash': 1000000,
|
|
218
|
+
'llama-3.3': 128000,
|
|
219
|
+
'llama-3.1': 128000,
|
|
220
|
+
'mistral-large': 128000,
|
|
221
|
+
'default': 32000,
|
|
222
|
+
};
|
|
223
|
+
function getContextLimit(model) {
|
|
224
|
+
for (const [key, limit] of Object.entries(CONTEXT_LIMITS)) {
|
|
225
|
+
if (model.toLowerCase().includes(key.toLowerCase())) {
|
|
226
|
+
return limit;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return CONTEXT_LIMITS.default;
|
|
130
230
|
}
|
|
131
|
-
|
|
132
|
-
|
|
231
|
+
function StatusBar({ provider, model, stats, mode, contextTokens, }) {
|
|
232
|
+
const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
233
|
+
const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
|
|
234
|
+
const displayModel = model.length > 25 ? model.slice(0, 22) + '...' : model;
|
|
235
|
+
const modeConfig = MODE_CONFIG[mode];
|
|
236
|
+
// Context usage indicator
|
|
237
|
+
const contextLimit = getContextLimit(model);
|
|
238
|
+
const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100));
|
|
239
|
+
const contextColor = contextPct > 80 ? 'red' : contextPct > 50 ? 'yellow' : 'green';
|
|
240
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [modeConfig.icon, " ", modeConfig.label, ' │ ', provider, ":", displayModel, ' │ ', _jsxs(Text, { color: contextColor, children: [formatTokens(contextTokens), "/", formatTokens(contextLimit)] }), ' │ ', formatTokens(stats.inputTokens + stats.outputTokens), " used", ' │ ', formatCost(stats.cost), ' │ ', _jsx(Text, { dimColor: true, children: "Esc: exit" })] })] }));
|
|
241
|
+
}
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Main Chat Component
|
|
244
|
+
// ============================================================================
|
|
245
|
+
function TerminalChat() {
|
|
133
246
|
const { exit } = useApp();
|
|
134
247
|
const { stdout } = useStdout();
|
|
135
|
-
|
|
248
|
+
const width = stdout?.columns || 80;
|
|
249
|
+
// Core state
|
|
136
250
|
const [input, setInput] = useState('');
|
|
137
251
|
const [messages, setMessages] = useState([]);
|
|
138
252
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
253
|
+
const [thinkingState, setThinkingState] = useState(null);
|
|
254
|
+
const [streamingResponse, setStreamingResponse] = useState('');
|
|
255
|
+
// Config state
|
|
139
256
|
const [provider, setProvider] = useState(config.get('defaultProvider'));
|
|
140
257
|
const [model, setModel] = useState(config.get('defaultModel'));
|
|
141
258
|
const [persona, setPersona] = useState(config.get('persona'));
|
|
142
|
-
//
|
|
143
|
-
const [
|
|
259
|
+
const [mode, setMode] = useState('hybrid'); // Default to hybrid mode
|
|
260
|
+
const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
|
|
261
|
+
// Modal state
|
|
262
|
+
const [modalMode, setModalMode] = useState('none');
|
|
263
|
+
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
144
264
|
const [availableModels, setAvailableModels] = useState([]);
|
|
145
|
-
const [modelLoading, setModelLoading] = useState(false);
|
|
146
|
-
// Upgrade state
|
|
147
|
-
const [upgradeMode, setUpgradeMode] = useState(false);
|
|
148
265
|
const [latestVersion, setLatestVersion] = useState(null);
|
|
149
|
-
|
|
266
|
+
// Stats
|
|
150
267
|
const [stats, setStats] = useState({
|
|
151
|
-
provider: selectProvider(config.get('defaultProvider')),
|
|
152
|
-
model: config.get('defaultModel') || DEFAULT_MODELS[selectProvider(config.get('defaultProvider'))],
|
|
153
268
|
inputTokens: 0,
|
|
154
269
|
outputTokens: 0,
|
|
155
270
|
cost: 0,
|
|
156
|
-
|
|
157
|
-
startTime: new Date(),
|
|
271
|
+
messageCount: 0,
|
|
158
272
|
});
|
|
159
|
-
|
|
273
|
+
const [contextTokens, setContextTokens] = useState(0);
|
|
274
|
+
// LLM conversation history
|
|
160
275
|
const llmMessages = useRef([
|
|
161
276
|
{ role: 'system', content: getSystemPrompt(persona) }
|
|
162
277
|
]);
|
|
163
|
-
//
|
|
278
|
+
// Estimate context tokens (rough: ~4 chars per token)
|
|
279
|
+
const estimateContextTokens = useCallback(() => {
|
|
280
|
+
let chars = 0;
|
|
281
|
+
for (const msg of llmMessages.current) {
|
|
282
|
+
if (typeof msg.content === 'string') {
|
|
283
|
+
chars += msg.content.length;
|
|
284
|
+
}
|
|
285
|
+
else if (Array.isArray(msg.content)) {
|
|
286
|
+
for (const block of msg.content) {
|
|
287
|
+
if (block.type === 'text') {
|
|
288
|
+
chars += block.text.length;
|
|
289
|
+
}
|
|
290
|
+
else if (block.type === 'image') {
|
|
291
|
+
chars += 1000; // Images count as ~250 tokens
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return Math.round(chars / 4);
|
|
297
|
+
}, []);
|
|
298
|
+
// Session state
|
|
299
|
+
const sessionRef = useRef(null);
|
|
300
|
+
const [autoRoute, setAutoRoute] = useState(false); // Auto model routing
|
|
301
|
+
const [memoryLoaded, setMemoryLoaded] = useState(false);
|
|
302
|
+
// Initialize session and load memory on mount
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const session = storage.getOrCreateSession(process.cwd());
|
|
305
|
+
sessionRef.current = session;
|
|
306
|
+
// Load memory context into system prompt
|
|
307
|
+
if (!memoryLoaded) {
|
|
308
|
+
const cwd = process.cwd();
|
|
309
|
+
const memoryContext = memory.buildMemoryContext(cwd);
|
|
310
|
+
if (memoryContext.trim()) {
|
|
311
|
+
// Append memory context to system prompt
|
|
312
|
+
const currentSystem = llmMessages.current[0];
|
|
313
|
+
if (currentSystem && currentSystem.role === 'system') {
|
|
314
|
+
const systemContent = typeof currentSystem.content === 'string'
|
|
315
|
+
? currentSystem.content
|
|
316
|
+
: '';
|
|
317
|
+
llmMessages.current[0] = {
|
|
318
|
+
role: 'system',
|
|
319
|
+
content: systemContent + '\n\n--- Project Context ---\n' + memoryContext,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
setMemoryLoaded(true);
|
|
324
|
+
// Execute session start hooks
|
|
325
|
+
hooks.executeHooks('session-start', {}).catch(() => { });
|
|
326
|
+
}
|
|
327
|
+
}, [memoryLoaded]);
|
|
328
|
+
// Derived values
|
|
329
|
+
const actualProvider = selectProvider(provider);
|
|
330
|
+
const actualModel = model || DEFAULT_MODELS[actualProvider];
|
|
331
|
+
const isModalActive = modalMode !== 'none';
|
|
332
|
+
// Add message helper
|
|
164
333
|
const addMessage = useCallback((type, content) => {
|
|
165
|
-
setMessages(prev => [...prev, {
|
|
334
|
+
setMessages(prev => [...prev, {
|
|
335
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
336
|
+
type,
|
|
337
|
+
content
|
|
338
|
+
}]);
|
|
166
339
|
}, []);
|
|
167
|
-
// Handle commands
|
|
340
|
+
// Handle slash commands
|
|
168
341
|
const handleCommand = useCallback(async (cmd) => {
|
|
169
342
|
const parts = cmd.split(/\s+/);
|
|
170
343
|
const command = parts[0].toLowerCase();
|
|
171
344
|
switch (command) {
|
|
172
345
|
case '/help':
|
|
173
346
|
case '/h':
|
|
174
|
-
addMessage('system', `Commands:
|
|
175
|
-
|
|
347
|
+
addMessage('system', `Commands:
|
|
348
|
+
/mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
|
|
349
|
+
/provider [name] - Switch AI provider
|
|
350
|
+
/model [name] - Switch model
|
|
351
|
+
/route [on|off|test] - Auto model routing by complexity
|
|
352
|
+
/persona [name] - Switch personality
|
|
353
|
+
/todo [add|done|list] - Manage TODOs
|
|
354
|
+
/plans [list|view] - View plan history
|
|
355
|
+
/session [list|info] - Session management
|
|
356
|
+
/history [search] - Chat history
|
|
357
|
+
/context [load|summary] - Context management
|
|
358
|
+
/summarize [context|compact] - Summarize/compact context
|
|
359
|
+
/clear - Clear conversation
|
|
360
|
+
/copy - Copy last response to clipboard
|
|
361
|
+
/export [file.md] - Export conversation to markdown
|
|
362
|
+
/edit - Edit and resend last message
|
|
363
|
+
/undo - Remove last exchange
|
|
364
|
+
/confirm [on|off] - Toggle risky op confirmation
|
|
365
|
+
/profile [name|save|del] - Switch/save/delete profiles
|
|
366
|
+
/mcp [add|remove|tools] - Manage MCP servers
|
|
367
|
+
/skills [add|remove] - Manage agent skills
|
|
368
|
+
/memory [init|add|show] - Project memory (CALLIOPE.md)
|
|
369
|
+
/project [init|show|run] - Project config (.calliope)
|
|
370
|
+
/find <pattern> - Fuzzy file search
|
|
371
|
+
/branch [new|switch] - Conversation branches
|
|
372
|
+
/theme [name|list] - Color themes
|
|
373
|
+
/hooks [list|add] - Pre/post tool hooks
|
|
374
|
+
/search <query> - Search conversation
|
|
375
|
+
/status - Show status
|
|
376
|
+
/config - Show config
|
|
377
|
+
/upgrade - Check for updates
|
|
378
|
+
/exit - Exit
|
|
379
|
+
|
|
380
|
+
File references: @filename, ./path, /absolute/path
|
|
381
|
+
Modes: 📋 Plan | 🔄 Hybrid | 🔧 Work
|
|
382
|
+
Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
|
|
383
|
+
break;
|
|
176
384
|
case '/provider':
|
|
177
385
|
case '/p':
|
|
178
386
|
if (parts[1]) {
|
|
179
387
|
const p = parts[1].toLowerCase();
|
|
180
388
|
setProvider(p);
|
|
181
|
-
setStats(s => ({ ...s, provider: selectProvider(p) }));
|
|
182
389
|
addMessage('system', `Provider: ${selectProvider(p)}`);
|
|
183
390
|
}
|
|
184
391
|
else {
|
|
185
|
-
addMessage('system', `Provider: ${
|
|
392
|
+
addMessage('system', `Provider: ${actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
|
|
186
393
|
}
|
|
187
|
-
|
|
394
|
+
break;
|
|
188
395
|
case '/model':
|
|
189
396
|
case '/m':
|
|
190
397
|
if (parts[1]) {
|
|
191
398
|
setModel(parts[1]);
|
|
192
|
-
setStats(s => ({ ...s, model: parts[1] }));
|
|
193
399
|
addMessage('system', `Model: ${parts[1]}`);
|
|
194
400
|
}
|
|
195
401
|
else {
|
|
196
|
-
|
|
197
|
-
setModelLoading(true);
|
|
198
|
-
addMessage('system', `Discovering models for ${selectProvider(provider)}...`);
|
|
402
|
+
addMessage('system', `Discovering models for ${actualProvider}...`);
|
|
199
403
|
try {
|
|
200
|
-
const models = await getAvailableModels(
|
|
404
|
+
const models = await getAvailableModels(actualProvider);
|
|
201
405
|
if (models.length > 0) {
|
|
202
406
|
setAvailableModels(models);
|
|
203
|
-
|
|
407
|
+
setModalMode('model');
|
|
204
408
|
}
|
|
205
409
|
else {
|
|
206
410
|
addMessage('error', 'No models found');
|
|
@@ -209,20 +413,15 @@ function App({ skipPermissions = false }) {
|
|
|
209
413
|
catch (e) {
|
|
210
414
|
addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
|
|
211
415
|
}
|
|
212
|
-
finally {
|
|
213
|
-
setModelLoading(false);
|
|
214
|
-
}
|
|
215
416
|
}
|
|
216
|
-
|
|
417
|
+
break;
|
|
217
418
|
case '/models':
|
|
218
|
-
|
|
219
|
-
setModelLoading(true);
|
|
220
|
-
addMessage('system', `Discovering models for ${selectProvider(provider)}...`);
|
|
419
|
+
addMessage('system', `Discovering models for ${actualProvider}...`);
|
|
221
420
|
try {
|
|
222
|
-
const models = await getAvailableModels(
|
|
421
|
+
const models = await getAvailableModels(actualProvider);
|
|
223
422
|
if (models.length > 0) {
|
|
224
423
|
setAvailableModels(models);
|
|
225
|
-
|
|
424
|
+
setModalMode('model');
|
|
226
425
|
}
|
|
227
426
|
else {
|
|
228
427
|
addMessage('error', 'No models found');
|
|
@@ -231,10 +430,18 @@ function App({ skipPermissions = false }) {
|
|
|
231
430
|
catch (e) {
|
|
232
431
|
addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
|
|
233
432
|
}
|
|
234
|
-
|
|
235
|
-
|
|
433
|
+
break;
|
|
434
|
+
case '/mode':
|
|
435
|
+
if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
|
|
436
|
+
const m = parts[1];
|
|
437
|
+
setMode(m);
|
|
438
|
+
addMessage('system', `Mode: ${MODE_CONFIG[m].icon} ${MODE_CONFIG[m].label} - ${MODE_CONFIG[m].description}`);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const currentConfig = MODE_CONFIG[mode];
|
|
442
|
+
addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (📋), hybrid (🔄), work (🔧)\nUse Shift+Tab to cycle`);
|
|
236
443
|
}
|
|
237
|
-
|
|
444
|
+
break;
|
|
238
445
|
case '/persona':
|
|
239
446
|
if (parts[1] && ['calliope', 'professional', 'minimal'].includes(parts[1])) {
|
|
240
447
|
const p = parts[1];
|
|
@@ -245,29 +452,634 @@ function App({ skipPermissions = false }) {
|
|
|
245
452
|
else {
|
|
246
453
|
addMessage('system', `Persona: ${persona} | Options: calliope, professional, minimal`);
|
|
247
454
|
}
|
|
248
|
-
|
|
455
|
+
break;
|
|
249
456
|
case '/clear':
|
|
250
457
|
case '/c':
|
|
251
458
|
setMessages([]);
|
|
252
459
|
llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
|
|
253
|
-
setStats(
|
|
254
|
-
|
|
460
|
+
setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
|
|
461
|
+
break;
|
|
462
|
+
case '/copy': {
|
|
463
|
+
// Copy last assistant response to clipboard
|
|
464
|
+
const lastAssistant = [...messages].reverse().find(m => m.type === 'assistant');
|
|
465
|
+
if (lastAssistant) {
|
|
466
|
+
try {
|
|
467
|
+
const { execSync } = await import('child_process');
|
|
468
|
+
// Try different clipboard commands based on platform
|
|
469
|
+
const content = lastAssistant.content;
|
|
470
|
+
if (process.platform === 'darwin') {
|
|
471
|
+
execSync('pbcopy', { input: content });
|
|
472
|
+
}
|
|
473
|
+
else if (process.platform === 'win32') {
|
|
474
|
+
execSync('clip', { input: content });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// Linux - try xclip, xsel, or wl-copy
|
|
478
|
+
try {
|
|
479
|
+
execSync('xclip -selection clipboard', { input: content });
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
try {
|
|
483
|
+
execSync('xsel --clipboard --input', { input: content });
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
execSync('wl-copy', { input: content });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
addMessage('system', '✓ Copied to clipboard');
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
addMessage('system', 'No assistant message to copy');
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case '/export': {
|
|
502
|
+
// Export conversation to markdown
|
|
503
|
+
const filename = parts[1] || `calliope-export-${Date.now()}.md`;
|
|
504
|
+
const fs = await import('fs');
|
|
505
|
+
const path = await import('path');
|
|
506
|
+
let markdown = `# Calliope Conversation Export\n\n`;
|
|
507
|
+
markdown += `**Date:** ${new Date().toLocaleString()}\n`;
|
|
508
|
+
markdown += `**Provider:** ${actualProvider}\n`;
|
|
509
|
+
markdown += `**Model:** ${actualModel}\n\n---\n\n`;
|
|
510
|
+
for (const msg of messages) {
|
|
511
|
+
if (msg.type === 'user') {
|
|
512
|
+
markdown += `## 👤 User\n\n${msg.content}\n\n`;
|
|
513
|
+
}
|
|
514
|
+
else if (msg.type === 'assistant') {
|
|
515
|
+
markdown += `## 🤖 Assistant\n\n${msg.content}\n\n`;
|
|
516
|
+
}
|
|
517
|
+
else if (msg.type === 'tool') {
|
|
518
|
+
markdown += `> 🔧 Tool: ${msg.content}\n\n`;
|
|
519
|
+
}
|
|
520
|
+
else if (msg.type === 'system') {
|
|
521
|
+
markdown += `> ℹ️ ${msg.content}\n\n`;
|
|
522
|
+
}
|
|
523
|
+
else if (msg.type === 'error') {
|
|
524
|
+
markdown += `> ⚠️ Error: ${msg.content}\n\n`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
528
|
+
fs.writeFileSync(filepath, markdown);
|
|
529
|
+
addMessage('system', `✓ Exported to ${filename}`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case '/edit': {
|
|
533
|
+
// Edit last user message
|
|
534
|
+
const lastUserIdx = [...messages].reverse().findIndex(m => m.type === 'user');
|
|
535
|
+
if (lastUserIdx >= 0) {
|
|
536
|
+
const lastUser = messages[messages.length - 1 - lastUserIdx];
|
|
537
|
+
setInput(lastUser.content);
|
|
538
|
+
addMessage('system', 'Edit the message above and press Enter to resend');
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
addMessage('system', 'No user message to edit');
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case '/undo': {
|
|
546
|
+
// Remove last exchange (user message + assistant response)
|
|
547
|
+
let removed = 0;
|
|
548
|
+
const newMessages = [...messages];
|
|
549
|
+
// Remove from the end until we've removed a user message
|
|
550
|
+
while (newMessages.length > 0 && removed < 10) {
|
|
551
|
+
const last = newMessages.pop();
|
|
552
|
+
removed++;
|
|
553
|
+
if (last?.type === 'user')
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
// Also remove from LLM context
|
|
557
|
+
while (llmMessages.current.length > 1) {
|
|
558
|
+
const last = llmMessages.current[llmMessages.current.length - 1];
|
|
559
|
+
if (last.role === 'user') {
|
|
560
|
+
llmMessages.current.pop();
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
llmMessages.current.pop();
|
|
564
|
+
}
|
|
565
|
+
setMessages(newMessages);
|
|
566
|
+
addMessage('system', `✓ Removed last ${removed} message(s)`);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
255
569
|
case '/status':
|
|
256
570
|
case '/s':
|
|
257
|
-
addMessage('system', `${
|
|
258
|
-
|
|
571
|
+
addMessage('system', `${actualProvider}:${actualModel} | ${stats.messageCount} msgs | ${stats.inputTokens + stats.outputTokens} tokens`);
|
|
572
|
+
break;
|
|
259
573
|
case '/config':
|
|
260
574
|
addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}`);
|
|
261
|
-
|
|
575
|
+
break;
|
|
262
576
|
case '/setup':
|
|
263
|
-
addMessage('system', 'Setup requires legacy CLI mode. Run: calliope --legacy then /setup');
|
|
264
|
-
return true;
|
|
265
577
|
case '/loop':
|
|
266
|
-
addMessage('system', '
|
|
267
|
-
|
|
268
|
-
case '/
|
|
269
|
-
|
|
270
|
-
|
|
578
|
+
addMessage('system', 'This feature requires legacy CLI. Run: calliope --legacy');
|
|
579
|
+
break;
|
|
580
|
+
case '/confirm':
|
|
581
|
+
if (parts[1] === 'on') {
|
|
582
|
+
setConfirmMode(true);
|
|
583
|
+
addMessage('system', '✓ Confirmation mode ON - will ask before risky operations');
|
|
584
|
+
}
|
|
585
|
+
else if (parts[1] === 'off') {
|
|
586
|
+
setConfirmMode(false);
|
|
587
|
+
addMessage('system', '⚠️ Confirmation mode OFF - risky operations will auto-execute');
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
addMessage('system', `Confirm mode: ${confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
case '/profile': {
|
|
594
|
+
const subCmd = parts[1];
|
|
595
|
+
if (subCmd === 'list' || !subCmd) {
|
|
596
|
+
const profiles = config.listProfiles();
|
|
597
|
+
const active = config.getActiveProfile();
|
|
598
|
+
const list = profiles.map(p => {
|
|
599
|
+
const marker = p.name === active ? '→ ' : ' ';
|
|
600
|
+
const tag = p.builtin ? '(built-in)' : '(custom)';
|
|
601
|
+
return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
|
|
602
|
+
}).join('\n');
|
|
603
|
+
addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
|
|
604
|
+
}
|
|
605
|
+
else if (subCmd === 'save' && parts[2]) {
|
|
606
|
+
const name = parts[2];
|
|
607
|
+
config.saveProfile(name, {
|
|
608
|
+
provider: provider,
|
|
609
|
+
model: model,
|
|
610
|
+
persona: persona,
|
|
611
|
+
confirmMode: confirmMode,
|
|
612
|
+
});
|
|
613
|
+
addMessage('system', `✓ Saved profile: ${name}`);
|
|
614
|
+
}
|
|
615
|
+
else if (subCmd === 'delete' && parts[2]) {
|
|
616
|
+
const name = parts[2];
|
|
617
|
+
if (config.deleteProfile(name)) {
|
|
618
|
+
addMessage('system', `✓ Deleted profile: ${name}`);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Load profile
|
|
626
|
+
const profile = config.getProfile(subCmd);
|
|
627
|
+
if (profile) {
|
|
628
|
+
setProvider(profile.provider);
|
|
629
|
+
if (profile.model)
|
|
630
|
+
setModel(profile.model);
|
|
631
|
+
setPersona(profile.persona);
|
|
632
|
+
if (profile.confirmMode !== undefined)
|
|
633
|
+
setConfirmMode(profile.confirmMode);
|
|
634
|
+
config.setActiveProfile(subCmd);
|
|
635
|
+
addMessage('system', `✓ Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case '/mcp': {
|
|
644
|
+
const subCmd = parts[1];
|
|
645
|
+
if (subCmd === 'list' || !subCmd) {
|
|
646
|
+
const servers = mcp.listServers();
|
|
647
|
+
if (servers.length === 0) {
|
|
648
|
+
addMessage('system', 'No MCP servers registered.\n\nUsage:\n /mcp add <url> - Register MCP server\n /mcp remove <id> - Remove server');
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const list = servers.map(s => {
|
|
652
|
+
const status = s.status === 'connected' ? '🟢' : s.status === 'error' ? '🔴' : '⚪';
|
|
653
|
+
return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
|
|
654
|
+
}).join('\n\n');
|
|
655
|
+
addMessage('system', `MCP Servers:\n\n${list}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
659
|
+
const url = parts[2];
|
|
660
|
+
addMessage('system', `Registering MCP server: ${url}...`);
|
|
661
|
+
try {
|
|
662
|
+
const server = await mcp.registerServer(url);
|
|
663
|
+
addMessage('system', `✓ Registered: ${server.name} (${server.tools.length} tools)`);
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
670
|
+
if (mcp.unregisterServer(parts[2])) {
|
|
671
|
+
addMessage('system', '✓ Server removed');
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
addMessage('error', 'Server not found');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else if (subCmd === 'refresh') {
|
|
678
|
+
const servers = mcp.listServers();
|
|
679
|
+
let connected = 0;
|
|
680
|
+
for (const s of servers) {
|
|
681
|
+
const updated = await mcp.refreshServer(s.id);
|
|
682
|
+
if (updated?.status === 'connected')
|
|
683
|
+
connected++;
|
|
684
|
+
}
|
|
685
|
+
addMessage('system', `Refreshed ${servers.length} servers (${connected} connected)`);
|
|
686
|
+
}
|
|
687
|
+
else if (subCmd === 'tools') {
|
|
688
|
+
const tools = mcp.getMCPTools();
|
|
689
|
+
if (tools.length === 0) {
|
|
690
|
+
addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
|
|
694
|
+
addMessage('system', `MCP Tools:\n\n${list}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
addMessage('system', 'Usage: /mcp [list|add <url>|remove <id>|refresh|tools]');
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case '/skills': {
|
|
703
|
+
const subCmd = parts[1];
|
|
704
|
+
if (subCmd === 'list' || !subCmd) {
|
|
705
|
+
const allSkills = skills.getSkills();
|
|
706
|
+
if (allSkills.length === 0) {
|
|
707
|
+
addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const list = allSkills.map(s => {
|
|
711
|
+
const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
|
|
712
|
+
return `• ${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
|
|
713
|
+
}).join('\n\n');
|
|
714
|
+
addMessage('system', `Installed Skills:\n\n${list}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
718
|
+
const source = parts[2];
|
|
719
|
+
addMessage('system', `Installing skill: ${source}...`);
|
|
720
|
+
try {
|
|
721
|
+
let skill;
|
|
722
|
+
if (source.startsWith('http')) {
|
|
723
|
+
skill = await skills.installFromGithub(source);
|
|
724
|
+
}
|
|
725
|
+
else if (fs.existsSync(source)) {
|
|
726
|
+
skill = skills.installLocalSkill(source);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
skill = await skills.installFromRegistry(source);
|
|
730
|
+
}
|
|
731
|
+
if (skill) {
|
|
732
|
+
addMessage('system', `✓ Installed: ${skill.metadata.name}`);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
addMessage('error', 'Failed to install skill');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch (e) {
|
|
739
|
+
addMessage('error', `Failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
743
|
+
if (skills.uninstallSkill(parts[2])) {
|
|
744
|
+
addMessage('system', '✓ Skill removed');
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
addMessage('error', 'Skill not found');
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else if (subCmd === 'info' && parts[2]) {
|
|
751
|
+
const skill = skills.getSkill(parts[2]);
|
|
752
|
+
if (skill) {
|
|
753
|
+
let info = `# ${skill.metadata.name}\n\n`;
|
|
754
|
+
info += `${skill.metadata.description}\n\n`;
|
|
755
|
+
if (skill.metadata.compatibility)
|
|
756
|
+
info += `Compatibility: ${skill.metadata.compatibility}\n`;
|
|
757
|
+
if (skill.metadata.license)
|
|
758
|
+
info += `License: ${skill.metadata.license}\n`;
|
|
759
|
+
if (skill.sourceUrl)
|
|
760
|
+
info += `Source: ${skill.sourceUrl}\n`;
|
|
761
|
+
addMessage('system', info);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
addMessage('error', 'Skill not found');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
addMessage('system', 'Usage: /skills [list|add <source>|remove <name>|info <name>]');
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case '/memory': {
|
|
773
|
+
const memory = await import('./memory.js');
|
|
774
|
+
const subCmd = parts[1];
|
|
775
|
+
const cwd = process.cwd();
|
|
776
|
+
if (subCmd === 'init') {
|
|
777
|
+
const memPath = memory.initProjectMemory(cwd);
|
|
778
|
+
addMessage('system', `Created: ${memPath}\nEdit the file to add context and preferences.`);
|
|
779
|
+
}
|
|
780
|
+
else if (subCmd === 'show' || !subCmd) {
|
|
781
|
+
const memPath = memory.findProjectMemory(cwd);
|
|
782
|
+
if (!memPath) {
|
|
783
|
+
addMessage('system', 'No CALLIOPE.md found.\nRun /memory init to create one.');
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const mem = memory.loadMemory(memPath);
|
|
787
|
+
let info = `Memory: ${memPath}\n\n`;
|
|
788
|
+
if (mem.context.length)
|
|
789
|
+
info += `**Context:**\n${mem.context.map(c => ` - ${c}`).join('\n')}\n\n`;
|
|
790
|
+
if (mem.preferences.length)
|
|
791
|
+
info += `**Preferences:**\n${mem.preferences.map(p => ` - ${p}`).join('\n')}\n\n`;
|
|
792
|
+
if (mem.history.length)
|
|
793
|
+
info += `**History:**\n${mem.history.slice(-5).map(h => ` - ${h}`).join('\n')}\n`;
|
|
794
|
+
addMessage('system', info);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
798
|
+
const type = parts[2];
|
|
799
|
+
const content = parts.slice(3).join(' ');
|
|
800
|
+
if (!content) {
|
|
801
|
+
addMessage('error', 'Usage: /memory add <type> <content>');
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
let memPath = memory.findProjectMemory(cwd);
|
|
805
|
+
if (!memPath) {
|
|
806
|
+
memPath = memory.initProjectMemory(cwd);
|
|
807
|
+
}
|
|
808
|
+
memory.addMemoryEntry(memPath, {
|
|
809
|
+
type,
|
|
810
|
+
content,
|
|
811
|
+
timestamp: new Date().toISOString().split('T')[0],
|
|
812
|
+
});
|
|
813
|
+
addMessage('system', `Added ${type}: ${content}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
else if (subCmd === 'remove' && parts[2]) {
|
|
817
|
+
const type = parts[2];
|
|
818
|
+
const content = parts.slice(3).join(' ');
|
|
819
|
+
const memPath = memory.findProjectMemory(cwd);
|
|
820
|
+
if (memPath && memory.removeMemoryEntry(memPath, type, content)) {
|
|
821
|
+
addMessage('system', `Removed matching ${type}`);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
addMessage('error', 'Entry not found');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else if (subCmd === 'global') {
|
|
828
|
+
const globalMem = memory.getGlobalMemory();
|
|
829
|
+
let info = 'Global Memory:\n\n';
|
|
830
|
+
if (globalMem.preferences.length)
|
|
831
|
+
info += `**Preferences:**\n${globalMem.preferences.map(p => ` - ${p}`).join('\n')}\n`;
|
|
832
|
+
if (globalMem.notes.length)
|
|
833
|
+
info += `**Notes:**\n${globalMem.notes.map(n => ` - ${n}`).join('\n')}\n`;
|
|
834
|
+
addMessage('system', info || 'No global memories yet.');
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
addMessage('system', 'Usage: /memory [init|show|add <type> <text>|remove <type> <text>|global]');
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
case '/find': {
|
|
842
|
+
const fuzzy = await import('./fuzzy-search.js');
|
|
843
|
+
const query = parts.slice(1).join(' ');
|
|
844
|
+
if (!query) {
|
|
845
|
+
addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
|
|
849
|
+
if (results.length === 0) {
|
|
850
|
+
addMessage('system', 'No files found');
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
|
|
854
|
+
addMessage('system', `Found ${results.length} files:\n\n${list}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
case '/branch': {
|
|
860
|
+
const branching = await import('./branching.js');
|
|
861
|
+
const subCmd = parts[1];
|
|
862
|
+
const sessionId = `session_${Date.now()}`; // Would use actual session ID
|
|
863
|
+
if (subCmd === 'list' || !subCmd) {
|
|
864
|
+
const tree = branching.getBranchTree(sessionId);
|
|
865
|
+
addMessage('system', `Branches:\n${tree}`);
|
|
866
|
+
}
|
|
867
|
+
else if (subCmd === 'new' && parts[2]) {
|
|
868
|
+
const branch = branching.createBranch(sessionId, parts[2], llmMessages.current, parts.slice(3).join(' '));
|
|
869
|
+
addMessage('system', `Created branch: ${branch.name}`);
|
|
870
|
+
}
|
|
871
|
+
else if (subCmd === 'switch' && parts[2]) {
|
|
872
|
+
const msgs = branching.switchBranch(sessionId, parts[2], llmMessages.current);
|
|
873
|
+
if (msgs) {
|
|
874
|
+
llmMessages.current = msgs;
|
|
875
|
+
addMessage('system', `Switched to branch: ${parts[2]}`);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
addMessage('error', 'Branch not found');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
else if (subCmd === 'delete' && parts[2]) {
|
|
882
|
+
if (branching.deleteBranch(sessionId, parts[2])) {
|
|
883
|
+
addMessage('system', 'Branch deleted');
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
addMessage('error', 'Cannot delete branch');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
|
|
891
|
+
}
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
case '/theme': {
|
|
895
|
+
const themes = await import('./themes.js');
|
|
896
|
+
const subCmd = parts[1];
|
|
897
|
+
if (subCmd === 'list' || !subCmd) {
|
|
898
|
+
const list = themes.listThemes();
|
|
899
|
+
const current = themes.getCurrentThemeName();
|
|
900
|
+
const formatted = list.map(t => {
|
|
901
|
+
const marker = t.name === current ? ' *' : '';
|
|
902
|
+
const custom = t.custom ? ' (custom)' : '';
|
|
903
|
+
return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
|
|
904
|
+
}).join('\n');
|
|
905
|
+
addMessage('system', `Available themes:\n${formatted}`);
|
|
906
|
+
}
|
|
907
|
+
else if (themes.setCurrentTheme(subCmd)) {
|
|
908
|
+
themes.clearThemeCache();
|
|
909
|
+
addMessage('system', `Theme set to: ${subCmd}`);
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
addMessage('error', `Theme not found: ${subCmd}`);
|
|
913
|
+
}
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
case '/hooks': {
|
|
917
|
+
const hooks = await import('./hooks.js');
|
|
918
|
+
const subCmd = parts[1];
|
|
919
|
+
if (subCmd === 'list' || !subCmd) {
|
|
920
|
+
addMessage('system', hooks.listHooksFormatted());
|
|
921
|
+
}
|
|
922
|
+
else if (subCmd === 'add' && parts[2]) {
|
|
923
|
+
const event = parts[2];
|
|
924
|
+
const command = parts.slice(3).join(' ');
|
|
925
|
+
if (!command) {
|
|
926
|
+
addMessage('system', 'Usage: /hooks add <event> <command>');
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
hooks.addHook({ event, name: `Hook for ${event}`, command, enabled: true, async: false });
|
|
930
|
+
addMessage('system', 'Hook added');
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else if (subCmd === 'init') {
|
|
934
|
+
hooks.initDefaultHooks();
|
|
935
|
+
addMessage('system', 'Default hooks initialized');
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
|
|
939
|
+
}
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
case '/search': {
|
|
943
|
+
const query = parts.slice(1).join(' ');
|
|
944
|
+
if (!query) {
|
|
945
|
+
addMessage('system', 'Usage: /search <query>\nSearch conversation history');
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
const lower = query.toLowerCase();
|
|
949
|
+
const matches = messages.filter(m => m.content.toLowerCase().includes(lower));
|
|
950
|
+
if (matches.length === 0) {
|
|
951
|
+
addMessage('system', 'No matches found');
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
const results = matches.slice(-10).map(m => {
|
|
955
|
+
const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
|
|
956
|
+
return `[${m.type}] ${preview}...`;
|
|
957
|
+
}).join('\n\n');
|
|
958
|
+
addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
case '/project': {
|
|
964
|
+
const projectConfig = await import('./project-config.js');
|
|
965
|
+
const subCmd = parts[1];
|
|
966
|
+
const cwd = process.cwd();
|
|
967
|
+
if (subCmd === 'init') {
|
|
968
|
+
const configPath = projectConfig.createProjectConfig(cwd);
|
|
969
|
+
addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
|
|
970
|
+
}
|
|
971
|
+
else if (subCmd === 'show' || !subCmd) {
|
|
972
|
+
const configPath = projectConfig.findProjectConfig(cwd);
|
|
973
|
+
if (!configPath) {
|
|
974
|
+
addMessage('system', 'No project config found.\nRun /project init to create one.');
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
const cfg = projectConfig.loadProjectConfig(configPath);
|
|
978
|
+
if (cfg) {
|
|
979
|
+
let info = `Config: ${configPath}\n\n`;
|
|
980
|
+
if (cfg.project)
|
|
981
|
+
info += `Project: ${cfg.project}\n`;
|
|
982
|
+
if (cfg.provider)
|
|
983
|
+
info += `Provider: ${cfg.provider}\n`;
|
|
984
|
+
if (cfg.model)
|
|
985
|
+
info += `Model: ${cfg.model}\n`;
|
|
986
|
+
if (cfg.tech?.length)
|
|
987
|
+
info += `Tech: ${cfg.tech.join(', ')}\n`;
|
|
988
|
+
if (cfg.conventions?.length)
|
|
989
|
+
info += `\nConventions:\n${cfg.conventions.map(c => ` - ${c}`).join('\n')}\n`;
|
|
990
|
+
if (cfg.commands)
|
|
991
|
+
info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
|
|
992
|
+
addMessage('system', info);
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
addMessage('error', 'Failed to parse config');
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else if (subCmd === 'run' && parts[2]) {
|
|
1000
|
+
const configPath = projectConfig.findProjectConfig(cwd);
|
|
1001
|
+
const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
|
|
1002
|
+
const cmdName = parts[2];
|
|
1003
|
+
if (cfg?.commands?.[cmdName]) {
|
|
1004
|
+
addMessage('system', `Running: ${cfg.commands[cmdName]}`);
|
|
1005
|
+
// Queue the command to run
|
|
1006
|
+
const { spawn } = await import('child_process');
|
|
1007
|
+
const proc = spawn('sh', ['-c', cfg.commands[cmdName]], { cwd, stdio: 'pipe' });
|
|
1008
|
+
let output = '';
|
|
1009
|
+
proc.stdout?.on('data', (d) => output += d.toString());
|
|
1010
|
+
proc.stderr?.on('data', (d) => output += d.toString());
|
|
1011
|
+
proc.on('close', (code) => {
|
|
1012
|
+
addMessage('system', `Exit ${code}\n${output}`);
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
addMessage('error', `Command not found: ${cmdName}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
addMessage('system', 'Usage: /project [init|show|run <cmd>]');
|
|
1021
|
+
}
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
case '/route':
|
|
1025
|
+
case '/autoroute': {
|
|
1026
|
+
if (parts[1] === 'on') {
|
|
1027
|
+
setAutoRoute(true);
|
|
1028
|
+
addMessage('system', '✓ Auto-routing ON - model selected based on task complexity');
|
|
1029
|
+
}
|
|
1030
|
+
else if (parts[1] === 'off') {
|
|
1031
|
+
setAutoRoute(false);
|
|
1032
|
+
addMessage('system', '✓ Auto-routing OFF - using fixed model');
|
|
1033
|
+
}
|
|
1034
|
+
else if (parts[1] === 'test' && parts[2]) {
|
|
1035
|
+
const testMsg = parts.slice(2).join(' ');
|
|
1036
|
+
const decision = modelRouter.routeRequest(testMsg, actualProvider);
|
|
1037
|
+
addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
const tiers = modelRouter.getAllTiers(actualProvider);
|
|
1041
|
+
addMessage('system', `Auto-route: ${autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
case '/summarize': {
|
|
1046
|
+
const subCmd = parts[1];
|
|
1047
|
+
if (subCmd === 'context' || !subCmd) {
|
|
1048
|
+
const msgCount = llmMessages.current.length;
|
|
1049
|
+
if (msgCount < 5) {
|
|
1050
|
+
addMessage('system', 'Not enough messages to summarize.');
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
const summary = summarization.extractKeyInfo(llmMessages.current);
|
|
1054
|
+
let info = 'Context Summary:\n\n';
|
|
1055
|
+
if (summary.topics.length)
|
|
1056
|
+
info += `**Topics:** ${summary.topics.join(', ')}\n`;
|
|
1057
|
+
if (summary.decisions.length)
|
|
1058
|
+
info += `**Decisions:**\n${summary.decisions.map(d => ` - ${d}`).join('\n')}\n`;
|
|
1059
|
+
if (summary.actions.length)
|
|
1060
|
+
info += `**Actions:**\n${summary.actions.map(a => ` - ${a}`).join('\n')}\n`;
|
|
1061
|
+
if (summary.codeChanges.length)
|
|
1062
|
+
info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map(c => ` - ${c}`).join('\n')}\n`;
|
|
1063
|
+
addMessage('system', info || 'No key information extracted.');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
else if (subCmd === 'compact') {
|
|
1067
|
+
// Summarize and compact the conversation
|
|
1068
|
+
const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 50000 });
|
|
1069
|
+
if (result.summarizedCount > 0) {
|
|
1070
|
+
llmMessages.current = result.messages;
|
|
1071
|
+
setContextTokens(estimateContextTokens());
|
|
1072
|
+
addMessage('system', `✓ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
addMessage('system', 'Context already within limits, no compaction needed.');
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
addMessage('system', 'Usage: /summarize [context|compact]');
|
|
1080
|
+
}
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
271
1083
|
case '/upgrade':
|
|
272
1084
|
addMessage('system', 'Checking for updates...');
|
|
273
1085
|
try {
|
|
@@ -275,69 +1087,322 @@ function App({ skipPermissions = false }) {
|
|
|
275
1087
|
const latest = await getLatestVersion();
|
|
276
1088
|
if (!latest) {
|
|
277
1089
|
addMessage('error', 'Could not check for updates');
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
const currentParts = current.split('.').map(Number);
|
|
281
|
-
const latestParts = latest.split('.').map(Number);
|
|
282
|
-
let hasUpdate = false;
|
|
283
|
-
for (let i = 0; i < 3; i++) {
|
|
284
|
-
if ((latestParts[i] || 0) > (currentParts[i] || 0)) {
|
|
285
|
-
hasUpdate = true;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
if ((latestParts[i] || 0) < (currentParts[i] || 0))
|
|
289
|
-
break;
|
|
1090
|
+
break;
|
|
290
1091
|
}
|
|
291
|
-
|
|
292
|
-
|
|
1092
|
+
const [cMaj, cMin, cPat] = current.split('.').map(Number);
|
|
1093
|
+
const [lMaj, lMin, lPat] = latest.split('.').map(Number);
|
|
1094
|
+
const hasUpdate = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
|
|
1095
|
+
if (hasUpdate) {
|
|
1096
|
+
setLatestVersion(latest);
|
|
1097
|
+
setModalMode('upgrade');
|
|
293
1098
|
}
|
|
294
1099
|
else {
|
|
295
|
-
|
|
296
|
-
setUpgradeMode(true);
|
|
1100
|
+
addMessage('system', `You're on the latest version (v${current})`);
|
|
297
1101
|
}
|
|
298
1102
|
}
|
|
299
1103
|
catch (e) {
|
|
300
1104
|
addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
|
|
301
1105
|
}
|
|
302
|
-
|
|
1106
|
+
break;
|
|
1107
|
+
case '/session':
|
|
1108
|
+
if (parts[1] === 'list') {
|
|
1109
|
+
const sessions = storage.listSessions(5);
|
|
1110
|
+
if (sessions.length === 0) {
|
|
1111
|
+
addMessage('system', 'No previous sessions found.');
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
const list = sessions.map(s => `${s.projectName} (${new Date(s.lastAccessedAt).toLocaleDateString()}) - ${s.messageCount} msgs`).join('\n');
|
|
1115
|
+
addMessage('system', `Recent sessions:\n${list}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else if (parts[1] === 'info') {
|
|
1119
|
+
const session = sessionRef.current;
|
|
1120
|
+
if (session) {
|
|
1121
|
+
addMessage('system', `Session: ${session.projectName}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}`);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
addMessage('system', 'No active session.');
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
addMessage('system', 'Usage: /session [list|info]');
|
|
1129
|
+
}
|
|
1130
|
+
break;
|
|
1131
|
+
case '/todo': {
|
|
1132
|
+
const subCommand = parts[1];
|
|
1133
|
+
if (subCommand === 'add' && parts.length > 2) {
|
|
1134
|
+
const content = parts.slice(2).join(' ');
|
|
1135
|
+
const isGlobal = content.includes('--global');
|
|
1136
|
+
const isHigh = content.includes('--priority') && content.includes('high');
|
|
1137
|
+
const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
|
|
1138
|
+
const todo = storage.addTodo(cleanContent, {
|
|
1139
|
+
global: isGlobal,
|
|
1140
|
+
priority: isHigh ? 'high' : 'normal',
|
|
1141
|
+
});
|
|
1142
|
+
addMessage('system', `✓ TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
|
|
1143
|
+
}
|
|
1144
|
+
else if (subCommand === 'done' && parts[2]) {
|
|
1145
|
+
const id = parts[2];
|
|
1146
|
+
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
1147
|
+
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
1148
|
+
if (todo) {
|
|
1149
|
+
storage.updateTodo(todo.id, { status: 'completed' });
|
|
1150
|
+
addMessage('system', `✓ TODO #${id} marked done`);
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
addMessage('error', `TODO #${id} not found`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
else if (subCommand === 'list' || !subCommand) {
|
|
1157
|
+
const sessionTodos = storage.getSessionTodos();
|
|
1158
|
+
const globalTodos = storage.getGlobalTodos();
|
|
1159
|
+
const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
|
|
1160
|
+
const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
|
|
1161
|
+
if (pending.length === 0 && completed.length === 0) {
|
|
1162
|
+
addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
let output = '📋 TODOs:\n';
|
|
1166
|
+
if (pending.length > 0) {
|
|
1167
|
+
output += pending.map(t => ` ${t.priority === 'high' ? '!' : '□'} #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
1168
|
+
}
|
|
1169
|
+
if (completed.length > 0) {
|
|
1170
|
+
output += '\n\nCompleted:\n' + completed.map(t => ` ✓ #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
1171
|
+
}
|
|
1172
|
+
addMessage('system', output);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
addMessage('system', 'Usage: /todo [add <task>|done <id>|list]');
|
|
1177
|
+
}
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
case '/plans': {
|
|
1181
|
+
const subCommand = parts[1];
|
|
1182
|
+
if (subCommand === 'list' || !subCommand) {
|
|
1183
|
+
const plans = storage.getPlans();
|
|
1184
|
+
if (plans.length === 0) {
|
|
1185
|
+
addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
const list = plans.slice(0, 5).map(p => `${p.status === 'completed' ? '✓' : '○'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
|
|
1189
|
+
addMessage('system', `📋 Plans:\n${list}`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else if (subCommand === 'view' && parts[2]) {
|
|
1193
|
+
const plans = storage.getPlans();
|
|
1194
|
+
const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
1195
|
+
if (plan) {
|
|
1196
|
+
const phases = plan.phases.map(ph => ` ${ph.status === 'completed' ? '✓' : '○'} ${ph.name} (${ph.risk} risk)`).join('\n');
|
|
1197
|
+
addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
addMessage('error', `Plan #${parts[2]} not found`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
addMessage('system', 'Usage: /plans [list|view <id>]');
|
|
1205
|
+
}
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case '/history': {
|
|
1209
|
+
const subCommand = parts[1];
|
|
1210
|
+
if (subCommand === 'search' && parts[2]) {
|
|
1211
|
+
const query = parts.slice(2).join(' ');
|
|
1212
|
+
const results = storage.searchChatHistory(query);
|
|
1213
|
+
if (results.length === 0) {
|
|
1214
|
+
addMessage('system', `No matches for "${query}"`);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
const list = results.slice(-5).map(m => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
|
|
1218
|
+
addMessage('system', `🔍 Found ${results.length} matches:\n${list}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
else if (subCommand === 'clear') {
|
|
1222
|
+
addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
const history = storage.getChatHistory(5);
|
|
1226
|
+
if (history.length === 0) {
|
|
1227
|
+
addMessage('system', 'No chat history yet.');
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
const list = history.map(m => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
|
|
1231
|
+
addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
case '/context': {
|
|
1237
|
+
const subCommand = parts[1];
|
|
1238
|
+
if (subCommand === 'load') {
|
|
1239
|
+
const limit = parseInt(parts[2]) || 20;
|
|
1240
|
+
const history = storage.getChatHistory(limit);
|
|
1241
|
+
if (history.length > 0) {
|
|
1242
|
+
// Load history into LLM context
|
|
1243
|
+
for (const msg of history) {
|
|
1244
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
1245
|
+
llmMessages.current.push({
|
|
1246
|
+
role: msg.role,
|
|
1247
|
+
content: msg.content,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
addMessage('system', `✓ Loaded ${history.length} messages into context`);
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
addMessage('system', 'No history to load.');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else if (subCommand === 'summary') {
|
|
1258
|
+
const msgCount = llmMessages.current.length;
|
|
1259
|
+
const estTokens = llmMessages.current.reduce((sum, m) => sum + (m.content?.length || 0) / 4, 0);
|
|
1260
|
+
addMessage('system', `Context: ${msgCount} messages (~${Math.round(estTokens)} tokens)`);
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
addMessage('system', 'Usage: /context [load [n]|summary]');
|
|
1264
|
+
}
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
303
1267
|
case '/exit':
|
|
304
1268
|
case '/quit':
|
|
305
1269
|
case '/q':
|
|
306
1270
|
exit();
|
|
307
|
-
|
|
1271
|
+
break;
|
|
308
1272
|
default:
|
|
309
|
-
addMessage('error', `Unknown command: ${command}
|
|
310
|
-
|
|
1273
|
+
addMessage('error', `Unknown command: ${command}. Type /help for help.`);
|
|
1274
|
+
}
|
|
1275
|
+
}, [actualProvider, actualModel, persona, stats, addMessage, exit]);
|
|
1276
|
+
// Run agent with user prompt
|
|
1277
|
+
const runAgent = useCallback(async (content) => {
|
|
1278
|
+
llmMessages.current.push({ role: 'user', content });
|
|
1279
|
+
setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
|
|
1280
|
+
setStreamingResponse('');
|
|
1281
|
+
// Auto-route to appropriate model based on task complexity
|
|
1282
|
+
let effectiveModel = model;
|
|
1283
|
+
if (autoRoute && typeof content === 'string') {
|
|
1284
|
+
const routeDecision = modelRouter.routeRequest(content, provider, {
|
|
1285
|
+
messageCount: stats.messageCount,
|
|
1286
|
+
hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
|
|
1287
|
+
});
|
|
1288
|
+
effectiveModel = routeDecision.model.model;
|
|
1289
|
+
if (effectiveModel !== model) {
|
|
1290
|
+
addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
|
|
1291
|
+
}
|
|
311
1292
|
}
|
|
312
|
-
}, [provider, model, persona, stats, addMessage, exit]);
|
|
313
|
-
// Run agent
|
|
314
|
-
const runAgent = useCallback(async (prompt) => {
|
|
315
|
-
llmMessages.current.push({ role: 'user', content: prompt });
|
|
316
|
-
setStats(s => ({ ...s, messages: s.messages + 1 }));
|
|
317
1293
|
const maxIterations = config.get('maxIterations');
|
|
318
|
-
let
|
|
319
|
-
while (iteration < maxIterations) {
|
|
320
|
-
iteration++;
|
|
1294
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
321
1295
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
1296
|
+
// Update thinking state for LLM call
|
|
1297
|
+
setThinkingState({
|
|
1298
|
+
status: i === 0 ? 'Analyzing request...' : 'Processing response...',
|
|
1299
|
+
detail: `Iteration ${i + 1}/${maxIterations}`,
|
|
1300
|
+
iteration: i + 1,
|
|
1301
|
+
maxIterations,
|
|
1302
|
+
});
|
|
1303
|
+
// Streaming callback for final response
|
|
1304
|
+
const onToken = (token) => {
|
|
1305
|
+
setThinkingState(null); // Clear thinking when streaming starts
|
|
1306
|
+
setStreamingResponse(prev => prev + token);
|
|
1307
|
+
};
|
|
1308
|
+
// Retry callback for error recovery
|
|
1309
|
+
const onRetry = (attempt, error, delayMs) => {
|
|
1310
|
+
setThinkingState({
|
|
1311
|
+
status: `Retrying... (attempt ${attempt + 1})`,
|
|
1312
|
+
detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
|
|
1313
|
+
iteration: i + 1,
|
|
1314
|
+
maxIterations,
|
|
1315
|
+
});
|
|
1316
|
+
};
|
|
1317
|
+
const response = await chat(provider, llmMessages.current, TOOLS, effectiveModel, onToken, onRetry);
|
|
1318
|
+
// Update token stats and cost
|
|
324
1319
|
if (response.usage) {
|
|
1320
|
+
const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
|
|
325
1321
|
setStats(s => ({
|
|
326
1322
|
...s,
|
|
327
1323
|
inputTokens: s.inputTokens + response.usage.inputTokens,
|
|
328
1324
|
outputTokens: s.outputTokens + response.usage.outputTokens,
|
|
1325
|
+
cost: s.cost + usageCost,
|
|
329
1326
|
}));
|
|
330
1327
|
}
|
|
331
1328
|
// Handle tool calls
|
|
332
|
-
if (response.toolCalls
|
|
1329
|
+
if (response.toolCalls?.length) {
|
|
333
1330
|
llmMessages.current.push({
|
|
334
1331
|
role: 'assistant',
|
|
335
1332
|
content: response.content,
|
|
336
1333
|
toolCalls: response.toolCalls,
|
|
337
1334
|
});
|
|
338
1335
|
for (const toolCall of response.toolCalls) {
|
|
339
|
-
|
|
1336
|
+
const args = toolCall.arguments;
|
|
1337
|
+
const toolPreview = String(args.command || args.path || '...');
|
|
1338
|
+
// Assess risk
|
|
1339
|
+
const risk = assessToolRisk(toolCall);
|
|
1340
|
+
const riskConfig = RISK_CONFIG[risk.level];
|
|
1341
|
+
const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
|
|
1342
|
+
// Special handling for think tool
|
|
1343
|
+
if (toolCall.name === 'think') {
|
|
1344
|
+
const thought = String(args.thought || '');
|
|
1345
|
+
setThinkingState({
|
|
1346
|
+
status: 'Reasoning...',
|
|
1347
|
+
detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
|
|
1348
|
+
thinking: thought,
|
|
1349
|
+
iteration: i + 1,
|
|
1350
|
+
maxIterations,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
setThinkingState({
|
|
1355
|
+
status: `Executing ${toolCall.name}...`,
|
|
1356
|
+
detail: toolPreview.substring(0, 60),
|
|
1357
|
+
thinking: undefined,
|
|
1358
|
+
iteration: i + 1,
|
|
1359
|
+
maxIterations,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
// In plan mode, don't execute tools (except think)
|
|
1363
|
+
if (mode === 'plan' && toolCall.name !== 'think') {
|
|
1364
|
+
addMessage('tool', `📋 ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
|
|
1365
|
+
llmMessages.current.push({
|
|
1366
|
+
role: 'tool',
|
|
1367
|
+
content: '[Plan mode: Tool not executed. Describe what this would do.]',
|
|
1368
|
+
toolCallId: toolCall.id,
|
|
1369
|
+
});
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
// Check if confirmation is required for risky operations
|
|
1373
|
+
if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
|
|
1374
|
+
// Show warning and skip execution
|
|
1375
|
+
const riskIcon = risk.level === 'critical' ? '🛑' : '⚠️';
|
|
1376
|
+
addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
|
|
1377
|
+
llmMessages.current.push({
|
|
1378
|
+
role: 'tool',
|
|
1379
|
+
content: `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`,
|
|
1380
|
+
toolCallId: toolCall.id,
|
|
1381
|
+
});
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
|
|
1385
|
+
// Execute pre-tool hooks
|
|
1386
|
+
const preHookResult = await hooks.checkHooksAllow('pre-tool', {
|
|
1387
|
+
tool: toolCall.name,
|
|
1388
|
+
toolArgs: args,
|
|
1389
|
+
});
|
|
1390
|
+
if (!preHookResult.allowed) {
|
|
1391
|
+
addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
|
|
1392
|
+
llmMessages.current.push({
|
|
1393
|
+
role: 'tool',
|
|
1394
|
+
content: `[Blocked by hook: ${preHookResult.reason}]`,
|
|
1395
|
+
toolCallId: toolCall.id,
|
|
1396
|
+
});
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
340
1399
|
const result = await executeTool(toolCall, process.cwd());
|
|
1400
|
+
// Execute post-tool hooks
|
|
1401
|
+
hooks.executeHooks('post-tool', {
|
|
1402
|
+
tool: toolCall.name,
|
|
1403
|
+
toolArgs: args,
|
|
1404
|
+
toolResult: result.result,
|
|
1405
|
+
}).catch(() => { });
|
|
341
1406
|
const preview = result.result.split('\n').slice(0, 3).join('\n');
|
|
342
1407
|
addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
|
|
343
1408
|
llmMessages.current.push({
|
|
@@ -348,19 +1413,31 @@ function App({ skipPermissions = false }) {
|
|
|
348
1413
|
}
|
|
349
1414
|
continue;
|
|
350
1415
|
}
|
|
351
|
-
// Final response
|
|
1416
|
+
// Final response - move streaming content to message history
|
|
1417
|
+
setThinkingState(null);
|
|
352
1418
|
llmMessages.current.push({ role: 'assistant', content: response.content });
|
|
353
1419
|
addMessage('assistant', response.content);
|
|
1420
|
+
setStreamingResponse('');
|
|
1421
|
+
setContextTokens(estimateContextTokens());
|
|
1422
|
+
// Auto-continue if response was truncated due to length
|
|
1423
|
+
if (response.finishReason === 'length') {
|
|
1424
|
+
addMessage('system', '(auto-continuing...)');
|
|
1425
|
+
llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
|
|
1426
|
+
continue; // Loop again to get continuation
|
|
1427
|
+
}
|
|
354
1428
|
break;
|
|
355
1429
|
}
|
|
356
1430
|
catch (error) {
|
|
357
|
-
|
|
358
|
-
|
|
1431
|
+
setThinkingState(null);
|
|
1432
|
+
setStreamingResponse('');
|
|
1433
|
+
addMessage('error', formatError(error));
|
|
359
1434
|
break;
|
|
360
1435
|
}
|
|
361
1436
|
}
|
|
362
|
-
|
|
363
|
-
|
|
1437
|
+
// Update context tokens after agent run
|
|
1438
|
+
setContextTokens(estimateContextTokens());
|
|
1439
|
+
}, [provider, model, addMessage, mode, estimateContextTokens]);
|
|
1440
|
+
// Handle input submission
|
|
364
1441
|
const handleSubmit = useCallback(async (value) => {
|
|
365
1442
|
const trimmed = value.trim();
|
|
366
1443
|
if (!trimmed || isProcessing)
|
|
@@ -370,54 +1447,59 @@ function App({ skipPermissions = false }) {
|
|
|
370
1447
|
await handleCommand(trimmed);
|
|
371
1448
|
return;
|
|
372
1449
|
}
|
|
373
|
-
|
|
1450
|
+
// Parse file references from input
|
|
1451
|
+
const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
|
|
1452
|
+
// Show user message (with file info if any)
|
|
1453
|
+
if (files.length > 0) {
|
|
1454
|
+
const fileInfo = formatFileInfo(files);
|
|
1455
|
+
addMessage('user', `${cleanText}\n📎 ${fileInfo}`);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
addMessage('user', trimmed);
|
|
1459
|
+
}
|
|
374
1460
|
setIsProcessing(true);
|
|
375
1461
|
try {
|
|
376
|
-
|
|
1462
|
+
// Build message content (with file/image support)
|
|
1463
|
+
let messageContent;
|
|
1464
|
+
if (files.length > 0) {
|
|
1465
|
+
const visionSupported = supportsVision(provider, model);
|
|
1466
|
+
const { content, warnings } = processFilesForMessage(cleanText || trimmed, files, visionSupported);
|
|
1467
|
+
// Show any warnings about files
|
|
1468
|
+
for (const warning of warnings) {
|
|
1469
|
+
addMessage('system', warning);
|
|
1470
|
+
}
|
|
1471
|
+
messageContent = content;
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
messageContent = trimmed;
|
|
1475
|
+
}
|
|
1476
|
+
await runAgent(messageContent);
|
|
377
1477
|
}
|
|
378
1478
|
finally {
|
|
379
1479
|
setIsProcessing(false);
|
|
1480
|
+
setThinkingState(null);
|
|
1481
|
+
setStreamingResponse('');
|
|
380
1482
|
}
|
|
381
|
-
}, [isProcessing, handleCommand, runAgent, addMessage]);
|
|
382
|
-
//
|
|
383
|
-
const [width, setWidth] = useState(stdout?.columns || 80);
|
|
384
|
-
useEffect(() => {
|
|
385
|
-
const handleResize = () => {
|
|
386
|
-
setWidth(stdout?.columns || 80);
|
|
387
|
-
};
|
|
388
|
-
process.stdout.on('resize', handleResize);
|
|
389
|
-
return () => {
|
|
390
|
-
process.stdout.off('resize', handleResize);
|
|
391
|
-
};
|
|
392
|
-
}, [stdout]);
|
|
393
|
-
// Escape to exit (but not when in model select mode)
|
|
394
|
-
useInput((_, key) => {
|
|
395
|
-
if (key.escape && !modelSelectMode)
|
|
396
|
-
exit();
|
|
397
|
-
}, { isActive: !modelSelectMode });
|
|
398
|
-
// Model selection handlers
|
|
1483
|
+
}, [isProcessing, handleCommand, runAgent, addMessage, provider, model]);
|
|
1484
|
+
// Modal handlers
|
|
399
1485
|
const handleModelSelect = useCallback((selectedModel) => {
|
|
400
1486
|
setModel(selectedModel);
|
|
401
|
-
setStats(s => ({ ...s, model: selectedModel }));
|
|
402
1487
|
addMessage('system', `Model: ${selectedModel}`);
|
|
403
|
-
|
|
1488
|
+
setModalMode('none');
|
|
404
1489
|
setAvailableModels([]);
|
|
405
1490
|
}, [addMessage]);
|
|
406
|
-
const
|
|
407
|
-
|
|
1491
|
+
const handleModalCancel = useCallback(() => {
|
|
1492
|
+
setModalMode('none');
|
|
408
1493
|
setAvailableModels([]);
|
|
409
|
-
|
|
410
|
-
}, [
|
|
411
|
-
// Upgrade handlers
|
|
1494
|
+
setLatestVersion(null);
|
|
1495
|
+
}, []);
|
|
412
1496
|
const handleUpgradeConfirm = useCallback(async () => {
|
|
413
|
-
|
|
414
|
-
setUpgrading(true);
|
|
1497
|
+
setModalMode('none');
|
|
415
1498
|
addMessage('system', 'Upgrading...');
|
|
416
1499
|
try {
|
|
417
1500
|
const success = await performUpgrade();
|
|
418
1501
|
if (success) {
|
|
419
1502
|
addMessage('system', 'Upgrade complete! Restarting...');
|
|
420
|
-
// Restart the CLI
|
|
421
1503
|
const { spawn } = await import('child_process');
|
|
422
1504
|
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
423
1505
|
stdio: 'inherit',
|
|
@@ -433,49 +1515,62 @@ function App({ skipPermissions = false }) {
|
|
|
433
1515
|
catch (e) {
|
|
434
1516
|
addMessage('error', `Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
435
1517
|
}
|
|
436
|
-
finally {
|
|
437
|
-
setUpgrading(false);
|
|
438
|
-
setLatestVersion(null);
|
|
439
|
-
}
|
|
440
|
-
}, [addMessage]);
|
|
441
|
-
const handleUpgradeCancel = useCallback(() => {
|
|
442
|
-
setUpgradeMode(false);
|
|
443
1518
|
setLatestVersion(null);
|
|
444
|
-
addMessage('system', 'Upgrade cancelled');
|
|
445
1519
|
}, [addMessage]);
|
|
446
|
-
|
|
447
|
-
|
|
1520
|
+
// Cycle through modes
|
|
1521
|
+
const cycleMode = useCallback(() => {
|
|
1522
|
+
setMode(current => {
|
|
1523
|
+
const modes = ['plan', 'hybrid', 'work'];
|
|
1524
|
+
const idx = modes.indexOf(current);
|
|
1525
|
+
const next = modes[(idx + 1) % modes.length];
|
|
1526
|
+
return next;
|
|
1527
|
+
});
|
|
1528
|
+
}, []);
|
|
1529
|
+
// Keyboard shortcuts (Escape to exit, Shift+Tab to cycle mode)
|
|
1530
|
+
useInput((input, key) => {
|
|
1531
|
+
if (key.escape && !isModalActive)
|
|
1532
|
+
exit();
|
|
1533
|
+
// Shift+Tab to cycle mode (key.shift && key.tab)
|
|
1534
|
+
if (key.shift && key.tab && !isProcessing) {
|
|
1535
|
+
cycleMode();
|
|
1536
|
+
}
|
|
1537
|
+
}, { isActive: !isModalActive });
|
|
1538
|
+
// Render
|
|
1539
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), streamingResponse && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "blue", children: "\u2502" }), _jsx(Text, { color: "cyan", children: "\u258C" })] })), isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Processing..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: isModalActive || isProcessing, mode: mode }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
|
|
1540
|
+
}
|
|
1541
|
+
// ============================================================================
|
|
1542
|
+
// App Wrapper & Entry Point
|
|
1543
|
+
// ============================================================================
|
|
1544
|
+
function App() {
|
|
1545
|
+
return _jsx(TerminalChat, {});
|
|
448
1546
|
}
|
|
449
|
-
// ANSI colors for banner
|
|
450
|
-
const
|
|
1547
|
+
// ANSI colors for pre-Ink banner
|
|
1548
|
+
const ansi = {
|
|
451
1549
|
reset: '\x1b[0m',
|
|
452
1550
|
cyan: '\x1b[36m',
|
|
453
1551
|
brightCyan: '\x1b[96m',
|
|
454
1552
|
dim: '\x1b[2m',
|
|
455
1553
|
};
|
|
456
|
-
// Print banner before Ink starts
|
|
457
1554
|
function printBanner(provider, model) {
|
|
458
1555
|
console.log();
|
|
459
|
-
console.log(`${
|
|
460
|
-
console.log(`${
|
|
461
|
-
console.log(`${
|
|
462
|
-
console.log(`${
|
|
463
|
-
console.log(`${
|
|
464
|
-
console.log(`${
|
|
1556
|
+
console.log(`${ansi.brightCyan}${BANNER_LINES[0]}${ansi.reset}`);
|
|
1557
|
+
console.log(`${ansi.brightCyan}${BANNER_LINES[1]}${ansi.reset}`);
|
|
1558
|
+
console.log(`${ansi.cyan}${BANNER_LINES[2]}${ansi.reset}`);
|
|
1559
|
+
console.log(`${ansi.cyan}${BANNER_LINES[3]}${ansi.reset}`);
|
|
1560
|
+
console.log(`${ansi.brightCyan}${BANNER_LINES[4]}${ansi.reset}`);
|
|
1561
|
+
console.log(`${ansi.cyan}${BANNER_LINES[5]}${ansi.reset}`);
|
|
465
1562
|
console.log();
|
|
466
|
-
console.log(`${
|
|
1563
|
+
console.log(`${ansi.dim} The Muse of Digital Eloquence${ansi.reset}`);
|
|
467
1564
|
console.log();
|
|
468
|
-
console.log(` ${
|
|
469
|
-
console.log(` ${
|
|
1565
|
+
console.log(` ${ansi.dim}v${getVersion()} | ${provider}:${model}${ansi.reset}`);
|
|
1566
|
+
console.log(` ${ansi.dim}/help for commands | ESC to exit${ansi.reset}`);
|
|
470
1567
|
console.log();
|
|
471
1568
|
}
|
|
472
|
-
// Export start function
|
|
473
1569
|
export async function startInkCLI(options = {}) {
|
|
474
|
-
// Print banner before Ink takes over
|
|
475
1570
|
const provider = selectProvider(config.get('defaultProvider'));
|
|
476
1571
|
const model = config.get('defaultModel') || DEFAULT_MODELS[provider];
|
|
477
1572
|
printBanner(provider, model);
|
|
478
|
-
const { waitUntilExit } = render(_jsx(App, {
|
|
1573
|
+
const { waitUntilExit } = render(_jsx(App, {}));
|
|
479
1574
|
await waitUntilExit();
|
|
480
1575
|
}
|
|
481
1576
|
//# sourceMappingURL=ui-cli.js.map
|