@oh-my-pi/pi-coding-agent 8.12.4 → 8.12.7
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 +6 -0
- package/package.json +7 -7
- package/src/lsp/index.ts +5 -4
- package/src/modes/components/visual-truncate.ts +15 -1
- package/src/modes/theme/theme.ts +12 -12
- package/src/tools/ask.ts +4 -1
- package/src/tools/read.ts +119 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.12.7] - 2026-01-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixed LSP servers showing as "unknown" in status display when server warmup fails
|
|
9
|
+
- Fixed Read tool loading entire file into memory when offset/limit was specified
|
|
10
|
+
|
|
5
11
|
## [8.12.2] - 2026-01-28
|
|
6
12
|
|
|
7
13
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.7",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,12 +79,12 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oh-my-pi/omp-stats": "8.12.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "8.12.
|
|
84
|
-
"@oh-my-pi/pi-ai": "8.12.
|
|
85
|
-
"@oh-my-pi/pi-natives": "8.12.
|
|
86
|
-
"@oh-my-pi/pi-tui": "8.12.
|
|
87
|
-
"@oh-my-pi/pi-utils": "8.12.
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.12.7",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.12.7",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.12.7",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.12.7",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.12.7",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.12.7",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
package/src/lsp/index.ts
CHANGED
|
@@ -107,7 +107,9 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions):
|
|
|
107
107
|
}),
|
|
108
108
|
);
|
|
109
109
|
|
|
110
|
-
for (
|
|
110
|
+
for (let i = 0; i < results.length; i++) {
|
|
111
|
+
const result = results[i];
|
|
112
|
+
const [name, serverConfig] = lspServers[i];
|
|
111
113
|
if (result.status === "fulfilled") {
|
|
112
114
|
servers.push({
|
|
113
115
|
name: result.value.name,
|
|
@@ -115,12 +117,11 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions):
|
|
|
115
117
|
fileTypes: result.value.fileTypes,
|
|
116
118
|
});
|
|
117
119
|
} else {
|
|
118
|
-
// Extract server name from error if possible
|
|
119
120
|
const errorMsg = result.reason?.message ?? String(result.reason);
|
|
120
121
|
servers.push({
|
|
121
|
-
name
|
|
122
|
+
name,
|
|
122
123
|
status: "error",
|
|
123
|
-
fileTypes:
|
|
124
|
+
fileTypes: serverConfig.fileTypes,
|
|
124
125
|
error: errorMsg,
|
|
125
126
|
});
|
|
126
127
|
}
|
|
@@ -11,6 +11,17 @@ export interface VisualTruncateResult {
|
|
|
11
11
|
skippedCount: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const textCache = new Map<number, Text>();
|
|
15
|
+
|
|
16
|
+
function getCachedText(paddingX: number): Text {
|
|
17
|
+
let text = textCache.get(paddingX);
|
|
18
|
+
if (!text) {
|
|
19
|
+
text = new Text("", paddingX, 0);
|
|
20
|
+
textCache.set(paddingX, text);
|
|
21
|
+
}
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Truncate text to a maximum number of visual lines (from the end).
|
|
16
27
|
* This accounts for line wrapping based on terminal width.
|
|
@@ -34,7 +45,10 @@ export function truncateToVisualLines(
|
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
// Create a temporary Text component to render and get visual lines
|
|
37
|
-
const tempText =
|
|
48
|
+
const tempText = getCachedText(paddingX);
|
|
49
|
+
if (tempText.getText() !== text) {
|
|
50
|
+
tempText.setText(text);
|
|
51
|
+
}
|
|
38
52
|
const allVisualLines = tempText.render(width);
|
|
39
53
|
|
|
40
54
|
if (allVisualLines.length <= maxVisualLines) {
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1180,8 +1180,8 @@ const langMap: Record<string, SymbolKey> = {
|
|
|
1180
1180
|
};
|
|
1181
1181
|
|
|
1182
1182
|
export class Theme {
|
|
1183
|
-
private fgColors:
|
|
1184
|
-
private bgColors:
|
|
1183
|
+
private fgColors: Record<ThemeColor, string>;
|
|
1184
|
+
private bgColors: Record<ThemeBg, string>;
|
|
1185
1185
|
private mode: ColorMode;
|
|
1186
1186
|
private symbols: SymbolMap;
|
|
1187
1187
|
private symbolPreset: SymbolPreset;
|
|
@@ -1190,18 +1190,18 @@ export class Theme {
|
|
|
1190
1190
|
fgColors: Record<ThemeColor, string | number>,
|
|
1191
1191
|
bgColors: Record<ThemeBg, string | number>,
|
|
1192
1192
|
mode: ColorMode,
|
|
1193
|
-
symbolPreset: SymbolPreset
|
|
1194
|
-
symbolOverrides: Record<
|
|
1193
|
+
symbolPreset: SymbolPreset,
|
|
1194
|
+
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1195
1195
|
) {
|
|
1196
1196
|
this.mode = mode;
|
|
1197
1197
|
this.symbolPreset = symbolPreset;
|
|
1198
|
-
this.fgColors =
|
|
1198
|
+
this.fgColors = {} as Record<ThemeColor, string>;
|
|
1199
1199
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
1200
|
-
this.fgColors
|
|
1200
|
+
this.fgColors[key] = fgAnsi(value, mode);
|
|
1201
1201
|
}
|
|
1202
|
-
this.bgColors =
|
|
1202
|
+
this.bgColors = {} as Record<ThemeBg, string>;
|
|
1203
1203
|
for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
|
|
1204
|
-
this.bgColors
|
|
1204
|
+
this.bgColors[key] = bgAnsi(value, mode);
|
|
1205
1205
|
}
|
|
1206
1206
|
// Build symbol map from preset + overrides
|
|
1207
1207
|
const baseSymbols = SYMBOL_PRESETS[symbolPreset];
|
|
@@ -1216,13 +1216,13 @@ export class Theme {
|
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
1218
|
fg(color: ThemeColor, text: string): string {
|
|
1219
|
-
const ansi = this.fgColors
|
|
1219
|
+
const ansi = this.fgColors[color];
|
|
1220
1220
|
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
|
1221
1221
|
return `${ansi}${text}\x1b[39m`; // Reset only foreground color
|
|
1222
1222
|
}
|
|
1223
1223
|
|
|
1224
1224
|
bg(color: ThemeBg, text: string): string {
|
|
1225
|
-
const ansi = this.bgColors
|
|
1225
|
+
const ansi = this.bgColors[color];
|
|
1226
1226
|
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
|
1227
1227
|
return `${ansi}${text}\x1b[49m`; // Reset only background color
|
|
1228
1228
|
}
|
|
@@ -1248,13 +1248,13 @@ export class Theme {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
|
|
1250
1250
|
getFgAnsi(color: ThemeColor): string {
|
|
1251
|
-
const ansi = this.fgColors
|
|
1251
|
+
const ansi = this.fgColors[color];
|
|
1252
1252
|
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
|
1253
1253
|
return ansi;
|
|
1254
1254
|
}
|
|
1255
1255
|
|
|
1256
1256
|
getBgAnsi(color: ThemeBg): string {
|
|
1257
|
-
const ansi = this.bgColors
|
|
1257
|
+
const ansi = this.bgColors[color];
|
|
1258
1258
|
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
|
1259
1259
|
return ansi;
|
|
1260
1260
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -304,7 +304,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
304
304
|
|
|
305
305
|
// Determine timeout based on settings and plan mode
|
|
306
306
|
const planModeEnabled = this.session.getPlanModeState?.()?.enabled ?? false;
|
|
307
|
-
|
|
307
|
+
// getAskTimeout returns: number (ms), null (disabled), or undefined (no settingsManager)
|
|
308
|
+
// Only fall back to default if undefined; preserve null as "disabled"
|
|
309
|
+
const rawTimeout = this.session.settingsManager?.getAskTimeout();
|
|
310
|
+
const settingsTimeout = rawTimeout === undefined ? DEFAULT_ASK_TIMEOUT_MS : rawTimeout;
|
|
308
311
|
const timeout = planModeEnabled ? null : settingsTimeout;
|
|
309
312
|
|
|
310
313
|
// Send notification if waiting and not suppressed
|
package/src/tools/read.ts
CHANGED
|
@@ -43,6 +43,87 @@ function isRemoteMountPath(absolutePath: string): boolean {
|
|
|
43
43
|
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Stream lines from a file, collecting only the requested range.
|
|
48
|
+
* Avoids loading the entire file into memory for large files.
|
|
49
|
+
*
|
|
50
|
+
* @param filePath - Path to the file
|
|
51
|
+
* @param startLine - 0-indexed start line
|
|
52
|
+
* @param maxLinesToCollect - Maximum lines to collect (from startLine)
|
|
53
|
+
* @param maxBytes - Maximum bytes to collect
|
|
54
|
+
* @returns Collected lines, total line count, and truncation info
|
|
55
|
+
*/
|
|
56
|
+
async function streamLinesFromFile(
|
|
57
|
+
filePath: string,
|
|
58
|
+
startLine: number,
|
|
59
|
+
maxLinesToCollect: number,
|
|
60
|
+
maxBytes: number,
|
|
61
|
+
): Promise<{
|
|
62
|
+
lines: string[];
|
|
63
|
+
totalFileLines: number;
|
|
64
|
+
collectedBytes: number;
|
|
65
|
+
stoppedByByteLimit: boolean;
|
|
66
|
+
}> {
|
|
67
|
+
const stream = Bun.file(filePath).stream();
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
|
|
70
|
+
const collectedLines: string[] = [];
|
|
71
|
+
let lineIndex = 0;
|
|
72
|
+
let collectedBytes = 0;
|
|
73
|
+
let stoppedByByteLimit = false;
|
|
74
|
+
let buffer = "";
|
|
75
|
+
let doneCollecting = false;
|
|
76
|
+
|
|
77
|
+
for await (const chunk of stream) {
|
|
78
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
79
|
+
|
|
80
|
+
for (let newlinePos = buffer.indexOf("\n"); newlinePos !== -1; newlinePos = buffer.indexOf("\n")) {
|
|
81
|
+
const line = buffer.slice(0, newlinePos);
|
|
82
|
+
buffer = buffer.slice(newlinePos + 1);
|
|
83
|
+
|
|
84
|
+
if (!doneCollecting && lineIndex >= startLine) {
|
|
85
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (collectedLines.length > 0 ? 1 : 0);
|
|
86
|
+
|
|
87
|
+
if (collectedBytes + lineBytes > maxBytes && collectedLines.length > 0) {
|
|
88
|
+
stoppedByByteLimit = true;
|
|
89
|
+
doneCollecting = true;
|
|
90
|
+
} else if (collectedLines.length < maxLinesToCollect) {
|
|
91
|
+
collectedLines.push(line);
|
|
92
|
+
collectedBytes += lineBytes;
|
|
93
|
+
if (collectedLines.length >= maxLinesToCollect) {
|
|
94
|
+
doneCollecting = true;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
doneCollecting = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lineIndex++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle remaining buffer (last line without trailing newline)
|
|
106
|
+
if (buffer.length > 0) {
|
|
107
|
+
if (!doneCollecting && lineIndex >= startLine && collectedLines.length < maxLinesToCollect) {
|
|
108
|
+
const lineBytes = Buffer.byteLength(buffer, "utf-8") + (collectedLines.length > 0 ? 1 : 0);
|
|
109
|
+
if (collectedBytes + lineBytes <= maxBytes || collectedLines.length === 0) {
|
|
110
|
+
collectedLines.push(buffer);
|
|
111
|
+
collectedBytes += lineBytes;
|
|
112
|
+
} else {
|
|
113
|
+
stoppedByByteLimit = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
lineIndex++;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
lines: collectedLines,
|
|
121
|
+
totalFileLines: lineIndex,
|
|
122
|
+
collectedBytes,
|
|
123
|
+
stoppedByByteLimit,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
46
127
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
47
128
|
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
|
48
129
|
const MAX_FUZZY_RESULTS = 5;
|
|
@@ -514,40 +595,51 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
514
595
|
content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
|
|
515
596
|
}
|
|
516
597
|
} else {
|
|
517
|
-
// Read as text
|
|
518
|
-
const file = Bun.file(absolutePath);
|
|
519
|
-
const textContent = await file.text();
|
|
520
|
-
const allLines = textContent.split("\n");
|
|
521
|
-
const totalFileLines = allLines.length;
|
|
522
|
-
|
|
523
|
-
// Apply offset if specified (1-indexed to 0-indexed)
|
|
598
|
+
// Read as text using streaming to avoid loading huge files into memory
|
|
524
599
|
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
525
600
|
const startLineDisplay = startLine + 1; // For display (1-indexed)
|
|
526
601
|
|
|
602
|
+
// Calculate how many lines to collect: user limit or default truncation limit
|
|
603
|
+
const maxLinesToCollect = limit !== undefined ? limit : DEFAULT_MAX_LINES;
|
|
604
|
+
|
|
605
|
+
// Stream the file, collecting only the needed lines
|
|
606
|
+
const streamResult = await streamLinesFromFile(absolutePath, startLine, maxLinesToCollect, DEFAULT_MAX_BYTES);
|
|
607
|
+
|
|
608
|
+
const { lines: collectedLines, totalFileLines, collectedBytes, stoppedByByteLimit } = streamResult;
|
|
609
|
+
|
|
527
610
|
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
528
|
-
if (startLine >=
|
|
611
|
+
if (startLine >= totalFileLines) {
|
|
529
612
|
const suggestion =
|
|
530
|
-
|
|
613
|
+
totalFileLines === 0
|
|
531
614
|
? "The file is empty."
|
|
532
|
-
: `Use offset=1 to read from the start, or offset=${
|
|
615
|
+
: `Use offset=1 to read from the start, or offset=${totalFileLines} to read the last line.`;
|
|
533
616
|
return toolResult<ReadToolDetails>()
|
|
534
|
-
.text(`Offset ${offset} is beyond end of file (${
|
|
617
|
+
.text(`Offset ${offset} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
|
|
535
618
|
.done();
|
|
536
619
|
}
|
|
537
620
|
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
621
|
+
// Build the selected content from collected lines
|
|
622
|
+
const selectedContent = collectedLines.join("\n");
|
|
623
|
+
const userLimitedLines = limit !== undefined ? collectedLines.length : undefined;
|
|
624
|
+
|
|
625
|
+
// Build truncation result from streaming data
|
|
626
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
627
|
+
const totalSelectedBytes = collectedBytes; // We don't know exact total bytes without reading all
|
|
628
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
629
|
+
|
|
630
|
+
const truncation: TruncationResult = {
|
|
631
|
+
content: selectedContent,
|
|
632
|
+
truncated: wasTruncated,
|
|
633
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : null,
|
|
634
|
+
totalLines: totalSelectedLines,
|
|
635
|
+
totalBytes: totalSelectedBytes,
|
|
636
|
+
outputLines: collectedLines.length,
|
|
637
|
+
outputBytes: collectedBytes,
|
|
638
|
+
lastLinePartial: false,
|
|
639
|
+
firstLineExceedsLimit: collectedLines.length === 0 && totalFileLines > startLine,
|
|
640
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
641
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
642
|
+
};
|
|
551
643
|
|
|
552
644
|
// Add line numbers if requested (uses setting default if not specified)
|
|
553
645
|
const shouldAddLineNumbers = lines ?? this.defaultLineNumbers;
|
|
@@ -566,7 +658,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
566
658
|
let outputText: string;
|
|
567
659
|
|
|
568
660
|
if (truncation.firstLineExceedsLimit) {
|
|
569
|
-
const firstLine =
|
|
661
|
+
const firstLine = collectedLines[0] ?? "";
|
|
570
662
|
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
571
663
|
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
572
664
|
|
|
@@ -592,8 +684,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
592
684
|
result: truncation,
|
|
593
685
|
options: { direction: "head", startLine: startLineDisplay, totalFileLines },
|
|
594
686
|
};
|
|
595
|
-
} else if (userLimitedLines !== undefined && startLine + userLimitedLines <
|
|
596
|
-
const remaining =
|
|
687
|
+
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) {
|
|
688
|
+
const remaining = totalFileLines - (startLine + userLimitedLines);
|
|
597
689
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
598
690
|
|
|
599
691
|
outputText = shouldAddLineNumbers
|