thincoder 0.12.59 → 0.12.61
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/CHANGELOG.md +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** /eng command: toggle engineering mode.
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
2
|
+
* No prerequisites (FR11): the mode works on any project — no methodology file,
|
|
3
|
+
* no docs tree, no git. The retired gate demanded a methodology file and offered
|
|
4
|
+
* to create it from a template that no longer ships — that option crashed
|
|
5
|
+
* (PORTABILITY P14). ctx: { agent, pushLine, pushLabel } */
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
|
|
7
|
+
import { dirname } from "node:path"
|
|
7
8
|
import { ansi, C } from "./ansi.mjs"
|
|
8
9
|
import { activeSlot, slotPath } from "../session.mjs"
|
|
10
|
+
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
9
11
|
|
|
10
|
-
const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
|
|
11
12
|
import { ENG_OFF_REMINDER } from "../agent.mjs"
|
|
12
13
|
|
|
13
14
|
/** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
|
|
@@ -25,53 +26,46 @@ export function writeSessionFile(p, data) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export async function handleEngCommand(ctx) {
|
|
28
|
-
const { agent, pushLine, pushLabel
|
|
29
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
29
30
|
agent.config.agent ??= {}
|
|
30
|
-
const methodologyPath = join(agent.cwd, "METHODOLOGY.md")
|
|
31
|
-
|
|
32
|
-
// Toggle on: check METHODOLOGY.md exists
|
|
33
|
-
if (!agent.config.agent.engineering) {
|
|
34
|
-
if (!existsSync(methodologyPath)) {
|
|
35
|
-
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
36
|
-
pushLine("METHODOLOGY.md not found in project root.", C.warn)
|
|
37
|
-
const choice = await showPicker("Create METHODOLOGY.md?", [
|
|
38
|
-
{ type: "header", text: "Engineering mode requires a methodology file" },
|
|
39
|
-
{ type: "item", text: "Yes, create from template", action: "create" },
|
|
40
|
-
{ type: "item", text: "No, cancel", action: "cancel" },
|
|
41
|
-
])
|
|
42
|
-
if (!choice || choice.action !== "create") return
|
|
43
|
-
const src = join(templateDir, "methodology-template.md")
|
|
44
|
-
copyFileSync(src, methodologyPath)
|
|
45
|
-
pushLine(`Created METHODOLOGY.md (from template) → edit it to fit your project`, C.tool)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
31
|
|
|
32
|
+
// Toggle on/off — no prerequisite check (FR11: engineering mode depends on no
|
|
33
|
+
// project file at all; a missing docs tree or git repo must not block it).
|
|
49
34
|
agent.config.agent.engineering = !agent.config.agent.engineering
|
|
35
|
+
// §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
|
|
36
|
+
// on the next toggle).
|
|
37
|
+
agent._advisorRuns = new Map()
|
|
50
38
|
if (!agent.config.agent.engineering) {
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
// R16 (2026-09-06 F-R16a): OFF 不清 token——有效 token 跨模式存活(仅 TTL 过期
|
|
40
|
+
// 在三清理时机删:恢复过滤 / 开模式清过期 / spawn 门禁拒时删槽)。
|
|
53
41
|
// OFF must reach the model too (2026-08-25): /auto pushes a reminder on toggle — the
|
|
54
42
|
// mode flip is invisible to the agent otherwise. (ON needs none here: the injector
|
|
55
43
|
// in agent.mjs already announces ON transitions on the next turn.)
|
|
56
44
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
57
45
|
agent._pendingReminders.push(ENG_OFF_REMINDER)
|
|
58
46
|
}
|
|
59
|
-
|
|
47
|
+
// 开工程模式(真实 OFF→ON 转换)→ 清过期 token(有效保留——用户裁定"打开工程
|
|
48
|
+
// 模式时应该清理"——F-R16b ②——与 eng tool enter 同语义)。
|
|
49
|
+
const clearedExpired = agent.config.agent.engineering ? purgeExpiredDesignTokens(agent) : 0
|
|
50
|
+
await persistEngineering(agent)
|
|
60
51
|
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
61
52
|
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
|
|
62
53
|
if (agent.config.agent.engineering) {
|
|
63
|
-
pushLine(` →
|
|
54
|
+
pushLine(` → design-before-code enforced (design review + user approval before code)`, C.dim)
|
|
55
|
+
if (clearedExpired > 0) {
|
|
56
|
+
pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
|
|
57
|
+
}
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
/**
|
|
68
|
-
*
|
|
69
|
-
* the CURRENT session slot
|
|
70
|
-
*
|
|
62
|
+
* Slot-only persistence (2026-09-08 — ENG-SESSION-PROVIDER-CLEANUP D1.1): the flipped flag
|
|
63
|
+
* goes into the CURRENT session slot only — the slot is the sole authority (shared with
|
|
64
|
+
* VS Code, per-session; config.json is just the initial default, no mirror write).
|
|
71
65
|
* The in-memory agent.config.agent.engineering (already flipped) stays the live authority for
|
|
72
66
|
* this process; saveSession also round-trips it on every turn-end write.
|
|
73
67
|
*/
|
|
74
|
-
async function persistEngineering(
|
|
68
|
+
async function persistEngineering(agent) {
|
|
75
69
|
const slot = activeSlot(agent.cwd)
|
|
76
70
|
try {
|
|
77
71
|
const p = slotPath(agent.cwd, slot)
|
|
@@ -80,11 +74,5 @@ async function persistEngineering(ctx, agent) {
|
|
|
80
74
|
data.engineering = agent.config.agent.engineering
|
|
81
75
|
writeSessionFile(p, data)
|
|
82
76
|
}
|
|
83
|
-
} catch { /* slot missing/unreadable —
|
|
84
|
-
if (ctx.persistRaw) {
|
|
85
|
-
await ctx.persistRaw((raw) => {
|
|
86
|
-
raw.agent ??= {}
|
|
87
|
-
raw.agent.engineering = agent.config.agent.engineering
|
|
88
|
-
})
|
|
89
|
-
}
|
|
77
|
+
} catch { /* slot missing/unreadable — in-memory flag already flipped; saveSession persists at turn end */ }
|
|
90
78
|
}
|
package/src/tui/cmd-exit.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/** /exit command: exit TUI
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export async function handleExitCommand(_ctx) {
|
|
8
|
-
process.exit(0)
|
|
1
|
+
/** /exit command: exit TUI via ctx.exit(F-2——RESIZE-MOUSE-LEAK-FIX——与 Ctrl+C 同退出
|
|
2
|
+
* 形态:cleanup 同步执行 + 延迟 exit——非同派发路径)。不再直调 process.exit(0)(零提前量——
|
|
3
|
+
* 清理押 'exit' 事件);handleSlash 返回后的无条件 render() 由 render-loop tuiActive 守卫
|
|
4
|
+
* 抑制(cleanup 已清活动态——不把 TUI 帧重绘到已恢复的主屏)。 */
|
|
5
|
+
export async function handleExitCommand(ctx) {
|
|
6
|
+
ctx.exit?.() // ctx.exit 由 index.mjs 装配(cleanup + 100ms 延迟 exit)
|
|
9
7
|
}
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -82,9 +82,15 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function removeServer(name) {
|
|
85
|
+
// D-F5a(#7)先盘后存:磁盘 fresh raw.mcp.servers 上只删目标条目——不整节写回内存
|
|
86
|
+
// agent.config.mcp.servers(内存含 reloadMcpFromDisk 的 keptConnected 尾巴——
|
|
87
|
+
// 整节写回会把对端磁盘上其他 server 的新改动一起抹掉);冲突放弃不留内存 ghost
|
|
88
|
+
await persistRaw((raw) => {
|
|
89
|
+
raw.mcp ??= { servers: [] }
|
|
90
|
+
if (!Array.isArray(raw.mcp.servers)) return
|
|
91
|
+
raw.mcp.servers = raw.mcp.servers.filter((s) => s?.name !== name)
|
|
92
|
+
})
|
|
85
93
|
agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
|
|
86
|
-
// 评审 #1:磁盘无 mcp 段时(T23 场景——mcp 段被整体删除而连接保留)raw.mcp 为 undefined → 先建段再写
|
|
87
|
-
await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = agent.config.mcp.servers })
|
|
88
94
|
// Remove from tool list
|
|
89
95
|
const { removeMcpTools } = await import("../mcp.mjs")
|
|
90
96
|
removeMcpTools(agent, name)
|
package/src/tui/cmd-model.mjs
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
+
import { parseModelRef } from "../config.mjs"
|
|
2
3
|
|
|
3
|
-
/** /model command: open model picker, or switch
|
|
4
|
+
/** /model command: open the session model picker, or switch the SESSION model directly via
|
|
5
|
+
* `/model <provider>:<model>`(MODEL-MERGE-SESSION——裁定③:裸 provider 拒——显式 p:m;
|
|
6
|
+
* MODEL-SELECTION v2:显式复合一律放行——仅[空值/裸值/未知 provider]无效(M4),候选清单
|
|
7
|
+
* 由运行期拉取决定——不再有成员校验)。
|
|
8
|
+
* /model 是会话级操作(写槽——不写 config)——config 默认模型走 /config → 默认模型。
|
|
4
9
|
* ctx: { agent, openModelPicker, selectModel, pushLine } */
|
|
5
10
|
export async function handleModelCommand(ctx, args = []) {
|
|
6
|
-
const raw = args[0]?.
|
|
11
|
+
const raw = args[0]?.trim()
|
|
7
12
|
if (!raw) {
|
|
8
13
|
ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
9
14
|
return
|
|
10
15
|
}
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!target) {
|
|
18
|
-
const available = ctx.agent.providers.map((p) => p.name).join(", ")
|
|
19
|
-
ctx.pushLine(`Unknown provider: ${providerName} (available: ${available})`, C.error)
|
|
16
|
+
// 首冒号复合解析(parseModelRef——裸 provider / 未知 provider / 空模型段 → ok:false;
|
|
17
|
+
// 候选外/多冒号一律放行——M4)
|
|
18
|
+
const parsed = parseModelRef(raw, ctx.agent.providers)
|
|
19
|
+
if (!parsed.ok) {
|
|
20
|
+
ctx.pushLine(`[error] ${parsed.reason}`, C.error)
|
|
21
|
+
ctx.pushLine(`/model 用法: /model provider:model(会话级——config 默认走 /config → 默认模型)`, C.dim)
|
|
20
22
|
return
|
|
21
23
|
}
|
|
22
|
-
await ctx.selectModel({ provider:
|
|
24
|
+
await ctx.selectModel({ provider: parsed.provider.name, model: parsed.model }).catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
23
25
|
}
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -9,8 +9,9 @@ export async function handleNewCommand(ctx) {
|
|
|
9
9
|
const doNewSession = () => {
|
|
10
10
|
const slot = newSession(agent.cwd)
|
|
11
11
|
// 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
|
|
12
|
-
// _engDesignToken
|
|
13
|
-
//
|
|
12
|
+
// 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
|
|
13
|
+
// 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
|
14
|
+
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
14
15
|
resetSessionState(agent)
|
|
15
16
|
agent._slot = slot
|
|
16
17
|
state.tasks = []
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -40,5 +40,12 @@ export async function handleReindexCommand(ctx) {
|
|
|
40
40
|
])
|
|
41
41
|
pushLine(` code: ${cr.total} files, +${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
42
42
|
pushLine(` doc: ${dr.total} files, +${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
43
|
+
// Unlisted extensions (PORTABILITY PO-9): files the index skipped because no
|
|
44
|
+
// index claims their extension — visible here + declarable in the project.
|
|
45
|
+
const unlisted = cr?.unlistedExts?.count ? cr.unlistedExts : dr?.unlistedExts
|
|
46
|
+
if (unlisted?.count > 0) {
|
|
47
|
+
const sample = unlisted.exts.map((e) => e.ext).join(" ")
|
|
48
|
+
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in .thincoder/conventions.json (index.codeExtensions / index.docExtensions) to index them`, C.warn)
|
|
49
|
+
}
|
|
43
50
|
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
44
51
|
}
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy } from "../session.mjs"
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "../session.mjs"
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
import { restoreLines } from "./startup.mjs"
|
|
4
4
|
import { stringWidth, sliceByWidth } from "./render.mjs"
|
|
@@ -19,8 +19,16 @@ export async function handleRenameCommand(ctx, args) {
|
|
|
19
19
|
pushLine(`Title too long (max 80 chars)`, C.error)
|
|
20
20
|
return
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// §12.2.5:renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
|
|
23
|
+
const r = renameSlot(agent.cwd, slot, title)
|
|
24
|
+
if (!r.ok) {
|
|
25
|
+
const why = {
|
|
26
|
+
"file-missing": `active session (slot ${slot}) not found`,
|
|
27
|
+
"parse-failure": `session file (slot ${slot}) is corrupted`,
|
|
28
|
+
"mtime-conflict": "session changed on disk concurrently — retry",
|
|
29
|
+
"invalid-slot": `invalid slot ${slot}`,
|
|
30
|
+
}[r.reason] ?? r.reason
|
|
31
|
+
pushLine(`Rename failed — ${why}`, C.error)
|
|
24
32
|
return
|
|
25
33
|
}
|
|
26
34
|
agent.title = title
|
|
@@ -37,6 +45,13 @@ export async function handleSessionCommand(ctx) {
|
|
|
37
45
|
pushLine("No sessions (use /new to start a new session)", C.dim)
|
|
38
46
|
return
|
|
39
47
|
}
|
|
48
|
+
// 2026-09-05 §10 D-5:本端高亮按端记录(● = 记录槽 ∈ 列表 ? 记录槽 : manifest active
|
|
49
|
+
// 回退——含"记录槽已被对端删除"的守卫);listSlots 的 manifest active 语义不变
|
|
50
|
+
// (ACP session/list 零变化——manifest active 保留为列表回退高亮)
|
|
51
|
+
const rec = readEndMarker(agent.cwd)
|
|
52
|
+
const highlightSlot = (rec?.slot != null && slots.some((s) => s.slot === rec.slot))
|
|
53
|
+
? rec.slot
|
|
54
|
+
: (slots.find((s) => s.isActive)?.slot ?? null)
|
|
40
55
|
const shortDate = (d) => {
|
|
41
56
|
const dt = new Date(d)
|
|
42
57
|
return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
|
|
@@ -53,7 +68,7 @@ export async function handleSessionCommand(ctx) {
|
|
|
53
68
|
const turns = s.turnCount > 0 ? `${s.turnCount} turns` : "0 turns"
|
|
54
69
|
const when = shortDate(s.updatedAt)
|
|
55
70
|
const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
|
|
56
|
-
const marker = s.
|
|
71
|
+
const marker = s.slot === highlightSlot ? " ●" : ""
|
|
57
72
|
return {
|
|
58
73
|
type: "item",
|
|
59
74
|
text: `Slot ${s.slot} │ ${turns} │ ${when} │ ${label}${model}${marker}`,
|
package/src/tui/cmd-submodel.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
2
|
|
|
3
3
|
/** Subagent role slots — each has an independent model override slot */
|
|
4
|
-
export const SUBMODEL_SLOTS = ["explore", "plan", "coder", "eng-coder"]
|
|
4
|
+
export const SUBMODEL_SLOTS = ["explore", "plan", "coder", "eng-coder", "eng-designer"]
|
|
5
5
|
|
|
6
6
|
/** Human-readable effective display with inheritance source. role=null → global slot.
|
|
7
7
|
* source semantics: "type" = role-specific override (subagentModels[role]),
|
|
@@ -21,12 +21,14 @@ function slotDisplay(agent, role) {
|
|
|
21
21
|
|
|
22
22
|
/** /submodel command: subagent model config — picker menu (default) or direct args.
|
|
23
23
|
* ctx: { agent, pushLine, showPicker, askQuestion, persistRaw, pickModelForSlot }
|
|
24
|
-
* /submodel → picker: global +
|
|
24
|
+
* /submodel → picker: global + 5 role slots
|
|
25
25
|
* /submodel <value> → set global default
|
|
26
26
|
* /submodel <type> <value> → set a role slot
|
|
27
27
|
* /submodel <type> → show a slot
|
|
28
28
|
* /submodel reset [type] → clear global (or a slot)
|
|
29
|
-
* value forms: provider:model | provider name | model name (same as subagent tool model arg)
|
|
29
|
+
* value forms: provider:model | provider name | model name (same as subagent tool model arg) —
|
|
30
|
+
* MODEL-MERGE-SESSION:会话模型 = provider:model 复合(agent.activeProvider/activeModel 双字段
|
|
31
|
+
* 恒非空——无渠道默认字段——父会话模型引用一律复合值)。 */
|
|
30
32
|
export async function handleSubmodelCommand(ctx, args = []) {
|
|
31
33
|
const { agent, pushLine, showPicker, askQuestion, persistRaw, pickModelForSlot } = ctx
|
|
32
34
|
const input = args.join(" ").trim()
|
|
@@ -85,7 +87,7 @@ export async function handleSubmodelCommand(ctx, args = []) {
|
|
|
85
87
|
return
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// ── Picker menu: global +
|
|
90
|
+
// ── Picker menu: global + 5 role slots ──
|
|
89
91
|
// for(;;) loop relies on showPicker's async/Promise suspension — every iteration
|
|
90
92
|
// awaits a user choice; Esc (null) exits. Mirrors openModelPicker's menu-loop pattern.
|
|
91
93
|
for (;;) {
|
|
@@ -132,7 +134,8 @@ export async function handleSubmodelCommand(ctx, args = []) {
|
|
|
132
134
|
pushLine(`Subagent global model set to \`${value}\`.`, C.text)
|
|
133
135
|
}
|
|
134
136
|
} else if (sub.action === "parent") {
|
|
135
|
-
|
|
137
|
+
// 会话复合值语义(MODEL-MERGE-SESSION——activeModel 恒非空——不再回退渠道字段)
|
|
138
|
+
const value = `${agent.activeProvider}:${agent.activeModel ?? ""}`
|
|
136
139
|
if (role) {
|
|
137
140
|
await persist((a) => { a.subagentModels ??= {}; a.subagentModels[role] = value })
|
|
138
141
|
} else {
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -97,16 +97,16 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
97
97
|
// 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
|
|
98
98
|
// 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
|
|
99
99
|
// 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
|
|
100
|
-
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
100
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
|
|
101
101
|
delete cur.reasoningEffort
|
|
102
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
102
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
103
103
|
}
|
|
104
104
|
} else if (e.action === "effort") {
|
|
105
105
|
// 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
|
|
106
106
|
// enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
|
|
107
|
-
if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
|
|
107
|
+
if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
|
|
108
108
|
cur.reasoningEffort = e.level
|
|
109
|
-
await syncProviderField("reasoningEffort", e.level)
|
|
109
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", e.level)
|
|
110
110
|
} else {
|
|
111
111
|
const enable = e.action === "on"
|
|
112
112
|
if (isEffortOnly) {
|
|
@@ -116,9 +116,9 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
116
116
|
// "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
|
|
117
117
|
// (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
|
|
118
118
|
else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
|
|
119
|
-
await syncProviderField("thinking", cur.thinking)
|
|
120
|
-
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
121
|
-
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
119
|
+
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
120
|
+
if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
121
|
+
else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
122
122
|
} else {
|
|
123
123
|
if (enable) {
|
|
124
124
|
cur.thinking = { type: thinkOnValue }
|
|
@@ -127,11 +127,11 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
127
127
|
cur.thinking = isCustomThink ? undefined : { type: "disabled" }
|
|
128
128
|
delete cur.reasoningEffort
|
|
129
129
|
}
|
|
130
|
-
await syncProviderField("thinking", cur.thinking)
|
|
130
|
+
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
131
131
|
if (enable) {
|
|
132
|
-
await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
132
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
133
133
|
} else {
|
|
134
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
134
|
+
await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/tui/cmd-undo.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from "node:fs"
|
|
9
|
-
import { join } from "node:path"
|
|
9
|
+
import { isAbsolute, join } from "node:path"
|
|
10
10
|
import { ansi, C } from "./ansi.mjs"
|
|
11
11
|
|
|
12
12
|
const MAX_UNDO = 50
|
|
@@ -20,7 +20,8 @@ export function snapshotForUndo(agent, toolName, args, cwd) {
|
|
|
20
20
|
const path = args.path ?? args.file
|
|
21
21
|
if (!path || typeof path !== "string") return
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// 绝对路径直接用(path.join 对绝对段不重置——Windows 反斜杠路径也不按 "/" 切分);相对路径整段 join(cwd)。
|
|
24
|
+
const abs = isAbsolute(path) ? path : join(cwd, path)
|
|
24
25
|
let backup = null
|
|
25
26
|
try {
|
|
26
27
|
if (existsSync(abs)) {
|
|
@@ -68,7 +69,7 @@ export async function handleUndoCommand(ctx) {
|
|
|
68
69
|
const e = await showPicker("Undo", entries)
|
|
69
70
|
if (!e) return
|
|
70
71
|
const item = stack[e.idx]
|
|
71
|
-
const abs = join(agent.cwd,
|
|
72
|
+
const abs = isAbsolute(item.path) ? item.path : join(agent.cwd, item.path)
|
|
72
73
|
|
|
73
74
|
try {
|
|
74
75
|
if (item.backup === null) {
|
package/src/tui/cmd-upgrade.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { upgradeFailureText } from "./update-notice.mjs"
|
|
2
3
|
|
|
3
4
|
/** /upgrade command: check for updates and optionally upgrade.
|
|
4
5
|
* ctx: { agent, pushLine, pushLabel, showPicker } */
|
|
5
6
|
export async function handleUpgradeCommand(ctx) {
|
|
6
|
-
const { pushLine, pushLabel, showPicker } = ctx
|
|
7
|
+
const { pushLine, pushLabel, showPicker, render } = ctx
|
|
7
8
|
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
8
9
|
const { readFileSync } = await import("node:fs")
|
|
9
10
|
|
|
@@ -31,13 +32,27 @@ export async function handleUpgradeCommand(ctx) {
|
|
|
31
32
|
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
32
33
|
const { exec } = await import("node:child_process")
|
|
33
34
|
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
let stdout = ""
|
|
36
|
+
let done = false
|
|
37
|
+
cp.stdout?.on("data", (d) => { stdout += d })
|
|
38
|
+
cp.stderr?.on("data", (d) => { stdout += d })
|
|
39
|
+
// 2026-09-05(advisor 🟡#5/#7):异步回调必须显式 render(pushLine 只写 lines 不绘制——
|
|
40
|
+
// handleSlash 返回后的统一 render 先于 exec 完成,结论行要等下次输入才可见);失败文案
|
|
41
|
+
// 复用 upgradeFailureText(附 npm 输出尾 ≤3 行——与 update-notice 后台路径同诊断能力)
|
|
42
|
+
cp.on("error", (e) => {
|
|
43
|
+
if (done) return
|
|
44
|
+
done = true
|
|
45
|
+
pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error)
|
|
46
|
+
render?.()
|
|
47
|
+
})
|
|
36
48
|
cp.on("close", (code) => {
|
|
49
|
+
if (done) return
|
|
50
|
+
done = true
|
|
37
51
|
if (code === 0) {
|
|
38
52
|
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
39
53
|
} else {
|
|
40
|
-
pushLine(
|
|
54
|
+
pushLine(upgradeFailureText(code, stdout), C.error)
|
|
41
55
|
}
|
|
56
|
+
render?.()
|
|
42
57
|
})
|
|
43
58
|
}
|
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
|
|
3
1
|
/** persistRaw / syncProviderField / maskKey: config read/write helpers.
|
|
4
2
|
* Extracted from index.mjs for shared use by slash-commands, wizard, and pickers.
|
|
5
|
-
* createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
* createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
|
|
4
|
+
* R10 F5(D-F5b):persistRaw 全链(磁盘新鲜读→mutate→写)收口进 config.mjs
|
|
5
|
+
* writeConfigAtomic——mtime 门控 + .bak + 冲突放弃(见 MULTI-INSTANCE-COLLAB.md §2a.2)。
|
|
6
|
+
* createConfigHelpers(agent, opts):opts.configPath 为测试注入缝(默认 configPath——
|
|
7
|
+
* 生产 index.mjs 单参调用零变化)。 */
|
|
8
|
+
export function createConfigHelpers(agent, opts = {}) {
|
|
9
|
+
const cfgPath = opts.configPath ?? null // null → 默认 configPath(动态 import 解)
|
|
10
|
+
|
|
11
|
+
/** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
|
|
12
|
+
* slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
|
|
8
13
|
async function persistRaw(mutate) {
|
|
9
|
-
const {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
saveConfig(raw)
|
|
14
|
+
const { writeConfigAtomic, configPath } = await import("../config.mjs")
|
|
15
|
+
const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
|
|
16
|
+
if (!r.ok) throw new Error("config changed on disk concurrently — retry")
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
/** Sync a field of
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Sync a field of a provider (by name) to the providers array and persist to disk.
|
|
20
|
+
* D-F5a(#1):磁盘 fresh raw.providers 上单字段补丁——不取 agent.providers 整数组
|
|
21
|
+
* (长跑进程的内存 providers 快照不得整体写回——会抹掉对端对磁盘其他改动的保留)。
|
|
22
|
+
* 内存 agent.providers 目标项同步镜像(旧行为:改内存 + 整节写盘——镜像保留内存
|
|
23
|
+
* 一致性,磁盘写只剩单字段)。value === undefined → 删除该字段。 */
|
|
24
|
+
async function syncProviderField(name, field, value) {
|
|
25
|
+
const mem = agent.providers?.find((p) => p?.name === name)
|
|
26
|
+
if (!mem) return // 内存无此 provider(旧语义 early return——不产生空写)
|
|
21
27
|
await persistRaw((raw) => {
|
|
22
|
-
raw.providers
|
|
28
|
+
raw.providers ??= []
|
|
29
|
+
const target = raw.providers.find((p) => p?.name === name)
|
|
30
|
+
if (!target) return // 磁盘目标已被对端删除 → 只做内存镜像,不落盘
|
|
31
|
+
if (value === undefined) delete target[field]
|
|
32
|
+
else target[field] = value
|
|
23
33
|
})
|
|
34
|
+
if (value === undefined) delete mem[field]
|
|
35
|
+
else mem[field] = value
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
/** Mask API key for display */
|
package/src/tui/distill-cmd.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export async function runDistill(ctx) {
|
|
|
22
22
|
}
|
|
23
23
|
let saved = 0
|
|
24
24
|
for (const c of candidates) {
|
|
25
|
-
pushLine(`── Candidate [${c.type}] ${c.title} (
|
|
25
|
+
pushLine(`── Candidate [${c.type}] ${c.title} (layer: ${c.layer ?? "personal"})`, C.warn)
|
|
26
26
|
for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
|
|
27
27
|
if (c.type === "rule") pushLine(" (rule type — consider writing manually; press y to extract)", C.warn)
|
|
28
28
|
const accept = await askPermission("distill-save", { title: c.title })
|