pi-compact-tools 0.3.4 → 0.4.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
@@ -20,19 +20,20 @@ The spinner animates during execution, then changes to a duration-aware completi
20
20
 
21
21
  ![Spinner and duration indicators](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/spinner-duration-optimized.gif)
22
22
 
23
- ### Expandable edit diffs
23
+ ### Initially visible edit diffs
24
24
 
25
- Click a completed `edit` row, or press `Ctrl+O`, to reveal and collapse its diff.
25
+ Completed `edit` rows show their full diff initially by default. The per-tool `auto_compact` setting controls which tools start in their compact summary state without disabling click or `Ctrl+O` interaction.
26
26
 
27
- ![Expandable edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
27
+ ![Edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
28
28
 
29
29
  ## Features
30
30
 
31
31
  - Compact rendering for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
32
32
  - Animated configurable spinner in fullscreen TUI mode
33
33
  - Millisecond-precision duration and configurable completion icons
34
- - Click or `Ctrl+O` to cycle through meaningful detail levels
35
- - Short previews, full output, and clean edit diffs
34
+ - Per-tool initial compaction with `auto_compact: true` or `false`
35
+ - Click or `Ctrl+O` always cycles through meaningful detail levels
36
+ - Short previews, full output, and initially visible edit diffs by default
36
37
  - Dark `│` and `└─` visual grouping
37
38
  - Windows, Unix, and classic Mac line-ending support
38
39
  - No Nerd Font requirement
@@ -78,6 +79,12 @@ Default configuration:
78
79
  {
79
80
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
80
81
  "tools": ["read", "write", "edit", "bash"],
82
+ "auto_compact": {
83
+ "read": true,
84
+ "write": true,
85
+ "edit": false,
86
+ "bash": true
87
+ },
81
88
  "previewLines": 10,
82
89
  "spinner": {
83
90
  "frames": ["◐", "◓", "◑", "◒"],
@@ -94,6 +101,14 @@ Default configuration:
94
101
 
95
102
  These defaults use emoji and standard Unicode, so they work without a Nerd Font. Fast tools finish immediately; no artificial spinner delay is added.
96
103
 
104
+ `auto_compact` controls each tool's initial state independently:
105
+
106
+ - `true` starts at the compact summary.
107
+ - `false` starts at the most detailed available level.
108
+ - Click or `Ctrl+O` continues to cycle through available levels in either case.
109
+
110
+ Unspecified entries inherit the previous configuration layer. By default, `read`, `write`, and `bash` start compact, while `edit` starts expanded so code diffs are immediately visible.
111
+
97
112
  `durationIndicators` must have ascending `underMs` values, with a final fallback entry that omits `underMs`. `color` is optional and accepts a supported theme color or six-digit hex value. Existing icon-only configurations remain compatible.
98
113
 
99
114
  To enable all Unix-compatible tools:
@@ -138,11 +153,7 @@ The escaped code points remain readable on GitHub and are decoded to Nerd Font i
138
153
  summary → arguments → output preview → full output → summary
139
154
  ```
140
155
 
141
- For `edit`:
142
-
143
- ```text
144
- summary → diff → summary
145
- ```
156
+ The `auto_compact` boolean only chooses where that cycle starts. With the default `edit: false`, the diff starts visible and the next click collapses it.
146
157
 
147
158
  ## Notes
148
159
 
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
3
3
  "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
4
+ "auto_compact": {
5
+ "read": true,
6
+ "write": true,
7
+ "edit": false,
8
+ "bash": true
9
+ },
4
10
  "previewLines": 10,
5
11
  "spinner": {
6
12
  "frames": ["◐", "◓", "◑", "◒"],
@@ -47,6 +47,7 @@ type CompactToolName = (typeof SUPPORTED_TOOLS)[number];
47
47
 
48
48
  export interface CompactToolsConfig {
49
49
  tools: CompactToolName[];
50
+ auto_compact: Record<CompactToolName, boolean>;
50
51
  previewLines: number;
51
52
  spinner: {
52
53
  frames: string[];
@@ -57,6 +58,16 @@ export interface CompactToolsConfig {
57
58
 
58
59
  export const DEFAULT_CONFIG: CompactToolsConfig = {
59
60
  tools: ["read", "write", "edit", "bash"],
61
+ auto_compact: {
62
+ read: true,
63
+ write: true,
64
+ edit: false,
65
+ bash: true,
66
+ powershell: true,
67
+ grep: true,
68
+ find: true,
69
+ ls: true,
70
+ },
60
71
  previewLines: 10,
61
72
  spinner: {
62
73
  frames: ["◐", "◓", "◑", "◒"],
@@ -122,6 +133,31 @@ function parseTools(value: unknown, path: string): CompactToolName[] | undefined
122
133
  return [...new Set(valid)];
123
134
  }
124
135
 
136
+ function parseAutoCompact(
137
+ base: CompactToolsConfig["auto_compact"],
138
+ value: unknown,
139
+ path: string,
140
+ ): CompactToolsConfig["auto_compact"] {
141
+ if (value === undefined) return base;
142
+ if (!isObject(value)) {
143
+ warnConfig(path, "auto_compact must be an object; using previous values");
144
+ return base;
145
+ }
146
+ const autoCompact = { ...base };
147
+ for (const [name, enabled] of Object.entries(value)) {
148
+ if (!SUPPORTED_TOOL_SET.has(name)) {
149
+ warnConfig(path, `ignoring unsupported auto_compact tool: ${name}`);
150
+ continue;
151
+ }
152
+ if (typeof enabled !== "boolean") {
153
+ warnConfig(path, `auto_compact.${name} must be true or false; using previous value`);
154
+ continue;
155
+ }
156
+ autoCompact[name as CompactToolName] = enabled;
157
+ }
158
+ return autoCompact;
159
+ }
160
+
125
161
  function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): CompactToolsConfig {
126
162
  if (value === undefined) return base;
127
163
  if (!isObject(value)) {
@@ -129,6 +165,7 @@ function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): Co
129
165
  return base;
130
166
  }
131
167
  const tools = value.tools === undefined ? base.tools : (parseTools(value.tools, path) ?? base.tools);
168
+ const auto_compact = parseAutoCompact(base.auto_compact, value.auto_compact, path);
132
169
  const previewValue = value.previewLines;
133
170
  const validPreviewLines = isIntegerInRange(previewValue, 1, 1_000);
134
171
  const previewLines = validPreviewLines ? previewValue : base.previewLines;
@@ -140,7 +177,7 @@ function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): Co
140
177
  if (value.durationIndicators !== undefined && durationIndicators === base.durationIndicators) {
141
178
  warnConfig(path, "invalid durationIndicators; using previous values");
142
179
  }
143
- return { tools, previewLines, spinner, durationIndicators };
180
+ return { tools, auto_compact, previewLines, spinner, durationIndicators };
144
181
  }
145
182
 
146
183
  function parseSpinnerConfig(
@@ -216,8 +253,8 @@ const executionTimings = globalState[EXECUTION_TIMINGS_KEY] ?? new Map<string, E
216
253
  globalState[SPINNING_ROWS_KEY] = spinningRows;
217
254
  globalState[EXECUTION_TIMINGS_KEY] = executionTimings;
218
255
 
219
- function advanceLevel(state: RowState, expanded: boolean): number {
220
- state.level ??= 0;
256
+ function advanceLevel(state: RowState, expanded: boolean, name: CompactToolName): number {
257
+ state.level ??= config.auto_compact[name] ? 0 : (state.availableLevels?.at(-1) ?? MAX_LEVEL);
221
258
  if (state.lastExpanded === undefined) {
222
259
  state.lastExpanded = expanded;
223
260
  return state.level;
@@ -231,9 +268,13 @@ function advanceLevel(state: RowState, expanded: boolean): number {
231
268
  return state.level;
232
269
  }
233
270
 
234
- function setAvailableLevels(state: RowState, levels: number[]): void {
271
+ function setAvailableLevels(state: RowState, levels: number[], name: CompactToolName): void {
235
272
  state.availableLevels = levels;
236
- if (!levels.includes(state.level ?? 0)) state.level = 0;
273
+ if (state.level === undefined) {
274
+ state.level = config.auto_compact[name] ? 0 : (levels.at(-1) ?? 0);
275
+ } else if (!levels.includes(state.level)) {
276
+ state.level = !config.auto_compact[name] && state.level === MAX_LEVEL ? (levels.at(-1) ?? 0) : 0;
277
+ }
237
278
  }
238
279
 
239
280
  function getOutputLevels(output: string): number[] {
@@ -509,7 +550,7 @@ function renderFileCall(
509
550
  // syncRow may restore a completed timing after /reload. Recompute from the
510
551
  // synchronized state so a stale local value cannot leave the spinner visible.
511
552
  const status = resolveCallStatus(ctx, state);
512
- const level = advanceLevel(state, ctx.expanded);
553
+ const level = advanceLevel(state, ctx.expanded, definition.name as CompactToolName);
513
554
  const callDetails = getCallDetails(definition.name, args);
514
555
  const argumentDetails = getFileArgumentDetails(definition.name, args);
515
556
  let text = `${renderIndicator(theme, state, status)} `;
@@ -554,8 +595,8 @@ function renderFileResult(
554
595
  const state = syncRow(ctx, options.isPartial, !options.isPartial);
555
596
  const output = getFileOutput(definition.name, ctx.args, result, ctx.isError);
556
597
  const hasArguments = Object.keys(getFileArgumentDetails(definition.name, ctx.args)).length > 0;
557
- const levels = definition.name === "edit" ? [0, 2] : [0, ...(hasArguments ? [1] : []), ...getOutputLevels(output)];
558
- setAvailableLevels(state, levels);
598
+ const levels = definition.name === "edit" ? [0, MAX_LEVEL] : [0, ...(hasArguments ? [1] : []), ...getOutputLevels(output)];
599
+ setAvailableLevels(state, levels, definition.name as CompactToolName);
559
600
 
560
601
  const level = state.level ?? 0;
561
602
  callOriginalEditResult(definition, result, options, theme, ctx, state, level);
@@ -614,7 +655,7 @@ function renderShellCall(
614
655
  // A completed timing can be restored inside syncRow. Derive the rendered
615
656
  // status afterward so completion immediately replaces the spinner.
616
657
  const status = resolveCallStatus(ctx, state);
617
- const level = advanceLevel(state, ctx.expanded);
658
+ const level = advanceLevel(state, ctx.expanded, name);
618
659
  const command = normalizeLineEndings(args.command ?? "");
619
660
  const displayedCommand = (level >= 1 ? command : firstLine(command)) || "…";
620
661
  let text = `${renderIndicator(theme, state, status)} `;
@@ -623,6 +664,7 @@ function renderShellCall(
623
664
  }
624
665
 
625
666
  function renderShellResult(
667
+ name: "bash" | "powershell",
626
668
  result: AgentToolResult<BashToolDetails | undefined>,
627
669
  options: ToolRenderResultOptions,
628
670
  theme: Theme,
@@ -631,7 +673,7 @@ function renderShellResult(
631
673
  const state = syncRow(ctx, options.isPartial, !options.isPartial);
632
674
  const output = getTextResult(result);
633
675
  const hasDetailedCall = /\r\n?|\n/.test(ctx.args.command ?? "");
634
- setAvailableLevels(state, [0, ...(hasDetailedCall ? [1] : []), ...getOutputLevels(output)]);
676
+ setAvailableLevels(state, [0, ...(hasDetailedCall ? [1] : []), ...getOutputLevels(output)], name);
635
677
  const level = state.level ?? 0;
636
678
  if (level < 2) return renderControls(theme, state, options.isPartial, ctx.isError);
637
679
 
@@ -667,7 +709,12 @@ function registerShellTool(pi: ExtensionAPI, definition: BuiltInDefinition): voi
667
709
  renderShell: "self",
668
710
  renderCall: (args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>) =>
669
711
  renderShellCall(definition.name as "bash" | "powershell", args, theme, ctx),
670
- renderResult: renderShellResult,
712
+ renderResult: (
713
+ result: AgentToolResult<BashToolDetails | undefined>,
714
+ options: ToolRenderResultOptions,
715
+ theme: Theme,
716
+ ctx: RenderContext<BashToolInput>,
717
+ ) => renderShellResult(definition.name as "bash" | "powershell", result, options, theme, ctx),
671
718
  } as ToolDefinition<any, BashToolDetails | undefined, RowState>);
672
719
  }
673
720
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-compact-tools",
3
- "version": "0.3.4",
3
+ "version": "0.4.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",
@@ -18,6 +18,27 @@
18
18
  },
19
19
  "default": ["read", "write", "edit", "bash"]
20
20
  },
21
+ "auto_compact": {
22
+ "description": "Whether each compact-rendered tool starts in its compact summary state. Click/Ctrl+O remains available regardless of this value.",
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "read": { "type": "boolean", "default": true },
27
+ "write": { "type": "boolean", "default": true },
28
+ "edit": { "type": "boolean", "default": false },
29
+ "bash": { "type": "boolean", "default": true },
30
+ "powershell": { "type": "boolean", "default": true },
31
+ "grep": { "type": "boolean", "default": true },
32
+ "find": { "type": "boolean", "default": true },
33
+ "ls": { "type": "boolean", "default": true }
34
+ },
35
+ "default": {
36
+ "read": true,
37
+ "write": true,
38
+ "edit": false,
39
+ "bash": true
40
+ }
41
+ },
21
42
  "previewLines": {
22
43
  "description": "Number of output lines shown before the full-output expansion level.",
23
44
  "type": "integer",
@@ -32,6 +32,12 @@ if (!Array.isArray(example.spinner?.frames) || example.spinner.frames.length ===
32
32
  if (!Number.isInteger(example.spinner.intervalMs)) {
33
33
  throw new Error("example spinner.intervalMs must be an integer");
34
34
  }
35
+ if (!example.auto_compact || example.auto_compact.edit !== false) {
36
+ throw new Error("example auto_compact policy must keep edit diffs visible by default");
37
+ }
38
+ if (Object.values(example.auto_compact).some((enabled) => typeof enabled !== "boolean")) {
39
+ throw new Error("example auto_compact values must be booleans");
40
+ }
35
41
  const indicators = example.durationIndicators;
36
42
  if (!Array.isArray(indicators) || indicators.length === 0 || indicators.at(-1)?.underMs !== undefined) {
37
43
  throw new Error("example durationIndicators must end with a fallback rule");
@@ -51,5 +57,8 @@ const schema = JSON.parse(
51
57
  if (!schema.properties?.durationIndicators?.items?.properties?.color) {
52
58
  throw new Error("schema must describe durationIndicators.color");
53
59
  }
60
+ if (!schema.properties?.auto_compact?.properties?.edit) {
61
+ throw new Error("schema must describe per-tool auto_compact policies");
62
+ }
54
63
 
55
64
  console.log("Resource checks passed");