mocode-ai 1.4.0 → 1.4.2
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/mocode-agent-host.js +1 -1
- package/dist/permissions/index.js +3 -3
- package/dist/runtime/input-injector.js +1 -1
- package/dist/sandbox/jail.js +3 -3
- package/dist/session/trace-metrics.js +18 -7
- package/dist/tools/builtins/computer.js +1 -1
- package/dist/ui/batch.js +10 -15
- package/dist/ui/prompt-internal/editor-operations.js +56 -0
- package/dist/ui/prompt-internal/editor.js +20 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
84
84
|
- **Interruptible and reversible** — Ctrl+C interrupts the current turn at any time (kills child processes recursively, rolls history back to before the turn started, leaves no half-finished tool calls). `/rollback` restores file changes from per-turn snapshots, with a per-file keep/undo choice — no git dependency required.
|
|
85
85
|
- **Input safety net** — Long prompts no longer fear a stray Enter: `Ctrl+G` opens an in-TUI composer popup (notepad-style editing — Enter inserts a newline, with soft wrap, selection, copy/cut/paste and undo; Ctrl+S fills the text back into the input box without sending). `Ctrl+R`/`Ctrl+P` fuzzy-search your input history (Enter only fills it back), and the post-send recall window widens to 2 seconds with any-key recall for long inputs.
|
|
86
86
|
- **Sandbox protection** — File reads/writes go through a sandbox that blocks out-of-bounds paths (`../../`, absolute paths outside the root, symlink escapes, etc.), so the agent never touches files outside your working directory.
|
|
87
|
-
- **Computer Use (high-risk, routed only for explicit GUI intent)** — When the request genuinely requires real mouse/keyboard interaction, the router can expose the `computer-control` group and feed each resulting screenshot back to the model. `/cu off` (or `MOCODE_COMPUTER_USE_ENABLED=false`) is a hard veto; `/cu on` merely allows routing and does not keep the tool permanently visible. The blast radius exceeds file tools because OS input bypasses the file sandbox. **Use a VM / sandbox / dedicated test machine**, not a daily driver. Every action still passes the permission gate, and plan mode always blocks it. Windows first; macOS/Linux pending.
|
|
87
|
+
- **Computer Use (high-risk, routed only for explicit GUI intent)** — When the request genuinely requires real mouse/keyboard interaction, the router can expose the `computer-control` group and feed each resulting screenshot back to the model. `/cu off` (or `MOCODE_COMPUTER_USE_ENABLED=false`) is a hard veto; `/cu on` merely allows routing and does not keep the tool permanently visible. The blast radius exceeds file tools because OS input bypasses the file sandbox. **Use a VM / sandbox / dedicated test machine**, not a daily driver. Every action still passes the permission gate, and plan mode always blocks it. Windows first; macOS/Linux pending.
|
|
88
88
|
|
|
89
89
|
## Features
|
|
90
90
|
|
package/README.zh-CN.md
CHANGED
|
@@ -83,7 +83,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
|
83
83
|
- **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
|
|
84
84
|
- **输入安全网** — 长 prompt 不再怕误按 Enter:`Ctrl+G` 弹出 TUI 内「输入面板」(记事本式编辑,Enter=换行、软换行、选区、复制/剪切/粘贴、撤销,Ctrl+S 填回输入框不自动发送);`Ctrl+R`/`Ctrl+P` 模糊搜索历史输入(Enter 只回填不发送);长文本误发后撤回窗口自动放宽到 2 秒且任意键可撤回。
|
|
85
85
|
- **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
|
|
86
|
-
- **Computer Use(高危,仅明确 GUI 意图时路由)** — 请求确实需要真实鼠标/键盘交互时,router 才可暴露 `computer-control`,并把每次动作后的截图回灌模型。`/cu off`(或 `MOCODE_COMPUTER_USE_ENABLED=false`)是硬否决;`/cu on` 仅允许按需路由,不会让工具常驻。它会绕过文件沙箱,**强烈建议只在 VM / 沙箱 / 专用测试机里使用**。每个动作仍走权限门,plan 模式永远屏蔽。Windows 首发,macOS/Linux
|
|
86
|
+
- **Computer Use(高危,仅明确 GUI 意图时路由)** — 请求确实需要真实鼠标/键盘交互时,router 才可暴露 `computer-control`,并把每次动作后的截图回灌模型。`/cu off`(或 `MOCODE_COMPUTER_USE_ENABLED=false`)是硬否决;`/cu on` 仅允许按需路由,不会让工具常驻。它会绕过文件沙箱,**强烈建议只在 VM / 沙箱 / 专用测试机里使用**。每个动作仍走权限门,plan 模式永远屏蔽。Windows 首发,macOS/Linux 待接入。
|
|
87
87
|
|
|
88
88
|
## 特性
|
|
89
89
|
|
package/bin/mocode-agent-host.js
CHANGED
|
@@ -28,7 +28,7 @@ function canonicalProjectRoot(root) {
|
|
|
28
28
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* computer 工具的 type/key
|
|
31
|
+
* computer 工具的 type/key 文本内容审查:
|
|
32
32
|
* 命中 URL / 密码形态 / 支付关键词时,无论是否已授权都强制 once 级确认——
|
|
33
33
|
* 这类文本是「把内容敲进任意应用」的载体,不能因 session/项目授权而放行后续所有输入。
|
|
34
34
|
* 纯函数,独立可单测。
|
|
@@ -54,7 +54,7 @@ export function permissionFingerprint(tool, args) {
|
|
|
54
54
|
}
|
|
55
55
|
else if (tool.name === 'computer') {
|
|
56
56
|
// computer:坐标/文本每次不同,进指纹会让每次点击都弹窗(功能上等同不可用)。
|
|
57
|
-
// 按动作粒度授权:允许一次 left_click =
|
|
57
|
+
// 按动作粒度授权:允许一次 left_click = 本会话允许任意坐标的左键。
|
|
58
58
|
subject = { computer: typeof args.action === 'string' ? args.action : 'unknown' };
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
@@ -157,7 +157,7 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
157
157
|
return 'allow';
|
|
158
158
|
// computer 的 type/key 命中敏感内容(URL/密码/支付)时强制 once 级确认:
|
|
159
159
|
// 不看任何授权缓存,也不提供 session/项目/永久授权选项——这类文本是「敲进任意应用」的载体,
|
|
160
|
-
//
|
|
160
|
+
// 一旦放行不该沿用到下一次输入。
|
|
161
161
|
const forceOnce = tool.name === 'computer' &&
|
|
162
162
|
(args.action === 'type' || args.action === 'key') &&
|
|
163
163
|
typeof args.text === 'string' &&
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 架构:InputInjector 接口与实现解耦——
|
|
5
5
|
* - 阶段一(MVP,本文件):PowerShell 常驻子进程,stdin 收 NDJSON 动作、stdout 回 NDJSON 结果,
|
|
6
6
|
* P/Invoke user32.dll(SendInput / SetCursorPos / GetCursorPos / mouse_event)。零新依赖。
|
|
7
|
-
* - 阶段二(
|
|
7
|
+
* - 阶段二(终态,本轮不做):rust/ enigo 二进制,同协议替换。
|
|
8
8
|
*
|
|
9
9
|
* 非 Windows 平台 MVP 显式抛错(诚实降级),不静默假装支持。
|
|
10
10
|
* 键名映射接受 Anthropic 风格键名(Return / Alt_L / ctrl+s),在 TS 侧映射成 VK 码,
|
package/dist/sandbox/jail.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 核心:resolve(root, input) → realpath(解软链)→ 前缀校验。
|
|
3
3
|
// 新文件(目标不存在)走「最近存在祖先 realpath 再拼回剩余分量」,同样挡住「祖先是出圈软链」。
|
|
4
4
|
import { realpathSync, existsSync } from 'node:fs';
|
|
5
|
-
import { resolve, relative, isAbsolute, dirname, basename, join } from 'node:path';
|
|
5
|
+
import { resolve, relative, isAbsolute, dirname, basename, join, posix, win32 } from 'node:path';
|
|
6
6
|
import { getSandboxRoot } from './root.js';
|
|
7
7
|
const isWin = process.platform === 'win32';
|
|
8
8
|
/** 当前沙箱根(绝对),未初始化回退 process.cwd()。 */
|
|
@@ -59,14 +59,14 @@ export function jailResolve(input) {
|
|
|
59
59
|
return abs;
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* glob pattern
|
|
62
|
+
* glob pattern 校验:同时拒绝 POSIX/Windows 绝对路径与含 `..` 段的 pattern。
|
|
63
63
|
* 返 null = 通过;返 string = 拒绝原因。形如 *.ts 的正常 pattern 放行。
|
|
64
64
|
*/
|
|
65
65
|
export function jailGlobPattern(pattern) {
|
|
66
66
|
const p = String(pattern ?? '').trim();
|
|
67
67
|
if (!p)
|
|
68
68
|
return '空 pattern';
|
|
69
|
-
if (isAbsolute(p))
|
|
69
|
+
if (posix.isAbsolute(p) || win32.isAbsolute(p))
|
|
70
70
|
return `不得为绝对路径: ${pattern}`;
|
|
71
71
|
const segs = p.split(/[\\/]/);
|
|
72
72
|
if (segs.includes('..'))
|
|
@@ -4,20 +4,29 @@ function countAskHumanCalls(events) {
|
|
|
4
4
|
}
|
|
5
5
|
export function reduceTraceMetrics(events) {
|
|
6
6
|
const ends = events.filter((event) => event.type === 'tool_call_end');
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const failedTools = new Set();
|
|
8
|
+
const earliestFailureStep = new Map();
|
|
9
|
+
const recoveredTools = new Set();
|
|
9
10
|
let successes = 0;
|
|
10
11
|
let tokens = 0;
|
|
11
12
|
let hasTokens = false;
|
|
12
13
|
for (const event of ends) {
|
|
14
|
+
const tool = typeof event.data.tool === 'string' ? event.data.tool : '';
|
|
13
15
|
const status = String(event.data.status ?? 'error');
|
|
14
16
|
if (status === 'success') {
|
|
15
17
|
successes++;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const failedStep = earliestFailureStep.get(tool);
|
|
19
|
+
if (tool && failedStep !== undefined && event.step !== undefined && event.step > failedStep) {
|
|
20
|
+
recoveredTools.add(tool);
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
|
-
else {
|
|
20
|
-
|
|
23
|
+
else if (tool) {
|
|
24
|
+
failedTools.add(tool);
|
|
25
|
+
if (event.step !== undefined) {
|
|
26
|
+
const previousStep = earliestFailureStep.get(tool);
|
|
27
|
+
if (previousStep === undefined || event.step < previousStep)
|
|
28
|
+
earliestFailureStep.set(tool, event.step);
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
for (const event of events) {
|
|
@@ -34,7 +43,9 @@ export function reduceTraceMetrics(events) {
|
|
|
34
43
|
return {
|
|
35
44
|
toolCalls: events.filter((event) => event.type === 'tool_call_start').length,
|
|
36
45
|
toolFailures: ends.length - successes,
|
|
37
|
-
toolRecovery:
|
|
46
|
+
toolRecovery: recoveredTools.size > 0,
|
|
47
|
+
toolRecoveryAttempts: failedTools.size,
|
|
48
|
+
toolRecoveries: recoveredTools.size,
|
|
38
49
|
firstSuccessRate: ends.length ? successes / ends.length : 1,
|
|
39
50
|
modelRetries,
|
|
40
51
|
retries: modelRetries,
|
package/dist/ui/batch.js
CHANGED
|
@@ -55,13 +55,11 @@ function visibleControl(s) {
|
|
|
55
55
|
.map((part, i) => (i % 2 === 1 ? part : part.replace(/[\x00-\x1f\x7f]/g, '·')))
|
|
56
56
|
.join('');
|
|
57
57
|
}
|
|
58
|
-
/** 自洽行统一收尾:行宽钳到
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
* 工具输出后整屏错乱)。truncateAnsi 保留行内 SGR 且断尾补 reset,再统一 \x1B[0m 收尾。 */
|
|
62
|
-
function sanitizeRow(s) {
|
|
58
|
+
/** 自洽行统一收尾:行宽钳到 cols + 行末补 reset。
|
|
59
|
+
* 纯函数导出供行宽/控制字符不变量测试;生产渲染默认传模块当前 maxCols。 */
|
|
60
|
+
export function sanitizeRow(s, cols = maxCols) {
|
|
63
61
|
const cleaned = visibleControl(s);
|
|
64
|
-
return truncateAnsi(cleaned,
|
|
62
|
+
return truncateAnsi(cleaned, cols) + '\x1B[0m';
|
|
65
63
|
}
|
|
66
64
|
export function isMutationToolName(name) {
|
|
67
65
|
return name === 'write_file' || name === 'edit_file';
|
|
@@ -313,23 +311,20 @@ function entryCaret(e, expanded) {
|
|
|
313
311
|
return CARET_NONE;
|
|
314
312
|
return expanded ? `${ui.accent}${CARET_EXPANDED}${ui.reset} ` : `${ui.dim}${CARET_COLLAPSED}${ui.reset} `;
|
|
315
313
|
}
|
|
316
|
-
|
|
317
|
-
* 单条第一层明细行。三角由 entry 自身的详情展开态决定(▸ 可展开 / ▾ 已展开 / 空白占位),
|
|
318
|
-
* 不再依赖 isLast —— 兄弟条目间没有嵌套语义,画分支符(├─/└─)纯属误导且引入宽度歧义。
|
|
319
|
-
*/
|
|
320
|
-
function buildEntryLine(b, index, extraIndent = '') {
|
|
314
|
+
function buildEntryLine(b, index, extraIndent = '', cols = maxCols) {
|
|
321
315
|
const e = b.entries[index];
|
|
322
316
|
const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
|
|
323
317
|
const caret = entryCaret(e, b.expandedEntries.has(index));
|
|
324
318
|
const failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
|
|
325
|
-
return sanitizeRow(`${extraIndent} ${caret}${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}
|
|
319
|
+
return sanitizeRow(`${extraIndent} ${caret}${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`, cols);
|
|
326
320
|
}
|
|
327
321
|
/** 第一层只展示有哪些调用及其简短结果,不展开完整输出。extraIndent 供子批嵌套加深缩进。
|
|
328
322
|
* fromIndex:运行态追加渲染时只产出 entries[fromIndex, ...) 的行(已渲染的不能重复输出)。 */
|
|
329
|
-
function buildExpandedLines(b, extraIndent = '', fromIndex = 0) {
|
|
323
|
+
export function buildExpandedLines(b, extraIndent = '', fromIndex = 0, cols = maxCols) {
|
|
330
324
|
const out = [];
|
|
331
|
-
|
|
332
|
-
|
|
325
|
+
const start = Math.max(0, Math.min(b.entries.length, Math.floor(fromIndex)));
|
|
326
|
+
for (let i = start; i < b.entries.length; i++)
|
|
327
|
+
out.push(buildEntryLine(b, i, extraIndent, cols));
|
|
333
328
|
return out;
|
|
334
329
|
}
|
|
335
330
|
/** 详情行缩进(现在与 index 无关,统一常量;保留签名以免调用点全改)。 */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function clampCursor(text, cursor) {
|
|
2
|
+
if (!Number.isFinite(cursor))
|
|
3
|
+
return text.length;
|
|
4
|
+
return Math.max(0, Math.min(text.length, Math.floor(cursor)));
|
|
5
|
+
}
|
|
6
|
+
/** 在光标处插入文本,返回更新后的文本与光标。 */
|
|
7
|
+
export function insertTextAt(text, cursor, inserted) {
|
|
8
|
+
const at = clampCursor(text, cursor);
|
|
9
|
+
return {
|
|
10
|
+
text: text.slice(0, at) + inserted + text.slice(at),
|
|
11
|
+
cursor: at + inserted.length,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** 删除光标前一个 UTF-16 单元;与 readline 提供的字符偏移语义保持一致。 */
|
|
15
|
+
export function deleteBackwardAt(text, cursor) {
|
|
16
|
+
const at = clampCursor(text, cursor);
|
|
17
|
+
if (at === 0)
|
|
18
|
+
return { text, cursor: at };
|
|
19
|
+
return {
|
|
20
|
+
text: text.slice(0, at - 1) + text.slice(at),
|
|
21
|
+
cursor: at - 1,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** 删除光标后的一个 UTF-16 单元。 */
|
|
25
|
+
export function deleteForwardAt(text, cursor) {
|
|
26
|
+
const at = clampCursor(text, cursor);
|
|
27
|
+
if (at === text.length)
|
|
28
|
+
return { text, cursor: at };
|
|
29
|
+
return {
|
|
30
|
+
text: text.slice(0, at) + text.slice(at + 1),
|
|
31
|
+
cursor: at,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** 删除当前逻辑行从行首到光标的内容。 */
|
|
35
|
+
export function deleteToLineStartAt(text, cursor) {
|
|
36
|
+
const at = clampCursor(text, cursor);
|
|
37
|
+
const lineStart = text.lastIndexOf('\n', at - 1) + 1;
|
|
38
|
+
if (lineStart === at)
|
|
39
|
+
return { text, cursor: at };
|
|
40
|
+
return {
|
|
41
|
+
text: text.slice(0, lineStart) + text.slice(at),
|
|
42
|
+
cursor: lineStart,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** 删除当前逻辑行从光标到行尾的内容;存在换行时一并删除换行并连接下一行。 */
|
|
46
|
+
export function deleteToLineEndAt(text, cursor) {
|
|
47
|
+
const at = clampCursor(text, cursor);
|
|
48
|
+
const newline = text.indexOf('\n', at);
|
|
49
|
+
if (newline < 0) {
|
|
50
|
+
return at === text.length ? { text, cursor: at } : { text: text.slice(0, at), cursor: at };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
text: text.slice(0, at) + text.slice(newline + 1),
|
|
54
|
+
cursor: at,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -8,6 +8,7 @@ import { t } from '../../i18n/index.js';
|
|
|
8
8
|
import { promptComposer } from '../composer.js';
|
|
9
9
|
import { promptHistorySearch } from '../history-picker.js';
|
|
10
10
|
import { CONFIRM_SEND_MS, LONG_INPUT_CHARS, confirmSendMode, ensurePasteDetector, pasteState } from './paste.js';
|
|
11
|
+
import { deleteBackwardAt, deleteForwardAt, deleteToLineEndAt, deleteToLineStartAt, insertTextAt, } from './editor-operations.js';
|
|
11
12
|
/** 非 TTY / 未进 alt screen 时退化为普通 readline 行输入(无菜单、单行)。 */
|
|
12
13
|
function questionFallback(prompt) {
|
|
13
14
|
return new Promise((res, rej) => {
|
|
@@ -218,9 +219,9 @@ export async function promptWithSlashMenu(opts) {
|
|
|
218
219
|
}
|
|
219
220
|
/** 在光标处插入文本(含换行则落进当前可编辑段,原样保留)。供短粘贴与可打印字符共用。 */
|
|
220
221
|
function insertText(text) {
|
|
221
|
-
const
|
|
222
|
-
segs[curSeg].text =
|
|
223
|
-
curOff
|
|
222
|
+
const edit = insertTextAt(segs[curSeg].text, curOff, text);
|
|
223
|
+
segs[curSeg].text = edit.text;
|
|
224
|
+
curOff = edit.cursor;
|
|
224
225
|
}
|
|
225
226
|
/** 落一段粘贴文本:长粘贴 → 在此处插入一个只读 chip(把当前可编辑段从光标切开成「前段 + chip + 后段」),
|
|
226
227
|
* 短粘贴 → 作为可编辑文本插入。连续长粘贴会产生相邻 chip,各自独立;两次粘贴之间敲的字是可编辑段,
|
|
@@ -477,27 +478,21 @@ export async function promptWithSlashMenu(opts) {
|
|
|
477
478
|
}
|
|
478
479
|
/** Ctrl+U:删到行首(当前可编辑段内的当前行;已在行首则 no-op,不跨入相邻粘贴块)。 */
|
|
479
480
|
function deleteToLineStart() {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
if (ls === curOff)
|
|
481
|
+
const edit = deleteToLineStartAt(segs[curSeg].text, curOff);
|
|
482
|
+
if (edit.text === segs[curSeg].text && edit.cursor === curOff)
|
|
483
483
|
return;
|
|
484
|
-
segs[curSeg].text =
|
|
485
|
-
curOff =
|
|
484
|
+
segs[curSeg].text = edit.text;
|
|
485
|
+
curOff = edit.cursor;
|
|
486
486
|
computeFiltered();
|
|
487
487
|
redraw();
|
|
488
488
|
}
|
|
489
489
|
/** Ctrl+K:删到行尾(当前可编辑段内的当前行;已在行尾则 no-op,不跨入相邻粘贴块)。 */
|
|
490
490
|
function deleteToLineEnd() {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
segs[curSeg].text = s.slice(0, curOff);
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
segs[curSeg].text = s.slice(0, curOff) + s.slice(ne + 1);
|
|
500
|
-
}
|
|
491
|
+
const edit = deleteToLineEndAt(segs[curSeg].text, curOff);
|
|
492
|
+
if (edit.text === segs[curSeg].text && edit.cursor === curOff)
|
|
493
|
+
return;
|
|
494
|
+
segs[curSeg].text = edit.text;
|
|
495
|
+
curOff = edit.cursor;
|
|
501
496
|
computeFiltered();
|
|
502
497
|
redraw();
|
|
503
498
|
}
|
|
@@ -683,10 +678,10 @@ export async function promptWithSlashMenu(opts) {
|
|
|
683
678
|
}
|
|
684
679
|
switch (key.name) {
|
|
685
680
|
case 'backspace': {
|
|
686
|
-
const
|
|
687
|
-
if (
|
|
688
|
-
segs[curSeg].text =
|
|
689
|
-
curOff
|
|
681
|
+
const edit = deleteBackwardAt(segs[curSeg].text, curOff);
|
|
682
|
+
if (edit.cursor !== curOff) {
|
|
683
|
+
segs[curSeg].text = edit.text;
|
|
684
|
+
curOff = edit.cursor;
|
|
690
685
|
computeFiltered();
|
|
691
686
|
redraw();
|
|
692
687
|
}
|
|
@@ -712,9 +707,9 @@ export async function promptWithSlashMenu(opts) {
|
|
|
712
707
|
return;
|
|
713
708
|
}
|
|
714
709
|
case 'delete': {
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
segs[curSeg].text =
|
|
710
|
+
const edit = deleteForwardAt(segs[curSeg].text, curOff);
|
|
711
|
+
if (edit.text !== segs[curSeg].text) {
|
|
712
|
+
segs[curSeg].text = edit.text;
|
|
718
713
|
computeFiltered();
|
|
719
714
|
redraw();
|
|
720
715
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "eslint . --fix",
|
|
29
29
|
"format": "prettier --write .",
|
|
30
30
|
"format:check": "prettier --check .",
|
|
31
|
-
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/work-discipline.ts",
|
|
31
|
+
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/quality-metrics.ts && tsx evals/work-discipline.ts",
|
|
32
32
|
"eval:coding": "tsx evals/coding/runner.ts",
|
|
33
33
|
"eval:coding:list": "tsx evals/coding/runner.ts --list",
|
|
34
34
|
"prepare": "npm run build"
|