@heyhuynhgiabuu/pi-pretty 0.1.6 → 0.1.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
@@ -1,20 +1,17 @@
1
1
  # pi-pretty
2
2
 
3
- A [pi](https://pi.dev) extension that enhances built-in tool output with **syntax highlighting**, **file-type icons**, **tree views**, and **colored status indicators** — all rendered directly in your terminal.
3
+ [![npm version](https://img.shields.io/npm/v/@heyhuynhgiabuu/pi-pretty)](https://www.npmjs.com/package/@heyhuynhgiabuu/pi-pretty)
4
+ [![GitHub release](https://img.shields.io/github/v/release/heyhuynhgiabuu/pi-pretty)](https://github.com/heyhuynhgiabuu/pi-pretty/releases/latest)
4
5
 
5
- > **Status:** Early release.
6
+ A [pi](https://pi.dev) extension that upgrades built-in tool output in the terminal without changing tool behavior.
6
7
 
7
- > Companion to [@heyhuynhgiabuu/pi-diff](https://github.com/heyhuynhgiabuu/pi-diff) which handles `write`/`edit` diffs.
8
+ It currently enhances:
8
9
 
9
- ## Features
10
+ - **`read`**: syntax-highlighted text previews with line numbers, plus inline image rendering when the terminal supports it
11
+ - **`bash`**: colored exit summary (`exit 0`/`exit 1`) with a preview body of command output
12
+ - **`ls` / `find` / `grep`**: Nerd Font file icons with tree/grouped layouts and clearer match rendering
10
13
 
11
- | Tool | Enhancement |
12
- |------|-------------|
13
- | **read** | Syntax-highlighted file content with line numbers (190+ languages via Shiki) |
14
- | **bash** | Colored exit status (`✓ exit 0` / `✗ exit 1`), line count |
15
- | **ls** | Tree-view directory listing with file-type icons (📁🟦🐍🦀…) |
16
- | **find** | Grouped results by directory with file-type icons |
17
- | **grep** | Highlighted pattern matches with file headers and line numbers |
14
+ > Companion to [@heyhuynhgiabuu/pi-diff](https://github.com/heyhuynhgiabuu/pi-diff) for `write`/`edit` diff rendering.
18
15
 
19
16
  ## Install
20
17
 
@@ -22,168 +19,49 @@ A [pi](https://pi.dev) extension that enhances built-in tool output with **synta
22
19
  pi install npm:@heyhuynhgiabuu/pi-pretty
23
20
  ```
24
21
 
25
- Or load directly for development:
22
+ Latest release: https://github.com/heyhuynhgiabuu/pi-pretty/releases/latest
26
23
 
27
- ```bash
28
- pi -e ./src/index.ts
29
- ```
30
-
31
- ## How It Works
32
-
33
- pi-pretty wraps the built-in SDK tools (`createReadTool`, `createBashTool`, `createLsTool`, `createFindTool`, `createGrepTool`). For each tool:
34
-
35
- 1. **Delegates** to the original `execute()` — no behavior changes
36
- 2. **Attaches metadata** to `result.details` for custom rendering
37
- 3. **Renders** enhanced output via `renderCall` / `renderResult`
38
-
39
- The agent sees the same tool results. Only the TUI display changes.
40
-
41
- ## Configuration
42
-
43
- All settings via environment variables. Add to your shell profile or `.envrc`:
44
-
45
- ### Theme
46
-
47
- | Variable | Default | Description |
48
- |----------|---------|-------------|
49
- | `PRETTY_THEME` | `github-dark` | Shiki theme for syntax highlighting |
50
-
51
- ### Limits
52
-
53
- | Variable | Default | Description |
54
- |----------|---------|-------------|
55
- | `PRETTY_MAX_HL_CHARS` | `80000` | Skip syntax highlighting above this |
56
- | `PRETTY_MAX_PREVIEW_LINES` | `80` | Max lines shown in rendered output |
57
- | `PRETTY_CACHE_LIMIT` | `128` | LRU cache entries for highlighted blocks |
58
-
59
- ### Example `.envrc`
24
+ Or load locally:
60
25
 
61
26
  ```bash
62
- export PRETTY_THEME="catppuccin-mocha"
63
- export PRETTY_MAX_PREVIEW_LINES=120
64
- ```
65
-
66
- ## Tool Details
67
-
68
- ### `read` — Syntax Highlighting
69
-
70
- When the agent reads a file, pi-pretty renders it with:
71
- - Shiki syntax highlighting (190+ languages, auto-detected from extension)
72
- - Line numbers in a left gutter
73
- - Long lines truncated with `›` indicator
74
- - Respects `offset` and `limit` parameters
75
-
76
- ### `bash` — Exit Status
77
-
78
- Bash command results show:
79
- - `✓ exit 0` in green for success
80
- - `✗ exit 1` in red for failure
81
- - `⚡ killed` in yellow for terminated processes
82
- - Line count for multi-line output
83
-
84
- ### `ls` — Tree View
85
-
86
- Directory listings rendered as:
87
- ```
88
- 3 entries
89
- ├── 📁 src
90
- ├── 📦 package.json
91
- └── 📖 README.md
27
+ pi -e ./src/index.ts
92
28
  ```
93
29
 
94
- File-type icons auto-detected from extension and filename.
30
+ ## Screenshots
95
31
 
96
- ### `find` Grouped Results
32
+ ![Bash and read rendering](media/bash-and-read.png)
33
+ *`bash` exit summary + output preview, and syntax-highlighted `read` text output.*
97
34
 
98
- Find results grouped by directory:
99
- ```
100
- 5 files
101
- 📁 src/
102
- ├── 🟦 index.ts
103
- └── 🟦 utils.ts
104
- 📁 test/
105
- ├── 🟦 index.test.ts
106
- └── 🟦 utils.test.ts
107
- ```
35
+ ![Icons and grep rendering](media/icons-and-grep.png)
36
+ *`ls`/`find`/`grep` with Nerd Font icons and grouped/tree-oriented rendering.*
108
37
 
109
- ### `grep` — Highlighted Matches
38
+ ![Inline image rendering](media/inline-image.png)
39
+ *`read` rendering an image inline in supported terminals.*
110
40
 
111
- Grep results with file headers and matched text highlighted:
112
- ```
113
- 3 matches
114
- 🟦 src/index.ts
115
- 12 │ const result = await createReadTool(cwd);
116
- 45 │ export function createReadTool(path: string) {
117
-
118
- 🟦 src/utils.ts
119
- 8 │ import { createReadTool } from "./index";
120
- ```
41
+ ## Terminal support for inline images
121
42
 
122
- ## File-Type Icons
123
-
124
- | Icon | Extensions |
125
- |------|-----------|
126
- | 🟦 | `.ts`, `.tsx`, `tsconfig.json` |
127
- | 🟨 | `.js`, `.jsx`, `.mjs`, `.cjs` |
128
- | 🐍 | `.py`, `pyproject.toml` |
129
- | 🦀 | `.rs`, `Cargo.toml` |
130
- | 🔵 | `.go`, `go.mod` |
131
- | ☕ | `.java` |
132
- | 🍊 | `.swift` |
133
- | 💎 | `.rb` |
134
- | 🌐 | `.html` |
135
- | 🎨 | `.css`, `.scss`, `.less` |
136
- | 📋 | `.json`, `.yaml`, `.toml` |
137
- | 📝 | `.md`, `.mdx` |
138
- | 🐚 | `.sh`, `.bash`, `.zsh` |
139
- | 🖼️ | `.png`, `.jpg`, `.svg`, `.webp` |
140
- | 📦 | `package.json` |
141
- | 🐳 | `Dockerfile` |
142
- | 🔐 | `.env`, `.envrc` |
143
- | 📖 | `README.md` |
144
- | ⚖️ | `LICENSE` |
145
-
146
- ## Architecture
43
+ Inline image previews are supported in **Ghostty**, **Kitty**, **iTerm2**, and **WezTerm**.
44
+ When running in **tmux**, pi-pretty uses passthrough escape sequences so inline image protocols still work.
147
45
 
148
- ```
149
- src/
150
- └── index.ts # Extension entry — wraps read/bash/ls/find/grep with pretty rendering
151
- ```
46
+ ## Configuration
152
47
 
153
- ### Key internals
48
+ Optional environment variables:
154
49
 
155
- | Component | Purpose |
156
- |-----------|---------|
157
- | `hlBlock()` | Shiki ANSI highlighting with LRU cache |
158
- | `renderFileContent()` | Line-numbered syntax-highlighted file display |
159
- | `renderBashOutput()` | Colored exit status + stderr detection |
160
- | `renderTree()` | Tree-view with connectors and file icons |
161
- | `renderFindResults()` | Directory-grouped file list with icons |
162
- | `renderGrepResults()` | Pattern-highlighted matches with file headers |
163
- | `fileIcon()` | Extension → emoji icon mapper |
164
- | `lang()` | Extension → Shiki language mapper |
50
+ - `PRETTY_THEME` (default: `github-dark`)
51
+ - `PRETTY_MAX_HL_CHARS` (default: `80000`)
52
+ - `PRETTY_MAX_PREVIEW_LINES` (default: `80`)
53
+ - `PRETTY_CACHE_LIMIT` (default: `128`)
54
+ - `PRETTY_ICONS` (`nerd` by default, set to `none` to disable icons)
165
55
 
166
56
  ## Development
167
57
 
168
58
  ```bash
169
- git clone https://github.com/heyhuynhgiabuu/pi-pretty.git
170
- cd pi-pretty
171
59
  npm install
172
60
  npm run typecheck
173
61
  npm run lint
174
62
  npm test
175
63
  ```
176
64
 
177
- ### Load in pi for testing
178
-
179
- ```bash
180
- pi -e ./src/index.ts
181
- ```
182
-
183
- ## Related
184
-
185
- - [@heyhuynhgiabuu/pi-diff](https://github.com/heyhuynhgiabuu/pi-diff) — Syntax-highlighted diffs for `write`/`edit` tools
186
-
187
65
  ## License
188
66
 
189
67
  MIT — [huynhgiabuu](https://github.com/heyhuynhgiabuu)
package/biome.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
3
- "organizeImports": {
4
- "enabled": true
5
- },
6
- "linter": {
7
- "enabled": true,
8
- "rules": {
9
- "recommended": true
10
- }
11
- },
12
- "formatter": {
13
- "enabled": true,
14
- "indentStyle": "tab",
15
- "lineWidth": 120
16
- }
2
+ "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
3
+ "assist": {
4
+ "enabled": true,
5
+ "actions": {
6
+ "source": {
7
+ "organizeImports": "on"
8
+ }
9
+ }
10
+ },
11
+ "linter": {
12
+ "enabled": true,
13
+ "rules": {
14
+ "recommended": true
15
+ }
16
+ },
17
+ "formatter": {
18
+ "enabled": true,
19
+ "indentStyle": "tab",
20
+ "lineWidth": 120
21
+ }
17
22
  }
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
1
  {
2
- "name": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.1.6",
4
- "description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
5
- "author": "huynhgiabuu",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/heyhuynhgiabuu/pi-pretty.git"
10
- },
11
- "homepage": "https://github.com/heyhuynhgiabuu/pi-pretty#readme",
12
- "bugs": {
13
- "url": "https://github.com/heyhuynhgiabuu/pi-pretty/issues"
14
- },
15
- "keywords": [
16
- "pi-package",
17
- "pi",
18
- "pi-extension",
19
- "syntax-highlighting",
20
- "shiki",
21
- "terminal",
22
- "pretty-print"
23
- ],
24
- "dependencies": {
25
- "@shikijs/cli": "^4.0.2"
26
- },
27
- "peerDependencies": {
28
- "@mariozechner/pi-coding-agent": "*",
29
- "@mariozechner/pi-tui": "*"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^20.0.0",
33
- "typescript": "^5.0.0",
34
- "@biomejs/biome": "^2.3.5",
35
- "vitest": "^4.0.18"
36
- },
37
- "scripts": {
38
- "build": "tsc",
39
- "typecheck": "tsc --noEmit",
40
- "lint": "biome check src/",
41
- "lint:fix": "biome check --fix src/",
42
- "test": "vitest run",
43
- "test:watch": "vitest"
44
- },
45
- "pi": {
46
- "extensions": [
47
- "./src/index.ts"
48
- ]
49
- }
2
+ "name": "@heyhuynhgiabuu/pi-pretty",
3
+ "version": "0.1.8",
4
+ "description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
5
+ "author": "huynhgiabuu",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/heyhuynhgiabuu/pi-pretty.git"
10
+ },
11
+ "homepage": "https://github.com/heyhuynhgiabuu/pi-pretty#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/heyhuynhgiabuu/pi-pretty/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi",
18
+ "pi-extension",
19
+ "syntax-highlighting",
20
+ "shiki",
21
+ "terminal",
22
+ "pretty-print"
23
+ ],
24
+ "dependencies": {
25
+ "@shikijs/cli": "^4.0.2"
26
+ },
27
+ "peerDependencies": {
28
+ "@mariozechner/pi-coding-agent": "*",
29
+ "@mariozechner/pi-tui": "*"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.0.0",
33
+ "typescript": "^5.0.0",
34
+ "@biomejs/biome": "^2.3.5",
35
+ "vitest": "^4.0.18"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "typecheck": "tsc --noEmit",
40
+ "lint": "biome check src/",
41
+ "lint:fix": "biome check --fix src/",
42
+ "test": "vitest run --passWithNoTests",
43
+ "test:watch": "vitest --passWithNoTests"
44
+ },
45
+ "pi": {
46
+ "extensions": [
47
+ "./src/index.ts"
48
+ ]
49
+ }
50
50
  }
package/src/index.ts CHANGED
@@ -33,8 +33,7 @@ import type { BundledLanguage, BundledTheme } from "shiki";
33
33
  // Config
34
34
  // ---------------------------------------------------------------------------
35
35
 
36
- const THEME: BundledTheme =
37
- (process.env.PRETTY_THEME as BundledTheme | undefined) ?? "github-dark";
36
+ const THEME: BundledTheme = (process.env.PRETTY_THEME as BundledTheme | undefined) ?? "github-dark";
38
37
 
39
38
  function envInt(name: string, fallback: number): number {
40
39
  const v = Number.parseInt(process.env[name] ?? "", 10);
@@ -49,7 +48,7 @@ const CACHE_LIMIT = envInt("PRETTY_CACHE_LIMIT", 128);
49
48
  // ANSI
50
49
  // ---------------------------------------------------------------------------
51
50
 
52
- const RST = "\x1b[0m";
51
+ let RST = "\x1b[0m";
53
52
  const BOLD = "\x1b[1m";
54
53
  const DIM = "\x1b[2m";
55
54
  const ITALIC = "\x1b[3m";
@@ -68,7 +67,34 @@ const FG_PURPLE = "\x1b[38;2;170;120;200m";
68
67
 
69
68
  const BG_STDERR = "\x1b[48;2;40;25;25m";
70
69
 
71
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
70
+ const BG_DEFAULT = "\x1b[49m";
71
+ let BG_BASE = BG_DEFAULT; // tool box base bg — updated from theme's toolSuccessBg
72
+
73
+ /** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
74
+ function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
75
+ const m = ansi.match(/\x1b\[(?:38|48);2;(\d+);(\d+);(\d+)m/);
76
+ return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
77
+ }
78
+
79
+ /** Read toolSuccessBg from the pi theme and update BG_BASE + RST.
80
+ * Call once when theme is first available. Idempotent. */
81
+ let _bgBaseResolved = false;
82
+ function resolveBaseBackground(theme: any): void {
83
+ if (_bgBaseResolved || !theme?.getBgAnsi) return;
84
+ _bgBaseResolved = true;
85
+ try {
86
+ const bgAnsi = theme.getBgAnsi("toolSuccessBg");
87
+ const parsed = parseAnsiRgb(bgAnsi);
88
+ if (parsed) {
89
+ BG_BASE = bgAnsi;
90
+ RST = `\x1b[0m${BG_BASE}`;
91
+ }
92
+ } catch { /* ignore — keep defaults */ }
93
+ }
94
+
95
+ const ESC_RE = "\u001b";
96
+ const ANSI_RE = new RegExp(`${ESC_RE}\\[[0-9;]*m`, "g");
97
+ const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([0-9;]*)m`, "g");
72
98
 
73
99
  // ---------------------------------------------------------------------------
74
100
  // Low-contrast fix (same as pi-diff)
@@ -79,19 +105,14 @@ function isLowContrastShikiFg(params: string): boolean {
79
105
  if (params === "38;5;0" || params === "38;5;8") return true;
80
106
  if (!params.startsWith("38;2;")) return false;
81
107
  const parts = params.split(";").map(Number);
82
- if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n)))
83
- return false;
108
+ if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
84
109
  const [, , r, g, b] = parts;
85
110
  const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
86
111
  return luminance < 72;
87
112
  }
88
113
 
89
114
  function normalizeShikiContrast(ansi: string): string {
90
- return ansi.replace(
91
- /\x1b\[([0-9;]*)m/g,
92
- (seq, params: string) =>
93
- isLowContrastShikiFg(params) ? FG_MUTED : seq,
94
- );
115
+ return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => (isLowContrastShikiFg(params) ? FG_MUTED : seq));
95
116
  }
96
117
 
97
118
  // ---------------------------------------------------------------------------
@@ -104,10 +125,7 @@ function strip(s: string): string {
104
125
 
105
126
  function termW(): number {
106
127
  const raw =
107
- process.stdout.columns ||
108
- (process.stderr as any).columns ||
109
- Number.parseInt(process.env.COLUMNS ?? "", 10) ||
110
- 200;
128
+ process.stdout.columns || (process.stderr as any).columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
111
129
  return Math.max(80, Math.min(raw - 4, 210));
112
130
  }
113
131
 
@@ -132,19 +150,54 @@ function lnum(n: number, w: number): string {
132
150
  // ---------------------------------------------------------------------------
133
151
 
134
152
  const EXT_LANG: Record<string, BundledLanguage> = {
135
- ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
136
- mjs: "javascript", cjs: "javascript",
137
- py: "python", rb: "ruby", rs: "rust", go: "go", java: "java",
138
- c: "c", cpp: "cpp", h: "c", hpp: "cpp", cs: "csharp",
139
- swift: "swift", kt: "kotlin",
140
- html: "html", css: "css", scss: "scss", less: "css",
141
- json: "json", jsonc: "jsonc", yaml: "yaml", yml: "yaml", toml: "toml",
142
- md: "markdown", mdx: "mdx", sql: "sql", sh: "bash", bash: "bash", zsh: "bash",
143
- lua: "lua", php: "php", dart: "dart", xml: "xml",
144
- graphql: "graphql", svelte: "svelte", vue: "vue",
145
- dockerfile: "dockerfile", makefile: "make",
146
- zig: "zig", nim: "nim", elixir: "elixir", ex: "elixir",
147
- erb: "erb", hbs: "handlebars",
153
+ ts: "typescript",
154
+ tsx: "tsx",
155
+ js: "javascript",
156
+ jsx: "jsx",
157
+ mjs: "javascript",
158
+ cjs: "javascript",
159
+ py: "python",
160
+ rb: "ruby",
161
+ rs: "rust",
162
+ go: "go",
163
+ java: "java",
164
+ c: "c",
165
+ cpp: "cpp",
166
+ h: "c",
167
+ hpp: "cpp",
168
+ cs: "csharp",
169
+ swift: "swift",
170
+ kt: "kotlin",
171
+ html: "html",
172
+ css: "css",
173
+ scss: "scss",
174
+ less: "css",
175
+ json: "json",
176
+ jsonc: "jsonc",
177
+ yaml: "yaml",
178
+ yml: "yaml",
179
+ toml: "toml",
180
+ md: "markdown",
181
+ mdx: "mdx",
182
+ sql: "sql",
183
+ sh: "bash",
184
+ bash: "bash",
185
+ zsh: "bash",
186
+ lua: "lua",
187
+ php: "php",
188
+ dart: "dart",
189
+ xml: "xml",
190
+ graphql: "graphql",
191
+ svelte: "svelte",
192
+ vue: "vue",
193
+ dockerfile: "dockerfile",
194
+ makefile: "make",
195
+ zig: "zig",
196
+ nim: "nim",
197
+ elixir: "elixir",
198
+ ex: "elixir",
199
+ erb: "erb",
200
+ hbs: "handlebars",
148
201
  };
149
202
 
150
203
  function lang(fp: string): BundledLanguage | undefined {
@@ -157,12 +210,41 @@ function lang(fp: string): BundledLanguage | undefined {
157
210
 
158
211
  // ---------------------------------------------------------------------------
159
212
  // Terminal image rendering (iTerm2 / Kitty / Ghostty inline image protocols)
213
+ // Handles tmux passthrough for image protocols.
160
214
  // ---------------------------------------------------------------------------
161
215
 
162
216
  type ImageProtocol = "iterm2" | "kitty" | "none";
163
217
 
164
- function detectImageProtocol(): ImageProtocol {
218
+ const IS_TMUX = !!process.env.TMUX;
219
+
220
+ /**
221
+ * Detect the outer terminal when running inside tmux.
222
+ * tmux sets TERM_PROGRAM=tmux, but the real terminal is often in
223
+ * the environment of the tmux server or can be inferred.
224
+ */
225
+ function getOuterTerminal(): string {
226
+ // Direct terminal (not in tmux)
165
227
  const term = process.env.TERM_PROGRAM ?? "";
228
+ if (term !== "tmux" && term !== "screen") return term;
229
+
230
+ // Inside tmux: check common env vars that leak through
231
+ // Ghostty sets this; iTerm2 sets LC_TERMINAL
232
+ if (process.env.LC_TERMINAL === "iTerm2") return "iTerm.app";
233
+
234
+ // TERM_PROGRAM_VERSION sometimes survives into tmux
235
+ // Try to detect via COLORTERM or other hints
236
+ if (process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
237
+
238
+ // Default: assume modern terminal if truecolor is supported
239
+ if (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit") {
240
+ // Can't determine exact terminal, but likely modern
241
+ return "unknown-modern";
242
+ }
243
+ return term;
244
+ }
245
+
246
+ function detectImageProtocol(): ImageProtocol {
247
+ const term = getOuterTerminal();
166
248
  // Ghostty and Kitty use the Kitty graphics protocol
167
249
  if (term === "ghostty" || term === "kitty") return "kitty";
168
250
  // iTerm2, WezTerm, Mintty support the iTerm2 protocol
@@ -171,20 +253,30 @@ function detectImageProtocol(): ImageProtocol {
171
253
  return "none";
172
254
  }
173
255
 
256
+ /**
257
+ * Wrap escape sequence for tmux passthrough.
258
+ * tmux requires: ESC Ptmux; <escaped-sequence> ESC \
259
+ * Inner ESC chars must be doubled.
260
+ */
261
+ function tmuxWrap(seq: string): string {
262
+ if (!IS_TMUX) return seq;
263
+ // Double all ESC chars inside the sequence
264
+ const escaped = seq.split("\x1b").join("\x1b\x1b");
265
+ return `\x1bPtmux;${escaped}\x1b\\`;
266
+ }
267
+
174
268
  /**
175
269
  * Render base64 image inline using iTerm2 inline image protocol.
176
270
  * Protocol: ESC ] 1337 ; File=[args] : base64data BEL
177
271
  */
178
- function renderIterm2Image(
179
- base64Data: string,
180
- opts: { width?: string; name?: string } = {},
181
- ): string {
272
+ function renderIterm2Image(base64Data: string, opts: { width?: string; name?: string } = {}): string {
182
273
  const args: string[] = ["inline=1", "preserveAspectRatio=1"];
183
274
  if (opts.width) args.push(`width=${opts.width}`);
184
275
  if (opts.name) args.push(`name=${Buffer.from(opts.name).toString("base64")}`);
185
- const byteSize = Math.ceil(base64Data.length * 3 / 4);
276
+ const byteSize = Math.ceil((base64Data.length * 3) / 4);
186
277
  args.push(`size=${byteSize}`);
187
- return `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
278
+ const seq = `\x1b]1337;File=${args.join(";")}:${base64Data}\x07`;
279
+ return tmuxWrap(seq);
188
280
  }
189
281
 
190
282
  /**
@@ -193,10 +285,7 @@ function renderIterm2Image(
193
285
  * Chunked in 4096-byte pieces as required by protocol.
194
286
  * Supported by: Kitty, Ghostty
195
287
  */
196
- function renderKittyImage(
197
- base64Data: string,
198
- opts: { cols?: number } = {},
199
- ): string {
288
+ function renderKittyImage(base64Data: string, opts: { cols?: number } = {}): string {
200
289
  const chunks: string[] = [];
201
290
  const CHUNK_SIZE = 4096;
202
291
 
@@ -207,13 +296,10 @@ function renderKittyImage(
207
296
  const more = isLast ? 0 : 1;
208
297
 
209
298
  if (isFirst) {
210
- // First chunk: include all metadata
211
- // a=T (transmit+display), f=100 (PNG), t=d (direct data)
212
299
  const colPart = opts.cols ? `,c=${opts.cols}` : "";
213
- chunks.push(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`);
300
+ chunks.push(tmuxWrap(`\x1b_Ga=T,f=100,t=d,m=${more}${colPart};${chunk}\x1b\\`));
214
301
  } else {
215
- // Continuation chunks
216
- chunks.push(`\x1b_Gm=${more};${chunk}\x1b\\`);
302
+ chunks.push(tmuxWrap(`\x1b_Gm=${more};${chunk}\x1b\\`));
217
303
  }
218
304
  }
219
305
 
@@ -240,96 +326,96 @@ const ICONS_MODE = (process.env.PRETTY_ICONS ?? "nerd").toLowerCase();
240
326
  const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
241
327
 
242
328
  // Nerd Font codepoints + ANSI color per file type
243
- const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
244
- const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
245
- const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
329
+ const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
330
+ const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
331
+ const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
246
332
 
247
333
  const EXT_ICON: Record<string, string> = {
248
334
  // TypeScript / JavaScript
249
- ts: `\x1b[38;2;49;120;198m\ue628${RST}`, // blue
250
- tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`, // react blue
251
- js: `\x1b[38;2;241;224;90m\ue74e${RST}`, // yellow
252
- jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`, // react cyan
335
+ ts: `\x1b[38;2;49;120;198m\ue628${RST}`, // blue
336
+ tsx: `\x1b[38;2;49;120;198m\ue7ba${RST}`, // react blue
337
+ js: `\x1b[38;2;241;224;90m\ue74e${RST}`, // yellow
338
+ jsx: `\x1b[38;2;97;218;251m\ue7ba${RST}`, // react cyan
253
339
  mjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
254
340
  cjs: `\x1b[38;2;241;224;90m\ue74e${RST}`,
255
341
 
256
342
  // Systems / Backend
257
- py: `\x1b[38;2;55;118;171m\ue73c${RST}`, // python blue
258
- rs: `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust orange
259
- go: `\x1b[38;2;0;173;216m\ue724${RST}`, // go cyan
260
- java: `\x1b[38;2;204;62;68m\ue738${RST}`, // java red
261
- swift: `\x1b[38;2;255;172;77m\ue755${RST}`, // swift orange
262
- rb: `\x1b[38;2;204;52;45m\ue739${RST}`, // ruby red
263
- kt: `\x1b[38;2;126;103;200m\ue634${RST}`, // kotlin purple
264
- c: `\x1b[38;2;85;154;211m\ue61e${RST}`, // c blue
265
- cpp: `\x1b[38;2;85;154;211m\ue61d${RST}`, // cpp blue
266
- h: `\x1b[38;2;140;160;185m\ue61e${RST}`, // header muted
267
- hpp: `\x1b[38;2;140;160;185m\ue61d${RST}`,
268
- cs: `\x1b[38;2;104;33;122m\ue648${RST}`, // c# purple
343
+ py: `\x1b[38;2;55;118;171m\ue73c${RST}`, // python blue
344
+ rs: `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust orange
345
+ go: `\x1b[38;2;0;173;216m\ue724${RST}`, // go cyan
346
+ java: `\x1b[38;2;204;62;68m\ue738${RST}`, // java red
347
+ swift: `\x1b[38;2;255;172;77m\ue755${RST}`, // swift orange
348
+ rb: `\x1b[38;2;204;52;45m\ue739${RST}`, // ruby red
349
+ kt: `\x1b[38;2;126;103;200m\ue634${RST}`, // kotlin purple
350
+ c: `\x1b[38;2;85;154;211m\ue61e${RST}`, // c blue
351
+ cpp: `\x1b[38;2;85;154;211m\ue61d${RST}`, // cpp blue
352
+ h: `\x1b[38;2;140;160;185m\ue61e${RST}`, // header muted
353
+ hpp: `\x1b[38;2;140;160;185m\ue61d${RST}`,
354
+ cs: `\x1b[38;2;104;33;122m\ue648${RST}`, // c# purple
269
355
 
270
356
  // Web
271
- html: `\x1b[38;2;228;77;38m\ue736${RST}`, // html orange
272
- css: `\x1b[38;2;66;165;245m\ue749${RST}`, // css blue
273
- scss: `\x1b[38;2;207;100;154m\ue749${RST}`, // scss pink
274
- less: `\x1b[38;2;66;165;245m\ue749${RST}`,
275
- vue: `\x1b[38;2;65;184;131m\ue6a0${RST}`, // vue green
276
- svelte: `\x1b[38;2;255;62;0m\ue697${RST}`, // svelte red-orange
357
+ html: `\x1b[38;2;228;77;38m\ue736${RST}`, // html orange
358
+ css: `\x1b[38;2;66;165;245m\ue749${RST}`, // css blue
359
+ scss: `\x1b[38;2;207;100;154m\ue749${RST}`, // scss pink
360
+ less: `\x1b[38;2;66;165;245m\ue749${RST}`,
361
+ vue: `\x1b[38;2;65;184;131m\ue6a0${RST}`, // vue green
362
+ svelte: `\x1b[38;2;255;62;0m\ue697${RST}`, // svelte red-orange
277
363
 
278
364
  // Config / Data
279
- json: `\x1b[38;2;241;224;90m\ue60b${RST}`, // json yellow
365
+ json: `\x1b[38;2;241;224;90m\ue60b${RST}`, // json yellow
280
366
  jsonc: `\x1b[38;2;241;224;90m\ue60b${RST}`,
281
- yaml: `\x1b[38;2;160;116;196m\ue6a8${RST}`, // yaml purple
282
- yml: `\x1b[38;2;160;116;196m\ue6a8${RST}`,
283
- toml: `\x1b[38;2;160;116;196m\ue6b2${RST}`, // toml purple
284
- xml: `\x1b[38;2;228;77;38m\ue619${RST}`, // xml orange
285
- sql: `\x1b[38;2;218;218;218m\ue706${RST}`, // sql gray
367
+ yaml: `\x1b[38;2;160;116;196m\ue6a8${RST}`, // yaml purple
368
+ yml: `\x1b[38;2;160;116;196m\ue6a8${RST}`,
369
+ toml: `\x1b[38;2;160;116;196m\ue6b2${RST}`, // toml purple
370
+ xml: `\x1b[38;2;228;77;38m\ue619${RST}`, // xml orange
371
+ sql: `\x1b[38;2;218;218;218m\ue706${RST}`, // sql gray
286
372
 
287
373
  // Markdown / Docs
288
- md: `\x1b[38;2;66;165;245m\ue73e${RST}`, // markdown blue
374
+ md: `\x1b[38;2;66;165;245m\ue73e${RST}`, // markdown blue
289
375
  mdx: `\x1b[38;2;66;165;245m\ue73e${RST}`,
290
376
 
291
377
  // Shell / Scripts
292
- sh: `\x1b[38;2;137;180;130m\ue795${RST}`, // shell green
378
+ sh: `\x1b[38;2;137;180;130m\ue795${RST}`, // shell green
293
379
  bash: `\x1b[38;2;137;180;130m\ue795${RST}`,
294
- zsh: `\x1b[38;2;137;180;130m\ue795${RST}`,
380
+ zsh: `\x1b[38;2;137;180;130m\ue795${RST}`,
295
381
  fish: `\x1b[38;2;137;180;130m\ue795${RST}`,
296
- lua: `\x1b[38;2;81;160;207m\ue620${RST}`, // lua blue
297
- php: `\x1b[38;2;137;147;186m\ue73d${RST}`, // php purple
298
- dart: `\x1b[38;2;87;182;240m\ue798${RST}`, // dart blue
382
+ lua: `\x1b[38;2;81;160;207m\ue620${RST}`, // lua blue
383
+ php: `\x1b[38;2;137;147;186m\ue73d${RST}`, // php purple
384
+ dart: `\x1b[38;2;87;182;240m\ue798${RST}`, // dart blue
299
385
 
300
386
  // Images
301
- png: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
302
- jpg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
387
+ png: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
388
+ jpg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
303
389
  jpeg: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
304
- gif: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
305
- svg: `\x1b[38;2;255;180;50m\uf1c5${RST}`,
390
+ gif: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
391
+ svg: `\x1b[38;2;255;180;50m\uf1c5${RST}`,
306
392
  webp: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
307
- ico: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
393
+ ico: `\x1b[38;2;160;116;196m\uf1c5${RST}`,
308
394
 
309
395
  // Misc
310
- lock: `\x1b[38;2;130;130;130m\uf023${RST}`, // lock gray
311
- env: `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
312
- graphql: `\x1b[38;2;224;51;144m\ue662${RST}`, // graphql pink
396
+ lock: `\x1b[38;2;130;130;130m\uf023${RST}`, // lock gray
397
+ env: `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
398
+ graphql: `\x1b[38;2;224;51;144m\ue662${RST}`, // graphql pink
313
399
  dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`,
314
400
  };
315
401
 
316
402
  const NAME_ICON: Record<string, string> = {
317
- "package.json": `\x1b[38;2;137;180;130m\ue71e${RST}`, // npm green
318
- "package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`, // npm gray
319
- "tsconfig.json": `\x1b[38;2;49;120;198m\ue628${RST}`, // ts blue
320
- "biome.json": `\x1b[38;2;96;165;250m\ue615${RST}`, // config blue
321
- ".gitignore": `\x1b[38;2;222;165;132m\ue702${RST}`, // git orange
322
- ".git": `\x1b[38;2;222;165;132m\ue702${RST}`,
323
- ".env": `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
324
- ".envrc": `\x1b[38;2;241;224;90m\ue615${RST}`,
325
- "dockerfile": `\x1b[38;2;56;152;236m\ue7b0${RST}`, // docker blue
326
- "makefile": `\x1b[38;2;130;130;130m\ue615${RST}`, // make gray
327
- "gnumakefile": `\x1b[38;2;130;130;130m\ue615${RST}`,
328
- "readme.md": `\x1b[38;2;66;165;245m\ue73e${RST}`, // readme blue
329
- "license": `\x1b[38;2;218;218;218m\ue60a${RST}`, // license white
330
- "cargo.toml": `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust
331
- "go.mod": `\x1b[38;2;0;173;216m\ue724${RST}`, // go
332
- "pyproject.toml": `\x1b[38;2;55;118;171m\ue73c${RST}`, // python
403
+ "package.json": `\x1b[38;2;137;180;130m\ue71e${RST}`, // npm green
404
+ "package-lock.json": `\x1b[38;2;130;130;130m\ue71e${RST}`, // npm gray
405
+ "tsconfig.json": `\x1b[38;2;49;120;198m\ue628${RST}`, // ts blue
406
+ "biome.json": `\x1b[38;2;96;165;250m\ue615${RST}`, // config blue
407
+ ".gitignore": `\x1b[38;2;222;165;132m\ue702${RST}`, // git orange
408
+ ".git": `\x1b[38;2;222;165;132m\ue702${RST}`,
409
+ ".env": `\x1b[38;2;241;224;90m\ue615${RST}`, // env yellow
410
+ ".envrc": `\x1b[38;2;241;224;90m\ue615${RST}`,
411
+ dockerfile: `\x1b[38;2;56;152;236m\ue7b0${RST}`, // docker blue
412
+ makefile: `\x1b[38;2;130;130;130m\ue615${RST}`, // make gray
413
+ gnumakefile: `\x1b[38;2;130;130;130m\ue615${RST}`,
414
+ "readme.md": `\x1b[38;2;66;165;245m\ue73e${RST}`, // readme blue
415
+ license: `\x1b[38;2;218;218;218m\ue60a${RST}`, // license white
416
+ "cargo.toml": `\x1b[38;2;222;165;132m\ue7a8${RST}`, // rust
417
+ "go.mod": `\x1b[38;2;0;173;216m\ue724${RST}`, // go
418
+ "pyproject.toml": `\x1b[38;2;55;118;171m\ue73c${RST}`, // python
333
419
  };
334
420
 
335
421
  function fileIcon(fp: string): string {
@@ -364,10 +450,7 @@ function _touch(k: string, v: string[]): string[] {
364
450
  return v;
365
451
  }
366
452
 
367
- async function hlBlock(
368
- code: string,
369
- language: BundledLanguage | undefined,
370
- ): Promise<string[]> {
453
+ async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
371
454
  if (!code) return [""];
372
455
  if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
373
456
 
@@ -431,32 +514,24 @@ async function renderFileContent(
431
514
  vis++;
432
515
  j++;
433
516
  }
434
- display = code.slice(0, j) + RST + FG_DIM + "›" + RST;
517
+ display = `${code.slice(0, j)}${RST}${FG_DIM}›${RST}`;
435
518
  }
436
519
  out.push(`${lnum(ln, nw)} ${FG_RULE}│${RST} ${display}${RST}`);
437
520
  }
438
521
 
439
522
  out.push(rule(tw));
440
523
  if (total > maxLines) {
441
- out.push(
442
- `${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`,
443
- );
524
+ out.push(`${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`);
444
525
  }
445
526
  return out.join("\n");
446
527
  }
447
528
 
448
529
  /** Render bash output with colored exit code and stderr highlighting. */
449
- function renderBashOutput(
450
- text: string,
451
- exitCode: number | null,
452
- ): { summary: string; body: string } {
530
+ function renderBashOutput(text: string, exitCode: number | null): { summary: string; body: string } {
453
531
  const isOk = exitCode === 0;
454
532
  const statusFg = isOk ? FG_GREEN : FG_RED;
455
533
  const statusIcon = isOk ? "✓" : "✗";
456
- const codeStr =
457
- exitCode !== null
458
- ? `${statusFg}${statusIcon} exit ${exitCode}${RST}`
459
- : `${FG_YELLOW}⚡ killed${RST}`;
534
+ const codeStr = exitCode !== null ? `${statusFg}${statusIcon} exit ${exitCode}${RST}` : `${FG_YELLOW}⚡ killed${RST}`;
460
535
 
461
536
  const lines = text.split("\n");
462
537
  const maxShow = MAX_PREVIEW_LINES;
@@ -497,9 +572,7 @@ function renderTree(text: string, basePath: string): string {
497
572
  }
498
573
 
499
574
  if (total > MAX_PREVIEW_LINES) {
500
- out.push(
501
- `${FG_RULE}└── ${RST}${FG_DIM}… ${total - MAX_PREVIEW_LINES} more entries${RST}`,
502
- );
575
+ out.push(`${FG_RULE}└── ${RST}${FG_DIM}… ${total - MAX_PREVIEW_LINES} more entries${RST}`);
503
576
  }
504
577
 
505
578
  return out.join("\n");
@@ -528,9 +601,7 @@ function renderFindResults(text: string): string {
528
601
  out.push(`${dirIcon()}${FG_BLUE}${BOLD}${dir}/${RST}`);
529
602
  for (let i = 0; i < files.length; i++) {
530
603
  if (count >= MAX_PREVIEW_LINES) {
531
- out.push(
532
- ` ${FG_DIM}… ${lines.length - count} more files${RST}`,
533
- );
604
+ out.push(` ${FG_DIM}… ${lines.length - count} more files${RST}`);
534
605
  return out.join("\n");
535
606
  }
536
607
  const isLast = i === files.length - 1;
@@ -545,13 +616,9 @@ function renderFindResults(text: string): string {
545
616
  }
546
617
 
547
618
  /** Render grep results with highlighted matches and line numbers. */
548
- async function renderGrepResults(
549
- text: string,
550
- pattern: string,
551
- ): Promise<string> {
619
+ async function renderGrepResults(text: string, pattern: string): Promise<string> {
552
620
  const lines = text.split("\n");
553
- if (!lines.length || (lines.length === 1 && !lines[0].trim()))
554
- return `${FG_DIM}(no matches)${RST}`;
621
+ if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
555
622
 
556
623
  const tw = termW();
557
624
  const out: string[] = [];
@@ -573,7 +640,7 @@ async function renderGrepResults(
573
640
  }
574
641
 
575
642
  // ripgrep-style: "file:line:content" or "file-line-content" or just "file"
576
- const fileMatch = line.match(/^(.+?)[:\-](\d+)[:\-](.*)$/);
643
+ const fileMatch = line.match(/^(.+?)[:-](\d+)[:-](.*)$/);
577
644
  if (fileMatch) {
578
645
  const [, file, lineNo, content] = fileMatch;
579
646
  if (file !== currentFile) {
@@ -586,10 +653,7 @@ async function renderGrepResults(
586
653
  const nw = Math.max(3, lineNo.length);
587
654
  let display = content;
588
655
  if (re) {
589
- display = content.replace(
590
- re,
591
- `${RST}${FG_YELLOW}${BOLD}$1${RST}`,
592
- );
656
+ display = content.replace(re, `${RST}${FG_YELLOW}${BOLD}$1${RST}`);
593
657
  }
594
658
  out.push(` ${lnum(Number(lineNo), nw)} ${FG_RULE}│${RST} ${display}${RST}`);
595
659
  count++;
@@ -683,24 +747,25 @@ export default function piPrettyExtension(pi: any): void {
683
747
  },
684
748
 
685
749
  renderCall(args: any, theme: any, ctx: any) {
750
+ resolveBaseBackground(theme);
686
751
  const fp = args?.path ?? "";
687
752
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
688
753
  const offset = args?.offset ? ` ${theme.fg("muted", `from line ${args.offset}`)}` : "";
689
754
  const limit = args?.limit ? ` ${theme.fg("muted", `(${args.limit} lines)`)}` : "";
690
- text.setText(
691
- `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
692
- );
755
+ text.setText(`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`);
693
756
  return text;
694
757
  },
695
758
 
696
759
  renderResult(result: any, _opt: any, theme: any, ctx: any) {
760
+ resolveBaseBackground(theme);
697
761
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
698
762
 
699
763
  if (ctx.isError) {
700
- const e = result.content
701
- ?.filter((c: any) => c.type === "text")
702
- .map((c: any) => c.text || "")
703
- .join("\n") ?? "Error";
764
+ const e =
765
+ result.content
766
+ ?.filter((c: any) => c.type === "text")
767
+ .map((c: any) => c.text || "")
768
+ .join("\n") ?? "Error";
704
769
  text.setText(`\n${theme.fg("error", e)}`);
705
770
  return text;
706
771
  }
@@ -712,7 +777,7 @@ export default function piPrettyExtension(pi: any): void {
712
777
  const tw = termW();
713
778
  const out: string[] = [];
714
779
  const fname = basename(d.filePath);
715
- const byteSize = Math.ceil((d.data as string).length * 3 / 4);
780
+ const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
716
781
  const sizeStr = humanSize(byteSize);
717
782
  const mimeStr = d.mimeType ?? "image";
718
783
 
@@ -725,10 +790,12 @@ export default function piPrettyExtension(pi: any): void {
725
790
  out.push(renderKittyImage(d.data, { cols: imgCols }));
726
791
  } else if (protocol === "iterm2") {
727
792
  const imgWidth = Math.min(tw - 4, 80);
728
- out.push(renderIterm2Image(d.data, {
729
- width: `${imgWidth}`,
730
- name: fname,
731
- }));
793
+ out.push(
794
+ renderIterm2Image(d.data, {
795
+ width: `${imgWidth}`,
796
+ name: fname,
797
+ }),
798
+ );
732
799
  } else {
733
800
  out.push(` ${FG_DIM}(Inline image preview requires Ghostty, iTerm2, WezTerm, or Kitty)${RST}`);
734
801
  }
@@ -787,15 +854,10 @@ export default function piPrettyExtension(pi: any): void {
787
854
  // Try to extract exit code from the output
788
855
  let exitCode: number | null = 0;
789
856
  if (textContent) {
790
- const exitMatch = textContent.match(
791
- /(?:exit code|exited with|exit status)[:\s]*(\d+)/i,
792
- );
857
+ const exitMatch = textContent.match(/(?:exit code|exited with|exit status)[:\s]*(\d+)/i);
793
858
  if (exitMatch) exitCode = Number(exitMatch[1]);
794
859
  // Check for common error indicators
795
- if (
796
- textContent.includes("command not found") ||
797
- textContent.includes("No such file")
798
- ) {
860
+ if (textContent.includes("command not found") || textContent.includes("No such file")) {
799
861
  exitCode = 1;
800
862
  }
801
863
  }
@@ -811,11 +873,10 @@ export default function piPrettyExtension(pi: any): void {
811
873
  },
812
874
 
813
875
  renderCall(args: any, theme: any, ctx: any) {
876
+ resolveBaseBackground(theme);
814
877
  const cmd = args?.command ?? "";
815
878
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
816
- const timeout = args?.timeout
817
- ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
818
- : "";
879
+ const timeout = args?.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
819
880
  text.setText(
820
881
  `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? cmd.slice(0, 77) + "…" : cmd)}${timeout}`,
821
882
  );
@@ -823,13 +884,15 @@ export default function piPrettyExtension(pi: any): void {
823
884
  },
824
885
 
825
886
  renderResult(result: any, _opt: any, theme: any, ctx: any) {
887
+ resolveBaseBackground(theme);
826
888
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
827
889
 
828
890
  if (ctx.isError) {
829
- const e = result.content
830
- ?.filter((c: any) => c.type === "text")
831
- .map((c: any) => c.text || "")
832
- .join("\n") ?? "Error";
891
+ const e =
892
+ result.content
893
+ ?.filter((c: any) => c.type === "text")
894
+ .map((c: any) => c.text || "")
895
+ .join("\n") ?? "Error";
833
896
  text.setText(`\n${theme.fg("error", e)}`);
834
897
  return text;
835
898
  }
@@ -863,9 +926,7 @@ export default function piPrettyExtension(pi: any): void {
863
926
  }
864
927
 
865
928
  const fallback = result.content?.[0]?.text ?? "done";
866
- text.setText(
867
- ` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
868
- );
929
+ text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
869
930
  return text;
870
931
  },
871
932
  });
@@ -891,9 +952,7 @@ export default function piPrettyExtension(pi: any): void {
891
952
  .join("\n");
892
953
 
893
954
  const fp = params.path ?? cwd;
894
- const entryCount = textContent
895
- ? textContent.trim().split("\n").filter(Boolean).length
896
- : 0;
955
+ const entryCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
897
956
 
898
957
  (result as any).details = {
899
958
  _type: "lsResult",
@@ -906,22 +965,23 @@ export default function piPrettyExtension(pi: any): void {
906
965
  },
907
966
 
908
967
  renderCall(args: any, theme: any, ctx: any) {
968
+ resolveBaseBackground(theme);
909
969
  const fp = args?.path ?? ".";
910
970
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
911
- text.setText(
912
- `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`,
913
- );
971
+ text.setText(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`);
914
972
  return text;
915
973
  },
916
974
 
917
975
  renderResult(result: any, _opt: any, theme: any, ctx: any) {
976
+ resolveBaseBackground(theme);
918
977
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
919
978
 
920
979
  if (ctx.isError) {
921
- const e = result.content
922
- ?.filter((c: any) => c.type === "text")
923
- .map((c: any) => c.text || "")
924
- .join("\n") ?? "Error";
980
+ const e =
981
+ result.content
982
+ ?.filter((c: any) => c.type === "text")
983
+ .map((c: any) => c.text || "")
984
+ .join("\n") ?? "Error";
925
985
  text.setText(`\n${theme.fg("error", e)}`);
926
986
  return text;
927
987
  }
@@ -935,9 +995,7 @@ export default function piPrettyExtension(pi: any): void {
935
995
  }
936
996
 
937
997
  const fallback = result.content?.[0]?.text ?? "listed";
938
- text.setText(
939
- ` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
940
- );
998
+ text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
941
999
  return text;
942
1000
  },
943
1001
  });
@@ -962,9 +1020,7 @@ export default function piPrettyExtension(pi: any): void {
962
1020
  .map((c: any) => c.text || "")
963
1021
  .join("\n");
964
1022
 
965
- const matchCount = textContent
966
- ? textContent.trim().split("\n").filter(Boolean).length
967
- : 0;
1023
+ const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
968
1024
 
969
1025
  (result as any).details = {
970
1026
  _type: "findResult",
@@ -977,23 +1033,24 @@ export default function piPrettyExtension(pi: any): void {
977
1033
  },
978
1034
 
979
1035
  renderCall(args: any, theme: any, ctx: any) {
1036
+ resolveBaseBackground(theme);
980
1037
  const pattern = args?.pattern ?? "";
981
1038
  const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
982
1039
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
983
- text.setText(
984
- `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
985
- );
1040
+ text.setText(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`);
986
1041
  return text;
987
1042
  },
988
1043
 
989
1044
  renderResult(result: any, _opt: any, theme: any, ctx: any) {
1045
+ resolveBaseBackground(theme);
990
1046
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
991
1047
 
992
1048
  if (ctx.isError) {
993
- const e = result.content
994
- ?.filter((c: any) => c.type === "text")
995
- .map((c: any) => c.text || "")
996
- .join("\n") ?? "Error";
1049
+ const e =
1050
+ result.content
1051
+ ?.filter((c: any) => c.type === "text")
1052
+ .map((c: any) => c.text || "")
1053
+ .join("\n") ?? "Error";
997
1054
  text.setText(`\n${theme.fg("error", e)}`);
998
1055
  return text;
999
1056
  }
@@ -1007,9 +1064,7 @@ export default function piPrettyExtension(pi: any): void {
1007
1064
  }
1008
1065
 
1009
1066
  const fallback = result.content?.[0]?.text ?? "found";
1010
- text.setText(
1011
- ` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
1012
- );
1067
+ text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
1013
1068
  return text;
1014
1069
  },
1015
1070
  });
@@ -1038,8 +1093,7 @@ export default function piPrettyExtension(pi: any): void {
1038
1093
  ? textContent
1039
1094
  .trim()
1040
1095
  .split("\n")
1041
- .filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/))
1042
- .length
1096
+ .filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/)).length
1043
1097
  : 0;
1044
1098
 
1045
1099
  (result as any).details = {
@@ -1053,24 +1107,25 @@ export default function piPrettyExtension(pi: any): void {
1053
1107
  },
1054
1108
 
1055
1109
  renderCall(args: any, theme: any, ctx: any) {
1110
+ resolveBaseBackground(theme);
1056
1111
  const pattern = args?.pattern ?? "";
1057
1112
  const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
1058
1113
  const glob = args?.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
1059
1114
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1060
- text.setText(
1061
- `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
1062
- );
1115
+ text.setText(`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`);
1063
1116
  return text;
1064
1117
  },
1065
1118
 
1066
1119
  renderResult(result: any, _opt: any, theme: any, ctx: any) {
1120
+ resolveBaseBackground(theme);
1067
1121
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1068
1122
 
1069
1123
  if (ctx.isError) {
1070
- const e = result.content
1071
- ?.filter((c: any) => c.type === "text")
1072
- .map((c: any) => c.text || "")
1073
- .join("\n") ?? "Error";
1124
+ const e =
1125
+ result.content
1126
+ ?.filter((c: any) => c.type === "text")
1127
+ .map((c: any) => c.text || "")
1128
+ .join("\n") ?? "Error";
1074
1129
  text.setText(`\n${theme.fg("error", e)}`);
1075
1130
  return text;
1076
1131
  }
@@ -1096,9 +1151,7 @@ export default function piPrettyExtension(pi: any): void {
1096
1151
  }
1097
1152
 
1098
1153
  const fallback = result.content?.[0]?.text ?? "searched";
1099
- text.setText(
1100
- ` ${theme.fg("dim", String(fallback).slice(0, 120))}`,
1101
- );
1154
+ text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
1102
1155
  return text;
1103
1156
  },
1104
1157
  });