@heyhuynhgiabuu/pi-pretty 0.3.1 → 0.3.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 +19 -1
- package/package.json +1 -1
- package/release-notes/v0.3.2.md +34 -0
- package/release-notes/v0.3.3.md +48 -0
- package/src/index.ts +198 -60
- package/test/fff-integration.test.ts +16 -3
- package/test/image-rendering.test.ts +165 -0
package/README.md
CHANGED
|
@@ -41,7 +41,25 @@ pi -e ./src/index.ts
|
|
|
41
41
|
## Terminal support for inline images
|
|
42
42
|
|
|
43
43
|
Inline image previews are supported in **Ghostty**, **Kitty**, **iTerm2**, and **WezTerm**.
|
|
44
|
-
When running in **tmux**, pi-pretty uses passthrough escape sequences
|
|
44
|
+
When running in **tmux**, pi-pretty uses passthrough escape sequences.
|
|
45
|
+
|
|
46
|
+
> tmux must allow passthrough. Enable it with:
|
|
47
|
+
>
|
|
48
|
+
> ```tmux
|
|
49
|
+
> set -g allow-passthrough on
|
|
50
|
+
> ```
|
|
51
|
+
>
|
|
52
|
+
> (or run once in a session: `tmux set -g allow-passthrough on`)
|
|
53
|
+
|
|
54
|
+
## FFF data directory
|
|
55
|
+
|
|
56
|
+
When FFF is available, pi-pretty now stores its frecency/history data under a pi-pretty-specific path:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
~/.pi/agent/pi-pretty/fff/
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This makes it clear that the cache belongs to this extension rather than Pi core.
|
|
45
63
|
|
|
46
64
|
## Configuration
|
|
47
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# pi-pretty v0.3.2
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
This patch release fixes inline image rendering reliability when running `pi-pretty` inside `tmux`.
|
|
5
|
+
|
|
6
|
+
## What changed
|
|
7
|
+
- Fixed terminal detection in tmux for:
|
|
8
|
+
- Kitty (`KITTY_WINDOW_ID`, `KITTY_PID`)
|
|
9
|
+
- WezTerm (`WEZTERM_EXECUTABLE`, `WEZTERM_CONFIG_DIR`, `WEZTERM_CONFIG_FILE`)
|
|
10
|
+
- Added tmux fallback detection via:
|
|
11
|
+
- `tmux display-message -p "#{client_termname}"`
|
|
12
|
+
- Replaced static tmux detection with runtime detection to avoid stale module-load state.
|
|
13
|
+
- Added explicit warning when tmux passthrough is disabled:
|
|
14
|
+
- `tmux allow-passthrough is off. Run: tmux set -g allow-passthrough on`
|
|
15
|
+
- Added non-PNG warning for Kitty/Ghostty image rendering path.
|
|
16
|
+
- Added test coverage for image protocol detection and tmux passthrough warning behavior.
|
|
17
|
+
- Updated README with tmux passthrough setup instructions.
|
|
18
|
+
|
|
19
|
+
## Files
|
|
20
|
+
- `src/index.ts`
|
|
21
|
+
- `test/image-rendering.test.ts`
|
|
22
|
+
- `README.md`
|
|
23
|
+
|
|
24
|
+
## Verification
|
|
25
|
+
- `npm run typecheck` ✅
|
|
26
|
+
- `npm test` ✅ (46 tests)
|
|
27
|
+
- `npm run lint` ⚠️ fails due to existing Biome diagnostics in legacy code paths (pre-existing, not introduced in this patch).
|
|
28
|
+
|
|
29
|
+
## Upgrade notes
|
|
30
|
+
When using `tmux`, enable passthrough:
|
|
31
|
+
|
|
32
|
+
```tmux
|
|
33
|
+
set -g allow-passthrough on
|
|
34
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# pi-pretty v0.3.3
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
This patch release combines the recent rendering and FFF-path fixes into a single `v0.3.3` release.
|
|
5
|
+
|
|
6
|
+
## What changed
|
|
7
|
+
- Fixed themed tool background rendering so `toolSuccessBg` and `toolErrorBg` fill the full rendered width for core tool output.
|
|
8
|
+
- Preserved the active tool background across ANSI reset sequences to avoid broken background bands inside highlighted output.
|
|
9
|
+
- Kept error rendering on `theme.fg("error", ...)` while switching error panels to `toolErrorBg`.
|
|
10
|
+
- Moved pi-pretty FFF data from:
|
|
11
|
+
- `~/.pi/agent/fff`
|
|
12
|
+
- To:
|
|
13
|
+
- `~/.pi/agent/pi-pretty/fff`
|
|
14
|
+
- Added a helper to resolve the pi-pretty-specific FFF directory under `getAgentDir()`.
|
|
15
|
+
- Updated README to document the pi-pretty-owned FFF data location.
|
|
16
|
+
- Added a test that verifies the generated frecency/history DB paths use the new per-extension location.
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
- `src/index.ts`
|
|
20
|
+
- `test/fff-integration.test.ts`
|
|
21
|
+
- `README.md`
|
|
22
|
+
- `package.json`
|
|
23
|
+
- `package-lock.json`
|
|
24
|
+
|
|
25
|
+
## Verification
|
|
26
|
+
- `npm run typecheck` ✅
|
|
27
|
+
- `npm test` ✅ (47 tests)
|
|
28
|
+
- `npm run lint` ⚠️ still reports pre-existing Biome diagnostics in legacy code paths; no new release-blocking failure was introduced for this patch.
|
|
29
|
+
|
|
30
|
+
## Upgrade notes
|
|
31
|
+
If your Pi theme defines:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"toolSuccessBg": "#1e2e1e",
|
|
36
|
+
"toolErrorBg": "#2e1e1e"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
core tool panels now render those backgrounds across the full line width instead of only behind printed characters.
|
|
41
|
+
|
|
42
|
+
When FFF is available, pi-pretty now stores frecency/history data under:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
~/.pi/agent/pi-pretty/fff/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Existing data under `~/.pi/agent/fff` is not migrated automatically by this release.
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* • Large-file fallback (skip highlighting, still show line numbers)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import * as childProcess from "node:child_process";
|
|
26
27
|
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
27
28
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
28
29
|
|
|
@@ -70,28 +71,41 @@ const FG_PURPLE = "\x1b[38;2;170;120;200m";
|
|
|
70
71
|
const BG_STDERR = "\x1b[48;2;40;25;25m";
|
|
71
72
|
|
|
72
73
|
const BG_DEFAULT = "\x1b[49m";
|
|
73
|
-
let BG_BASE = BG_DEFAULT; // tool box base bg — updated from theme's toolSuccessBg
|
|
74
|
+
let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from theme's toolSuccessBg
|
|
75
|
+
let BG_ERROR = BG_DEFAULT; // tool box error bg — updated from theme's toolErrorBg
|
|
76
|
+
|
|
77
|
+
type BgTheme = { getBgAnsi?: (key: string) => string };
|
|
78
|
+
type FgTheme = { fg: (key: string, text: string) => string };
|
|
74
79
|
|
|
75
80
|
/** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
|
|
76
81
|
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
77
|
-
const m = ansi.match(
|
|
82
|
+
const m = ansi.match(new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
|
|
78
83
|
return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
const bgAnsi = theme.getBgAnsi?.(key);
|
|
89
|
+
return bgAnsi && parseAnsiRgb(bgAnsi) ? bgAnsi : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read themed tool backgrounds and update BG_BASE / BG_ERROR + RST.
|
|
82
96
|
* Call once when theme is first available. Idempotent. */
|
|
83
97
|
let _bgBaseResolved = false;
|
|
84
|
-
function resolveBaseBackground(theme:
|
|
98
|
+
function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
85
99
|
if (_bgBaseResolved || !theme?.getBgAnsi) return;
|
|
86
100
|
_bgBaseResolved = true;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
|
|
102
|
+
BG_BASE = getThemeBgAnsi(theme, "toolSuccessBg") ?? BG_DEFAULT;
|
|
103
|
+
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
104
|
+
RST = `\x1b[0m${BG_BASE}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderToolError(error: string, theme: FgTheme): string {
|
|
108
|
+
return fillToolBackground(`\n${theme.fg("error", error)}`, BG_ERROR);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
const ESC_RE = "\u001b";
|
|
@@ -125,6 +139,25 @@ function strip(s: string): string {
|
|
|
125
139
|
return s.replace(ANSI_RE, "");
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
function preserveToolBackground(ansi: string, bg: string): string {
|
|
143
|
+
return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => {
|
|
144
|
+
const codes = params.split(";");
|
|
145
|
+
return params === "0" || codes.includes("49") ? `${seq}${bg}` : seq;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
150
|
+
const width = termW();
|
|
151
|
+
return text
|
|
152
|
+
.split("\n")
|
|
153
|
+
.map((line) => {
|
|
154
|
+
const normalized = preserveToolBackground(line, bg);
|
|
155
|
+
const padding = Math.max(0, width - strip(normalized).length);
|
|
156
|
+
return `${bg}${normalized}${" ".repeat(padding)}${RST}`;
|
|
157
|
+
})
|
|
158
|
+
.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
function termW(): number {
|
|
129
162
|
const raw =
|
|
130
163
|
process.stdout.columns || (process.stderr as any).columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
|
|
@@ -217,7 +250,45 @@ function lang(fp: string): BundledLanguage | undefined {
|
|
|
217
250
|
|
|
218
251
|
type ImageProtocol = "iterm2" | "kitty" | "none";
|
|
219
252
|
|
|
220
|
-
|
|
253
|
+
let _tmuxClientTermCache: string | null | undefined;
|
|
254
|
+
let _tmuxAllowPassthroughCache: boolean | null | undefined;
|
|
255
|
+
let _tmuxClientTermOverrideForTests: string | null | undefined;
|
|
256
|
+
let _tmuxAllowPassthroughOverrideForTests: boolean | null | undefined;
|
|
257
|
+
|
|
258
|
+
function isTmuxSession(): boolean {
|
|
259
|
+
return !!process.env.TMUX || /^(tmux|screen)/.test(process.env.TERM ?? "");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeTerminalName(term: string): string {
|
|
263
|
+
const t = term.toLowerCase();
|
|
264
|
+
if (t.includes("kitty")) return "kitty";
|
|
265
|
+
if (t.includes("ghostty")) return "ghostty";
|
|
266
|
+
if (t.includes("wezterm")) return "WezTerm";
|
|
267
|
+
if (t.includes("iterm")) return "iTerm.app";
|
|
268
|
+
if (t.includes("mintty")) return "mintty";
|
|
269
|
+
return term;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readTmuxClientTerm(): string | null {
|
|
273
|
+
if (_tmuxClientTermOverrideForTests !== undefined) {
|
|
274
|
+
return _tmuxClientTermOverrideForTests ? normalizeTerminalName(_tmuxClientTermOverrideForTests) : null;
|
|
275
|
+
}
|
|
276
|
+
if (!isTmuxSession()) return null;
|
|
277
|
+
if (_tmuxClientTermCache !== undefined) return _tmuxClientTermCache;
|
|
278
|
+
try {
|
|
279
|
+
const term = childProcess
|
|
280
|
+
.execFileSync("tmux", ["display-message", "-p", "#{client_termname}"], {
|
|
281
|
+
encoding: "utf8",
|
|
282
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
283
|
+
timeout: 200,
|
|
284
|
+
})
|
|
285
|
+
.trim();
|
|
286
|
+
_tmuxClientTermCache = term ? normalizeTerminalName(term) : null;
|
|
287
|
+
} catch {
|
|
288
|
+
_tmuxClientTermCache = null;
|
|
289
|
+
}
|
|
290
|
+
return _tmuxClientTermCache;
|
|
291
|
+
}
|
|
221
292
|
|
|
222
293
|
/**
|
|
223
294
|
* Detect the outer terminal when running inside tmux.
|
|
@@ -225,27 +296,34 @@ const IS_TMUX = !!process.env.TMUX;
|
|
|
225
296
|
* the environment of the tmux server or can be inferred.
|
|
226
297
|
*/
|
|
227
298
|
function getOuterTerminal(): string {
|
|
228
|
-
//
|
|
229
|
-
const term = process.env.TERM_PROGRAM ?? "";
|
|
230
|
-
if (term !== "tmux" && term !== "screen") return term;
|
|
231
|
-
|
|
232
|
-
// Inside tmux: check common env vars that leak through
|
|
233
|
-
// Ghostty sets this; iTerm2 sets LC_TERMINAL
|
|
299
|
+
// Environment hints that often survive inside tmux
|
|
234
300
|
if (process.env.LC_TERMINAL === "iTerm2") return "iTerm.app";
|
|
235
|
-
|
|
236
|
-
// TERM_PROGRAM_VERSION sometimes survives into tmux
|
|
237
|
-
// Try to detect via COLORTERM or other hints
|
|
238
301
|
if (process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
|
|
302
|
+
if (process.env.KITTY_WINDOW_ID || process.env.KITTY_PID) return "kitty";
|
|
303
|
+
if (process.env.WEZTERM_EXECUTABLE || process.env.WEZTERM_CONFIG_DIR || process.env.WEZTERM_CONFIG_FILE) {
|
|
304
|
+
return "WezTerm";
|
|
305
|
+
}
|
|
239
306
|
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
return "unknown-modern";
|
|
307
|
+
const termProgram = process.env.TERM_PROGRAM ?? "";
|
|
308
|
+
if (termProgram && termProgram !== "tmux" && termProgram !== "screen") {
|
|
309
|
+
return normalizeTerminalName(termProgram);
|
|
244
310
|
}
|
|
245
|
-
|
|
311
|
+
|
|
312
|
+
const tmuxClientTerm = readTmuxClientTerm();
|
|
313
|
+
if (tmuxClientTerm) return tmuxClientTerm;
|
|
314
|
+
|
|
315
|
+
const term = process.env.TERM ?? "";
|
|
316
|
+
if (term) return normalizeTerminalName(term);
|
|
317
|
+
if (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit") return "unknown-modern";
|
|
318
|
+
return termProgram;
|
|
246
319
|
}
|
|
247
320
|
|
|
248
321
|
function detectImageProtocol(): ImageProtocol {
|
|
322
|
+
const forced = (process.env.PRETTY_IMAGE_PROTOCOL ?? "").toLowerCase();
|
|
323
|
+
if (forced === "kitty" || forced === "iterm2" || forced === "none") {
|
|
324
|
+
return forced;
|
|
325
|
+
}
|
|
326
|
+
|
|
249
327
|
const term = getOuterTerminal();
|
|
250
328
|
// Ghostty and Kitty use the Kitty graphics protocol
|
|
251
329
|
if (term === "ghostty" || term === "kitty") return "kitty";
|
|
@@ -255,18 +333,67 @@ function detectImageProtocol(): ImageProtocol {
|
|
|
255
333
|
return "none";
|
|
256
334
|
}
|
|
257
335
|
|
|
336
|
+
function tmuxAllowsPassthrough(): boolean | null {
|
|
337
|
+
if (_tmuxAllowPassthroughOverrideForTests !== undefined) return _tmuxAllowPassthroughOverrideForTests;
|
|
338
|
+
if (!isTmuxSession()) return null;
|
|
339
|
+
if (_tmuxAllowPassthroughCache !== undefined) return _tmuxAllowPassthroughCache;
|
|
340
|
+
try {
|
|
341
|
+
const value = childProcess
|
|
342
|
+
.execFileSync("tmux", ["show-options", "-gv", "allow-passthrough"], {
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
345
|
+
timeout: 200,
|
|
346
|
+
})
|
|
347
|
+
.trim()
|
|
348
|
+
.toLowerCase();
|
|
349
|
+
_tmuxAllowPassthroughCache = value === "on" || value === "all";
|
|
350
|
+
} catch {
|
|
351
|
+
_tmuxAllowPassthroughCache = null;
|
|
352
|
+
}
|
|
353
|
+
return _tmuxAllowPassthroughCache;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getTmuxPassthroughWarning(protocol: ImageProtocol): string | null {
|
|
357
|
+
if (!isTmuxSession() || protocol === "none") return null;
|
|
358
|
+
if (tmuxAllowsPassthrough() === false) {
|
|
359
|
+
return "tmux allow-passthrough is off. Run: tmux set -g allow-passthrough on";
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
258
364
|
/**
|
|
259
365
|
* Wrap escape sequence for tmux passthrough.
|
|
260
366
|
* tmux requires: ESC Ptmux; <escaped-sequence> ESC \
|
|
261
367
|
* Inner ESC chars must be doubled.
|
|
262
368
|
*/
|
|
263
369
|
function tmuxWrap(seq: string): string {
|
|
264
|
-
if (!
|
|
370
|
+
if (!isTmuxSession()) return seq;
|
|
265
371
|
// Double all ESC chars inside the sequence
|
|
266
372
|
const escaped = seq.split("\x1b").join("\x1b\x1b");
|
|
267
373
|
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
268
374
|
}
|
|
269
375
|
|
|
376
|
+
export const __imageInternals = {
|
|
377
|
+
isTmuxSession,
|
|
378
|
+
getOuterTerminal,
|
|
379
|
+
detectImageProtocol,
|
|
380
|
+
tmuxWrap,
|
|
381
|
+
tmuxAllowsPassthrough,
|
|
382
|
+
getTmuxPassthroughWarning,
|
|
383
|
+
setTmuxClientTermOverrideForTests: (value: string | null | undefined) => {
|
|
384
|
+
_tmuxClientTermOverrideForTests = value;
|
|
385
|
+
},
|
|
386
|
+
setTmuxAllowPassthroughOverrideForTests: (value: boolean | null | undefined) => {
|
|
387
|
+
_tmuxAllowPassthroughOverrideForTests = value;
|
|
388
|
+
},
|
|
389
|
+
resetCachesForTests: () => {
|
|
390
|
+
_tmuxClientTermCache = undefined;
|
|
391
|
+
_tmuxAllowPassthroughCache = undefined;
|
|
392
|
+
_tmuxClientTermOverrideForTests = undefined;
|
|
393
|
+
_tmuxAllowPassthroughOverrideForTests = undefined;
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
270
397
|
/**
|
|
271
398
|
* Render base64 image inline using iTerm2 inline image protocol.
|
|
272
399
|
* Protocol: ESC ] 1337 ; File=[args] : base64data BEL
|
|
@@ -685,6 +812,10 @@ let _fffPartialIndex = false;
|
|
|
685
812
|
let _fffDbDir: string | null = null;
|
|
686
813
|
const FFF_SCAN_TIMEOUT = 15_000;
|
|
687
814
|
|
|
815
|
+
function getPiPrettyFffDir(agentDir: string): string {
|
|
816
|
+
return join(agentDir, "pi-pretty", "fff");
|
|
817
|
+
}
|
|
818
|
+
|
|
688
819
|
async function fffEnsureFinder(cwd: string): Promise<any> {
|
|
689
820
|
if (_fffFinder && !_fffFinder.isDestroyed) return _fffFinder;
|
|
690
821
|
if (!_fffModule || !_fffDbDir) return null;
|
|
@@ -779,7 +910,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
779
910
|
try {
|
|
780
911
|
_fffModule = require("@ff-labs/fff-node");
|
|
781
912
|
if (getAgentDir) {
|
|
782
|
-
_fffDbDir =
|
|
913
|
+
_fffDbDir = getPiPrettyFffDir(getAgentDir());
|
|
783
914
|
try {
|
|
784
915
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
785
916
|
} catch {}
|
|
@@ -788,7 +919,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
788
919
|
/* FFF not installed — SDK tools will be used */
|
|
789
920
|
}
|
|
790
921
|
} else if (_fffModule && getAgentDir) {
|
|
791
|
-
_fffDbDir =
|
|
922
|
+
_fffDbDir = getPiPrettyFffDir(getAgentDir());
|
|
792
923
|
try {
|
|
793
924
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
794
925
|
} catch {}
|
|
@@ -806,7 +937,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
806
937
|
|
|
807
938
|
if (!_fffDbDir) {
|
|
808
939
|
const agentDir = getAgentDir?.() ?? join(home, ".pi/agent");
|
|
809
|
-
_fffDbDir =
|
|
940
|
+
_fffDbDir = getPiPrettyFffDir(agentDir);
|
|
810
941
|
try {
|
|
811
942
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
812
943
|
} catch {}
|
|
@@ -883,7 +1014,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
883
1014
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
884
1015
|
const offset = args?.offset ? ` ${theme.fg("muted", `from line ${args.offset}`)}` : "";
|
|
885
1016
|
const limit = args?.limit ? ` ${theme.fg("muted", `(${args.limit} lines)`)}` : "";
|
|
886
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`);
|
|
1017
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`));
|
|
887
1018
|
return text;
|
|
888
1019
|
},
|
|
889
1020
|
|
|
@@ -897,7 +1028,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
897
1028
|
?.filter((c: any) => c.type === "text")
|
|
898
1029
|
.map((c: any) => c.text || "")
|
|
899
1030
|
.join("\n") ?? "Error";
|
|
900
|
-
text.setText(
|
|
1031
|
+
text.setText(renderToolError(e, theme));
|
|
901
1032
|
return text;
|
|
902
1033
|
}
|
|
903
1034
|
|
|
@@ -916,9 +1047,16 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
916
1047
|
out.push(rule(tw));
|
|
917
1048
|
|
|
918
1049
|
const protocol = detectImageProtocol();
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
out.push(
|
|
1050
|
+
const passthroughWarning = getTmuxPassthroughWarning(protocol);
|
|
1051
|
+
if (passthroughWarning) {
|
|
1052
|
+
out.push(` ${FG_YELLOW}${passthroughWarning}${RST}`);
|
|
1053
|
+
} else if (protocol === "kitty") {
|
|
1054
|
+
if (d.mimeType && d.mimeType !== "image/png") {
|
|
1055
|
+
out.push(` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`);
|
|
1056
|
+
} else {
|
|
1057
|
+
const imgCols = Math.min(tw - 4, 80);
|
|
1058
|
+
out.push(renderKittyImage(d.data, { cols: imgCols }));
|
|
1059
|
+
}
|
|
922
1060
|
} else if (protocol === "iterm2") {
|
|
923
1061
|
const imgWidth = Math.min(tw - 4, 80);
|
|
924
1062
|
out.push(
|
|
@@ -932,7 +1070,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
932
1070
|
}
|
|
933
1071
|
|
|
934
1072
|
out.push(rule(tw));
|
|
935
|
-
text.setText(out.join("\n"));
|
|
1073
|
+
text.setText(fillToolBackground(out.join("\n")));
|
|
936
1074
|
return text;
|
|
937
1075
|
}
|
|
938
1076
|
|
|
@@ -941,24 +1079,24 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
941
1079
|
if (ctx.state._rk !== key) {
|
|
942
1080
|
ctx.state._rk = key;
|
|
943
1081
|
const info = `${FG_DIM}${d.lineCount} lines${RST}`;
|
|
944
|
-
ctx.state._rt = ` ${info}
|
|
1082
|
+
ctx.state._rt = fillToolBackground(` ${info}`);
|
|
945
1083
|
|
|
946
1084
|
const maxShow = ctx.expanded ? d.lineCount : MAX_PREVIEW_LINES;
|
|
947
1085
|
renderFileContent(d.content, d.filePath, d.offset, maxShow)
|
|
948
1086
|
.then((rendered: string) => {
|
|
949
1087
|
if (ctx.state._rk !== key) return;
|
|
950
|
-
ctx.state._rt = ` ${info}\n${rendered}
|
|
1088
|
+
ctx.state._rt = fillToolBackground(` ${info}\n${rendered}`);
|
|
951
1089
|
ctx.invalidate();
|
|
952
1090
|
})
|
|
953
1091
|
.catch(() => {});
|
|
954
1092
|
}
|
|
955
|
-
text.setText(ctx.state._rt ?? ` ${FG_DIM}${d.lineCount} lines${RST}`);
|
|
1093
|
+
text.setText(ctx.state._rt ?? fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}`));
|
|
956
1094
|
return text;
|
|
957
1095
|
}
|
|
958
1096
|
|
|
959
1097
|
// Fallback
|
|
960
1098
|
const fallback = result.content?.[0]?.text ?? "read";
|
|
961
|
-
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1099
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
|
|
962
1100
|
return text;
|
|
963
1101
|
},
|
|
964
1102
|
});
|
|
@@ -1009,7 +1147,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1009
1147
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1010
1148
|
const timeout = args?.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
|
|
1011
1149
|
text.setText(
|
|
1012
|
-
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? cmd.slice(0, 77) + "…" : cmd)}${timeout}
|
|
1150
|
+
fillToolBackground(`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? cmd.slice(0, 77) + "…" : cmd)}${timeout}`),
|
|
1013
1151
|
);
|
|
1014
1152
|
return text;
|
|
1015
1153
|
},
|
|
@@ -1024,7 +1162,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1024
1162
|
?.filter((c: any) => c.type === "text")
|
|
1025
1163
|
.map((c: any) => c.text || "")
|
|
1026
1164
|
.join("\n") ?? "Error";
|
|
1027
|
-
text.setText(
|
|
1165
|
+
text.setText(renderToolError(e, theme));
|
|
1028
1166
|
return text;
|
|
1029
1167
|
}
|
|
1030
1168
|
|
|
@@ -1049,15 +1187,15 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1049
1187
|
if (lineCount > maxShow) {
|
|
1050
1188
|
out.push(`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`);
|
|
1051
1189
|
}
|
|
1052
|
-
text.setText(out.join("\n"));
|
|
1190
|
+
text.setText(fillToolBackground(out.join("\n")));
|
|
1053
1191
|
} else {
|
|
1054
|
-
text.setText(header);
|
|
1192
|
+
text.setText(fillToolBackground(header));
|
|
1055
1193
|
}
|
|
1056
1194
|
return text;
|
|
1057
1195
|
}
|
|
1058
1196
|
|
|
1059
1197
|
const fallback = result.content?.[0]?.text ?? "done";
|
|
1060
|
-
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1198
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
|
|
1061
1199
|
return text;
|
|
1062
1200
|
},
|
|
1063
1201
|
});
|
|
@@ -1099,7 +1237,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1099
1237
|
resolveBaseBackground(theme);
|
|
1100
1238
|
const fp = args?.path ?? ".";
|
|
1101
1239
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1102
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`);
|
|
1240
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`));
|
|
1103
1241
|
return text;
|
|
1104
1242
|
},
|
|
1105
1243
|
|
|
@@ -1113,7 +1251,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1113
1251
|
?.filter((c: any) => c.type === "text")
|
|
1114
1252
|
.map((c: any) => c.text || "")
|
|
1115
1253
|
.join("\n") ?? "Error";
|
|
1116
|
-
text.setText(
|
|
1254
|
+
text.setText(renderToolError(e, theme));
|
|
1117
1255
|
return text;
|
|
1118
1256
|
}
|
|
1119
1257
|
|
|
@@ -1121,12 +1259,12 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1121
1259
|
if (d?._type === "lsResult" && d.text) {
|
|
1122
1260
|
const tree = renderTree(d.text, d.path);
|
|
1123
1261
|
const info = `${FG_DIM}${d.entryCount} entries${RST}`;
|
|
1124
|
-
text.setText(` ${info}\n${tree}`);
|
|
1262
|
+
text.setText(fillToolBackground(` ${info}\n${tree}`));
|
|
1125
1263
|
return text;
|
|
1126
1264
|
}
|
|
1127
1265
|
|
|
1128
1266
|
const fallback = result.content?.[0]?.text ?? "listed";
|
|
1129
|
-
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1267
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
|
|
1130
1268
|
return text;
|
|
1131
1269
|
},
|
|
1132
1270
|
});
|
|
@@ -1205,7 +1343,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1205
1343
|
const pattern = args?.pattern ?? "";
|
|
1206
1344
|
const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
1207
1345
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1208
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`);
|
|
1346
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`));
|
|
1209
1347
|
return text;
|
|
1210
1348
|
},
|
|
1211
1349
|
|
|
@@ -1219,7 +1357,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1219
1357
|
?.filter((c: any) => c.type === "text")
|
|
1220
1358
|
.map((c: any) => c.text || "")
|
|
1221
1359
|
.join("\n") ?? "Error";
|
|
1222
|
-
text.setText(
|
|
1360
|
+
text.setText(renderToolError(e, theme));
|
|
1223
1361
|
return text;
|
|
1224
1362
|
}
|
|
1225
1363
|
|
|
@@ -1227,12 +1365,12 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1227
1365
|
if (d?._type === "findResult" && d.text) {
|
|
1228
1366
|
const rendered = renderFindResults(d.text);
|
|
1229
1367
|
const info = `${FG_DIM}${d.matchCount} files${RST}`;
|
|
1230
|
-
text.setText(` ${info}\n${rendered}`);
|
|
1368
|
+
text.setText(fillToolBackground(` ${info}\n${rendered}`));
|
|
1231
1369
|
return text;
|
|
1232
1370
|
}
|
|
1233
1371
|
|
|
1234
1372
|
const fallback = result.content?.[0]?.text ?? "found";
|
|
1235
|
-
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1373
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
|
|
1236
1374
|
return text;
|
|
1237
1375
|
},
|
|
1238
1376
|
});
|
|
@@ -1332,7 +1470,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1332
1470
|
const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
1333
1471
|
const glob = args?.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
|
|
1334
1472
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1335
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`);
|
|
1473
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`));
|
|
1336
1474
|
return text;
|
|
1337
1475
|
},
|
|
1338
1476
|
|
|
@@ -1346,7 +1484,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1346
1484
|
?.filter((c: any) => c.type === "text")
|
|
1347
1485
|
.map((c: any) => c.text || "")
|
|
1348
1486
|
.join("\n") ?? "Error";
|
|
1349
|
-
text.setText(
|
|
1487
|
+
text.setText(renderToolError(e, theme));
|
|
1350
1488
|
return text;
|
|
1351
1489
|
}
|
|
1352
1490
|
|
|
@@ -1356,22 +1494,22 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1356
1494
|
if (ctx.state._gk !== key) {
|
|
1357
1495
|
ctx.state._gk = key;
|
|
1358
1496
|
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
1359
|
-
ctx.state._gt = ` ${info}
|
|
1497
|
+
ctx.state._gt = fillToolBackground(` ${info}`);
|
|
1360
1498
|
|
|
1361
1499
|
renderGrepResults(d.text, d.pattern)
|
|
1362
1500
|
.then((rendered: string) => {
|
|
1363
1501
|
if (ctx.state._gk !== key) return;
|
|
1364
|
-
ctx.state._gt = ` ${info}\n${rendered}
|
|
1502
|
+
ctx.state._gt = fillToolBackground(` ${info}\n${rendered}`);
|
|
1365
1503
|
ctx.invalidate();
|
|
1366
1504
|
})
|
|
1367
1505
|
.catch(() => {});
|
|
1368
1506
|
}
|
|
1369
|
-
text.setText(ctx.state._gt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`);
|
|
1507
|
+
text.setText(ctx.state._gt ?? fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}`));
|
|
1370
1508
|
return text;
|
|
1371
1509
|
}
|
|
1372
1510
|
|
|
1373
1511
|
const fallback = result.content?.[0]?.text ?? "searched";
|
|
1374
|
-
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1512
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
|
|
1375
1513
|
return text;
|
|
1376
1514
|
},
|
|
1377
1515
|
});
|
|
@@ -175,6 +175,9 @@ describe("piPrettyExtension integration", () => {
|
|
|
175
175
|
|
|
176
176
|
function makeDeps(withFFF: boolean, finderOverrides?: Record<string, any>): PiPrettyDeps {
|
|
177
177
|
const finder = mkFinder(finderOverrides);
|
|
178
|
+
const fffModule = finderOverrides?.FileFinder
|
|
179
|
+
? { FileFinder: finderOverrides.FileFinder }
|
|
180
|
+
: { FileFinder: { create: vi.fn().mockReturnValue({ ok: true, value: finder }) } };
|
|
178
181
|
return {
|
|
179
182
|
sdk: {
|
|
180
183
|
createReadToolDefinition: mockToolFactory(readExec),
|
|
@@ -185,9 +188,7 @@ describe("piPrettyExtension integration", () => {
|
|
|
185
188
|
getAgentDir: () => "/tmp/pi-pretty-test",
|
|
186
189
|
},
|
|
187
190
|
TextComponent: class { private t = ""; setText(v: string) { this.t = v; } getText() { return this.t; } },
|
|
188
|
-
fffModule: withFFF
|
|
189
|
-
? { FileFinder: { create: vi.fn().mockReturnValue({ ok: true, value: finder }) } }
|
|
190
|
-
: undefined,
|
|
191
|
+
fffModule: withFFF ? fffModule : undefined,
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -445,6 +446,18 @@ describe("piPrettyExtension integration", () => {
|
|
|
445
446
|
// ---- session lifecycle ---------------------------------------------
|
|
446
447
|
|
|
447
448
|
describe("session lifecycle", () => {
|
|
449
|
+
it("stores FFF data under a pi-pretty-specific directory", async () => {
|
|
450
|
+
const create = vi.fn().mockReturnValue({ ok: true, value: mkFinder() });
|
|
451
|
+
load(true, { FileFinder: { create } });
|
|
452
|
+
const start = events.get("session_start")!;
|
|
453
|
+
expect(start, "session_start not registered").toBeDefined();
|
|
454
|
+
await start({}, { cwd: "/tmp/test" });
|
|
455
|
+
expect(create).toHaveBeenCalledWith(expect.objectContaining({
|
|
456
|
+
frecencyDbPath: "/tmp/pi-pretty-test/pi-pretty/fff/frecency.mdb",
|
|
457
|
+
historyDbPath: "/tmp/pi-pretty-test/pi-pretty/fff/history.mdb",
|
|
458
|
+
}));
|
|
459
|
+
});
|
|
460
|
+
|
|
448
461
|
it("shutdown → subsequent find falls back to SDK", async () => {
|
|
449
462
|
await loadWithFFF();
|
|
450
463
|
await events.get("session_shutdown")!();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import piPrettyExtension, { __imageInternals } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
const ENV_KEYS = [
|
|
6
|
+
"TMUX",
|
|
7
|
+
"TERM",
|
|
8
|
+
"TERM_PROGRAM",
|
|
9
|
+
"LC_TERMINAL",
|
|
10
|
+
"GHOSTTY_RESOURCES_DIR",
|
|
11
|
+
"KITTY_WINDOW_ID",
|
|
12
|
+
"KITTY_PID",
|
|
13
|
+
"WEZTERM_EXECUTABLE",
|
|
14
|
+
"WEZTERM_CONFIG_DIR",
|
|
15
|
+
"WEZTERM_CONFIG_FILE",
|
|
16
|
+
"COLORTERM",
|
|
17
|
+
"PRETTY_IMAGE_PROTOCOL",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
class MockText {
|
|
21
|
+
private text = "";
|
|
22
|
+
constructor(_text = "", _x = 0, _y = 0) {}
|
|
23
|
+
setText(value: string) {
|
|
24
|
+
this.text = value;
|
|
25
|
+
}
|
|
26
|
+
getText() {
|
|
27
|
+
return this.text;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mockToolFactory(exec: any) {
|
|
32
|
+
return (_cwd: string) => ({
|
|
33
|
+
name: "mock",
|
|
34
|
+
description: "mock",
|
|
35
|
+
parameters: { type: "object", properties: {} },
|
|
36
|
+
execute: exec,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadReadTool(readExec: any) {
|
|
41
|
+
const noopExec = async () => ({ content: [{ type: "text", text: "" }] });
|
|
42
|
+
const tools = new Map<string, any>();
|
|
43
|
+
const pi = {
|
|
44
|
+
registerTool: (tool: any) => tools.set(tool.name, tool),
|
|
45
|
+
registerCommand: () => {},
|
|
46
|
+
on: () => {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
piPrettyExtension(pi, {
|
|
50
|
+
sdk: {
|
|
51
|
+
createReadToolDefinition: mockToolFactory(readExec),
|
|
52
|
+
createBashToolDefinition: mockToolFactory(noopExec),
|
|
53
|
+
createLsToolDefinition: mockToolFactory(noopExec),
|
|
54
|
+
createFindToolDefinition: mockToolFactory(noopExec),
|
|
55
|
+
createGrepToolDefinition: mockToolFactory(noopExec),
|
|
56
|
+
getAgentDir: () => "/tmp/pi-pretty-test",
|
|
57
|
+
},
|
|
58
|
+
TextComponent: MockText,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return tools.get("read");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("image rendering terminal detection", () => {
|
|
65
|
+
const envSnapshot = new Map<string, string | undefined>();
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
for (const key of ENV_KEYS) {
|
|
69
|
+
envSnapshot.set(key, process.env[key]);
|
|
70
|
+
delete process.env[key];
|
|
71
|
+
}
|
|
72
|
+
__imageInternals.resetCachesForTests();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
for (const key of ENV_KEYS) {
|
|
77
|
+
const value = envSnapshot.get(key);
|
|
78
|
+
if (value === undefined) delete process.env[key];
|
|
79
|
+
else process.env[key] = value;
|
|
80
|
+
}
|
|
81
|
+
__imageInternals.resetCachesForTests();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("detects kitty protocol inside tmux via KITTY_WINDOW_ID", () => {
|
|
85
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
86
|
+
process.env.TERM_PROGRAM = "tmux";
|
|
87
|
+
process.env.KITTY_WINDOW_ID = "1";
|
|
88
|
+
|
|
89
|
+
expect(__imageInternals.getOuterTerminal()).toBe("kitty");
|
|
90
|
+
expect(__imageInternals.detectImageProtocol()).toBe("kitty");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("detects wezterm protocol inside tmux via WEZTERM_EXECUTABLE", () => {
|
|
94
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
95
|
+
process.env.TERM_PROGRAM = "tmux";
|
|
96
|
+
process.env.WEZTERM_EXECUTABLE = "/Applications/WezTerm.app/Contents/MacOS/wezterm";
|
|
97
|
+
|
|
98
|
+
expect(__imageInternals.getOuterTerminal()).toBe("WezTerm");
|
|
99
|
+
expect(__imageInternals.detectImageProtocol()).toBe("iterm2");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("falls back to tmux client term for outer terminal detection", () => {
|
|
103
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
104
|
+
process.env.TERM_PROGRAM = "tmux";
|
|
105
|
+
__imageInternals.setTmuxClientTermOverrideForTests("xterm-kitty");
|
|
106
|
+
|
|
107
|
+
expect(__imageInternals.getOuterTerminal()).toBe("kitty");
|
|
108
|
+
expect(__imageInternals.detectImageProtocol()).toBe("kitty");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("reports warning when tmux allow-passthrough is off", () => {
|
|
112
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
113
|
+
__imageInternals.setTmuxAllowPassthroughOverrideForTests(false);
|
|
114
|
+
|
|
115
|
+
expect(__imageInternals.getTmuxPassthroughWarning("kitty")).toContain("allow-passthrough is off");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("does not warn when tmux allow-passthrough is enabled", () => {
|
|
119
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
120
|
+
__imageInternals.setTmuxAllowPassthroughOverrideForTests(true);
|
|
121
|
+
|
|
122
|
+
expect(__imageInternals.getTmuxPassthroughWarning("kitty")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("renders explicit warning for read image when tmux passthrough is off", async () => {
|
|
126
|
+
process.env.TMUX = "/tmp/tmux-1000/default,123,0";
|
|
127
|
+
process.env.TERM_PROGRAM = "tmux";
|
|
128
|
+
process.env.KITTY_WINDOW_ID = "1";
|
|
129
|
+
__imageInternals.setTmuxAllowPassthroughOverrideForTests(false);
|
|
130
|
+
|
|
131
|
+
const readTool = loadReadTool(async () => ({
|
|
132
|
+
content: [{ type: "image", data: Buffer.from("fake").toString("base64"), mimeType: "image/png" }],
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
const result = await readTool.execute("t1", { path: "media/inline-image.png" }, null, null, {});
|
|
136
|
+
const rendered = readTool.renderResult(result, {}, {}, {
|
|
137
|
+
lastComponent: new MockText(),
|
|
138
|
+
isError: false,
|
|
139
|
+
state: {},
|
|
140
|
+
expanded: false,
|
|
141
|
+
invalidate: () => {},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(rendered.getText()).toContain("allow-passthrough is off");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("warns on non-PNG payloads for kitty protocol", async () => {
|
|
148
|
+
process.env.TERM_PROGRAM = "kitty";
|
|
149
|
+
|
|
150
|
+
const readTool = loadReadTool(async () => ({
|
|
151
|
+
content: [{ type: "image", data: Buffer.from("jpeg").toString("base64"), mimeType: "image/jpeg" }],
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
const result = await readTool.execute("t1", { path: "media/photo.jpg" }, null, null, {});
|
|
155
|
+
const rendered = readTool.renderResult(result, {}, {}, {
|
|
156
|
+
lastComponent: new MockText(),
|
|
157
|
+
isError: false,
|
|
158
|
+
state: {},
|
|
159
|
+
expanded: false,
|
|
160
|
+
invalidate: () => {},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(rendered.getText()).toContain("supports PNG payloads");
|
|
164
|
+
});
|
|
165
|
+
});
|