miii-cli 1.0.2 → 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 +12 -7
- package/dist/tasks/compactor.js +65 -26
- package/dist/tui/components/InputArea.js +30 -6
- package/dist/tui/hooks/useRunLoop.js +35 -10
- package/dist/tui/hooks/useSubmit.js +28 -0
- package/dist/tui/printer.js +72 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,20 +72,25 @@ This isn't autocomplete. Miii is a **full autonomous agent loop:**
|
|
|
72
72
|
|
|
73
73
|
● Researching: refactor auth module to use JWT
|
|
74
74
|
● Reading src/auth/session.ts
|
|
75
|
+
Read 42 lines
|
|
75
76
|
● Reading src/middleware/auth.ts
|
|
76
|
-
|
|
77
|
+
Read 28 lines
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
─ plan (2 actions)
|
|
80
|
+
◦ edit_file src/auth/session.ts
|
|
81
|
+
◦ edit_file src/middleware/auth.ts
|
|
79
82
|
|
|
80
83
|
⚠ edit_file src/auth/session.ts y approve n deny
|
|
81
84
|
> y
|
|
82
85
|
|
|
83
|
-
●
|
|
84
|
-
|
|
85
|
-
●
|
|
86
|
-
|
|
86
|
+
● edit_file src/auth/session.ts
|
|
87
|
+
Wrote 12 lines
|
|
88
|
+
● edit_file src/middleware/auth.ts
|
|
89
|
+
Wrote 8 lines
|
|
90
|
+
● run_tests
|
|
91
|
+
✅ Tests passed
|
|
87
92
|
|
|
88
|
-
─ refactor done —
|
|
93
|
+
─ refactor done — 2 file(s) processed
|
|
89
94
|
```
|
|
90
95
|
|
|
91
96
|
---
|
package/dist/tasks/compactor.js
CHANGED
|
@@ -1,52 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
import { chat } from '../llm/stream.js';
|
|
2
|
+
const COMPACT_THRESHOLD = 18;
|
|
3
|
+
const KEEP_RECENT = 6;
|
|
3
4
|
export function shouldCompact(messages) {
|
|
4
5
|
return messages.length > COMPACT_THRESHOLD;
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const COMPACT_SYSTEM = `You are a context summarizer for an AI coding agent session.
|
|
8
|
+
Your job: produce a dense, structured summary of the conversation so the agent can continue the task without losing context.
|
|
9
|
+
|
|
10
|
+
Output format (use exactly these headers):
|
|
11
|
+
|
|
12
|
+
## Task
|
|
13
|
+
One sentence: what the user asked for.
|
|
14
|
+
|
|
15
|
+
## Completed
|
|
16
|
+
Bullet list of actions taken (files edited, commands run, decisions made). Be specific — include file paths and outcomes.
|
|
17
|
+
|
|
18
|
+
## Current State
|
|
19
|
+
What is true right now: which files were changed, what tests showed, what is working or broken.
|
|
20
|
+
|
|
21
|
+
## Remaining
|
|
22
|
+
What still needs to be done, if anything.
|
|
23
|
+
|
|
24
|
+
## Key Context
|
|
25
|
+
Any constraints, errors encountered, important facts the agent must remember to continue correctly.
|
|
26
|
+
|
|
27
|
+
Be factual. No padding. Include file paths, error messages, and command outputs verbatim when relevant.`;
|
|
28
|
+
export async function compactContext(messages, cfg, goal) {
|
|
16
29
|
if (messages.length <= COMPACT_THRESHOLD)
|
|
17
30
|
return messages;
|
|
18
31
|
const system = messages[0]?.role === 'system' ? messages[0] : null;
|
|
32
|
+
const recent = messages.slice(messages.length - KEEP_RECENT);
|
|
33
|
+
const toSummarize = messages.slice(system ? 1 : 0, messages.length - KEEP_RECENT);
|
|
34
|
+
// Build conversation transcript for the summarizer
|
|
35
|
+
const transcript = toSummarize.map(m => {
|
|
36
|
+
const role = m.role === 'assistant' ? 'Assistant' : 'User';
|
|
37
|
+
const body = m.content.length > 2000 ? m.content.slice(0, 2000) + '\n[truncated]' : m.content;
|
|
38
|
+
return `### ${role}\n${body}`;
|
|
39
|
+
}).join('\n\n');
|
|
40
|
+
const userPrompt = [
|
|
41
|
+
goal ? `The user's goal: ${goal}\n` : '',
|
|
42
|
+
`Conversation to summarize:\n\n${transcript}`,
|
|
43
|
+
].join('');
|
|
44
|
+
let summary = '';
|
|
45
|
+
await chat({
|
|
46
|
+
...cfg,
|
|
47
|
+
messages: [
|
|
48
|
+
{ role: 'system', content: COMPACT_SYSTEM },
|
|
49
|
+
{ role: 'user', content: userPrompt },
|
|
50
|
+
],
|
|
51
|
+
onDone: (text) => { summary = text.trim(); },
|
|
52
|
+
onError: () => { },
|
|
53
|
+
});
|
|
54
|
+
// Fallback to dumb compaction if LLM fails
|
|
55
|
+
if (!summary)
|
|
56
|
+
return dumbCompact(messages, goal);
|
|
57
|
+
const summaryMsg = {
|
|
58
|
+
role: 'user',
|
|
59
|
+
content: `[Context compacted — ${toSummarize.length} messages summarised]\n\n${summary}`,
|
|
60
|
+
};
|
|
61
|
+
return [
|
|
62
|
+
...(system ? [system] : []),
|
|
63
|
+
summaryMsg,
|
|
64
|
+
...recent,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
function dumbCompact(messages, goal) {
|
|
68
|
+
const system = messages[0]?.role === 'system' ? messages[0] : null;
|
|
19
69
|
const userGoal = messages.find(m => m.role === 'user' && !m.content.startsWith('['));
|
|
20
|
-
const anchorCount = (system ? 1 : 0) + (userGoal ? 1 : 0);
|
|
21
|
-
const middle = messages.slice(anchorCount, messages.length - KEEP_RECENT);
|
|
22
70
|
const recent = messages.slice(messages.length - KEEP_RECENT);
|
|
71
|
+
const middle = messages.slice((system ? 1 : 0) + (userGoal ? 1 : 0), messages.length - KEEP_RECENT);
|
|
23
72
|
const toolResults = middle
|
|
24
73
|
.filter(m => m.role === 'user' && m.content.startsWith('Tool '))
|
|
25
|
-
.map(m => {
|
|
26
|
-
const lines = m.content.split('\n');
|
|
27
|
-
return `• ${lines[0]}`; // just the "Tool X result:" line
|
|
28
|
-
});
|
|
29
|
-
const assistantSummaries = middle
|
|
30
|
-
.filter(m => m.role === 'assistant' && m.content.trim().length > 0)
|
|
31
|
-
.map(m => m.content.slice(0, 120).replace(/\n/g, ' '));
|
|
74
|
+
.map(m => `• ${m.content.split('\n')[0]}`);
|
|
32
75
|
const parts = [`[context compacted — ${middle.length} messages summarised]`];
|
|
33
76
|
if (goal)
|
|
34
77
|
parts.push(`Goal: ${goal}`);
|
|
35
78
|
if (toolResults.length)
|
|
36
79
|
parts.push(`Completed:\n${toolResults.join('\n')}`);
|
|
37
|
-
if (assistantSummaries.length)
|
|
38
|
-
parts.push(`Last reasoning: ${assistantSummaries.at(-1)}`);
|
|
39
|
-
const summary = { role: 'user', content: parts.join('\n\n') };
|
|
40
80
|
return [
|
|
41
81
|
...(system ? [system] : []),
|
|
42
82
|
...(userGoal ? [userGoal] : []),
|
|
43
|
-
|
|
83
|
+
{ role: 'user', content: parts.join('\n\n') },
|
|
44
84
|
...recent,
|
|
45
85
|
];
|
|
46
86
|
}
|
|
47
87
|
/**
|
|
48
88
|
* Build a fresh isolated context for a single-file edit step.
|
|
49
|
-
* Keeps context tiny — avoids cross-file noise polluting the model.
|
|
50
89
|
*/
|
|
51
90
|
export function fileEditContext(systemPrompt, goal, filePath, fileContent, instruction) {
|
|
52
91
|
return [
|
|
@@ -7,6 +7,7 @@ import { AtPicker } from './AtPicker.js';
|
|
|
7
7
|
const BUILTIN_COMMANDS = [
|
|
8
8
|
// ── Session ──────────────────────────────────────────────────────────────
|
|
9
9
|
{ ns: 'builtin', name: 'new', description: 'start a fresh session with a new auto-named history' },
|
|
10
|
+
{ ns: 'builtin', name: 'compact', description: 'summarise conversation history now using the LLM — frees context before miii asks' },
|
|
10
11
|
{ ns: 'builtin', name: 'clear', description: 'wipe chat history for the current session' },
|
|
11
12
|
{ ns: 'builtin', name: 'sessions', description: 'list all saved sessions with message counts' },
|
|
12
13
|
{ ns: 'builtin', name: 'session', description: 'switch to a saved session — /session <name>' },
|
|
@@ -197,11 +198,15 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
197
198
|
useInput((input, key) => {
|
|
198
199
|
if (permissionRequest && onPermissionResponse) {
|
|
199
200
|
if (input === 'y' || input === 'Y') {
|
|
200
|
-
onPermissionResponse(
|
|
201
|
+
onPermissionResponse('yes');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (input === 'a' || input === 'A') {
|
|
205
|
+
onPermissionResponse('session');
|
|
201
206
|
return;
|
|
202
207
|
}
|
|
203
208
|
if (input === 'n' || input === 'N' || key.escape) {
|
|
204
|
-
onPermissionResponse(
|
|
209
|
+
onPermissionResponse('no');
|
|
205
210
|
return;
|
|
206
211
|
}
|
|
207
212
|
return;
|
|
@@ -445,13 +450,14 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
445
450
|
});
|
|
446
451
|
const { stdout } = useStdout();
|
|
447
452
|
const cols = stdout.columns ?? 80;
|
|
453
|
+
const availWidth = Math.max(20, cols - 4); // paddingX(2) + "> "(2)
|
|
448
454
|
const isProcessing = status !== 'idle';
|
|
449
455
|
const promptColor = (permissionRequest || compactRequest) ? 'yellow' : isProcessing ? 'yellow' : 'green';
|
|
450
456
|
const inHistory = historyIdx !== -1;
|
|
451
457
|
const hint = compactRequest
|
|
452
458
|
? 'y compact n keep full context'
|
|
453
459
|
: permissionRequest
|
|
454
|
-
? 'y approve n deny'
|
|
460
|
+
? 'y approve once a approve for session n deny'
|
|
455
461
|
: isProcessing
|
|
456
462
|
? 'esc interrupt'
|
|
457
463
|
: pasteLines > 0
|
|
@@ -466,10 +472,28 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
466
472
|
const pastePreview = pasteRef.current
|
|
467
473
|
? pasteRef.current.split('\n')[0].slice(0, cols - 6)
|
|
468
474
|
: '';
|
|
469
|
-
return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children:
|
|
470
|
-
?
|
|
471
|
-
: line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", hint] })] }));
|
|
475
|
+
return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: promptColor, bold: true, children: '> ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 3, children: [_jsx(Text, { color: "green", bold: true, children: "y once" }), _jsx(Text, { color: "cyan", bold: true, children: "a session" }), _jsx(Text, { color: "red", bold: true, children: "n deny" })] })) : compactRequest ? (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", bold: true, children: "y yes" }), _jsx(Text, { color: "red", bold: true, children: "n no" })] })) : pasteLines > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " line", pasteLines !== 1 ? 's' : ''] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] }), pastePreview && (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", pastePreview, pasteRef.current.split('\n')[0].length > cols - 6 ? '…' : ''] }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { children: isActive ? '█' : ' ' })) : (lines.map((line, i) => (_jsx(Text, { children: i === cursor.row
|
|
476
|
+
? viewportLine(line, cursor.col, availWidth, isActive)
|
|
477
|
+
: line.length > availWidth ? '…' + line.slice(line.length - availWidth + 1) : line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: '─'.repeat(Math.max(cols, 10)) }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", hint] })] }));
|
|
472
478
|
}
|
|
473
479
|
function renderLineWithCursor(line, col, showCursor) {
|
|
474
480
|
return line.slice(0, col) + (showCursor ? '█' : '') + line.slice(col);
|
|
475
481
|
}
|
|
482
|
+
function viewportLine(line, col, width, active) {
|
|
483
|
+
// If line fits, render normally
|
|
484
|
+
if (line.length < width)
|
|
485
|
+
return renderLineWithCursor(line, col, active);
|
|
486
|
+
// Slide window so cursor stays in view, roughly centered
|
|
487
|
+
let start = Math.max(0, col - Math.floor(width / 2));
|
|
488
|
+
if (start + width > line.length + 1) {
|
|
489
|
+
start = Math.max(0, line.length + 1 - width);
|
|
490
|
+
}
|
|
491
|
+
const hasLeft = start > 0;
|
|
492
|
+
const sliceW = width - (hasLeft ? 1 : 0) - 1; // -1 for right indicator space
|
|
493
|
+
const slice = line.slice(start, start + sliceW);
|
|
494
|
+
const hasRight = start + sliceW < line.length;
|
|
495
|
+
const adjCol = col - start;
|
|
496
|
+
return (hasLeft ? '…' : '') +
|
|
497
|
+
renderLineWithCursor(slice, Math.max(0, Math.min(adjCol, slice.length)), active) +
|
|
498
|
+
(hasRight ? '…' : '');
|
|
499
|
+
}
|
|
@@ -20,13 +20,14 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
20
20
|
const [compactRequest, setCompactRequest] = useState(null);
|
|
21
21
|
const compactResolveRef = useRef(null);
|
|
22
22
|
const checkpointRef = useRef(new Map());
|
|
23
|
+
const sessionApprovedRef = useRef(new Set());
|
|
23
24
|
const thinkingStartRef = useRef(0);
|
|
24
25
|
const extraToolsRef = useRef(extraTools);
|
|
25
26
|
extraToolsRef.current = extraTools;
|
|
26
27
|
const pushHistoryRef = useRef(pushHistory);
|
|
27
28
|
useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
|
|
28
|
-
const resolvePermission = useCallback((
|
|
29
|
-
permissionResolveRef.current?.(
|
|
29
|
+
const resolvePermission = useCallback((result) => {
|
|
30
|
+
permissionResolveRef.current?.(result);
|
|
30
31
|
permissionResolveRef.current = null;
|
|
31
32
|
setPermissionRequest(null);
|
|
32
33
|
}, []);
|
|
@@ -58,9 +59,19 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
58
59
|
compactResolveRef.current = resolve;
|
|
59
60
|
setCompactRequest({ messageCount: contextMsgs.length });
|
|
60
61
|
});
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
|
63
73
|
printer.systemMsg('keeping full context — responses may be slower');
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
abortRef.current = new AbortController();
|
|
66
77
|
await chat({
|
|
@@ -88,6 +99,8 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
88
99
|
if (displayText)
|
|
89
100
|
printer.assistantMsg(displayText);
|
|
90
101
|
pushHistoryRef.current({ role: 'assistant', content: fullText });
|
|
102
|
+
if (pendingTools.length)
|
|
103
|
+
printer.planSummary(pendingTools);
|
|
91
104
|
if (!pendingTools.length) {
|
|
92
105
|
const hasFencedCode = /```[\w]*\n[\s\S]{50,}?\n```/.test(fullText);
|
|
93
106
|
if (hasFencedCode && depth < MAX_TOOL_DEPTH - 1) {
|
|
@@ -109,11 +122,20 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
109
122
|
const tool = allTools.find(t => t.name === tc.name);
|
|
110
123
|
setCurrentTool(tc.name);
|
|
111
124
|
if (PERMISSION_TOOLS.has(tc.name)) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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') {
|
|
117
139
|
printer.systemMsg(`denied: ${tc.name}`);
|
|
118
140
|
next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user` });
|
|
119
141
|
break;
|
|
@@ -135,6 +157,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
135
157
|
try {
|
|
136
158
|
printer.toolCallStart(tc.name, tc.args);
|
|
137
159
|
const result = await tool.execute(tc.args);
|
|
160
|
+
printer.toolResultSummary(tc.name, tc.args, result);
|
|
138
161
|
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
139
162
|
printer.toolMsg(tc.name, result);
|
|
140
163
|
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
@@ -164,6 +187,7 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
164
187
|
printer.toolCallStart('run_tests', {});
|
|
165
188
|
const testResult = await testTool.execute({});
|
|
166
189
|
if (testResult && !testResult.startsWith('(no test script') && !testResult.startsWith('(no package.json')) {
|
|
190
|
+
printer.toolResultSummary('run_tests', {}, testResult);
|
|
167
191
|
printer.toolMsg('run_tests', testResult);
|
|
168
192
|
next.push({ role: 'user', content: `Test results after edits:\n${testResult}` });
|
|
169
193
|
}
|
|
@@ -189,8 +213,9 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
189
213
|
}, [config]);
|
|
190
214
|
const handleAbort = useCallback(() => {
|
|
191
215
|
abortRef.current?.abort();
|
|
216
|
+
sessionApprovedRef.current.clear();
|
|
192
217
|
if (permissionResolveRef.current) {
|
|
193
|
-
permissionResolveRef.current(
|
|
218
|
+
permissionResolveRef.current('no');
|
|
194
219
|
permissionResolveRef.current = null;
|
|
195
220
|
setPermissionRequest(null);
|
|
196
221
|
}
|
|
@@ -3,6 +3,7 @@ import { readFile, guardPath } from '../../files/ops.js';
|
|
|
3
3
|
import { getSystemPrompt } from '../../tools/index.js';
|
|
4
4
|
import { saveConfig } from '../../config.js';
|
|
5
5
|
import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../../sessions.js';
|
|
6
|
+
import { compactContext } from '../../tasks/compactor.js';
|
|
6
7
|
import { runDeepThink } from '../deepThink.js';
|
|
7
8
|
import { buildGitContext, looksCodeRelated } from '../git-context.js';
|
|
8
9
|
import { buildIndex } from '../../index/indexer.js';
|
|
@@ -162,6 +163,33 @@ export function useSubmit(deps) {
|
|
|
162
163
|
printer.systemMsg(`model → ${name}`);
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
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
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
165
193
|
if (cmd === '/new') {
|
|
166
194
|
if (saveTimerRef.current) {
|
|
167
195
|
clearTimeout(saveTimerRef.current);
|
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
|
: '';
|