agent-sh 0.12.13 → 0.12.15
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/dist/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +23 -38
- package/dist/agent/subagent.js +5 -8
- package/dist/agent/system-prompt.d.ts +0 -27
- package/dist/agent/system-prompt.js +5 -24
- package/dist/agent/tools/pwsh.js +36 -5
- package/dist/core.d.ts +3 -4
- package/dist/core.js +30 -27
- package/dist/executor.js +14 -9
- package/dist/extension-loader.js +7 -0
- package/dist/extensions/agent-backend.js +0 -1
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/file-autocomplete.js +3 -3
- package/dist/extensions/index.d.ts +4 -0
- package/dist/extensions/index.js +1 -0
- package/dist/extensions/shell-context.d.ts +7 -0
- package/dist/extensions/shell-context.js +129 -0
- package/dist/extensions/slash-commands.js +2 -2
- package/dist/index.js +15 -16
- package/dist/shell/index.d.ts +35 -0
- package/dist/shell/index.js +47 -0
- package/dist/types.d.ts +26 -32
- package/dist/utils/message-utils.d.ts +13 -0
- package/dist/utils/message-utils.js +26 -0
- package/examples/extensions/overlay-agent.ts +9 -3
- package/examples/extensions/peer-mesh.ts +10 -7
- package/examples/extensions/subagents.ts +2 -2
- package/examples/extensions/terminal-buffer.ts +3 -2
- package/examples/extensions/tmux-pane.ts +5 -1
- package/examples/extensions/user-shell.ts +1 -1
- package/package.json +1 -1
- package/dist/context-manager.d.ts +0 -45
- package/dist/context-manager.js +0 -242
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getSettings } from "../settings.js";
|
|
2
|
+
import { spillOutput } from "../utils/shell-output-spill.js";
|
|
3
|
+
export default function activate(ctx) {
|
|
4
|
+
const { bus } = ctx;
|
|
5
|
+
const exchanges = [];
|
|
6
|
+
let nextId = 1;
|
|
7
|
+
let currentCwd = process.cwd();
|
|
8
|
+
let agentShellActive = false;
|
|
9
|
+
let lastSeq = 0;
|
|
10
|
+
bus.on("shell:command-done", (e) => {
|
|
11
|
+
const lines = e.output.split("\n");
|
|
12
|
+
const s = getSettings();
|
|
13
|
+
// Long outputs spill to a tempfile so the agent can `read_file` them
|
|
14
|
+
// on demand instead of carrying the full text in LLM context.
|
|
15
|
+
let output = e.output;
|
|
16
|
+
let spillPath;
|
|
17
|
+
if (lines.length > s.shellTruncateThreshold) {
|
|
18
|
+
const id = nextId;
|
|
19
|
+
try {
|
|
20
|
+
spillPath = spillOutput(id, e.output);
|
|
21
|
+
output = buildSpillStub(lines, s.shellHeadLines, s.shellTailLines, spillPath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
output = e.output;
|
|
25
|
+
spillPath = undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exchanges.push({
|
|
29
|
+
id: nextId++,
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
cwd: e.cwd,
|
|
32
|
+
command: e.command,
|
|
33
|
+
output,
|
|
34
|
+
exitCode: e.exitCode,
|
|
35
|
+
outputLines: lines.length,
|
|
36
|
+
outputBytes: e.output.length,
|
|
37
|
+
source: agentShellActive ? "agent" : "user",
|
|
38
|
+
spillPath,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
bus.on("shell:cwd-change", (e) => { currentCwd = e.cwd; });
|
|
42
|
+
bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
|
|
43
|
+
bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
|
|
44
|
+
// Override core's process.cwd() default with the PTY-tracked value.
|
|
45
|
+
ctx.advise("cwd", () => currentCwd);
|
|
46
|
+
ctx.registerContextProducer("shell-events", () => {
|
|
47
|
+
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source !== "agent");
|
|
48
|
+
if (fresh.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
lastSeq = exchanges[exchanges.length - 1].id;
|
|
51
|
+
const text = fresh.map(formatExchangeTruncated).filter(Boolean).join("\n");
|
|
52
|
+
if (!text)
|
|
53
|
+
return null;
|
|
54
|
+
return `<shell_events>\n${text}\n</shell_events>`;
|
|
55
|
+
}, { mode: "per-query" });
|
|
56
|
+
ctx.define("shell:context-recent", (n = 25) => {
|
|
57
|
+
const recent = exchanges.slice(-n);
|
|
58
|
+
if (recent.length === 0)
|
|
59
|
+
return "No exchanges yet.";
|
|
60
|
+
return recent.map(exchangeOneLiner).join("\n");
|
|
61
|
+
});
|
|
62
|
+
ctx.define("shell:context-search", (query) => {
|
|
63
|
+
if (!query.trim())
|
|
64
|
+
return "No query provided.";
|
|
65
|
+
let regex;
|
|
66
|
+
try {
|
|
67
|
+
regex = new RegExp(query, "i");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
71
|
+
regex = new RegExp(words.map(escapeRegex).join("|"), "i");
|
|
72
|
+
}
|
|
73
|
+
const matches = [];
|
|
74
|
+
for (const ex of exchanges) {
|
|
75
|
+
const text = `${ex.command}\n${ex.output}`;
|
|
76
|
+
const lines = text.split("\n");
|
|
77
|
+
const matchingIndices = [];
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
if (regex.test(lines[i]))
|
|
80
|
+
matchingIndices.push(i);
|
|
81
|
+
}
|
|
82
|
+
if (matchingIndices.length > 0) {
|
|
83
|
+
const excerpts = matchingIndices.slice(0, 5).map((idx) => {
|
|
84
|
+
const start = Math.max(0, idx - 2);
|
|
85
|
+
const end = Math.min(lines.length, idx + 3);
|
|
86
|
+
return lines.slice(start, end).join("\n");
|
|
87
|
+
});
|
|
88
|
+
matches.push({ exchange: ex, excerpts });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (matches.length === 0)
|
|
92
|
+
return `No results found for "${query}".`;
|
|
93
|
+
const parts = [`Search results for "${query}" (${matches.length} exchanges):\n`];
|
|
94
|
+
for (const m of matches.slice(0, 20)) {
|
|
95
|
+
parts.push(`#${m.exchange.id} [shell_command]`);
|
|
96
|
+
for (const excerpt of m.excerpts)
|
|
97
|
+
parts.push(indent(excerpt, " "));
|
|
98
|
+
parts.push("");
|
|
99
|
+
}
|
|
100
|
+
return parts.join("\n");
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function formatExchangeTruncated(ex) {
|
|
104
|
+
const label = ex.source === "agent" ? "agent → shell" : "shell";
|
|
105
|
+
let s = `#${ex.id} [${label} cwd:${ex.cwd}] $ ${ex.command}\n`;
|
|
106
|
+
if (ex.output)
|
|
107
|
+
s += indent(ex.output, " ") + "\n";
|
|
108
|
+
if (ex.exitCode !== null)
|
|
109
|
+
s += ` exit ${ex.exitCode}\n`;
|
|
110
|
+
return s;
|
|
111
|
+
}
|
|
112
|
+
function exchangeOneLiner(ex) {
|
|
113
|
+
const label = ex.source === "agent" ? "agent → shell" : "shell";
|
|
114
|
+
return `#${ex.id} ${label} [cwd:${ex.cwd}]: ${ex.command} (${ex.outputLines} total lines, exit ${ex.exitCode ?? "?"})`;
|
|
115
|
+
}
|
|
116
|
+
function buildSpillStub(lines, headLines, tailLines, spillPath) {
|
|
117
|
+
const omitted = lines.length - headLines - tailLines;
|
|
118
|
+
return [
|
|
119
|
+
...lines.slice(0, headLines),
|
|
120
|
+
`[... ${omitted} lines truncated — full output at ${spillPath}; use read_file to expand ...]`,
|
|
121
|
+
...lines.slice(-tailLines),
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
function indent(text, prefix) {
|
|
125
|
+
return text.split("\n").map((line) => prefix + line).join("\n");
|
|
126
|
+
}
|
|
127
|
+
function escapeRegex(str) {
|
|
128
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
129
|
+
}
|
|
@@ -14,7 +14,7 @@ import { palette as p } from "../utils/palette.js";
|
|
|
14
14
|
import { discoverSkills, loadSkillContent } from "../agent/skills.js";
|
|
15
15
|
import { reloadExtensions } from "../extension-loader.js";
|
|
16
16
|
export default function activate(ctx) {
|
|
17
|
-
const { bus
|
|
17
|
+
const { bus } = ctx;
|
|
18
18
|
const commands = new Map();
|
|
19
19
|
const register = (cmd) => {
|
|
20
20
|
const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
|
|
@@ -128,7 +128,7 @@ export default function activate(ctx) {
|
|
|
128
128
|
});
|
|
129
129
|
// ── Skill commands (/skill:<name>) ────────────────────────────
|
|
130
130
|
const getSkills = () => {
|
|
131
|
-
const cwd =
|
|
131
|
+
const cwd = ctx.call("cwd") ?? process.cwd();
|
|
132
132
|
return discoverSkills(cwd);
|
|
133
133
|
};
|
|
134
134
|
const handleSkillCommand = (skillName, args) => {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { activateShell } from "./shell/index.js";
|
|
5
5
|
import { createCore } from "./core.js";
|
|
6
6
|
import { palette as p } from "./utils/palette.js";
|
|
7
7
|
import { loadBuiltinExtensions } from "./extensions/index.js";
|
|
@@ -199,26 +199,32 @@ async function main() {
|
|
|
199
199
|
process.stdout.write(`\x1b]0;agent-sh\x07`);
|
|
200
200
|
const cols = process.stdout.columns || 80;
|
|
201
201
|
const rows = process.stdout.rows || 24;
|
|
202
|
+
// Bound after activateShell — cleanup is wired into extCtx.quit before the
|
|
203
|
+
// shell exists, so the closure captures the var by reference.
|
|
204
|
+
let shell = null;
|
|
202
205
|
const cleanup = () => {
|
|
203
206
|
core.kill();
|
|
204
|
-
shell
|
|
207
|
+
shell?.kill();
|
|
205
208
|
if (process.stdin.isTTY) {
|
|
206
209
|
process.stdin.setRawMode(false);
|
|
207
210
|
}
|
|
208
211
|
process.exit(0);
|
|
209
212
|
};
|
|
213
|
+
// ── Extension context (must precede shell activation) ────────
|
|
214
|
+
if (process.env.DEBUG) {
|
|
215
|
+
console.error('[agent-sh] Setting up extensions...');
|
|
216
|
+
}
|
|
217
|
+
const extCtx = core.extensionContext({ quit: cleanup });
|
|
218
|
+
// ── Shell frontend bootstrap (special-cased; see src/shell/index.ts) ──
|
|
210
219
|
if (process.env.DEBUG) {
|
|
211
220
|
console.error('[agent-sh] Creating Shell...');
|
|
212
221
|
}
|
|
213
222
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
214
|
-
|
|
215
|
-
bus,
|
|
216
|
-
handlers: core.handlers,
|
|
223
|
+
shell = activateShell(extCtx, {
|
|
217
224
|
cols,
|
|
218
225
|
rows,
|
|
219
|
-
|
|
226
|
+
shellPath: config.shell || process.env.SHELL || "/bin/bash",
|
|
220
227
|
cwd: process.cwd(),
|
|
221
|
-
instanceId: core.instanceId,
|
|
222
228
|
onShowAgentInfo: () => {
|
|
223
229
|
if (agentInfo) {
|
|
224
230
|
return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
|
|
@@ -241,11 +247,6 @@ async function main() {
|
|
|
241
247
|
},
|
|
242
248
|
returnToSelf: true,
|
|
243
249
|
});
|
|
244
|
-
// ── Extensions ────────────────────────────────────────────────
|
|
245
|
-
if (process.env.DEBUG) {
|
|
246
|
-
console.error('[agent-sh] Setting up extensions...');
|
|
247
|
-
}
|
|
248
|
-
const extCtx = core.extensionContext({ quit: cleanup });
|
|
249
250
|
// Load built-in extensions (individually disableable via settings.disabledBuiltins)
|
|
250
251
|
await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
|
|
251
252
|
// Load user extensions (may register alternative agent backends)
|
|
@@ -273,7 +274,7 @@ async function main() {
|
|
|
273
274
|
// If none did, the built-in AgentLoop gets wired to bus events.
|
|
274
275
|
const { names: backendNames } = core.bus.emitPipe("config:get-backends", { names: [], active: null });
|
|
275
276
|
if (backendNames.length === 0) {
|
|
276
|
-
shell
|
|
277
|
+
shell?.kill();
|
|
277
278
|
console.error("\nagent-sh: no agent backend available.\n\n" +
|
|
278
279
|
" Export OPENROUTER_API_KEY or OPENAI_API_KEY for zero-config launch, or\n" +
|
|
279
280
|
" pass --api-key on the command line, or\n" +
|
|
@@ -354,9 +355,7 @@ async function main() {
|
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
});
|
|
357
|
-
|
|
358
|
-
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
359
|
-
});
|
|
358
|
+
// resize forwarding is set up inside activateShell; nothing to wire here.
|
|
360
359
|
shell.onExit((e) => {
|
|
361
360
|
core.kill();
|
|
362
361
|
if (process.stdin.isTTY) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend bootstrap. Loaded directly from src/index.ts (not the built-in
|
|
3
|
+
* extensions manifest) because PTY + stdin raw mode ownership is order-
|
|
4
|
+
* critical. For pluggable capability extensions see `src/extensions/`.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionContext } from "../types.js";
|
|
7
|
+
export interface ShellActivateOptions {
|
|
8
|
+
cols: number;
|
|
9
|
+
rows: number;
|
|
10
|
+
/** Path to the shell binary (zsh, bash, etc.). */
|
|
11
|
+
shellPath: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Optional callback used by the inline status indicator. */
|
|
14
|
+
onShowAgentInfo?: () => {
|
|
15
|
+
info: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface ShellHandle {
|
|
20
|
+
/** Terminate the PTY. */
|
|
21
|
+
kill(): void;
|
|
22
|
+
/** Subscribe to PTY exit. The frontend uses this to clean up + exit. */
|
|
23
|
+
onExit(callback: (e: {
|
|
24
|
+
exitCode: number;
|
|
25
|
+
signal?: number;
|
|
26
|
+
}) => void): void;
|
|
27
|
+
/** Forward terminal size changes to the PTY. */
|
|
28
|
+
resize(cols: number, rows: number): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Construct the Shell, wire resize forwarding, and register cleanup with the
|
|
32
|
+
* provided ExtensionContext. Returns a handle the caller (typically
|
|
33
|
+
* `src/index.ts`) uses to drive lifecycle from process-level events.
|
|
34
|
+
*/
|
|
35
|
+
export declare function activateShell(ctx: ExtensionContext, opts: ShellActivateOptions): ShellHandle;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Shell } from "./shell.js";
|
|
2
|
+
import { StdoutSurface } from "../utils/compositor.js";
|
|
3
|
+
import { TerminalBuffer } from "../utils/terminal-buffer.js";
|
|
4
|
+
/**
|
|
5
|
+
* Construct the Shell, wire resize forwarding, and register cleanup with the
|
|
6
|
+
* provided ExtensionContext. Returns a handle the caller (typically
|
|
7
|
+
* `src/index.ts`) uses to drive lifecycle from process-level events.
|
|
8
|
+
*/
|
|
9
|
+
export function activateShell(ctx, opts) {
|
|
10
|
+
// Stdout-as-default is a frontend choice, not a kernel one — a hub or
|
|
11
|
+
// web bridge would point these at its own surfaces.
|
|
12
|
+
const stdoutSurface = new StdoutSurface();
|
|
13
|
+
ctx.compositor.setDefault("agent", stdoutSurface);
|
|
14
|
+
ctx.compositor.setDefault("query", stdoutSurface);
|
|
15
|
+
ctx.compositor.setDefault("status", stdoutSurface);
|
|
16
|
+
// Lazy because @xterm/headless is optional; null when not installed.
|
|
17
|
+
let terminalBufferSingleton;
|
|
18
|
+
ctx.define("terminal-buffer", () => {
|
|
19
|
+
if (terminalBufferSingleton !== undefined)
|
|
20
|
+
return terminalBufferSingleton;
|
|
21
|
+
terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
|
|
22
|
+
return terminalBufferSingleton;
|
|
23
|
+
});
|
|
24
|
+
const shell = new Shell({
|
|
25
|
+
bus: ctx.bus,
|
|
26
|
+
handlers: { define: ctx.define, call: ctx.call },
|
|
27
|
+
cols: opts.cols,
|
|
28
|
+
rows: opts.rows,
|
|
29
|
+
shell: opts.shellPath,
|
|
30
|
+
cwd: opts.cwd,
|
|
31
|
+
instanceId: ctx.instanceId,
|
|
32
|
+
onShowAgentInfo: opts.onShowAgentInfo,
|
|
33
|
+
});
|
|
34
|
+
const onResize = () => {
|
|
35
|
+
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
36
|
+
};
|
|
37
|
+
process.stdout.on("resize", onResize);
|
|
38
|
+
ctx.onDispose(() => {
|
|
39
|
+
process.stdout.off("resize", onResize);
|
|
40
|
+
shell.kill();
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
kill: () => shell.kill(),
|
|
44
|
+
onExit: (callback) => shell.onExit(callback),
|
|
45
|
+
resize: (cols, rows) => shell.resize(cols, rows),
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { EventBus } from "./event-bus.js";
|
|
2
|
-
import type { ContextManager } from "./context-manager.js";
|
|
3
2
|
import type { ColorPalette } from "./utils/palette.js";
|
|
4
3
|
import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
5
4
|
import type { ToolDefinition } from "./agent/types.js";
|
|
6
|
-
import type { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
7
5
|
import type { Compositor } from "./utils/compositor.js";
|
|
8
6
|
import type { HistoryAdapter } from "./agent/history-file.js";
|
|
9
7
|
export type { ContentBlock } from "./event-bus.js";
|
|
@@ -20,8 +18,6 @@ export interface RemoteSessionOptions {
|
|
|
20
18
|
suppressQueryBox?: boolean;
|
|
21
19
|
/** Suppress usage stats line (default: true). */
|
|
22
20
|
suppressUsage?: boolean;
|
|
23
|
-
/** Set interactive-session dynamic context (default: false). */
|
|
24
|
-
interactive?: boolean;
|
|
25
21
|
}
|
|
26
22
|
export interface RemoteSession {
|
|
27
23
|
/** Submit a query to the agent from this session. */
|
|
@@ -100,7 +96,6 @@ export interface AgentShellConfig {
|
|
|
100
96
|
*/
|
|
101
97
|
export interface ExtensionContext {
|
|
102
98
|
bus: EventBus;
|
|
103
|
-
contextManager: ContextManager;
|
|
104
99
|
/** Stable per-instance identifier (4-char hex). */
|
|
105
100
|
readonly instanceId: string;
|
|
106
101
|
quit: () => void;
|
|
@@ -134,6 +129,31 @@ export interface ExtensionContext {
|
|
|
134
129
|
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
135
130
|
/** Remove a registered skill by name. */
|
|
136
131
|
removeSkill: (name: string) => void;
|
|
132
|
+
/**
|
|
133
|
+
* Register a context producer — a function that contributes a string
|
|
134
|
+
* (or `null` to skip) into one of two lifecycles:
|
|
135
|
+
*
|
|
136
|
+
* - `mode: "per-request"` (default) — fires on **every LLM request**,
|
|
137
|
+
* including each tool-loop iteration. Output is ephemerally wrapped
|
|
138
|
+
* in `<dynamic_context>` onto the trailing message at request time;
|
|
139
|
+
* never persisted. Use for "current state" signals (in-flight work,
|
|
140
|
+
* active mode, threshold warnings).
|
|
141
|
+
*
|
|
142
|
+
* - `mode: "per-query"` — fires **once at user-query start** in
|
|
143
|
+
* handleQuery. Output is wrapped in `<query_context>` and frozen into
|
|
144
|
+
* the user message; persists in conversation history. Use for
|
|
145
|
+
* "what happened between turns" signals (shell events, accumulated
|
|
146
|
+
* notifications, calendar/inbox deltas).
|
|
147
|
+
*
|
|
148
|
+
* In both modes producers run in registration order, non-null outputs
|
|
149
|
+
* joined with blank lines. When nothing contributes, no envelope tag
|
|
150
|
+
* is emitted.
|
|
151
|
+
*
|
|
152
|
+
* Returns a dispose fn that unregisters the producer.
|
|
153
|
+
*/
|
|
154
|
+
registerContextProducer: (name: string, producer: () => string | null, opts?: {
|
|
155
|
+
mode?: "per-request" | "per-query";
|
|
156
|
+
}) => () => void;
|
|
137
157
|
providers: {
|
|
138
158
|
configure: (id: string, opts: {
|
|
139
159
|
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
@@ -148,11 +168,6 @@ export interface ExtensionContext {
|
|
|
148
168
|
call: (name: string, ...args: any[]) => any;
|
|
149
169
|
/** Names of all registered handlers — for diagnostic / introspection use. */
|
|
150
170
|
list: () => string[];
|
|
151
|
-
/**
|
|
152
|
-
* Shared headless terminal buffer mirroring PTY output.
|
|
153
|
-
* Lazily created on first access. Returns null if @xterm/headless is not installed.
|
|
154
|
-
*/
|
|
155
|
-
terminalBuffer: TerminalBuffer | null;
|
|
156
171
|
/**
|
|
157
172
|
* Routes named render streams ("agent", "query", "status") to surfaces.
|
|
158
173
|
* Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
|
|
@@ -166,7 +181,7 @@ export interface ExtensionContext {
|
|
|
166
181
|
* optionally accepts queries. Handles all compositor routing, shell
|
|
167
182
|
* lifecycle advisors, and chrome suppression.
|
|
168
183
|
*
|
|
169
|
-
* const session = ctx.createRemoteSession({ surface
|
|
184
|
+
* const session = ctx.createRemoteSession({ surface });
|
|
170
185
|
* session.submit("what's on screen?");
|
|
171
186
|
* session.close(); // restores everything
|
|
172
187
|
*/
|
|
@@ -193,24 +208,3 @@ export interface TerminalSession {
|
|
|
193
208
|
done: boolean;
|
|
194
209
|
resolve?: (value: void) => void;
|
|
195
210
|
}
|
|
196
|
-
export type Exchange = {
|
|
197
|
-
type: "shell_command";
|
|
198
|
-
id: number;
|
|
199
|
-
timestamp: number;
|
|
200
|
-
cwd: string;
|
|
201
|
-
command: string;
|
|
202
|
-
/** In-context representation: full text if short, head+tail+path stub if spilled. */
|
|
203
|
-
output: string;
|
|
204
|
-
exitCode: number | null;
|
|
205
|
-
outputLines: number;
|
|
206
|
-
outputBytes: number;
|
|
207
|
-
/** Who initiated this command: "user" (typed) or "agent" (via user_shell). */
|
|
208
|
-
source: "user" | "agent";
|
|
209
|
-
/** Path to the tempfile holding the full captured output, if spilled. */
|
|
210
|
-
spillPath?: string;
|
|
211
|
-
} | {
|
|
212
|
-
type: "agent_query";
|
|
213
|
-
id: number;
|
|
214
|
-
timestamp: number;
|
|
215
|
-
query: string;
|
|
216
|
-
};
|
|
@@ -20,6 +20,19 @@ export declare function findToolCallIds(messages: any[], toolName: string, argFi
|
|
|
20
20
|
* replaced. Non-matching messages are passed through by reference.
|
|
21
21
|
*/
|
|
22
22
|
export declare function stubToolResults(messages: any[], callIds: Set<string>, stub: string): any[];
|
|
23
|
+
/**
|
|
24
|
+
* Prepend a `<dynamic_context>` block onto the trailing message.
|
|
25
|
+
*
|
|
26
|
+
* Wrapping the *trailing* message (rather than inserting at the head) keeps
|
|
27
|
+
* the [system] + [prior history] prefix byte-stable across turns, so the
|
|
28
|
+
* provider's prefix cache holds. Caller passes a copy; conversation state
|
|
29
|
+
* is never mutated.
|
|
30
|
+
*
|
|
31
|
+
* No-op when both `dynamicContext` and `toolPrompt` are empty — i.e. no
|
|
32
|
+
* extension registered any per-turn signal — so a vanilla session sends
|
|
33
|
+
* exactly `[system, ...history]` with no synthetic envelope.
|
|
34
|
+
*/
|
|
35
|
+
export declare function wrapTrailingWithDynamicContext(history: any[], dynamicContext: string, toolPrompt?: string): any[];
|
|
23
36
|
/**
|
|
24
37
|
* Deduplicate tool results: keep only the latest result for a given
|
|
25
38
|
* tool name + argument filter, replace all older results with a stub.
|
|
@@ -53,6 +53,32 @@ export function stubToolResults(messages, callIds, stub) {
|
|
|
53
53
|
return msg;
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Prepend a `<dynamic_context>` block onto the trailing message.
|
|
58
|
+
*
|
|
59
|
+
* Wrapping the *trailing* message (rather than inserting at the head) keeps
|
|
60
|
+
* the [system] + [prior history] prefix byte-stable across turns, so the
|
|
61
|
+
* provider's prefix cache holds. Caller passes a copy; conversation state
|
|
62
|
+
* is never mutated.
|
|
63
|
+
*
|
|
64
|
+
* No-op when both `dynamicContext` and `toolPrompt` are empty — i.e. no
|
|
65
|
+
* extension registered any per-turn signal — so a vanilla session sends
|
|
66
|
+
* exactly `[system, ...history]` with no synthetic envelope.
|
|
67
|
+
*/
|
|
68
|
+
export function wrapTrailingWithDynamicContext(history, dynamicContext, toolPrompt) {
|
|
69
|
+
const ctx = dynamicContext.trim();
|
|
70
|
+
const tp = (toolPrompt ?? "").trim();
|
|
71
|
+
if (!ctx && !tp)
|
|
72
|
+
return history;
|
|
73
|
+
if (history.length === 0)
|
|
74
|
+
return history;
|
|
75
|
+
const last = history[history.length - 1];
|
|
76
|
+
if (typeof last.content !== "string")
|
|
77
|
+
return history;
|
|
78
|
+
const blockBody = ctx && tp ? `${ctx}\n${tp}` : ctx || tp;
|
|
79
|
+
const wrappedContent = `<dynamic_context>\n${blockBody}\n</dynamic_context>\n\n${last.content}`;
|
|
80
|
+
return [...history.slice(0, -1), { ...last, content: wrappedContent }];
|
|
81
|
+
}
|
|
56
82
|
/**
|
|
57
83
|
* Deduplicate tool results: keep only the latest result for a given
|
|
58
84
|
* tool name + argument filter, replace all older results with a stub.
|
|
@@ -48,7 +48,8 @@ function createPanelSurface(panel: FloatingPanel): RenderSurface {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export default function activate(ctx: ExtensionContext): void {
|
|
51
|
-
const { bus, registerInstruction, createRemoteSession
|
|
51
|
+
const { bus, registerInstruction, createRemoteSession } = ctx;
|
|
52
|
+
const terminalBuffer = ctx.call("terminal-buffer");
|
|
52
53
|
|
|
53
54
|
const panel = new FloatingPanel(bus, {
|
|
54
55
|
trigger: "\x1c", // Ctrl+\
|
|
@@ -59,6 +60,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
59
60
|
const panelSurface = createPanelSurface(panel);
|
|
60
61
|
let session: RemoteSession | null = null;
|
|
61
62
|
|
|
63
|
+
// Tell the LLM it's running inside an overlay session. The matching
|
|
64
|
+
// system-prompt block (registered via registerInstruction below) describes
|
|
65
|
+
// how to behave in this mode.
|
|
66
|
+
ctx.registerContextProducer("interactive-session", () =>
|
|
67
|
+
session?.active ? "interactive-session: true" : null,
|
|
68
|
+
);
|
|
69
|
+
|
|
62
70
|
registerInstruction("Interactive Overlay Sessions", [
|
|
63
71
|
"When the dynamic context includes `interactive-session: true`, the user has summoned you",
|
|
64
72
|
"via a hotkey overlay from inside their live terminal. They may be in the middle of using",
|
|
@@ -77,7 +85,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
77
85
|
session = createRemoteSession({
|
|
78
86
|
surface: panelSurface,
|
|
79
87
|
suppressQueryBox: true,
|
|
80
|
-
interactive: true,
|
|
81
88
|
});
|
|
82
89
|
}
|
|
83
90
|
panel.setActive();
|
|
@@ -90,7 +97,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
90
97
|
session = createRemoteSession({
|
|
91
98
|
surface: panelSurface,
|
|
92
99
|
suppressQueryBox: true,
|
|
93
|
-
interactive: true,
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
});
|
|
@@ -190,10 +190,11 @@ class PeerServer {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
export default function activate(ctx: ExtensionContext): void {
|
|
193
|
-
const { bus,
|
|
193
|
+
const { bus, registerCommand, registerTool, registerInstruction, define } = ctx;
|
|
194
|
+
const getCwd = () => ctx.call("cwd") as string;
|
|
194
195
|
const startTime = Date.now();
|
|
195
196
|
|
|
196
|
-
const server = new PeerServer(ctx.instanceId,
|
|
197
|
+
const server = new PeerServer(ctx.instanceId, getCwd(), (...args) => ctx.call(...args));
|
|
197
198
|
server.start();
|
|
198
199
|
|
|
199
200
|
// Track PTY idle window so peer:terminal-send doesn't stomp on a busy shell.
|
|
@@ -203,13 +204,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
203
204
|
define("peer:info", () => ({
|
|
204
205
|
id: ctx.instanceId,
|
|
205
206
|
pid: process.pid,
|
|
206
|
-
cwd:
|
|
207
|
+
cwd: getCwd(),
|
|
207
208
|
uptime: Math.round((Date.now() - startTime) / 1000),
|
|
208
209
|
}));
|
|
209
210
|
server.expose("peer:info");
|
|
210
211
|
|
|
211
212
|
define("peer:terminal-read", () => {
|
|
212
|
-
const tb = ctx.
|
|
213
|
+
const tb = ctx.call("terminal-buffer");
|
|
213
214
|
if (!tb) return { text: "(terminal buffer not available)", altScreen: false };
|
|
214
215
|
return tb.readScreen({ includeScrollback: true });
|
|
215
216
|
});
|
|
@@ -229,15 +230,17 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
229
230
|
}
|
|
230
231
|
bus.emit("shell:pty-write", { data: interpretEscapes(keys) });
|
|
231
232
|
await new Promise((r) => setTimeout(r, typeof settleMs === "number" ? settleMs : SETTLE_MS));
|
|
232
|
-
const tb = ctx.
|
|
233
|
+
const tb = ctx.call("terminal-buffer");
|
|
233
234
|
return { sent: true, screen: tb ? tb.readScreen({ includeScrollback: false }) : null };
|
|
234
235
|
});
|
|
235
236
|
server.expose("peer:terminal-send");
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
// If shell-context isn't loaded, the underlying handler is undefined
|
|
239
|
+
// and these calls surface a clear error to the requesting peer.
|
|
240
|
+
define("peer:context-recent", (n: number = 15) => ctx.call("shell:context-recent", n));
|
|
238
241
|
server.expose("peer:context-recent");
|
|
239
242
|
|
|
240
|
-
define("peer:context-search", (query: string) =>
|
|
243
|
+
define("peer:context-search", (query: string) => ctx.call("shell:context-search", query));
|
|
241
244
|
server.expose("peer:context-search");
|
|
242
245
|
|
|
243
246
|
// ── Inbox + drained turn ──────────────────────────────────────
|
|
@@ -12,7 +12,7 @@ import type { ExtensionContext } from "agent-sh/types";
|
|
|
12
12
|
import { runSubagent } from "agent-sh/agent/subagent";
|
|
13
13
|
|
|
14
14
|
export default function activate(ctx: ExtensionContext): void {
|
|
15
|
-
const { bus, llmClient
|
|
15
|
+
const { bus, llmClient } = ctx;
|
|
16
16
|
if (!llmClient) return;
|
|
17
17
|
|
|
18
18
|
const allToolNames = () => ctx.getTools().map(t => t.name);
|
|
@@ -73,7 +73,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
73
73
|
|
|
74
74
|
const systemPrompt =
|
|
75
75
|
`You are a focused subagent. Complete the task and return a clear, concise result.\n` +
|
|
76
|
-
`Working directory: ${
|
|
76
|
+
`Working directory: ${ctx.call("cwd")}` +
|
|
77
77
|
(nuclearSummary ? `\n\n[Parent session history]\n${nuclearSummary}` : "");
|
|
78
78
|
|
|
79
79
|
try {
|
|
@@ -34,8 +34,9 @@ function settle(ms = 100): Promise<void> {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export default function activate(ctx: ExtensionContext): void {
|
|
37
|
-
const { bus,
|
|
38
|
-
|
|
37
|
+
const { bus, registerTool, registerInstruction } = ctx;
|
|
38
|
+
const tb = ctx.call("terminal-buffer");
|
|
39
|
+
if (!tb) return; // @xterm/headless not installed, or shell frontend not loaded
|
|
39
40
|
|
|
40
41
|
registerTool({
|
|
41
42
|
name: "terminal_read",
|
|
@@ -149,6 +149,11 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
149
149
|
|
|
150
150
|
let state: PaneState | null = null;
|
|
151
151
|
|
|
152
|
+
// Tell the LLM it's running inside an interactive pane session.
|
|
153
|
+
ctx.registerContextProducer("interactive-session", () =>
|
|
154
|
+
state?.mode === "rsplit" ? "interactive-session: true" : null,
|
|
155
|
+
);
|
|
156
|
+
|
|
152
157
|
registerInstruction("Tmux Interactive Session", [
|
|
153
158
|
"When the dynamic context includes `interactive-session: true`, the user is chatting",
|
|
154
159
|
"with you in a side pane next to their terminal. They may have a program running in",
|
|
@@ -229,7 +234,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
229
234
|
const session = createRemoteSession({
|
|
230
235
|
surface,
|
|
231
236
|
suppressQueryBox: true,
|
|
232
|
-
interactive: true,
|
|
233
237
|
});
|
|
234
238
|
|
|
235
239
|
state = { mode: "rsplit", paneId, ttyFd, session, server, client, sockPath, scriptPath };
|
|
@@ -18,7 +18,7 @@ import type { ToolDefinition } from "agent-sh/agent/types";
|
|
|
18
18
|
|
|
19
19
|
export default function activate(ctx: ExtensionContext): void {
|
|
20
20
|
const { bus, registerTool, registerInstruction } = ctx;
|
|
21
|
-
const getCwd = () => ctx.
|
|
21
|
+
const getCwd = () => ctx.call("cwd") as string;
|
|
22
22
|
|
|
23
23
|
// ── Tool ───────────────────────────────────────────────────────
|
|
24
24
|
|