thincoder 0.8.9 → 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 +17 -0
- package/package.json +1 -1
- package/src/tui/clipboard.mjs +38 -0
- package/src/tui/index.mjs +7 -10
- package/src/tui/key-handler.mjs +28 -2
- package/src/tui/render-frame.mjs +6 -5
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
package/src/tui/clipboard.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
112
|
+
const pasted = pasteAccum
|
|
112
113
|
pasteAccum = ""
|
|
113
114
|
if (pasted) {
|
|
114
|
-
|
|
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)
|
|
133
|
+
const pasted = after.slice(0, endIdx)
|
|
135
134
|
if (pasted) {
|
|
136
|
-
|
|
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)
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -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
|
|
260
|
-
|
|
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
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -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 "
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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)))}╮`
|