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
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Unified token budget manager.
3
+ *
4
+ * Splits a model's context window between two streams:
5
+ * - Shell context (user shell commands and outputs — situational awareness)
6
+ * - Conversation (agent messages and tool results — task continuity)
7
+ *
8
+ * The budget accounts for fixed overhead (system prompt, tool definitions,
9
+ * response reserve) and divides the remaining space by a configurable ratio.
10
+ */
11
+ import { getSettings } from "./settings.js";
12
+ /** Overhead estimates (tokens). */
13
+ const SYSTEM_PROMPT_OVERHEAD = 800;
14
+ const DYNAMIC_CONTEXT_OVERHEAD = 500; // conventions, metadata, skills list
15
+ const TOKENS_PER_TOOL_DEFINITION = 50;
16
+ const RESPONSE_RESERVE = 8192; // matches llm-client.ts default max_tokens
17
+ /** Fallback when contextWindow is unknown. */
18
+ const DEFAULT_CONTEXT_WINDOW = 60_000;
19
+ export class TokenBudget {
20
+ contextWindow;
21
+ toolCount;
22
+ constructor(contextWindow, toolCount = 0) {
23
+ this.contextWindow = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
24
+ this.toolCount = toolCount;
25
+ }
26
+ /** Update when model or tool set changes. */
27
+ update(contextWindow, toolCount) {
28
+ if (contextWindow != null)
29
+ this.contextWindow = contextWindow;
30
+ if (toolCount != null)
31
+ this.toolCount = toolCount;
32
+ }
33
+ /** Total tokens available for shell context + conversation content. */
34
+ get contentBudget() {
35
+ const overhead = SYSTEM_PROMPT_OVERHEAD +
36
+ DYNAMIC_CONTEXT_OVERHEAD +
37
+ this.toolCount * TOKENS_PER_TOOL_DEFINITION +
38
+ RESPONSE_RESERVE;
39
+ return Math.max(0, this.contextWindow - overhead);
40
+ }
41
+ /** Token budget for the shell context stream. */
42
+ get shellBudgetTokens() {
43
+ const ratio = getSettings().shellContextRatio;
44
+ return Math.floor(this.contentBudget * ratio);
45
+ }
46
+ /** Token budget for the conversation messages stream. */
47
+ get conversationBudgetTokens() {
48
+ return this.contentBudget - this.shellBudgetTokens;
49
+ }
50
+ }
package/dist/types.d.ts CHANGED
@@ -4,6 +4,8 @@ import type { LlmClient } from "./utils/llm-client.js";
4
4
  import type { ColorPalette } from "./utils/palette.js";
5
5
  import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
6
6
  import type { ToolDefinition } from "./agent/types.js";
7
+ import type { TerminalBuffer } from "./utils/terminal-buffer.js";
8
+ import type { FloatingPanel, FloatingPanelConfig } from "./utils/floating-panel.js";
7
9
  export type { ContentBlock } from "./event-bus.js";
8
10
  export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
9
11
  /** A model entry in the cycling list, optionally tied to a provider. */
@@ -66,6 +68,17 @@ export interface ExtensionContext {
66
68
  advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
67
69
  /** Call a named handler. */
68
70
  call: (name: string, ...args: any[]) => any;
71
+ /**
72
+ * Shared headless terminal buffer mirroring PTY output.
73
+ * Lazily created on first access. Returns null if @xterm/headless is not installed.
74
+ */
75
+ terminalBuffer: TerminalBuffer | null;
76
+ /**
77
+ * Create a floating panel overlay. The panel composites a bordered box
78
+ * over the terminal with input routing, dimmed background, and
79
+ * handler-based customization.
80
+ */
81
+ createFloatingPanel: (config: FloatingPanelConfig) => FloatingPanel;
69
82
  }
70
83
  /**
71
84
  * Configuration for a registered input mode.
@@ -88,12 +101,6 @@ export interface TerminalSession {
88
101
  done: boolean;
89
102
  resolve?: (value: void) => void;
90
103
  }
91
- export interface ToolCallRecord {
92
- tool: string;
93
- args: Record<string, unknown>;
94
- output: string;
95
- exitCode: number | null;
96
- }
97
104
  export type Exchange = {
98
105
  type: "shell_command";
99
106
  id: number;
@@ -111,20 +118,4 @@ export type Exchange = {
111
118
  id: number;
112
119
  timestamp: number;
113
120
  query: string;
114
- } | {
115
- type: "agent_response";
116
- id: number;
117
- timestamp: number;
118
- response: string;
119
- toolCalls: ToolCallRecord[];
120
- } | {
121
- type: "tool_execution";
122
- id: number;
123
- timestamp: number;
124
- tool: string;
125
- args: Record<string, unknown>;
126
- output: string;
127
- exitCode: number | null;
128
- outputLines: number;
129
- outputBytes: number;
130
121
  };
@@ -11,5 +11,15 @@ export declare const RESET = "\u001B[0m";
11
11
  * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
12
12
  */
13
13
  export declare function visibleLen(str: string): number;
14
+ /**
15
+ * Truncate a string to fit within `maxWidth` visible columns.
16
+ * Accounts for CJK double-width characters. Appends `…` if truncated.
17
+ */
18
+ export declare function truncateToWidth(str: string, maxWidth: number): string;
19
+ /**
20
+ * Pad a string with spaces to fill `targetWidth` visible columns.
21
+ * Accounts for CJK double-width characters.
22
+ */
23
+ export declare function padEndToWidth(str: string, targetWidth: number): string;
14
24
  /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
15
25
  export declare function stripAnsi(str: string): string;
@@ -70,6 +70,33 @@ export function visibleLen(str) {
70
70
  }
71
71
  return width;
72
72
  }
73
+ /**
74
+ * Truncate a string to fit within `maxWidth` visible columns.
75
+ * Accounts for CJK double-width characters. Appends `…` if truncated.
76
+ */
77
+ export function truncateToWidth(str, maxWidth) {
78
+ const clean = str.replace(/\x1b\[[^m]*m/g, "");
79
+ let width = 0;
80
+ let i = 0;
81
+ for (const char of clean) {
82
+ const cw = charWidth(char.codePointAt(0) ?? 0);
83
+ if (width + cw > maxWidth - 1) {
84
+ // Need room for the "…" (1 column wide)
85
+ return clean.slice(0, i) + "…";
86
+ }
87
+ width += cw;
88
+ i += char.length;
89
+ }
90
+ return clean;
91
+ }
92
+ /**
93
+ * Pad a string with spaces to fill `targetWidth` visible columns.
94
+ * Accounts for CJK double-width characters.
95
+ */
96
+ export function padEndToWidth(str, targetWidth) {
97
+ const gap = targetWidth - visibleLen(str);
98
+ return gap > 0 ? str + " ".repeat(gap) : str;
99
+ }
73
100
  /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
74
101
  export function stripAnsi(str) {
75
102
  return str
@@ -0,0 +1,227 @@
1
+ import { TerminalBuffer } from "./terminal-buffer.js";
2
+ import { HandlerRegistry } from "./handler-registry.js";
3
+ import type { EventBus } from "../event-bus.js";
4
+ import type { BorderStyle } from "./box-frame.js";
5
+ export interface FloatingPanelConfig {
6
+ /** Key sequence that toggles the panel (e.g. "\x1c" for Ctrl+\). */
7
+ trigger: string;
8
+ /** Panel width. Number = columns, string with % = percentage. Default: "80%". */
9
+ width?: number | string;
10
+ /** Max width in columns. Default: 100. */
11
+ maxWidth?: number;
12
+ /** Panel height. Number = rows, string with % = percentage. Default: "60%". */
13
+ height?: number | string;
14
+ /** Min content rows inside the panel. Default: 6. */
15
+ minHeight?: number;
16
+ /** Border style. Default: "rounded". */
17
+ borderStyle?: BorderStyle;
18
+ /**
19
+ * Show dimmed terminal content behind the panel. Default: true.
20
+ * Requires @xterm/headless — falls back to blank background if unavailable.
21
+ */
22
+ dimBackground?: boolean;
23
+ /** Auto-dismiss delay in ms when done (0 = auto-prompt for follow-up). Default: 0. */
24
+ autoDismissMs?: number;
25
+ /** Icon shown before the input cursor. Default: "\u276f". */
26
+ promptIcon?: string;
27
+ /**
28
+ * Pre-existing TerminalBuffer to reuse. If provided, the panel will
29
+ * not create its own headless terminal. Useful when sharing a buffer
30
+ * with other features (e.g. context injection, terminal_read tool).
31
+ */
32
+ terminalBuffer?: TerminalBuffer;
33
+ /**
34
+ * Handler namespace prefix. Default: "panel".
35
+ * All handlers are registered as `{prefix}:render-content`,
36
+ * `{prefix}:submit`, etc. Use different prefixes for multiple panels.
37
+ */
38
+ handlerPrefix?: string;
39
+ }
40
+ /**
41
+ * Context passed to the render-content handler.
42
+ */
43
+ export interface RenderContext {
44
+ /** Available width for content (inside box, excluding borders and padding). */
45
+ width: number;
46
+ /** Available height for content (rows inside box). */
47
+ height: number;
48
+ /** Current panel phase. */
49
+ phase: Phase;
50
+ /** Current input buffer text (during input phase). */
51
+ inputBuffer: string;
52
+ /** Current input cursor position (during input phase). */
53
+ inputCursor: number;
54
+ /** Current scroll offset. */
55
+ scrollOffset: number;
56
+ /** Built-in content lines (from appendText/appendLine). */
57
+ contentLines: readonly string[];
58
+ /** Current partial line being streamed. */
59
+ partialLine: string;
60
+ }
61
+ /**
62
+ * Result from render-content handler.
63
+ */
64
+ export interface RenderResult {
65
+ lines: string[];
66
+ /** Optional cursor position within the content area. */
67
+ cursor?: {
68
+ row: number;
69
+ col: number;
70
+ };
71
+ }
72
+ /**
73
+ * Box geometry computed from config + terminal size.
74
+ */
75
+ export interface BoxGeometry {
76
+ /** Terminal columns. */
77
+ cols: number;
78
+ /** Terminal rows. */
79
+ rows: number;
80
+ /** Box width in columns (including borders). */
81
+ boxW: number;
82
+ /** Box height in rows (including borders). */
83
+ boxH: number;
84
+ /** Box top offset (0-indexed row). */
85
+ boxTop: number;
86
+ /** Box left offset (0-indexed column). */
87
+ boxLeft: number;
88
+ /** Usable content width inside box. */
89
+ contentW: number;
90
+ /** Usable content height inside box. */
91
+ contentH: number;
92
+ }
93
+ /**
94
+ * Context passed to the render-frame handler.
95
+ */
96
+ export interface FrameContext {
97
+ /** Box geometry. */
98
+ geo: BoxGeometry;
99
+ /** Content render result (from render-content handler). */
100
+ content: RenderResult;
101
+ /** Background lines from the terminal buffer (null if no dimming). */
102
+ bgLines: string[] | null;
103
+ /** Current panel phase. */
104
+ phase: Phase;
105
+ /** Current title text. */
106
+ title: string;
107
+ /** Current footer text. */
108
+ footer: string;
109
+ /** Border characters for the configured border style. */
110
+ border: {
111
+ tl: string;
112
+ tr: string;
113
+ bl: string;
114
+ br: string;
115
+ h: string;
116
+ v: string;
117
+ };
118
+ }
119
+ /**
120
+ * Result from render-frame handler.
121
+ */
122
+ export interface FrameResult {
123
+ /** One string per terminal row. */
124
+ rows: string[];
125
+ /** ANSI sequence to position the cursor (empty string if no cursor). */
126
+ cursorSeq: string;
127
+ }
128
+ export type Phase = "idle" | "input" | "active" | "done";
129
+ export declare class FloatingPanel {
130
+ private readonly config;
131
+ private readonly bus;
132
+ private readonly border;
133
+ private readonly externalBuffer;
134
+ private readonly prefix;
135
+ /**
136
+ * Handler registry for this panel. Extensions use `handlers.advise()`
137
+ * to customize rendering and behavior.
138
+ *
139
+ * Registered handlers:
140
+ * - `{prefix}:render-content(ctx: RenderContext) -> RenderResult`
141
+ * - `{prefix}:render-frame(ctx: FrameContext) -> FrameResult`
142
+ * - `{prefix}:render-border-top(ctx: FrameContext) -> string`
143
+ * - `{prefix}:render-border-bottom(ctx: FrameContext) -> string`
144
+ * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
145
+ * - `{prefix}:submit(query: string) -> void`
146
+ * - `{prefix}:dismiss() -> void`
147
+ * - `{prefix}:show() -> void`
148
+ * - `{prefix}:input(data: string) -> boolean`
149
+ * - `{prefix}:build-row(content: string, width: number) -> string`
150
+ */
151
+ readonly handlers: HandlerRegistry;
152
+ private buffer;
153
+ private bufferInitialized;
154
+ /** All byte sequences that should be recognized as the trigger key. */
155
+ private readonly triggerSeqs;
156
+ private phase;
157
+ private _visible;
158
+ private _passthrough;
159
+ private editor;
160
+ private contentLines;
161
+ private currentPartialLine;
162
+ private scrollOffset;
163
+ private userScrolled;
164
+ private title;
165
+ private footer;
166
+ private renderTimer;
167
+ private resizeHandler;
168
+ private prevFrame;
169
+ private suppressNextRedraw;
170
+ private autoDismissTimer;
171
+ private ptyBuffer;
172
+ private usedAltScreen;
173
+ private wrapCache;
174
+ private wrapCacheWidth;
175
+ private passthroughTimer;
176
+ private prevSerialized;
177
+ constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
178
+ private registerDefaultHandlers;
179
+ private wireEvents;
180
+ /** Check whether data matches any encoding of the trigger key. */
181
+ private isTrigger;
182
+ private ensureBuffer;
183
+ /** Whether the panel has an active conversation (may be hidden). */
184
+ get active(): boolean;
185
+ /** Whether the panel is currently visible on screen. */
186
+ get visible(): boolean;
187
+ get terminalBuffer(): TerminalBuffer | null;
188
+ /** Open a fresh panel with a new conversation. */
189
+ open(): void;
190
+ /** Hide the panel without destroying conversation state. */
191
+ hide(): void;
192
+ /** Show the panel again after hide(), preserving conversation. */
193
+ show(): void;
194
+ /** Fully destroy the panel, resetting all state. */
195
+ dismiss(): void;
196
+ /** Common screen enter logic shared by open() and show(). */
197
+ private enterScreen;
198
+ appendText(text: string): void;
199
+ appendLine(line: string): void;
200
+ updateLastLine(fn: (line: string) => string): void;
201
+ clearContent(): void;
202
+ setTitle(title: string): void;
203
+ setFooter(footer: string): void;
204
+ setActive(): void;
205
+ setDone(): void;
206
+ scrollUp(lines?: number): void;
207
+ scrollDown(lines?: number): void;
208
+ getInput(): string;
209
+ requestRender(): void;
210
+ private handleIntercept;
211
+ /** Handle scroll input. Returns true if consumed. */
212
+ private handleScroll;
213
+ private handleInputKey;
214
+ /** Compute box geometry from config + current terminal size. */
215
+ computeGeometry(): BoxGeometry;
216
+ private buildFrame;
217
+ private scheduleRender;
218
+ private render;
219
+ /** Full screen teardown: exit alt screen, release stdout, force redraw. */
220
+ private teardownScreen;
221
+ /** Start rendering TerminalBuffer directly (no overlay box). */
222
+ private startPassthrough;
223
+ private stopPassthrough;
224
+ /** Render the TerminalBuffer's screen content directly (no overlay). */
225
+ private renderPassthrough;
226
+ private resolveSize;
227
+ }