junecoder 1.0.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 +2 -0
- package/agent.mjs +650 -0
- package/checkpoint.mjs +43 -0
- package/cli.js +199 -0
- package/config.mjs +118 -0
- package/context.mjs +160 -0
- package/distill.mjs +178 -0
- package/executor.mjs +207 -0
- package/mcp.mjs +209 -0
- package/memory.mjs +218 -0
- package/metaTools.mjs +523 -0
- package/package.json +26 -0
- package/provider.mjs +145 -0
- package/session.mjs +114 -0
- package/skills.mjs +147 -0
- package/tools/bash.mjs +41 -0
- package/tools/delete.mjs +57 -0
- package/tools/edit.mjs +71 -0
- package/tools/fetch.mjs +55 -0
- package/tools/glob.mjs +83 -0
- package/tools/grep.mjs +58 -0
- package/tools/index.mjs +41 -0
- package/tools/ls.mjs +62 -0
- package/tools/read.mjs +50 -0
- package/tools/websearch.mjs +58 -0
- package/tools/write.mjs +54 -0
- package/tools.mjs +15 -0
- package/tui.mjs +437 -0
package/tui.mjs
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tui.mjs — Terminal UI using raw ANSI sequences.
|
|
3
|
+
*
|
|
4
|
+
* Uses emitKeypressEvents for reliable keyboard input,
|
|
5
|
+
* proper CJK character width calculation, and frame-dedup rendering.
|
|
6
|
+
*
|
|
7
|
+
* Layout: header / conversation (scrollable) / todo panel / input box / status bar
|
|
8
|
+
*/
|
|
9
|
+
import { emitKeypressEvents } from "node:readline";
|
|
10
|
+
import { PassThrough } from "node:stream";
|
|
11
|
+
import { basename } from "node:path";
|
|
12
|
+
import { runAgent, ContinueError } from "./agent.mjs";
|
|
13
|
+
import { estimateTokens } from "./context.mjs";
|
|
14
|
+
import {
|
|
15
|
+
saveSession, clearSession, archiveCurrent,
|
|
16
|
+
listSlots, switchToSlot, sessionPath,
|
|
17
|
+
} from "./session.mjs";
|
|
18
|
+
import { PROVIDER_PRESETS as PRESETS } from "./config.mjs";
|
|
19
|
+
import { closeAllMcp } from "./mcp.mjs";
|
|
20
|
+
|
|
21
|
+
const ESC = "\x1b";
|
|
22
|
+
const ansi = {
|
|
23
|
+
hideCursor: `${ESC}[?25l`, showCursor: `${ESC}[?25h`,
|
|
24
|
+
altBuffer: `${ESC}[?1049h`, mainBuffer: `${ESC}[?1049l`,
|
|
25
|
+
mouseOn: `${ESC}[?1000h${ESC}[?1006h`, mouseOff: `${ESC}[?1000l${ESC}[?1006l`,
|
|
26
|
+
home: `${ESC}[H`, clearLine: `${ESC}[K`, reset: `${ESC}[0m`,
|
|
27
|
+
dim: `${ESC}[2m`, bold: `${ESC}[1m`,
|
|
28
|
+
fg: (n) => `${ESC}[${30 + n}m`, gray: `${ESC}[90m`,
|
|
29
|
+
};
|
|
30
|
+
const orange = `${ESC}[38;2;246;168;36m`;
|
|
31
|
+
const yellow = `${ESC}[38;2;253;224;71m`;
|
|
32
|
+
const C = {
|
|
33
|
+
user: yellow, assistant: orange, text: ansi.fg(7),
|
|
34
|
+
reason: `${ESC}[2m${ESC}[3m`, tool: orange,
|
|
35
|
+
error: ansi.fg(1), dim: ansi.gray, warn: ansi.fg(3),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function charWidth(cp) {
|
|
39
|
+
if ((cp >= 0x300 && cp <= 0x36f) || (cp >= 0x200b && cp <= 0x200f) || cp === 0xfe0f) return 0;
|
|
40
|
+
if ((cp >= 0x1100 && cp <= 0x115f) || (cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
41
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
|
|
42
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60) ||
|
|
43
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x1f000 && cp <= 0x1faff) ||
|
|
44
|
+
(cp >= 0x20000 && cp <= 0x3fffd) || (cp >= 0x2600 && cp <= 0x27bf)) return 2;
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
export function stringWidth(text) { let w = 0; for (const ch of text) w += charWidth(ch.codePointAt(0)); return w; }
|
|
48
|
+
function sliceByWidth(text, maxWidth) {
|
|
49
|
+
let w = 0, out = "";
|
|
50
|
+
for (const ch of text) { const cw = charWidth(ch.codePointAt(0)); if (w + cw > maxWidth) break; w += cw; out += ch; }
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function padByWidth(text, width) { return text + " ".repeat(Math.max(0, width - stringWidth(text))); }
|
|
54
|
+
|
|
55
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g;
|
|
56
|
+
export function sanitizeDisplay(s) {
|
|
57
|
+
if (typeof s !== "string") return "";
|
|
58
|
+
return s.replace(ANSI_RE, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
|
59
|
+
.replace(/\t/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/\n+$/, "");
|
|
60
|
+
}
|
|
61
|
+
export function wrapText(text, width) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
for (const rawLine of text.split("\n")) {
|
|
64
|
+
if (rawLine === "") { lines.push(""); continue; }
|
|
65
|
+
let line = rawLine;
|
|
66
|
+
while (stringWidth(line) > width) { const head = sliceByWidth(line, width); lines.push(head); line = line.slice([...head].length); }
|
|
67
|
+
lines.push(line);
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
export function layoutInput(chars, cursor, width) {
|
|
72
|
+
const PROMPT = "> ";
|
|
73
|
+
const lines = []; let cursorLine = 0, cursorCol = 0, cur = "", col = 0, firstLine = true;
|
|
74
|
+
const avail = () => firstLine ? width - 2 : width;
|
|
75
|
+
const flush = () => { lines.push((firstLine ? PROMPT : "") + cur); firstLine = false; cur = ""; col = 0; };
|
|
76
|
+
for (let i = 0; i <= chars.length; i++) {
|
|
77
|
+
if (i === cursor) { cursorLine = lines.length; cursorCol = (firstLine ? 2 : 0) + col; }
|
|
78
|
+
const ch = chars[i]; if (ch === undefined) break;
|
|
79
|
+
if (ch === "\n") { flush(); continue; }
|
|
80
|
+
const w = charWidth(ch.codePointAt(0)); if (col + w > avail()) flush();
|
|
81
|
+
cur += ch; col += w;
|
|
82
|
+
}
|
|
83
|
+
if (cur || lines.length === 0) flush();
|
|
84
|
+
return { lines, cursorLine, cursorCol };
|
|
85
|
+
}
|
|
86
|
+
function summarize(args) {
|
|
87
|
+
if (args === undefined || args === null) return "";
|
|
88
|
+
if (typeof args === "string") return args.slice(0, 100);
|
|
89
|
+
const s = JSON.stringify(args); return s.length > 100 ? s.slice(0, 97) + "..." : s;
|
|
90
|
+
}
|
|
91
|
+
function formatPermission(name, args) {
|
|
92
|
+
const cap = (s, n) => { if (typeof s !== "string") return ""; return s.length > n ? s.slice(0, n) + "\u2026" : s; };
|
|
93
|
+
const base = name.includes("/") ? name.split("/").pop() : name;
|
|
94
|
+
if (base === "bash") return cap(args.command ?? "", 1000).split("\n");
|
|
95
|
+
if (base === "write") return [`${args.path} (${(args.content ?? "").length} b)`, ...cap(args.content ?? "", 1000).split("\n")];
|
|
96
|
+
if (base === "edit") return [`${args.path}`, ...cap(args.old_string ?? "", 500).split("\n").map(l => "- " + l), " \u2193", ...cap(args.new_string ?? "", 500).split("\n").map(l => "+ " + l)];
|
|
97
|
+
if (base === "delete") return [`${args.path}${args.force ? " (force)" : ""}`];
|
|
98
|
+
if (base === "subagent") return cap(args.task ?? "", 500).split("\n");
|
|
99
|
+
return [cap(summarize(args), 300)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function startTUI(agent, opts = {}) {
|
|
103
|
+
if (!process.stdin.isTTY) throw new Error("TUI requires a TTY");
|
|
104
|
+
const state = {
|
|
105
|
+
lines: [], streaming: "", input: [], cursor: 0, history: [], historyIndex: -1,
|
|
106
|
+
scroll: 0, processing: false, controller: null, permission: null, permissionPreview: [],
|
|
107
|
+
question: null, tasks: agent.tasks ?? [], tokens: { prompt: 0, completion: 0 },
|
|
108
|
+
ctxCache: { len: -1, tokens: 0 }, reasoning: "", toolStreams: {},
|
|
109
|
+
subOutput: "", currentSub: null, currentTool: null, processingStarted: 0, status: "Ready",
|
|
110
|
+
};
|
|
111
|
+
if (state.tasks.length > 0 && state.tasks.every(t => t.status === "done")) state.tasks = [];
|
|
112
|
+
|
|
113
|
+
const keyStream = new PassThrough();
|
|
114
|
+
let mousePending = "", lastRenderedScroll = 0;
|
|
115
|
+
emitKeypressEvents(keyStream);
|
|
116
|
+
process.stdin.setRawMode(true);
|
|
117
|
+
process.stdout.write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn);
|
|
118
|
+
|
|
119
|
+
process.stdin.on("data", (chunk) => {
|
|
120
|
+
let text = mousePending + chunk.toString("utf8"); mousePending = "";
|
|
121
|
+
for (const m of text.matchAll(/\x1b\[<(\d+);\d+;\d+([Mm])/g)) {
|
|
122
|
+
if (Number(m[1]) === 64) state.scroll += 3;
|
|
123
|
+
else if (Number(m[1]) === 65) state.scroll = Math.max(0, state.scroll - 3);
|
|
124
|
+
}
|
|
125
|
+
text = text.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, "");
|
|
126
|
+
const tail = text.match(/\x1b\[<[\d;]*$/);
|
|
127
|
+
if (tail) { mousePending = tail[0]; text = text.slice(0, -tail[0].length); }
|
|
128
|
+
if (state.scroll !== lastRenderedScroll) { lastRenderedScroll = state.scroll; render(); }
|
|
129
|
+
if (text) keyStream.write(text);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let cleanedUp = false;
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
if (cleanedUp) return; cleanedUp = true;
|
|
135
|
+
try { archiveCurrent(agent.cwd); saveSession(agent, state.lines); } catch {}
|
|
136
|
+
try { closeAllMcp(agent); } catch {}
|
|
137
|
+
if (process.stdin.setRawMode) { try { process.stdin.setRawMode(false); } catch {} }
|
|
138
|
+
process.stdout.write(ansi.mouseOff + ansi.mainBuffer + ansi.showCursor + ansi.reset);
|
|
139
|
+
};
|
|
140
|
+
process.on("exit", cleanup);
|
|
141
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
142
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
143
|
+
|
|
144
|
+
const pushLine = (text, color) => {
|
|
145
|
+
state.lines.push({ text, color });
|
|
146
|
+
if (state.lines.length > 5000) state.lines.splice(0, 1000);
|
|
147
|
+
render();
|
|
148
|
+
};
|
|
149
|
+
const pushLabel = (text, color) => {
|
|
150
|
+
if (state.lines.length > 0) state.lines.push({ text: "", color: C.dim });
|
|
151
|
+
state.lines.push({ text, color }); render();
|
|
152
|
+
};
|
|
153
|
+
let assistantLabeled = false;
|
|
154
|
+
const ensureAssistantLabel = () => {
|
|
155
|
+
if (!assistantLabeled) { assistantLabeled = true; pushLabel("\u276f EasyCode:", ansi.bold + C.assistant); }
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
let lastFrame = "", renderTimer = null;
|
|
159
|
+
function scheduleRender() { if (renderTimer) return; renderTimer = setTimeout(() => { renderTimer = null; render(); }, 40); }
|
|
160
|
+
|
|
161
|
+
function render() {
|
|
162
|
+
const cols = process.stdout.columns || 80;
|
|
163
|
+
const rows = process.stdout.rows || 24;
|
|
164
|
+
const W = Math.max(20, cols - 1);
|
|
165
|
+
|
|
166
|
+
const layout = layoutInput(state.input, state.cursor, W - 6);
|
|
167
|
+
const MAX_INPUT_LINES = 5;
|
|
168
|
+
let inputOffset = 0;
|
|
169
|
+
if (layout.lines.length > MAX_INPUT_LINES) inputOffset = Math.min(layout.cursorLine, layout.lines.length - MAX_INPUT_LINES);
|
|
170
|
+
const inputLines = layout.lines.slice(inputOffset, inputOffset + MAX_INPUT_LINES);
|
|
171
|
+
|
|
172
|
+
let boxLines = inputLines;
|
|
173
|
+
if (state.question) {
|
|
174
|
+
const q = state.question;
|
|
175
|
+
boxLines = q.options.length > 0
|
|
176
|
+
? q.options.map((opt, i) => (i === (q.selected ?? 0) ? "\u25b8 " : " ") + opt)
|
|
177
|
+
: ["\u25b8 " + (q.answer ?? "")];
|
|
178
|
+
}
|
|
179
|
+
const inputBoxH = boxLines.length + 2;
|
|
180
|
+
|
|
181
|
+
const MAX_TASK_LINES = 5;
|
|
182
|
+
let visibleTasks = [];
|
|
183
|
+
if (state.tasks.length <= MAX_TASK_LINES) visibleTasks = state.tasks;
|
|
184
|
+
else {
|
|
185
|
+
visibleTasks = [...state.tasks.filter(t => t.status === "in_progress"),
|
|
186
|
+
...state.tasks.filter(t => t.status === "pending"),
|
|
187
|
+
...state.tasks.filter(t => t.status === "done")].slice(0, MAX_TASK_LINES);
|
|
188
|
+
}
|
|
189
|
+
const taskPanelH = visibleTasks.length;
|
|
190
|
+
const subOutLen = (state.subOutput && state.processing) ? wrapText(state.subOutput, W - 8).slice(-2).length : 0;
|
|
191
|
+
const permPreviewLen = state.permission ? 1 + state.permissionPreview.reduce((s, l) => s + wrapText(" " + l, W - 1).length, 0) : 0;
|
|
192
|
+
const convH = Math.max(1, rows - 1 - inputBoxH - 1 - taskPanelH - subOutLen - permPreviewLen);
|
|
193
|
+
|
|
194
|
+
const convLines = [];
|
|
195
|
+
for (const l of state.lines) {
|
|
196
|
+
for (const wrapped of wrapText(sanitizeDisplay(l.text), W)) convLines.push({ text: wrapped, color: l.color });
|
|
197
|
+
}
|
|
198
|
+
if (state.reasoning) { for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), W)) convLines.push({ text: wrapped, color: C.reason }); }
|
|
199
|
+
if (state.streaming) { for (const wrapped of wrapText(sanitizeDisplay(state.streaming), W)) convLines.push({ text: wrapped, color: C.text }); }
|
|
200
|
+
const allStreams = Object.values(state.toolStreams).join("");
|
|
201
|
+
if (allStreams) { const tail = sanitizeDisplay(allStreams.slice(-4000)); for (const wrapped of wrapText(tail, W)) convLines.push({ text: wrapped, color: C.dim }); }
|
|
202
|
+
|
|
203
|
+
const maxScroll = Math.max(0, convLines.length - convH);
|
|
204
|
+
state.scroll = Math.min(state.scroll, maxScroll);
|
|
205
|
+
const end = convLines.length - state.scroll;
|
|
206
|
+
const visible = convLines.slice(Math.max(0, end - convH), end);
|
|
207
|
+
const out = [`${ansi.home}${ansi.bold}${C.tool}EasyCode${ansi.reset}${ansi.dim} \u2502 ${sliceByWidth(agent.provider.model || "?", 30)} \u2502 ${sliceByWidth(basename(agent.cwd), Math.max(10, cols - 50))}${ansi.reset}${ansi.clearLine}`];
|
|
208
|
+
|
|
209
|
+
const pad = convH - visible.length;
|
|
210
|
+
for (let i = 0; i < pad; i++) out.push(ansi.clearLine);
|
|
211
|
+
for (const l of visible) out.push(`${l.color}${l.text}${ansi.reset}${ansi.clearLine}`);
|
|
212
|
+
|
|
213
|
+
for (const t of visibleTasks) {
|
|
214
|
+
const mark = t.status === "done" ? "\u2713" : t.status === "in_progress" ? "\u25b6" : "\u25cb";
|
|
215
|
+
const color = t.status === "done" ? `${C.dim}${ESC}[9m` : t.status === "in_progress" ? C.tool : C.text;
|
|
216
|
+
out.push(`${color} ${mark} ${sliceByWidth(t.title, W)}${ansi.reset}${ansi.clearLine}`);
|
|
217
|
+
}
|
|
218
|
+
if (state.subOutput && state.processing) {
|
|
219
|
+
for (const l of wrapText(state.subOutput, W - 8).slice(-2)) out.push(`${C.dim}[${state.currentSub}] ${l}${ansi.reset}${ansi.clearLine}`);
|
|
220
|
+
}
|
|
221
|
+
if (state.permission) {
|
|
222
|
+
out.push(`${ansi.bold}${C.warn}\u276f Permission${ansi.reset}${ansi.clearLine}`);
|
|
223
|
+
for (const line of state.permissionPreview) for (const wrapped of wrapText(" " + line, W)) out.push(`${C.warn}${wrapped}${ansi.reset}${ansi.clearLine}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let borderColor = C.tool, title;
|
|
227
|
+
if (state.question) { borderColor = C.tool; title = " " + sliceByWidth(state.question.text, W - 6) + " "; }
|
|
228
|
+
else if (state.permission) { borderColor = C.warn; title = state.permission.name === "continue" ? " Continue? (y/n) " : " Allow " + state.permission.name + "? (y/n/a) "; }
|
|
229
|
+
else if (state.processing) title = " Processing... ";
|
|
230
|
+
else title = " Input ";
|
|
231
|
+
const topBorder = "\u256d\u2500" + title + "\u2500".repeat(Math.max(0, W - 3 - stringWidth(title))) + "\u256e";
|
|
232
|
+
out.push(`${borderColor}${topBorder}${ansi.reset}${ansi.clearLine}`);
|
|
233
|
+
for (let i = 0; i < boxLines.length; i++) {
|
|
234
|
+
const content = padByWidth(boxLines[i], W - 4);
|
|
235
|
+
out.push(`${borderColor}\u2502${ansi.reset} ${content} ${borderColor}\u2502${ansi.reset}${ansi.clearLine}`);
|
|
236
|
+
}
|
|
237
|
+
const bottomBorder = "\u2570" + "\u2500".repeat(W - 2) + "\u256f";
|
|
238
|
+
out.push(`${borderColor}${bottomBorder}${ansi.reset}${ansi.clearLine}`);
|
|
239
|
+
|
|
240
|
+
const scrollHint = state.scroll > 0 ? " \u2502 scroll:" + state.scroll : "";
|
|
241
|
+
const taskHint = state.tasks.length > 0 ? " \u2502 \u2713" + state.tasks.filter(t => t.status === "done").length + "/" + state.tasks.length : "";
|
|
242
|
+
const tk = state.tokens, fmtK = n => n >= 10000 ? Math.round(n/1000) + "k" : n >= 1000 ? (n/1000).toFixed(1) + "k" : String(n);
|
|
243
|
+
const tokenHint = tk.prompt > 0 ? " \u2502 \u2191" + fmtK(tk.prompt) + " \u2193" + fmtK(tk.completion) : "";
|
|
244
|
+
const elapsed = state.processing ? " " + Math.floor((Date.now() - state.processingStarted)/1000) + "s" : "";
|
|
245
|
+
const toolHint = state.currentTool ? " " + state.currentTool + "\u2026" : "";
|
|
246
|
+
const statusText = state.processing ? state.status + toolHint + elapsed : state.status;
|
|
247
|
+
if (state.ctxCache.len !== agent.history.length) state.ctxCache = { len: agent.history.length, tokens: estimateTokens(agent.history) };
|
|
248
|
+
const ctxThreshold = agent.config?.agent?.compactThreshold ?? 750_000;
|
|
249
|
+
const ctxPct = Math.round((state.ctxCache.tokens / ctxThreshold) * 100);
|
|
250
|
+
const ctxHint = ctxPct > 0 ? (ctxPct >= 80 ? " \u2502 " + C.warn + "ctx " + ctxPct + "%" + ansi.reset + ansi.dim : " \u2502 ctx " + ctxPct + "%") : "";
|
|
251
|
+
let statusLine = statusText + taskHint + tokenHint + ctxHint + scrollHint + " \u2502 Enter:send \u2502 /:cmds \u2502 Ctrl+C:quit";
|
|
252
|
+
const autoBanner = agent.autoApprove ? C.warn + "AUTO" + ansi.reset + ansi.dim + "\u2502" : "";
|
|
253
|
+
const planBanner = agent.planMode ? C.tool + "PLAN" + ansi.reset + ansi.dim + "\u2502" : "";
|
|
254
|
+
statusLine = sliceByWidth(statusLine, Math.max(10, W));
|
|
255
|
+
out.push(ansi.dim + planBanner + autoBanner + (planBanner || autoBanner ? " " : "") + statusLine + ansi.reset + ansi.clearLine);
|
|
256
|
+
|
|
257
|
+
const frame = out.join("\r\n");
|
|
258
|
+
if (frame !== lastFrame) { lastFrame = frame; process.stdout.write(frame); }
|
|
259
|
+
|
|
260
|
+
if (state.processing || state.permission || state.question) process.stdout.write(ansi.hideCursor);
|
|
261
|
+
else {
|
|
262
|
+
const cursorRow = 1 + convH + taskPanelH + 2 + (layout.cursorLine - inputOffset);
|
|
263
|
+
const cursorCol = 3 + layout.cursorCol;
|
|
264
|
+
process.stdout.write(ESC + "[" + cursorRow + ";" + cursorCol + "H" + ansi.showCursor);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
process.stdout.on("resize", render);
|
|
268
|
+
|
|
269
|
+
keyStream.on("keypress", (str, key) => {
|
|
270
|
+
if (state.permission) {
|
|
271
|
+
const ch = (str || key.name || "").toLowerCase();
|
|
272
|
+
if (ch === "y" || key.name === "y") state.permission.resolve(true);
|
|
273
|
+
else if (ch === "n" || key.name === "n") state.permission.resolve(false);
|
|
274
|
+
else if (ch === "a" || key.name === "a") { agent.autoApprove = true; state.permission.resolve(true); pushLine(" [auto] Auto-approve ON.", C.warn); }
|
|
275
|
+
else return;
|
|
276
|
+
state.permission = null; state.permissionPreview = []; state.status = state.processing ? "Processing..." : "Ready"; render(); return;
|
|
277
|
+
}
|
|
278
|
+
if (state.question) {
|
|
279
|
+
if (key.name === "return" || key.name === "enter") {
|
|
280
|
+
const answer = state.question.options.length > 0 ? state.question.options[state.question.selected ?? 0] ?? "" : state.input.join("").trim();
|
|
281
|
+
state.input = []; state.cursor = 0; const resolve = state.question.resolve; state.question = null;
|
|
282
|
+
resolve(answer); state.status = state.processing ? "Processing..." : "Ready"; render(); return;
|
|
283
|
+
}
|
|
284
|
+
if (key.name === "escape") { const resolve = state.question.resolve; state.question = null; resolve(""); state.status = state.processing ? "Processing..." : "Ready"; render(); return; }
|
|
285
|
+
if (state.question.options.length > 0 && (key.name === "up" || key.name === "down")) {
|
|
286
|
+
state.question.selected = state.question.selected ?? 0; state.question.selected += key.name === "up" ? -1 : 1;
|
|
287
|
+
if (state.question.selected < 0) state.question.selected = state.question.options.length - 1;
|
|
288
|
+
if (state.question.selected >= state.question.options.length) state.question.selected = 0; render(); return;
|
|
289
|
+
}
|
|
290
|
+
if (state.question.options.length === 0) { editInput(str, key); render(); return; }
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (key.name === "c" && key.ctrl) {
|
|
294
|
+
if (state.processing && state.controller) { state.controller.abort(); state.status = "Aborting..."; render(); }
|
|
295
|
+
else { cleanup(); process.exit(0); }
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (key.name === "d" && key.ctrl && state.input.length === 0) { cleanup(); process.exit(0); return; }
|
|
299
|
+
if (key.name === "l" && key.ctrl) { process.stdout.write(ansi.home + ESC + "[2J"); lastFrame = ""; render(); return; }
|
|
300
|
+
if (key.name === "return" || key.name === "enter") { submit(); return; }
|
|
301
|
+
|
|
302
|
+
if (key.name === "up") {
|
|
303
|
+
if (state.history.length > 0 && state.historyIndex === -1) { state._savedInput = [...state.input]; state.historyIndex = state.history.length - 1; state.input = [...state.history[state.historyIndex]]; state.cursor = state.input.length; }
|
|
304
|
+
else if (state.historyIndex > 0) { state.historyIndex--; state.input = [...state.history[state.historyIndex]]; state.cursor = state.input.length; }
|
|
305
|
+
render(); return;
|
|
306
|
+
}
|
|
307
|
+
if (key.name === "down") {
|
|
308
|
+
if (state.historyIndex >= 0) { state.historyIndex++; if (state.historyIndex >= state.history.length) { state.historyIndex = -1; state.input = state._savedInput || []; state._savedInput = null; } else { state.input = [...state.history[state.historyIndex]]; } state.cursor = state.input.length; }
|
|
309
|
+
render(); return;
|
|
310
|
+
}
|
|
311
|
+
if (key.name === "pageup") { state.scroll += (rows - 4); render(); return; }
|
|
312
|
+
if (key.name === "pagedown") { state.scroll = Math.max(0, state.scroll - (rows - 4)); render(); return; }
|
|
313
|
+
editInput(str, key); render();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
function editInput(str, key) {
|
|
317
|
+
if (key.name === "backspace") { if (state.cursor > 0) { state.input.splice(state.cursor - 1, 1); state.cursor--; } return; }
|
|
318
|
+
if (key.name === "delete") { if (state.cursor < state.input.length) state.input.splice(state.cursor, 1); return; }
|
|
319
|
+
if (key.name === "left") { if (state.cursor > 0) state.cursor--; return; }
|
|
320
|
+
if (key.name === "right") { if (state.cursor < state.input.length) state.cursor++; return; }
|
|
321
|
+
if (key.name === "home") { state.cursor = 0; return; }
|
|
322
|
+
if (key.name === "end") { state.cursor = state.input.length; return; }
|
|
323
|
+
if (key.name === "k" && key.ctrl) { state.input = state.input.slice(0, state.cursor); return; }
|
|
324
|
+
if (key.name === "u" && key.ctrl) { state.input = state.input.slice(state.cursor); state.cursor = 0; return; }
|
|
325
|
+
if (key.name === "w" && key.ctrl) { while (state.cursor > 0 && state.input[state.cursor-1] === " ") { state.input.splice(state.cursor-1, 1); state.cursor--; } while (state.cursor > 0 && state.input[state.cursor-1] !== " ") { state.input.splice(state.cursor-1, 1); state.cursor--; } return; }
|
|
326
|
+
if (str && str.length > 0 && !key.ctrl && !key.meta && str !== "\r" && str !== "\n") { for (const ch of str) { state.input.splice(state.cursor, 0, ch); state.cursor++; } }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function submit() {
|
|
330
|
+
const text = state.input.join("").trim();
|
|
331
|
+
if (!text || state.processing) return;
|
|
332
|
+
state.input = []; state.cursor = 0; state.history.push(text); state.historyIndex = -1; state.scroll = 0;
|
|
333
|
+
if (text.startsWith("/")) { await handleSlash(text); return; }
|
|
334
|
+
pushLabel("\u276f You:", ansi.bold + C.user); pushLine(text, C.text);
|
|
335
|
+
try { const { createCheckpoint } = await import("./checkpoint.mjs"); await createCheckpoint(agent.cwd); } catch {}
|
|
336
|
+
assistantLabeled = false; state.processing = true; state.status = "Processing...";
|
|
337
|
+
state.streaming = ""; state.reasoning = ""; state.currentTool = null; state.toolStreams = {}; state.subOutput = "";
|
|
338
|
+
state.processingStarted = Date.now(); state.controller = new AbortController();
|
|
339
|
+
const ticker = setInterval(() => { if (state.processing) render(); }, 1000); render();
|
|
340
|
+
const callbacks = {
|
|
341
|
+
onToken: t => { ensureAssistantLabel(); state.streaming += t; scheduleRender(); },
|
|
342
|
+
onReasoning: t => { ensureAssistantLabel(); state.reasoning += t; scheduleRender(); },
|
|
343
|
+
onToolCall: (name, args) => { flushStream(); ensureAssistantLabel(); state.currentTool = name; pushLine(" [tool] " + name + " " + summarize(args), C.tool); },
|
|
344
|
+
onToolResult: (name, output, error) => { state.currentTool = null; const text = error ? "Error: " + error : (output || ""); const stream = state.toolStreams[name]; if (stream) { const tail = stream.trimEnd().slice(-4000); if (tail) pushLine(tail, C.dim); delete state.toolStreams[name]; } pushLine(" [done] " + name + " \u2192 " + sliceByWidth(sanitizeDisplay(text.split("\n")[0]), 100), C.dim); },
|
|
345
|
+
onToolOutput: (name, output, error) => { state.toolStreams[name] = (state.toolStreams[name] ?? "") + (error ? "Error: " + error : (output || "")); scheduleRender(); },
|
|
346
|
+
onPermissionRequest: (tool, args) => askPermission(tool.name || tool, args),
|
|
347
|
+
onQuestion: async (q) => askQuestion(q),
|
|
348
|
+
onCompress: () => pushLine(" [context] Compressed (history truncated)", C.warn),
|
|
349
|
+
onUsage: u => { state.tokens.prompt += u.prompt_tokens ?? 0; state.tokens.completion += u.completion_tokens ?? 0; },
|
|
350
|
+
onTaskUpdate: items => { state.tasks = items || []; const done = items.filter(i => i.status === "done").length; const cur = items.find(i => i.status === "in_progress"); pushLine(" [task] " + done + "/" + items.length + (cur ? " \u25b6 " + cur.title : ""), C.dim); render(); },
|
|
351
|
+
onTurnEnd: (() => { let n = 0; return () => { if (++n % 5 === 0) { try { saveSession(agent, state.lines); } catch {} } }; })(),
|
|
352
|
+
};
|
|
353
|
+
for (let resume = false; ; resume = true) {
|
|
354
|
+
try { await runAgent(agent, text, callbacks, { signal: state.controller.signal, resume }); flushStream(); break; }
|
|
355
|
+
catch (error) { flushStream();
|
|
356
|
+
if (error.name === "AbortError" || state.controller?.signal.aborted) { pushLine("[Aborted]", C.warn); break; }
|
|
357
|
+
if (error instanceof ContinueError) { pushLabel("\u276f Continue", ansi.bold + C.warn); pushLine("Ran " + error.turn + " turns (limit " + error.turn + "). Continue?", C.warn); const willContinue = await new Promise(resolve => { state.permission = { name: "continue", args: { turns: error.turn }, resolve }; state.status = "Continue after " + error.turn + " turns?"; render(); }); state.permission = null; if (!willContinue) { pushLine("[Cancelled]", C.warn); break; } pushLine("[Continuing\u2026]", C.tool); state.controller = new AbortController(); continue; }
|
|
358
|
+
pushLine("[error] " + error.message, C.error); break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
clearInterval(ticker); state.processing = false; state.controller = null; state.status = "Ready";
|
|
362
|
+
if (state.tasks.length > 0 && state.tasks.every(t => t.status === "done")) state.tasks = [];
|
|
363
|
+
try { saveSession(agent, state.lines); } catch {} render();
|
|
364
|
+
}
|
|
365
|
+
function flushStream() { if (state.reasoning) { pushLine(state.reasoning, C.reason); state.reasoning = ""; } if (state.streaming) { pushLine(state.streaming, C.text); state.streaming = ""; } }
|
|
366
|
+
function askPermission(name, args) { if (agent.autoApprove) { pushLine(" [auto] " + name + " " + summarize(args), C.warn); return Promise.resolve(true); } state.permissionPreview = formatPermission(name, args); return new Promise(resolve => { state.permission = { name, args, resolve }; state.status = "Waiting: " + name; render(); }); }
|
|
367
|
+
function askQuestion(text) { if (state.question) return Promise.resolve("(already waiting)"); pushLabel("\u276f Question", ansi.bold + C.tool); for (const line of text.split("\n")) pushLine(" " + line, C.text); return new Promise(resolve => { state.question = { text, options: [], resolve }; state.status = "Waiting..."; render(); }); }
|
|
368
|
+
|
|
369
|
+
async function handleSlash(text) {
|
|
370
|
+
const cmd = text.slice(1).split(/\s+/)[0].toLowerCase();
|
|
371
|
+
switch (cmd) {
|
|
372
|
+
case "help": for (const l of ["/help /plan /auto /model /session /clear /tasks /stats /new /distill /quit"]) pushLine(l, C.dim); break;
|
|
373
|
+
case "plan": agent.planMode = !agent.planMode; pushLine(" Plan mode " + (agent.planMode ? "ON" : "OFF"), C.tool); break;
|
|
374
|
+
case "auto": agent.autoApprove = !agent.autoApprove; pushLine(" Auto-approve " + (agent.autoApprove ? "ON" : "OFF"), agent.autoApprove ? C.warn : C.tool); break;
|
|
375
|
+
case "model": pushLine(" Model: " + agent.provider.model + " | Provider: " + (agent.provider.type || "?"), C.dim); break;
|
|
376
|
+
case "session": { const slots = listSlots(agent.cwd); pushLine("Sessions:", C.tool); if (slots.length === 0) pushLine(" (none)", C.dim); else for (const s of slots) pushLine(" " + s.label, C.dim); break; }
|
|
377
|
+
case "clear": archiveCurrent(agent.cwd); agent.history = []; state.lines = []; state.streaming = ""; state.reasoning = ""; state.tasks = []; state.scroll = 0; pushLine("Cleared (archived).", C.warn); break;
|
|
378
|
+
case "new": archiveCurrent(agent.cwd); agent.history = []; state.lines = []; state.streaming = ""; state.reasoning = ""; state.toolStreams = {}; state.tasks = []; state.scroll = 0; state.tokens = { prompt: 0, completion: 0 }; pushLine("New session.", C.tool); break;
|
|
379
|
+
case "tasks": if (state.tasks.length === 0) pushLine("No tasks.", C.dim); else for (const t of state.tasks) pushLine(" " + (t.status === "done" ? "\u2713" : t.status === "in_progress" ? "\u25b6" : "\u25cb") + " " + t.title, C.dim); break;
|
|
380
|
+
case "stats": pushLine("Tokens: \u2191" + state.tokens.prompt + " \u2193" + state.tokens.completion + " | History: " + agent.history.length + " msgs (~" + estimateTokens(agent.history) + " t) | Lines: " + state.lines.length, C.dim); break;
|
|
381
|
+
case "quit": case "exit": cleanup(); process.exit(0); return;
|
|
382
|
+
case "distill": {
|
|
383
|
+
if (!agent.memory) {
|
|
384
|
+
pushLine("[distill] 需要 memory 支持 (agent.memory 未初始化)", C.error);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
pushLine("[distill] 分析当前会话...", C.tool);
|
|
388
|
+
render();
|
|
389
|
+
try {
|
|
390
|
+
const { historyToTranscript, extractCandidates, saveCandidate } = await import("./distill.mjs");
|
|
391
|
+
const transcript = historyToTranscript(agent.history);
|
|
392
|
+
if (!transcript) { pushLine("[distill] 会话为空,没有可提取的内容", C.dim); break; }
|
|
393
|
+
const candidates = await extractCandidates(agent.provider, transcript, {});
|
|
394
|
+
if (candidates.length === 0) { pushLine("[distill] 本次会话没有值得沉淀的知识", C.dim); break; }
|
|
395
|
+
pushLine(`[distill] 发现 ${candidates.length} 条候选知识:`, C.tool);
|
|
396
|
+
render();
|
|
397
|
+
let saved = 0;
|
|
398
|
+
for (const c of candidates) {
|
|
399
|
+
pushLine(`\n [${c.type}] ${c.title}`, C.text);
|
|
400
|
+
pushLine(` ${c.content.slice(0, 200)}...`, C.dim);
|
|
401
|
+
const accept = await askPermission("distill-save", { title: c.title });
|
|
402
|
+
if (accept) {
|
|
403
|
+
const status = await saveCandidate(agent.memory, c, {});
|
|
404
|
+
pushLine(` ✓ ${status}`, C.tool);
|
|
405
|
+
saved++;
|
|
406
|
+
} else {
|
|
407
|
+
pushLine(" ✗ skipped", C.dim);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
pushLine(`[distill] 完成:入库 ${saved}/${candidates.length} 条`, C.tool);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
pushLine(`[distill] 失败: ${err.message}`, C.error);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
default: pushLine("Unknown: /" + cmd + " (try /help)", C.error);
|
|
417
|
+
}
|
|
418
|
+
render();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const restored = opts.restored;
|
|
422
|
+
if (restored && restored.displayLines) {
|
|
423
|
+
for (const raw of restored.displayLines) {
|
|
424
|
+
if (typeof raw === "string") state.lines.push({ text: raw, color: C.text });
|
|
425
|
+
else if (raw && typeof raw.text === "string") state.lines.push(raw);
|
|
426
|
+
}
|
|
427
|
+
if (restored.tasks) state.tasks = restored.tasks;
|
|
428
|
+
if (restored.planMode !== undefined) agent.planMode = restored.planMode;
|
|
429
|
+
state.status = "Session restored";
|
|
430
|
+
} else {
|
|
431
|
+
pushLine("", C.dim);
|
|
432
|
+
pushLine("EasyCode TUI \u2014 " + (agent.provider.model || ""), ansi.bold + C.tool);
|
|
433
|
+
pushLine("Type /help for commands, Ctrl+C to quit.", C.dim);
|
|
434
|
+
pushLine("", C.dim);
|
|
435
|
+
}
|
|
436
|
+
render();
|
|
437
|
+
}
|