@heyhuynhgiabuu/pi-pretty 0.6.18 → 0.6.20

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
  }
@@ -76,7 +100,7 @@ export function registerReadTool(
76
100
 
77
101
  const path = String(args.path ?? "");
78
102
  const label = theme.fg("error", theme.bold("→ read"));
79
- text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
103
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
80
104
  return text;
81
105
  },
82
106
 
@@ -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/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> {
@@ -138,10 +138,15 @@ export interface SdkToolDef {
138
138
 
139
139
  export interface SdkTools {
140
140
  createReadTool?: (cwd: string) => SdkToolDef;
141
+ createReadToolDefinition?: (cwd: string) => SdkToolDef;
141
142
  createBashTool?: (cwd: string) => SdkToolDef;
143
+ createBashToolDefinition?: (cwd: string) => SdkToolDef;
142
144
  createLsTool?: (cwd: string) => SdkToolDef;
145
+ createLsToolDefinition?: (cwd: string) => SdkToolDef;
143
146
  createFindTool?: (cwd: string) => SdkToolDef;
147
+ createFindToolDefinition?: (cwd: string) => SdkToolDef;
144
148
  createGrepTool?: (cwd: string) => SdkToolDef;
149
+ createGrepToolDefinition?: (cwd: string) => SdkToolDef;
145
150
  getAgentDir?: () => string;
146
151
  }
147
152
 
@@ -174,17 +179,6 @@ export interface CursorStore {
174
179
  get(id: string): unknown | undefined;
175
180
  }
176
181
 
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
182
  // ---------------------------------------------------------------------------
189
183
  // DI
190
184
  // ---------------------------------------------------------------------------
@@ -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
- };