agent-sh 0.4.0 → 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 -113
- 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 +80 -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 +90 -48
- package/dist/index.js +98 -122
- package/dist/input-handler.js +74 -7
- 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 +33 -26
- package/dist/types.d.ts +33 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- 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 -105
- package/dist/acp-client.js +0 -684
- 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, {
|
|
@@ -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
269
|
function showUserQuery(query, modeLabel) {
|
|
241
|
-
const boxW =
|
|
270
|
+
const boxW = writer.columns;
|
|
242
271
|
const contentW = boxW - 4;
|
|
243
272
|
const lines = [];
|
|
244
273
|
for (const raw of query.split("\n")) {
|
|
@@ -264,11 +293,25 @@ export default function activate(ctx) {
|
|
|
264
293
|
const title = modeLabel
|
|
265
294
|
? `${borderColor}${p.bold} ${modeLabel} ${p.reset}`
|
|
266
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
|
+
}
|
|
267
309
|
const framed = renderBoxFrame(lines, {
|
|
268
310
|
width: boxW,
|
|
269
311
|
style: "rounded",
|
|
270
312
|
borderColor,
|
|
271
313
|
title,
|
|
314
|
+
titleRight: modelLabel,
|
|
272
315
|
});
|
|
273
316
|
writer.write("\n");
|
|
274
317
|
for (const line of framed) {
|
|
@@ -392,9 +435,10 @@ export default function activate(ctx) {
|
|
|
392
435
|
drain();
|
|
393
436
|
}
|
|
394
437
|
}
|
|
438
|
+
// Thinking is always assumed available — the TUI renders thinking
|
|
439
|
+
// tokens whenever they arrive, regardless of backend.
|
|
395
440
|
function hasThinkingMode() {
|
|
396
|
-
|
|
397
|
-
return !mode || mode.id !== "off";
|
|
441
|
+
return true;
|
|
398
442
|
}
|
|
399
443
|
function startThinkingSpinner() {
|
|
400
444
|
if (!s.spinnerStartTime)
|
|
@@ -482,14 +526,13 @@ export default function activate(ctx) {
|
|
|
482
526
|
function showFileDiff(filePath, diff) {
|
|
483
527
|
if (diff.isIdentical)
|
|
484
528
|
return;
|
|
485
|
-
const boxW = Math.min(
|
|
529
|
+
const boxW = Math.min(120, writer.columns);
|
|
486
530
|
const contentW = boxW - 4;
|
|
487
531
|
const diffLines = renderDiff(diff, {
|
|
488
532
|
width: contentW,
|
|
489
533
|
filePath,
|
|
490
534
|
maxLines: getSettings().diffMaxLines,
|
|
491
535
|
trueColor: true,
|
|
492
|
-
mode: "unified",
|
|
493
536
|
});
|
|
494
537
|
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
495
538
|
const isTruncated = lastLine.includes("… ");
|
|
@@ -552,14 +595,13 @@ export default function activate(ctx) {
|
|
|
552
595
|
}
|
|
553
596
|
function showFileDiffCached(entry) {
|
|
554
597
|
const { filePath, diff } = entry;
|
|
555
|
-
const boxW = Math.min(
|
|
598
|
+
const boxW = Math.min(120, writer.columns);
|
|
556
599
|
const contentW = boxW - 4;
|
|
557
600
|
const diffLines = renderDiff(diff, {
|
|
558
601
|
width: contentW,
|
|
559
602
|
filePath,
|
|
560
603
|
maxLines: getSettings().diffMaxLines,
|
|
561
604
|
trueColor: true,
|
|
562
|
-
mode: "unified",
|
|
563
605
|
});
|
|
564
606
|
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
565
607
|
const framed = renderBoxFrame(body, {
|