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.
- package/CHANGELOG.md +36 -0
- package/package.json +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/run.mjs +9 -11
- package/src/agent/dispatch.mjs +38 -13
- package/src/agent/setup.mjs +2 -1
- package/src/agent.mjs +34 -0
- package/src/cli/make-agent.mjs +11 -5
- package/src/escape.mjs +43 -8
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/helpers.mjs +14 -5
- package/src/mcp/transport-http.mjs +79 -27
- package/src/mcp/transport-stdio.mjs +57 -3
- package/src/mcp/transport-ws.mjs +46 -12
- package/src/mcp.mjs +197 -58
- package/src/prompts/discipline.md +44 -1
- package/src/provider/anthropic.mjs +51 -18
- package/src/provider/core.mjs +163 -36
- package/src/provider/google.mjs +41 -15
- package/src/provider/rate.mjs +5 -0
- package/src/provider/responses.mjs +498 -0
- package/src/provider/retry.mjs +125 -0
- package/src/provider/sse.mjs +58 -24
- package/src/proxy.mjs +36 -6
- package/src/session-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +267 -306
- package/src/tools/bash.md +2 -2
- package/src/tools/execute.md +1 -1
- package/src/tools/execute.mjs +3 -3
- package/src/tools/fetch.md +1 -0
- package/src/tools/file.mjs +136 -11
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -6
- package/src/tools/git.mjs +68 -155
- package/src/tools/shared.mjs +5 -3
- package/src/tools/system.mjs +19 -1
- package/src/tools/web.mjs +44 -14
- package/src/tools/websearch.md +3 -1
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/cmd-new.mjs +6 -6
- package/src/tui/cmd-restore.mjs +27 -6
- package/src/tui/cmd-session.mjs +17 -4
- package/src/tui/fold-block.mjs +59 -11
- package/src/tui/index.mjs +59 -67
- package/src/tui/key-handler.mjs +3 -1
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +86 -8
- package/src/tui/render-conversation.mjs +260 -214
- package/src/tui/render-frame.mjs +22 -6
- package/src/tui/render-loop.mjs +11 -1
- package/src/tui/startup.mjs +1 -1
- package/src/tui/subagent-blocks.mjs +5 -1
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-args.mjs +4 -0
- package/src/tui/tool-events.mjs +1 -1
- package/src/tui/tui-lifecycle.mjs +45 -0
package/src/tui/render-loop.mjs
CHANGED
|
@@ -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`)
|
package/src/tui/startup.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/src/tui/tool-args.mjs
CHANGED
|
@@ -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()
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -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
|
+
}
|