thincoder 0.12.6 → 0.12.8

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 CHANGED
@@ -209,6 +209,13 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
209
209
 
210
210
  ## Changelog
211
211
 
212
+ ### 0.12.8 (2026-08)
213
+ - **Fix: pending-task pushback fires at most once** — the completion guard that reminds the model to update pending tasks before finishing could loop forever when a pending item could not be resolved. Now each task-list state earns exactly one reminder; if the model insists on finishing anyway, it is allowed to (updating the list via the task tool resets the budget). VS Code extension synced.
214
+
215
+ ### 0.12.7 (2026-08)
216
+ - **Fix: long replies and thinking are never folded** — the long-message folding feature (0.12.6) collapsed main output and reasoning behind a click when they exceeded 12 lines, hurting readability. Folding now applies to secondary dim-colored content (tool summaries/status) only; expanded blocks are exempt from the consecutive-dim folding so nothing folds twice.
217
+ - **Fix: wide tables misaligned in narrow terminals** — a many-column table that still exceeded the terminal width after column shrinking (e.g. 8 columns at 40 cols) wrapped at the terminal and misaligned. Table rows are now clipped with an ellipsis instead of ever exceeding the width.
218
+
212
219
  ### 0.12.6 (2026-08)
213
220
  - **Checkpoint v2 — full-file-copy snapshots** — snapshots now store complete copies of changed files (tracked + untracked) instead of a git diff patch: rollback works even after commits happened post-snapshot. New `versions` checkpoint action lists a file's historical copies across snapshots (time / size / content hash) and restores a specific version. **Full rollback is disabled** (as dangerous as a working-tree reset — silently discards post-snapshot work); oversized files (>5MB) are skipped with an explicit notice; files created after a snapshot are never deleted by a restore.
214
221
  - **Git destructive-command protection** — `checkout --` / `restore` / `reset --hard` / `clean -f` (including bypass variants like `checkout HEAD -- .`) auto-snapshot every uncommitted file **before** running, then execute without blocking — a model rollback can no longer destroy uncommitted work. Snapshot triggers slimmed to: destructive-git guard, pre-restore, manual.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.6",
3
+ "version": "0.12.8",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -54,13 +54,18 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
54
54
  )
55
55
  }
56
56
 
57
- // Pending tasks: remind the model before it declares itself done
58
- if (depth === 0 && agent.tasks.some((t) => t.status === "pending")) {
57
+ // Pending tasks: remind the model ONCE before it declares itself done.
58
+ // Deliberately capped at one pushback (reported pain: unbounded looping when a
59
+ // pending item can't be resolved). After the single reminder the model is free
60
+ // to finish — updating the task list (task tool) resets the budget, so a fresh
61
+ // list state earns one fresh reminder.
62
+ if (depth === 0 && agent.tasks.some((t) => t.status === "pending") && (agent._taskPushbacks ?? 0) < 1) {
63
+ agent._taskPushbacks = (agent._taskPushbacks ?? 0) + 1
59
64
  const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
60
65
  pushReal(agent, { role: "assistant", content: response.content })
61
66
  agent.history.push({
62
67
  role: "user",
63
- content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them.]`,
68
+ content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them. (This is your only reminder — if you choose not to, finish anyway.)]`,
64
69
  })
65
70
  callbacks.onTurnEnd?.(agent, turn)
66
71
  return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
@@ -77,6 +77,7 @@ export const taskTool = {
77
77
  const recentDone = raw.filter((t) => t.status === "done").slice(-3)
78
78
  const items = [...pending, ...recentDone].slice(0, 20)
79
79
  ctx.agent.tasks = items
80
+ ctx.agent._taskPushbacks = 0 // task list changed — the completion gate earns a fresh reminder
80
81
  ctx.agent._onTaskUpdate?.(items)
81
82
  const done = items.filter((i) => i.status === "done").length
82
83
  const open = items.length - done
@@ -52,13 +52,15 @@ function buildConvLines(state, cols) {
52
52
  text = highlightSearchMatches(text, state.search.query, l._searchMatches, state.search.index, state.search.matches, i)
53
53
  }
54
54
 
55
- // Long-message folding: a single line that wraps beyond LONG_FOLD_LINES display rows
56
- // collapses to [first, "… N more click/Enter to expand", last]. Keyed by the
57
- // source-line index (`long-${i}`) so the toggle survives re-renders. This is the
58
- // folding users actually seetool outputs, long replies, big error blocks.
55
+ // Long-message folding: a single DIM line (tool summaries / status output — secondary
56
+ // content) that wraps beyond LONG_FOLD_LINES display rows collapses to
57
+ // [first, "… N more click/expand", last]. MAIN OUTPUT (C.text replies) and
58
+ // THINKING (C.reason) are NEVER folded folding them hurt readability (reported
59
+ // regression: long replies and thinking collapsed behind a click). Keyed by the
60
+ // source-line index (`long-${i}`) so the toggle survives re-renders.
59
61
  const LONG_FOLD_LINES = 12
60
62
  const longKey = `long-${i}`
61
- const folded = state.foldEnabled !== false && !state.expandedBlocks?.has(longKey)
63
+ const folded = state.foldEnabled !== false && !state.expandedBlocks?.has(longKey) && l.color === C.dim
62
64
  const block = []
63
65
  for (const line of formatTables(sanitizeDisplay(text), cols - 1)) {
64
66
  for (const wrapped of wrapText(line, cols - 1)) {
@@ -72,6 +74,11 @@ function buildConvLines(state, cols) {
72
74
  convLines.push({ text: ` … ${block.length - 2} more lines — click to expand`, color: C.fold, _foldToggle: longKey, _src: i })
73
75
  convLines.push(block[block.length - 1])
74
76
  } else {
77
+ // Expanded long-DIM blocks must not re-trigger the consecutive-dim folding below
78
+ // (folding stacked on folding — reported regression). Only long blocks get the marker.
79
+ if (block.length > LONG_FOLD_LINES) {
80
+ for (const line of block) line._skipDimFold = true
81
+ }
75
82
  convLines.push(...block)
76
83
  }
77
84
  }
@@ -113,7 +120,9 @@ function buildConvLines(state, cols) {
113
120
  let j = i
114
121
  while (j < convLines.length && convLines[j].color === C.dim) j++
115
122
  const blockLen = j - i
116
- if (blockLen > FOLD_LINES) {
123
+ // Expanded long-fold blocks are exempt — otherwise folding stacks on folding
124
+ const hasExpandedLong = convLines.slice(i, j).some((l) => l._skipDimFold)
125
+ if (blockLen > FOLD_LINES && !hasExpandedLong) {
117
126
  const foldKey = `fold-${foldCounter++}`
118
127
  if (state.foldEnabled !== false && !state.expandedBlocks?.has(foldKey)) {
119
128
  folded.push(convLines[i])
@@ -108,14 +108,19 @@ function renderTable(block, width) {
108
108
  // Cell rendering: sliceByWidth truncates (single-line for header), padByWidth pads
109
109
  const fmtCell = (text, ci) => padByWidth(sliceByWidth(text, widths[ci]), widths[ci])
110
110
  const fmtRow = (cells) => "│ " + cells.map((c, i) => fmtCell(c, i)).join(" │ ") + " │"
111
+ // Many-column tables can still exceed `width` after shrinking to the 3-char floor
112
+ // (e.g. 8 columns → 8×3 + 25 borders = 49 > 40). Rows wider than the terminal would
113
+ // wrap and misalign — clip the row instead, with an ellipsis (fixes the reported
114
+ // "table no longer aligns" regression in narrow windows).
115
+ const clip = (line) => stringWidth(line) > width ? sliceByWidth(line, Math.max(1, width - 1)) + "…" : line
111
116
 
112
117
  // separator line
113
118
  const separator = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"
114
119
 
115
120
  const out = []
116
121
  // Header: single-line truncation (header labels are usually short, truncation beats wrapping)
117
- out.push(fmtRow(rows[0]))
118
- out.push(separator)
122
+ out.push(clip(fmtRow(rows[0])))
123
+ out.push(clip(separator))
119
124
 
120
125
  // Data rows: over-long cells wrap by column width; one logical row may produce multiple display lines
121
126
  for (let r = 2; r < rows.length; r++) {
@@ -123,7 +128,7 @@ function renderTable(block, width) {
123
128
  const wrapped = rows[r].map((cell, ci) => wrapText(cell, widths[ci]))
124
129
  const height = Math.max(...wrapped.map((lines) => lines.length))
125
130
  for (let lineIdx = 0; lineIdx < height; lineIdx++) {
126
- out.push(fmtRow(wrapped.map((lines) => lines[lineIdx] ?? "")))
131
+ out.push(clip(fmtRow(wrapped.map((lines) => lines[lineIdx] ?? ""))))
127
132
  }
128
133
  }
129
134