agent-sh 0.6.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 +3 -2
- package/dist/context-manager.js +16 -111
- package/dist/core.js +30 -1
- package/dist/event-bus.d.ts +37 -0
- package/dist/extensions/overlay-agent.d.ts +14 -0
- package/dist/extensions/overlay-agent.js +147 -0
- package/dist/extensions/slash-commands.js +28 -0
- package/dist/extensions/terminal-buffer.d.ts +14 -0
- package/dist/extensions/terminal-buffer.js +125 -0
- package/dist/extensions/tui-renderer.js +122 -84
- package/dist/index.js +4 -0
- package/dist/input-handler.js +6 -1
- package/dist/output-parser.js +8 -0
- package/dist/settings.d.ts +19 -2
- package/dist/settings.js +21 -3
- package/dist/shell.d.ts +5 -0
- package/dist/shell.js +31 -2
- package/dist/token-budget.d.ts +13 -0
- package/dist/token-budget.js +50 -0
- package/dist/types.d.ts +13 -22
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/floating-panel.d.ts +227 -0
- package/dist/utils/floating-panel.js +807 -0
- 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/output-writer.d.ts +14 -0
- package/dist/utils/output-writer.js +16 -0
- package/dist/utils/terminal-buffer.d.ts +69 -0
- package/dist/utils/terminal-buffer.js +179 -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/overlay-agent.ts +70 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/terminal-buffer.ts +184 -0
- package/package.json +5 -1
|
@@ -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";
|
|
@@ -71,6 +71,72 @@ export default function activate(ctx) {
|
|
|
71
71
|
const { bus, llmClient, define } = ctx;
|
|
72
72
|
const writer = new StdoutWriter();
|
|
73
73
|
const s = createRenderState();
|
|
74
|
+
// Suppress all TUI output while stdout is held (overlay extensions)
|
|
75
|
+
bus.on("shell:stdout-hold", () => { writer.hold(); });
|
|
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
|
+
});
|
|
74
140
|
// Track backend/model info for display on response border
|
|
75
141
|
let backendInfo = null;
|
|
76
142
|
bus.on("agent:info", (info) => { backendInfo = info; });
|
|
@@ -85,12 +151,16 @@ export default function activate(ctx) {
|
|
|
85
151
|
});
|
|
86
152
|
// ── Event subscriptions ─────────────────────────────────────
|
|
87
153
|
bus.on("agent:query", (e) => {
|
|
154
|
+
if (!shouldRender())
|
|
155
|
+
return;
|
|
88
156
|
s.spinnerStartTime = 0;
|
|
89
157
|
showUserQuery(e.query);
|
|
90
158
|
startAgentResponse();
|
|
91
159
|
startThinkingSpinner();
|
|
92
160
|
});
|
|
93
161
|
bus.on("agent:thinking-chunk", (e) => {
|
|
162
|
+
if (!shouldRender())
|
|
163
|
+
return;
|
|
94
164
|
s.thinkingPending = true;
|
|
95
165
|
if (!s.isThinking) {
|
|
96
166
|
s.isThinking = true;
|
|
@@ -115,6 +185,8 @@ export default function activate(ctx) {
|
|
|
115
185
|
}
|
|
116
186
|
});
|
|
117
187
|
bus.on("agent:response-chunk", (e) => {
|
|
188
|
+
if (!shouldRender())
|
|
189
|
+
return;
|
|
118
190
|
const { blocks } = e;
|
|
119
191
|
// Inject spacing: append \n to text blocks that precede non-text blocks
|
|
120
192
|
for (let i = 0; i < blocks.length; i++) {
|
|
@@ -147,17 +219,14 @@ export default function activate(ctx) {
|
|
|
147
219
|
let pendingUsage = null;
|
|
148
220
|
bus.on("agent:usage", (e) => { pendingUsage = e; });
|
|
149
221
|
bus.on("agent:response-done", () => {
|
|
222
|
+
if (!shouldRender())
|
|
223
|
+
return;
|
|
150
224
|
s.isThinking = false;
|
|
151
225
|
if (pendingUsage && s.renderer) {
|
|
152
226
|
const { prompt_tokens, completion_tokens } = pendingUsage;
|
|
153
227
|
const maxTokens = backendInfo?.contextWindow ?? 128_000;
|
|
154
|
-
// prompt_tokens of the latest call = current context usage
|
|
155
|
-
// (it includes the full conversation history)
|
|
156
|
-
const ctxK = (prompt_tokens / 1000).toFixed(1);
|
|
157
|
-
const maxK = (maxTokens / 1000).toFixed(0);
|
|
158
|
-
const pct = Math.min(100, (prompt_tokens / maxTokens) * 100).toFixed(0);
|
|
159
228
|
s.renderer.writeLine("");
|
|
160
|
-
s.renderer.writeLine(
|
|
229
|
+
s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
|
|
161
230
|
drain();
|
|
162
231
|
pendingUsage = null;
|
|
163
232
|
}
|
|
@@ -170,6 +239,8 @@ export default function activate(ctx) {
|
|
|
170
239
|
// Batch groups: kind → { total, rendered, headerShown }
|
|
171
240
|
let batchGroups = new Map();
|
|
172
241
|
bus.on("agent:tool-batch", (e) => {
|
|
242
|
+
if (!shouldRender())
|
|
243
|
+
return;
|
|
173
244
|
fencedTransform.flush();
|
|
174
245
|
finalizeToolGroup();
|
|
175
246
|
batchGroups = new Map();
|
|
@@ -182,6 +253,8 @@ export default function activate(ctx) {
|
|
|
182
253
|
}
|
|
183
254
|
});
|
|
184
255
|
bus.on("agent:tool-started", (e) => {
|
|
256
|
+
if (!shouldRender())
|
|
257
|
+
return;
|
|
185
258
|
fencedTransform.flush();
|
|
186
259
|
stopCurrentSpinner();
|
|
187
260
|
s.currentToolKind = e.kind;
|
|
@@ -245,6 +318,8 @@ export default function activate(ctx) {
|
|
|
245
318
|
}
|
|
246
319
|
});
|
|
247
320
|
bus.on("agent:tool-completed", (e) => {
|
|
321
|
+
if (!shouldRender())
|
|
322
|
+
return;
|
|
248
323
|
s.toolExitCode = e.exitCode;
|
|
249
324
|
if (e.exitCode !== 0)
|
|
250
325
|
s.toolGroupAllOk = false;
|
|
@@ -262,20 +337,28 @@ export default function activate(ctx) {
|
|
|
262
337
|
startThinkingSpinner();
|
|
263
338
|
}
|
|
264
339
|
});
|
|
265
|
-
bus.on("agent:tool-output-chunk", (e) =>
|
|
266
|
-
|
|
340
|
+
bus.on("agent:tool-output-chunk", (e) => { if (shouldRender())
|
|
341
|
+
writeCommandOutput(e.chunk); });
|
|
342
|
+
bus.on("agent:tool-output", () => { if (shouldRender())
|
|
343
|
+
flushCommandOutput(); });
|
|
267
344
|
bus.on("agent:cancelled", () => {
|
|
345
|
+
if (!shouldRender())
|
|
346
|
+
return;
|
|
268
347
|
s.isThinking = false;
|
|
269
348
|
stopCurrentSpinner();
|
|
270
349
|
showInfo("(cancelled)");
|
|
271
350
|
endAgentResponse();
|
|
272
351
|
});
|
|
273
352
|
bus.on("agent:processing-done", () => {
|
|
353
|
+
if (!shouldRender())
|
|
354
|
+
return;
|
|
274
355
|
s.isThinking = false;
|
|
275
356
|
stopCurrentSpinner();
|
|
276
357
|
endAgentResponse();
|
|
277
358
|
});
|
|
278
359
|
bus.on("agent:error", (e) => {
|
|
360
|
+
if (!shouldRender())
|
|
361
|
+
return;
|
|
279
362
|
stopCurrentSpinner();
|
|
280
363
|
showCollapsedThinking();
|
|
281
364
|
if (!s.renderer)
|
|
@@ -286,6 +369,8 @@ export default function activate(ctx) {
|
|
|
286
369
|
drain();
|
|
287
370
|
});
|
|
288
371
|
bus.on("permission:request", (e) => {
|
|
372
|
+
if (!shouldRender())
|
|
373
|
+
return;
|
|
289
374
|
stopCurrentSpinner();
|
|
290
375
|
flushCommandOutput();
|
|
291
376
|
if (s.renderer) {
|
|
@@ -331,9 +416,9 @@ export default function activate(ctx) {
|
|
|
331
416
|
function startAgentResponse() {
|
|
332
417
|
s.renderer = new MarkdownRenderer(writer.columns);
|
|
333
418
|
s.hadToolCalls = false;
|
|
334
|
-
// Preserve lastContentKind across responses so text→tool gaps work
|
|
335
419
|
s.renderer.printTopBorder();
|
|
336
420
|
drain();
|
|
421
|
+
ctx.call("tui:response-start");
|
|
337
422
|
}
|
|
338
423
|
/**
|
|
339
424
|
* Insert an empty line when transitioning between different content kinds
|
|
@@ -342,12 +427,15 @@ export default function activate(ctx) {
|
|
|
342
427
|
*/
|
|
343
428
|
let lastEmittedLineBlank = false;
|
|
344
429
|
function contentGap(kind) {
|
|
345
|
-
if (s.lastContentKind
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
|
349
438
|
}
|
|
350
|
-
writer.write("\n");
|
|
351
439
|
}
|
|
352
440
|
s.lastContentKind = kind;
|
|
353
441
|
}
|
|
@@ -363,6 +451,7 @@ export default function activate(ctx) {
|
|
|
363
451
|
closeToolLine();
|
|
364
452
|
stopCurrentSpinner();
|
|
365
453
|
if (s.renderer) {
|
|
454
|
+
ctx.call("tui:response-end", s.hadToolCalls);
|
|
366
455
|
s.renderer.flush();
|
|
367
456
|
s.renderer.printBottomBorder();
|
|
368
457
|
drain();
|
|
@@ -371,39 +460,6 @@ export default function activate(ctx) {
|
|
|
371
460
|
}
|
|
372
461
|
}
|
|
373
462
|
function showUserQuery(query) {
|
|
374
|
-
const boxW = writer.columns;
|
|
375
|
-
const contentW = boxW - 4;
|
|
376
|
-
let lines = [];
|
|
377
|
-
for (const raw of query.split("\n")) {
|
|
378
|
-
if (raw.length <= contentW) {
|
|
379
|
-
lines.push(`${p.accent}${raw}${p.reset}`);
|
|
380
|
-
}
|
|
381
|
-
else {
|
|
382
|
-
let remaining = raw;
|
|
383
|
-
while (remaining.length > contentW) {
|
|
384
|
-
let breakAt = remaining.lastIndexOf(" ", contentW);
|
|
385
|
-
if (breakAt <= 0)
|
|
386
|
-
breakAt = contentW;
|
|
387
|
-
lines.push(`${p.accent}${remaining.slice(0, breakAt)}${p.reset}`);
|
|
388
|
-
remaining = remaining.slice(breakAt).trimStart();
|
|
389
|
-
}
|
|
390
|
-
if (remaining)
|
|
391
|
-
lines.push(`${p.accent}${remaining}${p.reset}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// Truncate very long queries to keep the response visible
|
|
395
|
-
const MAX_QUERY_LINES = 20;
|
|
396
|
-
if (lines.length > MAX_QUERY_LINES) {
|
|
397
|
-
const overflow = lines.length - MAX_QUERY_LINES;
|
|
398
|
-
lines = [
|
|
399
|
-
...lines.slice(0, MAX_QUERY_LINES),
|
|
400
|
-
`${p.dim}… ${overflow} more lines${p.reset}`,
|
|
401
|
-
];
|
|
402
|
-
}
|
|
403
|
-
// Mode-specific border color and title
|
|
404
|
-
const borderColor = p.accent;
|
|
405
|
-
const title = `${p.accent}${p.bold}❯${p.reset}`;
|
|
406
|
-
// Backend/model label on the right (backend/model, highlighted)
|
|
407
463
|
const model = backendInfo?.model ?? llmClient?.model;
|
|
408
464
|
const backend = backendInfo?.name;
|
|
409
465
|
let modelLabel;
|
|
@@ -416,13 +472,7 @@ export default function activate(ctx) {
|
|
|
416
472
|
else if (backend) {
|
|
417
473
|
modelLabel = `${p.bold}${backend}${p.reset}`;
|
|
418
474
|
}
|
|
419
|
-
const framed =
|
|
420
|
-
width: boxW,
|
|
421
|
-
style: "rounded",
|
|
422
|
-
borderColor,
|
|
423
|
-
title,
|
|
424
|
-
titleRight: modelLabel,
|
|
425
|
-
});
|
|
475
|
+
const framed = ctx.call("tui:render-user-query", query, writer.columns, modelLabel);
|
|
426
476
|
writer.write("\n");
|
|
427
477
|
for (const line of framed) {
|
|
428
478
|
writer.write(line + "\n");
|
|
@@ -650,13 +700,7 @@ export default function activate(ctx) {
|
|
|
650
700
|
return;
|
|
651
701
|
stopCurrentSpinner();
|
|
652
702
|
const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
|
|
653
|
-
const
|
|
654
|
-
const summary = resultDisplay?.summary ? ` ${p.dim}${resultDisplay.summary}${p.reset}` : "";
|
|
655
|
-
const mark = exitCode === null
|
|
656
|
-
? `${p.muted}(timed out)${p.reset}`
|
|
657
|
-
: exitCode === 0
|
|
658
|
-
? `${p.success}✓${p.reset}${summary}${timer}`
|
|
659
|
-
: `${p.error}✗ exit ${exitCode}${p.reset}${summary}${timer}`;
|
|
703
|
+
const mark = ctx.call("tui:render-tool-complete", exitCode, elapsed, resultDisplay?.summary);
|
|
660
704
|
if (s.toolLineOpen && s.commandOutputLineCount === 0) {
|
|
661
705
|
writer.write(` ${mark}\n`);
|
|
662
706
|
s.toolLineOpen = false;
|
|
@@ -700,7 +744,10 @@ export default function activate(ctx) {
|
|
|
700
744
|
s.spinner = createSpinner({ startTime: s.spinnerStartTime });
|
|
701
745
|
s.spinnerInterval = setInterval(() => {
|
|
702
746
|
if (s.spinner) {
|
|
703
|
-
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);
|
|
704
751
|
writer.write(`\r ${line}\x1b[K`);
|
|
705
752
|
}
|
|
706
753
|
}, 80);
|
|
@@ -734,20 +781,8 @@ export default function activate(ctx) {
|
|
|
734
781
|
closeToolLine();
|
|
735
782
|
if (!s.renderer)
|
|
736
783
|
startAgentResponse();
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
: `${p.error}✗${p.reset}`;
|
|
740
|
-
const summary = s.toolGroupSummaries.length > 0
|
|
741
|
-
? ` ${p.dim}${s.toolGroupSummaries.join(", ")}${p.reset}`
|
|
742
|
-
: "";
|
|
743
|
-
const collapsed = s.toolGroupCount - s.toolGroupRendered;
|
|
744
|
-
if (collapsed > 0) {
|
|
745
|
-
s.renderer.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summary}`);
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
// All items visible — close the tree with └ mark + summary
|
|
749
|
-
s.renderer.writeLine(` ${p.muted}└${p.reset} ${mark}${summary}`);
|
|
750
|
-
}
|
|
784
|
+
const groupLine = ctx.call("tui:render-tool-group-summary", s.toolGroupCount, s.toolGroupRendered, s.toolGroupAllOk, s.toolGroupSummaries);
|
|
785
|
+
s.renderer.writeLine(groupLine);
|
|
751
786
|
drain();
|
|
752
787
|
s.toolGroupKind = undefined;
|
|
753
788
|
s.toolGroupCount = 0;
|
|
@@ -755,6 +790,9 @@ export default function activate(ctx) {
|
|
|
755
790
|
s.toolGroupRendered = 0;
|
|
756
791
|
s.toolGroupSummaries = [];
|
|
757
792
|
}
|
|
793
|
+
function renderCommandLine(line) {
|
|
794
|
+
return ctx.call("tui:render-command-output", line, s.currentToolKind);
|
|
795
|
+
}
|
|
758
796
|
function writeCommandOutput(chunk) {
|
|
759
797
|
if (!s.renderer)
|
|
760
798
|
return;
|
|
@@ -767,7 +805,7 @@ export default function activate(ctx) {
|
|
|
767
805
|
s.commandOutputBuffer = lines.pop();
|
|
768
806
|
for (const line of lines) {
|
|
769
807
|
if (s.commandOutputLineCount < maxLines) {
|
|
770
|
-
s.renderer.writeLine(
|
|
808
|
+
s.renderer.writeLine(renderCommandLine(line));
|
|
771
809
|
s.commandOutputLineCount++;
|
|
772
810
|
}
|
|
773
811
|
else {
|
|
@@ -787,7 +825,7 @@ export default function activate(ctx) {
|
|
|
787
825
|
: getSettings().maxCommandOutputLines;
|
|
788
826
|
if (s.commandOutputBuffer) {
|
|
789
827
|
if (s.commandOutputLineCount < maxLines) {
|
|
790
|
-
s.renderer.writeLine(
|
|
828
|
+
s.renderer.writeLine(renderCommandLine(s.commandOutputBuffer));
|
|
791
829
|
s.commandOutputLineCount++;
|
|
792
830
|
}
|
|
793
831
|
else {
|
|
@@ -802,14 +840,14 @@ export default function activate(ctx) {
|
|
|
802
840
|
const tail = s.commandOverflowLines.slice(-FAIL_OVERFLOW_MAX);
|
|
803
841
|
const skipped = s.commandOverflowLines.length - tail.length;
|
|
804
842
|
if (skipped > 0) {
|
|
805
|
-
s.renderer.writeLine(
|
|
843
|
+
s.renderer.writeLine(renderCommandLine(`… ${skipped} lines hidden`));
|
|
806
844
|
}
|
|
807
845
|
for (const line of tail) {
|
|
808
|
-
s.renderer.writeLine(
|
|
846
|
+
s.renderer.writeLine(renderCommandLine(line));
|
|
809
847
|
}
|
|
810
848
|
}
|
|
811
849
|
else if (s.commandOutputOverflow > 0 && maxLines > 0) {
|
|
812
|
-
s.renderer.writeLine(
|
|
850
|
+
s.renderer.writeLine(renderCommandLine(`… ${s.commandOutputOverflow} more lines`));
|
|
813
851
|
}
|
|
814
852
|
s.commandOutputOverflow = 0;
|
|
815
853
|
s.commandOverflowLines = [];
|
|
@@ -911,9 +949,9 @@ export default function activate(ctx) {
|
|
|
911
949
|
}
|
|
912
950
|
}
|
|
913
951
|
function showError(message) {
|
|
914
|
-
writer.write(
|
|
952
|
+
writer.write("\n" + ctx.call("tui:render-error", message) + "\n");
|
|
915
953
|
}
|
|
916
954
|
function showInfo(message) {
|
|
917
|
-
writer.write(
|
|
955
|
+
writer.write(ctx.call("tui:render-info", message) + "\n");
|
|
918
956
|
}
|
|
919
957
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import slashCommands from "./extensions/slash-commands.js";
|
|
|
9
9
|
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
10
10
|
import shellRecall from "./extensions/shell-recall.js";
|
|
11
11
|
import commandSuggest from "./extensions/command-suggest.js";
|
|
12
|
+
import terminalBuffer from "./extensions/terminal-buffer.js";
|
|
13
|
+
import overlayAgent from "./extensions/overlay-agent.js";
|
|
12
14
|
import { loadExtensions } from "./extension-loader.js";
|
|
13
15
|
import { getSettings } from "./settings.js";
|
|
14
16
|
import { discoverSkills } from "./agent/skills.js";
|
|
@@ -232,6 +234,8 @@ async function main() {
|
|
|
232
234
|
fileAutocomplete(extCtx);
|
|
233
235
|
shellRecall(extCtx);
|
|
234
236
|
commandSuggest(extCtx);
|
|
237
|
+
terminalBuffer(extCtx);
|
|
238
|
+
overlayAgent(extCtx);
|
|
235
239
|
// Load user extensions (may register alternative agent backends)
|
|
236
240
|
if (process.env.DEBUG) {
|
|
237
241
|
console.error('[agent-sh] Loading extensions...');
|
package/dist/input-handler.js
CHANGED
|
@@ -137,6 +137,10 @@ export class InputHandler {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
handleInput(data) {
|
|
140
|
+
// Allow extensions to capture raw input (e.g. overlay prompt during vim)
|
|
141
|
+
const intercepted = this.bus.emitPipe("input:intercept", { data, consumed: false });
|
|
142
|
+
if (intercepted.consumed)
|
|
143
|
+
return;
|
|
140
144
|
// If agent is running (processing a query), only Ctrl-C and control keys
|
|
141
145
|
if (this.ctx.isAgentActive()) {
|
|
142
146
|
if (data === "\x03") {
|
|
@@ -235,7 +239,8 @@ export class InputHandler {
|
|
|
235
239
|
this.enterMode(mode);
|
|
236
240
|
return; // don't process remaining chars
|
|
237
241
|
}
|
|
238
|
-
this.
|
|
242
|
+
if (!this.ctx.isForegroundBusy())
|
|
243
|
+
this.lineBuffer += ch;
|
|
239
244
|
this.ctx.writeToPty(ch);
|
|
240
245
|
}
|
|
241
246
|
}
|
package/dist/output-parser.js
CHANGED
|
@@ -109,7 +109,15 @@ export class OutputParser {
|
|
|
109
109
|
this.currentOutputCapture = "";
|
|
110
110
|
}
|
|
111
111
|
else {
|
|
112
|
+
// Cap capture buffer to avoid unbounded growth when a foreground
|
|
113
|
+
// program (tmux, vim, etc.) produces output without prompt markers.
|
|
114
|
+
// Keep only the tail — the final output is what matters for
|
|
115
|
+
// command-done context.
|
|
116
|
+
const MAX_CAPTURE = 128 * 1024; // 128 KB
|
|
112
117
|
this.currentOutputCapture += data;
|
|
118
|
+
if (this.currentOutputCapture.length > MAX_CAPTURE) {
|
|
119
|
+
this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
|
|
120
|
+
}
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
/**
|
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). */
|
package/dist/settings.js
CHANGED
|
@@ -21,6 +21,10 @@ const DEFAULTS = {
|
|
|
21
21
|
shellHeadLines: 5,
|
|
22
22
|
shellTailLines: 5,
|
|
23
23
|
recallExpandMaxLines: 100,
|
|
24
|
+
shellContextRatio: 0.35,
|
|
25
|
+
historyMaxBytes: 102400,
|
|
26
|
+
historyStartupEntries: 50,
|
|
27
|
+
nuclearMaxEntries: 200,
|
|
24
28
|
maxCommandOutputLines: 3,
|
|
25
29
|
readOutputMaxLines: 10,
|
|
26
30
|
diffMaxLines: 20,
|
|
@@ -86,15 +90,29 @@ export function resolveProvider(name) {
|
|
|
86
90
|
const provider = settings.providers?.[name];
|
|
87
91
|
if (!provider)
|
|
88
92
|
return null;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
93
|
+
const rawModels = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
|
|
94
|
+
const modelIds = [];
|
|
95
|
+
const caps = new Map();
|
|
96
|
+
for (const m of rawModels) {
|
|
97
|
+
if (typeof m === "string") {
|
|
98
|
+
modelIds.push(m);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
modelIds.push(m.id);
|
|
102
|
+
if (m.reasoning !== undefined || m.contextWindow !== undefined) {
|
|
103
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const defaultModel = provider.defaultModel ?? modelIds[0];
|
|
91
108
|
return {
|
|
92
109
|
id: name,
|
|
93
110
|
apiKey: provider.apiKey ? expandEnvVars(provider.apiKey) : undefined,
|
|
94
111
|
baseURL: provider.baseURL,
|
|
95
112
|
defaultModel,
|
|
96
|
-
models:
|
|
113
|
+
models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
|
|
97
114
|
contextWindow: provider.contextWindow,
|
|
115
|
+
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
98
116
|
};
|
|
99
117
|
}
|
|
100
118
|
/** Get all configured provider names. */
|
package/dist/shell.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare class Shell implements InputContext {
|
|
|
6
6
|
private inputHandler;
|
|
7
7
|
private outputParser;
|
|
8
8
|
private paused;
|
|
9
|
+
private stdoutHold;
|
|
10
|
+
private stdoutShow;
|
|
9
11
|
private echoSkip;
|
|
10
12
|
private agentActive;
|
|
11
13
|
private isZsh;
|
|
@@ -37,6 +39,9 @@ export declare class Shell implements InputContext {
|
|
|
37
39
|
* Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
|
|
38
40
|
* Use this after agent responses where stdout has moved far from where
|
|
39
41
|
* zle expects the cursor. The blank line is acceptable as a separator.
|
|
42
|
+
*
|
|
43
|
+
* Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
|
|
44
|
+
* can suppress it by setting `handled: true`.
|
|
40
45
|
*/
|
|
41
46
|
freshPrompt(): void;
|
|
42
47
|
onCommandEntered(command: string, cwd: string): void;
|
package/dist/shell.js
CHANGED
|
@@ -5,12 +5,15 @@ import * as pty from "node-pty";
|
|
|
5
5
|
import { InputHandler } from "./input-handler.js";
|
|
6
6
|
import { OutputParser } from "./output-parser.js";
|
|
7
7
|
import { getSettings } from "./settings.js";
|
|
8
|
+
import { RefCounter } from "./utils/output-writer.js";
|
|
8
9
|
export class Shell {
|
|
9
10
|
ptyProcess;
|
|
10
11
|
bus;
|
|
11
12
|
inputHandler;
|
|
12
13
|
outputParser;
|
|
13
14
|
paused = false;
|
|
15
|
+
stdoutHold = new RefCounter();
|
|
16
|
+
stdoutShow = new RefCounter();
|
|
14
17
|
echoSkip = false;
|
|
15
18
|
agentActive = false;
|
|
16
19
|
isZsh = false;
|
|
@@ -156,6 +159,20 @@ export class Shell {
|
|
|
156
159
|
this.setupOutput();
|
|
157
160
|
this.setupInput();
|
|
158
161
|
this.setupAgentLifecycle();
|
|
162
|
+
// Allow extensions to inject raw keystrokes into the PTY
|
|
163
|
+
this.bus.on("shell:pty-write", ({ data }) => {
|
|
164
|
+
this.ptyProcess.write(data);
|
|
165
|
+
});
|
|
166
|
+
// Allow extensions to resize the PTY (sends SIGWINCH to child)
|
|
167
|
+
this.bus.on("shell:pty-resize", ({ cols, rows }) => {
|
|
168
|
+
this.ptyProcess.resize(cols, rows);
|
|
169
|
+
});
|
|
170
|
+
// Ref-counted stdout hold — overlay extensions suppress PTY output
|
|
171
|
+
this.bus.on("shell:stdout-hold", () => { this.stdoutHold.increment(); });
|
|
172
|
+
this.bus.on("shell:stdout-release", () => { this.stdoutHold.decrement(); });
|
|
173
|
+
// Ref-counted stdout show — tools temporarily force output visible during agent processing
|
|
174
|
+
this.bus.on("shell:stdout-show", () => { this.stdoutShow.increment(); });
|
|
175
|
+
this.bus.on("shell:stdout-hide", () => { this.stdoutShow.decrement(); });
|
|
159
176
|
}
|
|
160
177
|
// ── InputContext implementation (delegates to OutputParser) ──
|
|
161
178
|
isForegroundBusy() {
|
|
@@ -197,9 +214,18 @@ export class Shell {
|
|
|
197
214
|
* Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
|
|
198
215
|
* Use this after agent responses where stdout has moved far from where
|
|
199
216
|
* zle expects the cursor. The blank line is acceptable as a separator.
|
|
217
|
+
*
|
|
218
|
+
* Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
|
|
219
|
+
* can suppress it by setting `handled: true`.
|
|
200
220
|
*/
|
|
201
221
|
freshPrompt() {
|
|
202
|
-
this.
|
|
222
|
+
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
223
|
+
cwd: this.outputParser.getCwd(),
|
|
224
|
+
handled: false,
|
|
225
|
+
});
|
|
226
|
+
if (!result.handled) {
|
|
227
|
+
this.ptyProcess.write("\n");
|
|
228
|
+
}
|
|
203
229
|
}
|
|
204
230
|
onCommandEntered(command, cwd) {
|
|
205
231
|
this.outputParser.onCommandEntered(command, cwd);
|
|
@@ -207,8 +233,11 @@ export class Shell {
|
|
|
207
233
|
// ── PTY I/O wiring ─────────────────────────────────────────
|
|
208
234
|
setupOutput() {
|
|
209
235
|
this.ptyProcess.onData((data) => {
|
|
236
|
+
this.bus.emit("shell:pty-data", { raw: data });
|
|
210
237
|
this.outputParser.processData(data);
|
|
211
|
-
if (this.
|
|
238
|
+
if (this.stdoutHold.active)
|
|
239
|
+
return;
|
|
240
|
+
if (this.paused && !this.stdoutShow.active)
|
|
212
241
|
return;
|
|
213
242
|
// During user_shell exec, skip the command echo (first line)
|
|
214
243
|
if (this.echoSkip) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class TokenBudget {
|
|
2
|
+
private contextWindow;
|
|
3
|
+
private toolCount;
|
|
4
|
+
constructor(contextWindow?: number, toolCount?: number);
|
|
5
|
+
/** Update when model or tool set changes. */
|
|
6
|
+
update(contextWindow?: number, toolCount?: number): void;
|
|
7
|
+
/** Total tokens available for shell context + conversation content. */
|
|
8
|
+
get contentBudget(): number;
|
|
9
|
+
/** Token budget for the shell context stream. */
|
|
10
|
+
get shellBudgetTokens(): number;
|
|
11
|
+
/** Token budget for the conversation messages stream. */
|
|
12
|
+
get conversationBudgetTokens(): number;
|
|
13
|
+
}
|