@heyhuynhgiabuu/pi-pretty 0.6.17 → 0.6.19

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/src/tools/read.ts CHANGED
@@ -1,27 +1,53 @@
1
1
  /* pi-pretty: read tool -- file reading with syntax highlighting and inline image support. */
2
2
 
3
+ import { basename, dirname } from "node:path";
3
4
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
- import { Image as TuiImage } from "@earendil-works/pi-tui";
5
5
  import {
6
6
  BG_BASE,
7
7
  BG_ERROR,
8
8
  FG_DIM,
9
9
  FG_LNUM,
10
10
  FG_RULE,
11
- MAX_PREVIEW_LINES,
12
11
  RST,
13
12
  resolveBaseBackground,
14
13
  TOOL_RESULT_INDENT,
15
14
  termWidth,
16
15
  } from "../config.js";
17
16
  import { normalizeLineEndings, shortPath } from "../helpers.js";
18
- import { fillToolBackground, renderFileContent, renderToolError, renderToolMetrics } from "../render.js";
17
+ import { fillToolBackground, renderFileContent, renderToolError } from "../render.js";
19
18
  import { resolveTextCtor } from "../tui-text.js";
20
19
  import type { ReadDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
21
20
  import { wrapExecuteWithMetrics } from "./metrics.js";
22
21
 
23
22
  type Result = AgentToolResult<Record<string, unknown>>;
24
23
 
24
+ function getSkillName(filePath: string, content: string): string | undefined {
25
+ if (basename(filePath) !== "SKILL.md") return undefined;
26
+
27
+ const lines = content.split("\n");
28
+ if (lines[0]?.trim() === "---") {
29
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
30
+ for (const line of lines.slice(1, end < 0 ? 1 : end)) {
31
+ const match = /^name\s*:\s*(.+?)\s*$/.exec(line);
32
+ if (!match) continue;
33
+ const value = match[1].trim();
34
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
35
+ return value.slice(1, -1).trim() || basename(dirname(filePath));
36
+ }
37
+ return value || basename(dirname(filePath));
38
+ }
39
+ }
40
+
41
+ return basename(dirname(filePath));
42
+ }
43
+
44
+ function renderSkillHeader(skillName: string, expanded: boolean, theme: ThemeLike): string {
45
+ const label = theme.fg("accent", "[skill]");
46
+ const name = theme.fg("toolTitle", skillName);
47
+ const hint = theme.fg("dim", `ctrl+o to ${expanded ? "collapse" : "expand"}`);
48
+ return `${label} ${name} ${hint}`;
49
+ }
50
+
25
51
  export function registerReadTool(
26
52
  pi: ExtensionAPI,
27
53
  cwd: string,
@@ -48,8 +74,6 @@ export function registerReadTool(
48
74
  result.details = {
49
75
  _type: "readImage",
50
76
  filePath: String(p.path ?? ""),
51
- data: imageBlock.data,
52
- mimeType: imageBlock.mimeType ?? "image/png",
53
77
  } as ReadDetails;
54
78
  return result;
55
79
  }
@@ -92,19 +116,13 @@ export function registerReadTool(
92
116
 
93
117
  const d = result.details as ReadDetails | undefined;
94
118
 
95
- // Image rendering — use pi-tui Image component
119
+ // Image content is preserved for ToolExecution's host-generic image pass.
120
+ // Keep the SDK's text note visible as a fallback when host images are hidden
121
+ // or unsupported by the terminal.
96
122
  if (d?._type === "readImage") {
97
- const mimeType = d.mimeType.startsWith("image/svg") ? "image/svg+xml" : d.mimeType;
98
- return new TuiImage(
99
- d.data,
100
- mimeType,
101
- {
102
- fallbackColor: (text) => theme.fg("toolTitle", text),
103
- },
104
- {
105
- filename: d.filePath,
106
- },
107
- );
123
+ const note = getText(result);
124
+ text.setText(note ? fillToolBackground(note, BG_BASE) : "");
125
+ return text;
108
126
  }
109
127
 
110
128
  // File content — line-numbered display
@@ -112,9 +130,16 @@ export function registerReadTool(
112
130
  const tw = termWidth();
113
131
  const lines = d.content.split("\n");
114
132
  const total = lines.length;
115
- const p2 = shortPath(cwd, home, String(d.filePath ?? ""));
133
+ const filePath = String(d.filePath ?? "");
134
+ const skillName = getSkillName(filePath, d.content);
135
+ const p2 = shortPath(cwd, home, filePath);
116
136
  const off2 = typeof d.offset === "number" ? `:${d.offset}` : "";
117
137
  if (!ctx.expanded) {
138
+ if (skillName) {
139
+ const header = renderSkillHeader(skillName, false, theme);
140
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
141
+ return text;
142
+ }
118
143
  text.setText(
119
144
  fillToolBackground(
120
145
  `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}\n${TOOL_RESULT_INDENT}${FG_DIM}${total} lines — ctrl+o to expand${RST}\n`,
@@ -129,7 +154,9 @@ export function registerReadTool(
129
154
  const gw = nw + 3;
130
155
  const cw = Math.max(1, tw - gw);
131
156
 
132
- const header = `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
157
+ const header = skillName
158
+ ? renderSkillHeader(skillName, true, theme)
159
+ : `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
133
160
  const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`];
134
161
  out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
135
162
  for (let i = 0; i < show.length; i++) {
@@ -156,7 +183,10 @@ export function registerReadTool(
156
183
  .split("\n")
157
184
  .map((l) => `${TOOL_RESULT_INDENT}${l}`)
158
185
  .join("\n");
159
- const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${padded}\n`;
186
+ const divider = skillName
187
+ ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n`
188
+ : "";
189
+ const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${divider}${padded}\n`;
160
190
  text.setText(fillToolBackground(rendered, BG_BASE));
161
191
  (ctx as any).state._rt = rendered;
162
192
  })
package/src/tui-text.ts CHANGED
@@ -1,52 +1,17 @@
1
- /**
2
- * Lazy resolver for pi-tui Text constructor.
3
- *
4
- * Returns the real Text class from @earendil-works/pi-tui, or a stub when
5
- * pi-tui is unavailable (e.g. not in jiti's alias map). This avoids crashing
6
- * during tool registration when pi-tui isn't aliased.
7
- *
8
- * The resolution is cached — the require() call happens at most once.
9
- */
1
+ import { Text } from "@earendil-works/pi-tui";
10
2
 
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- type TextCtor = new (t?: string, x?: number, y?: number) => { setText(v: string): void; [k: string]: any };
3
+ type TextCtor = new (
4
+ text?: string,
5
+ paddingX?: number,
6
+ paddingY?: number,
7
+ ) => {
8
+ setText(value: string): void;
9
+ };
13
10
 
14
- /** No-op stub that satisfies the Text interface so rendering doesn't crash. */
15
- class StubText {
16
- setText(_v: string): void {
17
- // no-op — pi-tui not available
18
- }
19
- }
20
-
21
- let _ctor: TextCtor | null = null;
22
- let _resolved = false;
23
-
24
- function resolve(): TextCtor {
25
- if (_ctor) return _ctor;
26
- _resolved = true;
27
- try {
28
- // eslint-disable-next-line @typescript-eslint/no-var-requires
29
- const { Text } = require("@earendil-works/pi-tui") as { Text: TextCtor };
30
- _ctor = Text;
31
- return _ctor;
32
- } catch {
33
- _ctor = StubText;
34
- return _ctor;
35
- }
36
- }
37
-
38
- /**
39
- * Returns a Text constructor, always valid. Falls back to StubText if
40
- * @earendil-works/pi-tui is unavailable (caught and cached).
41
- */
42
11
  export function getTextCtor(): TextCtor {
43
- return resolve();
12
+ return Text;
44
13
  }
45
14
 
46
- /**
47
- * Returns TextComp if provided, otherwise the lazy-resolved Text constructor.
48
- * Always returns a valid constructor (never undefined/null).
49
- */
50
15
  export function resolveTextCtor(TextComp?: TextCtor): TextCtor {
51
- return TextComp ?? resolve();
16
+ return TextComp ?? Text;
52
17
  }
package/src/types.ts CHANGED
@@ -51,7 +51,7 @@ export interface ComponentLike {
51
51
  // ---------------------------------------------------------------------------
52
52
 
53
53
  export type ReadDetails =
54
- | { _type: "readImage"; filePath: string; data: string; mimeType: string }
54
+ | { _type: "readImage"; filePath: string }
55
55
  | { _type: "readFile"; filePath: string; content: string; offset: number; lineCount: number };
56
56
 
57
57
  export interface BashDetails extends Record<string, unknown> {
@@ -174,17 +174,6 @@ export interface CursorStore {
174
174
  get(id: string): unknown | undefined;
175
175
  }
176
176
 
177
- // ---------------------------------------------------------------------------
178
- // Constraint parsing
179
- // ---------------------------------------------------------------------------
180
-
181
- export interface ConstraintParseResult {
182
- ok: boolean;
183
- globs: string[];
184
- tokens: string[];
185
- error?: string;
186
- }
187
-
188
177
  // ---------------------------------------------------------------------------
189
178
  // DI
190
179
  // ---------------------------------------------------------------------------
@@ -1,14 +0,0 @@
1
- import { FG_DIM, RST, TOOL_RESULT_INDENT } from "./config.js";
2
-
3
- /** Shown in collapsed tool results; Pi keybinding is still `app.tools.expand` (Ctrl+O). */
4
- export const COLLAPSED_EXPAND_LABEL = "ctrl+o to expand";
5
-
6
- /** Collapsed hint line (no extra blank lines above/below). */
7
- export function collapsedExpandBlock(countLabel: string): string {
8
- return `${TOOL_RESULT_INDENT}${FG_DIM}${countLabel} — ${COLLAPSED_EXPAND_LABEL}${RST}\n`;
9
- }
10
-
11
- /** When line count / metrics already appear on the header line (bash). */
12
- export function collapsedExpandFooter(): string {
13
- return `${TOOL_RESULT_INDENT}${FG_DIM}${COLLAPSED_EXPAND_LABEL}${RST}\n`;
14
- }
package/src/expand.ts DELETED
@@ -1,10 +0,0 @@
1
- import { MAX_PREVIEW_LINES } from "./config.js";
2
-
3
- /** Render context from pi tool UI (Ctrl+O toggles `expanded` per tool block). */
4
- export type ToolRenderCtx = { expanded?: boolean };
5
-
6
- /** Lines to show in tool result body when collapsed vs expanded. */
7
- export function previewLineCount(ctx: ToolRenderCtx, totalLines: number): number {
8
- if (ctx.expanded) return totalLines;
9
- return Math.min(totalLines, MAX_PREVIEW_LINES);
10
- }
package/src/image.ts DELETED
@@ -1,129 +0,0 @@
1
- /**
2
- * pi-pretty: Terminal image rendering (iTerm2 / Kitty / Ghostty inline image protocols).
3
- * Handles tmux passthrough for image protocols.
4
- */
5
-
6
- import { execFileSync } from "node:child_process";
7
-
8
- type ImageProtocol = "iterm2" | "kitty" | "none";
9
-
10
- let _tmuxClientTermCache: string | null | undefined;
11
- let _tmuxAllowPassthroughCache: boolean | null | undefined;
12
- let _tmuxClientTermOverrideForTests: string | null | undefined;
13
- let _tmuxAllowPassthroughOverrideForTests: boolean | null | undefined;
14
-
15
- function isTmuxSession(): boolean {
16
- return !!process.env.TMUX || /^(tmux|screen)/.test(process.env.TERM ?? "");
17
- }
18
-
19
- function normalizeTerminalName(term: string): string {
20
- const t = term.toLowerCase();
21
- if (t.includes("kitty")) return "kitty";
22
- if (t.includes("ghostty")) return "ghostty";
23
- if (t.includes("wezterm")) return "WezTerm";
24
- if (t.includes("iterm")) return "iTerm.app";
25
- if (t.includes("mintty")) return "mintty";
26
- return term;
27
- }
28
-
29
- function readTmuxClientTerm(): string | null {
30
- if (_tmuxClientTermOverrideForTests !== undefined) {
31
- return _tmuxClientTermOverrideForTests ? normalizeTerminalName(_tmuxClientTermOverrideForTests) : null;
32
- }
33
- if (!isTmuxSession()) return null;
34
- if (_tmuxClientTermCache !== undefined) return _tmuxClientTermCache;
35
- try {
36
- const term = execFileSync("tmux", ["display-message", "-p", "#{client_termname}"], {
37
- encoding: "utf8",
38
- stdio: ["ignore", "pipe", "ignore"],
39
- timeout: 200,
40
- }).trim();
41
- _tmuxClientTermCache = term ? normalizeTerminalName(term) : null;
42
- } catch {
43
- _tmuxClientTermCache = null;
44
- }
45
- return _tmuxClientTermCache;
46
- }
47
-
48
- function getOuterTerminal(): string {
49
- if (process.env.LC_TERMINAL === "iTerm2") return "iTerm.app";
50
- if (process.env.GHOSTTY_RESOURCES_DIR) return "ghostty";
51
- if (process.env.KITTY_WINDOW_ID || process.env.KITTY_PID) return "kitty";
52
- if (process.env.WEZTERM_EXECUTABLE || process.env.WEZTERM_CONFIG_DIR || process.env.WEZTERM_CONFIG_FILE) {
53
- return "WezTerm";
54
- }
55
- const termProgram = process.env.TERM_PROGRAM ?? "";
56
- if (termProgram && termProgram !== "tmux" && termProgram !== "screen") {
57
- return normalizeTerminalName(termProgram);
58
- }
59
- const tmuxClientTerm = readTmuxClientTerm();
60
- if (tmuxClientTerm) return tmuxClientTerm;
61
- const term = process.env.TERM ?? "";
62
- if (term) return normalizeTerminalName(term);
63
- if (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit") return "unknown-modern";
64
- return termProgram;
65
- }
66
-
67
- function detectImageProtocol(): ImageProtocol {
68
- const forced = (process.env.PRETTY_IMAGE_PROTOCOL ?? "").toLowerCase();
69
- if (forced === "kitty" || forced === "iterm2" || forced === "none") return forced;
70
- const term = getOuterTerminal();
71
- if (term === "ghostty" || term === "kitty") return "kitty";
72
- if (["iTerm.app", "WezTerm", "mintty"].includes(term)) return "iterm2";
73
- if (process.env.LC_TERMINAL === "iTerm2") return "iterm2";
74
- return "none";
75
- }
76
-
77
- function tmuxAllowsPassthrough(): boolean | null {
78
- if (_tmuxAllowPassthroughOverrideForTests !== undefined) return _tmuxAllowPassthroughOverrideForTests;
79
- if (!isTmuxSession()) return null;
80
- if (_tmuxAllowPassthroughCache !== undefined) return _tmuxAllowPassthroughCache;
81
- try {
82
- const value = execFileSync("tmux", ["show-options", "-gv", "allow-passthrough"], {
83
- encoding: "utf8",
84
- stdio: ["ignore", "pipe", "ignore"],
85
- timeout: 200,
86
- })
87
- .trim()
88
- .toLowerCase();
89
- _tmuxAllowPassthroughCache = value === "on" || value === "all";
90
- } catch {
91
- _tmuxAllowPassthroughCache = null;
92
- }
93
- return _tmuxAllowPassthroughCache;
94
- }
95
-
96
- function getTmuxPassthroughWarning(protocol: ImageProtocol): string | null {
97
- if (!isTmuxSession() || protocol === "none") return null;
98
- if (tmuxAllowsPassthrough() === false) {
99
- return "tmux allow-passthrough is off. Run: tmux set -g allow-passthrough on";
100
- }
101
- return null;
102
- }
103
-
104
- function tmuxWrap(seq: string): string {
105
- if (!isTmuxSession()) return seq;
106
- const escaped = seq.split("\x1b").join("\x1b\x1b");
107
- return `\x1bPtmux;${escaped}\x1b\\`;
108
- }
109
-
110
- export const __imageInternals = {
111
- isTmuxSession,
112
- getOuterTerminal,
113
- detectImageProtocol,
114
- tmuxWrap,
115
- tmuxAllowsPassthrough,
116
- getTmuxPassthroughWarning,
117
- setTmuxClientTermOverrideForTests: (value: string | null | undefined) => {
118
- _tmuxClientTermOverrideForTests = value;
119
- },
120
- setTmuxAllowPassthroughOverrideForTests: (value: boolean | null | undefined) => {
121
- _tmuxAllowPassthroughOverrideForTests = value;
122
- },
123
- resetCachesForTests: () => {
124
- _tmuxClientTermCache = undefined;
125
- _tmuxAllowPassthroughCache = undefined;
126
- _tmuxClientTermOverrideForTests = undefined;
127
- _tmuxAllowPassthroughOverrideForTests = undefined;
128
- },
129
- };