agent-sh 0.3.1 → 0.5.0
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 +66 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
|
@@ -2,90 +2,187 @@
|
|
|
2
2
|
* Slash commands extension.
|
|
3
3
|
*
|
|
4
4
|
* Registers built-in slash commands on the event bus:
|
|
5
|
+
* - Listens for "command:register" to accept commands from extensions
|
|
5
6
|
* - Responds to "autocomplete:request" pipe for /-prefixed completions
|
|
6
7
|
* - Handles "command:execute" events and dispatches to matching handler
|
|
7
8
|
* - Uses "ui:info"/"ui:error" for user feedback (no direct TUI dependency)
|
|
9
|
+
*
|
|
10
|
+
* Argument completion is composable: any extension can onPipe("autocomplete:request")
|
|
11
|
+
* and check payload.command / payload.commandArgs to add completions for any command.
|
|
8
12
|
*/
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
10
13
|
import { palette as p } from "../utils/palette.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
bus.emit("ui:info", { message: "Session cleared." });
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
bus.emit("ui:error", {
|
|
31
|
-
message: `Failed to reset session: ${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
},
|
|
14
|
+
import { discoverSkills, loadSkillContent } from "../agent/skills.js";
|
|
15
|
+
export default function activate({ bus, contextManager }) {
|
|
16
|
+
const commands = new Map();
|
|
17
|
+
const register = (cmd) => {
|
|
18
|
+
const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
|
|
19
|
+
commands.set(name, { ...cmd, name });
|
|
20
|
+
};
|
|
21
|
+
// ── Built-in commands ─────────────────────────────────────────
|
|
22
|
+
register({
|
|
23
|
+
name: "/help",
|
|
24
|
+
description: "Show available commands",
|
|
25
|
+
handler: () => {
|
|
26
|
+
const maxLen = Math.max(...[...commands.values()].map(c => c.name.length));
|
|
27
|
+
const pad = maxLen + 2;
|
|
28
|
+
const lines = [...commands.values()].map((c) => ` ${p.accent}${c.name.padEnd(pad)}${p.reset} ${c.description}`);
|
|
29
|
+
bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
|
|
35
30
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
bus.emit("ui:error", { message: "Failed to copy to clipboard." });
|
|
56
|
-
}
|
|
57
|
-
},
|
|
31
|
+
});
|
|
32
|
+
register({
|
|
33
|
+
name: "/model",
|
|
34
|
+
description: "Cycle to next model, or switch to a specific one",
|
|
35
|
+
handler: (args) => {
|
|
36
|
+
const name = args.trim();
|
|
37
|
+
if (!name) {
|
|
38
|
+
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
39
|
+
const current = models.find((m) => m.model === active);
|
|
40
|
+
const label = current
|
|
41
|
+
? `${current.model}${current.provider ? ` [${current.provider}]` : ""}`
|
|
42
|
+
: active ?? "none";
|
|
43
|
+
bus.emit("ui:info", { message: `Model: ${label}` });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
bus.emit("config:switch-model", { model: name });
|
|
47
|
+
}
|
|
58
48
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
});
|
|
50
|
+
register({
|
|
51
|
+
name: "/thinking",
|
|
52
|
+
description: "Set thinking/reasoning effort level",
|
|
53
|
+
handler: (args) => {
|
|
54
|
+
const level = args.trim();
|
|
55
|
+
if (!level) {
|
|
56
|
+
const { level: current, levels, supported } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
|
|
57
|
+
const status = supported ? current : `${current} (not supported by current model)`;
|
|
58
|
+
bus.emit("ui:info", { message: `Thinking: ${status} (options: ${levels.join(", ")})` });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
bus.emit("config:set-thinking", { level });
|
|
62
|
+
}
|
|
65
63
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
});
|
|
65
|
+
register({
|
|
66
|
+
name: "/backend",
|
|
67
|
+
description: "List or switch agent backend",
|
|
68
|
+
handler: (args) => {
|
|
69
|
+
const name = args.trim();
|
|
70
|
+
if (!name) {
|
|
71
|
+
bus.emit("config:list-backends", {});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
bus.emit("config:switch-backend", { name });
|
|
75
|
+
}
|
|
72
76
|
},
|
|
73
|
-
|
|
74
|
-
//
|
|
77
|
+
});
|
|
78
|
+
// ── Extension registration ────────────────────────────────────
|
|
79
|
+
bus.on("command:register", (cmd) => {
|
|
80
|
+
register(cmd);
|
|
81
|
+
});
|
|
82
|
+
// ── Skill commands (/skill:<name>) ────────────────────────────
|
|
83
|
+
const getSkills = () => {
|
|
84
|
+
const cwd = contextManager?.getCwd() ?? process.cwd();
|
|
85
|
+
return discoverSkills(cwd);
|
|
86
|
+
};
|
|
87
|
+
const handleSkillCommand = (skillName, args) => {
|
|
88
|
+
const skills = getSkills();
|
|
89
|
+
const skill = skills.find(s => s.name === skillName);
|
|
90
|
+
if (!skill) {
|
|
91
|
+
bus.emit("ui:error", { message: `Unknown skill: ${skillName}` });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const content = loadSkillContent(skill);
|
|
95
|
+
if (!content) {
|
|
96
|
+
bus.emit("ui:error", { message: `Failed to load skill: ${skillName}` });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const query = args.trim()
|
|
100
|
+
? `${content}\n\n${args.trim()}`
|
|
101
|
+
: content;
|
|
102
|
+
bus.emit("agent:submit", { query });
|
|
103
|
+
};
|
|
104
|
+
// ── Autocomplete: command names ───────────────────────────────
|
|
75
105
|
bus.onPipe("autocomplete:request", (payload) => {
|
|
76
106
|
if (!payload.buffer.startsWith("/"))
|
|
77
107
|
return payload;
|
|
108
|
+
// Argument completion is handled by separate pipe handlers below
|
|
109
|
+
if (payload.command)
|
|
110
|
+
return payload;
|
|
78
111
|
const prefix = payload.buffer.toLowerCase();
|
|
79
|
-
const matching = commands
|
|
112
|
+
const matching = [...commands.values()]
|
|
80
113
|
.filter((c) => c.name.toLowerCase().startsWith(prefix))
|
|
81
114
|
.map((c) => ({ name: c.name, description: c.description }));
|
|
115
|
+
// Skill commands
|
|
116
|
+
if (prefix.startsWith("/skill:") || "/skill:".startsWith(prefix)) {
|
|
117
|
+
const skills = getSkills();
|
|
118
|
+
for (const skill of skills) {
|
|
119
|
+
const name = `/skill:${skill.name}`;
|
|
120
|
+
if (name.toLowerCase().startsWith(prefix)) {
|
|
121
|
+
matching.push({ name, description: skill.description });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
82
125
|
if (matching.length === 0)
|
|
83
126
|
return payload;
|
|
84
127
|
return { ...payload, items: [...payload.items, ...matching] };
|
|
85
128
|
});
|
|
86
|
-
//
|
|
129
|
+
// ── Autocomplete: /model arguments ─────────────────────────────
|
|
130
|
+
bus.onPipe("autocomplete:request", (payload) => {
|
|
131
|
+
if (payload.command !== "/model")
|
|
132
|
+
return payload;
|
|
133
|
+
const partial = (payload.commandArgs ?? "").toLowerCase();
|
|
134
|
+
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
135
|
+
const items = models
|
|
136
|
+
.filter((m) => m.model.toLowerCase().includes(partial))
|
|
137
|
+
.slice(0, 15)
|
|
138
|
+
.map((m) => ({
|
|
139
|
+
name: `/model ${m.model}`,
|
|
140
|
+
description: `${m.provider ? `[${m.provider}]` : ""}${m.model === active ? " (active)" : ""}`,
|
|
141
|
+
}));
|
|
142
|
+
if (items.length === 0)
|
|
143
|
+
return payload;
|
|
144
|
+
return { ...payload, items: [...payload.items, ...items] };
|
|
145
|
+
});
|
|
146
|
+
// ── Autocomplete: /thinking arguments ─────────────────────────
|
|
147
|
+
bus.onPipe("autocomplete:request", (payload) => {
|
|
148
|
+
if (payload.command !== "/thinking")
|
|
149
|
+
return payload;
|
|
150
|
+
const partial = (payload.commandArgs ?? "").toLowerCase();
|
|
151
|
+
const { level: current, levels } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
|
|
152
|
+
const items = levels
|
|
153
|
+
.filter((l) => l.startsWith(partial))
|
|
154
|
+
.map((l) => ({
|
|
155
|
+
name: `/thinking ${l}`,
|
|
156
|
+
description: l === current ? "(active)" : "",
|
|
157
|
+
}));
|
|
158
|
+
if (items.length === 0)
|
|
159
|
+
return payload;
|
|
160
|
+
return { ...payload, items: [...payload.items, ...items] };
|
|
161
|
+
});
|
|
162
|
+
// ── Autocomplete: /backend arguments ──────────────────────────
|
|
163
|
+
bus.onPipe("autocomplete:request", (payload) => {
|
|
164
|
+
if (payload.command !== "/backend")
|
|
165
|
+
return payload;
|
|
166
|
+
const partial = (payload.commandArgs ?? "").toLowerCase();
|
|
167
|
+
const { names, active } = bus.emitPipe("config:get-backends", { names: [], active: null });
|
|
168
|
+
const items = names
|
|
169
|
+
.filter((n) => n.toLowerCase().startsWith(partial))
|
|
170
|
+
.map((n) => ({
|
|
171
|
+
name: `/backend ${n}`,
|
|
172
|
+
description: n === active ? "(active)" : "",
|
|
173
|
+
}));
|
|
174
|
+
if (items.length === 0)
|
|
175
|
+
return payload;
|
|
176
|
+
return { ...payload, items: [...payload.items, ...items] };
|
|
177
|
+
});
|
|
178
|
+
// ── Dispatch ──────────────────────────────────────────────────
|
|
87
179
|
bus.on("command:execute", (e) => {
|
|
88
|
-
|
|
180
|
+
if (e.name.startsWith("/skill:")) {
|
|
181
|
+
const skillName = e.name.slice("/skill:".length);
|
|
182
|
+
handleSkillCommand(skillName, e.args);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const cmd = commands.get(e.name);
|
|
89
186
|
if (cmd) {
|
|
90
187
|
const result = cmd.handler(e.args);
|
|
91
188
|
if (result instanceof Promise) {
|
|
@@ -47,7 +47,6 @@ function createRenderState() {
|
|
|
47
47
|
spinnerOpts: {},
|
|
48
48
|
spinnerInterval: null,
|
|
49
49
|
spinnerStartTime: 0,
|
|
50
|
-
lastCommand: "",
|
|
51
50
|
toolLineOpen: false,
|
|
52
51
|
currentToolKind: undefined,
|
|
53
52
|
commandOutputBuffer: "",
|
|
@@ -60,9 +59,12 @@ function createRenderState() {
|
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
61
|
export default function activate(ctx) {
|
|
63
|
-
const { bus,
|
|
62
|
+
const { bus, llmClient, define } = ctx;
|
|
64
63
|
const writer = new StdoutWriter();
|
|
65
64
|
const s = createRenderState();
|
|
65
|
+
// Track backend/model info for display on response border
|
|
66
|
+
let backendInfo = null;
|
|
67
|
+
bus.on("agent:info", (info) => { backendInfo = info; });
|
|
66
68
|
// ── Register fenced block transform (code blocks → ContentBlock) ──
|
|
67
69
|
// Nobody is special — tui-renderer uses the same primitive as any extension.
|
|
68
70
|
const fencedTransform = createFencedBlockTransform(bus, {
|
|
@@ -75,7 +77,7 @@ export default function activate(ctx) {
|
|
|
75
77
|
// ── Event subscriptions ─────────────────────────────────────
|
|
76
78
|
bus.on("agent:query", (e) => {
|
|
77
79
|
s.spinnerStartTime = 0;
|
|
78
|
-
showUserQuery(e.query);
|
|
80
|
+
showUserQuery(e.query, e.modeLabel);
|
|
79
81
|
startAgentResponse();
|
|
80
82
|
startThinkingSpinner();
|
|
81
83
|
});
|
|
@@ -104,46 +106,54 @@ export default function activate(ctx) {
|
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
108
|
bus.on("agent:response-chunk", (e) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
block.text += "\n";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const block of blocks) {
|
|
118
|
-
switch (block.type) {
|
|
119
|
-
case "text":
|
|
120
|
-
if (block.text)
|
|
121
|
-
writeAgentText(block.text);
|
|
122
|
-
break;
|
|
123
|
-
case "code-block":
|
|
124
|
-
writeCodeBlock(block.language, block.code);
|
|
125
|
-
break;
|
|
126
|
-
case "image":
|
|
127
|
-
writeInlineImage(block.data);
|
|
128
|
-
break;
|
|
129
|
-
case "raw":
|
|
130
|
-
flushForRaw();
|
|
131
|
-
writer.write(block.escape);
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
109
|
+
const { blocks } = e;
|
|
110
|
+
// Inject spacing: append \n to text blocks that precede non-text blocks
|
|
111
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
112
|
+
const block = blocks[i];
|
|
113
|
+
const next = blocks[i + 1];
|
|
114
|
+
if (block.type === "text" && next && next.type !== "text") {
|
|
115
|
+
block.text += "\n";
|
|
134
116
|
}
|
|
135
117
|
}
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
for (const block of blocks) {
|
|
119
|
+
switch (block.type) {
|
|
120
|
+
case "text":
|
|
121
|
+
if (block.text)
|
|
122
|
+
writeAgentText(block.text);
|
|
123
|
+
break;
|
|
124
|
+
case "code-block":
|
|
125
|
+
writeCodeBlock(block.language, block.code);
|
|
126
|
+
break;
|
|
127
|
+
case "image":
|
|
128
|
+
writeInlineImage(block.data);
|
|
129
|
+
break;
|
|
130
|
+
case "raw":
|
|
131
|
+
flushForRaw();
|
|
132
|
+
writer.write(block.escape);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
138
135
|
}
|
|
139
136
|
});
|
|
137
|
+
// Track token usage for display
|
|
138
|
+
let pendingUsage = null;
|
|
139
|
+
bus.on("agent:usage", (e) => { pendingUsage = e; });
|
|
140
140
|
bus.on("agent:response-done", () => {
|
|
141
141
|
s.isThinking = false;
|
|
142
|
+
if (pendingUsage && s.renderer) {
|
|
143
|
+
const { prompt_tokens, completion_tokens } = pendingUsage;
|
|
144
|
+
const maxTokens = backendInfo?.contextWindow ?? 128_000;
|
|
145
|
+
// prompt_tokens of the latest call = current context usage
|
|
146
|
+
// (it includes the full conversation history)
|
|
147
|
+
const ctxK = (prompt_tokens / 1000).toFixed(1);
|
|
148
|
+
const maxK = (maxTokens / 1000).toFixed(0);
|
|
149
|
+
const pct = Math.min(100, (prompt_tokens / maxTokens) * 100).toFixed(0);
|
|
150
|
+
s.renderer.writeLine("");
|
|
151
|
+
s.renderer.writeLine(`${p.dim}⬆ ${prompt_tokens} ⬇ ${completion_tokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`);
|
|
152
|
+
drain();
|
|
153
|
+
pendingUsage = null;
|
|
154
|
+
}
|
|
142
155
|
endAgentResponse();
|
|
143
156
|
});
|
|
144
|
-
bus.on("agent:tool-call", (e) => {
|
|
145
|
-
s.lastCommand = e.tool;
|
|
146
|
-
});
|
|
147
157
|
bus.on("agent:tool-started", (e) => {
|
|
148
158
|
fencedTransform.flush();
|
|
149
159
|
stopCurrentSpinner();
|
|
@@ -159,9 +169,8 @@ export default function activate(ctx) {
|
|
|
159
169
|
s.hadToolCalls = true;
|
|
160
170
|
}
|
|
161
171
|
else {
|
|
162
|
-
showToolCall(e.title,
|
|
172
|
+
showToolCall(e.title, "", e);
|
|
163
173
|
}
|
|
164
|
-
s.lastCommand = "";
|
|
165
174
|
});
|
|
166
175
|
bus.on("agent:tool-completed", (e) => {
|
|
167
176
|
showToolComplete(e.exitCode);
|
|
@@ -182,7 +191,15 @@ export default function activate(ctx) {
|
|
|
182
191
|
stopCurrentSpinner();
|
|
183
192
|
endAgentResponse();
|
|
184
193
|
});
|
|
185
|
-
bus.on("agent:error", (e) =>
|
|
194
|
+
bus.on("agent:error", (e) => {
|
|
195
|
+
stopCurrentSpinner();
|
|
196
|
+
showCollapsedThinking();
|
|
197
|
+
if (!s.renderer)
|
|
198
|
+
startAgentResponse();
|
|
199
|
+
s.renderer.writeLine(`${p.error}Error: ${e.message}${p.reset}`);
|
|
200
|
+
s.renderer.writeLine("");
|
|
201
|
+
drain();
|
|
202
|
+
});
|
|
186
203
|
bus.on("permission:request", (e) => {
|
|
187
204
|
stopCurrentSpinner();
|
|
188
205
|
flushCommandOutput();
|
|
@@ -191,11 +208,12 @@ export default function activate(ctx) {
|
|
|
191
208
|
drain();
|
|
192
209
|
}
|
|
193
210
|
if (e.kind === "file-write" && e.metadata?.diff) {
|
|
211
|
+
showCollapsedThinking();
|
|
194
212
|
showFileDiff(e.title, e.metadata.diff);
|
|
195
213
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
// Don't endAgentResponse() here — permission requests that aren't
|
|
215
|
+
// file-write diffs are handled inline (auto-approved or by extensions).
|
|
216
|
+
// Closing the response prematurely causes double separator borders.
|
|
199
217
|
});
|
|
200
218
|
bus.on("input:keypress", (e) => {
|
|
201
219
|
if (e.key === "\x0f")
|
|
@@ -203,8 +221,17 @@ export default function activate(ctx) {
|
|
|
203
221
|
if (e.key === "\x14")
|
|
204
222
|
toggleThinkingDisplay(); // Ctrl+T
|
|
205
223
|
});
|
|
206
|
-
bus.on("ui:info", (e) =>
|
|
224
|
+
bus.on("ui:info", (e) => {
|
|
225
|
+
stopCurrentSpinner();
|
|
226
|
+
showInfo(e.message);
|
|
227
|
+
// Restart spinner if agent is still processing
|
|
228
|
+
if (s.renderer)
|
|
229
|
+
startThinkingSpinner();
|
|
230
|
+
});
|
|
207
231
|
bus.on("ui:error", (e) => showError(e.message));
|
|
232
|
+
bus.on("ui:suggestion", (e) => {
|
|
233
|
+
writer.write(`${p.dim}💡 ${e.text}${p.reset}\n`);
|
|
234
|
+
});
|
|
208
235
|
// ── Rendering functions ─────────────────────────────────────
|
|
209
236
|
function drain() {
|
|
210
237
|
if (!s.renderer)
|
|
@@ -224,6 +251,7 @@ export default function activate(ctx) {
|
|
|
224
251
|
if (!s.renderer)
|
|
225
252
|
startAgentResponse();
|
|
226
253
|
s.renderer.writeLine(`${p.muted}… thinking${p.reset}`);
|
|
254
|
+
s.renderer.writeLine("");
|
|
227
255
|
s.thinkingPending = false;
|
|
228
256
|
}
|
|
229
257
|
}
|
|
@@ -234,11 +262,12 @@ export default function activate(ctx) {
|
|
|
234
262
|
s.renderer.flush();
|
|
235
263
|
s.renderer.printBottomBorder();
|
|
236
264
|
drain();
|
|
265
|
+
writer.write("\n");
|
|
237
266
|
s.renderer = null;
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
|
-
function showUserQuery(query) {
|
|
241
|
-
const boxW =
|
|
269
|
+
function showUserQuery(query, modeLabel) {
|
|
270
|
+
const boxW = writer.columns;
|
|
242
271
|
const contentW = boxW - 4;
|
|
243
272
|
const lines = [];
|
|
244
273
|
for (const raw of query.split("\n")) {
|
|
@@ -258,11 +287,31 @@ export default function activate(ctx) {
|
|
|
258
287
|
lines.push(`${p.accent}${remaining}${p.reset}`);
|
|
259
288
|
}
|
|
260
289
|
}
|
|
290
|
+
// Mode-specific border color and title
|
|
291
|
+
const isExecute = modeLabel === "Execute";
|
|
292
|
+
const borderColor = isExecute ? p.success : p.accent;
|
|
293
|
+
const title = modeLabel
|
|
294
|
+
? `${borderColor}${p.bold} ${modeLabel} ${p.reset}`
|
|
295
|
+
: `${p.accent}${p.bold}❯${p.reset}`;
|
|
296
|
+
// Backend/model label on the right (backend/model, highlighted)
|
|
297
|
+
const model = backendInfo?.model ?? llmClient?.model;
|
|
298
|
+
const backend = backendInfo?.name;
|
|
299
|
+
let modelLabel;
|
|
300
|
+
if (backend && model) {
|
|
301
|
+
modelLabel = `${p.dim}${backend}/${p.reset}${p.bold}${model}${p.reset}`;
|
|
302
|
+
}
|
|
303
|
+
else if (model) {
|
|
304
|
+
modelLabel = `${p.bold}${model}${p.reset}`;
|
|
305
|
+
}
|
|
306
|
+
else if (backend) {
|
|
307
|
+
modelLabel = `${p.bold}${backend}${p.reset}`;
|
|
308
|
+
}
|
|
261
309
|
const framed = renderBoxFrame(lines, {
|
|
262
310
|
width: boxW,
|
|
263
311
|
style: "rounded",
|
|
264
|
-
borderColor
|
|
265
|
-
title
|
|
312
|
+
borderColor,
|
|
313
|
+
title,
|
|
314
|
+
titleRight: modelLabel,
|
|
266
315
|
});
|
|
267
316
|
writer.write("\n");
|
|
268
317
|
for (const line of framed) {
|
|
@@ -386,9 +435,10 @@ export default function activate(ctx) {
|
|
|
386
435
|
drain();
|
|
387
436
|
}
|
|
388
437
|
}
|
|
438
|
+
// Thinking is always assumed available — the TUI renders thinking
|
|
439
|
+
// tokens whenever they arrive, regardless of backend.
|
|
389
440
|
function hasThinkingMode() {
|
|
390
|
-
|
|
391
|
-
return !mode || mode.id !== "off";
|
|
441
|
+
return true;
|
|
392
442
|
}
|
|
393
443
|
function startThinkingSpinner() {
|
|
394
444
|
if (!s.spinnerStartTime)
|
|
@@ -476,14 +526,13 @@ export default function activate(ctx) {
|
|
|
476
526
|
function showFileDiff(filePath, diff) {
|
|
477
527
|
if (diff.isIdentical)
|
|
478
528
|
return;
|
|
479
|
-
const boxW = Math.min(
|
|
529
|
+
const boxW = Math.min(120, writer.columns);
|
|
480
530
|
const contentW = boxW - 4;
|
|
481
531
|
const diffLines = renderDiff(diff, {
|
|
482
532
|
width: contentW,
|
|
483
533
|
filePath,
|
|
484
534
|
maxLines: getSettings().diffMaxLines,
|
|
485
535
|
trueColor: true,
|
|
486
|
-
mode: "unified",
|
|
487
536
|
});
|
|
488
537
|
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
489
538
|
const isTruncated = lastLine.includes("… ");
|
|
@@ -546,14 +595,13 @@ export default function activate(ctx) {
|
|
|
546
595
|
}
|
|
547
596
|
function showFileDiffCached(entry) {
|
|
548
597
|
const { filePath, diff } = entry;
|
|
549
|
-
const boxW = Math.min(
|
|
598
|
+
const boxW = Math.min(120, writer.columns);
|
|
550
599
|
const contentW = boxW - 4;
|
|
551
600
|
const diffLines = renderDiff(diff, {
|
|
552
601
|
width: contentW,
|
|
553
602
|
filePath,
|
|
554
603
|
maxLines: getSettings().diffMaxLines,
|
|
555
604
|
trueColor: true,
|
|
556
|
-
mode: "unified",
|
|
557
605
|
});
|
|
558
606
|
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
559
607
|
const framed = renderBoxFrame(body, {
|
|
@@ -572,7 +620,17 @@ export default function activate(ctx) {
|
|
|
572
620
|
s.showThinkingText = !s.showThinkingText;
|
|
573
621
|
if (s.spinner) {
|
|
574
622
|
stopCurrentSpinner();
|
|
575
|
-
|
|
623
|
+
if (s.showThinkingText) {
|
|
624
|
+
// Expanding: replace spinner with thinking text header
|
|
625
|
+
if (!s.renderer)
|
|
626
|
+
startAgentResponse();
|
|
627
|
+
s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
|
|
628
|
+
drain();
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
// Collapsing: restart spinner with updated hint
|
|
632
|
+
startThinkingSpinner();
|
|
633
|
+
}
|
|
576
634
|
return;
|
|
577
635
|
}
|
|
578
636
|
if (!s.isThinking)
|