@oh-my-pi/pi-coding-agent 13.14.0 → 13.14.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/package.json +7 -7
- package/src/exec/bash-executor.ts +7 -5
- package/src/modes/components/bash-execution.ts +40 -11
- package/src/modes/components/python-execution.ts +2 -3
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/command-controller.ts +0 -2
- package/src/session/streaming-output.ts +87 -37
- package/src/tools/bash-interactive.ts +2 -6
- package/src/tools/python.ts +2 -2
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": "13.14.
|
|
4
|
+
"version": "13.14.2",
|
|
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",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.14.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.14.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.14.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.14.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.14.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.14.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.14.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.14.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.14.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.14.2",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.14.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.14.2",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -69,11 +69,16 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
69
69
|
onChunk: options?.onChunk,
|
|
70
70
|
artifactPath: options?.artifactPath,
|
|
71
71
|
artifactId: options?.artifactId,
|
|
72
|
+
// Throttle the streaming preview callback to avoid saturating the
|
|
73
|
+
// event loop when commands produce massive output (e.g. seq 1 50M).
|
|
74
|
+
chunkThrottleMs: options?.onChunk ? 50 : 0,
|
|
72
75
|
});
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
// sink.push() is synchronous — buffer management, counters, and onChunk
|
|
78
|
+
// all run inline. File writes (artifact path) are handled asynchronously
|
|
79
|
+
// inside the sink. No promise chain needed.
|
|
75
80
|
const enqueueChunk = (chunk: string) => {
|
|
76
|
-
|
|
81
|
+
sink.push(chunk);
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
if (options?.signal?.aborted) {
|
|
@@ -160,8 +165,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
160
165
|
hardTimeoutDeferred.promise.then(() => ({ kind: "hard-timeout" as const })),
|
|
161
166
|
]);
|
|
162
167
|
|
|
163
|
-
await pendingChunks;
|
|
164
|
-
|
|
165
168
|
if (winner.kind === "hard-timeout") {
|
|
166
169
|
if (shellSession) {
|
|
167
170
|
resetSession = true;
|
|
@@ -215,7 +218,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
215
218
|
if (userSignal) {
|
|
216
219
|
userSignal.removeEventListener("abort", abortHandler);
|
|
217
220
|
}
|
|
218
|
-
await pendingChunks;
|
|
219
221
|
if (resetSession) {
|
|
220
222
|
shellSessions.delete(sessionKey);
|
|
221
223
|
}
|
|
@@ -6,13 +6,17 @@ import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
|
6
6
|
import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
8
8
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
9
|
-
import { getSixelLineMask, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
9
|
+
import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
10
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
11
11
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
12
12
|
|
|
13
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
14
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const STREAMING_LINE_CAP = PREVIEW_LINES * 5;
|
|
15
16
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
17
|
+
// Minimum interval between processing incoming chunks for display (ms).
|
|
18
|
+
// Chunks arriving faster than this are accumulated and processed in one batch.
|
|
19
|
+
const CHUNK_THROTTLE_MS = 50;
|
|
16
20
|
|
|
17
21
|
export class BashExecutionComponent extends Container {
|
|
18
22
|
#outputLines: string[] = [];
|
|
@@ -21,7 +25,10 @@ export class BashExecutionComponent extends Container {
|
|
|
21
25
|
#loader: Loader;
|
|
22
26
|
#truncation?: TruncationMeta;
|
|
23
27
|
#expanded = false;
|
|
28
|
+
#displayDirty = false;
|
|
29
|
+
#chunkGate = false;
|
|
24
30
|
#contentContainer: Container;
|
|
31
|
+
#headerText: Text;
|
|
25
32
|
|
|
26
33
|
constructor(
|
|
27
34
|
private readonly command: string,
|
|
@@ -45,8 +52,8 @@ export class BashExecutionComponent extends Container {
|
|
|
45
52
|
this.addChild(this.#contentContainer);
|
|
46
53
|
|
|
47
54
|
// Command header
|
|
48
|
-
|
|
49
|
-
this.#contentContainer.addChild(
|
|
55
|
+
this.#headerText = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);
|
|
56
|
+
this.#contentContainer.addChild(this.#headerText);
|
|
50
57
|
|
|
51
58
|
// Loader
|
|
52
59
|
this.#loader = new Loader(
|
|
@@ -72,14 +79,22 @@ export class BashExecutionComponent extends Container {
|
|
|
72
79
|
|
|
73
80
|
override invalidate(): void {
|
|
74
81
|
super.invalidate();
|
|
82
|
+
this.#displayDirty = false;
|
|
75
83
|
this.#updateDisplay();
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
appendOutput(chunk: string): void {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
87
|
+
// During high-throughput output (e.g. seq 1 500M), processing every
|
|
88
|
+
// chunk would saturate the event loop. Instead, accept one chunk per
|
|
89
|
+
// throttle window and drop the rest — the OutputSink captures everything
|
|
90
|
+
// for the artifact, and setComplete() replaces with the final output.
|
|
91
|
+
if (this.#chunkGate) return;
|
|
92
|
+
this.#chunkGate = true;
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
this.#chunkGate = false;
|
|
95
|
+
}, CHUNK_THROTTLE_MS);
|
|
96
|
+
|
|
97
|
+
const incomingLines = chunk.split("\n");
|
|
83
98
|
if (this.#outputLines.length > 0 && incomingLines.length > 0) {
|
|
84
99
|
const lastIndex = this.#outputLines.length - 1;
|
|
85
100
|
const mergedLines = [`${this.#outputLines[lastIndex]}${incomingLines[0]}`, ...incomingLines.slice(1)];
|
|
@@ -90,7 +105,12 @@ export class BashExecutionComponent extends Container {
|
|
|
90
105
|
this.#outputLines.push(...this.#clampLinesPreservingSixel(incomingLines));
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
// Cap stored lines during streaming to avoid unbounded memory growth
|
|
109
|
+
if (this.#outputLines.length > STREAMING_LINE_CAP) {
|
|
110
|
+
this.#outputLines = this.#outputLines.slice(-STREAMING_LINE_CAP);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.#displayDirty = true;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
setComplete(
|
|
@@ -115,6 +135,14 @@ export class BashExecutionComponent extends Container {
|
|
|
115
135
|
this.#updateDisplay();
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
override render(width: number): string[] {
|
|
139
|
+
if (this.#displayDirty) {
|
|
140
|
+
this.#displayDirty = false;
|
|
141
|
+
this.#updateDisplay();
|
|
142
|
+
}
|
|
143
|
+
return super.render(width);
|
|
144
|
+
}
|
|
145
|
+
|
|
118
146
|
#updateDisplay(): void {
|
|
119
147
|
const availableLines = this.#outputLines;
|
|
120
148
|
|
|
@@ -122,15 +150,16 @@ export class BashExecutionComponent extends Container {
|
|
|
122
150
|
const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
|
|
123
151
|
const hiddenLineCount = availableLines.length - previewLogicalLines.length;
|
|
124
152
|
const sixelLineMask =
|
|
125
|
-
TERMINAL.imageProtocol === ImageProtocol.Sixel
|
|
153
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled()
|
|
154
|
+
? getSixelLineMask(availableLines)
|
|
155
|
+
: undefined;
|
|
126
156
|
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
127
157
|
|
|
128
158
|
// Rebuild content container
|
|
129
159
|
this.#contentContainer.clear();
|
|
130
160
|
|
|
131
161
|
// Command header
|
|
132
|
-
|
|
133
|
-
this.#contentContainer.addChild(header);
|
|
162
|
+
this.#contentContainer.addChild(this.#headerText);
|
|
134
163
|
|
|
135
164
|
// Output
|
|
136
165
|
if (availableLines.length > 0) {
|
|
@@ -72,9 +72,8 @@ export class PythonExecutionComponent extends Container {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
appendOutput(chunk: string): void {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const newLines = clean.split("\n").map(line => this.#clampDisplayLine(line));
|
|
75
|
+
// Chunk is pre-sanitized by OutputSink.push() — no need to sanitize again.
|
|
76
|
+
const newLines = chunk.split("\n").map(line => this.#clampDisplayLine(line));
|
|
78
77
|
if (this.#outputLines.length > 0 && newLines.length > 0) {
|
|
79
78
|
this.#outputLines[this.#outputLines.length - 1] = this.#clampDisplayLine(
|
|
80
79
|
`${this.#outputLines[this.#outputLines.length - 1]}${newLines[0]}`,
|
|
@@ -105,17 +105,16 @@ export class ToolExecutionComponent extends Container {
|
|
|
105
105
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
106
106
|
#convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
107
107
|
// Spinner animation for partial task results
|
|
108
|
-
#spinnerFrame
|
|
108
|
+
#spinnerFrame?: number;
|
|
109
109
|
#spinnerInterval?: NodeJS.Timeout;
|
|
110
110
|
// Track if args are still being streamed (for edit/write spinner)
|
|
111
111
|
#argsComplete = false;
|
|
112
112
|
#renderState: {
|
|
113
|
-
spinnerFrame
|
|
113
|
+
spinnerFrame?: number;
|
|
114
114
|
expanded: boolean;
|
|
115
115
|
isPartial: boolean;
|
|
116
116
|
renderContext?: Record<string, unknown>;
|
|
117
117
|
} = {
|
|
118
|
-
spinnerFrame: 0,
|
|
119
118
|
expanded: false,
|
|
120
119
|
isPartial: true,
|
|
121
120
|
};
|
|
@@ -328,10 +327,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
328
327
|
this.#spinnerInterval = setInterval(() => {
|
|
329
328
|
const frameCount = theme.spinnerFrames.length;
|
|
330
329
|
if (frameCount === 0) return;
|
|
331
|
-
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
330
|
+
this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
|
|
332
331
|
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
|
333
332
|
this.#ui.requestRender();
|
|
334
|
-
// NO updateDisplay() — existing component closures read from renderState
|
|
335
333
|
}, 80);
|
|
336
334
|
} else if (!needsSpinner && this.#spinnerInterval) {
|
|
337
335
|
clearInterval(this.#spinnerInterval);
|
|
@@ -346,6 +344,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
346
344
|
if (this.#spinnerInterval) {
|
|
347
345
|
clearInterval(this.#spinnerInterval);
|
|
348
346
|
this.#spinnerInterval = undefined;
|
|
347
|
+
this.#spinnerFrame = undefined;
|
|
349
348
|
}
|
|
350
349
|
}
|
|
351
350
|
|
|
@@ -690,7 +690,6 @@ export class CommandController {
|
|
|
690
690
|
chunk => {
|
|
691
691
|
if (this.ctx.bashComponent) {
|
|
692
692
|
this.ctx.bashComponent.appendOutput(chunk);
|
|
693
|
-
this.ctx.ui.requestRender();
|
|
694
693
|
}
|
|
695
694
|
},
|
|
696
695
|
{ excludeFromContext },
|
|
@@ -732,7 +731,6 @@ export class CommandController {
|
|
|
732
731
|
chunk => {
|
|
733
732
|
if (this.ctx.pythonComponent) {
|
|
734
733
|
this.ctx.pythonComponent.appendOutput(chunk);
|
|
735
|
-
this.ctx.ui.requestRender();
|
|
736
734
|
}
|
|
737
735
|
},
|
|
738
736
|
{ excludeFromContext },
|
|
@@ -32,6 +32,8 @@ export interface OutputSinkOptions {
|
|
|
32
32
|
artifactId?: string;
|
|
33
33
|
spillThreshold?: number;
|
|
34
34
|
onChunk?: (chunk: string) => void;
|
|
35
|
+
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
36
|
+
chunkThrottleMs?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface TruncationResult {
|
|
@@ -521,6 +523,7 @@ export class OutputSink {
|
|
|
521
523
|
#totalBytes = 0;
|
|
522
524
|
#sawData = false;
|
|
523
525
|
#truncated = false;
|
|
526
|
+
#lastChunkTime = 0;
|
|
524
527
|
|
|
525
528
|
#file?: {
|
|
526
529
|
path: string;
|
|
@@ -528,22 +531,46 @@ export class OutputSink {
|
|
|
528
531
|
sink: Bun.FileSink;
|
|
529
532
|
};
|
|
530
533
|
|
|
534
|
+
// Queue of chunks waiting for the file sink to be created.
|
|
535
|
+
#pendingFileWrites?: string[];
|
|
536
|
+
#fileReady = false;
|
|
537
|
+
|
|
531
538
|
readonly #artifactPath?: string;
|
|
532
539
|
readonly #artifactId?: string;
|
|
533
540
|
readonly #spillThreshold: number;
|
|
534
541
|
readonly #onChunk?: (chunk: string) => void;
|
|
542
|
+
readonly #chunkThrottleMs: number;
|
|
535
543
|
|
|
536
544
|
constructor(options?: OutputSinkOptions) {
|
|
537
|
-
const {
|
|
545
|
+
const {
|
|
546
|
+
artifactPath,
|
|
547
|
+
artifactId,
|
|
548
|
+
spillThreshold = DEFAULT_MAX_BYTES,
|
|
549
|
+
onChunk,
|
|
550
|
+
chunkThrottleMs = 0,
|
|
551
|
+
} = options ?? {};
|
|
538
552
|
this.#artifactPath = artifactPath;
|
|
539
553
|
this.#artifactId = artifactId;
|
|
540
554
|
this.#spillThreshold = spillThreshold;
|
|
541
555
|
this.#onChunk = onChunk;
|
|
556
|
+
this.#chunkThrottleMs = chunkThrottleMs;
|
|
542
557
|
}
|
|
543
558
|
|
|
544
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Push a chunk of output. The buffer management and onChunk callback run
|
|
561
|
+
* synchronously. File sink writes are deferred and serialized internally.
|
|
562
|
+
*/
|
|
563
|
+
push(chunk: string): void {
|
|
545
564
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
546
|
-
|
|
565
|
+
|
|
566
|
+
// Throttled onChunk: only call the callback when enough time has passed.
|
|
567
|
+
if (this.#onChunk) {
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
570
|
+
this.#lastChunkTime = now;
|
|
571
|
+
this.#onChunk(chunk);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
547
574
|
|
|
548
575
|
const dataBytes = Buffer.byteLength(chunk, "utf-8");
|
|
549
576
|
this.#totalBytes += dataBytes;
|
|
@@ -556,10 +583,9 @@ export class OutputSink {
|
|
|
556
583
|
const threshold = this.#spillThreshold;
|
|
557
584
|
const willOverflow = this.#bufferBytes + dataBytes > threshold;
|
|
558
585
|
|
|
559
|
-
// Write to file if
|
|
560
|
-
if (this.#file != null || willOverflow) {
|
|
561
|
-
|
|
562
|
-
await sink?.write(chunk);
|
|
586
|
+
// Write to artifact file if configured and past the threshold
|
|
587
|
+
if (this.#artifactPath && (this.#file != null || willOverflow)) {
|
|
588
|
+
this.#writeToFile(chunk);
|
|
563
589
|
}
|
|
564
590
|
|
|
565
591
|
if (!willOverflow) {
|
|
@@ -589,14 +615,64 @@ export class OutputSink {
|
|
|
589
615
|
if (this.#file) this.#truncated = true;
|
|
590
616
|
}
|
|
591
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Write a chunk to the artifact file. Handles the async file sink creation
|
|
620
|
+
* by queuing writes until the sink is ready, then draining synchronously.
|
|
621
|
+
*/
|
|
622
|
+
#writeToFile(chunk: string): void {
|
|
623
|
+
if (this.#fileReady && this.#file) {
|
|
624
|
+
// Fast path: file sink exists, write synchronously
|
|
625
|
+
this.#file.sink.write(chunk);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// File sink not yet created — queue this chunk and kick off creation
|
|
629
|
+
if (!this.#pendingFileWrites) {
|
|
630
|
+
this.#pendingFileWrites = [chunk];
|
|
631
|
+
void this.#createFileSink();
|
|
632
|
+
} else {
|
|
633
|
+
this.#pendingFileWrites.push(chunk);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async #createFileSink(): Promise<void> {
|
|
638
|
+
if (!this.#artifactPath || this.#fileReady) return;
|
|
639
|
+
try {
|
|
640
|
+
const sink = Bun.file(this.#artifactPath).writer();
|
|
641
|
+
this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
|
|
642
|
+
|
|
643
|
+
// Flush existing buffer to file BEFORE it gets trimmed further.
|
|
644
|
+
if (this.#buffer.length > 0) {
|
|
645
|
+
sink.write(this.#buffer);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Drain any chunks that arrived while the sink was being created
|
|
649
|
+
if (this.#pendingFileWrites) {
|
|
650
|
+
for (const pending of this.#pendingFileWrites) {
|
|
651
|
+
sink.write(pending);
|
|
652
|
+
}
|
|
653
|
+
this.#pendingFileWrites = undefined;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
this.#fileReady = true;
|
|
657
|
+
} catch {
|
|
658
|
+
try {
|
|
659
|
+
await this.#file?.sink?.end();
|
|
660
|
+
} catch {
|
|
661
|
+
/* ignore */
|
|
662
|
+
}
|
|
663
|
+
this.#file = undefined;
|
|
664
|
+
this.#pendingFileWrites = undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
592
668
|
createInput(): WritableStream<Uint8Array | string> {
|
|
593
669
|
const dec = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
594
|
-
const finalize =
|
|
595
|
-
|
|
670
|
+
const finalize = () => {
|
|
671
|
+
this.push(dec.decode());
|
|
596
672
|
};
|
|
597
673
|
return new WritableStream({
|
|
598
|
-
write:
|
|
599
|
-
|
|
674
|
+
write: chunk => {
|
|
675
|
+
this.push(typeof chunk === "string" ? chunk : dec.decode(chunk, { stream: true }));
|
|
600
676
|
},
|
|
601
677
|
close: finalize,
|
|
602
678
|
abort: finalize,
|
|
@@ -620,32 +696,6 @@ export class OutputSink {
|
|
|
620
696
|
artifactId: this.#file?.artifactId,
|
|
621
697
|
};
|
|
622
698
|
}
|
|
623
|
-
|
|
624
|
-
// -- private ---------------------------------------------------------------
|
|
625
|
-
|
|
626
|
-
async #ensureFileSink(): Promise<Bun.FileSink | null> {
|
|
627
|
-
if (!this.#artifactPath) return null;
|
|
628
|
-
if (this.#file) return this.#file.sink;
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
const sink = Bun.file(this.#artifactPath).writer();
|
|
632
|
-
this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
|
|
633
|
-
|
|
634
|
-
// Flush existing buffer to file BEFORE it gets trimmed further.
|
|
635
|
-
if (this.#buffer.length > 0) {
|
|
636
|
-
await sink.write(this.#buffer);
|
|
637
|
-
}
|
|
638
|
-
return sink;
|
|
639
|
-
} catch {
|
|
640
|
-
try {
|
|
641
|
-
await this.#file?.sink?.end();
|
|
642
|
-
} catch {
|
|
643
|
-
/* ignore */
|
|
644
|
-
}
|
|
645
|
-
this.#file = undefined;
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
699
|
}
|
|
650
700
|
|
|
651
701
|
// =============================================================================
|
|
@@ -295,7 +295,6 @@ export async function runInteractiveBashPty(
|
|
|
295
295
|
},
|
|
296
296
|
): Promise<BashInteractiveResult> {
|
|
297
297
|
const sink = new OutputSink({ artifactPath: options.artifactPath, artifactId: options.artifactId });
|
|
298
|
-
let pendingChunks = Promise.resolve();
|
|
299
298
|
const result = await ui.custom<BashInteractiveResult>(
|
|
300
299
|
(tui, uiTheme, _keybindings, done) => {
|
|
301
300
|
const session = new PtySession();
|
|
@@ -309,7 +308,6 @@ export async function runInteractiveBashPty(
|
|
|
309
308
|
tui.requestRender();
|
|
310
309
|
void (async () => {
|
|
311
310
|
await component.flushOutput();
|
|
312
|
-
await pendingChunks;
|
|
313
311
|
const summary = await sink.dump();
|
|
314
312
|
done({
|
|
315
313
|
exitCode: run.exitCode,
|
|
@@ -362,15 +360,13 @@ export async function runInteractiveBashPty(
|
|
|
362
360
|
if (finished || err || !chunk) return;
|
|
363
361
|
component.appendOutput(chunk);
|
|
364
362
|
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
365
|
-
|
|
363
|
+
sink.push(normalizedChunk);
|
|
366
364
|
tui.requestRender();
|
|
367
365
|
},
|
|
368
366
|
)
|
|
369
367
|
.then(finalize)
|
|
370
368
|
.catch(error => {
|
|
371
|
-
|
|
372
|
-
.then(() => sink.push(`PTY error: ${error instanceof Error ? error.message : String(error)}\n`))
|
|
373
|
-
.catch(() => {});
|
|
369
|
+
sink.push(`PTY error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
374
370
|
finalize({ exitCode: undefined, cancelled: false, timedOut: false });
|
|
375
371
|
});
|
|
376
372
|
return component;
|
package/src/tools/python.ts
CHANGED
|
@@ -289,8 +289,8 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
289
289
|
const executorOptions: PythonExecutorOptions = {
|
|
290
290
|
...baseExecutorOptions,
|
|
291
291
|
reset: isFirstCell ? reset : false,
|
|
292
|
-
onChunk:
|
|
293
|
-
|
|
292
|
+
onChunk: chunk => {
|
|
293
|
+
outputSink!.push(chunk);
|
|
294
294
|
},
|
|
295
295
|
};
|
|
296
296
|
|