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.
Files changed (41) 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 +0 -1
  14. package/dist/context-manager.js +5 -110
  15. package/dist/core.js +14 -0
  16. package/dist/event-bus.d.ts +14 -0
  17. package/dist/extensions/overlay-agent.d.ts +4 -1
  18. package/dist/extensions/overlay-agent.js +115 -11
  19. package/dist/extensions/slash-commands.js +28 -0
  20. package/dist/extensions/terminal-buffer.js +9 -4
  21. package/dist/extensions/tui-renderer.js +119 -84
  22. package/dist/settings.d.ts +19 -2
  23. package/dist/settings.js +21 -3
  24. package/dist/shell.js +4 -0
  25. package/dist/token-budget.d.ts +13 -0
  26. package/dist/token-budget.js +50 -0
  27. package/dist/types.d.ts +0 -22
  28. package/dist/utils/ansi.d.ts +10 -0
  29. package/dist/utils/ansi.js +27 -0
  30. package/dist/utils/floating-panel.d.ts +32 -3
  31. package/dist/utils/floating-panel.js +296 -79
  32. package/dist/utils/line-editor.d.ts +9 -0
  33. package/dist/utils/line-editor.js +44 -0
  34. package/dist/utils/markdown.js +3 -3
  35. package/dist/utils/terminal-buffer.d.ts +4 -0
  36. package/dist/utils/terminal-buffer.js +13 -0
  37. package/dist/utils/tool-display.d.ts +1 -0
  38. package/dist/utils/tool-display.js +1 -1
  39. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  40. package/examples/extensions/pi-bridge/index.ts +87 -2
  41. package/package.json +1 -1
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.js CHANGED
@@ -163,6 +163,10 @@ export class Shell {
163
163
  this.bus.on("shell:pty-write", ({ data }) => {
164
164
  this.ptyProcess.write(data);
165
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
+ });
166
170
  // Ref-counted stdout hold — overlay extensions suppress PTY output
167
171
  this.bus.on("shell:stdout-hold", () => { this.stdoutHold.increment(); });
168
172
  this.bus.on("shell:stdout-release", () => { this.stdoutHold.decrement(); });
@@ -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
+ }
@@ -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
@@ -101,12 +101,6 @@ export interface TerminalSession {
101
101
  done: boolean;
102
102
  resolve?: (value: void) => void;
103
103
  }
104
- export interface ToolCallRecord {
105
- tool: string;
106
- args: Record<string, unknown>;
107
- output: string;
108
- exitCode: number | null;
109
- }
110
104
  export type Exchange = {
111
105
  type: "shell_command";
112
106
  id: number;
@@ -124,20 +118,4 @@ export type Exchange = {
124
118
  id: number;
125
119
  timestamp: number;
126
120
  query: string;
127
- } | {
128
- type: "agent_response";
129
- id: number;
130
- timestamp: number;
131
- response: string;
132
- toolCalls: ToolCallRecord[];
133
- } | {
134
- type: "tool_execution";
135
- id: number;
136
- timestamp: number;
137
- tool: string;
138
- args: Record<string, unknown>;
139
- output: string;
140
- exitCode: number | null;
141
- outputLines: number;
142
- outputBytes: number;
143
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
@@ -20,7 +20,7 @@ export interface FloatingPanelConfig {
20
20
  * Requires @xterm/headless — falls back to blank background if unavailable.
21
21
  */
22
22
  dimBackground?: boolean;
23
- /** Auto-dismiss delay in ms when done (0 = disabled). Default: 0. */
23
+ /** Auto-dismiss delay in ms when done (0 = auto-prompt for follow-up). Default: 0. */
24
24
  autoDismissMs?: number;
25
25
  /** Icon shown before the input cursor. Default: "\u276f". */
26
26
  promptIcon?: string;
@@ -144,6 +144,7 @@ export declare class FloatingPanel {
144
144
  * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
145
145
  * - `{prefix}:submit(query: string) -> void`
146
146
  * - `{prefix}:dismiss() -> void`
147
+ * - `{prefix}:show() -> void`
147
148
  * - `{prefix}:input(data: string) -> boolean`
148
149
  * - `{prefix}:build-row(content: string, width: number) -> string`
149
150
  */
@@ -153,29 +154,47 @@ export declare class FloatingPanel {
153
154
  /** All byte sequences that should be recognized as the trigger key. */
154
155
  private readonly triggerSeqs;
155
156
  private phase;
157
+ private _visible;
158
+ private _passthrough;
156
159
  private editor;
157
160
  private contentLines;
158
161
  private currentPartialLine;
159
162
  private scrollOffset;
163
+ private userScrolled;
160
164
  private title;
161
165
  private footer;
162
166
  private renderTimer;
163
- private autoDismissTimer;
164
167
  private resizeHandler;
165
168
  private prevFrame;
166
169
  private suppressNextRedraw;
170
+ private autoDismissTimer;
167
171
  private ptyBuffer;
168
172
  private usedAltScreen;
173
+ private wrapCache;
174
+ private wrapCacheWidth;
175
+ private passthroughTimer;
176
+ private prevSerialized;
169
177
  constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
170
178
  private registerDefaultHandlers;
171
179
  private wireEvents;
172
180
  /** Check whether data matches any encoding of the trigger key. */
173
181
  private isTrigger;
174
182
  private ensureBuffer;
183
+ /** Whether the panel has an active conversation (may be hidden). */
175
184
  get active(): boolean;
185
+ /** Whether the panel is currently visible on screen. */
186
+ get visible(): boolean;
176
187
  get terminalBuffer(): TerminalBuffer | null;
188
+ /** Open a fresh panel with a new conversation. */
177
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. */
178
195
  dismiss(): void;
196
+ /** Common screen enter logic shared by open() and show(). */
197
+ private enterScreen;
179
198
  appendText(text: string): void;
180
199
  appendLine(line: string): void;
181
200
  updateLastLine(fn: (line: string) => string): void;
@@ -184,15 +203,25 @@ export declare class FloatingPanel {
184
203
  setFooter(footer: string): void;
185
204
  setActive(): void;
186
205
  setDone(): void;
206
+ scrollUp(lines?: number): void;
207
+ scrollDown(lines?: number): void;
187
208
  getInput(): string;
188
209
  requestRender(): void;
189
210
  private handleIntercept;
211
+ /** Handle scroll input. Returns true if consumed. */
212
+ private handleScroll;
190
213
  private handleInputKey;
191
214
  /** Compute box geometry from config + current terminal size. */
192
215
  computeGeometry(): BoxGeometry;
193
216
  private buildFrame;
194
217
  private scheduleRender;
195
218
  private render;
196
- private restoreScreen;
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;
197
226
  private resolveSize;
198
227
  }