thincoder 0.8.3 → 0.8.4
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/agent-tools/goal.mjs +5 -5
- package/src/agent-tools/plan.mjs +2 -3
- package/src/agent-tools/task.mjs +3 -22
- package/src/agent.mjs +43 -74
- package/src/config.mjs +1 -0
- package/src/context.mjs +1 -6
- package/src/prompts/coder.md +0 -6
- package/src/prompts/discipline.md +6 -0
- package/src/prompts/main.md +24 -23
- package/src/prompts/system.md +1 -1
- package/src/session.mjs +1 -3
- package/src/tools/file.mjs +4 -2
- package/src/tools/ls.md +1 -1
- package/src/tools/shared.mjs +6 -0
- package/src/tui/ansi.mjs +1 -1
- package/src/tui/cmd-auto.mjs +2 -11
- package/src/tui/cmd-config.mjs +107 -22
- package/src/tui/cmd-extract.mjs +10 -2
- package/src/tui/cmd-goal.mjs +0 -9
- package/src/tui/cmd-help.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +33 -20
- package/src/tui/cmd-new.mjs +32 -12
- package/src/tui/cmd-plan.mjs +2 -11
- package/src/tui/cmd-think.mjs +2 -9
- package/src/tui/distill-cmd.mjs +1 -0
- package/src/tui/key-handler.mjs +3 -3
- package/src/tui/slash-commands.mjs +8 -8
package/package.json
CHANGED
package/src/agent-tools/goal.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
export const goalTool = {
|
|
8
8
|
name: "goal",
|
|
9
9
|
description:
|
|
10
|
-
"Manage a long-running autonomous goal
|
|
11
|
-
"action='set': create
|
|
12
|
-
"action='complete': mark
|
|
13
|
-
"action='blocked': report an impasse (requires 'reason')
|
|
14
|
-
"action='cancel': abandon the goal
|
|
10
|
+
"Manage a long-running autonomous goal. " +
|
|
11
|
+
"action='set': create or replace the goal — must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
|
|
12
|
+
"action='complete': mark achieved — only after the criterion's check has actually passed. " +
|
|
13
|
+
"action='blocked': report an impasse (requires 'reason') — only after 3 genuine attempts. " +
|
|
14
|
+
"action='cancel': abandon the goal.",
|
|
15
15
|
parameters: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
package/src/agent-tools/plan.mjs
CHANGED
|
@@ -19,13 +19,12 @@ export const planTool = {
|
|
|
19
19
|
if (args.action === "exit") {
|
|
20
20
|
ctx.agent.planMode = false
|
|
21
21
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
22
|
-
ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF.
|
|
22
|
+
ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. No need for a task list (plan already covered that) or further confirmation.]")
|
|
23
23
|
return "Plan mode exited. You may now edit files and run commands."
|
|
24
24
|
}
|
|
25
25
|
ctx.agent.planMode = true
|
|
26
|
-
ctx.agent._turnsInPlanMode = 0
|
|
27
26
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
28
|
-
ctx.agent._pendingReminders.push("[System reminder: plan mode is now ON. Workflow: (1) explore/read codebase with read-only tools, (2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit'.
|
|
27
|
+
ctx.agent._pendingReminders.push("[System reminder: plan mode is now ON. Workflow: (1) explore/read codebase with read-only tools, (2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands.]")
|
|
29
28
|
return "Plan mode activated. You are now restricted to READ-ONLY tools. Explore the codebase, understand the architecture, design a solution. Present your plan to the user for approval before writing any code."
|
|
30
29
|
},
|
|
31
30
|
}
|
package/src/agent-tools/task.mjs
CHANGED
|
@@ -8,25 +8,8 @@ const VALID_TASK_STATUS = new Set(["pending", "in_progress", "done"])
|
|
|
8
8
|
export const taskTool = {
|
|
9
9
|
name: "task",
|
|
10
10
|
description:
|
|
11
|
-
"Plan and track a task list for complex multi-step work.
|
|
12
|
-
"
|
|
13
|
-
"When to use:\n" +
|
|
14
|
-
"- Multi-step tasks that span several tool calls — create the list BEFORE starting work\n" +
|
|
15
|
-
"- After receiving new multi-step instructions, capture the requirements as tasks first\n" +
|
|
16
|
-
"- Planning a sequence of edits before making them\n" +
|
|
17
|
-
"- Tracking investigation progress across a large codebase search\n" +
|
|
18
|
-
"\n" +
|
|
19
|
-
"When NOT to use:\n" +
|
|
20
|
-
"- Single-shot requests answerable in one or two tool calls\n" +
|
|
21
|
-
"- Trivial requests or purely conversational replies\n" +
|
|
22
|
-
"\n" +
|
|
23
|
-
"Discipline:\n" +
|
|
24
|
-
"- Keep exactly ONE item in_progress; mark it before starting that item\n" +
|
|
25
|
-
"- CALL THIS TOOL AGAIN to mark each item done as soon as you complete it — do not batch completions at the end\n" +
|
|
26
|
-
"- Never mark an item done if tests are failing, the implementation is partial, or errors remain\n" +
|
|
27
|
-
"- If blocked, keep the item in_progress (or add a new pending item describing the blocker) and tell the user\n" +
|
|
28
|
-
"- Avoid churn: don't re-call without real progress; never finish with stale pending items\n" +
|
|
29
|
-
"\n" +
|
|
11
|
+
"Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
|
|
12
|
+
"Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
|
|
30
13
|
"Statuses: pending | in_progress | done.",
|
|
31
14
|
parameters: {
|
|
32
15
|
type: "object",
|
|
@@ -56,12 +39,10 @@ export const taskTool = {
|
|
|
56
39
|
const recentDone = raw.filter((t) => t.status === "done").slice(-3)
|
|
57
40
|
const items = [...pending, ...recentDone].slice(0, 20)
|
|
58
41
|
ctx.agent.tasks = items
|
|
59
|
-
ctx.agent._turnsSinceTaskUpdate = 0
|
|
60
42
|
ctx.agent._onTaskUpdate?.(items)
|
|
61
43
|
const done = items.filter((i) => i.status === "done").length
|
|
62
44
|
const open = items.length - done
|
|
63
45
|
return `Task list updated: ${done}/${items.length} done` +
|
|
64
|
-
(open > 0 ? ` — ${open} item(s) still open
|
|
65
|
-
`\nEnsure you keep using the task list to track progress: mark items done immediately after finishing them, and keep exactly one item in_progress while work is underway.`
|
|
46
|
+
(open > 0 ? ` — ${open} item(s) still open.` : " — all done.")
|
|
66
47
|
},
|
|
67
48
|
}
|
package/src/agent.mjs
CHANGED
|
@@ -44,13 +44,11 @@ export {
|
|
|
44
44
|
// If multiple agents/databases are supported in the future, switch to per-agent cache or import each time.
|
|
45
45
|
let _reindexFile = null
|
|
46
46
|
const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
|
|
47
|
-
const MAX_VERIFY_RETRIES = 3
|
|
48
|
-
const MAX_VERIFY_PUSHBACKS = 2
|
|
49
47
|
const STALL_WINDOW_SIZE = 5
|
|
50
48
|
const STALL_THRESHOLD = 3
|
|
51
49
|
const GOAL_BUDGET_WARN_RATIO = 0.75
|
|
52
|
-
const
|
|
53
|
-
const
|
|
50
|
+
const MAX_VERIFY_PUSHBACKS = 2
|
|
51
|
+
const MAX_VERIFY_RETRIES = 3
|
|
54
52
|
|
|
55
53
|
/** Create a new agent state object with all fields initialized to defaults */
|
|
56
54
|
export function createAgent({
|
|
@@ -65,7 +63,6 @@ export function createAgent({
|
|
|
65
63
|
planMode, autoApprove, goal,
|
|
66
64
|
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined,
|
|
67
65
|
_touchedFiles: [], _verifyRetries: 0,
|
|
68
|
-
_turnsSinceTaskUpdate: 0, _turnsInPlanMode: 0,
|
|
69
66
|
_pendingReminders: [],
|
|
70
67
|
_sessionStart: sessionStart,
|
|
71
68
|
_lastPromptTokens: null, _usageAtLen: null,
|
|
@@ -90,8 +87,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
90
87
|
const recentCallSigs = []
|
|
91
88
|
|
|
92
89
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
93
|
-
agent._turnsSinceTaskUpdate++
|
|
94
|
-
if (agent.planMode) agent._turnsInPlanMode++
|
|
95
90
|
|
|
96
91
|
const lastRole = agent.history.at(-1)?.role
|
|
97
92
|
if (lastRole === "user" || lastRole === "tool") {
|
|
@@ -134,47 +129,54 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
134
129
|
|
|
135
130
|
if (response.toolCalls.length === 0) {
|
|
136
131
|
if (!response.content) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
agent.history.push({
|
|
143
|
-
role: "user",
|
|
144
|
-
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply. Never mention this reminder to the user.]",
|
|
145
|
-
})
|
|
146
|
-
continue
|
|
132
|
+
throw new Error(
|
|
133
|
+
"LLM returned empty response (likely reasoning exhausted or output truncated). " +
|
|
134
|
+
"Try lowering reasoning effort if this persists (/think in TUI). " +
|
|
135
|
+
`Provider: ${agent.provider.model}`
|
|
136
|
+
)
|
|
147
137
|
}
|
|
148
|
-
if (depth === 0 && agent.
|
|
149
|
-
agent.
|
|
138
|
+
if (depth === 0 && agent.tasks.some((t) => t.status === "pending")) {
|
|
139
|
+
const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
|
|
150
140
|
agent.history.push({ role: "assistant", content: response.content })
|
|
151
141
|
agent.history.push({
|
|
152
142
|
role: "user",
|
|
153
|
-
content: `[System reminder:
|
|
143
|
+
content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them.]`,
|
|
154
144
|
})
|
|
155
145
|
continue
|
|
156
146
|
}
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
// --- verify guard: push model to verify mutated files before completion ---
|
|
148
|
+
if (depth === 0 && agent.config.verifyGuard === true) {
|
|
149
|
+
if (agent._mutatedThisRun && !agent._verifiedThisRun && guardPushbacks < MAX_VERIFY_PUSHBACKS) {
|
|
150
|
+
guardPushbacks++
|
|
159
151
|
agent.history.push({ role: "assistant", content: response.content })
|
|
160
|
-
|
|
152
|
+
agent.history.push({
|
|
153
|
+
role: "user",
|
|
154
|
+
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply.]",
|
|
155
|
+
})
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
|
|
159
|
+
agent._verifyRetries++
|
|
160
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
161
|
+
agent.history.push({
|
|
162
|
+
role: "user",
|
|
163
|
+
content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
|
|
164
|
+
})
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
|
|
168
|
+
if (honestReminderInjected) {
|
|
169
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
170
|
+
return response.content
|
|
171
|
+
}
|
|
172
|
+
honestReminderInjected = true
|
|
173
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
174
|
+
agent.history.push({
|
|
175
|
+
role: "user",
|
|
176
|
+
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
|
|
177
|
+
})
|
|
178
|
+
continue
|
|
161
179
|
}
|
|
162
|
-
honestReminderInjected = true
|
|
163
|
-
agent.history.push({ role: "assistant", content: response.content })
|
|
164
|
-
agent.history.push({
|
|
165
|
-
role: "user",
|
|
166
|
-
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
|
|
167
|
-
})
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
if (depth === 0 && agent.tasks.some((t) => t.status === "pending")) {
|
|
171
|
-
const pending = agent.tasks.filter((t) => t.status === "pending").map((t) => t.title).join(", ")
|
|
172
|
-
agent.history.push({ role: "assistant", content: response.content })
|
|
173
|
-
agent.history.push({
|
|
174
|
-
role: "user",
|
|
175
|
-
content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them. Never mention this reminder to the user.]`,
|
|
176
|
-
})
|
|
177
|
-
continue
|
|
178
180
|
}
|
|
179
181
|
agent.history.push({ role: "assistant", content: response.content })
|
|
180
182
|
return response.content
|
|
@@ -263,7 +265,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
263
265
|
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
264
266
|
agent.history.push({
|
|
265
267
|
role: "user",
|
|
266
|
-
content: `[System reminder: you have made the identical tool call (${last3[0].slice(0, 120)}) 3 times in a row — you are likely stuck in a loop. Change approach: diagnose the root cause differently, try an alternative, or ask the user.
|
|
268
|
+
content: `[System reminder: you have made the identical tool call (${last3[0].slice(0, 120)}) 3 times in a row — you are likely stuck in a loop. Change approach: diagnose the root cause differently, try an alternative, or ask the user.]`,
|
|
267
269
|
})
|
|
268
270
|
recentCallSigs.length = 0
|
|
269
271
|
}
|
|
@@ -283,41 +285,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
283
285
|
`<untrusted_completion_criterion>${escapeXml(agent.goal.criteria)}</untrusted_completion_criterion>\n` +
|
|
284
286
|
(pct >= GOAL_BUDGET_WARN_RATIO ? `WARNING: ${Math.round(pct * 100)}% of the turn budget is used — avoid starting new discretionary work; finish, or report status to the user.\n` : "") +
|
|
285
287
|
`Completion audit: mark complete only when the criteria's check has actually run and passed — weak or indirect evidence, plans, and summaries are NOT completion.\n` +
|
|
286
|
-
`Blocked audit: report blocked only after the same condition persists across 3 genuine attempts (the goal tool counts)
|
|
287
|
-
`Stay focused. Never mention this reminder to the user.]`,
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Task reminders
|
|
292
|
-
if (depth === 0 && agent._turnsSinceTaskUpdate >= TASK_REMINDER_INTERVAL) {
|
|
293
|
-
const hasIncomplete = agent.tasks.some((t) => t.status !== "done")
|
|
294
|
-
if (agent.tasks.length > 0 && hasIncomplete) {
|
|
295
|
-
const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
|
|
296
|
-
agent.history.push({
|
|
297
|
-
role: "user",
|
|
298
|
-
content: `[System reminder: active task list, last updated ${agent._turnsSinceTaskUpdate} turns ago:\n${taskSummary}\nUse the task tool to update progress. Never mention this reminder to the user.]`,
|
|
299
|
-
})
|
|
300
|
-
} else if (agent.tasks.length === 0) {
|
|
301
|
-
agent.history.push({
|
|
302
|
-
role: "user",
|
|
303
|
-
content: "[System reminder: no task list is being tracked. If the current work is a multi-step task, consider using the task tool to plan and track progress. This is a gentle reminder; ignore it if not applicable. Never mention this reminder to the user.]",
|
|
304
|
-
})
|
|
305
|
-
} else {
|
|
306
|
-
agent.history.push({
|
|
307
|
-
role: "user",
|
|
308
|
-
content: "[System reminder: all tracked tasks are marked done. Use the task tool to clear the list or add new tasks if there's more work. Never mention this reminder to the user.]",
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
agent._turnsSinceTaskUpdate = 0
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Plan mode guidance
|
|
315
|
-
if (agent.planMode && agent._turnsInPlanMode >= PLAN_REMINDER_INTERVAL) {
|
|
316
|
-
agent.history.push({
|
|
317
|
-
role: "user",
|
|
318
|
-
content: "[System reminder: plan mode still active after several turns. Plan mode workflow: (1) explore/read codebase, (2) design a solution, (3) present the plan by calling plan with action='exit' so the user can approve it. If you've explored enough, exit plan mode now. Never mention this reminder to the user.]",
|
|
288
|
+
`Blocked audit: report blocked only after the same condition persists across 3 genuine attempts (the goal tool counts).]`,
|
|
319
289
|
})
|
|
320
|
-
agent._turnsInPlanMode = 0
|
|
321
290
|
}
|
|
322
291
|
|
|
323
292
|
callbacks.onTurnEnd?.(agent, turn)
|
package/src/config.mjs
CHANGED
package/src/context.mjs
CHANGED
|
@@ -48,8 +48,7 @@ Work log:
|
|
|
48
48
|
const COMPACTION_PREFIX =
|
|
49
49
|
"[Context was automatically compacted. Below is a summary of earlier work. " +
|
|
50
50
|
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
51
|
-
"but re-verify transient state
|
|
52
|
-
"Design decisions made earlier may be summarized — if you recall a decision that is missing from the summary, check memory_search or re-examine the code.]\n\n"
|
|
51
|
+
"but re-verify transient state with tools. Check memory_search for any missing decisions.]\n\n"
|
|
53
52
|
|
|
54
53
|
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
55
54
|
export const COMPRESS_FAILURE_LIMIT = 3
|
|
@@ -130,10 +129,6 @@ function applyCompression(agent, headEnd, tailStart, note) {
|
|
|
130
129
|
})
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
// Reset tracking counters (context rebuilt, start counting from scratch)
|
|
134
|
-
agent._turnsSinceTaskUpdate = 0
|
|
135
|
-
agent._turnsInPlanMode = 0
|
|
136
|
-
|
|
137
132
|
// Plan mode compaction: re-inject plan mode guidance
|
|
138
133
|
if (agent.planMode) {
|
|
139
134
|
agent.history.push({
|
package/src/prompts/coder.md
CHANGED
|
@@ -9,12 +9,6 @@ Guidelines:
|
|
|
9
9
|
- Be thorough: include what you did, which files you changed, why, and any caveats
|
|
10
10
|
- If the task is ambiguous, note the ambiguity in your report; do not ask the user
|
|
11
11
|
- It is always OK to say "this is too hard for me." Bad work is worse than no work — you will not be penalized for escalating
|
|
12
|
-
- Before the final review, do a quick quality self-check on the code you wrote:
|
|
13
|
-
1. Is this the simplest solution? Could fewer lines or fewer changes achieve the same result?
|
|
14
|
-
2. Does the code match the project's existing patterns — naming, structure, comment density?
|
|
15
|
-
3. Did you touch files or functions beyond the original task? If so, explain why — necessary consequences of your change are expected, but flag them explicitly
|
|
16
|
-
4. Did the implementation match the task description? Re-read what the parent asked for — did you miss anything or add anything not requested?
|
|
17
|
-
5. Are there edge cases or error paths you missed? If so, note them in your report
|
|
18
12
|
- BEFORE finishing, do a final review of your work:
|
|
19
13
|
1. Run the test suite — confirm all tests pass
|
|
20
14
|
2. If no existing test covers your change, add at least one test
|
|
@@ -14,6 +14,12 @@ Coding discipline (rigor over speed—tokens spent on verification are well spen
|
|
|
14
14
|
- Before finalizing any implementation, pause and think through edge cases: what could go wrong? what happens on failure? what boundary conditions exist? Reason about the failure modes — then handle or document the fallback. "It works on my machine" is not completion.
|
|
15
15
|
- After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
|
|
16
16
|
- Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
|
|
17
|
+
- After completing a batch of edits, pause and self-review:
|
|
18
|
+
1. Is this the simplest solution? Would fewer lines or fewer files do the job?
|
|
19
|
+
2. Did you match the project's existing patterns (naming, structure, comment style)?
|
|
20
|
+
3. Did you change anything unrelated to the task? If so, explain why it was necessary.
|
|
21
|
+
4. Did the implementation match the design? Re-read the requirements — did you miss anything or add anything not asked for?
|
|
22
|
+
5. Do existing tests cover the change? If not, add at least one test — never skip this.
|
|
17
23
|
|
|
18
24
|
Testing discipline (right check at the right time — don't run the full suite for every line change):
|
|
19
25
|
- After every write/edit of .mjs/.js files: call syntax_check immediately — it catches parse errors in milliseconds
|
package/src/prompts/main.md
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
Main-agent
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
Main-agent role — only the top-level agent has these capabilities. Subagents do not.
|
|
2
|
+
|
|
3
|
+
You are the lead engineer: you see the full picture, you coordinate complex work, and you are ultimately responsible for the result.
|
|
4
|
+
|
|
5
|
+
**Your coordination capabilities:**
|
|
6
|
+
|
|
7
|
+
Plan before building — for complex multi-step tasks, enter plan mode first. Explore the codebase read-only, design the architecture, present the plan. When approved, exit plan mode and implement in the same batch — no intermediate task list needed.
|
|
8
|
+
|
|
9
|
+
Delegate well — spawn subagents for independent subtasks. Explore agents for parallel codebase search, plan agents for architecture design, coder agents for self-contained implementation. Delegate breadth-first exploration; do precision edits yourself. Never give parallel subagents tasks that edit the same files. 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
|
+
|
|
11
|
+
Set goals for autonomous work — long-running tasks need a verifiable completion criterion (a machine-checkable proof, not vague effort). Completion claims are audited; declaring blocked requires 3 genuine attempts against the same condition.
|
|
12
|
+
|
|
13
|
+
Load skills when relevant — project skills (.thincoder/skills/) contain reusable workflows and reference material.
|
|
14
|
+
|
|
15
|
+
**How you finish:**
|
|
16
|
+
|
|
17
|
+
After a batch of edits, pause and self-review:
|
|
18
|
+
1. Simplest solution? Fewer lines or files?
|
|
19
|
+
2. Matches existing patterns?
|
|
20
|
+
3. Changed anything unrelated? If so, explain why.
|
|
21
|
+
4. Matches the design? Re-read the requirements — missed anything? Added anything not asked for?
|
|
22
|
+
5. Do tests cover it? If not, add at least one.
|
|
23
|
+
|
|
24
|
+
Then call verify. Run verify after your last edit, not before. If you could not verify, say so explicitly — never present unverified work as done.
|
package/src/prompts/system.md
CHANGED
|
@@ -14,6 +14,7 @@ Communicate fully. Missing information costs far more than extra tokens — cont
|
|
|
14
14
|
- Honesty over saving face: if you can't do something, explain what you tried and what blocked you. Never invent a fake solution, never silently substitute, never hide failure behind something that looks complete.
|
|
15
15
|
|
|
16
16
|
**Rules:**
|
|
17
|
+
- System reminders are messages starting with `[System reminder:]`. They are injected by the framework (not the user), contain authoritative guidance, and you must comply silently — never mention them in your reply.
|
|
17
18
|
- Prefer tool calls over guessing. Read files before modifying them. When in doubt, search more, not less — context is cheap, mistakes are expensive.
|
|
18
19
|
- When you need multiple independent pieces of information (e.g. reading several files), make all independent tool calls in the SAME response so they can run in parallel.
|
|
19
20
|
- When the user asks a question, answer it. When they describe a task, do it. When unsure which they meant, ask before acting — once. Never guess at ambiguous intent.
|
|
@@ -38,4 +39,3 @@ Communicate fully. Missing information costs far more than extra tokens — cont
|
|
|
38
39
|
3. code_search — last. Searches source code by function/class name, JSDoc, or code patterns. Use to find existing implementations, usage examples, or the definition of a symbol you found in repo_outline.
|
|
39
40
|
These three tools together replace blind grep. Use them in order: structure first, then intent, then details.
|
|
40
41
|
- CRITICAL: you are a coding agent, not a student. The code you read may have bugs, outdated patterns, or technical debt — it is the PROBLEM to solve, not a reference to imitate. Read existing code to understand what it does, not to copy how it does it. When something looks wrong, say so. When you see bad patterns, don't propagate them.
|
|
41
|
-
- Some user messages start with [System reminder:]. These are injected by the framework, not written by the user. They contain authoritative guidance. Comply with them silently — never mention them to the user.
|
package/src/session.mjs
CHANGED
|
@@ -214,9 +214,7 @@ export function applySession(agent, data) {
|
|
|
214
214
|
agent.goal = data.goal ?? null
|
|
215
215
|
agent._pendingReminders = data.pendingReminders ?? []
|
|
216
216
|
agent._sessionStart = data.sessionStart ?? null
|
|
217
|
-
// Reset
|
|
218
|
-
agent._turnsSinceTaskUpdate = 0
|
|
219
|
-
agent._turnsInPlanMode = 0
|
|
217
|
+
// Reset stall/compaction state on session switch
|
|
220
218
|
agent._compressFailures = 0
|
|
221
219
|
agent._verifyRetries = 0
|
|
222
220
|
agent._verifyPassed = false
|
package/src/tools/file.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
MAX_READ_LINES,
|
|
5
5
|
gitDiffOne,
|
|
6
6
|
autoSyntaxCheck,
|
|
7
|
-
resolveInCwd
|
|
7
|
+
resolveInCwd,
|
|
8
|
+
resolveExternal,
|
|
8
9
|
} from "./shared.mjs";
|
|
9
10
|
import { mkdir } from "node:fs/promises";
|
|
10
11
|
import { readFile } from "node:fs/promises";
|
|
@@ -25,12 +26,13 @@ export const readTool = {
|
|
|
25
26
|
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
26
27
|
offset: { type: "number", description: "1-based line number to start from" },
|
|
27
28
|
limit: { type: "number", description: `Max lines to return (default ${MAX_READ_LINES})` },
|
|
29
|
+
allowExternal: { type: "boolean", description: "Allow reading files outside the working directory. Only set true when the user explicitly provided an external path — never use this to explore beyond cwd on your own." },
|
|
28
30
|
},
|
|
29
31
|
required: ["path"],
|
|
30
32
|
},
|
|
31
33
|
readonly: true,
|
|
32
34
|
async execute(args, ctx) {
|
|
33
|
-
const abs = resolveInCwd(ctx, args.path)
|
|
35
|
+
const abs = args.allowExternal ? resolveExternal(ctx, args.path) : resolveInCwd(ctx, args.path)
|
|
34
36
|
// Large file guard: check size first, reject reading entire file if >10MB (offset/limit only affect the returned slice, not buffering)
|
|
35
37
|
const st = await stat(abs).catch(() => null)
|
|
36
38
|
if (st && st.size > MAX_FILE_READ_BYTES) throw new Error(`File too large (${Math.round(st.size / 1_000_000)}MB > 10MB limit). Use bash with head/tail or grep for targeted extraction.`)
|
package/src/tools/ls.md
CHANGED
package/src/tools/shared.mjs
CHANGED
|
@@ -169,6 +169,12 @@ export function resolveInCwd(ctx, p) {
|
|
|
169
169
|
return resolved
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** Resolve a path relative to cwd without boundary check — use only when the user explicitly provides an external path */
|
|
173
|
+
export function resolveExternal(ctx, p) {
|
|
174
|
+
const cwd = realCwd(ctx.cwd)
|
|
175
|
+
return resolve(cwd, p)
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
/** Coarse segmentation for destructive pre-check (also splits on > >> < so destructive detection still works through redirection) */
|
|
173
179
|
export function shellSegments(command) {
|
|
174
180
|
return command.split(/&&|\|\||>>|\$\(|[;|\n<>]|`|[(]/)
|
package/src/tui/ansi.mjs
CHANGED
package/src/tui/cmd-auto.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
|
|
3
1
|
/** /auto command: toggle auto-approve mode.
|
|
4
|
-
* ctx: { agent
|
|
2
|
+
* ctx: { agent } */
|
|
5
3
|
export async function handleAutoCommand(ctx) {
|
|
6
|
-
const { agent
|
|
4
|
+
const { agent } = ctx
|
|
7
5
|
agent.autoApprove = !agent.autoApprove
|
|
8
6
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
9
7
|
if (agent.autoApprove) {
|
|
@@ -11,11 +9,4 @@ export async function handleAutoCommand(ctx) {
|
|
|
11
9
|
} else {
|
|
12
10
|
agent._pendingReminders.push("[System reminder: AUTO mode is now OFF. Destructive tool calls now require user approval again. Confirm before writing files, running commands, or spawning subagents.]")
|
|
13
11
|
}
|
|
14
|
-
pushLabel(`❯ Auto`, ansi.bold + (agent.autoApprove ? C.warn : C.tool))
|
|
15
|
-
pushLine(
|
|
16
|
-
agent.autoApprove
|
|
17
|
-
? `AUTO ON: all tool calls (write/bash/subagent) auto-approved. For long tasks. /auto to disable.`
|
|
18
|
-
: `AUTO OFF: destructive tool calls require per-use approval again.`,
|
|
19
|
-
agent.autoApprove ? C.warn : C.dim,
|
|
20
|
-
)
|
|
21
12
|
}
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -1,30 +1,46 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
|
-
/** /config command:
|
|
4
|
+
/** /config command: view and set agent/embedding config.
|
|
5
5
|
* Extracted from slash-commands.mjs.
|
|
6
6
|
* ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
|
|
7
7
|
export async function handleConfigCommand(ctx) {
|
|
8
8
|
const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
|
|
9
9
|
const { configPath } = await import("../config.mjs")
|
|
10
|
-
const cp = configPath
|
|
11
10
|
const ac = agent.config?.agent ?? {}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
11
|
+
const ec = agent.config?.embedding ?? {}
|
|
12
|
+
|
|
13
|
+
function cfgSummary() {
|
|
14
|
+
const tn = `${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`
|
|
15
|
+
const vg = ac.verifyGuard === true ? "on" : "off"
|
|
16
|
+
return `agent.maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn} | verifyGuard=${vg} | embedding=${agent.memory?.embedder ? "on" : "off"}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mainEntries = [
|
|
20
|
+
{ type: "header", text: `Config: ${cfgSummary()}` },
|
|
21
|
+
{ type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
|
|
22
|
+
{ type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
|
|
23
|
+
{ type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
|
|
24
|
+
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
25
|
+
{ type: "item", text: "Set embedding API key", action: "embedkey" },
|
|
26
|
+
{ type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
|
|
27
|
+
{ type: "item", text: "View full config", action: "view" },
|
|
16
28
|
]
|
|
29
|
+
|
|
17
30
|
openPicker({
|
|
18
31
|
title: "Config",
|
|
19
|
-
entries,
|
|
32
|
+
entries: mainEntries,
|
|
20
33
|
onSelect: async (e) => {
|
|
21
34
|
if (e.action === "view") {
|
|
35
|
+
const cp = configPath
|
|
22
36
|
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
23
37
|
pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
|
|
24
38
|
pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
|
|
25
|
-
|
|
26
|
-
pushLine(`agent:
|
|
27
|
-
pushLine(`
|
|
39
|
+
pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
|
|
40
|
+
pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
|
|
41
|
+
pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
|
|
42
|
+
pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
|
|
43
|
+
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
|
|
28
44
|
pushLine(`Config file: ${cp}`, C.dim)
|
|
29
45
|
return
|
|
30
46
|
}
|
|
@@ -42,30 +58,99 @@ export async function handleConfigCommand(ctx) {
|
|
|
42
58
|
pushLine(`Embedding key saved, vector search enabled`, C.tool)
|
|
43
59
|
return
|
|
44
60
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const parts = settext.split(/\s+/)
|
|
49
|
-
const [path, value] = [parts[0], parts.slice(1).join(" ")]
|
|
50
|
-
if (!path || !value) { pushLine("Usage: <path> <value> e.g. agent.maxTurns 80", C.error); return }
|
|
61
|
+
// Boolean toggle: agent.verifyGuard
|
|
62
|
+
if (e.action === "agent.verifyGuard") {
|
|
63
|
+
const newVal = ac.verifyGuard !== true // toggle: undefined/false → true, true → false
|
|
51
64
|
try {
|
|
52
|
-
const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
|
|
53
65
|
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
|
|
66
|
+
raw.agent ??= {}
|
|
67
|
+
raw.agent.verifyGuard = newVal
|
|
68
|
+
const { saveConfig, loadConfig } = await import("../config.mjs")
|
|
58
69
|
saveConfig(raw)
|
|
59
70
|
const cfg = loadConfig()
|
|
60
71
|
agent.provider = cfg.provider
|
|
61
72
|
agent.providers = cfg.providersList
|
|
62
73
|
agent.activeProvider = cfg.activeProvider
|
|
63
74
|
agent.config = cfg
|
|
75
|
+
agent.config.agent ??= {}
|
|
64
76
|
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
65
|
-
pushLine(`
|
|
77
|
+
pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
|
|
66
78
|
} catch (error) {
|
|
67
79
|
pushLine(`Save failed: ${error.message}`, C.error)
|
|
68
80
|
}
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
// embedding.model picker
|
|
84
|
+
if (e.action === "embedding.model") {
|
|
85
|
+
const models = [
|
|
86
|
+
{ label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
|
|
87
|
+
{ label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
|
|
88
|
+
{ label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
|
|
89
|
+
{ label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
|
|
90
|
+
{ label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
|
|
91
|
+
]
|
|
92
|
+
const currentVal = ec.model ?? "BAAI/bge-m3"
|
|
93
|
+
openPicker({
|
|
94
|
+
title: `Embedding model (current: ${currentVal})`,
|
|
95
|
+
entries: [
|
|
96
|
+
{ type: "header", text: `Current: ${currentVal}` },
|
|
97
|
+
...models.map((m) => ({ type: "item", text: m.label, action: m.value })),
|
|
98
|
+
],
|
|
99
|
+
onSelect: async (sel) => {
|
|
100
|
+
try {
|
|
101
|
+
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
102
|
+
raw.embedding ??= {}
|
|
103
|
+
raw.embedding.model = sel.action
|
|
104
|
+
const { saveConfig, loadConfig } = await import("../config.mjs")
|
|
105
|
+
saveConfig(raw)
|
|
106
|
+
const cfg = loadConfig()
|
|
107
|
+
agent.provider = cfg.provider
|
|
108
|
+
agent.providers = cfg.providersList
|
|
109
|
+
agent.activeProvider = cfg.activeProvider
|
|
110
|
+
agent.config = cfg
|
|
111
|
+
agent.config.agent ??= {}
|
|
112
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
113
|
+
pushLine(`embedding.model = ${sel.action}`, C.tool)
|
|
114
|
+
} catch (error) {
|
|
115
|
+
pushLine(`Save failed: ${error.message}`, C.error)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
// Numeric config items: ask for value, parse as number
|
|
122
|
+
const isNumeric = e.action.startsWith("agent.")
|
|
123
|
+
const label = e.action
|
|
124
|
+
const current = e.action === "agent.maxTurns" ? (ac.maxTurns ?? 100)
|
|
125
|
+
: e.action === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
|
|
126
|
+
: e.action === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
|
|
127
|
+
: ""
|
|
128
|
+
const prompt = `${label} (current: ${current}):`
|
|
129
|
+
const val = await askQuestion(prompt)
|
|
130
|
+
if (!val) return
|
|
131
|
+
try {
|
|
132
|
+
const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
|
|
133
|
+
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
134
|
+
if (isNumeric) {
|
|
135
|
+
const keys = label.split(".")
|
|
136
|
+
let obj = raw
|
|
137
|
+
for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
|
|
138
|
+
const num = Number(val)
|
|
139
|
+
if (isNaN(num)) { pushLine("Value must be a number", C.error); return }
|
|
140
|
+
obj[keys[keys.length - 1]] = num
|
|
141
|
+
}
|
|
142
|
+
saveConfig(raw)
|
|
143
|
+
const cfg = loadConfig()
|
|
144
|
+
agent.provider = cfg.provider
|
|
145
|
+
agent.providers = cfg.providersList
|
|
146
|
+
agent.activeProvider = cfg.activeProvider
|
|
147
|
+
agent.config = cfg
|
|
148
|
+
agent.config.agent ??= {}
|
|
149
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
150
|
+
pushLine(`${label} = ${val}`, C.tool)
|
|
151
|
+
pushLine("(restart to apply to existing agent state)", C.dim)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
pushLine(`Save failed: ${error.message}`, C.error)
|
|
69
154
|
}
|
|
70
155
|
},
|
|
71
156
|
})
|
package/src/tui/cmd-extract.mjs
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { C } from "./ansi.mjs"
|
|
2
|
+
|
|
1
3
|
/** /extract command: extract knowledge from current session.
|
|
2
|
-
* ctx: { runDistill } */
|
|
4
|
+
* ctx: { runDistill, state, pushLine } */
|
|
3
5
|
export async function handleExtractCommand(ctx) {
|
|
4
|
-
|
|
6
|
+
const { runDistill, state, pushLine } = ctx
|
|
7
|
+
pushLine("[extract] Analyzing session...", C.dim)
|
|
8
|
+
const count = await runDistill()
|
|
9
|
+
const msg = count > 0
|
|
10
|
+
? `Knowledge extracted: ${count} candidate(s) saved to memory (use /skills to list, agent will recall via memory_search)`
|
|
11
|
+
: "No new knowledge found in this session."
|
|
12
|
+
pushLine(msg, count > 0 ? C.tool : C.dim)
|
|
5
13
|
}
|
package/src/tui/cmd-goal.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
|
|
3
1
|
/** /goal command: set/view/cancel long-term goal.
|
|
4
2
|
* Extracted from slash-commands.mjs.
|
|
5
3
|
* ctx: { agent, pushLine, pushLabel, openPicker, askQuestion } */
|
|
@@ -19,7 +17,6 @@ export async function handleGoalCommand(ctx) {
|
|
|
19
17
|
onSelect: async (e) => {
|
|
20
18
|
if (e.action === "view") {
|
|
21
19
|
const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
|
|
22
|
-
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
23
20
|
pushLine(`Goal: ${agent.goal.objective}`, C.tool)
|
|
24
21
|
if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
|
|
25
22
|
pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
@@ -27,8 +24,6 @@ export async function handleGoalCommand(ctx) {
|
|
|
27
24
|
}
|
|
28
25
|
if (e.action === "cancel") {
|
|
29
26
|
agent.goal = null
|
|
30
|
-
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
31
|
-
pushLine(`Goal cancelled.`, C.dim)
|
|
32
27
|
return
|
|
33
28
|
}
|
|
34
29
|
// set — requires entering goal text
|
|
@@ -38,10 +33,6 @@ export async function handleGoalCommand(ctx) {
|
|
|
38
33
|
const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
|
|
39
34
|
const criteria = semi ? goalText.slice(semi + 1).trim() : ""
|
|
40
35
|
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
41
|
-
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
42
|
-
pushLine(`Goal set: ${objective}`, C.tool)
|
|
43
|
-
if (criteria) pushLine(` Criteria: ${criteria}`, C.dim)
|
|
44
|
-
else pushLine(` ⚠ No criteria provided — the agent will determine its own criteria as it works on this goal`, C.warn)
|
|
45
36
|
},
|
|
46
37
|
})
|
|
47
38
|
}
|
package/src/tui/cmd-help.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
5
5
|
export async function handleHelpCommand(ctx) {
|
|
6
6
|
const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
|
|
7
7
|
const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
|
|
8
|
-
const order = ["Agent", "Session", "
|
|
8
|
+
const order = ["Agent", "Session", "Project", "System"]
|
|
9
9
|
const byGroup = new Map()
|
|
10
10
|
for (const c of SLASH_COMMANDS) {
|
|
11
11
|
if (!c.group) continue
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -120,26 +120,39 @@ export async function handleMcpCommand(ctx) {
|
|
|
120
120
|
return
|
|
121
121
|
}
|
|
122
122
|
if (e.action === "add") {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
// Pick transport type first, then ask name + URL/command
|
|
124
|
+
openPicker({
|
|
125
|
+
title: "MCP Transport",
|
|
126
|
+
entries: [
|
|
127
|
+
{ type: "header", text: "Select server transport" },
|
|
128
|
+
{ type: "item", text: "HTTP (https://…)", action: "http" },
|
|
129
|
+
{ type: "item", text: "WebSocket (ws://…)", action: "ws" },
|
|
130
|
+
{ type: "item", text: "stdio (local command)", action: "stdio" },
|
|
131
|
+
],
|
|
132
|
+
onSelect: async (te) => {
|
|
133
|
+
const name = await askQuestion("Server name:")
|
|
134
|
+
if (!name) return
|
|
135
|
+
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
136
|
+
if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
|
|
137
|
+
if (te.action === "stdio") {
|
|
138
|
+
const cmd = await askQuestion("Command (e.g. npx, python):")
|
|
139
|
+
if (!cmd) return
|
|
140
|
+
const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
|
|
141
|
+
const args = argsInput ? argsInput.split(/\s+/) : undefined
|
|
142
|
+
await addAndConnect(ctx, { name, command: cmd, args })
|
|
143
|
+
} else {
|
|
144
|
+
const urlPrompt = te.action === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
|
|
145
|
+
const url = await askQuestion(urlPrompt)
|
|
146
|
+
if (!url) return
|
|
147
|
+
const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
|
|
148
|
+
const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
|
|
149
|
+
const srv = te.action === "ws"
|
|
150
|
+
? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
151
|
+
: { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
152
|
+
await addAndConnect(ctx, srv)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
})
|
|
143
156
|
}
|
|
144
157
|
},
|
|
145
158
|
})
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -2,17 +2,37 @@ import { clearSession } from "../session.mjs"
|
|
|
2
2
|
import { C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
4
|
/** /new command: start new session (old session archived to slot).
|
|
5
|
-
* ctx: { agent, state, pushLine } */
|
|
5
|
+
* ctx: { agent, state, pushLine, openPicker, render } */
|
|
6
6
|
export async function handleNewCommand(ctx) {
|
|
7
|
-
const { agent, state, pushLine } = ctx
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const { agent, state, pushLine, openPicker, render } = ctx
|
|
8
|
+
|
|
9
|
+
const doNewSession = () => {
|
|
10
|
+
agent.history = []
|
|
11
|
+
agent.tasks = []
|
|
12
|
+
agent.planMode = false
|
|
13
|
+
agent.goal = null
|
|
14
|
+
agent._pendingReminders = []
|
|
15
|
+
state.tasks = []
|
|
16
|
+
state.lines = []
|
|
17
|
+
state.streaming = ""
|
|
18
|
+
clearSession(agent.cwd)
|
|
19
|
+
render()
|
|
20
|
+
pushLine("New session started (old session archived to slot; /session to view)", C.dim)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (agent.history.length > 0) {
|
|
24
|
+
openPicker({
|
|
25
|
+
title: "Start new session?",
|
|
26
|
+
entries: [
|
|
27
|
+
{ type: "item", text: "Yes, archive current and start new", action: "yes" },
|
|
28
|
+
{ type: "item", text: "Cancel", action: "no" },
|
|
29
|
+
],
|
|
30
|
+
defaultIndex: 1,
|
|
31
|
+
onSelect: (e) => {
|
|
32
|
+
if (e.action === "yes") doNewSession()
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
doNewSession()
|
|
18
38
|
}
|
package/src/tui/cmd-plan.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
|
|
3
1
|
/** /plan command: toggle plan mode (read-only explore → design → implement).
|
|
4
|
-
* ctx: { agent
|
|
2
|
+
* ctx: { agent } */
|
|
5
3
|
export async function handlePlanCommand(ctx) {
|
|
6
|
-
const { agent
|
|
4
|
+
const { agent } = ctx
|
|
7
5
|
agent.planMode = !agent.planMode
|
|
8
6
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
9
7
|
if (agent.planMode) {
|
|
@@ -11,11 +9,4 @@ export async function handlePlanCommand(ctx) {
|
|
|
11
9
|
} else {
|
|
12
10
|
agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes.]")
|
|
13
11
|
}
|
|
14
|
-
pushLabel(`❯ Plan`, ansi.bold + (agent.planMode ? C.tool : C.dim))
|
|
15
|
-
pushLine(
|
|
16
|
-
agent.planMode
|
|
17
|
-
? `Plan mode ON: read-only tools only. Design first, then implement. /plan again to exit.`
|
|
18
|
-
: `Plan mode OFF: you may now edit files and run commands.`,
|
|
19
|
-
agent.planMode ? C.tool : C.dim,
|
|
20
|
-
)
|
|
21
12
|
}
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
|
|
3
1
|
/** /think command: toggle thinking mode, set reasoning effort.
|
|
4
2
|
* Extracted from slash-commands.mjs.
|
|
5
|
-
* ctx: { agent,
|
|
3
|
+
* ctx: { agent, openPicker, syncProviderField } */
|
|
6
4
|
export async function handleThinkCommand(ctx) {
|
|
7
|
-
const { agent,
|
|
5
|
+
const { agent, openPicker, syncProviderField } = ctx
|
|
8
6
|
const cur = agent.provider
|
|
9
7
|
const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
|
|
10
8
|
const { specForModel } = await import("../config.mjs")
|
|
@@ -30,8 +28,6 @@ export async function handleThinkCommand(ctx) {
|
|
|
30
28
|
if (e.action === "effort") {
|
|
31
29
|
cur.reasoningEffort = e.level
|
|
32
30
|
await syncProviderField("reasoningEffort", e.level)
|
|
33
|
-
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
34
|
-
pushLine(`Reasoning effort set to ${e.level}`, C.tool)
|
|
35
31
|
} else {
|
|
36
32
|
const enable = e.action === "on"
|
|
37
33
|
if (isEffortOnly) {
|
|
@@ -47,9 +43,6 @@ export async function handleThinkCommand(ctx) {
|
|
|
47
43
|
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
48
44
|
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
49
45
|
}
|
|
50
|
-
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
51
|
-
pushLine(`Thinking mode ${enable ? "On" : "Off"}`, C.tool)
|
|
52
|
-
if (enable) pushLine(`Reasoning effort: ${cur.reasoningEffort}`, C.dim)
|
|
53
46
|
}
|
|
54
47
|
},
|
|
55
48
|
})
|
package/src/tui/distill-cmd.mjs
CHANGED
package/src/tui/key-handler.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export function createKeyHandler(ctx) {
|
|
|
43
43
|
if (q.options.length > 0) {
|
|
44
44
|
// options mode: ↑↓ select, Enter confirm, Esc cancel
|
|
45
45
|
if (key.name === "escape") {
|
|
46
|
-
q.resolve("
|
|
46
|
+
q.resolve("")
|
|
47
47
|
state.question = null
|
|
48
48
|
state.status = "Processing..."
|
|
49
49
|
render()
|
|
@@ -64,13 +64,13 @@ export function createKeyHandler(ctx) {
|
|
|
64
64
|
} else {
|
|
65
65
|
// free text: type answer, Enter submit, Esc cancel
|
|
66
66
|
if (key.name === "escape") {
|
|
67
|
-
q.resolve("
|
|
67
|
+
q.resolve("")
|
|
68
68
|
state.question = null
|
|
69
69
|
state.status = "Processing..."
|
|
70
70
|
render()
|
|
71
71
|
} else if (key.name === "return") {
|
|
72
72
|
const answer = (q.answer ?? "").trim()
|
|
73
|
-
q.resolve(answer || "
|
|
73
|
+
q.resolve(answer || "")
|
|
74
74
|
state.question = null
|
|
75
75
|
state.status = "Processing..."
|
|
76
76
|
pushLine(` → ${answer || "(empty)"}`, C.tool)
|
|
@@ -33,18 +33,18 @@ export const SLASH_COMMANDS = [
|
|
|
33
33
|
{ name: "/model", group: "Agent", desc: "select model & manage providers" },
|
|
34
34
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
35
35
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
36
|
-
{ name: "/
|
|
37
|
-
{ name: "/skills", group: "Tools", desc: "list project skills" },
|
|
38
|
-
{ name: "/mcp", group: "Tools", desc: "manage MCP servers" },
|
|
39
|
-
{ name: "/config", group: "Config", desc: "config management (embedding / agent)" },
|
|
40
|
-
{ name: "/reindex", group: "Config", desc: "rebuild memory index" },
|
|
36
|
+
{ name: "/config", group: "Agent", desc: "config management (embedding / agent)" },
|
|
41
37
|
{ name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
|
|
42
38
|
{ name: "/session", group: "Session", desc: "list/switch archived sessions" },
|
|
43
39
|
{ name: "/clear", group: "Session", desc: "clear screen" },
|
|
44
40
|
{ name: "/extract", group: "Session", desc: "extract knowledge from session" },
|
|
45
|
-
{ name: "/
|
|
46
|
-
{ name: "/
|
|
47
|
-
{ name: "/
|
|
41
|
+
{ name: "/init", group: "Project", desc: "generate project AGENTS.md skeleton" },
|
|
42
|
+
{ name: "/skills", group: "Project", desc: "list project skills" },
|
|
43
|
+
{ name: "/mcp", group: "Project", desc: "manage MCP servers" },
|
|
44
|
+
{ name: "/reindex", group: "Project", desc: "rebuild memory index" },
|
|
45
|
+
{ name: "/restore", group: "Project", desc: "restore checkpoint" },
|
|
46
|
+
{ name: "/exit", group: "System", desc: "exit" },
|
|
47
|
+
{ name: "/help", group: "System", desc: "this list" },
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
/** Command → handler mapping table */
|