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 +1 -1
- package/src/session.mjs +22 -0
- package/src/tools/question.md +1 -1
- package/src/tui/cmd-config.mjs +11 -4
- package/src/tui/cmd-session.mjs +25 -2
- package/src/tui/layout.mjs +8 -1
- package/src/tui/slash-commands.mjs +3 -1
package/package.json
CHANGED
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
|
|
package/src/tools/question.md
CHANGED
|
@@ -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
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -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
|
|
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
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -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"
|
package/src/tui/layout.mjs
CHANGED
|
@@ -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,
|