mocode-ai 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.js +5 -1
- package/dist/repl/index.js +28 -10
- package/dist/ui/prompt.js +136 -7
- package/dist/ui/theme.js +2 -0
- package/package.json +1 -1
package/dist/agent/index.js
CHANGED
|
@@ -31,7 +31,9 @@ function parseArgs(raw) {
|
|
|
31
31
|
* 思考段用 layout.beginSegment / eraseSegmentBack 精确按物理行折叠(修预存按 \n 数行漏擦 bug),
|
|
32
32
|
* 原文存入 collapsedThinkings 供 /think N 重打。
|
|
33
33
|
*/
|
|
34
|
-
export async function runAgent(history, userInput, collapsedThinkings = [], signal
|
|
34
|
+
export async function runAgent(history, userInput, collapsedThinkings = [], signal,
|
|
35
|
+
/** 每步 chat() 返回后回调:repl 据此重算并重画状态行 context 用量条(运行中实时刷新,不冻结在轮首)。 */
|
|
36
|
+
onContextUpdate) {
|
|
35
37
|
// 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
36
38
|
// 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
|
|
37
39
|
const savedHistory = history.slice();
|
|
@@ -138,6 +140,8 @@ export async function runAgent(history, userInput, collapsedThinkings = [], sign
|
|
|
138
140
|
}
|
|
139
141
|
contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
140
142
|
spinner.stop();
|
|
143
|
+
// lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
|
|
144
|
+
onContextUpdate?.();
|
|
141
145
|
if (result.toolCalls.length > 0) {
|
|
142
146
|
// 思考后直接进入 tool_call(无 text):先把可见的思考段折叠
|
|
143
147
|
flushThinkCollapsed();
|
package/dist/repl/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { stdin, stdout } from 'node:process';
|
|
|
4
4
|
import { config } from '../config/index.js';
|
|
5
5
|
import { runAgent } from '../agent/index.js';
|
|
6
6
|
import { ui } from '../ui/theme.js';
|
|
7
|
-
import { bannerString, displayWidth, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
|
|
7
|
+
import { bannerString, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
|
|
8
8
|
import * as layout from '../ui/layout.js';
|
|
9
9
|
import { promptWithSlashMenu, promptTurnPicker } from '../ui/prompt.js';
|
|
10
10
|
import { tools } from '../tools/registry.js';
|
|
@@ -173,14 +173,30 @@ function stopRunningListener() {
|
|
|
173
173
|
emitter.off('keypress', onRunningKey);
|
|
174
174
|
currentAbort = null;
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 把用户消息格式化为带满宽背景色的文本(上滑时易辨认用户消息)。
|
|
178
|
+
* 每行用 padEndDisplay 填充到终端宽度(含 ❯ / 缩进),背景色 SGR 包裹整行 + 行末 reset。
|
|
179
|
+
* 满宽 pad 使终端背景色覆盖整行(含行尾空单元格),上滑滚动时用户消息呈连续色块、与 assistant 正文区分。
|
|
180
|
+
*/
|
|
181
|
+
function formatUserMessage(lines) {
|
|
182
|
+
const cols = layout.getGeo().cols;
|
|
183
|
+
const promptW = displayWidth(PROMPT);
|
|
184
|
+
const indent = ' '.repeat(promptW);
|
|
185
|
+
const { userBg, reset } = ui;
|
|
186
|
+
return (lines
|
|
187
|
+
.map((l, i) => {
|
|
188
|
+
const prefix = i === 0 ? PROMPT : indent;
|
|
189
|
+
const full = prefix + l;
|
|
190
|
+
const padded = padEndDisplay(full, cols);
|
|
191
|
+
return `${userBg}${padded}${reset}`;
|
|
192
|
+
})
|
|
193
|
+
.join('\n') + '\n');
|
|
194
|
+
}
|
|
176
195
|
/** 把多行提交输入回显进内容区(❯ 首行,续行按 prompt 宽度缩进)。仅 TUI 态回显(非 TTY 由 readline 自带回显)。 */
|
|
177
196
|
function echoInput(lines) {
|
|
178
197
|
if (!layout.isActive())
|
|
179
198
|
return;
|
|
180
|
-
|
|
181
|
-
const echo = lines.map((l, i) => (i === 0 ? `${PROMPT}${l}` : `${indent}${l}`)).join('\n') +
|
|
182
|
-
'\n';
|
|
183
|
-
layout.contentWrite(echo);
|
|
199
|
+
layout.contentWrite(formatUserMessage(lines));
|
|
184
200
|
}
|
|
185
201
|
/** 把任意消息 content 拍平成字符串(OpenAI 可能 string / null / 多模态数组)。 */
|
|
186
202
|
function textOf(c) {
|
|
@@ -202,16 +218,13 @@ function textOf(c) {
|
|
|
202
218
|
* 内容长于屏时 viewport 显尾(最近轮次),PgUp 可看更早——与流式态一致。
|
|
203
219
|
*/
|
|
204
220
|
export function renderHistory(history) {
|
|
205
|
-
const indent = ' '.repeat(displayWidth(PROMPT));
|
|
206
221
|
const idToName = new Map();
|
|
207
222
|
for (const m of history) {
|
|
208
223
|
if (m.role === 'system')
|
|
209
224
|
continue;
|
|
210
225
|
if (m.role === 'user') {
|
|
211
226
|
const lines = textOf(m.content).split('\n');
|
|
212
|
-
layout.contentWrite(lines
|
|
213
|
-
.map((l, i) => (i === 0 ? `${PROMPT}${l}` : `${indent}${l}`))
|
|
214
|
-
.join('\n') + '\n');
|
|
227
|
+
layout.contentWrite(formatUserMessage(lines));
|
|
215
228
|
continue;
|
|
216
229
|
}
|
|
217
230
|
if (m.role === 'assistant') {
|
|
@@ -510,7 +523,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null)
|
|
|
510
523
|
}
|
|
511
524
|
try {
|
|
512
525
|
const signal = startRunningListener(placeholder);
|
|
513
|
-
|
|
526
|
+
// 运行中每步 chat() 返回后刷新状态行 context 用量条(用 fresh lastUsage / 估算),
|
|
527
|
+
// 否则整轮冻结在轮首 refreshStatusBase 的值,「执行 grep」时 2k/1000k 不动。
|
|
528
|
+
await runAgent(history, joined, collapsedThinkings, signal, () => {
|
|
529
|
+
refreshStatusBase(history);
|
|
530
|
+
layout.drawStatusBar();
|
|
531
|
+
});
|
|
514
532
|
// 成功轮次自动落盘(崩溃也保住上一轮);新会话首轮分配 id
|
|
515
533
|
if (!currentSessionId)
|
|
516
534
|
currentSessionId = newSessionId();
|
package/dist/ui/prompt.js
CHANGED
|
@@ -3,6 +3,40 @@ import { stdin, stdout } from 'node:process';
|
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import { displayWidth, padEndDisplay, truncateDisplay } from './render.js';
|
|
5
5
|
import * as layout from './layout.js';
|
|
6
|
+
// ── 粘贴检测(块级 + 时间窗)──
|
|
7
|
+
// 块级:多字节大块(len>8)或含 CR/LF 的小块 = 粘贴(键盘单键 1 字节,Enter=\r 单字节)。
|
|
8
|
+
// 时间窗:每块重置 50ms 计时器,静默 50ms 即"粘贴结束"→ onPasteEnd。跨多块的大粘贴(块间 <50ms)累积进
|
|
9
|
+
// 同一个 pasteParts、末尾一次性落 chip——避免"首块成 chip、后续块泄成文本"。粘贴中 onKey 把键累积进
|
|
10
|
+
// pasteParts(不编辑 lines)。不启用 bracketed paste——emitKeypressEvents 会把 \x1B[200~ 标记当按键砸进输入框。
|
|
11
|
+
let pasting = false;
|
|
12
|
+
let pasteParts = [];
|
|
13
|
+
let pasteTimer = null;
|
|
14
|
+
let onPasteEnd = null; // 粘贴结束回调(prompt 注入:落 chip 或保留为文本)
|
|
15
|
+
let pasteDetectorInstalled = false;
|
|
16
|
+
function ensurePasteDetector() {
|
|
17
|
+
if (pasteDetectorInstalled)
|
|
18
|
+
return;
|
|
19
|
+
pasteDetectorInstalled = true;
|
|
20
|
+
stdin.on('data', (chunk) => {
|
|
21
|
+
const len = chunk.length;
|
|
22
|
+
const hasNL = typeof chunk === 'string'
|
|
23
|
+
? chunk.indexOf('\r') >= 0 || chunk.indexOf('\n') >= 0
|
|
24
|
+
: chunk.indexOf(0x0d) >= 0 || chunk.indexOf(0x0a) >= 0;
|
|
25
|
+
// 大块(>8 字节)或含换行的多字节块 = 粘贴;键盘单键(1 字节,含 Enter=\r)不触发
|
|
26
|
+
if (!(len > 8 || (len > 1 && hasNL)))
|
|
27
|
+
return;
|
|
28
|
+
pasting = true;
|
|
29
|
+
if (pasteTimer)
|
|
30
|
+
clearTimeout(pasteTimer);
|
|
31
|
+
const t = setTimeout(() => {
|
|
32
|
+
pasteTimer = null;
|
|
33
|
+
pasting = false;
|
|
34
|
+
onPasteEnd?.();
|
|
35
|
+
}, 50);
|
|
36
|
+
t.unref();
|
|
37
|
+
pasteTimer = t;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
6
40
|
/** 非 TTY / 未进 alt screen 时退化为普通 readline 行输入(无菜单、单行)。 */
|
|
7
41
|
function questionFallback(prompt) {
|
|
8
42
|
return new Promise((res, rej) => {
|
|
@@ -38,6 +72,8 @@ export async function promptWithSlashMenu(opts) {
|
|
|
38
72
|
: [''];
|
|
39
73
|
let cl = lines.length - 1; // 光标行(0-based)= 末行
|
|
40
74
|
let cc = lines[cl].length; // 光标在该行的字符索引 = 末行末尾
|
|
75
|
+
let justSawCR = false; // \r\n 合一:粘贴的 \r 折行后,紧跟的 \n 吞掉(避免折两行)
|
|
76
|
+
let chip = null; // 原子粘贴块:整段封进 [预览…],不可编辑;提交时拼回全文
|
|
41
77
|
let menuOpen = false;
|
|
42
78
|
let selected = 0;
|
|
43
79
|
let filtered = [];
|
|
@@ -62,6 +98,58 @@ export async function promptWithSlashMenu(opts) {
|
|
|
62
98
|
function cursorCol() {
|
|
63
99
|
return displayWidth(lines[cl].slice(0, cc));
|
|
64
100
|
}
|
|
101
|
+
/** chip 预览前缀:整段扁平化(行界→空格,避免框内折行)取前 ~20 列,超长 truncateDisplay 自带 …;末尾空格与 suffix 分隔。 */
|
|
102
|
+
function chipPrefix() {
|
|
103
|
+
return chip ? `[${truncateDisplay(chip.split('\n').join(' '), 20)}] ` : '';
|
|
104
|
+
}
|
|
105
|
+
/** 供 layout 画的行:chip 存在时把前缀拼到第 0 行前(chip 原子显示,不可编辑;光标活在 suffix)。 */
|
|
106
|
+
function dispLines() {
|
|
107
|
+
if (!chip)
|
|
108
|
+
return lines;
|
|
109
|
+
const pre = chipPrefix();
|
|
110
|
+
return lines.length > 0 ? [pre + lines[0], ...lines.slice(1)] : [pre];
|
|
111
|
+
}
|
|
112
|
+
/** 供 layout 定位光标列:chip 在第 0 行时偏移 chipPrefix 宽度(suffix 光标始终在 chip 之后)。 */
|
|
113
|
+
function dispCursorCol() {
|
|
114
|
+
return chip && cl === 0 ? displayWidth(chipPrefix()) + cursorCol() : cursorCol();
|
|
115
|
+
}
|
|
116
|
+
/** 在光标处插入文本(含换行则拆行)。供短粘贴 finalize 落为可编辑文本。 */
|
|
117
|
+
function insertText(text) {
|
|
118
|
+
const parts = text.split('\n');
|
|
119
|
+
const before = lines[cl].slice(0, cc);
|
|
120
|
+
const after = lines[cl].slice(cc);
|
|
121
|
+
const newLines = [before + parts[0]];
|
|
122
|
+
for (let i = 1; i < parts.length; i++)
|
|
123
|
+
newLines.push(parts[i]);
|
|
124
|
+
newLines[newLines.length - 1] += after;
|
|
125
|
+
lines.splice(cl, 1, ...newLines);
|
|
126
|
+
cl = cl + parts.length - 1;
|
|
127
|
+
cc = parts[parts.length - 1].length;
|
|
128
|
+
}
|
|
129
|
+
/** 粘贴结束:长粘贴(>8 行或 >400 字符)落/并进 chip(原子,整段封预览),短粘贴落为可编辑文本。 */
|
|
130
|
+
function finalizePaste() {
|
|
131
|
+
if (resolved) {
|
|
132
|
+
pasteParts = [];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const buf = pasteParts.join('');
|
|
136
|
+
pasteParts = [];
|
|
137
|
+
justSawCR = false;
|
|
138
|
+
if (!buf)
|
|
139
|
+
return;
|
|
140
|
+
const isLong = buf.split('\n').length > 8 || buf.length > 400;
|
|
141
|
+
if (isLong) {
|
|
142
|
+
chip = chip == null ? buf : chip + '\n' + buf; // 多块粘贴:并进同一 chip
|
|
143
|
+
lines = [''];
|
|
144
|
+
cl = 0;
|
|
145
|
+
cc = 0;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
insertText(buf);
|
|
149
|
+
}
|
|
150
|
+
computeFiltered();
|
|
151
|
+
redraw();
|
|
152
|
+
}
|
|
65
153
|
function computeFiltered() {
|
|
66
154
|
if (cl === 0 && lines[0].startsWith('/')) {
|
|
67
155
|
filtered = opts.commands.filter((c) => c.name.startsWith(lines[0]));
|
|
@@ -77,9 +165,9 @@ export async function promptWithSlashMenu(opts) {
|
|
|
77
165
|
function redraw() {
|
|
78
166
|
layout.paintInput({
|
|
79
167
|
prompt: opts.prompt,
|
|
80
|
-
lines,
|
|
168
|
+
lines: dispLines(),
|
|
81
169
|
cursorLine: cl,
|
|
82
|
-
cursorCol:
|
|
170
|
+
cursorCol: dispCursorCol(),
|
|
83
171
|
menu: menuLines().length ? { lines: menuLines() } : null,
|
|
84
172
|
});
|
|
85
173
|
}
|
|
@@ -91,6 +179,13 @@ export async function promptWithSlashMenu(opts) {
|
|
|
91
179
|
// 忽略
|
|
92
180
|
}
|
|
93
181
|
emitter.removeListener('keypress', onKey);
|
|
182
|
+
if (pasteTimer) {
|
|
183
|
+
clearTimeout(pasteTimer);
|
|
184
|
+
pasteTimer = null;
|
|
185
|
+
}
|
|
186
|
+
pasting = false;
|
|
187
|
+
pasteParts = [];
|
|
188
|
+
onPasteEnd = null;
|
|
94
189
|
stdin.pause();
|
|
95
190
|
}
|
|
96
191
|
function finish(value) {
|
|
@@ -100,14 +195,16 @@ export async function promptWithSlashMenu(opts) {
|
|
|
100
195
|
cleanup();
|
|
101
196
|
resolve(value);
|
|
102
197
|
}
|
|
103
|
-
/** 提交:菜单打开时先补全选中项到第 0 行。 */
|
|
198
|
+
/** 提交:菜单打开时先补全选中项到第 0 行。chip 与 suffix 拼回全文(chip 在前,换行接 suffix)。 */
|
|
104
199
|
function submit() {
|
|
105
200
|
if (menuOpen && filtered[selected]) {
|
|
106
201
|
lines = [filtered[selected].name];
|
|
107
202
|
cl = 0;
|
|
108
203
|
cc = lines[0].length;
|
|
109
204
|
}
|
|
110
|
-
|
|
205
|
+
const suffix = lines.join('\n');
|
|
206
|
+
const content = chip ? (suffix.length > 0 ? chip + '\n' + suffix : chip) : suffix;
|
|
207
|
+
finish(content === '' ? [''] : content.split('\n'));
|
|
111
208
|
}
|
|
112
209
|
/** 插换行:在光标处断行。 */
|
|
113
210
|
function insertNewline() {
|
|
@@ -122,6 +219,29 @@ export async function promptWithSlashMenu(opts) {
|
|
|
122
219
|
function onKey(_str, key) {
|
|
123
220
|
if (resolved || !key)
|
|
124
221
|
return;
|
|
222
|
+
// 粘贴中:把键累积进 pasteParts(换行 \r\n 合一),不编辑 lines;末尾 finalizePaste 统一落 chip/文本
|
|
223
|
+
if (pasting) {
|
|
224
|
+
if (key.ctrl && key.name === 'c') {
|
|
225
|
+
cleanup();
|
|
226
|
+
reject(new Error('SIGINT'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const s = key.sequence ?? '';
|
|
230
|
+
const isReturn = key.name === 'return' || key.name === 'enter';
|
|
231
|
+
if (s === '\n' && justSawCR) {
|
|
232
|
+
justSawCR = false; // \r\n 的 \n:已随 \r 折行,吞掉
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isReturn || s === '\r' || s === '\n') {
|
|
236
|
+
justSawCR = s === '\r'; // \r 标记,待可能的尾随 \n
|
|
237
|
+
pasteParts.push('\n');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
justSawCR = false;
|
|
241
|
+
if (s && s >= ' ' && !key.ctrl && !key.meta)
|
|
242
|
+
pasteParts.push(s);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
125
245
|
// 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,Ctrl+↑↓ 与 plain ↑/↓ 单行。
|
|
126
246
|
// plain ↑/↓ 仅在单行输入且菜单关闭时作滚动(多行编辑留给光标移动,菜单打开留给选项);
|
|
127
247
|
// 兼鼠标滚轮——WT alt 屏(经 \x1B[?1007h)滚轮转发 ↑/↓。
|
|
@@ -171,17 +291,17 @@ export async function promptWithSlashMenu(opts) {
|
|
|
171
291
|
return;
|
|
172
292
|
}
|
|
173
293
|
const isReturn = key.name === 'return' || key.name === 'enter';
|
|
174
|
-
// 换行:Ctrl+J / Alt+Enter(meta)/ Shift+Enter(终端区分时)/
|
|
294
|
+
// 换行:Ctrl+J / Alt+Enter(meta)/ Shift+Enter(终端区分时)/ lone LF
|
|
295
|
+
// (粘贴的 CR/LF 已在上方 pasting 分支累积进 pasteParts,不会到此)
|
|
175
296
|
const wantNewline = (key.ctrl && key.name === 'j') ||
|
|
176
297
|
(key.meta && isReturn) ||
|
|
177
298
|
(key.shift && isReturn) ||
|
|
178
299
|
(key.sequence === '\n' && !key.ctrl);
|
|
179
|
-
// 换行(Ctrl+J / Alt+Enter / Shift+Enter / 粘贴 LF)
|
|
180
300
|
if (wantNewline) {
|
|
181
301
|
insertNewline();
|
|
182
302
|
return;
|
|
183
303
|
}
|
|
184
|
-
// 提交(plain Enter)
|
|
304
|
+
// 提交(键盘 plain Enter)
|
|
185
305
|
if (isReturn && !key.shift && !key.meta && !key.ctrl) {
|
|
186
306
|
submit();
|
|
187
307
|
return;
|
|
@@ -204,6 +324,12 @@ export async function promptWithSlashMenu(opts) {
|
|
|
204
324
|
computeFiltered();
|
|
205
325
|
redraw();
|
|
206
326
|
}
|
|
327
|
+
else if (chip) {
|
|
328
|
+
// 光标在 suffix 开头(紧贴 ] 后):退格删整个 chip(原子)
|
|
329
|
+
chip = null;
|
|
330
|
+
computeFiltered();
|
|
331
|
+
redraw();
|
|
332
|
+
}
|
|
207
333
|
return;
|
|
208
334
|
case 'up':
|
|
209
335
|
if (menuOpen && filtered.length) {
|
|
@@ -284,6 +410,8 @@ export async function promptWithSlashMenu(opts) {
|
|
|
284
410
|
return new Promise((res, rej) => {
|
|
285
411
|
resolve = res;
|
|
286
412
|
reject = rej;
|
|
413
|
+
ensurePasteDetector(); // 首次调用在 emitKeypressEvents 之前装 data 监听器(保序:mine 先于 解析器)
|
|
414
|
+
onPasteEnd = finalizePaste; // 粘贴结束回调:落 chip 或保留文本
|
|
287
415
|
readline.emitKeypressEvents(stdin);
|
|
288
416
|
let rawOk = true;
|
|
289
417
|
try {
|
|
@@ -395,6 +523,7 @@ export async function promptTurnPicker(items) {
|
|
|
395
523
|
return new Promise((res, rej) => {
|
|
396
524
|
resolve = res;
|
|
397
525
|
reject = rej;
|
|
526
|
+
ensurePasteDetector();
|
|
398
527
|
readline.emitKeypressEvents(stdin);
|
|
399
528
|
let rawOk = true;
|
|
400
529
|
try {
|
package/dist/ui/theme.js
CHANGED