mocode-ai 0.1.4 → 0.1.6
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 +196 -188
- package/dist/agent/core.js +309 -0
- package/dist/agent/index.js +62 -249
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +81 -36
- package/dist/context/classifier.js +83 -0
- package/dist/context/encoders/index.js +10 -0
- package/dist/context/encoders/passthrough.js +23 -0
- package/dist/context/index.js +10 -0
- package/dist/context/pipeline.js +84 -0
- package/dist/context/registry.js +30 -0
- package/dist/context/types.js +12 -0
- package/dist/index.js +12 -2
- package/dist/llm/index.js +11 -9
- package/dist/memory/index.js +2 -2
- package/dist/memory/reflect.js +11 -11
- package/dist/memory/store.js +4 -4
- package/dist/repl/index.js +269 -109
- package/dist/sandbox/command.js +41 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/jail.js +75 -0
- package/dist/sandbox/policy.js +58 -0
- package/dist/sandbox/root.js +19 -0
- package/dist/session/compact.js +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +7 -7
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +10 -5
- package/dist/tools/builtins/grep.js +14 -9
- package/dist/tools/builtins/index.js +7 -1
- package/dist/tools/builtins/memory-forget.js +2 -2
- package/dist/tools/builtins/memory-list.js +2 -2
- package/dist/tools/builtins/memory-save.js +7 -7
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/memory-update.js +6 -6
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +47 -8
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +16 -0
- package/dist/tools/registry.js +18 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +24 -3
- package/dist/ui/layout.js +295 -81
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +259 -5
- package/dist/ui/render.js +1 -1
- package/dist/ui/theme.js +146 -13
- package/package.json +2 -2
package/dist/ui/layout.js
CHANGED
|
@@ -2,31 +2,49 @@ import { stdin, stdout } from 'node:process';
|
|
|
2
2
|
import { charWidth, displayWidth, truncateDisplay, ansiDisplayWidth, wrapByDisplayWidth, fmtElapsed, } from './render.js';
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import * as content from './content.js';
|
|
5
|
+
import * as mouse from './mouse.js';
|
|
6
|
+
import { renderMarkdown } from './markdown.js';
|
|
5
7
|
// ── 内部状态 ──
|
|
6
8
|
let active = false;
|
|
7
9
|
let mode = 'input';
|
|
8
10
|
let footerH = 4; // 1 状态行 + 1 上线 + 输入行数 + 1 下线(上下线框住输入区)
|
|
9
11
|
let contentRow = 1; // 续写位行(1-based,屏坐标,[1,contentBottom])
|
|
10
12
|
let contentCol = 1; // 续写位列(1-based)
|
|
11
|
-
let segmentStartRow = 1; //
|
|
13
|
+
let segmentStartRow = 1; // 当前 md 段起始屏行(供 contentWriteMd 定位段末续写位;段内行数由 content 段标记跟踪)
|
|
12
14
|
let scrollOffset = 0; // 滚动回看距尾行数(0=尾,跟随新内容);>0 时 viewport 显历史、状态行显滚动指示
|
|
13
15
|
let base = null;
|
|
14
16
|
let statusText = '';
|
|
15
17
|
let spinnerFrame;
|
|
16
18
|
let turnStart = null; // RUNNING 态起点(Date.now());INPUT 态为 null。composeStatus 据此拼走时。
|
|
17
19
|
let turnTimer = null; // 走时刷新计时器(独立于 spinner):流式期间 spinner 停转,由它续刷状态行。
|
|
20
|
+
// 运行态状态行 chip 心跳帧(♥/♡ 明灭)。turnTimer 每 tick 推进一帧,让状态行前导符在 agent
|
|
21
|
+
// 运行时跳动——agent 的 spinner 走内容区续写位(paintLiveAtCursor),不调 setStatus,
|
|
22
|
+
// 故状态行 chip 靠 turnTimer 独立驱动。INPUT 态 runningFrame=-1,composeStatus 退回静态 ◆。
|
|
23
|
+
const RUNNING_FRAMES = ['♥', '♡'];
|
|
24
|
+
let runningFrame = -1;
|
|
25
|
+
// 运行态用户打字时暂停流式物理写:流式每个 token 要 cup 到 contentRow 写入,IME 候选窗逐光标移动跟踪会跟过去;
|
|
26
|
+
// 用户打字期间只喂缓冲、不物理写,光标留输入框;停手 USER_ACTIVE_PAUSE_MS 后 flush 重画缓冲内容。
|
|
27
|
+
let userActiveUntil = 0; // 打字活跃截止时刻(Date.now()+PAUSE);0=未活跃
|
|
28
|
+
let flushTimer = null; // 用户停手后 flush 缓冲内容(repaintViewport)
|
|
29
|
+
const USER_ACTIVE_PAUSE_MS = 1500;
|
|
18
30
|
let lastView = null;
|
|
19
31
|
let lastMenuStartRow = 0; // 上次菜单起始屏行(供擦除)
|
|
20
32
|
let lastMenuRows = 0;
|
|
21
33
|
let resizeTimer = null;
|
|
22
34
|
let exitHandler = null;
|
|
23
35
|
let sigwinchHandler = null;
|
|
36
|
+
// markdown 流式段:agent onText 的 chunk 累积到 mdBuf,每 chunk 把整段经 renderMarkdown
|
|
37
|
+
// 渲成自洽行,replace 缓冲段(content.setLines)+ repaintViewport 重画。mdActive 期间任何
|
|
38
|
+
// 非 md 写(contentWrite)先 commitMd 收尾(清 segMark,后续写不再被 setLines 截断)。
|
|
39
|
+
let mdActive = false;
|
|
40
|
+
let mdBuf = '';
|
|
24
41
|
const esc = {
|
|
25
42
|
altOn: '\x1B[?1049h',
|
|
26
43
|
altOff: '\x1B[?1049l',
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
mouseOn: '\x1B[?1000h\x1B[?1006h', // SGR 鼠标:按键事件追踪(1000)+SGR 编码(1006)→ \x1B[<btn;col;rowM,经 mouse.consumeMouse 重组后滚轮滚动。副作用:TUI 内鼠标选区复制失效,按 Shift 用鼠标可让终端放行做原生选区
|
|
45
|
+
mouseOff: '\x1B[?1006l\x1B[?1000l', // 关:反序(先关 SGR 编码再关追踪)
|
|
29
46
|
cursorShow: '\x1B[?25h',
|
|
47
|
+
cursorHide: '\x1B[?25l',
|
|
30
48
|
clearLine: '\x1B[2K',
|
|
31
49
|
home: '\x1B[H',
|
|
32
50
|
};
|
|
@@ -68,11 +86,55 @@ export function setRegion(fh) {
|
|
|
68
86
|
contentRow = g.contentBottom; // 底栏撑高挤掉内容:钳到新区底
|
|
69
87
|
return g;
|
|
70
88
|
}
|
|
89
|
+
/** 运行态真光标(隐藏)的归位点 = 输入框光标位:行 = 动态 contentBottom+3(resize 安全),列对齐 renderDimInputRow 的假光标(空→3,有字→❯+截断文本宽+1)。供 IME 锚定。 */
|
|
90
|
+
function runningCaretPos() {
|
|
91
|
+
const g = getGeo();
|
|
92
|
+
const text = lastView?.dim ? lastView.lines[0] ?? '' : '';
|
|
93
|
+
const contentW = Math.max(0, g.cols - 3); // ❯ =2 + 光标=1
|
|
94
|
+
const w = displayWidth(truncateDisplay(text, contentW));
|
|
95
|
+
return { row: g.contentBottom + 3, col: Math.min(g.cols, 2 + w + 1) };
|
|
96
|
+
}
|
|
71
97
|
export function contentMode() {
|
|
72
98
|
if (!active)
|
|
73
99
|
return;
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
const g = getGeo();
|
|
101
|
+
if (mode === 'running' && scrollOffset === 0) {
|
|
102
|
+
// 运行态:真光标归输入框光标位(供 IME 锚定,气泡不跟流式跑);假光标做可见指示
|
|
103
|
+
const p = runningCaretPos();
|
|
104
|
+
stdout.write(cup(p.row, p.col));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// INPUT 态 / 滚动回看:归续写位(滚动回看时归内容区底,viewport 锁历史)
|
|
108
|
+
stdout.write(cup(scrollOffset === 0 ? contentRow : g.contentBottom, scrollOffset === 0 ? contentCol : 1));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** 运行态用户是否在打字(近期有按键)——是则暂停流式物理写(contentWrite/drawStatusBar/paintLiveAtCursor 跳过物理写),光标留输入框,IME 候选窗稳定不跟流式跑。 */
|
|
112
|
+
export function isStreamingPaused() {
|
|
113
|
+
return mode === 'running' && Date.now() < userActiveUntil;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 运行态用户打字时调(repl onRunningKey 每次按键):标记活跃 + 重置 flush 定时器。
|
|
117
|
+
* 活跃期间流式只喂缓冲不物理写(光标不入内容区);用户停手 USER_ACTIVE_PAUSE_MS 后 flush:
|
|
118
|
+
* repaintViewport 重画缓冲内容 + drawStatusBar 刷状态行 + 光标归输入框。
|
|
119
|
+
* 解决 IME 候选窗跟流式输出跑:流式写必须 cup 到 contentRow,IME 逐光标跟踪→气泡跟跑;打字时暂停写则光标不动。
|
|
120
|
+
*/
|
|
121
|
+
export function setUserActive() {
|
|
122
|
+
if (!active || mode !== 'running')
|
|
123
|
+
return;
|
|
124
|
+
userActiveUntil = Date.now() + USER_ACTIVE_PAUSE_MS;
|
|
125
|
+
if (flushTimer)
|
|
126
|
+
clearTimeout(flushTimer);
|
|
127
|
+
flushTimer = setTimeout(() => {
|
|
128
|
+
flushTimer = null;
|
|
129
|
+
userActiveUntil = 0;
|
|
130
|
+
if (active && mode === 'running' && scrollOffset === 0) {
|
|
131
|
+
repaintViewport(); // 重画内容区(显示活跃期间缓冲的流式内容)
|
|
132
|
+
drawStatusBar(); // 刷状态行(活跃期间冻住,现恢复)
|
|
133
|
+
const p = runningCaretPos();
|
|
134
|
+
stdout.write(cup(p.row, p.col));
|
|
135
|
+
}
|
|
136
|
+
}, USER_ACTIVE_PAUSE_MS);
|
|
137
|
+
flushTimer.unref();
|
|
76
138
|
}
|
|
77
139
|
/**
|
|
78
140
|
* 内容区正文写:CUP 到续写位 + 写出 + 按字符宽度/折行更新续写位 + 同步入 content 缓冲(供滚动回看)。
|
|
@@ -87,14 +149,22 @@ export function contentWrite(s) {
|
|
|
87
149
|
stdout.write(s);
|
|
88
150
|
return;
|
|
89
151
|
}
|
|
152
|
+
if (mdActive)
|
|
153
|
+
commitMd(); // 非 md 写接续:先收尾 md 段(清 segMark,否则 setLines 会截到旧段)
|
|
90
154
|
// 滚动回看时(scrollOffset>0)只喂缓冲 + 推进续写位,不物理写——否则新流式内容覆盖 viewport 历史行。
|
|
91
|
-
// 回尾(scrollOffset===0)
|
|
92
|
-
|
|
93
|
-
|
|
155
|
+
// 回尾(scrollOffset===0)才物理写;写起点 = 当前续写位(loop 会推进续写位,故先捕获)。
|
|
156
|
+
const startRow = contentRow;
|
|
157
|
+
const startCol = contentCol;
|
|
94
158
|
const g = getGeo();
|
|
95
159
|
const cols = g.cols;
|
|
96
160
|
const bottom = g.contentBottom;
|
|
97
161
|
const advanceRow = (r) => (r >= bottom ? bottom : r + 1); // 到底则滚动,续写位留底
|
|
162
|
+
// 滚动回看冻结(仿 ink store.pushBlock 的 offset+=newLines):scrollOffset>0 时新内容只入缓冲,
|
|
163
|
+
// 但缓冲增长后若 offset 不变,下次 scrollBy→repaintViewport 会取漂移窗口、把新内容画进历史视图
|
|
164
|
+
// (「工具消息跳到上面覆盖」bug;滚回底又正常,因 buffer 正确)。故入缓冲前后算 totalRows 差,
|
|
165
|
+
// offset += 差以冻住视图(窗口停原绝对行,无需 repaint;滚回底自显尾部新内容)。
|
|
166
|
+
const scrolled = scrollOffset > 0;
|
|
167
|
+
const totalBefore = scrolled ? content.totalRows() : 0;
|
|
98
168
|
let i = 0;
|
|
99
169
|
while (i < s.length) {
|
|
100
170
|
const rest = s.slice(i);
|
|
@@ -161,58 +231,117 @@ export function contentWrite(s) {
|
|
|
161
231
|
content.feedChar(ch);
|
|
162
232
|
i += ch.length;
|
|
163
233
|
}
|
|
234
|
+
// 跨调用 pending-wrap 提交:循环末若 contentCol>cols(末字符恰好落在最后一列,终端 pending-wrap),
|
|
235
|
+
// 必须在此提交换行——否则下一 chunk 的 cup(startRow, cols+1) 被终端 clamp 到 col=cols 且 CUP 清除
|
|
236
|
+
// pending,下一可打印字符会覆盖上一行末字符(「新输出插到上文中间」bug);paintLiveAtCursor 也会
|
|
237
|
+
// cup 到 cols+1 并 \x1B[2K 擦掉整行。提交后模拟与终端都停在 (下一行,1)、无 pending,等价 ink 的
|
|
238
|
+
// 「新输出永远在底」。代价:chunk 恰在末列结束且下一 chunk 以 \n 开头时多一个空行(cosmetic,远好于覆盖)。
|
|
239
|
+
// 模拟归一化在物理写 guard 之外(滚动态也要保 buffer 自洽);物理补的 \n 在 guard 之内(滚动态/暂停态不物理写)。
|
|
240
|
+
const pendingWrap = contentCol > cols;
|
|
241
|
+
if (pendingWrap) {
|
|
242
|
+
content.breakRow();
|
|
243
|
+
contentRow = advanceRow(contentRow);
|
|
244
|
+
contentCol = 1;
|
|
245
|
+
}
|
|
246
|
+
// 滚动回看冻结:新内容入缓冲后 offset += delta(contentWrite 只增故 ≥0;md 路径可负),钳到 [0, maxOff],
|
|
247
|
+
// viewport 窗口停原绝对行(不漂移到新工具消息)。无需 repaint——窗口未变(屏仍显原历史)。
|
|
248
|
+
if (scrolled) {
|
|
249
|
+
const delta = content.totalRows() - totalBefore;
|
|
250
|
+
if (delta !== 0) {
|
|
251
|
+
const maxOff = Math.max(0, content.totalRows() - g.contentBottom);
|
|
252
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, maxOff));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 物理写(回尾 offset=0 且未暂停):cup 写起点 + s + (pending 时补 \n 提交换行)+ (运行态 cup 回输入框)。
|
|
256
|
+
// 用户打字中(isStreamingPaused)跳过物理写、只喂缓冲——光标不入内容区,IME 候选窗不跟流式跑;
|
|
257
|
+
// 停手后 setUserActive 的 flush 定时器 repaintViewport 重画缓冲内容。
|
|
258
|
+
if (scrollOffset === 0 && !isStreamingPaused()) {
|
|
259
|
+
let out = cup(startRow, startCol) + s;
|
|
260
|
+
if (pendingWrap)
|
|
261
|
+
out += '\n'; // 滚动区域底行触发 DECSTBM 上滚、中段 LF 下移到 (下一行,1)
|
|
262
|
+
if (mode === 'running') {
|
|
263
|
+
const p = runningCaretPos();
|
|
264
|
+
out += cup(p.row, p.col);
|
|
265
|
+
}
|
|
266
|
+
stdout.write(out);
|
|
267
|
+
}
|
|
164
268
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
269
|
+
/** 进入 markdown 流式段:标记段起点(layout 续写位 + content.beginSegment),清 accumulator。 */
|
|
270
|
+
function beginMdSegment() {
|
|
167
271
|
segmentStartRow = contentRow;
|
|
168
272
|
content.beginSegment();
|
|
273
|
+
mdBuf = '';
|
|
274
|
+
mdActive = true;
|
|
275
|
+
}
|
|
276
|
+
/** 提交 markdown 段:清 accumulator + content.commitSegment(后续非 md 写不再被 setLines 截断)。 */
|
|
277
|
+
function commitMd() {
|
|
278
|
+
if (!mdActive)
|
|
279
|
+
return;
|
|
280
|
+
mdActive = false;
|
|
281
|
+
mdBuf = '';
|
|
282
|
+
content.commitSegment();
|
|
169
283
|
}
|
|
170
284
|
/**
|
|
171
|
-
*
|
|
285
|
+
* markdown 正文写(替代 contentWrite 用于 agent onText):累积 chunk 到 mdBuf,每 chunk 把整段
|
|
286
|
+
* mdBuf 经 renderMarkdown 渲成自洽 ANSI 行,replace 缓冲段(content.setLines 截旧 + 写新),
|
|
287
|
+
* repaintViewport 重画(仅尾 offset=0 且未暂停)。流式安全:未闭合 fence 照常 emit 进行中代码块;
|
|
288
|
+
* renderMarkdown 内部 memo by text 使重复渲染命中缓存。非 TTY / 未激活:直接 stdout.write(与
|
|
289
|
+
* contentWrite 一致,管道流式可见)。
|
|
172
290
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* - 否则未滚动:从段起点 segmentStartRow 擦。
|
|
177
|
-
* 不越界到区域外(不擦底栏)。返回擦除的屏幕行数。
|
|
178
|
-
*
|
|
179
|
-
* **滚动回看态(scrollOffset>0)只删缓冲、不物理擦**:段在滚动态经 contentWrite 只喂缓冲、未物理写屏,
|
|
180
|
-
* 屏上无段可见,此时按屏行 \x1B[2K 物理擦会清掉用户正在看的历史视图(思考折叠把整屏擦白,而非在底部默默隐藏)。
|
|
181
|
-
* 故滚动态仅 content.eraseSegment() 删缓冲 + 复位续写位到段起点(使回尾后续写落在折叠标题位),不发任何
|
|
182
|
-
* cup/clearLine;回尾时 repaintViewport 从缓冲重画即显折叠标题。返回 0(未物理擦)。
|
|
291
|
+
* 续写位 = 段末下一行(段占 lines.length 行从 segmentStartRow 起);超可视区则滚动留 contentBottom。
|
|
292
|
+
* 物理重画用 repaintViewport(全内容区,原子一次 write 无闪烁)— md 段是缓冲尾,viewport 显尾即显段。
|
|
293
|
+
* 用户打字中(isStreamingPaused)或滚动回看(scrollOffset>0)只更新缓冲,停手 flush / 回尾时显。
|
|
183
294
|
*/
|
|
184
|
-
export function
|
|
185
|
-
if (!active)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const segRows = content.eraseSegment(); // 删缓冲段,返回段物理行数
|
|
189
|
-
if (segRows <= 0)
|
|
190
|
-
return 0;
|
|
191
|
-
if (scrollOffset > 0) {
|
|
192
|
-
// 滚动回看态:段未物理写屏,不物理擦(否则清穿历史视图);仅复位续写位到段起点
|
|
193
|
-
contentRow = segmentStartRow;
|
|
194
|
-
contentCol = 1;
|
|
195
|
-
return 0;
|
|
295
|
+
export function contentWriteMd(s) {
|
|
296
|
+
if (!active || !ui.isTTY) {
|
|
297
|
+
stdout.write(s);
|
|
298
|
+
return;
|
|
196
299
|
}
|
|
300
|
+
if (!mdActive)
|
|
301
|
+
beginMdSegment();
|
|
302
|
+
mdBuf += s;
|
|
303
|
+
const g = getGeo();
|
|
304
|
+
// 滚动回看冻结(同 contentWrite):scrollOffset>0 时 setLines 替换段会改缓冲行数,若 offset 不变,
|
|
305
|
+
// 下次 scrollBy→repaintViewport 取漂移窗口、把流式正文画进历史视图。故 setLines 前后算 totalRows 差,
|
|
306
|
+
// offset += delta(可负:setLines 重渲染可能缩行)冻住视图。md 段在尾,窗口在上方,冻结后不重叠。
|
|
307
|
+
const scrolled = scrollOffset > 0;
|
|
308
|
+
const totalBefore = scrolled ? content.totalRows() : 0;
|
|
309
|
+
const lines = renderMarkdown(mdBuf, g.cols);
|
|
310
|
+
content.setLines(lines);
|
|
311
|
+
const segRows = lines.length;
|
|
197
312
|
const available = g.contentBottom - segmentStartRow + 1;
|
|
198
|
-
|
|
199
|
-
if (start < g.contentTop)
|
|
200
|
-
start = g.contentTop;
|
|
201
|
-
let rows = (contentRow - start) + (contentCol > 1 ? 1 : 0);
|
|
202
|
-
const maxRows = g.contentBottom - start + 1;
|
|
203
|
-
if (rows > maxRows)
|
|
204
|
-
rows = maxRows;
|
|
205
|
-
if (rows <= 0)
|
|
206
|
-
return 0;
|
|
207
|
-
let p = '';
|
|
208
|
-
for (let i = 0; i < rows; i++) {
|
|
209
|
-
p += cup(start + i, 1) + esc.clearLine;
|
|
210
|
-
}
|
|
211
|
-
stdout.write(p);
|
|
212
|
-
contentRow = start;
|
|
313
|
+
contentRow = segRows >= available ? g.contentBottom : segmentStartRow + segRows;
|
|
213
314
|
contentCol = 1;
|
|
214
|
-
|
|
215
|
-
|
|
315
|
+
if (scrolled) {
|
|
316
|
+
const delta = content.totalRows() - totalBefore;
|
|
317
|
+
if (delta !== 0) {
|
|
318
|
+
const maxOff = Math.max(0, content.totalRows() - g.contentBottom);
|
|
319
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, maxOff));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (scrollOffset === 0 && !isStreamingPaused()) {
|
|
323
|
+
repaintViewport();
|
|
324
|
+
if (mode === 'running') {
|
|
325
|
+
const p = runningCaretPos();
|
|
326
|
+
stdout.write(cup(p.row, p.col));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* 把一段完整(非流式)assistant 文本一次性渲染成 markdown 写入(供 renderHistory 回显
|
|
332
|
+
* /resume / /rollback 复显上下文):渲染一次 → join('\n') → 经 contentWrite 增量写
|
|
333
|
+
* (无段 erase / 无 repaintViewport,无闪烁)。与流式 contentWriteMd 的区别:不累积 chunk、
|
|
334
|
+
* 不走段替换——一次性把渲染结果当普通带色文本喂入(渲染行各自 ≤ cols、自洽带色,contentWrite
|
|
335
|
+
* 的 SGR/折行状态机原样接纳)。非 TTY:退化为 stdout.write 原文(同 contentWrite)。
|
|
336
|
+
*/
|
|
337
|
+
export function contentWriteMdOnce(s) {
|
|
338
|
+
if (!active || !ui.isTTY) {
|
|
339
|
+
stdout.write(s);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const g = getGeo();
|
|
343
|
+
const lines = renderMarkdown(s, g.cols);
|
|
344
|
+
contentWrite(lines.join('\n'));
|
|
216
345
|
}
|
|
217
346
|
/** 清空内容区(保留底栏):全屏清 + 重设区域 + 续写位归 (1,1) + 清缓冲 + 回尾。底栏由调用方随后重画。 */
|
|
218
347
|
export function clearContent() {
|
|
@@ -224,6 +353,8 @@ export function clearContent() {
|
|
|
224
353
|
contentCol = 1;
|
|
225
354
|
segmentStartRow = 1;
|
|
226
355
|
scrollOffset = 0;
|
|
356
|
+
mdActive = false;
|
|
357
|
+
mdBuf = '';
|
|
227
358
|
content.reset();
|
|
228
359
|
stdout.write(esc.home);
|
|
229
360
|
}
|
|
@@ -278,7 +409,15 @@ export function resetScroll() {
|
|
|
278
409
|
function composeStatus(status, cols) {
|
|
279
410
|
const sep = ' ';
|
|
280
411
|
const sepW = sep.length;
|
|
281
|
-
|
|
412
|
+
// 运行态(含滚动回看)用心跳帧(♥/♡)替换静态 ◆:agent 跑起来时状态行前导符始终跳动,
|
|
413
|
+
// 滑到上面看历史也能感知 agent 在跑。帧宽=1(与 ◆ 同),fixed 不变,布局不乱。INPUT 态仍显 ◆。
|
|
414
|
+
const spinning = mode === 'running' && runningFrame >= 0;
|
|
415
|
+
const lead = spinning ? `${RUNNING_FRAMES[runningFrame]} ` : `◆ `;
|
|
416
|
+
// 模式 chip:lead 之后、model 之前。auto 显 dim 'auto'(常态),plan 显亮黄 'plan'(切换时颜色+文字都变,明显)。
|
|
417
|
+
const modeChip = status.modeTag
|
|
418
|
+
? ` ${status.modeTag === 'plan' ? ui.yellow : ui.dim}${status.modeTag}${ui.reset} `
|
|
419
|
+
: '';
|
|
420
|
+
const modeChipW = status.modeTag ? 2 + displayWidth(status.modeTag) : 0; // 2 = 前导+尾随空格
|
|
282
421
|
const model = truncateDisplay(status.model, 22);
|
|
283
422
|
const ctx = status.contextBar; // 已带色
|
|
284
423
|
const ctxW = ansiDisplayWidth(ctx);
|
|
@@ -289,18 +428,24 @@ function composeStatus(status, cols) {
|
|
|
289
428
|
st = `历史 ↑${scrollOffset} (PgDn 回底)`; // 滚动回看显历史指示,不显走时
|
|
290
429
|
}
|
|
291
430
|
else {
|
|
292
|
-
|
|
293
|
-
//
|
|
431
|
+
// head = spinner 帧 + 状态文字。常态 agent 轮二者皆空:agent spinner 不调 setStatus,
|
|
432
|
+
// 其文字在内联 paintLiveAtCursor 显,状态行不重复——只显走时。命令态(如 /compact 的「压缩中」)
|
|
433
|
+
// 走 setStatus,在此显帧 + 文字(无内联,不重复)。
|
|
434
|
+
const head = (status.spinnerFrame ? status.spinnerFrame + ' ' : '') + status.status;
|
|
294
435
|
if (mode === 'running' && turnStart != null) {
|
|
295
|
-
|
|
436
|
+
// RUNNING 态追加走时:整轮从 enterRunningMode 起计时,200ms 计时器续刷使其连续递增。
|
|
437
|
+
st = head ? `${head} · ${fmtElapsed(Date.now() - turnStart)}` : fmtElapsed(Date.now() - turnStart);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
st = head; // INPUT 等态(如"空闲")
|
|
296
441
|
}
|
|
297
442
|
}
|
|
298
443
|
const stW = displayWidth(st);
|
|
299
|
-
const fixed = displayWidth(lead) + displayWidth(model) + sepW * 3 + ctxW + stW;
|
|
444
|
+
const fixed = displayWidth(lead) + modeChipW + displayWidth(model) + sepW * 3 + ctxW + stW;
|
|
300
445
|
const cwdBudget = cols - fixed - 1;
|
|
301
446
|
const cwd = cwdBudget >= 6 ? truncateDisplay(status.cwd, cwdBudget) : '';
|
|
302
447
|
return [
|
|
303
|
-
`${ui.brightCyan}${lead}${ui.reset}${ui.bold}${model}${ui.reset}`,
|
|
448
|
+
`${spinning ? ui.brightMagenta : ui.brightCyan}${lead}${ui.reset}${modeChip}${ui.bold}${model}${ui.reset}`,
|
|
304
449
|
sep,
|
|
305
450
|
ctx,
|
|
306
451
|
sep,
|
|
@@ -313,12 +458,21 @@ function composeStatus(status, cols) {
|
|
|
313
458
|
export function drawStatusBar(status) {
|
|
314
459
|
if (!active || !base)
|
|
315
460
|
return;
|
|
461
|
+
if (isStreamingPaused())
|
|
462
|
+
return; // 用户打字中:状态行暂不刷(避免光标去 statusRow 扰动 IME),停手后 flush 会刷
|
|
316
463
|
const s = status ?? { ...base, status: statusText, spinnerFrame };
|
|
317
464
|
const g = getGeo();
|
|
318
465
|
const statusRow = g.contentBottom + 1;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
466
|
+
// 一次写入:cup 状态行 + clearLine + 状态 + cup 回(运行态→输入框供 IME 锚定;否则续写位/内容区底)
|
|
467
|
+
let out = cup(statusRow, 1) + esc.clearLine + composeStatus(s, g.cols);
|
|
468
|
+
if (mode === 'running' && scrollOffset === 0) {
|
|
469
|
+
const p = runningCaretPos();
|
|
470
|
+
out += cup(p.row, p.col);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
out += cup(scrollOffset === 0 ? contentRow : g.contentBottom, scrollOffset === 0 ? contentCol : 1);
|
|
474
|
+
}
|
|
475
|
+
stdout.write(out);
|
|
322
476
|
}
|
|
323
477
|
/** 更新易变状态(状态文字 + spinner 帧)并重画状态行。spinner / agent 用。 */
|
|
324
478
|
export function setStatus(status, frame) {
|
|
@@ -336,7 +490,11 @@ function startTurnTimer() {
|
|
|
336
490
|
if (!active)
|
|
337
491
|
return;
|
|
338
492
|
stopTurnTimer();
|
|
339
|
-
|
|
493
|
+
runningFrame = 0; // 进入运行态:启动状态行 chip 心跳(首帧立即生效)
|
|
494
|
+
turnTimer = setInterval(() => {
|
|
495
|
+
runningFrame = (runningFrame + 1) % RUNNING_FRAMES.length; // 推进心跳帧,让前导符跳动
|
|
496
|
+
drawStatusBar();
|
|
497
|
+
}, 200);
|
|
340
498
|
turnTimer.unref();
|
|
341
499
|
}
|
|
342
500
|
/** 停走时计时器。enterInputMode / exitAltScreen 调。 */
|
|
@@ -352,17 +510,28 @@ function stopTurnTimer() {
|
|
|
352
510
|
* 配合 clearLiveAtCursor 在 spinner 停时清掉,随后 contentWrite 的结果即写在该行(spinner 不入历史缓冲)。
|
|
353
511
|
*/
|
|
354
512
|
export function paintLiveAtCursor(text) {
|
|
355
|
-
if (!active || !ui.isTTY || scrollOffset !== 0)
|
|
513
|
+
if (!active || !ui.isTTY || scrollOffset !== 0 || isStreamingPaused())
|
|
356
514
|
return;
|
|
357
|
-
|
|
515
|
+
// 一次写入:cup 续写位 + clearLine + 帧 + (运行态 cup 回输入框)——避免光标在续写位歇脚被 IME 跟踪
|
|
516
|
+
let out = cup(contentRow, contentCol) + esc.clearLine + text;
|
|
517
|
+
if (mode === 'running') {
|
|
518
|
+
const p = runningCaretPos();
|
|
519
|
+
out += cup(p.row, p.col);
|
|
520
|
+
}
|
|
521
|
+
stdout.write(out);
|
|
358
522
|
}
|
|
359
523
|
/** 清掉续写位那行瞬时活动文本(配合 paintLiveAtCursor)。 */
|
|
360
524
|
export function clearLiveAtCursor() {
|
|
361
|
-
if (!active || !ui.isTTY || scrollOffset !== 0)
|
|
525
|
+
if (!active || !ui.isTTY || scrollOffset !== 0 || isStreamingPaused())
|
|
362
526
|
return;
|
|
363
|
-
|
|
527
|
+
let out = cup(contentRow, contentCol) + esc.clearLine;
|
|
528
|
+
if (mode === 'running') {
|
|
529
|
+
const p = runningCaretPos();
|
|
530
|
+
out += cup(p.row, p.col);
|
|
531
|
+
}
|
|
532
|
+
stdout.write(out);
|
|
364
533
|
}
|
|
365
|
-
/** 更新状态行基线(模型 / context / cwd)。repl
|
|
534
|
+
/** 更新状态行基线(模型 / context / cwd / 模式标识)。repl 在轮次边界与切模式时调。 */
|
|
366
535
|
export function setStatusBase(b) {
|
|
367
536
|
base = b;
|
|
368
537
|
}
|
|
@@ -509,7 +678,7 @@ export function paintInput(view) {
|
|
|
509
678
|
const prefix = vis.startVis === 0 && i === 0 ? view.prompt : indent;
|
|
510
679
|
let text;
|
|
511
680
|
if (view.dim) {
|
|
512
|
-
text =
|
|
681
|
+
text = renderDimInputRow(view.prompt, line, view.placeholder ?? '', g.cols);
|
|
513
682
|
}
|
|
514
683
|
else if (showCaret && i === vis.visLine) {
|
|
515
684
|
// 块状光标:反白光标右侧字符(cur),行末(无字符)反白一个空格——示"现在在哪输入"
|
|
@@ -535,8 +704,9 @@ export function paintInput(view) {
|
|
|
535
704
|
}
|
|
536
705
|
// 6. 光标
|
|
537
706
|
if (view.dim) {
|
|
538
|
-
//
|
|
539
|
-
|
|
707
|
+
// 运行态:真光标归输入框光标位(供 IME 锚定);滚动回看时归内容区底(viewport 锁历史)
|
|
708
|
+
const p = runningCaretPos();
|
|
709
|
+
buf += cup(scrollOffset === 0 ? p.row : g.contentBottom, scrollOffset === 0 ? p.col : 1);
|
|
540
710
|
}
|
|
541
711
|
else {
|
|
542
712
|
const r = firstInputRow + vis.visLine;
|
|
@@ -547,30 +717,50 @@ export function paintInput(view) {
|
|
|
547
717
|
stdout.write(buf); // 整帧一次写出(footerH 不变时):终端原子应用,无中间空白→不闪烁
|
|
548
718
|
}
|
|
549
719
|
/**
|
|
550
|
-
*
|
|
720
|
+
* 渲染运行态输入行(dim):❯ + (有打字:dim 文本 + 末尾反白块状光标;无打字:反白光标在起点 + dim 占位 ghost)。
|
|
721
|
+
* 光标是「画出来」的反白块(非真终端光标)——真光标仍归续写位,避开与 contentWrite/drawStatusBar 争用;
|
|
722
|
+
* 输入行在 RUNNING 期间只被按键回显重画(流式/spinner 不碰这行),故画上去的光标能稳住。
|
|
723
|
+
* 内容截断到 cols-promptW-1(留 ❯ 与光标各 1 cell),防超长软折行写穿下线。
|
|
724
|
+
*/
|
|
725
|
+
function renderDimInputRow(prompt, text, placeholder, cols) {
|
|
726
|
+
const promptW = displayWidth(prompt);
|
|
727
|
+
const caret = `${ui.reverse} ${ui.reset}`; // 反白块状光标(1 cell,与 INPUT 态同款)
|
|
728
|
+
const contentW = Math.max(0, cols - promptW - 1);
|
|
729
|
+
if (text.length > 0) {
|
|
730
|
+
// 有打字:❯ dim + 文本(dim) + 反白光标(末尾)
|
|
731
|
+
return `${ui.dim}${prompt}${truncateDisplay(text, contentW)}${ui.reset}${caret}`;
|
|
732
|
+
}
|
|
733
|
+
// 空:❯ dim + 反白光标(打字起点) + dim 占位 ghost
|
|
734
|
+
const p = placeholder ? truncateDisplay(placeholder, contentW) : '';
|
|
735
|
+
return `${ui.dim}${prompt}${ui.reset}${caret}${ui.dim}${p}${ui.reset}`;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* 运行态 typeahead 回显:定向写输入行(底栏输入框),把 dim 占位换成已打字文本 + 反白块状光标(无打字时光标在起点)。
|
|
551
739
|
* 只 cup 输入行 + clearLine + dim 文本 + 归位——不调 setRegion(运行中禁多行,避免 DECSTBM 抖动)、
|
|
552
|
-
* 不重画状态行/contentBottom、不用 ED。同步 lastView 为 dim
|
|
553
|
-
*
|
|
740
|
+
* 不重画状态行/contentBottom、不用 ED。同步 lastView 为 dim 视图(text 与 placeholder 拆开存),使 scrollBy/resize
|
|
741
|
+
* 的 repaint 仍显当前回显 + 光标。真光标归续写位(滚动回看时归内容区底)——不入输入框,假光标已画在行内。
|
|
554
742
|
*/
|
|
555
743
|
export function paintRunningInputEcho(text, placeholder) {
|
|
556
744
|
if (!active || !base)
|
|
557
745
|
return;
|
|
558
746
|
const g = getGeo();
|
|
559
747
|
const inputRow = g.contentBottom + 3; // 运行态 footerH 恒 4:状态(+1)+上线(+2)+输入行(+3);下线在 rows
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
// 同步 lastView:
|
|
748
|
+
stdout.write(cup(inputRow, 1) +
|
|
749
|
+
esc.clearLine +
|
|
750
|
+
renderDimInputRow('❯ ', text, placeholder, g.cols));
|
|
751
|
+
// 同步 lastView:text 与 placeholder 拆开存,使滚动/resize 的 repaint 重画时光标位置正确(空→起点,有字→末尾)
|
|
564
752
|
lastView = {
|
|
565
753
|
prompt: '❯ ',
|
|
566
|
-
lines: [
|
|
754
|
+
lines: [text],
|
|
755
|
+
placeholder,
|
|
567
756
|
cursorLine: 0,
|
|
568
757
|
cursorCol: 0,
|
|
569
758
|
menu: null,
|
|
570
759
|
dim: true,
|
|
571
760
|
};
|
|
572
|
-
//
|
|
573
|
-
|
|
761
|
+
// 真光标归输入框光标位(供 IME 锚定,与假光标同位);滚动回看时归内容区底
|
|
762
|
+
const p = runningCaretPos();
|
|
763
|
+
stdout.write(cup(scrollOffset === 0 ? p.row : g.contentBottom, scrollOffset === 0 ? p.col : 1));
|
|
574
764
|
}
|
|
575
765
|
/** 重画当前视图(resize / 内部用)。 */
|
|
576
766
|
export function repaint() {
|
|
@@ -586,8 +776,19 @@ export function enterInputMode(status = '空闲') {
|
|
|
586
776
|
mode = 'input';
|
|
587
777
|
statusText = status;
|
|
588
778
|
spinnerFrame = undefined;
|
|
779
|
+
runningFrame = -1; // 回 INPUT 态:停状态行 chip 旋转,composeStatus 退回静态 ◆
|
|
589
780
|
turnStart = null; // 停走时
|
|
590
781
|
stopTurnTimer();
|
|
782
|
+
// 运行态若有未 flush 的缓冲内容(用户打字暂停了流式写),切回 INPUT 前重画内容区显示之,免丢内容
|
|
783
|
+
if (flushTimer) {
|
|
784
|
+
clearTimeout(flushTimer);
|
|
785
|
+
flushTimer = null;
|
|
786
|
+
}
|
|
787
|
+
if (userActiveUntil) {
|
|
788
|
+
userActiveUntil = 0;
|
|
789
|
+
if (active)
|
|
790
|
+
repaintViewport();
|
|
791
|
+
}
|
|
591
792
|
if (active && base) {
|
|
592
793
|
setRegion(4); // 1 状态 + 1 上线 + 1 输入 + 1 下线
|
|
593
794
|
paintInput({
|
|
@@ -597,6 +798,7 @@ export function enterInputMode(status = '空闲') {
|
|
|
597
798
|
cursorCol: 0,
|
|
598
799
|
menu: null,
|
|
599
800
|
});
|
|
801
|
+
stdout.write(esc.cursorShow); // 回 INPUT 态:显真光标(运行态藏了)
|
|
600
802
|
}
|
|
601
803
|
}
|
|
602
804
|
/** 进入运行态:底栏输入行改 dim 占位,光标回续写位。footerH 恒 4(状态+上线+输入+下线)。新轮回尾(确保新内容可见)。 */
|
|
@@ -610,7 +812,8 @@ export function enterRunningMode(status, placeholder) {
|
|
|
610
812
|
setRegion(4); // 1 状态 + 1 上线 + 1 输入 + 1 下线
|
|
611
813
|
paintInput({
|
|
612
814
|
prompt: '❯ ',
|
|
613
|
-
lines: [
|
|
815
|
+
lines: [''],
|
|
816
|
+
placeholder,
|
|
614
817
|
cursorLine: 0,
|
|
615
818
|
cursorCol: 0,
|
|
616
819
|
menu: null,
|
|
@@ -618,6 +821,9 @@ export function enterRunningMode(status, placeholder) {
|
|
|
618
821
|
});
|
|
619
822
|
startTurnTimer(); // 续刷状态行走时(流式期间 spinner 停转,由它兜底)
|
|
620
823
|
contentMode();
|
|
824
|
+
// 真光标可见,停在输入框光标位(供 IME 锚定气泡到光标处)。打字时 setUserActive 暂停流式写、
|
|
825
|
+
// 光标稳定在输入框;流式写时瞬时到 contentRow 但合并写入很快回输入框(光标跟流式是终端常态)。
|
|
826
|
+
// 不再藏光标:藏了 IME 气泡会跑到输入框最右边而非光标处(WT 不锚定隐藏光标)。
|
|
621
827
|
}
|
|
622
828
|
}
|
|
623
829
|
// ── alt screen 生命周期 + 钩子 ──
|
|
@@ -626,12 +832,14 @@ export function enterAltScreen() {
|
|
|
626
832
|
return;
|
|
627
833
|
active = true;
|
|
628
834
|
stdout.write(esc.altOn);
|
|
629
|
-
stdout.write(esc.
|
|
835
|
+
stdout.write(esc.mouseOn); // SGR 鼠标追踪:滚轮发 \x1B[<btn;col;rowM 报表,经 mouse.consumeMouse 重组 → scrollBy(滚轮滚动靠此 + onKey/onRunningKey 顶部守卫)
|
|
630
836
|
setRegion(4); // 1 状态 + 1 上线 + 1 输入 + 1 下线(底栏始终含上下线)
|
|
631
837
|
contentRow = 1;
|
|
632
838
|
contentCol = 1;
|
|
633
839
|
segmentStartRow = 1;
|
|
634
840
|
scrollOffset = 0;
|
|
841
|
+
mdActive = false;
|
|
842
|
+
mdBuf = '';
|
|
635
843
|
content.reset();
|
|
636
844
|
exitHandler = () => exitAltScreen();
|
|
637
845
|
process.on('exit', exitHandler);
|
|
@@ -666,6 +874,7 @@ export function exitAltScreen() {
|
|
|
666
874
|
active = false;
|
|
667
875
|
stopTurnTimer(); // 兜底清走时计时器(防异常退出泄漏)
|
|
668
876
|
turnStart = null;
|
|
877
|
+
mouse.resetMouse(); // 清鼠标重组残留(防退出后状态泄漏到下次进 alt 屏)
|
|
669
878
|
// raw 还原独立 try:非 TTY / 不支持时 setRawMode 抛错,不应阻断 stdout 恢复(alt 退屏必须执行)。
|
|
670
879
|
try {
|
|
671
880
|
stdin.setRawMode(false); // 还原 raw(RUNNING 态常驻 raw,退出时必须还原,否则终端残留 raw 模式)
|
|
@@ -676,7 +885,7 @@ export function exitAltScreen() {
|
|
|
676
885
|
try {
|
|
677
886
|
stdout.write('\x1B[r'); // 复位 DECSTBM margins
|
|
678
887
|
stdout.write(esc.cursorShow);
|
|
679
|
-
stdout.write(esc.
|
|
888
|
+
stdout.write(esc.mouseOff); // 关 SGR 鼠标追踪(反序:先 1006l 再 1000l)
|
|
680
889
|
stdout.write(esc.altOff); // 退 alt(恢复主屏 + 光标)
|
|
681
890
|
}
|
|
682
891
|
catch {
|
|
@@ -704,6 +913,11 @@ export function exitAltScreen() {
|
|
|
704
913
|
clearTimeout(resizeTimer);
|
|
705
914
|
resizeTimer = null;
|
|
706
915
|
}
|
|
916
|
+
if (flushTimer) {
|
|
917
|
+
clearTimeout(flushTimer);
|
|
918
|
+
flushTimer = null;
|
|
919
|
+
}
|
|
920
|
+
userActiveUntil = 0;
|
|
707
921
|
}
|
|
708
922
|
export function isActive() {
|
|
709
923
|
return active;
|