miii-cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -56
- package/dist/config.js +18 -1
- package/dist/init.js +17 -1
- package/dist/llm/stream.js +41 -0
- package/dist/mcp/client.js +110 -0
- package/dist/setup.js +183 -0
- package/dist/tasks/compactor.js +65 -26
- package/dist/tools/index.js +3 -2
- package/dist/tui/InputBar.js +27 -12
- package/dist/tui/components/ConfigPicker.js +178 -0
- package/dist/tui/components/InputArea.js +86 -44
- package/dist/tui/hooks/useRunLoop.js +94 -10
- package/dist/tui/hooks/useSession.js +6 -6
- package/dist/tui/hooks/useSubmit.js +88 -23
- package/dist/tui/printer.js +72 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
2
3
|
import { chat } from '../../llm/stream.js';
|
|
3
4
|
import { tools as staticTools } from '../../tools/index.js';
|
|
4
5
|
import { StreamParser, extractBareToolCall } from '../../parser/stream-parser.js';
|
|
@@ -8,6 +9,7 @@ const MAX_TOOL_DEPTH = 6;
|
|
|
8
9
|
const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'patch_file', 'delete_file']);
|
|
9
10
|
const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
|
|
10
11
|
const PERMISSION_TOOLS = new Set(['edit_file', 'patch_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
|
|
12
|
+
const CHECKPOINT_TOOLS = new Set(['edit_file', 'patch_file', 'create_file', 'delete_file']);
|
|
11
13
|
export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef) {
|
|
12
14
|
const [status, setStatus] = useState('idle');
|
|
13
15
|
const [tick, setTick] = useState(0);
|
|
@@ -15,16 +17,25 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
15
17
|
const [taskLabel, setTaskLabel] = useState();
|
|
16
18
|
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
17
19
|
const permissionResolveRef = useRef(null);
|
|
20
|
+
const [compactRequest, setCompactRequest] = useState(null);
|
|
21
|
+
const compactResolveRef = useRef(null);
|
|
22
|
+
const checkpointRef = useRef(new Map());
|
|
23
|
+
const sessionApprovedRef = useRef(new Set());
|
|
18
24
|
const thinkingStartRef = useRef(0);
|
|
19
25
|
const extraToolsRef = useRef(extraTools);
|
|
20
26
|
extraToolsRef.current = extraTools;
|
|
21
27
|
const pushHistoryRef = useRef(pushHistory);
|
|
22
28
|
useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
|
|
23
|
-
const resolvePermission = useCallback((
|
|
24
|
-
permissionResolveRef.current?.(
|
|
29
|
+
const resolvePermission = useCallback((result) => {
|
|
30
|
+
permissionResolveRef.current?.(result);
|
|
25
31
|
permissionResolveRef.current = null;
|
|
26
32
|
setPermissionRequest(null);
|
|
27
33
|
}, []);
|
|
34
|
+
const resolveCompact = useCallback((approved) => {
|
|
35
|
+
compactResolveRef.current?.(approved);
|
|
36
|
+
compactResolveRef.current = null;
|
|
37
|
+
setCompactRequest(null);
|
|
38
|
+
}, []);
|
|
28
39
|
useEffect(() => {
|
|
29
40
|
if (status === 'idle')
|
|
30
41
|
return;
|
|
@@ -38,9 +49,30 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
38
49
|
return;
|
|
39
50
|
}
|
|
40
51
|
setStatus('thinking');
|
|
41
|
-
if (depth === 0)
|
|
52
|
+
if (depth === 0) {
|
|
42
53
|
thinkingStartRef.current = Date.now();
|
|
43
|
-
|
|
54
|
+
checkpointRef.current.clear();
|
|
55
|
+
}
|
|
56
|
+
let msgs = contextMsgs;
|
|
57
|
+
if (shouldCompact(contextMsgs)) {
|
|
58
|
+
const approved = await new Promise(resolve => {
|
|
59
|
+
compactResolveRef.current = resolve;
|
|
60
|
+
setCompactRequest({ messageCount: contextMsgs.length });
|
|
61
|
+
});
|
|
62
|
+
if (approved) {
|
|
63
|
+
printer.systemMsg('compacting context…');
|
|
64
|
+
msgs = await compactContext(contextMsgs, {
|
|
65
|
+
provider: config.provider,
|
|
66
|
+
model: currentModelRef.current,
|
|
67
|
+
baseUrl: config.baseUrl,
|
|
68
|
+
apiKey: config.apiKey,
|
|
69
|
+
}, goal);
|
|
70
|
+
printer.systemMsg(`compacted: ${contextMsgs.length} → ${msgs.length} messages`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
printer.systemMsg('keeping full context — responses may be slower');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
44
76
|
abortRef.current = new AbortController();
|
|
45
77
|
await chat({
|
|
46
78
|
provider: config.provider,
|
|
@@ -67,6 +99,8 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
67
99
|
if (displayText)
|
|
68
100
|
printer.assistantMsg(displayText);
|
|
69
101
|
pushHistoryRef.current({ role: 'assistant', content: fullText });
|
|
102
|
+
if (pendingTools.length)
|
|
103
|
+
printer.planSummary(pendingTools);
|
|
70
104
|
if (!pendingTools.length) {
|
|
71
105
|
const hasFencedCode = /```[\w]*\n[\s\S]{50,}?\n```/.test(fullText);
|
|
72
106
|
if (hasFencedCode && depth < MAX_TOOL_DEPTH - 1) {
|
|
@@ -88,20 +122,42 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
88
122
|
const tool = allTools.find(t => t.name === tc.name);
|
|
89
123
|
setCurrentTool(tc.name);
|
|
90
124
|
if (PERMISSION_TOOLS.has(tc.name)) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
const sessionKey = tc.name;
|
|
126
|
+
let decision;
|
|
127
|
+
if (sessionApprovedRef.current.has(sessionKey)) {
|
|
128
|
+
decision = 'yes';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
decision = await new Promise(resolve => {
|
|
132
|
+
permissionResolveRef.current = resolve;
|
|
133
|
+
setPermissionRequest({ toolName: tc.name, args: tc.args });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (decision === 'session')
|
|
137
|
+
sessionApprovedRef.current.add(sessionKey);
|
|
138
|
+
if (decision === 'no') {
|
|
96
139
|
printer.systemMsg(`denied: ${tc.name}`);
|
|
97
140
|
next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user` });
|
|
98
141
|
break;
|
|
99
142
|
}
|
|
143
|
+
// Checkpoint: store pre-execution file state
|
|
144
|
+
if (CHECKPOINT_TOOLS.has(tc.name)) {
|
|
145
|
+
const path = tc.args.path;
|
|
146
|
+
if (path && !checkpointRef.current.has(path)) {
|
|
147
|
+
try {
|
|
148
|
+
checkpointRef.current.set(path, readFileSync(path, 'utf-8'));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
checkpointRef.current.set(path, null);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
100
155
|
}
|
|
101
156
|
if (tool) {
|
|
102
157
|
try {
|
|
103
158
|
printer.toolCallStart(tc.name, tc.args);
|
|
104
159
|
const result = await tool.execute(tc.args);
|
|
160
|
+
printer.toolResultSummary(tc.name, tc.args, result);
|
|
105
161
|
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
106
162
|
printer.toolMsg(tc.name, result);
|
|
107
163
|
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
@@ -131,6 +187,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
131
187
|
printer.toolCallStart('run_tests', {});
|
|
132
188
|
const testResult = await testTool.execute({});
|
|
133
189
|
if (testResult && !testResult.startsWith('(no test script') && !testResult.startsWith('(no package.json')) {
|
|
190
|
+
printer.toolResultSummary('run_tests', {}, testResult);
|
|
134
191
|
printer.toolMsg('run_tests', testResult);
|
|
135
192
|
next.push({ role: 'user', content: `Test results after edits:\n${testResult}` });
|
|
136
193
|
}
|
|
@@ -156,11 +213,37 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
156
213
|
}, [config]);
|
|
157
214
|
const handleAbort = useCallback(() => {
|
|
158
215
|
abortRef.current?.abort();
|
|
216
|
+
sessionApprovedRef.current.clear();
|
|
159
217
|
if (permissionResolveRef.current) {
|
|
160
|
-
permissionResolveRef.current(
|
|
218
|
+
permissionResolveRef.current('no');
|
|
161
219
|
permissionResolveRef.current = null;
|
|
162
220
|
setPermissionRequest(null);
|
|
163
221
|
}
|
|
222
|
+
if (compactResolveRef.current) {
|
|
223
|
+
compactResolveRef.current(false);
|
|
224
|
+
compactResolveRef.current = null;
|
|
225
|
+
setCompactRequest(null);
|
|
226
|
+
}
|
|
227
|
+
// Restore checkpointed files
|
|
228
|
+
if (checkpointRef.current.size > 0) {
|
|
229
|
+
let restored = 0;
|
|
230
|
+
for (const [path, content] of checkpointRef.current) {
|
|
231
|
+
try {
|
|
232
|
+
if (content === null) {
|
|
233
|
+
if (existsSync(path))
|
|
234
|
+
unlinkSync(path);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
writeFileSync(path, content, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
restored++;
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
242
|
+
}
|
|
243
|
+
checkpointRef.current.clear();
|
|
244
|
+
if (restored > 0)
|
|
245
|
+
printer.systemMsg(`restored ${restored} file(s) to pre-session state`);
|
|
246
|
+
}
|
|
164
247
|
setStatus('idle');
|
|
165
248
|
}, []);
|
|
166
249
|
return {
|
|
@@ -170,5 +253,6 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
170
253
|
thinkingStartRef,
|
|
171
254
|
runLoop, handleAbort,
|
|
172
255
|
permissionRequest, resolvePermission,
|
|
256
|
+
compactRequest, resolveCompact,
|
|
173
257
|
};
|
|
174
258
|
}
|
|
@@ -6,22 +6,22 @@ import * as printer from '../printer.js';
|
|
|
6
6
|
import { loadLongMemory, saveLongMemory, mergeFacts, formatMemoryBlock } from '../../memory/store.js';
|
|
7
7
|
import { extractFacts } from '../../memory/extractor.js';
|
|
8
8
|
const SHORT_MEMORY_SIZE = 40;
|
|
9
|
-
function buildSystemPrompt(cwd, facts) {
|
|
10
|
-
return getSystemPrompt(`\n- CWD: ${cwd}
|
|
9
|
+
function buildSystemPrompt(cwd, facts, extraTools = []) {
|
|
10
|
+
return getSystemPrompt(`\n- CWD: ${cwd}`, extraTools) + formatMemoryBlock(facts);
|
|
11
11
|
}
|
|
12
|
-
export function useSession(initialSession, cwd, config) {
|
|
12
|
+
export function useSession(initialSession, cwd, config, extraTools = []) {
|
|
13
13
|
const [sessionName, setSessionName] = useState(initialSession);
|
|
14
14
|
const sessionNameRef = useRef(initialSession);
|
|
15
15
|
const historyRef = useRef([]);
|
|
16
16
|
const saveTimerRef = useRef(null);
|
|
17
17
|
const firstMessageSentRef = useRef(false);
|
|
18
18
|
const longMemoryRef = useRef([]);
|
|
19
|
-
const systemPromptRef = useRef(buildSystemPrompt(cwd, []));
|
|
19
|
+
const systemPromptRef = useRef(buildSystemPrompt(cwd, [], extraTools));
|
|
20
20
|
useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
const facts = loadLongMemory(initialSession);
|
|
23
23
|
longMemoryRef.current = facts;
|
|
24
|
-
systemPromptRef.current = buildSystemPrompt(cwd, facts);
|
|
24
|
+
systemPromptRef.current = buildSystemPrompt(cwd, facts, extraTools);
|
|
25
25
|
if (facts.length)
|
|
26
26
|
printer.systemMsg(`long memory: ${facts.length} facts loaded`);
|
|
27
27
|
const history = loadSession(initialSession);
|
|
@@ -51,7 +51,7 @@ export function useSession(initialSession, cwd, config) {
|
|
|
51
51
|
return;
|
|
52
52
|
const updated = mergeFacts(longMemoryRef.current, newFacts);
|
|
53
53
|
longMemoryRef.current = updated;
|
|
54
|
-
systemPromptRef.current = buildSystemPrompt(cwd, updated);
|
|
54
|
+
systemPromptRef.current = buildSystemPrompt(cwd, updated, extraTools);
|
|
55
55
|
saveLongMemory(sessionNameRef.current, updated);
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'react';
|
|
2
2
|
import { readFile, guardPath } from '../../files/ops.js';
|
|
3
3
|
import { getSystemPrompt } from '../../tools/index.js';
|
|
4
|
+
import { saveConfig } from '../../config.js';
|
|
4
5
|
import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../../sessions.js';
|
|
6
|
+
import { compactContext } from '../../tasks/compactor.js';
|
|
5
7
|
import { runDeepThink } from '../deepThink.js';
|
|
6
8
|
import { buildGitContext, looksCodeRelated } from '../git-context.js';
|
|
7
|
-
import { getTavilyKey, saveTavilyKey } from '../../tavily/client.js';
|
|
8
9
|
import { buildIndex } from '../../index/indexer.js';
|
|
9
10
|
import { indexStats, clearIndex } from '../../index/store.js';
|
|
10
11
|
import { embed } from '../../index/embedder.js';
|
|
@@ -34,7 +35,7 @@ export function useSubmit(deps) {
|
|
|
34
35
|
const depsRef = useRef(deps);
|
|
35
36
|
depsRef.current = deps;
|
|
36
37
|
const handleSubmit = useCallback(async (text) => {
|
|
37
|
-
const { config, skills, cwd, version, currentModelRef, setCurrentModel, historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef,
|
|
38
|
+
const { config, skills, cwd, version, currentModelRef, setCurrentModel, historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef, setPlanningMode, runLoop, buildContext, pushHistory, setSessionName, renameFromMessage, setStatus, setTaskLabel, setCurrentTool, runRefactor, handleGit, lastGitStatusRef, mcpTools, setConfig, setConfigOpen, } = depsRef.current;
|
|
38
39
|
const cmd = text.trim();
|
|
39
40
|
if (cmd === '?') {
|
|
40
41
|
printer.systemMsg('shortcuts:\n' +
|
|
@@ -56,21 +57,6 @@ export function useSubmit(deps) {
|
|
|
56
57
|
printer.systemMsg(`miii v${version ?? 'unknown'}`);
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
if (cmd === '/tavily-key' || cmd.startsWith('/tavily-key ')) {
|
|
60
|
-
const key = cmd.slice(11).trim();
|
|
61
|
-
if (!key) {
|
|
62
|
-
const existing = getTavilyKey();
|
|
63
|
-
printer.systemMsg(existing ? 'Tavily key set (use /tavily-key <key> to update)' : 'No Tavily key set. Usage: /tavily-key tvly-...');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!key.startsWith('tvly-')) {
|
|
67
|
-
printer.systemMsg('Key should start with tvly-. Get yours at https://tavily.com');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
saveTavilyKey(key);
|
|
71
|
-
printer.systemMsg('Tavily API key saved to ~/.config/miii/tavily.key (mode 600)');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
60
|
if (cmd === '/skills' || cmd.startsWith('/skills ')) {
|
|
75
61
|
const sub = cmd.slice(7).trim();
|
|
76
62
|
if (!sub || sub === 'list') {
|
|
@@ -110,6 +96,62 @@ export function useSubmit(deps) {
|
|
|
110
96
|
printer.systemMsg('usage: /skills install <name> | /skills uninstall <name> | /skills list');
|
|
111
97
|
return;
|
|
112
98
|
}
|
|
99
|
+
if (cmd === '/config' || cmd.startsWith('/config ')) {
|
|
100
|
+
const sub = cmd.slice(7).trim();
|
|
101
|
+
if (!sub) {
|
|
102
|
+
setConfigOpen(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (sub.startsWith('provider ')) {
|
|
106
|
+
const val = sub.slice(9).trim();
|
|
107
|
+
const valid = ['ollama', 'anthropic', 'openai-compat'];
|
|
108
|
+
if (!valid.includes(val)) {
|
|
109
|
+
printer.systemMsg(`valid providers: ${valid.join(', ')}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setConfig(c => ({ ...c, provider: val }));
|
|
113
|
+
saveConfig({ provider: val });
|
|
114
|
+
printer.systemMsg(`provider → ${val}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (sub.startsWith('model ')) {
|
|
118
|
+
const val = sub.slice(6).trim();
|
|
119
|
+
if (!val) {
|
|
120
|
+
printer.systemMsg('usage: /config model <name>');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setConfig(c => ({ ...c, model: val }));
|
|
124
|
+
saveConfig({ model: val });
|
|
125
|
+
setCurrentModel(val);
|
|
126
|
+
currentModelRef.current = val;
|
|
127
|
+
printer.systemMsg(`model → ${val}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (sub.startsWith('key ')) {
|
|
131
|
+
const val = sub.slice(4).trim();
|
|
132
|
+
if (!val) {
|
|
133
|
+
printer.systemMsg('usage: /config key <api-key>');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
setConfig(c => ({ ...c, apiKey: val }));
|
|
137
|
+
saveConfig({ apiKey: val });
|
|
138
|
+
printer.systemMsg(`apiKey → ${val.slice(0, 8)}…`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (sub.startsWith('url ')) {
|
|
142
|
+
const val = sub.slice(4).trim();
|
|
143
|
+
if (!val) {
|
|
144
|
+
printer.systemMsg('usage: /config url <base-url>');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
setConfig(c => ({ ...c, baseUrl: val }));
|
|
148
|
+
saveConfig({ baseUrl: val });
|
|
149
|
+
printer.systemMsg(`baseUrl → ${val}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
printer.systemMsg('usage: /config [provider|model|key|url] <value>');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
113
155
|
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
114
156
|
const name = cmd.slice(6).trim();
|
|
115
157
|
if (!name) {
|
|
@@ -121,8 +163,31 @@ export function useSubmit(deps) {
|
|
|
121
163
|
printer.systemMsg(`model → ${name}`);
|
|
122
164
|
return;
|
|
123
165
|
}
|
|
124
|
-
if (cmd === '/
|
|
125
|
-
|
|
166
|
+
if (cmd === '/compact') {
|
|
167
|
+
const full = buildContext();
|
|
168
|
+
if (full.length <= 2) {
|
|
169
|
+
printer.systemMsg('nothing to compact');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
printer.systemMsg(`compacting ${full.length} messages…`);
|
|
173
|
+
setStatus('thinking');
|
|
174
|
+
try {
|
|
175
|
+
const compacted = await compactContext(full, {
|
|
176
|
+
provider: config.provider,
|
|
177
|
+
model: currentModelRef.current,
|
|
178
|
+
baseUrl: config.baseUrl,
|
|
179
|
+
apiKey: config.apiKey,
|
|
180
|
+
}, undefined);
|
|
181
|
+
historyRef.current = compacted.filter(m => m.role !== 'system');
|
|
182
|
+
saveSession(sessionNameRef.current, historyRef.current);
|
|
183
|
+
printer.systemMsg(`compacted: ${full.length} → ${compacted.length} messages`);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
printer.errorMsg(`compact failed: ${e}`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
setStatus('idle');
|
|
190
|
+
}
|
|
126
191
|
return;
|
|
127
192
|
}
|
|
128
193
|
if (cmd === '/new') {
|
|
@@ -135,7 +200,7 @@ export function useSubmit(deps) {
|
|
|
135
200
|
historyRef.current = [];
|
|
136
201
|
setSessionName(newName);
|
|
137
202
|
setPlanningMode(false);
|
|
138
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}
|
|
203
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`, mcpTools);
|
|
139
204
|
printer.systemMsg(`new session → ${newName}`);
|
|
140
205
|
return;
|
|
141
206
|
}
|
|
@@ -143,7 +208,7 @@ export function useSubmit(deps) {
|
|
|
143
208
|
historyRef.current = [];
|
|
144
209
|
saveSession(sessionNameRef.current, []);
|
|
145
210
|
setPlanningMode(false);
|
|
146
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}
|
|
211
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`, mcpTools);
|
|
147
212
|
printer.systemMsg('chat cleared');
|
|
148
213
|
return;
|
|
149
214
|
}
|
|
@@ -201,7 +266,7 @@ export function useSubmit(deps) {
|
|
|
201
266
|
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
202
267
|
const topic = cmd.slice(5).trim();
|
|
203
268
|
setPlanningMode(true);
|
|
204
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure
|
|
269
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure.`, mcpTools);
|
|
205
270
|
const msg = topic ? `I want to plan: ${topic}` : 'I want to start planning. Help me think through my goals step by step.';
|
|
206
271
|
printer.userMsg(msg);
|
|
207
272
|
pushHistory({ role: 'user', content: msg });
|
|
@@ -210,7 +275,7 @@ export function useSubmit(deps) {
|
|
|
210
275
|
}
|
|
211
276
|
if (cmd === '/plan:done') {
|
|
212
277
|
setPlanningMode(false);
|
|
213
|
-
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}
|
|
278
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`, mcpTools);
|
|
214
279
|
printer.systemMsg('planning mode off');
|
|
215
280
|
return;
|
|
216
281
|
}
|
package/dist/tui/printer.js
CHANGED
|
@@ -192,12 +192,82 @@ function toolLabel(name, args) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
export function planSummary(tools) {
|
|
196
|
+
if (!tools.length)
|
|
197
|
+
return;
|
|
198
|
+
const header = gray(`─ plan (${tools.length} action${tools.length === 1 ? '' : 's'})`);
|
|
199
|
+
write(header + '\n');
|
|
200
|
+
for (const t of tools) {
|
|
201
|
+
const dot = DELETE_TOOLS.has(t.name) ? red('◦') : EDIT_TOOLS.has(t.name) ? green('◦') : blue('◦');
|
|
202
|
+
const label = toolLabel(t.name, t.args);
|
|
203
|
+
write(` ${dot} ${gray(label)}\n`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
195
206
|
export function toolCallStart(name, args) {
|
|
196
207
|
const dot = DELETE_TOOLS.has(name) ? red('●') : EDIT_TOOLS.has(name) ? green('●') : blue('●');
|
|
197
|
-
write(
|
|
208
|
+
write(`\n${dot} ${bold(toolLabel(name, args))}\n`);
|
|
209
|
+
}
|
|
210
|
+
export function toolResultSummary(name, args, result) {
|
|
211
|
+
const a = args;
|
|
212
|
+
const lines = result.trim().split('\n').filter(Boolean);
|
|
213
|
+
let summary = '';
|
|
214
|
+
switch (name) {
|
|
215
|
+
case 'edit_file':
|
|
216
|
+
case 'write_file': {
|
|
217
|
+
const n = (a.content ?? '').split('\n').length;
|
|
218
|
+
summary = `Wrote ${n} line${n === 1 ? '' : 's'}`;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case 'create_file': {
|
|
222
|
+
const n = (a.content ?? '').split('\n').length;
|
|
223
|
+
summary = `Created file · ${n} line${n === 1 ? '' : 's'}`;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case 'patch_file':
|
|
227
|
+
summary = lines[0] ?? 'Applied patch';
|
|
228
|
+
break;
|
|
229
|
+
case 'delete_file':
|
|
230
|
+
summary = 'Deleted';
|
|
231
|
+
break;
|
|
232
|
+
case 'move_file':
|
|
233
|
+
summary = `Moved → ${a.to ?? ''}`;
|
|
234
|
+
break;
|
|
235
|
+
case 'read_file': {
|
|
236
|
+
const n = lines.length;
|
|
237
|
+
summary = `Read ${n} line${n === 1 ? '' : 's'}`;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'list_files':
|
|
241
|
+
summary = `Found ${lines.length} file${lines.length === 1 ? '' : 's'}`;
|
|
242
|
+
break;
|
|
243
|
+
case 'run_command':
|
|
244
|
+
case 'run_tests':
|
|
245
|
+
case 'git_commit':
|
|
246
|
+
case 'git_status':
|
|
247
|
+
case 'git_diff':
|
|
248
|
+
case 'git_log': {
|
|
249
|
+
const first = lines[0]?.slice(0, 80) ?? '';
|
|
250
|
+
const more = lines.length > 1 ? ` (+${lines.length - 1} more)` : '';
|
|
251
|
+
summary = first + more;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'web_search':
|
|
255
|
+
summary = `Found ${lines.length} result${lines.length === 1 ? '' : 's'}`;
|
|
256
|
+
break;
|
|
257
|
+
case 'web_extract':
|
|
258
|
+
summary = `Extracted ${lines.length} line${lines.length === 1 ? '' : 's'}`;
|
|
259
|
+
break;
|
|
260
|
+
case 'search_codebase':
|
|
261
|
+
summary = lines[0]?.slice(0, 80) ?? 'Done';
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
summary = lines[0]?.slice(0, 80) ?? 'Done';
|
|
265
|
+
}
|
|
266
|
+
if (summary)
|
|
267
|
+
write(gray(` ${summary}`) + '\n');
|
|
198
268
|
}
|
|
199
269
|
export function toolMsg(name, result) {
|
|
200
|
-
const preview = result.length >
|
|
270
|
+
const preview = result.length > 600 ? result.slice(0, 600) + '…' : result;
|
|
201
271
|
const body = preview.trim()
|
|
202
272
|
? preview.split('\n').map(l => gray(' ' + l)).join('\n')
|
|
203
273
|
: '';
|