agent-sh 0.14.2 → 0.14.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/dist/shell/events.d.ts +2 -0
- package/dist/shell/output-parser.d.ts +11 -22
- package/dist/shell/output-parser.js +16 -34
- package/dist/shell/shell-context.d.ts +3 -6
- package/dist/shell/shell-context.js +15 -7
- package/dist/shell/shell.js +3 -1
- package/dist/shell/strategies/types.d.ts +6 -0
- package/dist/shell/strategies/zsh.js +7 -0
- package/examples/extensions/ashi/src/cli.ts +32 -4
- package/examples/extensions/ashi/src/commands.ts +2 -20
- package/examples/extensions/ashi/src/default-schema-renderers.ts +28 -25
- package/examples/extensions/ashi/src/frontend.ts +298 -60
- package/examples/extensions/ashi/src/schema.ts +32 -53
- package/examples/extensions/ashi/src/session-store.ts +60 -13
- package/examples/extensions/ashi/src/shell-mode.ts +52 -0
- package/examples/extensions/ashi/src/status-footer.ts +19 -5
- package/examples/extensions/ashi/src/theme.ts +2 -1
- package/package.json +5 -1
package/dist/shell/events.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ declare module "../core/event-bus.js" {
|
|
|
19
19
|
};
|
|
20
20
|
"shell:agent-exec-start": Record<string, never>;
|
|
21
21
|
"shell:agent-exec-done": Record<string, never>;
|
|
22
|
+
/** Mark the next user-emitted shell command as excluded from <shell_events>. */
|
|
23
|
+
"shell:user-exec-exclude-next": Record<string, never>;
|
|
22
24
|
"shell:pty-data": {
|
|
23
25
|
raw: string;
|
|
24
26
|
};
|
|
@@ -1,42 +1,31 @@
|
|
|
1
1
|
import type { EventBus } from "../core/event-bus.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export interface OutputParserOpts {
|
|
3
|
+
/** Optional shell-specific cleanup applied to raw output before stripAnsi. */
|
|
4
|
+
cleanOutput?(raw: string): string;
|
|
5
|
+
}
|
|
6
6
|
export declare class OutputParser {
|
|
7
7
|
private bus;
|
|
8
8
|
private cwd;
|
|
9
9
|
private ownTag;
|
|
10
|
+
private cleanOutput;
|
|
10
11
|
private currentOutputCapture;
|
|
11
12
|
private lastCommand;
|
|
12
13
|
private foregroundBusy;
|
|
13
14
|
private promptReady;
|
|
14
|
-
constructor(bus: EventBus, initialCwd: string, ownTag: string);
|
|
15
|
-
/** Process a chunk of PTY output data. */
|
|
15
|
+
constructor(bus: EventBus, initialCwd: string, ownTag: string, opts?: OutputParserOpts);
|
|
16
16
|
processData(data: string): void;
|
|
17
|
-
/** Called when user presses Enter on a non-empty line. */
|
|
18
17
|
onCommandEntered(command: string, cwd: string): void;
|
|
19
|
-
/** Whether the shell's prompt is fully rendered and ready for input. */
|
|
20
18
|
isPromptReady(): boolean;
|
|
21
19
|
isForegroundBusy(): boolean;
|
|
22
20
|
getCwd(): string;
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
27
|
-
* completion. Returns data with the OSC stripped out.
|
|
28
|
-
*/
|
|
21
|
+
/** Pulls the actual command from the shell's OSC 9997 preexec marker —
|
|
22
|
+
* more reliable than the InputHandler's lineBuffer, which can't track
|
|
23
|
+
* history recall or tab completion. Returns data with the OSC stripped. */
|
|
29
24
|
private handlePreexec;
|
|
30
25
|
private parseOSC7;
|
|
31
|
-
/**
|
|
32
|
-
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
|
33
|
-
* Each time a prompt appears, we finalize the previous command's output.
|
|
34
|
-
*/
|
|
26
|
+
/** OSC 9999 marker — each occurrence finalizes the previous command's output. */
|
|
35
27
|
private parsePromptMarker;
|
|
36
|
-
/**
|
|
37
|
-
* Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
|
|
38
|
-
* and the shell is ready for input.
|
|
39
|
-
*/
|
|
28
|
+
/** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
|
|
40
29
|
private parsePromptEnd;
|
|
41
30
|
private removeEchoedCommand;
|
|
42
31
|
}
|
|
@@ -4,32 +4,27 @@ import { stripAnsi } from "../utils/ansi.js";
|
|
|
4
4
|
const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
|
|
5
5
|
const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
|
|
6
6
|
const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
|
|
7
|
-
/**
|
|
8
|
-
* Parses PTY output to detect command boundaries, track cwd,
|
|
9
|
-
* and emit shell events. Owns the command lifecycle state.
|
|
10
|
-
*/
|
|
11
7
|
export class OutputParser {
|
|
12
8
|
bus;
|
|
13
9
|
cwd;
|
|
14
10
|
ownTag;
|
|
11
|
+
cleanOutput;
|
|
15
12
|
currentOutputCapture = "";
|
|
16
13
|
lastCommand = "";
|
|
17
14
|
foregroundBusy = false;
|
|
18
15
|
promptReady = false;
|
|
19
|
-
constructor(bus, initialCwd, ownTag) {
|
|
16
|
+
constructor(bus, initialCwd, ownTag, opts = {}) {
|
|
20
17
|
this.bus = bus;
|
|
21
18
|
this.cwd = initialCwd;
|
|
22
|
-
// Strip the "id=" prefix; we compare the value alone.
|
|
23
19
|
this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
|
|
20
|
+
this.cleanOutput = opts.cleanOutput ?? ((raw) => raw);
|
|
24
21
|
}
|
|
25
|
-
/** Process a chunk of PTY output data. */
|
|
26
22
|
processData(data) {
|
|
27
23
|
this.parseOSC7(data);
|
|
28
24
|
data = this.handlePreexec(data);
|
|
29
25
|
this.parsePromptMarker(data);
|
|
30
26
|
this.parsePromptEnd(data);
|
|
31
27
|
}
|
|
32
|
-
/** Called when user presses Enter on a non-empty line. */
|
|
33
28
|
onCommandEntered(command, cwd) {
|
|
34
29
|
this.lastCommand = command;
|
|
35
30
|
this.currentOutputCapture = "";
|
|
@@ -39,7 +34,6 @@ export class OutputParser {
|
|
|
39
34
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
|
-
/** Whether the shell's prompt is fully rendered and ready for input. */
|
|
43
37
|
isPromptReady() {
|
|
44
38
|
return this.promptReady;
|
|
45
39
|
}
|
|
@@ -49,13 +43,9 @@ export class OutputParser {
|
|
|
49
43
|
getCwd() {
|
|
50
44
|
return this.cwd;
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* This carries the actual command text from the shell — more reliable than
|
|
56
|
-
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
57
|
-
* completion. Returns data with the OSC stripped out.
|
|
58
|
-
*/
|
|
46
|
+
/** Pulls the actual command from the shell's OSC 9997 preexec marker —
|
|
47
|
+
* more reliable than the InputHandler's lineBuffer, which can't track
|
|
48
|
+
* history recall or tab completion. Returns data with the OSC stripped. */
|
|
59
49
|
handlePreexec(data) {
|
|
60
50
|
const match = PREEXEC_RE.exec(data);
|
|
61
51
|
if (!match)
|
|
@@ -66,7 +56,8 @@ export class OutputParser {
|
|
|
66
56
|
}
|
|
67
57
|
const command = match[2];
|
|
68
58
|
this.lastCommand = command;
|
|
69
|
-
|
|
59
|
+
// Discard echo accumulated before preexec.
|
|
60
|
+
this.currentOutputCapture = "";
|
|
70
61
|
if (!this.foregroundBusy) {
|
|
71
62
|
this.foregroundBusy = true;
|
|
72
63
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
@@ -84,21 +75,16 @@ export class OutputParser {
|
|
|
84
75
|
}
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
|
89
|
-
* Each time a prompt appears, we finalize the previous command's output.
|
|
90
|
-
*/
|
|
78
|
+
/** OSC 9999 marker — each occurrence finalizes the previous command's output. */
|
|
91
79
|
parsePromptMarker(data) {
|
|
92
80
|
const match = PROMPT_RE.exec(data);
|
|
93
81
|
if (match) {
|
|
94
82
|
if (match[1] !== this.ownTag) {
|
|
95
|
-
// Nested
|
|
96
|
-
// foreground output, do not finalize our own command.
|
|
83
|
+
// Nested or untagged emission: keep as opaque foreground output.
|
|
97
84
|
this.currentOutputCapture += data;
|
|
98
85
|
return;
|
|
99
86
|
}
|
|
100
87
|
const markerIdx = match.index;
|
|
101
|
-
// Capture any output that arrived in the same chunk before the marker
|
|
102
88
|
if (markerIdx > 0) {
|
|
103
89
|
this.currentOutputCapture += data.slice(0, markerIdx);
|
|
104
90
|
}
|
|
@@ -108,7 +94,8 @@ export class OutputParser {
|
|
|
108
94
|
this.bus.emit("shell:foreground-busy", { busy: false });
|
|
109
95
|
}
|
|
110
96
|
if (this.lastCommand) {
|
|
111
|
-
const
|
|
97
|
+
const raw = this.cleanOutput(this.currentOutputCapture);
|
|
98
|
+
const output = stripAnsi(raw).trim();
|
|
112
99
|
const cleaned = this.removeEchoedCommand(output, this.lastCommand);
|
|
113
100
|
this.bus.emit("shell:command-done", {
|
|
114
101
|
command: this.lastCommand,
|
|
@@ -121,21 +108,16 @@ export class OutputParser {
|
|
|
121
108
|
this.currentOutputCapture = "";
|
|
122
109
|
}
|
|
123
110
|
else {
|
|
124
|
-
// Cap
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// command-done context.
|
|
128
|
-
const MAX_CAPTURE = 128 * 1024; // 128 KB
|
|
111
|
+
// Cap to the tail so a long-running foreground program (tmux, vim)
|
|
112
|
+
// emitting output without prompt markers can't grow this unboundedly.
|
|
113
|
+
const MAX_CAPTURE = 128 * 1024;
|
|
129
114
|
this.currentOutputCapture += data;
|
|
130
115
|
if (this.currentOutputCapture.length > MAX_CAPTURE) {
|
|
131
116
|
this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
119
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
|
|
137
|
-
* and the shell is ready for input.
|
|
138
|
-
*/
|
|
120
|
+
/** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
|
|
139
121
|
parsePromptEnd(data) {
|
|
140
122
|
const match = READY_RE.exec(data);
|
|
141
123
|
if (!match)
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* user-shell exchanges) signals. Frontends without a PTY skip this
|
|
5
|
-
* built-in and the agent runs cwd-aware via core's process.cwd() default.
|
|
6
|
-
*/
|
|
1
|
+
/** Tracks PTY commands and cwd, spills long outputs, contributes per-query
|
|
2
|
+
* `<cwd>` (always) and `<shell_events>` (fresh user exchanges). Frontends
|
|
3
|
+
* without a PTY skip this and fall back to core's process.cwd() default. */
|
|
7
4
|
import type { ExtensionContext } from "./host-types.js";
|
|
8
5
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -6,12 +6,13 @@ export default function activate(ctx) {
|
|
|
6
6
|
let nextId = 1;
|
|
7
7
|
let currentCwd = process.cwd();
|
|
8
8
|
let agentShellActive = false;
|
|
9
|
+
let nextUserExcluded = false;
|
|
9
10
|
let lastSeq = 0;
|
|
10
11
|
bus.on("shell:command-done", (e) => {
|
|
11
12
|
const lines = e.output.split("\n");
|
|
12
13
|
const s = getSettings();
|
|
13
|
-
// Long outputs spill to a tempfile
|
|
14
|
-
//
|
|
14
|
+
// Long outputs spill to a tempfile the agent can `read_file` instead of
|
|
15
|
+
// carrying the full text in LLM context.
|
|
15
16
|
let output = e.output;
|
|
16
17
|
let spillPath;
|
|
17
18
|
if (lines.length > s.shellTruncateThreshold) {
|
|
@@ -25,6 +26,13 @@ export default function activate(ctx) {
|
|
|
25
26
|
spillPath = undefined;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
const source = agentShellActive
|
|
30
|
+
? "agent"
|
|
31
|
+
: nextUserExcluded
|
|
32
|
+
? "user-excluded"
|
|
33
|
+
: "user";
|
|
34
|
+
if (nextUserExcluded)
|
|
35
|
+
nextUserExcluded = false;
|
|
28
36
|
exchanges.push({
|
|
29
37
|
id: nextId++,
|
|
30
38
|
timestamp: Date.now(),
|
|
@@ -34,22 +42,22 @@ export default function activate(ctx) {
|
|
|
34
42
|
exitCode: e.exitCode,
|
|
35
43
|
outputLines: lines.length,
|
|
36
44
|
outputBytes: e.output.length,
|
|
37
|
-
source
|
|
45
|
+
source,
|
|
38
46
|
spillPath,
|
|
39
47
|
});
|
|
40
48
|
});
|
|
41
49
|
bus.on("shell:cwd-change", (e) => { currentCwd = e.cwd; });
|
|
42
50
|
bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
|
|
43
51
|
bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
|
|
44
|
-
|
|
52
|
+
bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
|
|
45
53
|
ctx.advise("cwd", () => currentCwd);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
54
|
+
// Advise the core handler directly: this loads before the agent host
|
|
55
|
+
// attaches `ctx.agent`, so the sugar isn't available yet.
|
|
48
56
|
ctx.advise("query-context:build", (next) => {
|
|
49
57
|
const base = next();
|
|
50
58
|
const part = (() => {
|
|
51
59
|
const cwdTag = `<cwd>${currentCwd}</cwd>`;
|
|
52
|
-
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source
|
|
60
|
+
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source === "user");
|
|
53
61
|
if (fresh.length === 0)
|
|
54
62
|
return cwdTag;
|
|
55
63
|
lastSeq = exchanges[exchanges.length - 1].id;
|
package/dist/shell/shell.js
CHANGED
|
@@ -77,7 +77,9 @@ export class Shell {
|
|
|
77
77
|
clearOpost();
|
|
78
78
|
this.bus = opts.bus;
|
|
79
79
|
this.handlers = opts.handlers;
|
|
80
|
-
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag
|
|
80
|
+
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag, {
|
|
81
|
+
cleanOutput: this.strategy.cleanOutput?.bind(this.strategy),
|
|
82
|
+
});
|
|
81
83
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
82
84
|
// but it covers uncaught exceptions and normal process.exit paths)
|
|
83
85
|
if (this.tmpDir) {
|
|
@@ -47,4 +47,10 @@ export interface ShellStrategy {
|
|
|
47
47
|
* Returns null if the shell can't redraw — caller falls back to freshPrompt.
|
|
48
48
|
*/
|
|
49
49
|
redrawEscape(): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Strip shell-specific artifacts from raw PTY output before stripAnsi
|
|
52
|
+
* collapses SGR codes (e.g. zsh's PROMPT_SP inverse-video `%`). Default
|
|
53
|
+
* is identity — most shells need no cleanup.
|
|
54
|
+
*/
|
|
55
|
+
cleanOutput?(raw: string): string;
|
|
50
56
|
}
|
|
@@ -69,4 +69,11 @@ export const zshStrategy = {
|
|
|
69
69
|
redrawEscape() {
|
|
70
70
|
return "\x1b[9999~";
|
|
71
71
|
},
|
|
72
|
+
cleanOutput(raw) {
|
|
73
|
+
return raw.replace(PROMPT_SP_RE, "");
|
|
74
|
+
},
|
|
72
75
|
};
|
|
76
|
+
/** PROMPT_SP marker (inverse-video PROMPT_EOL_MARK, default `%`) zsh prints
|
|
77
|
+
* before a prompt when prior output didn't end at column 0. Matching the
|
|
78
|
+
* inverse-video wrapper preserves legitimate trailing `%`. */
|
|
79
|
+
const PROMPT_SP_RE = /\x1b\[7m.\x1b\[(?:0|27)m/g;
|
|
@@ -7,8 +7,22 @@ import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
|
7
7
|
import { loadExtensions } from "agent-sh/extension-loader";
|
|
8
8
|
import { activateAgent } from "agent-sh/agent";
|
|
9
9
|
import { getSettings } from "agent-sh/settings";
|
|
10
|
+
import { Shell } from "agent-sh/shell";
|
|
11
|
+
import type { Terminal } from "agent-sh/shell/terminal";
|
|
12
|
+
import activateShellContext from "agent-sh/shell/context";
|
|
10
13
|
import type { AppConfig } from "agent-sh/types";
|
|
11
14
|
|
|
15
|
+
/** No-op: ashi renders via pi-tui, the PTY only needs to exist. */
|
|
16
|
+
function headlessTerminal(): Terminal {
|
|
17
|
+
return {
|
|
18
|
+
write() {},
|
|
19
|
+
onInput: () => () => {},
|
|
20
|
+
onResize: () => () => {},
|
|
21
|
+
cols: () => 100,
|
|
22
|
+
rows: () => 30,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
import { mountAshi } from "./frontend.js";
|
|
13
27
|
import { MultiSessionStore } from "./multi-session-store.js";
|
|
14
28
|
import { registerForkCommands, applyBranchMessages } from "./commands.js";
|
|
@@ -104,7 +118,6 @@ async function main(): Promise<void> {
|
|
|
104
118
|
process.exit(1);
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
// ── Pi-tui frontend
|
|
108
121
|
const config = parseArgs(rawArgs);
|
|
109
122
|
|
|
110
123
|
if (!process.stdin.isTTY) {
|
|
@@ -125,11 +138,13 @@ async function main(): Promise<void> {
|
|
|
125
138
|
|
|
126
139
|
let stopFrontend: (() => void) | null = null;
|
|
127
140
|
|
|
141
|
+
let shellRef: { kill(): void } | null = null;
|
|
128
142
|
const cleanup = (): void => {
|
|
129
|
-
try { stopFrontend?.(); } catch {
|
|
130
|
-
try {
|
|
143
|
+
try { stopFrontend?.(); } catch {}
|
|
144
|
+
try { shellRef?.kill(); } catch {}
|
|
145
|
+
try { core.kill(); } catch {}
|
|
131
146
|
if (process.stdin.isTTY) {
|
|
132
|
-
try { process.stdin.setRawMode(false); } catch {
|
|
147
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
133
148
|
}
|
|
134
149
|
process.exit(0);
|
|
135
150
|
};
|
|
@@ -137,8 +152,21 @@ async function main(): Promise<void> {
|
|
|
137
152
|
const ctx = core.extensionContext({ quit: cleanup });
|
|
138
153
|
|
|
139
154
|
activateAgent(ctx);
|
|
155
|
+
activateShellContext(ctx);
|
|
140
156
|
await loadBuiltinExtensions(ctx);
|
|
141
157
|
|
|
158
|
+
const shell = new Shell({
|
|
159
|
+
bus: core.bus,
|
|
160
|
+
handlers: { define: ctx.define, call: ctx.call },
|
|
161
|
+
cols: 100,
|
|
162
|
+
rows: 30,
|
|
163
|
+
shell: process.env.SHELL ?? "/bin/bash",
|
|
164
|
+
cwd: process.cwd(),
|
|
165
|
+
instanceId: ctx.instanceId,
|
|
166
|
+
terminal: headlessTerminal(),
|
|
167
|
+
});
|
|
168
|
+
shellRef = shell;
|
|
169
|
+
|
|
142
170
|
const loaded = await loadExtensions(ctx, config.extensions);
|
|
143
171
|
core.bus.emit("core:extensions-loaded", { names: loaded });
|
|
144
172
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "agent-sh/types";
|
|
2
2
|
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
3
|
-
import type { AgentMessage } from "./session-store.js";
|
|
4
3
|
import type { Capture } from "./capture.js";
|
|
5
4
|
|
|
6
5
|
export function registerForkCommands(
|
|
@@ -12,14 +11,13 @@ export function registerForkCommands(
|
|
|
12
11
|
): void {
|
|
13
12
|
const { bus } = ctx;
|
|
14
13
|
|
|
15
|
-
ctx.registerCommand("fork", "
|
|
14
|
+
ctx.registerCommand("fork", "Pick a past user message to edit, or a branch tip to switch to", async (args) => {
|
|
16
15
|
const arg = args.trim();
|
|
17
16
|
if (arg === "") {
|
|
18
17
|
await openTreePicker();
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
|
-
const
|
|
22
|
-
const matches = branch.filter((e) => e.id.startsWith(arg));
|
|
20
|
+
const matches = getStore().current().getAllEntries().filter((e) => e.id.startsWith(arg));
|
|
23
21
|
if (matches.length === 0) {
|
|
24
22
|
bus.emit("ui:error", { message: `fork: no entry matches "${arg}"` });
|
|
25
23
|
return;
|
|
@@ -34,22 +32,6 @@ export function registerForkCommands(
|
|
|
34
32
|
bus.emit("ui:info", { message: `fork: rewound to ${target.id}` });
|
|
35
33
|
await rebuildChat();
|
|
36
34
|
});
|
|
37
|
-
|
|
38
|
-
ctx.registerCommand("branch", "Show the active branch (root → leaf)", async () => {
|
|
39
|
-
const branch = getStore().current().getBranch();
|
|
40
|
-
if (branch.length === 0) {
|
|
41
|
-
bus.emit("ui:info", { message: "branch: empty" });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const lines = branch.map((e) => {
|
|
45
|
-
if (e.type === "session") return `[${e.id}] session start (${e.cwd})`;
|
|
46
|
-
if (e.type === "compaction") return `[${e.id}] compaction (firstKept=${e.firstKeptId})`;
|
|
47
|
-
const msg = (e as { message: AgentMessage }).message;
|
|
48
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
49
|
-
return `[${e.id}] ${msg.role}: ${text.slice(0, 60)}`;
|
|
50
|
-
});
|
|
51
|
-
bus.emit("ui:info", { message: `branch (${branch.length} entries):\n${lines.join("\n")}` });
|
|
52
|
-
});
|
|
53
35
|
}
|
|
54
36
|
|
|
55
37
|
export function applyBranchMessages(
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// Default schema-style renderers shipped with ashi. Each
|
|
2
|
-
//
|
|
3
|
-
// "@guanyilun/ashi/render" surface, proving the schema covers ashi's own
|
|
4
|
-
// variety.
|
|
1
|
+
// Default schema-style renderers shipped with ashi. Each uses only the public
|
|
2
|
+
// "@guanyilun/ashi/render" surface — they could equally well live externally.
|
|
5
3
|
|
|
6
4
|
import type { ExtensionContext } from "agent-sh/types";
|
|
7
|
-
import type { RenderModel, Segment, ToolDisplay, TitleIcon } from "./schema.js";
|
|
5
|
+
import type { RenderModel, Segment, ToolDisplay, TitleIcon, Color } from "./schema.js";
|
|
8
6
|
|
|
9
7
|
function parseRaw(raw: unknown): Record<string, unknown> {
|
|
10
8
|
if (typeof raw === "string") {
|
|
@@ -37,9 +35,6 @@ const accentSeg = (text: string): Segment => ({ text, style: { color: "accent" }
|
|
|
37
35
|
const mutedSeg = (text: string): Segment => ({ text, style: { color: "muted" } });
|
|
38
36
|
const warnSeg = (text: string): Segment => ({ text, style: { color: "warning" } });
|
|
39
37
|
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// bash — full command toggle on Ctrl+O, syntax-highlighted, streaming output.
|
|
42
|
-
|
|
43
38
|
interface BashInit { command: string; timeout?: number }
|
|
44
39
|
|
|
45
40
|
const bashModel: RenderModel<BashInit> = {
|
|
@@ -62,8 +57,27 @@ const bashModel: RenderModel<BashInit> = {
|
|
|
62
57
|
},
|
|
63
58
|
};
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
/** User-typed `!` shell commands. `▸` mirrors the status-footer glyph; the
|
|
61
|
+
* right-aligned tag disambiguates private vs public on scrollback. */
|
|
62
|
+
function makeUserBashModel(opts: { private: boolean }): RenderModel<BashInit> {
|
|
63
|
+
const color: Color = opts.private ? "bashModePrivate" : "bashMode";
|
|
64
|
+
const prefixSeg: Segment = { text: "▸ ", style: { bold: true, color } };
|
|
65
|
+
const tagText = opts.private ? "shell · private" : "shell";
|
|
66
|
+
const tagSeg: Segment = { text: tagText, style: { color, dim: true } };
|
|
67
|
+
return {
|
|
68
|
+
initial: ({ rawInput }) => {
|
|
69
|
+
const r = parseRaw(rawInput);
|
|
70
|
+
return { command: str(r.command) ?? "…", timeout: num(r.timeout) };
|
|
71
|
+
},
|
|
72
|
+
view: (s, env): ToolDisplay => ({
|
|
73
|
+
title: [prefixSeg, { text: env.expanded ? s.command : compact(s.command), highlight: "bash" }],
|
|
74
|
+
titleRight: [tagSeg],
|
|
75
|
+
status: s.status,
|
|
76
|
+
body: { kind: "stream", text: s.output },
|
|
77
|
+
expandable: true,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
67
81
|
|
|
68
82
|
interface ReadInit { path: string; range?: string }
|
|
69
83
|
|
|
@@ -94,9 +108,6 @@ const readModel: RenderModel<ReadInit> = {
|
|
|
94
108
|
}),
|
|
95
109
|
};
|
|
96
110
|
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// grep / glob / ls — pattern + scope.
|
|
99
|
-
|
|
100
111
|
interface GrepInit { pattern: string; scope: string; extras: string }
|
|
101
112
|
|
|
102
113
|
const grepModel: RenderModel<GrepInit> = {
|
|
@@ -159,11 +170,6 @@ const lsModel: RenderModel<LsInit> = {
|
|
|
159
170
|
}),
|
|
160
171
|
};
|
|
161
172
|
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// edit_file / write_file — path + framework-supplied diff body. The "Edited
|
|
164
|
-
// /path (+N -M)" streaming text is suppressed because the diff body already
|
|
165
|
-
// shows that information; per-line output reappears via expand on Ctrl+O.
|
|
166
|
-
|
|
167
173
|
interface EditInit { path: string; verb: string }
|
|
168
174
|
|
|
169
175
|
function editLikeModel(verb: string): RenderModel<EditInit> {
|
|
@@ -177,8 +183,8 @@ function editLikeModel(verb: string): RenderModel<EditInit> {
|
|
|
177
183
|
titleIcon: "edit",
|
|
178
184
|
title: [nameSeg(`${s.verb} `), accentSeg(s.path)],
|
|
179
185
|
status: s.status,
|
|
180
|
-
// Collapsed
|
|
181
|
-
//
|
|
186
|
+
// Collapsed shows just the diff (the "Edited /path (+N -M)" stream
|
|
187
|
+
// line would only restate the call); expand adds the stream output.
|
|
182
188
|
body: s.hasDiff
|
|
183
189
|
? (env.expanded
|
|
184
190
|
? { kind: "compound", parts: [{ kind: "diff" }, { kind: "stream", text: s.output }] }
|
|
@@ -189,9 +195,6 @@ function editLikeModel(verb: string): RenderModel<EditInit> {
|
|
|
189
195
|
};
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
// default — fallback for any tool without a specific renderer.
|
|
194
|
-
|
|
195
198
|
interface DefaultInit { title: string; detail?: string; icon: TitleIcon }
|
|
196
199
|
|
|
197
200
|
const defaultModel: RenderModel<DefaultInit> = {
|
|
@@ -212,10 +215,10 @@ const defaultModel: RenderModel<DefaultInit> = {
|
|
|
212
215
|
}),
|
|
213
216
|
};
|
|
214
217
|
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
|
|
217
218
|
export function registerDefaultSchemaRenderers(ctx: ExtensionContext): void {
|
|
218
219
|
ctx.define("ashi:render-tool:bash", () => bashModel);
|
|
220
|
+
ctx.define("ashi:render-tool:user_bash", () => makeUserBashModel({ private: false }));
|
|
221
|
+
ctx.define("ashi:render-tool:user_bash_private", () => makeUserBashModel({ private: true }));
|
|
219
222
|
ctx.define("ashi:render-tool:read_file", () => readModel);
|
|
220
223
|
ctx.define("ashi:render-tool:read", () => readModel);
|
|
221
224
|
ctx.define("ashi:render-tool:grep", () => grepModel);
|