@nedleeds/pi-compact-ui 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # pi-compact-ui
2
2
 
3
- Compact, expandable rendering for pi's built-in `read`, `write`, `edit`, and `bash` tools, bundled with a polished GitHub Dark theme.
3
+ Compact, expandable rendering for all of Pi's built-in tools, bundled with a polished GitHub Dark theme.
4
4
 
5
5
  Designed for a focused, low-noise terminal workflow.
6
6
 
7
- ![Spinner and duration indicators](https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/spinner-status.gif)
7
+ ![Built-in tool support](https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/built-in-tool-support.png)
8
8
 
9
9
  ## Demo
10
10
 
11
+ **Spinner and status** – track active execution and see duration-aware completion indicators.
12
+
13
+ ![Spinner and duration indicators](https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/spinner-status.gif)
14
+
11
15
  **Keyboard expansion** – press `Ctrl+O` to cycle through detail levels.
12
16
 
13
17
  ![Ctrl+O expansion](https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/keyboard-expand.gif)
@@ -18,7 +22,8 @@ Designed for a focused, low-noise terminal workflow.
18
22
 
19
23
  ## Features
20
24
 
21
- - Compact one-line tool calls
25
+ - Compact one-line rendering for configurable built-in tools
26
+ - Supports `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
22
27
  - Click or `Ctrl+O` to cycle through available detail levels
23
28
  - Skips argument and output levels that contain no additional information
24
29
  - Animated, configurable tool spinner
@@ -28,6 +33,21 @@ Designed for a focused, low-noise terminal workflow.
28
33
  - GitHub Dark theme with distinct tool, output, success, and error colors
29
34
  - No Nerd Font requirement
30
35
 
36
+ ## Supported tools
37
+
38
+ | Tool | Purpose | Compact by default |
39
+ | --- | --- | :---: |
40
+ | `read` | Read files and images | Yes |
41
+ | `write` | Create or overwrite files | Yes |
42
+ | `edit` | Apply exact text replacements with clean diffs | Yes |
43
+ | `bash` | Run shell commands | Yes |
44
+ | `grep` | Search file contents | Opt-in |
45
+ | `find` | Find files by glob pattern | Opt-in |
46
+ | `ls` | List directory contents | Opt-in |
47
+ | `powershell` | Run PowerShell commands | Opt-in |
48
+
49
+ Enable opt-in tools with the [`tools` configuration](#configuration). PowerShell is intended for Windows; the other seven tools are demonstrated above on macOS.
50
+
31
51
  ## Requirements
32
52
 
33
53
  - pi `0.85.1` or newer is recommended
@@ -85,6 +105,7 @@ Default configuration:
85
105
  ```json
86
106
  {
87
107
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/schemas/compact-tools.schema.json",
108
+ "tools": ["read", "write", "edit", "bash"],
88
109
  "previewLines": 10,
89
110
  "spinner": {
90
111
  "frames": ["◐", "◓", "◑", "◒"],
@@ -99,6 +120,16 @@ Default configuration:
99
120
  }
100
121
  ```
101
122
 
123
+ `tools` selects which built-in definitions receive compact rendering. Omitted tools retain Pi's default renderer. The array replaces, rather than extends, the previous configuration layer. To enable every Unix-compatible tool:
124
+
125
+ ```json
126
+ {
127
+ "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"]
128
+ }
129
+ ```
130
+
131
+ `powershell` is also supported and can be selected explicitly on Windows. Unsupported names are ignored with a warning.
132
+
102
133
  `durationIndicators` must use ascending `underMs` values. The final entry must omit `underMs` and acts as the fallback.
103
134
 
104
135
  ## Expansion levels
@@ -119,7 +150,7 @@ summary → diff → summary
119
150
 
120
151
  ## Compatibility and limitations
121
152
 
122
- - The package overrides pi's built-in `read`, `write`, `edit`, and `bash` definitions while preserving their execution behavior and metadata.
153
+ - The package overrides only the built-in definitions selected by `tools`, while preserving their execution behavior and metadata.
123
154
  - Another extension overriding the same tool names may conflict depending on extension load order.
124
155
  - Tools registered by other extensions are not modified.
125
156
  - Duration is measured from the first execution render and is intended as a UI estimate.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/schemas/compact-tools.schema.json",
3
+ "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
3
4
  "previewLines": 10,
4
5
  "spinner": {
5
6
  "frames": ["◐", "◓", "◑", "◒"],
@@ -15,6 +15,10 @@ import {
15
15
  CONFIG_DIR_NAME,
16
16
  createBashToolDefinition,
17
17
  createEditToolDefinition,
18
+ createFindToolDefinition,
19
+ createGrepToolDefinition,
20
+ createLsToolDefinition,
21
+ createPowerShellToolDefinition,
18
22
  createReadToolDefinition,
19
23
  createWriteToolDefinition,
20
24
  getAgentDir,
@@ -24,6 +28,10 @@ import { Container, Text, visibleWidth } from "@earendil-works/pi-tui";
24
28
 
25
29
  const MAX_LEVEL = 3;
26
30
  const CONFIG_FILE = "compact-tools.json";
31
+ const SUPPORTED_TOOLS = ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"] as const;
32
+ const SUPPORTED_TOOL_SET = new Set<string>(SUPPORTED_TOOLS);
33
+
34
+ type CompactToolName = (typeof SUPPORTED_TOOLS)[number];
27
35
 
28
36
  export interface DurationIndicatorConfig {
29
37
  underMs?: number;
@@ -31,6 +39,7 @@ export interface DurationIndicatorConfig {
31
39
  }
32
40
 
33
41
  export interface CompactToolsConfig {
42
+ tools: CompactToolName[];
34
43
  previewLines: number;
35
44
  spinner: {
36
45
  frames: string[];
@@ -40,6 +49,7 @@ export interface CompactToolsConfig {
40
49
  }
41
50
 
42
51
  export const DEFAULT_CONFIG: CompactToolsConfig = {
52
+ tools: ["read", "write", "edit", "bash"],
43
53
  previewLines: 10,
44
54
  spinner: {
45
55
  frames: ["◐", "◓", "◑", "◒"],
@@ -49,11 +59,12 @@ export const DEFAULT_CONFIG: CompactToolsConfig = {
49
59
  { underMs: 1_000, icon: "⚡️" },
50
60
  { underMs: 10_000, icon: "🚀" },
51
61
  { underMs: 30_000, icon: "🔥" },
52
- { icon: "" },
62
+ { icon: "" },
53
63
  ],
54
64
  };
55
65
 
56
66
  let config = DEFAULT_CONFIG;
67
+ const registeredCompactTools = new Set<CompactToolName>();
57
68
 
58
69
  interface RowState {
59
70
  level?: number;
@@ -92,6 +103,17 @@ function isNonEmptyStringArray(value: unknown): value is string[] {
92
103
  return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
93
104
  }
94
105
 
106
+ function parseTools(value: unknown, path: string): CompactToolName[] | undefined {
107
+ if (!Array.isArray(value)) {
108
+ warnConfig(path, "tools must be an array; using previous values");
109
+ return undefined;
110
+ }
111
+ const valid = value.filter((item): item is CompactToolName => typeof item === "string" && SUPPORTED_TOOL_SET.has(item));
112
+ const invalid = value.filter((item) => typeof item !== "string" || !SUPPORTED_TOOL_SET.has(item));
113
+ if (invalid.length > 0) warnConfig(path, `ignoring unsupported tools: ${invalid.map(String).join(", ")}`);
114
+ return [...new Set(valid)];
115
+ }
116
+
95
117
  function parseDurationIndicators(value: unknown): DurationIndicatorConfig[] | undefined {
96
118
  if (!Array.isArray(value) || value.length === 0) return undefined;
97
119
  const rules: DurationIndicatorConfig[] = [];
@@ -114,6 +136,7 @@ function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): Co
114
136
  warnConfig(path, "expected a JSON object; using previous values");
115
137
  return base;
116
138
  }
139
+ const tools = value.tools === undefined ? base.tools : (parseTools(value.tools, path) ?? base.tools);
117
140
  const previewValue = value.previewLines;
118
141
  const validPreviewLines = isIntegerInRange(previewValue, 1, 1_000);
119
142
  const previewLines = validPreviewLines ? previewValue : base.previewLines;
@@ -125,7 +148,7 @@ function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): Co
125
148
  if (value.durationIndicators !== undefined && durationIndicators === base.durationIndicators) {
126
149
  warnConfig(path, "invalid durationIndicators; using previous values");
127
150
  }
128
- return { previewLines, spinner, durationIndicators };
151
+ return { tools, previewLines, spinner, durationIndicators };
129
152
  }
130
153
 
131
154
  function parseSpinnerConfig(
@@ -339,10 +362,19 @@ function getPathArg(args: ToolArgs): string {
339
362
  return typeof value === "string" ? firstLine(value) : "";
340
363
  }
341
364
 
365
+ function getCallDetails(name: string, args: ToolArgs): string {
366
+ const path = getPathArg(args) || (name === "grep" || name === "find" || name === "ls" ? "." : "");
367
+ const pattern = typeof args.pattern === "string" ? firstLine(args.pattern) : "…";
368
+ if (name === "grep") return `/${pattern}/ in ${path}`;
369
+ if (name === "find") return `${pattern} in ${path}`;
370
+ return path;
371
+ }
372
+
342
373
  function getFileArgumentDetails(name: string, args: ToolArgs): ToolArgs {
343
374
  if (name === "edit") return {};
344
375
  const omitted = new Set(["path", "file_path"]);
345
376
  if (name === "write") omitted.add("content");
377
+ if (name === "grep" || name === "find") omitted.add("pattern");
346
378
  return Object.fromEntries(Object.entries(args).filter(([key, value]) => !omitted.has(key) && value !== undefined));
347
379
  }
348
380
 
@@ -374,7 +406,9 @@ function callOriginalEditResult(
374
406
  }
375
407
 
376
408
  function getFileOutput(name: string, args: ToolArgs, result: AgentToolResult<unknown>, isError: boolean): string {
377
- if (isError || name === "read") return getTextResult(result);
409
+ if (isError || name === "read" || name === "grep" || name === "find" || name === "ls") {
410
+ return getTextResult(result);
411
+ }
378
412
  if (name === "write") return String(args.content ?? "");
379
413
  return "";
380
414
  }
@@ -388,11 +422,11 @@ function renderFileCall(
388
422
  const running = ctx.executionStarted && ctx.isPartial;
389
423
  const state = syncRow(ctx, running);
390
424
  const level = advanceLevel(state, ctx.expanded);
391
- const path = getPathArg(args);
425
+ const callDetails = getCallDetails(definition.name, args);
392
426
  const argumentDetails = getFileArgumentDetails(definition.name, args);
393
427
  let text = `${renderIndicator(theme, state, running, ctx.isError, ctx.isPartial && !ctx.executionStarted)} `;
394
428
  text += theme.fg("toolTitle", theme.bold(definition.name));
395
- if (path) text += ` ${theme.fg("toolOutput", path)}`;
429
+ if (callDetails) text += ` ${theme.fg("toolOutput", callDetails)}`;
396
430
 
397
431
  const container = new Container();
398
432
  container.addChild(new Text(text, 1, 0));
@@ -463,14 +497,19 @@ function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void
463
497
  pi.registerTool(tool as ToolDefinition<any, any, RowState>);
464
498
  }
465
499
 
466
- function renderBashCall(args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>): Text {
500
+ function renderShellCall(
501
+ name: "bash" | "powershell",
502
+ args: BashToolInput,
503
+ theme: Theme,
504
+ ctx: RenderContext<BashToolInput>,
505
+ ): Text {
467
506
  const running = ctx.executionStarted && ctx.isPartial;
468
507
  const state = syncRow(ctx, running);
469
508
  const level = advanceLevel(state, ctx.expanded);
470
509
  const command = args.command ?? "";
471
510
  const displayedCommand = (level >= 1 ? command : firstLine(command)) || "…";
472
511
  let text = `${renderIndicator(theme, state, running, ctx.isError, ctx.isPartial && !ctx.executionStarted)} `;
473
- text += `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("toolOutput", displayedCommand)}`;
512
+ text += `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("toolOutput", displayedCommand)}`;
474
513
  if (level >= 1 && args.timeout) text += theme.fg("dim", ` (timeout: ${args.timeout}s)`);
475
514
  return new Text(text, 1, 0);
476
515
  }
@@ -496,21 +535,40 @@ function renderBashResult(
496
535
  return container;
497
536
  }
498
537
 
499
- function registerBashTool(pi: ExtensionAPI, cwd: string): void {
500
- const original = createBashToolDefinition(cwd);
501
- pi.registerTool<typeof original.parameters, BashToolDetails | undefined, RowState>({
502
- ...original,
538
+ function createBuiltInDefinition(name: CompactToolName, cwd: string): BuiltInDefinition {
539
+ switch (name) {
540
+ case "read": return createReadToolDefinition(cwd);
541
+ case "write": return createWriteToolDefinition(cwd);
542
+ case "edit": return createEditToolDefinition(cwd);
543
+ case "bash": return createBashToolDefinition(cwd);
544
+ case "powershell": return createPowerShellToolDefinition(cwd);
545
+ case "grep": return createGrepToolDefinition(cwd);
546
+ case "find": return createFindToolDefinition(cwd);
547
+ case "ls": return createLsToolDefinition(cwd);
548
+ }
549
+ }
550
+
551
+ function registerShellTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
552
+ pi.registerTool({
553
+ ...definition,
503
554
  renderShell: "self",
504
- renderCall: renderBashCall,
555
+ renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
556
+ renderShellCall(definition.name as "bash" | "powershell", args, theme, ctx),
505
557
  renderResult: renderBashResult,
506
- });
558
+ } as ToolDefinition<any, BashToolDetails | undefined, RowState>);
507
559
  }
508
560
 
509
561
  function registerBuiltInTools(pi: ExtensionAPI, cwd: string): void {
510
- registerFileTool(pi, createReadToolDefinition(cwd));
511
- registerFileTool(pi, createEditToolDefinition(cwd));
512
- registerFileTool(pi, createWriteToolDefinition(cwd));
513
- registerBashTool(pi, cwd);
562
+ for (const name of registeredCompactTools) {
563
+ if (!config.tools.includes(name)) pi.registerTool(createBuiltInDefinition(name, cwd));
564
+ }
565
+ registeredCompactTools.clear();
566
+ for (const name of config.tools) {
567
+ const definition = createBuiltInDefinition(name, cwd);
568
+ if (name === "bash" || name === "powershell") registerShellTool(pi, definition);
569
+ else registerFileTool(pi, definition);
570
+ registeredCompactTools.add(name);
571
+ }
514
572
  }
515
573
 
516
574
  function applyConfig(pi: ExtensionAPI, cwd: string, projectTrusted: boolean): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nedleeds/pi-compact-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Compact, expandable UI for pi's built-in tools, bundled with a polished GitHub Dark theme.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "themes": [
42
42
  "./themes/github-dark-pro.json"
43
43
  ],
44
- "image": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/spinner-status.gif"
44
+ "image": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/built-in-tool-support.png"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@earendil-works/pi-coding-agent": "*",
@@ -8,6 +8,16 @@
8
8
  "$schema": {
9
9
  "type": "string"
10
10
  },
11
+ "tools": {
12
+ "description": "Built-in tools that use compact rendering. Omitted tools keep Pi's default renderer.",
13
+ "type": "array",
14
+ "uniqueItems": true,
15
+ "items": {
16
+ "type": "string",
17
+ "enum": ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"]
18
+ },
19
+ "default": ["read", "write", "edit", "bash"]
20
+ },
11
21
  "previewLines": {
12
22
  "description": "Number of output lines shown before the full-output expansion level.",
13
23
  "type": "integer",