mocode-ai 1.1.5 → 1.1.7
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 +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/agent/core.js +59 -2
- package/dist/agent/work-discipline.js +34 -34
- package/dist/config/index.js +1 -0
- package/dist/llm/index.js +5 -2
- package/dist/repl/index.js +52 -14
- package/dist/ui/intervention.js +0 -1
- package/dist/ui/layout.js +73 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,19 @@ MoCode explores your code, reads/writes/edits files, runs shell commands, and se
|
|
|
10
10
|
|
|
11
11
|
## Architecture
|
|
12
12
|
|
|
13
|
+
## Engineering discipline
|
|
14
|
+
|
|
15
|
+
MoCode encodes "how to take coding seriously" into the agent's own behavior, not just into the prompt:
|
|
16
|
+
|
|
17
|
+
- **Plan → Build → Verify → Fix four-phase discipline** — Injected fresh each turn into `buildBasePrompt`, with per-model-family light adaptation. The agent must restate the task, plan, and acceptance signal before touching anything; changes must pass an automatic validation gate; on failure the agent enters a Fix phase and feeds real command output back as a fresh observation. Evidence: `src/agent/work-discipline.ts` + `evals/work-discipline.ts` (6 assertions). Basis: [`docs/coding-harness-quality-roadmap.md` §4.1 PROMPT-01](docs/coding-harness-quality-roadmap.md).
|
|
18
|
+
- **Pre-Completion Checklist middleware** — `finish`/`stop` is blocked when `mutation > 0 && no tool call && validation !== 'passed'`. Simple read-only tasks deliberately bypass it to avoid noise. Evidence: `src/agent/middleware/checklist.ts` + `evals/checklist.ts` (6 assertions).
|
|
19
|
+
- **Reflective retry + thrash throttling** — Errors are classified into 6 categories (`retry-classifier`); the same tool with the same args ≥3 times appends a hint to switch strategy; failed traces stay in context but receive a targeted reflection prompt instead of a blind retry. Evidence: `src/tools/retry.ts` + `src/agent/retry-classifier.ts` + `evals/retry-classifier.ts` (9 assertions).
|
|
20
|
+
- **`ask_human` as a deliberate de-escalation** — No blind guessing. On whitelisted scenarios (sandbox deviation, ambiguous params, conflicting user instructions) the agent prefers "disclose rather than guess" and will explicitly call `ask_human` to pop a panel for your decision (with a call budget). Evidence: ASK_WHITELIST_SECTION in `src/agent/work-discipline.ts` + `evals/ask-budget.ts` (6 assertions).
|
|
21
|
+
- **Verification cascade V0 → V3 with content fingerprint cache** — Changes pass through file post-conditions → scoped tsc/eslint → targeted unit tests → affected npm scripts, in that order; the first actionable failure stops the cascade; a SHA-256 content cache skips repeated work on unchanged files. Evidence: `src/validators/` plus the VER chapters in the main roadmap.
|
|
22
|
+
- **Five-zone context controls + token self-calibration** — Five independent dials (`autoCompact` / `contextOptimize` / `contextRelprune` / `contextLifecycle` / `contextBudget`), each with its own `MOCODE_*=false` kill switch; even with all five off, observations still age through the lifecycle. Token estimation uses EWMA to self-calibrate against real provider usage rather than trusting the estimator. Evidence: the five modules under `src/context/` plus the `MOCODE_*` switches in `src/config/index.ts`.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
13
26
|
MoCode is organized as a layered runtime: the terminal experience drives an autonomous core, the core reaches capabilities through a guarded execution plane, and a persistent intelligence layer keeps long-running work coherent.
|
|
14
27
|
|
|
15
28
|
<p align="center"><img src="./assets/architecture/system-overview.svg" alt="MoCode layered system architecture" width="100%"></p>
|
package/README.zh-CN.md
CHANGED
|
@@ -64,6 +64,19 @@ MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,
|
|
|
64
64
|
|
|
65
65
|
## 为什么用 mocode
|
|
66
66
|
|
|
67
|
+
## 工程化纪律
|
|
68
|
+
|
|
69
|
+
mocode 把"如何认真写代码"这件事也写进了 agent 自身的行为准则,而不是只靠 prompt 教:
|
|
70
|
+
|
|
71
|
+
- **Plan → Build → Verify → Fix 四阶段纪律** — 每轮 prompt 现拼现读注入 `buildBasePrompt`,并按模型家族做轻量适配;agent 必须先复述任务、规划与验收信号,再动手,改动必经自动验证门,失败时进入修复阶段并把真实命令输出当新观察反馈。证据:`src/agent/work-discipline.ts` + `evals/work-discipline.ts`(6 块断言)。依据:[`docs/coding-harness-quality-roadmap.md` §4.1 PROMPT-01](docs/coding-harness-quality-roadmap.md)。
|
|
72
|
+
- **Pre-Completion Checklist 硬关卡** — 在 `mutation > 0 && no tool call && validation !== 'passed'` 三个条件同时成立前,`finish`/`stop` 不会被放行;简单无改动的任务刻意不触发,避免噪音。证据:`src/agent/middleware/checklist.ts` + `evals/checklist.ts`(6 块断言)。
|
|
73
|
+
- **反思式重试 + thrash 节流** — 错误按 6 类分类(`retry-classifier`),同一工具同参数 ≥3 次追加 hint 提醒换策略;失败 trace 留在上下文中但有针对性反思 prompt 注入,而不是盲目重试。证据:`src/tools/retry.ts` + `src/agent/retry-classifier.ts` + `evals/retry-classifier.ts`(9 块断言)。
|
|
74
|
+
- **ask_human 卡点降级** — 不盲猜:遇到 sandbox 偏差、参数二义、用户指令冲突等白名单场景时,agent 倾向"披露而不是瞎猜",必要时显式调用 `ask_human` 弹面板让你拍板(带调用预算)。证据:`src/agent/work-discipline.ts` 的 ASK_WHITELIST_SECTION + `evals/ask-budget.ts`(6 块断言)。
|
|
75
|
+
- **验证瀑布 V0 → V3 + 内容指纹缓存** — 改动先走文件后置条件 → 受限 tsc/eslint → 定向单测 → 受影响 npm 脚本,首个可操作失败立刻停;SHA-256 文件指纹缓存保证未改动文件不重复劳动。证据:`src/validators/` + 主路线图 VER 章节。
|
|
76
|
+
- **五区上下文控制 + token 自校准** — 五个独立开关各自把控一档(`autoCompact` / `contextOptimize` / `contextRelprune` / `contextLifecycle` / `contextBudget`),即使全关观察结果也按生命周期老化;token 估算走 EWMA 自动校准真实 provider 用量,而不是死信估算函数。证据:`src/context/` 五模块 + `src/config/index.ts` 的 `MOCODE_*` 开关。
|
|
77
|
+
|
|
78
|
+
## 为什么用 mocode
|
|
79
|
+
|
|
67
80
|
mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
68
81
|
|
|
69
82
|
- **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
|
package/dist/agent/core.js
CHANGED
|
@@ -201,6 +201,34 @@ export function askHumanBudgetAnnotation(askHumanCountThisTurn, status) {
|
|
|
201
201
|
// 第 3+ 次(超过预算):强硬提示,鼓励模型停下问自己是否还有意义。
|
|
202
202
|
return `\n\n[ask budget EXCEEDED] This is ask_human call #${askHumanCountThisTurn} this turn (budget = ${ASK_HUMAN_PER_TURN_BUDGET}). Stop asking; choose a default, implement, and disclose the choice in your final reply. Continuing to ask is more harmful than a documented guess.`;
|
|
203
203
|
}
|
|
204
|
+
/** NARR-01: 工具轮旁白(assistant 消息同时带 content + tool_calls)的软预算。
|
|
205
|
+
* 超出即追加回压提示;未超只 emit trace(可度量,不打扰)。
|
|
206
|
+
* 单位是 code point 而非字节——中文一字一 point,英文一字母一 point,
|
|
207
|
+
* 对两种语言都是"一句话大约多长"的直觉量级。 */
|
|
208
|
+
export const NARRATION_CHAR_BUDGET = 120;
|
|
209
|
+
/** 分类工具轮旁白。返回 null 表示这一轮没有旁白(纯工具调用,理想情况)。
|
|
210
|
+
* hint 仅在超预算时非 null:prompt 里的"工具轮保持静默"是软约束,
|
|
211
|
+
* 这里给出机制层面的回压,同时让 trace 能统计旁白率。 */
|
|
212
|
+
export function classifyNarration(content, toolCallCount) {
|
|
213
|
+
const text = content?.trim() ?? '';
|
|
214
|
+
if (!text)
|
|
215
|
+
return null;
|
|
216
|
+
const chars = [...text].length;
|
|
217
|
+
if (chars <= NARRATION_CHAR_BUDGET) {
|
|
218
|
+
return { chars, overBudget: false, hint: null };
|
|
219
|
+
}
|
|
220
|
+
const plural = toolCallCount === 1 ? '' : 's';
|
|
221
|
+
return {
|
|
222
|
+
chars,
|
|
223
|
+
overBudget: true,
|
|
224
|
+
hint: `\n\n[narration] Your previous message emitted ${chars} characters of prose alongside ` +
|
|
225
|
+
`${toolCallCount} tool call${plural} (soft budget = ${NARRATION_CHAR_BUDGET}). ` +
|
|
226
|
+
'Interstitial commentary costs the user screen space and costs you context on every ' +
|
|
227
|
+
'later step. For the rest of this turn, emit prose mid-turn ONLY to (a) flag a genuine ' +
|
|
228
|
+
'decision fork needing user input, (b) disclose an error or risk, or (c) deliver the ' +
|
|
229
|
+
'final answer once tools are done. Otherwise call the next tool with no preamble.',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
204
232
|
function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
205
233
|
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
206
234
|
const ageAware = config.contextOptimize ? ageAwareStateFor(history) : null;
|
|
@@ -243,6 +271,15 @@ export async function runAgentCore(opts) {
|
|
|
243
271
|
// 预算超过时,工具结果尾部追加 ask budget 提示,不直接拒绝调用(更轻量,
|
|
244
272
|
// 也避免和现有 permission 系统的"拒绝"语义重叠)。
|
|
245
273
|
let askHumanCountThisTurn = 0;
|
|
274
|
+
// NARR-01: 上一条 assistant 消息(带 tool_calls)超出旁白预算时,待注入的回压提示。
|
|
275
|
+
// 只挂在本批次的第一条工具结果上并立即清空——重复注入会变成新的噪声源。
|
|
276
|
+
let pendingNarrationHint = null;
|
|
277
|
+
/** 取出并清空待注入的旁白回压提示(每批工具只消费一次)。 */
|
|
278
|
+
const takeNarrationHint = () => {
|
|
279
|
+
const hint = pendingNarrationHint;
|
|
280
|
+
pendingNarrationHint = null;
|
|
281
|
+
return hint;
|
|
282
|
+
};
|
|
246
283
|
// PROMPT-02: 解析 preCompletionChecklist 选项。undefined = 启用默认 middleware;
|
|
247
284
|
// false = opt-out(checklist 自身调试用);function = 调用方自定义。
|
|
248
285
|
const _checklistMiddleware = createPreCompletionChecklistMiddleware();
|
|
@@ -556,6 +593,22 @@ export async function runAgentCore(opts) {
|
|
|
556
593
|
function: { name: tc.name, arguments: tc.arguments },
|
|
557
594
|
})),
|
|
558
595
|
});
|
|
596
|
+
// NARR-01: 这条 assistant 消息同时带正文 + tool_calls,即"工具轮旁白"。
|
|
597
|
+
// prompt 里的"工具轮保持静默"只是软约束,这里补上机制层:
|
|
598
|
+
// - 永远 emit trace(旁白率可度量,不再靠肉眼感觉);
|
|
599
|
+
// - 超预算时把回压提示挂到本批第一条工具结果尾部(复用 thrash/ask
|
|
600
|
+
// budget 已验证的注入接缝,不新增消息、不打断工具流)。
|
|
601
|
+
const narration = classifyNarration(result.content, result.toolCalls.length);
|
|
602
|
+
if (narration) {
|
|
603
|
+
emitTrace('narration', {
|
|
604
|
+
chars: narration.chars,
|
|
605
|
+
toolCalls: result.toolCalls.length,
|
|
606
|
+
budget: NARRATION_CHAR_BUDGET,
|
|
607
|
+
overBudget: narration.overBudget,
|
|
608
|
+
step,
|
|
609
|
+
});
|
|
610
|
+
pendingNarrationHint = narration.hint;
|
|
611
|
+
}
|
|
559
612
|
// 工具分组执行(保 tool_calls 原顺序):safe parallel 工具照常并发;连续
|
|
560
613
|
// resource-locked mutation 先按序完成权限预检,再按 canonical resource lock 启动。
|
|
561
614
|
// registry 对所有真实资源访问统一持锁,所以不同 Agent 间的 read/write/process 也不会竞态。
|
|
@@ -685,7 +738,9 @@ export async function runAgentCore(opts) {
|
|
|
685
738
|
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
686
739
|
}
|
|
687
740
|
const askBudget = askHumanBudgetAnnotation(askHumanCountThisTurn, outcome.status);
|
|
688
|
-
|
|
741
|
+
// NARR-01: 旁白回压提示只挂本批第一条结果(takeNarrationHint 自清空)。
|
|
742
|
+
const narrationHint = takeNarrationHint();
|
|
743
|
+
const finalOutput = `${annotated}${askBudget ?? ''}${narrationHint ?? ''}`;
|
|
689
744
|
pushToolResult(history, tc, finalOutput, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
690
745
|
}
|
|
691
746
|
hooks.onToolDone?.();
|
|
@@ -874,7 +929,9 @@ export async function runAgentCore(opts) {
|
|
|
874
929
|
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
875
930
|
}
|
|
876
931
|
const askBudget = askHumanBudgetAnnotation(askHumanCountThisTurn, outcome.status);
|
|
877
|
-
|
|
932
|
+
// NARR-01: 旁白回压提示只挂本批第一条结果(takeNarrationHint 自清空)。
|
|
933
|
+
const narrationHint = takeNarrationHint();
|
|
934
|
+
const finalOutput = `${annotated}${askBudget ?? ''}${narrationHint ?? ''}`;
|
|
878
935
|
pushToolResult(history, tc, finalOutput, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
879
936
|
const invalidatedFiles = [...new Set([
|
|
880
937
|
...(outcome.changedFiles ?? []),
|
|
@@ -33,30 +33,30 @@ export function inferModelFamily(model) {
|
|
|
33
33
|
* 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
|
|
34
34
|
* 标签上做轻量变体。保持短小,详细的完成检查由动态 checklist 按需注入。
|
|
35
35
|
*/
|
|
36
|
-
const CORE_SECTION = `## Working discipline — coding tasks (Build-and-Self-Verify)
|
|
37
|
-
|
|
38
|
-
Treat "verification" as a first-class part of the task, not an afterthought. Use the smallest evidence-driven loop below.
|
|
39
|
-
|
|
40
|
-
### Phase 1 — Plan & Discover
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- Ask only when an unresolved choice is high-impact or user-owned; otherwise follow repository evidence and proceed.
|
|
44
|
-
|
|
45
|
-
### Phase 2 — Build
|
|
46
|
-
- Make the smallest coherent change; avoid unrelated refactors.
|
|
47
|
-
- Add or update a focused test when behavior changes and the project has an applicable test suite.
|
|
48
|
-
- Re-read only when a dependent edit needs fresh exact content or state may be stale.
|
|
49
|
-
|
|
50
|
-
### Phase 3 — Verify
|
|
51
|
-
- Run the smallest executable check that proves the requested behavior, then read its complete result.
|
|
52
|
-
- Compare evidence with the user's request, not merely with the diff.
|
|
53
|
-
|
|
54
|
-
### Phase 4 — Fix
|
|
55
|
-
- Diagnose the root cause, make a focused correction, and rerun the relevant check.
|
|
56
|
-
- After two identical failures, change the approach instead of repeating the same call.
|
|
57
|
-
|
|
58
|
-
**Hard rule (non-negotiable):** "I read the code and it looks right" is not a completion signal. Report the verification performed, or state clearly why it could not be run.
|
|
59
|
-
|
|
36
|
+
const CORE_SECTION = `## Working discipline — coding tasks (Build-and-Self-Verify)
|
|
37
|
+
|
|
38
|
+
Treat "verification" as a first-class part of the task, not an afterthought. Use the smallest evidence-driven loop below.
|
|
39
|
+
|
|
40
|
+
### Phase 1 — Plan & Discover
|
|
41
|
+
- Restate the request in one sentence ONLY when it admits two or more materially different readings; name the reading you picked and move on. An unambiguous request gets no restatement — start working. This is the single exception to staying silent during tool-calling turns.
|
|
42
|
+
- Settle on the goal and a concrete acceptance signal before inspecting the relevant code; keep them internal unless the user has to weigh in.
|
|
43
|
+
- Ask only when an unresolved choice is high-impact or user-owned; otherwise follow repository evidence and proceed.
|
|
44
|
+
|
|
45
|
+
### Phase 2 — Build
|
|
46
|
+
- Make the smallest coherent change; avoid unrelated refactors.
|
|
47
|
+
- Add or update a focused test when behavior changes and the project has an applicable test suite.
|
|
48
|
+
- Re-read only when a dependent edit needs fresh exact content or state may be stale.
|
|
49
|
+
|
|
50
|
+
### Phase 3 — Verify
|
|
51
|
+
- Run the smallest executable check that proves the requested behavior, then read its complete result.
|
|
52
|
+
- Compare evidence with the user's request, not merely with the diff.
|
|
53
|
+
|
|
54
|
+
### Phase 4 — Fix
|
|
55
|
+
- Diagnose the root cause, make a focused correction, and rerun the relevant check.
|
|
56
|
+
- After two identical failures, change the approach instead of repeating the same call.
|
|
57
|
+
|
|
58
|
+
**Hard rule (non-negotiable):** "I read the code and it looks right" is not a completion signal. Report the verification performed, or state clearly why it could not be run.
|
|
59
|
+
|
|
60
60
|
**Hard rule (non-negotiable):** Never invent file paths, APIs, config keys, flags, or behavior. Every claim about the codebase must trace to tool output in this conversation; explicitly label anything you have not verified as an assumption.`;
|
|
61
61
|
/**
|
|
62
62
|
* 把核心段适配到指定 model family:只替换首行(语序 / 强动词),段标题
|
|
@@ -68,16 +68,16 @@ function adapt(_model, opener) {
|
|
|
68
68
|
return CORE_SECTION.replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
|
|
69
69
|
}
|
|
70
70
|
/** ASK-01: only user-owned, high-impact choices should interrupt autonomous execution. */
|
|
71
|
-
const ASK_WHITELIST_SECTION = `## When to ask instead of guess
|
|
72
|
-
|
|
73
|
-
Call \`ask_human\` before coding only when repository evidence cannot resolve a user-owned, high-impact choice:
|
|
74
|
-
1. irreversible deletion, migration, security, permission, or external side effect;
|
|
75
|
-
2. public API compatibility (keep, deprecate, rename, or remove);
|
|
76
|
-
3. multiple reasonable options that materially change product behavior;
|
|
77
|
-
4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
|
|
78
|
-
|
|
79
|
-
For naming, implementation detail, and verification commands, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
|
|
80
|
-
|
|
71
|
+
const ASK_WHITELIST_SECTION = `## When to ask instead of guess
|
|
72
|
+
|
|
73
|
+
Call \`ask_human\` before coding only when repository evidence cannot resolve a user-owned, high-impact choice:
|
|
74
|
+
1. irreversible deletion, migration, security, permission, or external side effect;
|
|
75
|
+
2. public API compatibility (keep, deprecate, rename, or remove);
|
|
76
|
+
3. multiple reasonable options that materially change product behavior;
|
|
77
|
+
4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
|
|
78
|
+
|
|
79
|
+
For naming, implementation detail, and verification commands, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
|
|
80
|
+
|
|
81
81
|
Budget: at most 2 \`ask_human\` calls per turn. Beyond that, use the safest reversible default and disclose it in the final reply.`;
|
|
82
82
|
/**
|
|
83
83
|
* 拼出纪律段 + ASK-01 卡点白名单。返回完整段(两段用 \`\\n\\n\` 隔开);
|
package/dist/config/index.js
CHANGED
|
@@ -200,6 +200,7 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
200
200
|
${buildCodegraphSection()}
|
|
201
201
|
|
|
202
202
|
## Tool use
|
|
203
|
+
- During tool-calling turns, stay silent unless something important enough must reach the user — otherwise just call the tool and let it run.
|
|
203
204
|
- Go directly to a known path or symbol; use discovery tools only when the location is unknown.
|
|
204
205
|
- Edit against a FRESH read: before any edit_file/write_file, call read_file on the exact path and copy both its latest hash and the exact target text. Never reconstruct old_string from a grep/summary/diff — those lose whitespace and indentation and cause edit failures.
|
|
205
206
|
- A read_file hash from before a compaction, session resume, edit conflict, or external change is STALE and will be rejected — re-read rather than reuse an old hash.
|
package/dist/llm/index.js
CHANGED
|
@@ -141,8 +141,11 @@ function logRetry(attempt, err, waitMs) {
|
|
|
141
141
|
const e = err;
|
|
142
142
|
const tag = e.status ? `HTTP ${e.status}` : e.name || 'Error';
|
|
143
143
|
const msg = e.message ?? '未知错误';
|
|
144
|
-
//
|
|
145
|
-
|
|
144
|
+
// 走 console.error 而非 process.stderr.write:TUI active 时 layout.installConsoleGuard
|
|
145
|
+
// 已把 console.* 劫持到 contentWrite(内容区),错误会落在 agent 输出区,不污染输入框;
|
|
146
|
+
// 非 TTY(管道 / CI / 启动早期 TUI 未启)降级走原生 console.error → stderr,行为与改造前一致。
|
|
147
|
+
// 不写前导 \n —— contentWrite 由续写位管位置,前置换行会留空行;结尾 \n 由劫持逻辑补。
|
|
148
|
+
console.error(`[llm] 第 ${attempt}/${RETRY_MAX_ATTEMPTS} 次失败(${tag}: ${msg}),${(waitMs / 1000).toFixed(1)}s 后重试…`);
|
|
146
149
|
}
|
|
147
150
|
let createImplOverride = null;
|
|
148
151
|
/** 仅供单测用:覆盖 chat() 内部实际调用的 create 桩。生产代码不要碰。 */
|
package/dist/repl/index.js
CHANGED
|
@@ -342,6 +342,7 @@ const emitter = stdin;
|
|
|
342
342
|
// ── 运行态交互(typeahead 输入 + 滚动回看 + Ctrl+C 中断)──
|
|
343
343
|
// 只在 await runAgent() 期间挂载;/resume /rollback /compact 等走 askLine(cooked readline)的分支不挂(避免抢 stdin)。
|
|
344
344
|
let runningInput = ''; // 运行中已打字缓冲(单行;agent 结束后预填下一轮 INPUT 态)
|
|
345
|
+
let runningCursor = 0; // 缓冲内光标字符索引(0..len);运行态支持任意位置编辑,与空闲态一致
|
|
345
346
|
let runningPlaceholder = '';
|
|
346
347
|
let currentAbort = null;
|
|
347
348
|
let pendingPrefill = null; // /rollback 选中后预填的 user 输入(下轮 INPUT 态消费)
|
|
@@ -391,7 +392,8 @@ function onRunningKey(_str, key) {
|
|
|
391
392
|
if (key.ctrl && key.name === 'c') {
|
|
392
393
|
if (runningInput.length > 0) {
|
|
393
394
|
runningInput = '';
|
|
394
|
-
|
|
395
|
+
runningCursor = 0;
|
|
396
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
395
397
|
}
|
|
396
398
|
else if (currentAbort && !currentAbort.signal.aborted) {
|
|
397
399
|
appendCurrentSessionRuntimeEvent('abort', { phase: 'requested', source: 'keyboard' });
|
|
@@ -400,16 +402,46 @@ function onRunningKey(_str, key) {
|
|
|
400
402
|
return;
|
|
401
403
|
}
|
|
402
404
|
const s = key.sequence ?? '';
|
|
405
|
+
// 光标移动(单行 typeahead,光标可任意位置,与空闲态一致)
|
|
406
|
+
if (key.name === 'left') {
|
|
407
|
+
runningCursor = Math.max(0, runningCursor - 1);
|
|
408
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (key.name === 'right') {
|
|
412
|
+
runningCursor = Math.min(runningInput.length, runningCursor + 1);
|
|
413
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
|
|
417
|
+
runningCursor = 0;
|
|
418
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
|
|
422
|
+
runningCursor = runningInput.length;
|
|
423
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
403
426
|
if (key.name === 'backspace') {
|
|
404
|
-
if (
|
|
405
|
-
runningInput = runningInput.slice(0, -1);
|
|
406
|
-
|
|
427
|
+
if (runningCursor > 0) {
|
|
428
|
+
runningInput = runningInput.slice(0, runningCursor - 1) + runningInput.slice(runningCursor);
|
|
429
|
+
runningCursor--;
|
|
430
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (key.name === 'delete') {
|
|
435
|
+
if (runningCursor < runningInput.length) {
|
|
436
|
+
runningInput = runningInput.slice(0, runningCursor) + runningInput.slice(runningCursor + 1);
|
|
437
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
407
438
|
}
|
|
408
439
|
return;
|
|
409
440
|
}
|
|
410
441
|
if (key.name === 'escape') {
|
|
411
442
|
runningInput = '';
|
|
412
|
-
|
|
443
|
+
runningCursor = 0;
|
|
444
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
413
445
|
return;
|
|
414
446
|
}
|
|
415
447
|
// Enter / Ctrl+J:运行中 no-op(单行 typeahead;agent 结束后预填,用户在 INPUT 态按 Enter 提交)
|
|
@@ -418,21 +450,25 @@ function onRunningKey(_str, key) {
|
|
|
418
450
|
(key.ctrl && key.name === 'j')) {
|
|
419
451
|
return;
|
|
420
452
|
}
|
|
421
|
-
// 可打印字符(>= 空格,非 ctrl/meta)→
|
|
453
|
+
// 可打印字符(>= 空格,非 ctrl/meta)→ 光标处插入 + 非 dim 回显(与空闲态一致)
|
|
422
454
|
if (s && s >= ' ' && !key.ctrl && !key.meta) {
|
|
423
|
-
runningInput
|
|
424
|
-
|
|
455
|
+
runningInput = runningInput.slice(0, runningCursor) + s + runningInput.slice(runningCursor);
|
|
456
|
+
runningCursor += s.length;
|
|
457
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
425
458
|
}
|
|
426
459
|
}
|
|
427
|
-
/** 鼠标右键单击输入框(未拖动)时 layout
|
|
460
|
+
/** 鼠标右键单击输入框(未拖动)时 layout 读剪贴板后回调:在光标处插入 typeahead 缓冲(单行,换行折为空格)。 */
|
|
428
461
|
function onRunningMousePaste(text) {
|
|
429
|
-
|
|
430
|
-
|
|
462
|
+
const flat = text.replace(/[\r\n]+/g, ' ');
|
|
463
|
+
runningInput = runningInput.slice(0, runningCursor) + flat + runningInput.slice(runningCursor);
|
|
464
|
+
runningCursor += flat.length;
|
|
465
|
+
layout.paintRunningInput(runningInput, runningCursor, runningPlaceholder);
|
|
431
466
|
}
|
|
432
467
|
/** 进入运行态:挂 keypress 监听 + raw mode + 新建 abort 控制器,返回其 signal。在 await runAgent 前、enterRunningMode 后调。 */
|
|
433
468
|
function startRunningListener(placeholder) {
|
|
434
469
|
runningPlaceholder = placeholder;
|
|
435
470
|
runningInput = '';
|
|
471
|
+
runningCursor = 0;
|
|
436
472
|
emitKeypressEvents(stdin); // 幂等:首轮 prompt 已永久挂解析器,这里防御性再调
|
|
437
473
|
try {
|
|
438
474
|
stdin.setRawMode(true);
|
|
@@ -762,6 +798,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
762
798
|
renderHistory(history);
|
|
763
799
|
// 强制回尾:同 /resume 命令,renderHistory 展开详情会设 scrollOffset>0,需复位避免闪烁。
|
|
764
800
|
layout.resetScroll();
|
|
801
|
+
layout.repaintViewport();
|
|
765
802
|
}
|
|
766
803
|
else {
|
|
767
804
|
layout.writeBanner(bannerLines(banner()));
|
|
@@ -1047,11 +1084,12 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1047
1084
|
lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
|
|
1048
1085
|
layout.clearContent();
|
|
1049
1086
|
renderHistory(history);
|
|
1050
|
-
// 强制回尾:renderHistory 展开 mutation 工具详情会经 contentInsertAfter 设置 scrollOffset>0;
|
|
1051
|
-
// 若不复位,后续 showLiveBatch 在冻结视口下 repaintViewport 会导致工具信息闪烁/滚动消失。
|
|
1052
|
-
layout.resetScroll();
|
|
1053
1087
|
// 末尾 \n\n:与后续用户消息(❯ bubble)之间空一行。
|
|
1054
1088
|
layout.contentWrite(`${ui.dim}${t('repl.resumed', { id: loaded.id })}${ui.reset}\n\n`);
|
|
1089
|
+
// 强制回尾:renderHistory 展开 mutation 工具详情会经 contentInsertAfter 设置 scrollOffset>0;
|
|
1090
|
+
// 先把"已续接会话"提示写入缓冲,再统一回尾重画,避免切换后视口停留在历史顶部。
|
|
1091
|
+
layout.resetScroll();
|
|
1092
|
+
layout.repaintViewport();
|
|
1055
1093
|
}
|
|
1056
1094
|
let hasSubmittedInput = false;
|
|
1057
1095
|
while (true) {
|
package/dist/ui/intervention.js
CHANGED
package/dist/ui/layout.js
CHANGED
|
@@ -127,8 +127,11 @@ const esc = {
|
|
|
127
127
|
// 落在内容区 → 复制当前选区(若有)到剪贴板(clipboard.ts),静默不弹提示。
|
|
128
128
|
// - 滚轮报表(button&64)转 scrollBy。
|
|
129
129
|
// 代价:终端原生框选被鼠标捕获接管——想用终端原生选区可按住 Shift(多数终端放行)。
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
// 1007l=关闭 Alternate Scroll Mode:VSCode xterm.js 进 alt screen(?1049)后默认开 1007,
|
|
131
|
+
// 滚轮发 Up/Down 方向键而非 SGR 鼠标报表;不关的话滚轮会绕过 mouse.swallow 直接触发
|
|
132
|
+
// prompt 的历史导航(recallPrevious/recallNext)→ "输入框打字时滚轮跳到上一条历史消息"。
|
|
133
|
+
mouseOn: '\x1B[?1007l\x1B[?1000h\x1B[?1002h\x1B[?1006h',
|
|
134
|
+
mouseOff: '\x1B[?1006l\x1B[?1002l\x1B[?1000l\x1B[?1007h', // 关:反序;末尾恢复 1007(交还终端默认)
|
|
132
135
|
cursorShow: '\x1B[?25h',
|
|
133
136
|
cursorHide: '\x1B[?25l',
|
|
134
137
|
clearLine: '\x1B[2K',
|
|
@@ -172,13 +175,23 @@ export function setRegion(fh) {
|
|
|
172
175
|
contentRow = g.contentBottom; // 底栏撑高挤掉内容:钳到新区底
|
|
173
176
|
return g;
|
|
174
177
|
}
|
|
175
|
-
/** 运行态真光标(隐藏)的归位点 = 输入框光标位:行 = 动态 contentBottom+4(resize 安全)
|
|
178
|
+
/** 运行态真光标(隐藏)的归位点 = 输入框光标位:行 = 动态 contentBottom+4(resize 安全)。
|
|
179
|
+
* 非 dim 运行态(与空闲态同色、可任意位置编辑)→ 归当前编辑位,供 IME 锚定气泡到光标处;
|
|
180
|
+
* dim 占位(空 + placeholder)兼容态→ 归输入框起点。供 IME 锚定。 */
|
|
176
181
|
function runningCaretPos() {
|
|
177
182
|
const g = getGeo();
|
|
183
|
+
const row = g.contentBottom + 4;
|
|
184
|
+
if (lastView && !lastView.dim) {
|
|
185
|
+
const promptW = displayWidth(lastView.prompt);
|
|
186
|
+
const line = lastView.lines[lastView.cursorLine] ?? '';
|
|
187
|
+
const before = line.slice(0, lastView.cursorCol);
|
|
188
|
+
const col = Math.min(g.cols, promptW + displayWidth(before) + 1);
|
|
189
|
+
return { row, col };
|
|
190
|
+
}
|
|
178
191
|
const text = lastView?.dim ? lastView.lines[0] ?? '' : '';
|
|
179
192
|
const contentW = Math.max(0, g.cols - 3); // ❯ =2 + 光标=1
|
|
180
193
|
const w = displayWidth(truncateDisplayHead(text, contentW));
|
|
181
|
-
return { row
|
|
194
|
+
return { row, col: Math.min(g.cols, 2 + w + 1) };
|
|
182
195
|
}
|
|
183
196
|
export function contentMode() {
|
|
184
197
|
if (!active)
|
|
@@ -529,6 +542,11 @@ export function clearContent() {
|
|
|
529
542
|
// /theme、/clear、/resume 等命令 clearContent 后紧接 writeBanner 的场景均依赖此重置。
|
|
530
543
|
bannerH = 0;
|
|
531
544
|
bannerRows = [];
|
|
545
|
+
// 清内容区必须同时作废旧菜单擦除坐标:picker(/resume /rollback /theme)把菜单画在内容区底部,
|
|
546
|
+
// 菜单行号缓存在 lastMenuStartRow/lastMenuRows;若不清零,后续 paintInput 会按旧坐标“擦菜单”,
|
|
547
|
+
// 把刚 renderHistory/contentWrite 写好的内容(如“已续接会话”提示)清掉,导致用户要滚动一下才刷新。
|
|
548
|
+
lastMenuStartRow = 0;
|
|
549
|
+
lastMenuRows = 0;
|
|
532
550
|
content.reset();
|
|
533
551
|
notifyContentReset(); // batch 渲染器同步重置(batch 摘要行索引全部失效)
|
|
534
552
|
stdout.write(esc.home);
|
|
@@ -1842,34 +1860,67 @@ function renderDimInputRow(prompt, text, placeholder, cols) {
|
|
|
1842
1860
|
return `${ui.dim}${prompt}${p}${ui.reset}`;
|
|
1843
1861
|
}
|
|
1844
1862
|
/**
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
|
|
1863
|
+
* 单行运行态滑窗:以光标为中心,向左右扩展填满 contentW-1(留 1 cell 给光标),
|
|
1864
|
+
* 返回可见子串与光标在子串内的显示列。保证光标恒可见,且不软折行(运行态输入框恒单行,
|
|
1865
|
+
* 不触发 setRegion/ED,避免流式期间底栏抖动——见 scripts/check-layout.ts 断言)。
|
|
1866
|
+
*/
|
|
1867
|
+
function windowSingleLine(text, cursor, contentW) {
|
|
1868
|
+
const n = text.length;
|
|
1869
|
+
if (n === 0)
|
|
1870
|
+
return { shown: '', curDisp: 0 };
|
|
1871
|
+
const totalDisp = displayWidth(text);
|
|
1872
|
+
if (totalDisp <= contentW)
|
|
1873
|
+
return { shown: text, curDisp: displayWidth(text.slice(0, cursor)) };
|
|
1874
|
+
let i = cursor;
|
|
1875
|
+
let j = cursor;
|
|
1876
|
+
let w = 0;
|
|
1877
|
+
const cw = (idx) => charWidth(text.codePointAt(idx) ?? 0);
|
|
1878
|
+
while (i > 0 && w + cw(i - 1) <= contentW - 1) {
|
|
1879
|
+
w += cw(i - 1);
|
|
1880
|
+
i--;
|
|
1881
|
+
}
|
|
1882
|
+
while (j < n && w + cw(j) <= contentW - 1) {
|
|
1883
|
+
w += cw(j);
|
|
1884
|
+
j++;
|
|
1885
|
+
}
|
|
1886
|
+
return { shown: text.slice(i, j), curDisp: displayWidth(text.slice(i, cursor)) };
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* 运行态 typeahead 回显:定向写输入行(底栏输入框),非 dim(与空闲态同色)、光标可任意位置、单行。
|
|
1890
|
+
* 只 cup 输入行 + clearLine + 文本 + cup 真光标到编辑位——不调 setRegion(运行中禁多行,避免 DECSTBM 抖动)、
|
|
1891
|
+
* 不重画状态行/contentBottom、不用 ED。同步 lastView 为运行视图(非 dim),使 scrollBy/resize 的 repaint
|
|
1892
|
+
* 仍显当前回显 + 光标。空且带 placeholder 时显 dim ghost(提示 agent 状态;非用户输入,不违反"打字不变灰")。
|
|
1893
|
+
* 选区高亮由 paintInput 路径(paint 时鼠标拖选)承担,此处轻量不重复实现。
|
|
1849
1894
|
*/
|
|
1850
|
-
export function
|
|
1895
|
+
export function paintRunningInput(text, cursor, placeholder) {
|
|
1851
1896
|
if (!active || !base)
|
|
1852
1897
|
return;
|
|
1853
1898
|
const g = getGeo();
|
|
1854
|
-
const inputRow = g.contentBottom + 4; // 运行态 footerH 恒
|
|
1855
|
-
|
|
1899
|
+
const inputRow = g.contentBottom + 4; // 运行态 footerH 恒 6(单行,无 setRegion)
|
|
1900
|
+
const promptW = displayWidth('❯ ');
|
|
1901
|
+
const contentW = Math.max(1, g.cols - promptW);
|
|
1902
|
+
let outLine;
|
|
1903
|
+
let curCol; // 输入框内的显示列(不含 prompt)
|
|
1904
|
+
if (text.length === 0 && placeholder) {
|
|
1905
|
+
outLine = `${ui.dim}❯ ${truncateDisplay(placeholder, contentW)}${ui.reset}`;
|
|
1906
|
+
curCol = 0;
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
const { shown, curDisp } = windowSingleLine(text, cursor, contentW);
|
|
1910
|
+
outLine = `❯ ${shown}`; // 正常色 —— 运行态打字与空闲态一致
|
|
1911
|
+
curCol = curDisp;
|
|
1912
|
+
}
|
|
1856
1913
|
lastView = {
|
|
1857
1914
|
prompt: '❯ ',
|
|
1858
1915
|
lines: [text],
|
|
1859
1916
|
placeholder,
|
|
1860
1917
|
cursorLine: 0,
|
|
1861
|
-
cursorCol:
|
|
1918
|
+
cursorCol: cursor,
|
|
1862
1919
|
menu: null,
|
|
1863
|
-
dim
|
|
1920
|
+
// 不置 dim:运行态输入框与空闲态同色、可任意位置编辑
|
|
1864
1921
|
};
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
// (conhost IME 不跟随 cup 后续移动,须让打字前光标已在输入框);拆两次 write 会暂留 contentBottom 显白块。
|
|
1868
|
-
const p = runningCaretPos();
|
|
1869
|
-
stdout.write(cup(inputRow, 1) +
|
|
1870
|
-
esc.clearLine +
|
|
1871
|
-
renderDimInputRow('❯ ', text, placeholder, g.cols) +
|
|
1872
|
-
cup(p.row, p.col));
|
|
1922
|
+
const cursorCol = Math.min(g.cols, promptW + curCol + 1);
|
|
1923
|
+
stdout.write(cup(inputRow, 1) + esc.clearLine + outLine + cup(inputRow, cursorCol));
|
|
1873
1924
|
}
|
|
1874
1925
|
/** 重画当前视图(resize / 内部用)。 */
|
|
1875
1926
|
export function repaint() {
|
|
@@ -1913,7 +1964,7 @@ export function enterInputMode(status = t('repl.idle')) {
|
|
|
1913
1964
|
stdout.write(esc.cursorShow); // 回 INPUT 态:显真光标(运行态藏了)
|
|
1914
1965
|
}
|
|
1915
1966
|
}
|
|
1916
|
-
/**
|
|
1967
|
+
/** 进入运行态:底栏输入行与空闲态同色(非 dim)、可任意位置编辑,cursor 留输入框。footerH 恒 6(虚拟空+spinner行+上线+输入+下线+model行)。新轮回尾(确保新内容可见)。 */
|
|
1917
1968
|
export function enterRunningMode(status, placeholder) {
|
|
1918
1969
|
mode = 'running';
|
|
1919
1970
|
statusText = status;
|
|
@@ -1930,7 +1981,6 @@ export function enterRunningMode(status, placeholder) {
|
|
|
1930
1981
|
cursorLine: 0,
|
|
1931
1982
|
cursorCol: 0,
|
|
1932
1983
|
menu: null,
|
|
1933
|
-
dim: true,
|
|
1934
1984
|
});
|
|
1935
1985
|
startTurnTimer(); // 续刷状态行走时(流式期间 spinner 停转,由它兜底)
|
|
1936
1986
|
contentMode();
|