thincoder 0.8.11 → 0.8.12
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 +11 -0
- package/bin/thincoder.mjs +115 -0
- package/package.json +1 -1
- package/src/advisor.mjs +105 -0
- package/src/agent/dispatch.mjs +35 -0
- package/src/agent/setup.mjs +9 -7
- package/src/agent-tools/subagent.mjs +1 -1
- package/src/agent-tools/timer.mjs +41 -0
- package/src/agent-tools/verify.mjs +165 -56
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +128 -20
- package/src/auto-think.mjs +83 -0
- package/src/cli/make-agent.mjs +9 -0
- package/src/config.mjs +18 -18
- package/src/git/checkpoint.mjs +2 -1
- package/src/git/gitmem.mjs +8 -2
- package/src/markdown.mjs +1 -1
- package/src/mcp/transport-http.mjs +8 -2
- package/src/memory/code-index.mjs +2 -2
- package/src/memory/code-sync.mjs +92 -35
- package/src/memory/core.mjs +10 -1
- package/src/memory/docs.mjs +24 -26
- package/src/memory/schema.mjs +16 -3
- package/src/prompts/coder.md +7 -4
- package/src/prompts/discipline.md +47 -15
- package/src/prompts/main.md +15 -11
- package/src/prompts/system.md +33 -7
- package/src/provider/core.mjs +134 -14
- package/src/provider/index.mjs +1 -1
- package/src/rules.mjs +53 -0
- package/src/session.mjs +1 -1
- package/src/tools/file.mjs +82 -1
- package/src/tools/hashline_edit.md +12 -0
- package/src/tools/index.mjs +6 -4
- package/src/tools/linter.md +13 -0
- package/src/tools/linter.mjs +146 -0
- package/src/tools/read.md +3 -2
- package/src/tools/repomap.mjs +14 -9
- package/src/tui/agent-turn.mjs +9 -2
- package/src/tui/ansi.mjs +1 -0
- package/src/tui/cmd-advisor.mjs +68 -0
- package/src/tui/cmd-think.mjs +36 -10
- package/src/tui/index.mjs +2 -1
- package/src/tui/key-handler.mjs +36 -1
- package/src/tui/layout.mjs +3 -1
- package/src/tui/pickers.mjs +15 -15
- package/src/tui/render-frame.mjs +17 -8
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tools/repomap-parse.mjs +0 -168
package/src/tui/render-frame.mjs
CHANGED
|
@@ -40,8 +40,11 @@ export function renderFrame(state, agent, opts) {
|
|
|
40
40
|
const thinking = agent.provider.thinking
|
|
41
41
|
const effort = agent.provider.reasoningEffort
|
|
42
42
|
const isMultimodal = specForModel(model).multimodal
|
|
43
|
+
const spec = specForModel(model)
|
|
44
|
+
const thinkOnValue = spec.thinkOnValue ?? "enabled"
|
|
43
45
|
const thinkBadge = thinking?.type === "disabled" ? "│ think: off"
|
|
44
|
-
: effort ? `│ think: ${effort}`
|
|
46
|
+
: effort ? `│ think: ${effort}`
|
|
47
|
+
: thinking?.type === thinkOnValue ? "│ think: on" : ""
|
|
45
48
|
|
|
46
49
|
const out = [ansi.home]
|
|
47
50
|
let cursorRow = 0, cursorCol = 0
|
|
@@ -145,17 +148,19 @@ export function renderFrame(state, agent, opts) {
|
|
|
145
148
|
// ---- queue preview ----
|
|
146
149
|
if (panels.queue) {
|
|
147
150
|
const preview = sliceByWidth(state.queue[0].text, W - 20)
|
|
148
|
-
out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete${ansi.reset}${ansi.clearLine}`)
|
|
151
|
+
out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete │ Ctrl+I inject${ansi.reset}${ansi.clearLine}`)
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
// ---- input box ----
|
|
152
155
|
const { borderColor, title } = inputBoxStyle(state)
|
|
153
156
|
let topBorder
|
|
154
|
-
if (title === " Input " || title === " Question ") {
|
|
157
|
+
if (title === " Input " || title === " Question " || title === " Inject Message ") {
|
|
155
158
|
const parts = []
|
|
156
159
|
if (title === " Input ") parts.push(" Ctrl+U clear ")
|
|
157
160
|
if (title === " Question ") parts.push(" Enter submit ")
|
|
161
|
+
if (title === " Inject Message ") parts.push(" Enter send, Esc cancel ")
|
|
158
162
|
parts.push(" Ctrl+V paste ")
|
|
163
|
+
parts.push(" Ctrl+I inject ")
|
|
159
164
|
const hint = parts.join("")
|
|
160
165
|
topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
|
|
161
166
|
} else {
|
|
@@ -173,9 +178,10 @@ export function renderFrame(state, agent, opts) {
|
|
|
173
178
|
const statusLine = buildStatusLine(state, agent, { cols, slashCommands })
|
|
174
179
|
const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
|
|
175
180
|
const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
|
|
176
|
-
const
|
|
181
|
+
const advisorBanner = agent.config?.advisor?.enabled ? `${C.advisor} ADVISOR${ansi.reset}${ansi.dim}│` : ""
|
|
182
|
+
const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.enabled ? " ADVISOR│ " : "")
|
|
177
183
|
const statusMax = cols - 1 - (bannerPrefix ? stringWidth(bannerPrefix) : 0)
|
|
178
|
-
out.push(`${ansi.dim}${planBanner}${autoBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
|
|
184
|
+
out.push(`${ansi.dim}${planBanner}${autoBanner}${advisorBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
|
|
179
185
|
|
|
180
186
|
const frame = out.join("\r\n")
|
|
181
187
|
|
|
@@ -239,7 +245,10 @@ function buildConvLines(state, cols) {
|
|
|
239
245
|
function inputBoxStyle(state) {
|
|
240
246
|
let borderColor = C.tool
|
|
241
247
|
let title
|
|
242
|
-
if (state.
|
|
248
|
+
if (state.interruptPrompt) {
|
|
249
|
+
borderColor = C.warn
|
|
250
|
+
title = " Inject Message "
|
|
251
|
+
} else if (state.question) {
|
|
243
252
|
borderColor = C.tool
|
|
244
253
|
title = " Question "
|
|
245
254
|
} else if (state.permission) {
|
|
@@ -308,7 +317,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
308
317
|
const fmtK = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`)
|
|
309
318
|
const cacheTotal = tk.cacheHit + tk.cacheMiss
|
|
310
319
|
const tokenHint = tk.prompt > 0
|
|
311
|
-
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
|
|
320
|
+
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${tk.reasoningTokens > 0 ? ` ✦${fmtK(tk.reasoningTokens)}` : ""}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
|
|
312
321
|
: ""
|
|
313
322
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
314
323
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
@@ -321,7 +330,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
321
330
|
: ` │ context ${ctxPct}%`
|
|
322
331
|
: ""
|
|
323
332
|
const queueHint = state.queue.length > 0 ? ` │ queue: ${state.queue.length}` : ""
|
|
324
|
-
return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
|
|
333
|
+
return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit`
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
/** Summarize tool args for subagent panel display (one line, short). Pure. */
|
|
@@ -21,6 +21,7 @@ import { handleGoalCommand } from "./cmd-goal.mjs"
|
|
|
21
21
|
import { handleSkillsCommand } from "./cmd-skills.mjs"
|
|
22
22
|
import { handleMcpCommand } from "./cmd-mcp.mjs"
|
|
23
23
|
import { handleAutoCommand } from "./cmd-auto.mjs"
|
|
24
|
+
import { handleAdvisorCommand } from "./cmd-advisor.mjs"
|
|
24
25
|
import { handleThinkCommand } from "./cmd-think.mjs"
|
|
25
26
|
import { handleModelCommand } from "./cmd-model.mjs"
|
|
26
27
|
import { handleConfigCommand } from "./cmd-config.mjs"
|
|
@@ -31,6 +32,7 @@ import { handleUpgradeCommand } from "./cmd-upgrade.mjs"
|
|
|
31
32
|
export const SLASH_COMMANDS = [
|
|
32
33
|
{ name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
|
|
33
34
|
{ name: "/auto", group: "Agent", desc: "toggle auto-approve" },
|
|
35
|
+
{ name: "/advisor", group: "Agent", desc: "toggle advisor review & select model" },
|
|
34
36
|
{ name: "/model", group: "Agent", desc: "select model & manage providers" },
|
|
35
37
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
36
38
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
@@ -63,6 +65,7 @@ const HANDLERS = {
|
|
|
63
65
|
"/skills": handleSkillsCommand,
|
|
64
66
|
"/mcp": handleMcpCommand,
|
|
65
67
|
"/auto": handleAutoCommand,
|
|
68
|
+
"/advisor": handleAdvisorCommand,
|
|
66
69
|
"/think": handleThinkCommand,
|
|
67
70
|
"/model": handleModelCommand,
|
|
68
71
|
"/config": handleConfigCommand,
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* repomap-parse.mjs — repo dependency graph parser (zero dependencies, pure regex)
|
|
3
|
-
* Gets known file list from code_chunks, parses each file's import/export relationships in real time,
|
|
4
|
-
* builds forward dependency graph + reverse reference graph. Shared by repomap.mjs's buildSummary / buildOutline.
|
|
5
|
-
*/
|
|
6
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
7
|
-
import { join } from "node:path"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Scan all files, build forward dependency graph + reverse reference graph.
|
|
11
|
-
* Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
|
|
12
|
-
*/
|
|
13
|
-
export function buildDepGraph(db, cwd) {
|
|
14
|
-
const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
|
|
15
|
-
if (allFiles.length === 0) return null
|
|
16
|
-
|
|
17
|
-
const deps = new Map() // path → { imports: Set, exports: Set, size: number, dir: string }
|
|
18
|
-
const importers = new Map() // importee → Set<importer>
|
|
19
|
-
|
|
20
|
-
for (const rel of allFiles) {
|
|
21
|
-
const abs = join(cwd, ...rel.split("/"))
|
|
22
|
-
if (!existsSync(abs)) continue
|
|
23
|
-
const text = readFileSync(abs, "utf8")
|
|
24
|
-
const lines = text.split("\n")
|
|
25
|
-
const ext = rel.slice(rel.lastIndexOf(".")).toLowerCase()
|
|
26
|
-
|
|
27
|
-
let imports, exports
|
|
28
|
-
if (ext === ".py") {
|
|
29
|
-
const py = parsePyOutline(lines)
|
|
30
|
-
imports = py.imports
|
|
31
|
-
exports = py.symbols
|
|
32
|
-
} else {
|
|
33
|
-
imports = parseImports(lines, ext)
|
|
34
|
-
exports = parseExports(lines, ext)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Resolve import paths to relative paths (handle ./ ../)
|
|
38
|
-
const resolved = []
|
|
39
|
-
for (let imp of imports) {
|
|
40
|
-
if (imp.startsWith("./")) imp = imp.slice(2)
|
|
41
|
-
const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ""
|
|
42
|
-
const parts = imp.split("/")
|
|
43
|
-
if (parts[0] === "..") {
|
|
44
|
-
const up = dir.split("/").filter(Boolean)
|
|
45
|
-
let i = 0
|
|
46
|
-
while (parts[i] === ".." && up.length > 0) { up.pop(); i++ }
|
|
47
|
-
resolved.push([...up, ...parts.slice(i)].join("/"))
|
|
48
|
-
} else {
|
|
49
|
-
resolved.push(dir ? `${dir}/${imp}` : imp)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "."
|
|
54
|
-
deps.set(rel, { imports: new Set(resolved), exports: new Set(exports), size: Math.floor(text.length / 1024), dir })
|
|
55
|
-
|
|
56
|
-
for (const r of resolved) {
|
|
57
|
-
if (!importers.has(r)) importers.set(r, new Set())
|
|
58
|
-
importers.get(r).add(rel)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { deps, importers, fileCount: allFiles.length }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------- internal implementation
|
|
66
|
-
|
|
67
|
-
function normalizeExt(p) {
|
|
68
|
-
return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
|
|
72
|
-
function parseImports(lines, ext) {
|
|
73
|
-
const imports = []
|
|
74
|
-
const text = lines.join("\n")
|
|
75
|
-
// standard import
|
|
76
|
-
const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
|
|
77
|
-
let m
|
|
78
|
-
while ((m = re.exec(text))) {
|
|
79
|
-
const raw = m[1] || m[2]
|
|
80
|
-
if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
|
|
81
|
-
imports.push(normalizeExt(raw))
|
|
82
|
-
}
|
|
83
|
-
// re-export: export { x } from './module'
|
|
84
|
-
const reExportRe = /export\s*\{[^}]*\}\s*from\s*['"]([^'"]+)['"]/g
|
|
85
|
-
while ((m = reExportRe.exec(text))) {
|
|
86
|
-
const raw = m[1]
|
|
87
|
-
if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
|
|
88
|
-
imports.push(normalizeExt(raw))
|
|
89
|
-
}
|
|
90
|
-
return [...new Set(imports)]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Extract JS/TS file export symbols */
|
|
94
|
-
function parseExports(lines, ext) {
|
|
95
|
-
const exports = []
|
|
96
|
-
const text = lines.join("\n")
|
|
97
|
-
// export function/class/const/let/var name
|
|
98
|
-
const namedRe = /export\s+(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+)|(?:const|let|var)\s+(\w+))/g
|
|
99
|
-
let m
|
|
100
|
-
while ((m = namedRe.exec(text))) {
|
|
101
|
-
exports.push(m[1] || m[2] || m[3])
|
|
102
|
-
}
|
|
103
|
-
// export default function/class name / export default expression
|
|
104
|
-
const defaultRe = /export\s+default\s+(?:(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+))|(\w+))/g
|
|
105
|
-
while ((m = defaultRe.exec(text))) {
|
|
106
|
-
const name = m[1] || m[2] || m[3]
|
|
107
|
-
if (name) exports.push(name)
|
|
108
|
-
else if (!exports.some((e) => e === "default")) exports.push("default")
|
|
109
|
-
}
|
|
110
|
-
// export { a, b as c } — prefer the "as" alias as the exported name
|
|
111
|
-
const braceRe = /export\s*\{([^}]+)\}/g
|
|
112
|
-
while ((m = braceRe.exec(text))) {
|
|
113
|
-
for (const name of m[1].split(",")) {
|
|
114
|
-
const parts = name.trim().split(/\s+/)
|
|
115
|
-
// "a as b" → b (exported name), "a" → a
|
|
116
|
-
const exported = parts.length >= 3 ? parts[2] : parts[0]
|
|
117
|
-
if (exported) exports.push(exported)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// export const { a, b } = ... (destructured export)
|
|
121
|
-
const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
|
|
122
|
-
while ((m = destructRe.exec(text))) {
|
|
123
|
-
for (const name of m[1].split(",")) {
|
|
124
|
-
const parts = name.trim().split(/\s*:\s*/)
|
|
125
|
-
const n = parts[0].trim()
|
|
126
|
-
if (n) exports.push(n)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return [...new Set(exports)]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Extract Python imports and top-level def/class */
|
|
133
|
-
function parsePyOutline(lines) {
|
|
134
|
-
const imports = []
|
|
135
|
-
const symbols = []
|
|
136
|
-
for (const line of lines) {
|
|
137
|
-
const fromRe = line.match(/^from\s+(\S+)\s+import\s+(.+)/)
|
|
138
|
-
if (fromRe) {
|
|
139
|
-
const rel = pyRelPath(fromRe[1])
|
|
140
|
-
if (rel) imports.push(rel)
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
const impRe = line.match(/^import\s+(.+)/)
|
|
144
|
-
if (impRe) {
|
|
145
|
-
for (const mod of impRe[1].split(",")) {
|
|
146
|
-
const rel = pyRelPath(mod.trim().split(/\s+/)[0])
|
|
147
|
-
if (rel) imports.push(rel)
|
|
148
|
-
}
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
const defRe = line.match(/^(?:async\s+)?(?:def|class)\s+(\w+)/)
|
|
152
|
-
if (defRe) symbols.push(defRe[1])
|
|
153
|
-
}
|
|
154
|
-
return { imports: [...new Set(imports)], symbols: [...new Set(symbols)] }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Python relative import → relative file path:
|
|
159
|
-
* Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
|
|
160
|
-
* Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
|
|
161
|
-
*/
|
|
162
|
-
function pyRelPath(mod) {
|
|
163
|
-
if (!mod?.startsWith(".")) return null
|
|
164
|
-
const dots = mod.match(/^\.+/)[0].length
|
|
165
|
-
const rest = mod.slice(dots).replaceAll(".", "/")
|
|
166
|
-
if (!rest) return null
|
|
167
|
-
return normalizeExt("../".repeat(dots - 1) + rest)
|
|
168
|
-
}
|