agent-sh 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/agent/agent-loop.d.ts +2 -2
- package/dist/agent/agent-loop.js +106 -13
- package/dist/agent/conversation-state.d.ts +39 -9
- package/dist/agent/conversation-state.js +336 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +175 -0
- package/dist/agent/system-prompt.d.ts +2 -2
- package/dist/agent/system-prompt.js +25 -4
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/context-manager.d.ts +3 -2
- package/dist/context-manager.js +16 -111
- package/dist/core.js +30 -1
- package/dist/event-bus.d.ts +37 -0
- package/dist/extensions/overlay-agent.d.ts +14 -0
- package/dist/extensions/overlay-agent.js +147 -0
- package/dist/extensions/slash-commands.js +28 -0
- package/dist/extensions/terminal-buffer.d.ts +14 -0
- package/dist/extensions/terminal-buffer.js +125 -0
- package/dist/extensions/tui-renderer.js +122 -84
- package/dist/index.js +4 -0
- package/dist/input-handler.js +6 -1
- package/dist/output-parser.js +8 -0
- package/dist/settings.d.ts +19 -2
- package/dist/settings.js +21 -3
- package/dist/shell.d.ts +5 -0
- package/dist/shell.js +31 -2
- package/dist/token-budget.d.ts +13 -0
- package/dist/token-budget.js +50 -0
- package/dist/types.d.ts +13 -22
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/floating-panel.d.ts +227 -0
- package/dist/utils/floating-panel.js +807 -0
- package/dist/utils/line-editor.d.ts +9 -0
- package/dist/utils/line-editor.js +44 -0
- package/dist/utils/markdown.js +3 -3
- package/dist/utils/output-writer.d.ts +14 -0
- package/dist/utils/output-writer.js +16 -0
- package/dist/utils/terminal-buffer.d.ts +69 -0
- package/dist/utils/terminal-buffer.js +179 -0
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/overlay-agent.ts +70 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/terminal-buffer.ts +184 -0
- package/package.json +5 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { MarkdownRenderer } from "../utils/markdown.js";
|
|
2
|
+
import { palette as p } from "../utils/palette.js";
|
|
3
|
+
import { renderToolCall, formatElapsed, } from "../utils/tool-display.js";
|
|
4
|
+
export default function activate(ctx) {
|
|
5
|
+
const { bus, advise, call, createFloatingPanel } = ctx;
|
|
6
|
+
const panel = createFloatingPanel({
|
|
7
|
+
trigger: "\x1c", // Ctrl+\
|
|
8
|
+
dimBackground: true,
|
|
9
|
+
});
|
|
10
|
+
// Suppress TUI renderer when overlay owns agent output
|
|
11
|
+
advise("tui:should-render-agent", (next) => {
|
|
12
|
+
return panel.active ? false : next();
|
|
13
|
+
});
|
|
14
|
+
// Signal interactive overlay mode in dynamic context
|
|
15
|
+
advise("dynamic-context:build", (next) => {
|
|
16
|
+
const base = next();
|
|
17
|
+
if (!panel.active)
|
|
18
|
+
return base;
|
|
19
|
+
return base + "\ninteractive-session: true\n";
|
|
20
|
+
});
|
|
21
|
+
// ── Conversation state (persists across hide/show) ─────────
|
|
22
|
+
const messages = [];
|
|
23
|
+
let renderer = null;
|
|
24
|
+
let currentAssistantMsg = null;
|
|
25
|
+
// ── Tool state ─────────────────────────────────────────────
|
|
26
|
+
let toolStartTime = 0;
|
|
27
|
+
function getContentWidth() {
|
|
28
|
+
return panel.computeGeometry().contentW;
|
|
29
|
+
}
|
|
30
|
+
/** Rebuild panel content from full message history. */
|
|
31
|
+
function rebuildContent() {
|
|
32
|
+
panel.clearContent();
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
for (const line of msg.lines) {
|
|
35
|
+
panel.appendLine(line);
|
|
36
|
+
}
|
|
37
|
+
panel.appendLine("");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Append a line to current assistant message and panel (if visible). */
|
|
41
|
+
function appendLine(line) {
|
|
42
|
+
currentAssistantMsg?.lines.push(line);
|
|
43
|
+
if (panel.visible)
|
|
44
|
+
panel.appendLine(line);
|
|
45
|
+
}
|
|
46
|
+
function drainRenderer() {
|
|
47
|
+
if (!renderer)
|
|
48
|
+
return;
|
|
49
|
+
for (const line of renderer.drainLines()) {
|
|
50
|
+
appendLine(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function flushRenderer() {
|
|
54
|
+
if (!renderer)
|
|
55
|
+
return;
|
|
56
|
+
renderer.flush();
|
|
57
|
+
drainRenderer();
|
|
58
|
+
}
|
|
59
|
+
function startAssistantMessage() {
|
|
60
|
+
flushRenderer();
|
|
61
|
+
currentAssistantMsg = { role: "assistant", lines: [] };
|
|
62
|
+
messages.push(currentAssistantMsg);
|
|
63
|
+
renderer = new MarkdownRenderer(getContentWidth());
|
|
64
|
+
}
|
|
65
|
+
function finalizeAssistantMessage() {
|
|
66
|
+
flushRenderer();
|
|
67
|
+
renderer = null;
|
|
68
|
+
currentAssistantMsg = null;
|
|
69
|
+
}
|
|
70
|
+
// ── Panel lifecycle ────────────────────────────────────────
|
|
71
|
+
panel.handlers.advise("panel:submit", (_next, query) => {
|
|
72
|
+
messages.push({
|
|
73
|
+
role: "user",
|
|
74
|
+
lines: [`${p.accent}${p.bold}❯${p.reset} ${query}`],
|
|
75
|
+
});
|
|
76
|
+
panel.setActive();
|
|
77
|
+
rebuildContent();
|
|
78
|
+
startAssistantMessage();
|
|
79
|
+
bus.emit("agent:submit", { query });
|
|
80
|
+
});
|
|
81
|
+
panel.handlers.advise("panel:show", (_next) => {
|
|
82
|
+
rebuildContent();
|
|
83
|
+
if (renderer)
|
|
84
|
+
drainRenderer();
|
|
85
|
+
});
|
|
86
|
+
// ── Stream agent response into panel ───────────────────────
|
|
87
|
+
bus.on("agent:response-chunk", (e) => {
|
|
88
|
+
if (!panel.active)
|
|
89
|
+
return;
|
|
90
|
+
if (!currentAssistantMsg)
|
|
91
|
+
startAssistantMessage();
|
|
92
|
+
for (const block of e.blocks) {
|
|
93
|
+
if (block.type === "text" && block.text) {
|
|
94
|
+
renderer.push(block.text);
|
|
95
|
+
drainRenderer();
|
|
96
|
+
}
|
|
97
|
+
else if (block.type === "code-block") {
|
|
98
|
+
flushRenderer();
|
|
99
|
+
// Reuse the shared code-block handler
|
|
100
|
+
call("render:code-block", block.language, block.code, getContentWidth());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Capture lines emitted by render:code-block into the overlay
|
|
105
|
+
advise("render:code-block", (next, language, code, width) => {
|
|
106
|
+
if (!panel.active)
|
|
107
|
+
return next(language, code, width);
|
|
108
|
+
// Render code block as indented dim lines for the overlay
|
|
109
|
+
const label = language ? `${p.dim}${language}${p.reset}` : "";
|
|
110
|
+
if (label)
|
|
111
|
+
appendLine(label);
|
|
112
|
+
for (const codeLine of code.split("\n")) {
|
|
113
|
+
appendLine(` ${p.dim}${codeLine}${p.reset}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
bus.on("agent:tool-started", (e) => {
|
|
117
|
+
if (!panel.active)
|
|
118
|
+
return;
|
|
119
|
+
if (!currentAssistantMsg)
|
|
120
|
+
startAssistantMessage();
|
|
121
|
+
flushRenderer();
|
|
122
|
+
toolStartTime = Date.now();
|
|
123
|
+
const lines = renderToolCall({
|
|
124
|
+
title: e.title,
|
|
125
|
+
kind: e.kind,
|
|
126
|
+
icon: e.icon,
|
|
127
|
+
locations: e.locations,
|
|
128
|
+
rawInput: e.rawInput,
|
|
129
|
+
displayDetail: e.displayDetail,
|
|
130
|
+
}, getContentWidth());
|
|
131
|
+
for (const line of lines)
|
|
132
|
+
appendLine(line);
|
|
133
|
+
});
|
|
134
|
+
bus.on("agent:tool-completed", (e) => {
|
|
135
|
+
if (!panel.active)
|
|
136
|
+
return;
|
|
137
|
+
const elapsed = toolStartTime ? formatElapsed(Date.now() - toolStartTime) : "";
|
|
138
|
+
const mark = call("tui:render-tool-complete", e.exitCode, elapsed, undefined);
|
|
139
|
+
appendLine(` ${mark}`);
|
|
140
|
+
});
|
|
141
|
+
bus.on("agent:processing-done", () => {
|
|
142
|
+
if (!panel.active)
|
|
143
|
+
return;
|
|
144
|
+
finalizeAssistantMessage();
|
|
145
|
+
panel.setDone();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -75,6 +75,34 @@ export default function activate({ bus, contextManager }) {
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
|
+
register({
|
|
79
|
+
name: "/compact",
|
|
80
|
+
description: "Compact conversation (move full content to nuclear summaries)",
|
|
81
|
+
handler: () => {
|
|
82
|
+
bus.emit("agent:compact-request", {});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
register({
|
|
86
|
+
name: "/context",
|
|
87
|
+
description: "Show context budget usage",
|
|
88
|
+
handler: () => {
|
|
89
|
+
const stats = bus.emitPipe("context:get-stats", {
|
|
90
|
+
activeTokens: 0,
|
|
91
|
+
nuclearEntries: 0,
|
|
92
|
+
recallArchiveSize: 0,
|
|
93
|
+
budgetTokens: 0,
|
|
94
|
+
});
|
|
95
|
+
const pct = stats.budgetTokens > 0
|
|
96
|
+
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
97
|
+
: 0;
|
|
98
|
+
const lines = [
|
|
99
|
+
`Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
100
|
+
`Nuclear entries: ${stats.nuclearEntries} in-context`,
|
|
101
|
+
`Recall archive: ${stats.recallArchiveSize} entries`,
|
|
102
|
+
];
|
|
103
|
+
bus.emit("ui:info", { message: lines.join("\n") });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
78
106
|
// ── Extension registration ────────────────────────────────────
|
|
79
107
|
bus.on("command:register", (cmd) => {
|
|
80
108
|
register(cmd);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in terminal buffer extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers two agent tools:
|
|
5
|
+
* - terminal_read: get the current screen contents + cursor position
|
|
6
|
+
* - terminal_keys: send raw keystrokes into the user's live PTY
|
|
7
|
+
*
|
|
8
|
+
* Together these let the agent operate inside interactive programs
|
|
9
|
+
* (vim, htop, less, etc.) by reading the screen and typing keys.
|
|
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({ bus, terminalBuffer: tb, registerTool }: ExtensionContext): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Interpret C-style escape sequences (e.g. \r → CR, \x1b → ESC). */
|
|
2
|
+
function interpretEscapes(str) {
|
|
3
|
+
return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq) => {
|
|
4
|
+
if (seq === "r")
|
|
5
|
+
return "\r";
|
|
6
|
+
if (seq === "n")
|
|
7
|
+
return "\n";
|
|
8
|
+
if (seq === "t")
|
|
9
|
+
return "\t";
|
|
10
|
+
if (seq === "\\")
|
|
11
|
+
return "\\";
|
|
12
|
+
if (seq === "0")
|
|
13
|
+
return "\0";
|
|
14
|
+
if (seq.startsWith("x"))
|
|
15
|
+
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
16
|
+
return seq;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function settle(ms = 100) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
23
|
+
if (!tb)
|
|
24
|
+
return; // @xterm/headless not installed
|
|
25
|
+
registerTool({
|
|
26
|
+
name: "terminal_read",
|
|
27
|
+
description: "Read what is currently visible on the user's terminal screen. Returns clean text (ANSI stripped) " +
|
|
28
|
+
"with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
|
|
29
|
+
"Use this to observe what the user sees — helpful for answering questions about terminal output, " +
|
|
30
|
+
"diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
|
|
31
|
+
input_schema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {},
|
|
34
|
+
},
|
|
35
|
+
showOutput: true,
|
|
36
|
+
getDisplayInfo: () => ({
|
|
37
|
+
kind: "read",
|
|
38
|
+
icon: "⊞",
|
|
39
|
+
locations: [],
|
|
40
|
+
}),
|
|
41
|
+
async execute() {
|
|
42
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
43
|
+
const info = [
|
|
44
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
45
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
46
|
+
].join(", ");
|
|
47
|
+
return {
|
|
48
|
+
content: `[${info}]\n\n${text}`,
|
|
49
|
+
exitCode: 0,
|
|
50
|
+
isError: false,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
registerTool({
|
|
55
|
+
name: "terminal_keys",
|
|
56
|
+
description: "Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
|
|
57
|
+
"Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
|
|
58
|
+
"or to type commands at the shell prompt. Do NOT use user_shell for this — user_shell runs a new " +
|
|
59
|
+
"command in a subshell, while terminal_keys types into whatever is currently on screen.\n\n" +
|
|
60
|
+
"Escape sequences for special keys:\n" +
|
|
61
|
+
" - Escape: \\x1b\n" +
|
|
62
|
+
" - Enter/Return: \\r\n" +
|
|
63
|
+
" - Tab: \\t\n" +
|
|
64
|
+
" - Ctrl+C: \\x03\n" +
|
|
65
|
+
" - Ctrl+D: \\x04\n" +
|
|
66
|
+
" - Ctrl+Z: \\x1a\n" +
|
|
67
|
+
" - Arrow keys: \\x1b[A (up), \\x1b[B (down), \\x1b[C (right), \\x1b[D (left)\n" +
|
|
68
|
+
" - Backspace: \\x7f\n\n" +
|
|
69
|
+
"Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\" (Escape, :q!, Enter).\n" +
|
|
70
|
+
"Always call terminal_read after sending keys to verify the result.",
|
|
71
|
+
input_schema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
keys: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "The keystrokes to send. Use \\x1b for Escape, \\r for Enter, \\t for Tab, " +
|
|
77
|
+
"\\x03 for Ctrl+C, etc. Regular characters are sent as-is.",
|
|
78
|
+
},
|
|
79
|
+
settle_ms: {
|
|
80
|
+
type: "number",
|
|
81
|
+
description: "Milliseconds to wait after sending keys for the terminal to settle before " +
|
|
82
|
+
"returning (default: 150). Increase for slow programs.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ["keys"],
|
|
86
|
+
},
|
|
87
|
+
showOutput: false,
|
|
88
|
+
getDisplayInfo: () => ({
|
|
89
|
+
kind: "execute",
|
|
90
|
+
icon: "⌨",
|
|
91
|
+
locations: [],
|
|
92
|
+
}),
|
|
93
|
+
formatCall: (args) => {
|
|
94
|
+
const keys = args.keys;
|
|
95
|
+
return keys
|
|
96
|
+
.replace(/\\x1b|\x1b/g, "ESC")
|
|
97
|
+
.replace(/\\r|\r/g, "⏎")
|
|
98
|
+
.replace(/\\n|\n/g, "↵")
|
|
99
|
+
.replace(/\\t|\t/g, "TAB")
|
|
100
|
+
.replace(/\\x03|\x03/g, "^C")
|
|
101
|
+
.replace(/\\x04|\x04/g, "^D")
|
|
102
|
+
.replace(/\\x7f|\x7f/g, "BS");
|
|
103
|
+
},
|
|
104
|
+
async execute(args) {
|
|
105
|
+
const raw = args.keys;
|
|
106
|
+
const keys = interpretEscapes(raw);
|
|
107
|
+
const settleMs = args.settle_ms ?? 150;
|
|
108
|
+
bus.emit("shell:stdout-show", {});
|
|
109
|
+
process.stdout.write("\n");
|
|
110
|
+
bus.emit("shell:pty-write", { data: keys });
|
|
111
|
+
await settle(settleMs);
|
|
112
|
+
bus.emit("shell:stdout-hide", {});
|
|
113
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
114
|
+
const info = [
|
|
115
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
116
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
117
|
+
].join(", ");
|
|
118
|
+
return {
|
|
119
|
+
content: `Keys sent. Screen after:\n[${info}]\n\n${text}`,
|
|
120
|
+
exitCode: 0,
|
|
121
|
+
isError: false,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|