thincoder 0.12.62 → 0.12.64

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.
Files changed (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +257 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -2,7 +2,7 @@ import { ansi, C } from "./ansi.mjs"
2
2
  import { cloneEntry, fieldPicker, maskToken } from "./cmd-mcp-form.mjs"
3
3
 
4
4
  /** /mcp command handler: view/add/edit/remove/test/reconnect MCP server.
5
- * ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C }
5
+ * ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C, confirmDelete }
6
6
  * MCP.md §5(2026-09-02,v2):列表即菜单(F7/D-2)、edit/add 统一字段 picker 表单
7
7
  * (F3/F3b/D-1——表单机制在 cmd-mcp-form.mjs,本文件只调 fieldPicker)、保存前预览
8
8
  * +探活(F2:探活失败回同一 fieldPicker——AC2)、AI 降 transport picker 末位(F4)、
@@ -37,7 +37,7 @@ async function addAndConnect(ctx, srv) {
37
37
  agent.config.mcp.servers.push(srv)
38
38
  try {
39
39
  pushLine(`[mcp] Connecting ${srv.name}...`, C.dim)
40
- const { connectMcpServer } = await import("../mcp.mjs")
40
+ const { connectMcpServer } = await import("@thincoder/core/mcp.mjs")
41
41
  const tools = await connectMcpServer(srv)
42
42
  agent.tools.push(...tools)
43
43
  pushLabel(`❯ MCP`, ansi.bold + C.tool)
@@ -50,7 +50,7 @@ async function addAndConnect(ctx, srv) {
50
50
  }
51
51
 
52
52
  export async function handleMcpCommand(ctx, args = []) {
53
- const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw } = ctx
53
+ const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, confirmDelete = () => { throw new Error("ctx.confirmDelete missing — deletion refused (fail-closed)") } } = ctx
54
54
  // 每轮重读:原本无 mcp 配置时 `?? []` 会拿到游离数组,Add server 后快照过期
55
55
  const getServers = () => agent.config?.mcp?.servers ?? []
56
56
  // D-3/T24:本菜单会话内最新一次磁盘重读是否失败(畸形 config.json → 内存态兜底)
@@ -61,7 +61,7 @@ export async function handleMcpCommand(ctx, args = []) {
61
61
  * 测试注入 tmp config 路径用(生产 undefined → 默认 ~/.thincoder)。 */
62
62
  let changedNames = [] // 最近一次成功重读的对账结果(T23 ⚠ 标记)
63
63
  async function reloadFromDisk() {
64
- const { reloadMcpFromDisk } = await import("../config.mjs")
64
+ const { reloadMcpFromDisk } = await import("@thincoder/core/config.mjs")
65
65
  const r = reloadMcpFromDisk(agent, ctx.configPath)
66
66
  diskUnreadable = !r.ok
67
67
  if (r.ok) changedNames = r.changedNames
@@ -85,6 +85,7 @@ export async function handleMcpCommand(ctx, args = []) {
85
85
  // D-F5a(#7)先盘后存:磁盘 fresh raw.mcp.servers 上只删目标条目——不整节写回内存
86
86
  // agent.config.mcp.servers(内存含 reloadMcpFromDisk 的 keptConnected 尾巴——
87
87
  // 整节写回会把对端磁盘上其他 server 的新改动一起抹掉);冲突放弃不留内存 ghost
88
+ if (!(await confirmDelete(`Remove MCP server "${name}"?`))) return
88
89
  await persistRaw((raw) => {
89
90
  raw.mcp ??= { servers: [] }
90
91
  if (!Array.isArray(raw.mcp.servers)) return
@@ -92,14 +93,14 @@ export async function handleMcpCommand(ctx, args = []) {
92
93
  })
93
94
  agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
94
95
  // Remove from tool list
95
- const { removeMcpTools } = await import("../mcp.mjs")
96
+ const { removeMcpTools } = await import("@thincoder/core/mcp.mjs")
96
97
  removeMcpTools(agent, name)
97
98
  pushLine(`[mcp] ${name} removed`, C.tool)
98
99
  }
99
100
 
100
101
  async function connectServer(name) {
101
102
  const srv = getServers().find((s) => s.name === name)
102
- const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
103
+ const { removeMcpTools, connectMcpServer } = await import("@thincoder/core/mcp.mjs")
103
104
  removeMcpTools(agent, name)
104
105
  try {
105
106
  pushLine(`[mcp] Reconnecting ${name}...`, C.dim)
@@ -130,7 +131,7 @@ export async function handleMcpCommand(ctx, args = []) {
130
131
  * agent.tools、探完即关)。未保存的临时 entry 直接传 probe(§5 D-1)。 */
131
132
  async function probeLineFor(entry) {
132
133
  pushLine(`[mcp] Probing ${entry.name}...`, C.dim)
133
- const { probeMcpServer } = await import("../mcp.mjs")
134
+ const { probeMcpServer } = await import("@thincoder/core/mcp.mjs")
134
135
  const r = await probeMcpServer(entry)
135
136
  return r.ok ? `✓ ${r.toolCount} tools, ${r.latencyMs}ms` : `✗ ${r.error}`
136
137
  }
@@ -160,7 +161,7 @@ export async function handleMcpCommand(ctx, args = []) {
160
161
  if (!description) return
161
162
  pushLine("[mcp] Generating config from description...", C.dim)
162
163
  try {
163
- const { chat } = await import("../provider/index.mjs")
164
+ const { chat } = await import("@thincoder/core/provider/index.mjs")
164
165
  const res = await chat(agent.provider, {
165
166
  messages: [{
166
167
  role: "user",
@@ -1,5 +1,5 @@
1
1
  import { C } from "./ansi.mjs"
2
- import { parseModelRef } from "../config.mjs"
2
+ import { parseModelRef } from "@thincoder/core/config.mjs"
3
3
 
4
4
  /** /model command: open the session model picker, or switch the SESSION model directly via
5
5
  * `/model <provider>:<model>`(MODEL-MERGE-SESSION——裁定③:裸 provider 拒——显式 p:m;
@@ -1,4 +1,4 @@
1
- import { newSession, resetSessionState, bindRecordStore, slotPath } from "../session.mjs"
1
+ import { newSession, resetSessionState, bindRecordStore, slotPath } from "@thincoder/core/session.mjs"
2
2
  import { C } from "./ansi.mjs"
3
3
 
4
4
  /** /new command: start a new session in a fresh slot.
@@ -6,8 +6,9 @@ import { C } from "./ansi.mjs"
6
6
  export async function handleNewCommand(ctx) {
7
7
  const { agent, state, pushLine, showPicker, render } = ctx
8
8
 
9
- const doNewSession = () => {
10
- const slot = newSession(agent.cwd)
9
+ // F-MI7:newSession = async(探测束)——doNewSession 随之为 async,调用点 await
10
+ const doNewSession = async () => {
11
+ const slot = await newSession(agent.cwd, { releaseStale: true }) // F-CR1:/new 释放本进程残留认领(保留集 = {新槽});ACP 四点不传(F-CR3)
11
12
  // 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
12
13
  // 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
13
14
  // 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
@@ -30,8 +31,8 @@ export async function handleNewCommand(ctx) {
30
31
  { type: "item", text: "Yes, start new session in a new slot", action: "yes" },
31
32
  { type: "item", text: "Cancel", action: "no" },
32
33
  ], { defaultIndex: 1 })
33
- if (e?.action === "yes") doNewSession()
34
+ if (e?.action === "yes") await doNewSession()
34
35
  return
35
36
  }
36
- doNewSession()
37
+ await doNewSession()
37
38
  }
@@ -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)
@@ -4,7 +4,7 @@ import { C } from "./ansi.mjs"
4
4
  * ctx: { agent, distillOpts, pushLine } */
5
5
  export async function handleReindexCommand(ctx) {
6
6
  const { agent, distillOpts, pushLine } = ctx
7
- const { syncDir, codeSync, docSync } = await import("../memory.mjs")
7
+ const { syncDir, codeSync, docSync } = await import("@thincoder/core/memory.mjs")
8
8
  pushLine("[reindex] Rebuilding index...", C.tool)
9
9
  agent.memory.db.prepare("DELETE FROM files").run()
10
10
  agent.memory.db.prepare("DELETE FROM code_chunks").run()
@@ -5,14 +5,14 @@ import { ansi, C } from "./ansi.mjs"
5
5
  * (CHECKPOINT.md D8). ctx: { agent, showPicker, pushLine, pushLabel } */
6
6
  export async function handleRestoreCommand(ctx) {
7
7
  const { agent, showPicker, pushLine, pushLabel } = ctx
8
- const { listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
8
+ const { listCheckpoints, rewind, isGitRepo } = await import("@thincoder/core/git/checkpoint.mjs")
9
9
  if (!isGitRepo(agent.cwd)) {
10
10
  pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
11
11
  return
12
12
  }
13
13
  // F6 lazy fallback——与 git 工具 checkpoint list/create 入口一致(git-checkpoint.mjs):外部
14
14
  // git commit 后(HEAD 时间 > 最新快照)先清空过期快照,/restore 不列出 commit 前状态。
15
- const { lazyClearIfCommitted } = await import("../tools/git-checkpoint.mjs")
15
+ const { lazyClearIfCommitted } = await import("@thincoder/core/tools/git-checkpoint.mjs")
16
16
  await lazyClearIfCommitted(agent.cwd)
17
17
  const cps = await listCheckpoints(agent.cwd)
18
18
  if (cps.length === 0) {
@@ -1,4 +1,5 @@
1
- import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor } from "../session.mjs"
1
+ import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile } from "@thincoder/core/session.mjs"
2
+ import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
2
3
  import { ansi, C } from "./ansi.mjs"
3
4
  import { restoreLines } from "./startup.mjs"
4
5
  import { stringWidth, sliceByWidth } from "./render.mjs"
@@ -81,6 +82,26 @@ export async function handleSessionCommand(ctx) {
81
82
  // 2026-08-31 会诊 deepseek 🔴:目标槽被另一活进程占用时 switchToSlot 不认领(避免
82
83
  // 劫持对方 active),本次会话仍读到数据,但下次保存会 fork 到新槽——提前提示。
83
84
  const occ = slotOccupancy(agent.cwd, e.slot)
85
+ // #41 先判后切(MANIFEST.md §2.8 F3 · 父侧裁定 3):目标槽**合值**(§2.2 会话权威值
86
+ // 「槽带 `engineering` 字段 ? 槽值 : `config.agent.engineering`」)=== true 时,判据前置于
87
+ // `switchToSlot`——判据落其后会留下「指针已切、会话未换」的半态(下次 `saveSession` 把旧
88
+ // 会话写回目标槽 = 跨槽覆盖)。预读 = `loadSlotFile`(**纯读**——无认领副作用)。
89
+ // 触发条件 = 合值(遗留槽(无 `engineering` 字段)自工程会话切入时合值 = true ⇒ 预判据
90
+ // 照触发——F3 触发条件口径);槽不可读(preview null)不判:该次切换本就以「Slot not
91
+ // found」收场(switchToSlot 返回 null),无半态面。拒 ⇒ 切槽整体不发生(switchToSlot /
92
+ // applySession 均不调——零副作用)+ 原因行;准 ⇒ 既有切换 + applySession 之后一行附着。
93
+ const preview = loadSlotFile(agent.cwd, e.slot)
94
+ const slotEng = preview?.engineering
95
+ const targetEng = preview != null && (slotEng !== undefined ? slotEng === true : agent.config?.agent?.engineering === true)
96
+ let manifestAfterSwitch = null
97
+ if (targetEng) {
98
+ const r = resolveEngineeringManifest(agent.cwd, { writer: "main" })
99
+ if (!r.ok) {
100
+ pushLine(`Cannot switch to slot ${e.slot} — ${r.message} (slot unchanged)`, C.warn)
101
+ return
102
+ }
103
+ manifestAfterSwitch = r.manifest
104
+ }
84
105
  const data = switchToSlot(agent.cwd, e.slot)
85
106
  if (!data) {
86
107
  pushLine(`Slot ${e.slot} not found`, C.dim)
@@ -90,6 +111,8 @@ export async function handleSessionCommand(ctx) {
90
111
  pushLine(`⚠ Slot ${e.slot} is being used by another live process (${occ.owner}) — continuing here will create a new copy on the next save`, C.warn)
91
112
  }
92
113
  applySession(agent, data, occ.occupied ? {} : { slot: e.slot })
114
+ // #41 准翻列:附着动作 = 翻转面自己赋值(`agent.manifest` ← 判据结果——相位行当回合起活)。
115
+ if (manifestAfterSwitch) agent.manifest = manifestAfterSwitch
93
116
  // Rebuild from history (lazy) — the display snapshot is deprecated.
94
117
  // §14.3.6:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
95
118
  // 未绑定(占用分支——模式 F)回退全量数组。
@@ -4,7 +4,7 @@ import { ansi, C } from "./ansi.mjs"
4
4
  * ctx: { agent, pushLine, pushLabel } */
5
5
  export async function handleSkillsCommand(ctx) {
6
6
  const { agent, pushLine, pushLabel } = ctx
7
- const { loadSkills } = await import("../skills.mjs")
7
+ const { loadSkills } = await import("@thincoder/core/skills.mjs")
8
8
  const skills = await loadSkills(agent.cwd)
9
9
  pushLabel(`❯ Skills`, ansi.bold + C.tool)
10
10
  if (skills.length === 0) {
@@ -5,7 +5,7 @@ import { ansi, C } from "./ansi.mjs"
5
5
 
6
6
  export async function handleThinkCommand(ctx, args = []) {
7
7
  const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
8
- const { specForModel } = await import("../config.mjs")
8
+ const { specForModel } = await import("@thincoder/core/config.mjs")
9
9
 
10
10
  // Fast path: direct args — exit immediately
11
11
  const cur = agent.provider
@@ -33,7 +33,8 @@ export async function handleThinkCommand(ctx, args = []) {
33
33
  if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
34
34
  await applyThink({ action: "effort", level }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
35
35
  pushLabel("❯ Think", ansi.bold + C.tool)
36
- pushLine(`Thinking effort: ${level}`, C.tool)
36
+ // none = 关思考档(applyThink 内归一到 off 路径)⇒ 回执同 off 文案,不比照档位报“…none”
37
+ pushLine(level === "none" ? "Thinking: OFF" : `Thinking effort: ${level}`, C.tool)
37
38
  return
38
39
  }
39
40
  if (sub) { pushLine("Usage: /think [on|off|effort <level>]", C.error); return }
@@ -78,18 +79,29 @@ export async function handleThinkCommand(ctx, args = []) {
78
79
  pushLine(`Auto-think: ${newAuto ? "ON" : "OFF"}`, C.tool)
79
80
  return // exit loop — no useful actions remain when auto mode just changed (T8b 锁定——toggle 即退是已批准语义——2026-09-05 曾误改 OFF 回菜单——测试抓回归——已撤销)
80
81
  } else if (e.action === "effort") {
81
- pushLine(`Reasoning effort: ${e.level}`, C.tool)
82
+ pushLine(e.level === "none" ? "Thinking: OFF" : `Reasoning effort: ${e.level}`, C.tool)
82
83
  } else {
84
+ // 与菜单头(:47-48)同式:`thinking:null`(NF1 显式 off)必须落 OFF——本式缺 `!== null` 守卫时
85
+ // effort 型 off 后回执误报 ON(同一状态、两处公式——单一口径 = 面板式)。
83
86
  const nowEnabled = cur.thinking?.type === thinkOnValue
84
- || (cur.thinking?.type === undefined && !isCustomThink)
87
+ || (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
85
88
  pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
86
89
  }
87
90
  }
88
91
  }
89
92
 
90
- /** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop */
91
- async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
93
+ /** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop.
94
+ * Named export (2026-09-20): the on-default-effort rule (§2.8) is unit-tested by driving this
95
+ * directly with a synthetic spec — behavior otherwise unchanged.
96
+ * Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。 */
97
+ export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
92
98
  const cur = agent.provider
99
+ // `effort` 档位取 `"none"` = **关思考**(非“强度零”):与本面 off 动作同语义 ⇒ 归一到 off 路径
100
+ // (各族取其原生 off 形:effort 型 = `thinking:null`+删 effort;type 型 = `{type:"disabled"}`)。
101
+ // 不归一的两处违约:① 菜单头同拍显示「Thinking: ON | Effort: none」自相矛盾(off 标记缺失);
102
+ // ② 残留 `reasoningEffort:"none"` 会被载荷层当强度档送(`provider/core.mjs:197-204`),与百炼
103
+ // qwen 侧 off 判据(`config.mjs:133-139` 的 `thinking === null ⇒ false`)错位 ⇒ 服务端仍思考。
104
+ if (e.action === "effort" && e.level === "none") e = { action: "off" }
93
105
  if (e.action === "auto") {
94
106
  const cfg = agent.config.agent ??= {}
95
107
  cfg.autoThink = !cfg.autoThink
@@ -113,9 +125,10 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
113
125
  // Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
114
126
  // delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
115
127
  if (!enable) { cur.thinking = null; delete cur.reasoningEffort }
116
- // "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
117
- // (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
118
- else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
128
+ // "on" 默认 effort 取 spec 枚举的**首个非 "none"** 档(批次档 §1.8-② / 设计 §2.8 D-9):硬编码 "high"
129
+ // 对 qwen3.8-max(enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置);而直接取枚举
130
+ // 首项在新档(首项 "none")等于把思考关掉 ⇒ 跳过 "none";枚举全 "none" 时与无枚举同型 ⇒ 回退 "high"
131
+ else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.find((v) => v !== "none") ?? "high" }
119
132
  await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
120
133
  if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
121
134
  else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
@@ -11,7 +11,7 @@ export function createConfigHelpers(agent, opts = {}) {
11
11
  /** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
12
12
  * slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
13
13
  async function persistRaw(mutate) {
14
- const { writeConfigAtomic, configPath } = await import("../config.mjs")
14
+ const { writeConfigAtomic, configPath } = await import("@thincoder/core/config.mjs")
15
15
  const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
16
16
  if (!r.ok) throw new Error("config changed on disk concurrently — retry")
17
17
  }
@@ -3,17 +3,17 @@
3
3
  *
4
4
  * 职责(§15.3.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
5
5
  * 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
6
- * `src/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §23.3.1,D2 单源);
6
+ * `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §23.3.1,D2 单源);
7
7
  * 本模块只承载 TUI 面常量与对账。
8
8
  *
9
- * 依赖方向:本模块只引 `./ansi.mjs` + `../text-budget.mjs`(叶子——不引 subagent 族,
9
+ * 依赖方向:本模块只引 `./ansi.mjs` + `@thincoder/core/text-budget.mjs`(叶子——不引 subagent 族,
10
10
  * 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
11
11
  *
12
12
  * 计量口径 = UTF-16 码元(`String.length`——TUI.md §15.2 表 1);行数维(5000 行环 /
13
13
  * 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
14
14
  */
15
15
  import { C } from "./ansi.mjs"
16
- import { capText, appendCappedText, fillMarker } from "../text-budget.mjs"
16
+ import { capText, appendCappedText, fillMarker } from "@thincoder/core/text-budget.mjs"
17
17
 
18
18
  export { capText }
19
19
 
@@ -35,8 +35,10 @@ export const SUB_BLOCK_CHAR_LIMIT = 128_000
35
35
  export const ADVISOR_TEXT_MAX_CHARS = 128_000
36
36
  /** 流式缓冲(`state.streaming` / `state.reasoning`)累积额度。 */
37
37
  export const STREAM_MAX_CHARS = 256_000
38
- /** `state.lines` 全部行与载体文本总量(超出:与 5000 行环同款裁头 1000 行 + 收据行)。 */
38
+ /** `state.lines` 全部行与载体文本总量(超出:最小步进裁头 + 冻结锚点平移 + 收据行——保底见下行)。 */
39
39
  export const LINES_CHAR_BUDGET = 2_000_000
40
+ /** 字符超额裁剪保底行数——裁剪不得使 `state.lines` 低于该值;触底仍未脱额即停(接受超额——上界 = 保底 × 单行上限)。 */
41
+ export const LINES_TRIM_FLOOR = 200
40
42
  /** 搜索匹配计数上限(超出截断 + 提示行)。 */
41
43
  export const SEARCH_MATCH_CAP = 10_000
42
44
 
@@ -140,6 +142,14 @@ export function accountLine(state, l) {
140
142
  l._budgetChars = next
141
143
  }
142
144
 
145
+ /** 行移除负向出账(§5.5 记账口径——增删均须过账;不变式 `_linesChars === Σ lineChars`):
146
+ * `state._linesChars -= lineChars(l)`,钳 0。与 `accountLine` 不可互替——后者以行对象
147
+ * `_budgetChars` 幂等(行从 `state.lines` 脱落后重调增量恒 0 ⇒ 账不降),移除需本导出。 */
148
+ export function releaseLine(state, l) {
149
+ if (!l) return
150
+ state._linesChars = Math.max(0, (state._linesChars ?? 0) - lineChars(l))
151
+ }
152
+
143
153
  /** 直算对账(双算法对照 / 初始化 / 漂移自愈——测试直算同源)。 */
144
154
  export function accountAll(state) {
145
155
  let total = 0
@@ -152,10 +162,19 @@ export function accountAll(state) {
152
162
  return total
153
163
  }
154
164
 
165
+ /** 收据行长度预留(`syncLineBudget` 单轮 need 计入的收据余量:前缀 32 + N 数字位 + 尾 17 < 96——覆 7 位数余量)。 */
166
+ const RECEIPT_RESERVE_CHARS = 96
167
+
155
168
  /**
156
- * `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` → 与 5000 行环
157
- * 同款裁头 1000 行 + 收据行;`onTrim(state, removedCount)` 由调用方注入冻结锚点平移
158
- * (display-budget 不引 subagent 族——避环)。幂等(未超限零动作)。
169
+ * `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` → **最小步进**裁头 +
170
+ * 收据行,且不使 `lines.length` 低于 `LINES_TRIM_FLOOR`(触底 = 内层 break——接受超额,
171
+ * 不靠守卫退出:FLOOR+1 且残余超额时守卫仍为真)。每轮裁「至额度内所需最少行数」
172
+ * (need = 超额 + 收据上界预留;上限 = 可裁至保底的行数);`onTrim(state, removedCount)` 由
173
+ * 调用方注入冻结锚点平移(display-budget 不引 subagent 族——避环)。`pushLineLike` 注入面(无生产调用):
174
+ * 仅**头插**语义下自洽——尾插会使收据位置、收据 N 口径与「净位移 = 丢行数 − 1」约定失真。
175
+ * 幂等(未超限 / 行数 ≤ 保底)。
176
+ * 旧颗粒 `min(1000, lines.length-1)` 在行数 ≤1001 且超额时一轮裁到 1 行(用户实测收据
177
+ * 「1 lines remaining」——TUI-HISTORY-TRIM 批修复:保底 200 + 最小步进)。
159
178
  * @returns {number} 本轮裁掉的行数
160
179
  */
161
180
  export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {}) {
@@ -163,11 +182,13 @@ export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {
163
182
  if (!Array.isArray(lines)) return 0
164
183
  if (!Number.isFinite(state._linesChars)) accountAll(state)
165
184
  let removed = 0
166
- while (state._linesChars > LINES_CHAR_BUDGET && lines.length > 1) {
167
- const take = Math.min(1000, lines.length - 1)
168
- const dropped = lines.splice(0, take)
185
+ while (state._linesChars > LINES_CHAR_BUDGET && lines.length > LINES_TRIM_FLOOR) {
186
+ const limit = lines.length - LINES_TRIM_FLOOR
187
+ const need = state._linesChars - LINES_CHAR_BUDGET + RECEIPT_RESERVE_CHARS
188
+ let take = 0
169
189
  let freed = 0
170
- for (const l of dropped) freed += lineChars(l)
190
+ while (take < limit && freed < need) { freed += lineChars(lines[take]); take++ }
191
+ lines.splice(0, take)
171
192
  state._linesChars = Math.max(0, state._linesChars - freed)
172
193
  removed += take
173
194
  const receipt = `... [earlier messages trimmed — ${lines.length} lines remaining]`
@@ -179,6 +200,7 @@ export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {
179
200
  state._linesChars += receipt.length
180
201
  }
181
202
  if (typeof onTrim === "function") onTrim(state, take)
203
+ if (take >= limit) break // 触底(可裁上限用尽)——接受超额;收敛靠本 break,不靠守卫退出
182
204
  }
183
205
  return removed
184
206
  }
package/src/tui/index.mjs CHANGED
@@ -19,15 +19,16 @@
19
19
  */
20
20
 
21
21
  import { emitKeypressEvents } from "node:readline"
22
+ import { readFileSync } from "node:fs"
22
23
  import { PassThrough } from "node:stream"
23
- import { saveSession } from "../session.mjs"
24
- import { closeAllMcp } from "../mcp.mjs"
24
+ import { saveSession } from "@thincoder/core/session.mjs"
25
+ import { closeAllMcp } from "@thincoder/core/mcp.mjs"
25
26
  import { ansi, C } from "./ansi.mjs"
26
27
  import { createRenderLoop } from "./render-loop.mjs"
27
28
  import { makeDimsState } from "./dims.mjs"
28
29
  import { SLASH_COMMANDS, createSlashCommands } from "./slash-commands.mjs"
29
30
  import { createWizard } from "./wizard.mjs"
30
- import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
31
+ import { writeStartupSequence, writeLoadingLine, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
31
32
  import { createPickers } from "./pickers.mjs"
32
33
  import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
33
34
  import { createInteraction } from "./interaction.mjs"
@@ -45,11 +46,11 @@ import { startLedgerSurface } from "./ledger-surface.mjs"
45
46
  export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
46
47
 
47
48
  /**
48
- * SESSION.md §8 D-S2 — TUI 启动首帧前的 provider 重选流程:
49
+ * SESSION.md §6.8 D-S2 — TUI 启动首帧前的 provider 重选流程:
49
50
  * provider 无效(`_providerInvalid` 标记或 provider 为 null)→ 先弹模型选择 picker
50
51
  * (复用 openModelPicker,展示当前可用 providers);用户选定后继续正常启动。
51
- * 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model 选择或
52
- * /provider 配置")——绝不因无 provider 拒绝进入。返回 true 表示弹过选择流程。
52
+ * 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model 配置
53
+ * 渠道与模型")——绝不因无 provider 拒绝进入。返回 true 表示弹过选择流程。
53
54
  */
54
55
  export async function promptProviderIfInvalid(agent, openModelPicker, pushLine) {
55
56
  if (!(agent._providerInvalid || !agent.provider)) return false
@@ -60,7 +61,7 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
60
61
  const hasProviders = (agent.providers?.length ?? 0) > 0
61
62
  pushLine(hasProviders && !agent.config?.defaultModel
62
63
  ? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
63
- : "未配置有效 provider,可用 /model 选择或 /provider 配置", C.warn)
64
+ : "未配置有效 provider,可用 /model 配置渠道与模型", C.warn)
64
65
  }
65
66
  return true
66
67
  }
@@ -159,6 +160,13 @@ export async function startTUI(agent, opts = {}) {
159
160
  // modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
160
161
  // translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
161
162
  writeStartupSequence()
163
+ // 启动加载画面(2026-09-21 用户):冷启动剩余期(memory 同步 / 索引扫描)非黑。
164
+ // 静态读(render-frame 先例——module-load 一次 ✗ 首帧 render 覆盖本行 ✗ 无需清除逻辑)。
165
+ {
166
+ let v = ""
167
+ try { v = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version } catch { /* 尽力面 */ }
168
+ writeLoadingLine(undefined, v)
169
+ }
162
170
  setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
163
171
 
164
172
  const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
@@ -406,7 +414,7 @@ export async function startTUI(agent, opts = {}) {
406
414
  const { persistRaw, syncProviderField, maskKey } = createConfigHelpers(agent)
407
415
 
408
416
  // Model picker + generic picker: implemented in pickers.mjs
409
- const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot } = createPickers({
417
+ const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot, confirmDelete } = createPickers({
410
418
  agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
411
419
  })
412
420
 
@@ -423,7 +431,7 @@ export async function startTUI(agent, opts = {}) {
423
431
  const { handleSlash, completions, handleTab } = createSlashCommands({
424
432
  agent, state, distillOpts,
425
433
  pushLine, pushLabel, render,
426
- showPicker, closePicker, askQuestion, askPermission,
434
+ showPicker, closePicker, askQuestion, askPermission, confirmDelete,
427
435
  persistRaw, syncProviderField, maskKey,
428
436
  openModelPicker: () => openModelPicker(),
429
437
  selectModel,
@@ -458,7 +466,7 @@ export async function startTUI(agent, opts = {}) {
458
466
 
459
467
  // ---------------------------------------------------------- Startup screen + background indexing
460
468
 
461
- // SESSION.md §8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
469
+ // SESSION.md §6.8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
462
470
  // → 先弹模型选择 picker(keyStream 已挂 keypress,Esc/Enter 可用);Esc → 提示行,仍进 TUI
463
471
  await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
464
472
 
@@ -1,5 +1,6 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
- import { detectDanger } from "../tools/shared.mjs"
2
+ import { detectDanger } from "@thincoder/core/tools/shared.mjs"
3
+ import { askPermission as coreAskPermission } from "@thincoder/core/permission.mjs"
3
4
 
4
5
  /** Interaction primitives: permission approval + question input.
5
6
  * Extracted from index.mjs, receives closure dependencies via createInteraction(ctx).
@@ -9,6 +10,11 @@ import { detectDanger } from "../tools/shared.mjs"
9
10
  * answer mode (the user supplements/corrects the AI's preset choices). */
10
11
  export const QUESTION_CUSTOM = "\u0001custom-answer"
11
12
 
13
+ /** y/n-only modal family(判据单源——key-modes / render-frame 同引):`continue`(回合帽)
14
+ * 与 `retry`(provider 失败面)同形 ⇒ 仅 `y` / `n`。`a` 不属该族键面(会翻会话级 AUTO)。 */
15
+ const YES_NO_MODALS = new Set(["continue", "retry"])
16
+ export function isYesNoModal(name) { return YES_NO_MODALS.has(name) }
17
+
12
18
  export function createInteraction(ctx) {
13
19
  const { agent, state, pushLine, pushLabel, render, summarize } = ctx
14
20
 
@@ -62,12 +68,15 @@ export function createInteraction(ctx) {
62
68
  pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
63
69
  return Promise.resolve(true)
64
70
  }
65
- // store preview content in permissionPreview, rendered above input box next to "Allow?" prompt
66
- state.permissionPreview = formatPermission(name, args)
67
- return new Promise((resolve) => {
68
- state.permission = { name, args, resolve }
69
- state.status = `Waiting: ${name}`
70
- render()
71
+ // 闸语义 / 请示流程 = 核单源(P2 批 §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
72
+ // 注入(permissionPreview 渲染在输入框上方,与 "Allow?" 提示同帧)。
73
+ return coreAskPermission(name, args, {
74
+ ask: ({ name: n, toolArgs }) => new Promise((resolve) => {
75
+ state.permissionPreview = formatPermission(n, toolArgs)
76
+ state.permission = { name: n, args: toolArgs, resolve }
77
+ state.status = `Waiting: ${n}`
78
+ render()
79
+ }),
71
80
  })
72
81
  }
73
82
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { C } from "./ansi.mjs"
9
9
  import { readClipboardText, insertPastedText } from "./clipboard.mjs"
10
- import { QUESTION_CUSTOM } from "./interaction.mjs"
10
+ import { QUESTION_CUSTOM, isYesNoModal } from "./interaction.mjs"
11
11
  import { inputContentWidth } from "./layout.mjs"
12
12
  import { moveCursorVertical } from "./render.mjs"
13
13
 
@@ -35,14 +35,19 @@ export function denyModalForOwner(state, key, { pushLine, render } = {}) {
35
35
  }
36
36
 
37
37
  /** permission 确认模态:y/n/a(a = approve + AUTO ON);batch(§16 D-B1):a/o/n(Esc = deny)。
38
+ * `continue` / `retry` = y/n-only 族(判据单源 `isYesNoModal`——X8 跟进①:`retry` 框不
39
+ * 广告 `a`,零会话级 AUTO 副作用)。
38
40
  * consume:valid 键或 Esc 走 resolve 分支;其余键静默吞掉(模态独占)。 */
39
41
  export function handlePermissionMode(str, key, ctx) {
40
42
  const { state, agent, pushLine, render } = ctx
41
43
  if (!state.permission) return false
42
44
  const answer = (str || "").toLowerCase()
45
+ // `yOnly` = 本框键面仅 y/n(continue / retry);`isContinue` 仅保轨迹行豁免(continue 有
46
+ // 自有输出行——retry **保留** `[approved]` / `[denied]` 轨迹行)。
43
47
  const isContinue = state.permission.name === "continue"
48
+ const yOnly = isYesNoModal(state.permission.name)
44
49
  const isBatch = Boolean(state.permission.batch)
45
- const validKeys = isContinue ? ["y", "n"] : isBatch ? ["a", "o", "n"] : ["y", "n", "a"]
50
+ const validKeys = yOnly ? ["y", "n"] : isBatch ? ["a", "o", "n"] : ["y", "n", "a"]
46
51
  if (validKeys.includes(answer) || key.name === "escape") {
47
52
  const { resolve, name } = state.permission
48
53
  state.permission = null
@@ -58,13 +63,13 @@ export function handlePermissionMode(str, key, ctx) {
58
63
  render()
59
64
  return true
60
65
  }
61
- if (answer === "a" && !isContinue) {
66
+ if (answer === "a" && !yOnly) {
62
67
  agent.autoApprove = true
63
68
  agent._pendingReminders = agent._pendingReminders ?? []
64
69
  agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
65
70
  pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
66
71
  }
67
- const approved = answer === "y" || (answer === "a" && !isContinue)
72
+ const approved = answer === "y" || (answer === "a" && !yOnly)
68
73
  // leave trail: record approval/denial in conversation (continue prompt has its own output, don't duplicate)
69
74
  if (!isContinue) {
70
75
  pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)