thincoder 0.12.63 → 0.12.65
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/CHANGELOG.md +42 -0
- package/README.md +132 -79
- package/bin/thincoder.mjs +38 -12
- package/package.json +3 -3
- package/src/acp/handlers-session.mjs +42 -1
- package/src/acp/handlers-slots.mjs +1 -1
- package/src/cli/make-agent.mjs +17 -16
- package/src/tui/agent-turn.mjs +5 -2
- package/src/tui/cmd-advisor.mjs +1 -1
- package/src/tui/cmd-eng.mjs +12 -1
- package/src/tui/cmd-mcp-form.mjs +11 -5
- package/src/tui/cmd-new.mjs +1 -1
- package/src/tui/cmd-plan.mjs +9 -0
- package/src/tui/conversation-writer.mjs +62 -0
- package/src/tui/index.mjs +43 -297
- package/src/tui/input-face.mjs +187 -0
- package/src/tui/key-handler-busy.mjs +42 -0
- package/src/tui/key-handler-ctrlc.mjs +118 -0
- package/src/tui/key-handler-edit.mjs +115 -0
- package/src/tui/key-handler-modals.mjs +87 -0
- package/src/tui/key-handler-scroll.mjs +110 -0
- package/src/tui/key-handler.mjs +32 -382
- package/src/tui/ledger-surface.mjs +14 -67
- package/src/tui/render-frame.mjs +13 -4
- package/src/tui/subagent-freeze.mjs +3 -3
- package/src/tui/suspension-drive.mjs +35 -24
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/tui-state.mjs +65 -0
- package/src/tui/turn-face.mjs +64 -0
- package/src/tui/wrapped-spawn.mjs +6 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
3
|
+
"version": "0.12.65",
|
|
4
|
+
"description": "Thin coding agent - zero third-party dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
7
7
|
"agent",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=24"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@thincoder/core": "^0.9.
|
|
23
|
+
"@thincoder/core": "^0.9.3"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
|
@@ -8,11 +8,22 @@
|
|
|
8
8
|
* 受门 handler 首行求值,真 ⇒ 放行、假 ⇒ `-32000`。客户端可以不调 `authenticate`
|
|
9
9
|
* (契约逐字「**May** return an `auth_required` error … **if** the agent requires
|
|
10
10
|
* authentication」)。
|
|
11
|
+
*
|
|
12
|
+
* #168①(2026-09-22 · SESSION.md §6.15 / §6.16):`session/close` 入释放面——关闭会话 ⇒
|
|
13
|
+
* 释放该会话认领(保留集 = 本进程其余在存会话的活绑定槽,同 cwd;落盘判据三条沿用)。
|
|
11
14
|
*/
|
|
12
15
|
import { resolve } from "node:path"
|
|
13
|
-
import { normalizeCwd, bindRecordStore, newSession, slotPath } from "@thincoder/core/session.mjs"
|
|
16
|
+
import { loadManifest, normalizeCwd, bindRecordStore, newSession, saveManifest, slotPath } from "@thincoder/core/session.mjs"
|
|
17
|
+
// 认领释放谓词单源(SESSION.md §6.2 / §6.16——与 VSC 端壳 `releaseClaimsAll` 同源档;
|
|
18
|
+
// core `session.mjs` 未 re-export 该名——直引 manifest 档,先例 = `thincoder-vscode/src/extension/session-io.mjs`)。
|
|
19
|
+
import { staleClaims } from "@thincoder/core/session-slots-manifest.mjs"
|
|
20
|
+
import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
|
|
14
21
|
import { ACP_ERRORS } from "./transport.mjs"
|
|
15
22
|
|
|
23
|
+
/** ENG-PLAN-EXCLUSION(FR31 ② · E7「命令面逐面钉定」):工程真值 = 核单源同键
|
|
24
|
+
* `agent.config.agent.engineering`。 */
|
|
25
|
+
const isEngineering = (agent) => agent?.config?.agent?.engineering === true
|
|
26
|
+
|
|
16
27
|
/** Session-level config options exposed to clients — schema `SessionConfigOption.required = ["id","name"]`
|
|
17
28
|
* (响应侧用 `id`;请求侧 `SetSessionConfigOptionRequest` 用 `configId`——不对称属契约本身 · §11.3 G2-8). */
|
|
18
29
|
export const CONFIG_OPTIONS = [
|
|
@@ -102,6 +113,22 @@ export function createSessionHandlers(ctx) {
|
|
|
102
113
|
return { session: s }
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
/** #168①(SESSION.md §6.15 / §6.16——2026-09-22 定裁 · 台账 #168①):`session/close` ⇒
|
|
117
|
+
* 释放被关闭会话的槽认领。公式代入:释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`;
|
|
118
|
+
* **保留集 = 本进程其余在存会话的活绑定槽**(同 cwd——ACP 单 cwd 模型)。取值点(实施轮定位报告):
|
|
119
|
+
* 会话记录容器 = `ctx.sessions`(`Map<id, session>`——`thincoder-cli/src/acp.mjs:98` 单容器);
|
|
120
|
+
* 槽值 = `session.agent._slot`(认领写入同源 = `handlers-slots.mjs:96-100` / `:151-155` /
|
|
121
|
+
* `handlers-session.mjs` 的 newSession 认领点)。落盘判据三条由核 `saveManifest` `opts.release`
|
|
122
|
+
* 自动继承(fresh 同次快照 / 值条件删除 / 内存认领表移除)——**零新增写盘路径**;本进程无残留认领
|
|
123
|
+
* ⇒ 零写早退(判据单源 `staleClaims`——同核 `releaseClaimsAll` 早退形)。 */
|
|
124
|
+
const releaseClosedSessionSlot = () => {
|
|
125
|
+
const cwd = getCwd()
|
|
126
|
+
const m = loadManifest(cwd)
|
|
127
|
+
const keep = [...sessions.values()].map((s) => s.agent?._slot).filter((slot) => slot != null)
|
|
128
|
+
if (staleClaims(m, keep).length === 0) return
|
|
129
|
+
saveManifest(cwd, m, null, { release: keep })
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
return {
|
|
106
133
|
"session/new": async (params) => {
|
|
107
134
|
const gate = requireConfigured()
|
|
@@ -194,7 +221,9 @@ export function createSessionHandlers(ctx) {
|
|
|
194
221
|
// Abort any in-flight turn first — the client is gone, the agent must
|
|
195
222
|
// stop consuming LLM tokens and emitting notifications.
|
|
196
223
|
found.session.cancel()
|
|
224
|
+
// 先出容器再释放(保留集 = 其余在存会话——被关闭者不留在保留集内)。
|
|
197
225
|
sessions.delete(String(params.sessionId))
|
|
226
|
+
releaseClosedSessionSlot() // #168①:释放该会话认领(保留集 = 同 cwd 其余在存会话槽)
|
|
198
227
|
log(`session ${params.sessionId} closed by client`)
|
|
199
228
|
}
|
|
200
229
|
return {}
|
|
@@ -209,6 +238,13 @@ export function createSessionHandlers(ctx) {
|
|
|
209
238
|
if (!configId || value === undefined) {
|
|
210
239
|
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "set_config_option requires configId and value" } }
|
|
211
240
|
}
|
|
241
|
+
// ENG-PLAN-EXCLUSION(FR31 ② / 评审轮 1 发现 #4 出口钉定):工程模式 ⇒ plan 面**前置判**
|
|
242
|
+
// ⇒ 携共用文案的 INVALID_PARAMS。`applyConfigOption` 布尔契约零改(`false` 仍 = unknown
|
|
243
|
+
// configId——工程拒绝不经该路径,下方 `:unknown configId` 误导文案对 mode=plan 不再可达);
|
|
244
|
+
// `mode:"normal"` 照常接受(唯一合法态,幂等)。
|
|
245
|
+
if (configId === "mode" && value === "plan" && isEngineering(found.session.agent)) {
|
|
246
|
+
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: PLAN_ENGINEERING_REFUSED } }
|
|
247
|
+
}
|
|
212
248
|
const applied = applyConfigOption(found.session.agent, configId, value)
|
|
213
249
|
if (!applied) {
|
|
214
250
|
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `unknown configId: ${configId}` } }
|
|
@@ -229,6 +265,11 @@ export function createSessionHandlers(ctx) {
|
|
|
229
265
|
if (params.mode !== "plan" && params.mode !== "normal") {
|
|
230
266
|
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "mode must be plan or normal" } }
|
|
231
267
|
}
|
|
268
|
+
// ENG-PLAN-EXCLUSION(FR31 ② / 同 #4):工程模式 + `mode:"plan"` ⇒ 前置判 ⇒ 携共用文案
|
|
269
|
+
// 的 INVALID_PARAMS(状态零变、零通知);`normal` 照常(幂等)。
|
|
270
|
+
if (params.mode === "plan" && isEngineering(found.session.agent)) {
|
|
271
|
+
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: PLAN_ENGINEERING_REFUSED } }
|
|
272
|
+
}
|
|
232
273
|
found.session.agent.planMode = params.mode === "plan"
|
|
233
274
|
notifyRef.current("session/update", {
|
|
234
275
|
sessionId: String(params.sessionId),
|
|
@@ -29,7 +29,7 @@ export function createSlotsHandlers(ctx) {
|
|
|
29
29
|
|
|
30
30
|
/** #41 装载后一行重估(F3 准翻列 / F4):`applySession` 已按槽订正 config ⇒ 工程模式会话
|
|
31
31
|
* 就地判据(单源)+ 附着——缺档格 `writer:'main'` 建档;非工程模式零动作(零 manifest I/O)。
|
|
32
|
-
* 判据不通过 ⇒
|
|
32
|
+
* 判据不通过 ⇒ 保持不附着(装载已发生——本行是「装载后订正」位;启动侧恒不拒,拒面 = 「翻转可否进」)+ 留痕。 */
|
|
33
33
|
const reattach = (ag) => {
|
|
34
34
|
if (ag?.config?.agent?.engineering !== true) return
|
|
35
35
|
const r = resolveEngineeringManifest(getCwd(), { writer: "main" })
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { discoverRules } from "@thincoder/core/rules.mjs"
|
|
|
8
8
|
// (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪ 门控 read_image)。
|
|
9
9
|
// 传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失(门判据 = specForModel(model)?.multimodal)。
|
|
10
10
|
import { assembleBuiltinTools } from "@thincoder/core/tools/index.mjs"
|
|
11
|
-
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
11
|
+
import { resolveEngineeringManifest, projectView } from "@thincoder/core/manifest.mjs"
|
|
12
12
|
|
|
13
13
|
/** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
|
|
14
14
|
* 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
|
|
@@ -123,9 +123,9 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
|
|
|
123
123
|
agent.activeModel = provider.model ?? null
|
|
124
124
|
agent._mcpWarnings = mcpWarnings
|
|
125
125
|
// M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
|
|
126
|
-
// 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null
|
|
127
|
-
//
|
|
128
|
-
// docs/core/design/MANIFEST.md §2.2。
|
|
126
|
+
// 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null);非 ok 态不抛(启动零拒绝)。
|
|
127
|
+
// 钩子后移的可观察后果(报明 / 建档晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在
|
|
128
|
+
// 进入正常循环之前)见 docs/core/design/MANIFEST.md §2.2。
|
|
129
129
|
attachManifest(agent, { cwd, slotData })
|
|
130
130
|
// SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
|
|
131
131
|
// 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
|
|
@@ -145,20 +145,20 @@ export async function assembleAgent({ excludeTools = [], slotData } = {}) {
|
|
|
145
145
|
* 「恢复槽带 `engineering` 字段 ? 槽值 : `agent.config.agent.engineering`」(槽字段在场判据
|
|
146
146
|
* 与 `applySession` 同款——`thincoder-core/session.mjs:314-317`)。普通会话 → **装配钩子
|
|
147
147
|
* 零 manifest I/O**(不读 / 不拒 / 不建档)+ `agent.manifest = null`(清残留附着——复用
|
|
148
|
-
* agent
|
|
149
|
-
*
|
|
150
|
-
*
|
|
148
|
+
* agent 跨模式防陈旧)。
|
|
149
|
+
*
|
|
150
|
+
* 工程模式分支 = 核单源决策树 `resolveEngineeringManifest`(KD-M1-20);**非 fatal**
|
|
151
|
+
* (KD-M1-25 / M1-29——**启动零拒绝**):档合法 / 梯②④⑤ 缺档(轻动作就地建档,`writer:'main'`)
|
|
152
|
+
* ⇒ 附着;歧义(≥2 候选)/ 档非法 / 建档失败 ⇒ **不抛**——`agent.manifest = null` + 记
|
|
153
|
+
* `agent._projectView`(形状 = `projectView` 返回面 + `created` 建档标记——§2.2 钩子段)。
|
|
154
|
+
* 需要项目参数者各自报明(情境行 §2.6 / 动作侧各自报错——§2.9 C)。
|
|
151
155
|
*
|
|
152
156
|
* `slotData` = 恢复槽记录,由调用方经**参数注入**(`bin` TUI 分支既有 `resumeSlot` 调用
|
|
153
157
|
* 前移——KD-M1-15:钩子内不调 `resumeSlot`(该函数非纯读:GC / 认领 / 端标写);`chat` /
|
|
154
158
|
* ACP 装配无会话 ⇒ 不传 ⇒ config 回退)。重估点不传(`applySession` 已按槽订正 config)。
|
|
155
|
-
*
|
|
156
|
-
* #41(薄包装——KD-M1-20 / AC-19):工程模式分支的决策树 = 核单源
|
|
157
|
-
* `resolveEngineeringManifest`(非抛错形态);本函数只把它翻回「抛错」——四条出口
|
|
158
|
-
* (附着 / 缺档建档 / 根不可解析抛 / 档非法抛)的**结果与文案逐字不变**(原句由决策树构造)。
|
|
159
|
-
* @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest)
|
|
159
|
+
* @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest / agent._projectView)
|
|
160
160
|
* @param {{cwd?: string, slotData?: object|null}} [opts]
|
|
161
|
-
* @returns {object|null} 附着后的 manifest /
|
|
161
|
+
* @returns {object|null} 附着后的 manifest / 非 ok 态与普通会话 null
|
|
162
162
|
*/
|
|
163
163
|
export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
|
|
164
164
|
const slotEng = slotData?.engineering
|
|
@@ -167,10 +167,11 @@ export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
|
|
|
167
167
|
agent.manifest = null // 清残留附着(KD-M1-12)——普通会话不读 / 不拒 / 不建档
|
|
168
168
|
return null
|
|
169
169
|
}
|
|
170
|
-
//
|
|
170
|
+
// 决策树(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3;漏传 ⇒ 缺档格退化
|
|
171
|
+
// `init-failed`)。任何非 ok 态都不抛(KD-M1-25)。
|
|
171
172
|
const r = resolveEngineeringManifest(cwd, { writer: "main" })
|
|
172
|
-
|
|
173
|
-
agent.manifest = r.manifest
|
|
173
|
+
agent._projectView = { ...projectView(cwd), created: r.ok && r.created === true }
|
|
174
|
+
agent.manifest = r.ok ? r.manifest : null
|
|
174
175
|
return agent.manifest
|
|
175
176
|
}
|
|
176
177
|
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
|
21
21
|
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §17 段迁 suspension-drive.mjs
|
|
22
22
|
import { ensureSessionTitle } from "@thincoder/core/generate-title.mjs"
|
|
23
23
|
import { logEvent, errText } from "@thincoder/core/log.mjs"
|
|
24
|
+
import { t } from "@thincoder/core/i18n.mjs"
|
|
24
25
|
import { suspensionSession, poolLive } from "./suspension-drive.mjs"
|
|
25
26
|
|
|
26
27
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
@@ -191,11 +192,11 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
191
192
|
// §2 统一规则自动 resume(无人值守授权);手动档静默拒绝(部分消化留在
|
|
192
193
|
// 历史,会话回挂起——结果不丢,只是不再烧轮次)。
|
|
193
194
|
if (agent.autoApprove) {
|
|
194
|
-
pushLine("
|
|
195
|
+
pushLine(t("digest.capAuto"), C.dim)
|
|
195
196
|
state.controller = makeController()
|
|
196
197
|
continue
|
|
197
198
|
}
|
|
198
|
-
pushLine(
|
|
199
|
+
pushLine(t("digest.capStop", { turns: error.turn }), C.warn)
|
|
199
200
|
if (opts?._logOutcome) opts._logOutcome.result = "stopped"
|
|
200
201
|
break
|
|
201
202
|
}
|
|
@@ -339,6 +340,8 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
339
340
|
// 承诺)——逐条直发;斜杠命令直接执行(保序);回合后的续发由递归层同循环续取。
|
|
340
341
|
while (state.queue.length > 0 && !state.processing) {
|
|
341
342
|
const head = state.queue.shift()
|
|
343
|
+
// F16(TUI.md §7.5):消费回执——queued dim 行(消费事实的可见锚;对位 [continuing…])
|
|
344
|
+
pushLine("[sending queued message]", C.tool)
|
|
342
345
|
// 残项斜杠命令直接执行——保序、绝不合并
|
|
343
346
|
if (head.text.startsWith("/")) {
|
|
344
347
|
await handleSlash(head.text)
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
27
27
|
// Other advisor keys (model/thinking/effort/timeout) stay config-scoped — persist() only.
|
|
28
28
|
const persistGuard = async () => {
|
|
29
29
|
try {
|
|
30
|
-
const p = slotPath(agent.cwd, activeSlot(agent.cwd))
|
|
30
|
+
const p = slotPath(agent.cwd, agent._slot ?? activeSlot(agent.cwd))
|
|
31
31
|
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
32
32
|
if (data && typeof data === "object" && Array.isArray(data.history)) {
|
|
33
33
|
data.advisor = { ...(typeof data.advisor === "object" && data.advisor !== null ? data.advisor : {}), guard: cfg.guard === true }
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { purgeExpiredDesignTokens } from "@thincoder/core/token-ttl.mjs"
|
|
|
12
12
|
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
13
13
|
|
|
14
14
|
import { ENG_OFF_REMINDER } from "@thincoder/core/agent.mjs"
|
|
15
|
+
import { clearPlanMode } from "@thincoder/core/agent-tools/plan.mjs"
|
|
15
16
|
|
|
16
17
|
/** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
|
|
17
18
|
* private-import; cmd-advisor's guard toggle shares this helper). */
|
|
@@ -48,6 +49,9 @@ export async function handleEngCommand(ctx) {
|
|
|
48
49
|
agent.manifest = r.manifest
|
|
49
50
|
}
|
|
50
51
|
agent.config.agent.engineering = !agent.config.agent.engineering
|
|
52
|
+
// FR31 ③ / KD10(翻转点②·CLI `/eng`):ON ⇒ 清 plan 残留(内存位 + 未注入的 plan 提示语);
|
|
53
|
+
// OFF 方向不动(普通模式零改——FR31 边界)。提示行仅在真清到东西时出(不喧宾)。
|
|
54
|
+
const planReset = agent.config.agent.engineering ? clearPlanMode(agent) : false
|
|
51
55
|
// §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
|
|
52
56
|
// on the next toggle).
|
|
53
57
|
agent._advisorRuns = new Map()
|
|
@@ -68,6 +72,9 @@ export async function handleEngCommand(ctx) {
|
|
|
68
72
|
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
|
|
69
73
|
if (agent.config.agent.engineering) {
|
|
70
74
|
pushLine(` → design-before-code enforced (design review + user approval before code)`, C.dim)
|
|
75
|
+
if (planReset) {
|
|
76
|
+
pushLine(` → plan mode reset (engineering mode excludes plan mode)`, C.dim)
|
|
77
|
+
}
|
|
71
78
|
if (clearedExpired > 0) {
|
|
72
79
|
pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
|
|
73
80
|
}
|
|
@@ -80,14 +87,18 @@ export async function handleEngCommand(ctx) {
|
|
|
80
87
|
* VS Code, per-session; config.json is just the initial default, no mirror write).
|
|
81
88
|
* The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
|
|
82
89
|
* this process; saveSession also round-trips it on every turn-end write.
|
|
90
|
+
* 槽写也担 FR31 ③ 的槽位收正(T12):ON 时 `data.planMode = false`——否则下次装载/恢复
|
|
91
|
+
* 把 plan-active 推回 UI(半状态复活面;验收判据 = 「内存 + 槽位」双清零)。OFF 方向零改。
|
|
83
92
|
*/
|
|
84
93
|
async function persistEngineering(agent) {
|
|
85
|
-
|
|
94
|
+
// #167:绑定优先(先例 = cmd-session.mjs)——粘性槽优先于 activeSlot(后者有认领副作用)。
|
|
95
|
+
const slot = agent._slot ?? activeSlot(agent.cwd)
|
|
86
96
|
try {
|
|
87
97
|
const p = slotPath(agent.cwd, slot)
|
|
88
98
|
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
89
99
|
if (data && typeof data === "object" && Array.isArray(data.history)) {
|
|
90
100
|
data.engineering = agent.config.agent.engineering
|
|
101
|
+
if (agent.config.agent.engineering) data.planMode = false
|
|
91
102
|
writeSessionFile(p, data)
|
|
92
103
|
}
|
|
93
104
|
} catch { /* slot missing/unreadable — in-memory flag already flipped; saveSession persists at turn end */ }
|
package/src/tui/cmd-mcp-form.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
+
// #58(hygiene-sweep 批):headers/env 现值脱敏判据 = 核 settings 同源单点(不再各自为政)。
|
|
3
|
+
import { isSensitiveKey } from "@thincoder/core/agent-tools/settings.mjs"
|
|
2
4
|
|
|
3
5
|
/** MCP.md §5 v2(D-1):edit/add 统一字段 picker 表单机制。
|
|
4
6
|
* fieldPicker 循环:picker 列字段行(label + 当前值打码)+ `✓ Save & test` 末行;
|
|
@@ -111,15 +113,19 @@ function formEntries(entry, transport, mode) {
|
|
|
111
113
|
]
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
/**
|
|
116
|
+
/** #58(hygiene-sweep 批):headers/env 现值列示——敏感键名(谓词 = 核 settings 同源
|
|
117
|
+
* `isSensitiveKey`)的值位一律 `••••`(人读面不再明文);非敏感键名保可读(`k=v`)。 */
|
|
118
|
+
function currentPairs(v) {
|
|
119
|
+
const pairs = Object.entries(v ?? {}).map(([k, val]) => (isSensitiveKey(k) ? `${k}=••••` : `${k}=${val}`))
|
|
120
|
+
return pairs.length ? pairs.join(", ") : "none"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 字段输入提示 `(current: …)`——token 打码(maskToken)、headers/env 列键值对(敏感值位遮)、
|
|
115
124
|
* args 空格串接;空值 → "none"。 */
|
|
116
125
|
function currentText(entry, field) {
|
|
117
126
|
const v = entry[field]
|
|
118
127
|
if (field === "token") return v ? maskToken(v) : "none"
|
|
119
|
-
if (field === "headers" || field === "env")
|
|
120
|
-
const pairs = Object.entries(v ?? {}).map(([k, val]) => `${k}=${val}`)
|
|
121
|
-
return pairs.length ? pairs.join(", ") : "none"
|
|
122
|
-
}
|
|
128
|
+
if (field === "headers" || field === "env") return currentPairs(v)
|
|
123
129
|
if (field === "args") return (v ?? []).length ? v.join(" ") : "none"
|
|
124
130
|
return String(v ?? "") || "none"
|
|
125
131
|
}
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export async function handleNewCommand(ctx) {
|
|
|
8
8
|
|
|
9
9
|
// F-MI7:newSession = async(探测束)——doNewSession 随之为 async,调用点 await
|
|
10
10
|
const doNewSession = async () => {
|
|
11
|
-
const slot = await newSession(agent.cwd)
|
|
11
|
+
const slot = await newSession(agent.cwd, { releaseStale: true }) // F-CR1:/new 释放本进程残留认领(保留集 = {新槽});ACP 四点不传(F-CR3)
|
|
12
12
|
// 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
|
|
13
13
|
// 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
|
|
14
14
|
// 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
package/src/tui/cmd-plan.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/** /plan command: toggle plan mode (read-only explore → design → implement).
|
|
2
2
|
* ctx: { agent, pushLine, pushLabel } */
|
|
3
3
|
import { ansi, C } from "./ansi.mjs"
|
|
4
|
+
import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
|
|
4
5
|
|
|
5
6
|
export async function handlePlanCommand(ctx) {
|
|
6
7
|
const { agent, pushLine, pushLabel } = ctx
|
|
8
|
+
// ENG-PLAN-EXCLUSION(FR31 ② · 设计 `ENGINEERING-MODE-V2.md` §2.3 E7「命令面逐面钉定」):
|
|
9
|
+
// 工程模式 ⇒ 拒绝——**前置判**:零翻转(不写 `planMode`)+ 提示行(共用文案常量逐字,
|
|
10
|
+
// 与 ACP 两面同源);无成功标签(标签 = 成功回显——拒时零标签防假成功,同 `/eng` 先例)。
|
|
11
|
+
// 真值 = `agent.config.agent.engineering`(核单源同键);普通模式四路径全带宽(FR31 边界)。
|
|
12
|
+
if (agent.config?.agent?.engineering === true) {
|
|
13
|
+
pushLine(PLAN_ENGINEERING_REFUSED, C.warn)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
7
16
|
agent.planMode = !agent.planMode
|
|
8
17
|
pushLabel("❯ Plan", ansi.bold + C.tool)
|
|
9
18
|
pushLine(`Plan mode: ${agent.planMode ? "ON" : "OFF"}`, C.tool)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { capLine, accountLine, accountAll, syncLineBudget } from "./display-budget.mjs"
|
|
3
|
+
import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
4
|
+
|
|
5
|
+
/** 对话写入面(2026-09-22 structure-debt §2.2 · #159):pushLine(行额度单点)/ pushLabel
|
|
6
|
+
* (消息标签 + 呼吸空行)/ ensureAssistantLabel(回合内单次 assistant 标签位)——三块自
|
|
7
|
+
* index.mjs startTUI 逐字搬移(仅去包裹)。
|
|
8
|
+
* `render` 由调用侧以惰性转发承接(renderLoop 于 startTUI 后段装配——TDZ 语义保持)。
|
|
9
|
+
* @param {{ state: object, render: Function }} ctx
|
|
10
|
+
* @returns {{ pushLine: Function, pushLabel: Function, ensureAssistantLabel: Function,
|
|
11
|
+
* assistantLabeled: boolean }} */
|
|
12
|
+
export function createConversationWriter({ state, render }) {
|
|
13
|
+
const pushLine = (text, color, kind) => {
|
|
14
|
+
// TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):行额度单点——行文本过 capLine
|
|
15
|
+
// (LINE_MAX_CHARS——单行巨内容/无换行巨 chunk 的堵口)+ 总量账 + 预算对账。
|
|
16
|
+
const line = { text: capLine(text), color, _kind: kind }
|
|
17
|
+
state.lines.push(line)
|
|
18
|
+
accountLine(state, line)
|
|
19
|
+
if (state.lines.length > 5000) {
|
|
20
|
+
state.lines.splice(0, 1000)
|
|
21
|
+
state.lines.unshift({ text: `... [earlier messages trimmed — ${state.lines.length} lines remaining]`, color: C.dim })
|
|
22
|
+
// 2026-09-03 修复轮(冻结锚点):头裁对在途 settled 锚点整体前移(锚点是绝对
|
|
23
|
+
// 流位置)——不校正则池空补发冻结(freezeSubTaskLines splice)落点漂移;校正量
|
|
24
|
+
// = 净位移(裁 1000 补 1 标记行 → −999,code review round1 #3)。
|
|
25
|
+
shiftFreezeAnchors(state, 1000)
|
|
26
|
+
accountAll(state) // 行对象集合已变(splice/unshift 直写)——直算重对账
|
|
27
|
+
}
|
|
28
|
+
syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
29
|
+
render()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Message block label: blank line + label line. Breathing space between user/assistant messages */
|
|
33
|
+
const pushLabel = (text, color) => {
|
|
34
|
+
// 行额度同 pushLine(§15.3.1——恢复行/标签行同口径)
|
|
35
|
+
if (state.lines.length > 0) {
|
|
36
|
+
const blank = { text: "", color: C.dim }
|
|
37
|
+
state.lines.push(blank)
|
|
38
|
+
accountLine(state, blank)
|
|
39
|
+
}
|
|
40
|
+
const line = { text: capLine(text), color }
|
|
41
|
+
state.lines.push(line)
|
|
42
|
+
accountLine(state, line)
|
|
43
|
+
syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
|
|
44
|
+
render()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Only emit the assistant label once per turn (on first token or first tool call)
|
|
48
|
+
let assistantLabeled = false
|
|
49
|
+
const ensureAssistantLabel = () => {
|
|
50
|
+
if (!assistantLabeled) {
|
|
51
|
+
assistantLabeled = true
|
|
52
|
+
pushLabel(`❯ ThinCoder:`, ansi.bold + C.assistant)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
pushLine,
|
|
57
|
+
pushLabel,
|
|
58
|
+
ensureAssistantLabel,
|
|
59
|
+
get assistantLabeled() { return assistantLabeled },
|
|
60
|
+
set assistantLabeled(v) { assistantLabeled = v },
|
|
61
|
+
}
|
|
62
|
+
}
|