@oh-my-pi/pi-coding-agent 11.10.2 → 11.10.4

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,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.10.4] - 2026-02-10
6
+
7
+ ### Added
8
+
9
+ - Hashline diff computation with `computeHashlineDiff` function for preview rendering of hashline-mode edits
10
+ - Streaming preview display for hashline edits in tool execution UI showing edit sources and destinations
11
+ - Streaming hash line computation with progress updates via `onUpdate` callback in read tool
12
+ - Optional `onCollectedLine` callback parameter to `streamLinesFromFile` for line collection tracking
13
+
14
+ ### Changed
15
+
16
+ - Edit tool renderer now displays computed preview diffs for hashline operations before execution
17
+ - Read tool now streams hash lines incrementally instead of computing them all at once, improving responsiveness for large files
18
+ - Refactored hash line formatting to use async `streamHashLinesFromLines` for better performance
19
+
20
+ ## [11.10.3] - 2026-02-10
21
+ ### Added
22
+
23
+ - Exported `./patch/*` subpath for direct access to patch utilities
24
+
5
25
  ## [11.10.2] - 2026-02-10
6
26
 
7
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.10.2",
3
+ "version": "11.10.4",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -58,6 +58,10 @@
58
58
  "types": "./src/internal-urls/*.ts",
59
59
  "import": "./src/internal-urls/*.ts"
60
60
  },
61
+ "./patch/*": {
62
+ "types": "./src/patch/*.ts",
63
+ "import": "./src/patch/*.ts"
64
+ },
61
65
  "./*": {
62
66
  "types": "./src/*.ts",
63
67
  "import": "./src/*.ts"
@@ -80,12 +84,12 @@
80
84
  },
81
85
  "dependencies": {
82
86
  "@mozilla/readability": "0.6.0",
83
- "@oh-my-pi/omp-stats": "11.10.2",
84
- "@oh-my-pi/pi-agent-core": "11.10.2",
85
- "@oh-my-pi/pi-ai": "11.10.2",
86
- "@oh-my-pi/pi-natives": "11.10.2",
87
- "@oh-my-pi/pi-tui": "11.10.2",
88
- "@oh-my-pi/pi-utils": "11.10.2",
87
+ "@oh-my-pi/omp-stats": "11.10.4",
88
+ "@oh-my-pi/pi-agent-core": "11.10.4",
89
+ "@oh-my-pi/pi-ai": "11.10.4",
90
+ "@oh-my-pi/pi-natives": "11.10.4",
91
+ "@oh-my-pi/pi-tui": "11.10.4",
92
+ "@oh-my-pi/pi-utils": "11.10.4",
89
93
  "@sinclair/typebox": "^0.34.48",
90
94
  "ajv": "^8.17.1",
91
95
  "chalk": "^5.6.2",
@@ -6,9 +6,9 @@ import {
6
6
  type Model,
7
7
  normalizeDomain,
8
8
  } from "@oh-my-pi/pi-ai";
9
- import { type ConfigError, ConfigFile } from "@oh-my-pi/pi-coding-agent/config";
10
9
  import { type Static, Type } from "@sinclair/typebox";
11
10
  import AjvModule from "ajv";
11
+ import { type ConfigError, ConfigFile } from "../config";
12
12
  import type { ThemeColor } from "../modes/theme/theme";
13
13
  import type { AuthStorage } from "../session/auth-storage";
14
14
 
@@ -13,14 +13,14 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import type { ModelRole } from "@oh-my-pi/pi-coding-agent/config/model-registry";
17
- import { type EditMode, normalizeEditMode } from "@oh-my-pi/pi-coding-agent/patch";
18
16
  import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
19
17
  import { YAML } from "bun";
20
18
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
21
19
  import { getAgentDbPath, getAgentDir } from "../config";
20
+ import type { ModelRole } from "../config/model-registry";
22
21
  import { loadCapability } from "../discovery";
23
22
  import { setColorBlindMode, setSymbolPreset, setTheme } from "../modes/theme/theme";
23
+ import { type EditMode, normalizeEditMode } from "../patch";
24
24
  import { AgentStorage } from "../session/agent-storage";
25
25
  import { withFileLock } from "./file-lock";
26
26
  import {
@@ -15,7 +15,13 @@ import {
15
15
  import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
16
16
  import type { Theme } from "../../modes/theme/theme";
17
17
  import { theme } from "../../modes/theme/theme";
18
- import { computeEditDiff, computePatchDiff, type EditDiffError, type EditDiffResult } from "../../patch";
18
+ import {
19
+ computeEditDiff,
20
+ computeHashlineDiff,
21
+ computePatchDiff,
22
+ type EditDiffError,
23
+ type EditDiffResult,
24
+ } from "../../patch";
19
25
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
20
26
  import {
21
27
  formatArgsInline,
@@ -192,6 +198,24 @@ export class ToolExecutionComponent extends Container {
192
198
  });
193
199
  return;
194
200
  }
201
+ const edits = this.#args?.edits;
202
+ if (path && Array.isArray(edits)) {
203
+ const argsKey = JSON.stringify({ path, edits });
204
+ if (this.#editDiffArgsKey === argsKey) return;
205
+ this.#editDiffArgsKey = argsKey;
206
+
207
+ computeHashlineDiff({ path, edits }, this.#cwd).then(result => {
208
+ if (this.#editDiffArgsKey === argsKey) {
209
+ this.#editDiffPreview = result;
210
+ if ("diff" in result && result.diff) {
211
+ (this.#args as Record<string, unknown>).previewDiff = result.diff;
212
+ }
213
+ this.#updateDisplay();
214
+ this.#ui.requestRender();
215
+ }
216
+ });
217
+ return;
218
+ }
195
219
 
196
220
  const oldText = this.#args?.old_text;
197
221
  const newText = this.#args?.new_text;
package/src/patch/diff.ts CHANGED
@@ -8,8 +8,9 @@ import * as Diff from "diff";
8
8
  import { resolveToCwd } from "../tools/path-utils";
9
9
  import { previewPatch } from "./applicator";
10
10
  import { DEFAULT_FUZZY_THRESHOLD, findMatch } from "./fuzzy";
11
+ import { applyHashlineEdits } from "./hashline";
11
12
  import { adjustIndentation, normalizeToLF, stripBom } from "./normalize";
12
- import type { DiffError, DiffResult, PatchInput } from "./types";
13
+ import type { DiffError, DiffResult, HashlineEdit, PatchInput } from "./types";
13
14
  import { EditMatchError } from "./types";
14
15
 
15
16
  // ═══════════════════════════════════════════════════════════════════════════
@@ -363,3 +364,45 @@ export async function computePatchDiff(
363
364
  return { error: err instanceof Error ? err.message : String(err) };
364
365
  }
365
366
  }
367
+ /**
368
+ * Compute the diff for a hashline operation without applying it.
369
+ * Used for preview rendering in the TUI before hashline-mode edits execute.
370
+ */
371
+ export async function computeHashlineDiff(
372
+ input: { path: string; edits: HashlineEdit[] },
373
+ cwd: string,
374
+ ): Promise<DiffResult | DiffError> {
375
+ const { path, edits } = input;
376
+ const absolutePath = resolveToCwd(path, cwd);
377
+
378
+ try {
379
+ const file = Bun.file(absolutePath);
380
+ try {
381
+ if (!(await file.exists())) {
382
+ return { error: `File not found: ${path}` };
383
+ }
384
+ } catch {
385
+ return { error: `File not found: ${path}` };
386
+ }
387
+
388
+ let rawContent: string;
389
+ try {
390
+ rawContent = await file.text();
391
+ } catch (error) {
392
+ const message = error instanceof Error ? error.message : String(error);
393
+ return { error: message || `Unable to read ${path}` };
394
+ }
395
+
396
+ const { text: content } = stripBom(rawContent);
397
+ const normalizedContent = normalizeToLF(content);
398
+
399
+ const result = applyHashlineEdits(normalizedContent, edits);
400
+ if (normalizedContent === result.content) {
401
+ return { error: `No changes would be made to ${path}. The edits produce identical content.` };
402
+ }
403
+
404
+ return generateDiffString(normalizedContent, result.content);
405
+ } catch (err) {
406
+ return { error: err instanceof Error ? err.message : String(err) };
407
+ }
408
+ }
@@ -44,7 +44,14 @@ import { EditMatchError } from "./types";
44
44
  // Application
45
45
  export { applyPatch, defaultFileSystem, previewPatch } from "./applicator";
46
46
  // Diff generation
47
- export { computeEditDiff, computePatchDiff, generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
47
+ export {
48
+ computeEditDiff,
49
+ computeHashlineDiff,
50
+ computePatchDiff,
51
+ generateDiffString,
52
+ generateUnifiedDiffString,
53
+ replaceText,
54
+ } from "./diff";
48
55
 
49
56
  // Fuzzy matching
50
57
  export { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch as findEditMatch, findMatch, seekSequence } from "./fuzzy";
@@ -21,7 +21,7 @@ import {
21
21
  } from "../tools/render-utils";
22
22
  import type { RenderCallOptions } from "../tools/renderers";
23
23
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
24
- import type { DiffError, DiffResult, Operation } from "./types";
24
+ import type { DiffError, DiffResult, HashlineEdit, Operation } from "./types";
25
25
 
26
26
  // ═══════════════════════════════════════════════════════════════════════════
27
27
  // LSP Batching
@@ -77,6 +77,12 @@ interface EditRenderArgs {
77
77
  op?: Operation;
78
78
  rename?: string;
79
79
  diff?: string;
80
+ /**
81
+ * Computed preview diff (used when tool args don't include a diff, e.g. hashline mode).
82
+ */
83
+ previewDiff?: string;
84
+ // Hashline mode fields
85
+ edits?: HashlineEdit[];
80
86
  }
81
87
 
82
88
  /** Extended context for edit tool rendering */
@@ -94,22 +100,80 @@ function countLines(text: string): number {
94
100
  return text.split("\n").length;
95
101
  }
96
102
 
97
- function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme): string {
103
+ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
98
104
  if (!diff) return "";
99
105
  const lines = diff.split("\n");
100
106
  const total = lines.length;
101
107
  const displayLines = lines.slice(-EDIT_STREAMING_PREVIEW_LINES);
102
108
  const hidden = total - displayLines.length;
103
-
104
109
  let text = "\n\n";
105
110
  if (hidden > 0) {
106
111
  text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
107
112
  }
108
113
  text += renderDiffColored(displayLines.join("\n"), { filePath: rawPath });
109
- text += uiTheme.fg("dim", `\n… (streaming)`);
114
+ text += uiTheme.fg("dim", `\n… (${label})`);
110
115
  return text;
111
116
  }
112
117
 
118
+ function formatStreamingHashlineEdits(edits: HashlineEdit[], uiTheme: Theme, ui: ToolUIKit): string {
119
+ const MAX_EDITS = 4;
120
+ const MAX_DST_LINES = 8;
121
+
122
+ let text = "\n\n";
123
+ text += uiTheme.fg("dim", `[${edits.length} hashline edit${edits.length === 1 ? "" : "s"}]`);
124
+ text += "\n";
125
+
126
+ let shownEdits = 0;
127
+ let shownDstLines = 0;
128
+
129
+ for (const edit of edits) {
130
+ shownEdits++;
131
+ if (shownEdits > MAX_EDITS) break;
132
+
133
+ text += uiTheme.fg("toolOutput", ui.truncate(replaceTabs(formatHashlineSrc(edit.src)), 120));
134
+ text += "\n";
135
+
136
+ if (edit.dst === "") {
137
+ text += uiTheme.fg("dim", ui.truncate(" (delete)", 120));
138
+ text += "\n";
139
+ continue;
140
+ }
141
+
142
+ const dstLines = edit.dst.split("\n");
143
+ for (const dstLine of dstLines) {
144
+ shownDstLines++;
145
+ if (shownDstLines > MAX_DST_LINES) break;
146
+ text += uiTheme.fg("toolOutput", ui.truncate(replaceTabs(`+ ${dstLine}`), 120));
147
+ text += "\n";
148
+ }
149
+ if (shownDstLines > MAX_DST_LINES) break;
150
+ }
151
+
152
+ if (edits.length > MAX_EDITS) {
153
+ text += uiTheme.fg("dim", `… (${edits.length - MAX_EDITS} more edits)`);
154
+ }
155
+ if (shownDstLines > MAX_DST_LINES) {
156
+ text += uiTheme.fg("dim", `\n… (${shownDstLines - MAX_DST_LINES} more dst lines)`);
157
+ }
158
+
159
+ return text.trimEnd();
160
+
161
+ function formatHashlineSrc(src: HashlineEdit["src"]): string {
162
+ switch (src.kind) {
163
+ case "single":
164
+ return `• single ${src.ref}`;
165
+ case "range":
166
+ return `• range ${src.start}..${src.end}`;
167
+ case "insertAfter":
168
+ return `• insertAfter ${src.after}..`;
169
+ case "insertBefore":
170
+ return `• insertBefore ..${src.before}`;
171
+ case "substring":
172
+ return `• substring ${JSON.stringify(src.needle)}`;
173
+ }
174
+ }
175
+ }
176
+
113
177
  function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
114
178
  const icon = uiTheme.getLangIcon(language);
115
179
  if (lineCount !== null) {
@@ -175,8 +239,12 @@ export const editToolRenderer = {
175
239
  let text = `${ui.title(opTitle)} ${spinner ? `${spinner} ` : ""}${editIcon} ${pathDisplay}`;
176
240
 
177
241
  // Show streaming preview of diff/content
178
- if (args.diff && args.op) {
242
+ if (args.previewDiff) {
243
+ text += formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
244
+ } else if (args.diff && args.op) {
179
245
  text += formatStreamingDiff(args.diff, rawPath, uiTheme);
246
+ } else if (args.edits && args.edits.length > 0) {
247
+ text += formatStreamingHashlineEdits(args.edits, uiTheme, ui);
180
248
  } else if (args.diff) {
181
249
  const previewLines = args.diff.split("\n");
182
250
  const maxLines = 6;
@@ -4,7 +4,6 @@
4
4
  * Tree-based rendering with collapsed/expanded states for web search results.
5
5
  */
6
6
 
7
- import { getSearchProvider } from "@oh-my-pi/pi-coding-agent/web/search/provider";
8
7
  import type { Component } from "@oh-my-pi/pi-tui";
9
8
  import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
10
9
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
@@ -23,6 +22,7 @@ import {
23
22
  } from "../../tools/render-utils";
24
23
  import { renderStatusLine, renderTreeList } from "../../tui";
25
24
  import { CachedOutputBlock } from "../../tui/output-block";
25
+ import { getSearchProvider } from "./provider";
26
26
  import type { SearchResponse } from "./types";
27
27
 
28
28
  const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;