agent-sh 0.12.1 → 0.12.3
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 +10 -4
- package/dist/agent/agent-loop.js +34 -16
- package/dist/agent/conversation-state.d.ts +4 -0
- package/dist/agent/conversation-state.js +44 -0
- package/dist/agent/skills.js +2 -2
- package/dist/agent/system-prompt.js +2 -3
- package/dist/agent/tools/bash.js +10 -3
- package/dist/agent/types.d.ts +3 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.js +5 -3
- package/dist/event-bus.d.ts +22 -0
- package/dist/event-bus.js +51 -3
- package/dist/extension-loader.js +1 -0
- package/dist/extensions/agent-backend.js +4 -1
- package/dist/extensions/openrouter.js +32 -0
- package/dist/index.js +1 -0
- package/dist/init.js +1 -2
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +7 -3
- package/dist/shell/input-handler.d.ts +8 -18
- package/dist/shell/input-handler.js +57 -227
- package/dist/shell/output-parser.d.ts +2 -1
- package/dist/shell/output-parser.js +33 -18
- package/dist/shell/shell.d.ts +1 -0
- package/dist/shell/shell.js +9 -7
- package/dist/shell/tui-input-view.d.ts +37 -0
- package/dist/shell/tui-input-view.js +140 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/compositor.d.ts +7 -1
- package/dist/utils/compositor.js +13 -1
- package/dist/utils/floating-panel.d.ts +6 -2
- package/dist/utils/floating-panel.js +17 -17
- package/dist/utils/ref-counter.d.ts +9 -0
- package/dist/utils/ref-counter.js +9 -0
- package/package.json +3 -1
- package/dist/utils/frame-renderer.d.ts +0 -26
- package/dist/utils/frame-renderer.js +0 -76
- package/dist/utils/output-writer.d.ts +0 -36
- package/dist/utils/output-writer.js +0 -45
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { stripAnsi } from "../utils/ansi.js";
|
|
2
|
+
// Self-emitted form: \e]<num>;id=<own>;<body>\a — only this is honored.
|
|
3
|
+
// Anything else (mismatched tag, untagged) is ignored as opaque foreground output.
|
|
4
|
+
const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
|
|
5
|
+
const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
|
|
6
|
+
const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
|
|
2
7
|
/**
|
|
3
8
|
* Parses PTY output to detect command boundaries, track cwd,
|
|
4
9
|
* and emit shell events. Owns the command lifecycle state.
|
|
@@ -6,13 +11,16 @@ import { stripAnsi } from "../utils/ansi.js";
|
|
|
6
11
|
export class OutputParser {
|
|
7
12
|
bus;
|
|
8
13
|
cwd;
|
|
14
|
+
ownTag;
|
|
9
15
|
currentOutputCapture = "";
|
|
10
16
|
lastCommand = "";
|
|
11
17
|
foregroundBusy = false;
|
|
12
18
|
promptReady = false;
|
|
13
|
-
constructor(bus, initialCwd) {
|
|
19
|
+
constructor(bus, initialCwd, ownTag) {
|
|
14
20
|
this.bus = bus;
|
|
15
21
|
this.cwd = initialCwd;
|
|
22
|
+
// Strip the "id=" prefix; we compare the value alone.
|
|
23
|
+
this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
|
|
16
24
|
}
|
|
17
25
|
/** Process a chunk of PTY output data. */
|
|
18
26
|
processData(data) {
|
|
@@ -49,24 +57,22 @@ export class OutputParser {
|
|
|
49
57
|
* completion. Returns data with the OSC stripped out.
|
|
50
58
|
*/
|
|
51
59
|
handlePreexec(data) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
if (idx === -1)
|
|
60
|
+
const match = PREEXEC_RE.exec(data);
|
|
61
|
+
if (!match)
|
|
55
62
|
return data;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return data
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (match[1] !== this.ownTag) {
|
|
64
|
+
// Nested instance or untagged foreign emission — strip and ignore.
|
|
65
|
+
return data.slice(0, match.index) + data.slice(match.index + match[0].length);
|
|
66
|
+
}
|
|
67
|
+
const command = match[2];
|
|
61
68
|
this.lastCommand = command;
|
|
62
|
-
this.currentOutputCapture = ""; // discard
|
|
69
|
+
this.currentOutputCapture = ""; // discard echo accumulated before preexec
|
|
63
70
|
if (!this.foregroundBusy) {
|
|
64
71
|
this.foregroundBusy = true;
|
|
65
72
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
66
73
|
}
|
|
67
74
|
this.bus.emit("shell:command-start", { command, cwd: this.cwd });
|
|
68
|
-
|
|
69
|
-
return data.slice(endIdx + 1);
|
|
75
|
+
return data.slice(match.index + match[0].length);
|
|
70
76
|
}
|
|
71
77
|
parseOSC7(data) {
|
|
72
78
|
const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
|
|
@@ -83,9 +89,15 @@ export class OutputParser {
|
|
|
83
89
|
* Each time a prompt appears, we finalize the previous command's output.
|
|
84
90
|
*/
|
|
85
91
|
parsePromptMarker(data) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
const match = PROMPT_RE.exec(data);
|
|
93
|
+
if (match) {
|
|
94
|
+
if (match[1] !== this.ownTag) {
|
|
95
|
+
// Nested instance or untagged foreign emission — treat as opaque
|
|
96
|
+
// foreground output, do not finalize our own command.
|
|
97
|
+
this.currentOutputCapture += data;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const markerIdx = match.index;
|
|
89
101
|
// Capture any output that arrived in the same chunk before the marker
|
|
90
102
|
if (markerIdx > 0) {
|
|
91
103
|
this.currentOutputCapture += data.slice(0, markerIdx);
|
|
@@ -125,9 +137,12 @@ export class OutputParser {
|
|
|
125
137
|
* and the shell is ready for input.
|
|
126
138
|
*/
|
|
127
139
|
parsePromptEnd(data) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
const match = READY_RE.exec(data);
|
|
141
|
+
if (!match)
|
|
142
|
+
return;
|
|
143
|
+
if (match[1] !== this.ownTag)
|
|
144
|
+
return;
|
|
145
|
+
this.promptReady = true;
|
|
131
146
|
}
|
|
132
147
|
removeEchoedCommand(output, command) {
|
|
133
148
|
const lines = output.split("\n");
|
package/dist/shell/shell.d.ts
CHANGED
package/dist/shell/shell.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as pty from "node-pty";
|
|
|
5
5
|
import { InputHandler } from "./input-handler.js";
|
|
6
6
|
import { OutputParser } from "./output-parser.js";
|
|
7
7
|
import { getSettings } from "../settings.js";
|
|
8
|
-
import { RefCounter } from "../utils/
|
|
8
|
+
import { RefCounter } from "../utils/ref-counter.js";
|
|
9
9
|
export class Shell {
|
|
10
10
|
ptyProcess;
|
|
11
11
|
bus;
|
|
@@ -43,8 +43,10 @@ export class Shell {
|
|
|
43
43
|
}
|
|
44
44
|
const shellBin = (isZsh || isBash) ? opts.shell : "/bin/bash";
|
|
45
45
|
let shellArgs;
|
|
46
|
+
// Per-instance tag so nested agent-sh hooks don't cross-trigger.
|
|
47
|
+
const instanceTag = `id=${opts.instanceId}`;
|
|
46
48
|
const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
47
|
-
const promptMarker =
|
|
49
|
+
const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
|
|
48
50
|
const titleCmd = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
|
|
49
51
|
this.isZsh = isZsh;
|
|
50
52
|
const settings = getSettings();
|
|
@@ -69,11 +71,11 @@ export class Shell {
|
|
|
69
71
|
"# Preexec hook: emit actual command text so agent-sh can track",
|
|
70
72
|
"# history-recalled and tab-completed commands accurately",
|
|
71
73
|
"__agent_sh_preexec() {",
|
|
72
|
-
|
|
74
|
+
` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
|
|
73
75
|
"}",
|
|
74
76
|
"preexec_functions+=(__agent_sh_preexec)",
|
|
75
77
|
];
|
|
76
|
-
zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init",
|
|
78
|
+
zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "else", " __agent_sh_line_init() {", ` printf "\\e]9998;${instanceTag};READY\\a"`, " }", "fi", "zle -N zle-line-init __agent_sh_line_init", "", "# Hidden widget to trigger prompt redraw from Node.js side", "# Bound to an unused escape sequence that no real key produces", "__agent_sh_redraw() {", " zle reset-prompt", "}", "zle -N __agent_sh_redraw", "bindkey '\\e[9999~' __agent_sh_redraw");
|
|
77
79
|
fs.writeFileSync(path.join(this.tmpDir, ".zshrc"), zshrcLines.join("\n") + "\n");
|
|
78
80
|
env.ZDOTDIR = this.tmpDir;
|
|
79
81
|
shellArgs = ["--no-globalrcs"];
|
|
@@ -106,12 +108,12 @@ export class Shell {
|
|
|
106
108
|
" __agent_sh_preexec_ran=1",
|
|
107
109
|
" local this_cmd",
|
|
108
110
|
` this_cmd=$(HISTTIMEFORMAT='' builtin history 1 | command sed 's/^ *[0-9]* *//')`,
|
|
109
|
-
` printf '\\e]9997;%s\\a' "$this_cmd"`,
|
|
111
|
+
` printf '\\e]9997;${instanceTag};%s\\a' "$this_cmd"`,
|
|
110
112
|
"}",
|
|
111
113
|
"trap '__agent_sh_emit_preexec' DEBUG",
|
|
112
114
|
"",
|
|
113
115
|
"# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
|
|
114
|
-
|
|
116
|
+
`case "$PS1" in *9998*) ;; *) PS1="\${PS1}\\[\\e]9998;${instanceTag};READY\\a\\]";; esac`,
|
|
115
117
|
"",
|
|
116
118
|
"# Mirrors the zsh \\e[9999~ reset-prompt widget — used by agent-sh",
|
|
117
119
|
"# to repaint the prompt in place. All keymaps so `set -o vi` works.",
|
|
@@ -155,7 +157,7 @@ export class Shell {
|
|
|
155
157
|
}
|
|
156
158
|
this.bus = opts.bus;
|
|
157
159
|
this.handlers = opts.handlers;
|
|
158
|
-
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
160
|
+
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag);
|
|
159
161
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
160
162
|
// but it covers uncaught exceptions and normal process.exit paths)
|
|
161
163
|
if (this.tmpDir) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer for the input-mode prompt and autocomplete dropdown.
|
|
3
|
+
* Owns screen state (cursor row/col, autocomplete line count) and the
|
|
4
|
+
* ANSI redraw. The controller drives it via a small VM shape.
|
|
5
|
+
*/
|
|
6
|
+
import type { RenderSurface } from "../utils/compositor.js";
|
|
7
|
+
export interface PromptVM {
|
|
8
|
+
showBuffer: boolean;
|
|
9
|
+
displayText: string;
|
|
10
|
+
displayCursor: number;
|
|
11
|
+
indicator: string;
|
|
12
|
+
promptIcon: string;
|
|
13
|
+
agentInfo: {
|
|
14
|
+
info: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface AutocompleteVM {
|
|
18
|
+
items: {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}[];
|
|
22
|
+
selected: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class TuiInputView {
|
|
25
|
+
private cursorRowsBelow;
|
|
26
|
+
private cursorTermCol;
|
|
27
|
+
private autocompleteLines;
|
|
28
|
+
private readonly surface;
|
|
29
|
+
constructor(surface?: RenderSurface);
|
|
30
|
+
resetCursor(): void;
|
|
31
|
+
enableModeKeys(): void;
|
|
32
|
+
disableModeKeys(): void;
|
|
33
|
+
clearPromptArea(): void;
|
|
34
|
+
drawPrompt(vm: PromptVM): void;
|
|
35
|
+
drawAutocomplete(vm: AutocompleteVM): void;
|
|
36
|
+
clearAutocomplete(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer for the input-mode prompt and autocomplete dropdown.
|
|
3
|
+
* Owns screen state (cursor row/col, autocomplete line count) and the
|
|
4
|
+
* ANSI redraw. The controller drives it via a small VM shape.
|
|
5
|
+
*/
|
|
6
|
+
import { visibleLen } from "../utils/ansi.js";
|
|
7
|
+
import { palette as p } from "../utils/palette.js";
|
|
8
|
+
import { StdoutSurface } from "../utils/compositor.js";
|
|
9
|
+
export class TuiInputView {
|
|
10
|
+
cursorRowsBelow = 0;
|
|
11
|
+
cursorTermCol = 1;
|
|
12
|
+
autocompleteLines = 0;
|
|
13
|
+
surface;
|
|
14
|
+
constructor(surface) {
|
|
15
|
+
this.surface = surface ?? new StdoutSurface();
|
|
16
|
+
}
|
|
17
|
+
resetCursor() {
|
|
18
|
+
this.cursorRowsBelow = 0;
|
|
19
|
+
this.cursorTermCol = 1;
|
|
20
|
+
}
|
|
21
|
+
enableModeKeys() {
|
|
22
|
+
// Kitty progressive enhancement + bracket paste (Shift+Enter → \x1b[13;2u).
|
|
23
|
+
this.surface.write("\x1b[>1u\x1b[?2004h");
|
|
24
|
+
}
|
|
25
|
+
disableModeKeys() {
|
|
26
|
+
this.surface.write("\x1b[<u\x1b[?2004l");
|
|
27
|
+
}
|
|
28
|
+
clearPromptArea() {
|
|
29
|
+
if (this.cursorRowsBelow > 0) {
|
|
30
|
+
this.surface.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
31
|
+
}
|
|
32
|
+
this.surface.write("\r\x1b[J");
|
|
33
|
+
this.cursorRowsBelow = 0;
|
|
34
|
+
}
|
|
35
|
+
drawPrompt(vm) {
|
|
36
|
+
const termW = this.surface.columns;
|
|
37
|
+
if (this.cursorRowsBelow > 0) {
|
|
38
|
+
this.surface.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
39
|
+
}
|
|
40
|
+
this.surface.write("\r\x1b[J");
|
|
41
|
+
const infoPrefix = vm.agentInfo.info
|
|
42
|
+
? `${vm.agentInfo.info} ${p.success}${vm.indicator}${p.reset} `
|
|
43
|
+
: `${p.success}${vm.indicator}${p.reset} `;
|
|
44
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + vm.promptIcon + " " + p.reset;
|
|
45
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(vm.promptIcon) + 1;
|
|
46
|
+
const display = vm.showBuffer ? vm.displayText : "";
|
|
47
|
+
const dCursor = vm.showBuffer ? vm.displayCursor : 0;
|
|
48
|
+
if (!vm.showBuffer) {
|
|
49
|
+
this.surface.write(promptPrefix);
|
|
50
|
+
const N = promptVisLen;
|
|
51
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
52
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
53
|
+
}
|
|
54
|
+
else if (!display.includes("\n")) {
|
|
55
|
+
// DECSC/DECRC bracket the after-cursor text so the cursor lands mid-line.
|
|
56
|
+
const before = display.slice(0, dCursor);
|
|
57
|
+
const after = display.slice(dCursor);
|
|
58
|
+
this.surface.write(promptPrefix + p.accent + before + p.reset +
|
|
59
|
+
"\x1b7" +
|
|
60
|
+
p.accent + after + p.reset +
|
|
61
|
+
"\x1b8");
|
|
62
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
63
|
+
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
64
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const lines = display.split("\n");
|
|
68
|
+
const indent = " ".repeat(promptVisLen);
|
|
69
|
+
let charsRemaining = dCursor;
|
|
70
|
+
let cursorLine = 0;
|
|
71
|
+
for (let li = 0; li < lines.length; li++) {
|
|
72
|
+
if (charsRemaining <= lines[li].length) {
|
|
73
|
+
cursorLine = li;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
charsRemaining -= lines[li].length + 1;
|
|
77
|
+
cursorLine = li + 1;
|
|
78
|
+
}
|
|
79
|
+
let output = "";
|
|
80
|
+
let cursorRowFromTop = 0;
|
|
81
|
+
let rowsSoFar = 0;
|
|
82
|
+
for (let li = 0; li < lines.length; li++) {
|
|
83
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
84
|
+
const lineText = lines[li];
|
|
85
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
86
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
87
|
+
if (li === cursorLine) {
|
|
88
|
+
const before = lineText.slice(0, charsRemaining);
|
|
89
|
+
const after = lineText.slice(charsRemaining);
|
|
90
|
+
output += prefix + p.accent + before + p.reset;
|
|
91
|
+
output += "\x1b7";
|
|
92
|
+
output += p.accent + after + p.reset;
|
|
93
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
94
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
95
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
99
|
+
}
|
|
100
|
+
if (li < lines.length - 1)
|
|
101
|
+
output += "\n";
|
|
102
|
+
rowsSoFar += lineTermRows;
|
|
103
|
+
}
|
|
104
|
+
this.surface.write(output + "\x1b8");
|
|
105
|
+
this.cursorRowsBelow = cursorRowFromTop;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
drawAutocomplete(vm) {
|
|
109
|
+
if (vm.items.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (let i = 0; i < vm.items.length; i++) {
|
|
113
|
+
const item = vm.items[i];
|
|
114
|
+
const selected = i === vm.selected;
|
|
115
|
+
if (selected) {
|
|
116
|
+
lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.surface.write("\n" + lines.join("\n"));
|
|
123
|
+
this.autocompleteLines = lines.length;
|
|
124
|
+
if (this.autocompleteLines > 0) {
|
|
125
|
+
this.surface.write(`\x1b[${this.autocompleteLines}A`);
|
|
126
|
+
}
|
|
127
|
+
// Absolute column set — preceding \n may have scrolled, invalidating DECSC.
|
|
128
|
+
this.surface.write(`\x1b[${this.cursorTermCol}G`);
|
|
129
|
+
}
|
|
130
|
+
clearAutocomplete() {
|
|
131
|
+
if (this.autocompleteLines <= 0)
|
|
132
|
+
return;
|
|
133
|
+
// CSI B (cursor down, bounded) so we don't scroll on the last row.
|
|
134
|
+
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
135
|
+
this.surface.write("\x1b[B\x1b[2K");
|
|
136
|
+
}
|
|
137
|
+
this.surface.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
138
|
+
this.autocompleteLines = 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -48,6 +48,9 @@ export interface AgentMode {
|
|
|
48
48
|
reasoning?: boolean;
|
|
49
49
|
/** Provider supports the reasoning_effort parameter. */
|
|
50
50
|
supportsReasoningEffort?: boolean;
|
|
51
|
+
/** Echo reasoning_content back on assistant turns. Required by DeepSeek;
|
|
52
|
+
* default off (leaky shims may forward it to the model as OOD input). */
|
|
53
|
+
echoReasoning?: boolean;
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Backend-agnostic LLM interface exposed via `ctx.llm`. Backends fulfill it
|
|
@@ -146,6 +149,9 @@ export interface ExtensionContext {
|
|
|
146
149
|
* Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
|
|
147
150
|
*/
|
|
148
151
|
compositor: Compositor;
|
|
152
|
+
/** Teardown callback fired on /reload. For resources the scoped context
|
|
153
|
+
* can't track: process listeners, timers, watchers, sockets. */
|
|
154
|
+
onDispose: (fn: () => void) => void;
|
|
149
155
|
/**
|
|
150
156
|
* Create a remote session that routes agent output to a surface and
|
|
151
157
|
* optionally accepts queries. Handles all compositor routing, shell
|
|
@@ -36,6 +36,10 @@ export interface RenderSurface {
|
|
|
36
36
|
writeLine(line: string): void;
|
|
37
37
|
/** Available width in columns. */
|
|
38
38
|
readonly columns: number;
|
|
39
|
+
/** Available height in rows. */
|
|
40
|
+
readonly rows: number;
|
|
41
|
+
/** Subscribe to size changes. Returns unsubscribe. */
|
|
42
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
39
43
|
}
|
|
40
44
|
export interface Compositor {
|
|
41
45
|
/** Get the currently active surface for a stream. */
|
|
@@ -48,11 +52,13 @@ export interface Compositor {
|
|
|
48
52
|
}
|
|
49
53
|
/** Silent sink — drops all output. Used when no surface is registered. */
|
|
50
54
|
export declare const nullSurface: RenderSurface;
|
|
51
|
-
/** Surface backed by process.stdout. */
|
|
55
|
+
/** Surface backed by process.stdout — the only sanctioned bridge to it. */
|
|
52
56
|
export declare class StdoutSurface implements RenderSurface {
|
|
53
57
|
write(text: string): void;
|
|
54
58
|
writeLine(line: string): void;
|
|
55
59
|
get columns(): number;
|
|
60
|
+
get rows(): number;
|
|
61
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
56
62
|
}
|
|
57
63
|
export declare class DefaultCompositor implements Compositor {
|
|
58
64
|
private defaults;
|
package/dist/utils/compositor.js
CHANGED
|
@@ -29,8 +29,10 @@ export const nullSurface = {
|
|
|
29
29
|
write() { },
|
|
30
30
|
writeLine() { },
|
|
31
31
|
get columns() { return 80; },
|
|
32
|
+
get rows() { return 24; },
|
|
33
|
+
onResize() { return () => { }; },
|
|
32
34
|
};
|
|
33
|
-
/** Surface backed by process.stdout. */
|
|
35
|
+
/** Surface backed by process.stdout — the only sanctioned bridge to it. */
|
|
34
36
|
export class StdoutSurface {
|
|
35
37
|
write(text) {
|
|
36
38
|
if (process.stdout.writable) {
|
|
@@ -46,6 +48,14 @@ export class StdoutSurface {
|
|
|
46
48
|
get columns() {
|
|
47
49
|
return process.stdout.columns || 80;
|
|
48
50
|
}
|
|
51
|
+
get rows() {
|
|
52
|
+
return process.stdout.rows || 24;
|
|
53
|
+
}
|
|
54
|
+
onResize(cb) {
|
|
55
|
+
const handler = () => cb(this.columns, this.rows);
|
|
56
|
+
process.stdout.on("resize", handler);
|
|
57
|
+
return () => { process.stdout.off("resize", handler); };
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
export class DefaultCompositor {
|
|
51
61
|
defaults = new Map();
|
|
@@ -111,6 +121,8 @@ export class DefaultCompositor {
|
|
|
111
121
|
target.writeLine(line);
|
|
112
122
|
},
|
|
113
123
|
get columns() { return target.columns; },
|
|
124
|
+
get rows() { return target.rows; },
|
|
125
|
+
onResize: (cb) => target.onResize(cb),
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
}
|
|
@@ -2,6 +2,7 @@ import { TerminalBuffer } from "./terminal-buffer.js";
|
|
|
2
2
|
import { HandlerRegistry } from "./handler-registry.js";
|
|
3
3
|
import type { EventBus } from "../event-bus.js";
|
|
4
4
|
import type { BorderStyle } from "./box-frame.js";
|
|
5
|
+
import { type RenderSurface } from "./compositor.js";
|
|
5
6
|
export interface FloatingPanelConfig {
|
|
6
7
|
/** Key sequence that toggles the panel (e.g. "\x1c" for Ctrl+\). */
|
|
7
8
|
trigger: string;
|
|
@@ -36,6 +37,8 @@ export interface FloatingPanelConfig {
|
|
|
36
37
|
* `{prefix}:submit`, etc. Use different prefixes for multiple panels.
|
|
37
38
|
*/
|
|
38
39
|
handlerPrefix?: string;
|
|
40
|
+
/** Render sink + viewport. Defaults to a fresh StdoutSurface. */
|
|
41
|
+
surface?: RenderSurface;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* Context passed to the render-content handler.
|
|
@@ -129,6 +132,7 @@ export type Phase = "idle" | "input" | "active" | "done";
|
|
|
129
132
|
export declare class FloatingPanel {
|
|
130
133
|
private readonly config;
|
|
131
134
|
private readonly bus;
|
|
135
|
+
private readonly surface;
|
|
132
136
|
private readonly border;
|
|
133
137
|
private readonly externalBuffer;
|
|
134
138
|
private readonly prefix;
|
|
@@ -164,7 +168,7 @@ export declare class FloatingPanel {
|
|
|
164
168
|
private title;
|
|
165
169
|
private footer;
|
|
166
170
|
private renderTimer;
|
|
167
|
-
private
|
|
171
|
+
private resizeUnsub;
|
|
168
172
|
private prevFrame;
|
|
169
173
|
private suppressNextRedraw;
|
|
170
174
|
private autoDismissTimer;
|
|
@@ -213,7 +217,7 @@ export declare class FloatingPanel {
|
|
|
213
217
|
/** Handle scroll input. Returns true if consumed. */
|
|
214
218
|
private handleScroll;
|
|
215
219
|
private handleInputKey;
|
|
216
|
-
/** Compute box geometry from config + current
|
|
220
|
+
/** Compute box geometry from config + current viewport. */
|
|
217
221
|
computeGeometry(): BoxGeometry;
|
|
218
222
|
private buildFrame;
|
|
219
223
|
private scheduleRender;
|
|
@@ -35,6 +35,7 @@ import { wrapLine } from "./markdown.js";
|
|
|
35
35
|
import { LineEditor } from "./line-editor.js";
|
|
36
36
|
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
37
37
|
import { HandlerRegistry } from "./handler-registry.js";
|
|
38
|
+
import { StdoutSurface } from "./compositor.js";
|
|
38
39
|
// ── ANSI constants ──────────────────────────────────────────────
|
|
39
40
|
const DIM = "\x1b[2m";
|
|
40
41
|
const RESET = "\x1b[0m";
|
|
@@ -74,6 +75,7 @@ export class FloatingPanel {
|
|
|
74
75
|
// ── Configuration ───────────────────────────────────────────
|
|
75
76
|
config;
|
|
76
77
|
bus;
|
|
78
|
+
surface;
|
|
77
79
|
border;
|
|
78
80
|
externalBuffer;
|
|
79
81
|
prefix;
|
|
@@ -112,7 +114,7 @@ export class FloatingPanel {
|
|
|
112
114
|
title = "";
|
|
113
115
|
footer = "";
|
|
114
116
|
renderTimer = null;
|
|
115
|
-
|
|
117
|
+
resizeUnsub = null;
|
|
116
118
|
prevFrame = [];
|
|
117
119
|
suppressNextRedraw = false;
|
|
118
120
|
autoDismissTimer = null;
|
|
@@ -124,6 +126,7 @@ export class FloatingPanel {
|
|
|
124
126
|
prevSerialized = "";
|
|
125
127
|
constructor(bus, config, handlers) {
|
|
126
128
|
this.bus = bus;
|
|
129
|
+
this.surface = config.surface ?? new StdoutSurface();
|
|
127
130
|
this.externalBuffer = config.terminalBuffer;
|
|
128
131
|
this.prefix = config.handlerPrefix ?? "panel";
|
|
129
132
|
this.handlers = handlers ?? new HandlerRegistry();
|
|
@@ -436,10 +439,9 @@ export class FloatingPanel {
|
|
|
436
439
|
this.bus.emit("shell:stdout-hold", {});
|
|
437
440
|
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
438
441
|
if (this.usedAltScreen) {
|
|
439
|
-
|
|
442
|
+
this.surface.write("\x1b[?1049h");
|
|
440
443
|
}
|
|
441
|
-
this.
|
|
442
|
-
process.stdout.on("resize", this.resizeHandler);
|
|
444
|
+
this.resizeUnsub = this.surface.onResize(() => { this.prevFrame = []; this.render(); });
|
|
443
445
|
this.render();
|
|
444
446
|
}
|
|
445
447
|
// ── Public content API ──────────────────────────────────────
|
|
@@ -674,10 +676,10 @@ export class FloatingPanel {
|
|
|
674
676
|
}
|
|
675
677
|
}
|
|
676
678
|
// ── Geometry ───────────────────────────────────────────────
|
|
677
|
-
/** Compute box geometry from config + current
|
|
679
|
+
/** Compute box geometry from config + current viewport. */
|
|
678
680
|
computeGeometry() {
|
|
679
|
-
const cols =
|
|
680
|
-
const rows =
|
|
681
|
+
const cols = this.surface.columns;
|
|
682
|
+
const rows = this.surface.rows;
|
|
681
683
|
const boxW = Math.min(this.resolveSize(this.config.width, cols - 4), this.config.maxWidth);
|
|
682
684
|
const boxH = Math.min(this.resolveSize(this.config.height, rows - 4), Math.max(this.config.minHeight + 2, rows - 4));
|
|
683
685
|
const boxTop = Math.floor((rows - boxH) / 2);
|
|
@@ -744,24 +746,22 @@ export class FloatingPanel {
|
|
|
744
746
|
out.push(cursorSeq);
|
|
745
747
|
out.push(SYNC_END);
|
|
746
748
|
if (this.prevFrame.length === 0 || dirty) {
|
|
747
|
-
|
|
749
|
+
this.surface.write(out.join(""));
|
|
748
750
|
}
|
|
749
751
|
this.prevFrame = frame;
|
|
750
752
|
}
|
|
751
753
|
// ── Screen helpers ────────────────────────────────────────
|
|
752
754
|
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
753
755
|
teardownScreen() {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
this.resizeHandler = null;
|
|
757
|
-
}
|
|
756
|
+
this.resizeUnsub?.();
|
|
757
|
+
this.resizeUnsub = null;
|
|
758
758
|
this.suppressNextRedraw = true;
|
|
759
759
|
// Re-check alt screen state: the program we overlaid may have exited
|
|
760
760
|
// (e.g. agent quit vim via terminal_keys) while the panel was active.
|
|
761
761
|
const stillInAltScreen = !this.usedAltScreen && !!this.buffer?.altScreen;
|
|
762
762
|
const programExited = !this.usedAltScreen && !stillInAltScreen;
|
|
763
763
|
if (this.usedAltScreen) {
|
|
764
|
-
|
|
764
|
+
this.surface.write("\x1b[?1049l");
|
|
765
765
|
}
|
|
766
766
|
// Replay PTY output that arrived while the overlay was active.
|
|
767
767
|
// Without this, commands run by the agent (e.g. user_shell ls)
|
|
@@ -769,7 +769,7 @@ export class FloatingPanel {
|
|
|
769
769
|
// from before the overlay opened, losing any shell output produced
|
|
770
770
|
// during the session.
|
|
771
771
|
if (this.ptyBuffer) {
|
|
772
|
-
|
|
772
|
+
this.surface.write(this.ptyBuffer);
|
|
773
773
|
}
|
|
774
774
|
this.ptyBuffer = "";
|
|
775
775
|
this.bus.emit("shell:stdout-release", {});
|
|
@@ -778,8 +778,8 @@ export class FloatingPanel {
|
|
|
778
778
|
// or the overlaid program exited (e.g. agent quit vim) and we
|
|
779
779
|
// discarded its stale buffer — SIGWINCH makes the shell redraw
|
|
780
780
|
// its prompt cleanly.
|
|
781
|
-
const cols =
|
|
782
|
-
const rows =
|
|
781
|
+
const cols = this.surface.columns;
|
|
782
|
+
const rows = this.surface.rows;
|
|
783
783
|
this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
|
|
784
784
|
setTimeout(() => {
|
|
785
785
|
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
@@ -808,7 +808,7 @@ export class FloatingPanel {
|
|
|
808
808
|
const serialized = this.buffer.serialize();
|
|
809
809
|
if (serialized && serialized !== this.prevSerialized) {
|
|
810
810
|
this.prevSerialized = serialized;
|
|
811
|
-
|
|
811
|
+
this.surface.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
814
|
resolveSize(spec, available) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Simple ref-counted counter. Increment/decrement never goes below zero. */
|
|
2
|
+
export class RefCounter {
|
|
3
|
+
count = 0;
|
|
4
|
+
increment() { this.count++; }
|
|
5
|
+
decrement() { this.count = Math.max(0, this.count - 1); }
|
|
6
|
+
reset() { this.count = 0; }
|
|
7
|
+
get active() { return this.count > 0; }
|
|
8
|
+
get value() { return this.count; }
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core.js",
|
|
@@ -127,6 +127,8 @@
|
|
|
127
127
|
"node": ">=18"
|
|
128
128
|
},
|
|
129
129
|
"dependencies": {
|
|
130
|
+
"@xterm/addon-serialize": "^0.13.0",
|
|
131
|
+
"@xterm/headless": "^5.5.0",
|
|
130
132
|
"cli-highlight": "^2.1.11",
|
|
131
133
|
"diff": "^9.0.0",
|
|
132
134
|
"marked": "^17.0.6",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Differential frame renderer.
|
|
3
|
-
*
|
|
4
|
-
* Accepts a frame (string[]) and writes only the lines that changed
|
|
5
|
-
* compared to the previous frame. Designed for scrolling content
|
|
6
|
-
* (not full-screen ownership like pi-tui).
|
|
7
|
-
*
|
|
8
|
-
* Fast paths:
|
|
9
|
-
* 1. First render → write everything
|
|
10
|
-
* 2. Append-only → write only new lines
|
|
11
|
-
* 3. Last line changed → \r overwrite (for spinner / partial streaming)
|
|
12
|
-
* 4. General diff → cursor-up, rewrite changed region, cursor-down
|
|
13
|
-
*/
|
|
14
|
-
import type { OutputWriter } from "./output-writer.js";
|
|
15
|
-
export declare class FrameRenderer {
|
|
16
|
-
private writer;
|
|
17
|
-
private prevLines;
|
|
18
|
-
constructor(writer: OutputWriter);
|
|
19
|
-
/**
|
|
20
|
-
* Render a new frame, writing only the diff to the output.
|
|
21
|
-
* Each line in `lines` should NOT include a trailing newline.
|
|
22
|
-
*/
|
|
23
|
-
update(lines: string[]): void;
|
|
24
|
-
/** Reset state — next update will be treated as a first render. */
|
|
25
|
-
reset(): void;
|
|
26
|
-
}
|