thincoder 0.12.52 → 0.12.54

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 (58) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +1 -1
  3. package/src/acp.mjs +60 -18
  4. package/src/advisor/run.mjs +9 -11
  5. package/src/agent/dispatch.mjs +38 -13
  6. package/src/agent/setup.mjs +2 -1
  7. package/src/agent.mjs +34 -0
  8. package/src/cli/make-agent.mjs +11 -5
  9. package/src/escape.mjs +43 -8
  10. package/src/git/checkpoint.mjs +32 -6
  11. package/src/mcp/helpers.mjs +14 -5
  12. package/src/mcp/transport-http.mjs +79 -27
  13. package/src/mcp/transport-stdio.mjs +57 -3
  14. package/src/mcp/transport-ws.mjs +46 -12
  15. package/src/mcp.mjs +197 -58
  16. package/src/prompts/discipline.md +44 -1
  17. package/src/provider/anthropic.mjs +51 -18
  18. package/src/provider/core.mjs +163 -36
  19. package/src/provider/google.mjs +41 -15
  20. package/src/provider/rate.mjs +5 -0
  21. package/src/provider/responses.mjs +498 -0
  22. package/src/provider/retry.mjs +125 -0
  23. package/src/provider/sse.mjs +58 -24
  24. package/src/proxy.mjs +36 -6
  25. package/src/session-migrate.mjs +6 -0
  26. package/src/session-slots.mjs +361 -0
  27. package/src/session.mjs +267 -306
  28. package/src/tools/bash.md +2 -2
  29. package/src/tools/execute.md +1 -1
  30. package/src/tools/execute.mjs +3 -3
  31. package/src/tools/fetch.md +1 -0
  32. package/src/tools/file.mjs +136 -11
  33. package/src/tools/git-checkpoint.mjs +143 -0
  34. package/src/tools/git-ext.mjs +173 -0
  35. package/src/tools/git.md +21 -6
  36. package/src/tools/git.mjs +68 -155
  37. package/src/tools/shared.mjs +5 -3
  38. package/src/tools/system.mjs +19 -1
  39. package/src/tools/web.mjs +44 -14
  40. package/src/tools/websearch.md +3 -1
  41. package/src/tui/ansi.mjs +2 -0
  42. package/src/tui/cmd-new.mjs +6 -6
  43. package/src/tui/cmd-restore.mjs +27 -6
  44. package/src/tui/cmd-session.mjs +17 -4
  45. package/src/tui/fold-block.mjs +59 -11
  46. package/src/tui/index.mjs +59 -67
  47. package/src/tui/key-handler.mjs +3 -1
  48. package/src/tui/layout.mjs +81 -25
  49. package/src/tui/mouse.mjs +86 -8
  50. package/src/tui/render-conversation.mjs +260 -214
  51. package/src/tui/render-frame.mjs +22 -6
  52. package/src/tui/render-loop.mjs +11 -1
  53. package/src/tui/startup.mjs +1 -1
  54. package/src/tui/subagent-blocks.mjs +5 -1
  55. package/src/tui/subagent-panel.mjs +81 -0
  56. package/src/tui/tool-args.mjs +4 -0
  57. package/src/tui/tool-events.mjs +1 -1
  58. package/src/tui/tui-lifecycle.mjs +45 -0
@@ -63,6 +63,16 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
63
63
  // restored to full width only after flush stopped the output). dims are
64
64
  // updated by: startup seed + a delayed re-sample + resize events.
65
65
  const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || startupDims.cols, rows: process.stdout.rows || startupDims.rows }
66
+ // 2026-08-31 流式跟随(会诊 kimi 简化 + deepseek/glm 锚定):_followTail 即完整语义——
67
+ // 开 → 渲染前钉底(tool/pushLine 行同样生效,无跟随空洞);关(用户上滚)→ 锚定补偿:
68
+ // scroll 是距底偏移,暂停期间 convLen 增长会把用户读的行顶走——按增长量补偿。
69
+ const convLen = countConvLines(state, dims.cols, dims.rows)
70
+ if (state._followTail) {
71
+ state.scroll = 0
72
+ } else if (state._pauseAnchorLen != null && convLen > state._pauseAnchorLen) {
73
+ state.scroll += convLen - state._pauseAnchorLen
74
+ }
75
+ state._pauseAnchorLen = convLen
66
76
 
67
77
  // NOTE (§7.2 D6): the old state.outputPanels prune is gone — output panels
68
78
  // are abolished; subagent blocks live in the conversation and are never
@@ -104,7 +114,7 @@ export function createRenderLoop(state, agent, ctx, pushLine, write = (s) => pro
104
114
  const hasOverlay = state.permission || state.question || state.picker || state.wizard?.step === "provider"
105
115
  const cursorSuffix = hasOverlay ? "" : `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`
106
116
 
107
- if (out.length || cursorSuffix) write(ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix)
117
+ if (out.length || cursorSuffix) write(ansi.wrapOff + ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix + ansi.wrapOn)
108
118
  } catch (e) {
109
119
  // Don't let a render error crash the TUI
110
120
  if (process.env.THINCODER_DEBUG_RENDER) process.stderr.write(`[render-error] ${e?.stack ?? e}\n`)
@@ -7,7 +7,7 @@ import { slimToolResultForDisplay } from "./tool-events.mjs"
7
7
  * the latest INITIAL_HISTORY_MESSAGES, then PgUp-at-top loads HISTORY_PAGE_MESSAGES
8
8
  * more. Rebuilding an 8000-message session eagerly froze startup + first render. */
9
9
  export const INITIAL_HISTORY_MESSAGES = 200
10
- export const HISTORY_PAGE_MESSAGES = 50
10
+ export const HISTORY_PAGE_MESSAGES = 20
11
11
 
12
12
  /**
13
13
  * Convert history[startIdx, endIdx) into conversation source lines (label lines
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { C } from "./ansi.mjs"
15
+ import { describeToolArgs } from "./tool-args.mjs"
15
16
 
16
17
  /** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
17
18
  export const SUB_PREFIX_RE = /^([\w-]+)#(\d+)\//
@@ -307,7 +308,10 @@ export function routeSubToolCall(state, name, args, scheduleRender) {
307
308
  sub.currentTool = name.slice(subMatch[0].length)
308
309
  sub.toolArgs = args
309
310
  sub.approval = null
310
- appendSubBlock(sub, "tool", `❯ ${sub.currentTool}${args?.command ? " — " + String(args.command).replace(/\s+/g, " ").trim().slice(0, 80) : ""}\n`, { fresh: true })
311
+ // 2026-08-31: 工具行带参数摘要(与主 agent 工具块同款 describeToolArgs 单源)——
312
+ // 此前只显示工具名(bash 独享 "— 命令"),read/grep/glob 等全裸名。
313
+ const argsDesc = describeToolArgs(sub.currentTool, args)
314
+ appendSubBlock(sub, "tool", `❯ ${sub.currentTool}${argsDesc ? " " + argsDesc : ""}\n`, { fresh: true })
311
315
  scheduleRender()
312
316
  return true
313
317
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * subagent-panel.mjs — 运行中子 agent 固定底部面板渲染(AGENT-LOOP.md §7.2.1 D1/D2)。
3
+ *
4
+ * 面板位于 conversation 与 todo 之间(布局顺序 header → conversation → 面板 →
5
+ * todo → picker → permission → queue → input → status),高度完全自适应 = 全部
6
+ * 运行中区块的渲染行数(F2,会话区被挤小);无运行中区块 → 返回 [](F6 空态,
7
+ * 无悬空分隔线)。子 agent 完成后立即冻结进会话流(subagent-blocks.mjs
8
+ * freezeSubTaskLines,✓ 头 + 可展开,§7.2 D4 现状不变),面板下一帧自然移除
9
+ * 该区块(F5)——本模块只渲染 `!done` 条目。
10
+ *
11
+ * 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
12
+ * (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
13
+ * 渲染)——若本函数放 render-frame 会引入 layout↔render-frame 循环依赖。
14
+ *
15
+ * 区块渲染逻辑自 render-conversation.mjs buildConvLines runningSubs 段迁移
16
+ * (§7.2.1 D2):折叠头 `[▶/⏸ key · model · elapsed · turn] state`(⏸ = 等待
17
+ * 审批态图标,sub.approval 非空时显示)+ tail 3;展开态经 fold-block.mjs 公共
18
+ * 组件(renderBlockTimeline + renderExpandedBlock,60% 封顶 + 块内滚动)。
19
+ * 折叠状态 key = `sub-${key}` 跨 turn 保持(D5,与冻结区块同一 key——冻结边界
20
+ * 无缝衔接)。
21
+ */
22
+ import { C } from "./ansi.mjs"
23
+ import { sliceByWidth, stringWidth } from "./render.mjs"
24
+ import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
25
+
26
+ /**
27
+ * 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
28
+ * 展开全量)。maxRows = 终端行数(展开态 60% 封顶窗口化);省略 = 不封顶
29
+ * (单测/无终端环境)。
30
+ * @returns {Array<{text: string, color: string, ...}>}
31
+ */
32
+ export function renderSubagentPanel(state, cols, maxRows) {
33
+ const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
34
+ if (runningSubs.length === 0) return []
35
+ const out = []
36
+ // 面板顶部边界线(现状分隔线语义迁移,§7.2.1 D2/NF2)——面板存在即画线,
37
+ // 无运行区块时面板整体不渲染(F6:无悬空线)。
38
+ out.push({ text: "─".repeat(Math.max(1, cols - 1)), color: C.dim, _skipDimFold: true })
39
+ for (const sub of runningSubs) {
40
+ const foldKey = `sub-${sub.key}`
41
+ // 头部摘要:`[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
42
+ // ⏸ = 等待审批态(sub.approval 非空,评审 #5 定义);图标在括号内,
43
+ // 与冻结头 `[✓ …]` 格式统一(任务简报 UI 决策)。
44
+ const icon = sub.approval ? "⏸" : "▶"
45
+ const elapsed = Math.floor((Date.now() - sub.started) / 1000)
46
+ // 评审 #1 宽度预算:模型名先单独按显示宽度截断([model] token 原样记录可长
47
+ // 20-30+ 字符,不截断则括号前缀宽度不可预算、状态区被挤出终端右边距);
48
+ // 再量括号前缀实际显示宽度,状态区按 cols - bracketWidth - 2 截断——整行
49
+ // ≤ cols 铁律(TUI 布局纪律:任何写入帧的行 ≤ cols)。极端窄终端下括号
50
+ // 前缀自身也按 cols-2 截断兜底(状态区宁可让位也不撑破帧)。
51
+ const modelPart = sub.model
52
+ ? ` · ${sliceByWidth(sub.model, Math.max(8, Math.floor(cols / 3)))}`
53
+ : ""
54
+ const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
55
+ const bracket = sliceByWidth(`[${icon} ${sub.key}${modelPart} · ${elapsed}s${turnPart}]`, Math.max(1, cols - 2))
56
+ const bracketWidth = stringWidth(bracket)
57
+ let statePart
58
+ if (sub.approval) statePart = `等待审批: ${sub.approval}`
59
+ else if (sub.currentTool) statePart = sub.currentTool
60
+ else statePart = "thinking..."
61
+ const argSummary = sub.currentTool && sub.toolArgs?.command
62
+ ? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
63
+ : ""
64
+ out.push({
65
+ text: `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`,
66
+ color: C.tool,
67
+ _foldToggle: foldKey,
68
+ })
69
+ if (isExpanded(state, foldKey)) {
70
+ // 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
71
+ const body = renderBlockTimeline(sub.blocks, cols)
72
+ out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
73
+ } else {
74
+ // 折叠态:tail 3 非空 block 行(最近活动),dim。
75
+ for (const line of foldTailLines(sub.blocks)) {
76
+ out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim })
77
+ }
78
+ }
79
+ }
80
+ return out
81
+ }
@@ -36,6 +36,10 @@ export function describeToolArgs(name, args) {
36
36
  const p = a.path ? ` in "${a.path}"` : ""
37
37
  return `/${String(pat)}/${p}`
38
38
  }
39
+ case "ls": {
40
+ const p = a.path ? String(a.path) : "."
41
+ return a.filter ? `${p} (filter: ${String(a.filter)})` : p
42
+ }
39
43
  case "websearch": return String(a.query ?? "")
40
44
  case "subagent": case "coder": case "explore": case "plan": case "eng-coder": {
41
45
  const task = String(a.task ?? "").replace(/\s+/g, " ").trim()
@@ -406,7 +406,7 @@ export function buildToolCallbacks(deps) {
406
406
  // the compressed version — persist it so the session file ends up compressed. Silent:
407
407
  // a save failure must never surface after the turn already returned.
408
408
  onDistilled: () => {
409
- try { saveSessionImpl(agent, state.lines) } catch { /* 静默 */ }
409
+ try { saveSessionImpl(agent, state.lines) } catch (e) { console.error(`[session] distilled save failed: ${e.message}`) }
410
410
  },
411
411
  onUsage: (usage) => {
412
412
  state.tokens.prompt += usage.prompt_tokens ?? 0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * tui-lifecycle.mjs — TUI 生命周期终端序列(2026-08-31 advisor round1 🔴:index.mjs
3
+ * 超 500 行硬限拆分——启动序列、退出清理序列与退出闭包从 index.mjs 移入本模块)。
4
+ */
5
+
6
+ import { ansi } from "./ansi.mjs"
7
+
8
+ /** TUI 启动序列:alt buffer + 隐藏光标 + 鼠标/粘贴/键盘增强 + 禁环绕。
9
+ * wrapOff(DECRST 7)为 2026-08-31 会诊的最终防线:Ambiguous 宽度字符(│/—/●/▸/…/↑↓)
10
+ * 在中文 locale 终端渲染 2 格而 stringWidth 按 1 格算 → 行实际超宽 → 物理 wrap 污染
11
+ * 下一行 + \x1b[K 清错行 → picker 残影;禁环绕后超宽行硬截断在边距,不可能跨行污染。
12
+ * 每帧 write 再包 wrapOff/wrapOn(render-loop),退出经 writeCleanupSequence 恢复。 */
13
+ export function writeStartupSequence(write = (s) => process.stdout.write(s)) {
14
+ write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn + ansi.keyboardPush + ansi.modifyOtherKeysOn + ansi.wrapOff)
15
+ }
16
+
17
+ /** TUI 清理序列:清屏 + 关闭鼠标/粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。 */
18
+ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
19
+ write(ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
20
+ }
21
+
22
+ /** 退出清理闭包:保存会话(同步)+ 关闭 MCP 子进程 + 恢复终端。幂等(cleanedUp 守卫)。 */
23
+ export function createExitCleanup({ agent, saveSession, closeAllMcp }) {
24
+ let cleanedUp = false
25
+ return () => {
26
+ if (cleanedUp) return
27
+ cleanedUp = true
28
+ // Save session before exit (synchronous write).
29
+ // Archiving to a slot is handled by /new and /session switch — not on every exit,
30
+ // otherwise simply opening and closing the TUI repeatedly would fill all slots with duplicates.
31
+ try {
32
+ saveSession(agent)
33
+ } catch {
34
+ // Save failure shouldn't block exit
35
+ }
36
+ // Kill MCP stdio subprocesses, don't leave orphans
37
+ try {
38
+ closeAllMcp(agent)
39
+ } catch {
40
+ // Can't close? fine, process is exiting anyway
41
+ }
42
+ process.stdin.setRawMode(false)
43
+ writeCleanupSequence()
44
+ }
45
+ }