mocode-ai 1.3.9 → 1.4.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 +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { stdin } from 'node:process';
|
|
2
|
+
// ── 二次确认发送(MOCODE_CONFIRM_SEND=long 时启用,默认关)──
|
|
3
|
+
// 长 prompt 首次 Enter 只武装,窗口内再按一次才真发。默认 opt-in:新交互默认不打扰既有习惯。
|
|
4
|
+
export const CONFIRM_SEND_MS = 2000;
|
|
5
|
+
export const LONG_INPUT_CHARS = 120;
|
|
6
|
+
/** 二次确认模式:only 'long' 会启用(长/多行输入需按两次 Enter)。每次读 env,便于运行期改配置生效。 */
|
|
7
|
+
export function confirmSendMode() {
|
|
8
|
+
return process.env.MOCODE_CONFIRM_SEND === 'long' ? 'long' : 'never';
|
|
9
|
+
}
|
|
10
|
+
// ── 粘贴检测(块级 + 时间窗)──
|
|
11
|
+
// 用状态对象包裹模块级可变变量,使拆分到不同文件的 editor/pickers 都能直接读写同一状态。
|
|
12
|
+
// 块级:多字节大块(len>8)或含 CR/LF 的小块 = 粘贴(键盘单键 1 字节,Enter=\r 单字节)。
|
|
13
|
+
// 时间窗:每块重置 50ms 计时器,静默 50ms 即"粘贴结束"→ onPasteEnd。跨多块的大粘贴(块间 <50ms)累积进
|
|
14
|
+
// 同一个 pasteParts、末尾一次性落 chip——避免"首块成 chip、后续块泄成文本"。粘贴中 onKey 把键累积进
|
|
15
|
+
// pasteParts(不编辑 lines)。不启用 bracketed paste——emitKeypressEvents 会把 \x1B[200~ 标记当按键砸进输入框。
|
|
16
|
+
export const pasteState = {
|
|
17
|
+
pasting: false,
|
|
18
|
+
pasteParts: [],
|
|
19
|
+
pasteTimer: null,
|
|
20
|
+
/** 粘贴结束回调(prompt 注入:落 chip 或保留为文本) */
|
|
21
|
+
onPasteEnd: null,
|
|
22
|
+
};
|
|
23
|
+
let pasteDetectorInstalled = false;
|
|
24
|
+
export function ensurePasteDetector() {
|
|
25
|
+
if (pasteDetectorInstalled)
|
|
26
|
+
return;
|
|
27
|
+
pasteDetectorInstalled = true;
|
|
28
|
+
stdin.on('data', (chunk) => {
|
|
29
|
+
const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
30
|
+
// 剔除 SGR 鼠标报告(\x1B[<…M/m,连半截也匹配):拖拽选区时终端连续发 motion 报表,
|
|
31
|
+
// 一个 chunk 里可能拼多条(22+ 字符 > 16 阈值)误判粘贴 50ms、把后续真按键泄进 pasteParts。
|
|
32
|
+
const text = raw.replace(/\x1b\[<[0-9;]*[Mm]/g, '');
|
|
33
|
+
const hasNL = text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0;
|
|
34
|
+
// 按字符数(码点)判粘贴,非字节:CJK 汉字占 3 UTF-8 字节,旧阈值(len>8 字节)会把 IME 提交的
|
|
35
|
+
// 3-10 个汉字误判为粘贴 → 进 50ms 缓冲 → finalizePaste→insertText 落字(且旧 insertText 把光标
|
|
36
|
+
// 置插入文本长度而非末尾,致"后续打字插到行中间")。改:含换行(多行粘贴)或 >16 字符(大块单行
|
|
37
|
+
// 粘贴)才算粘贴;普通 IME 提交走正常按键路径(逐字直插、光标随进、无延迟)。
|
|
38
|
+
const charCount = [...text].length;
|
|
39
|
+
if (!(charCount > 16 || (charCount > 1 && hasNL)))
|
|
40
|
+
return;
|
|
41
|
+
pasteState.pasting = true;
|
|
42
|
+
if (pasteState.pasteTimer)
|
|
43
|
+
clearTimeout(pasteState.pasteTimer);
|
|
44
|
+
const t = setTimeout(() => {
|
|
45
|
+
pasteState.pasteTimer = null;
|
|
46
|
+
pasteState.pasting = false;
|
|
47
|
+
pasteState.onPasteEnd?.();
|
|
48
|
+
}, 50);
|
|
49
|
+
t.unref();
|
|
50
|
+
pasteState.pasteTimer = t;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { stdin } from 'node:process';
|
|
3
|
+
import { ui } from '../theme.js';
|
|
4
|
+
import { displayWidth, truncateDisplay } from '../render.js';
|
|
5
|
+
import * as layout from '../layout.js';
|
|
6
|
+
import * as mouse from '../mouse.js';
|
|
7
|
+
import { t } from '../../i18n/index.js';
|
|
8
|
+
import { ensurePasteDetector } from './paste.js';
|
|
9
|
+
/**
|
|
10
|
+
* 轮次选择菜单(供 /rollback 菜单化选择):↑/↓ 导航、Enter 选中、Esc/Ctrl+D 取消。
|
|
11
|
+
* 把 items 画成向上展开的菜单(经 layout.paintInput,与斜杠菜单同套渲染),输入框行作操作提示。
|
|
12
|
+
* 返回选中的 0-based 下标;null=取消 / 非 TTY / 空列表。纯导航(不收文本输入)。
|
|
13
|
+
* 长列表(超屏高)自动开窗保光标可见;默认聚焦末项(最新轮次,靠近输入框)。
|
|
14
|
+
*/
|
|
15
|
+
export async function promptTurnPicker(items) {
|
|
16
|
+
if (!layout.isActive() || items.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const emitter = stdin;
|
|
19
|
+
const hint = t('prompt.chooseRollback');
|
|
20
|
+
let selected = items.length - 1; // 默认聚焦最新(末项,菜单底、靠近输入框)
|
|
21
|
+
let resolved = false;
|
|
22
|
+
let resolve;
|
|
23
|
+
let reject;
|
|
24
|
+
/** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见;末项在底(靠近输入框)。 */
|
|
25
|
+
function menuLines() {
|
|
26
|
+
const g = layout.getGeo();
|
|
27
|
+
const maxRows = Math.max(1, g.contentBottom);
|
|
28
|
+
let start = 0;
|
|
29
|
+
if (items.length > maxRows) {
|
|
30
|
+
start = Math.max(0, Math.min(selected - Math.floor(maxRows / 2), items.length - maxRows));
|
|
31
|
+
}
|
|
32
|
+
const count = Math.min(maxRows, items.length);
|
|
33
|
+
const cols = g.cols;
|
|
34
|
+
return Array.from({ length: count }, (_, i) => {
|
|
35
|
+
const idx = start + i;
|
|
36
|
+
// 选中项:▸/序号/正文均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
|
|
37
|
+
const isSel = idx === selected;
|
|
38
|
+
const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
|
|
39
|
+
const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
|
|
40
|
+
const num = `${color}${idx + 1}${ui.reset}`;
|
|
41
|
+
const text = truncateDisplay(items[idx].firstLine, cols - 6);
|
|
42
|
+
return `${marker} ${num} ${color}${text}${ui.reset}`;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function redraw() {
|
|
46
|
+
layout.paintInput({
|
|
47
|
+
prompt: '❯ ',
|
|
48
|
+
lines: [hint],
|
|
49
|
+
cursorLine: 0,
|
|
50
|
+
cursorCol: displayWidth(hint),
|
|
51
|
+
menu: { lines: menuLines() },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function cleanup() {
|
|
55
|
+
layout.setMouseEnabled(true); // 恢复鼠标框选/滚轮(面板期间被禁,防拖拽覆盖菜单)
|
|
56
|
+
try {
|
|
57
|
+
stdin.setRawMode(false);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// 忽略
|
|
61
|
+
}
|
|
62
|
+
emitter.removeListener('keypress', onKey);
|
|
63
|
+
stdin.pause();
|
|
64
|
+
}
|
|
65
|
+
function finish(value) {
|
|
66
|
+
if (resolved)
|
|
67
|
+
return;
|
|
68
|
+
resolved = true;
|
|
69
|
+
cleanup();
|
|
70
|
+
resolve(value);
|
|
71
|
+
}
|
|
72
|
+
function onKey(_str, key) {
|
|
73
|
+
if (resolved || !key)
|
|
74
|
+
return;
|
|
75
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
76
|
+
return; // 鼠标 fragment 吞掉(框选已禁,滚轮 handleMouseEvent no-op)
|
|
77
|
+
if (key.ctrl && key.name === 'c') {
|
|
78
|
+
cleanup();
|
|
79
|
+
reject(new Error('SIGINT'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.ctrl && key.name === 'd') {
|
|
83
|
+
finish(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
switch (key.name) {
|
|
87
|
+
case 'up':
|
|
88
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
89
|
+
redraw();
|
|
90
|
+
return;
|
|
91
|
+
case 'down':
|
|
92
|
+
selected = (selected + 1) % items.length;
|
|
93
|
+
redraw();
|
|
94
|
+
return;
|
|
95
|
+
case 'return':
|
|
96
|
+
case 'enter':
|
|
97
|
+
finish(selected);
|
|
98
|
+
return;
|
|
99
|
+
case 'escape':
|
|
100
|
+
finish(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return new Promise((res, rej) => {
|
|
105
|
+
resolve = res;
|
|
106
|
+
reject = rej;
|
|
107
|
+
layout.setMouseEnabled(false); // 面板期间禁鼠标框选/滚轮(防拖拽 viewport 重画覆盖菜单)
|
|
108
|
+
ensurePasteDetector();
|
|
109
|
+
readline.emitKeypressEvents(stdin);
|
|
110
|
+
let rawOk = true;
|
|
111
|
+
try {
|
|
112
|
+
stdin.setRawMode(true);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
rawOk = false;
|
|
116
|
+
}
|
|
117
|
+
if (!rawOk) {
|
|
118
|
+
res(null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
stdin.resume();
|
|
122
|
+
emitter.on('keypress', onKey);
|
|
123
|
+
redraw();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 会话选择菜单(供 /resume 菜单化选择):↑/↓ 导航、Enter 续接、Esc/Ctrl+D 取消、a 切换「仅最近 N / 全部」。
|
|
128
|
+
* 把 items 画成向上展开的菜单(经 layout.paintInput,与斜杠菜单 / 轮次菜单同套渲染),输入框行作操作提示。
|
|
129
|
+
* 默认仅显示最近 N 条(默认 N=10,取 items 前缀——调用方按 createdAt 降序传入则前缀=最新 N);
|
|
130
|
+
* items 多于 N 时按 a 展开全部,再按 a 折回最近 N。选中项 cyan+bold + ▸ 高亮(同 promptTurnPicker)。
|
|
131
|
+
* 返回选中的 item(含 id);null=取消 / 非 TTY / 空列表。纯导航(不收文本输入)。
|
|
132
|
+
* 长列表(超屏高)自动开窗保光标可见;默认聚焦首项(最新会话,Enter 即续接最近一条)。
|
|
133
|
+
*/
|
|
134
|
+
export async function promptSessionPicker(items, recentCap = 10) {
|
|
135
|
+
if (!layout.isActive() || items.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const emitter = stdin;
|
|
138
|
+
const cap = Math.max(1, recentCap);
|
|
139
|
+
const canToggle = items.length > cap; // 不超过 cap 时无切换意义(本就全显)
|
|
140
|
+
let showAll = !canToggle; // 超过 cap 才默认折叠到最近 N,否则全显
|
|
141
|
+
let selected = 0; // 默认聚焦首项(调用方降序传入 → 最新会话)
|
|
142
|
+
let resolved = false;
|
|
143
|
+
let resolve;
|
|
144
|
+
let reject;
|
|
145
|
+
/** 当前可见项:折叠态取前 cap 条(=最近 N),展开态取全部。 */
|
|
146
|
+
function visible() {
|
|
147
|
+
return showAll ? items : items.slice(0, cap);
|
|
148
|
+
}
|
|
149
|
+
/** 输入框行的操作提示;折叠态显「a 全部(N)」,展开态显「a 仅最近N」。不超 cap 时无 a 项。 */
|
|
150
|
+
function hint() {
|
|
151
|
+
const base = t('prompt.chooseResume');
|
|
152
|
+
if (!canToggle)
|
|
153
|
+
return base;
|
|
154
|
+
return showAll ? t('prompt.recent', { count: cap }) : t('prompt.all', { count: items.length });
|
|
155
|
+
}
|
|
156
|
+
/** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见。行格式:▸ N title subtitle。 */
|
|
157
|
+
function menuLines() {
|
|
158
|
+
const g = layout.getGeo();
|
|
159
|
+
const cols = g.cols;
|
|
160
|
+
const maxRows = Math.max(1, g.contentBottom);
|
|
161
|
+
const vis = visible();
|
|
162
|
+
let start = 0;
|
|
163
|
+
if (vis.length > maxRows) {
|
|
164
|
+
start = Math.max(0, Math.min(selected - Math.floor(maxRows / 2), vis.length - maxRows));
|
|
165
|
+
}
|
|
166
|
+
const count = Math.min(maxRows, vis.length);
|
|
167
|
+
return Array.from({ length: count }, (_, i) => {
|
|
168
|
+
const idx = start + i;
|
|
169
|
+
// 选中项:▸/序号/正文/副标题均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
|
|
170
|
+
const isSel = idx === selected;
|
|
171
|
+
const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
|
|
172
|
+
const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
|
|
173
|
+
const num = String(idx + 1);
|
|
174
|
+
const it = vis[idx];
|
|
175
|
+
const title = it.title || '(无)';
|
|
176
|
+
const sub = it.subtitle ?? '';
|
|
177
|
+
const leadW = displayWidth(num) + 4; // "▸ " + num + " "
|
|
178
|
+
let subW = sub ? displayWidth(sub) + 2 : 0; // " " + sub
|
|
179
|
+
let titleW = cols - leadW - subW;
|
|
180
|
+
if (titleW < 4 && sub) {
|
|
181
|
+
// 太窄:先丢副标题把空间让给标题
|
|
182
|
+
subW = 0;
|
|
183
|
+
titleW = cols - leadW;
|
|
184
|
+
}
|
|
185
|
+
const titleT = titleW > 0 ? truncateDisplay(title, titleW) : '';
|
|
186
|
+
const subPart = subW > 0 ? ` ${sub}` : '';
|
|
187
|
+
return `${marker} ${color}${num} ${titleT}${subPart}${ui.reset}`;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function redraw() {
|
|
191
|
+
const h = hint();
|
|
192
|
+
layout.paintInput({
|
|
193
|
+
prompt: '❯ ',
|
|
194
|
+
lines: [h],
|
|
195
|
+
cursorLine: 0,
|
|
196
|
+
cursorCol: displayWidth(h),
|
|
197
|
+
menu: { lines: menuLines() },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function cleanup() {
|
|
201
|
+
layout.setMouseEnabled(true);
|
|
202
|
+
try {
|
|
203
|
+
stdin.setRawMode(false);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// 忽略
|
|
207
|
+
}
|
|
208
|
+
emitter.removeListener('keypress', onKey);
|
|
209
|
+
stdin.pause();
|
|
210
|
+
}
|
|
211
|
+
function finish(value) {
|
|
212
|
+
if (resolved)
|
|
213
|
+
return;
|
|
214
|
+
resolved = true;
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve(value);
|
|
217
|
+
}
|
|
218
|
+
function onKey(_str, key) {
|
|
219
|
+
if (resolved || !key)
|
|
220
|
+
return;
|
|
221
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
222
|
+
return;
|
|
223
|
+
if (key.ctrl && key.name === 'c') {
|
|
224
|
+
cleanup();
|
|
225
|
+
reject(new Error('SIGINT'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (key.ctrl && key.name === 'd') {
|
|
229
|
+
finish(null);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// a 切换「仅最近 N / 全部」:纯导航菜单无文本输入,a 自由;key.name 不分大小写,Shift+A 亦触发。
|
|
233
|
+
if (canToggle && key.name === 'a' && !key.ctrl && !key.meta) {
|
|
234
|
+
showAll = !showAll;
|
|
235
|
+
const vis = visible();
|
|
236
|
+
if (selected > vis.length - 1)
|
|
237
|
+
selected = vis.length - 1; // 折回 cap 时选中项越界则钳到末项
|
|
238
|
+
redraw();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const n = visible().length;
|
|
242
|
+
switch (key.name) {
|
|
243
|
+
case 'up':
|
|
244
|
+
selected = (selected - 1 + n) % n;
|
|
245
|
+
redraw();
|
|
246
|
+
return;
|
|
247
|
+
case 'down':
|
|
248
|
+
selected = (selected + 1) % n;
|
|
249
|
+
redraw();
|
|
250
|
+
return;
|
|
251
|
+
case 'return':
|
|
252
|
+
case 'enter':
|
|
253
|
+
finish(visible()[selected] ?? null);
|
|
254
|
+
return;
|
|
255
|
+
case 'escape':
|
|
256
|
+
finish(null);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return new Promise((res, rej) => {
|
|
261
|
+
resolve = res;
|
|
262
|
+
reject = rej;
|
|
263
|
+
layout.setMouseEnabled(false);
|
|
264
|
+
ensurePasteDetector();
|
|
265
|
+
readline.emitKeypressEvents(stdin);
|
|
266
|
+
let rawOk = true;
|
|
267
|
+
try {
|
|
268
|
+
stdin.setRawMode(true);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
rawOk = false;
|
|
272
|
+
}
|
|
273
|
+
if (!rawOk) {
|
|
274
|
+
res(null);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
stdin.resume();
|
|
278
|
+
emitter.on('keypress', onKey);
|
|
279
|
+
redraw();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 主题选择菜单(供 /theme):↑/↓ 导航、Enter 切换、Esc/Ctrl+D 取消、Ctrl+C 抛 SIGINT(调用方 try/catch)。
|
|
284
|
+
* 复用 SessionPickerItem 形状({id,title,subtitle?});选中项 cyan+bold + ▸ 高亮(同 /resume 菜单)。
|
|
285
|
+
* 返回选中的 item(含 id);null=取消 / 非 TTY / 空列表。纯导航(不收文本输入)。
|
|
286
|
+
* 长列表超屏高自动开窗保光标可见;默认聚焦首项。
|
|
287
|
+
*/
|
|
288
|
+
export async function promptThemePicker(items) {
|
|
289
|
+
if (!layout.isActive() || items.length === 0)
|
|
290
|
+
return null;
|
|
291
|
+
const emitter = stdin;
|
|
292
|
+
let selected = 0;
|
|
293
|
+
let resolved = false;
|
|
294
|
+
let resolve;
|
|
295
|
+
let reject;
|
|
296
|
+
const hint = t('prompt.chooseSwitch');
|
|
297
|
+
/** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见。行格式:▸ N title subtitle。 */
|
|
298
|
+
function menuLines() {
|
|
299
|
+
const g = layout.getGeo();
|
|
300
|
+
const cols = g.cols;
|
|
301
|
+
const maxRows = Math.max(1, g.contentBottom);
|
|
302
|
+
let start = 0;
|
|
303
|
+
if (items.length > maxRows) {
|
|
304
|
+
start = Math.max(0, Math.min(selected - Math.floor(maxRows / 2), items.length - maxRows));
|
|
305
|
+
}
|
|
306
|
+
const count = Math.min(maxRows, items.length);
|
|
307
|
+
return Array.from({ length: count }, (_, i) => {
|
|
308
|
+
const idx = start + i;
|
|
309
|
+
const isSel = idx === selected;
|
|
310
|
+
const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
|
|
311
|
+
const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
|
|
312
|
+
const num = String(idx + 1);
|
|
313
|
+
const it = items[idx];
|
|
314
|
+
const title = it.title || '(无)';
|
|
315
|
+
const sub = it.subtitle ?? '';
|
|
316
|
+
const leadW = displayWidth(num) + 4; // "▸ " + num + " "
|
|
317
|
+
let subW = sub ? displayWidth(sub) + 2 : 0; // " " + sub
|
|
318
|
+
let titleW = cols - leadW - subW;
|
|
319
|
+
if (titleW < 4 && sub) {
|
|
320
|
+
subW = 0;
|
|
321
|
+
titleW = cols - leadW;
|
|
322
|
+
}
|
|
323
|
+
const titleT = titleW > 0 ? truncateDisplay(title, titleW) : '';
|
|
324
|
+
const subPart = subW > 0 ? ` ${sub}` : '';
|
|
325
|
+
return `${marker} ${color}${num} ${titleT}${subPart}${ui.reset}`;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function redraw() {
|
|
329
|
+
layout.paintInput({
|
|
330
|
+
prompt: '❯ ',
|
|
331
|
+
lines: [hint],
|
|
332
|
+
cursorLine: 0,
|
|
333
|
+
cursorCol: displayWidth(hint),
|
|
334
|
+
menu: { lines: menuLines() },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function cleanup() {
|
|
338
|
+
layout.setMouseEnabled(true);
|
|
339
|
+
try {
|
|
340
|
+
stdin.setRawMode(false);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// 忽略
|
|
344
|
+
}
|
|
345
|
+
emitter.removeListener('keypress', onKey);
|
|
346
|
+
stdin.pause();
|
|
347
|
+
}
|
|
348
|
+
function finish(value) {
|
|
349
|
+
if (resolved)
|
|
350
|
+
return;
|
|
351
|
+
resolved = true;
|
|
352
|
+
cleanup();
|
|
353
|
+
resolve(value);
|
|
354
|
+
}
|
|
355
|
+
function onKey(_str, key) {
|
|
356
|
+
if (resolved || !key)
|
|
357
|
+
return;
|
|
358
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
359
|
+
return;
|
|
360
|
+
if (key.ctrl && key.name === 'c') {
|
|
361
|
+
cleanup();
|
|
362
|
+
reject(new Error('SIGINT'));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (key.ctrl && key.name === 'd') {
|
|
366
|
+
finish(null);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const n = items.length;
|
|
370
|
+
switch (key.name) {
|
|
371
|
+
case 'up':
|
|
372
|
+
selected = (selected - 1 + n) % n;
|
|
373
|
+
redraw();
|
|
374
|
+
return;
|
|
375
|
+
case 'down':
|
|
376
|
+
selected = (selected + 1) % n;
|
|
377
|
+
redraw();
|
|
378
|
+
return;
|
|
379
|
+
case 'return':
|
|
380
|
+
case 'enter':
|
|
381
|
+
finish(items[selected] ?? null);
|
|
382
|
+
return;
|
|
383
|
+
case 'escape':
|
|
384
|
+
finish(null);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return new Promise((res, rej) => {
|
|
389
|
+
resolve = res;
|
|
390
|
+
reject = rej;
|
|
391
|
+
layout.setMouseEnabled(false);
|
|
392
|
+
ensurePasteDetector();
|
|
393
|
+
readline.emitKeypressEvents(stdin);
|
|
394
|
+
let rawOk = true;
|
|
395
|
+
try {
|
|
396
|
+
stdin.setRawMode(true);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
rawOk = false;
|
|
400
|
+
}
|
|
401
|
+
if (!rawOk) {
|
|
402
|
+
res(null);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
stdin.resume();
|
|
406
|
+
emitter.on('keypress', onKey);
|
|
407
|
+
redraw();
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 回滚方式二选一菜单(供 /rollback 选完轮次后):↑/↓ 导航、Enter 确认、Esc/Ctrl+D 保留文件。
|
|
412
|
+
* 两个选项:1=撤销文件改动(恢复到回滚前);2=只撤销消息,保留文件改动。
|
|
413
|
+
* 复用轮次/会话菜单的渲染骨架(raw mode + layout.paintInput 的 menu),选中项 cyan+bold + ▸ 高亮。
|
|
414
|
+
* 返回 true=撤销文件 / false=只撤销消息 / null=取消(调用方按 false=保留文件处理)。
|
|
415
|
+
* 默认聚焦首项(撤销文件)。无文件改动时调用方不应调此菜单(直接走只撤销消息)。
|
|
416
|
+
*/
|
|
417
|
+
export async function promptRevertChoice(fileCount) {
|
|
418
|
+
if (!layout.isActive())
|
|
419
|
+
return null;
|
|
420
|
+
const emitter = stdin;
|
|
421
|
+
const hint = t('prompt.keepFiles');
|
|
422
|
+
const detail = fileCount > 0 ? t('prompt.revertDetail', { count: fileCount }) : '';
|
|
423
|
+
const items = [t('prompt.revertFiles', { detail }), t('prompt.messagesOnly')];
|
|
424
|
+
let selected = 0; // 默认聚焦首项(撤销文件)
|
|
425
|
+
let resolved = false;
|
|
426
|
+
let resolve;
|
|
427
|
+
let reject;
|
|
428
|
+
function menuLines() {
|
|
429
|
+
const cols = layout.getGeo().cols;
|
|
430
|
+
return items.map((text, idx) => {
|
|
431
|
+
const isSel = idx === selected;
|
|
432
|
+
const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
|
|
433
|
+
const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
|
|
434
|
+
const num = `${color}${idx + 1}${ui.reset}`;
|
|
435
|
+
const t = truncateDisplay(text, cols - 6);
|
|
436
|
+
return `${marker} ${num} ${color}${t}${ui.reset}`;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function redraw() {
|
|
440
|
+
layout.paintInput({
|
|
441
|
+
prompt: '❯ ',
|
|
442
|
+
lines: [hint],
|
|
443
|
+
cursorLine: 0,
|
|
444
|
+
cursorCol: displayWidth(hint),
|
|
445
|
+
menu: { lines: menuLines() },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function cleanup() {
|
|
449
|
+
layout.setMouseEnabled(true);
|
|
450
|
+
try {
|
|
451
|
+
stdin.setRawMode(false);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// 忽略
|
|
455
|
+
}
|
|
456
|
+
emitter.removeListener('keypress', onKey);
|
|
457
|
+
stdin.pause();
|
|
458
|
+
}
|
|
459
|
+
function finish(value) {
|
|
460
|
+
if (resolved)
|
|
461
|
+
return;
|
|
462
|
+
resolved = true;
|
|
463
|
+
cleanup();
|
|
464
|
+
resolve(value);
|
|
465
|
+
}
|
|
466
|
+
function onKey(_str, key) {
|
|
467
|
+
if (resolved || !key)
|
|
468
|
+
return;
|
|
469
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
470
|
+
return;
|
|
471
|
+
if (key.ctrl && key.name === 'c') {
|
|
472
|
+
cleanup();
|
|
473
|
+
reject(new Error('SIGINT'));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (key.ctrl && key.name === 'd') {
|
|
477
|
+
finish(null);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
switch (key.name) {
|
|
481
|
+
case 'up':
|
|
482
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
483
|
+
redraw();
|
|
484
|
+
return;
|
|
485
|
+
case 'down':
|
|
486
|
+
selected = (selected + 1) % items.length;
|
|
487
|
+
redraw();
|
|
488
|
+
return;
|
|
489
|
+
case 'return':
|
|
490
|
+
case 'enter':
|
|
491
|
+
finish(selected === 0); // true=撤销文件, false=只撤销消息
|
|
492
|
+
return;
|
|
493
|
+
case 'escape':
|
|
494
|
+
finish(null); // 取消=保留文件
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return new Promise((res, rej) => {
|
|
499
|
+
resolve = res;
|
|
500
|
+
reject = rej;
|
|
501
|
+
layout.setMouseEnabled(false);
|
|
502
|
+
ensurePasteDetector();
|
|
503
|
+
readline.emitKeypressEvents(stdin);
|
|
504
|
+
let rawOk = true;
|
|
505
|
+
try {
|
|
506
|
+
stdin.setRawMode(true);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
rawOk = false;
|
|
510
|
+
}
|
|
511
|
+
if (!rawOk) {
|
|
512
|
+
res(null);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
stdin.resume();
|
|
516
|
+
emitter.on('keypress', onKey);
|
|
517
|
+
redraw();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|