pi-zentui 0.1.3 → 0.1.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/README.md CHANGED
@@ -84,6 +84,7 @@ On first run, Zentui creates a config file at:
84
84
 
85
85
  ```json
86
86
  {
87
+ "projectRefreshIntervalMs": 30000,
87
88
  "icons": {
88
89
  "cwd": "󰝰",
89
90
  "git": "",
@@ -109,18 +110,11 @@ On first run, Zentui creates a config file at:
109
110
  "tokens": "muted",
110
111
  "cost": "success",
111
112
  "separator": "borderMuted"
112
- },
113
- "tools": {
114
- "style": "compact"
115
113
  }
116
114
  }
117
115
  ```
118
116
 
119
- `tools.style` controls the initial built-in tool output style:
120
-
121
- - `compact` — one-line tool calls by default
122
- - `truncated` — expanded preview with long output truncated
123
- - `full` — expanded full output
117
+ `projectRefreshIntervalMs` controls how often Zentui refreshes project status (git/runtime) while Pi is idle. Set it to `0` to disable polling; invalid values or values below 5000 ms fall back to `30000`.
124
118
 
125
119
  ### Color values
126
120
 
@@ -3,9 +3,12 @@ import { join } from "node:path";
3
3
  import { getAgentDir } from "@mariozechner/pi-coding-agent";
4
4
 
5
5
  export type ColorSpec = string;
6
- export type ToolOutputStyle = "compact" | "truncated" | "full";
6
+
7
+ const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
8
+ const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
7
9
 
8
10
  export type PolishedTuiConfig = {
11
+ projectRefreshIntervalMs: number;
9
12
  icons: {
10
13
  cwd: string;
11
14
  git: string;
@@ -32,9 +35,6 @@ export type PolishedTuiConfig = {
32
35
  cost: ColorSpec;
33
36
  separator: ColorSpec;
34
37
  };
35
- tools: {
36
- style: ToolOutputStyle;
37
- };
38
38
  };
39
39
 
40
40
  export const configPath = join(getAgentDir(), "zentui.json");
@@ -88,6 +88,7 @@ const themeColorTokens = new Set([
88
88
  ]);
89
89
 
90
90
  export const defaultConfig: PolishedTuiConfig = {
91
+ projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
91
92
  icons: {
92
93
  cwd: "󰝰",
93
94
  git: "",
@@ -114,9 +115,6 @@ export const defaultConfig: PolishedTuiConfig = {
114
115
  cost: "success",
115
116
  separator: "borderMuted",
116
117
  },
117
- tools: {
118
- style: "compact",
119
- },
120
118
  };
121
119
 
122
120
  function isHexColor(value: string): boolean {
@@ -141,10 +139,16 @@ function isRecord(value: unknown): value is ConfigRecord {
141
139
  return value !== null && typeof value === "object" && !Array.isArray(value);
142
140
  }
143
141
 
144
- function parseToolOutputStyle(value: unknown): ToolOutputStyle {
145
- return value === "truncated" || value === "full" || value === "compact"
146
- ? value
147
- : defaultConfig.tools.style;
142
+ function parseProjectRefreshIntervalMs(value: unknown): number {
143
+ if (value === 0) return 0;
144
+ if (typeof value !== "number" || !Number.isFinite(value)) {
145
+ return defaultConfig.projectRefreshIntervalMs;
146
+ }
147
+
148
+ const interval = Math.round(value);
149
+ return interval >= MIN_PROJECT_REFRESH_INTERVAL_MS
150
+ ? interval
151
+ : defaultConfig.projectRefreshIntervalMs;
148
152
  }
149
153
 
150
154
  export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
@@ -173,9 +177,8 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
173
177
  const colors = isRecord(config.colors)
174
178
  ? (config.colors as Partial<PolishedTuiConfig["colors"]>)
175
179
  : {};
176
- const tools = isRecord(config.tools) ? (config.tools as Partial<PolishedTuiConfig["tools"]>) : {};
177
-
178
180
  return {
181
+ projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
179
182
  icons: {
180
183
  ...defaultConfig.icons,
181
184
  ...icons,
@@ -184,10 +187,6 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
184
187
  ...defaultConfig.colors,
185
188
  ...colors,
186
189
  },
187
- tools: {
188
- ...defaultConfig.tools,
189
- style: parseToolOutputStyle(tools.style),
190
- },
191
190
  };
192
191
  }
193
192
 
@@ -6,9 +6,9 @@ import type {
6
6
  Theme,
7
7
  } from "@mariozechner/pi-coding-agent";
8
8
  import { type EditorTheme, type TUI, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
- import { registerCompactTools } from "./compact-tools";
10
9
  import { type PolishedTuiConfig, colorize, ensureConfigExists, loadConfig } from "./config";
11
10
  import { type GitStatusSummary, emptyGitStatus, readGitStatus } from "./git";
11
+ import { type StopProjectRefreshInterval, startProjectRefreshInterval } from "./project-refresh";
12
12
  import { type RuntimeInfo, readRuntimeInfo } from "./runtime";
13
13
  import { PolishedEditor, patchUserMessageComponent, restoreUserMessageComponent } from "./ui";
14
14
 
@@ -127,8 +127,6 @@ function formatCwdLabel(cwd: string, cwdIcon: string): string {
127
127
  }
128
128
 
129
129
  export default function (pi: ExtensionAPI) {
130
- registerCompactTools(pi);
131
-
132
130
  const state: FooterState = {
133
131
  modelLabel: "no-model",
134
132
  providerLabel: "Unknown",
@@ -141,6 +139,7 @@ export default function (pi: ExtensionAPI) {
141
139
 
142
140
  let currentConfig: PolishedTuiConfig = loadConfig();
143
141
  let requestFooterRender: (() => void) | undefined;
142
+ let stopProjectRefreshInterval: StopProjectRefreshInterval = () => {};
144
143
  let projectRefreshInFlight = false;
145
144
  let projectRefreshPending = false;
146
145
 
@@ -291,6 +290,11 @@ export default function (pi: ExtensionAPI) {
291
290
  patchUserMessageComponent(ctx.ui.theme);
292
291
  installFooter(ctx);
293
292
  installEditor(ctx);
293
+ stopProjectRefreshInterval();
294
+ stopProjectRefreshInterval = startProjectRefreshInterval(
295
+ currentConfig.projectRefreshIntervalMs,
296
+ () => scheduleProjectRefresh(ctx),
297
+ );
294
298
  scheduleProjectRefresh(ctx);
295
299
  refresh();
296
300
  };
@@ -300,6 +304,8 @@ export default function (pi: ExtensionAPI) {
300
304
  });
301
305
 
302
306
  pi.on("session_shutdown", async () => {
307
+ stopProjectRefreshInterval();
308
+ stopProjectRefreshInterval = () => {};
303
309
  restoreUserMessageComponent();
304
310
  });
305
311
 
@@ -0,0 +1,13 @@
1
+ export type StopProjectRefreshInterval = () => void;
2
+
3
+ export function startProjectRefreshInterval(
4
+ intervalMs: number,
5
+ refresh: () => void,
6
+ ): StopProjectRefreshInterval {
7
+ if (intervalMs <= 0) return () => {};
8
+
9
+ const timer = setInterval(refresh, intervalMs);
10
+ timer.unref?.();
11
+
12
+ return () => clearInterval(timer);
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-zentui",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,565 +0,0 @@
1
- import { homedir } from "node:os";
2
- import type {
3
- AgentToolResult,
4
- ExtensionAPI,
5
- Theme,
6
- ToolDefinition,
7
- ToolRenderResultOptions,
8
- } from "@mariozechner/pi-coding-agent";
9
- import {
10
- createBashToolDefinition,
11
- createEditToolDefinition,
12
- createFindToolDefinition,
13
- createGrepToolDefinition,
14
- createLsToolDefinition,
15
- createReadToolDefinition,
16
- createWriteToolDefinition,
17
- getLanguageFromPath,
18
- highlightCode,
19
- renderDiff,
20
- } from "@mariozechner/pi-coding-agent";
21
- import { Box, type Component, Container, Spacer, Text } from "@mariozechner/pi-tui";
22
- import { type ToolOutputStyle, loadConfig } from "./config";
23
-
24
- type BuiltInDefinitions = ReturnType<typeof createBuiltInDefinitions>;
25
- type ToolArgs = Record<string, unknown>;
26
-
27
- type CompactState = {
28
- summary?: string;
29
- error?: boolean;
30
- errorText?: string;
31
- };
32
-
33
- type CompactRenderContext = {
34
- args: ToolArgs;
35
- state: CompactState;
36
- isError: boolean;
37
- lastComponent?: Component;
38
- invalidate: () => void;
39
- };
40
-
41
- type CompactRenderCall = (args: ToolArgs, theme: Theme, context: CompactRenderContext) => Component;
42
- type CompactExpandedRenderer = (
43
- result: AgentToolResult<unknown>,
44
- theme: Theme,
45
- context: CompactRenderContext,
46
- error: boolean,
47
- mode: ExpandedOutputMode,
48
- ) => string;
49
- type CompactRenderResult = (
50
- result: AgentToolResult<unknown>,
51
- options: ToolRenderResultOptions,
52
- theme: Theme,
53
- context: CompactRenderContext,
54
- ) => Component;
55
-
56
- type OutputMode = "one-line" | "preview" | "full";
57
- type ExpandedOutputMode = Exclude<OutputMode, "one-line">;
58
- type ExpandedBoxStatus = "pending" | "success" | "error";
59
- type ToolBackground = Parameters<Theme["bg"]>[0];
60
-
61
- const PREVIEW_LINES = 12;
62
- const OUTPUT_MODES: OutputMode[] = ["one-line", "preview", "full"];
63
-
64
- let outputMode: OutputMode = "one-line";
65
- let observedPiExpanded = false;
66
-
67
- const home = homedir();
68
- const definitionsByCwd = new Map<string, BuiltInDefinitions>();
69
-
70
- function createBuiltInDefinitions(cwd: string) {
71
- return {
72
- bash: createBashToolDefinition(cwd),
73
- edit: createEditToolDefinition(cwd),
74
- find: createFindToolDefinition(cwd),
75
- grep: createGrepToolDefinition(cwd),
76
- ls: createLsToolDefinition(cwd),
77
- read: createReadToolDefinition(cwd),
78
- write: createWriteToolDefinition(cwd),
79
- };
80
- }
81
-
82
- function getBuiltIns(cwd: string): BuiltInDefinitions {
83
- let definitions = definitionsByCwd.get(cwd);
84
- if (!definitions) {
85
- definitions = createBuiltInDefinitions(cwd);
86
- definitionsByCwd.set(cwd, definitions);
87
- }
88
- return definitions;
89
- }
90
-
91
- function stripAtPrefix(path: string): string {
92
- return path.startsWith("@") ? path.slice(1) : path;
93
- }
94
-
95
- function shortPath(path: unknown, fallback = "."): string {
96
- if (typeof path !== "string" || path.length === 0) return fallback;
97
- const cleaned = stripAtPrefix(path);
98
- return cleaned.startsWith(home) ? `~${cleaned.slice(home.length)}` : cleaned;
99
- }
100
-
101
- function quote(value: unknown): string {
102
- return `"${String(value ?? "")}"`;
103
- }
104
-
105
- function truncate(value: unknown, max = 120): string {
106
- const text = String(value ?? "")
107
- .replace(/\s+/g, " ")
108
- .trim();
109
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
110
- }
111
-
112
- function plural(count: number, one: string, many = `${one}s`): string {
113
- return `${count} ${count === 1 ? one : many}`;
114
- }
115
-
116
- function asRecord(value: unknown): Record<string, unknown> {
117
- return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
118
- }
119
-
120
- function resultDetails(result: AgentToolResult<unknown>): Record<string, unknown> {
121
- return asRecord(result.details);
122
- }
123
-
124
- function isResultTruncated(result: AgentToolResult<unknown>): boolean {
125
- return Boolean(asRecord(resultDetails(result).truncation).truncated);
126
- }
127
-
128
- function textContent(result: AgentToolResult<unknown>): string {
129
- const block = result.content.find((item) => item.type === "text");
130
- return block?.type === "text" ? block.text : "";
131
- }
132
-
133
- function hasImage(result: AgentToolResult<unknown>): boolean {
134
- return result.content.some((item) => item.type === "image");
135
- }
136
-
137
- function visibleLineCount(text: string): number {
138
- return text
139
- .split("\n")
140
- .map((line) => line.trim())
141
- .filter(
142
- (line) =>
143
- line.length > 0 && !line.startsWith("[Showing ") && !line.startsWith("[Output truncated"),
144
- ).length;
145
- }
146
-
147
- function firstUsefulLine(text: string): string | undefined {
148
- return text
149
- .split("\n")
150
- .map((line) => line.trim())
151
- .find((line) => line.length > 0);
152
- }
153
-
154
- function normalizeDisplayText(text: string): string {
155
- return text.replace(/\r/g, "");
156
- }
157
-
158
- function replaceTabs(text: string): string {
159
- return text.replace(/\t/g, " ");
160
- }
161
-
162
- function trimTrailingEmptyLines(lines: string[]): string[] {
163
- let end = lines.length;
164
- while (end > 0 && lines[end - 1] === "") end--;
165
- return lines.slice(0, end);
166
- }
167
-
168
- function limitRenderedText(text: string, mode: ExpandedOutputMode, theme: Theme): string {
169
- if (mode === "full") return text;
170
-
171
- const lines = trimTrailingEmptyLines(text.split("\n"));
172
- if (lines.length <= PREVIEW_LINES) return lines.join("\n");
173
-
174
- const hidden = lines.length - PREVIEW_LINES;
175
- return `${lines.slice(0, PREVIEW_LINES).join("\n")}\n${theme.fg(
176
- "dim",
177
- `… ${hidden} more lines (Ctrl+O for full)`,
178
- )}`;
179
- }
180
-
181
- function nextOutputMode(mode: OutputMode): OutputMode {
182
- return OUTPUT_MODES[(OUTPUT_MODES.indexOf(mode) + 1) % OUTPUT_MODES.length] ?? "one-line";
183
- }
184
-
185
- function outputModeForToolStyle(style: ToolOutputStyle): OutputMode {
186
- switch (style) {
187
- case "full":
188
- return "full";
189
- case "truncated":
190
- return "preview";
191
- default:
192
- return "one-line";
193
- }
194
- }
195
-
196
- function configuredOutputMode(expanded: boolean): OutputMode {
197
- const configured = outputModeForToolStyle(loadConfig().tools.style);
198
- return configured === "one-line" && expanded ? "preview" : configured;
199
- }
200
-
201
- function syncOutputModeWithPiToggle(expanded: boolean) {
202
- if (expanded === observedPiExpanded) return;
203
- observedPiExpanded = expanded;
204
- outputMode = nextOutputMode(outputMode);
205
- }
206
-
207
- function argPath(context: CompactRenderContext): string | undefined {
208
- const rawPath = context.args.path ?? context.args.file_path;
209
- return typeof rawPath === "string" ? stripAtPrefix(rawPath) : undefined;
210
- }
211
-
212
- function renderHighlightedSource(
213
- source: string,
214
- path: string | undefined,
215
- theme: Theme,
216
- error: boolean,
217
- mode: ExpandedOutputMode,
218
- ): string {
219
- const normalized = replaceTabs(normalizeDisplayText(source)).trimEnd();
220
- if (!normalized) return "";
221
- if (error) return limitRenderedText(theme.fg("error", normalized), mode, theme);
222
-
223
- const language = path ? getLanguageFromPath(path) : undefined;
224
- if (!language) return limitRenderedText(theme.fg("muted", normalized), mode, theme);
225
-
226
- try {
227
- return limitRenderedText(
228
- trimTrailingEmptyLines(highlightCode(normalized, language)).join("\n"),
229
- mode,
230
- theme,
231
- );
232
- } catch {
233
- return limitRenderedText(theme.fg("muted", normalized), mode, theme);
234
- }
235
- }
236
-
237
- function highlightShellCommand(command: unknown, theme: Theme): string {
238
- const normalized = truncate(command, 180);
239
- if (!normalized) return theme.fg("muted", "...");
240
-
241
- try {
242
- return trimTrailingEmptyLines(highlightCode(normalized, "bash")).join(" ");
243
- } catch {
244
- return theme.fg("accent", normalized);
245
- }
246
- }
247
-
248
- function renderExpandedBox(content: string, theme: Theme, status: ExpandedBoxStatus) {
249
- const background: ToolBackground =
250
- status === "error" ? "toolErrorBg" : status === "pending" ? "toolPendingBg" : "toolSuccessBg";
251
- const box = new Box(3, 1, (text: string) => theme.bg(background, text));
252
- box.addChild(new Text(content, 0, 0));
253
- return box;
254
- }
255
-
256
- function renderPlainExpanded(
257
- result: AgentToolResult<unknown>,
258
- theme: Theme,
259
- _context: CompactRenderContext,
260
- error: boolean,
261
- mode: ExpandedOutputMode,
262
- ): string {
263
- return limitRenderedText(
264
- theme.fg(error ? "error" : "muted", textContent(result).trimEnd()),
265
- mode,
266
- theme,
267
- );
268
- }
269
-
270
- function renderReadExpanded(
271
- result: AgentToolResult<unknown>,
272
- theme: Theme,
273
- context: CompactRenderContext,
274
- error: boolean,
275
- mode: ExpandedOutputMode,
276
- ): string {
277
- return renderHighlightedSource(textContent(result), argPath(context), theme, error, mode);
278
- }
279
-
280
- function renderBashExpanded(
281
- result: AgentToolResult<unknown>,
282
- theme: Theme,
283
- _context: CompactRenderContext,
284
- error: boolean,
285
- mode: ExpandedOutputMode,
286
- ): string {
287
- const output = textContent(result).trimEnd();
288
- return output ? limitRenderedText(theme.fg(error ? "error" : "muted", output), mode, theme) : "";
289
- }
290
-
291
- function renderWriteExpanded(
292
- result: AgentToolResult<unknown>,
293
- theme: Theme,
294
- context: CompactRenderContext,
295
- error: boolean,
296
- mode: ExpandedOutputMode,
297
- ) {
298
- if (error) return renderPlainExpanded(result, theme, context, true, mode);
299
- const source =
300
- typeof context.args.content === "string" ? context.args.content : textContent(result);
301
- return renderHighlightedSource(source, argPath(context), theme, false, mode);
302
- }
303
-
304
- function renderEditExpanded(
305
- result: AgentToolResult<unknown>,
306
- theme: Theme,
307
- context: CompactRenderContext,
308
- error: boolean,
309
- mode: ExpandedOutputMode,
310
- ): string {
311
- if (error) return renderPlainExpanded(result, theme, context, true, mode);
312
- const diff = resultDetails(result).diff;
313
- if (typeof diff === "string" && diff.length > 0) {
314
- return limitRenderedText(renderDiff(diff, { filePath: argPath(context) }), mode, theme);
315
- }
316
- return renderPlainExpanded(result, theme, context, false, mode);
317
- }
318
-
319
- function setCompactState(context: CompactRenderContext, next: CompactState) {
320
- const state = context.state;
321
- const changed =
322
- state.summary !== next.summary ||
323
- state.error !== next.error ||
324
- state.errorText !== next.errorText;
325
- state.summary = next.summary;
326
- state.error = next.error;
327
- state.errorText = next.errorText;
328
-
329
- // renderCall runs before renderResult. Re-render once after renderResult stores
330
- // the final summary so the count/status can stay on the same compact line.
331
- if (changed) queueMicrotask(() => context.invalidate());
332
- }
333
-
334
- function suffix(state: CompactState): string {
335
- if (state.errorText) return ` — ${truncate(state.errorText, 100)}`;
336
- if (state.summary) return ` (${state.summary})`;
337
- return "";
338
- }
339
-
340
- function compactCall(format: (args: ToolArgs, state: CompactState) => string): CompactRenderCall {
341
- return (args, theme, context) => {
342
- const state = context.state;
343
- const component =
344
- context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
345
- const color = state.error || context.isError ? "error" : "muted";
346
- component.setText(theme.fg(color, format(args, state)));
347
- return component;
348
- };
349
- }
350
-
351
- const compactBashCall: CompactRenderCall = (args, theme, context) => {
352
- const state = context.state;
353
- const component =
354
- context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
355
- const command = truncate(args.command, 180);
356
- const timeout = args.timeout !== undefined ? ` [timeout=${args.timeout}s]` : "";
357
- const meta = `${timeout}${suffix(state)}`;
358
-
359
- if (state.error || context.isError) {
360
- component.setText(theme.fg("error", `→ $ ${command}${meta}`));
361
- } else {
362
- component.setText(
363
- `${theme.fg("muted", "→ ")}${theme.fg("accent", "$")} ${highlightShellCommand(
364
- command,
365
- theme,
366
- )}${theme.fg("dim", meta)}`,
367
- );
368
- }
369
-
370
- return component;
371
- };
372
-
373
- function compactResult(
374
- summarize: (
375
- result: AgentToolResult<unknown>,
376
- context: CompactRenderContext,
377
- ) => string | undefined,
378
- renderExpanded: CompactExpandedRenderer = renderPlainExpanded,
379
- gapBeforeExpanded = true,
380
- ): CompactRenderResult {
381
- return (result, { expanded, isPartial }, theme, context) => {
382
- const output = textContent(result);
383
- const error = Boolean(context.isError);
384
- const errorText = error ? firstUsefulLine(output) : undefined;
385
- const summary = isPartial ? "running" : summarize(result, context);
386
-
387
- setCompactState(context, { summary, error, errorText });
388
-
389
- syncOutputModeWithPiToggle(Boolean(expanded));
390
- if (outputMode === "one-line") return new Text("", 0, 0);
391
-
392
- const mode: ExpandedOutputMode = outputMode === "full" ? "full" : "preview";
393
- const expandedText = renderExpanded(result, theme, context, error, mode);
394
- if (!expandedText || expandedText.trim() === "") return new Text("", 0, 0);
395
-
396
- const status: ExpandedBoxStatus = error ? "error" : isPartial ? "pending" : "success";
397
- const box = renderExpandedBox(expandedText, theme, status);
398
- if (!gapBeforeExpanded) return box;
399
-
400
- const container = new Container();
401
- container.addChild(new Spacer(1));
402
- container.addChild(box);
403
- return container;
404
- };
405
- }
406
-
407
- function summarizeRead(result: AgentToolResult<unknown>): string | undefined {
408
- if (hasImage(result)) return "image";
409
- const lines = visibleLineCount(textContent(result));
410
- const truncated = isResultTruncated(result);
411
- return lines > 0 ? `${plural(lines, "line")}${truncated ? ", truncated" : ""}` : undefined;
412
- }
413
-
414
- function summarizeBash(
415
- result: AgentToolResult<unknown>,
416
- context: CompactRenderContext,
417
- ): string | undefined {
418
- const output = textContent(result);
419
- if (context.isError) {
420
- const exit = output.match(/Command exited with code (\d+)/i)?.[1];
421
- if (exit) return `exit ${exit}`;
422
- }
423
- const lines = visibleLineCount(output);
424
- const truncated = isResultTruncated(result);
425
- if (lines === 0 || output.trim() === "(no output)") return "done";
426
- return `${plural(lines, "line")}${truncated ? ", truncated" : ""}`;
427
- }
428
-
429
- function summarizeEdit(result: AgentToolResult<unknown>): string | undefined {
430
- const diff = resultDetails(result).diff;
431
- if (typeof diff !== "string") return "done";
432
- let additions = 0;
433
- let removals = 0;
434
- for (const line of diff.split("\n")) {
435
- if (line.startsWith("+") && !line.startsWith("+++")) additions++;
436
- if (line.startsWith("-") && !line.startsWith("---")) removals++;
437
- }
438
- return `+${additions}/-${removals}`;
439
- }
440
-
441
- function summarizeWrite(): string {
442
- return "written";
443
- }
444
-
445
- function summarizeCount(noun: string, many = `${noun}s`) {
446
- return (result: AgentToolResult<unknown>): string | undefined => {
447
- const count = visibleLineCount(textContent(result));
448
- if (count === 0) return `0 ${many}`;
449
- return plural(count, noun, many);
450
- };
451
- }
452
-
453
- function registerCompactBuiltIn(
454
- pi: ExtensionAPI,
455
- name: keyof BuiltInDefinitions,
456
- renderCall: CompactRenderCall,
457
- renderResult: CompactRenderResult,
458
- ) {
459
- const initialDefinition = getBuiltIns(process.cwd())[name] as ToolDefinition;
460
-
461
- pi.registerTool({
462
- ...initialDefinition,
463
- renderShell: "self",
464
- async execute(
465
- toolCallId: string,
466
- params: Parameters<ToolDefinition["execute"]>[1],
467
- signal: AbortSignal | undefined,
468
- onUpdate: Parameters<ToolDefinition["execute"]>[3],
469
- ctx: Parameters<ToolDefinition["execute"]>[4],
470
- ) {
471
- const definition = getBuiltIns(ctx.cwd)[name] as ToolDefinition;
472
- return definition.execute(toolCallId, params, signal, onUpdate, ctx);
473
- },
474
- renderCall: renderCall as NonNullable<ToolDefinition["renderCall"]>,
475
- renderResult: renderResult as NonNullable<ToolDefinition["renderResult"]>,
476
- });
477
- }
478
-
479
- export function registerCompactTools(pi: ExtensionAPI) {
480
- pi.on("session_start", async (_event, ctx) => {
481
- observedPiExpanded = ctx.ui.getToolsExpanded();
482
- outputMode = configuredOutputMode(observedPiExpanded);
483
- });
484
-
485
- registerCompactBuiltIn(
486
- pi,
487
- "read",
488
- compactCall((args, state) => {
489
- const options: string[] = [];
490
- if (args.offset !== undefined) options.push(`offset=${args.offset}`);
491
- if (args.limit !== undefined) options.push(`limit=${args.limit}`);
492
- return `→ Read ${shortPath(args.path)}${
493
- options.length ? ` [${options.join(", ")}]` : ""
494
- }${suffix(state)}`;
495
- }),
496
- compactResult(summarizeRead, renderReadExpanded),
497
- );
498
-
499
- registerCompactBuiltIn(
500
- pi,
501
- "bash",
502
- compactBashCall,
503
- compactResult(summarizeBash, renderBashExpanded),
504
- );
505
-
506
- registerCompactBuiltIn(
507
- pi,
508
- "edit",
509
- compactCall((args, state) => {
510
- const edits = Array.isArray(args.edits) ? ` [${plural(args.edits.length, "edit")}]` : "";
511
- return `→ Edit ${shortPath(args.path)}${edits}${suffix(state)}`;
512
- }),
513
- compactResult(summarizeEdit, renderEditExpanded),
514
- );
515
-
516
- registerCompactBuiltIn(
517
- pi,
518
- "write",
519
- compactCall((args, state) => {
520
- const lines =
521
- typeof args.content === "string"
522
- ? ` [${plural(args.content.split("\n").length, "line")}]`
523
- : "";
524
- return `→ Write ${shortPath(args.path)}${lines}${suffix(state)}`;
525
- }),
526
- compactResult(summarizeWrite, renderWriteExpanded),
527
- );
528
-
529
- registerCompactBuiltIn(
530
- pi,
531
- "find",
532
- compactCall((args, state) => {
533
- const limit = args.limit !== undefined ? ` [limit=${args.limit}]` : "";
534
- return `* Glob ${quote(args.pattern)} in ${shortPath(args.path)}${limit}${suffix(state)}`;
535
- }),
536
- compactResult(summarizeCount("match", "matches")),
537
- );
538
-
539
- registerCompactBuiltIn(
540
- pi,
541
- "grep",
542
- compactCall((args, state) => {
543
- const parts: string[] = [];
544
- if (args.glob) parts.push(`glob=${args.glob}`);
545
- if (args.ignoreCase) parts.push("ignoreCase=true");
546
- if (args.literal) parts.push("literal=true");
547
- if (args.context !== undefined) parts.push(`context=${args.context}`);
548
- if (args.limit !== undefined) parts.push(`limit=${args.limit}`);
549
- return `* Grep ${quote(args.pattern)} in ${shortPath(args.path)}${
550
- parts.length ? ` [${parts.join(", ")}]` : ""
551
- }${suffix(state)}`;
552
- }),
553
- compactResult(summarizeCount("match", "matches")),
554
- );
555
-
556
- registerCompactBuiltIn(
557
- pi,
558
- "ls",
559
- compactCall((args, state) => {
560
- const limit = args.limit !== undefined ? ` [limit=${args.limit}]` : "";
561
- return `→ List ${shortPath(args.path)}${limit}${suffix(state)}`;
562
- }),
563
- compactResult(summarizeCount("entry", "entries")),
564
- );
565
- }