@oh-my-pi/pi-utils 17.2.15 → 17.3.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 +7 -0
- package/dist/types/json-parse.d.ts +15 -6
- package/dist/types/ptree.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +2 -0
- package/package.json +2 -2
- package/src/json-parse.ts +18 -7
- package/src/ptree.ts +2 -2
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +6 -6
- package/src/vendor/mermaid-ascii/ascii/draw.ts +2 -2
- package/src/vendor/mermaid-ascii/text-metrics.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.0] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Optimized performance of partial JSON parsing for long streaming tool-call arguments.
|
|
10
|
+
- Fixed Mermaid ASCII multi-word edge labels where routed lines would show through spaces.
|
|
11
|
+
|
|
5
12
|
## [17.2.15] - 2026-08-12
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
@@ -21,8 +21,8 @@ export declare function parseJsonWithRepair<T>(json: string): T;
|
|
|
21
21
|
export declare function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T;
|
|
22
22
|
/**
|
|
23
23
|
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
24
|
-
* re-parse a streaming tool-call argument buffer.
|
|
25
|
-
*
|
|
24
|
+
* re-parse a streaming tool-call argument buffer. Acts as the floor of the
|
|
25
|
+
* geometric gate — see {@link parseStreamingJsonThrottled}.
|
|
26
26
|
*/
|
|
27
27
|
export declare const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
28
28
|
/**
|
|
@@ -30,14 +30,23 @@ export declare const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
|
30
30
|
*
|
|
31
31
|
* Tool calls arrive as a long sequence of small deltas — calling
|
|
32
32
|
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
33
|
-
* each time, giving O(N²) work in the total buffer length.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* each time, giving O(N²) work in the total buffer length. A fixed re-parse
|
|
34
|
+
* floor alone does NOT fix this: with `minGrowthBytes` constant, a buffer of
|
|
35
|
+
* length N is parsed N/minGrowthBytes times at an average cost of N/2, which
|
|
36
|
+
* is still O(N²) (the constant just shrinks). Long `write` payloads — where
|
|
37
|
+
* the buffer is the whole file — made this the dominant main-thread stall
|
|
38
|
+
* during streaming.
|
|
39
|
+
*
|
|
40
|
+
* Instead the gate scales geometrically: once the buffer is large, a re-parse
|
|
41
|
+
* requires growth proportional to the current length (`len / 32`, floored at
|
|
42
|
+
* `minGrowthBytes`). Parse points then form a geometric progression, so a
|
|
43
|
+
* buffer of length N is parsed O(log N) times for O(N log N) total work,
|
|
44
|
+
* while small buffers keep the snappy fixed-cadence updates.
|
|
36
45
|
*
|
|
37
46
|
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
38
47
|
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
39
48
|
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
40
|
-
* updates by at most
|
|
49
|
+
* updates, by at most ~3% of the accumulated content for large buffers.
|
|
41
50
|
*
|
|
42
51
|
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
43
52
|
* when the buffer has not grown enough to warrant a re-parse.
|
package/dist/types/ptree.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare class ChildProcess<In extends InMask = InMask> {
|
|
|
78
78
|
/** Returns the truncated stderr tail (last 32KB). */
|
|
79
79
|
peekStderr(): string;
|
|
80
80
|
nothrow(): this;
|
|
81
|
-
kill(reason?: Exception): void;
|
|
81
|
+
kill(reason?: Exception, gracefulMs?: number): void;
|
|
82
82
|
text(): Promise<string>;
|
|
83
83
|
blob(): Promise<Blob>;
|
|
84
84
|
json(): Promise<unknown>;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* right of its lead cell; canvas writes keep the pair atomic.
|
|
7
7
|
*/
|
|
8
8
|
export declare const WIDE_PAD = "\0";
|
|
9
|
+
/** Opaque label-space placeholder that serializes back to a regular space. */
|
|
10
|
+
export declare const LABEL_SPACE = "\u0001";
|
|
9
11
|
/**
|
|
10
12
|
* Display width of a string in terminal columns, summed over grapheme
|
|
11
13
|
* clusters so it always equals `toCells(text).length`. ASCII-only strings
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.
|
|
4
|
+
"version": "17.3.1",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.3.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/json-parse.ts
CHANGED
|
@@ -579,8 +579,8 @@ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: str
|
|
|
579
579
|
|
|
580
580
|
/**
|
|
581
581
|
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
582
|
-
* re-parse a streaming tool-call argument buffer.
|
|
583
|
-
*
|
|
582
|
+
* re-parse a streaming tool-call argument buffer. Acts as the floor of the
|
|
583
|
+
* geometric gate — see {@link parseStreamingJsonThrottled}.
|
|
584
584
|
*/
|
|
585
585
|
export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
586
586
|
|
|
@@ -589,14 +589,23 @@ export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
|
589
589
|
*
|
|
590
590
|
* Tool calls arrive as a long sequence of small deltas — calling
|
|
591
591
|
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
592
|
-
* each time, giving O(N²) work in the total buffer length.
|
|
593
|
-
*
|
|
594
|
-
*
|
|
592
|
+
* each time, giving O(N²) work in the total buffer length. A fixed re-parse
|
|
593
|
+
* floor alone does NOT fix this: with `minGrowthBytes` constant, a buffer of
|
|
594
|
+
* length N is parsed N/minGrowthBytes times at an average cost of N/2, which
|
|
595
|
+
* is still O(N²) (the constant just shrinks). Long `write` payloads — where
|
|
596
|
+
* the buffer is the whole file — made this the dominant main-thread stall
|
|
597
|
+
* during streaming.
|
|
598
|
+
*
|
|
599
|
+
* Instead the gate scales geometrically: once the buffer is large, a re-parse
|
|
600
|
+
* requires growth proportional to the current length (`len / 32`, floored at
|
|
601
|
+
* `minGrowthBytes`). Parse points then form a geometric progression, so a
|
|
602
|
+
* buffer of length N is parsed O(log N) times for O(N log N) total work,
|
|
603
|
+
* while small buffers keep the snappy fixed-cadence updates.
|
|
595
604
|
*
|
|
596
605
|
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
597
606
|
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
598
607
|
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
599
|
-
* updates by at most
|
|
608
|
+
* updates, by at most ~3% of the accumulated content for large buffers.
|
|
600
609
|
*
|
|
601
610
|
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
602
611
|
* when the buffer has not grown enough to warrant a re-parse.
|
|
@@ -607,7 +616,9 @@ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
|
|
|
607
616
|
minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
|
|
608
617
|
): { value: T; parsedLen: number } | null {
|
|
609
618
|
const len = partialJson?.length ?? 0;
|
|
610
|
-
if (len === 0
|
|
619
|
+
if (len === 0) return null;
|
|
620
|
+
const growth = Math.max(minGrowthBytes, len >> 5);
|
|
621
|
+
if (lastParsedLen > 0 && len - lastParsedLen < growth) return null;
|
|
611
622
|
return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
|
|
612
623
|
}
|
|
613
624
|
|
package/src/ptree.ts
CHANGED
|
@@ -217,11 +217,11 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
217
217
|
return this;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
kill(reason?: Exception) {
|
|
220
|
+
kill(reason?: Exception, gracefulMs?: number) {
|
|
221
221
|
if (reason && !this.#exitReasonPending) this.#exitReasonPending = reason;
|
|
222
222
|
if (!this.proc.killed)
|
|
223
223
|
void Process.fromPid(this.proc.pid)
|
|
224
|
-
?.terminate()
|
|
224
|
+
?.terminate(gracefulMs === undefined ? undefined : { gracefulMs })
|
|
225
225
|
?.catch(e => void e);
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types'
|
|
10
10
|
import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi'
|
|
11
|
-
import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
|
|
11
|
+
import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Create a blank canvas filled with spaces.
|
|
@@ -189,7 +189,7 @@ export function isJunctionChar(c: string): boolean {
|
|
|
189
189
|
* letter/digit test misses.
|
|
190
190
|
*/
|
|
191
191
|
function isLabelChar(c: string): boolean {
|
|
192
|
-
return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
|
|
192
|
+
return c === LABEL_SPACE || c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -268,7 +268,7 @@ export function mergeCanvases(
|
|
|
268
268
|
for (let x = 0; x < overlay.length; x++) {
|
|
269
269
|
for (let y = 0; y < overlay[0]!.length; y++) {
|
|
270
270
|
const c = overlay[x]![y]!
|
|
271
|
-
// WIDE_PAD cells are written atomically with their lead below
|
|
271
|
+
// Spaces are transparent; WIDE_PAD cells are written atomically with their lead below
|
|
272
272
|
if (c === ' ' || c === WIDE_PAD) continue
|
|
273
273
|
const mx = x + offset.x
|
|
274
274
|
const my = y + offset.y
|
|
@@ -327,8 +327,8 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
|
|
|
327
327
|
let line = ''
|
|
328
328
|
for (let x = 0; x <= maxX; x++) {
|
|
329
329
|
const c = canvas[x]![y]!
|
|
330
|
-
// Skip wide-glyph continuation cells
|
|
331
|
-
if (c !== WIDE_PAD) line += c
|
|
330
|
+
// Skip wide-glyph continuation cells and restore opaque label spaces.
|
|
331
|
+
if (c !== WIDE_PAD) line += c === LABEL_SPACE ? ' ' : c
|
|
332
332
|
}
|
|
333
333
|
lines.push(line)
|
|
334
334
|
} else {
|
|
@@ -338,7 +338,7 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
|
|
|
338
338
|
for (let x = 0; x <= maxX; x++) {
|
|
339
339
|
const c = canvas[x]![y]!
|
|
340
340
|
if (c === WIDE_PAD) continue
|
|
341
|
-
chars.push(c)
|
|
341
|
+
chars.push(c === LABEL_SPACE ? ' ' : c)
|
|
342
342
|
roles.push(roleCanvas[x]?.[y] ?? null)
|
|
343
343
|
}
|
|
344
344
|
lines.push(colorizeLine(chars, roles, theme, colorMode))
|
|
@@ -21,7 +21,7 @@ import { gridToDrawingCoord, lineToDrawing } from './grid'
|
|
|
21
21
|
import { splitLines } from './multiline-utils'
|
|
22
22
|
import { getCorners } from './shapes/corners'
|
|
23
23
|
import { getShapeAttachmentPoint } from './shapes/index'
|
|
24
|
-
import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
|
|
24
|
+
import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
|
|
25
25
|
|
|
26
26
|
// ============================================================================
|
|
27
27
|
// Node drawing — renders a node using shape-aware rendering
|
|
@@ -679,7 +679,7 @@ function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isU
|
|
|
679
679
|
for (let i = 0; i < lines.length; i++) {
|
|
680
680
|
const lineText = lines[i]!
|
|
681
681
|
const startX = middleX - Math.floor(displayWidth(lineText) / 2)
|
|
682
|
-
drawText(canvas, { x: startX, y: startY + i }, lineText)
|
|
682
|
+
drawText(canvas, { x: startX, y: startY + i }, lineText.replaceAll(' ', LABEL_SPACE))
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
|