@oh-my-pi/pi-coding-agent 15.3.0 → 15.3.2

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
@@ -118,22 +118,8 @@ function formatTextWithMode(
118
118
  startNum: number,
119
119
  shouldAddHashLines: boolean,
120
120
  shouldAddLineNumbers: boolean,
121
- truncatedLines?: ReadonlySet<number>,
122
121
  ): string {
123
- if (shouldAddHashLines) {
124
- if (!truncatedLines || truncatedLines.size === 0) return formatHashLines(text, startNum);
125
- // Column-truncated lines hash differently from the on-disk line that the
126
- // edit verifier reads back. Drop the anchor (`LINE|TEXT` instead of
127
- // `LINE+HASH|TEXT`) so the model treats the line as un-anchorable rather
128
- // than copying a hash that will be rejected as stale.
129
- const lines = text.split("\n");
130
- return lines
131
- .map((line, i) => {
132
- const ln = startNum + i;
133
- return truncatedLines.has(ln) ? `${ln}${HL_BODY_SEP}${line}` : formatHashLine(ln, line);
134
- })
135
- .join("\n");
136
- }
122
+ if (shouldAddHashLines) return formatHashLines(text, startNum);
137
123
  if (shouldAddLineNumbers) return prependLineNumbers(text, startNum);
138
124
  return text;
139
125
  }
@@ -1045,14 +1031,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1045
1031
  }
1046
1032
 
1047
1033
  const collectedLines = streamResult.lines;
1048
- const truncatedLineNumbers = new Set<number>();
1049
1034
  if (!rawSelector && maxColumns > 0) {
1050
1035
  for (let i = 0; i < collectedLines.length; i++) {
1051
1036
  const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
1052
1037
  if (wasTruncated) {
1053
1038
  collectedLines[i] = text;
1054
1039
  columnTruncated = maxColumns;
1055
- truncatedLineNumbers.add(range.startLine + i);
1056
1040
  }
1057
1041
  }
1058
1042
  }
@@ -1062,15 +1046,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1062
1046
  }
1063
1047
 
1064
1048
  const blockText = collectedLines.join("\n");
1065
- blocks.push(
1066
- formatTextWithMode(
1067
- blockText,
1068
- range.startLine,
1069
- shouldAddHashLines,
1070
- shouldAddLineNumbers,
1071
- truncatedLineNumbers,
1072
- ),
1073
- );
1049
+ blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
1074
1050
  }
1075
1051
 
1076
1052
  let outputText = blocks.join("\n\n…\n\n");
@@ -1814,14 +1790,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1814
1790
  // view — column truncation surfaces separately via `.limits()`.
1815
1791
  const rawSelector = isRawSelector(parsed);
1816
1792
  const maxColumns = resolveOutputMaxColumns(this.session.settings);
1817
- const truncatedLineNumbers = new Set<number>();
1818
1793
  if (!rawSelector && maxColumns > 0) {
1819
1794
  for (let i = 0; i < collectedLines.length; i++) {
1820
1795
  const { text, wasTruncated } = truncateLine(collectedLines[i], maxColumns);
1821
1796
  if (wasTruncated) {
1822
1797
  collectedLines[i] = text;
1823
1798
  columnTruncated = maxColumns;
1824
- truncatedLineNumbers.add(startLineDisplay + i);
1825
1799
  }
1826
1800
  }
1827
1801
  }
@@ -1855,13 +1829,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1855
1829
  let capturedDisplayContent: { text: string; startLine: number } | undefined;
1856
1830
  const formatText = (text: string, startNum: number): string => {
1857
1831
  capturedDisplayContent = { text, startLine: startNum };
1858
- return formatTextWithMode(
1859
- text,
1860
- startNum,
1861
- shouldAddHashLines,
1862
- shouldAddLineNumbers,
1863
- truncatedLineNumbers,
1864
- );
1832
+ return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
1865
1833
  };
1866
1834
 
1867
1835
  let outputText: string;
@@ -1,6 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import type { ClipboardImage } from "@oh-my-pi/pi-natives";
3
3
  import * as native from "@oh-my-pi/pi-natives";
4
+ import { logger } from "@oh-my-pi/pi-utils";
4
5
 
5
6
  function hasDisplay(): boolean {
6
7
  return process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
@@ -80,7 +81,7 @@ if ($img -ne $null) {
80
81
  }
81
82
  `;
82
83
 
83
- const POWERSHELL_TIMEOUT_MS = 5000;
84
+ const POWERSHELL_TIMEOUT_MS = 8000;
84
85
 
85
86
  /**
86
87
  * Read a clipboard image through the Windows host's PowerShell.
@@ -104,6 +105,12 @@ async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
104
105
  try {
105
106
  stdout = await new Response(proc.stdout).text();
106
107
  await proc.exited;
108
+ } catch (err) {
109
+ // powershell.exe is a Windows process reached over WSL interop; if it
110
+ // doesn't reap cleanly, swallow the error so the dispatcher can fall
111
+ // through to the native bridge instead of throwing.
112
+ logger.warn("clipboard: powershell read failed", { error: String(err) });
113
+ return null;
107
114
  } finally {
108
115
  clearTimeout(timer);
109
116
  }
@@ -136,8 +143,12 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
136
143
  if (isWsl()) {
137
144
  const image = await readImageViaPowerShell();
138
145
  if (image) return image;
139
- // Fall through: arboard may still succeed on a future WSLg release.
140
- } else if (!hasDisplay()) {
146
+ // Fall through: arboard may still succeed on a future WSLg release
147
+ // but only when we actually have a display server. Headless WSL has
148
+ // no display, so arboard would reject anyway.
149
+ }
150
+
151
+ if (!hasDisplay()) {
141
152
  return null;
142
153
  }
143
154
 
@@ -23,16 +23,27 @@ export interface ResizedImage {
23
23
  // binding constraint once images are downsized to 1568px (Anthropic's internal threshold).
24
24
  const DEFAULT_MAX_BYTES = 500 * 1024;
25
25
 
26
- const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
26
+ const DEFAULT_OPTIONS: Required<Omit<ImageResizeOptions, "excludeWebP">> = {
27
27
  // Anthropic's "internal recommended size" — Claude internally caps images at
28
28
  // 1568px on the longest edge before vision processing.
29
29
  maxWidth: 1568,
30
30
  maxHeight: 1568,
31
31
  maxBytes: DEFAULT_MAX_BYTES,
32
32
  jpegQuality: 80,
33
- excludeWebP: Bun.env.OMP_NO_WEBP !== undefined,
34
33
  };
35
34
 
35
+ /**
36
+ * Read `OMP_NO_WEBP` per-call so runtime toggles take effect.
37
+ * Only `"1"` and `"true"` (case-insensitive) enable exclusion — an empty string
38
+ * or `"0"` MUST be treated as disabled.
39
+ */
40
+ function isWebPExcluded(): boolean {
41
+ const raw = Bun.env.OMP_NO_WEBP;
42
+ if (raw === undefined) return false;
43
+ const v = raw.toLowerCase();
44
+ return v === "1" || v === "true";
45
+ }
46
+
36
47
  /** Pick the smallest of N encoded buffers. */
37
48
  function pickSmallest(...candidates: Array<{ buffer: Uint8Array; mimeType: string }>): {
38
49
  buffer: Uint8Array;
@@ -62,7 +73,8 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
62
73
  * off the JS thread when the terminal (`.bytes()`) is awaited.
63
74
  */
64
75
  export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
65
- const opts = { ...DEFAULT_OPTIONS, ...options };
76
+ const excludeWebP = options?.excludeWebP ?? isWebPExcluded();
77
+ const opts = { ...DEFAULT_OPTIONS, ...options, excludeWebP };
66
78
  const inputBuffer = Buffer.from(img.data, "base64");
67
79
 
68
80
  try {
@@ -75,7 +87,12 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
75
87
  // still get JPEG-compressed.
76
88
  const originalSize = inputBuffer.length;
77
89
  const comfortableSize = opts.maxBytes / 4;
78
- if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
90
+ if (
91
+ originalWidth <= opts.maxWidth &&
92
+ originalHeight <= opts.maxHeight &&
93
+ originalSize <= comfortableSize &&
94
+ !(opts.excludeWebP && sourceMime === "image/webp")
95
+ ) {
79
96
  return {
80
97
  buffer: inputBuffer,
81
98
  mimeType: sourceMime,
@@ -251,7 +268,13 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
251
268
  },
252
269
  };
253
270
  } catch {
254
- // Failed to load image
271
+ // Bun.Image rejected the input — we cannot decode/re-encode it.
272
+ // When the caller demanded WebP exclusion AND the original is WebP,
273
+ // returning the original buffer would silently violate that contract,
274
+ // so surface an explicit error instead.
275
+ if (excludeWebP && (img.mimeType === "image/webp" || !img.mimeType)) {
276
+ throw new Error("resizeImage: failed to decode image and cannot honor excludeWebP for a WebP source");
277
+ }
255
278
  return {
256
279
  buffer: inputBuffer,
257
280
  mimeType: img.mimeType,