thincoder 0.8.5 → 0.8.7
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/prompts/discipline.md +1 -1
- package/src/prompts/main.md +1 -1
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/index.mjs +39 -11
- package/src/tui/render-frame.mjs +9 -1
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ Coding discipline (rigor over speed—tokens spent on verification are well spen
|
|
|
16
16
|
- After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
|
|
17
17
|
- Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
|
|
18
18
|
- After completing a batch of edits, pause and self-review:
|
|
19
|
-
1. Is
|
|
19
|
+
1. Is it correct? Does every line do exactly what it claims, with no off-by-one, no missing edge case, no silent failure?
|
|
20
20
|
2. Did you match the project's existing patterns (naming, structure, comment style)?
|
|
21
21
|
3. Did you change anything unrelated to the task? If so, explain why it was necessary.
|
|
22
22
|
4. Did the implementation match the design? Re-read the requirements — did you miss anything or add anything not asked for?
|
package/src/prompts/main.md
CHANGED
|
@@ -15,7 +15,7 @@ Load skills when relevant — project skills (.thincoder/skills/) contain reusab
|
|
|
15
15
|
**How you finish:**
|
|
16
16
|
|
|
17
17
|
After a batch of edits, pause and self-review:
|
|
18
|
-
1.
|
|
18
|
+
1. Is it correct? Every line does exactly what it claims — no off-by-one, no missing edge case, no silent failure.
|
|
19
19
|
2. Matches existing patterns?
|
|
20
20
|
3. Changed anything unrelated? If so, explain why.
|
|
21
21
|
4. Matches the design? Re-read the requirements — missed anything? Added anything not asked for?
|
package/src/tui/ansi.mjs
CHANGED
package/src/tui/index.mjs
CHANGED
|
@@ -97,8 +97,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
97
97
|
let pasteAccum = ""
|
|
98
98
|
|
|
99
99
|
process.stdin.on("data", (chunk) => {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
try {
|
|
101
|
+
let text = mousePending + utf8Decoder.decode(chunk, { stream: true })
|
|
102
|
+
mousePending = ""
|
|
102
103
|
|
|
103
104
|
// Bracketed paste: terminal wraps pasted text in \x1b[200~ ... \x1b[201~
|
|
104
105
|
// Insert pasted content directly into state.input to avoid slow char-by-char keypress render
|
|
@@ -169,6 +170,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
169
170
|
render()
|
|
170
171
|
}
|
|
171
172
|
if (text) keyStream.write(text)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
pushLine(`[input-error] ${e.message || e}`, C.error)
|
|
175
|
+
}
|
|
172
176
|
})
|
|
173
177
|
|
|
174
178
|
let cleanedUp = false
|
|
@@ -223,6 +227,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
223
227
|
// Frame dedup + streaming rate limit: skip re-rendering unchanged frames (prevents flicker);
|
|
224
228
|
// merge token flood to ~25fps
|
|
225
229
|
let lastFrame = ""
|
|
230
|
+
let lastCursorRow = -1, lastCursorCol = -1
|
|
226
231
|
let renderTimer = null
|
|
227
232
|
|
|
228
233
|
function scheduleRender() {
|
|
@@ -234,7 +239,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
function render() {
|
|
237
|
-
|
|
242
|
+
try {
|
|
243
|
+
// Side effects: reset scroll + update ctxCache + clamp overlay scroll
|
|
238
244
|
// (renderFrame is pure, side effects concentrated here)
|
|
239
245
|
const dims = { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
|
|
240
246
|
const layout = computeLayout(state, dims)
|
|
@@ -260,17 +266,32 @@ export async function startTUI(agent, opts = {}) {
|
|
|
260
266
|
})
|
|
261
267
|
if (frame !== lastFrame) {
|
|
262
268
|
lastFrame = frame
|
|
263
|
-
|
|
269
|
+
// Single write: home + hide cursor + frame + clear-tail + cursor position — prevents flicker
|
|
270
|
+
let out = ansi.home + ansi.hideCursor + frame + ansi.clearToEnd
|
|
271
|
+
if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
|
|
272
|
+
out += ansi.hideCursor
|
|
273
|
+
} else {
|
|
274
|
+
out += `\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`
|
|
275
|
+
lastCursorRow = cursorRow
|
|
276
|
+
lastCursorCol = cursorCol
|
|
277
|
+
}
|
|
278
|
+
process.stdout.write(out)
|
|
279
|
+
} else if (cursorRow !== lastCursorRow || cursorCol !== lastCursorCol) {
|
|
280
|
+
// Frame unchanged but cursor moved (e.g. arrow keys) — only reposition cursor
|
|
281
|
+
lastCursorRow = cursorRow
|
|
282
|
+
lastCursorCol = cursorCol
|
|
283
|
+
if (!(state.permission || state.question || state.picker || state.wizard?.step === "provider")) {
|
|
284
|
+
process.stdout.write(`\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`)
|
|
285
|
+
}
|
|
264
286
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
process.stdout.write(ansi.hideCursor)
|
|
268
|
-
} else {
|
|
269
|
-
process.stdout.write(`${"\x1b"}[${cursorRow};${cursorCol}H${ansi.showCursor}`)
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Don't let a render error crash the TUI
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
|
|
273
|
-
process.stdout.on("resize",
|
|
292
|
+
process.stdout.on("resize", () => {
|
|
293
|
+
try { render() } catch { /* resize error — ignore */ }
|
|
294
|
+
})
|
|
274
295
|
|
|
275
296
|
// ---------------------------------------------------------- Submit
|
|
276
297
|
|
|
@@ -375,7 +396,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
375
396
|
wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems,
|
|
376
397
|
renderWizard, pushLine, cleanup,
|
|
377
398
|
})
|
|
378
|
-
keyStream.on("keypress",
|
|
399
|
+
keyStream.on("keypress", (str, key) => {
|
|
400
|
+
try {
|
|
401
|
+
onKeypress(str, key)
|
|
402
|
+
} catch (e) {
|
|
403
|
+
pushLine(`[input-error] ${e.message || e}`, C.error)
|
|
404
|
+
render()
|
|
405
|
+
}
|
|
406
|
+
})
|
|
379
407
|
|
|
380
408
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
381
409
|
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -194,8 +194,15 @@ export function countConvLines(state, cols) {
|
|
|
194
194
|
return buildConvLines(state, cols).length
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
/** Build conversation lines from state (sanitized + wrapped). Pure.
|
|
197
|
+
/** Build conversation lines from state (sanitized + wrapped). Pure.
|
|
198
|
+
* Cached: avoids O(n) rebuild on cursor moves — only recomputes when conversation grows/changes. */
|
|
199
|
+
let _convCache = { key: "", cols: 0, lines: [] }
|
|
198
200
|
function buildConvLines(state, cols) {
|
|
201
|
+
// Cheap cache key: structural hints that change whenever the conversation changes
|
|
202
|
+
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
203
|
+
const key = `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${Object.keys(state.toolStreams).length}`
|
|
204
|
+
if (_convCache.key === key && _convCache.cols === cols) return _convCache.lines
|
|
205
|
+
|
|
199
206
|
const convLines = []
|
|
200
207
|
for (const l of state.lines) {
|
|
201
208
|
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
@@ -223,6 +230,7 @@ function buildConvLines(state, cols) {
|
|
|
223
230
|
convLines.push({ text: wrapped, color: C.dim })
|
|
224
231
|
}
|
|
225
232
|
}
|
|
233
|
+
_convCache = { key, cols, lines: convLines }
|
|
226
234
|
return convLines
|
|
227
235
|
}
|
|
228
236
|
|