thincoder 0.8.8 → 0.8.10

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
@@ -205,6 +205,23 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
205
205
 
206
206
  ## Changelog
207
207
 
208
+ ### 0.8.10 (2026-07)
209
+ - **Bugfix**: pasted text now lands in the active TUI text target — the API key prompt when adding a provider via `/model` (and any free-text `askQuestion`) now accepts paste correctly. Previously, bracketed-paste injection in the terminal was always written to the main input box, so pasting into a question prompt appeared as "nothing happened" and orphaned the text into the input box after the question closed. Both bracketed-paste (Windows Terminal / most modern terminals) and Ctrl+V-as-key-event (legacy conhost) now route through a single `insertPastedText` helper that targets the question answer, options-list (ignored), or main input box as appropriate
210
+
211
+ ### 0.8.9 (2026-07)
212
+ - **Bugfix**: `wizardProviderItems` was defined inside `createWizard()` but not included in the return statement, causing key-handler to crash with `TypeError` during provider selection on first launch (/ new config)
213
+
214
+ ### 0.8.8 (2026-07)
215
+ - **Bugfix**: remove env whitelist from bash tool — child processes receive full parent environment
216
+ - **Bugfix**: reduce TUI flicker — single `write` with `home` + `clearToEnd` instead of separate cursor moves
217
+ - **Feat**: auto update check on startup + `/upgrade` command
218
+
219
+ ### 0.8.3 (2026-07)
220
+ - **Output panels**: tools with `outputPanel` flag stream to scrolled panel, auto-collapse to summary on completion (bash, long tool results)
221
+ - **Checkpoint enhancements**: `cat` for file preview from snapshots, per-file rewind, auto-recover on apply failure, escape-hatch hints on errors
222
+ - **Bash safety**: `checkpoint-before-destructive` discipline rule; bash guard guides checkpoint instead of just commit/stash
223
+ - **Code review fixes**: output friendliness, readability, English-only strings, TUI polish
224
+
208
225
  ### 0.8.0 (2026-07)
209
226
  - **TUI rendering overhaul**: layout engine (`layout.mjs`) — panels are positioned declaratively by priority instead of hand-pinned arithmetic; `renderFrame` is now a pure function (no state mutation); cursor position derived from layout coordinates. Status bar slash-command hints moved from if-else chain to lookup table. Three Chinese UI strings fixed to English
210
227
  - **Subagent panel redesigned**: per-instance tracking (`role#id/` prefix) — parallel subagents of the same role no longer overwrite each other. Shows current tool name + args, or streaming text last line (what it's writing right now), instead of truncated 200-char token fragments. `onToolCall` relayed from subagent to parent TUI. Only the earliest running subagent is marked done on completion (not all)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,5 +1,43 @@
1
1
  import { C } from "./ansi.mjs"
2
2
 
3
+ /** Read text from system clipboard. Returns empty string on failure. */
4
+ export async function readClipboardText() {
5
+ try {
6
+ const { execFile } = await import("node:child_process")
7
+ const isWin = process.platform === "win32"
8
+ const isMac = process.platform === "darwin"
9
+ if (isWin) {
10
+ return await new Promise((resolve) => execFile("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], { timeout: 5000 }, (err, stdout) => resolve(err ? "" : stdout)))
11
+ } else if (isMac) {
12
+ return await new Promise((resolve) => execFile("pbpaste", [], { timeout: 5000 }, (err, stdout) => resolve(err ? "" : stdout)))
13
+ } else {
14
+ return await new Promise((resolve) => execFile("xclip", ["-selection", "clipboard", "-o"], { timeout: 5000 }, (err, stdout) => resolve(err ? "" : stdout)))
15
+ }
16
+ } catch {
17
+ return ""
18
+ }
19
+ }
20
+
21
+ /** Insert pasted text into the active text target.
22
+ * Free-text question active → append to its answer (single-line field: newlines stripped).
23
+ * Options question active → ignore (no text field; must not leak into the input box).
24
+ * Otherwise → splice into the main input box at cursor (newlines kept, tabs → 2 spaces).
25
+ * Shared by bracketed paste (stdin data handler) and Ctrl+V clipboard read,
26
+ * so pasted content lands in the same place regardless of how the terminal delivered it. */
27
+ export function insertPastedText(state, rawText) {
28
+ if (!rawText) return
29
+ const q = state.question
30
+ if (q) {
31
+ if (q.options.length > 0) return
32
+ q.answer = (q.answer ?? "") + rawText.replace(/[\r\n]+/g, "")
33
+ return
34
+ }
35
+ const text = rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
36
+ const chars = [...text]
37
+ state.input.splice(state.cursor, 0, ...chars)
38
+ state.cursor += chars.length
39
+ }
40
+
3
41
  /** Ctrl+V / Alt+V: read clipboard image → write temp file in working directory → insert read_image command into input box.
4
42
  * Extracted from index.mjs.
5
43
  * ctx: { agent, state, pushLine, render } */
package/src/tui/index.mjs CHANGED
@@ -29,7 +29,7 @@ import { createWizard } from "./wizard.mjs"
29
29
  import { createPickers } from "./pickers.mjs"
30
30
  import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
31
31
  import { createInteraction } from "./interaction.mjs"
32
- import { pasteClipboardImage as pasteClipboardImageImpl } from "./clipboard.mjs"
32
+ import { pasteClipboardImage as pasteClipboardImageImpl, insertPastedText } from "./clipboard.mjs"
33
33
  import { runAgentTurn } from "./agent-turn.mjs"
34
34
  import { createKeyHandler } from "./key-handler.mjs"
35
35
  import { showStartup, backgroundIndex } from "./startup.mjs"
@@ -102,18 +102,17 @@ export async function startTUI(agent, opts = {}) {
102
102
  mousePending = ""
103
103
 
104
104
  // Bracketed paste: terminal wraps pasted text in \x1b[200~ ... \x1b[201~
105
- // Insert pasted content directly into state.input to avoid slow char-by-char keypress render
105
+ // Route pasted content to the active text target (question answer / input box) in one shot,
106
+ // avoiding slow char-by-char keypress render — see insertPastedText in clipboard.mjs
106
107
  if (pasteMode) {
107
108
  const endIdx = text.indexOf("\x1b[201~")
108
109
  if (endIdx >= 0) {
109
110
  pasteAccum += text.slice(0, endIdx)
110
111
  pasteMode = false
111
- const pasted = pasteAccum.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
112
+ const pasted = pasteAccum
112
113
  pasteAccum = ""
113
114
  if (pasted) {
114
- const chars = [...pasted]
115
- state.input.splice(state.cursor, 0, ...chars)
116
- state.cursor += chars.length
115
+ insertPastedText(state, pasted)
117
116
  render()
118
117
  }
119
118
  text = text.slice(endIdx + 6)
@@ -131,11 +130,9 @@ export async function startTUI(agent, opts = {}) {
131
130
  const endIdx = after.indexOf("\x1b[201~")
132
131
  if (endIdx >= 0) {
133
132
  // Paste begin and end in the same chunk: insert pasted content directly
134
- const pasted = after.slice(0, endIdx).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
133
+ const pasted = after.slice(0, endIdx)
135
134
  if (pasted) {
136
- const chars = [...pasted]
137
- state.input.splice(state.cursor, 0, ...chars)
138
- state.cursor += chars.length
135
+ insertPastedText(state, pasted)
139
136
  render()
140
137
  }
141
138
  text = before + after.slice(endIdx + 6)
@@ -1,4 +1,5 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
+ import { readClipboardText, insertPastedText } from "./clipboard.mjs"
2
3
 
3
4
  /** Keyboard event dispatch: permission confirm / question / picker / wizard / edit / scroll / history / paste.
4
5
  * Extracted from index.mjs.
@@ -69,6 +70,7 @@ export function createKeyHandler(ctx) {
69
70
  state.status = "Processing..."
70
71
  render()
71
72
  } else if (key.name === "return") {
73
+ if (q._pasting) return // block Enter while paste is in flight
72
74
  const answer = (q.answer ?? "").trim()
73
75
  q.resolve(answer || "")
74
76
  state.question = null
@@ -78,6 +80,18 @@ export function createKeyHandler(ctx) {
78
80
  } else if (key.name === "backspace") {
79
81
  q.answer = (q.answer ?? "").slice(0, -1)
80
82
  render()
83
+ } else if (key.ctrl && !key.alt && key.name === "v") {
84
+ // Ctrl+V paste: read clipboard text (fires when the terminal passes Ctrl+V through
85
+ // as a key event; bracketed-paste terminals are handled upstream in the stdin handler)
86
+ if (q._pasting) return
87
+ q._pasting = true
88
+ readClipboardText().then((text) => {
89
+ q._pasting = false
90
+ if (text) {
91
+ insertPastedText(state, text)
92
+ render()
93
+ }
94
+ }).catch(() => { q._pasting = false })
81
95
  } else if (str && !key.ctrl && !key.meta) {
82
96
  q.answer = (q.answer ?? "") + str
83
97
  render()
@@ -256,8 +270,20 @@ export function createKeyHandler(ctx) {
256
270
  return
257
271
  }
258
272
 
259
- // Ctrl+V (Unix) / Alt+V (Windows): paste clipboard image save temp file → insert read_image into input box
260
- const isPasteImage = (key.name === "v" && (key.ctrl || key.meta)) || (key.name === "v" && key.alt)
273
+ // Ctrl+V: paste clipboard text into the active text target
274
+ if (key.ctrl && !key.alt && key.name === "v") {
275
+ ;(async () => {
276
+ const text = await readClipboardText()
277
+ if (text) {
278
+ insertPastedText(state, text)
279
+ render()
280
+ }
281
+ })()
282
+ return
283
+ }
284
+
285
+ // Ctrl+Alt+V (Windows) / Alt+V: paste clipboard image
286
+ const isPasteImage = key.name === "v" && key.alt
261
287
  if (isPasteImage) {
262
288
  pasteClipboardImage(agent).catch((e) => pushLine(`[error] ${e.message}`, C.error))
263
289
  return
@@ -151,11 +151,12 @@ export function renderFrame(state, agent, opts) {
151
151
  // ---- input box ----
152
152
  const { borderColor, title } = inputBoxStyle(state)
153
153
  let topBorder
154
- if (title === " Input " && isMultimodal) {
155
- const hint = platform === "win32" ? " Alt+V paste " : " Ctrl+V paste "
156
- topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
157
- } else if (title === " Input ") {
158
- const hint = " Ctrl+U clear "
154
+ if (title === " Input " || title === " Question ") {
155
+ const parts = []
156
+ if (title === " Input ") parts.push(" Ctrl+U clear ")
157
+ if (title === " Question ") parts.push(" Enter submit ")
158
+ parts.push(" Ctrl+V paste ")
159
+ const hint = parts.join("")
159
160
  topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
160
161
  } else {
161
162
  topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 3 - stringWidth(title)))}╮`
@@ -164,5 +164,5 @@ export function createWizard(ctx) {
164
164
  ctx.openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
165
165
  }
166
166
 
167
- return { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
167
+ return { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard, wizardProviderItems }
168
168
  }