@xynogen/pix-pretty 1.7.18 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.7.18",
3
+ "version": "1.7.20",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/types.ts CHANGED
@@ -185,6 +185,7 @@ export type FindResultDetails = {
185
185
  _type: "findResult";
186
186
  text: string;
187
187
  pattern: string;
188
+ path?: string;
188
189
  matchCount: number;
189
190
  };
190
191
 
@@ -192,6 +193,7 @@ export type GrepResultDetails = {
192
193
  _type: "grepResult";
193
194
  text: string;
194
195
  pattern: string;
196
+ path?: string;
195
197
  matchCount: number;
196
198
  };
197
199
 
package/src/utils.test.ts CHANGED
@@ -2,7 +2,14 @@ import { describe, expect, it } from "bun:test";
2
2
 
3
3
  import { MAX_PREVIEW_LINES } from "./config.js";
4
4
  import type { FgTheme } from "./types.js";
5
- import { pluralize, renderDimPreview } from "./utils.js";
5
+ import {
6
+ formatCollapsedToolRow,
7
+ hideCollapsedToolCall,
8
+ pluralize,
9
+ renderCollapsedToolRow,
10
+ renderDimPreview,
11
+ setResultDetails,
12
+ } from "./utils.js";
6
13
 
7
14
  // Strip ANSI escapes so assertions test content, not color codes.
8
15
  const ANSI = /\x1b\[[0-9;]*m/g;
@@ -29,6 +36,47 @@ describe("pluralize", () => {
29
36
  });
30
37
  });
31
38
 
39
+ describe("collapsed tool rows", () => {
40
+ const rowTheme = { fg: (_key: string, text: string) => text, bold: (text: string) => text };
41
+
42
+ it("renders a consistent status, tool, target, and metadata row", () => {
43
+ expect(formatCollapsedToolRow(rowTheme, "read", "src/a.ts", "12 lines")).toBe(
44
+ "✓ read src/a.ts · 12 lines",
45
+ );
46
+ expect(plain(renderCollapsedToolRow(rowTheme, "read", "src/a.ts", "12 lines"))).toContain(
47
+ "✓ read src/a.ts · 12 lines",
48
+ );
49
+ });
50
+
51
+ it("hides only collapsed, non-expanded call rows", () => {
52
+ let value = "unchanged";
53
+ expect(hideCollapsedToolCall({ collapsed: true }, false, (text) => (value = text))).toBe(true);
54
+ expect(value).toBe("");
55
+ expect(hideCollapsedToolCall({ collapsed: true }, true, () => {})).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe("setResultDetails", () => {
60
+ it("preserves upstream metadata while adding renderer details", () => {
61
+ const result = {
62
+ content: [{ type: "text" as const, text: "output" }],
63
+ details: {
64
+ truncation: { truncated: true, totalLines: 500 },
65
+ fullOutputPath: "/tmp/full.log",
66
+ },
67
+ };
68
+
69
+ setResultDetails(result, { _type: "bashResult", exitCode: 0 });
70
+
71
+ expect(result.details as Record<string, unknown>).toEqual({
72
+ truncation: { truncated: true, totalLines: 500 },
73
+ fullOutputPath: "/tmp/full.log",
74
+ _type: "bashResult",
75
+ exitCode: 0,
76
+ });
77
+ });
78
+ });
79
+
32
80
  describe("renderDimPreview", () => {
33
81
  it("renders 'done' for empty input", () => {
34
82
  expect(plain(renderDimPreview("", theme))).toContain("done");
@@ -79,7 +127,9 @@ describe("renderDimPreview", () => {
79
127
  expect(plain(raw)).toContain("foo bar foo");
80
128
  });
81
129
 
82
- it("does not throw on an invalid highlight regex", () => {
83
- expect(() => renderDimPreview("text", theme, { highlight: "(" })).not.toThrow();
130
+ it("treats regex metacharacters as literal highlight text", () => {
131
+ const raw = renderDimPreview("call(foo)", theme, { highlight: "(" });
132
+ expect(plain(raw)).toContain("call(foo)");
133
+ expect(raw).toContain("\x1b[");
84
134
  });
85
135
  });
package/src/utils.ts CHANGED
@@ -52,6 +52,55 @@ export function pluralize(count: number, noun: string, plural?: string): string
52
52
  return `${count} ${count === 1 ? noun : (plural ?? `${noun}s`)}`;
53
53
  }
54
54
 
55
+ export type CollapsedToolStatus = "success" | "error" | "warning";
56
+
57
+ type CollapsedToolTheme = {
58
+ fg: (
59
+ key: "success" | "error" | "warning" | "toolTitle" | "muted" | "dim",
60
+ text: string,
61
+ ) => string;
62
+ bold: (text: string) => string;
63
+ };
64
+
65
+ /** Format the shared one-row content without assuming a render shell. */
66
+ export function formatCollapsedToolRow(
67
+ theme: CollapsedToolTheme,
68
+ tool: string,
69
+ target: string,
70
+ meta = "",
71
+ status: CollapsedToolStatus = "success",
72
+ ): string {
73
+ const icon = status === "success" ? "✓" : status === "warning" ? "⚡" : "✗";
74
+ const parts = [
75
+ `${theme.fg(status, icon)} ${theme.fg("toolTitle", theme.bold(tool))}`,
76
+ target ? theme.fg("muted", target) : "",
77
+ meta ? `${theme.fg("dim", "·")} ${theme.fg("dim", meta)}` : "",
78
+ ].filter(Boolean);
79
+ return parts.join(" ");
80
+ }
81
+
82
+ /** Render shared one-row content for tools using the self-rendered shell. */
83
+ export function renderCollapsedToolRow(
84
+ theme: CollapsedToolTheme,
85
+ tool: string,
86
+ target: string,
87
+ meta = "",
88
+ status: CollapsedToolStatus = "success",
89
+ ): string {
90
+ return fillToolBackground(` ${formatCollapsedToolRow(theme, tool, target, meta, status)}`);
91
+ }
92
+
93
+ /** Hide renderCall after its paired result has auto-collapsed. */
94
+ export function hideCollapsedToolCall(
95
+ state: { collapsed?: boolean },
96
+ expanded: boolean,
97
+ setText: (text: string) => void,
98
+ ): boolean {
99
+ if (!state.collapsed || expanded) return false;
100
+ setText("");
101
+ return true;
102
+ }
103
+
55
104
  export type DimPreviewOptions = {
56
105
  maxLines?: number;
57
106
  header?: string;
@@ -59,21 +108,23 @@ export type DimPreviewOptions = {
59
108
  highlight?: string;
60
109
  };
61
110
 
62
- function safeHighlightRegex(pattern: string): RegExp | null {
63
- try {
64
- return new RegExp(`(${pattern})`, "gi");
65
- } catch {
66
- return null;
67
- }
68
- }
111
+ function dimLineWithHighlight(line: string, theme: FgTheme, pattern?: string): string {
112
+ if (!pattern) return theme.fg("dim", line);
113
+ const foldedLine = line.toLocaleLowerCase();
114
+ const foldedPattern = pattern.toLocaleLowerCase();
115
+ if (!foldedPattern) return theme.fg("dim", line);
69
116
 
70
- function dimLineWithHighlight(line: string, theme: FgTheme, re: RegExp | null): string {
71
- if (!re) return theme.fg("dim", line);
72
- // split with capture group: odd indexes are matches
73
- return line
74
- .split(re)
75
- .map((part, i) => (i % 2 ? `${FG_GREEN}${BOLD}${part}${RST}` : theme.fg("dim", part)))
76
- .join("");
117
+ const parts: string[] = [];
118
+ let start = 0;
119
+ for (;;) {
120
+ const match = foldedLine.indexOf(foldedPattern, start);
121
+ if (match < 0) break;
122
+ if (match > start) parts.push(theme.fg("dim", line.slice(start, match)));
123
+ parts.push(`${FG_GREEN}${BOLD}${line.slice(match, match + pattern.length)}${RST}`);
124
+ start = match + pattern.length;
125
+ }
126
+ if (start < line.length) parts.push(theme.fg("dim", line.slice(start)));
127
+ return parts.length > 0 ? parts.join("") : theme.fg("dim", line);
77
128
  }
78
129
 
79
130
  export function renderDimPreview(
@@ -82,12 +133,12 @@ export function renderDimPreview(
82
133
  opts: DimPreviewOptions = {},
83
134
  ): string {
84
135
  const maxLines = opts.maxLines ?? MAX_PREVIEW_LINES;
85
- const re = opts.highlight ? safeHighlightRegex(opts.highlight) : null;
136
+ const highlight = opts.highlight;
86
137
  const output = normalizeLineEndings(text).trim() || "done";
87
138
  const lines = output.split("\n");
88
139
  const preview = lines
89
140
  .slice(0, maxLines)
90
- .map((line) => ` ${dimLineWithHighlight(line, theme, re)}`);
141
+ .map((line) => ` ${dimLineWithHighlight(line, theme, highlight)}`);
91
142
  if (opts.header) preview.unshift(` ${theme.fg("dim", opts.header)}`);
92
143
  if (lines.length > maxLines) {
93
144
  const more = pluralize(lines.length - maxLines, "more line");
@@ -208,8 +259,13 @@ export function getTextContent(result: ToolResultLike): string {
208
259
  );
209
260
  }
210
261
 
262
+ /** Add renderer metadata without discarding execution metadata from the upstream tool. */
211
263
  export function setResultDetails<T>(result: ToolResultLike, details: T): void {
212
- result.details = details;
264
+ const upstream =
265
+ result.details && typeof result.details === "object"
266
+ ? (result.details as Record<string, unknown>)
267
+ : undefined;
268
+ result.details = upstream ? { ...upstream, ...details } : details;
213
269
  }
214
270
 
215
271
  export function makeTextResult<TDetails>(