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 +3 -3
- package/extensions/compact-tools-invocation.ts +182 -8
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
81
|
-
|
|
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
|
|
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
|
|
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 =
|
|
294
|
+
summary = summarizeTextSearch(executable, words);
|
|
121
295
|
} else if (["find", "fd", "get-childitem", "ls", "dir"].includes(executable)) {
|
|
122
|
-
summary = executable
|
|
123
|
-
} else if (
|
|
124
|
-
summary =
|
|
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)) {
|