pi-compact-tools 0.7.7 → 0.7.8

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
@@ -30,11 +30,11 @@ Completed `edit` rows show only the lines actually removed (`-`) or added (`+`),
30
30
 
31
31
  - Compact rendering for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
32
32
  - One continuously glowing working label above the input prompt across thinking and tool execution
33
- - Semantic built-in progress labels and automatically cleaned custom/MCP tool names without exposing invocation arguments
33
+ - Semantic built-in progress labels and automatically cleaned custom/MCP tool names; collapsed text-search rows include the searched pattern
34
34
  - Four-step `Ctrl+T` thinking cycle when provider detail is available: summary, detail, summary, then hidden
35
35
  - Millisecond-precision duration and completed output line counts with neutral status text
36
36
  - Per-tool hidden or persistent bounded-preview collapsed state with `auto_compact: true` or `false`
37
- - Primary invocation targets such as paths and patterns stay visible; collapsed shell calls show concise intent summaries
37
+ - Primary invocation targets such as paths and patterns stay visible, including common collapsed shell file operations
38
38
  - Click and `Ctrl+O` follow Pi's built-in expansion state while preview rows collapse back to their preview
39
39
  - Edit patches display only removed (`-`) and added (`+`) lines, omitting unchanged context
40
40
  - Configurable `previewLines` prevents large diffs, HTML, and shell output from flooding the transcript
@@ -125,7 +125,7 @@ one-line summary → summary + │-indented detail → one-line summary → Thin
125
125
 
126
126
  Standalone bold lines and Markdown headings split one provider thinking run into logical sections. Each section gets its own summary and optional `│`-indented detail. Summary-only views stay uncluttered with no control hint; the control row appears only while provider-supplied detail is visible. When no thinking detail exists, `Ctrl+T` falls back to Pi's normal visible/hidden toggle instead of showing a duplicate detail phase. While thinking streams, a slightly faster highlight sweeps from the Thinking summary color to white across Pi's working label at a steady interval rather than depending on token updates. The transcript summary remains visually stable. The transformation is display-only; complete thinking remains unchanged in the session and model context.
127
127
 
128
- Primary file and search targets stay visible. Collapsed `bash` and `powershell` rows describe intent—such as `Run tests`, `Check repository status`, or `Search text`—without printing arguments; expanding the row reveals the exact command. After every supported tool completes, the status reports the expanded result size as `Done in …s (N lines)` instead of repeating options such as context and limit. Counts use the returned text for reads, searches, listings, and shell commands, written content for `write`, and only the displayed `-`/`+` lines for `edit`. The count is computed once after completion and cached with the row, so streaming and repeated expansion do not add ongoing work. Large payloads represented as results follow the result toggle.
128
+ Primary file and search targets stay visible. Collapsed `bash` and `powershell` rows describe intent—such as `Run tests` or `Check repository status`. Text searches retain their primary pattern (for example, `Search text "TODO|FIXME"`), and common file commands retain their operands (for example, `Run rm "build"` or `Find files "*.test.ts" in "src"`). Expanding the row reveals the exact command. After every supported tool completes, the status reports the expanded result size as `Done in …s (N lines)` instead of repeating options such as context and limit. Counts use the returned text for reads, searches, listings, and shell commands, written content for `write`, and only the displayed `-`/`+` lines for `edit`. The count is computed once after completion and cached with the row, so streaming and repeated expansion do not add ongoing work. Large payloads represented as results follow the result toggle.
129
129
 
130
130
  A mouse click toggles one row using Pi's native result state. `Ctrl+O` remains owned by Pi and expands or collapses all tool rows consistently, including tools not rendered by this extension. A row with `auto_compact: false` cycles predictably between its bounded preview and the complete result, so repeated clicks never remove the row and fall through to a neighboring Thinking block. The extension does not intercept the global keybinding.
131
131
 
@@ -77,18 +77,192 @@ function shellSegments(command: string): string[] {
77
77
  return segments;
78
78
  }
79
79
 
80
- function unquote(value: string): string {
81
- return value.replace(/^["']|["']$/gu, "");
80
+ function shellWords(command: string, powershell = false): string[] {
81
+ const words: string[] = [];
82
+ let current = "";
83
+ let quote: "'" | '"' | undefined;
84
+ const input = command.trim();
85
+ for (let index = 0; index < input.length; index++) {
86
+ const character = input[index]!;
87
+ if (powershell && character === "`" && quote !== "'") {
88
+ const next = input[index + 1];
89
+ if (next !== undefined) {
90
+ current += next;
91
+ index++;
92
+ }
93
+ continue;
94
+ }
95
+ if (!powershell && character === "\\" && quote !== "'") {
96
+ const next = input[index + 1];
97
+ if (next === undefined) {
98
+ current += "\\";
99
+ continue;
100
+ }
101
+ // In double quotes, Bash preserves backslashes before ordinary characters.
102
+ if (quote === '"' && !['$', '`', '"', "\\", "\n"].includes(next)) {
103
+ current += "\\";
104
+ continue;
105
+ }
106
+ current += next;
107
+ index++;
108
+ continue;
109
+ }
110
+ if (quote) {
111
+ if (character === quote) quote = undefined;
112
+ else current += character;
113
+ continue;
114
+ }
115
+ if (character === "'" || character === '"') {
116
+ quote = character;
117
+ continue;
118
+ }
119
+ if (/\s/u.test(character)) {
120
+ if (current) words.push(current);
121
+ current = "";
122
+ continue;
123
+ }
124
+ current += character;
125
+ }
126
+ if (current) words.push(current);
127
+ return words;
128
+ }
129
+
130
+ const SEARCH_OPTIONS_WITH_VALUES = new Set([
131
+ "-a", "--after-context", "-b", "--before-context", "-c", "--context", "--context-separator",
132
+ "-d", "--max-depth", "--dfa-size-limit", "-f", "--file", "-g", "--glob", "--iglob",
133
+ "-m", "--max-count", "--max-columns", "--path-separator", "--pre", "--pre-glob",
134
+ "--regex-size-limit", "--replace", "--sort", "--sortr", "-t", "--type", "--type-not",
135
+ "--encoding", "--engine", "--field-context-separator", "--field-match-separator", "--hostname-bin",
136
+ "--binary-files", "--exclude", "--exclude-from", "--exclude-dir", "--include", "--label",
137
+ ]);
138
+
139
+ function searchPattern(executable: string, words: string[]): string | undefined {
140
+ const args = words.slice(1);
141
+ for (let index = 0; index < args.length; index++) {
142
+ const argument = args[index] ?? "";
143
+ const lower = argument.toLowerCase();
144
+ if (executable === "select-string") {
145
+ if (lower === "-pattern") return args[index + 1];
146
+ if (lower.startsWith("-pattern:")) return argument.slice(argument.indexOf(":") + 1);
147
+ if (!argument.startsWith("-")) return argument;
148
+ continue;
149
+ }
150
+ if (lower === "-e" || lower === "--regexp") return args[index + 1];
151
+ if (lower.startsWith("--regexp=")) return argument.slice(argument.indexOf("=") + 1);
152
+ if (/^-e.+/u.test(argument)) return argument.slice(2);
153
+ if (argument === "--") return args[index + 1];
154
+ const optionName = lower.split("=", 1)[0] ?? lower;
155
+ if (SEARCH_OPTIONS_WITH_VALUES.has(optionName) && !argument.includes("=")) {
156
+ index++;
157
+ continue;
158
+ }
159
+ if (argument.startsWith("-")) continue;
160
+ return argument;
161
+ }
162
+ return undefined;
163
+ }
164
+
165
+ function summarizeTextSearch(executable: string, words: string[]): string {
166
+ const pattern = searchPattern(executable, words);
167
+ return pattern === undefined ? "Search text" : `Search text ${JSON.stringify(pattern)}`;
168
+ }
169
+
170
+ function formatTargets(targets: string[]): string {
171
+ const visible = targets.slice(0, 2).map((target) => JSON.stringify(target)).join(", ");
172
+ const remaining = targets.length - 2;
173
+ return remaining > 0 ? `${visible} + ${remaining} more` : visible;
174
+ }
175
+
176
+ function plainOperands(words: string[]): string[] {
177
+ const separator = words.indexOf("--");
178
+ if (separator >= 0) return words.slice(separator + 1);
179
+ return words.slice(1).filter((word) => !word.startsWith("-"));
180
+ }
181
+
182
+ function optionValue(words: string[], names: string[]): string | undefined {
183
+ for (let index = 1; index < words.length; index++) {
184
+ const word = words[index] ?? "";
185
+ const lower = word.toLowerCase();
186
+ if (names.includes(lower)) return words[index + 1];
187
+ const prefix = names.find((name) => lower.startsWith(`${name}=`) || lower.startsWith(`${name}:`));
188
+ if (prefix) return word.slice(prefix.length + 1);
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ const FD_OPTIONS_WITH_VALUES = new Set([
194
+ "-d", "--max-depth", "-e", "--extension", "-E", "--exclude", "-t", "--type",
195
+ "-x", "--exec", "-X", "--exec-batch", "-j", "--threads", "--base-directory",
196
+ "--changed-before", "--changed-within", "--format", "--glob", "--path-separator",
197
+ "--search-path", "--size", "--strip-cwd-prefix",
198
+ ].map((option) => option.toLowerCase()));
199
+
200
+ function fdOperands(words: string[]): string[] {
201
+ const operands: string[] = [];
202
+ for (let index = 1; index < words.length; index++) {
203
+ const word = words[index] ?? "";
204
+ if (word === "--") return [...operands, ...words.slice(index + 1)];
205
+ const optionName = word.toLowerCase().split("=", 1)[0] ?? word.toLowerCase();
206
+ if (FD_OPTIONS_WITH_VALUES.has(optionName) && !word.includes("=")) {
207
+ index++;
208
+ continue;
209
+ }
210
+ if (!word.startsWith("-")) operands.push(word);
211
+ }
212
+ return operands;
213
+ }
214
+
215
+ function summarizeFileDiscovery(executable: string, words: string[]): string {
216
+ if (executable === "find") {
217
+ const paths: string[] = [];
218
+ for (let index = 1; index < words.length; index++) {
219
+ const word = words[index] ?? "";
220
+ if (word.startsWith("-") || word === "!" || word === "(") break;
221
+ paths.push(word);
222
+ }
223
+ const pattern = optionValue(words, ["-name", "-iname", "-path", "-ipath", "-regex"]);
224
+ const location = formatTargets(paths.length > 0 ? paths : ["."]);
225
+ return pattern ? `Find files ${JSON.stringify(pattern)} in ${location}` : `Find files in ${location}`;
226
+ }
227
+ if (executable === "fd") {
228
+ const operands = fdOperands(words);
229
+ const pattern = operands[0];
230
+ const locations = operands.slice(1);
231
+ if (!pattern) return "Find files";
232
+ return locations.length > 0
233
+ ? `Find files ${JSON.stringify(pattern)} in ${formatTargets(locations)}`
234
+ : `Find files ${JSON.stringify(pattern)}`;
235
+ }
236
+ const explicitPath = optionValue(words, ["-path", "-literalpath"]);
237
+ const targets = explicitPath ? [explicitPath] : plainOperands(words);
238
+ return targets.length > 0 ? `List files ${formatTargets(targets)}` : "List files";
239
+ }
240
+
241
+ function summarizeFileOperation(executable: string, words: string[]): string | undefined {
242
+ const targets = plainOperands(words);
243
+ if (targets.length === 0) return undefined;
244
+ if (["rm", "del", "erase", "remove-item"].includes(executable)) return `Run ${executable} ${formatTargets(targets)}`;
245
+ if (["cat", "type", "get-content"].includes(executable)) return `Read file ${formatTargets(targets)}`;
246
+ if (["mkdir", "md", "new-item"].includes(executable)) return `Create ${formatTargets(targets)}`;
247
+ if (["rmdir", "rd"].includes(executable)) return `Remove directory ${formatTargets(targets)}`;
248
+ if (["touch"].includes(executable)) return `Touch ${formatTargets(targets)}`;
249
+ if (["cp", "copy", "copy-item"].includes(executable) && targets.length >= 2) {
250
+ return `Copy ${JSON.stringify(targets[0])} to ${JSON.stringify(targets.at(-1))}`;
251
+ }
252
+ if (["mv", "move", "move-item", "ren", "rename-item"].includes(executable) && targets.length >= 2) {
253
+ return `Move ${JSON.stringify(targets[0])} to ${JSON.stringify(targets.at(-1))}`;
254
+ }
255
+ return undefined;
82
256
  }
83
257
 
84
- /** A calm collapsed label that describes intent without exposing command arguments. */
258
+ /** A calm collapsed label that describes intent while retaining primary command targets. */
85
259
  export function summarizeShellCommand(name: string, command: string): string {
86
260
  const allSegments = shellSegments(normalizeLineEndings(command));
87
261
  const segments = allSegments.filter((segment) => !/^\s*(?:cd|pushd|popd)\b/iu.test(segment));
88
262
  const primary = segments[0] ?? allSegments[0] ?? "";
89
263
  if (!primary) return name === "powershell" ? "Prepare PowerShell command" : "Prepare shell command";
90
264
 
91
- const words = primary.trim().split(/\s+/u).map(unquote);
265
+ const words = shellWords(primary, name === "powershell");
92
266
  while (words.length > 0) {
93
267
  if (/^[A-Za-z_][A-Za-z\d_]*=.*/u.test(words[0] ?? "")
94
268
  || ["command", "env", "sudo", "time"].includes((words[0] ?? "").toLowerCase())) {
@@ -117,11 +291,11 @@ export function summarizeShellCommand(name: string, command: string): string {
117
291
  } else if (executable === "git" && ["fetch", "pull", "push", "clone"].includes(action)) {
118
292
  summary = `${action[0]!.toUpperCase()}${action.slice(1)} repository`;
119
293
  } else if (["rg", "grep", "select-string"].includes(executable)) {
120
- summary = "Search text";
294
+ summary = summarizeTextSearch(executable, words);
121
295
  } else if (["find", "fd", "get-childitem", "ls", "dir"].includes(executable)) {
122
- summary = executable === "ls" || executable === "dir" || executable === "get-childitem" ? "List files" : "Find files";
123
- } else if (["cat", "type", "get-content"].includes(executable)) {
124
- summary = "Read file content";
296
+ summary = summarizeFileDiscovery(executable, words);
297
+ } else if (summarizeFileOperation(executable, words)) {
298
+ summary = summarizeFileOperation(executable, words)!;
125
299
  } else if (["tsc", "mypy", "pyright"].includes(executable)) {
126
300
  summary = "Check types";
127
301
  } else if (["jest", "vitest", "pytest", "invoke-pester"].includes(executable)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-compact-tools",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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",