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.
Files changed (82) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/bin/thincoder.mjs +17 -3
  3. package/package.json +3 -7
  4. package/src/acp/bridge.mjs +1 -1
  5. package/src/acp.mjs +60 -18
  6. package/src/advisor/messages.mjs +4 -2
  7. package/src/advisor/run.mjs +2 -2
  8. package/src/agent/dispatch.mjs +66 -26
  9. package/src/agent/helpers.mjs +13 -2
  10. package/src/agent/setup.mjs +16 -3
  11. package/src/agent/spawn-child.mjs +3 -1
  12. package/src/agent-tools/advisor.mjs +19 -9
  13. package/src/agent-tools/eng.mjs +2 -0
  14. package/src/agent-tools/subagent-check.mjs +107 -0
  15. package/src/agent-tools/subagent.mjs +205 -42
  16. package/src/agent.mjs +68 -3
  17. package/src/cli/make-agent.mjs +25 -0
  18. package/src/cli/memory-command.mjs +28 -7
  19. package/src/config.mjs +120 -8
  20. package/src/context.mjs +28 -7
  21. package/src/escape.mjs +110 -22
  22. package/src/git/checkpoint.mjs +32 -6
  23. package/src/mcp/transport-http.mjs +13 -1
  24. package/src/mcp.mjs +52 -7
  25. package/src/memory/core.mjs +78 -10
  26. package/src/memory/docs.mjs +33 -7
  27. package/src/memory.mjs +1 -1
  28. package/src/model-specs.mjs +23 -0
  29. package/src/prompts/discipline.md +17 -3
  30. package/src/prompts/engineering.md +62 -5
  31. package/src/prompts/main.md +1 -0
  32. package/src/prompts/system.md +2 -1
  33. package/src/provider/anthropic.mjs +7 -5
  34. package/src/provider/core.mjs +90 -26
  35. package/src/provider/google.mjs +57 -24
  36. package/src/provider/normalize.mjs +1 -1
  37. package/src/provider/rate.mjs +0 -2
  38. package/src/provider/responses.mjs +8 -13
  39. package/src/provider/sse.mjs +20 -0
  40. package/src/session-migrate.mjs +6 -0
  41. package/src/session-slots.mjs +361 -0
  42. package/src/session.mjs +282 -306
  43. package/src/tools/apply_patch.md +2 -0
  44. package/src/tools/bash.md +2 -2
  45. package/src/tools/edit-batch.mjs +104 -0
  46. package/src/tools/edit.md +3 -0
  47. package/src/tools/execute.md +4 -4
  48. package/src/tools/execute.mjs +14 -22
  49. package/src/tools/file.mjs +17 -55
  50. package/src/tools/file_ops.md +1 -1
  51. package/src/tools/git-checkpoint.mjs +143 -0
  52. package/src/tools/git-ext.mjs +173 -0
  53. package/src/tools/git.md +21 -8
  54. package/src/tools/git.mjs +55 -177
  55. package/src/tools/lint.md +1 -1
  56. package/src/tools/linter.mjs +9 -37
  57. package/src/tools/patch.mjs +1 -1
  58. package/src/tools/shared.mjs +7 -20
  59. package/src/tui/agent-turn.mjs +3 -3
  60. package/src/tui/ansi.mjs +2 -0
  61. package/src/tui/clipboard.mjs +2 -2
  62. package/src/tui/cmd-eng.mjs +1 -0
  63. package/src/tui/cmd-mcp-form.mjs +197 -0
  64. package/src/tui/cmd-mcp.mjs +255 -114
  65. package/src/tui/cmd-new.mjs +6 -6
  66. package/src/tui/cmd-restore.mjs +27 -6
  67. package/src/tui/cmd-session.mjs +17 -4
  68. package/src/tui/index.mjs +28 -27
  69. package/src/tui/interaction.mjs +28 -1
  70. package/src/tui/key-handler.mjs +14 -2
  71. package/src/tui/layout.mjs +81 -25
  72. package/src/tui/mouse.mjs +41 -2
  73. package/src/tui/pickers.mjs +62 -4
  74. package/src/tui/render-conversation.mjs +36 -93
  75. package/src/tui/render-frame.mjs +40 -16
  76. package/src/tui/render-loop.mjs +1 -1
  77. package/src/tui/render.mjs +4 -4
  78. package/src/tui/startup.mjs +4 -2
  79. package/src/tui/subagent-blocks.mjs +119 -4
  80. package/src/tui/subagent-panel.mjs +81 -0
  81. package/src/tui/tool-events.mjs +61 -16
  82. package/src/tui/tui-lifecycle.mjs +45 -0
@@ -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
- finishSubTask(state, SUBAGENT_ROLES, result.includes(TURN_CAP_MARK) ? "turn cap reached work may be partial" : null)
255
- // Freeze the finished blocks into the conversation stream (user report
256
- // 2026-08-30): a pinned tail section left every block stuck above the
257
- // input box forever. As lines they scroll away, stay expandable via the
258
- // dim auto-fold, and subTasks releases the entry.
259
- freezeDoneSubTasks(state)
260
- // Subagent report preview (max 8 lines) displayed directly in conversation
261
- const lines = result.split("\n")
262
- const preview = lines.slice(0, SUBAGENT_PREVIEW_LINES).map((l) => l.slice(0, PREVIEW_LINE_CHARS)).join("\n")
263
- if (preview) pushLine(preview, C.dim)
264
- if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
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
- onCompress: () => {
403
- pushLine(" [context] Context too long, auto-compacted (early conversation summarized by LLM, task state preserved)", C.warn)
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
+ }