thincoder 0.12.53 → 0.12.58
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 +74 -0
- package/bin/thincoder.mjs +17 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/messages.mjs +4 -2
- package/src/advisor/run.mjs +2 -2
- package/src/agent/dispatch.mjs +66 -26
- package/src/agent/helpers.mjs +13 -2
- package/src/agent/setup.mjs +16 -3
- package/src/agent/spawn-child.mjs +3 -1
- package/src/agent-tools/advisor.mjs +19 -9
- package/src/agent-tools/eng.mjs +2 -0
- package/src/agent-tools/subagent-check.mjs +107 -0
- package/src/agent-tools/subagent.mjs +205 -42
- package/src/agent.mjs +68 -3
- package/src/cli/make-agent.mjs +25 -0
- package/src/cli/memory-command.mjs +28 -7
- package/src/config.mjs +120 -8
- package/src/context.mjs +28 -7
- package/src/escape.mjs +110 -22
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/core.mjs +78 -10
- package/src/memory/docs.mjs +33 -7
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +23 -0
- package/src/prompts/discipline.md +17 -3
- package/src/prompts/engineering.md +62 -5
- package/src/prompts/main.md +1 -0
- package/src/prompts/system.md +2 -1
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +90 -26
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +282 -306
- package/src/tools/apply_patch.md +2 -0
- package/src/tools/bash.md +2 -2
- package/src/tools/edit-batch.mjs +104 -0
- package/src/tools/edit.md +3 -0
- package/src/tools/execute.md +4 -4
- package/src/tools/execute.mjs +14 -22
- package/src/tools/file.mjs +17 -55
- package/src/tools/file_ops.md +1 -1
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -8
- package/src/tools/git.mjs +55 -177
- package/src/tools/lint.md +1 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/patch.mjs +1 -1
- package/src/tools/shared.mjs +7 -20
- package/src/tui/agent-turn.mjs +3 -3
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +255 -114
- 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/index.mjs +28 -27
- package/src/tui/interaction.mjs +28 -1
- package/src/tui/key-handler.mjs +14 -2
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +41 -2
- package/src/tui/pickers.mjs +62 -4
- package/src/tui/render-conversation.mjs +36 -93
- package/src/tui/render-frame.mjs +40 -16
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +4 -2
- package/src/tui/subagent-blocks.mjs +119 -4
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-events.mjs +61 -16
- package/src/tui/tui-lifecycle.mjs +45 -0
package/src/tui/tool-events.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor
|
|
|
19
19
|
import {
|
|
20
20
|
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
21
21
|
routeSubToolOutput, finishSubTask, finishSubTasksByRole, finishSubTaskByModel, freezeDoneSubTasks,
|
|
22
|
+
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
22
23
|
} from "./subagent-blocks.mjs"
|
|
23
24
|
import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
24
25
|
|
|
@@ -106,6 +107,21 @@ function settleToolBlock(state, name, toolId, summary) {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
|
|
111
|
+
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
112
|
+
* keeps running, so its activity block must not be frozen at spawn time (it
|
|
113
|
+
* freezes via the ⟦ev⟧done event emitted at settle time — §15 D-A3). A real blocking
|
|
114
|
+
* report that happens to parse as this shape is a freak accident; the only
|
|
115
|
+
* consequence would be a late block freeze at turn end (cosmetic). */
|
|
116
|
+
function isAsyncSpawnResult(result) {
|
|
117
|
+
try {
|
|
118
|
+
const o = JSON.parse(result)
|
|
119
|
+
return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
|
|
120
|
+
} catch {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
|
|
110
126
|
|
|
111
127
|
/** Find the live tool-block carrier for a tool event: exact id match when the
|
|
@@ -130,8 +146,7 @@ function findToolBlock(state, name, toolId) {
|
|
|
130
146
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
131
147
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
132
148
|
export function buildToolCallbacks(deps) {
|
|
133
|
-
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askQuestion, saveSessionImpl } = deps
|
|
134
|
-
|
|
149
|
+
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
|
|
135
150
|
// NOTE: advisor buffers (_advisorThink/advisorStreaming) are cleared here too.
|
|
136
151
|
// Timing safety: onToolResult flushes _advisorThink into history and empties
|
|
137
152
|
// the buffers BEFORE onTurnEnd can call flushStream (tool result is
|
|
@@ -251,17 +266,23 @@ export function buildToolCallbacks(deps) {
|
|
|
251
266
|
// carrier) and the turn sweep would mislabel it "(interrupted)" — every
|
|
252
267
|
// successful subagent call showed that banner (consult P1, 2026-08-30).
|
|
253
268
|
settleToolBlock(state, name, toolId, "completed")
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
269
|
+
// Async spawn (§15 D-A1): the result is a status JSON, not a report — the
|
|
270
|
+
// child KEEPS running; skip the freeze (it would tombstone a live block
|
|
271
|
+
// and drop its relay stream). The block freezes on the ⟦ev⟧done event
|
|
272
|
+
// emitted at turn-end collection.
|
|
273
|
+
if (!isAsyncSpawnResult(result)) {
|
|
274
|
+
finishSubTask(state, SUBAGENT_ROLES, result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null)
|
|
275
|
+
// Freeze the finished blocks into the conversation stream (user report
|
|
276
|
+
// 2026-08-30): a pinned tail section left every ✓ block stuck above the
|
|
277
|
+
// input box forever. As lines they scroll away, stay expandable via the
|
|
278
|
+
// dim auto-fold, and subTasks releases the entry.
|
|
279
|
+
freezeDoneSubTasks(state)
|
|
280
|
+
// Subagent report preview (max 8 lines) displayed directly in conversation
|
|
281
|
+
const lines = result.split("\n")
|
|
282
|
+
const preview = lines.slice(0, SUBAGENT_PREVIEW_LINES).map((l) => l.slice(0, PREVIEW_LINE_CHARS)).join("\n")
|
|
283
|
+
if (preview) pushLine(preview, C.dim)
|
|
284
|
+
if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
|
|
285
|
+
}
|
|
265
286
|
} else if (name === "escalate") {
|
|
266
287
|
// 飞刀 post-op report landed → freeze its block into the conversation too.
|
|
267
288
|
settleToolBlock(state, name, toolId, "completed")
|
|
@@ -398,15 +419,39 @@ export function buildToolCallbacks(deps) {
|
|
|
398
419
|
scheduleRender()
|
|
399
420
|
},
|
|
400
421
|
onPermissionRequest: (name, args) => askPermission(name, args),
|
|
422
|
+
// Merged batch ask (§16 D-B1): one confirmation for N non-readonly tools in
|
|
423
|
+
// the same response — "approve all / one by one / deny" (key-handler resolves
|
|
424
|
+
// the verdict string; approveAll is batch-scope only, never the AUTO flag).
|
|
425
|
+
onBatchPermissionRequest: (req) => askBatchPermission(req),
|
|
401
426
|
onQuestion: (text, options) => askQuestion(text, options),
|
|
402
|
-
|
|
403
|
-
|
|
427
|
+
// Compression lifecycle (CONTEXT-COMPACTION.md §7 D-C2): the compression session renders
|
|
428
|
+
// as a subagent-style panel block — start → running panel ("Compressing context…" +
|
|
429
|
+
// "summarizing N messages" + elapsed ticker), fail → error text ONLY (no degradation note —
|
|
430
|
+
// that belongs to the 3-consecutive-failures fallback), success → frozen "Compressed: N
|
|
431
|
+
// tokens freed → summary (Xs)" / fallback → "truncated to N messages". The summary BODY
|
|
432
|
+
// never enters the panel or the stream (the summary call is silent). Replaces the old
|
|
433
|
+
// one-line "[context] Context too long..." warn (user ruling: panel, not a status line).
|
|
434
|
+
onCompressStart: (info) => {
|
|
435
|
+
ensureCompressPanel(state, info)
|
|
436
|
+
scheduleRender()
|
|
437
|
+
},
|
|
438
|
+
onCompressFail: (error) => {
|
|
439
|
+
// Q3 (CONTEXT-COMPACTION §7 F3): failure is no longer silent — visible on the panel AND
|
|
440
|
+
// logged to stderr so the error is traceable (400/timeout/network).
|
|
441
|
+
console.error("[context] compression failed:", error?.message ?? error)
|
|
442
|
+
markCompressFailed(state, error)
|
|
443
|
+
scheduleRender()
|
|
444
|
+
},
|
|
445
|
+
onCompress: (info) => {
|
|
446
|
+
if (info?.mode === "fallback") markCompressFallback(state, info)
|
|
447
|
+
else markCompressDone(state, info)
|
|
448
|
+
scheduleRender()
|
|
404
449
|
},
|
|
405
450
|
// Async distillation landed (SEND-STALL-DISTILL §2.3): the machine line was replaced by
|
|
406
451
|
// the compressed version — persist it so the session file ends up compressed. Silent:
|
|
407
452
|
// a save failure must never surface after the turn already returned.
|
|
408
453
|
onDistilled: () => {
|
|
409
|
-
try { saveSessionImpl(agent, state.lines) } catch {
|
|
454
|
+
try { saveSessionImpl(agent, state.lines) } catch (e) { console.error(`[session] distilled save failed: ${e.message}`) }
|
|
410
455
|
},
|
|
411
456
|
onUsage: (usage) => {
|
|
412
457
|
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
|
+
}
|