thincoder 0.11.0 → 0.11.1
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/package.json +1 -1
- package/src/advisor.mjs +360 -72
- package/src/agent/helpers.mjs +7 -3
- 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 +73 -21
- package/src/auto-think.mjs +23 -5
- package/src/config.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/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 +16 -0
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +21 -16
- package/src/tui/agent-turn.mjs +76 -64
- package/src/tui/cmd-advisor.mjs +119 -18
- package/src/tui/index.mjs +7 -187
- package/src/tui/key-handler.mjs +8 -2
- package/src/tui/layout.mjs +1 -1
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +27 -103
- package/src/tui/render-loop.mjs +181 -0
package/src/tui/render-frame.mjs
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* The legacy renderFrame() wrapper is kept for compatibility.
|
|
8
8
|
*/
|
|
9
9
|
import { ansi, C, ESC } from "./ansi.mjs"
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
sliceByWidth, stringWidth, wrapText, formatTables, sanitizeDisplay,
|
|
13
|
-
} from "./render.mjs"
|
|
10
|
+
import { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
11
|
+
import { sliceByWidth, stringWidth, wrapText, formatTables, sanitizeDisplay } from "./render.mjs"
|
|
14
12
|
import { specForModel } from "../config.mjs"
|
|
13
|
+
import { computeLayout, MAX_SUB_LINES } from "./layout.mjs"
|
|
15
14
|
import { basename } from "node:path"
|
|
16
15
|
|
|
16
|
+
export { convCacheKey, renderConversation, countConvLines } from "./render-conversation.mjs"
|
|
17
|
+
|
|
17
18
|
// ---------- status bar slash-command hints ----------
|
|
18
19
|
const SLASH_HINTS = {
|
|
19
20
|
"/config": "open config menu",
|
|
@@ -44,30 +45,6 @@ export function renderHeader(agent, cols) {
|
|
|
44
45
|
return `${ansi.bold}${C.tool} ThinCoder ${ansi.reset}${ansi.dim}│ ${sliceByWidth(model, 30)}${thinkBadge ? " " + thinkBadge : ""} │ ${sliceByWidth(basename(agent.cwd), Math.max(10, cols - 60))}${ansi.reset}`
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
* Compute a cheap cache key for the conversation panel.
|
|
49
|
-
* Structural hints that change ONLY when the conversation actually changes.
|
|
50
|
-
* Used by the incremental renderer to skip rebuilding convLines when nothing changed.
|
|
51
|
-
*/
|
|
52
|
-
export function convCacheKey(state) {
|
|
53
|
-
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
54
|
-
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${Object.keys(state.toolStreams).length}|${state.foldEnabled !== false ? "f" : "u"}`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Conversation panel (scrollable, variable height). Returns exactly `visibleH` lines. */
|
|
58
|
-
export function renderConversation(state, cols, visibleH, scroll) {
|
|
59
|
-
const convLines = buildConvLines(state, cols)
|
|
60
|
-
const maxScroll = Math.max(0, convLines.length - visibleH)
|
|
61
|
-
const clamped = Math.min(scroll, maxScroll)
|
|
62
|
-
const end = convLines.length - clamped
|
|
63
|
-
const visible = convLines.slice(Math.max(0, end - visibleH), end)
|
|
64
|
-
const pad = visibleH - visible.length
|
|
65
|
-
const out = []
|
|
66
|
-
for (let i = 0; i < pad; i++) out.push("")
|
|
67
|
-
for (const l of visible) out.push(`${l.color}${l.text}${ansi.reset}`)
|
|
68
|
-
return out
|
|
69
|
-
}
|
|
70
|
-
|
|
71
48
|
/** Todo/task panel. Returns empty array when no tasks visible. */
|
|
72
49
|
export function renderTodo(visibleTasks, cols) {
|
|
73
50
|
return visibleTasks.map((t) => {
|
|
@@ -204,17 +181,36 @@ export function renderInputBox(state, W, boxLines, cols, inputLayout, inputOffse
|
|
|
204
181
|
|
|
205
182
|
for (let li = 0; li < boxLines.length; li++) {
|
|
206
183
|
const l = boxLines[li]
|
|
207
|
-
|
|
208
|
-
|
|
184
|
+
const original = sliceByWidth(l, W - 4)
|
|
185
|
+
let content = original
|
|
186
|
+
const contentWidth = stringWidth(original)
|
|
187
|
+
let fillLen = W - 4 - contentWidth
|
|
209
188
|
|
|
210
189
|
if (li === curLine && curCol >= 0) {
|
|
211
|
-
const beforeWidth = Math.min(curCol,
|
|
190
|
+
const beforeWidth = Math.min(curCol, contentWidth)
|
|
212
191
|
const before = sliceByWidth(content, beforeWidth)
|
|
213
192
|
const atIdx = before.length // character index (not display width — CJK chars diverge)
|
|
214
193
|
const at = content[atIdx] ?? " "
|
|
215
194
|
const after = content.slice(atIdx + 1)
|
|
216
195
|
content = before + `${ansi.reset}\x1b[7m${at}\x1b[27m${ansi.reset}` + after
|
|
196
|
+
// Cursor at end of input: at=[\x20] adds one display column — compensate fill.
|
|
197
|
+
// When fill is already 0 (content fills the line), don't add the extra space at all
|
|
198
|
+
// — instead, move the cursor to the last real character.
|
|
199
|
+
if (atIdx >= original.length) {
|
|
200
|
+
if (fillLen > 0) {
|
|
201
|
+
fillLen = Math.max(0, fillLen - 1)
|
|
202
|
+
} else {
|
|
203
|
+
// No room for extra space: place cursor on last character instead
|
|
204
|
+
const lastIdx = original.length - 1
|
|
205
|
+
if (lastIdx >= 0) {
|
|
206
|
+
const before2 = sliceByWidth(original, stringWidth(original.slice(0, lastIdx)))
|
|
207
|
+
const at2 = original[lastIdx] ?? " "
|
|
208
|
+
content = before2 + `${ansi.reset}\x1b[7m${at2}\x1b[27m${ansi.reset}`
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
217
212
|
}
|
|
213
|
+
const fill = " ".repeat(Math.max(0, fillLen))
|
|
218
214
|
|
|
219
215
|
out.push(`${borderColor}│${ansi.reset} ${content}${fill} ${borderColor}│${ansi.reset}`)
|
|
220
216
|
}
|
|
@@ -314,78 +310,6 @@ export function renderFrame(state, agent, opts) {
|
|
|
314
310
|
// Internal helpers (unchanged from original)
|
|
315
311
|
// ====================================================================
|
|
316
312
|
|
|
317
|
-
export function countConvLines(state, cols) {
|
|
318
|
-
return buildConvLines(state, cols).length
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
let _convCache = { key: "", cols: 0, lines: [] }
|
|
322
|
-
function buildConvLines(state, cols) {
|
|
323
|
-
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
324
|
-
const key = convCacheKey(state)
|
|
325
|
-
if (_convCache.key === key && _convCache.cols === cols) return _convCache.lines
|
|
326
|
-
|
|
327
|
-
const convLines = []
|
|
328
|
-
for (const l of state.lines) {
|
|
329
|
-
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
330
|
-
for (const wrapped of wrapText(line, cols - 1)) {
|
|
331
|
-
convLines.push({ text: wrapped, color: l.color, _foldId: l._foldId })
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Messages after the conversation (streaming / thinking / tool output):
|
|
336
|
-
// appended after history lines so they appear at the bottom.
|
|
337
|
-
if (state.reasoning) {
|
|
338
|
-
for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), cols - 1)) {
|
|
339
|
-
convLines.push({ text: wrapped, color: C.reason })
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (state.streaming) {
|
|
343
|
-
for (const line of formatTables(sanitizeDisplay(state.streaming), cols - 1)) {
|
|
344
|
-
for (const wrapped of wrapText(line, cols - 1)) {
|
|
345
|
-
convLines.push({ text: wrapped, color: C.text })
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const allStreams = Object.values(state.toolStreams).join("")
|
|
350
|
-
if (allStreams) {
|
|
351
|
-
const tail = sanitizeDisplay(allStreams.slice(-4000))
|
|
352
|
-
for (const wrapped of wrapText(tail, cols - 1)) {
|
|
353
|
-
convLines.push({ text: wrapped, color: C.dim })
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ---- Fold long blocks (> 8 consecutive dim lines) ----
|
|
358
|
-
const FOLD_LINES = 8
|
|
359
|
-
let foldCounter = 0
|
|
360
|
-
const folded = []
|
|
361
|
-
let i = 0
|
|
362
|
-
while (i < convLines.length) {
|
|
363
|
-
const line = convLines[i]
|
|
364
|
-
// Only fold dim-colored lines (tool results, subagent previews)
|
|
365
|
-
if (line.color === C.dim) {
|
|
366
|
-
let j = i
|
|
367
|
-
while (j < convLines.length && convLines[j].color === C.dim) j++
|
|
368
|
-
const blockLen = j - i
|
|
369
|
-
if (blockLen > FOLD_LINES) {
|
|
370
|
-
const foldKey = `fold-${foldCounter++}`
|
|
371
|
-
if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
|
|
372
|
-
// Show first 2 lines + fold hint
|
|
373
|
-
folded.push(convLines[i])
|
|
374
|
-
if (blockLen > 2) folded.push(convLines[i + 1])
|
|
375
|
-
folded.push({ text: ` … ${blockLen - 2} more lines — Enter to expand`, color: C.fold, _foldToggle: foldKey })
|
|
376
|
-
i = j
|
|
377
|
-
continue
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
folded.push(line)
|
|
382
|
-
i++
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
_convCache = { key, cols, lines: folded }
|
|
386
|
-
return folded
|
|
387
|
-
}
|
|
388
|
-
|
|
389
313
|
function inputBoxStyle(state) {
|
|
390
314
|
let borderColor = C.tool
|
|
391
315
|
let title
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-loop.mjs — frame scheduler + incremental panel rendering
|
|
3
|
+
* Extracted from src/tui/index.mjs to keep the TUI entry point under 500 lines.
|
|
4
|
+
*/
|
|
5
|
+
import { computeLayout } from "./layout.mjs"
|
|
6
|
+
import {
|
|
7
|
+
renderFrame, countConvLines, convCacheKey,
|
|
8
|
+
renderHeader, renderConversation, renderTodo, renderSubagent,
|
|
9
|
+
renderOutput, renderPermission, renderQueue, renderPicker,
|
|
10
|
+
renderInputBox, renderStatus,
|
|
11
|
+
} from "./render-frame.mjs"
|
|
12
|
+
import { estimateTokens } from "../context.mjs"
|
|
13
|
+
import { ansi, C } from "./ansi.mjs"
|
|
14
|
+
|
|
15
|
+
const MIN_RENDER_INTERVAL_MS = 16
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create the render loop closure. Returns { render, scheduleRender }.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} state — TUI state
|
|
21
|
+
* @param {object} agent — agent instance
|
|
22
|
+
* @param {object} ctx — mutable context: { startupDims, SLASH_COMMANDS, showUpdateNotice }
|
|
23
|
+
* @param {Function} pushLine — for error logging
|
|
24
|
+
*/
|
|
25
|
+
export function createRenderLoop(state, agent, ctx, pushLine) {
|
|
26
|
+
const { startupDims, SLASH_COMMANDS } = ctx
|
|
27
|
+
const panelCache = new Map()
|
|
28
|
+
let lastCols = 0, lastRows = 0
|
|
29
|
+
let lastConvKey = "", lastConvCols = 0, lastConvScroll = -1
|
|
30
|
+
const convLineCache = []
|
|
31
|
+
let renderRequested = false, renderTimer = null, lastRenderAt = 0
|
|
32
|
+
|
|
33
|
+
function scheduleRender() {
|
|
34
|
+
if (renderTimer) return
|
|
35
|
+
const elapsed = performance.now() - lastRenderAt
|
|
36
|
+
const delay = Math.max(0, MIN_RENDER_INTERVAL_MS - elapsed)
|
|
37
|
+
renderTimer = setTimeout(() => {
|
|
38
|
+
renderTimer = null
|
|
39
|
+
if (!renderRequested) return
|
|
40
|
+
renderRequested = false
|
|
41
|
+
lastRenderAt = performance.now()
|
|
42
|
+
doRender()
|
|
43
|
+
if (renderRequested) scheduleRender()
|
|
44
|
+
}, delay)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render() {
|
|
48
|
+
if (renderRequested) return
|
|
49
|
+
renderRequested = true
|
|
50
|
+
process.nextTick(() => scheduleRender())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildPanel(name, panelLayout, lines, cacheKey) {
|
|
54
|
+
if (!panelLayout) {
|
|
55
|
+
if (panelCache.has(name)) panelCache.delete(name)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
const content = lines.join("\r\n")
|
|
59
|
+
const cached = panelCache.get(name)
|
|
60
|
+
const effectiveKey = cacheKey ?? content
|
|
61
|
+
if (cached && cached.y === panelLayout.y && cached.h === panelLayout.h && cached.key === effectiveKey) return null
|
|
62
|
+
const rows = []
|
|
63
|
+
for (let i = 0; i < panelLayout.h; i++) {
|
|
64
|
+
rows.push(`\x1b[${panelLayout.y + 1 + i};1H${lines[i] ?? ""}\x1b[K`)
|
|
65
|
+
}
|
|
66
|
+
panelCache.set(name, { y: panelLayout.y, h: panelLayout.h, key: effectiveKey })
|
|
67
|
+
return rows.join("")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function layoutStructureChanged(layout) {
|
|
71
|
+
for (const [name, cached] of panelCache) {
|
|
72
|
+
const p = layout.panels[name] ?? null
|
|
73
|
+
if (p == null) return true
|
|
74
|
+
if (p.y !== cached.y || p.h !== cached.h) return true
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function doRender() {
|
|
80
|
+
try {
|
|
81
|
+
const startupCols = startupDims.cols, startupRows = startupDims.rows
|
|
82
|
+
const dims = { cols: process.stdout.columns || startupCols, rows: process.stdout.rows || startupRows }
|
|
83
|
+
const layout = computeLayout(state, dims)
|
|
84
|
+
const { W, panels, inputLayout, inputOffset, boxLines, visibleTasks, allSubs, permPreviewLines, overlay } = layout
|
|
85
|
+
|
|
86
|
+
const convLines = countConvLines(state, dims.cols)
|
|
87
|
+
state.scroll = Math.min(state.scroll, Math.max(0, convLines - panels.conversation.h))
|
|
88
|
+
if (overlay && panels.picker) {
|
|
89
|
+
const winH = panels.picker.h - 1
|
|
90
|
+
if (overlay.selectedLine < overlay.scroll) overlay.scroll = overlay.selectedLine
|
|
91
|
+
if (overlay.selectedLine >= overlay.scroll + winH) overlay.scroll = overlay.selectedLine - winH + 1
|
|
92
|
+
}
|
|
93
|
+
if (state.ctxCache.len !== agent.history.length) {
|
|
94
|
+
state.ctxCache = { len: agent.history.length, tokens: estimateTokens(agent.history) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Deferred upgrade notice — pop when no overlay is active
|
|
98
|
+
if (ctx.pendingNoticeReady(state)) {
|
|
99
|
+
const notice = state.pendingNotice
|
|
100
|
+
state.pendingNotice = null
|
|
101
|
+
ctx.showUpdateNotice(notice).catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Terminal resize or panel layout shift → full redraw
|
|
105
|
+
if (dims.cols !== lastCols || dims.rows !== lastRows || layoutStructureChanged(layout)) {
|
|
106
|
+
lastCols = dims.cols; lastRows = dims.rows
|
|
107
|
+
for (const [name, panelLayout] of Object.entries(panels)) {
|
|
108
|
+
if (!panelLayout) { panelCache.delete(name); continue }
|
|
109
|
+
const cached = panelCache.get(name)
|
|
110
|
+
if (cached) { cached.y = panelLayout.y; cached.h = panelLayout.h }
|
|
111
|
+
}
|
|
112
|
+
const isStreaming = state.processing && !state.permission && !state.question && !state.picker
|
|
113
|
+
const isWizard = state.wizard?.step === "provider"
|
|
114
|
+
const { frame, cursorRow, cursorCol } = renderFrame(state, agent, { cols: dims.cols, rows: dims.rows, slashCommands: SLASH_COMMANDS })
|
|
115
|
+
if (isStreaming) {
|
|
116
|
+
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`)
|
|
117
|
+
} else if (isWizard) {
|
|
118
|
+
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + ansi.hideCursor)
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write(ansi.syncUpdateStart + ansi.home + frame + ansi.clearToEnd + ansi.syncUpdateEnd + `\x1b[${cursorRow};${cursorCol}H${ansi.hideCursor}`)
|
|
121
|
+
}
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- Incremental rendering (layout stable) ----
|
|
126
|
+
const out = []
|
|
127
|
+
const push = (s) => { if (s != null) out.push(s) }
|
|
128
|
+
|
|
129
|
+
push(buildPanel("header", panels.header, [renderHeader(agent, dims.cols)]))
|
|
130
|
+
push(buildPanel("status", panels.status, [renderStatus(state, agent, dims.cols, SLASH_COMMANDS)]))
|
|
131
|
+
push(buildPanel("inputBox", panels.inputBox, renderInputBox(state, W, boxLines, dims.cols, inputLayout, inputOffset)))
|
|
132
|
+
|
|
133
|
+
const convKey = convCacheKey(state)
|
|
134
|
+
const convChanged = convKey !== lastConvKey || dims.cols !== lastConvCols || state.scroll !== lastConvScroll
|
|
135
|
+
if (convChanged) {
|
|
136
|
+
lastConvKey = convKey; lastConvCols = dims.cols; lastConvScroll = state.scroll
|
|
137
|
+
const lines = renderConversation(state, dims.cols, panels.conversation.h, state.scroll)
|
|
138
|
+
const y = panels.conversation.y + 1
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
if (lines[i] !== convLineCache[i]) {
|
|
141
|
+
out.push(`\x1b[${y + i};1H${lines[i]}\x1b[K`)
|
|
142
|
+
convLineCache[i] = lines[i]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (convLineCache.length > lines.length) {
|
|
146
|
+
for (let i = lines.length; i < convLineCache.length; i++) {
|
|
147
|
+
out.push(`\x1b[${y + i};1H\x1b[K`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
convLineCache.length = lines.length
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
push(buildPanel("todo", panels.todo, renderTodo(visibleTasks, dims.cols)))
|
|
154
|
+
push(buildPanel("subagent", panels.subagent, renderSubagent(allSubs, W)))
|
|
155
|
+
push(buildPanel("output", panels.output, renderOutput(state, W, panels.output?.h ?? 0),
|
|
156
|
+
// Key by append counter, not text length: panel text is capped at 4000 chars (agent-turn),
|
|
157
|
+
// so length stops changing once the cap is hit and the panel would freeze mid-stream.
|
|
158
|
+
Object.values(state.outputPanels).filter(p => !p.done).map(p => p.seq ?? 0).join(",")))
|
|
159
|
+
push(buildPanel("permission", panels.permission, renderPermission(permPreviewLines)))
|
|
160
|
+
if (panels.queue) push(buildPanel("queue", panels.queue, [renderQueue(state, W)]))
|
|
161
|
+
else panelCache.delete("queue")
|
|
162
|
+
if (panels.picker) push(buildPanel("picker", panels.picker, renderPicker(state, dims.cols, panels.picker, overlay)))
|
|
163
|
+
else panelCache.delete("picker")
|
|
164
|
+
|
|
165
|
+
for (const p of Object.values(state.outputPanels)) {
|
|
166
|
+
if (p._pendingDone) { p.done = true; delete p._pendingDone }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cr = panels.inputBox.y + 1 + (inputLayout.cursorLine - inputOffset) + 1
|
|
170
|
+
const cc = 3 + inputLayout.cursorCol
|
|
171
|
+
const hasOverlay = state.permission || state.question || state.picker || state.wizard?.step === "provider"
|
|
172
|
+
const cursorSuffix = hasOverlay ? "" : `\x1b[${cr};${cc}H${ansi.hideCursor}`
|
|
173
|
+
|
|
174
|
+
if (out.length || cursorSuffix) process.stdout.write(ansi.syncUpdateStart + out.join("") + ansi.syncUpdateEnd + cursorSuffix)
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Don't let a render error crash the TUI
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { render, scheduleRender }
|
|
181
|
+
}
|