thincoder 0.8.5 → 0.8.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -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 this the simplest solution? Would fewer lines or fewer files do the job?
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?
@@ -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. Simplest solution? Fewer lines or files?
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
@@ -15,6 +15,8 @@ export const ansi = {
15
15
  bracketedPasteOff: `${ESC}[?2004l`,
16
16
  home: `${ESC}[H`,
17
17
  clearLine: `${ESC}[K`,
18
+ clearToEnd: `${ESC}[J`,
19
+ clearScreen: `${ESC}[2J`,
18
20
  reset: `${ESC}[0m`,
19
21
  dim: `${ESC}[2m`,
20
22
  bold: `${ESC}[1m`,
package/src/tui/index.mjs CHANGED
@@ -223,6 +223,7 @@ export async function startTUI(agent, opts = {}) {
223
223
  // Frame dedup + streaming rate limit: skip re-rendering unchanged frames (prevents flicker);
224
224
  // merge token flood to ~25fps
225
225
  let lastFrame = ""
226
+ let lastCursorRow = -1, lastCursorCol = -1
226
227
  let renderTimer = null
227
228
 
228
229
  function scheduleRender() {
@@ -260,13 +261,23 @@ export async function startTUI(agent, opts = {}) {
260
261
  })
261
262
  if (frame !== lastFrame) {
262
263
  lastFrame = frame
263
- process.stdout.write(ansi.hideCursor + frame)
264
- }
265
- // Cursor: position inside input box when editing; hide during permission/menu mode
266
- if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
267
- process.stdout.write(ansi.hideCursor)
268
- } else {
269
- process.stdout.write(`${"\x1b"}[${cursorRow};${cursorCol}H${ansi.showCursor}`)
264
+ // Single write: home + hide cursor + frame + clear-tail + cursor position — prevents flicker
265
+ let out = ansi.home + ansi.hideCursor + frame + ansi.clearToEnd
266
+ if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
267
+ out += ansi.hideCursor
268
+ } else {
269
+ out += `\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`
270
+ lastCursorRow = cursorRow
271
+ lastCursorCol = cursorCol
272
+ }
273
+ process.stdout.write(out)
274
+ } else if (cursorRow !== lastCursorRow || cursorCol !== lastCursorCol) {
275
+ // Frame unchanged but cursor moved (e.g. arrow keys) — only reposition cursor
276
+ lastCursorRow = cursorRow
277
+ lastCursorCol = cursorCol
278
+ if (!(state.permission || state.question || state.picker || state.wizard?.step === "provider")) {
279
+ process.stdout.write(`\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`)
280
+ }
270
281
  }
271
282
  }
272
283
 
@@ -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