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 CHANGED
@@ -1,61 +1,19 @@
1
1
  # pi-compact-tools
2
2
 
3
- Compact, expandable rendering for Pi's built-in tools, with a polished GitHub Dark theme.
3
+ Compact, expandable rendering for Pi's built-in tools, with a GitHub Dark theme.
4
4
 
5
- > Formerly `@nedleeds/pi-compact-ui`. New installations should use `pi-compact-tools`.
5
+ ![Compact rows for every built-in tool](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/compact-workflow.gif)
6
6
 
7
- ## Demo
7
+ ![Syntax-highlighted edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
8
8
 
9
- The UI uses portable Unicode symbols and does not require a Nerd Font.
10
-
11
- ### Compact tool workflow
12
-
13
- Active, successful, and failed tool calls stay compact while preserving useful status and timing information.
14
-
15
- ![Compact tool workflow with Nerd Font](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/nerd-font-workflow-optimized.gif)
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
- ![Spinner and elapsed-time display](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/spinner-duration-optimized.gif)
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
- ![Edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
9
+ ![Numbered read code view](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/code-aware-results.gif)
39
10
 
40
11
  ## Features
41
12
 
42
- - Compact rendering for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
43
- - One continuously glowing working label above the input prompt across thinking and tool execution
44
- - Semantic built-in progress labels and automatically cleaned custom/MCP tool names; collapsed text-search rows include the searched pattern
45
- - Four-step `Ctrl+T` thinking cycle when provider detail is available: summary, detail, summary, then hidden
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
- Or install from GitHub:
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
- Configuration is optional. Create `~/.pi/agent/compact-tools.json` for global settings or `<project>/.pi/compact-tools.json` for a trusted project.
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
- Every tool call title uses `⦁`. Pending calls use dark gray; active calls smoothly pulse through fourteen cosine-eased RGB frames at a 45 ms interval without disappearing. Animation starts while arguments stream, so `write` and `edit` remain visibly active before their filesystem operation begins. Successful titles use green, and failed titles use red. `Done`, `Failed`, elapsed time, and line counts use the same neutral control color without printing inline click or keyboard hints. A single Pi working label animates independently of token arrival and reports `Thinking…`, `Responding…`, semantic built-in states such as `Reading file…`, or cleaned custom states such as `Using jira search…`. Invocation arguments remain in the tool row instead of being duplicated above the prompt. No separate spinner glyph is shown above the prompt.
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+T` cycles every thinking block through:
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 = ansi256ToRgb(index);
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
- export function colorizeRgb(theme: Theme, color: Rgb, text: string): string {
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[38;5;${rgbToAnsi256(color)}m${text}\x1b[39m`;
77
+ return `\x1b[${set};5;${rgbToAnsi256(color)}m${text}\x1b[${reset}m`;
63
78
  }
64
- return `\x1b[38;2;${color.r};${color.g};${color.b}m${text}\x1b[39m`;
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
- if (typeof theme.getColorMode === "function" && theme.getColorMode() === "256color") {
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
- return `${(elapsedMs / 1000).toFixed(3)}s`;
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 { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
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
- const highlighted = highlightCodeLines(lines, getLanguageFromPath(path), theme);
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 { getLanguageFromPath, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
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 getLanguageFromPath(`fence.${name}`) ?? name;
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
- pi.on("agent_start", () => this.setMessage("Thinking…"));
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.0",
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/nerd-font-workflow-optimized.gif"
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.85.1",
53
- "@earendil-works/pi-tui": "0.85.1",
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"