thincoder 0.12.33 → 0.12.35

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
@@ -15,7 +15,7 @@ Design philosophy (the entire meaning of the name): if the Node standard library
15
15
  - **Fix-verify loop**: file changes without `verify` get pushed back — syntax check + tests must pass before the agent can claim completion (auto-repair up to 3 rounds)
16
16
  - **Checkpoint system**: auto-snapshot before every user task, `list`/`create`/`rewind` tools for the model, single-file restore — rewinding itself is reversible (pre-rewind state auto-saved)
17
17
  - **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools guided by "structure → intent → details"
18
- - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from seventeen providers — DeepSeek / Kimi / Kimi For Coding / GLM / Qwen / Qwen Token Plan / MiniMax / OpenAI / Claude / Gemini / Grok / Mistral / Volcengine Ark (豆包) / Hunyuan (腾讯混元) / SiliconFlow (硅基流动) / OpenRouter / Groq. No legacy model compatibility, no local model support. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all deeply adapted.
18
+ - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from seventeen providers — DeepSeek / Kimi / Kimi For Coding / GLM / Qwen / Qwen Token Plan / MiniMax / OpenAI / Claude / Gemini / Grok / Mistral / Volcengine Ark (豆包) / Hunyuan (腾讯混元) / SiliconFlow (硅基流动) / OpenRouter / Groq. No legacy model compatibility. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all deeply adapted.
19
19
  - **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
20
20
  - **Memory system**: three layers (personal/project/team), FTS5 + vector RRF hybrid retrieval, git-friendly markdown format
21
21
  - **Two-phase tool scheduling**: permission prompts serialized, read-only tools parallelized, side-effect tools serialized
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.33",
3
+ "version": "0.12.35",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -361,13 +361,11 @@ function extractUnfixedIssues(priorText) {
361
361
  export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null) {
362
362
  const onOutput = callbacks?.onOutput
363
363
  const signal = callbacks?.signal
364
- const cfg = agent.config?.advisor
365
364
  const startTime = Date.now()
366
-
367
- // Engineering mode overrides advisor toggle reviews are mandatory regardless
368
- if (!cfg?.enabled && !agent.config?.agent?.engineering) {
369
- return "Advisor: not enabled (set advisor.enabled in config.json)."
370
- }
365
+
366
+ // Advisor reviews are ALWAYS available (2026-08-21 semantic refactor): the
367
+ // former advisor.enabled gate is removed — review capability has no off
368
+ // switch; only the guard (completion pushback) is opt-in via advisor.guard.
371
369
 
372
370
  // Mechanical convergence cap — refuse further reviews once the protocol has run
373
371
  // its rounds. _advisorRound counts completed advisor calls (incremented by the
@@ -111,10 +111,11 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
111
111
  }
112
112
 
113
113
  // --- advisor guard: review of mutated files before completion ---
114
- // Active by default when advisor.enabled is set (opt-out via guard: false),
115
- // and NEVER in engineering mode.
114
+ // OPT-IN ONLY (advisor.guard === true, default OFF 2026-08-21 semantic
115
+ // refactor), and NEVER in engineering mode. The advisor tool itself is always
116
+ // available; this guard only controls whether completion is pushed back.
116
117
  const cfg = agent.config?.advisor
117
- const advisorReview = cfg?.enabled && cfg?.guard !== false
118
+ const advisorReview = cfg?.guard === true
118
119
  if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
119
120
  // Cap sync: beyond MAX_ADVISOR_ROUNDS the advisor tool refuses to review
120
121
  // (run.mjs convergence cap) — pushing back further would loop forever
@@ -4,7 +4,7 @@
4
4
  import { configDir } from "../config.mjs"
5
5
  import { readFileSync, readdirSync, existsSync } from "node:fs"
6
6
  import { homedir } from "node:os"
7
- import { writeFile, mkdir } from "node:fs/promises"
7
+ import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
8
8
  import { join } from "node:path"
9
9
  import { execSync } from "node:child_process"
10
10
 
@@ -23,6 +23,9 @@ export const REPORT_CONTINUATION =
23
23
  const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
24
24
  const TOOL_RESULT_PREVIEW = 2_000
25
25
 
26
+ /** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
27
+ export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
28
+
26
29
  const GIT_TIMEOUT_MS = 5000
27
30
  const MAX_GIT_CHANGES_DISPLAY = 20
28
31
 
@@ -39,11 +42,35 @@ export function tryCanonicalize(name, args) {
39
42
  try { return name + ":" + JSON.stringify(JSON.parse(args)) } catch { return name + ":" + args }
40
43
  }
41
44
 
42
- /** Offload oversized tool results (>16k chars) to disk, returning a preview + file path */
43
- export async function offloadToolResult(text, callId) {
45
+ /**
46
+ * Best-effort write-time self-cleanup: delete files in dir whose mtime exceeds TMP_RETENTION_MS.
47
+ * Subdirectories are never touched; every failure is silent — cleanup must not affect offload.
48
+ */
49
+ export async function cleanupOldToolResults(dir) {
50
+ const now = Date.now()
51
+ let entries
52
+ try {
53
+ entries = await readdir(dir, { withFileTypes: true })
54
+ } catch {
55
+ return // dir missing or unreadable → nothing to clean
56
+ }
57
+ for (const entry of entries) {
58
+ if (!entry.isFile()) continue // subdirectories untouched
59
+ try {
60
+ const st = await stat(join(dir, entry.name))
61
+ if (now - st.mtimeMs > TMP_RETENTION_MS) await unlink(join(dir, entry.name))
62
+ } catch {
63
+ /* entry vanished concurrently or I/O error — best effort, keep going */
64
+ }
65
+ }
66
+ }
67
+
68
+ /** Offload oversized tool results (>16k chars) to disk, returning a preview + file path.
69
+ * Writes trigger write-time self-cleanup of the offload dir first (dir param overridable for tests). */
70
+ export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results")) {
44
71
  if (text.length <= TOOL_RESULT_OFFLOAD_LIMIT) return text
45
72
  try {
46
- const dir = join(configDir, "tool-results")
73
+ await cleanupOldToolResults(dir)
47
74
  await mkdir(dir, { recursive: true })
48
75
  const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
49
76
  await writeFile(file, text, "utf8")
@@ -53,7 +53,7 @@ export const subagentTool = {
53
53
  name: "subagent",
54
54
  description:
55
55
  "Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
56
- "Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
56
+ "Use role='explore' for codebase search/analysis (read-only, fast — specify thoroughness in the task: quick / medium / thorough (default medium)), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
57
57
  "Writing the prompt:\n" +
58
58
  "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
59
59
  "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
package/src/config.mjs CHANGED
@@ -52,7 +52,7 @@ const DEFAULTS = {
52
52
  consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
53
53
  consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
54
54
  streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
55
- advisor: { enabled: false }, // code review; { enabled: true, provider: "deepseek", model: "deepseek-chat", thinking: { type: "enabled" }, reasoningEffort: "max", guard: true }
55
+ advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort overrides. Deprecated: enabled (2026-08-21)
56
56
  autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
57
57
  engineering: false, // strict methodology enforcement — read METHODOLOGY.md, design-before-code
58
58
  },
@@ -19,6 +19,16 @@ subagents only.
19
19
  non-functional standards. Clarification is DONE when each layer is concrete
20
20
  enough to design against (the user confirms, or the answers stop changing
21
21
  the requirement). Do NOT start the design before this.
22
+ - **Plan confirmation before writing any doc — no exemptions.** When
23
+ clarification is DONE, and before writing the requirements doc (or the
24
+ design doc), state in plain text your understanding of the requirement
25
+ plus your next-step plan, and WAIT for the user's explicit confirmation
26
+ ("OK / 可以 / continue"-type reply) before writing. No confirmation,
27
+ silence, or a new question from the user → do not write. Even if you
28
+ are completely sure you understand, you must still write the plan out
29
+ and wait — "this is obvious enough to skip asking" is never a valid
30
+ reason. Writing docs is a writing action — it is under the same
31
+ discipline.
22
32
  2. **Design.** Write the design document in `docs/` (problem statement,
23
33
  solution approach, full affected-file list, verifiable acceptance criteria).
24
34
  Do NOT open any code file for editing before this document exists.
@@ -15,3 +15,8 @@ Guidelines:
15
15
  - Complete the search efficiently and report findings in a structured format
16
16
  - If the expected pattern doesn't exist, report that explicitly: what you searched for, which tools you used, and that nothing matched. "Probably there" is not a finding — only report what you actually saw.
17
17
  - If something is ambiguous, note it in your report; do not ask the user
18
+
19
+ **Thoroughness levels** — pick the depth the task actually needs (the parent agent may state one in the task description):
20
+ - quick — a single targeted search answering one specific question
21
+ - medium — the default: a moderate multi-pronged search, several probes in parallel
22
+ - thorough — exhaustive analysis across multiple locations and naming conventions; your report must list what you searched for and what you did NOT find
@@ -10,6 +10,7 @@ For tasks that match the Coding discipline's "complex" tier, plan mode is your d
10
10
 
11
11
  Delegate well — spawn subagents for independent subtasks.
12
12
  - Explore agents for parallel codebase search, plan agents for architecture design, coder agents for self-contained implementation.
13
+ - When delegating an explore agent, state the thoroughness in the task description — quick / medium / thorough — graded by need; unspecified means the default.
13
14
  - Delegate breadth-first exploration; do precision edits yourself.
14
15
  - Never give parallel subagents tasks that edit the same files — conflicts waste everyone's time.
15
16
  - When a coder subagent finishes, verify its report: read the files it claims to have changed, run the tests — do not trust subagent reports blindly.
@@ -10,7 +10,9 @@ Programming is collaborative labor between you and the human. The human decides
10
10
  - **Read design docs first.** Use `doc_search` to find relevant design docs, AGENTS.md, and architecture decisions. Code without design context is guesswork. If docs conflict with code, docs are right. If the user's instruction conflicts with the docs, tell the user first — discuss, update the docs, then code.
11
11
  - **Check existing code.** Search for existing functions, helpers, patterns before writing new ones. Duplicates are technical debt.
12
12
  - **Understand intent.** Ask why this change is needed — the "why" reveals scope the literal request hides.
13
- - **Confirm understanding.** State what you believe the user asked for and what you plan to deliver. Wait for confirmation. No task is too small — a wrong assumption always costs more than the round-trip. Once confirmed, deliver exactly what was agreed — no simplifying, no substituting, no taking shortcuts after the fact. Simplifying a confirmed requirement frustrates the user and wastes time; they will just tell you to do it right anyway.
13
+ - **Confirm understanding.** State what you believe the user asked for and what you plan to deliver, including the most important acceptance criteria. Wait for confirmation. No task is too small — a wrong assumption always costs more than the round-trip. Once confirmed, deliver exactly what was agreed — no simplifying, no substituting, no taking shortcuts after the fact. Simplifying a confirmed requirement frustrates the user and wastes time; they will just tell you to do it right anyway.
14
+ - **Confirm before any file-writing action — no exemptions.** Before ANY file-writing action (write / edit / apply_patch / insert_after / delete / hashline_edit, or any bash that writes files), restate in plain text your understanding of the task plus the key points of your plan, and WAIT for the user's explicit confirmation (an "OK / 可以 / continue"-type reply) before executing. No confirmation, silence, or the user answering with a new question or a new requirement → do not touch anything, no matter how small or obvious the change seems. Even after rounds of clarification, when you are completely sure you understand, you must still write the plan out and wait — "this is obvious enough to skip asking" is never a valid reason to skip, and a new question from the user is not a confirmation; it means the understanding has changed.
15
+ - **Re-confirm when the requirement changes.** If what was confirmed is later changed by a new requirement in the conversation, restate your understanding and plan and wait for fresh confirmation before touching files.
14
16
 
15
17
  **How you work — while coding:**
16
18
  - When you need multiple independent pieces of information, call tools in parallel — read files, search, grep all at once.
package/src/session.mjs CHANGED
@@ -297,6 +297,28 @@ export function deleteSlot(cwd, slot) {
297
297
  return true
298
298
  }
299
299
 
300
+ /** Rename a slot: update the slot file's title + the manifest metadata (shared with VS Code). */
301
+ export function renameSlot(cwd, slot, title) {
302
+ const n = Number(slot)
303
+ if (!Number.isInteger(n) || n < 1) return false
304
+ const p = slotPath(cwd, n)
305
+ if (!existsSync(p)) return false
306
+ let data
307
+ try {
308
+ data = JSON.parse(readFileSync(p, "utf8"))
309
+ } catch {
310
+ return false
311
+ }
312
+ data.title = title
313
+ writeSessionFile(p, data)
314
+ const m = loadManifest(cwd)
315
+ if (m.slots[n]) {
316
+ m.slots[n] = slotDigest(data)
317
+ saveManifest(cwd, m)
318
+ }
319
+ return true
320
+ }
321
+
300
322
 
301
323
  // ========== legacy transient prefix cleanup ==========
302
324
 
@@ -2,7 +2,7 @@ Ask the user a question and wait for their response. Use when the task is ambigu
2
2
 
3
3
  Parameters:
4
4
  - question (required): The question to ask the user
5
- - options: Array of single-choice options for the user to pick from (optional)
5
+ - options: Array of single-choice options for the user to pick from (optional). MUST be plain strings, e.g. ["A", "B", "C"] — never objects.
6
6
 
7
7
  Notes:
8
8
  - The agent loop pauses until the user answers
@@ -94,7 +94,7 @@ export async function runAgentTurn(ctx, text) {
94
94
  const callbacks = {
95
95
  onToken: (t) => {
96
96
  // Subagent streaming: prefix format role#id/ → extract id, update subTask streaming text
97
- const subMatch = t.match(/^(\w+)#(\d+)\//)
97
+ const subMatch = t.match(/^([\w-]+)#(\d+)\//)
98
98
  if (subMatch) {
99
99
  const key = `${subMatch[1]}#${subMatch[2]}`
100
100
  const payload = t.slice(subMatch[0].length)
@@ -114,7 +114,7 @@ export async function runAgentTurn(ctx, text) {
114
114
  },
115
115
  onReasoning: (t) => {
116
116
  // Subagent reasoning tokens also carry role#id/ prefix, go into subTasks panel
117
- const subMatch = t.match(/^(\w+)#(\d+)\//)
117
+ const subMatch = t.match(/^([\w-]+)#(\d+)\//)
118
118
  if (subMatch) {
119
119
  const key = `${subMatch[1]}#${subMatch[2]}`
120
120
  if (!state.subTasks[key]) {
@@ -129,7 +129,7 @@ export async function runAgentTurn(ctx, text) {
129
129
  },
130
130
  onToolCall: (name, args) => {
131
131
  // Subagent tool call: prefix role#id/toolName → update subTask current tool
132
- const subMatch = name.match(/^(\w+)#(\d+)\//)
132
+ const subMatch = name.match(/^([\w-]+)#(\d+)\//)
133
133
  if (subMatch) {
134
134
  const key = `${subMatch[1]}#${subMatch[2]}`
135
135
  const toolName = name.slice(subMatch[0].length)
@@ -1,4 +1,4 @@
1
- /** /advisor command: toggle advisor on/off, select model, configure thinking.
1
+ /** /advisor command: configure review model/thinking and toggle the review guard.
2
2
  * Interactive loop UX — stays in menu after each action, Esc to exit.
3
3
  * ctx: { agent, showPicker, pushLine, pushLabel, persistRaw } */
4
4
  import { ansi, C } from "./ansi.mjs"
@@ -6,6 +6,9 @@ import { ansi, C } from "./ansi.mjs"
6
6
  export async function handleAdvisorCommand(ctx) {
7
7
  const { agent, showPicker, pushLine, pushLabel } = ctx
8
8
  const cfg = agent.config.advisor ??= {}
9
+ // Deprecated field cleanup (2026-08-21): advisor.enabled is no longer read
10
+ // anywhere; drop it here so persist() never re-writes the stale flag.
11
+ delete cfg.enabled
9
12
 
10
13
  const persist = async () => {
11
14
  if (ctx.persistRaw) {
@@ -21,13 +24,12 @@ export async function handleAdvisorCommand(ctx) {
21
24
 
22
25
  // ── State helpers ──
23
26
  function advisorStatus() {
24
- const enabled = cfg.enabled === true
25
27
  const curModel = cfg.model || agent.provider.model
26
28
  const thinkInfo = cfg.thinking === null ? "off"
27
29
  : cfg.thinking?.type === "disabled" ? "off"
28
30
  : cfg.reasoningEffort ? `on (${cfg.reasoningEffort})`
29
31
  : cfg.thinking ? `on (${cfg.thinking.type})` : "(main)"
30
- return `Advisor: ${enabled ? "ON" : "OFF"} | Model: ${curModel} | Think: ${thinkInfo}`
32
+ return `Advisor: always available | Model: ${curModel} | Think: ${thinkInfo}`
31
33
  }
32
34
 
33
35
  function headerLine() {
@@ -105,14 +107,12 @@ export async function handleAdvisorCommand(ctx) {
105
107
  // ── Main loop ──
106
108
  let mainIdx = 0
107
109
  for (;;) {
108
- const enabled = cfg.enabled === true
109
110
  const curProvider = cfg.provider || "(main)"
110
111
  const curModel = cfg.model || agent.provider.model
111
112
  const guardInfo = cfg.guard === true ? "on" : "off"
112
113
 
113
114
  const entries = [
114
115
  { type: "header", text: headerLine() },
115
- { type: "item", text: `Advisor: ${enabled ? "ON" : "OFF"}`, action: "toggle" },
116
116
  { type: "item", text: `Model: ${curModel}`, action: "model", note: `Provider: ${curProvider}` },
117
117
  { type: "item", text: `Thinking: ${advisorStatus().split("|")[2]?.trim() || "(main)"}`, action: "thinking" },
118
118
  { type: "item", text: `Guard: ${guardInfo}`, action: "guard" },
@@ -125,21 +125,13 @@ export async function handleAdvisorCommand(ctx) {
125
125
 
126
126
  if (choice.action === "view") {
127
127
  pushLabel("❯ Advisor", ansi.bold + C.tool)
128
- pushLine(`Status: ${enabled ? "ON" : "OFF"}`, C.dim)
128
+ pushLine(`Status: always available`, C.dim)
129
129
  pushLine(`Model: ${curModel} (provider: ${curProvider})`, C.dim)
130
130
  pushLine(`Guard: ${guardInfo}`, C.dim)
131
131
  pushLine(`Thinking: ${advisorStatus().split("|")[2]?.trim() || "(main)"}`, C.dim)
132
132
  continue
133
133
  }
134
134
 
135
- if (choice.action === "toggle") {
136
- cfg.enabled = !cfg.enabled
137
- await persist().catch(err => pushLine(`[error] Advisor toggle: ${err.message}`, C.error))
138
- pushLabel("❯ Advisor", ansi.bold + C.tool)
139
- pushLine(`Advisor: ${cfg.enabled ? "ON" : "OFF"}`, C.tool)
140
- continue
141
- }
142
-
143
135
  if (choice.action === "guard") {
144
136
  cfg.guard = !(cfg.guard === true)
145
137
  await persist().catch(err => pushLine(`[error] ${err.message}`, C.error))
@@ -143,8 +143,15 @@ export async function handleConfigCommand(ctx, args = []) {
143
143
  if (sub) { pushLine("Usage: /config [embedkey]", C.error); return }
144
144
 
145
145
  /** 会诊/飞刀候选池子菜单:列出 / 添加 / 编辑 effort / 删除 consultModels 条目。 */
146
- async function pickEffort(current) {
147
- const levels = ["none", "min", "low", "medium", "high", "max"]
146
+ async function pickEffort(current, model) {
147
+ const { specForModel } = await import("../config.mjs")
148
+ const enumList = model ? specForModel(model).reasoningEffortEnum : null
149
+ // The model's reasoning-effort enum is HETEROGENEOUS across providers (deepseek:
150
+ // low/high/max; qwen3.8-max: xhigh/medium/low; kimi: 7 levels). A fixed
151
+ // min/low/medium/high/max list made the user pick values that the runtime then
152
+ // silently dropped as out-of-enum (2026-08-17 audit). Show the model's real enum.
153
+ if (!enumList || enumList.length === 0) return null // model has no effort — skip
154
+ const levels = ["none", ...enumList] // "none" = clear the effort
148
155
  const entries = levels.map((l) => ({ type: "item", text: l === current ? `${l} ← current` : l, action: l }))
149
156
  const c = await showPicker("Reasoning effort", entries, { defaultIndex: Math.max(0, levels.indexOf(current ?? "none")) })
150
157
  return c ? c.action : null // Esc → null (keep unchanged)
@@ -167,7 +174,7 @@ export async function handleConfigCommand(ctx, args = []) {
167
174
  // pickModelForSlot reuses /model's provider list + async-fetched model list.
168
175
  const picked = await pickModelForSlot()
169
176
  if (!picked) continue
170
- const effort = await pickEffort(null)
177
+ const effort = await pickEffort(null, picked.model)
171
178
  const entry = { provider: picked.provider, model: picked.model }
172
179
  if (effort && effort !== "none") entry.effort = effort
173
180
  const next = [...cm, entry]
@@ -192,7 +199,7 @@ export async function handleConfigCommand(ctx, args = []) {
192
199
  pushLabel("❯ Config", ansi.bold + C.tool)
193
200
  pushLine(`Removed ${tag}`, C.tool)
194
201
  } else if (s.action === "effort") {
195
- const effort = await pickEffort(m.effort)
202
+ const effort = await pickEffort(m.effort, m.model)
196
203
  if (effort === null) { continue } // Esc 保持
197
204
  const next = cm.map((x, i) => {
198
205
  if (i !== c.index) return x
@@ -1,7 +1,30 @@
1
- import { listSlots, switchToSlot, applySession } from "../session.mjs"
1
+ import { listSlots, switchToSlot, applySession, renameSlot, activeSlot } from "../session.mjs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
  import { restoreLines } from "./startup.mjs"
4
4
 
5
+ /** /rename <title> — rename the ACTIVE session (slot file + manifest, shared with VS Code). */
6
+ export async function handleRenameCommand(ctx, args) {
7
+ const { agent, pushLine, pushLabel, render } = ctx
8
+ const slot = activeSlot(agent.cwd)
9
+ const current = agent.title || "(untitled)"
10
+ const title = args.join(" ").trim()
11
+ if (!title) {
12
+ pushLine(`Usage: /rename <new title> (current: ${current})`, C.warn)
13
+ return
14
+ }
15
+ if (title.length > 80) {
16
+ pushLine(`Title too long (max 80 chars)`, C.error)
17
+ return
18
+ }
19
+ if (!renameSlot(agent.cwd, slot, title)) {
20
+ pushLine(`Rename failed — active session (slot ${slot}) not found`, C.error)
21
+ return
22
+ }
23
+ agent.title = title
24
+ pushLabel(`── Session renamed: "${title}" ──`, C.warn)
25
+ render()
26
+ }
27
+
5
28
  /** /session command: list/switch session slots.
6
29
  * ctx: { agent, state, showPicker, pushLine, pushLabel, render } */
7
30
  export async function handleSessionCommand(ctx) {
@@ -17,7 +40,7 @@ export async function handleSessionCommand(ctx) {
17
40
  }
18
41
  const truncate = (s, n) => s.length <= n ? s : s.slice(0, n - 1) + "…"
19
42
  const entries = [
20
- { type: "header", text: `Sessions (● = active, ↑↓ select, Enter switch, Esc cancel)` },
43
+ { type: "header", text: `Sessions (● = active, ↑↓ select, Enter switch, Esc cancel; /rename <title> renames the active one)` },
21
44
  ...slots.map((s) => {
22
45
  const label = s.title || (s.firstMessage ? `"${truncate(s.firstMessage, 40)}"` : "(empty)")
23
46
  const turns = s.turnCount > 0 ? `${s.turnCount} turns` : "0 turns"
@@ -10,6 +10,13 @@
10
10
  import { layoutInput, wrapText } from "./render.mjs"
11
11
  import { QUESTION_CUSTOM } from "./interaction.mjs"
12
12
 
13
+ /** 防御:question options 声明为 string[],但 LLM 可能误传对象;取 label/text/title 兜底,避免渲染 "[object Object]"。 */
14
+ function optText(opt) {
15
+ if (typeof opt === "string") return opt
16
+ return opt?.label ?? opt?.text ?? opt?.title ?? String(opt)
17
+ }
18
+
19
+
13
20
  const MAX_INPUT_LINES = 5
14
21
  const MAX_TASK_LINES = 5
15
22
  export const MAX_SUB_LINES = 4
@@ -38,7 +45,7 @@ export function computeLayout(state, { cols, rows }) {
38
45
  const sel = q.selected ?? 0
39
46
  const start = Math.max(0, Math.min(sel - 2, q.options.length - QWIN))
40
47
  boxLines = q.options.slice(start, start + QWIN).map((opt, i) =>
41
- (start + i === sel ? "▸ " : " ") + (opt === QUESTION_CUSTOM ? "✍ Custom answer…" : opt))
48
+ (start + i === sel ? "▸ " : " ") + (opt === QUESTION_CUSTOM ? "✍ Custom answer…" : optText(opt)))
42
49
  } else {
43
50
  boxLines = ["▸ " + (q.answer ?? "")]
44
51
  }
@@ -268,9 +268,9 @@ export function renderStatus(state, agent, cols, slashCommands) {
268
268
  const statusLine = buildStatusLine(state, agent, { cols, slashCommands })
269
269
  const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
270
270
  const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
271
- const advisorBanner = agent.config?.advisor?.enabled ? `${C.advisor} ADVISOR${ansi.reset}${ansi.dim}│` : ""
271
+ const advisorBanner = agent.config?.advisor?.guard === true ? `${C.advisor} GUARD${ansi.reset}${ansi.dim}│` : ""
272
272
  const engBanner = agent.config?.agent?.engineering ? `${C.advisor} ENG${ansi.reset}${ansi.dim}│` : ""
273
- const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.enabled ? " ADVISOR│ " : "") + (agent.config?.agent?.engineering ? " ENG│ " : "")
273
+ const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.guard === true ? " GUARD│ " : "") + (agent.config?.agent?.engineering ? " ENG│ " : "")
274
274
  const statusMax = cols - 1 - (bannerPrefix ? stringWidth(bannerPrefix) : 0)
275
275
  return `${ansi.dim}${planBanner}${autoBanner}${advisorBanner}${engBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}`
276
276
  }
@@ -13,7 +13,7 @@ import { specForModel } from "../config.mjs"
13
13
  import { handleClearCommand } from "./cmd-clear.mjs"
14
14
  import { handleNewCommand } from "./cmd-new.mjs"
15
15
  import { handleExitCommand } from "./cmd-exit.mjs"
16
- import { handleSessionCommand } from "./cmd-session.mjs"
16
+ import { handleSessionCommand, handleRenameCommand } from "./cmd-session.mjs"
17
17
  import { handleReindexCommand } from "./cmd-reindex.mjs"
18
18
  import { handleInitCommand } from "./cmd-init.mjs"
19
19
  import { handleRestoreCommand } from "./cmd-restore.mjs"
@@ -49,6 +49,7 @@ export const SLASH_COMMANDS = [
49
49
  { name: "/config", group: "System", desc: "agent config (embedding, proxy, turns, threshold, consult pool)" },
50
50
  { name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
51
51
  { name: "/session", group: "Session", desc: "list/switch archived sessions" },
52
+ { name: "/rename", group: "Session", desc: "rename the active session" },
52
53
  { name: "/clear", group: "Session", desc: "clear screen" },
53
54
  { name: "/fold", group: "Session", desc: "toggle result folding on/off" },
54
55
  { name: "/undo", group: "Session", desc: "undo recent file modifications" },
@@ -69,6 +70,7 @@ export const SLASH_ALIASES = { "/h": "/help", "/x": "/exit", "/m": "/model", "/p
69
70
  export const HANDLERS = {
70
71
  "/clear": handleClearCommand,
71
72
  "/new": handleNewCommand,
73
+ "/rename": handleRenameCommand,
72
74
  "/exit": handleExitCommand,
73
75
  "/session": handleSessionCommand,
74
76
  "/reindex": handleReindexCommand,