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,175 @@
1
+ // ── Tool classification ───────────────────────────────────────────
2
+ /** Read-only tools whose results are dropped at Tier 1→2 (agent can re-read). */
3
+ export const READ_ONLY_TOOLS = new Set([
4
+ "read_file", "grep", "glob", "ls", "search",
5
+ ]);
6
+ /** State-changing tools whose summaries are kept in nuclear memory. */
7
+ export const WRITE_TOOLS = new Set([
8
+ "write_file", "edit_file", "write", "edit", "patch",
9
+ ]);
10
+ // ── Nuclear entry generation ──────────────────────────────────────
11
+ /**
12
+ * Generate nuclear entries from a logical turn (a sequence of messages
13
+ * starting with a user message, followed by assistant + tool messages).
14
+ */
15
+ export function toNuclearEntries(messages, startSeq, instanceId) {
16
+ const entries = [];
17
+ let seq = startSeq;
18
+ const ts = Date.now();
19
+ for (const msg of messages) {
20
+ if (msg.role === "user") {
21
+ const text = typeof msg.content === "string" ? msg.content : "";
22
+ // Skip compaction markers
23
+ if (text.startsWith("["))
24
+ continue;
25
+ entries.push({
26
+ seq: seq++, ts, iid: instanceId,
27
+ kind: "user",
28
+ sum: `user: "${truncate(text, 80)}"`,
29
+ });
30
+ }
31
+ else if (msg.role === "assistant") {
32
+ // Process tool calls
33
+ if ("tool_calls" in msg && msg.tool_calls) {
34
+ for (const tc of msg.tool_calls) {
35
+ if (!("function" in tc))
36
+ continue;
37
+ const name = tc.function.name;
38
+ let args = {};
39
+ try {
40
+ args = JSON.parse(tc.function.arguments);
41
+ }
42
+ catch { }
43
+ // Store the tool call — we'll enrich it when we see the result
44
+ entries.push({
45
+ seq: seq++, ts, iid: instanceId,
46
+ kind: "tool",
47
+ tool: name,
48
+ sum: summarizeToolCall(name, args),
49
+ });
50
+ }
51
+ }
52
+ else if (typeof msg.content === "string" && msg.content) {
53
+ entries.push({
54
+ seq: seq++, ts, iid: instanceId,
55
+ kind: "agent",
56
+ sum: `agent: "${truncate(msg.content, 60)}"`,
57
+ });
58
+ }
59
+ }
60
+ else if (msg.role === "tool") {
61
+ // Enrich the most recent tool entry with result info
62
+ const content = typeof msg.content === "string" ? msg.content : "";
63
+ const lastTool = findLastTool(entries);
64
+ if (lastTool) {
65
+ const isError = content.startsWith("Error:");
66
+ if (isError) {
67
+ lastTool.kind = "error";
68
+ lastTool.sum = `error: ${lastTool.tool} ${truncate(content.slice(7).trim(), 80)}`;
69
+ }
70
+ else {
71
+ lastTool.sum = enrichWithResult(lastTool.tool ?? "", lastTool.sum, content);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return entries;
77
+ }
78
+ // ── Formatting ────────────────────────────────────────────────────
79
+ /** Format a nuclear entry as a display line (for in-context injection). */
80
+ export function formatNuclearLine(entry) {
81
+ const time = new Date(entry.ts).toLocaleTimeString("en-US", {
82
+ hour: "2-digit", minute: "2-digit", hour12: false,
83
+ });
84
+ return `#${entry.seq} ${time} ${entry.sum}`;
85
+ }
86
+ // ── Serialization (JSONL for history file) ────────────────────────
87
+ /** Serialize a nuclear entry to a JSONL line. */
88
+ export function serializeEntry(entry) {
89
+ return JSON.stringify(entry);
90
+ }
91
+ /** Deserialize a JSONL line to a nuclear entry. Returns null on parse failure. */
92
+ export function deserializeEntry(line) {
93
+ try {
94
+ const obj = JSON.parse(line);
95
+ if (typeof obj.seq === "number" && typeof obj.sum === "string") {
96
+ return obj;
97
+ }
98
+ return null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ // ── Classification helpers ────────────────────────────────────────
105
+ /** Check if a nuclear entry represents a read-only action (should be dropped). */
106
+ export function isReadOnly(entry) {
107
+ return entry.kind === "tool" && entry.tool != null && READ_ONLY_TOOLS.has(entry.tool);
108
+ }
109
+ // ── Internal helpers ──────────────────────────────────────────────
110
+ function truncate(text, maxLen) {
111
+ const oneLine = text.replace(/\n/g, " ").trim();
112
+ return oneLine.length > maxLen ? oneLine.slice(0, maxLen) + "..." : oneLine;
113
+ }
114
+ function findLastTool(entries) {
115
+ for (let i = entries.length - 1; i >= 0; i--) {
116
+ if (entries[i].kind === "tool")
117
+ return entries[i];
118
+ }
119
+ return undefined;
120
+ }
121
+ function summarizeToolCall(name, args) {
122
+ switch (name) {
123
+ case "bash":
124
+ return `bash: ${truncate(String(args.command ?? ""), 60)}`;
125
+ case "user_shell":
126
+ return `user_shell: ${truncate(String(args.command ?? ""), 60)}`;
127
+ case "edit_file":
128
+ return `edit_file ${args.path ?? ""}`;
129
+ case "write_file":
130
+ case "write":
131
+ return `write_file ${args.path ?? args.file_path ?? ""}`;
132
+ case "read_file":
133
+ return `read_file ${args.path ?? args.file_path ?? ""}`;
134
+ case "grep":
135
+ return `grep "${truncate(String(args.pattern ?? ""), 30)}"`;
136
+ case "glob":
137
+ return `glob ${args.pattern ?? ""}`;
138
+ case "ls":
139
+ return `ls ${args.path ?? "."}`;
140
+ case "display":
141
+ return `display: ${truncate(String(args.command ?? ""), 60)}`;
142
+ default:
143
+ return `${name}`;
144
+ }
145
+ }
146
+ function enrichWithResult(toolName, summary, result) {
147
+ const lines = result.split("\n");
148
+ const lineCount = lines.length;
149
+ switch (toolName) {
150
+ case "bash":
151
+ case "user_shell": {
152
+ // Extract exit code from result if present
153
+ const exitMatch = result.match(/exit code[:\s]*(\d+)/i) ?? result.match(/exit\s+(\d+)/);
154
+ const exitCode = exitMatch ? exitMatch[1] : "0";
155
+ return `${summary} (exit ${exitCode}, ${lineCount} lines)`;
156
+ }
157
+ case "edit_file":
158
+ case "edit": {
159
+ // Try to extract +/- counts from result
160
+ const addMatch = result.match(/\+(\d+)/);
161
+ const delMatch = result.match(/-(\d+)/);
162
+ if (addMatch || delMatch) {
163
+ return `${summary} (+${addMatch?.[1] ?? 0}/-${delMatch?.[1] ?? 0})`;
164
+ }
165
+ return `${summary} (edited)`;
166
+ }
167
+ case "write_file":
168
+ case "write": {
169
+ const created = result.toLowerCase().includes("created") ? "created" : "written";
170
+ return `${summary} (${created}, ${lineCount} lines)`;
171
+ }
172
+ default:
173
+ return `${summary} (${lineCount} lines)`;
174
+ }
175
+ }
@@ -4,11 +4,11 @@ import type { ContextManager } from "../context-manager.js";
4
4
  * Static system prompt — identical across all queries, cacheable.
5
5
  * Contains only identity and behavioral instructions.
6
6
  */
7
- export declare const STATIC_SYSTEM_PROMPT = "You are an AI coding assistant embedded in agent-sh, a terminal shell.\nYou have access to the user's shell environment and can read, write, and execute code.\nYou share the user's working directory, environment variables, and shell history.\n\n# Tool Decision Guide\n\nYou have three categories of tools \u2014 choose based on who needs the output and\nwhether the command has lasting effects:\n\n**Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):\nUse these to investigate, search, read, and modify files. Output is returned\nto you for reasoning \u2014 the user doesn't see it directly.\n\n**Display** (display):\nUse this to show output to the user in their terminal. The user sees the\noutput directly, but it is NOT returned to you. Use when:\n- The user asks to see something (cat a file, git log, git diff, man page)\n- The output is for the user to read, not for you to process\n\n**Live shell** (user_shell):\nUse this to run commands with lasting effects in the user's real shell. Use for:\n- Commands that affect shell state (cd, export, source)\n- Installing packages, starting servers, running builds\n- Any command where the user wants real side effects\n- Set return_output=true only if you need to inspect the result\n\nDefault to scratchpad tools for your own investigation. Use display when the\nuser is the intended audience. Use user_shell when the command has real effects.\n\n# Tool Usage Guidelines\n- Use read_file before editing a file you haven't seen\n- Prefer edit_file over write_file for modifying existing files\n- Use grep/glob to find files before reading them\n- Keep bash commands focused; avoid long-running blocking commands\n- Always check command exit codes for errors";
7
+ export declare const STATIC_SYSTEM_PROMPT = "You are an AI coding assistant embedded in agent-sh, a terminal shell.\nYou have access to the user's shell environment and can read, write, and execute code.\nYou share the user's working directory, environment variables, and shell history.\n\n# Tool Decision Guide\n\nYou have three categories of tools \u2014 choose based on who needs the output and\nwhether the command has lasting effects:\n\n**Scratchpad tools** (bash, read_file, grep, glob, ls, edit_file, write_file):\nUse these to investigate, search, read, and modify files. Output is returned\nto you for reasoning \u2014 the user doesn't see it directly.\n\n**Display** (display):\nUse this to show output to the user in their terminal. The user sees the\noutput directly, but it is NOT returned to you. Use when:\n- The user asks to see something (cat a file, git log, git diff, man page)\n- The output is for the user to read, not for you to process\n\n**Live shell** (user_shell):\nUse this to run complete, non-interactive commands in the user's real shell. Use for:\n- Commands that affect shell state (cd, export, source)\n- Installing packages, starting servers, running builds\n- Any command where the user wants real side effects\n- Set return_output=true only if you need to inspect the result\n\n**Terminal interaction** (terminal_read, terminal_keys):\nUse these to observe and interact with what is currently on the user's terminal screen.\n- terminal_read: see what the user sees (current screen contents, cursor position)\n- terminal_keys: send keystrokes as if the user typed them\nUse for: driving interactive programs (vim, htop, less, ssh, REPLs), answering questions\nabout what's on screen, or typing at the shell prompt when a program is already running.\nDo NOT use user_shell to interact with an already-running program \u2014 use these instead.\n\nDefault to scratchpad tools for your own investigation. Use display when the\nuser is the intended audience. Use user_shell when the command has real effects.\nUse terminal_read/terminal_keys when interacting with what's already on screen.\n\n# Interactive Overlay Sessions\n\nWhen the dynamic context includes `interactive-session: true`, the user has summoned you\nvia a hotkey overlay from inside their live terminal. They may be in the middle of using\na program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:\n- Start with terminal_read if you need to understand what's on screen.\n- Prefer terminal_keys to interact with whatever is currently running.\n- Use user_shell only for running new, standalone commands \u2014 not for interacting with\n what's already on screen.\n- Keep responses concise \u2014 the user is in the middle of a workflow.\n\n# Tool Usage Guidelines\n- Use read_file before editing a file you haven't seen\n- Prefer edit_file over write_file for modifying existing files\n- Use grep/glob to find files before reading them\n- Keep bash commands focused; avoid long-running blocking commands\n- Always check command exit codes for errors";
8
8
  /**
9
9
  * Build the dynamic context — injected as a user message before each query.
10
10
  * Contains everything that changes: tools, shell context, conventions, cwd.
11
11
  *
12
12
  * Runs through the "agent:dynamic-context" pipe so extensions can append.
13
13
  */
14
- export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager): string;
14
+ export declare function buildDynamicContext(tools: ToolDefinition[], contextManager: ContextManager, shellBudgetTokens?: number): string;
@@ -56,14 +56,34 @@ output directly, but it is NOT returned to you. Use when:
56
56
  - The output is for the user to read, not for you to process
57
57
 
58
58
  **Live shell** (user_shell):
59
- Use this to run commands with lasting effects in the user's real shell. Use for:
59
+ Use this to run complete, non-interactive commands in the user's real shell. Use for:
60
60
  - Commands that affect shell state (cd, export, source)
61
61
  - Installing packages, starting servers, running builds
62
62
  - Any command where the user wants real side effects
63
63
  - Set return_output=true only if you need to inspect the result
64
64
 
65
+ **Terminal interaction** (terminal_read, terminal_keys):
66
+ Use these to observe and interact with what is currently on the user's terminal screen.
67
+ - terminal_read: see what the user sees (current screen contents, cursor position)
68
+ - terminal_keys: send keystrokes as if the user typed them
69
+ Use for: driving interactive programs (vim, htop, less, ssh, REPLs), answering questions
70
+ about what's on screen, or typing at the shell prompt when a program is already running.
71
+ Do NOT use user_shell to interact with an already-running program — use these instead.
72
+
65
73
  Default to scratchpad tools for your own investigation. Use display when the
66
74
  user is the intended audience. Use user_shell when the command has real effects.
75
+ Use terminal_read/terminal_keys when interacting with what's already on screen.
76
+
77
+ # Interactive Overlay Sessions
78
+
79
+ When the dynamic context includes \`interactive-session: true\`, the user has summoned you
80
+ via a hotkey overlay from inside their live terminal. They may be in the middle of using
81
+ a program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:
82
+ - Start with terminal_read if you need to understand what's on screen.
83
+ - Prefer terminal_keys to interact with whatever is currently running.
84
+ - Use user_shell only for running new, standalone commands — not for interacting with
85
+ what's already on screen.
86
+ - Keep responses concise — the user is in the middle of a workflow.
67
87
 
68
88
  # Tool Usage Guidelines
69
89
  - Use read_file before editing a file you haven't seen
@@ -77,7 +97,7 @@ user is the intended audience. Use user_shell when the command has real effects.
77
97
  *
78
98
  * Runs through the "agent:dynamic-context" pipe so extensions can append.
79
99
  */
80
- export function buildDynamicContext(tools, contextManager) {
100
+ export function buildDynamicContext(tools, contextManager, shellBudgetTokens) {
81
101
  const sections = [];
82
102
  // Tools
83
103
  sections.push("# Available Tools\n" +
@@ -92,8 +112,9 @@ export function buildDynamicContext(tools, contextManager) {
92
112
  if (skills.length > 0) {
93
113
  sections.push(`You have access to ${skills.length} skill(s). Use the list_skills tool to see them, then read_file to load one.`);
94
114
  }
95
- // Shell context
96
- const shellContext = contextManager.getContext();
115
+ // Shell context — pass token budget converted to bytes (~4 chars/token)
116
+ const shellBudgetBytes = shellBudgetTokens != null ? shellBudgetTokens * 4 : undefined;
117
+ const shellContext = contextManager.getContext(shellBudgetBytes);
97
118
  if (shellContext) {
98
119
  sections.push(shellContext);
99
120
  }
@@ -8,7 +8,10 @@
8
8
  export function createUserShellTool(opts) {
9
9
  return {
10
10
  name: "user_shell",
11
- description: "Run a command with lasting effects in the user's live shell (cd, export, install packages, start servers, apply changes). Output is shown directly to the user but NOT returned to you by default — set return_output=true if you need to inspect the result.",
11
+ description: "Run a complete, non-interactive command in the user's live shell (cd, export, install packages, start servers, git commands). " +
12
+ "Use this for commands that have side effects or that the user wants to see. Output is shown directly to the user but NOT returned " +
13
+ "to you by default — set return_output=true if you need to inspect the result. " +
14
+ "Do NOT use this to interact with programs that are already running in the terminal — use terminal_keys/terminal_read instead.",
12
15
  input_schema: {
13
16
  type: "object",
14
17
  properties: {
@@ -1,13 +1,14 @@
1
1
  import type { EventBus } from "./event-bus.js";
2
+ import type { HandlerRegistry } from "./utils/handler-registry.js";
2
3
  export declare class ContextManager {
3
4
  private exchanges;
4
5
  private nextId;
5
6
  private currentCwd;
6
7
  private sessionStart;
7
- private pendingToolCalls;
8
8
  private firstPrompt;
9
9
  private agentShellActive;
10
- constructor(bus: EventBus);
10
+ private handlers;
11
+ constructor(bus: EventBus, handlers?: HandlerRegistry);
11
12
  getCwd(): string;
12
13
  /**
13
14
  * Build the <shell_context> block for the agent prompt.
@@ -1,19 +1,18 @@
1
1
  import { getSettings } from "./settings.js";
2
- // Non-configurable thresholds (agent response and tool output follow shell settings)
3
- const AGENT_RESPONSE_TRUNCATE_THRESHOLD = 20;
4
- const AGENT_RESPONSE_HEAD_LINES = 15;
5
- const TOOL_TRUNCATE_THRESHOLD = 20;
6
- const TOOL_HEAD_LINES = 5;
7
- const TOOL_TAIL_LINES = 5;
8
2
  export class ContextManager {
9
3
  exchanges = [];
10
4
  nextId = 1;
11
5
  currentCwd;
12
6
  sessionStart;
13
- pendingToolCalls = [];
14
7
  firstPrompt = true;
15
8
  agentShellActive = false; // true while user_shell command is executing
16
- constructor(bus) {
9
+ handlers = null;
10
+ constructor(bus, handlers) {
11
+ if (handlers) {
12
+ this.handlers = handlers;
13
+ // Extensions can advise this to inject extra context (e.g. terminal buffer)
14
+ handlers.define("context:build-extra", () => "");
15
+ }
17
16
  this.currentCwd = process.cwd();
18
17
  this.sessionStart = Date.now();
19
18
  // ── Subscribe to shell events ──
@@ -37,46 +36,11 @@ export class ContextManager {
37
36
  bus.on("shell:agent-exec-start", () => { this.agentShellActive = true; });
38
37
  bus.on("shell:agent-exec-done", () => { this.agentShellActive = false; });
39
38
  // ── Subscribe to agent events ──
39
+ // Only track queries (as markers). Agent responses and tool outputs
40
+ // live exclusively in ConversationState to avoid duplication.
40
41
  bus.on("agent:query", (e) => {
41
- this.pendingToolCalls = [];
42
42
  this.addExchange({ type: "agent_query", query: e.query });
43
43
  });
44
- bus.on("agent:response-done", (e) => {
45
- this.addExchange({
46
- type: "agent_response",
47
- response: e.response,
48
- toolCalls: this.pendingToolCalls,
49
- });
50
- this.pendingToolCalls = [];
51
- });
52
- bus.on("agent:tool-call", (e) => {
53
- // Accumulate tool calls for the agent_response summary
54
- this.pendingToolCalls.push({
55
- tool: e.tool,
56
- args: e.args,
57
- output: "",
58
- exitCode: null,
59
- });
60
- });
61
- bus.on("agent:tool-output", (e) => {
62
- // Update the last pending tool call with output
63
- const last = this.pendingToolCalls[this.pendingToolCalls.length - 1];
64
- if (last) {
65
- last.output = e.output;
66
- last.exitCode = e.exitCode;
67
- }
68
- // Also store as a separate exchange for chronological log
69
- const lines = e.output.split("\n");
70
- this.addExchange({
71
- type: "tool_execution",
72
- tool: e.tool,
73
- args: {},
74
- output: e.output,
75
- exitCode: e.exitCode,
76
- outputLines: lines.length,
77
- outputBytes: e.output.length,
78
- });
79
- });
80
44
  }
81
45
  // ── Public query API ──────────────────────────────────────────
82
46
  getCwd() {
@@ -225,7 +189,6 @@ export class ContextManager {
225
189
  */
226
190
  clear() {
227
191
  this.exchanges = [];
228
- this.pendingToolCalls = [];
229
192
  this.firstPrompt = true;
230
193
  // Don't reset nextId — IDs should be globally unique within a session
231
194
  }
@@ -243,12 +206,7 @@ export class ContextManager {
243
206
  const s = getSettings();
244
207
  ex.output = truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id);
245
208
  }
246
- else if (ex.type === "agent_response") {
247
- ex.response = truncateHead(ex.response, AGENT_RESPONSE_TRUNCATE_THRESHOLD, AGENT_RESPONSE_HEAD_LINES, ex.id);
248
- }
249
- else if (ex.type === "tool_execution") {
250
- ex.output = truncateOutput(ex.output, TOOL_TRUNCATE_THRESHOLD, TOOL_HEAD_LINES, TOOL_TAIL_LINES, ex.id);
251
- }
209
+ // agent_query has no output to truncate
252
210
  }
253
211
  // Pass 2: budget enforcement — strip output from oldest if over budget
254
212
  let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
@@ -258,12 +216,6 @@ export class ContextManager {
258
216
  if (ex.type === "shell_command") {
259
217
  ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
260
218
  }
261
- else if (ex.type === "tool_execution") {
262
- ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
263
- }
264
- else if (ex.type === "agent_response") {
265
- ex.response = `[response omitted, use shell_recall tool to expand id ${ex.id}]`;
266
- }
267
219
  totalSize -= before - this.exchangeSize(ex);
268
220
  }
269
221
  return result;
@@ -282,7 +234,8 @@ export class ContextManager {
282
234
  out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
283
235
  out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
284
236
  out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
285
- out += `- You can browse or search session history with shell_recall.\n`;
237
+ out += `- You can browse or search shell history with shell_recall.\n`;
238
+ out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
286
239
  out += `\n`;
287
240
  this.firstPrompt = false;
288
241
  }
@@ -291,6 +244,10 @@ export class ContextManager {
291
244
  for (const ex of exchanges) {
292
245
  out += "\n" + this.formatExchangeTruncated(ex);
293
246
  }
247
+ // Allow extensions to inject extra context (e.g. terminal buffer snapshot)
248
+ const extra = this.handlers?.call("context:build-extra");
249
+ if (extra)
250
+ out += "\n" + extra + "\n";
294
251
  out += "\n</shell_context>\n";
295
252
  return out;
296
253
  }
@@ -316,25 +273,6 @@ export class ContextManager {
316
273
  }
317
274
  case "agent_query":
318
275
  return `#${ex.id} [you] > ${ex.query}\n`;
319
- case "agent_response": {
320
- let s = `#${ex.id} [agent] `;
321
- if (ex.response)
322
- s += ex.response.split("\n")[0] + "\n";
323
- if (ex.response.includes("\n")) {
324
- const rest = ex.response.slice(ex.response.indexOf("\n") + 1);
325
- if (rest.trim())
326
- s += indent(rest, " ") + "\n";
327
- }
328
- return s;
329
- }
330
- case "tool_execution": {
331
- let s = `#${ex.id} [tool] ${ex.tool}\n`;
332
- if (ex.output)
333
- s += indent(ex.output, " ") + "\n";
334
- if (ex.exitCode !== null)
335
- s += ` exit ${ex.exitCode}\n`;
336
- return s;
337
- }
338
276
  }
339
277
  }
340
278
  formatExchangeFull(ex) {
@@ -351,16 +289,6 @@ export class ContextManager {
351
289
  }
352
290
  case "agent_query":
353
291
  return `#${ex.id} [you] > ${ex.query}`;
354
- case "agent_response":
355
- return `#${ex.id} [agent]\n${ex.response}`;
356
- case "tool_execution": {
357
- let s = `#${ex.id} [tool] ${ex.tool} (${ex.outputLines} lines, ${ex.outputBytes} bytes)\n`;
358
- if (ex.output)
359
- s += ex.output + "\n";
360
- if (ex.exitCode !== null)
361
- s += `exit ${ex.exitCode}\n`;
362
- return s;
363
- }
364
292
  }
365
293
  }
366
294
  exchangeOneLiner(ex) {
@@ -371,12 +299,6 @@ export class ContextManager {
371
299
  }
372
300
  case "agent_query":
373
301
  return `#${ex.id} query: ${ex.query}`;
374
- case "agent_response": {
375
- const preview = ex.response.split("\n")[0]?.slice(0, 80) ?? "";
376
- return `#${ex.id} agent: ${preview}${ex.response.length > 80 ? "..." : ""}`;
377
- }
378
- case "tool_execution":
379
- return `#${ex.id} tool: ${ex.tool} (${ex.outputLines} lines, exit ${ex.exitCode ?? "?"})`;
380
302
  }
381
303
  }
382
304
  exchangeSearchText(ex) {
@@ -385,10 +307,6 @@ export class ContextManager {
385
307
  return `${ex.command}\n${ex.output}`;
386
308
  case "agent_query":
387
309
  return ex.query;
388
- case "agent_response":
389
- return ex.response;
390
- case "tool_execution":
391
- return `${ex.tool}\n${ex.output}`;
392
310
  }
393
311
  }
394
312
  exchangeSize(ex) {
@@ -397,10 +315,6 @@ export class ContextManager {
397
315
  return ex.command.length + ex.output.length;
398
316
  case "agent_query":
399
317
  return ex.query.length;
400
- case "agent_response":
401
- return ex.response.length;
402
- case "tool_execution":
403
- return ex.tool.length + ex.output.length;
404
318
  }
405
319
  }
406
320
  }
@@ -416,15 +330,6 @@ function truncateOutput(text, threshold, headLines, tailLines, id) {
416
330
  ...lines.slice(-tailLines),
417
331
  ].join("\n");
418
332
  }
419
- function truncateHead(text, threshold, headLines, id) {
420
- const lines = text.split("\n");
421
- if (lines.length <= threshold)
422
- return text;
423
- return [
424
- ...lines.slice(0, headLines),
425
- `[... truncated, use shell_recall tool with expand and id ${id} for full response ...]`,
426
- ].join("\n");
427
- }
428
333
  function indent(text, prefix) {
429
334
  return text
430
335
  .split("\n")
package/dist/core.js CHANGED
@@ -25,6 +25,8 @@ import * as streamTransform from "./utils/stream-transform.js";
25
25
  import * as settingsMod from "./settings.js";
26
26
  import { resolveProvider, getProviderNames } from "./settings.js";
27
27
  import { HandlerRegistry } from "./utils/handler-registry.js";
28
+ import { TerminalBuffer } from "./utils/terminal-buffer.js";
29
+ import { FloatingPanel } from "./utils/floating-panel.js";
28
30
  // Re-export types that library consumers need
29
31
  export { EventBus } from "./event-bus.js";
30
32
  export { palette, setPalette, resetPalette } from "./utils/palette.js";
@@ -33,7 +35,7 @@ export { LlmClient } from "./utils/llm-client.js";
33
35
  export function createCore(config) {
34
36
  const bus = new EventBus();
35
37
  const handlers = new HandlerRegistry();
36
- const contextManager = new ContextManager(bus);
38
+ const contextManager = new ContextManager(bus, handlers);
37
39
  // ── Resolve provider ─────────────────────────────────────────
38
40
  const settings = settingsMod.getSettings();
39
41
  let activeProvider = null;
@@ -173,6 +175,20 @@ export function createCore(config) {
173
175
  supportsReasoningEffort: p.supportsReasoningEffort,
174
176
  modelCapabilities: caps.size > 0 ? caps : undefined,
175
177
  });
178
+ // Push registered models into the agent loop so they appear in
179
+ // autocomplete and are selectable via /model.
180
+ const addModes = modelIds.map((m) => {
181
+ const mc = caps.get(m);
182
+ return {
183
+ model: m,
184
+ provider: p.id,
185
+ providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
186
+ contextWindow: mc?.contextWindow,
187
+ reasoning: mc?.reasoning,
188
+ supportsReasoningEffort: p.supportsReasoningEffort,
189
+ };
190
+ });
191
+ bus.emit("config:add-modes", { modes: addModes });
176
192
  });
177
193
  bus.on("config:switch-provider", ({ provider: name }) => {
178
194
  const p = providerRegistry.get(name);
@@ -216,6 +232,14 @@ export function createCore(config) {
216
232
  bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
217
233
  bus.emit("config:changed", {});
218
234
  });
235
+ // ── Lazy singleton terminal buffer ──────────────────────────
236
+ let terminalBufferSingleton; // undefined = not yet created
237
+ const getTerminalBuffer = () => {
238
+ if (terminalBufferSingleton !== undefined)
239
+ return terminalBufferSingleton;
240
+ terminalBufferSingleton = TerminalBuffer.createWired(bus);
241
+ return terminalBufferSingleton;
242
+ };
219
243
  return {
220
244
  bus,
221
245
  contextManager,
@@ -292,6 +316,11 @@ export function createCore(config) {
292
316
  define: (name, fn) => handlers.define(name, fn),
293
317
  advise: (name, wrapper) => handlers.advise(name, wrapper),
294
318
  call: (name, ...args) => handlers.call(name, ...args),
319
+ get terminalBuffer() { return getTerminalBuffer(); },
320
+ createFloatingPanel: (config) => {
321
+ const tb = config.dimBackground !== false ? getTerminalBuffer() : null;
322
+ return new FloatingPanel(bus, { ...config, terminalBuffer: tb ?? undefined });
323
+ },
295
324
  };
296
325
  },
297
326
  kill() {
@@ -22,6 +22,25 @@ export interface ShellEvents {
22
22
  };
23
23
  "shell:agent-exec-start": Record<string, never>;
24
24
  "shell:agent-exec-done": Record<string, never>;
25
+ "shell:pty-data": {
26
+ raw: string;
27
+ };
28
+ "shell:pty-write": {
29
+ data: string;
30
+ };
31
+ "shell:pty-resize": {
32
+ cols: number;
33
+ rows: number;
34
+ };
35
+ "shell:buffer-request": Record<string, never>;
36
+ "shell:buffer-snapshot": {
37
+ text: string;
38
+ altScreen: boolean;
39
+ cursor: {
40
+ x: number;
41
+ y: number;
42
+ };
43
+ };
25
44
  "agent:submit": {
26
45
  query: string;
27
46
  };
@@ -123,6 +142,14 @@ export interface ShellEvents {
123
142
  "input:keypress": {
124
143
  key: string;
125
144
  };
145
+ "input:intercept": {
146
+ data: string;
147
+ consumed: boolean;
148
+ };
149
+ "shell:stdout-hold": Record<string, never>;
150
+ "shell:stdout-release": Record<string, never>;
151
+ "shell:stdout-show": Record<string, never>;
152
+ "shell:stdout-hide": Record<string, never>;
126
153
  "agent:terminal-intercept": {
127
154
  command: string;
128
155
  cwd: string;
@@ -148,6 +175,13 @@ export interface ShellEvents {
148
175
  contextWindow?: number;
149
176
  };
150
177
  "agent:reset-session": Record<string, never>;
178
+ "agent:compact-request": Record<string, never>;
179
+ "context:get-stats": {
180
+ activeTokens: number;
181
+ nuclearEntries: number;
182
+ recallArchiveSize: number;
183
+ budgetTokens: number;
184
+ };
151
185
  "agent:register-backend": {
152
186
  name: string;
153
187
  kill: () => void;
@@ -187,6 +221,9 @@ export interface ShellEvents {
187
221
  "config:set-modes": {
188
222
  modes: AgentMode[];
189
223
  };
224
+ "config:add-modes": {
225
+ modes: AgentMode[];
226
+ };
190
227
  "provider:register": {
191
228
  id: string;
192
229
  apiKey?: string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Built-in overlay agent.
3
+ *
4
+ * Provides a hotkey (Ctrl+\) to summon the agent from anywhere — even
5
+ * inside vim, htop, or ssh. Composites a floating response box on top
6
+ * of the current terminal content.
7
+ *
8
+ * Rendering reuses the shared tui:render-* handlers so that extensions
9
+ * advising those handlers affect both the main TUI and the overlay.
10
+ *
11
+ * Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
12
+ */
13
+ import type { ExtensionContext } from "../types.js";
14
+ export default function activate(ctx: ExtensionContext): void;