thincoder 0.8.10 → 0.8.12
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 +16 -0
- package/bin/thincoder.mjs +115 -0
- package/package.json +1 -1
- package/src/advisor.mjs +105 -0
- package/src/agent/dispatch.mjs +35 -0
- package/src/agent/helpers.mjs +1 -1
- package/src/agent/setup.mjs +21 -7
- package/src/agent-tools/subagent.mjs +1 -1
- package/src/agent-tools/timer.mjs +41 -0
- package/src/agent-tools/verify.mjs +165 -56
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +128 -20
- package/src/auto-think.mjs +83 -0
- package/src/cli/make-agent.mjs +9 -0
- package/src/config.mjs +18 -18
- package/src/git/checkpoint.mjs +2 -1
- package/src/git/gitmem.mjs +8 -2
- package/src/markdown.mjs +1 -1
- package/src/mcp/transport-http.mjs +8 -2
- package/src/memory/code-index.mjs +2 -2
- package/src/memory/code-sync.mjs +92 -35
- package/src/memory/core.mjs +10 -1
- package/src/memory/docs.mjs +24 -26
- package/src/memory/schema.mjs +16 -3
- package/src/prompts/coder.md +7 -4
- package/src/prompts/discipline.md +55 -16
- package/src/prompts/main.md +15 -11
- package/src/prompts/system.md +33 -7
- package/src/provider/core.mjs +186 -20
- package/src/provider/index.mjs +1 -1
- package/src/rules.mjs +53 -0
- package/src/session.mjs +1 -1
- package/src/tools/checklist.md +7 -0
- package/src/tools/checklist.mjs +114 -0
- package/src/tools/file.mjs +82 -1
- package/src/tools/hashline_edit.md +12 -0
- package/src/tools/index.mjs +7 -3
- package/src/tools/linter.md +13 -0
- package/src/tools/linter.mjs +146 -0
- package/src/tools/read.md +3 -2
- package/src/tools/repomap.mjs +14 -9
- package/src/tui/agent-turn.mjs +9 -2
- package/src/tui/ansi.mjs +1 -0
- package/src/tui/cmd-advisor.mjs +68 -0
- package/src/tui/cmd-think.mjs +36 -10
- package/src/tui/index.mjs +2 -1
- package/src/tui/key-handler.mjs +36 -1
- package/src/tui/layout.mjs +3 -1
- package/src/tui/pickers.mjs +15 -15
- package/src/tui/render-frame.mjs +17 -8
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tools/repomap-parse.mjs +0 -168
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
export async function handleThinkCommand(ctx) {
|
|
5
5
|
const { agent, openPicker, syncProviderField } = ctx
|
|
6
6
|
const cur = agent.provider
|
|
7
|
-
const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
|
|
8
7
|
const { specForModel } = await import("../config.mjs")
|
|
9
8
|
const spec = specForModel(cur.model)
|
|
10
9
|
const isEffortOnly = spec.thinkApi === "effort"
|
|
10
|
+
const thinkOnValue = spec.thinkOnValue ?? "enabled"
|
|
11
|
+
const isCustomThink = thinkOnValue !== "enabled"
|
|
12
|
+
// "enabled" when thinking.type matches the model's enabled value, or when thinking is absent and the model is NOT a custom-think model (defaults to on for standard models)
|
|
13
|
+
const thinkingEnabled = cur.thinking?.type === thinkOnValue || (cur.thinking?.type === undefined && !isCustomThink)
|
|
11
14
|
const entries = []
|
|
15
|
+
// Auto-think: classify difficulty per-prompt and auto-set reasoning effort
|
|
16
|
+
const autoThinkEnabled = agent.config?.agent?.autoThink === true
|
|
17
|
+
entries.push({ type: "item", text: `Auto: ${autoThinkEnabled ? "ON" : "OFF"}`, action: "auto" })
|
|
12
18
|
if (!isEffortOnly) {
|
|
13
|
-
entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
19
|
+
if (!autoThinkEnabled) entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
14
20
|
}
|
|
15
|
-
if (spec.reasoningEffortEnum) {
|
|
21
|
+
if (spec.reasoningEffortEnum && !autoThinkEnabled) {
|
|
16
22
|
for (const level of spec.reasoningEffortEnum) {
|
|
17
23
|
const mark = cur.reasoningEffort === level ? "▸ " : " "
|
|
18
24
|
entries.push({ type: "item", text: `${mark}effort: ${level}`, action: "effort", level })
|
|
19
25
|
}
|
|
20
|
-
} else {
|
|
26
|
+
} else if (!autoThinkEnabled) {
|
|
21
27
|
entries.push({ type: "item", text: "effort: high", action: "effort", level: "high" })
|
|
22
28
|
entries.push({ type: "item", text: "effort: max", action: "effort", level: "max" })
|
|
23
29
|
}
|
|
@@ -25,7 +31,19 @@ export async function handleThinkCommand(ctx) {
|
|
|
25
31
|
title: "Think",
|
|
26
32
|
entries,
|
|
27
33
|
onSelect: async (e) => {
|
|
28
|
-
if (e.action === "
|
|
34
|
+
if (e.action === "auto") {
|
|
35
|
+
const cfg = agent.config.agent ??= {}
|
|
36
|
+
cfg.autoThink = !cfg.autoThink
|
|
37
|
+
agent._pendingReminders = agent._pendingReminders ?? []
|
|
38
|
+
if (cfg.autoThink) {
|
|
39
|
+
// Turn off manual effort — auto will set it per-turn
|
|
40
|
+
delete cur.reasoningEffort
|
|
41
|
+
await syncProviderField("reasoningEffort", undefined)
|
|
42
|
+
agent._pendingReminders.push("[System reminder: Auto-think is now ON. Reasoning effort will be automatically set per-task based on difficulty classification.]")
|
|
43
|
+
} else {
|
|
44
|
+
agent._pendingReminders.push("[System reminder: Auto-think is now OFF. Reasoning effort will remain at its current manual setting.]")
|
|
45
|
+
}
|
|
46
|
+
} else if (e.action === "effort") {
|
|
29
47
|
cur.reasoningEffort = e.level
|
|
30
48
|
await syncProviderField("reasoningEffort", e.level)
|
|
31
49
|
} else {
|
|
@@ -36,12 +54,20 @@ export async function handleThinkCommand(ctx) {
|
|
|
36
54
|
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
37
55
|
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
38
56
|
} else {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
if (enable) {
|
|
58
|
+
cur.thinking = { type: thinkOnValue }
|
|
59
|
+
if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
60
|
+
} else {
|
|
61
|
+
// Custom-think models (MiniMax "adaptive") don't support "disabled" — remove the field instead
|
|
62
|
+
cur.thinking = isCustomThink ? undefined : { type: "disabled" }
|
|
63
|
+
delete cur.reasoningEffort
|
|
64
|
+
}
|
|
42
65
|
await syncProviderField("thinking", cur.thinking)
|
|
43
|
-
if (
|
|
44
|
-
|
|
66
|
+
if (enable) {
|
|
67
|
+
await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
68
|
+
} else {
|
|
69
|
+
await syncProviderField("reasoningEffort", undefined)
|
|
70
|
+
}
|
|
45
71
|
}
|
|
46
72
|
}
|
|
47
73
|
},
|
package/src/tui/index.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
63
63
|
picker: null, // model picker { entries, lines, index, scroll, selectedLine }
|
|
64
64
|
wizard: null, // first-launch config wizard { step, index, scroll, selectedLine, fields, error, lines }
|
|
65
65
|
tasks: agent.tasks ?? [], // task list from task tool (progress shown in status bar); carried over on session restore, auto-collapsed when all done
|
|
66
|
-
tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // cumulative token usage (shown in status bar)
|
|
66
|
+
tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0, reasoningTokens: 0 }, // cumulative token usage (shown in status bar)
|
|
67
67
|
ctxCache: { len: -1, tokens: 0 }, // context utilization estimate cache (estimateTokens is O(n), only recompute when history grows)
|
|
68
68
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
69
69
|
completion: null, // Tab completion state { candidates, index }
|
|
@@ -74,6 +74,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
74
74
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
75
75
|
status: "Ready",
|
|
76
76
|
queue: [], // queued messages while processing: [{ text }], auto-dequeued when current turn finishes
|
|
77
|
+
interruptPrompt: null, // Ctrl+I interrupt message input: { text: "" } or null
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -91,7 +91,10 @@ export function createKeyHandler(ctx) {
|
|
|
91
91
|
insertPastedText(state, text)
|
|
92
92
|
render()
|
|
93
93
|
}
|
|
94
|
-
}).catch(() => {
|
|
94
|
+
}).catch((e) => {
|
|
95
|
+
q._pasting = false
|
|
96
|
+
console.error(`[tui] clipboard paste failed: ${e.message}`)
|
|
97
|
+
})
|
|
95
98
|
} else if (str && !key.ctrl && !key.meta) {
|
|
96
99
|
q.answer = (q.answer ?? "") + str
|
|
97
100
|
render()
|
|
@@ -111,6 +114,38 @@ export function createKeyHandler(ctx) {
|
|
|
111
114
|
setTimeout(() => process.exit(0), 100)
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
// Ctrl+I: interrupt current generation and inject a message (time-travel inject)
|
|
118
|
+
if (key.ctrl && !key.alt && key.name === "i") {
|
|
119
|
+
if (state.processing && state.controller && !state.interruptPrompt) {
|
|
120
|
+
state.interruptPrompt = { text: "" }
|
|
121
|
+
render()
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Interrupt prompt mode: type message, Enter to inject, Esc to cancel
|
|
127
|
+
if (state.interruptPrompt) {
|
|
128
|
+
if (key.name === "escape") {
|
|
129
|
+
state.interruptPrompt = null
|
|
130
|
+
render()
|
|
131
|
+
} else if (key.name === "return") {
|
|
132
|
+
const msg = (state.interruptPrompt.text ?? "").trim()
|
|
133
|
+
state.interruptPrompt = null
|
|
134
|
+
if (msg) {
|
|
135
|
+
pushLine(` [inject] ${msg}`, C.warn)
|
|
136
|
+
state.controller.abort({ interrupt: true, message: msg })
|
|
137
|
+
render()
|
|
138
|
+
}
|
|
139
|
+
} else if (key.name === "backspace") {
|
|
140
|
+
state.interruptPrompt.text = state.interruptPrompt.text.slice(0, -1)
|
|
141
|
+
render()
|
|
142
|
+
} else if (str && !key.ctrl && !key.meta) {
|
|
143
|
+
state.interruptPrompt.text += str.replace(/[\r\n]+/g, "")
|
|
144
|
+
render()
|
|
145
|
+
}
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
// generic list picker: ↑↓ move, Enter confirm, Esc cancel
|
|
115
150
|
if (state.picker) {
|
|
116
151
|
const items = state.picker?.entries.filter((e) => e.type === "item") ?? []
|
package/src/tui/layout.mjs
CHANGED
|
@@ -22,7 +22,9 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
22
22
|
const W = Math.max(20, cols - 1)
|
|
23
23
|
|
|
24
24
|
// --- input box ---
|
|
25
|
-
const
|
|
25
|
+
const inputBuf = state.interruptPrompt ? [...state.interruptPrompt.text] : state.input
|
|
26
|
+
const inputCursor = state.interruptPrompt ? inputBuf.length : state.cursor
|
|
27
|
+
const inputLayout = layoutInput(inputBuf, inputCursor, W - 4)
|
|
26
28
|
let inputOffset = 0
|
|
27
29
|
if (inputLayout.lines.length > MAX_INPUT_LINES) {
|
|
28
30
|
inputOffset = Math.min(inputLayout.cursorLine, inputLayout.lines.length - MAX_INPUT_LINES)
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -185,27 +185,27 @@ export function createPickers(ctx) {
|
|
|
185
185
|
openPicker({
|
|
186
186
|
title: "Add Provider",
|
|
187
187
|
entries: presetEntries,
|
|
188
|
-
onCancel: () => openModelPicker().catch(() => {}),
|
|
188
|
+
onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
|
|
189
189
|
onSelect: async (se) => {
|
|
190
190
|
if (se.kind === "custom") {
|
|
191
191
|
const name = await askQuestion("Enter provider name:")
|
|
192
|
-
if (!name) { openModelPicker().catch(() => {}); return }
|
|
193
|
-
if (agent.providers.some((p) => p.name === name)) { openModelPicker().catch(() => {}); return }
|
|
192
|
+
if (!name) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
193
|
+
if (agent.providers.some((p) => p.name === name)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
194
194
|
const baseURLRaw = await askQuestion("Enter baseURL (e.g. https://api.example.com/v1):")
|
|
195
|
-
if (!baseURLRaw) { openModelPicker().catch(() => {}); return }
|
|
195
|
+
if (!baseURLRaw) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
196
196
|
const baseURL = baseURLRaw.replace(/\/+$/, "")
|
|
197
|
-
if (!/^https?:\/\//.test(baseURL)) { openModelPicker().catch(() => {}); return }
|
|
197
|
+
if (!/^https?:\/\//.test(baseURL)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
198
198
|
const model = await askQuestion("Enter model name:")
|
|
199
|
-
if (!model) { openModelPicker().catch(() => {}); return }
|
|
199
|
+
if (!model) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
200
200
|
agent.providers.push({ name, baseURL, model })
|
|
201
201
|
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
202
202
|
const key = await askQuestion(`Enter API key for ${name} (leave empty to skip):`)
|
|
203
203
|
if (key) { await setProviderKey(name, key) }
|
|
204
|
-
openModelPicker().catch(() => {})
|
|
204
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
205
205
|
return
|
|
206
206
|
}
|
|
207
207
|
// preset
|
|
208
|
-
if (agent.providers.some((p) => p.name === se.name)) { openModelPicker().catch(() => {}); return }
|
|
208
|
+
if (agent.providers.some((p) => p.name === se.name)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
209
209
|
const preset = PRESETS[se.name]
|
|
210
210
|
const providerCfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
|
|
211
211
|
if (preset.thinking) providerCfg.thinking = preset.thinking
|
|
@@ -217,7 +217,7 @@ export function createPickers(ctx) {
|
|
|
217
217
|
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
218
218
|
const presetKey = await askQuestion(`Enter API key for ${se.name} (leave empty to skip):`)
|
|
219
219
|
if (presetKey) await setProviderKey(se.name, presetKey)
|
|
220
|
-
openModelPicker().catch(() => {})
|
|
220
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
221
221
|
},
|
|
222
222
|
})
|
|
223
223
|
}
|
|
@@ -225,7 +225,7 @@ export function createPickers(ctx) {
|
|
|
225
225
|
/** Remove provider (cannot remove the currently active one) */
|
|
226
226
|
async function removeProviderFlow() {
|
|
227
227
|
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
228
|
-
if (candidates.length === 0) { openModelPicker().catch(() => {}); return }
|
|
228
|
+
if (candidates.length === 0) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
229
229
|
const removeEntries = [
|
|
230
230
|
{ type: "header", text: "Select provider to remove (current one cannot be removed)" },
|
|
231
231
|
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
@@ -233,12 +233,12 @@ export function createPickers(ctx) {
|
|
|
233
233
|
openPicker({
|
|
234
234
|
title: "Remove Provider",
|
|
235
235
|
entries: removeEntries,
|
|
236
|
-
onCancel: () => openModelPicker().catch(() => {}),
|
|
236
|
+
onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
|
|
237
237
|
onSelect: async (se) => {
|
|
238
238
|
const at = agent.providers.findIndex((p) => p.name === se.name)
|
|
239
239
|
agent.providers.splice(at, 1)
|
|
240
240
|
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
241
|
-
openModelPicker().catch(() => {})
|
|
241
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
242
242
|
},
|
|
243
243
|
})
|
|
244
244
|
}
|
|
@@ -256,12 +256,12 @@ export function createPickers(ctx) {
|
|
|
256
256
|
openPicker({
|
|
257
257
|
title: "Configure API Key",
|
|
258
258
|
entries: keyEntries,
|
|
259
|
-
onCancel: () => openModelPicker().catch(() => {}),
|
|
259
|
+
onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
|
|
260
260
|
onSelect: async (se) => {
|
|
261
261
|
const key = await askQuestion(`Enter API key for ${se.name}:`)
|
|
262
|
-
if (!key) { openModelPicker().catch(() => {}); return }
|
|
262
|
+
if (!key) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
|
|
263
263
|
await setProviderKey(se.name, key)
|
|
264
|
-
openModelPicker().catch(() => {})
|
|
264
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
265
265
|
},
|
|
266
266
|
})
|
|
267
267
|
}
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -40,8 +40,11 @@ export function renderFrame(state, agent, opts) {
|
|
|
40
40
|
const thinking = agent.provider.thinking
|
|
41
41
|
const effort = agent.provider.reasoningEffort
|
|
42
42
|
const isMultimodal = specForModel(model).multimodal
|
|
43
|
+
const spec = specForModel(model)
|
|
44
|
+
const thinkOnValue = spec.thinkOnValue ?? "enabled"
|
|
43
45
|
const thinkBadge = thinking?.type === "disabled" ? "│ think: off"
|
|
44
|
-
: effort ? `│ think: ${effort}`
|
|
46
|
+
: effort ? `│ think: ${effort}`
|
|
47
|
+
: thinking?.type === thinkOnValue ? "│ think: on" : ""
|
|
45
48
|
|
|
46
49
|
const out = [ansi.home]
|
|
47
50
|
let cursorRow = 0, cursorCol = 0
|
|
@@ -145,17 +148,19 @@ export function renderFrame(state, agent, opts) {
|
|
|
145
148
|
// ---- queue preview ----
|
|
146
149
|
if (panels.queue) {
|
|
147
150
|
const preview = sliceByWidth(state.queue[0].text, W - 20)
|
|
148
|
-
out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete${ansi.reset}${ansi.clearLine}`)
|
|
151
|
+
out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete │ Ctrl+I inject${ansi.reset}${ansi.clearLine}`)
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
// ---- input box ----
|
|
152
155
|
const { borderColor, title } = inputBoxStyle(state)
|
|
153
156
|
let topBorder
|
|
154
|
-
if (title === " Input " || title === " Question ") {
|
|
157
|
+
if (title === " Input " || title === " Question " || title === " Inject Message ") {
|
|
155
158
|
const parts = []
|
|
156
159
|
if (title === " Input ") parts.push(" Ctrl+U clear ")
|
|
157
160
|
if (title === " Question ") parts.push(" Enter submit ")
|
|
161
|
+
if (title === " Inject Message ") parts.push(" Enter send, Esc cancel ")
|
|
158
162
|
parts.push(" Ctrl+V paste ")
|
|
163
|
+
parts.push(" Ctrl+I inject ")
|
|
159
164
|
const hint = parts.join("")
|
|
160
165
|
topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
|
|
161
166
|
} else {
|
|
@@ -173,9 +178,10 @@ export function renderFrame(state, agent, opts) {
|
|
|
173
178
|
const statusLine = buildStatusLine(state, agent, { cols, slashCommands })
|
|
174
179
|
const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
|
|
175
180
|
const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
|
|
176
|
-
const
|
|
181
|
+
const advisorBanner = agent.config?.advisor?.enabled ? `${C.advisor} ADVISOR${ansi.reset}${ansi.dim}│` : ""
|
|
182
|
+
const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.enabled ? " ADVISOR│ " : "")
|
|
177
183
|
const statusMax = cols - 1 - (bannerPrefix ? stringWidth(bannerPrefix) : 0)
|
|
178
|
-
out.push(`${ansi.dim}${planBanner}${autoBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
|
|
184
|
+
out.push(`${ansi.dim}${planBanner}${autoBanner}${advisorBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
|
|
179
185
|
|
|
180
186
|
const frame = out.join("\r\n")
|
|
181
187
|
|
|
@@ -239,7 +245,10 @@ function buildConvLines(state, cols) {
|
|
|
239
245
|
function inputBoxStyle(state) {
|
|
240
246
|
let borderColor = C.tool
|
|
241
247
|
let title
|
|
242
|
-
if (state.
|
|
248
|
+
if (state.interruptPrompt) {
|
|
249
|
+
borderColor = C.warn
|
|
250
|
+
title = " Inject Message "
|
|
251
|
+
} else if (state.question) {
|
|
243
252
|
borderColor = C.tool
|
|
244
253
|
title = " Question "
|
|
245
254
|
} else if (state.permission) {
|
|
@@ -308,7 +317,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
308
317
|
const fmtK = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`)
|
|
309
318
|
const cacheTotal = tk.cacheHit + tk.cacheMiss
|
|
310
319
|
const tokenHint = tk.prompt > 0
|
|
311
|
-
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
|
|
320
|
+
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${tk.reasoningTokens > 0 ? ` ✦${fmtK(tk.reasoningTokens)}` : ""}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
|
|
312
321
|
: ""
|
|
313
322
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
314
323
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
@@ -321,7 +330,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
321
330
|
: ` │ context ${ctxPct}%`
|
|
322
331
|
: ""
|
|
323
332
|
const queueHint = state.queue.length > 0 ? ` │ queue: ${state.queue.length}` : ""
|
|
324
|
-
return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
|
|
333
|
+
return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit`
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
/** Summarize tool args for subagent panel display (one line, short). Pure. */
|
|
@@ -21,6 +21,7 @@ import { handleGoalCommand } from "./cmd-goal.mjs"
|
|
|
21
21
|
import { handleSkillsCommand } from "./cmd-skills.mjs"
|
|
22
22
|
import { handleMcpCommand } from "./cmd-mcp.mjs"
|
|
23
23
|
import { handleAutoCommand } from "./cmd-auto.mjs"
|
|
24
|
+
import { handleAdvisorCommand } from "./cmd-advisor.mjs"
|
|
24
25
|
import { handleThinkCommand } from "./cmd-think.mjs"
|
|
25
26
|
import { handleModelCommand } from "./cmd-model.mjs"
|
|
26
27
|
import { handleConfigCommand } from "./cmd-config.mjs"
|
|
@@ -31,6 +32,7 @@ import { handleUpgradeCommand } from "./cmd-upgrade.mjs"
|
|
|
31
32
|
export const SLASH_COMMANDS = [
|
|
32
33
|
{ name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
|
|
33
34
|
{ name: "/auto", group: "Agent", desc: "toggle auto-approve" },
|
|
35
|
+
{ name: "/advisor", group: "Agent", desc: "toggle advisor review & select model" },
|
|
34
36
|
{ name: "/model", group: "Agent", desc: "select model & manage providers" },
|
|
35
37
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
36
38
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
@@ -63,6 +65,7 @@ const HANDLERS = {
|
|
|
63
65
|
"/skills": handleSkillsCommand,
|
|
64
66
|
"/mcp": handleMcpCommand,
|
|
65
67
|
"/auto": handleAutoCommand,
|
|
68
|
+
"/advisor": handleAdvisorCommand,
|
|
66
69
|
"/think": handleThinkCommand,
|
|
67
70
|
"/model": handleModelCommand,
|
|
68
71
|
"/config": handleConfigCommand,
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* repomap-parse.mjs — repo dependency graph parser (zero dependencies, pure regex)
|
|
3
|
-
* Gets known file list from code_chunks, parses each file's import/export relationships in real time,
|
|
4
|
-
* builds forward dependency graph + reverse reference graph. Shared by repomap.mjs's buildSummary / buildOutline.
|
|
5
|
-
*/
|
|
6
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
7
|
-
import { join } from "node:path"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Scan all files, build forward dependency graph + reverse reference graph.
|
|
11
|
-
* Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
|
|
12
|
-
*/
|
|
13
|
-
export function buildDepGraph(db, cwd) {
|
|
14
|
-
const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
|
|
15
|
-
if (allFiles.length === 0) return null
|
|
16
|
-
|
|
17
|
-
const deps = new Map() // path → { imports: Set, exports: Set, size: number, dir: string }
|
|
18
|
-
const importers = new Map() // importee → Set<importer>
|
|
19
|
-
|
|
20
|
-
for (const rel of allFiles) {
|
|
21
|
-
const abs = join(cwd, ...rel.split("/"))
|
|
22
|
-
if (!existsSync(abs)) continue
|
|
23
|
-
const text = readFileSync(abs, "utf8")
|
|
24
|
-
const lines = text.split("\n")
|
|
25
|
-
const ext = rel.slice(rel.lastIndexOf(".")).toLowerCase()
|
|
26
|
-
|
|
27
|
-
let imports, exports
|
|
28
|
-
if (ext === ".py") {
|
|
29
|
-
const py = parsePyOutline(lines)
|
|
30
|
-
imports = py.imports
|
|
31
|
-
exports = py.symbols
|
|
32
|
-
} else {
|
|
33
|
-
imports = parseImports(lines, ext)
|
|
34
|
-
exports = parseExports(lines, ext)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Resolve import paths to relative paths (handle ./ ../)
|
|
38
|
-
const resolved = []
|
|
39
|
-
for (let imp of imports) {
|
|
40
|
-
if (imp.startsWith("./")) imp = imp.slice(2)
|
|
41
|
-
const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ""
|
|
42
|
-
const parts = imp.split("/")
|
|
43
|
-
if (parts[0] === "..") {
|
|
44
|
-
const up = dir.split("/").filter(Boolean)
|
|
45
|
-
let i = 0
|
|
46
|
-
while (parts[i] === ".." && up.length > 0) { up.pop(); i++ }
|
|
47
|
-
resolved.push([...up, ...parts.slice(i)].join("/"))
|
|
48
|
-
} else {
|
|
49
|
-
resolved.push(dir ? `${dir}/${imp}` : imp)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "."
|
|
54
|
-
deps.set(rel, { imports: new Set(resolved), exports: new Set(exports), size: Math.floor(text.length / 1024), dir })
|
|
55
|
-
|
|
56
|
-
for (const r of resolved) {
|
|
57
|
-
if (!importers.has(r)) importers.set(r, new Set())
|
|
58
|
-
importers.get(r).add(rel)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { deps, importers, fileCount: allFiles.length }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------- internal implementation
|
|
66
|
-
|
|
67
|
-
function normalizeExt(p) {
|
|
68
|
-
return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
|
|
72
|
-
function parseImports(lines, ext) {
|
|
73
|
-
const imports = []
|
|
74
|
-
const text = lines.join("\n")
|
|
75
|
-
// standard import
|
|
76
|
-
const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
|
|
77
|
-
let m
|
|
78
|
-
while ((m = re.exec(text))) {
|
|
79
|
-
const raw = m[1] || m[2]
|
|
80
|
-
if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
|
|
81
|
-
imports.push(normalizeExt(raw))
|
|
82
|
-
}
|
|
83
|
-
// re-export: export { x } from './module'
|
|
84
|
-
const reExportRe = /export\s*\{[^}]*\}\s*from\s*['"]([^'"]+)['"]/g
|
|
85
|
-
while ((m = reExportRe.exec(text))) {
|
|
86
|
-
const raw = m[1]
|
|
87
|
-
if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
|
|
88
|
-
imports.push(normalizeExt(raw))
|
|
89
|
-
}
|
|
90
|
-
return [...new Set(imports)]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Extract JS/TS file export symbols */
|
|
94
|
-
function parseExports(lines, ext) {
|
|
95
|
-
const exports = []
|
|
96
|
-
const text = lines.join("\n")
|
|
97
|
-
// export function/class/const/let/var name
|
|
98
|
-
const namedRe = /export\s+(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+)|(?:const|let|var)\s+(\w+))/g
|
|
99
|
-
let m
|
|
100
|
-
while ((m = namedRe.exec(text))) {
|
|
101
|
-
exports.push(m[1] || m[2] || m[3])
|
|
102
|
-
}
|
|
103
|
-
// export default function/class name / export default expression
|
|
104
|
-
const defaultRe = /export\s+default\s+(?:(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+))|(\w+))/g
|
|
105
|
-
while ((m = defaultRe.exec(text))) {
|
|
106
|
-
const name = m[1] || m[2] || m[3]
|
|
107
|
-
if (name) exports.push(name)
|
|
108
|
-
else if (!exports.some((e) => e === "default")) exports.push("default")
|
|
109
|
-
}
|
|
110
|
-
// export { a, b as c } — prefer the "as" alias as the exported name
|
|
111
|
-
const braceRe = /export\s*\{([^}]+)\}/g
|
|
112
|
-
while ((m = braceRe.exec(text))) {
|
|
113
|
-
for (const name of m[1].split(",")) {
|
|
114
|
-
const parts = name.trim().split(/\s+/)
|
|
115
|
-
// "a as b" → b (exported name), "a" → a
|
|
116
|
-
const exported = parts.length >= 3 ? parts[2] : parts[0]
|
|
117
|
-
if (exported) exports.push(exported)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// export const { a, b } = ... (destructured export)
|
|
121
|
-
const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
|
|
122
|
-
while ((m = destructRe.exec(text))) {
|
|
123
|
-
for (const name of m[1].split(",")) {
|
|
124
|
-
const parts = name.trim().split(/\s*:\s*/)
|
|
125
|
-
const n = parts[0].trim()
|
|
126
|
-
if (n) exports.push(n)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return [...new Set(exports)]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Extract Python imports and top-level def/class */
|
|
133
|
-
function parsePyOutline(lines) {
|
|
134
|
-
const imports = []
|
|
135
|
-
const symbols = []
|
|
136
|
-
for (const line of lines) {
|
|
137
|
-
const fromRe = line.match(/^from\s+(\S+)\s+import\s+(.+)/)
|
|
138
|
-
if (fromRe) {
|
|
139
|
-
const rel = pyRelPath(fromRe[1])
|
|
140
|
-
if (rel) imports.push(rel)
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
const impRe = line.match(/^import\s+(.+)/)
|
|
144
|
-
if (impRe) {
|
|
145
|
-
for (const mod of impRe[1].split(",")) {
|
|
146
|
-
const rel = pyRelPath(mod.trim().split(/\s+/)[0])
|
|
147
|
-
if (rel) imports.push(rel)
|
|
148
|
-
}
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
const defRe = line.match(/^(?:async\s+)?(?:def|class)\s+(\w+)/)
|
|
152
|
-
if (defRe) symbols.push(defRe[1])
|
|
153
|
-
}
|
|
154
|
-
return { imports: [...new Set(imports)], symbols: [...new Set(symbols)] }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Python relative import → relative file path:
|
|
159
|
-
* Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
|
|
160
|
-
* Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
|
|
161
|
-
*/
|
|
162
|
-
function pyRelPath(mod) {
|
|
163
|
-
if (!mod?.startsWith(".")) return null
|
|
164
|
-
const dots = mod.match(/^\.+/)[0].length
|
|
165
|
-
const rest = mod.slice(dots).replaceAll(".", "/")
|
|
166
|
-
if (!rest) return null
|
|
167
|
-
return normalizeExt("../".repeat(dots - 1) + rest)
|
|
168
|
-
}
|