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.
Files changed (50) hide show
  1. package/README.md +5 -1
  2. package/dist/agent/agent-loop.d.ts +2 -2
  3. package/dist/agent/agent-loop.js +106 -13
  4. package/dist/agent/conversation-state.d.ts +39 -9
  5. package/dist/agent/conversation-state.js +336 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +175 -0
  10. package/dist/agent/system-prompt.d.ts +2 -2
  11. package/dist/agent/system-prompt.js +25 -4
  12. package/dist/agent/tools/user-shell.js +4 -1
  13. package/dist/context-manager.d.ts +3 -2
  14. package/dist/context-manager.js +16 -111
  15. package/dist/core.js +30 -1
  16. package/dist/event-bus.d.ts +37 -0
  17. package/dist/extensions/overlay-agent.d.ts +14 -0
  18. package/dist/extensions/overlay-agent.js +147 -0
  19. package/dist/extensions/slash-commands.js +28 -0
  20. package/dist/extensions/terminal-buffer.d.ts +14 -0
  21. package/dist/extensions/terminal-buffer.js +125 -0
  22. package/dist/extensions/tui-renderer.js +122 -84
  23. package/dist/index.js +4 -0
  24. package/dist/input-handler.js +6 -1
  25. package/dist/output-parser.js +8 -0
  26. package/dist/settings.d.ts +19 -2
  27. package/dist/settings.js +21 -3
  28. package/dist/shell.d.ts +5 -0
  29. package/dist/shell.js +31 -2
  30. package/dist/token-budget.d.ts +13 -0
  31. package/dist/token-budget.js +50 -0
  32. package/dist/types.d.ts +13 -22
  33. package/dist/utils/ansi.d.ts +10 -0
  34. package/dist/utils/ansi.js +27 -0
  35. package/dist/utils/floating-panel.d.ts +227 -0
  36. package/dist/utils/floating-panel.js +807 -0
  37. package/dist/utils/line-editor.d.ts +9 -0
  38. package/dist/utils/line-editor.js +44 -0
  39. package/dist/utils/markdown.js +3 -3
  40. package/dist/utils/output-writer.d.ts +14 -0
  41. package/dist/utils/output-writer.js +16 -0
  42. package/dist/utils/terminal-buffer.d.ts +69 -0
  43. package/dist/utils/terminal-buffer.js +179 -0
  44. package/dist/utils/tool-display.d.ts +1 -0
  45. package/dist/utils/tool-display.js +1 -1
  46. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  47. package/examples/extensions/overlay-agent.ts +70 -0
  48. package/examples/extensions/pi-bridge/index.ts +87 -2
  49. package/examples/extensions/terminal-buffer.ts +184 -0
  50. 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, renderSpinnerLine, formatElapsed, } from "../utils/tool-display.js";
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(`${p.dim}⬆ ${prompt_tokens} ⬇ ${completion_tokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`);
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) => writeCommandOutput(e.chunk));
266
- bus.on("agent:tool-output", () => flushCommandOutput());
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 && s.lastContentKind !== kind) {
346
- if (s.renderer) {
347
- s.renderer.flush();
348
- drain();
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 = renderBoxFrame(lines, {
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 timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
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 line = renderSpinnerLine(s.spinner, s.spinnerLabel, s.spinnerOpts);
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 mark = s.toolGroupAllOk
738
- ? `${p.success}✓${p.reset}`
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(`${p.dim} ${line}${p.reset}`);
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(`${p.dim} ${s.commandOutputBuffer}${p.reset}`);
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(`${p.dim} … ${skipped} lines hidden${p.reset}`);
843
+ s.renderer.writeLine(renderCommandLine(`… ${skipped} lines hidden`));
806
844
  }
807
845
  for (const line of tail) {
808
- s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
846
+ s.renderer.writeLine(renderCommandLine(line));
809
847
  }
810
848
  }
811
849
  else if (s.commandOutputOverflow > 0 && maxLines > 0) {
812
- s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
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(`\n${p.error}Error: ${message}${p.reset}\n`);
952
+ writer.write("\n" + ctx.call("tui:render-error", message) + "\n");
915
953
  }
916
954
  function showInfo(message) {
917
- writer.write(`${p.muted}${message}${p.reset}\n`);
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...');
@@ -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.lineBuffer += ch;
242
+ if (!this.ctx.isForegroundBusy())
243
+ this.lineBuffer += ch;
239
244
  this.ctx.writeToPty(ch);
240
245
  }
241
246
  }
@@ -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
  /**
@@ -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 models = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
90
- const defaultModel = provider.defaultModel ?? models[0];
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: models.length ? models : (defaultModel ? [defaultModel] : []),
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.ptyProcess.write("\n");
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.paused)
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
+ }