agent-sh 0.14.2 → 0.14.4
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 +3 -0
- package/dist/shell/output-parser.d.ts +11 -22
- package/dist/shell/output-parser.js +20 -37
- 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
|
@@ -8,6 +8,7 @@ declare module "../core/event-bus.js" {
|
|
|
8
8
|
"shell:command-done": {
|
|
9
9
|
command: string;
|
|
10
10
|
output: string;
|
|
11
|
+
outputRaw: string;
|
|
11
12
|
cwd: string;
|
|
12
13
|
exitCode: number | null;
|
|
13
14
|
};
|
|
@@ -19,6 +20,8 @@ declare module "../core/event-bus.js" {
|
|
|
19
20
|
};
|
|
20
21
|
"shell:agent-exec-start": Record<string, never>;
|
|
21
22
|
"shell:agent-exec-done": Record<string, never>;
|
|
23
|
+
/** Mark the next user-emitted shell command as excluded from <shell_events>. */
|
|
24
|
+
"shell:user-exec-exclude-next": Record<string, never>;
|
|
22
25
|
"shell:pty-data": {
|
|
23
26
|
raw: string;
|
|
24
27
|
};
|
|
@@ -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,11 +94,13 @@ export class OutputParser {
|
|
|
108
94
|
this.bus.emit("shell:foreground-busy", { busy: false });
|
|
109
95
|
}
|
|
110
96
|
if (this.lastCommand) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
97
|
+
const raw = this.cleanOutput(this.currentOutputCapture);
|
|
98
|
+
const output = this.removeEchoedCommand(stripAnsi(raw).trim(), this.lastCommand);
|
|
99
|
+
const outputRaw = this.removeEchoedCommand(raw.trim(), this.lastCommand);
|
|
113
100
|
this.bus.emit("shell:command-done", {
|
|
114
101
|
command: this.lastCommand,
|
|
115
|
-
output
|
|
102
|
+
output,
|
|
103
|
+
outputRaw,
|
|
116
104
|
cwd: this.cwd,
|
|
117
105
|
exitCode: null,
|
|
118
106
|
});
|
|
@@ -121,21 +109,16 @@ export class OutputParser {
|
|
|
121
109
|
this.currentOutputCapture = "";
|
|
122
110
|
}
|
|
123
111
|
else {
|
|
124
|
-
// Cap
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// command-done context.
|
|
128
|
-
const MAX_CAPTURE = 128 * 1024; // 128 KB
|
|
112
|
+
// Cap to the tail so a long-running foreground program (tmux, vim)
|
|
113
|
+
// emitting output without prompt markers can't grow this unboundedly.
|
|
114
|
+
const MAX_CAPTURE = 128 * 1024;
|
|
129
115
|
this.currentOutputCapture += data;
|
|
130
116
|
if (this.currentOutputCapture.length > MAX_CAPTURE) {
|
|
131
117
|
this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
|
|
132
118
|
}
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
|
|
137
|
-
* and the shell is ready for input.
|
|
138
|
-
*/
|
|
121
|
+
/** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
|
|
139
122
|
parsePromptEnd(data) {
|
|
140
123
|
const match = READY_RE.exec(data);
|
|
141
124
|
if (!match)
|
|
@@ -146,7 +129,7 @@ export class OutputParser {
|
|
|
146
129
|
}
|
|
147
130
|
removeEchoedCommand(output, command) {
|
|
148
131
|
const lines = output.split("\n");
|
|
149
|
-
if (lines.length > 0 && lines[0].includes(command.slice(0, 20))) {
|
|
132
|
+
if (lines.length > 0 && stripAnsi(lines[0]).includes(command.slice(0, 20))) {
|
|
150
133
|
return lines.slice(1).join("\n").trim();
|
|
151
134
|
}
|
|
152
135
|
return output;
|
|
@@ -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);
|