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.
Files changed (49) hide show
  1. package/README.md +11 -0
  2. package/bin/thincoder.mjs +115 -0
  3. package/package.json +1 -1
  4. package/src/advisor.mjs +105 -0
  5. package/src/agent/dispatch.mjs +35 -0
  6. package/src/agent/setup.mjs +9 -7
  7. package/src/agent-tools/subagent.mjs +1 -1
  8. package/src/agent-tools/timer.mjs +41 -0
  9. package/src/agent-tools/verify.mjs +165 -56
  10. package/src/agent-tools.mjs +1 -0
  11. package/src/agent.mjs +128 -20
  12. package/src/auto-think.mjs +83 -0
  13. package/src/cli/make-agent.mjs +9 -0
  14. package/src/config.mjs +18 -18
  15. package/src/git/checkpoint.mjs +2 -1
  16. package/src/git/gitmem.mjs +8 -2
  17. package/src/markdown.mjs +1 -1
  18. package/src/mcp/transport-http.mjs +8 -2
  19. package/src/memory/code-index.mjs +2 -2
  20. package/src/memory/code-sync.mjs +92 -35
  21. package/src/memory/core.mjs +10 -1
  22. package/src/memory/docs.mjs +24 -26
  23. package/src/memory/schema.mjs +16 -3
  24. package/src/prompts/coder.md +7 -4
  25. package/src/prompts/discipline.md +47 -15
  26. package/src/prompts/main.md +15 -11
  27. package/src/prompts/system.md +33 -7
  28. package/src/provider/core.mjs +134 -14
  29. package/src/provider/index.mjs +1 -1
  30. package/src/rules.mjs +53 -0
  31. package/src/session.mjs +1 -1
  32. package/src/tools/file.mjs +82 -1
  33. package/src/tools/hashline_edit.md +12 -0
  34. package/src/tools/index.mjs +6 -4
  35. package/src/tools/linter.md +13 -0
  36. package/src/tools/linter.mjs +146 -0
  37. package/src/tools/read.md +3 -2
  38. package/src/tools/repomap.mjs +14 -9
  39. package/src/tui/agent-turn.mjs +9 -2
  40. package/src/tui/ansi.mjs +1 -0
  41. package/src/tui/cmd-advisor.mjs +68 -0
  42. package/src/tui/cmd-think.mjs +36 -10
  43. package/src/tui/index.mjs +2 -1
  44. package/src/tui/key-handler.mjs +36 -1
  45. package/src/tui/layout.mjs +3 -1
  46. package/src/tui/pickers.mjs +15 -15
  47. package/src/tui/render-frame.mjs +17 -8
  48. package/src/tui/slash-commands.mjs +3 -0
  49. package/src/tools/repomap-parse.mjs +0 -168
@@ -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}` : thinking?.type === "enabled" ? "│ think: on" : ""
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 bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "")
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.question) {
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
- }