thincoder 0.10.0 → 0.11.1
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 +1 -1
- package/package.json +1 -1
- package/src/advisor.mjs +360 -72
- package/src/agent/helpers.mjs +7 -3
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/advisor.mjs +36 -0
- package/src/agent-tools/plan.mjs +53 -2
- package/src/agent-tools/subagent.mjs +7 -1
- package/src/agent-tools/timer.mjs +1 -1
- package/src/agent-tools/verify.mjs +1 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +73 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +7 -0
- package/src/config.mjs +47 -20
- package/src/prompts/advisor-round1.md +23 -0
- package/src/prompts/advisor-round2.md +26 -0
- package/src/prompts/advisor-round3.md +24 -0
- package/src/prompts/coder.md +6 -2
- package/src/prompts/discipline.md +23 -6
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +13 -6
- package/src/provider/anthropic.mjs +190 -0
- package/src/provider/core.mjs +42 -130
- package/src/provider/google.mjs +199 -0
- package/src/provider/sse.mjs +112 -0
- package/src/proxy.mjs +236 -0
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/fetch.md +2 -1
- package/src/tools/git.mjs +125 -156
- package/src/tools/index.mjs +9 -9
- package/src/tools/linter.mjs +46 -32
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +16 -0
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +115 -89
- package/src/tools/websearch.md +5 -3
- package/src/tui/agent-turn.mjs +86 -75
- package/src/tui/cmd-advisor.mjs +138 -49
- package/src/tui/cmd-clear.mjs +11 -17
- package/src/tui/cmd-config.mjs +226 -142
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-fold.mjs +2 -3
- package/src/tui/cmd-goal.mjs +58 -27
- package/src/tui/cmd-help.mjs +3 -1
- package/src/tui/cmd-mcp.mjs +178 -142
- package/src/tui/cmd-model.mjs +15 -4
- package/src/tui/cmd-new.mjs +7 -13
- package/src/tui/cmd-restore.mjs +12 -16
- package/src/tui/cmd-session.mjs +28 -32
- package/src/tui/cmd-think.mjs +75 -50
- package/src/tui/cmd-undo.mjs +19 -23
- package/src/tui/cmd-upgrade.mjs +22 -26
- package/src/tui/index.mjs +61 -215
- package/src/tui/key-handler.mjs +56 -20
- package/src/tui/layout.mjs +13 -3
- package/src/tui/pickers.mjs +151 -182
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +56 -114
- package/src/tui/render-loop.mjs +181 -0
- package/src/tui/slash-commands.mjs +26 -16
package/src/tui/cmd-fold.mjs
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { C } from "./ansi.mjs"
|
|
5
5
|
|
|
6
|
-
export async function handleFoldCommand(ctx) {
|
|
6
|
+
export async function handleFoldCommand(ctx, args = []) {
|
|
7
7
|
const { state } = ctx
|
|
8
|
-
const
|
|
9
|
-
const arg = text.split(/\s+/)[1]
|
|
8
|
+
const arg = args[0]?.toLowerCase()
|
|
10
9
|
if (arg === "on") {
|
|
11
10
|
state.foldEnabled = true
|
|
12
11
|
ctx.pushLine("Folding: on (long tool results are collapsed)", C.dim)
|
package/src/tui/cmd-goal.mjs
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
+
import { C } from "./ansi.mjs"
|
|
2
|
+
|
|
1
3
|
/** /goal command: set/view/cancel long-term goal.
|
|
2
4
|
* Extracted from slash-commands.mjs.
|
|
3
|
-
* ctx: { agent, pushLine, pushLabel,
|
|
4
|
-
export async function handleGoalCommand(ctx) {
|
|
5
|
-
const { agent, pushLine, pushLabel,
|
|
5
|
+
* ctx: { agent, pushLine, pushLabel, showPicker, askQuestion } */
|
|
6
|
+
export async function handleGoalCommand(ctx, args = []) {
|
|
7
|
+
const { agent, pushLine, pushLabel, showPicker, askQuestion } = ctx
|
|
8
|
+
|
|
9
|
+
function setGoal(goalText) {
|
|
10
|
+
const semiIdx = goalText.indexOf(";") >= 0 ? goalText.indexOf(";") : goalText.indexOf(";")
|
|
11
|
+
const objective = semiIdx >= 0 ? goalText.slice(0, semiIdx).trim() : goalText.trim()
|
|
12
|
+
const criteria = semiIdx >= 0 ? goalText.slice(semiIdx + 1).trim() : ""
|
|
13
|
+
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function viewGoal() {
|
|
17
|
+
const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
|
|
18
|
+
pushLine(`Goal: ${agent.goal.objective}`, C.tool)
|
|
19
|
+
if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
|
|
20
|
+
pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Direct args: /goal set <text> │ /goal cancel │ /goal view
|
|
24
|
+
const sub = args[0]?.toLowerCase()
|
|
25
|
+
if (sub === "set") {
|
|
26
|
+
const goalText = args.slice(1).join(" ")
|
|
27
|
+
if (!goalText) { pushLine("Usage: /goal set <objective>[; criteria]", C.error); return }
|
|
28
|
+
setGoal(goalText)
|
|
29
|
+
pushLine(`Goal set: ${agent.goal.objective}`, C.tool)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (sub === "cancel") {
|
|
33
|
+
if (!agent.goal) { pushLine("No goal set", C.dim); return }
|
|
34
|
+
agent.goal = null
|
|
35
|
+
pushLine("Goal cancelled", C.tool)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (sub === "view") {
|
|
39
|
+
if (!agent.goal) { pushLine("No goal set", C.dim); return }
|
|
40
|
+
viewGoal()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (sub) { pushLine("Usage: /goal [set <text>|cancel|view]", C.error); return }
|
|
44
|
+
|
|
6
45
|
const entries = [
|
|
7
46
|
{ type: "header", text: agent.goal ? `Current goal: ${agent.goal.objective.slice(0, 60)}` : "Actions" },
|
|
8
47
|
{ type: "item", text: "Set new goal", action: "set" },
|
|
@@ -11,28 +50,20 @@ export async function handleGoalCommand(ctx) {
|
|
|
11
50
|
entries.push({ type: "item", text: "Cancel goal", action: "cancel" })
|
|
12
51
|
entries.push({ type: "item", text: "View details", action: "view" })
|
|
13
52
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const goalText = await askQuestion("Enter goal description (; separates criteria)")
|
|
31
|
-
if (!goalText) return
|
|
32
|
-
const semi = goalText.indexOf(";") >= 0 ? ";" : goalText.indexOf(";") >= 0 ? ";" : null
|
|
33
|
-
const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
|
|
34
|
-
const criteria = semi ? goalText.slice(semi + 1).trim() : ""
|
|
35
|
-
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
36
|
-
},
|
|
37
|
-
})
|
|
53
|
+
// 先 await picker 返回(选中即关闭),再 askQuestion —— 两者不共存
|
|
54
|
+
const e = await showPicker("Goal", entries)
|
|
55
|
+
if (!e) return
|
|
56
|
+
if (e.action === "view") {
|
|
57
|
+
viewGoal()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (e.action === "cancel") {
|
|
61
|
+
agent.goal = null
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
// set — requires entering goal text
|
|
65
|
+
const goalText = await askQuestion("Enter goal description (; separates criteria)")
|
|
66
|
+
if (!goalText) return
|
|
67
|
+
setGoal(goalText)
|
|
68
|
+
pushLine(`Goal set: ${agent.goal.objective}`, C.tool) // 与直参路径口径一致
|
|
38
69
|
}
|
package/src/tui/cmd-help.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { SLASH_ALIASES } from "./slash-commands.mjs"
|
|
2
3
|
|
|
3
4
|
/** /help command: list all slash commands and aliases.
|
|
4
5
|
* ctx: { pushLine, pushLabel, SLASH_COMMANDS } */
|
|
5
6
|
export async function handleHelpCommand(ctx) {
|
|
6
7
|
const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
|
|
7
|
-
|
|
8
|
+
// reverse the shared alias table: command → alias
|
|
9
|
+
const aliasList = Object.fromEntries(Object.entries(SLASH_ALIASES).map(([alias, cmd]) => [cmd, alias]))
|
|
8
10
|
const order = ["Agent", "Session", "Project", "System"]
|
|
9
11
|
const byGroup = new Map()
|
|
10
12
|
for (const c of SLASH_COMMANDS) {
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
2
2
|
|
|
3
3
|
/** /mcp command handler: view/add/remove/reconnect MCP server.
|
|
4
4
|
* Extracted from slash-commands.mjs, includes /mcp-specific parseHeaders / addAndConnect helpers.
|
|
5
|
-
* ctx: { agent, pushLine, pushLabel,
|
|
5
|
+
* ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C } */
|
|
6
6
|
|
|
7
7
|
function parseHeaders(pairs) {
|
|
8
8
|
const headers = {}
|
|
@@ -41,106 +41,62 @@ async function addAndConnect(ctx, srv) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export async function handleMcpCommand(ctx) {
|
|
45
|
-
const { agent, pushLine, pushLabel,
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
export async function handleMcpCommand(ctx, args = []) {
|
|
45
|
+
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw } = ctx
|
|
46
|
+
// 每轮重读:原本无 mcp 配置时 `?? []` 会拿到游离数组,Add server 后快照过期
|
|
47
|
+
const getServers = () => agent.config?.mcp?.servers ?? []
|
|
48
|
+
|
|
49
|
+
function listServers() {
|
|
50
|
+
const servers = getServers()
|
|
51
|
+
pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
|
|
52
|
+
if (servers.length === 0) {
|
|
53
|
+
pushLine(" (no MCP server configured)", C.dim)
|
|
54
|
+
}
|
|
55
|
+
for (const srv of servers) {
|
|
56
|
+
const connected = agent.tools.some((t) => t._mcpName === srv.name)
|
|
57
|
+
const mark = connected ? "●" : "○"
|
|
58
|
+
const color = connected ? C.tool : C.dim
|
|
59
|
+
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
60
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
61
|
+
pushLine(` ${mark} ${srv.name} (${desc})${connected ? ` — ${toolCount} tools` : ""}`, color)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function removeServer(name) {
|
|
66
|
+
agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
|
|
67
|
+
await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
|
|
68
|
+
// Remove from tool list
|
|
69
|
+
const { removeMcpTools } = await import("../mcp.mjs")
|
|
70
|
+
removeMcpTools(agent, name)
|
|
71
|
+
pushLine(`[mcp] ${name} removed`, C.tool)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function connectServer(name) {
|
|
75
|
+
const srv = getServers().find((s) => s.name === name)
|
|
76
|
+
const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
|
|
77
|
+
removeMcpTools(agent, name)
|
|
78
|
+
try {
|
|
79
|
+
pushLine(`[mcp] Reconnecting ${name}...`, C.dim)
|
|
80
|
+
const tools = await connectMcpServer(srv)
|
|
81
|
+
agent.tools.push(...tools)
|
|
82
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
83
|
+
pushLine(`${name} reconnected, ${tools.length} tools available.`, C.tool)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
pushLine(`[mcp] ${name}: ${error.message}`, C.error)
|
|
86
|
+
}
|
|
57
87
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const color = connected ? C.tool : C.dim
|
|
71
|
-
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
72
|
-
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
73
|
-
pushLine(` ${mark} ${srv.name} (${desc})${connected ? ` — ${toolCount} tools` : ""}`, color)
|
|
74
|
-
}
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
if (e.action === "remove") {
|
|
78
|
-
const removeEntries = [
|
|
79
|
-
{ type: "header", text: "Select server to remove" },
|
|
80
|
-
...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
|
|
81
|
-
]
|
|
82
|
-
openPicker({
|
|
83
|
-
title: "Remove MCP Server",
|
|
84
|
-
entries: removeEntries,
|
|
85
|
-
onSelect: async (se) => {
|
|
86
|
-
// Remove from config
|
|
87
|
-
agent.config.mcp.servers = servers.filter((s) => s.name !== se.name)
|
|
88
|
-
await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
|
|
89
|
-
// Remove from tool list
|
|
90
|
-
const { removeMcpTools } = await import("../mcp.mjs")
|
|
91
|
-
removeMcpTools(agent, se.name)
|
|
92
|
-
pushLine(`[mcp] ${se.name} removed`, C.tool)
|
|
93
|
-
},
|
|
94
|
-
})
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
if (e.action === "connect") {
|
|
98
|
-
const connectEntries = [
|
|
99
|
-
{ type: "header", text: "Select server to reconnect" },
|
|
100
|
-
...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
|
|
101
|
-
]
|
|
102
|
-
openPicker({
|
|
103
|
-
title: "Reconnect MCP",
|
|
104
|
-
entries: connectEntries,
|
|
105
|
-
onSelect: async (se) => {
|
|
106
|
-
const srv = servers.find((s) => s.name === se.name)
|
|
107
|
-
const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
|
|
108
|
-
removeMcpTools(agent, se.name)
|
|
109
|
-
try {
|
|
110
|
-
pushLine(`[mcp] Reconnecting ${se.name}...`, C.dim)
|
|
111
|
-
const tools = await connectMcpServer(srv)
|
|
112
|
-
agent.tools.push(...tools)
|
|
113
|
-
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
114
|
-
pushLine(`${se.name} reconnected, ${tools.length} tools available.`, C.tool)
|
|
115
|
-
} catch (error) {
|
|
116
|
-
pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
})
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
if (e.action === "add") {
|
|
123
|
-
// Pick transport type first, then ask name + URL/command
|
|
124
|
-
openPicker({
|
|
125
|
-
title: "MCP Transport",
|
|
126
|
-
entries: [
|
|
127
|
-
{ type: "header", text: "Select transport or use AI assist" },
|
|
128
|
-
{ type: "item", text: "🤖 Describe with AI — natural language → config", action: "ai" },
|
|
129
|
-
{ type: "item", text: "HTTP (https://…)", action: "http" },
|
|
130
|
-
{ type: "item", text: "WebSocket (ws://…)", action: "ws" },
|
|
131
|
-
{ type: "item", text: "stdio (local command)", action: "stdio" },
|
|
132
|
-
],
|
|
133
|
-
onSelect: async (te) => {
|
|
134
|
-
if (te.action === "ai") {
|
|
135
|
-
const description = await askQuestion("Describe the MCP server you want to add (e.g. 'a filesystem server that gives access to /tmp'):")
|
|
136
|
-
if (!description) return
|
|
137
|
-
pushLine("[mcp] Generating config from description...", C.dim)
|
|
138
|
-
try {
|
|
139
|
-
const { chat } = await import("../provider/index.mjs")
|
|
140
|
-
const res = await chat(agent.provider, {
|
|
141
|
-
messages: [{
|
|
142
|
-
role: "user",
|
|
143
|
-
content: `Generate an MCP server configuration JSON from this description. Return ONLY the JSON object, no explanation.
|
|
88
|
+
|
|
89
|
+
async function addWithTransport(transport) {
|
|
90
|
+
if (transport === "ai") {
|
|
91
|
+
const description = await askQuestion("Describe the MCP server you want to add (e.g. 'a filesystem server that gives access to /tmp'):")
|
|
92
|
+
if (!description) return
|
|
93
|
+
pushLine("[mcp] Generating config from description...", C.dim)
|
|
94
|
+
try {
|
|
95
|
+
const { chat } = await import("../provider/index.mjs")
|
|
96
|
+
const res = await chat(agent.provider, {
|
|
97
|
+
messages: [{
|
|
98
|
+
role: "user",
|
|
99
|
+
content: `Generate an MCP server configuration JSON from this description. Return ONLY the JSON object, no explanation.
|
|
144
100
|
|
|
145
101
|
Description: "${description}"
|
|
146
102
|
|
|
@@ -153,48 +109,128 @@ Example HTTP: {"name":"filesystem","url":"https://example.com/mcp","headers":{"A
|
|
|
153
109
|
Example stdio: {"name":"filesystem","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}
|
|
154
110
|
|
|
155
111
|
Return ONLY the JSON object:`,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
})
|
|
160
|
-
const jsonMatch = (res.content ?? "").match(/\{[\s\S]*\}/)
|
|
161
|
-
if (!jsonMatch) { pushLine("[mcp] AI response not valid JSON", C.error); return }
|
|
162
|
-
const srv = JSON.parse(jsonMatch[0])
|
|
163
|
-
if (!srv.name) { pushLine("[mcp] AI response missing 'name' field", C.error); return }
|
|
164
|
-
// Show preview and confirm
|
|
165
|
-
pushLine(`[mcp] Generated config: ${JSON.stringify(srv)}`, C.tool)
|
|
166
|
-
const confirm = await askQuestion("Add this server? (y/n):")
|
|
167
|
-
if (confirm?.toLowerCase() !== "y") { pushLine("[mcp] Cancelled", C.dim); return }
|
|
168
|
-
await addAndConnect(ctx, srv)
|
|
169
|
-
} catch (err) {
|
|
170
|
-
pushLine(`[mcp] AI generation failed: ${err.message}`, C.error)
|
|
171
|
-
}
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
const name = await askQuestion("Server name:")
|
|
175
|
-
if (!name) return
|
|
176
|
-
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
177
|
-
if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
|
|
178
|
-
if (te.action === "stdio") {
|
|
179
|
-
const cmd = await askQuestion("Command (e.g. npx, python):")
|
|
180
|
-
if (!cmd) return
|
|
181
|
-
const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
|
|
182
|
-
const args = argsInput ? argsInput.split(/\s+/) : undefined
|
|
183
|
-
await addAndConnect(ctx, { name, command: cmd, args })
|
|
184
|
-
} else {
|
|
185
|
-
const urlPrompt = te.action === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
|
|
186
|
-
const url = await askQuestion(urlPrompt)
|
|
187
|
-
if (!url) return
|
|
188
|
-
const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
|
|
189
|
-
const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
|
|
190
|
-
const srv = te.action === "ws"
|
|
191
|
-
? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
192
|
-
: { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
193
|
-
await addAndConnect(ctx, srv)
|
|
194
|
-
}
|
|
195
|
-
},
|
|
112
|
+
}],
|
|
113
|
+
tools: [],
|
|
114
|
+
signal: AbortSignal.timeout(15_000),
|
|
196
115
|
})
|
|
116
|
+
const jsonMatch = (res.content ?? "").match(/\{[\s\S]*\}/)
|
|
117
|
+
if (!jsonMatch) { pushLine("[mcp] AI response not valid JSON", C.error); return }
|
|
118
|
+
const srv = JSON.parse(jsonMatch[0])
|
|
119
|
+
if (!srv.name) { pushLine("[mcp] AI response missing 'name' field", C.error); return }
|
|
120
|
+
// Show preview and confirm
|
|
121
|
+
pushLine(`[mcp] Generated config: ${JSON.stringify(srv)}`, C.tool)
|
|
122
|
+
const confirm = await askQuestion("Add this server? (y/n):")
|
|
123
|
+
if (confirm?.toLowerCase() !== "y") { pushLine("[mcp] Cancelled", C.dim); return }
|
|
124
|
+
await addAndConnect(ctx, srv)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
pushLine(`[mcp] AI generation failed: ${err.message}`, C.error)
|
|
197
127
|
}
|
|
198
|
-
|
|
199
|
-
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
const name = await askQuestion("Server name:")
|
|
131
|
+
if (!name) return
|
|
132
|
+
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
133
|
+
if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
|
|
134
|
+
if (transport === "stdio") {
|
|
135
|
+
const cmd = await askQuestion("Command (e.g. npx, python):")
|
|
136
|
+
if (!cmd) return
|
|
137
|
+
const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
|
|
138
|
+
const cmdArgs = argsInput ? argsInput.split(/\s+/) : undefined
|
|
139
|
+
await addAndConnect(ctx, { name, command: cmd, args: cmdArgs })
|
|
140
|
+
} else {
|
|
141
|
+
const urlPrompt = transport === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
|
|
142
|
+
const url = await askQuestion(urlPrompt)
|
|
143
|
+
if (!url) return
|
|
144
|
+
const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
|
|
145
|
+
const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
|
|
146
|
+
const srv = transport === "ws"
|
|
147
|
+
? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
148
|
+
: { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
|
|
149
|
+
await addAndConnect(ctx, srv)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function addFlow() {
|
|
154
|
+
// Pick transport type first, then ask name + URL/command
|
|
155
|
+
const te = await showPicker("MCP Transport", [
|
|
156
|
+
{ type: "header", text: "Select transport or use AI assist" },
|
|
157
|
+
{ type: "item", text: "🤖 Describe with AI — natural language → config", action: "ai" },
|
|
158
|
+
{ type: "item", text: "HTTP (https://…)", action: "http" },
|
|
159
|
+
{ type: "item", text: "WebSocket (ws://…)", action: "ws" },
|
|
160
|
+
{ type: "item", text: "stdio (local command)", action: "stdio" },
|
|
161
|
+
])
|
|
162
|
+
if (te) await addWithTransport(te.action)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** remove/connect 的服务器选择 picker + 执行。返回 true = 已执行;false = Esc 取消。 */
|
|
166
|
+
async function pickAndRun(action) {
|
|
167
|
+
const servers = getServers()
|
|
168
|
+
if (servers.length === 0) {
|
|
169
|
+
pushLine("[mcp] no MCP server configured", C.error)
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
const subEntries = [
|
|
173
|
+
{ type: "header", text: action === "remove" ? "Select server to remove" : "Select server to reconnect" },
|
|
174
|
+
...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
|
|
175
|
+
]
|
|
176
|
+
const se = await showPicker(action === "remove" ? "Remove MCP Server" : "Reconnect MCP", subEntries)
|
|
177
|
+
if (!se) return false // Esc 取消
|
|
178
|
+
if (action === "remove") await removeServer(se.name)
|
|
179
|
+
else await connectServer(se.name)
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Direct args: /mcp list │ /mcp add │ /mcp http|ws|stdio|ai │ /mcp remove [name] │ /mcp connect [name]
|
|
184
|
+
const sub = args[0]?.toLowerCase()
|
|
185
|
+
if (sub === "list") { listServers(); return }
|
|
186
|
+
if (sub === "add") { await addFlow(); return }
|
|
187
|
+
if (sub === "http" || sub === "ws" || sub === "stdio" || sub === "ai") { await addWithTransport(sub); return }
|
|
188
|
+
if (sub === "remove" || sub === "connect") {
|
|
189
|
+
const name = args[1]
|
|
190
|
+
if (name) {
|
|
191
|
+
const servers = getServers()
|
|
192
|
+
if (!servers.some((s) => s.name === name)) {
|
|
193
|
+
pushLine(`[mcp] no server named "${name}" (${servers.map((s) => s.name).join(", ") || "none configured"})`, C.error)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (sub === "remove") await removeServer(name)
|
|
197
|
+
else await connectServer(name)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
// 已明确 remove/connect 意图但没带 name → 直接进服务器选择 picker,不落主菜单
|
|
201
|
+
await pickAndRun(sub)
|
|
202
|
+
return
|
|
203
|
+
} else if (sub) {
|
|
204
|
+
pushLine("Usage: /mcp [list|add|http|ws|stdio|ai|remove [name]|connect [name]]", C.error)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 主菜单循环:选中即关闭,子菜单 Esc 返回主菜单,主菜单 Esc 退出
|
|
209
|
+
for (;;) {
|
|
210
|
+
const servers = getServers()
|
|
211
|
+
const entries = [
|
|
212
|
+
{ type: "header", text: `${servers.length} MCP servers configured` },
|
|
213
|
+
{ type: "item", text: "View list", action: "list" },
|
|
214
|
+
{ type: "item", text: "Add server", action: "add" },
|
|
215
|
+
]
|
|
216
|
+
if (servers.length > 0) {
|
|
217
|
+
entries.push(
|
|
218
|
+
{ type: "item", text: "Remove server", action: "remove" },
|
|
219
|
+
{ type: "item", text: "Reconnect server", action: "connect" },
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
const e = await showPicker("MCP", entries)
|
|
223
|
+
if (!e) return // Esc 退出
|
|
224
|
+
if (e.action === "list") {
|
|
225
|
+
listServers()
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
if (e.action === "add") {
|
|
229
|
+
await addFlow()
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
const done = await pickAndRun(e.action)
|
|
233
|
+
if (!done) continue // 子菜单 Esc → 回主菜单
|
|
234
|
+
return
|
|
235
|
+
}
|
|
200
236
|
}
|
package/src/tui/cmd-model.mjs
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
2
|
|
|
3
|
-
/** /model command: open model picker
|
|
4
|
-
* ctx: { openModelPicker, pushLine } */
|
|
5
|
-
export async function handleModelCommand(ctx) {
|
|
6
|
-
|
|
3
|
+
/** /model command: open model picker, or switch provider directly via `/model <provider>`.
|
|
4
|
+
* ctx: { agent, openModelPicker, selectModel, pushLine } */
|
|
5
|
+
export async function handleModelCommand(ctx, args = []) {
|
|
6
|
+
const name = args[0]?.toLowerCase()
|
|
7
|
+
if (!name) {
|
|
8
|
+
ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
const target = ctx.agent.providers.find((p) => p.name.toLowerCase() === name)
|
|
12
|
+
if (!target) {
|
|
13
|
+
const available = ctx.agent.providers.map((p) => p.name).join(", ")
|
|
14
|
+
ctx.pushLine(`Unknown provider: ${args[0]} (available: ${available})`, C.error)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
await ctx.selectModel({ provider: target.name, model: target.model }).catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
7
18
|
}
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -2,9 +2,9 @@ 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, showPicker, render } */
|
|
6
6
|
export async function handleNewCommand(ctx) {
|
|
7
|
-
const { agent, state, pushLine,
|
|
7
|
+
const { agent, state, pushLine, showPicker, render } = ctx
|
|
8
8
|
|
|
9
9
|
const doNewSession = () => {
|
|
10
10
|
agent.history = []
|
|
@@ -21,17 +21,11 @@ export async function handleNewCommand(ctx) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (agent.history.length > 0) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
],
|
|
30
|
-
defaultIndex: 1,
|
|
31
|
-
onSelect: (e) => {
|
|
32
|
-
if (e.action === "yes") doNewSession()
|
|
33
|
-
},
|
|
34
|
-
})
|
|
24
|
+
const e = await showPicker("Start new session?", [
|
|
25
|
+
{ type: "item", text: "Yes, archive current and start new", action: "yes" },
|
|
26
|
+
{ type: "item", text: "Cancel", action: "no" },
|
|
27
|
+
], { defaultIndex: 1 })
|
|
28
|
+
if (e?.action === "yes") doNewSession()
|
|
35
29
|
return
|
|
36
30
|
}
|
|
37
31
|
doNewSession()
|
package/src/tui/cmd-restore.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
2
|
|
|
3
3
|
/** /restore command: list git checkpoints and roll back to selected snapshot.
|
|
4
|
-
* ctx: { agent,
|
|
4
|
+
* ctx: { agent, showPicker, pushLine, pushLabel } */
|
|
5
5
|
export async function handleRestoreCommand(ctx) {
|
|
6
|
-
const { agent,
|
|
6
|
+
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
7
7
|
const { listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
|
|
8
8
|
if (!isGitRepo(agent.cwd)) {
|
|
9
9
|
pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
|
|
@@ -22,18 +22,14 @@ export async function handleRestoreCommand(ctx) {
|
|
|
22
22
|
id: cp.id,
|
|
23
23
|
})),
|
|
24
24
|
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pushLine(`[rewind] ${error.message}`, C.error)
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
})
|
|
25
|
+
const e = await showPicker("Restore Checkpoint", entries)
|
|
26
|
+
if (!e) return
|
|
27
|
+
try {
|
|
28
|
+
const summary = await rewind(agent.cwd, e.id)
|
|
29
|
+
pushLabel(`❯ Rewind`, ansi.bold + C.warn)
|
|
30
|
+
pushLine(`Restored to ${e.id}: patch ${summary.patchApplied ? "applied" : "none"}, deleted ${summary.deleted} new files, restored ${summary.restored} file(s)`, C.tool)
|
|
31
|
+
pushLine("(current state saved as new checkpoint; /restore again to go back)", C.dim)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
pushLine(`[rewind] ${error.message}`, C.error)
|
|
34
|
+
}
|
|
39
35
|
}
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -2,41 +2,37 @@ import { listSlots, switchToSlot, applySession } from "../session.mjs"
|
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
4
|
/** /session command: list/switch archived session slots.
|
|
5
|
-
* ctx: { agent, state,
|
|
5
|
+
* ctx: { agent, state, showPicker, pushLine, pushLabel, render } */
|
|
6
6
|
export async function handleSessionCommand(ctx) {
|
|
7
|
-
const { agent, state,
|
|
7
|
+
const { agent, state, showPicker, pushLine, pushLabel, render } = ctx
|
|
8
8
|
const slots = listSlots(agent.cwd)
|
|
9
9
|
if (slots.length === 0) {
|
|
10
10
|
pushLine("No archived sessions (use /new and old sessions auto-archive to slots)", C.dim)
|
|
11
|
-
|
|
12
|
-
const entries = [
|
|
13
|
-
{ type: "header", text: `Archived sessions (↑↓ select, Enter switch, Esc cancel)` },
|
|
14
|
-
...slots.map((s) => ({
|
|
15
|
-
type: "item",
|
|
16
|
-
text: `Slot ${s.slot} — ${s.date}`,
|
|
17
|
-
slot: s.slot,
|
|
18
|
-
})),
|
|
19
|
-
]
|
|
20
|
-
openPicker({
|
|
21
|
-
title: "Sessions",
|
|
22
|
-
entries,
|
|
23
|
-
onSelect: (e) => {
|
|
24
|
-
const data = switchToSlot(agent.cwd, e.slot)
|
|
25
|
-
if (!data) {
|
|
26
|
-
pushLine(`Slot ${e.slot} not found`, C.dim)
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
applySession(agent, data)
|
|
30
|
-
state.lines = data.display.length
|
|
31
|
-
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
32
|
-
: []
|
|
33
|
-
state.tasks = agent.tasks ?? []
|
|
34
|
-
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
35
|
-
state.tasks = []
|
|
36
|
-
}
|
|
37
|
-
pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
|
|
38
|
-
render()
|
|
39
|
-
},
|
|
40
|
-
})
|
|
11
|
+
return
|
|
41
12
|
}
|
|
13
|
+
const entries = [
|
|
14
|
+
{ type: "header", text: `Archived sessions (↑↓ select, Enter switch, Esc cancel)` },
|
|
15
|
+
...slots.map((s) => ({
|
|
16
|
+
type: "item",
|
|
17
|
+
text: `Slot ${s.slot} — ${s.date}`,
|
|
18
|
+
slot: s.slot,
|
|
19
|
+
})),
|
|
20
|
+
]
|
|
21
|
+
const e = await showPicker("Sessions", entries)
|
|
22
|
+
if (!e) return
|
|
23
|
+
const data = switchToSlot(agent.cwd, e.slot)
|
|
24
|
+
if (!data) {
|
|
25
|
+
pushLine(`Slot ${e.slot} not found`, C.dim)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
applySession(agent, data)
|
|
29
|
+
state.lines = data.display.length
|
|
30
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
31
|
+
: []
|
|
32
|
+
state.tasks = agent.tasks ?? []
|
|
33
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
34
|
+
state.tasks = []
|
|
35
|
+
}
|
|
36
|
+
pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
|
|
37
|
+
render()
|
|
42
38
|
}
|