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.
@@ -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
- * Parses PTY output to detect command boundaries, track cwd,
4
- * and emit shell events. Owns the command lifecycle state.
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
- * Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
25
- * This carries the actual command text from the shell more reliable than
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
- // ── Parsing ─────────────────────────────────────────────────
53
- /**
54
- * Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
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
- this.currentOutputCapture = ""; // discard echo accumulated before preexec
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 instance or untagged foreign emission treat as opaque
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 output = stripAnsi(this.currentOutputCapture).trim();
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 capture buffer to avoid unbounded growth when a foreground
125
- // program (tmux, vim, etc.) produces output without prompt markers.
126
- // Keep only the tail — the final output is what matters for
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
- * Tracks PTY commands and cwd, spills long outputs, contributes the
3
- * per-query `<cwd>` (always) and `<shell_events>` (when there are fresh
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 so the agent can `read_file` them
14
- // on demand instead of carrying the full text in LLM context.
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: agentShellActive ? "agent" : "user",
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
- // Override core's process.cwd() default with the PTY-tracked value.
52
+ bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
45
53
  ctx.advise("cwd", () => currentCwd);
46
- // Advises the core handler directly: shell-context loads before the
47
- // agent host attaches `ctx.agent`, so the sugar isn't available yet.
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 !== "agent");
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;
@@ -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 { /* ignore */ }
130
- try { core.kill(); } catch { /* ignore */ }
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 { /* ignore */ }
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", "Rewind and branch: /fork (interactive picker) or /fork <id-prefix>", async (args) => {
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 branch = getStore().current().getBranch();
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 model below could
2
- // equally well live in an external extension — they use only the public
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
- // read file path + optional offset:limit range.
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-with-diff: diff only (the "Edited /path (+N -M)" stream line
181
- // restates the call line). Expanded-with-diff: diff + stream output.
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);