thincoder 0.12.33 → 0.12.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.33",
3
+ "version": "0.12.34",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
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
@@ -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
  }
@@ -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,