agent-sh 0.7.0 → 0.8.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 +5 -1
- package/dist/agent/agent-loop.d.ts +2 -2
- package/dist/agent/agent-loop.js +106 -13
- package/dist/agent/conversation-state.d.ts +39 -9
- package/dist/agent/conversation-state.js +336 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +175 -0
- package/dist/agent/system-prompt.d.ts +2 -2
- package/dist/agent/system-prompt.js +25 -4
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.js +14 -0
- package/dist/event-bus.d.ts +14 -0
- package/dist/extensions/overlay-agent.d.ts +4 -1
- package/dist/extensions/overlay-agent.js +115 -11
- package/dist/extensions/slash-commands.js +28 -0
- package/dist/extensions/terminal-buffer.js +9 -4
- package/dist/extensions/tui-renderer.js +119 -84
- package/dist/settings.d.ts +19 -2
- package/dist/settings.js +21 -3
- package/dist/shell.js +4 -0
- package/dist/token-budget.d.ts +13 -0
- package/dist/token-budget.js +50 -0
- package/dist/types.d.ts +0 -22
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/floating-panel.d.ts +32 -3
- package/dist/utils/floating-panel.js +296 -79
- package/dist/utils/line-editor.d.ts +9 -0
- package/dist/utils/line-editor.js +44 -0
- package/dist/utils/markdown.js +3 -3
- package/dist/utils/terminal-buffer.d.ts +4 -0
- package/dist/utils/terminal-buffer.js +13 -0
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/package.json +1 -1
|
@@ -1,43 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export default function activate(
|
|
1
|
+
import { MarkdownRenderer } from "../utils/markdown.js";
|
|
2
|
+
import { palette as p } from "../utils/palette.js";
|
|
3
|
+
import { renderToolCall, formatElapsed, } from "../utils/tool-display.js";
|
|
4
|
+
export default function activate(ctx) {
|
|
5
|
+
const { bus, advise, call, createFloatingPanel } = ctx;
|
|
5
6
|
const panel = createFloatingPanel({
|
|
6
7
|
trigger: "\x1c", // Ctrl+\
|
|
7
8
|
dimBackground: true,
|
|
8
|
-
autoDismissMs: 2000,
|
|
9
9
|
});
|
|
10
|
+
// Suppress TUI renderer when overlay owns agent output
|
|
11
|
+
advise("tui:should-render-agent", (next) => {
|
|
12
|
+
return panel.active ? false : next();
|
|
13
|
+
});
|
|
14
|
+
// Signal interactive overlay mode in dynamic context
|
|
15
|
+
advise("dynamic-context:build", (next) => {
|
|
16
|
+
const base = next();
|
|
17
|
+
if (!panel.active)
|
|
18
|
+
return base;
|
|
19
|
+
return base + "\ninteractive-session: true\n";
|
|
20
|
+
});
|
|
21
|
+
// ── Conversation state (persists across hide/show) ─────────
|
|
22
|
+
const messages = [];
|
|
23
|
+
let renderer = null;
|
|
24
|
+
let currentAssistantMsg = null;
|
|
25
|
+
// ── Tool state ─────────────────────────────────────────────
|
|
26
|
+
let toolStartTime = 0;
|
|
27
|
+
function getContentWidth() {
|
|
28
|
+
return panel.computeGeometry().contentW;
|
|
29
|
+
}
|
|
30
|
+
/** Rebuild panel content from full message history. */
|
|
31
|
+
function rebuildContent() {
|
|
32
|
+
panel.clearContent();
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
for (const line of msg.lines) {
|
|
35
|
+
panel.appendLine(line);
|
|
36
|
+
}
|
|
37
|
+
panel.appendLine("");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Append a line to current assistant message and panel (if visible). */
|
|
41
|
+
function appendLine(line) {
|
|
42
|
+
currentAssistantMsg?.lines.push(line);
|
|
43
|
+
if (panel.visible)
|
|
44
|
+
panel.appendLine(line);
|
|
45
|
+
}
|
|
46
|
+
function drainRenderer() {
|
|
47
|
+
if (!renderer)
|
|
48
|
+
return;
|
|
49
|
+
for (const line of renderer.drainLines()) {
|
|
50
|
+
appendLine(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function flushRenderer() {
|
|
54
|
+
if (!renderer)
|
|
55
|
+
return;
|
|
56
|
+
renderer.flush();
|
|
57
|
+
drainRenderer();
|
|
58
|
+
}
|
|
59
|
+
function startAssistantMessage() {
|
|
60
|
+
flushRenderer();
|
|
61
|
+
currentAssistantMsg = { role: "assistant", lines: [] };
|
|
62
|
+
messages.push(currentAssistantMsg);
|
|
63
|
+
renderer = new MarkdownRenderer(getContentWidth());
|
|
64
|
+
}
|
|
65
|
+
function finalizeAssistantMessage() {
|
|
66
|
+
flushRenderer();
|
|
67
|
+
renderer = null;
|
|
68
|
+
currentAssistantMsg = null;
|
|
69
|
+
}
|
|
10
70
|
// ── Panel lifecycle ────────────────────────────────────────
|
|
11
71
|
panel.handlers.advise("panel:submit", (_next, query) => {
|
|
72
|
+
messages.push({
|
|
73
|
+
role: "user",
|
|
74
|
+
lines: [`${p.accent}${p.bold}❯${p.reset} ${query}`],
|
|
75
|
+
});
|
|
12
76
|
panel.setActive();
|
|
13
|
-
|
|
14
|
-
|
|
77
|
+
rebuildContent();
|
|
78
|
+
startAssistantMessage();
|
|
15
79
|
bus.emit("agent:submit", { query });
|
|
16
80
|
});
|
|
81
|
+
panel.handlers.advise("panel:show", (_next) => {
|
|
82
|
+
rebuildContent();
|
|
83
|
+
if (renderer)
|
|
84
|
+
drainRenderer();
|
|
85
|
+
});
|
|
17
86
|
// ── Stream agent response into panel ───────────────────────
|
|
18
87
|
bus.on("agent:response-chunk", (e) => {
|
|
19
88
|
if (!panel.active)
|
|
20
89
|
return;
|
|
90
|
+
if (!currentAssistantMsg)
|
|
91
|
+
startAssistantMessage();
|
|
21
92
|
for (const block of e.blocks) {
|
|
22
93
|
if (block.type === "text" && block.text) {
|
|
23
|
-
|
|
94
|
+
renderer.push(block.text);
|
|
95
|
+
drainRenderer();
|
|
96
|
+
}
|
|
97
|
+
else if (block.type === "code-block") {
|
|
98
|
+
flushRenderer();
|
|
99
|
+
// Reuse the shared code-block handler
|
|
100
|
+
call("render:code-block", block.language, block.code, getContentWidth());
|
|
24
101
|
}
|
|
25
102
|
}
|
|
26
103
|
});
|
|
104
|
+
// Capture lines emitted by render:code-block into the overlay
|
|
105
|
+
advise("render:code-block", (next, language, code, width) => {
|
|
106
|
+
if (!panel.active)
|
|
107
|
+
return next(language, code, width);
|
|
108
|
+
// Render code block as indented dim lines for the overlay
|
|
109
|
+
const label = language ? `${p.dim}${language}${p.reset}` : "";
|
|
110
|
+
if (label)
|
|
111
|
+
appendLine(label);
|
|
112
|
+
for (const codeLine of code.split("\n")) {
|
|
113
|
+
appendLine(` ${p.dim}${codeLine}${p.reset}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
27
116
|
bus.on("agent:tool-started", (e) => {
|
|
28
117
|
if (!panel.active)
|
|
29
118
|
return;
|
|
30
|
-
|
|
119
|
+
if (!currentAssistantMsg)
|
|
120
|
+
startAssistantMessage();
|
|
121
|
+
flushRenderer();
|
|
122
|
+
toolStartTime = Date.now();
|
|
123
|
+
const lines = renderToolCall({
|
|
124
|
+
title: e.title,
|
|
125
|
+
kind: e.kind,
|
|
126
|
+
icon: e.icon,
|
|
127
|
+
locations: e.locations,
|
|
128
|
+
rawInput: e.rawInput,
|
|
129
|
+
displayDetail: e.displayDetail,
|
|
130
|
+
}, getContentWidth());
|
|
131
|
+
for (const line of lines)
|
|
132
|
+
appendLine(line);
|
|
31
133
|
});
|
|
32
134
|
bus.on("agent:tool-completed", (e) => {
|
|
33
135
|
if (!panel.active)
|
|
34
136
|
return;
|
|
35
|
-
const
|
|
36
|
-
|
|
137
|
+
const elapsed = toolStartTime ? formatElapsed(Date.now() - toolStartTime) : "";
|
|
138
|
+
const mark = call("tui:render-tool-complete", e.exitCode, elapsed, undefined);
|
|
139
|
+
appendLine(` ${mark}`);
|
|
37
140
|
});
|
|
38
141
|
bus.on("agent:processing-done", () => {
|
|
39
142
|
if (!panel.active)
|
|
40
143
|
return;
|
|
144
|
+
finalizeAssistantMessage();
|
|
41
145
|
panel.setDone();
|
|
42
146
|
});
|
|
43
147
|
}
|
|
@@ -75,6 +75,34 @@ export default function activate({ bus, contextManager }) {
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
|
+
register({
|
|
79
|
+
name: "/compact",
|
|
80
|
+
description: "Compact conversation (move full content to nuclear summaries)",
|
|
81
|
+
handler: () => {
|
|
82
|
+
bus.emit("agent:compact-request", {});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
register({
|
|
86
|
+
name: "/context",
|
|
87
|
+
description: "Show context budget usage",
|
|
88
|
+
handler: () => {
|
|
89
|
+
const stats = bus.emitPipe("context:get-stats", {
|
|
90
|
+
activeTokens: 0,
|
|
91
|
+
nuclearEntries: 0,
|
|
92
|
+
recallArchiveSize: 0,
|
|
93
|
+
budgetTokens: 0,
|
|
94
|
+
});
|
|
95
|
+
const pct = stats.budgetTokens > 0
|
|
96
|
+
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
97
|
+
: 0;
|
|
98
|
+
const lines = [
|
|
99
|
+
`Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
100
|
+
`Nuclear entries: ${stats.nuclearEntries} in-context`,
|
|
101
|
+
`Recall archive: ${stats.recallArchiveSize} entries`,
|
|
102
|
+
];
|
|
103
|
+
bus.emit("ui:info", { message: lines.join("\n") });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
78
106
|
// ── Extension registration ────────────────────────────────────
|
|
79
107
|
bus.on("command:register", (cmd) => {
|
|
80
108
|
register(cmd);
|
|
@@ -24,9 +24,10 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
|
24
24
|
return; // @xterm/headless not installed
|
|
25
25
|
registerTool({
|
|
26
26
|
name: "terminal_read",
|
|
27
|
-
description: "Read the
|
|
27
|
+
description: "Read what is currently visible on the user's terminal screen. Returns clean text (ANSI stripped) " +
|
|
28
28
|
"with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
|
|
29
|
-
"Use this to
|
|
29
|
+
"Use this to observe what the user sees — helpful for answering questions about terminal output, " +
|
|
30
|
+
"diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
|
|
30
31
|
input_schema: {
|
|
31
32
|
type: "object",
|
|
32
33
|
properties: {},
|
|
@@ -52,8 +53,11 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
|
52
53
|
});
|
|
53
54
|
registerTool({
|
|
54
55
|
name: "terminal_keys",
|
|
55
|
-
description: "Send keystrokes
|
|
56
|
-
"
|
|
56
|
+
description: "Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
|
|
57
|
+
"Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
|
|
58
|
+
"or to type commands at the shell prompt. Do NOT use user_shell for this — user_shell runs a new " +
|
|
59
|
+
"command in a subshell, while terminal_keys types into whatever is currently on screen.\n\n" +
|
|
60
|
+
"Escape sequences for special keys:\n" +
|
|
57
61
|
" - Escape: \\x1b\n" +
|
|
58
62
|
" - Enter/Return: \\r\n" +
|
|
59
63
|
" - Tab: \\t\n" +
|
|
@@ -105,6 +109,7 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
|
105
109
|
process.stdout.write("\n");
|
|
106
110
|
bus.emit("shell:pty-write", { data: keys });
|
|
107
111
|
await settle(settleMs);
|
|
112
|
+
bus.emit("shell:stdout-hide", {});
|
|
108
113
|
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
109
114
|
const info = [
|
|
110
115
|
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
@@ -14,7 +14,7 @@ import { highlight } from "cli-highlight";
|
|
|
14
14
|
import { MarkdownRenderer, wrapLine } from "../utils/markdown.js";
|
|
15
15
|
import { createFencedBlockTransform } from "../utils/stream-transform.js";
|
|
16
16
|
import { palette as p } from "../utils/palette.js";
|
|
17
|
-
import { renderToolCall, createSpinner,
|
|
17
|
+
import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
|
|
18
18
|
import { renderDiff } from "../utils/diff-renderer.js";
|
|
19
19
|
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
20
20
|
import { getSettings } from "../settings.js";
|
|
@@ -74,6 +74,69 @@ export default function activate(ctx) {
|
|
|
74
74
|
// Suppress all TUI output while stdout is held (overlay extensions)
|
|
75
75
|
bus.on("shell:stdout-hold", () => { writer.hold(); });
|
|
76
76
|
bus.on("shell:stdout-release", () => { writer.release(); });
|
|
77
|
+
// Gate: other extensions (e.g. overlay) can advise this to suppress
|
|
78
|
+
// TUI rendering of agent output while they own the display.
|
|
79
|
+
define("tui:should-render-agent", () => true);
|
|
80
|
+
function shouldRender() { return ctx.call("tui:should-render-agent"); }
|
|
81
|
+
// ── Advisable rendering handlers ───────────────────────────────
|
|
82
|
+
// Extensions advise these to customize how the TUI renders content.
|
|
83
|
+
// Each handler receives data and returns rendered strings.
|
|
84
|
+
define("tui:response-start", () => { });
|
|
85
|
+
define("tui:response-end", (_hadToolCalls) => { });
|
|
86
|
+
define("tui:render-info", (message) => `${p.muted}${message}${p.reset}`);
|
|
87
|
+
define("tui:render-error", (message) => `${p.error}Error: ${message}${p.reset}`);
|
|
88
|
+
define("tui:render-usage", (promptTokens, completionTokens, maxTokens) => {
|
|
89
|
+
const ctxK = (promptTokens / 1000).toFixed(1);
|
|
90
|
+
const maxK = (maxTokens / 1000).toFixed(0);
|
|
91
|
+
const pct = Math.min(100, (promptTokens / maxTokens) * 100).toFixed(0);
|
|
92
|
+
return `${p.dim}⬆ ${promptTokens} ⬇ ${completionTokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`;
|
|
93
|
+
});
|
|
94
|
+
define("tui:render-content-gap", (fromKind, toKind) => fromKind !== toKind ? "\n" : null);
|
|
95
|
+
define("tui:render-tool-complete", (exitCode, elapsed, summary) => {
|
|
96
|
+
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
97
|
+
const summaryStr = summary ? ` ${p.dim}${summary}${p.reset}` : "";
|
|
98
|
+
if (exitCode === null)
|
|
99
|
+
return `${p.muted}(timed out)${p.reset}`;
|
|
100
|
+
if (exitCode === 0)
|
|
101
|
+
return `${p.success}✓${p.reset}${summaryStr}${timer}`;
|
|
102
|
+
return `${p.error}✗ exit ${exitCode}${p.reset}${summaryStr}${timer}`;
|
|
103
|
+
});
|
|
104
|
+
define("tui:render-tool-group-summary", (count, rendered, allOk, summaries) => {
|
|
105
|
+
const mark = allOk ? `${p.success}✓${p.reset}` : `${p.error}✗${p.reset}`;
|
|
106
|
+
const summaryStr = summaries.length > 0 ? ` ${p.dim}${summaries.join(", ")}${p.reset}` : "";
|
|
107
|
+
const collapsed = count - rendered;
|
|
108
|
+
if (collapsed > 0) {
|
|
109
|
+
return ` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summaryStr}`;
|
|
110
|
+
}
|
|
111
|
+
return ` ${p.muted}└${p.reset} ${mark}${summaryStr}`;
|
|
112
|
+
});
|
|
113
|
+
define("tui:render-command-output", (line, _kind) => `${p.dim} ${line}${p.reset}`);
|
|
114
|
+
define("tui:render-spinner", (label, frame, elapsed, hint) => {
|
|
115
|
+
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
116
|
+
const hintStr = hint ? ` ${p.dim}${hint}${p.reset}` : "";
|
|
117
|
+
return `${p.accent}${frame} ${label}...${p.reset}${timer}${hintStr}`;
|
|
118
|
+
});
|
|
119
|
+
define("tui:render-user-query", (query, width, modelLabel) => {
|
|
120
|
+
const contentW = width - 4;
|
|
121
|
+
let lines = [];
|
|
122
|
+
for (const raw of query.split("\n")) {
|
|
123
|
+
for (const wrapped of wrapLine(`${p.accent}${raw}${p.reset}`, contentW)) {
|
|
124
|
+
lines.push(wrapped);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const MAX_QUERY_LINES = 20;
|
|
128
|
+
if (lines.length > MAX_QUERY_LINES) {
|
|
129
|
+
const overflow = lines.length - MAX_QUERY_LINES;
|
|
130
|
+
lines = [...lines.slice(0, MAX_QUERY_LINES), `${p.dim}… ${overflow} more lines${p.reset}`];
|
|
131
|
+
}
|
|
132
|
+
return renderBoxFrame(lines, {
|
|
133
|
+
width,
|
|
134
|
+
style: "rounded",
|
|
135
|
+
borderColor: p.accent,
|
|
136
|
+
title: `${p.accent}${p.bold}❯${p.reset}`,
|
|
137
|
+
titleRight: modelLabel,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
77
140
|
// Track backend/model info for display on response border
|
|
78
141
|
let backendInfo = null;
|
|
79
142
|
bus.on("agent:info", (info) => { backendInfo = info; });
|
|
@@ -88,12 +151,16 @@ export default function activate(ctx) {
|
|
|
88
151
|
});
|
|
89
152
|
// ── Event subscriptions ─────────────────────────────────────
|
|
90
153
|
bus.on("agent:query", (e) => {
|
|
154
|
+
if (!shouldRender())
|
|
155
|
+
return;
|
|
91
156
|
s.spinnerStartTime = 0;
|
|
92
157
|
showUserQuery(e.query);
|
|
93
158
|
startAgentResponse();
|
|
94
159
|
startThinkingSpinner();
|
|
95
160
|
});
|
|
96
161
|
bus.on("agent:thinking-chunk", (e) => {
|
|
162
|
+
if (!shouldRender())
|
|
163
|
+
return;
|
|
97
164
|
s.thinkingPending = true;
|
|
98
165
|
if (!s.isThinking) {
|
|
99
166
|
s.isThinking = true;
|
|
@@ -118,6 +185,8 @@ export default function activate(ctx) {
|
|
|
118
185
|
}
|
|
119
186
|
});
|
|
120
187
|
bus.on("agent:response-chunk", (e) => {
|
|
188
|
+
if (!shouldRender())
|
|
189
|
+
return;
|
|
121
190
|
const { blocks } = e;
|
|
122
191
|
// Inject spacing: append \n to text blocks that precede non-text blocks
|
|
123
192
|
for (let i = 0; i < blocks.length; i++) {
|
|
@@ -150,17 +219,14 @@ export default function activate(ctx) {
|
|
|
150
219
|
let pendingUsage = null;
|
|
151
220
|
bus.on("agent:usage", (e) => { pendingUsage = e; });
|
|
152
221
|
bus.on("agent:response-done", () => {
|
|
222
|
+
if (!shouldRender())
|
|
223
|
+
return;
|
|
153
224
|
s.isThinking = false;
|
|
154
225
|
if (pendingUsage && s.renderer) {
|
|
155
226
|
const { prompt_tokens, completion_tokens } = pendingUsage;
|
|
156
227
|
const maxTokens = backendInfo?.contextWindow ?? 128_000;
|
|
157
|
-
// prompt_tokens of the latest call = current context usage
|
|
158
|
-
// (it includes the full conversation history)
|
|
159
|
-
const ctxK = (prompt_tokens / 1000).toFixed(1);
|
|
160
|
-
const maxK = (maxTokens / 1000).toFixed(0);
|
|
161
|
-
const pct = Math.min(100, (prompt_tokens / maxTokens) * 100).toFixed(0);
|
|
162
228
|
s.renderer.writeLine("");
|
|
163
|
-
s.renderer.writeLine(
|
|
229
|
+
s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
|
|
164
230
|
drain();
|
|
165
231
|
pendingUsage = null;
|
|
166
232
|
}
|
|
@@ -173,6 +239,8 @@ export default function activate(ctx) {
|
|
|
173
239
|
// Batch groups: kind → { total, rendered, headerShown }
|
|
174
240
|
let batchGroups = new Map();
|
|
175
241
|
bus.on("agent:tool-batch", (e) => {
|
|
242
|
+
if (!shouldRender())
|
|
243
|
+
return;
|
|
176
244
|
fencedTransform.flush();
|
|
177
245
|
finalizeToolGroup();
|
|
178
246
|
batchGroups = new Map();
|
|
@@ -185,6 +253,8 @@ export default function activate(ctx) {
|
|
|
185
253
|
}
|
|
186
254
|
});
|
|
187
255
|
bus.on("agent:tool-started", (e) => {
|
|
256
|
+
if (!shouldRender())
|
|
257
|
+
return;
|
|
188
258
|
fencedTransform.flush();
|
|
189
259
|
stopCurrentSpinner();
|
|
190
260
|
s.currentToolKind = e.kind;
|
|
@@ -248,6 +318,8 @@ export default function activate(ctx) {
|
|
|
248
318
|
}
|
|
249
319
|
});
|
|
250
320
|
bus.on("agent:tool-completed", (e) => {
|
|
321
|
+
if (!shouldRender())
|
|
322
|
+
return;
|
|
251
323
|
s.toolExitCode = e.exitCode;
|
|
252
324
|
if (e.exitCode !== 0)
|
|
253
325
|
s.toolGroupAllOk = false;
|
|
@@ -265,20 +337,28 @@ export default function activate(ctx) {
|
|
|
265
337
|
startThinkingSpinner();
|
|
266
338
|
}
|
|
267
339
|
});
|
|
268
|
-
bus.on("agent:tool-output-chunk", (e) =>
|
|
269
|
-
|
|
340
|
+
bus.on("agent:tool-output-chunk", (e) => { if (shouldRender())
|
|
341
|
+
writeCommandOutput(e.chunk); });
|
|
342
|
+
bus.on("agent:tool-output", () => { if (shouldRender())
|
|
343
|
+
flushCommandOutput(); });
|
|
270
344
|
bus.on("agent:cancelled", () => {
|
|
345
|
+
if (!shouldRender())
|
|
346
|
+
return;
|
|
271
347
|
s.isThinking = false;
|
|
272
348
|
stopCurrentSpinner();
|
|
273
349
|
showInfo("(cancelled)");
|
|
274
350
|
endAgentResponse();
|
|
275
351
|
});
|
|
276
352
|
bus.on("agent:processing-done", () => {
|
|
353
|
+
if (!shouldRender())
|
|
354
|
+
return;
|
|
277
355
|
s.isThinking = false;
|
|
278
356
|
stopCurrentSpinner();
|
|
279
357
|
endAgentResponse();
|
|
280
358
|
});
|
|
281
359
|
bus.on("agent:error", (e) => {
|
|
360
|
+
if (!shouldRender())
|
|
361
|
+
return;
|
|
282
362
|
stopCurrentSpinner();
|
|
283
363
|
showCollapsedThinking();
|
|
284
364
|
if (!s.renderer)
|
|
@@ -289,6 +369,8 @@ export default function activate(ctx) {
|
|
|
289
369
|
drain();
|
|
290
370
|
});
|
|
291
371
|
bus.on("permission:request", (e) => {
|
|
372
|
+
if (!shouldRender())
|
|
373
|
+
return;
|
|
292
374
|
stopCurrentSpinner();
|
|
293
375
|
flushCommandOutput();
|
|
294
376
|
if (s.renderer) {
|
|
@@ -334,9 +416,9 @@ export default function activate(ctx) {
|
|
|
334
416
|
function startAgentResponse() {
|
|
335
417
|
s.renderer = new MarkdownRenderer(writer.columns);
|
|
336
418
|
s.hadToolCalls = false;
|
|
337
|
-
// Preserve lastContentKind across responses so text→tool gaps work
|
|
338
419
|
s.renderer.printTopBorder();
|
|
339
420
|
drain();
|
|
421
|
+
ctx.call("tui:response-start");
|
|
340
422
|
}
|
|
341
423
|
/**
|
|
342
424
|
* Insert an empty line when transitioning between different content kinds
|
|
@@ -345,12 +427,15 @@ export default function activate(ctx) {
|
|
|
345
427
|
*/
|
|
346
428
|
let lastEmittedLineBlank = false;
|
|
347
429
|
function contentGap(kind) {
|
|
348
|
-
if (s.lastContentKind
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
430
|
+
if (s.lastContentKind) {
|
|
431
|
+
const gap = ctx.call("tui:render-content-gap", s.lastContentKind, kind);
|
|
432
|
+
if (gap) {
|
|
433
|
+
if (s.renderer) {
|
|
434
|
+
s.renderer.flush();
|
|
435
|
+
drain();
|
|
436
|
+
}
|
|
437
|
+
writer.write(gap);
|
|
352
438
|
}
|
|
353
|
-
writer.write("\n");
|
|
354
439
|
}
|
|
355
440
|
s.lastContentKind = kind;
|
|
356
441
|
}
|
|
@@ -366,6 +451,7 @@ export default function activate(ctx) {
|
|
|
366
451
|
closeToolLine();
|
|
367
452
|
stopCurrentSpinner();
|
|
368
453
|
if (s.renderer) {
|
|
454
|
+
ctx.call("tui:response-end", s.hadToolCalls);
|
|
369
455
|
s.renderer.flush();
|
|
370
456
|
s.renderer.printBottomBorder();
|
|
371
457
|
drain();
|
|
@@ -374,39 +460,6 @@ export default function activate(ctx) {
|
|
|
374
460
|
}
|
|
375
461
|
}
|
|
376
462
|
function showUserQuery(query) {
|
|
377
|
-
const boxW = writer.columns;
|
|
378
|
-
const contentW = boxW - 4;
|
|
379
|
-
let lines = [];
|
|
380
|
-
for (const raw of query.split("\n")) {
|
|
381
|
-
if (raw.length <= contentW) {
|
|
382
|
-
lines.push(`${p.accent}${raw}${p.reset}`);
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
let remaining = raw;
|
|
386
|
-
while (remaining.length > contentW) {
|
|
387
|
-
let breakAt = remaining.lastIndexOf(" ", contentW);
|
|
388
|
-
if (breakAt <= 0)
|
|
389
|
-
breakAt = contentW;
|
|
390
|
-
lines.push(`${p.accent}${remaining.slice(0, breakAt)}${p.reset}`);
|
|
391
|
-
remaining = remaining.slice(breakAt).trimStart();
|
|
392
|
-
}
|
|
393
|
-
if (remaining)
|
|
394
|
-
lines.push(`${p.accent}${remaining}${p.reset}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// Truncate very long queries to keep the response visible
|
|
398
|
-
const MAX_QUERY_LINES = 20;
|
|
399
|
-
if (lines.length > MAX_QUERY_LINES) {
|
|
400
|
-
const overflow = lines.length - MAX_QUERY_LINES;
|
|
401
|
-
lines = [
|
|
402
|
-
...lines.slice(0, MAX_QUERY_LINES),
|
|
403
|
-
`${p.dim}… ${overflow} more lines${p.reset}`,
|
|
404
|
-
];
|
|
405
|
-
}
|
|
406
|
-
// Mode-specific border color and title
|
|
407
|
-
const borderColor = p.accent;
|
|
408
|
-
const title = `${p.accent}${p.bold}❯${p.reset}`;
|
|
409
|
-
// Backend/model label on the right (backend/model, highlighted)
|
|
410
463
|
const model = backendInfo?.model ?? llmClient?.model;
|
|
411
464
|
const backend = backendInfo?.name;
|
|
412
465
|
let modelLabel;
|
|
@@ -419,13 +472,7 @@ export default function activate(ctx) {
|
|
|
419
472
|
else if (backend) {
|
|
420
473
|
modelLabel = `${p.bold}${backend}${p.reset}`;
|
|
421
474
|
}
|
|
422
|
-
const framed =
|
|
423
|
-
width: boxW,
|
|
424
|
-
style: "rounded",
|
|
425
|
-
borderColor,
|
|
426
|
-
title,
|
|
427
|
-
titleRight: modelLabel,
|
|
428
|
-
});
|
|
475
|
+
const framed = ctx.call("tui:render-user-query", query, writer.columns, modelLabel);
|
|
429
476
|
writer.write("\n");
|
|
430
477
|
for (const line of framed) {
|
|
431
478
|
writer.write(line + "\n");
|
|
@@ -653,13 +700,7 @@ export default function activate(ctx) {
|
|
|
653
700
|
return;
|
|
654
701
|
stopCurrentSpinner();
|
|
655
702
|
const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
|
|
656
|
-
const
|
|
657
|
-
const summary = resultDisplay?.summary ? ` ${p.dim}${resultDisplay.summary}${p.reset}` : "";
|
|
658
|
-
const mark = exitCode === null
|
|
659
|
-
? `${p.muted}(timed out)${p.reset}`
|
|
660
|
-
: exitCode === 0
|
|
661
|
-
? `${p.success}✓${p.reset}${summary}${timer}`
|
|
662
|
-
: `${p.error}✗ exit ${exitCode}${p.reset}${summary}${timer}`;
|
|
703
|
+
const mark = ctx.call("tui:render-tool-complete", exitCode, elapsed, resultDisplay?.summary);
|
|
663
704
|
if (s.toolLineOpen && s.commandOutputLineCount === 0) {
|
|
664
705
|
writer.write(` ${mark}\n`);
|
|
665
706
|
s.toolLineOpen = false;
|
|
@@ -703,7 +744,10 @@ export default function activate(ctx) {
|
|
|
703
744
|
s.spinner = createSpinner({ startTime: s.spinnerStartTime });
|
|
704
745
|
s.spinnerInterval = setInterval(() => {
|
|
705
746
|
if (s.spinner) {
|
|
706
|
-
const
|
|
747
|
+
const frame = SPINNER_FRAMES[s.spinner.frame % SPINNER_FRAMES.length];
|
|
748
|
+
s.spinner.frame++;
|
|
749
|
+
const elapsed = formatElapsed(Date.now() - s.spinner.startTime);
|
|
750
|
+
const line = ctx.call("tui:render-spinner", s.spinnerLabel, frame, elapsed, s.spinnerOpts.hint);
|
|
707
751
|
writer.write(`\r ${line}\x1b[K`);
|
|
708
752
|
}
|
|
709
753
|
}, 80);
|
|
@@ -737,20 +781,8 @@ export default function activate(ctx) {
|
|
|
737
781
|
closeToolLine();
|
|
738
782
|
if (!s.renderer)
|
|
739
783
|
startAgentResponse();
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
: `${p.error}✗${p.reset}`;
|
|
743
|
-
const summary = s.toolGroupSummaries.length > 0
|
|
744
|
-
? ` ${p.dim}${s.toolGroupSummaries.join(", ")}${p.reset}`
|
|
745
|
-
: "";
|
|
746
|
-
const collapsed = s.toolGroupCount - s.toolGroupRendered;
|
|
747
|
-
if (collapsed > 0) {
|
|
748
|
-
s.renderer.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summary}`);
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
// All items visible — close the tree with └ mark + summary
|
|
752
|
-
s.renderer.writeLine(` ${p.muted}└${p.reset} ${mark}${summary}`);
|
|
753
|
-
}
|
|
784
|
+
const groupLine = ctx.call("tui:render-tool-group-summary", s.toolGroupCount, s.toolGroupRendered, s.toolGroupAllOk, s.toolGroupSummaries);
|
|
785
|
+
s.renderer.writeLine(groupLine);
|
|
754
786
|
drain();
|
|
755
787
|
s.toolGroupKind = undefined;
|
|
756
788
|
s.toolGroupCount = 0;
|
|
@@ -758,6 +790,9 @@ export default function activate(ctx) {
|
|
|
758
790
|
s.toolGroupRendered = 0;
|
|
759
791
|
s.toolGroupSummaries = [];
|
|
760
792
|
}
|
|
793
|
+
function renderCommandLine(line) {
|
|
794
|
+
return ctx.call("tui:render-command-output", line, s.currentToolKind);
|
|
795
|
+
}
|
|
761
796
|
function writeCommandOutput(chunk) {
|
|
762
797
|
if (!s.renderer)
|
|
763
798
|
return;
|
|
@@ -770,7 +805,7 @@ export default function activate(ctx) {
|
|
|
770
805
|
s.commandOutputBuffer = lines.pop();
|
|
771
806
|
for (const line of lines) {
|
|
772
807
|
if (s.commandOutputLineCount < maxLines) {
|
|
773
|
-
s.renderer.writeLine(
|
|
808
|
+
s.renderer.writeLine(renderCommandLine(line));
|
|
774
809
|
s.commandOutputLineCount++;
|
|
775
810
|
}
|
|
776
811
|
else {
|
|
@@ -790,7 +825,7 @@ export default function activate(ctx) {
|
|
|
790
825
|
: getSettings().maxCommandOutputLines;
|
|
791
826
|
if (s.commandOutputBuffer) {
|
|
792
827
|
if (s.commandOutputLineCount < maxLines) {
|
|
793
|
-
s.renderer.writeLine(
|
|
828
|
+
s.renderer.writeLine(renderCommandLine(s.commandOutputBuffer));
|
|
794
829
|
s.commandOutputLineCount++;
|
|
795
830
|
}
|
|
796
831
|
else {
|
|
@@ -805,14 +840,14 @@ export default function activate(ctx) {
|
|
|
805
840
|
const tail = s.commandOverflowLines.slice(-FAIL_OVERFLOW_MAX);
|
|
806
841
|
const skipped = s.commandOverflowLines.length - tail.length;
|
|
807
842
|
if (skipped > 0) {
|
|
808
|
-
s.renderer.writeLine(
|
|
843
|
+
s.renderer.writeLine(renderCommandLine(`… ${skipped} lines hidden`));
|
|
809
844
|
}
|
|
810
845
|
for (const line of tail) {
|
|
811
|
-
s.renderer.writeLine(
|
|
846
|
+
s.renderer.writeLine(renderCommandLine(line));
|
|
812
847
|
}
|
|
813
848
|
}
|
|
814
849
|
else if (s.commandOutputOverflow > 0 && maxLines > 0) {
|
|
815
|
-
s.renderer.writeLine(
|
|
850
|
+
s.renderer.writeLine(renderCommandLine(`… ${s.commandOutputOverflow} more lines`));
|
|
816
851
|
}
|
|
817
852
|
s.commandOutputOverflow = 0;
|
|
818
853
|
s.commandOverflowLines = [];
|
|
@@ -914,9 +949,9 @@ export default function activate(ctx) {
|
|
|
914
949
|
}
|
|
915
950
|
}
|
|
916
951
|
function showError(message) {
|
|
917
|
-
writer.write(
|
|
952
|
+
writer.write("\n" + ctx.call("tui:render-error", message) + "\n");
|
|
918
953
|
}
|
|
919
954
|
function showInfo(message) {
|
|
920
|
-
writer.write(
|
|
955
|
+
writer.write(ctx.call("tui:render-info", message) + "\n");
|
|
921
956
|
}
|
|
922
957
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const CONFIG_DIR: string;
|
|
2
|
+
/** Per-model capability overrides. */
|
|
3
|
+
export interface ModelCapabilityConfig {
|
|
4
|
+
/** Model identifier. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Whether the model supports reasoning/thinking tokens. */
|
|
7
|
+
reasoning?: boolean;
|
|
8
|
+
/** Context window size in tokens for this specific model. */
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
}
|
|
2
11
|
/** Provider profile — a named LLM configuration. */
|
|
3
12
|
export interface ProviderConfig {
|
|
4
13
|
/** API key (supports $ENV_VAR syntax for runtime expansion). */
|
|
@@ -7,8 +16,8 @@ export interface ProviderConfig {
|
|
|
7
16
|
baseURL?: string;
|
|
8
17
|
/** Default model to use. Falls back to first entry in models list. */
|
|
9
18
|
defaultModel?: string;
|
|
10
|
-
/** Models available for cycling. */
|
|
11
|
-
models?: string[];
|
|
19
|
+
/** Models available for cycling. Plain strings or objects with capabilities. */
|
|
20
|
+
models?: (string | ModelCapabilityConfig)[];
|
|
12
21
|
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
13
22
|
contextWindow?: number;
|
|
14
23
|
}
|
|
@@ -35,6 +44,14 @@ export interface Settings {
|
|
|
35
44
|
shellTailLines?: number;
|
|
36
45
|
/** Max lines for recall expand before requiring line ranges. */
|
|
37
46
|
recallExpandMaxLines?: number;
|
|
47
|
+
/** Fraction of content budget allocated to shell context (0-1, default 0.35). */
|
|
48
|
+
shellContextRatio?: number;
|
|
49
|
+
/** Max history file size in bytes (default: 102400 = 100KB). */
|
|
50
|
+
historyMaxBytes?: number;
|
|
51
|
+
/** Number of prior history entries to load on startup (default: 50). */
|
|
52
|
+
historyStartupEntries?: number;
|
|
53
|
+
/** Max nuclear entries kept in-context before flushing to history file (default: 200). */
|
|
54
|
+
nuclearMaxEntries?: number;
|
|
38
55
|
/** Max command output lines shown inline in TUI. */
|
|
39
56
|
maxCommandOutputLines?: number;
|
|
40
57
|
/** Max read tool output lines shown inline in TUI (0 = hide). */
|