mocode-ai 0.4.8 → 0.4.10
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 +18 -0
- package/dist/agent/core.d.ts +112 -0
- package/dist/agent/core.js +56 -13
- package/dist/agent/index.d.ts +19 -0
- package/dist/agent/index.js +1 -1
- package/dist/agent/mode.d.ts +29 -0
- package/dist/agent/spawn.d.ts +36 -0
- package/dist/attachments/image.d.ts +22 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/config/file.d.ts +23 -0
- package/dist/config/index.d.ts +114 -0
- package/dist/config/index.js +61 -5
- package/dist/config/presets.d.ts +57 -0
- package/dist/context/budget.d.ts +79 -0
- package/dist/context/classifier.d.ts +14 -0
- package/dist/context/encoders/_util.d.ts +11 -0
- package/dist/context/encoders/code.d.ts +2 -0
- package/dist/context/encoders/doc.d.ts +14 -0
- package/dist/context/encoders/graph.d.ts +11 -0
- package/dist/context/encoders/index.d.ts +2 -0
- package/dist/context/encoders/log.d.ts +2 -0
- package/dist/context/encoders/memory.d.ts +2 -0
- package/dist/context/encoders/passthrough.d.ts +11 -0
- package/dist/context/encoders/search.d.ts +2 -0
- package/dist/context/encoders/summary.d.ts +12 -0
- package/dist/context/encoders/table.d.ts +2 -0
- package/dist/context/encoders/tree.d.ts +2 -0
- package/dist/context/index.d.ts +6 -0
- package/dist/context/lifecycle.d.ts +51 -0
- package/dist/context/pipeline.d.ts +9 -0
- package/dist/context/registry.d.ts +9 -0
- package/dist/context/relevance.d.ts +65 -0
- package/dist/context/types.d.ts +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/llm/capabilities.d.ts +19 -0
- package/dist/llm/index.d.ts +111 -0
- package/dist/llm/index.js +22 -1
- package/dist/memory/discover.d.ts +15 -0
- package/dist/memory/index.d.ts +13 -0
- package/dist/memory/reflect.d.ts +34 -0
- package/dist/memory/store.d.ts +124 -0
- package/dist/permissions/index.js +138 -0
- package/dist/pet/bridge.d.ts +73 -0
- package/dist/pet/protocol.d.ts +83 -0
- package/dist/pet/state.d.ts +28 -0
- package/dist/plan/active.d.ts +23 -0
- package/dist/plan/index.d.ts +13 -0
- package/dist/plan/state.d.ts +2 -0
- package/dist/plan/store.d.ts +78 -0
- package/dist/project-skill/index.js +180 -0
- package/dist/project-skill/initializer.js +178 -0
- package/dist/project-snapshot/index.js +85 -0
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/project-snapshot/static-files.js +87 -0
- package/dist/repl/index.d.ts +22 -0
- package/dist/repl/index.js +268 -2
- package/dist/rollback/index.d.ts +79 -0
- package/dist/sandbox/command.d.ts +3 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/jail.d.ts +17 -0
- package/dist/sandbox/policy.d.ts +21 -0
- package/dist/sandbox/root.d.ts +5 -0
- package/dist/session/compact.d.ts +92 -0
- package/dist/session/drop.d.ts +19 -0
- package/dist/session/index.d.ts +13 -0
- package/dist/session/persist.d.ts +30 -0
- package/dist/session/scheduler.d.ts +53 -0
- package/dist/skills/discover.d.ts +33 -0
- package/dist/skills/index.d.ts +13 -0
- package/dist/tools/builtins/ask-human.d.ts +9 -0
- package/dist/tools/builtins/codegraph.d.ts +2 -0
- package/dist/tools/builtins/codegraph.js +17 -4
- package/dist/tools/builtins/drop-context.d.ts +18 -0
- package/dist/tools/builtins/edit-file.d.ts +2 -0
- package/dist/tools/builtins/edit-file.js +1 -0
- package/dist/tools/builtins/glob.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/index.d.ts +2 -0
- package/dist/tools/builtins/index.js +7 -0
- package/dist/tools/builtins/memory-forget.d.ts +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -0
- package/dist/tools/builtins/memory-list.d.ts +2 -0
- package/dist/tools/builtins/memory-save.d.ts +2 -0
- package/dist/tools/builtins/memory-save.js +1 -0
- package/dist/tools/builtins/memory-search.d.ts +2 -0
- package/dist/tools/builtins/memory-update.d.ts +2 -0
- package/dist/tools/builtins/memory-update.js +1 -0
- package/dist/tools/builtins/project-skill-update.js +104 -0
- package/dist/tools/builtins/read-file.d.ts +2 -0
- package/dist/tools/builtins/run-command.d.ts +2 -0
- package/dist/tools/builtins/run-command.js +1 -0
- package/dist/tools/builtins/switch-mode.d.ts +2 -0
- package/dist/tools/builtins/task.d.ts +2 -0
- package/dist/tools/builtins/task.js +1 -0
- package/dist/tools/builtins/todolist.d.ts +2 -0
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/tools/builtins/use-skill.d.ts +2 -0
- package/dist/tools/builtins/web-fetch.d.ts +2 -0
- package/dist/tools/builtins/web-search.d.ts +2 -0
- package/dist/tools/builtins/web-search.js +12 -1
- package/dist/tools/builtins/write-file.d.ts +2 -0
- package/dist/tools/builtins/write-file.js +1 -0
- package/dist/tools/constants.d.ts +52 -0
- package/dist/tools/registry.d.ts +18 -0
- package/dist/tools/types.d.ts +42 -0
- package/dist/ui/batch.d.ts +67 -0
- package/dist/ui/batch.js +19 -2
- package/dist/ui/clipboard.d.ts +9 -0
- package/dist/ui/content.d.ts +55 -0
- package/dist/ui/content.js +68 -3
- package/dist/ui/diff.d.ts +34 -0
- package/dist/ui/diff.js +15 -24
- package/dist/ui/intervention.d.ts +46 -0
- package/dist/ui/layout.d.ts +225 -0
- package/dist/ui/layout.js +67 -11
- package/dist/ui/markdown.d.ts +46 -0
- package/dist/ui/mouse.d.ts +29 -0
- package/dist/ui/prompt.d.ts +61 -0
- package/dist/ui/render.d.ts +60 -0
- package/dist/ui/spinner.d.ts +26 -0
- package/dist/ui/theme.d.ts +47 -0
- package/dist/updater/index.d.ts +22 -0
- package/package.json +1 -1
package/dist/ui/content.js
CHANGED
|
@@ -136,6 +136,60 @@ export function lineAt(abs) {
|
|
|
136
136
|
const all = snapshot();
|
|
137
137
|
return abs >= 0 && abs < all.length ? all[abs] : null;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* 找「绝对行索引 < absStart 的最近一条用户消息」的文本(用于滚动回看时的「我刚发的
|
|
141
|
+
* 请求」sticky banner)。算法:从 absStart - 1 往上扫,识别「用户气泡行」(由 repl
|
|
142
|
+
* formatUserMessage 写入,剥 SGR 后首字符 = ❯);再从此行往下吞连续的同类行
|
|
143
|
+
* 收集多行消息,join('\n')。
|
|
144
|
+
*
|
|
145
|
+
* 检测 user-bubble 行靠「剥 SGR 后以 ❯ 开头」而非 userBg SGR 前缀:rowStartSgr 继承自
|
|
146
|
+
* 上一行末(可能残留 dim/cyan),且 bubble 起手有 userBg 包裹,直接 startsWith(userBg)
|
|
147
|
+
* 在残留 rowStartSgr 场景会漏判。剥光所有 SGR 后 → 首字符稳定为 ❯ (repl.PROMPT)。
|
|
148
|
+
*
|
|
149
|
+
* 返回的文本已经剥离 ANSI + 提示符,可直接给 banner 渲染(再做截断 / 折叠)。
|
|
150
|
+
* absStart ≤ 0 返 null(没东西在视口上方)。
|
|
151
|
+
*/
|
|
152
|
+
export function lastUserMessageBefore(absStart) {
|
|
153
|
+
if (absStart <= 0)
|
|
154
|
+
return null;
|
|
155
|
+
const all = snapshot();
|
|
156
|
+
const SGR = /\x1B\[[0-9;]*m/g;
|
|
157
|
+
const isUserBubbleRow = (row) => {
|
|
158
|
+
// 剥光 SGR 后首字符 = ❯(repl.PROMPT 首字)→ 是 user bubble;
|
|
159
|
+
// agent 行首字符可能是 ● / ╭ / │ / 数字 / 字母,绝不会撞 ❯。
|
|
160
|
+
const visible = row.replace(SGR, '');
|
|
161
|
+
return visible.startsWith('❯');
|
|
162
|
+
};
|
|
163
|
+
// 1) 从 absStart - 1 往上扫,找第一条 user bubble 行(最近的 user-bubble 的最后一行)
|
|
164
|
+
let bubbleEnd = -1;
|
|
165
|
+
for (let i = Math.min(absStart, all.length) - 1; i >= 0; i--) {
|
|
166
|
+
if (isUserBubbleRow(all[i])) {
|
|
167
|
+
bubbleEnd = i;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (bubbleEnd < 0)
|
|
172
|
+
return null;
|
|
173
|
+
// 2) 往上找气泡起点(连续 user-bubble 行块 = 同一条 user 消息)
|
|
174
|
+
let bubbleStart = bubbleEnd;
|
|
175
|
+
while (bubbleStart - 1 >= 0 && isUserBubbleRow(all[bubbleStart - 1])) {
|
|
176
|
+
bubbleStart--;
|
|
177
|
+
}
|
|
178
|
+
// 3) 收文本:按显示字符剥 SGR + 续行/末填充空格 + 首行 prompt
|
|
179
|
+
// 首行剥前导 '❯ ' 序列 → banner 自己再加回 [banner_prompt] + <text>;
|
|
180
|
+
// 一次剥光所有重复的 ❯(防止用户手敲 prompt / 多重回显 → 出现 '❯ ❯ ...' 双提示符)。
|
|
181
|
+
const joinedText = all
|
|
182
|
+
.slice(bubbleStart, bubbleEnd + 1)
|
|
183
|
+
.map((raw, idx) => {
|
|
184
|
+
let stripped = raw.replace(SGR, '');
|
|
185
|
+
if (idx === 0)
|
|
186
|
+
stripped = stripped.replace(/^(?:❯\s*)+/, ''); // 剥光所有前导 ❯(含每个后面的可选空格)
|
|
187
|
+
return stripped.trimEnd();
|
|
188
|
+
})
|
|
189
|
+
.join('\n')
|
|
190
|
+
.replace(/\n+$/, '');
|
|
191
|
+
return joinedText || null;
|
|
192
|
+
}
|
|
139
193
|
/**
|
|
140
194
|
* 在绝对行索引 after(0-based,已 commit)之后插入 N 条自洽行。
|
|
141
195
|
* 用于「折叠摘要行下展开明细」——把详情行在已写入摘要行后面塞入缓冲,
|
|
@@ -143,8 +197,8 @@ export function lineAt(abs) {
|
|
|
143
197
|
*
|
|
144
198
|
* 行须自洽(每行带行末 \x1B[0m),不经 feedChar/feedSgr,直接入 rows。
|
|
145
199
|
* after < 0 视为在所有已 commit 行之前插入;after ≥ 已 commit 行数则追加到末尾。
|
|
146
|
-
*
|
|
147
|
-
* md
|
|
200
|
+
* 若 segMark 活跃且插入点在 segMark.rowIdx 之前,则 segMark.rowIdx 跟着平移(否则
|
|
201
|
+
* 流式 md 段活跃期间展开/折叠 batch 后,下一个 md chunk 的 setLines 截断到错误位置)。
|
|
148
202
|
*
|
|
149
203
|
* 后置条件:插入后 hasCurrent=false(新空行不由本函数建立);调用方须自行决定续写位
|
|
150
204
|
* (BatchRenderer 在插入后调 layout.contentWrite 续写,新 \n 自然在详情块后建新行)。
|
|
@@ -162,6 +216,11 @@ export function insertAfter(after, lines) {
|
|
|
162
216
|
// after 是绝对行索引;若超过 committed(例如快照时 hasCurrent=true),钳到末尾
|
|
163
217
|
const target = after < 0 ? 0 : Math.min(after + 1, committed);
|
|
164
218
|
rows.splice(target, 0, ...lines);
|
|
219
|
+
// 流式 md 段活跃期间:插入点在段起点之前 → segMark.rowIdx 平移,
|
|
220
|
+
// 否则下一个 md chunk 的 setLines 截断到旧位置,展开行被砍掉。
|
|
221
|
+
if (segMark && target <= segMark.rowIdx) {
|
|
222
|
+
segMark.rowIdx += lines.length;
|
|
223
|
+
}
|
|
165
224
|
if (rows.length > MAX_ROWS + 512)
|
|
166
225
|
rows.splice(0, rows.length - MAX_ROWS);
|
|
167
226
|
}
|
|
@@ -185,5 +244,11 @@ export function deleteFrom(startIdx, n) {
|
|
|
185
244
|
if (startIdx >= committed)
|
|
186
245
|
return;
|
|
187
246
|
const end = Math.min(startIdx + n, committed);
|
|
188
|
-
|
|
247
|
+
const actual = end - startIdx;
|
|
248
|
+
rows.splice(startIdx, actual);
|
|
249
|
+
// 流式 md 段活跃期间:删除区间全部在段起点之前 → segMark.rowIdx 回退,
|
|
250
|
+
// 否则下一个 md chunk 的 setLines 截断到旧位置,段内容错位。
|
|
251
|
+
if (segMark && startIdx + actual <= segMark.rowIdx) {
|
|
252
|
+
segMark.rowIdx -= actual;
|
|
253
|
+
}
|
|
189
254
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件改动 diff 渲染(仿 Claude Code):
|
|
3
|
+
* Update(path) / Create(path) + Added N lines, removed M lines
|
|
4
|
+
* 正文带文件行号 + `-` 红 / `+` 绿 gutter + 代码经 cli-highlight 语法高亮。
|
|
5
|
+
*
|
|
6
|
+
* 纯渲染叶子模块——不依赖 config / 业务;颜色经 theme 非 TTY 退化为空串,
|
|
7
|
+
* 输出供 layout.contentWrite 写入(区域滚动 / 底栏保护 / 续写位跟踪均由其内置)。
|
|
8
|
+
* 高亮逐行做(每行自成 SGR 闭区间,末尾补 reset 防跨行泄漏);多行构造(块注释 / 模板串)的内部行可能不上色,可接受。
|
|
9
|
+
*/
|
|
10
|
+
/** 行级 diff 单元:ctx 不变 / add 新增 / del 删除。 */
|
|
11
|
+
export type DiffOp = 'ctx' | 'add' | 'del';
|
|
12
|
+
export interface DiffLine {
|
|
13
|
+
op: DiffOp;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 行级 LCS diff。返回 op 序列(ctx/add/del)。
|
|
18
|
+
* 任一边超 MAX_FULL_DIFF_LINES 返回 null(降级,由调用方走「省略 diff」分支)。
|
|
19
|
+
*/
|
|
20
|
+
export declare function lineDiff(oldS: string, newS: string): DiffLine[] | null;
|
|
21
|
+
/**
|
|
22
|
+
* 渲染文件改动块(供 layout.contentWrite 写入)。
|
|
23
|
+
* - kind:'edit' 头行用 Update;kind:'write' 用 Update(覆盖)/ Create(oldStr===null 新建)。
|
|
24
|
+
* - oldStr===null 表示新建:正文只显 + 行,行号从 1。
|
|
25
|
+
* - startLine:diff 首行对应的文件行号(edit_file 由调用方定位 old_string 起始行;write_file 传 1)。
|
|
26
|
+
* - diff 过大(超 MAX_FULL_DIFF_LINES)时仅显头行 + 「file too large, diff omitted」。
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderFileChange(opts: {
|
|
29
|
+
path: string;
|
|
30
|
+
kind: 'edit' | 'write';
|
|
31
|
+
oldStr: string | null;
|
|
32
|
+
newStr: string;
|
|
33
|
+
startLine?: number;
|
|
34
|
+
}): string;
|
package/dist/ui/diff.js
CHANGED
|
@@ -191,14 +191,11 @@ function compactCtx(ops) {
|
|
|
191
191
|
}
|
|
192
192
|
return items;
|
|
193
193
|
}
|
|
194
|
-
function gutterOf(op
|
|
195
|
-
// restoreBg:add/del 行在 pushBody 处已铺底色,gutter 的 `${ui.reset}` 会清掉底色,所以
|
|
196
|
-
// reset 后必须重发 bg SGR,让该行后续字符继续带底色(否则 gutter 后一小段会变无色)。
|
|
197
|
-
const rb = restoreBg;
|
|
194
|
+
function gutterOf(op) {
|
|
198
195
|
if (op === 'del')
|
|
199
|
-
return `${ui.red}-${ui.reset}
|
|
196
|
+
return `${ui.red}-${ui.reset}`;
|
|
200
197
|
if (op === 'add')
|
|
201
|
-
return `${ui.green}+${ui.reset}
|
|
198
|
+
return `${ui.green}+${ui.reset}`;
|
|
202
199
|
return `${ui.dim} ${ui.reset}`;
|
|
203
200
|
}
|
|
204
201
|
function lineWord(n) {
|
|
@@ -264,41 +261,35 @@ function renderBody(head, counts, items, padW, startLine, lang) {
|
|
|
264
261
|
let overflow = 0;
|
|
265
262
|
const pushBody = (num, op, text) => {
|
|
266
263
|
const numStr = String(num).padStart(padW);
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// 才能让该行后续字符(代码区)继续带底色。
|
|
264
|
+
// 行级底色:仅代码区包裹主题底色(addBg/delBg),行号与 gutter 不着底色
|
|
265
|
+
// (避免整行被背景化,符合 GitHub / VSCode 视觉)。行末由 codeText 内置 reset 闭合 →
|
|
266
|
+
// bg 不污染下一行。
|
|
271
267
|
// 同时,代码区由 cli-highlight 渲染,内部会反复 ${fg}tok${reset}tok${fg}tok${reset}…
|
|
272
268
|
// 每个 reset 都会清掉外层 bg → 后面的 token 变无色。所以要在每个 reset 后重发 bg SGR
|
|
273
|
-
// (行末那枚 reset 是收尾的,后面紧跟换行而非字符,不需补 bg)
|
|
274
|
-
// 计数 = 内部重发点。
|
|
269
|
+
// (行末那枚 reset 是收尾的,后面紧跟换行而非字符,不需补 bg)。
|
|
275
270
|
const bg = op === 'add' ? ui.addBg : op === 'del' ? ui.delBg : '';
|
|
271
|
+
const rawCode = codeText(text, lang);
|
|
276
272
|
if (bg === '') {
|
|
277
|
-
lines.push(`${BODY_INDENT}${ui.gray}${numStr}${ui.reset} ${gutterOf(op)} ${
|
|
273
|
+
lines.push(`${BODY_INDENT}${ui.gray}${numStr}${ui.reset} ${gutterOf(op)} ${rawCode}`);
|
|
278
274
|
return;
|
|
279
275
|
}
|
|
280
|
-
const
|
|
276
|
+
const codeWithBg = `${bg}${rawCode}${ui.reset}`;
|
|
281
277
|
// 在每个非行末的 ${ui.reset} 后重发 bg。行末 reset 紧跟行尾或换行,无需补。
|
|
282
|
-
// 简单做法:把除最后一个 reset 外的所有 reset 后都补 bg——但要注意 cli-highlight
|
|
283
|
-
// 输出的代码区段内部还有"省略号截断"${ui.dim}…${ui.reset},它的 reset 也需补 bg。
|
|
284
278
|
// 用 split 走一遍:找出所有 reset 位置(除最后那个),在其后插入 bg。
|
|
285
279
|
const resetStr = ui.reset;
|
|
286
|
-
const lastResetIdx =
|
|
280
|
+
const lastResetIdx = codeWithBg.lastIndexOf(resetStr);
|
|
287
281
|
if (lastResetIdx < 0) {
|
|
288
|
-
lines.push(
|
|
282
|
+
lines.push(`${BODY_INDENT}${ui.gray}${numStr}${ui.reset} ${gutterOf(op)} ${codeWithBg}`);
|
|
289
283
|
return;
|
|
290
284
|
}
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const tail = raw.slice(lastResetIdx);
|
|
294
|
-
// 头部所有 reset 之后插 bg(注:用 split 重建)
|
|
285
|
+
const prefix = codeWithBg.slice(0, lastResetIdx);
|
|
286
|
+
const tail = codeWithBg.slice(lastResetIdx);
|
|
295
287
|
const parts = prefix.split(resetStr);
|
|
296
|
-
// parts[i] 是第 i 段,紧跟一段 reset(最后一段后无 reset,故少一个元素)
|
|
297
288
|
let rebuilt = parts[0];
|
|
298
289
|
for (let i = 1; i < parts.length; i++) {
|
|
299
290
|
rebuilt += resetStr + bg + parts[i];
|
|
300
291
|
}
|
|
301
|
-
lines.push(rebuilt + tail);
|
|
292
|
+
lines.push(`${BODY_INDENT}${ui.gray}${numStr}${ui.reset} ${gutterOf(op)} ${rebuilt + tail}`);
|
|
302
293
|
};
|
|
303
294
|
for (const it of items) {
|
|
304
295
|
if (shown >= MAX_BODY_LINES) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 介入面板:agent 执行中遇到需要用户决策的岔路时,弹问题 + 选项让用户选择(ask_human 工具调用)。
|
|
3
|
+
* 仿 Claude Code 的 ask / AskUserQuestion:用户可挑预设选项,也可选"自定义输入"自由作答。
|
|
4
|
+
*
|
|
5
|
+
* 渲染复用 promptTurnPicker 的模式(layout.paintInput:菜单向上展开进内容区底、提示/输入放底栏),
|
|
6
|
+
* 不碰固定边框、不用 DECSC/DECRC、逐行 \x1B[2K 擦除(符合 TUI 不变量)。
|
|
7
|
+
*
|
|
8
|
+
* 两个冲突由本函数自行处理(因 ask_human 在 executeTool 内被调,此时 agent 处 RUNNING 态):
|
|
9
|
+
* 1. spinner 在转(agent 在 await executeTool 前调了 spinner.start('执行 ask_human'),onFrame 每 80ms
|
|
10
|
+
* paintLiveAtCursor 画帧会覆盖面板)→ 进入前 Spinner.pauseCurrent() 停转。
|
|
11
|
+
* 2. repl 的 onRunningKey 在整个 runAgent 期间已挂载(↑/↓ 当滚动、Ctrl+C 当中断、可打印键当 typeahead
|
|
12
|
+
* 回显)→ 快照并摘掉现有 keypress 监听、挂自己的 onKey,退出 finally 按原序恢复。
|
|
13
|
+
* 滚轮:面板内滚轮只滚动查看 agent 输出内容(↑↓ 才选菜单),与 onRunningKey 一致;菜单经 scrollBy 末尾
|
|
14
|
+
* repaint()→paintInput(lastView) 重画,不被 viewport 滚动覆盖。进入时 resetScroll 回尾,退出时若滚过再回尾。
|
|
15
|
+
* 退出时 paintInput(dim,menu:null) 擦菜单 + 复位 lastMenuRows,再 resetScroll(面板内滚过)/repaintViewport
|
|
16
|
+
* 从缓冲重画内容区(恢复被菜单覆盖的 ● ask_human 行);顺序不可反——反了 stale lastMenuRows 会擦掉已恢复的内容。
|
|
17
|
+
*
|
|
18
|
+
* 非 TTY(管道/CI):不弹面板,choice 自动选第一项、input 返回 seed/空,打一行 stderr 日志,不阻塞。
|
|
19
|
+
*/
|
|
20
|
+
export type InterventionType = 'choice' | 'input';
|
|
21
|
+
/** choice 单个选项:label 是选项本身(选中后回传的值),detail 是选它意味着什么/取舍(可选,渲染时与 label 同行、用全角括号()包裹)。 */
|
|
22
|
+
export interface ChoiceOption {
|
|
23
|
+
label: string;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface InterventionRequest {
|
|
27
|
+
type: InterventionType;
|
|
28
|
+
/** 问题/标题,显示在面板顶部。 */
|
|
29
|
+
title: string;
|
|
30
|
+
/** choice 的选项列表(2~6 个);为空则降级 input。纯字符串 = 无说明,等价 {label}。 */
|
|
31
|
+
options?: Array<string | ChoiceOption>;
|
|
32
|
+
/** 背景说明,显示在标题下(可多行)。 */
|
|
33
|
+
detail?: string;
|
|
34
|
+
/** input 模式预填文本。 */
|
|
35
|
+
seed?: string;
|
|
36
|
+
/** choice 末尾是否追加「其他(自定义输入)」项。默认 true;只有调用方确实想关掉才传 false。 */
|
|
37
|
+
allowCustom?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface InterventionResult {
|
|
40
|
+
/** selected=选中某选项;submitted=提交自定义文本;cancelled=用户取消(Esc/Ctrl+C)。 */
|
|
41
|
+
action: 'selected' | 'submitted' | 'cancelled';
|
|
42
|
+
/** selected=选项文本;submitted=输入文本;cancelled 时无。 */
|
|
43
|
+
value?: string;
|
|
44
|
+
}
|
|
45
|
+
/** 弹出介入面板,阻塞直到用户完成选择。 */
|
|
46
|
+
export declare function promptIntervention(req: InterventionRequest): Promise<InterventionResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全屏 TUI 布局(参考 Claude Code):alt screen + 单滚动区域(内容区)+ 区域外固定底栏(状态行 + 输入框)。
|
|
3
|
+
*
|
|
4
|
+
* 只依赖最成熟、Windows(WT / conhost)最稳的 ANSI 子集:
|
|
5
|
+
* - alt screen \x1B[?1049h / \x1B[?1049l
|
|
6
|
+
* - DECSTBM 滚动区域 \x1B[<top>;<bottom>r (区域内写满自动在区域内滚动,区域外底栏不被顶)
|
|
7
|
+
* - 逐行擦除 \x1B[2K / \x1B[K (菜单 / 清屏,绝不用 ED \x1B[J 做边界擦除)
|
|
8
|
+
* - CUP 绝对定位 \x1B[<row>;<col>H
|
|
9
|
+
* 不用原点模式(\x1B[?6h)、不依赖 ED 边界、不用 DECSC/DECRC、不用 IL/DL/SU/SD——这些在 conhost 上不可靠。
|
|
10
|
+
*
|
|
11
|
+
* 续写位(contentRow / contentCol)由 contentWrite 按字符宽度 + 折行模拟跟踪;CUP 到它即可随时回到内容续写位,
|
|
12
|
+
* 故底栏刷新(状态行 / spinner)无需保存/恢复光标——CUP 回续写位即可。
|
|
13
|
+
*
|
|
14
|
+
* 关键洞察:RUNNING 态也支持 typeahead 输入与滚动回看。INPUT 态光标常驻输入框;RUNNING 态光标在续写位,
|
|
15
|
+
* 运行中打的字经 paintRunningInputEcho 作 dim 回显(占位行换成已打文本,光标仍留续写位,不与流式争用);
|
|
16
|
+
* 运行中可滚动回看——contentWrite 在 scrollOffset>0 时只喂缓冲不物理写(否则新流式覆盖 viewport 历史行),
|
|
17
|
+
* 回尾(scrollOffset===0)才 cup 续写位写出。两态切换不重设区域(底栏高度恒 = 2),仅多行输入换行撑高时才 setRegion。
|
|
18
|
+
*
|
|
19
|
+
* 非 TTY:全部空操作,contentWrite 退化为 stdout.write,与改造前内联行为一致。
|
|
20
|
+
*/
|
|
21
|
+
export interface Geo {
|
|
22
|
+
rows: number;
|
|
23
|
+
cols: number;
|
|
24
|
+
footerH: number;
|
|
25
|
+
contentTop: number;
|
|
26
|
+
contentBottom: number;
|
|
27
|
+
}
|
|
28
|
+
export interface StatusBarData {
|
|
29
|
+
model: string;
|
|
30
|
+
contextBar: string;
|
|
31
|
+
cwd: string;
|
|
32
|
+
status: string;
|
|
33
|
+
spinnerFrame?: string;
|
|
34
|
+
modeTag?: string;
|
|
35
|
+
/** 活跃 plan 短摘要(无 plan=undefined,空串=有 plan 但 chip 不显)。repl 透传 getActivePlanSummary()。 */
|
|
36
|
+
planSummary?: string;
|
|
37
|
+
/** 本轮最后一条 token 用量(modeTag chip 右边显示)。后端不开 include_usage 时 undefined。
|
|
38
|
+
* cachedTokens 可选 —— 后端未报或老 ChatUsage 字面量不带此字段时 UI 不会显示缓存标记。 */
|
|
39
|
+
lastTurnUsage?: {
|
|
40
|
+
promptTokens: number;
|
|
41
|
+
completionTokens: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
cachedTokens?: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface InputView {
|
|
47
|
+
prompt: string;
|
|
48
|
+
lines: string[];
|
|
49
|
+
cursorLine: number;
|
|
50
|
+
cursorCol: number;
|
|
51
|
+
menu: {
|
|
52
|
+
lines: string[];
|
|
53
|
+
} | null;
|
|
54
|
+
dim?: boolean;
|
|
55
|
+
placeholder?: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function getGeo(fh?: number): Geo;
|
|
58
|
+
/** (重)设滚动区域 [1, rows-footerH];设完 \x1B[H 归位(conhost 设 DECSTBM 时光标留旧位置行为未定义)。 */
|
|
59
|
+
export declare function setRegion(fh: number): Geo;
|
|
60
|
+
export declare function contentMode(): void;
|
|
61
|
+
/** 运行态用户是否在打字(近期有按键)——是则暂停流式物理写(contentWrite/drawStatusBar/paintLiveAtCursor 跳过物理写),光标留输入框,IME 候选窗稳定不跟流式跑。 */
|
|
62
|
+
export declare function isStreamingPaused(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* 运行态用户打字时调(repl onRunningKey 每次按键):标记活跃 + 重置 flush 定时器。
|
|
65
|
+
* 活跃期间流式只喂缓冲不物理写(光标不入内容区);用户停手 USER_ACTIVE_PAUSE_MS 后 flush:
|
|
66
|
+
* repaintViewport 重画缓冲内容 + drawStatusBar 刷状态行 + 光标归输入框。
|
|
67
|
+
* 解决 IME 候选窗跟流式输出跑:流式写必须 cup 到 contentRow,IME 逐光标跟踪→气泡跟跑;打字时暂停写则光标不动。
|
|
68
|
+
*/
|
|
69
|
+
export declare function setUserActive(): void;
|
|
70
|
+
/**
|
|
71
|
+
* 内容区正文写:CUP 到续写位 + 写出 + 按字符宽度/折行更新续写位 + 同步入 content 缓冲(供滚动回看)。
|
|
72
|
+
* 滚动区域内写满自动在区域内滚动,底栏在区域外不被顶。非 TTY / 未激活:直接 stdout.write(内联退化)。
|
|
73
|
+
*
|
|
74
|
+
* 逐字符分类(按码点取字符,SGR 序列整段识别):SGR 码 → 0 宽跟踪 + content.feedSgr;其他 CSI/OSC → 跳过
|
|
75
|
+
* (callers 理论上不传);\n / 折行 / tab 换行 → 行推进 + content.breakRow;可见字符 → charWidth 跟踪 +
|
|
76
|
+
* content.feedChar。SGR 在终端 0 宽,故续写位只按可见字符推进(与终端实际光标一致)。
|
|
77
|
+
*/
|
|
78
|
+
export declare function contentWrite(s: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* markdown 正文写(替代 contentWrite 用于 agent onText):累积 chunk 到 mdBuf,每 chunk 把整段
|
|
81
|
+
* mdBuf 经 renderMarkdown 渲成自洽 ANSI 行,replace 缓冲段(content.setLines 截旧 + 写新),
|
|
82
|
+
* repaintViewport 重画(仅尾 offset=0 且未暂停)。流式安全:未闭合 fence 照常 emit 进行中代码块;
|
|
83
|
+
* renderMarkdown 内部 memo by text 使重复渲染命中缓存。非 TTY / 未激活:直接 stdout.write(与
|
|
84
|
+
* contentWrite 一致,管道流式可见)。
|
|
85
|
+
*
|
|
86
|
+
* 续写位 = 段末下一行(段占 lines.length 行从 segmentStartRow 起);超可视区则滚动留 contentBottom。
|
|
87
|
+
* 物理重画用 repaintViewport(全内容区,原子一次 write 无闪烁)— md 段是缓冲尾,viewport 显尾即显段。
|
|
88
|
+
* 滚动回看(scrollOffset>0)只更新缓冲不物理写(回尾时显);打字中照常物理写——单次 write 结尾 cup 回输入框,IME 锚定不动。
|
|
89
|
+
*/
|
|
90
|
+
export declare function contentWriteMd(s: string): void;
|
|
91
|
+
/**
|
|
92
|
+
* 把一段完整(非流式)assistant 文本一次性渲染成 markdown 写入(供 renderHistory 回显
|
|
93
|
+
* /resume / /rollback 复显上下文):渲染一次 → join('\n') → 经 contentWrite 增量写
|
|
94
|
+
* (无段 erase / 无 repaintViewport,无闪烁)。与流式 contentWriteMd 的区别:不累积 chunk、
|
|
95
|
+
* 不走段替换——一次性把渲染结果当普通带色文本喂入(渲染行各自 ≤ cols、自洽带色,contentWrite
|
|
96
|
+
* 的 SGR/折行状态机原样接纳)。非 TTY:退化为 stdout.write 原文(同 contentWrite)。
|
|
97
|
+
*/
|
|
98
|
+
export declare function contentWriteMdOnce(s: string): void;
|
|
99
|
+
/** 清空内容区(保留底栏):全屏清 + 重设区域 + 续写位归 (1,1) + 清缓冲 + 回尾。底栏由调用方随后重画。 */
|
|
100
|
+
export declare function clearContent(): void;
|
|
101
|
+
/**
|
|
102
|
+
* 撤销(rewind)内容区末尾 n 物理行 —— 供 "刚 echoInput 写下用户气泡、用户撤回"
|
|
103
|
+
* 把气泡从可视区与 buffer 一起拿掉。content.rewind 弹出 buffer 末 n 行;layout 这边
|
|
104
|
+
* 同步上溯 contentRow(contentTop 钳位,防下溢)、scrollOffset 钳到新 totalRows、
|
|
105
|
+
* 最后 repaintViewport 重画可视区(running 态回 runningCaretPos,input 态回续写位)。
|
|
106
|
+
*
|
|
107
|
+
* 与 clearContent 不同:只擦最近一段、保留更早内容与 buffer;光标归内容区底,
|
|
108
|
+
* 不是 (1,1)。常发生在 RUNNING 态的 pending 窗口内 recall —— recall 后调用方
|
|
109
|
+
* 通常再 enterInputMode 切回 INPUT 视觉,光标由后续 paintInput 重画。
|
|
110
|
+
*/
|
|
111
|
+
export declare function rewindContent(rowsToRewind: number): void;
|
|
112
|
+
/**
|
|
113
|
+
* 在绝对行索引 after 后插入自洽行(详情展开用)。
|
|
114
|
+
* 滚动回看冻结:scrollOffset>0 时 offset += delta 把窗口冻在原绝对行,
|
|
115
|
+
* 与 contentWrite/contentWriteMd 同模式。插入后 repaintViewport 重画 viewport。
|
|
116
|
+
*
|
|
117
|
+
* 续写位(contentRow):若原写头在插入点之后,前移 lines.length,保持相对位置;
|
|
118
|
+
* 若原写头 ≤ after,不变(新行在写头之后)。非 TTY 直接调 content.insertAfter。
|
|
119
|
+
*/
|
|
120
|
+
export declare function contentInsertAfter(after: number, lines: string[]): void;
|
|
121
|
+
/**
|
|
122
|
+
* 从绝对行索引 startIdx 起删 n 行(折叠回退用)。
|
|
123
|
+
* 滚动回看冻结同 insertAfter:offset -= delta,钳 ≥ 0。续写位若在删区后前移 delta。
|
|
124
|
+
*/
|
|
125
|
+
export declare function contentDeleteFrom(startIdx: number, n: number): void;
|
|
126
|
+
/** 当前内容物理行总数(含当前未提交行)。供 batch 渲染器在 endBatch 时定位摘要行索引。 */
|
|
127
|
+
export declare function totalRows(): number;
|
|
128
|
+
/** 清空内容区时通知 batch 渲染器重置(摘要行映射与展开态)。 */
|
|
129
|
+
export declare function notifyContentReset(): void;
|
|
130
|
+
/** 是否处于滚动回看态(offset>0,内容区显历史)。prompt 据此在非滚动键时回尾。 */
|
|
131
|
+
export declare function isScrolled(): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* 重画内容区 viewport:按 scrollOffset 取缓冲尾窗,逐行 cup+clearline+rowtext 映射到屏 1..contentBottom。
|
|
134
|
+
* offset=0 即尾窗(== 实时屏,resize / 回尾时用)。清行含 contentBottom——顺带擦 WT 边距漏影(状态行重复)。
|
|
135
|
+
* 有活跃选区(鼠标拖拽中)时,对选中范围套反白——纯视觉,不影响缓冲内容。
|
|
136
|
+
*/
|
|
137
|
+
export declare function repaintViewport(): void;
|
|
138
|
+
/** 滚动 delta 行(正=往新、负=往旧);钳 [0, max(0, total-contentBottom)];变则重画 + 刷底栏(显指示、光标回输入框)。 */
|
|
139
|
+
export declare function scrollBy(delta: number): void;
|
|
140
|
+
/** 清活跃选区(不复制),若原有选区则重画去掉反白。供 ESC / 点击别处 / 退出滚动态等场景调。 */
|
|
141
|
+
export declare function clearSelection(): void;
|
|
142
|
+
/**
|
|
143
|
+
* 注册"点击输入框粘贴"回调:当前持有文本输入的模块(prompt.ts 的 promptWithSlashMenu / repl 运行态
|
|
144
|
+
* typeahead)在挂键盘监听时调,退出时传 null 注销。点击输入行(未拖动)时 layout 读剪贴板后回调此函数,
|
|
145
|
+
* 由回调方完成插入(各输入状态的行/光标结构不同,layout 不掺和文本编辑逻辑,只管"贴入"这个动作触发)。
|
|
146
|
+
*/
|
|
147
|
+
export declare function setPasteHandler(fn: ((text: string) => void) | null): void;
|
|
148
|
+
/** 注册"点击输入框 → 改光标"回调:prompt.ts 等文本所有者挂此回调后,
|
|
149
|
+
* 鼠标左键落输入行算出的新 (line, col) 会在 prompt 下次 redraw 前喂入此回调
|
|
150
|
+
* (prompt 用它写自己的 cl / cc,真正改 source of truth)。
|
|
151
|
+
* 传 null 注销(在 cleanup / 退出输入态时调,防下个 prompt 实例被旧回调污染)。 */
|
|
152
|
+
export declare function setCursorChangeHandler(fn: ((line: number, col: number) => void) | null): void;
|
|
153
|
+
/** picker / 介入面板期间禁用鼠标选区与拖拽(避免 viewport 重画覆盖菜单);滚轮仍可用。面板退出后恢复。 */
|
|
154
|
+
export declare function setMouseEnabled(v: boolean): void;
|
|
155
|
+
/** 回尾(offset=0);仅当原本滚动过才重画(避免每轮 enterRunningMode 闪烁)。 */
|
|
156
|
+
export declare function resetScroll(): void;
|
|
157
|
+
/** 发消息轮首短时锁住滚动:吸收 stdin 残留滚轮事件(发消息前后的滚轮惯性 / OS 缓冲延迟到达),
|
|
158
|
+
* 防 resetScroll 回尾后被 onRunningKey 接到的残留事件重新滚上去——致 agent 输出进缓冲、显示在历史区。
|
|
159
|
+
* 锁只挡 scrollBy(滚轮 / PgUp-PgDn);不影响 contentWrite 写屏(offset=0 时照常物理写,agent 输出从底部开始)。
|
|
160
|
+
* 默认 SCROLL_LOCK_MS 后自动解锁;enterInputMode(轮末)也清锁。用户轮中后段仍可上滑(满足"输出时能看历史")。 */
|
|
161
|
+
export declare function lockScrollToBottom(ms?: number): void;
|
|
162
|
+
/** 解锁(轮末 enterInputMode / 测试用)。 */
|
|
163
|
+
export declare function unlockScroll(): void;
|
|
164
|
+
/** 轮首滚动锁是否生效(测试用)。 */
|
|
165
|
+
export declare function isScrollLocked(): boolean;
|
|
166
|
+
/** 画状态行(plan 行 + spinner 行 + model 行,三行)。RUNNING 态 spinner 频繁调。
|
|
167
|
+
* 行号(footerH=6):
|
|
168
|
+
* plan 行 = contentBottom+1 (活跃 plan 时显 chip;无则空)
|
|
169
|
+
* spinner 行 = contentBottom+2 (◆ 空闲 / ⠹ 思考中… / etc)
|
|
170
|
+
* 上线 = contentBottom+3 (画在 paintInput)
|
|
171
|
+
* 输入行 = contentBottom+4
|
|
172
|
+
* 下线 = contentBottom+5
|
|
173
|
+
* model 行 = rows (屏底:auto + ctx + cwd) */
|
|
174
|
+
export declare function drawStatusBar(status?: StatusBarData): void;
|
|
175
|
+
/** 更新易变状态(状态文字 + spinner 帧)并重画状态行。spinner / agent 用。 */
|
|
176
|
+
export declare function setStatus(status: string, frame?: string): void;
|
|
177
|
+
/**
|
|
178
|
+
* 在续写位画一行瞬时活动文本(spinner 帧):不进缓冲、不推进续写位,逐行 clearLine 重画。
|
|
179
|
+
* 仅 TTY + offset=0(实时尾)+ 非打字暂停态时物理写屏;滚动态跳过(由状态行 spinner 兜底,且避免覆盖 viewport 历史行)。
|
|
180
|
+
* 配合 clearLiveAtCursor 在 spinner 停时清掉,随后 contentWrite 的结果即写在该行(spinner 不入历史缓冲)。
|
|
181
|
+
*
|
|
182
|
+
* 记 frameRow/frameCol = 实际画帧位置;clearLiveAtCursor 清"这行"而非当前续写位,防续写位漂移时清错行。
|
|
183
|
+
* 续写位漂移时(运行期间被某次 contentWrite 推进,罕见但 557e678 后可能)先清旧 frameRow 再画新位,避免旧帧残留。
|
|
184
|
+
* 打字暂停态(isStreamingPaused)spinner 不画帧——恢复 557e678 前的 spinner 隐形行为(只关 spinner,不动 contentWrite)。
|
|
185
|
+
*/
|
|
186
|
+
export declare function paintLiveAtCursor(text: string): void;
|
|
187
|
+
/** 清掉 paintLiveAtCursor 画过的那行瞬时活动文本。清"实际画过的位置"(frameRow),非当前续写位。
|
|
188
|
+
* 不加 isStreamingPaused 守卫——stop 时必须无条件清(只写一次、结尾 cup 回输入框,不扰 IME);否则打字中 stop 会跳过清帧、制造泄漏。
|
|
189
|
+
* 滚动态(scrollOffset≠0)也必须清残留帧——否则旧 spinner 帧卡在历史视图某行(「思考中在消息堆里」根因)。 */
|
|
190
|
+
export declare function clearLiveAtCursor(): void;
|
|
191
|
+
/** 更新状态行基线(模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token chip)。repl 在轮次边界与切模式时调。 */
|
|
192
|
+
export declare function setStatusBase(b: {
|
|
193
|
+
model: string;
|
|
194
|
+
contextBar: string;
|
|
195
|
+
cwd: string;
|
|
196
|
+
modeTag?: string;
|
|
197
|
+
planSummary?: string;
|
|
198
|
+
lastTurnUsage?: {
|
|
199
|
+
promptTokens: number;
|
|
200
|
+
completionTokens: number;
|
|
201
|
+
totalTokens: number;
|
|
202
|
+
};
|
|
203
|
+
}): void;
|
|
204
|
+
/**
|
|
205
|
+
* 画输入区:擦旧菜单 → (必要时)setRegion → 画状态行 + 输入行 + 向上菜单,光标留输入框(dim 时回续写位)。
|
|
206
|
+
* prompt.ts 每次按键调;enterInputMode / enterRunningMode 也调(空 / dim)。
|
|
207
|
+
*/
|
|
208
|
+
export declare function paintInput(view: InputView): void;
|
|
209
|
+
/**
|
|
210
|
+
* 运行态 typeahead 回显:定向写输入行(底栏输入框),把 dim 占位换成已打字文本 + 反白块状光标(无打字时光标在起点)。
|
|
211
|
+
* 只 cup 输入行 + clearLine + dim 文本 + 归位——不调 setRegion(运行中禁多行,避免 DECSTBM 抖动)、
|
|
212
|
+
* 不重画状态行/contentBottom、不用 ED。同步 lastView 为 dim 视图(text 与 placeholder 拆开存),使 scrollBy/resize
|
|
213
|
+
* 的 repaint 仍显当前回显 + 光标。真光标归续写位(滚动回看时归内容区底)——不入输入框,假光标已画在行内。
|
|
214
|
+
*/
|
|
215
|
+
export declare function paintRunningInputEcho(text: string, placeholder: string): void;
|
|
216
|
+
/** 重画当前视图(resize / 内部用)。 */
|
|
217
|
+
export declare function repaint(): void;
|
|
218
|
+
/** 进入输入态:画空输入框 + 状态行,光标入输入框。 */
|
|
219
|
+
export declare function enterInputMode(status?: string): void;
|
|
220
|
+
/** 进入运行态:底栏输入行改 dim 占位,光标回续写位。footerH 恒 6(虚拟空+spinner行+上线+输入+下线+model行)。新轮回尾(确保新内容可见)。 */
|
|
221
|
+
export declare function enterRunningMode(status: string, placeholder: string): void;
|
|
222
|
+
export declare function enterAltScreen(): void;
|
|
223
|
+
/** 复原:复位 margins + 显光标 + 退 alt + 还原 raw。幂等。 */
|
|
224
|
+
export declare function exitAltScreen(): void;
|
|
225
|
+
export declare function isActive(): boolean;
|
package/dist/ui/layout.js
CHANGED
|
@@ -34,6 +34,8 @@ let runningFrame = -1;
|
|
|
34
34
|
let userActiveUntil = 0; // 打字活跃截止时刻(Date.now()+PAUSE);0=未活跃
|
|
35
35
|
let flushTimer = null; // 用户停手后 flush 缓冲内容(repaintViewport)
|
|
36
36
|
const USER_ACTIVE_PAUSE_MS = 1500;
|
|
37
|
+
/** Sticky banner 的 ❯ 前缀(同 repl 的 PROMPT;常量统一视觉)。 */
|
|
38
|
+
const BANNER_PROMPT = '❯ ';
|
|
37
39
|
let lastView = null;
|
|
38
40
|
let lastMenuStartRow = 0; // 上次菜单起始屏行(供擦除)
|
|
39
41
|
let lastMenuRows = 0;
|
|
@@ -443,6 +445,11 @@ export function contentInsertAfter(after, lines) {
|
|
|
443
445
|
const delta = content.totalRows() - totalBefore;
|
|
444
446
|
if (delta === 0)
|
|
445
447
|
return;
|
|
448
|
+
// 流式 md 段活跃期间:插入点在段起点之前 → segmentStartRow 平移,
|
|
449
|
+
// 否则 contentWriteMd 的续写位算错,展开行被下一个 setLines 砍掉。
|
|
450
|
+
if (mdActive && after + 1 < segmentStartRow) {
|
|
451
|
+
segmentStartRow += delta;
|
|
452
|
+
}
|
|
446
453
|
// 续写位:原写头绝对行 = viewport 起点 + (contentRow-1) [offset=0];偏移后才一致
|
|
447
454
|
if (scrollOffset === 0 && contentRow > after + 1) {
|
|
448
455
|
contentRow = Math.min(contentRow + delta, g.contentBottom);
|
|
@@ -469,6 +476,11 @@ export function contentDeleteFrom(startIdx, n) {
|
|
|
469
476
|
const delta = totalBefore - content.totalRows();
|
|
470
477
|
if (delta === 0)
|
|
471
478
|
return;
|
|
479
|
+
// 流式 md 段活跃期间:删除区间全部在段起点之前 → segmentStartRow 回退,
|
|
480
|
+
// 否则 contentWriteMd 的续写位算错,段内容错位。
|
|
481
|
+
if (mdActive && startIdx + delta <= segmentStartRow) {
|
|
482
|
+
segmentStartRow = Math.max(1, segmentStartRow - delta);
|
|
483
|
+
}
|
|
472
484
|
if (scrollOffset === 0 && contentRow > startIdx + 1) {
|
|
473
485
|
contentRow = Math.max(1, contentRow - delta);
|
|
474
486
|
}
|
|
@@ -562,6 +574,12 @@ function highlightRange(line, colStart, colEnd) {
|
|
|
562
574
|
* 重画内容区 viewport:按 scrollOffset 取缓冲尾窗,逐行 cup+clearline+rowtext 映射到屏 1..contentBottom。
|
|
563
575
|
* offset=0 即尾窗(== 实时屏,resize / 回尾时用)。清行含 contentBottom——顺带擦 WT 边距漏影(状态行重复)。
|
|
564
576
|
* 有活跃选区(鼠标拖拽中)时,对选中范围套反白——纯视觉,不影响缓冲内容。
|
|
577
|
+
*
|
|
578
|
+
* **滚动回看 sticky banner(输入框上方固定标题的姊妹需求)**:
|
|
579
|
+
* 仅在 scrollOffset > 0 时,在 viewport 第 1 行顶部叠一行横幅,显示「当前 viewport 顶上
|
|
580
|
+
* 那条用户消息」的预览。用户上滑翻历史时,横幅内容随滚动到的 user→agent 对应关系变化;
|
|
581
|
+
* 滑到底(offset===0)自动消失(实时屏可见,无需 banner)。满宽 padding + 底色对比反色
|
|
582
|
+
* 提示「↑ 这是上方滚走的内容」,不与内容区行内 SGR 冲突。
|
|
565
583
|
*/
|
|
566
584
|
export function repaintViewport() {
|
|
567
585
|
if (!active)
|
|
@@ -570,11 +588,27 @@ export function repaintViewport() {
|
|
|
570
588
|
const h = g.contentBottom;
|
|
571
589
|
const slice = content.sliceFromEnd(scrollOffset, h);
|
|
572
590
|
const sel = normalizeSelection();
|
|
573
|
-
|
|
591
|
+
// viewportAbsStart() 无选区时也用:sticky banner 必须按真实窗口头算,不能降级 0(否则 banner 永不出现)。
|
|
592
|
+
const absStart = viewportAbsStart();
|
|
593
|
+
// sticky banner:仅 scrollOffset>0 时显示;offset=0 即实时屏,user 气泡本来就在视口内,无需 banner。
|
|
594
|
+
const bannerText = scrollOffset > 0
|
|
595
|
+
? content.lastUserMessageBefore(absStart)
|
|
596
|
+
: null;
|
|
597
|
+
const BANNER_ROW = 1; // 横幅占 viewport 第 1 行(会把原第 1 行内容遮住 —— 1 行换"我在看啥"的可读性,可接受)
|
|
574
598
|
let p = '';
|
|
575
599
|
for (let r = 1; r <= h; r++) {
|
|
576
600
|
let line = slice[r - 1] ?? '';
|
|
577
|
-
if (
|
|
601
|
+
if (bannerText && r === BANNER_ROW) {
|
|
602
|
+
// banner 行:满宽 userBg + ❯ + 单行截断(多行 ⏎ 折叠)+ userBg 补到底 + 底线分隔短横
|
|
603
|
+
const cols = g.cols;
|
|
604
|
+
const oneLine = bannerText.replace(/\s*\n\s*/g, ' ⏎ ');
|
|
605
|
+
const promptW = displayWidth(BANNER_PROMPT);
|
|
606
|
+
const avail = Math.max(1, cols - promptW);
|
|
607
|
+
const truncated = truncateDisplayHead(oneLine, avail);
|
|
608
|
+
const padCount = Math.max(0, cols - promptW - displayWidth(truncated));
|
|
609
|
+
line = `${ui.userBg}${BANNER_PROMPT}${truncated}${' '.repeat(padCount)}${ui.reset}`;
|
|
610
|
+
}
|
|
611
|
+
else if (sel) {
|
|
578
612
|
const absLine = absStart + r - 1;
|
|
579
613
|
if (absLine >= sel.startLine && absLine <= sel.endLine) {
|
|
580
614
|
const lineW = ansiDisplayWidth(line);
|
|
@@ -1133,28 +1167,50 @@ function composeSpinnerLine(status, cols) {
|
|
|
1133
1167
|
function composeModelLine(status, cols) {
|
|
1134
1168
|
const ctx = status.contextBar; // 已带色
|
|
1135
1169
|
const ctxW = ansiDisplayWidth(ctx);
|
|
1136
|
-
// 左段:模式标识 + 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1170
|
+
// 左段:模式标识 + 切换提示(灰)+ 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1137
1171
|
const modeTag = status.modeTag ?? '';
|
|
1138
1172
|
const modeColor = modeTag === 'plan' ? ui.yellow : ui.brightCyan;
|
|
1139
1173
|
const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
|
|
1140
1174
|
const modeW = modeTag ? displayWidth(modeTag) : 0;
|
|
1175
|
+
// 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
|
|
1176
|
+
const HINT = 'Shift+Tab 切换模式';
|
|
1177
|
+
const hintW = modeTag ? displayWidth(HINT) : 0;
|
|
1178
|
+
const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
|
|
1141
1179
|
const tokChip = formatTurnTokenChip(status.lastTurnUsage);
|
|
1142
1180
|
const tokW = displayWidth(stripAnsi(tokChip));
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1181
|
+
// 合并左段:段间留 2 空格分隔。极窄时 hint 与 chip 都可能藏掉。
|
|
1182
|
+
// 优先级:modeTag(必) > chip(提示累计 token,有信息量)> hint(纯说明性,窄时最先省)。
|
|
1183
|
+
const sepMH = modePart && (hintPart || tokChip) ? ' ' : '';
|
|
1184
|
+
const sepHT = (hintPart && tokChip) ? ' ' : '';
|
|
1185
|
+
const leftStr = `${modePart}${sepMH}${hintPart}${sepHT}${tokChip}`;
|
|
1186
|
+
const leftW = modeW
|
|
1187
|
+
+ (modePart && (hintPart || tokChip) ? sepMH.length : 0)
|
|
1188
|
+
+ hintW
|
|
1189
|
+
+ ((hintPart && tokChip) ? sepHT.length : 0)
|
|
1190
|
+
+ tokW;
|
|
1147
1191
|
// 右段:ctx + sep + cwd,右端对齐。cwd 按预算截断,极窄(<6)隐藏。
|
|
1148
|
-
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd
|
|
1192
|
+
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd、再按 hint→chip 顺序省。
|
|
1149
1193
|
const minGap = 2;
|
|
1150
1194
|
let cwdBudget = cols - leftW - minGap - ctxW - STATUS_SEP_W - 1;
|
|
1151
1195
|
let cwd = cwdBudget >= 6 ? truncateDisplay(status.cwd, cwdBudget) : '';
|
|
1152
1196
|
let cwdW = displayWidth(cwd);
|
|
1153
1197
|
let rightStr = `${ctx}${STATUS_SEP}${ui.dim}${cwd}${ui.reset}`;
|
|
1154
1198
|
let rightW = ctxW + STATUS_SEP_W + cwdW;
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
|
|
1199
|
+
// 极窄:逐步降级——先藏 hint,再藏 token chip,只剩 modeTag 与右段挤。
|
|
1200
|
+
// 这样 80 列宽终端下 hint 和 chip 都能稳住,只 <50 列才退化到只剩 modeTag。
|
|
1201
|
+
if (leftW + minGap + rightW > cols) {
|
|
1202
|
+
// 优先藏 hint(纯说明,信息密度最低):仅留 modePart + sep + tokChip
|
|
1203
|
+
if (modePart && tokChip && hintPart) {
|
|
1204
|
+
const leftStr2 = `${modePart}${sepMH}${tokChip}`;
|
|
1205
|
+
const leftW2 = modeW + sepMH.length + tokW;
|
|
1206
|
+
if (leftW2 + minGap + rightW <= cols) {
|
|
1207
|
+
return twoColumn(leftStr2, leftW2, rightStr, rightW, cols);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// 再藏 token chip:仅留 modePart
|
|
1211
|
+
if (modePart && tokChip) {
|
|
1212
|
+
return twoColumn(modePart, modeW, rightStr, rightW, cols);
|
|
1213
|
+
}
|
|
1158
1214
|
}
|
|
1159
1215
|
return twoColumn(leftStr, leftW, rightStr, rightW, cols);
|
|
1160
1216
|
}
|