pi-hashline-edit-pro 2.7.0 → 2.7.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/read.ts CHANGED
@@ -3,14 +3,15 @@ import {
3
3
  createReadTool,
4
4
  formatSize,
5
5
  truncateHead,
6
+ DEFAULT_MAX_BYTES,
6
7
  DEFAULT_MAX_LINES,
7
8
  type TruncationResult,
8
9
  } from "@earendil-works/pi-coding-agent";
9
10
  import { Type } from "typebox";
10
- import { MAX_READ_LINE_BYTES } from "./constants";
11
11
  import { loadFileKindAndText } from "./file-kind";
12
+ import { MAX_OVERSIZED_WARNING_LINES } from "./constants";
12
13
  import { readNormFile, safeSnapId } from "./file-reader";
13
- import { lineHashes, fmtRegion, HASH_SEP, MAX_HASH_LINES } from "./hashline";
14
+ import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
14
15
  import { toCwd } from "./paths";
15
16
  import { abortIf, makePrepareArguments, visLines } from "./utils";
16
17
  import { recordServedSafe } from "./served";
@@ -56,7 +57,7 @@ export async function fmtReadPreview(
56
57
  options: { offset?: number; limit?: number },
57
58
  precomputedHashes?: string[],
58
59
  path?: string,
59
- maxLineBytes = MAX_READ_LINE_BYTES,
60
+ maxLineBytes = DEFAULT_MAX_BYTES,
60
61
  maxTruncLines = DEFAULT_MAX_LINES,
61
62
  ): Promise<{ text: string; truncation?: TruncationResult; nextOffset?: number; servedHashes: string[] }> {
62
63
  const allLines = visLines(text);
@@ -100,21 +101,27 @@ export async function fmtReadPreview(
100
101
  const oversized = rowSizes.filter((row) => row.bytes > maxBytes);
101
102
  const rows = rowSizes.map((row, index) =>
102
103
  row.bytes > maxBytes
103
- ? `[Line ${row.lineNumber} is ${formatSize(row.bytes)}, exceeds ${formatSize(maxBytes)}; content not shown. Use bash: sed -n '${row.lineNumber}p' <path> | head -c ${maxBytes}]`
104
+ ? fmtRow(selectedHashes[index]!, `[Line ${row.lineNumber} is ${formatSize(row.bytes)}, exceeds ${formatSize(maxBytes)}; content not shown. Use bash: sed -n '${row.lineNumber}p' <path> | head -c ${maxBytes}]`)
104
105
  : fmtRegion([selectedHashes[index]!], [selected[index]!]),
105
106
  );
106
107
  const skippedTruncation = truncateHead(rows.join("\n"), { maxBytes, maxLines: maxTruncLines });
107
108
  const shownRowCount = skippedTruncation.content === "" ? 0 : skippedTruncation.content.split("\n").length;
108
109
  const lastShownLine = shownRowCount > 0 ? startLine + shownRowCount - 1 : startLine - 1;
109
- const oversizedIndexes = new Set(rowSizes.map((row, index) => row.bytes > maxBytes ? index : -1).filter((index) => index >= 0));
110
110
  const servedHashes: string[] = [];
111
111
  for (let index = 0; index < Math.min(shownRowCount, rows.length); index++) {
112
- if (!oversizedIndexes.has(index)) servedHashes.push(selectedHashes[index]!);
112
+ servedHashes.push(selectedHashes[index]!);
113
113
  }
114
- const lineLabel = oversized.length === 1 ? `Line ${oversized[0]!.lineNumber}` : `Lines ${oversized.map((row) => row.lineNumber).join(", ")}`;
115
- const verb = oversized.length === 1 ? "exceeds" : "exceed";
116
- const addresses = oversized.map((row) => `${row.lineNumber}p`).join(";");
117
- const warning = `[${lineLabel} ${verb} ${formatSize(maxBytes)}; content not shown. Inspect with bash: sed -n '${addresses}' <path> | head -c ${maxBytes}]`;
114
+ const listed = oversized.slice(0, MAX_OVERSIZED_WARNING_LINES);
115
+ const hiddenCount = oversized.length - listed.length;
116
+ const lineLabel = oversized.length === 1
117
+ ? `Line ${oversized[0]!.lineNumber}`
118
+ : `Lines ${listed.map((row) => row.lineNumber).join(', ')}${hiddenCount > 0 ? `, ... (+${hiddenCount} more)` : ''}`;
119
+ const verb = oversized.length === 1 ? 'exceeds' : 'exceed';
120
+ const addresses = listed.map((row) => `${row.lineNumber}p`).join(';');
121
+ const moreHint = hiddenCount > 0
122
+ ? ` ${hiddenCount} more oversized line(s). Use read with offset to inspect them.`
123
+ : '';
124
+ const warning = `[${lineLabel} ${verb} ${formatSize(maxBytes)}; content not shown. Inspect with bash: sed -n '${addresses}' <path> | head -c ${maxBytes}${moreHint}]`;
118
125
  let preview = skippedTruncation.content;
119
126
  let nextOffset: number | undefined;
120
127
  if (shownRowCount > 0 && (skippedTruncation.truncated || lastShownLine < totalLines)) {
@@ -1,4 +1,5 @@
1
1
  import * as Diff from "diff";
2
+ import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
2
3
  import {
3
4
  _lineHashesPure,
4
5
  ANCHOR_LEN,
@@ -55,14 +56,23 @@ const ELLIPSIS_MARKER: unique symbol = Symbol("ellipsis");
55
56
  const isEllipsisMarker = (line: string | symbol): line is symbol =>
56
57
  line === ELLIPSIS_MARKER;
57
58
 
59
+ export interface DiffLimits {
60
+ maxLineBytes?: number;
61
+ maxBytes?: number;
62
+ unlimited?: boolean;
63
+ }
64
+
58
65
  export function genDiff(
59
66
  oldContent: string,
60
67
  newContent: string,
61
68
  contextLines = 2,
62
69
  newContentHashes?: string[],
63
70
  oldContentHashes?: string[],
71
+ limits?: DiffLimits,
64
72
  ): { diff: string; firstChangedLine: number | undefined } {
65
73
  const effectiveNewHashes = newContentHashes ?? _lineHashesPure(newContent);
74
+ const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
75
+ const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
66
76
 
67
77
  const parts = Diff.diffLines(oldContent, newContent);
68
78
  const output: string[] = [];
@@ -70,8 +80,42 @@ export function genDiff(
70
80
  let oldLineNum = 1;
71
81
  let lastWasChange = false;
72
82
  let firstChangedLine: number | undefined;
83
+ let outBytes = 0;
84
+ let stopped = false;
85
+ let diffTruncated = false;
86
+
87
+ const emitPlain = (line: string): void => {
88
+ if (stopped) return;
89
+ const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
90
+ if (outBytes + lineBytes > maxBytes) {
91
+ stopped = true;
92
+ diffTruncated = true;
93
+ return;
94
+ }
95
+ outBytes += lineBytes;
96
+ output.push(line);
97
+ };
98
+
99
+ const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined): void => {
100
+ if (stopped) return;
101
+ const full = fmtDiffLine(prefix, line, hash);
102
+ const rowBytes = Buffer.byteLength(full, "utf-8");
103
+ if (rowBytes > maxLineBytes) {
104
+ const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
105
+ emitPlain(fmtDiffLine(prefix, marker, hash));
106
+ return;
107
+ }
108
+ if (outBytes + rowBytes + 1 > maxBytes) {
109
+ stopped = true;
110
+ diffTruncated = true;
111
+ return;
112
+ }
113
+ outBytes += rowBytes + 1;
114
+ output.push(full);
115
+ };
73
116
 
74
117
  for (let i = 0; i < parts.length; i++) {
118
+ if (stopped) break;
75
119
  const part = parts[i]!;
76
120
  const raw = part.value.split("\n");
77
121
  if (raw[raw.length - 1] === "") raw.pop();
@@ -80,16 +124,18 @@ export function genDiff(
80
124
  if (part.added || part.removed) {
81
125
  if (firstChangedLine === undefined) firstChangedLine = newLineNum;
82
126
  for (let k = 0; k < displayLines.length; k++) {
127
+ if (stopped) break;
83
128
  if (part.added) {
84
129
  const hash = effectiveNewHashes[newLineNum - 1];
85
- output.push(fmtDiffLine("+", displayLines[k]!, hash));
130
+ emitRow("+", displayLines[k]!, hash);
86
131
  newLineNum++;
87
132
  } else {
88
133
  const hash = oldContentHashes?.[oldLineNum - 1];
89
- output.push(fmtDiffLine("-", displayLines[k]!, hash));
134
+ emitRow("-", displayLines[k]!, hash);
90
135
  oldLineNum++;
91
136
  }
92
137
  }
138
+ if (stopped) break;
93
139
  lastWasChange = true;
94
140
  continue;
95
141
  }
@@ -152,24 +198,25 @@ export function genDiff(
152
198
  }
153
199
 
154
200
  if (skipStart > 0) {
155
- output.push(" ...");
201
+ emitPlain(" ...");
156
202
  newLineNum += skipStart;
157
203
  oldLineNum += skipStart;
158
204
  }
159
205
  for (const line of linesToShow) {
206
+ if (stopped) break;
160
207
  if (isEllipsisMarker(line)) {
161
- output.push(" ...");
208
+ emitPlain(" ...");
162
209
  newLineNum += skipMiddle;
163
210
  oldLineNum += skipMiddle;
164
211
  continue;
165
212
  }
166
213
  const hash = effectiveNewHashes[newLineNum - 1];
167
- output.push(fmtDiffLine(" ", line, hash));
214
+ emitRow(" ", line, hash);
168
215
  newLineNum++;
169
216
  oldLineNum++;
170
217
  }
171
218
  if (skipTail > 0) {
172
- output.push(" ...");
219
+ emitPlain(" ...");
173
220
  }
174
221
  } else {
175
222
  newLineNum += displayLines.length;
@@ -178,6 +225,11 @@ export function genDiff(
178
225
  lastWasChange = false;
179
226
  }
180
227
 
228
+ if (diffTruncated) {
229
+ output.push(" ...");
230
+ output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
231
+ }
232
+
181
233
  return { diff: output.join("\n"), firstChangedLine };
182
234
  }
183
235
 
@@ -185,9 +237,41 @@ export function genPatch(
185
237
  path: string,
186
238
  oldContent: string,
187
239
  newContent: string,
188
- ): string {
189
- return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
240
+ limits?: DiffLimits,
241
+ ): { patch: string; truncated: boolean } {
242
+ const full = Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
190
243
  context: 4,
191
244
  headerOptions: Diff.FILE_HEADERS_ONLY,
192
245
  });
246
+ const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
247
+ const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
248
+ const out: string[] = [];
249
+ let outBytes = 0;
250
+ let truncated = false;
251
+ for (const line of full.split("\n")) {
252
+ const lineBytes = Buffer.byteLength(line, "utf-8");
253
+ if (lineBytes > maxLineBytes) {
254
+ truncated = true;
255
+ const prefix = /^[ +-]/.test(line) ? line[0]! : "";
256
+ const marker = `${prefix}[Patch line is ${formatSize(lineBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
257
+ const markerBytes = Buffer.byteLength(marker, "utf-8") + 1;
258
+ if (outBytes + markerBytes > maxBytes) {
259
+ break;
260
+ }
261
+ outBytes += markerBytes;
262
+ out.push(marker);
263
+ continue;
264
+ }
265
+ if (outBytes + lineBytes + 1 > maxBytes) {
266
+ truncated = true;
267
+ break;
268
+ }
269
+ outBytes += lineBytes + 1;
270
+ out.push(line);
271
+ }
272
+ if (truncated) {
273
+ out.push("...");
274
+ out.push(`[patch truncated at ${formatSize(maxBytes)}; the patch cannot be applied as-is. Use read to see the full file.]`);
275
+ }
276
+ return { patch: out.join("\n"), truncated };
193
277
  }
@@ -1,5 +1,5 @@
1
1
  import { Markdown, Text } from "@earendil-works/pi-tui";
2
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { normReq } from "./replace-normalize";
4
4
  import type { ReqParams, ReplaceDetails } from "./replace";
5
5
  import { isRec } from "./utils";
@@ -145,20 +145,46 @@ export function isApplied(
145
145
  );
146
146
  }
147
147
 
148
+ const RESULT_PREVIEW_LINES = 16;
149
+
150
+ function expandHint(): string {
151
+ try {
152
+ return keyHint("app.tools.expand", "to expand");
153
+ } catch {
154
+ return "ctrl+o to expand";
155
+ }
156
+ }
157
+
158
+ function extractSummary(text: string | undefined): string | undefined {
159
+ if (!text) return undefined;
160
+ if (text.includes("│")) return undefined;
161
+ const warningsIdx = text.indexOf("\n\nWarnings:");
162
+ const summary = warningsIdx >= 0 ? text.slice(0, warningsIdx) : text;
163
+ return summary.length > 0 ? summary : undefined;
164
+ }
165
+
148
166
  export function buildAppliedText(
149
167
  text: string | undefined,
150
168
  details: ReplaceDetails | undefined,
151
169
  theme: FgT,
170
+ expanded: boolean,
152
171
  ): string | undefined {
153
172
  const sections: string[] = [];
154
-
173
+ const summary = extractSummary(text);
174
+ if (summary) sections.push(summary);
155
175
  if (details?.diff) {
156
- sections.push(fmtResult(details.diff, theme));
176
+ const diffLines = details.diff.split("\n");
177
+ const diffSection = expanded
178
+ ? fmtResult(details.diff, theme)
179
+ : fmtPreview(details.diff, false, theme);
180
+ const hint =
181
+ !expanded && diffLines.length > RESULT_PREVIEW_LINES
182
+ ? ` (${expandHint()})`
183
+ : "";
184
+ sections.push(`${diffSection}${hint}`);
157
185
  }
158
-
159
186
  const warnings = extractWarnings(text);
160
187
  if (warnings) sections.push(warnings);
161
-
162
188
  return sections.length > 0 ? sections.join("\n\n") : undefined;
163
189
  }
164
190
 
@@ -317,7 +343,7 @@ export function renderEditResult(
317
343
  : new Text("", 0, 0);
318
344
  }
319
345
  if (isApplied(result.details)) {
320
- const appliedText = buildAppliedText(renderedText, result.details, theme);
346
+ const appliedText = buildAppliedText(renderedText, result.details, theme, context.expanded === true);
321
347
  return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
322
348
  }
323
349
  if (!renderedText) return new Text("", 0, 0);
@@ -1,3 +1,4 @@
1
+ import type { NEdit } from "./hashline";
1
2
  import type { ReplaceDetails } from "./replace";
2
3
  import { genDiff, genPatch } from "./replace-diff";
3
4
  import { visLines, clipLine } from "./utils";
@@ -27,14 +28,9 @@ export type RMeta = {
27
28
  removedLines: number;
28
29
  };
29
30
 
30
- type NEditEntry = {
31
- loc: string;
32
- currentContent: string;
33
- };
34
-
35
31
  export interface NoopInput {
36
32
  path: string;
37
- noopEdit: NEditEntry | undefined;
33
+ noopEdit: NEdit | undefined;
38
34
  snapshotId?: string;
39
35
  editMeta: RMeta;
40
36
  warnings: string[] | undefined;
@@ -157,11 +153,13 @@ export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
157
153
  removedLines,
158
154
  });
159
155
 
156
+ const patchResult = genPatch(path, originalNormalized, result);
160
157
  return {
161
158
  content: [{ type: "text", text }],
162
159
  details: {
163
160
  diff: diffResult.diff,
164
- patch: genPatch(path, originalNormalized, result),
161
+ patch: patchResult.patch,
162
+ ...(patchResult.truncated ? { patchTruncated: true as const } : {}),
165
163
  firstChangedLine:
166
164
  editMeta.firstChangedLine ?? diffResult.firstChangedLine,
167
165
  snapshotId,
@@ -126,26 +126,15 @@ export function regUndo(pi: ExtensionAPI): void {
126
126
  if (errCode(error) !== "ENOENT") throw error;
127
127
  }
128
128
 
129
- if (currentRaw === undefined) {
130
- await clearUndo(mutationTargetPath);
131
- return {
132
- content: [
133
- {
134
- type: "text",
135
- text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file no longer exists.`
136
- },
137
- ],
138
- isError: true,
139
- details: {},
140
- };
141
- }
142
- if (currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)) {
143
- await clearUndo(mutationTargetPath);
129
+ if (
130
+ currentRaw !== undefined &&
131
+ currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)
132
+ ) {
144
133
  return {
145
134
  content: [
146
135
  {
147
136
  type: "text",
148
- text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. Call read() to inspect the current state.`
137
+ text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. The undo record is kept; once the file matches the edited state again, undo_last_change will succeed. Call read() to inspect the current state.`
149
138
  },
150
139
  ],
151
140
  isError: true,
@@ -153,20 +142,19 @@ export function regUndo(pi: ExtensionAPI): void {
153
142
  };
154
143
  }
155
144
 
156
- const { text: currentStripped } = stripBOM(currentRaw);
157
- const currentNormalized = toLF(currentStripped);
145
+ await writeAtomic(
146
+ mutationTargetPath,
147
+ undo.bom + restoreEndings(undo.content, undo.originalEnding),
148
+ );
149
+
150
+ const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
158
151
  const currentHashes = await lineHashes(currentNormalized, mutationTargetPath);
159
- const diffResult = genDiff(undo.content, currentNormalized, 0, undefined, undo.hashes);
152
+ const diffResult = genDiff(undo.content, undo.resultContent, 0, undefined, undo.hashes, { unlimited: true });
160
153
  const linesAddedByReplace = cntDiff(diffResult.diff, "+");
161
154
  const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
162
155
  const restoredRange = changedRange(currentNormalized, undo.content);
163
156
  const undoDiff = genDiff(currentNormalized, undo.content, 1, undo.hashes, currentHashes).diff;
164
157
 
165
- await writeAtomic(
166
- mutationTargetPath,
167
- undo.bom + restoreEndings(undo.content, undo.originalEnding),
168
- );
169
-
170
158
  try {
171
159
  const store = await loadHashStore();
172
160
  upsertSnapshot(store, mutationTargetPath, contentChecksum(undo.content), splitLines(undo.content).length, undo.hashes);
@@ -180,6 +168,9 @@ export function regUndo(pi: ExtensionAPI): void {
180
168
  const parts: string[] = [
181
169
  `Undone last change on ${path}.`,
182
170
  ];
171
+ if (currentRaw === undefined) {
172
+ parts.push("The file was deleted; restored it from undo history.");
173
+ }
183
174
  if (linesAddedByReplace > 0 || linesRemovedByReplace > 0) {
184
175
  parts.push(
185
176
  `Removed ${linesAddedByReplace} line(s), restored ${linesRemovedByReplace} line(s).`,
@@ -189,6 +180,7 @@ export function regUndo(pi: ExtensionAPI): void {
189
180
  "Call read for fresh anchors.",
190
181
  );
191
182
 
183
+ const patchResult = genPatch(path, currentNormalized, undo.content);
192
184
  return {
193
185
  content: [
194
186
  {
@@ -198,7 +190,8 @@ export function regUndo(pi: ExtensionAPI): void {
198
190
  ],
199
191
  details: {
200
192
  diff: undoDiff,
201
- patch: genPatch(path, currentNormalized, undo.content),
193
+ patch: patchResult.patch,
194
+ ...(patchResult.truncated ? { patchTruncated: true as const } : {}),
202
195
  metrics: buildMetrics({
203
196
  classification: "applied",
204
197
  editsAttempted: 1,
package/src/replace.ts CHANGED
@@ -31,9 +31,8 @@ import {
31
31
  type RPreview,
32
32
  type RRState,
33
33
  } from "./replace-render";
34
- export { reuseText, reuseMarkdown } from "./replace-render";
35
34
  import { loadP, loadGuide } from "./prompts";
36
- import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
35
+ import { loadHashStore, findSnapshotPaths, findServedPaths, type HashStore } from "./hash-store";
37
36
  import { getServed, recordServedSafe } from "./served";
38
37
  import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
39
38
  import { commitEdit } from "./commit";
@@ -50,11 +49,11 @@ const replacementLinesSchema = Type.Array(
50
49
  );
51
50
 
52
51
  const removeFromSchema = Type.String({
53
- description: "Bare 3-char HASH only (e.g. \"aB3\"): copy just the hash from the leftmost column of a read row like `aB3│content`; never the line content. Marks the FIRST line to remove (inclusive)",
52
+ description: "Bare 3-char anchor only (e.g. \"aB3\"): copy just the anchor from the leftmost column of a read row like `aB3│content`; never the line content. Marks the FIRST line to remove (inclusive)",
54
53
  });
55
54
 
56
55
  const removeToSchema = Type.String({
57
- description: "Bare 3-char HASH only (e.g. \"aB3\"): copy just the hash from the leftmost column of a read row like `aB3│content`; never the line content. Marks the LAST line to remove (inclusive)",
56
+ description: "Bare 3-char anchor only (e.g. \"aB3\"): copy just the anchor from the leftmost column of a read row like `aB3│content`; never the line content. Marks the LAST line to remove (inclusive)",
58
57
  });
59
58
 
60
59
  export const editToolSchema = Type.Object(
@@ -76,6 +75,7 @@ export type ReqParams = {
76
75
  export type ReplaceDetails = {
77
76
  diff: string;
78
77
  patch?: string;
78
+ patchTruncated?: boolean;
79
79
  firstChangedLine?: number;
80
80
  snapshotId?: string;
81
81
  classification?: "noop";
@@ -149,7 +149,7 @@ async function resolveMissingPath(
149
149
  } catch {
150
150
  return undefined;
151
151
  }
152
- const matches = findSnapshotPaths(store, hashes);
152
+ const matches = [...new Set([...findSnapshotPaths(store, hashes), ...findServedPaths(store, hashes)])];
153
153
  if (matches.length === 1) {
154
154
  return {
155
155
  path: matches[0]!,
package/src/served.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { loadHashStore, parseHashList, type HashStore } from "./hash-store";
1
+ import { loadHashStore, parseStoredHashes, type HashStore } from "./hash-store";
2
2
  import { HASH_CLASS } from "./hashline/alphabet";
3
3
 
4
4
  const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
@@ -14,8 +14,7 @@ export function servedHashesFromDiff(diff: string): string[] {
14
14
 
15
15
  export function getServed(store: HashStore, path: string): Set<string> | undefined {
16
16
  const row = store.stmts.servedGet(path);
17
- if (!row) return undefined;
18
- const parsed = parseHashList(row.hashes as string, () => store.stmts.servedDelete(path));
17
+ const parsed = parseStoredHashes(row, () => store.stmts.servedDelete(path));
19
18
  if (!parsed) return undefined;
20
19
  return new Set(parsed);
21
20
  }
package/src/utils.ts CHANGED
@@ -93,6 +93,19 @@ export function firstNonEmpty(lines: string[]): string | undefined {
93
93
  return idx >= 0 ? lines[idx] : undefined;
94
94
  }
95
95
 
96
+ export function truncateToBytes(s: string, maxBytes: number): string {
97
+ if (Buffer.byteLength(s, "utf-8") <= maxBytes) return s;
98
+ let out = "";
99
+ let bytes = 0;
100
+ for (const ch of s) {
101
+ const chBytes = Buffer.byteLength(ch, "utf-8");
102
+ if (bytes + chBytes > maxBytes) break;
103
+ out += ch;
104
+ bytes += chBytes;
105
+ }
106
+ return out;
107
+ }
108
+
96
109
  export function clipLine(line: string, maxLen = 200): string {
97
110
  const flat = line.replace(/\n/g, "\\n");
98
111
  return flat.length > maxLen ? `${flat.slice(0, maxLen)}...` : flat;