thincoder 0.12.51 → 0.12.53

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 +49 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/src/acp/bridge.mjs +1 -0
  5. package/src/advisor/run.mjs +9 -11
  6. package/src/agent/dispatch.mjs +38 -13
  7. package/src/agent/helpers.mjs +1 -1
  8. package/src/agent/setup.mjs +2 -2
  9. package/src/agent-tools/consult.mjs +0 -1
  10. package/src/agent-tools/skill.mjs +1 -1
  11. package/src/agent-tools/task.mjs +0 -2
  12. package/src/agent-tools/verify.mjs +0 -1
  13. package/src/agent.mjs +36 -3
  14. package/src/cli/make-agent.mjs +11 -5
  15. package/src/config.mjs +8 -103
  16. package/src/mcp/helpers.mjs +14 -5
  17. package/src/mcp/transport-http.mjs +79 -27
  18. package/src/mcp/transport-stdio.mjs +57 -3
  19. package/src/mcp/transport-ws.mjs +46 -12
  20. package/src/mcp.mjs +197 -58
  21. package/src/model-specs.mjs +108 -0
  22. package/src/prompts/discipline.md +43 -0
  23. package/src/prompts/system.md +1 -1
  24. package/src/provider/anthropic.mjs +51 -18
  25. package/src/provider/core.mjs +121 -102
  26. package/src/provider/google.mjs +41 -15
  27. package/src/provider/normalize.mjs +81 -0
  28. package/src/provider/rate.mjs +5 -0
  29. package/src/provider/responses.mjs +498 -0
  30. package/src/provider/retry.mjs +125 -0
  31. package/src/provider/sse.mjs +58 -24
  32. package/src/proxy.mjs +36 -6
  33. package/src/tools/bash.md +2 -2
  34. package/src/tools/execute.md +1 -1
  35. package/src/tools/execute.mjs +3 -3
  36. package/src/tools/fetch.md +1 -0
  37. package/src/tools/file.mjs +136 -11
  38. package/src/tools/git.md +4 -2
  39. package/src/tools/git.mjs +38 -11
  40. package/src/tools/shared.mjs +6 -3
  41. package/src/tools/system.mjs +19 -1
  42. package/src/tools/web.mjs +44 -14
  43. package/src/tools/websearch.md +3 -1
  44. package/src/tui/agent-turn.mjs +2 -10
  45. package/src/tui/clipboard.mjs +3 -1
  46. package/src/tui/dims.mjs +20 -47
  47. package/src/tui/fold-block.mjs +59 -11
  48. package/src/tui/index.mjs +65 -75
  49. package/src/tui/key-handler.mjs +4 -1
  50. package/src/tui/mouse.mjs +47 -7
  51. package/src/tui/render-conversation.mjs +226 -124
  52. package/src/tui/render-frame.mjs +7 -2
  53. package/src/tui/render-loop.mjs +10 -0
  54. package/src/tui/render.mjs +12 -1
  55. package/src/tui/startup.mjs +1 -2
  56. package/src/tui/subagent-blocks.mjs +6 -1
  57. package/src/tui/tool-args.mjs +4 -0
  58. package/src/tui/tool-events.mjs +2 -4
package/src/tui/index.mjs CHANGED
@@ -29,9 +29,9 @@ import { createPickers } from "./pickers.mjs"
29
29
  import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
30
30
  import { createInteraction } from "./interaction.mjs"
31
31
  import { pasteClipboardImage as pasteClipboardImageImpl, insertPastedText, translateShiftEnter, stripKeyboardProtocol } from "./clipboard.mjs"
32
- import { parseMouseClicks, handleMouseClick } from "./mouse.mjs"
32
+ import { parseMouseClicks, handleMouseClick, handleWheel } from "./mouse.mjs"
33
33
  import { runAgentTurn } from "./agent-turn.mjs"
34
- import { createKeyHandler } from "./key-handler.mjs"
34
+ import { createKeyHandler, convMaxScroll } from "./key-handler.mjs"
35
35
  import { showStartup, backgroundIndex, historyToLines, HISTORY_PAGE_MESSAGES } from "./startup.mjs"
36
36
  import { countConvLines } from "./render-conversation.mjs"
37
37
  import { createConfigHelpers } from "./config-helpers.mjs"
@@ -77,6 +77,8 @@ export async function startTUI(agent, opts = {}) {
77
77
  historyIndex: -1,
78
78
  _draft: null, // stashed unsent input while navigating history (restored on down past newest)
79
79
  scroll: 0, // scroll lines from bottom upward
80
+ _foldScroll: new Map(), // 2026-08-31 块内滚动:foldKey → 窗口 offset(展开块 ▲▼ 翻窗)
81
+ _followTail: true, // 2026-08-31 流式跟随:渲染前 scroll=0;用户上滚暂停、到底/新消息恢复
80
82
  processing: false,
81
83
  controller: null, // AbortController for current agent run
82
84
  permission: null, // { name, args, resolve }
@@ -134,8 +136,41 @@ export async function startTUI(agent, opts = {}) {
134
136
  let pasteMode = false
135
137
  let pasteAccum = ""
136
138
 
139
+ /** 懒加载更早历史(2026-08-31 用户约定:"滚动到头自动加载"——滚轮/PgUp 到顶皆触发;
140
+ * 2026-08-31 前只挂 PgUp 键 = 违约,滚轮到头无反应)。加载后滚动补偿保持锚定。
141
+ * 外层作用域:data 回调(滚轮分支)与 createKeyHandler ctx(PgUp 分支)共用。 */
142
+ const loadOlder = () => {
143
+ if (!state._hasOlder) return
144
+ const full = agent._fullHistory ?? []
145
+ const loaded = state._historyLoaded
146
+ const start = Math.max(0, full.length - loaded - HISTORY_PAGE_MESSAGES)
147
+ const end = full.length - loaded
148
+ if (start >= end) return
149
+
150
+ const d = state.dims ? state.dims.get() : {}
151
+ const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
152
+ const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
153
+
154
+ if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
155
+ state._lineIdCounter = state._lineIdCounter ?? 0
156
+ const older = historyToLines(full, start, end)
157
+ for (const l of older) l._lineId = ++state._lineIdCounter
158
+ state.lines.unshift(...older)
159
+ state._historyLoaded += end - start
160
+ state._hasOlder = start > 0
161
+ if (state._hasOlder) {
162
+ state.lines.unshift({ text: `… ${start} more earlier messages (scroll to top to load)`, color: C.dim })
163
+ }
164
+
165
+ const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
166
+ state.scroll += Math.max(0, after - before)
167
+ render()
168
+ }
169
+
170
+
137
171
  process.stdin.on("data", (chunk) => {
138
172
  try {
173
+
139
174
  let text = mousePending + utf8Decoder.decode(chunk, { stream: true })
140
175
  mousePending = ""
141
176
 
@@ -183,12 +218,24 @@ export async function startTUI(agent, opts = {}) {
183
218
  }
184
219
  }
185
220
 
186
- // Scroll wheel: \x1b[<64;…M = scroll up, \x1b[<65;…M = scroll down (3 lines each)
187
- for (const m of text.matchAll(/\x1b\[<(\d+);\d+;\d+([Mm])/g)) {
188
- if (Number(m[1]) === 64) {
189
- state.scroll += 3
190
- } else if (Number(m[1]) === 65) {
191
- state.scroll = Math.max(0, state.scroll - 3)
221
+ // Scroll wheel: \x1b[<64;col;rowM = up, \x1b[<65;col;rowM = down3 lines each
222
+ // 2026-08-31:坐标命中展开块内容行 块内滚动(handleWheel);未命中 会话滚动(现状)
223
+ // eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
224
+ for (const m of text.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g)) {
225
+ const button = Number(m[1])
226
+ if (button === 64 || button === 65) {
227
+ const consumed = handleWheel(mouseCtx(), button, Number(m[2]), Number(m[3]))
228
+ if (!consumed) {
229
+ if (button === 64) {
230
+ state.scroll += 3
231
+ state._followTail = false // 2026-08-31:用户上滚 = 暂停流式跟随(不抢视角)
232
+ // 2026-08-31 用户约定修复:滚动到头自动加载(原来只挂 PgUp 键——违约)
233
+ if (state._hasOlder && state.scroll >= convMaxScroll(state)) loadOlder()
234
+ } else {
235
+ state.scroll = Math.max(0, state.scroll - 3)
236
+ if (state.scroll === 0) state._followTail = true // 滚回底部恢复跟随
237
+ }
238
+ }
192
239
  }
193
240
  }
194
241
 
@@ -203,7 +250,9 @@ export async function startTUI(agent, opts = {}) {
203
250
  }
204
251
 
205
252
  // Strip complete mouse sequences; keep incomplete tail for reassembly with next chunk
253
+ // eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
206
254
  text = text.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, "")
255
+ // eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
207
256
  const tail = text.match(/\x1b\[<[\d;]*$/)
208
257
  if (tail) {
209
258
  mousePending = tail[0]
@@ -263,43 +312,6 @@ export async function startTUI(agent, opts = {}) {
263
312
  render()
264
313
  }
265
314
 
266
- /**
267
- * Load the next earlier page of restored history (lazy, parity with VS Code
268
- * loadOlder). Prepends earlier source lines and compensates scroll so the
269
- * visible content does not jump. Called from key-handler when PgUp hits the top.
270
- */
271
- const loadOlder = () => {
272
- if (!state._hasOlder) return
273
- const full = agent._fullHistory ?? []
274
- const loaded = state._historyLoaded
275
- const start = Math.max(0, full.length - loaded - HISTORY_PAGE_MESSAGES)
276
- const end = full.length - loaded
277
- if (start >= end) return
278
-
279
- const d = state.dims ? state.dims.get() : {}
280
- const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
281
- const before = countConvLines(state, cols, d.rows ?? (process.stdout.rows || 24))
282
-
283
- // Drop the old placeholder, prepend the older page, re-add the placeholder
284
- // (with an updated count) only if more remain.
285
- if (state.lines[0]?.text?.startsWith("… ")) state.lines.shift()
286
- state._lineIdCounter = state._lineIdCounter ?? 0
287
- const older = historyToLines(full, start, end)
288
- for (const l of older) l._lineId = ++state._lineIdCounter
289
- state.lines.unshift(...older)
290
- state._historyLoaded += end - start
291
- state._hasOlder = start > 0
292
- if (state._hasOlder) {
293
- state.lines.unshift({ text: `… ${start} more earlier messages (PgUp at top to load)`, color: C.dim })
294
- }
295
-
296
- // Scroll compensation: prepending N display rows must move scroll by N to
297
- // keep the previously-visible bottom-anchored content in place.
298
- const after = countConvLines(state, cols, (state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24))
299
- state.scroll += Math.max(0, after - before)
300
- render()
301
- }
302
-
303
315
  // Only emit the assistant label once per turn (on first token or first tool call)
304
316
  let assistantLabeled = false
305
317
  const ensureAssistantLabel = () => {
@@ -317,38 +329,14 @@ export async function startTUI(agent, opts = {}) {
317
329
  pushLine)
318
330
  const { render, scheduleRender } = renderLoop
319
331
 
332
+ // Resize events are genuine dimension changes on every terminal (2026-08-31
333
+ // simplification: the earlier settle-timer/double-confirm machinery was built
334
+ // on the misdiagnosed ConPTY-stale hypothesis and even stalled drag-shrink).
335
+ // Any sane sample — larger or smaller — is accepted immediately; the cache
336
+ // self-corrects on the next real resize.
320
337
  process.stdout.on("resize", () => {
321
338
  try { state.dims.refresh(); render() } catch { /* resize error — ignore */ }
322
339
  })
323
- // Startup convergence (2026-08-30 consult, residual fix): ConPTY can report
324
- // a stale-small 80 at launch AND keep reporting it for a while — sawValid is
325
- // useless as a stop condition there (80 passes the sanity gate), which let
326
- // the session lock at 80 until the first turn's finally re-sampled. Run the
327
- // FULL window (~4.5s): any growth is accepted immediately by the asymmetric
328
- // rule and repaints; the window just bounds how long we keep looking.
329
- let startupRetries = 0
330
- const startupResampler = setInterval(() => {
331
- try {
332
- const before = state.dims.get()
333
- const after = state.dims.refresh()
334
- if (after.cols !== before.cols || after.rows !== before.rows) render()
335
- if (++startupRetries >= 30) clearInterval(startupResampler)
336
- } catch { /* ignore */ }
337
- }, 150)
338
- // Idle watchdog (2026-08-30 consult): the ONLY mid-session recovery channel.
339
- // ConPTY reports stale-small sizes during output activity, so sampling is
340
- // gated on idleness — skipped while processing/streaming or within 500ms of
341
- // the last render (heavy output); a confirmed shrink also repaints.
342
- const idleResampler = setInterval(() => {
343
- try {
344
- const recentRender = performance.now() - renderLoop.lastRenderAt < 500
345
- if (state.processing || state.streaming || state.reasoning || recentRender) return
346
- const before = state.dims.get()
347
- const after = state.dims.refresh()
348
- if (after.cols !== before.cols || after.rows !== before.rows) render()
349
- } catch { /* ignore */ }
350
- }, 2000)
351
- idleResampler.unref?.()
352
340
 
353
341
  // ---------------------------------------------------------- Submit
354
342
 
@@ -362,6 +350,7 @@ export async function startTUI(agent, opts = {}) {
362
350
  state.historyIndex = -1
363
351
  if (!wasInHistory) state._draft = null // submitted — the draft is now history. Keep draft when submitting from history mode (↓ can recover)
364
352
  state.scroll = 0
353
+ state._followTail = true // 2026-08-31 会诊 deepseek:新消息恢复跟随(注释曾承诺、实现缺漏)
365
354
 
366
355
  // Slash commands: handled locally, don't enter agent loop
367
356
  if (text.startsWith("/")) {
@@ -408,7 +397,7 @@ export async function startTUI(agent, opts = {}) {
408
397
  // Agent loop: implemented in agent-turn.mjs
409
398
  const turnCtx = {
410
399
  agent, state, pushLine, pushLabel, render, scheduleRender: render, ensureAssistantLabel,
411
- askPermission, askQuestion, handleSlash: null, summarize,
400
+ askPermission, askQuestion, handleSlash: null,
412
401
  get assistantLabeled() { return assistantLabeled },
413
402
  set assistantLabeled(v) { assistantLabeled = v },
414
403
  }
@@ -469,6 +458,7 @@ export async function startTUI(agent, opts = {}) {
469
458
 
470
459
  // Mouse clicks (SGR \x1b[<0;col;rowM) — picker selection + fold expansion.
471
460
  const onMouseClick = (col, row) => handleMouseClick({ state, render, popPicker }, col, row)
461
+ const mouseCtx = () => ({ state, render })
472
462
 
473
463
  // ---------------------------------------------------------- Startup screen + background indexing
474
464
 
@@ -6,7 +6,7 @@ import { countConvLines } from "./render-conversation.mjs"
6
6
  import { QUESTION_CUSTOM } from "./interaction.mjs"
7
7
 
8
8
  /** Current conversation max scroll offset (display lines beyond the visible panel). */
9
- function convMaxScroll(state) {
9
+ export function convMaxScroll(state) {
10
10
  // Single source (Windows ConPTY instability, 2026-08-30) — cached dims.
11
11
  const d = state.dims ? state.dims.get() : {}
12
12
  const cols = d.cols ?? ((state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80))
@@ -255,6 +255,7 @@ export function createKeyHandler(ctx) {
255
255
  } else if (str && !key.ctrl && !key.meta) {
256
256
  // 输入即过滤;粘贴的多行文本先去换行(与输入框清洗口径一致),仍含控制字符则整段丢弃
257
257
  const text = str.replace(/[\r\n]+/g, "")
258
+ // eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
258
259
  if (text && !/[\x00-\x1f\x7f]/.test(text)) applyFilter(p.filter + text)
259
260
  }
260
261
  return
@@ -297,11 +298,13 @@ export function createKeyHandler(ctx) {
297
298
  } else {
298
299
  state.scroll += Math.max(1, ((state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24)) - 8)
299
300
  }
301
+ state._followTail = false // 2026-08-31:用户上滚 = 暂停流式跟随
300
302
  render()
301
303
  return
302
304
  }
303
305
  if (key.name === "pagedown") {
304
306
  state.scroll = Math.max(0, state.scroll - Math.max(1, ((state.dims?.get() ?? {}).rows ?? (process.stdout.rows || 24)) - 8))
307
+ if (state.scroll === 0) state._followTail = true // 滚回底部恢复跟随
305
308
  render()
306
309
  return
307
310
  }
package/src/tui/mouse.mjs CHANGED
@@ -15,12 +15,46 @@
15
15
  * - folded-block hint click = expand it
16
16
  */
17
17
  import { computeLayout } from "./layout.mjs"
18
- import { buildConvLines } from "./render-conversation.mjs"
19
- import { toggleFoldBlock } from "./fold-block.mjs"
18
+ import { buildConvLines, convViewport } from "./render-conversation.mjs"
19
+ import { toggleFoldBlock, scrollFoldBlock, foldScrollOffset } from "./fold-block.mjs"
20
+
21
+ /** 2026-08-31 滚轮事件分派(用户需求"展开块能滚动阅读全文"):坐标命中展开块内容行 →
22
+ * 块内逐行滚动(scrollFoldBlock ±3);未命中 → 会话滚动(调用方继续处理)。
23
+ * ctx: { state, render };返回 true = 已消费(块内滚动),false = 调用方走会话滚动。 */
24
+ export function handleWheel(ctx, button, col, row) {
25
+ const { state } = ctx
26
+ const dir = button === 64 ? -1 : 1 // 64=滚上(offset 减)、65=滚下
27
+ const r = row - 1
28
+ if (r < 0) return false
29
+ const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
30
+ const layout = computeLayout(state, dims)
31
+ const P = layout.panels
32
+ if (r < P.conversation.y || r >= P.conversation.y + P.conversation.h) return false
33
+ const convLines = buildConvLines(state, dims.cols, dims.rows)
34
+ const gIdx = convGlobalIndex(convLines.length, P.conversation.h, state.scroll ?? 0)(r - P.conversation.y)
35
+ if (gIdx === null) return false
36
+ const lineEl = convLines[gIdx]
37
+ if (!lineEl?._foldBlock) return false
38
+ // 2026-08-31 会诊 glm:标记不完整(_foldTotal 缺失=退化路径)不消费——交还会话滚动
39
+ // (total=0 会让下方边界守卫短路 → 卡在块里回归)
40
+ if (!lineEl._foldTotal) return false
41
+ // 2026-08-31 穿出语义:块内已到边界(向上滚在顶 / 向下滚在底)→ 交还会话滚动——
42
+ // 否则滚轮永远被块吃掉,会话顶/懒加载不可达(用户实测路径"经过展开块滚不到顶")
43
+ const before = foldScrollOffset(state, lineEl._foldBlock)
44
+ const winH = lineEl._foldWindow ?? 1
45
+ const total = lineEl._foldTotal
46
+ if (dir < 0 && before <= 0) return false // 块顶滚上 → 穿出(会话滚动 → 顶部自动加载)
47
+ if (dir > 0 && before >= total - winH) return false // 块底滚下 → 穿出
48
+ scrollFoldBlock(state, lineEl._foldBlock, dir, 3)
49
+ ctx.render?.()
50
+ return true
51
+ }
52
+
20
53
 
21
54
  /** Extract left-click presses from a chunk. Returns [{ col, row }] (1-based). */
22
55
  export function parseMouseClicks(text) {
23
56
  const out = []
57
+ // eslint-disable-next-line no-control-regex -- 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
24
58
  for (const m of text.matchAll(/\x1b\[<0;(\d+);(\d+)M/g)) {
25
59
  out.push({ col: Number(m[1]), row: Number(m[2]) })
26
60
  }
@@ -29,13 +63,11 @@ export function parseMouseClicks(text) {
29
63
 
30
64
  /** Map a 0-based screen row to a conversation line index (same math as renderConversation). */
31
65
  export function convGlobalIndex(convLen, convH, scroll) {
32
- const maxScroll = Math.max(0, convLen - convH)
33
- const clamped = Math.min(scroll, maxScroll)
34
- const end = convLen - clamped
35
- const start = Math.max(0, end - convH) // content shorter than the panel: rows start at 0
66
+ const { start, pad } = convViewport(convLen, convH, scroll)
36
67
  return (localRow) => {
37
68
  if (localRow < 0 || localRow >= convH) return null
38
- const idx = start + localRow
69
+ if (localRow < pad) return null // 顶部 pad 空行不是内容行(2026-08-31 会诊 kimi 缺陷 1)
70
+ const idx = start + localRow - pad
39
71
  return idx >= 0 && idx < convLen ? idx : null
40
72
  }
41
73
  }
@@ -75,6 +107,14 @@ export function handleMouseClick(ctx, col, row) {
75
107
  const gIdx = convGlobalIndex(convLines.length, P.conversation.h, state.scroll ?? 0)(r - P.conversation.y)
76
108
  if (gIdx === null) return false
77
109
  const lineEl = convLines[gIdx]
110
+ if (lineEl?._foldScrollUp || lineEl?._foldScrollDown) {
111
+ // 2026-08-31 块内滚动:▲/▼ 控制行点击翻窗(60% 封顶保留、窗口随翻滚动,全文可达)
112
+ scrollFoldBlock(state, lineEl._foldScrollUp ?? lineEl._foldScrollDown,
113
+ lineEl._foldScrollUp ? -1 : 1, lineEl._foldWindow ?? 1,
114
+ typeof lineEl._foldTotal === "number" ? Math.max(0, lineEl._foldTotal - (lineEl._foldWindow ?? 1)) : undefined)
115
+ render()
116
+ return true
117
+ }
78
118
  if (!lineEl?._foldToggle) return false
79
119
  // Bidirectional toggle — single source in fold-block.mjs (expand a folded
80
120
  // block, collapse an expanded one).