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.
- package/CHANGELOG.md +49 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/acp/bridge.mjs +1 -0
- package/src/advisor/run.mjs +9 -11
- package/src/agent/dispatch.mjs +38 -13
- package/src/agent/helpers.mjs +1 -1
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/consult.mjs +0 -1
- package/src/agent-tools/skill.mjs +1 -1
- package/src/agent-tools/task.mjs +0 -2
- package/src/agent-tools/verify.mjs +0 -1
- package/src/agent.mjs +36 -3
- package/src/cli/make-agent.mjs +11 -5
- package/src/config.mjs +8 -103
- 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/model-specs.mjs +108 -0
- package/src/prompts/discipline.md +43 -0
- package/src/prompts/system.md +1 -1
- package/src/provider/anthropic.mjs +51 -18
- package/src/provider/core.mjs +121 -102
- package/src/provider/google.mjs +41 -15
- package/src/provider/normalize.mjs +81 -0
- 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/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.md +4 -2
- package/src/tools/git.mjs +38 -11
- package/src/tools/shared.mjs +6 -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/agent-turn.mjs +2 -10
- package/src/tui/clipboard.mjs +3 -1
- package/src/tui/dims.mjs +20 -47
- package/src/tui/fold-block.mjs +59 -11
- package/src/tui/index.mjs +65 -75
- package/src/tui/key-handler.mjs +4 -1
- package/src/tui/mouse.mjs +47 -7
- package/src/tui/render-conversation.mjs +226 -124
- package/src/tui/render-frame.mjs +7 -2
- package/src/tui/render-loop.mjs +10 -0
- package/src/tui/render.mjs +12 -1
- package/src/tui/startup.mjs +1 -2
- package/src/tui/subagent-blocks.mjs +6 -1
- package/src/tui/tool-args.mjs +4 -0
- 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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
// Scroll wheel: \x1b[<64;col;rowM = up, \x1b[<65;col;rowM = down(3 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,
|
|
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
|
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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).
|