agent-sh 0.4.0 → 0.5.0

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.
Files changed (76) hide show
  1. package/README.md +66 -113
  2. package/dist/agent/agent-loop.d.ts +85 -0
  3. package/dist/agent/agent-loop.js +611 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +117 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +98 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +62 -0
  18. package/dist/agent/tools/edit-file.d.ts +2 -0
  19. package/dist/agent/tools/edit-file.js +95 -0
  20. package/dist/agent/tools/glob.d.ts +2 -0
  21. package/dist/agent/tools/glob.js +55 -0
  22. package/dist/agent/tools/grep.d.ts +2 -0
  23. package/dist/agent/tools/grep.js +77 -0
  24. package/dist/agent/tools/list-skills.d.ts +2 -0
  25. package/dist/agent/tools/list-skills.js +28 -0
  26. package/dist/agent/tools/ls.d.ts +2 -0
  27. package/dist/agent/tools/ls.js +43 -0
  28. package/dist/agent/tools/read-file.d.ts +2 -0
  29. package/dist/agent/tools/read-file.js +55 -0
  30. package/dist/agent/tools/user-shell.d.ts +13 -0
  31. package/dist/agent/tools/user-shell.js +57 -0
  32. package/dist/agent/tools/write-file.d.ts +2 -0
  33. package/dist/agent/tools/write-file.js +74 -0
  34. package/dist/agent/types.d.ts +44 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/core.d.ts +24 -14
  37. package/dist/core.js +260 -36
  38. package/dist/event-bus.d.ts +80 -14
  39. package/dist/event-bus.js +10 -1
  40. package/dist/extension-loader.js +12 -1
  41. package/dist/extensions/command-suggest.d.ts +10 -0
  42. package/dist/extensions/command-suggest.js +41 -0
  43. package/dist/extensions/slash-commands.d.ts +1 -1
  44. package/dist/extensions/slash-commands.js +161 -64
  45. package/dist/extensions/tui-renderer.js +90 -48
  46. package/dist/index.js +98 -122
  47. package/dist/input-handler.js +74 -7
  48. package/dist/output-parser.d.ts +7 -0
  49. package/dist/output-parser.js +27 -0
  50. package/dist/settings.d.ts +53 -2
  51. package/dist/settings.js +45 -2
  52. package/dist/shell.js +33 -26
  53. package/dist/types.d.ts +33 -6
  54. package/dist/utils/box-frame.d.ts +3 -1
  55. package/dist/utils/box-frame.js +12 -5
  56. package/dist/utils/llm-client.d.ts +45 -0
  57. package/dist/utils/llm-client.js +60 -0
  58. package/dist/utils/markdown.js +2 -2
  59. package/dist/utils/stream-transform.js +20 -47
  60. package/dist/utils/tool-display.js +15 -5
  61. package/examples/extensions/claude-code-bridge/README.md +35 -0
  62. package/examples/extensions/claude-code-bridge/index.ts +198 -0
  63. package/examples/extensions/claude-code-bridge/package.json +11 -0
  64. package/examples/extensions/openrouter.ts +87 -0
  65. package/examples/extensions/pi-bridge/README.md +35 -0
  66. package/examples/extensions/pi-bridge/index.ts +265 -0
  67. package/examples/extensions/pi-bridge/package.json +13 -0
  68. package/examples/extensions/subagents.ts +87 -0
  69. package/package.json +3 -5
  70. package/dist/acp-client.d.ts +0 -105
  71. package/dist/acp-client.js +0 -684
  72. package/dist/extensions/shell-exec.d.ts +0 -24
  73. package/dist/extensions/shell-exec.js +0 -188
  74. package/dist/mcp-server.d.ts +0 -13
  75. package/dist/mcp-server.js +0 -234
  76. package/examples/pi-agent-sh.ts +0 -166
@@ -2,90 +2,187 @@
2
2
  * Slash commands extension.
3
3
  *
4
4
  * Registers built-in slash commands on the event bus:
5
+ * - Listens for "command:register" to accept commands from extensions
5
6
  * - Responds to "autocomplete:request" pipe for /-prefixed completions
6
7
  * - Handles "command:execute" events and dispatches to matching handler
7
8
  * - Uses "ui:info"/"ui:error" for user feedback (no direct TUI dependency)
9
+ *
10
+ * Argument completion is composable: any extension can onPipe("autocomplete:request")
11
+ * and check payload.command / payload.commandArgs to add completions for any command.
8
12
  */
9
- import { execSync } from "node:child_process";
10
13
  import { palette as p } from "../utils/palette.js";
11
- export default function activate({ bus, getAcpClient, quit }) {
12
- const commands = [
13
- {
14
- name: "/help",
15
- description: "Show available commands",
16
- handler: () => {
17
- const lines = commands.map((c) => ` ${p.accent}${c.name.padEnd(12)}${p.reset} ${c.description}`);
18
- bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
19
- },
20
- },
21
- {
22
- name: "/clear",
23
- description: "Start a new agent session",
24
- handler: async () => {
25
- try {
26
- await getAcpClient().resetSession();
27
- bus.emit("ui:info", { message: "Session cleared." });
28
- }
29
- catch (err) {
30
- bus.emit("ui:error", {
31
- message: `Failed to reset session: ${err instanceof Error ? err.message : String(err)}`,
32
- });
33
- }
34
- },
14
+ import { discoverSkills, loadSkillContent } from "../agent/skills.js";
15
+ export default function activate({ bus, contextManager }) {
16
+ const commands = new Map();
17
+ const register = (cmd) => {
18
+ const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
19
+ commands.set(name, { ...cmd, name });
20
+ };
21
+ // ── Built-in commands ─────────────────────────────────────────
22
+ register({
23
+ name: "/help",
24
+ description: "Show available commands",
25
+ handler: () => {
26
+ const maxLen = Math.max(...[...commands.values()].map(c => c.name.length));
27
+ const pad = maxLen + 2;
28
+ const lines = [...commands.values()].map((c) => ` ${p.accent}${c.name.padEnd(pad)}${p.reset} ${c.description}`);
29
+ bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
35
30
  },
36
- {
37
- name: "/copy",
38
- description: "Copy last agent response to clipboard",
39
- handler: () => {
40
- const text = getAcpClient().getLastResponseText();
41
- if (!text) {
42
- bus.emit("ui:info", { message: "No agent response to copy." });
43
- return;
44
- }
45
- try {
46
- if (process.platform === "darwin") {
47
- execSync("pbcopy", { input: text });
48
- }
49
- else {
50
- execSync("xclip -selection clipboard", { input: text });
51
- }
52
- bus.emit("ui:info", { message: "Copied to clipboard." });
53
- }
54
- catch {
55
- bus.emit("ui:error", { message: "Failed to copy to clipboard." });
56
- }
57
- },
31
+ });
32
+ register({
33
+ name: "/model",
34
+ description: "Cycle to next model, or switch to a specific one",
35
+ handler: (args) => {
36
+ const name = args.trim();
37
+ if (!name) {
38
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
39
+ const current = models.find((m) => m.model === active);
40
+ const label = current
41
+ ? `${current.model}${current.provider ? ` [${current.provider}]` : ""}`
42
+ : active ?? "none";
43
+ bus.emit("ui:info", { message: `Model: ${label}` });
44
+ }
45
+ else {
46
+ bus.emit("config:switch-model", { model: name });
47
+ }
58
48
  },
59
- {
60
- name: "/compact",
61
- description: "Ask agent to summarize the conversation",
62
- handler: async () => {
63
- await getAcpClient().sendPrompt("Please provide a concise summary of our conversation so far and the current state of the work.");
64
- },
49
+ });
50
+ register({
51
+ name: "/thinking",
52
+ description: "Set thinking/reasoning effort level",
53
+ handler: (args) => {
54
+ const level = args.trim();
55
+ if (!level) {
56
+ const { level: current, levels, supported } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
57
+ const status = supported ? current : `${current} (not supported by current model)`;
58
+ bus.emit("ui:info", { message: `Thinking: ${status} (options: ${levels.join(", ")})` });
59
+ }
60
+ else {
61
+ bus.emit("config:set-thinking", { level });
62
+ }
65
63
  },
66
- {
67
- name: "/quit",
68
- description: "Exit agent-sh",
69
- handler: () => {
70
- quit();
71
- },
64
+ });
65
+ register({
66
+ name: "/backend",
67
+ description: "List or switch agent backend",
68
+ handler: (args) => {
69
+ const name = args.trim();
70
+ if (!name) {
71
+ bus.emit("config:list-backends", {});
72
+ }
73
+ else {
74
+ bus.emit("config:switch-backend", { name });
75
+ }
72
76
  },
73
- ];
74
- // Provide command completions for /-prefixed input
77
+ });
78
+ // ── Extension registration ────────────────────────────────────
79
+ bus.on("command:register", (cmd) => {
80
+ register(cmd);
81
+ });
82
+ // ── Skill commands (/skill:<name>) ────────────────────────────
83
+ const getSkills = () => {
84
+ const cwd = contextManager?.getCwd() ?? process.cwd();
85
+ return discoverSkills(cwd);
86
+ };
87
+ const handleSkillCommand = (skillName, args) => {
88
+ const skills = getSkills();
89
+ const skill = skills.find(s => s.name === skillName);
90
+ if (!skill) {
91
+ bus.emit("ui:error", { message: `Unknown skill: ${skillName}` });
92
+ return;
93
+ }
94
+ const content = loadSkillContent(skill);
95
+ if (!content) {
96
+ bus.emit("ui:error", { message: `Failed to load skill: ${skillName}` });
97
+ return;
98
+ }
99
+ const query = args.trim()
100
+ ? `${content}\n\n${args.trim()}`
101
+ : content;
102
+ bus.emit("agent:submit", { query });
103
+ };
104
+ // ── Autocomplete: command names ───────────────────────────────
75
105
  bus.onPipe("autocomplete:request", (payload) => {
76
106
  if (!payload.buffer.startsWith("/"))
77
107
  return payload;
108
+ // Argument completion is handled by separate pipe handlers below
109
+ if (payload.command)
110
+ return payload;
78
111
  const prefix = payload.buffer.toLowerCase();
79
- const matching = commands
112
+ const matching = [...commands.values()]
80
113
  .filter((c) => c.name.toLowerCase().startsWith(prefix))
81
114
  .map((c) => ({ name: c.name, description: c.description }));
115
+ // Skill commands
116
+ if (prefix.startsWith("/skill:") || "/skill:".startsWith(prefix)) {
117
+ const skills = getSkills();
118
+ for (const skill of skills) {
119
+ const name = `/skill:${skill.name}`;
120
+ if (name.toLowerCase().startsWith(prefix)) {
121
+ matching.push({ name, description: skill.description });
122
+ }
123
+ }
124
+ }
82
125
  if (matching.length === 0)
83
126
  return payload;
84
127
  return { ...payload, items: [...payload.items, ...matching] };
85
128
  });
86
- // Handle command execution
129
+ // ── Autocomplete: /model arguments ─────────────────────────────
130
+ bus.onPipe("autocomplete:request", (payload) => {
131
+ if (payload.command !== "/model")
132
+ return payload;
133
+ const partial = (payload.commandArgs ?? "").toLowerCase();
134
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
135
+ const items = models
136
+ .filter((m) => m.model.toLowerCase().includes(partial))
137
+ .slice(0, 15)
138
+ .map((m) => ({
139
+ name: `/model ${m.model}`,
140
+ description: `${m.provider ? `[${m.provider}]` : ""}${m.model === active ? " (active)" : ""}`,
141
+ }));
142
+ if (items.length === 0)
143
+ return payload;
144
+ return { ...payload, items: [...payload.items, ...items] };
145
+ });
146
+ // ── Autocomplete: /thinking arguments ─────────────────────────
147
+ bus.onPipe("autocomplete:request", (payload) => {
148
+ if (payload.command !== "/thinking")
149
+ return payload;
150
+ const partial = (payload.commandArgs ?? "").toLowerCase();
151
+ const { level: current, levels } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
152
+ const items = levels
153
+ .filter((l) => l.startsWith(partial))
154
+ .map((l) => ({
155
+ name: `/thinking ${l}`,
156
+ description: l === current ? "(active)" : "",
157
+ }));
158
+ if (items.length === 0)
159
+ return payload;
160
+ return { ...payload, items: [...payload.items, ...items] };
161
+ });
162
+ // ── Autocomplete: /backend arguments ──────────────────────────
163
+ bus.onPipe("autocomplete:request", (payload) => {
164
+ if (payload.command !== "/backend")
165
+ return payload;
166
+ const partial = (payload.commandArgs ?? "").toLowerCase();
167
+ const { names, active } = bus.emitPipe("config:get-backends", { names: [], active: null });
168
+ const items = names
169
+ .filter((n) => n.toLowerCase().startsWith(partial))
170
+ .map((n) => ({
171
+ name: `/backend ${n}`,
172
+ description: n === active ? "(active)" : "",
173
+ }));
174
+ if (items.length === 0)
175
+ return payload;
176
+ return { ...payload, items: [...payload.items, ...items] };
177
+ });
178
+ // ── Dispatch ──────────────────────────────────────────────────
87
179
  bus.on("command:execute", (e) => {
88
- const cmd = commands.find((c) => c.name === e.name);
180
+ if (e.name.startsWith("/skill:")) {
181
+ const skillName = e.name.slice("/skill:".length);
182
+ handleSkillCommand(skillName, e.args);
183
+ return;
184
+ }
185
+ const cmd = commands.get(e.name);
89
186
  if (cmd) {
90
187
  const result = cmd.handler(e.args);
91
188
  if (result instanceof Promise) {
@@ -47,7 +47,6 @@ function createRenderState() {
47
47
  spinnerOpts: {},
48
48
  spinnerInterval: null,
49
49
  spinnerStartTime: 0,
50
- lastCommand: "",
51
50
  toolLineOpen: false,
52
51
  currentToolKind: undefined,
53
52
  commandOutputBuffer: "",
@@ -60,9 +59,12 @@ function createRenderState() {
60
59
  };
61
60
  }
62
61
  export default function activate(ctx) {
63
- const { bus, getAcpClient, define } = ctx;
62
+ const { bus, llmClient, define } = ctx;
64
63
  const writer = new StdoutWriter();
65
64
  const s = createRenderState();
65
+ // Track backend/model info for display on response border
66
+ let backendInfo = null;
67
+ bus.on("agent:info", (info) => { backendInfo = info; });
66
68
  // ── Register fenced block transform (code blocks → ContentBlock) ──
67
69
  // Nobody is special — tui-renderer uses the same primitive as any extension.
68
70
  const fencedTransform = createFencedBlockTransform(bus, {
@@ -104,46 +106,54 @@ export default function activate(ctx) {
104
106
  }
105
107
  });
106
108
  bus.on("agent:response-chunk", (e) => {
107
- if (e.blocks) {
108
- // Inject spacing: append \n to text blocks that precede non-text blocks
109
- const blocks = e.blocks;
110
- for (let i = 0; i < blocks.length; i++) {
111
- const block = blocks[i];
112
- const next = blocks[i + 1];
113
- if (block.type === "text" && next && next.type !== "text") {
114
- block.text += "\n";
115
- }
116
- }
117
- for (const block of blocks) {
118
- switch (block.type) {
119
- case "text":
120
- if (block.text)
121
- writeAgentText(block.text);
122
- break;
123
- case "code-block":
124
- writeCodeBlock(block.language, block.code);
125
- break;
126
- case "image":
127
- writeInlineImage(block.data);
128
- break;
129
- case "raw":
130
- flushForRaw();
131
- writer.write(block.escape);
132
- break;
133
- }
109
+ const { blocks } = e;
110
+ // Inject spacing: append \n to text blocks that precede non-text blocks
111
+ for (let i = 0; i < blocks.length; i++) {
112
+ const block = blocks[i];
113
+ const next = blocks[i + 1];
114
+ if (block.type === "text" && next && next.type !== "text") {
115
+ block.text += "\n";
134
116
  }
135
117
  }
136
- else {
137
- writeAgentText(e.text);
118
+ for (const block of blocks) {
119
+ switch (block.type) {
120
+ case "text":
121
+ if (block.text)
122
+ writeAgentText(block.text);
123
+ break;
124
+ case "code-block":
125
+ writeCodeBlock(block.language, block.code);
126
+ break;
127
+ case "image":
128
+ writeInlineImage(block.data);
129
+ break;
130
+ case "raw":
131
+ flushForRaw();
132
+ writer.write(block.escape);
133
+ break;
134
+ }
138
135
  }
139
136
  });
137
+ // Track token usage for display
138
+ let pendingUsage = null;
139
+ bus.on("agent:usage", (e) => { pendingUsage = e; });
140
140
  bus.on("agent:response-done", () => {
141
141
  s.isThinking = false;
142
+ if (pendingUsage && s.renderer) {
143
+ const { prompt_tokens, completion_tokens } = pendingUsage;
144
+ const maxTokens = backendInfo?.contextWindow ?? 128_000;
145
+ // prompt_tokens of the latest call = current context usage
146
+ // (it includes the full conversation history)
147
+ const ctxK = (prompt_tokens / 1000).toFixed(1);
148
+ const maxK = (maxTokens / 1000).toFixed(0);
149
+ const pct = Math.min(100, (prompt_tokens / maxTokens) * 100).toFixed(0);
150
+ s.renderer.writeLine("");
151
+ s.renderer.writeLine(`${p.dim}⬆ ${prompt_tokens} ⬇ ${completion_tokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`);
152
+ drain();
153
+ pendingUsage = null;
154
+ }
142
155
  endAgentResponse();
143
156
  });
144
- bus.on("agent:tool-call", (e) => {
145
- s.lastCommand = e.tool;
146
- });
147
157
  bus.on("agent:tool-started", (e) => {
148
158
  fencedTransform.flush();
149
159
  stopCurrentSpinner();
@@ -159,9 +169,8 @@ export default function activate(ctx) {
159
169
  s.hadToolCalls = true;
160
170
  }
161
171
  else {
162
- showToolCall(e.title, s.lastCommand, e);
172
+ showToolCall(e.title, "", e);
163
173
  }
164
- s.lastCommand = "";
165
174
  });
166
175
  bus.on("agent:tool-completed", (e) => {
167
176
  showToolComplete(e.exitCode);
@@ -182,7 +191,15 @@ export default function activate(ctx) {
182
191
  stopCurrentSpinner();
183
192
  endAgentResponse();
184
193
  });
185
- bus.on("agent:error", (e) => showError(e.message));
194
+ bus.on("agent:error", (e) => {
195
+ stopCurrentSpinner();
196
+ showCollapsedThinking();
197
+ if (!s.renderer)
198
+ startAgentResponse();
199
+ s.renderer.writeLine(`${p.error}Error: ${e.message}${p.reset}`);
200
+ s.renderer.writeLine("");
201
+ drain();
202
+ });
186
203
  bus.on("permission:request", (e) => {
187
204
  stopCurrentSpinner();
188
205
  flushCommandOutput();
@@ -191,11 +208,12 @@ export default function activate(ctx) {
191
208
  drain();
192
209
  }
193
210
  if (e.kind === "file-write" && e.metadata?.diff) {
211
+ showCollapsedThinking();
194
212
  showFileDiff(e.title, e.metadata.diff);
195
213
  }
196
- else {
197
- endAgentResponse();
198
- }
214
+ // Don't endAgentResponse() here — permission requests that aren't
215
+ // file-write diffs are handled inline (auto-approved or by extensions).
216
+ // Closing the response prematurely causes double separator borders.
199
217
  });
200
218
  bus.on("input:keypress", (e) => {
201
219
  if (e.key === "\x0f")
@@ -203,8 +221,17 @@ export default function activate(ctx) {
203
221
  if (e.key === "\x14")
204
222
  toggleThinkingDisplay(); // Ctrl+T
205
223
  });
206
- bus.on("ui:info", (e) => showInfo(e.message));
224
+ bus.on("ui:info", (e) => {
225
+ stopCurrentSpinner();
226
+ showInfo(e.message);
227
+ // Restart spinner if agent is still processing
228
+ if (s.renderer)
229
+ startThinkingSpinner();
230
+ });
207
231
  bus.on("ui:error", (e) => showError(e.message));
232
+ bus.on("ui:suggestion", (e) => {
233
+ writer.write(`${p.dim}💡 ${e.text}${p.reset}\n`);
234
+ });
208
235
  // ── Rendering functions ─────────────────────────────────────
209
236
  function drain() {
210
237
  if (!s.renderer)
@@ -224,6 +251,7 @@ export default function activate(ctx) {
224
251
  if (!s.renderer)
225
252
  startAgentResponse();
226
253
  s.renderer.writeLine(`${p.muted}… thinking${p.reset}`);
254
+ s.renderer.writeLine("");
227
255
  s.thinkingPending = false;
228
256
  }
229
257
  }
@@ -234,11 +262,12 @@ export default function activate(ctx) {
234
262
  s.renderer.flush();
235
263
  s.renderer.printBottomBorder();
236
264
  drain();
265
+ writer.write("\n");
237
266
  s.renderer = null;
238
267
  }
239
268
  }
240
269
  function showUserQuery(query, modeLabel) {
241
- const boxW = Math.min(84, writer.columns);
270
+ const boxW = writer.columns;
242
271
  const contentW = boxW - 4;
243
272
  const lines = [];
244
273
  for (const raw of query.split("\n")) {
@@ -264,11 +293,25 @@ export default function activate(ctx) {
264
293
  const title = modeLabel
265
294
  ? `${borderColor}${p.bold} ${modeLabel} ${p.reset}`
266
295
  : `${p.accent}${p.bold}❯${p.reset}`;
296
+ // Backend/model label on the right (backend/model, highlighted)
297
+ const model = backendInfo?.model ?? llmClient?.model;
298
+ const backend = backendInfo?.name;
299
+ let modelLabel;
300
+ if (backend && model) {
301
+ modelLabel = `${p.dim}${backend}/${p.reset}${p.bold}${model}${p.reset}`;
302
+ }
303
+ else if (model) {
304
+ modelLabel = `${p.bold}${model}${p.reset}`;
305
+ }
306
+ else if (backend) {
307
+ modelLabel = `${p.bold}${backend}${p.reset}`;
308
+ }
267
309
  const framed = renderBoxFrame(lines, {
268
310
  width: boxW,
269
311
  style: "rounded",
270
312
  borderColor,
271
313
  title,
314
+ titleRight: modelLabel,
272
315
  });
273
316
  writer.write("\n");
274
317
  for (const line of framed) {
@@ -392,9 +435,10 @@ export default function activate(ctx) {
392
435
  drain();
393
436
  }
394
437
  }
438
+ // Thinking is always assumed available — the TUI renders thinking
439
+ // tokens whenever they arrive, regardless of backend.
395
440
  function hasThinkingMode() {
396
- const mode = getAcpClient().getCurrentMode();
397
- return !mode || mode.id !== "off";
441
+ return true;
398
442
  }
399
443
  function startThinkingSpinner() {
400
444
  if (!s.spinnerStartTime)
@@ -482,14 +526,13 @@ export default function activate(ctx) {
482
526
  function showFileDiff(filePath, diff) {
483
527
  if (diff.isIdentical)
484
528
  return;
485
- const boxW = Math.min(84, writer.columns);
529
+ const boxW = Math.min(120, writer.columns);
486
530
  const contentW = boxW - 4;
487
531
  const diffLines = renderDiff(diff, {
488
532
  width: contentW,
489
533
  filePath,
490
534
  maxLines: getSettings().diffMaxLines,
491
535
  trueColor: true,
492
- mode: "unified",
493
536
  });
494
537
  const lastLine = diffLines[diffLines.length - 1] ?? "";
495
538
  const isTruncated = lastLine.includes("… ");
@@ -552,14 +595,13 @@ export default function activate(ctx) {
552
595
  }
553
596
  function showFileDiffCached(entry) {
554
597
  const { filePath, diff } = entry;
555
- const boxW = Math.min(84, writer.columns);
598
+ const boxW = Math.min(120, writer.columns);
556
599
  const contentW = boxW - 4;
557
600
  const diffLines = renderDiff(diff, {
558
601
  width: contentW,
559
602
  filePath,
560
603
  maxLines: getSettings().diffMaxLines,
561
604
  trueColor: true,
562
- mode: "unified",
563
605
  });
564
606
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
565
607
  const framed = renderBoxFrame(body, {