@oh-my-pi/pi-coding-agent 6.8.0 → 6.8.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/CHANGELOG.md +23 -0
- package/examples/extensions/plan-mode.ts +8 -8
- package/examples/extensions/tools.ts +7 -7
- package/package.json +6 -6
- package/src/cli/session-picker.ts +5 -2
- package/src/core/agent-session.ts +18 -5
- package/src/core/auth-storage.ts +13 -1
- package/src/core/bash-executor.ts +5 -4
- package/src/core/exec.ts +4 -2
- package/src/core/extensions/types.ts +1 -1
- package/src/core/hooks/types.ts +4 -3
- package/src/core/mcp/transports/http.ts +35 -27
- package/src/core/prompt-templates.ts +1 -1
- package/src/core/python-gateway-coordinator.ts +5 -4
- package/src/core/ssh/ssh-executor.ts +1 -1
- package/src/core/tools/lsp/client.ts +1 -1
- package/src/core/tools/patch/applicator.ts +38 -24
- package/src/core/tools/patch/diff.ts +7 -3
- package/src/core/tools/patch/fuzzy.ts +19 -1
- package/src/core/tools/patch/index.ts +4 -1
- package/src/core/tools/patch/types.ts +4 -0
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/task/executor.ts +100 -64
- package/src/core/tools/task/worker.ts +44 -14
- package/src/core/tools/web-fetch.ts +47 -7
- package/src/core/tools/web-scrapers/youtube.ts +6 -49
- package/src/lib/worktree/collapse.ts +3 -3
- package/src/lib/worktree/git.ts +6 -40
- package/src/lib/worktree/index.ts +1 -1
- package/src/main.ts +7 -5
- package/src/modes/interactive/components/login-dialog.ts +6 -2
- package/src/modes/interactive/components/tool-execution.ts +4 -0
- package/src/modes/interactive/interactive-mode.ts +3 -0
- package/src/utils/clipboard.ts +3 -5
- package/src/core/tools/task/model-resolver.ts +0 -206
|
@@ -215,7 +215,25 @@ export function findMatch(
|
|
|
215
215
|
if (exactIndex !== -1) {
|
|
216
216
|
const occurrences = content.split(target).length - 1;
|
|
217
217
|
if (occurrences > 1) {
|
|
218
|
-
|
|
218
|
+
// Find line numbers and previews for each occurrence (up to 5)
|
|
219
|
+
const contentLines = content.split("\n");
|
|
220
|
+
const occurrenceLines: number[] = [];
|
|
221
|
+
const occurrencePreviews: string[] = [];
|
|
222
|
+
let searchStart = 0;
|
|
223
|
+
for (let i = 0; i < 5; i++) {
|
|
224
|
+
const idx = content.indexOf(target, searchStart);
|
|
225
|
+
if (idx === -1) break;
|
|
226
|
+
const lineNumber = content.slice(0, idx).split("\n").length;
|
|
227
|
+
occurrenceLines.push(lineNumber);
|
|
228
|
+
// Extract 3 lines starting from match (0-indexed)
|
|
229
|
+
const previewLines = contentLines.slice(lineNumber - 1, lineNumber + 2);
|
|
230
|
+
const preview = previewLines
|
|
231
|
+
.map((line, i) => ` ${lineNumber + i} | ${line.length > 60 ? `${line.slice(0, 57)}...` : line}`)
|
|
232
|
+
.join("\n");
|
|
233
|
+
occurrencePreviews.push(preview);
|
|
234
|
+
searchStart = idx + 1;
|
|
235
|
+
}
|
|
236
|
+
return { occurrences, occurrenceLines, occurrencePreviews };
|
|
219
237
|
}
|
|
220
238
|
const startLine = content.slice(0, exactIndex).split("\n").length;
|
|
221
239
|
return {
|
|
@@ -390,8 +390,11 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
393
|
+
const previews = matchOutcome.occurrencePreviews?.join("\n\n") ?? "";
|
|
394
|
+
const moreMsg = matchOutcome.occurrences > 5 ? ` (showing first 5 of ${matchOutcome.occurrences})` : "";
|
|
393
395
|
throw new Error(
|
|
394
|
-
`Found ${matchOutcome.occurrences} occurrences
|
|
396
|
+
`Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\n` +
|
|
397
|
+
`Add more context lines to disambiguate.`,
|
|
395
398
|
);
|
|
396
399
|
}
|
|
397
400
|
|
|
@@ -40,6 +40,10 @@ export interface MatchOutcome {
|
|
|
40
40
|
closest?: FuzzyMatch;
|
|
41
41
|
/** Number of occurrences if multiple exact matches found */
|
|
42
42
|
occurrences?: number;
|
|
43
|
+
/** Line numbers where occurrences were found (1-indexed) */
|
|
44
|
+
occurrenceLines?: number[];
|
|
45
|
+
/** Preview snippets for each occurrence (up to 5) */
|
|
46
|
+
occurrencePreviews?: string[];
|
|
43
47
|
/** Number of fuzzy matches above threshold */
|
|
44
48
|
fuzzyMatches?: number;
|
|
45
49
|
}
|
package/src/core/tools/python.ts
CHANGED
|
@@ -10,13 +10,13 @@ import type { EventBus } from "../../event-bus";
|
|
|
10
10
|
import { callTool } from "../../mcp/client";
|
|
11
11
|
import type { MCPManager } from "../../mcp/manager";
|
|
12
12
|
import type { ModelRegistry } from "../../model-registry";
|
|
13
|
+
import { formatModelString, parseModelPattern } from "../../model-resolver";
|
|
13
14
|
import { checkPythonKernelAvailability } from "../../python-kernel";
|
|
14
15
|
import type { ToolSession } from "..";
|
|
15
16
|
import { LspTool } from "../lsp/index";
|
|
16
17
|
import type { LspParams } from "../lsp/types";
|
|
17
18
|
import { PythonTool } from "../python";
|
|
18
19
|
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
19
|
-
import { resolveModelPattern } from "./model-resolver";
|
|
20
20
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
21
21
|
import {
|
|
22
22
|
type AgentDefinition,
|
|
@@ -296,10 +296,26 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
const serializedSettings = options.settingsManager?.serialize();
|
|
299
|
-
const availableModels = options.modelRegistry?.getAvailable()
|
|
300
|
-
|
|
301
|
-
// Resolve
|
|
302
|
-
const
|
|
299
|
+
const availableModels = options.modelRegistry?.getAvailable() ?? [];
|
|
300
|
+
|
|
301
|
+
// Resolve model pattern to provider/modelId string
|
|
302
|
+
const modelPattern = modelOverride ?? agent.model;
|
|
303
|
+
let resolvedModel: string | undefined;
|
|
304
|
+
if (modelPattern) {
|
|
305
|
+
// Handle omp/<role> or pi/<role> aliases (e.g., "omp/slow", "pi/fast")
|
|
306
|
+
let effectivePattern = modelPattern;
|
|
307
|
+
const lower = modelPattern.toLowerCase();
|
|
308
|
+
if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
|
|
309
|
+
const role = lower.startsWith("omp/") ? modelPattern.slice(4) : modelPattern.slice(3);
|
|
310
|
+
const roles = serializedSettings?.modelRoles as Record<string, string> | undefined;
|
|
311
|
+
const configured = roles?.[role] ?? roles?.[role.toLowerCase()];
|
|
312
|
+
if (configured) {
|
|
313
|
+
effectivePattern = configured;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const { model } = parseModelPattern(effectivePattern, availableModels);
|
|
317
|
+
resolvedModel = model ? formatModelString(model) : undefined;
|
|
318
|
+
}
|
|
303
319
|
const sessionFile = subtaskSessionFile ?? null;
|
|
304
320
|
const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
|
|
305
321
|
|
|
@@ -343,7 +359,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
343
359
|
let abortSent = false;
|
|
344
360
|
let abortReason: AbortReason | undefined;
|
|
345
361
|
let terminationScheduled = false;
|
|
346
|
-
let
|
|
362
|
+
let terminated = false;
|
|
363
|
+
let terminationTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
364
|
+
let pendingTerminationTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
347
365
|
let finalize: ((message: Extract<SubagentWorkerResponse, { type: "done" }>) => void) | null = null;
|
|
348
366
|
const listenerController = new AbortController();
|
|
349
367
|
const listenerSignal = listenerController.signal;
|
|
@@ -416,28 +434,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
416
434
|
const scheduleTermination = () => {
|
|
417
435
|
if (terminationScheduled) return;
|
|
418
436
|
terminationScheduled = true;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
},
|
|
439
|
-
{ once: true, signal: listenerSignal },
|
|
440
|
-
);
|
|
437
|
+
terminationTimeoutId = setTimeout(() => {
|
|
438
|
+
terminationTimeoutId = null;
|
|
439
|
+
if (resolved || terminated) return;
|
|
440
|
+
terminated = true;
|
|
441
|
+
try {
|
|
442
|
+
worker.terminate();
|
|
443
|
+
} catch {
|
|
444
|
+
// Ignore termination errors
|
|
445
|
+
}
|
|
446
|
+
if (finalize && !resolved) {
|
|
447
|
+
finalize({
|
|
448
|
+
type: "done",
|
|
449
|
+
exitCode: 1,
|
|
450
|
+
durationMs: Date.now() - startTime,
|
|
451
|
+
error: abortReason === "signal" ? "Aborted" : "Worker terminated after tool completion",
|
|
452
|
+
aborted: abortReason === "signal",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}, 2000);
|
|
441
456
|
};
|
|
442
457
|
|
|
443
458
|
const requestAbort = (reason: AbortReason) => {
|
|
@@ -461,28 +476,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
461
476
|
// Worker already terminated, nothing to do
|
|
462
477
|
}
|
|
463
478
|
// Cancel pending termination if it exists
|
|
464
|
-
|
|
465
|
-
pendingTerminationController.abort();
|
|
466
|
-
pendingTerminationController = null;
|
|
467
|
-
}
|
|
479
|
+
cancelPendingTermination();
|
|
468
480
|
scheduleTermination();
|
|
469
481
|
};
|
|
470
482
|
|
|
471
483
|
const schedulePendingTermination = () => {
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
if (pendingTerminationTimeoutId || abortSent || terminationScheduled || resolved) return;
|
|
485
|
+
pendingTerminationTimeoutId = setTimeout(() => {
|
|
486
|
+
pendingTerminationTimeoutId = null;
|
|
487
|
+
if (!resolved) {
|
|
488
|
+
requestAbort("terminate");
|
|
489
|
+
}
|
|
490
|
+
}, 2000);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const cancelPendingTermination = () => {
|
|
494
|
+
if (pendingTerminationTimeoutId) {
|
|
495
|
+
clearTimeout(pendingTerminationTimeoutId);
|
|
496
|
+
pendingTerminationTimeoutId = null;
|
|
497
|
+
}
|
|
486
498
|
};
|
|
487
499
|
|
|
488
500
|
// Handle abort signal
|
|
@@ -655,9 +667,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
655
667
|
// Accumulate tokens for progress display
|
|
656
668
|
progress.tokens += getUsageTokens(messageUsage);
|
|
657
669
|
}
|
|
658
|
-
// If pending termination, now we have tokens - terminate
|
|
659
|
-
if (
|
|
660
|
-
|
|
670
|
+
// If pending termination, now we have tokens - terminate immediately
|
|
671
|
+
if (pendingTerminationTimeoutId) {
|
|
672
|
+
cancelPendingTermination();
|
|
673
|
+
requestAbort("terminate");
|
|
661
674
|
}
|
|
662
675
|
break;
|
|
663
676
|
}
|
|
@@ -714,7 +727,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
714
727
|
|
|
715
728
|
const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
|
|
716
729
|
const cleanup = () => {
|
|
717
|
-
pendingTerminationController = null;
|
|
718
730
|
listenerController.abort();
|
|
719
731
|
};
|
|
720
732
|
finalize = (message) => {
|
|
@@ -723,10 +735,18 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
723
735
|
cleanup();
|
|
724
736
|
resolve(message);
|
|
725
737
|
};
|
|
738
|
+
const postMessageSafe = (message: unknown) => {
|
|
739
|
+
if (resolved || terminated) return;
|
|
740
|
+
try {
|
|
741
|
+
worker.postMessage(message);
|
|
742
|
+
} catch {
|
|
743
|
+
// Worker already terminated
|
|
744
|
+
}
|
|
745
|
+
};
|
|
726
746
|
const handleMCPCall = async (request: MCPToolCallRequest) => {
|
|
727
747
|
const mcpManager = options.mcpManager;
|
|
728
748
|
if (!mcpManager) {
|
|
729
|
-
|
|
749
|
+
postMessageSafe({
|
|
730
750
|
type: "mcp_tool_result",
|
|
731
751
|
callId: request.callId,
|
|
732
752
|
error: "MCP not available",
|
|
@@ -743,13 +763,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
743
763
|
})(),
|
|
744
764
|
request.timeoutMs,
|
|
745
765
|
);
|
|
746
|
-
|
|
766
|
+
postMessageSafe({
|
|
747
767
|
type: "mcp_tool_result",
|
|
748
768
|
callId: request.callId,
|
|
749
769
|
result: { content: result.content ?? [], isError: result.isError },
|
|
750
770
|
});
|
|
751
771
|
} catch (error) {
|
|
752
|
-
|
|
772
|
+
postMessageSafe({
|
|
753
773
|
type: "mcp_tool_result",
|
|
754
774
|
callId: request.callId,
|
|
755
775
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -767,7 +787,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
767
787
|
|
|
768
788
|
const handlePythonCall = async (request: PythonToolCallRequest) => {
|
|
769
789
|
if (!pythonTool) {
|
|
770
|
-
|
|
790
|
+
postMessageSafe({
|
|
771
791
|
type: "python_tool_result",
|
|
772
792
|
callId: request.callId,
|
|
773
793
|
error: "Python proxy not available",
|
|
@@ -785,7 +805,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
785
805
|
request.params as { code: string; timeout?: number; workdir?: string; reset?: boolean },
|
|
786
806
|
combinedSignal,
|
|
787
807
|
);
|
|
788
|
-
|
|
808
|
+
postMessageSafe({
|
|
789
809
|
type: "python_tool_result",
|
|
790
810
|
callId: request.callId,
|
|
791
811
|
result: { content: result.content ?? [], details: result.details },
|
|
@@ -797,7 +817,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
797
817
|
: error instanceof Error
|
|
798
818
|
? error.message
|
|
799
819
|
: String(error);
|
|
800
|
-
|
|
820
|
+
postMessageSafe({
|
|
801
821
|
type: "python_tool_result",
|
|
802
822
|
callId: request.callId,
|
|
803
823
|
error: message,
|
|
@@ -816,7 +836,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
816
836
|
|
|
817
837
|
const handleLspCall = async (request: LspToolCallRequest) => {
|
|
818
838
|
if (!lspTool) {
|
|
819
|
-
|
|
839
|
+
postMessageSafe({
|
|
820
840
|
type: "lsp_tool_result",
|
|
821
841
|
callId: request.callId,
|
|
822
842
|
error: "LSP proxy not available",
|
|
@@ -828,7 +848,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
828
848
|
lspTool.execute(request.callId, request.params as LspParams, signal),
|
|
829
849
|
request.timeoutMs,
|
|
830
850
|
);
|
|
831
|
-
|
|
851
|
+
postMessageSafe({
|
|
832
852
|
type: "lsp_tool_result",
|
|
833
853
|
callId: request.callId,
|
|
834
854
|
result: { content: result.content ?? [], details: result.details },
|
|
@@ -840,7 +860,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
840
860
|
: error instanceof Error
|
|
841
861
|
? error.message
|
|
842
862
|
: String(error);
|
|
843
|
-
|
|
863
|
+
postMessageSafe({
|
|
844
864
|
type: "lsp_tool_result",
|
|
845
865
|
callId: request.callId,
|
|
846
866
|
error: message,
|
|
@@ -881,10 +901,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
881
901
|
return;
|
|
882
902
|
}
|
|
883
903
|
if (message.type === "done") {
|
|
904
|
+
// Worker is exiting - mark as terminated to prevent calling terminate() on dead worker
|
|
905
|
+
terminated = true;
|
|
884
906
|
finalize?.(message);
|
|
885
907
|
}
|
|
886
908
|
};
|
|
887
909
|
const onError = (event: WorkerErrorEvent) => {
|
|
910
|
+
// Worker error likely means it's dead or dying
|
|
911
|
+
terminated = true;
|
|
888
912
|
finalize?.({
|
|
889
913
|
type: "done",
|
|
890
914
|
exitCode: 1,
|
|
@@ -893,6 +917,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
893
917
|
});
|
|
894
918
|
};
|
|
895
919
|
const onMessageError = () => {
|
|
920
|
+
// Message error may indicate worker is in bad state
|
|
921
|
+
terminated = true;
|
|
896
922
|
finalize?.({
|
|
897
923
|
type: "done",
|
|
898
924
|
exitCode: 1,
|
|
@@ -902,6 +928,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
902
928
|
};
|
|
903
929
|
const onClose = () => {
|
|
904
930
|
// Worker terminated unexpectedly (crashed or was killed without sending done)
|
|
931
|
+
// Mark as terminated since the worker is already dead - calling terminate() again would crash
|
|
932
|
+
terminated = true;
|
|
905
933
|
const abortMessage =
|
|
906
934
|
abortSent && abortReason === "signal"
|
|
907
935
|
? "Worker terminated after abort"
|
|
@@ -932,11 +960,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
932
960
|
}
|
|
933
961
|
});
|
|
934
962
|
|
|
935
|
-
// Cleanup
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
963
|
+
// Cleanup - cancel any pending timeouts first
|
|
964
|
+
if (terminationTimeoutId) {
|
|
965
|
+
clearTimeout(terminationTimeoutId);
|
|
966
|
+
terminationTimeoutId = null;
|
|
967
|
+
}
|
|
968
|
+
cancelPendingTermination();
|
|
969
|
+
if (!terminated) {
|
|
970
|
+
terminated = true;
|
|
971
|
+
try {
|
|
972
|
+
worker.terminate();
|
|
973
|
+
} catch {
|
|
974
|
+
// Ignore termination errors
|
|
975
|
+
}
|
|
940
976
|
}
|
|
941
977
|
|
|
942
978
|
let exitCode = done.exitCode;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
17
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { logger, postmortem, untilAborted } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import type { TSchema } from "@sinclair/typebox";
|
|
20
20
|
import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
|
|
21
21
|
import type { AgentSessionEvent } from "../../agent-session";
|
|
@@ -377,17 +377,29 @@ function createPythonProxyTool(): CustomTool<typeof pythonSchema> {
|
|
|
377
377
|
description: getPythonToolDescription(),
|
|
378
378
|
parameters: pythonSchema,
|
|
379
379
|
execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
c
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
380
|
+
try {
|
|
381
|
+
const timeoutMs = getPythonCallTimeoutMs(params as PythonToolParams);
|
|
382
|
+
const result = await callPythonToolViaParent(params as PythonToolParams, signal, timeoutMs);
|
|
383
|
+
return {
|
|
384
|
+
content:
|
|
385
|
+
result?.content?.map((c) =>
|
|
386
|
+
c.type === "text"
|
|
387
|
+
? { type: "text" as const, text: c.text ?? "" }
|
|
388
|
+
: { type: "text" as const, text: JSON.stringify(c) },
|
|
389
|
+
) ?? [],
|
|
390
|
+
details: result?.details as PythonToolDetails | undefined,
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return {
|
|
394
|
+
content: [
|
|
395
|
+
{
|
|
396
|
+
type: "text" as const,
|
|
397
|
+
text: `Python error: ${error instanceof Error ? error.message : String(error)}`,
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
details: { isError: true } as PythonToolDetails,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
391
403
|
},
|
|
392
404
|
};
|
|
393
405
|
}
|
|
@@ -780,10 +792,18 @@ function handleAbort(): void {
|
|
|
780
792
|
}
|
|
781
793
|
}
|
|
782
794
|
|
|
783
|
-
const reportFatal = (message: string): void => {
|
|
795
|
+
const reportFatal = async (message: string): Promise<void> => {
|
|
796
|
+
// Run postmortem cleanup first to ensure child processes are killed
|
|
797
|
+
try {
|
|
798
|
+
await postmortem.cleanup();
|
|
799
|
+
} catch {
|
|
800
|
+
// Ignore cleanup errors
|
|
801
|
+
}
|
|
802
|
+
const error = new Error(message);
|
|
803
|
+
|
|
784
804
|
const runState = activeRun;
|
|
785
805
|
if (runState) {
|
|
786
|
-
runState.abortController.abort();
|
|
806
|
+
runState.abortController.abort(error);
|
|
787
807
|
if (runState.session) {
|
|
788
808
|
void runState.session.abort();
|
|
789
809
|
}
|
|
@@ -821,6 +841,16 @@ self.addEventListener("error", (event) => {
|
|
|
821
841
|
self.addEventListener("unhandledrejection", (event) => {
|
|
822
842
|
const reason = event.reason;
|
|
823
843
|
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
844
|
+
|
|
845
|
+
// Avoid terminating active runs on tool-level errors that bubble as rejections.
|
|
846
|
+
if (activeRun) {
|
|
847
|
+
logger.error("Unhandled rejection in subagent worker", { error: message });
|
|
848
|
+
if ("preventDefault" in event && typeof event.preventDefault === "function") {
|
|
849
|
+
event.preventDefault();
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
824
854
|
reportFatal(`Unhandled rejection: ${message}`);
|
|
825
855
|
});
|
|
826
856
|
|
|
@@ -4,8 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { ptree } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import { $ } from "bun";
|
|
9
9
|
import { nanoid } from "nanoid";
|
|
10
10
|
import { parse as parseHtml } from "node-html-parser";
|
|
11
11
|
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
@@ -75,18 +75,58 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
75
75
|
* Execute a command and return stdout
|
|
76
76
|
*/
|
|
77
77
|
|
|
78
|
+
type WritableLike = {
|
|
79
|
+
write: (chunk: string | Uint8Array) => unknown;
|
|
80
|
+
flush?: () => unknown;
|
|
81
|
+
end?: () => unknown;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const textEncoder = new TextEncoder();
|
|
85
|
+
|
|
86
|
+
async function writeStdin(handle: unknown, input: string | Buffer): Promise<void> {
|
|
87
|
+
if (!handle || typeof handle === "number") return;
|
|
88
|
+
if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
|
|
89
|
+
const writer = (handle as WritableStream<Uint8Array>).getWriter();
|
|
90
|
+
try {
|
|
91
|
+
const chunk = typeof input === "string" ? textEncoder.encode(input) : new Uint8Array(input);
|
|
92
|
+
await writer.write(chunk);
|
|
93
|
+
} finally {
|
|
94
|
+
await writer.close();
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sink = handle as WritableLike;
|
|
100
|
+
sink.write(input);
|
|
101
|
+
if (sink.flush) sink.flush();
|
|
102
|
+
if (sink.end) sink.end();
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
async function exec(
|
|
79
106
|
cmd: string,
|
|
80
107
|
args: string[],
|
|
81
108
|
options?: { timeout?: number; input?: string | Buffer },
|
|
82
109
|
): Promise<{ stdout: string; stderr: string; ok: boolean }> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
const proc = ptree.cspawn([cmd, ...args], {
|
|
111
|
+
stdin: options?.input ? "pipe" : null,
|
|
112
|
+
timeout: options?.timeout,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (options?.input) {
|
|
116
|
+
await writeStdin(proc.stdin, options.input);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
|
120
|
+
try {
|
|
121
|
+
await proc.exited;
|
|
122
|
+
} catch {
|
|
123
|
+
// Handle non-zero exit or timeout
|
|
124
|
+
}
|
|
125
|
+
|
|
86
126
|
return {
|
|
87
|
-
stdout
|
|
88
|
-
stderr
|
|
89
|
-
ok:
|
|
127
|
+
stdout,
|
|
128
|
+
stderr,
|
|
129
|
+
ok: proc.exitCode === 0,
|
|
90
130
|
};
|
|
91
131
|
}
|
|
92
132
|
|
|
@@ -2,7 +2,6 @@ import { unlinkSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { cspawn } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type { FileSink } from "bun";
|
|
6
5
|
import { nanoid } from "nanoid";
|
|
7
6
|
import { ensureTool } from "../../../utils/tools-manager";
|
|
8
7
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
@@ -16,59 +15,17 @@ async function exec(
|
|
|
16
15
|
args: string[],
|
|
17
16
|
options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
|
|
18
17
|
): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const onAbort = () => controller.abort(options?.signal?.reason ?? new Error("Aborted"));
|
|
21
|
-
if (options?.signal) {
|
|
22
|
-
if (options.signal.aborted) {
|
|
23
|
-
onAbort();
|
|
24
|
-
} else {
|
|
25
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const timeoutId =
|
|
29
|
-
options?.timeout && options.timeout > 0
|
|
30
|
-
? setTimeout(() => controller.abort(new Error("Timeout")), options.timeout)
|
|
31
|
-
: undefined;
|
|
32
18
|
const proc = cspawn([cmd, ...args], {
|
|
33
|
-
signal:
|
|
19
|
+
signal: options?.signal,
|
|
20
|
+
timeout: options?.timeout,
|
|
21
|
+
stdin: options?.input ? Buffer.from(options.input) : undefined,
|
|
34
22
|
});
|
|
35
23
|
|
|
36
|
-
if (options?.input && proc.stdin) {
|
|
37
|
-
const stdin = proc.stdin as FileSink;
|
|
38
|
-
const payload = typeof options.input === "string" ? new TextEncoder().encode(options.input) : options.input;
|
|
39
|
-
stdin.write(payload);
|
|
40
|
-
const flushed = stdin.flush();
|
|
41
|
-
if (flushed instanceof Promise) {
|
|
42
|
-
await flushed;
|
|
43
|
-
}
|
|
44
|
-
const ended = stdin.end();
|
|
45
|
-
if (ended instanceof Promise) {
|
|
46
|
-
await ended;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
24
|
const [stdout, stderr, exitResult] = await Promise.all([
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
(
|
|
54
|
-
try {
|
|
55
|
-
await proc.exited;
|
|
56
|
-
return proc.exitCode ?? 0;
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (err && typeof err === "object" && "exitCode" in err) {
|
|
59
|
-
const exitValue = (err as { exitCode?: number }).exitCode;
|
|
60
|
-
if (typeof exitValue === "number") {
|
|
61
|
-
return exitValue;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
65
|
-
}
|
|
66
|
-
})(),
|
|
25
|
+
proc.stdout.text(),
|
|
26
|
+
proc.stderr.text(),
|
|
27
|
+
proc.exited.then(() => proc.exitCode ?? 0),
|
|
67
28
|
]);
|
|
68
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
69
|
-
if (options?.signal) {
|
|
70
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
71
|
-
}
|
|
72
29
|
|
|
73
30
|
return {
|
|
74
31
|
stdout,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
2
|
import { WorktreeError, WorktreeErrorCode } from "./errors";
|
|
3
|
-
import { git,
|
|
3
|
+
import { git, gitWithInput } from "./git";
|
|
4
4
|
import { find, remove, type Worktree } from "./operations";
|
|
5
5
|
|
|
6
6
|
export type CollapseStrategy = "simple" | "merge-base" | "rebase";
|
|
@@ -121,10 +121,10 @@ async function collapseRebase(src: Worktree, dst: Worktree): Promise<string> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
async function applyDiff(diff: string, targetPath: string): Promise<void> {
|
|
124
|
-
let result = await
|
|
124
|
+
let result = await gitWithInput(["apply"], diff, targetPath);
|
|
125
125
|
if (result.code === 0) return;
|
|
126
126
|
|
|
127
|
-
result = await
|
|
127
|
+
result = await gitWithInput(["apply", "--3way"], diff, targetPath);
|
|
128
128
|
if (result.code === 0) return;
|
|
129
129
|
|
|
130
130
|
throw new WorktreeError(
|