thincoder 0.11.0 → 0.12.0
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/README.md +8 -0
- package/package.json +1 -1
- package/src/advisor.mjs +535 -72
- package/src/agent/helpers.mjs +18 -5
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/advisor.mjs +36 -0
- package/src/agent-tools/plan.mjs +53 -2
- package/src/agent-tools/subagent.mjs +7 -1
- package/src/agent-tools/timer.mjs +1 -1
- package/src/agent-tools/verify.mjs +1 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +80 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +20 -0
- package/src/config.mjs +1 -1
- package/src/mcp/transport-stdio.mjs +4 -3
- package/src/mcp.mjs +1 -1
- package/src/prompts/advisor-round1.md +23 -0
- package/src/prompts/advisor-round2.md +26 -0
- package/src/prompts/advisor-round3.md +24 -0
- package/src/prompts/coder.md +1 -0
- package/src/prompts/discipline.md +15 -1
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +5 -1
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +6 -126
- package/src/provider/google.mjs +4 -2
- package/src/provider/sse.mjs +112 -0
- package/src/skills.mjs +67 -31
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/git.mjs +9 -6
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +43 -2
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +21 -16
- package/src/tui/agent-turn.mjs +130 -73
- package/src/tui/cmd-advisor.mjs +237 -41
- package/src/tui/cmd-auto.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +4 -2
- package/src/tui/cmd-plan.mjs +6 -8
- package/src/tui/cmd-think.mjs +93 -70
- package/src/tui/index.mjs +9 -188
- package/src/tui/interaction.mjs +2 -1
- package/src/tui/key-handler.mjs +8 -4
- package/src/tui/layout.mjs +3 -2
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +94 -168
- package/src/tui/render-loop.mjs +110 -0
package/src/tui/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Large logic blocks extracted to independent modules:
|
|
7
7
|
* agent-turn.mjs — agent loop + callback construction
|
|
8
|
-
*
|
|
8
|
+
* render-loop.mjs — frame scheduler + incremental panel rendering
|
|
9
9
|
* startup.mjs — startup screen + session restore + background indexing
|
|
10
10
|
* interaction.mjs — permission approval + Q&A
|
|
11
11
|
* pickers.mjs — generic list picker + model picker
|
|
@@ -20,15 +20,8 @@ import { emitKeypressEvents } from "node:readline"
|
|
|
20
20
|
import { PassThrough } from "node:stream"
|
|
21
21
|
import { saveSession, archiveCurrent, listSlots } from "../session.mjs"
|
|
22
22
|
import { closeAllMcp } from "../mcp.mjs"
|
|
23
|
-
import { estimateTokens } from "../context.mjs"
|
|
24
23
|
import { ansi, C } from "./ansi.mjs"
|
|
25
|
-
import {
|
|
26
|
-
renderFrame, countConvLines, convCacheKey,
|
|
27
|
-
renderHeader, renderConversation, renderTodo, renderSubagent,
|
|
28
|
-
renderOutput, renderPermission, renderQueue, renderPicker,
|
|
29
|
-
renderInputBox, renderStatus,
|
|
30
|
-
} from "./render-frame.mjs"
|
|
31
|
-
import { computeLayout } from "./layout.mjs"
|
|
24
|
+
import { createRenderLoop } from "./render-loop.mjs"
|
|
32
25
|
import { SLASH_COMMANDS, SLASH_ALIASES, createSlashCommands } from "./slash-commands.mjs"
|
|
33
26
|
import { createWizard } from "./wizard.mjs"
|
|
34
27
|
import { createPickers } from "./pickers.mjs"
|
|
@@ -87,7 +80,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
87
80
|
completion: null, // Tab completion state { candidates, index }
|
|
88
81
|
toolStreams: {}, // per-tool live output (isolated by tool name, parallel tools don't interleave)
|
|
89
82
|
subTasks: {}, // sub-agent panel: { roleName: { role, text, done } }, one line per role, marked done briefly after completion
|
|
90
|
-
outputPanels: {}, //
|
|
83
|
+
outputPanels: {}, // tool output panels: { toolName: { parts: [{kind, text}], len, done, closeAt } } — streamed live during execution, kept visible for a grace period after completion
|
|
91
84
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
92
85
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
93
86
|
status: "Ready",
|
|
@@ -247,184 +240,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
247
240
|
|
|
248
241
|
// ---------------------------------------------------------- Render
|
|
249
242
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
let renderRequested = false, renderTimer = null, lastRenderAt = 0
|
|
256
|
-
const MIN_RENDER_INTERVAL_MS = 16 // ~60fps cap, matching pi-tui
|
|
257
|
-
|
|
258
|
-
function scheduleRender() {
|
|
259
|
-
if (renderTimer) return
|
|
260
|
-
const elapsed = performance.now() - lastRenderAt
|
|
261
|
-
const delay = Math.max(0, MIN_RENDER_INTERVAL_MS - elapsed)
|
|
262
|
-
renderTimer = setTimeout(() => {
|
|
263
|
-
renderTimer = null
|
|
264
|
-
if (!renderRequested) return
|
|
265
|
-
renderRequested = false
|
|
266
|
-
lastRenderAt = performance.now()
|
|
267
|
-
doRender()
|
|
268
|
-
if (renderRequested) scheduleRender() // more requests arrived during render
|
|
269
|
-
}, delay)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Rate-limited render entry point. All call sites use this. */
|
|
273
|
-
function render() {
|
|
274
|
-
if (renderRequested) return
|
|
275
|
-
renderRequested = true
|
|
276
|
-
// process.nextTick merges multiple synchronous render() calls
|
|
277
|
-
// within the same tick into a single scheduleRender call.
|
|
278
|
-
process.nextTick(() => scheduleRender())
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Build ANSI content for a panel at its layout position. Returns null if unchanged. */
|
|
282
|
-
function buildPanel(name, panelLayout, lines, cacheKey) {
|
|
283
|
-
if (!panelLayout) {
|
|
284
|
-
if (panelCache.has(name)) panelCache.delete(name)
|
|
285
|
-
return null
|
|
286
|
-
}
|
|
287
|
-
const content = lines.join("\r\n")
|
|
288
|
-
const cached = panelCache.get(name)
|
|
289
|
-
const effectiveKey = cacheKey ?? content
|
|
290
|
-
if (cached && cached.y === panelLayout.y && cached.h === panelLayout.h && cached.key === effectiveKey) return null
|
|
291
|
-
const rows = []
|
|
292
|
-
for (let i = 0; i < panelLayout.h; i++) {
|
|
293
|
-
rows.push(`\x1b[${panelLayout.y + 1 + i};1H${lines[i] ?? ""}\x1b[K`)
|
|
294
|
-
}
|
|
295
|
-
panelCache.set(name, { y: panelLayout.y, h: panelLayout.h, key: effectiveKey })
|
|
296
|
-
return rows.join("")
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Detect if panel layout structure changed (appeared/disappeared/shifted).
|
|
300
|
-
* Only checks panels that are ALREADY cached — new panels (not yet written)
|
|
301
|
-
* are not a structural change; the incremental path will write them naturally. */
|
|
302
|
-
function layoutStructureChanged(layout) {
|
|
303
|
-
for (const [name, cached] of panelCache) {
|
|
304
|
-
const p = layout.panels[name] ?? null
|
|
305
|
-
if (p == null) return true // cached panel disappeared → layout changed
|
|
306
|
-
if (p.y !== cached.y || p.h !== cached.h) return true // shifted/resized
|
|
307
|
-
}
|
|
308
|
-
return false
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function doRender() {
|
|
312
|
-
try {
|
|
313
|
-
const dims = { cols: process.stdout.columns || startupCols, rows: process.stdout.rows || startupRows }
|
|
314
|
-
const layout = computeLayout(state, dims)
|
|
315
|
-
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, allSubs, permPreviewLines, overlay } = layout
|
|
316
|
-
|
|
317
|
-
// Side effects: clamp scroll + overlay + update ctxCache
|
|
318
|
-
const convLines = countConvLines(state, dims.cols)
|
|
319
|
-
state.scroll = Math.min(state.scroll, Math.max(0, convLines - panels.conversation.h))
|
|
320
|
-
if (overlay && panels.picker) {
|
|
321
|
-
const winH = panels.picker.h - 1
|
|
322
|
-
if (overlay.selectedLine < overlay.scroll) overlay.scroll = overlay.selectedLine
|
|
323
|
-
if (overlay.selectedLine >= overlay.scroll + winH) overlay.scroll = overlay.selectedLine - winH + 1
|
|
324
|
-
}
|
|
325
|
-
if (state.ctxCache.len !== agent.history.length) {
|
|
326
|
-
state.ctxCache = { len: agent.history.length, tokens: estimateTokens(agent.history) }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 后台更新提示:等 picker/权限确认/提问弹层全部关闭后再弹,不硬抢
|
|
330
|
-
// (key-handler 分支顺序 permission → question → picker,任一激活时弹了也摸不到)
|
|
331
|
-
if (pendingNoticeReady(state)) {
|
|
332
|
-
const notice = state.pendingNotice
|
|
333
|
-
state.pendingNotice = null
|
|
334
|
-
showUpdateNotice(notice).catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Terminal resize or panel layout shift → full redraw (using legacy renderFrame).
|
|
338
|
-
// Don't clear panelCache — update positions so the next incremental check
|
|
339
|
-
// sees correct Y/h. Content keys will be stale, forcing a one-time rewrite
|
|
340
|
-
// per panel on the next frame (much cheaper than another full redraw).
|
|
341
|
-
if (dims.cols !== lastCols || dims.rows !== lastRows || layoutStructureChanged(layout)) {
|
|
342
|
-
lastCols = dims.cols; lastRows = dims.rows
|
|
343
|
-
// Update cached panel positions (content stays stale → next frame rewrites)
|
|
344
|
-
for (const [name, panelLayout] of Object.entries(panels)) {
|
|
345
|
-
if (!panelLayout) { panelCache.delete(name); continue }
|
|
346
|
-
const cached = panelCache.get(name)
|
|
347
|
-
if (cached) { cached.y = panelLayout.y; cached.h = panelLayout.h }
|
|
348
|
-
}
|
|
349
|
-
const isStreaming = state.processing && !state.permission && !state.question && !state.picker
|
|
350
|
-
const isWizard = state.wizard?.step === "provider"
|
|
351
|
-
// Content + cursor in a single write. Hardware cursor stays hidden —
|
|
352
|
-
// the visual cursor is drawn in the input box as SGR reverse video.
|
|
353
|
-
// Position for IME, hide for visual (matching pi-tui).
|
|
354
|
-
const { frame, cursorRow, cursorCol } = renderFrame(state, agent, { cols: dims.cols, rows: dims.rows, slashCommands: SLASH_COMMANDS })
|
|
355
|
-
if (isStreaming) {
|
|
356
|
-
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`)
|
|
357
|
-
} else if (isWizard) {
|
|
358
|
-
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + ansi.hideCursor)
|
|
359
|
-
} else {
|
|
360
|
-
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`)
|
|
361
|
-
}
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ---- Incremental rendering (layout stable) ----
|
|
366
|
-
// pi-tui pattern: content inside sync-update block; cursor outside.
|
|
367
|
-
// DECSET 2026 buffers all panel writes and renders them atomically.
|
|
368
|
-
// Cursor hide/show/position MUST be outside — otherwise the terminal's
|
|
369
|
-
// internal cursor state machine and the sync render buffer can disagree.
|
|
370
|
-
const out = []
|
|
371
|
-
const push = (s) => { if (s != null) out.push(s) }
|
|
372
|
-
|
|
373
|
-
// Always-visible panels
|
|
374
|
-
push(buildPanel("header", panels.header, [renderHeader(agent, dims.cols)]))
|
|
375
|
-
push(buildPanel("status", panels.status, [renderStatus(state, agent, dims.cols, SLASH_COMMANDS)]))
|
|
376
|
-
push(buildPanel("inputBox", panels.inputBox, renderInputBox(state, W, boxLines, dims.cols, inputLayout, inputOffset)))
|
|
377
|
-
|
|
378
|
-
// Conversation: line-level cache — only push changed lines
|
|
379
|
-
const convKey = convCacheKey(state)
|
|
380
|
-
const convChanged = convKey !== lastConvKey || dims.cols !== lastConvCols || state.scroll !== lastConvScroll
|
|
381
|
-
if (convChanged) {
|
|
382
|
-
lastConvKey = convKey; lastConvCols = dims.cols; lastConvScroll = state.scroll
|
|
383
|
-
const lines = renderConversation(state, dims.cols, panels.conversation.h, state.scroll)
|
|
384
|
-
const y = panels.conversation.y + 1
|
|
385
|
-
for (let i = 0; i < lines.length; i++) {
|
|
386
|
-
if (lines[i] !== convLineCache[i]) {
|
|
387
|
-
out.push(`\x1b[${y + i};1H${lines[i]}\x1b[K`)
|
|
388
|
-
convLineCache[i] = lines[i]
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (convLineCache.length > lines.length) {
|
|
392
|
-
for (let i = lines.length; i < convLineCache.length; i++) {
|
|
393
|
-
out.push(`\x1b[${y + i};1H\x1b[K`)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
convLineCache.length = lines.length
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Conditional panels
|
|
400
|
-
push(buildPanel("todo", panels.todo, renderTodo(visibleTasks, dims.cols)))
|
|
401
|
-
push(buildPanel("subagent", panels.subagent, renderSubagent(allSubs, W)))
|
|
402
|
-
push(buildPanel("output", panels.output, renderOutput(state, W, panels.output?.h ?? 0)))
|
|
403
|
-
push(buildPanel("permission", panels.permission, renderPermission(permPreviewLines)))
|
|
404
|
-
if (panels.queue) push(buildPanel("queue", panels.queue, [renderQueue(state, W)]))
|
|
405
|
-
else panelCache.delete("queue")
|
|
406
|
-
if (panels.picker) push(buildPanel("picker", panels.picker, renderPicker(state, dims.cols, panels.picker, overlay)))
|
|
407
|
-
else panelCache.delete("picker")
|
|
408
|
-
|
|
409
|
-
// Determine cursor suffix — appended to the same write() as the sync block.
|
|
410
|
-
// MUST position the cursor at the input box even when hidden: the terminal's
|
|
411
|
-
// cursor position determines where the IME candidate window appears.
|
|
412
|
-
// pi-tui's positionHardwareCursor does the same — positions first, then
|
|
413
|
-
// decides show/hide based on showHardwareCursor.
|
|
414
|
-
// Hardware cursor stays hidden — the visual cursor is drawn in the input
|
|
415
|
-
// box text as SGR reverse video (matching pi-tui's approach).
|
|
416
|
-
// We still position the hardware cursor for IME candidate window placement.
|
|
417
|
-
const cr = panels.inputBox.y + 1 + (inputLayout.cursorLine - inputOffset) + 1
|
|
418
|
-
const cc = 3 + inputLayout.cursorCol
|
|
419
|
-
const hasOverlay = state.permission || state.question || state.picker || state.wizard?.step === "provider"
|
|
420
|
-
const cursorSuffix = hasOverlay ? "" : `\x1b[${cr};${cc}H${ansi.hideCursor}`
|
|
421
|
-
|
|
422
|
-
// Single write: sync markers + content + cursor — atomic as far as the terminal is concerned
|
|
423
|
-
if (out.length || cursorSuffix) process.stdout.write(ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix)
|
|
424
|
-
} catch (e) {
|
|
425
|
-
// Don't let a render error crash the TUI
|
|
426
|
-
}
|
|
427
|
-
}
|
|
243
|
+
const renderLoop = createRenderLoop(state, agent,
|
|
244
|
+
{ startupDims: { cols: startupCols, rows: startupRows }, SLASH_COMMANDS,
|
|
245
|
+
pendingNoticeReady, get showUpdateNotice() { return showUpdateNotice } },
|
|
246
|
+
pushLine)
|
|
247
|
+
const { render, scheduleRender } = renderLoop
|
|
428
248
|
|
|
429
249
|
process.stdout.on("resize", () => {
|
|
430
250
|
try { render() } catch { /* resize error — ignore */ }
|
|
@@ -596,5 +416,6 @@ export async function startTUI(agent, opts = {}) {
|
|
|
596
416
|
|
|
597
417
|
function summarize(obj) {
|
|
598
418
|
const s = JSON.stringify(obj)
|
|
419
|
+
if (s === "{}") return "" // no-arg tools (advisor/verify/…) — don't render empty braces
|
|
599
420
|
return s.length > 80 ? s.slice(0, 80) + "…" : s
|
|
600
421
|
}
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -37,7 +37,8 @@ export function createInteraction(ctx) {
|
|
|
37
37
|
function askPermission(name, args) {
|
|
38
38
|
// auto mode: fully authorized, no more prompts
|
|
39
39
|
if (agent.autoApprove) {
|
|
40
|
-
|
|
40
|
+
const argSummary = summarize(args)
|
|
41
|
+
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
41
42
|
return Promise.resolve(true)
|
|
42
43
|
}
|
|
43
44
|
// store preview content in permissionPreview, rendered above input box next to "Allow?" prompt
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -24,8 +24,6 @@ export function createKeyHandler(ctx) {
|
|
|
24
24
|
state.status = "Processing..."
|
|
25
25
|
if (answer === "a" && !isContinue) {
|
|
26
26
|
agent.autoApprove = true
|
|
27
|
-
agent._pendingReminders = agent._pendingReminders ?? []
|
|
28
|
-
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
|
|
29
27
|
pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
|
|
30
28
|
}
|
|
31
29
|
const approved = answer === "y" || (answer === "a" && !isContinue)
|
|
@@ -140,8 +138,14 @@ export function createKeyHandler(ctx) {
|
|
|
140
138
|
const msg = (state.interruptPrompt.text ?? "").trim()
|
|
141
139
|
state.interruptPrompt = null
|
|
142
140
|
if (msg) {
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
// Guard: if the turn already finished while the user was typing, the controller
|
|
142
|
+
// may have been replaced or already aborted — don't abort a live turn by mistake.
|
|
143
|
+
if (state.processing && state.controller && !state.controller.signal.aborted) {
|
|
144
|
+
pushLine(` [inject] ${msg}`, C.warn)
|
|
145
|
+
state.controller.abort({ interrupt: true, message: msg })
|
|
146
|
+
} else {
|
|
147
|
+
pushLine(` [inject — turn ended, message queued] ${msg}`, C.dim)
|
|
148
|
+
}
|
|
145
149
|
render()
|
|
146
150
|
}
|
|
147
151
|
} else if (key.name === "backspace") {
|
package/src/tui/layout.mjs
CHANGED
|
@@ -69,8 +69,9 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
69
69
|
? Math.min(allSubs.length, MAX_SUB_LINES) + (allSubs.length > MAX_SUB_LINES ? 1 : 0)
|
|
70
70
|
: 0
|
|
71
71
|
|
|
72
|
-
// Tool output panels: max 8 lines per panel, capped at reasonable total
|
|
73
|
-
|
|
72
|
+
// Tool output panels: max 8 lines per panel, capped at reasonable total.
|
|
73
|
+
// Done panels stay visible until their closeAt grace elapses (render loop prunes them).
|
|
74
|
+
const panels = Object.values(state.outputPanels).filter((p) => !p.done || (p.closeAt ?? 0) > Date.now())
|
|
74
75
|
const outputPanelsH = panels.length > 0 ? Math.min(panels.length * 8, rows - 10) : 0
|
|
75
76
|
|
|
76
77
|
// Permission preview (height depends on wrapped content)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-conversation.mjs — conversation panel line builder
|
|
3
|
+
* Extracted from render-frame.mjs.
|
|
4
|
+
*/
|
|
5
|
+
import { ansi, C } from "./ansi.mjs"
|
|
6
|
+
import { formatTables, sanitizeDisplay, wrapText } from "./render.mjs"
|
|
7
|
+
|
|
8
|
+
let _convCache = { key: "", cols: 0, lines: [] }
|
|
9
|
+
|
|
10
|
+
export function convCacheKey(state) {
|
|
11
|
+
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
12
|
+
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${Object.keys(state.toolStreams).length}|${state.foldEnabled !== false ? "f" : "u"}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildConvLines(state, cols) {
|
|
16
|
+
const key = convCacheKey(state)
|
|
17
|
+
if (_convCache.key === key && _convCache.cols === cols) return _convCache.lines
|
|
18
|
+
|
|
19
|
+
const convLines = []
|
|
20
|
+
for (const l of state.lines) {
|
|
21
|
+
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
22
|
+
for (const wrapped of wrapText(line, cols - 1)) {
|
|
23
|
+
convLines.push({ text: wrapped, color: l.color, _foldId: l._foldId })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (state.reasoning) {
|
|
28
|
+
for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), cols - 1)) {
|
|
29
|
+
convLines.push({ text: wrapped, color: C.reason })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (state.streaming) {
|
|
33
|
+
for (const line of formatTables(sanitizeDisplay(state.streaming), cols - 1)) {
|
|
34
|
+
for (const wrapped of wrapText(line, cols - 1)) {
|
|
35
|
+
convLines.push({ text: wrapped, color: C.text })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const allStreams = Object.values(state.toolStreams).join("")
|
|
40
|
+
if (allStreams) {
|
|
41
|
+
const tail = sanitizeDisplay(allStreams.slice(-4000))
|
|
42
|
+
for (const wrapped of wrapText(tail, cols - 1)) {
|
|
43
|
+
convLines.push({ text: wrapped, color: C.dim })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fold long blocks (> 8 consecutive dim lines)
|
|
48
|
+
const FOLD_LINES = 8
|
|
49
|
+
let foldCounter = 0
|
|
50
|
+
const folded = []
|
|
51
|
+
let i = 0
|
|
52
|
+
while (i < convLines.length) {
|
|
53
|
+
const line = convLines[i]
|
|
54
|
+
if (line.color === C.dim) {
|
|
55
|
+
let j = i
|
|
56
|
+
while (j < convLines.length && convLines[j].color === C.dim) j++
|
|
57
|
+
const blockLen = j - i
|
|
58
|
+
if (blockLen > FOLD_LINES) {
|
|
59
|
+
const foldKey = `fold-${foldCounter++}`
|
|
60
|
+
if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
|
|
61
|
+
folded.push(convLines[i])
|
|
62
|
+
if (blockLen > 2) folded.push(convLines[i + 1])
|
|
63
|
+
folded.push({ text: ` … ${blockLen - 2} more lines — Enter to expand`, color: C.fold, _foldToggle: foldKey })
|
|
64
|
+
i = j
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
folded.push(line)
|
|
70
|
+
i++
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_convCache = { key, cols, lines: folded }
|
|
74
|
+
return folded
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function countConvLines(state, cols) {
|
|
78
|
+
return buildConvLines(state, cols).length
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function renderConversation(state, cols, visibleH, scroll) {
|
|
82
|
+
const convLines = buildConvLines(state, cols)
|
|
83
|
+
const maxScroll = Math.max(0, convLines.length - visibleH)
|
|
84
|
+
const clamped = Math.min(scroll, maxScroll)
|
|
85
|
+
const end = convLines.length - clamped
|
|
86
|
+
const visible = convLines.slice(Math.max(0, end - visibleH), end)
|
|
87
|
+
const pad = visibleH - visible.length
|
|
88
|
+
const out = []
|
|
89
|
+
for (let p = 0; p < pad; p++) out.push("")
|
|
90
|
+
for (const l of visible) out.push(`${l.color ?? ""}${l.text}${ansi.reset}`)
|
|
91
|
+
return out
|
|
92
|
+
}
|