pi-compact-tools 0.8.0 → 0.8.3
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 +15 -125
- package/extensions/compact-tools-color.ts +24 -8
- package/extensions/compact-tools-core.ts +11 -1
- package/extensions/compact-tools-language.ts +146 -0
- package/extensions/compact-tools-layout.ts +7 -2
- package/extensions/compact-tools-markdown.ts +3 -2
- package/extensions/compact-tools-progress.ts +11 -1
- package/extensions/compact-tools.ts +8 -3
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,61 +1,19 @@
|
|
|
1
1
|
# pi-compact-tools
|
|
2
2
|
|
|
3
|
-
Compact, expandable rendering for Pi's built-in tools, with a
|
|
3
|
+
Compact, expandable rendering for Pi's built-in tools, with a GitHub Dark theme.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Compact tool workflow
|
|
12
|
-
|
|
13
|
-
Active, successful, and failed tool calls stay compact while preserving useful status and timing information.
|
|
14
|
-
|
|
15
|
-

|
|
16
|
-
|
|
17
|
-
### Progress and duration display
|
|
18
|
-
|
|
19
|
-
The working label above the prompt uses the Thinking summary color with a soft white light sweep. Every tool state uses the same `⦁` glyph; running tools use a fourteen-frame cosine-eased RGB pulse between dim and bright gray, then settle to green on success or red on failure. Elapsed time remains visible in the neutral control color.
|
|
20
|
-
|
|
21
|
-

|
|
22
|
-
|
|
23
|
-
### Code-aware results
|
|
24
|
-
|
|
25
|
-
Completed `edit` rows render a syntax-highlighted diff: a line-number and `+`/`-` gutter beside the code, tinted rows for additions and deletions, and `⋮` where context is skipped. Collapsed rows keep one line of context around each change so the edit itself stays visible; expanding with a click or `Ctrl+O` restores the full context the patch carries.
|
|
26
|
-
|
|
27
|
-
```text
|
|
28
|
-
⦁ edit src/load.ts
|
|
29
|
-
│ 3 export async function load(path: string): Promise<string> {
|
|
30
|
-
│ 4 - const raw = await readFile(path, "utf8");
|
|
31
|
-
│ 4 + const raw = await readFile(path, "utf-8");
|
|
32
|
-
│ 5 return raw.trim();
|
|
33
|
-
└─ Done in 0.012s (2 lines)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`read` and `write` results use the same numbered, syntax-highlighted layout without the diff signs. `read` line numbers start at the requested `offset`, and Pi's continuation notice (`[Showing lines …]`) is shown as a dim note below the code. Markdown files keep their exact source but style headings, lists, quotes, links, emphasis, and inline code, while fenced blocks are highlighted in their declared language. Files without a recognized language keep the plain output color instead of a guessed highlight.
|
|
37
|
-
|
|
38
|
-

|
|
9
|
+

|
|
39
10
|
|
|
40
11
|
## Features
|
|
41
12
|
|
|
42
|
-
- Compact
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- Millisecond-precision duration and completed output line counts with neutral status text
|
|
47
|
-
- Per-tool hidden or persistent bounded-preview collapsed state with `auto_compact: true` or `false`
|
|
48
|
-
- Primary invocation targets such as paths and patterns stay visible, including common collapsed shell file operations
|
|
49
|
-
- Click and `Ctrl+O` follow Pi's built-in expansion state while preview rows collapse back to their preview
|
|
50
|
-
- Edit results render as syntax-highlighted diffs with a line-number gutter, tinted change rows, and `⋮` context breaks
|
|
51
|
-
- `read` and `write` contents use the same numbered, syntax-highlighted layout, with `read` line numbers following `offset`
|
|
52
|
-
- Markdown files keep their exact source but style headings, lists, quotes, links, and inline code, and highlight fenced code blocks in their declared language
|
|
53
|
-
- Configurable `previewLines` prevents large diffs, HTML, and shell output from flooding the transcript
|
|
54
|
-
- Width-cached expanded results, edit-diff processing, and unchanged result reuse during status animation
|
|
55
|
-
- Dark `│` and `└─` visual grouping
|
|
56
|
-
- Distinct cool-blue thinking text
|
|
57
|
-
- Truecolor and 256-color terminal support, plus Windows, Unix, and classic Mac line endings
|
|
58
|
-
- No Nerd Font requirement
|
|
13
|
+
- Compact rows for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`, with status, duration, and line count
|
|
14
|
+
- Syntax-highlighted edit diffs and numbered `read`/`write` code views, including Markdown code fences
|
|
15
|
+
- Animated working label above the prompt and a `Ctrl+T` thinking summary/detail cycle
|
|
16
|
+
- No Nerd Font required
|
|
59
17
|
|
|
60
18
|
## Install
|
|
61
19
|
|
|
@@ -63,93 +21,27 @@ Completed `edit` rows render a syntax-highlighted diff: a line-number and `+`/`-
|
|
|
63
21
|
pi install npm:pi-compact-tools
|
|
64
22
|
```
|
|
65
23
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
pi install git:github.com/nedleeds/pi-compact-tools
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Restart Pi or run `/reload`.
|
|
73
|
-
|
|
74
|
-
Migrating from the previous package name:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
pi remove npm:@nedleeds/pi-compact-ui
|
|
78
|
-
pi install npm:pi-compact-tools
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Theme
|
|
82
|
-
|
|
83
|
-
Select `github-dark-pro` in Pi's settings, or add it to `~/.pi/agent/settings.json`:
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"theme": "github-dark-pro"
|
|
88
|
-
}
|
|
89
|
-
```
|
|
24
|
+
Then restart Pi or run `/reload`. For the bundled theme, set `"theme": "github-dark-pro"` in `~/.pi/agent/settings.json`.
|
|
90
25
|
|
|
91
26
|
## Configuration
|
|
92
27
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Default configuration:
|
|
28
|
+
Optional. Put it in `~/.pi/agent/compact-tools.json` or `<project>/.pi/compact-tools.json`:
|
|
96
29
|
|
|
97
30
|
```json
|
|
98
31
|
{
|
|
99
|
-
"$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
|
|
100
32
|
"tools": ["read", "write", "edit", "bash"],
|
|
101
33
|
"previewLines": 10,
|
|
102
|
-
"auto_compact": {
|
|
103
|
-
"read": true,
|
|
104
|
-
"write": true,
|
|
105
|
-
"edit": false,
|
|
106
|
-
"bash": true
|
|
107
|
-
}
|
|
34
|
+
"auto_compact": { "read": true, "write": true, "edit": false, "bash": true }
|
|
108
35
|
}
|
|
109
36
|
```
|
|
110
37
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`auto_compact` controls each tool's initial result state:
|
|
114
|
-
|
|
115
|
-
- `true` starts with the result hidden.
|
|
116
|
-
- `false` starts with at most `previewLines` rendered rows visible.
|
|
117
|
-
- Clicking or pressing `Ctrl+O` expands the complete result through Pi's built-in expansion state.
|
|
118
|
-
|
|
119
|
-
`previewLines` accepts `1`–`100` and defaults to `10`. It limits the collapsed preview; the complete result remains available. Unspecified entries inherit the previous configuration layer. By default, `read`, `write`, and `bash` start hidden, while `edit` starts with a short diff preview centered on the changed lines.
|
|
120
|
-
|
|
121
|
-
To enable all Unix-compatible tools:
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"tools": ["read", "write", "edit", "bash", "grep", "find", "ls"]
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Add `"powershell"` on Windows if desired.
|
|
38
|
+
- `tools`: tools to render compactly. Also available: `grep`, `find`, `ls`, `powershell`.
|
|
39
|
+
- `auto_compact`: `true` starts hidden, `false` starts with a preview of up to `previewLines` rows (`1`–`100`).
|
|
130
40
|
|
|
131
41
|
## Controls
|
|
132
42
|
|
|
133
|
-
`Ctrl+
|
|
134
|
-
|
|
135
|
-
```text
|
|
136
|
-
one-line summary → summary + │-indented detail → one-line summary → Thinking...
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
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.
|
|
140
|
-
|
|
141
|
-
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 changed `-`/`+` 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.
|
|
142
|
-
|
|
143
|
-
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.
|
|
144
|
-
|
|
145
|
-
## Notes
|
|
146
|
-
|
|
147
|
-
- Pi `0.85.1+` and Node.js `22.19.0+` are recommended.
|
|
148
|
-
- The working indicator and mouse interaction are provided by Pi's TUI; `Ctrl+O` also works in regular mode.
|
|
149
|
-
- Duration covers the full rendered tool-call lifecycle, including streamed `write` and `edit` arguments.
|
|
150
|
-
- Syntax highlighting is skipped, keeping line numbers, for content over 512 KB, 10,000 lines, or a single 4 KB line. Pi already truncates `read` output to 2,000 lines or 50 KB.
|
|
151
|
-
- Highlighting runs per diff hunk or `read` range, so a hunk or `offset` that starts inside a block comment or Markdown code fence may color its first lines as surrounding text.
|
|
152
|
-
- Selected tools override definitions with the same names so their compact renderers are restored reliably after `/reload`; other extensions overriding those tools may conflict by load order.
|
|
43
|
+
- Click a row or press `Ctrl+O` to expand or collapse results.
|
|
44
|
+
- `Ctrl+T` cycles thinking: summary → detail → summary → hidden.
|
|
153
45
|
|
|
154
46
|
## Development
|
|
155
47
|
|
|
@@ -159,8 +51,6 @@ npm run check
|
|
|
159
51
|
pi -e .
|
|
160
52
|
```
|
|
161
53
|
|
|
162
|
-
Pi extensions execute with full system access. Review third-party extension source before installation.
|
|
163
|
-
|
|
164
54
|
## License
|
|
165
55
|
|
|
166
56
|
MIT
|
|
@@ -25,16 +25,29 @@ function ansi256ToRgb(index: number): Rgb {
|
|
|
25
25
|
return { r: gray, g: gray, b: gray };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const ANSI_256_RGB: readonly Rgb[] = Array.from({ length: 256 }, (_, index) => ansi256ToRgb(index));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The nearest-color search runs per character on 256-color terminals, so both the
|
|
32
|
+
* palette and the answers are kept. Callers interpolate between a handful of theme
|
|
33
|
+
* colors, so the memo holds tens of entries rather than growing with output size.
|
|
34
|
+
*/
|
|
35
|
+
const ansi256ByRgb = new Map<number, number>();
|
|
36
|
+
|
|
28
37
|
function rgbToAnsi256(rgb: Rgb): number {
|
|
38
|
+
const key = (rgb.r << 16) | (rgb.g << 8) | rgb.b;
|
|
39
|
+
const memoized = ansi256ByRgb.get(key);
|
|
40
|
+
if (memoized !== undefined) return memoized;
|
|
29
41
|
let bestIndex = 0;
|
|
30
42
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
31
43
|
for (let index = 0; index < 256; index++) {
|
|
32
|
-
const candidate =
|
|
44
|
+
const candidate = ANSI_256_RGB[index]!;
|
|
33
45
|
const distance = (rgb.r - candidate.r) ** 2 + (rgb.g - candidate.g) ** 2 + (rgb.b - candidate.b) ** 2;
|
|
34
46
|
if (distance >= bestDistance) continue;
|
|
35
47
|
bestIndex = index;
|
|
36
48
|
bestDistance = distance;
|
|
37
49
|
}
|
|
50
|
+
ansi256ByRgb.set(key, bestIndex);
|
|
38
51
|
return bestIndex;
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -57,11 +70,17 @@ export function interpolateRgb(from: Rgb, to: Rgb, amount: number): Rgb {
|
|
|
57
70
|
};
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
/** Shared by the foreground (38/39) and background (48/49) SGR pairs. */
|
|
74
|
+
function paintRgb(theme: Theme, color: Rgb, text: string, set: 38 | 48): string {
|
|
75
|
+
const reset = set === 38 ? 39 : 49;
|
|
61
76
|
if (typeof theme.getColorMode === "function" && theme.getColorMode() === "256color") {
|
|
62
|
-
return `\x1b[
|
|
77
|
+
return `\x1b[${set};5;${rgbToAnsi256(color)}m${text}\x1b[${reset}m`;
|
|
63
78
|
}
|
|
64
|
-
return `\x1b[
|
|
79
|
+
return `\x1b[${set};2;${color.r};${color.g};${color.b}m${text}\x1b[${reset}m`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function colorizeRgb(theme: Theme, color: Rgb, text: string): string {
|
|
83
|
+
return paintRgb(theme, color, text, 38);
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
function isDarkTheme(theme: Theme): boolean {
|
|
@@ -82,8 +101,5 @@ export function diffTintRgb(theme: Theme, color: ThemeForeground, amount = 0.16)
|
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
export function fillRgb(theme: Theme, color: Rgb, text: string): string {
|
|
85
|
-
|
|
86
|
-
return `\x1b[48;5;${rgbToAnsi256(color)}m${text}\x1b[49m`;
|
|
87
|
-
}
|
|
88
|
-
return `\x1b[48;2;${color.r};${color.g};${color.b}m${text}\x1b[49m`;
|
|
104
|
+
return paintRgb(theme, color, text, 48);
|
|
89
105
|
}
|
|
@@ -38,6 +38,16 @@ export function classifyCallStatus(isError: boolean, executionStarted: boolean,
|
|
|
38
38
|
return executionStarted ? "running" : "pending";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Sub-minute durations keep millisecond precision; longer runs switch to the
|
|
43
|
+
* minute and hour grouping Pi's own bash and powershell renderers use.
|
|
44
|
+
*/
|
|
41
45
|
export function formatDurationMs(elapsedMs: number): string {
|
|
42
|
-
|
|
46
|
+
const seconds = elapsedMs / 1000;
|
|
47
|
+
if (seconds < 60) return `${seconds.toFixed(3)}s`;
|
|
48
|
+
const totalSeconds = Math.floor(seconds);
|
|
49
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
50
|
+
const remainderSeconds = totalSeconds % 60;
|
|
51
|
+
if (minutes < 60) return `${minutes}m ${remainderSeconds}s`;
|
|
52
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainderSeconds}s`;
|
|
43
53
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pi resolves a language from the file extension and deliberately never falls back to
|
|
5
|
+
* content auto-detection, because highlight.js guesses confidently and wrongly on the
|
|
6
|
+
* things people read most: prose scores as AppleScript or LiveCode, logs and `git log`
|
|
7
|
+
* output as Apache, a one-line TypeScript statement as AutoIt. Relevance scores do not
|
|
8
|
+
* separate those from real matches, and the library is not reachable from this package
|
|
9
|
+
* anyway — Pi keeps it nested.
|
|
10
|
+
*
|
|
11
|
+
* So this module only widens the deterministic signals: names that identify a file
|
|
12
|
+
* without an extension, extensions Pi's table omits, and an interpreter the file
|
|
13
|
+
* declares itself. Anything still unknown stays unhighlighted rather than guessed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Files whose name, not extension, states the language. Keyed by lowercased basename. */
|
|
17
|
+
const FILENAME_LANGUAGES: Readonly<Record<string, string>> = {
|
|
18
|
+
dockerfile: "dockerfile",
|
|
19
|
+
containerfile: "dockerfile",
|
|
20
|
+
makefile: "makefile",
|
|
21
|
+
gnumakefile: "makefile",
|
|
22
|
+
"cmakelists.txt": "cmake",
|
|
23
|
+
rakefile: "ruby",
|
|
24
|
+
gemfile: "ruby",
|
|
25
|
+
guardfile: "ruby",
|
|
26
|
+
podfile: "ruby",
|
|
27
|
+
brewfile: "ruby",
|
|
28
|
+
vagrantfile: "ruby",
|
|
29
|
+
jenkinsfile: "groovy",
|
|
30
|
+
".bashrc": "bash",
|
|
31
|
+
".bash_profile": "bash",
|
|
32
|
+
".bash_aliases": "bash",
|
|
33
|
+
".zshrc": "bash",
|
|
34
|
+
".zprofile": "bash",
|
|
35
|
+
".profile": "bash",
|
|
36
|
+
".editorconfig": "ini",
|
|
37
|
+
".npmrc": "ini",
|
|
38
|
+
".yarnrc": "ini",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Extensions Pi's own table does not cover. Unsupported names degrade to plain text. */
|
|
42
|
+
const EXTENSION_LANGUAGES: Readonly<Record<string, string>> = {
|
|
43
|
+
mts: "typescript",
|
|
44
|
+
cts: "typescript",
|
|
45
|
+
jsonc: "json",
|
|
46
|
+
json5: "json",
|
|
47
|
+
bat: "dos",
|
|
48
|
+
cmd: "dos",
|
|
49
|
+
psm1: "powershell",
|
|
50
|
+
psd1: "powershell",
|
|
51
|
+
ini: "ini",
|
|
52
|
+
cfg: "ini",
|
|
53
|
+
conf: "ini",
|
|
54
|
+
properties: "properties",
|
|
55
|
+
diff: "diff",
|
|
56
|
+
patch: "diff",
|
|
57
|
+
svg: "xml",
|
|
58
|
+
gradle: "groovy",
|
|
59
|
+
groovy: "groovy",
|
|
60
|
+
dart: "dart",
|
|
61
|
+
nix: "nix",
|
|
62
|
+
zig: "zig",
|
|
63
|
+
mm: "objectivec",
|
|
64
|
+
pl: "perl",
|
|
65
|
+
pm: "perl",
|
|
66
|
+
rake: "ruby",
|
|
67
|
+
kts: "kotlin",
|
|
68
|
+
sc: "scala",
|
|
69
|
+
vb: "vbnet",
|
|
70
|
+
jl: "julia",
|
|
71
|
+
cr: "crystal",
|
|
72
|
+
awk: "awk",
|
|
73
|
+
tcl: "tcl",
|
|
74
|
+
f90: "fortran",
|
|
75
|
+
f95: "fortran",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Interpreters seen after `#!`, with any version suffix already stripped. */
|
|
79
|
+
const INTERPRETER_LANGUAGES: Readonly<Record<string, string>> = {
|
|
80
|
+
bash: "bash",
|
|
81
|
+
sh: "bash",
|
|
82
|
+
zsh: "bash",
|
|
83
|
+
dash: "bash",
|
|
84
|
+
ksh: "bash",
|
|
85
|
+
fish: "fish",
|
|
86
|
+
python: "python",
|
|
87
|
+
node: "javascript",
|
|
88
|
+
deno: "typescript",
|
|
89
|
+
bun: "typescript",
|
|
90
|
+
ruby: "ruby",
|
|
91
|
+
perl: "perl",
|
|
92
|
+
php: "php",
|
|
93
|
+
lua: "lua",
|
|
94
|
+
awk: "awk",
|
|
95
|
+
gawk: "awk",
|
|
96
|
+
rscript: "r",
|
|
97
|
+
pwsh: "powershell",
|
|
98
|
+
powershell: "powershell",
|
|
99
|
+
tclsh: "tcl",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function basename(path: string): string {
|
|
103
|
+
const separator = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
104
|
+
return separator === -1 ? path : path.slice(separator + 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `python3.12` and `ruby2.7` name the same languages as `python` and `ruby`. */
|
|
108
|
+
function stripVersion(interpreter: string): string {
|
|
109
|
+
return interpreter.replace(/[\d.]+$/u, "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read the language a `#!` line declares, e.g. `#!/usr/bin/env -S python3 -u`. */
|
|
113
|
+
export function languageFromShebang(line: string): string | undefined {
|
|
114
|
+
const shebang = line.match(/^#!\s*(\S+)((?:\s+\S+)*)$/u);
|
|
115
|
+
if (!shebang) return undefined;
|
|
116
|
+
const command = basename(shebang[1]!).toLowerCase();
|
|
117
|
+
if (command !== "env") return INTERPRETER_LANGUAGES[stripVersion(command)];
|
|
118
|
+
// `env` forwards to the first argument that is not one of its own flags.
|
|
119
|
+
for (const argument of shebang[2]!.trim().split(/\s+/u)) {
|
|
120
|
+
if (!argument || argument.startsWith("-") || argument.includes("=")) continue;
|
|
121
|
+
return INTERPRETER_LANGUAGES[stripVersion(basename(argument).toLowerCase())];
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve a language from the path alone, without looking at file contents. */
|
|
127
|
+
export function languageFromPath(path: string): string | undefined {
|
|
128
|
+
const fromPi = getLanguageFromPath(path);
|
|
129
|
+
if (fromPi) return fromPi;
|
|
130
|
+
const name = basename(path).toLowerCase();
|
|
131
|
+
const byName = FILENAME_LANGUAGES[name];
|
|
132
|
+
if (byName) return byName;
|
|
133
|
+
// `.env`, `.env.local`, and `.env.production` all hold the same KEY=value shape.
|
|
134
|
+
if (name === ".env" || name.startsWith(".env.")) return "ini";
|
|
135
|
+
const extension = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : undefined;
|
|
136
|
+
return extension ? EXTENSION_LANGUAGES[extension] : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a language for a file view. `firstLine` is only consulted when it is genuinely
|
|
141
|
+
* the file's first line, so a `read` starting at an offset never mistakes a comment for
|
|
142
|
+
* a shebang.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveLanguage(path: string, firstLine?: string): string | undefined {
|
|
145
|
+
return languageFromPath(path) ?? (firstLine === undefined ? undefined : languageFromShebang(firstLine));
|
|
146
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Component } from "@earendil-works/pi-tui";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
10
|
import { diffTintRgb, fillRgb } from "./compact-tools-color.ts";
|
|
11
11
|
import { normalizeLineEndings } from "./compact-tools-core.ts";
|
|
12
|
+
import { resolveLanguage } from "./compact-tools-language.ts";
|
|
12
13
|
import { highlightMarkdown } from "./compact-tools-markdown.ts";
|
|
13
14
|
import type { ToolArgs } from "./compact-tools-types.ts";
|
|
14
15
|
|
|
@@ -304,7 +305,11 @@ type CodeRowsOptions = {
|
|
|
304
305
|
|
|
305
306
|
/** Render numbered, syntax-highlighted code rows shared by edit diffs and read/write file views. */
|
|
306
307
|
function renderCodeRows(lines: CodeDiffLine[], path: string, theme: Theme, options: CodeRowsOptions): Component {
|
|
307
|
-
|
|
308
|
+
// A shebang only speaks for the file when the first row really is line 1, so a
|
|
309
|
+
// read at an offset or a diff hunk never reads one out of a comment.
|
|
310
|
+
const first = lines[0];
|
|
311
|
+
const shebangLine = first?.lineNumber === 1 && first.kind !== "separator" ? first.content : undefined;
|
|
312
|
+
const highlighted = highlightCodeLines(lines, resolveLanguage(path, shebangLine), theme);
|
|
308
313
|
const numberWidth = Math.max(1, ...lines.map((line) => String(line.lineNumber ?? "").length));
|
|
309
314
|
const prefix = theme.fg("border", " \u2502 ");
|
|
310
315
|
const prefixWidth = visibleWidth(prefix);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { languageFromPath } from "./compact-tools-language.ts";
|
|
2
3
|
|
|
3
4
|
type Fence = { char: string; length: number; language: string | undefined; start: number };
|
|
4
5
|
|
|
@@ -14,7 +15,7 @@ const INLINE = /(`+)(.+?)\1|\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*(?=\S)(.+?)\*\*|__(?
|
|
|
14
15
|
function fenceLanguage(info: string): string | undefined {
|
|
15
16
|
const name = info.toLowerCase();
|
|
16
17
|
if (!name) return undefined;
|
|
17
|
-
return
|
|
18
|
+
return languageFromPath(`fence.${name}`) ?? name;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function highlightInline(text: string, theme: Theme): string {
|
|
@@ -80,24 +80,34 @@ export class ProgressController {
|
|
|
80
80
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
81
81
|
|
|
82
82
|
constructor(pi: ExtensionAPI) {
|
|
83
|
-
|
|
83
|
+
// Every handler leaves early while unbound. Pi keeps dispatching to this
|
|
84
|
+
// extension in non-TUI modes, and guarding at the top keeps that work — and
|
|
85
|
+
// any future handler added here — out of the detached path.
|
|
86
|
+
pi.on("agent_start", () => {
|
|
87
|
+
if (!this.context) return;
|
|
88
|
+
this.setMessage("Thinking…");
|
|
89
|
+
});
|
|
84
90
|
pi.on("message_update", (event) => {
|
|
91
|
+
if (!this.context) return;
|
|
85
92
|
const type = event.assistantMessageEvent.type;
|
|
86
93
|
if (type === "thinking_start" || type === "thinking_delta") this.setMessage("Thinking…");
|
|
87
94
|
else if (type === "text_start" || type === "text_delta") this.setMessage("Responding…");
|
|
88
95
|
else if (type === "toolcall_start" || type === "toolcall_delta") this.setMessage("Preparing tools…");
|
|
89
96
|
});
|
|
90
97
|
pi.on("tool_execution_start", (event) => {
|
|
98
|
+
if (!this.context) return;
|
|
91
99
|
const message = formatToolProgress(event.toolName, event.args ?? {});
|
|
92
100
|
this.activeTools.set(event.toolCallId, message);
|
|
93
101
|
this.setMessage(message);
|
|
94
102
|
});
|
|
95
103
|
pi.on("tool_execution_end", (event) => {
|
|
104
|
+
if (!this.context) return;
|
|
96
105
|
this.activeTools.delete(event.toolCallId);
|
|
97
106
|
const remaining = [...this.activeTools.values()].at(-1);
|
|
98
107
|
this.setMessage(remaining ?? "Processing results…");
|
|
99
108
|
});
|
|
100
109
|
pi.on("agent_end", () => {
|
|
110
|
+
if (!this.context) return;
|
|
101
111
|
this.activeTools.clear();
|
|
102
112
|
this.clearMessage();
|
|
103
113
|
});
|
|
@@ -356,6 +356,14 @@ function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
export default function compactTools(pi: ExtensionAPI): void {
|
|
359
|
+
// Pi caches this factory and re-invokes it with a fresh `pi` for every session
|
|
360
|
+
// replacement (/resume, /new, /fork) as well as /reload, each time starting from
|
|
361
|
+
// an empty tool registry. The module itself survives those invocations, so the
|
|
362
|
+
// memo below has to be dropped here or configure() mistakes the previous
|
|
363
|
+
// instance's registration for this one and never installs the renderers.
|
|
364
|
+
registeredConfiguration = undefined;
|
|
365
|
+
registeredTools.clear();
|
|
366
|
+
|
|
359
367
|
const thinkingCycle = new ThinkingCycleController(pi);
|
|
360
368
|
const progress = new ProgressController(pi);
|
|
361
369
|
// Register once while the extension runtime is being built. In particular, this
|
|
@@ -376,8 +384,5 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
376
384
|
thinkingCycle.dispose();
|
|
377
385
|
progress.dispose();
|
|
378
386
|
runtime.reset(event.reason !== "reload");
|
|
379
|
-
// Some Pi hosts rebuild the active tool registry during reload while keeping
|
|
380
|
-
// this module instance. Force the next session to restore our renderers.
|
|
381
|
-
if (event.reason === "reload") registeredConfiguration = undefined;
|
|
382
387
|
});
|
|
383
388
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
"themes": [
|
|
43
43
|
"./themes/github-dark-pro.json"
|
|
44
44
|
],
|
|
45
|
-
"image": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/
|
|
45
|
+
"image": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/compact-workflow.gif"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@earendil-works/pi-coding-agent": "
|
|
49
|
-
"@earendil-works/pi-tui": "
|
|
48
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
49
|
+
"@earendil-works/pi-tui": ">=0.85.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
53
|
-
"@earendil-works/pi-tui": "0.
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.86.0",
|
|
53
|
+
"@earendil-works/pi-tui": "0.86.0",
|
|
54
54
|
"@types/node": "^24.0.0",
|
|
55
55
|
"tsx": "^4.23.13",
|
|
56
56
|
"typescript": "^5.9.0"
|