@oh-my-pi/pi-coding-agent 14.5.0 → 14.5.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.5.1] - 2026-04-26
6
+
7
+ ### Removed
8
+
9
+ - Removed `\t` escaped-tab indentation autocorrect from hashline and atom edit modes (and the `PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS` environment toggle); literal `\t` in edit content is now preserved verbatim
10
+ - Removed the suspicious-`\uDDDD` warning preflight from hashline edits
11
+ - Removed the hand-rolled JSON unescape fallback in the streaming edit-arg renderer; partial fragments that fail `JSON.parse` are now surfaced raw rather than partially decoded with a non-spec-compliant unescaper that mishandled lone surrogates
12
+
5
13
  ## [14.4.3] - 2026-04-26
6
14
  ### Added
7
15
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.5.0",
4
+ "version": "14.5.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.20.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "@oh-my-pi/omp-stats": "14.5.0",
50
- "@oh-my-pi/pi-agent-core": "14.5.0",
51
- "@oh-my-pi/pi-ai": "14.5.0",
52
- "@oh-my-pi/pi-natives": "14.5.0",
53
- "@oh-my-pi/pi-tui": "14.5.0",
54
- "@oh-my-pi/pi-utils": "14.5.0",
49
+ "@oh-my-pi/omp-stats": "14.5.1",
50
+ "@oh-my-pi/pi-agent-core": "14.5.1",
51
+ "@oh-my-pi/pi-ai": "14.5.1",
52
+ "@oh-my-pi/pi-natives": "14.5.1",
53
+ "@oh-my-pi/pi-tui": "14.5.1",
54
+ "@oh-my-pi/pi-utils": "14.5.1",
55
55
  "@puppeteer/browsers": "^2.13.0",
56
56
  "@sinclair/typebox": "^0.34.49",
57
57
  "@xterm/headless": "^6.0.0",
@@ -557,31 +557,6 @@ function validateNoConflictingAnchorOps(edits: AtomEdit[]): void {
557
557
  // Apply
558
558
  // ═══════════════════════════════════════════════════════════════════════════
559
559
 
560
- function maybeAutocorrectEscapedTabIndentation(edits: AtomEdit[], warnings: string[]): void {
561
- const enabled = Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS !== "0";
562
- if (!enabled) return;
563
- for (const edit of edits) {
564
- if (edit.op !== "splice" && edit.op !== "pre" && edit.op !== "post") continue;
565
- if (edit.lines.length === 0) continue;
566
- const hasEscapedTabs = edit.lines.some(line => line.includes("\\t"));
567
- if (!hasEscapedTabs) continue;
568
- const hasRealTabs = edit.lines.some(line => line.includes("\t"));
569
- if (hasRealTabs) continue;
570
- let correctedCount = 0;
571
- const corrected = edit.lines.map(line =>
572
- line.replace(/^((?:\\t)+)/, escaped => {
573
- correctedCount += escaped.length / 2;
574
- return "\t".repeat(escaped.length / 2);
575
- }),
576
- );
577
- if (correctedCount === 0) continue;
578
- edit.lines = corrected;
579
- warnings.push(
580
- `Auto-corrected escaped tab indentation in edit: converted leading \\t sequence(s) to real tab characters`,
581
- );
582
- }
583
- }
584
-
585
560
  export interface AtomNoopEdit {
586
561
  editIndex: number;
587
562
  loc: string;
@@ -612,7 +587,6 @@ export function applyAtomEdits(
612
587
  throw new HashlineMismatchError(mismatches, fileLines);
613
588
  }
614
589
  validateNoConflictingAnchorOps(edits);
615
- maybeAutocorrectEscapedTabIndentation(edits, warnings);
616
590
 
617
591
  const trackFirstChanged = (line: number) => {
618
592
  if (firstChangedLine === undefined || line < firstChangedLine) {
@@ -681,55 +681,6 @@ export function tryRebaseAnchor(
681
681
  return found;
682
682
  }
683
683
 
684
- function isEscapedTabAutocorrectEnabled(): boolean {
685
- switch (Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS) {
686
- case "0":
687
- return false;
688
- case "1":
689
- return true;
690
- default:
691
- return true;
692
- }
693
- }
694
-
695
- function maybeAutocorrectEscapedTabIndentation(edits: HashlineEdit[], warnings: string[]): void {
696
- if (!isEscapedTabAutocorrectEnabled()) return;
697
- for (const edit of edits) {
698
- if (edit.lines.length === 0) continue;
699
- const hasEscapedTabs = edit.lines.some(line => line.includes("\\t"));
700
- if (!hasEscapedTabs) continue;
701
- const hasRealTabs = edit.lines.some(line => line.includes("\t"));
702
- if (hasRealTabs) continue;
703
- let correctedCount = 0;
704
- const corrected = edit.lines.map(line =>
705
- line.replace(/^((?:\\t)+)/, escaped => {
706
- correctedCount += escaped.length / 2;
707
- return "\t".repeat(escaped.length / 2);
708
- }),
709
- );
710
- if (correctedCount === 0) continue;
711
- edit.lines = corrected;
712
- warnings.push(
713
- `Auto-corrected escaped tab indentation in edit: converted leading \\t sequence(s) to real tab characters`,
714
- );
715
- }
716
- }
717
-
718
- function maybeWarnSuspiciousUnicodeEscapePlaceholder(edits: HashlineEdit[], warnings: string[]): void {
719
- for (const edit of edits) {
720
- if (edit.lines.length === 0) continue;
721
- if (!edit.lines.some(line => /\\uDDDD/i.test(line))) continue;
722
- warnings.push(
723
- `Detected literal \\uDDDD in edit content; no autocorrection applied. Verify whether this should be a real Unicode escape or plain text.`,
724
- );
725
- }
726
- }
727
-
728
- function runHashlinePreflightSanitizers(edits: HashlineEdit[], warnings: string[]): void {
729
- maybeAutocorrectEscapedTabIndentation(edits, warnings);
730
- maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings);
731
- }
732
-
733
684
  function ensureHashlineEditHasContent(edit: HashlineEdit): void {
734
685
  if (edit.lines.length === 0) {
735
686
  edit.lines = [""];
@@ -1026,7 +977,6 @@ export function applyHashlineEdits(
1026
977
  if (mismatches.length > 0) {
1027
978
  throw new HashlineMismatchError(mismatches, fileLines);
1028
979
  }
1029
- runHashlinePreflightSanitizers(edits, warnings);
1030
980
  for (const edit of edits) {
1031
981
  collectBoundaryDuplicationWarning(edit, originalFileLines, warnings);
1032
982
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Edit tool renderer and LSP batching helpers.
3
3
  */
4
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
4
5
  import type { Component } from "@oh-my-pi/pi-tui";
5
6
  import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
6
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -157,32 +158,16 @@ function filePathFromEditEntry(p: string | undefined): string | undefined {
157
158
  }
158
159
 
159
160
  function decodePartialJsonStringFragment(fragment: string): string {
160
- let text = fragment;
161
+ // Trim a trailing partial escape so JSON.parse sees a well-formed string.
162
+ let text = fragment.replace(/\\u[0-9a-fA-F]{0,3}$/, "");
161
163
  const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
162
- if (trailingBackslashes % 2 === 1) {
163
- text = text.slice(0, -1);
164
- }
164
+ if (trailingBackslashes % 2 === 1) text = text.slice(0, -1);
165
165
  try {
166
166
  return JSON.parse(`"${text}"`) as string;
167
167
  } catch {
168
- return text
169
- .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)))
170
- .replace(/\\(["\\/bfnrt])/g, (_, ch: string) => {
171
- switch (ch) {
172
- case "b":
173
- return "\b";
174
- case "f":
175
- return "\f";
176
- case "n":
177
- return "\n";
178
- case "r":
179
- return "\r";
180
- case "t":
181
- return "\t";
182
- default:
183
- return ch;
184
- }
185
- });
168
+ // Streaming fragment isn't a valid JSON string yet; surface it raw rather
169
+ // than ad-hoc unescaping that mishandles surrogates and partial escapes.
170
+ return text;
186
171
  }
187
172
  }
188
173
 
@@ -243,11 +228,11 @@ function formatEditDescription(
243
228
  };
244
229
  }
245
230
 
246
- function renderPlainTextPreview(text: string, uiTheme: Theme): string {
247
- const previewLines = text.split("\n");
231
+ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string): string {
232
+ const previewLines = sanitizeText(text).split("\n");
248
233
  let preview = "\n\n";
249
234
  for (const line of previewLines.slice(0, CALL_TEXT_PREVIEW_LINES)) {
250
- preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), CALL_TEXT_PREVIEW_WIDTH))}\n`;
235
+ preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line, filePath), CALL_TEXT_PREVIEW_WIDTH))}\n`;
251
236
  }
252
237
  if (previewLines.length > CALL_TEXT_PREVIEW_LINES) {
253
238
  preview += uiTheme.fg("dim", `… ${previewLines.length - CALL_TEXT_PREVIEW_LINES} more lines`);
@@ -284,7 +269,7 @@ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: T
284
269
  if (!preview.diff && !preview.error) continue;
285
270
  const header = uiTheme.fg("dim", `\n\n\u2500\u2500 ${shortenPath(preview.path)} \u2500\u2500`);
286
271
  if (preview.error) {
287
- parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
272
+ parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error, preview.path))}`);
288
273
  continue;
289
274
  }
290
275
  if (preview.diff) {
@@ -311,10 +296,10 @@ function getCallPreview(
311
296
  return formatStreamingDiff(args.diff, rawPath, uiTheme);
312
297
  }
313
298
  if (args.diff) {
314
- return renderPlainTextPreview(args.diff, uiTheme);
299
+ return renderPlainTextPreview(args.diff, uiTheme, rawPath);
315
300
  }
316
301
  if (args.newText || args.patch) {
317
- return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme);
302
+ return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme, rawPath);
318
303
  }
319
304
  return "";
320
305
  }
@@ -438,7 +423,7 @@ export const editToolRenderer = {
438
423
  }
439
424
  text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
440
425
  if (applyPatchSummary?.error) {
441
- text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error), CALL_TEXT_PREVIEW_WIDTH))}`;
426
+ text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
442
427
  }
443
428
 
444
429
  return new Text(text, 0, 0);
@@ -529,13 +514,13 @@ function renderSingleFileResult(
529
514
 
530
515
  if (isError) {
531
516
  if (errorText) {
532
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
517
+ text += `\n\n${uiTheme.fg("error", replaceTabs(errorText, rawPath))}`;
533
518
  }
534
519
  } else if (details?.diff) {
535
520
  text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
536
521
  } else if (editDiffPreview) {
537
522
  if ("error" in editDiffPreview) {
538
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
523
+ text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath))}`;
539
524
  } else if (editDiffPreview.diff) {
540
525
  text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
541
526
  }
@@ -1,3 +1,4 @@
1
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
1
2
  import { getIndentation } from "@oh-my-pi/pi-utils";
2
3
  import * as Diff from "diff";
3
4
  import { theme } from "../../modes/theme/theme";
@@ -15,7 +16,7 @@ const DIM_OFF = "\x1b[22m";
15
16
  */
16
17
  function visualizeIndent(text: string, filePath?: string): string {
17
18
  const match = text.match(/^([ \t]+)/);
18
- if (!match) return replaceTabs(text);
19
+ if (!match) return replaceTabs(text, filePath);
19
20
  const indent = match[1];
20
21
  const rest = text.slice(indent.length);
21
22
  const tabWidth = getIndentation(filePath);
@@ -30,7 +31,7 @@ function visualizeIndent(text: string, filePath?: string): string {
30
31
  visible += `${DIM}·${DIM_OFF}`;
31
32
  }
32
33
  }
33
- return `${visible}${replaceTabs(rest)}`;
34
+ return `${visible}${replaceTabs(rest, filePath)}`;
34
35
  }
35
36
 
36
37
  /**
@@ -106,7 +107,7 @@ export interface RenderDiffOptions {
106
107
  * - Added lines: green, with inverse on changed tokens
107
108
  */
108
109
  export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
109
- const lines = diffText.split("\n");
110
+ const lines = sanitizeText(diffText).split("\n");
110
111
  const result: string[] = [];
111
112
  const parsedLines = lines.map(parseDiffLine);
112
113
  const lineNumberWidth = parsedLines.reduce((width, parsed) => {
@@ -138,7 +139,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
138
139
 
139
140
  if (!parsed) {
140
141
  prevLineNum = "";
141
- result.push(theme.fg("toolDiffContext", line));
142
+ result.push(theme.fg("toolDiffContext", replaceTabs(line, options.filePath)));
142
143
  i++;
143
144
  continue;
144
145
  }
@@ -172,12 +172,18 @@ export class EventController {
172
172
  const signature = `${textContent}\u0000${imageCount}`;
173
173
 
174
174
  this.#resetReadGroup();
175
- if (this.ctx.optimisticUserMessageSignature !== signature) {
175
+ const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
176
+ if (!wasOptimistic) {
176
177
  this.ctx.addMessageToChat(event.message);
177
178
  }
178
179
  this.ctx.optimisticUserMessageSignature = undefined;
179
180
 
180
- if (!event.message.synthetic) {
181
+ // Clear the editor only when the submission did not originate from this
182
+ // session's optimistic flow (which already cleared the editor at submit
183
+ // time). Clearing here on the optimistic path would race with the user
184
+ // typing the next prompt while the previous large redraw lands and erase
185
+ // their in-progress draft (#783).
186
+ if (!event.message.synthetic && !wasOptimistic) {
181
187
  this.ctx.editor.setText("");
182
188
  this.ctx.updatePendingMessagesDisplay();
183
189
  }
@@ -14,7 +14,7 @@ import { SearchProvider } from "./base";
14
14
  import { findCredential, isApiKeyAvailable } from "./utils";
15
15
 
16
16
  const ZAI_MCP_URL = "https://api.z.ai/api/mcp/web_search_prime/mcp";
17
- const ZAI_TOOL_NAME = "webSearchPrime";
17
+ const ZAI_TOOL_NAME = "web_search_prime";
18
18
  const DEFAULT_NUM_RESULTS = 10;
19
19
 
20
20
  export interface ZaiSearchParams {