thincoder 0.12.1 → 0.12.3
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 +18 -5
- package/package.json +1 -1
- package/src/advisor/history.mjs +112 -0
- package/src/advisor/messages.mjs +182 -0
- package/src/advisor/repos.mjs +133 -0
- package/src/advisor/run.mjs +346 -0
- package/src/advisor.mjs +109 -509
- package/src/agent/completion.mjs +119 -0
- package/src/agent/dispatch.mjs +54 -7
- package/src/agent/post-turn.mjs +70 -0
- package/src/agent/setup.mjs +93 -5
- package/src/agent-tools/advisor.mjs +159 -12
- package/src/agent-tools/eng.mjs +64 -0
- package/src/agent-tools/subagent.mjs +73 -3
- package/src/agent-tools/task.mjs +45 -6
- package/src/agent-tools/verify.mjs +18 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +110 -150
- package/src/cli/make-agent.mjs +1 -0
- package/src/cli/setup-wizard.mjs +1 -0
- package/src/config.mjs +22 -4
- package/src/prompts/advisor-design.md +43 -0
- package/src/prompts/advisor-round1.md +11 -4
- package/src/prompts/advisor-round2.md +12 -7
- package/src/prompts/advisor-round3.md +11 -6
- package/src/prompts/coder.md +9 -3
- package/src/prompts/discipline.md +12 -96
- package/src/prompts/eng-coder.md +34 -0
- package/src/prompts/engineering-sub.md +12 -0
- package/src/prompts/engineering.md +96 -0
- package/src/prompts/main.md +1 -1
- package/src/prompts/methodology-template.md +39 -0
- package/src/prompts/plan.md +2 -2
- package/src/prompts/system.md +43 -61
- package/src/session.mjs +270 -89
- package/src/skills.mjs +48 -15
- package/src/tools/apply_patch.md +1 -1
- package/src/tools/codemode.mjs +10 -4
- package/src/tools/delete.md +1 -0
- package/src/tools/edit.md +1 -1
- package/src/tools/execute.md +5 -0
- package/src/tools/file.mjs +4 -0
- package/src/tools/git.md +15 -0
- package/src/tools/git.mjs +1 -6
- package/src/tools/lint.md +8 -0
- package/src/tools/linter.mjs +1 -5
- package/src/tools/lsp.md +7 -0
- package/src/tools/lsp.mjs +8 -9
- package/src/tools/patch.mjs +1 -29
- package/src/tools/read_image.md +5 -1
- package/src/tools/system.mjs +1 -1
- package/src/tools/web.mjs +3 -3
- package/src/tui/agent-turn.mjs +169 -66
- package/src/tui/cmd-config.mjs +12 -0
- package/src/tui/cmd-eng.mjs +44 -0
- package/src/tui/cmd-exit.mjs +6 -16
- package/src/tui/cmd-fold.mjs +3 -4
- package/src/tui/cmd-model.mjs +11 -6
- package/src/tui/cmd-new.mjs +5 -5
- package/src/tui/cmd-session.mjs +21 -11
- package/src/tui/cmd-think.mjs +1 -0
- package/src/tui/index.mjs +7 -6
- package/src/tui/key-handler.mjs +132 -4
- package/src/tui/layout.mjs +5 -5
- package/src/tui/pickers.mjs +184 -44
- package/src/tui/render-conversation.mjs +49 -11
- package/src/tui/render-frame.mjs +38 -12
- package/src/tui/render-loop.mjs +2 -1
- package/src/tui/slash-commands.mjs +11 -7
- package/src/tui/startup.mjs +4 -3
- package/src/tui/wizard.mjs +3 -0
- package/src/tools/checkpoint.md +0 -15
- package/src/tools/git_diff.md +0 -11
- package/src/tools/git_log.md +0 -10
- package/src/tools/git_status.md +0 -8
- package/src/tools/linter.md +0 -13
- package/src/tools/syntax_check.md +0 -10
package/src/tools/file.mjs
CHANGED
|
@@ -122,6 +122,7 @@ export const writeTool = {
|
|
|
122
122
|
required: ["path", "content"],
|
|
123
123
|
},
|
|
124
124
|
readonly: false,
|
|
125
|
+
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
125
126
|
async execute(args, ctx) {
|
|
126
127
|
const abs = resolveInCwd(ctx, args.path)
|
|
127
128
|
await mkdir(dirname(abs), { recursive: true })
|
|
@@ -149,6 +150,7 @@ export const editTool = {
|
|
|
149
150
|
required: ["path", "old_string", "new_string"],
|
|
150
151
|
},
|
|
151
152
|
readonly: false,
|
|
153
|
+
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
152
154
|
async execute(args, ctx) {
|
|
153
155
|
const abs = resolveInCwd(ctx, args.path)
|
|
154
156
|
if (!args.old_string) {
|
|
@@ -194,6 +196,7 @@ export const insertAfterTool = {
|
|
|
194
196
|
required: ["path", "content"],
|
|
195
197
|
},
|
|
196
198
|
readonly: false,
|
|
199
|
+
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
197
200
|
async execute(args, ctx) {
|
|
198
201
|
const abs = resolveInCwd(ctx, args.path)
|
|
199
202
|
const text = normalizeEOL(await readFile(abs, "utf8"))
|
|
@@ -257,6 +260,7 @@ export const hashlineEditTool = {
|
|
|
257
260
|
required: ["path", "old_hashes", "new_content"],
|
|
258
261
|
},
|
|
259
262
|
readonly: false,
|
|
263
|
+
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
260
264
|
async execute(args, ctx) {
|
|
261
265
|
const abs = resolveInCwd(ctx, args.path)
|
|
262
266
|
if (!args.old_hashes?.length) throw new Error("old_hashes must not be empty — read the file with hashes=true to get line hashes")
|
package/src/tools/git.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Run a git command. Use this to see uncommitted changes, staged changes, diff against a ref, recent commits, or manage checkpoints. Only works inside a git repository.
|
|
2
|
+
- action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.
|
|
3
|
+
- action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.
|
|
4
|
+
- action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.
|
|
5
|
+
- action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
- action (required): diff / status / log / checkpoint
|
|
9
|
+
- staged: (diff) Show staged changes instead of working tree
|
|
10
|
+
- path: (diff/log/checkpoint:cat/checkpoint:rewind) File or directory to scope to
|
|
11
|
+
- ref: (diff) Compare against this ref (default HEAD)
|
|
12
|
+
- count: (log) Number of commits (default 10)
|
|
13
|
+
- oneline: (log) One-line-per-commit format
|
|
14
|
+
- checkpointAction: (checkpoint) list snapshots / create one / restore by id / read file from snapshot
|
|
15
|
+
- checkpointId: (checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)
|
package/src/tools/git.mjs
CHANGED
|
@@ -9,12 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
export const gitTool = {
|
|
11
11
|
name: "git",
|
|
12
|
-
description:
|
|
13
|
-
"Run a git command. Use this to see uncommitted changes, staged changes, diff against a ref, recent commits, or manage checkpoints. Only works inside a git repository.\n" +
|
|
14
|
-
"- action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.\n" +
|
|
15
|
-
"- action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.\n" +
|
|
16
|
-
"- action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.\n" +
|
|
17
|
-
"- action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).",
|
|
12
|
+
description: DESC("git"),
|
|
18
13
|
parameters: {
|
|
19
14
|
type: "object",
|
|
20
15
|
properties: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.
|
|
2
|
+
Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).
|
|
3
|
+
With 'full', runs the language-aware cascade: eslint → tsc –noEmit → node --check (JS/TS/TSX); ruff (Python); cargo check (Rust); go vet (Go).
|
|
4
|
+
Use the fast default after every write/edit; use 'full' before declaring a task complete.
|
|
5
|
+
|
|
6
|
+
Parameters:
|
|
7
|
+
- path: File to check (default: most recently modified file)
|
|
8
|
+
- full: Run the full language-aware cascade instead of just node --check (default false)
|
package/src/tools/linter.mjs
CHANGED
|
@@ -5,11 +5,7 @@ import { join, relative } from "node:path"
|
|
|
5
5
|
|
|
6
6
|
export const lintTool = {
|
|
7
7
|
name: "lint",
|
|
8
|
-
description:
|
|
9
|
-
"Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.\n" +
|
|
10
|
-
"Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).\n" +
|
|
11
|
-
"With 'full', runs the language-aware cascade: eslint → tsc –noEmit → node --check (JS/TS/TSX); ruff (Python); cargo check (Rust); go vet (Go).\n" +
|
|
12
|
-
"Use the fast default after every write/edit; use 'full' before declaring a task complete.",
|
|
8
|
+
description: DESC("lint"),
|
|
13
9
|
parameters: {
|
|
14
10
|
type: "object",
|
|
15
11
|
properties: {
|
package/src/tools/lsp.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- subcommand (required): LSP operation — "definition" | "references" | "hover" | "symbols" | "diagnostics"
|
|
5
|
+
- uri (required): Target file path (relative to project root)
|
|
6
|
+
- line: 1-based line number (for definition/references/hover)
|
|
7
|
+
- character: 1-based character offset (for definition/references/hover)
|
package/src/tools/lsp.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { spawn } from "node:child_process"
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs"
|
|
19
19
|
import { join, extname } from "node:path"
|
|
20
|
+
import { DESC } from "./shared.mjs"
|
|
20
21
|
|
|
21
22
|
// ---- JSON-RPC transport over stdio ----
|
|
22
23
|
|
|
@@ -184,9 +185,7 @@ async function ensureOpen(proc, uri, ext) {
|
|
|
184
185
|
|
|
185
186
|
export const lspTool = {
|
|
186
187
|
name: "lsp",
|
|
187
|
-
description:
|
|
188
|
-
"LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. " +
|
|
189
|
-
"Use this to understand code structure without grep-guessing function locations or type shapes.",
|
|
188
|
+
description: DESC("lsp"),
|
|
190
189
|
parameters: {
|
|
191
190
|
type: "object",
|
|
192
191
|
properties: {
|
|
@@ -240,7 +239,7 @@ export const lspTool = {
|
|
|
240
239
|
textDocument: { uri },
|
|
241
240
|
position: { line: args.line - 1, character: args.character - 1 },
|
|
242
241
|
}, 10)
|
|
243
|
-
if (!res?.result) return "
|
|
242
|
+
if (!res?.result) return "(no definition found)"
|
|
244
243
|
const locs = Array.isArray(res.result) ? res.result : [res.result]
|
|
245
244
|
return locs.map((l) => {
|
|
246
245
|
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
@@ -255,7 +254,7 @@ export const lspTool = {
|
|
|
255
254
|
position: { line: args.line - 1, character: args.character - 1 },
|
|
256
255
|
context: { includeDeclaration: false },
|
|
257
256
|
}, 10)
|
|
258
|
-
if (!res?.result?.length) return "
|
|
257
|
+
if (!res?.result?.length) return "(no references found)"
|
|
259
258
|
return res.result.slice(0, 50).map((l) => {
|
|
260
259
|
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
261
260
|
return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
|
|
@@ -268,7 +267,7 @@ export const lspTool = {
|
|
|
268
267
|
textDocument: { uri },
|
|
269
268
|
position: { line: args.line - 1, character: args.character - 1 },
|
|
270
269
|
}, 10)
|
|
271
|
-
if (!res?.result?.contents) return "
|
|
270
|
+
if (!res?.result?.contents) return "(no hover info)"
|
|
272
271
|
const contents = res.result.contents
|
|
273
272
|
if (typeof contents === "string") return contents
|
|
274
273
|
if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : c.value).join("\n")
|
|
@@ -280,7 +279,7 @@ export const lspTool = {
|
|
|
280
279
|
const res = await request(proc, "textDocument/documentSymbol", {
|
|
281
280
|
textDocument: { uri },
|
|
282
281
|
}, 10)
|
|
283
|
-
if (!res?.result?.length) return "
|
|
282
|
+
if (!res?.result?.length) return "(no symbols found)"
|
|
284
283
|
function render(nodes, depth) {
|
|
285
284
|
const lines = []
|
|
286
285
|
for (const n of nodes) {
|
|
@@ -295,7 +294,7 @@ export const lspTool = {
|
|
|
295
294
|
|
|
296
295
|
case "diagnostics": {
|
|
297
296
|
const diags = proc._diagnostics?.[uri]
|
|
298
|
-
if (!diags?.length) return "
|
|
297
|
+
if (!diags?.length) return "(no diagnostics)"
|
|
299
298
|
return diags.slice(0, 30).map((d) => {
|
|
300
299
|
const sev = { 1: "ERROR", 2: "WARN", 3: "INFO", 4: "HINT" }[d.severity] || "?"
|
|
301
300
|
return `L${d.range.start.line + 1}: ${sev}: ${d.message}${d.code ? ` [${d.code}]` : ""}`
|
|
@@ -306,7 +305,7 @@ export const lspTool = {
|
|
|
306
305
|
return `Unknown subcommand: ${args.subcommand}`
|
|
307
306
|
}
|
|
308
307
|
} catch (err) {
|
|
309
|
-
return `
|
|
308
|
+
return `lsp error: ${err.message}`
|
|
310
309
|
}
|
|
311
310
|
},
|
|
312
311
|
}
|
package/src/tools/patch.mjs
CHANGED
|
@@ -159,36 +159,7 @@ export const applyPatchTool = {
|
|
|
159
159
|
},
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// ---------------------------------------------------------------- syntax_check
|
|
163
162
|
|
|
164
|
-
export const syntaxCheckTool = {
|
|
165
|
-
name: "syntax_check",
|
|
166
|
-
description: DESC("syntax_check"),
|
|
167
|
-
parameters: {
|
|
168
|
-
type: "object",
|
|
169
|
-
properties: {
|
|
170
|
-
path: { type: "string", description: "File path (.js/.mjs/.cjs only)" },
|
|
171
|
-
},
|
|
172
|
-
required: ["path"],
|
|
173
|
-
},
|
|
174
|
-
readonly: true,
|
|
175
|
-
execute(args, ctx) {
|
|
176
|
-
const abs = resolveInCwd(ctx, args.path)
|
|
177
|
-
if (!/\.(?:[mc]?js)$/.test(abs)) {
|
|
178
|
-
return `syntax_check only supports .js/.mjs/.cjs files; ${args.path} skipped.`
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
execFileSync(process.execPath, ["--check", abs], {
|
|
182
|
-
cwd: ctx.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
183
|
-
})
|
|
184
|
-
return `Syntax OK: ${args.path}`
|
|
185
|
-
} catch (e) {
|
|
186
|
-
// node --check writes errors to stderr
|
|
187
|
-
const msg = (e.stderr || e.stdout || e.message || "").trim()
|
|
188
|
-
return `Syntax error in ${args.path}:\n${msg || "(unknown)"}`
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
}
|
|
192
163
|
|
|
193
164
|
// ---------------------------------------------------------------- bash
|
|
194
165
|
|
|
@@ -204,6 +175,7 @@ export const deleteTool = {
|
|
|
204
175
|
required: ["path"],
|
|
205
176
|
},
|
|
206
177
|
readonly: false,
|
|
178
|
+
touchedPaths(args) { return args.path ? [args.path] : [] },
|
|
207
179
|
async execute(args, ctx) {
|
|
208
180
|
const abs = resolveInCwd(ctx, args.path)
|
|
209
181
|
let s
|
package/src/tools/read_image.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
Read an image file and return it as multimodal content visible to the model. Use this to view screenshots, UI mockups, diagrams, or any visual content. The model only sees images through this tool — it cannot "see" files directly. Supports png, jpg, gif, webp, bmp, svg. The image is base64-encoded and included in the response. Large images (>20MB) are rejected.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Parameters:
|
|
4
|
+
- path (required): Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp, bmp, svg.
|
|
5
|
+
|
|
6
|
+
Notes:
|
|
7
|
+
- This tool only works with models that support vision/image input (Kimi K3, Qwen3.7, MiniMax M3). Pure text models (DeepSeek V4, GLM-5) will receive an error.
|
package/src/tools/system.mjs
CHANGED
|
@@ -378,7 +378,7 @@ export const lsTool = {
|
|
|
378
378
|
}),
|
|
379
379
|
)
|
|
380
380
|
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
381
|
-
if (rows.length === 0) return "(
|
|
381
|
+
if (rows.length === 0) return "(no entries)"
|
|
382
382
|
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
383
383
|
return truncate(out.join("\n"))
|
|
384
384
|
},
|
package/src/tools/web.mjs
CHANGED
|
@@ -77,12 +77,12 @@ export const websearchTool = {
|
|
|
77
77
|
const engine = ENGINES.find(e => e.name === args.engine)
|
|
78
78
|
if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
|
|
79
79
|
const fetched = await fetchEngine(engine, args.query, page, ctx)
|
|
80
|
-
if (!fetched || fetched.results.length === 0) return
|
|
81
|
-
return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
80
|
+
if (!fetched || fetched.results.length === 0) return "(no results)"
|
|
81
|
+
return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
82
82
|
}
|
|
83
83
|
const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, ctx))
|
|
84
84
|
const fetched = (await Promise.all(promises)).filter(Boolean)
|
|
85
|
-
if (fetched.length === 0) return "(no results
|
|
85
|
+
if (fetched.length === 0) return "(no results)"
|
|
86
86
|
const merged = [], indexes = fetched.map(() => 0)
|
|
87
87
|
let done = false
|
|
88
88
|
while (!done && merged.length < limit) {
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -3,6 +3,22 @@ import { saveSession } from "../session.mjs"
|
|
|
3
3
|
import { sliceByWidth } from "./render.mjs"
|
|
4
4
|
import { ansi, C } from "./ansi.mjs"
|
|
5
5
|
|
|
6
|
+
/** Tool execution start timestamps (performance.now ms), keyed by tool name. */
|
|
7
|
+
const _toolTicks = Object.create(null)
|
|
8
|
+
|
|
9
|
+
/** Per-tool streaming preview line limits — tools with verbose output get more lines */
|
|
10
|
+
const LIVE_LINE_LIMITS = {
|
|
11
|
+
bash: 10,
|
|
12
|
+
advisor: 15,
|
|
13
|
+
read: 3,
|
|
14
|
+
grep: 8,
|
|
15
|
+
glob: 8,
|
|
16
|
+
search: 8,
|
|
17
|
+
websearch: 8,
|
|
18
|
+
code_search: 8,
|
|
19
|
+
doc_search: 8,
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
/** Execute one agent conversation turn (triggered by submit or queue).
|
|
7
23
|
* Extracted from index.mjs: agent loop + callback construction + error handling + queue processing.
|
|
8
24
|
* ctx: { agent, state, pushLine, pushLabel, render, scheduleRender,
|
|
@@ -29,6 +45,8 @@ export async function runAgentTurn(ctx, text) {
|
|
|
29
45
|
state.status = "Processing..."
|
|
30
46
|
state.streaming = ""
|
|
31
47
|
state.reasoning = ""
|
|
48
|
+
state.advisorStreaming = ""
|
|
49
|
+
state._advisorThink = ""
|
|
32
50
|
state.subTasks = {}
|
|
33
51
|
state.currentTool = null
|
|
34
52
|
state.processingStarted = Date.now()
|
|
@@ -49,6 +67,8 @@ export async function runAgentTurn(ctx, text) {
|
|
|
49
67
|
pushLine(state.streaming, C.text)
|
|
50
68
|
state.streaming = ""
|
|
51
69
|
}
|
|
70
|
+
state.advisorStreaming = ""
|
|
71
|
+
state._advisorThink = ""
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
const callbacks = {
|
|
@@ -102,14 +122,36 @@ export async function runAgentTurn(ctx, text) {
|
|
|
102
122
|
scheduleRender()
|
|
103
123
|
return
|
|
104
124
|
}
|
|
125
|
+
if (name === "advisor") { state.advisorStreaming = ""; state._advisorThink = "" }
|
|
105
126
|
flushStream()
|
|
106
127
|
ensureAssistantLabel()
|
|
107
128
|
state.currentTool = name
|
|
129
|
+
// Update status bar with current tool and key arguments for user visibility
|
|
130
|
+
if (name === "bash" && args.command) {
|
|
131
|
+
const cmd = args.command.replace(/\s+/g, " ").trim()
|
|
132
|
+
state.status = `Running: ${cmd.length > 50 ? cmd.slice(0, 50) + "…" : cmd}`
|
|
133
|
+
} else if ((name === "read" || name === "write" || name === "edit" || name === "grep" || name === "glob") && args.path) {
|
|
134
|
+
state.status = `${name}: ${args.path}`
|
|
135
|
+
} else if (name === "grep" && args.pattern) {
|
|
136
|
+
state.status = `grep: ${args.pattern}`
|
|
137
|
+
} else if (name === "glob" && args.pattern) {
|
|
138
|
+
state.status = `glob: ${args.pattern}`
|
|
139
|
+
} else if (name === "websearch" && args.query) {
|
|
140
|
+
state.status = `search: ${args.query.length > 40 ? args.query.slice(0, 40) + "…" : args.query}`
|
|
141
|
+
} else if (name === "advisor") {
|
|
142
|
+
state.status = `advisor review (round ${(agent._advisorRound || 0) + 1})`
|
|
143
|
+
} else {
|
|
144
|
+
state.status = `tool: ${name}`
|
|
145
|
+
}
|
|
108
146
|
// Advisor: tag the round in the tool title — the model's own "第N轮" narration
|
|
109
147
|
// is unreliable (it glues onto the previous line), so the round belongs here.
|
|
110
148
|
const roundTag = name === "advisor" ? ` (round ${(agent._advisorRound || 0) + 1})` : ""
|
|
111
149
|
const argSummary = summarize(args)
|
|
112
|
-
|
|
150
|
+
// Inline block title — panel tools get both the title AND the
|
|
151
|
+
// streaming output panel, complementary display.
|
|
152
|
+
const color = ({ advisor: C.advisor, bash: C.warn, verify: C.tool }[name] ?? C.text)
|
|
153
|
+
pushLine(`❯ ${name}${roundTag}${argSummary ? ` ${argSummary}` : ""}`, color)
|
|
154
|
+
_toolTicks[name] = performance.now()
|
|
113
155
|
},
|
|
114
156
|
onToolResult: (name, result) => {
|
|
115
157
|
state.currentTool = null
|
|
@@ -137,83 +179,74 @@ export async function runAgentTurn(ctx, text) {
|
|
|
137
179
|
if (state.processing) render()
|
|
138
180
|
}, 3000)
|
|
139
181
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Keep the panel visible for a 3s grace period (layout filters by closeAt);
|
|
145
|
-
// the render loop prunes it once expired. No defer hacks needed — row-diff
|
|
146
|
-
// repaints whatever should be on screen.
|
|
147
|
-
panel.done = true
|
|
148
|
-
panel.closeAt = Date.now() + 3000
|
|
149
|
-
scheduleRender()
|
|
150
|
-
if (name === "advisor") {
|
|
151
|
-
const text = String(result ?? "")
|
|
152
|
-
const lines = text.split("\n")
|
|
153
|
-
const maxShow = Math.min(60, lines.length)
|
|
154
|
-
// Push as single multiline block so formatTables aligns MD table columns
|
|
155
|
-
const shown = lines.slice(0, maxShow).map((l) => ` ${l.slice(0, 200)}`).join("\n")
|
|
156
|
-
pushLine(shown, C.advisor)
|
|
157
|
-
if (lines.length > maxShow) pushLine(` ... (${lines.length - maxShow} more lines — call advisor again or scroll through the tool result for full output)`, C.dim)
|
|
158
|
-
} else {
|
|
159
|
-
const summary = formatPanelSummary(name, result)
|
|
160
|
-
if (summary) pushLine(` ${summary}`, C.dim)
|
|
182
|
+
if (!isSubagent && name !== "advisor") {
|
|
183
|
+
// Remove live streaming lines — done line handles the summary.
|
|
184
|
+
for (let i = state.lines.length - 1; i >= 0; i--) {
|
|
185
|
+
if (state.lines[i]._live === name) state.lines.splice(i, 1)
|
|
161
186
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} else if (stream) {
|
|
165
|
-
const tail = stream.trimEnd().slice(-4000)
|
|
166
|
-
if (tail) pushLine(tail, C.dim)
|
|
167
|
-
delete state.toolStreams[name]
|
|
187
|
+
const summary = formatToolSummary(name, result)
|
|
188
|
+
if (summary) pushLine(` ${summary}`, C.dim)
|
|
168
189
|
}
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
if (name === "advisor") { state.advisorStreaming = ""; state._advisorThink = "" }
|
|
191
|
+
// Done line for ALL tools (panel area abolished — inline only).
|
|
192
|
+
if (!isSubagent) {
|
|
193
|
+
const elapsed = _toolTicks[name] ? ` (${Math.round(performance.now() - _toolTicks[name])}ms)` : ""
|
|
194
|
+
const summary = formatToolSummary(name, result)
|
|
195
|
+
const tail = summary ? ` → ${sliceByWidth(summary, 60)}` : ""
|
|
196
|
+
pushLine(`❯ ${name} — done${elapsed}${tail}`, C.dim)
|
|
172
197
|
}
|
|
198
|
+
delete _toolTicks[name]
|
|
173
199
|
},
|
|
174
200
|
onToolOutput: (name, chunk) => {
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// per-kind coloring in renderOutput so reasoning / answer / tool progress are distinct.
|
|
178
|
-
let panel = state.outputPanels[name]
|
|
179
|
-
if (!panel) {
|
|
180
|
-
// Lazy-create panel: defensive against race conditions where setupOutputPanel
|
|
181
|
-
// hasn't fired yet or the callbacks chain dropped it (subagent relay, reconnect, etc.)
|
|
182
|
-
state.outputPanels[name] = { parts: [], len: 0, done: false }
|
|
183
|
-
panel = state.outputPanels[name]
|
|
184
|
-
}
|
|
201
|
+
// All tools use inline conversation blocks — panel area is abolished.
|
|
202
|
+
// Stream up to 5 preview lines; the full result is in the tool message.
|
|
185
203
|
const part = typeof chunk === "string"
|
|
186
|
-
? { kind: "text", text: chunk }
|
|
187
|
-
: { kind: chunk?.kind ?? "text", text: String(chunk?.text ?? "") }
|
|
204
|
+
? { kind: "text", text: chunk.trimEnd() }
|
|
205
|
+
: { kind: chunk?.kind ?? "text", text: String(chunk?.text ?? "").trimEnd() }
|
|
188
206
|
if (!part.text) return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
panel.len += part.text.length
|
|
197
|
-
// Cap at 4000 chars, trimming oldest parts first
|
|
198
|
-
while (panel.len > 4000 && panel.parts.length > 1) {
|
|
199
|
-
const first = panel.parts[0]
|
|
200
|
-
const excess = panel.len - 4000
|
|
201
|
-
if (first.text.length <= excess) {
|
|
202
|
-
panel.len -= first.text.length
|
|
203
|
-
panel.parts.shift()
|
|
207
|
+
if (name === "advisor") {
|
|
208
|
+
// Accumulate to buffer — formatTables + wrapText in render-conversation
|
|
209
|
+
// handles markdown formatting, same as main agent response.
|
|
210
|
+
const raw = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
|
|
211
|
+
const kind = typeof chunk === "string" ? "text" : (chunk?.kind ?? "text")
|
|
212
|
+
if (kind === "think") {
|
|
213
|
+
state._advisorThink = (state._advisorThink || "") + raw
|
|
204
214
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
state.advisorStreaming += raw
|
|
216
|
+
}
|
|
217
|
+
scheduleRender()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
// Rolling output — show latest N lines with fold marker per tool.
|
|
221
|
+
// _live marker per tool enables per-tool pruning without affecting other content.
|
|
222
|
+
const color = ({ think: C.reason, tool: C.tool }[part.kind] ?? C.dim)
|
|
223
|
+
for (const line of part.text.split("\n")) {
|
|
224
|
+
const trimmed = line.trimEnd()
|
|
225
|
+
if (!trimmed) continue
|
|
226
|
+
state.lines.push({ text: `│ ${trimmed}`, color, _live: name })
|
|
227
|
+
}
|
|
228
|
+
// Prune: keep at most N lines + "│ …" fold marker per tool (configurable, tool-specific)
|
|
229
|
+
const configLimit = agent.config?.agent?.streamPreviewLines
|
|
230
|
+
const toolLimit = LIVE_LINE_LIMITS[name]
|
|
231
|
+
const previewLines = configLimit ?? toolLimit ?? 5
|
|
232
|
+
let count = 0
|
|
233
|
+
let hasFold = false
|
|
234
|
+
for (let i = state.lines.length - 1; i >= 0; i--) {
|
|
235
|
+
if (state.lines[i]._live === name) {
|
|
236
|
+
if (++count > previewLines) {
|
|
237
|
+
if (!hasFold) {
|
|
238
|
+
state.lines[i] = { text: "│ …", color: C.dim, _live: name }
|
|
239
|
+
hasFold = true; count = previewLines
|
|
240
|
+
} else {
|
|
241
|
+
state.lines.splice(i, 1)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
207
244
|
}
|
|
208
245
|
}
|
|
209
246
|
scheduleRender()
|
|
210
247
|
},
|
|
211
248
|
onPermissionRequest: (name, args) => askPermission(name, args),
|
|
212
249
|
onQuestion: (text, options) => askQuestion(text, options),
|
|
213
|
-
setupOutputPanel: (name) => {
|
|
214
|
-
state.outputPanels[name] = { parts: [], len: 0, done: false }
|
|
215
|
-
scheduleRender()
|
|
216
|
-
},
|
|
217
250
|
onCompress: () => {
|
|
218
251
|
pushLine(" [context] Context too long, auto-compacted (early conversation summarized by LLM, task state preserved)", C.warn)
|
|
219
252
|
},
|
|
@@ -243,6 +276,17 @@ export async function runAgentTurn(ctx, text) {
|
|
|
243
276
|
onTurnEnd: (() => {
|
|
244
277
|
let n = 0
|
|
245
278
|
return () => {
|
|
279
|
+
// Flush pending reasoning/streaming before the next turn starts.
|
|
280
|
+
// Guard pushbacks (verify/advisor) continue the agent loop without
|
|
281
|
+
// returning to the TUI — without flushing, old thinking bleeds into
|
|
282
|
+
// the next turn and the guard reminder is invisible.
|
|
283
|
+
flushStream()
|
|
284
|
+
// Mirror the last system-reminder from agent.history so guard
|
|
285
|
+
// pushback messages appear in the conversation at the right spot.
|
|
286
|
+
const last = agent.history.at(-1)
|
|
287
|
+
if (last?.role === "user" && typeof last.content === "string" && last.content.startsWith("[System reminder:")) {
|
|
288
|
+
pushLine(last.content, C.warn)
|
|
289
|
+
}
|
|
246
290
|
if (++n % 5 !== 0) return
|
|
247
291
|
try { saveSessionImpl(agent, state.lines) } catch (e) { console.error(`[session] incremental save failed: ${e.message}`) }
|
|
248
292
|
}
|
|
@@ -303,6 +347,8 @@ export async function runAgentTurn(ctx, text) {
|
|
|
303
347
|
clearInterval(ticker)
|
|
304
348
|
state.processing = false
|
|
305
349
|
state.subTasks = {}
|
|
350
|
+
state.advisorStreaming = ""
|
|
351
|
+
state._advisorThink = ""
|
|
306
352
|
state.controller = null
|
|
307
353
|
state.status = "Ready"
|
|
308
354
|
// Auto-collapse todo panel when all tasks done (matching kimi-code TUI; agent.tasks are preserved)
|
|
@@ -333,15 +379,55 @@ export async function runAgentTurn(ctx, text) {
|
|
|
333
379
|
}
|
|
334
380
|
}
|
|
335
381
|
|
|
336
|
-
/** Extract a one-line summary from
|
|
337
|
-
function
|
|
382
|
+
/** Extract a one-line summary from tool output for the done line */
|
|
383
|
+
function formatToolSummary(name, result) {
|
|
338
384
|
if (name === "verify") return _verifySummary(result)
|
|
339
385
|
if (name === "bash") return _bashSummary(result)
|
|
386
|
+
if (name === "advisor") return _advisorSummary(result)
|
|
387
|
+
if (name === "read" || name === "read_file") return _readSummary(result)
|
|
388
|
+
if (name === "write" || name === "write_file") return _writeSummary(result)
|
|
389
|
+
if (name === "grep" || name === "search") return _grepSummary(result)
|
|
390
|
+
if (name === "glob") return _globSummary(result)
|
|
340
391
|
// Default: first non-empty line
|
|
341
392
|
const first = result.split("\n").find((l) => l.trim())
|
|
342
393
|
return first ? `${name}: ${first.slice(0, 100)}` : null
|
|
343
394
|
}
|
|
344
395
|
|
|
396
|
+
function _readSummary(result) {
|
|
397
|
+
const lines = result.split("\n")
|
|
398
|
+
// Look for line count in result
|
|
399
|
+
const countMatch = result.match(/(\d+) lines?/)
|
|
400
|
+
if (countMatch) return `${countMatch[1]} lines`
|
|
401
|
+
// Fallback: count actual lines
|
|
402
|
+
return `${lines.length} lines`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function _writeSummary(result) {
|
|
406
|
+
// Extract file size or confirmation
|
|
407
|
+
if (result.includes("wrote") || result.includes("created")) {
|
|
408
|
+
const sizeMatch = result.match(/(\d+)(?:\s*(?:bytes?|chars?))/i)
|
|
409
|
+
return sizeMatch ? `wrote ${sizeMatch[1]} bytes` : "wrote file"
|
|
410
|
+
}
|
|
411
|
+
const first = result.split("\n").find((l) => l.trim())
|
|
412
|
+
return first ? first.slice(0, 80) : "wrote"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function _grepSummary(result) {
|
|
416
|
+
const lines = result.split("\n").filter((l) => l.trim())
|
|
417
|
+
const count = lines.length
|
|
418
|
+
if (count === 0) return "no matches"
|
|
419
|
+
if (count === 1) return "1 match"
|
|
420
|
+
return `${count} matches`
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function _globSummary(result) {
|
|
424
|
+
const lines = result.split("\n").filter((l) => l.trim())
|
|
425
|
+
const count = lines.length
|
|
426
|
+
if (count === 0) return "no files"
|
|
427
|
+
if (count === 1) return "1 file"
|
|
428
|
+
return `${count} files`
|
|
429
|
+
}
|
|
430
|
+
|
|
345
431
|
/**
|
|
346
432
|
* bash result format: "[stdout]:\n<out>\n\n[stderr]:\n<err>\n\n(exit code 0)".
|
|
347
433
|
* The first non-empty line is always the "[stdout]:" marker — useless as a summary.
|
|
@@ -357,6 +443,23 @@ function _bashSummary(result) {
|
|
|
357
443
|
return parts.length > 0 ? `bash: ${parts.join(" ")}` : null
|
|
358
444
|
}
|
|
359
445
|
|
|
446
|
+
function _advisorSummary(result) {
|
|
447
|
+
const text = String(result ?? "")
|
|
448
|
+
if (/no 🔴|all.*(?:resolved|fixed|pass)/im.test(text)) return "advisor: passed"
|
|
449
|
+
// Error / skip messages — extract the reason after "Advisor:"
|
|
450
|
+
const errMatch = text.trimStart().match(/^Advisor:\s*(.+)/)
|
|
451
|
+
if (errMatch) return `advisor: ${errMatch[1].split(".")[0]}`
|
|
452
|
+
const critical = (text.match(/\| \d+ \|.*\| 🔴/g) || []).length
|
|
453
|
+
const advisory = (text.match(/\| \d+ \|.*\| 🟡/g) || []).length
|
|
454
|
+
const style = (text.match(/\| \d+ \|.*\| 🔵/g) || []).length
|
|
455
|
+
const parts = []
|
|
456
|
+
if (critical) parts.push(`${critical} critical`)
|
|
457
|
+
if (advisory) parts.push(`${advisory} advisory`)
|
|
458
|
+
if (style) parts.push(`${style} style`)
|
|
459
|
+
if (parts.length === 0) return null
|
|
460
|
+
return `advisor: ${parts.join(", ")}`
|
|
461
|
+
}
|
|
462
|
+
|
|
360
463
|
function _verifySummary(result) {
|
|
361
464
|
const lines = result.split("\n")
|
|
362
465
|
const summary = []
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -38,6 +38,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
38
38
|
const cfg = loadConfig()
|
|
39
39
|
injectProxy(cfg.providersList, cfg)
|
|
40
40
|
const runtimeName = agent.activeProvider
|
|
41
|
+
const runtimeModel = agent.activeModel
|
|
41
42
|
agent.providers = cfg.providersList
|
|
42
43
|
agent.config = cfg
|
|
43
44
|
agent.config.agent ??= {}
|
|
@@ -45,9 +46,20 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
45
46
|
if (runtimeName && runtimeName !== cfg.activeProvider && keep) {
|
|
46
47
|
// 运行时选择在新配置里仍存在 → 保持(provider 为注入 proxyUri 后的新对象)
|
|
47
48
|
agent.activeProvider = runtimeName
|
|
49
|
+
agent.activeModel = runtimeModel
|
|
48
50
|
agent.provider = { ...keep }
|
|
51
|
+
if (agent.activeModel) agent.provider.model = agent.activeModel
|
|
52
|
+
} else if (runtimeName && runtimeName === cfg.activeProvider && runtimeModel) {
|
|
53
|
+
// Same provider, runtime had a model override — keep it
|
|
54
|
+
agent.activeProvider = cfg.activeProvider
|
|
55
|
+
agent.activeModel = runtimeModel
|
|
56
|
+
const p = cfg.providersList.find((pr) => pr.name === cfg.activeProvider)
|
|
57
|
+
agent.provider = p ? { ...p } : cfg.provider
|
|
58
|
+
agent.provider.model = runtimeModel
|
|
59
|
+
agent.provider.proxyUri = p?.proxyUri
|
|
49
60
|
} else {
|
|
50
61
|
agent.activeProvider = cfg.activeProvider
|
|
62
|
+
agent.activeModel = cfg.activeModel ?? null
|
|
51
63
|
agent.provider = cfg.provider
|
|
52
64
|
agent.provider.proxyUri = cfg.providersList.find((p) => p.name === cfg.activeProvider)?.proxyUri
|
|
53
65
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** /eng command: toggle engineering mode.
|
|
2
|
+
* Requires METHODOLOGY.md in project root. Offers to create one if missing.
|
|
3
|
+
* ctx: { agent, pushLine, pushLabel, persistRaw, showPicker } */
|
|
4
|
+
import { existsSync, copyFileSync } from "node:fs"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
import { ansi, C } from "./ansi.mjs"
|
|
8
|
+
|
|
9
|
+
const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
|
|
10
|
+
|
|
11
|
+
export async function handleEngCommand(ctx) {
|
|
12
|
+
const { agent, pushLine, pushLabel, persistRaw, showPicker } = ctx
|
|
13
|
+
agent.config.agent ??= {}
|
|
14
|
+
const methodologyPath = join(agent.cwd, "METHODOLOGY.md")
|
|
15
|
+
|
|
16
|
+
// Toggle on: check METHODOLOGY.md exists
|
|
17
|
+
if (!agent.config.agent.engineering) {
|
|
18
|
+
if (!existsSync(methodologyPath)) {
|
|
19
|
+
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
20
|
+
pushLine("METHODOLOGY.md not found in project root.", C.warn)
|
|
21
|
+
const choice = await showPicker("Create METHODOLOGY.md?", [
|
|
22
|
+
{ type: "header", text: "Engineering mode requires a methodology file" },
|
|
23
|
+
{ type: "item", text: "Yes, create from template", action: "create" },
|
|
24
|
+
{ type: "item", text: "No, cancel", action: "cancel" },
|
|
25
|
+
])
|
|
26
|
+
if (!choice || choice.action !== "create") return
|
|
27
|
+
const src = join(templateDir, "methodology-template.md")
|
|
28
|
+
copyFileSync(src, methodologyPath)
|
|
29
|
+
pushLine(`Created METHODOLOGY.md (from template) → edit it to fit your project`, C.tool)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
agent.config.agent.engineering = !agent.config.agent.engineering
|
|
34
|
+
if (!agent.config.agent.engineering) agent._engDesignToken = null // invalidate stale token
|
|
35
|
+
await persistRaw((raw) => {
|
|
36
|
+
raw.agent ??= {}
|
|
37
|
+
raw.agent.engineering = agent.config.agent.engineering
|
|
38
|
+
})
|
|
39
|
+
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
40
|
+
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"}`, C.tool)
|
|
41
|
+
if (agent.config.agent.engineering) {
|
|
42
|
+
pushLine(` → strictly following ${methodologyPath}`, C.dim)
|
|
43
|
+
}
|
|
44
|
+
}
|