@oh-my-pi/pi-coding-agent 13.16.0 → 13.16.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 +14 -0
- package/package.json +7 -7
- package/src/commit/agentic/tools/analyze-file.ts +1 -0
- package/src/config/settings-schema.ts +7 -0
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +7 -0
- package/src/extensibility/extensions/types.ts +4 -0
- package/src/ipy/cancellation.ts +28 -0
- package/src/ipy/executor.ts +252 -77
- package/src/ipy/kernel.ts +181 -35
- package/src/ipy/modules.ts +39 -4
- package/src/modes/acp/acp-agent.ts +1 -0
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/print-mode.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/grep.md +2 -2
- package/src/sdk.ts +17 -1
- package/src/session/agent-session.ts +6 -0
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +2 -0
- package/src/tools/browser.ts +51 -2
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +21 -17
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +3 -2
package/src/ipy/kernel.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { Settings } from "../config/settings";
|
|
4
4
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
5
|
+
import { createCancellationError, getAbortReason, getExecutionCancellationError } from "./cancellation";
|
|
5
6
|
import { acquireSharedGateway, releaseSharedGateway, shutdownSharedGateway } from "./gateway-coordinator";
|
|
6
7
|
import { loadPythonModules } from "./modules";
|
|
7
8
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
@@ -35,6 +36,94 @@ function getExternalGatewayConfig(): ExternalGatewayConfig | null {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const STARTUP_CLEANUP_TIMEOUT_MS = 2_000;
|
|
40
|
+
const WEBSOCKET_CONNECT_TIMEOUT_MS = 10_000;
|
|
41
|
+
|
|
42
|
+
interface KernelLifecycleOptions {
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
deadlineMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface KernelShutdownOptions {
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getRemainingTimeMs(deadlineMs?: number): number | undefined {
|
|
53
|
+
if (deadlineMs === undefined) return undefined;
|
|
54
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function throwIfStartupExecutionFailed(
|
|
58
|
+
result: Pick<KernelExecuteResult, "cancelled" | "status" | "timedOut">,
|
|
59
|
+
signal: AbortSignal | undefined,
|
|
60
|
+
failureMessage: string,
|
|
61
|
+
): void {
|
|
62
|
+
if (result.cancelled) {
|
|
63
|
+
throw getExecutionCancellationError(result, signal, failureMessage);
|
|
64
|
+
}
|
|
65
|
+
if (result.status === "error") {
|
|
66
|
+
throw new Error(failureMessage);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createAbortedSignal(reason: Error): AbortSignal {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
controller.abort(reason);
|
|
73
|
+
return controller.signal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function combineAbortSignal(
|
|
77
|
+
options: KernelLifecycleOptions,
|
|
78
|
+
timeoutCapMs?: number,
|
|
79
|
+
fallbackReason = "Operation aborted",
|
|
80
|
+
): AbortSignal | undefined {
|
|
81
|
+
if (options.signal?.aborted) {
|
|
82
|
+
return options.signal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const signals: AbortSignal[] = [];
|
|
86
|
+
if (options.signal) {
|
|
87
|
+
signals.push(options.signal);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remainingMs = getRemainingTimeMs(options.deadlineMs);
|
|
91
|
+
const timeoutMs =
|
|
92
|
+
remainingMs === undefined
|
|
93
|
+
? timeoutCapMs
|
|
94
|
+
: timeoutCapMs === undefined
|
|
95
|
+
? remainingMs
|
|
96
|
+
: Math.min(remainingMs, timeoutCapMs);
|
|
97
|
+
|
|
98
|
+
if (timeoutMs !== undefined) {
|
|
99
|
+
if (timeoutMs <= 0) {
|
|
100
|
+
return createAbortedSignal(createCancellationError("TimeoutError", fallbackReason));
|
|
101
|
+
}
|
|
102
|
+
signals.push(AbortSignal.timeout(timeoutMs));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (signals.length === 0) return undefined;
|
|
106
|
+
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
|
|
110
|
+
if (!signal?.aborted) return;
|
|
111
|
+
throw getAbortReason(signal, fallbackReason);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getStartupExecuteOptions(options: KernelLifecycleOptions): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
|
|
115
|
+
return {
|
|
116
|
+
signal: combineAbortSignal(options, undefined, "Python kernel startup aborted"),
|
|
117
|
+
timeoutMs: getRemainingTimeMs(options.deadlineMs),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getStartupCleanupTimeoutMs(deadlineMs?: number): number {
|
|
122
|
+
const remainingMs = getRemainingTimeMs(deadlineMs);
|
|
123
|
+
if (remainingMs === undefined || remainingMs <= 0) return STARTUP_CLEANUP_TIMEOUT_MS;
|
|
124
|
+
return Math.min(STARTUP_CLEANUP_TIMEOUT_MS, remainingMs);
|
|
125
|
+
}
|
|
126
|
+
|
|
38
127
|
export interface JupyterHeader {
|
|
39
128
|
msg_id: string;
|
|
40
129
|
session: string;
|
|
@@ -93,7 +182,7 @@ export interface PreludeHelper {
|
|
|
93
182
|
category: string;
|
|
94
183
|
}
|
|
95
184
|
|
|
96
|
-
interface KernelStartOptions {
|
|
185
|
+
interface KernelStartOptions extends KernelLifecycleOptions {
|
|
97
186
|
cwd: string;
|
|
98
187
|
env?: Record<string, string | undefined>;
|
|
99
188
|
useSharedGateway?: boolean;
|
|
@@ -339,9 +428,12 @@ export class PythonKernel {
|
|
|
339
428
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
340
429
|
}
|
|
341
430
|
|
|
431
|
+
const startup = { signal: options.signal, deadlineMs: options.deadlineMs };
|
|
432
|
+
const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
|
|
433
|
+
|
|
342
434
|
const externalConfig = getExternalGatewayConfig();
|
|
343
435
|
if (externalConfig) {
|
|
344
|
-
return PythonKernel.#startWithExternalGateway(externalConfig, options.cwd, options.env);
|
|
436
|
+
return PythonKernel.#startWithExternalGateway(externalConfig, options.cwd, options.env, startup);
|
|
345
437
|
}
|
|
346
438
|
|
|
347
439
|
if (options.useSharedGateway === false) {
|
|
@@ -349,6 +441,7 @@ export class PythonKernel {
|
|
|
349
441
|
}
|
|
350
442
|
|
|
351
443
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
444
|
+
throwIfAborted(startupSignal, "Python kernel startup aborted");
|
|
352
445
|
try {
|
|
353
446
|
const sharedResult = await logger.timeAsync("PythonKernel.start:acquireSharedGateway", () =>
|
|
354
447
|
acquireSharedGateway(options.cwd),
|
|
@@ -357,7 +450,7 @@ export class PythonKernel {
|
|
|
357
450
|
throw new Error("Shared Python gateway unavailable");
|
|
358
451
|
}
|
|
359
452
|
const kernel = await logger.timeAsync("PythonKernel.start:startWithSharedGateway", () =>
|
|
360
|
-
PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env),
|
|
453
|
+
PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env, startup),
|
|
361
454
|
);
|
|
362
455
|
return kernel;
|
|
363
456
|
} catch (err) {
|
|
@@ -382,16 +475,20 @@ export class PythonKernel {
|
|
|
382
475
|
config: ExternalGatewayConfig,
|
|
383
476
|
cwd: string,
|
|
384
477
|
env?: Record<string, string | undefined>,
|
|
478
|
+
startup: KernelLifecycleOptions = {},
|
|
385
479
|
): Promise<PythonKernel> {
|
|
386
480
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
387
481
|
if (config.token) {
|
|
388
482
|
headers.Authorization = `token ${config.token}`;
|
|
389
483
|
}
|
|
390
484
|
|
|
485
|
+
const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
|
|
486
|
+
throwIfAborted(startupSignal, "Python kernel startup aborted");
|
|
391
487
|
const createResponse = await fetch(`${config.url}/api/kernels`, {
|
|
392
488
|
method: "POST",
|
|
393
489
|
headers,
|
|
394
490
|
body: JSON.stringify({ name: "python3" }),
|
|
491
|
+
signal: startupSignal,
|
|
395
492
|
});
|
|
396
493
|
|
|
397
494
|
if (!createResponse.ok) {
|
|
@@ -412,16 +509,23 @@ export class PythonKernel {
|
|
|
412
509
|
);
|
|
413
510
|
|
|
414
511
|
try {
|
|
415
|
-
await kernel.#connectWebSocket();
|
|
416
|
-
await kernel.#initializeKernelEnvironment(cwd, env);
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
512
|
+
await kernel.#connectWebSocket(startup);
|
|
513
|
+
await kernel.#initializeKernelEnvironment(cwd, env, startup);
|
|
514
|
+
const preludeOptions = getStartupExecuteOptions(startup);
|
|
515
|
+
const preludeResult = await kernel.execute(PYTHON_PRELUDE, {
|
|
516
|
+
...preludeOptions,
|
|
517
|
+
silent: true,
|
|
518
|
+
storeHistory: false,
|
|
519
|
+
});
|
|
520
|
+
throwIfStartupExecutionFailed(
|
|
521
|
+
preludeResult,
|
|
522
|
+
preludeOptions.signal,
|
|
523
|
+
"Failed to initialize Python kernel prelude",
|
|
524
|
+
);
|
|
525
|
+
await loadPythonModules(kernel, { cwd, signal: startup.signal, deadlineMs: startup.deadlineMs });
|
|
422
526
|
return kernel;
|
|
423
527
|
} catch (err: unknown) {
|
|
424
|
-
await kernel.shutdown();
|
|
528
|
+
await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
|
|
425
529
|
throw err;
|
|
426
530
|
}
|
|
427
531
|
}
|
|
@@ -430,12 +534,16 @@ export class PythonKernel {
|
|
|
430
534
|
gatewayUrl: string,
|
|
431
535
|
cwd: string,
|
|
432
536
|
env?: Record<string, string | undefined>,
|
|
537
|
+
startup: KernelLifecycleOptions = {},
|
|
433
538
|
): Promise<PythonKernel> {
|
|
539
|
+
const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
|
|
540
|
+
throwIfAborted(startupSignal, "Python kernel startup aborted");
|
|
434
541
|
const createResponse = await logger.timeAsync("startWithSharedGateway:createKernel", () =>
|
|
435
542
|
fetch(`${gatewayUrl}/api/kernels`, {
|
|
436
543
|
method: "POST",
|
|
437
544
|
headers: { "Content-Type": "application/json" },
|
|
438
545
|
body: JSON.stringify({ name: "python3" }),
|
|
546
|
+
signal: startupSignal,
|
|
439
547
|
}),
|
|
440
548
|
);
|
|
441
549
|
|
|
@@ -458,46 +566,70 @@ export class PythonKernel {
|
|
|
458
566
|
const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
|
|
459
567
|
|
|
460
568
|
try {
|
|
461
|
-
await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket());
|
|
462
|
-
await logger.timeAsync("startWithSharedGateway:initEnv", () =>
|
|
569
|
+
await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket(startup));
|
|
570
|
+
await logger.timeAsync("startWithSharedGateway:initEnv", () =>
|
|
571
|
+
kernel.#initializeKernelEnvironment(cwd, env, startup),
|
|
572
|
+
);
|
|
573
|
+
const preludeOptions = getStartupExecuteOptions(startup);
|
|
463
574
|
const preludeResult = await logger.timeAsync("startWithSharedGateway:prelude", () =>
|
|
464
|
-
kernel.execute(PYTHON_PRELUDE, {
|
|
575
|
+
kernel.execute(PYTHON_PRELUDE, {
|
|
576
|
+
...preludeOptions,
|
|
577
|
+
silent: true,
|
|
578
|
+
storeHistory: false,
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
581
|
+
throwIfStartupExecutionFailed(
|
|
582
|
+
preludeResult,
|
|
583
|
+
preludeOptions.signal,
|
|
584
|
+
"Failed to initialize Python kernel prelude",
|
|
585
|
+
);
|
|
586
|
+
await logger.timeAsync("startWithSharedGateway:loadModules", () =>
|
|
587
|
+
loadPythonModules(kernel, { cwd, signal: startup.signal, deadlineMs: startup.deadlineMs }),
|
|
465
588
|
);
|
|
466
|
-
if (preludeResult.cancelled || preludeResult.status === "error") {
|
|
467
|
-
throw new Error("Failed to initialize Python kernel prelude");
|
|
468
|
-
}
|
|
469
|
-
await logger.timeAsync("startWithSharedGateway:loadModules", () => loadPythonModules(kernel, { cwd }));
|
|
470
589
|
return kernel;
|
|
471
590
|
} catch (err: unknown) {
|
|
472
|
-
await kernel.shutdown();
|
|
591
|
+
await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
|
|
473
592
|
throw err;
|
|
474
593
|
}
|
|
475
594
|
}
|
|
476
595
|
|
|
477
|
-
async #connectWebSocket(): Promise<void> {
|
|
596
|
+
async #connectWebSocket(options: KernelLifecycleOptions = {}): Promise<void> {
|
|
478
597
|
const wsBase = this.gatewayUrl.replace(/^http/, "ws");
|
|
479
598
|
let wsUrl = `${wsBase}/api/kernels/${this.kernelId}/channels`;
|
|
480
599
|
if (this.#authToken) {
|
|
481
600
|
wsUrl += `?token=${encodeURIComponent(this.#authToken)}`;
|
|
482
601
|
}
|
|
483
602
|
|
|
603
|
+
const connectSignal = combineAbortSignal(options, WEBSOCKET_CONNECT_TIMEOUT_MS, "WebSocket connection timeout");
|
|
604
|
+
throwIfAborted(connectSignal, "WebSocket connection timeout");
|
|
605
|
+
|
|
484
606
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
485
607
|
const ws = new WebSocket(wsUrl);
|
|
486
608
|
ws.binaryType = "arraybuffer";
|
|
487
609
|
let settled = false;
|
|
488
610
|
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
settled = true;
|
|
493
|
-
reject(new Error("WebSocket connection timeout"));
|
|
611
|
+
const finalize = (): void => {
|
|
612
|
+
if (connectSignal) {
|
|
613
|
+
connectSignal.removeEventListener("abort", onAbort);
|
|
494
614
|
}
|
|
495
|
-
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const onAbort = () => {
|
|
618
|
+
ws.close();
|
|
619
|
+
if (settled) return;
|
|
620
|
+
settled = true;
|
|
621
|
+
finalize();
|
|
622
|
+
reject(getAbortReason(connectSignal, "WebSocket connection timeout"));
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
if (connectSignal) {
|
|
626
|
+
connectSignal.addEventListener("abort", onAbort, { once: true });
|
|
627
|
+
}
|
|
496
628
|
|
|
497
629
|
ws.onopen = () => {
|
|
498
630
|
if (settled) return;
|
|
499
631
|
settled = true;
|
|
500
|
-
|
|
632
|
+
finalize();
|
|
501
633
|
this.#ws = ws;
|
|
502
634
|
resolve();
|
|
503
635
|
};
|
|
@@ -506,7 +638,7 @@ export class PythonKernel {
|
|
|
506
638
|
const error = new Error(`WebSocket error: ${event}`);
|
|
507
639
|
if (!settled) {
|
|
508
640
|
settled = true;
|
|
509
|
-
|
|
641
|
+
finalize();
|
|
510
642
|
reject(error);
|
|
511
643
|
return;
|
|
512
644
|
}
|
|
@@ -520,7 +652,7 @@ export class PythonKernel {
|
|
|
520
652
|
this.#ws = null;
|
|
521
653
|
if (!settled) {
|
|
522
654
|
settled = true;
|
|
523
|
-
|
|
655
|
+
finalize();
|
|
524
656
|
reject(new Error("WebSocket closed before connection"));
|
|
525
657
|
return;
|
|
526
658
|
}
|
|
@@ -561,7 +693,11 @@ export class PythonKernel {
|
|
|
561
693
|
return promise;
|
|
562
694
|
}
|
|
563
695
|
|
|
564
|
-
async #initializeKernelEnvironment(
|
|
696
|
+
async #initializeKernelEnvironment(
|
|
697
|
+
cwd: string,
|
|
698
|
+
env?: Record<string, string | undefined>,
|
|
699
|
+
options: KernelLifecycleOptions = {},
|
|
700
|
+
): Promise<void> {
|
|
565
701
|
const envEntries = Object.entries(env ?? {}).filter(([, value]) => value !== undefined);
|
|
566
702
|
const envPayload = Object.fromEntries(envEntries);
|
|
567
703
|
const initScript = [
|
|
@@ -572,10 +708,13 @@ export class PythonKernel {
|
|
|
572
708
|
"for __omp_key, __omp_val in __omp_env.items():\n os.environ[__omp_key] = __omp_val",
|
|
573
709
|
"if __omp_cwd not in sys.path:\n sys.path.insert(0, __omp_cwd)",
|
|
574
710
|
].join("\n");
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
711
|
+
const executeOptions = getStartupExecuteOptions(options);
|
|
712
|
+
const result = await this.execute(initScript, {
|
|
713
|
+
...executeOptions,
|
|
714
|
+
silent: true,
|
|
715
|
+
storeHistory: false,
|
|
716
|
+
});
|
|
717
|
+
throwIfStartupExecutionFailed(result, executeOptions.signal, "Failed to initialize Python kernel environment");
|
|
579
718
|
}
|
|
580
719
|
|
|
581
720
|
#abortPendingExecutions(reason: string): void {
|
|
@@ -842,16 +981,23 @@ export class PythonKernel {
|
|
|
842
981
|
}
|
|
843
982
|
}
|
|
844
983
|
|
|
845
|
-
async shutdown(): Promise<void> {
|
|
984
|
+
async shutdown(options?: KernelShutdownOptions): Promise<void> {
|
|
846
985
|
if (this.#disposed) return;
|
|
847
986
|
this.#disposed = true;
|
|
848
987
|
this.#alive = false;
|
|
849
988
|
this.#abortPendingExecutions("Kernel shutdown");
|
|
850
989
|
|
|
990
|
+
const shutdownSignal = combineAbortSignal(
|
|
991
|
+
{ signal: options?.signal },
|
|
992
|
+
options?.timeoutMs,
|
|
993
|
+
"Python kernel shutdown timed out",
|
|
994
|
+
);
|
|
995
|
+
|
|
851
996
|
try {
|
|
852
997
|
await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
|
|
853
998
|
method: "DELETE",
|
|
854
999
|
headers: this.#authHeaders(),
|
|
1000
|
+
signal: shutdownSignal,
|
|
855
1001
|
});
|
|
856
1002
|
} catch (err: unknown) {
|
|
857
1003
|
logger.warn("Failed to delete kernel via API", { error: err instanceof Error ? err.message : String(err) });
|
package/src/ipy/modules.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { getExecutionCancellationError } from "./cancellation";
|
|
4
5
|
|
|
5
6
|
export type PythonModuleSource = "user" | "project";
|
|
6
7
|
|
|
@@ -13,13 +14,14 @@ export interface PythonModuleEntry {
|
|
|
13
14
|
export interface PythonModuleExecuteResult {
|
|
14
15
|
status: "ok" | "error";
|
|
15
16
|
cancelled: boolean;
|
|
17
|
+
timedOut?: boolean;
|
|
16
18
|
error?: { name: string; value: string; traceback: string[] };
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface PythonModuleExecutor {
|
|
20
22
|
execute: (
|
|
21
23
|
code: string,
|
|
22
|
-
options?: { silent?: boolean; storeHistory?: boolean },
|
|
24
|
+
options?: { signal?: AbortSignal; timeoutMs?: number; silent?: boolean; storeHistory?: boolean },
|
|
23
25
|
) => Promise<PythonModuleExecuteResult>;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -30,6 +32,12 @@ export interface DiscoverPythonModulesOptions {
|
|
|
30
32
|
agentDir?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface LoadPythonModulesOptions extends DiscoverPythonModulesOptions {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
deadlineMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
interface ModuleCandidate {
|
|
34
42
|
name: string;
|
|
35
43
|
path: string;
|
|
@@ -61,6 +69,25 @@ async function readModuleContent(candidate: ModuleCandidate): Promise<PythonModu
|
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
function createTimeoutError(message: string): Error {
|
|
73
|
+
const error = new Error(message);
|
|
74
|
+
error.name = "TimeoutError";
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function requireModuleExecutionTimeoutMs(options: LoadPythonModulesOptions): number | undefined {
|
|
79
|
+
if (options.deadlineMs === undefined) {
|
|
80
|
+
return options.timeoutMs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const remainingMs = options.deadlineMs - Date.now();
|
|
84
|
+
if (remainingMs <= 0) {
|
|
85
|
+
throw createTimeoutError("Python module loading timed out");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return remainingMs;
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
/**
|
|
65
92
|
* Discover Python prelude extension modules from user and project directories.
|
|
66
93
|
*/
|
|
@@ -95,12 +122,20 @@ export async function discoverPythonModules(options: DiscoverPythonModulesOption
|
|
|
95
122
|
*/
|
|
96
123
|
export async function loadPythonModules(
|
|
97
124
|
executor: PythonModuleExecutor,
|
|
98
|
-
options:
|
|
125
|
+
options: LoadPythonModulesOptions = {},
|
|
99
126
|
): Promise<PythonModuleEntry[]> {
|
|
100
127
|
const modules = await discoverPythonModules(options);
|
|
101
128
|
for (const module of modules) {
|
|
102
|
-
const result = await executor.execute(module.content, {
|
|
103
|
-
|
|
129
|
+
const result = await executor.execute(module.content, {
|
|
130
|
+
signal: options.signal,
|
|
131
|
+
timeoutMs: requireModuleExecutionTimeoutMs(options),
|
|
132
|
+
silent: true,
|
|
133
|
+
storeHistory: false,
|
|
134
|
+
});
|
|
135
|
+
if (result.cancelled) {
|
|
136
|
+
throw getExecutionCancellationError(result, options.signal, `Failed to load Python module ${module.path}`);
|
|
137
|
+
}
|
|
138
|
+
if (result.status === "error") {
|
|
104
139
|
const details = result.error ? `${result.error.name}: ${result.error.value}` : "unknown error";
|
|
105
140
|
throw new Error(`Failed to load Python module ${module.path}: ${details}`);
|
|
106
141
|
}
|
|
@@ -76,7 +76,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
76
76
|
image.data,
|
|
77
77
|
image.mimeType,
|
|
78
78
|
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
79
|
-
{ maxWidthCells:
|
|
79
|
+
{ maxWidthCells: settings.get("tui.maxInlineImageColumns") },
|
|
80
80
|
),
|
|
81
81
|
);
|
|
82
82
|
continue;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type TUI,
|
|
15
15
|
} from "@oh-my-pi/pi-tui";
|
|
16
16
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { settings } from "../../config/settings";
|
|
17
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
18
19
|
import { theme } from "../../modes/theme/theme";
|
|
19
20
|
import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../patch";
|
|
@@ -530,7 +531,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
530
531
|
imageData,
|
|
531
532
|
imageMimeType,
|
|
532
533
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
533
|
-
{ maxWidthCells:
|
|
534
|
+
{ maxWidthCells: settings.get("tui.maxInlineImageColumns") },
|
|
534
535
|
);
|
|
535
536
|
this.#imageComponents.push(imageComponent);
|
|
536
537
|
this.addChild(imageComponent);
|
|
@@ -123,6 +123,7 @@ export class ExtensionUiController {
|
|
|
123
123
|
};
|
|
124
124
|
const contextActions: ExtensionContextActions = {
|
|
125
125
|
getModel: () => this.ctx.session.model,
|
|
126
|
+
getSearchDb: () => this.ctx.session.searchDb,
|
|
126
127
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
127
128
|
abort: () => this.ctx.session.abort(),
|
|
128
129
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -384,6 +385,7 @@ export class ExtensionUiController {
|
|
|
384
385
|
};
|
|
385
386
|
const contextActions: ExtensionContextActions = {
|
|
386
387
|
getModel: () => this.ctx.session.model,
|
|
388
|
+
getSearchDb: () => this.ctx.session.searchDb,
|
|
387
389
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
388
390
|
abort: () => this.ctx.session.abort(),
|
|
389
391
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -581,6 +583,7 @@ export class ExtensionUiController {
|
|
|
581
583
|
sessionManager: this.ctx.session.sessionManager,
|
|
582
584
|
modelRegistry: this.ctx.session.modelRegistry,
|
|
583
585
|
model: this.ctx.session.model,
|
|
586
|
+
searchDb: this.ctx.session.searchDb,
|
|
584
587
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
585
588
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
586
589
|
hasQueuedMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
@@ -550,6 +550,7 @@ export class InputController {
|
|
|
550
550
|
return createPromptActionAutocompleteProvider({
|
|
551
551
|
commands,
|
|
552
552
|
basePath,
|
|
553
|
+
searchDb: this.ctx.session.searchDb,
|
|
553
554
|
keybindings: this.ctx.keybindings,
|
|
554
555
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
555
556
|
copyPrompt: () => this.handleCopyPrompt(),
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -76,6 +76,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
76
76
|
// ExtensionContextActions
|
|
77
77
|
{
|
|
78
78
|
getModel: () => session.model,
|
|
79
|
+
getSearchDb: () => session.searchDb,
|
|
79
80
|
isIdle: () => !session.isStreaming,
|
|
80
81
|
abort: () => session.abort(),
|
|
81
82
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
1
2
|
import {
|
|
2
3
|
type AutocompleteItem,
|
|
3
4
|
type AutocompleteProvider,
|
|
@@ -23,6 +24,7 @@ interface PromptActionAutocompleteItem extends AutocompleteItem {
|
|
|
23
24
|
interface PromptActionAutocompleteOptions {
|
|
24
25
|
commands: SlashCommand[];
|
|
25
26
|
basePath: string;
|
|
27
|
+
searchDb?: SearchDb;
|
|
26
28
|
keybindings: KeybindingsManager;
|
|
27
29
|
copyCurrentLine: () => void;
|
|
28
30
|
copyPrompt: () => void;
|
|
@@ -90,8 +92,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
90
92
|
#baseProvider: CombinedAutocompleteProvider;
|
|
91
93
|
#actions: PromptActionDefinition[];
|
|
92
94
|
|
|
93
|
-
constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
|
|
94
|
-
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
|
|
95
|
+
constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[], searchDb?: SearchDb) {
|
|
96
|
+
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath, searchDb);
|
|
95
97
|
this.#actions = actions;
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -227,5 +229,5 @@ export function createPromptActionAutocompleteProvider(
|
|
|
227
229
|
},
|
|
228
230
|
];
|
|
229
231
|
|
|
230
|
-
return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
|
|
232
|
+
return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions, options.searchDb);
|
|
231
233
|
}
|
|
@@ -404,6 +404,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
404
404
|
// ExtensionContextActions
|
|
405
405
|
{
|
|
406
406
|
getModel: () => session.agent.state.model,
|
|
407
|
+
getSearchDb: () => session.searchDb,
|
|
407
408
|
isIdle: () => !session.isStreaming,
|
|
408
409
|
abort: () => session.abort(),
|
|
409
410
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
@@ -2,7 +2,7 @@ Performs structural AST-aware rewrites via native ast-grep.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Use for codemods and structural rewrites where plain text replace is unsafe
|
|
5
|
-
- Narrow scope with `path` before replacing (`path` accepts files, directories, glob patterns, or comma
|
|
5
|
+
- Narrow scope with `path` before replacing (`path` accepts files, directories, glob patterns, or comma-separated path lists; use `glob` for an additional filter relative to `path`)
|
|
6
6
|
- Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path`/`glob` narrow
|
|
7
7
|
- Treat parse issues as a scoping or pattern-shape signal: tighten `path`/`lang`, or rewrite the pattern into valid syntax before retrying
|
|
8
8
|
- Metavariables captured in each rewrite pattern (`$A`, `$$$ARGS`) are substituted into that entry's rewrite template
|
|
@@ -2,7 +2,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Use this when syntax shape matters more than raw text (calls, declarations, specific language constructs)
|
|
5
|
-
- Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, glob patterns, or comma
|
|
5
|
+
- Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, glob patterns, or comma-separated path lists; use `glob` for an additional filter relative to `path`)
|
|
6
6
|
- Default to language-scoped search in mixed repositories: pair `path` + `glob` + explicit `lang` to avoid parse-noise from non-source files
|
|
7
7
|
- `pat` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
|
|
8
8
|
- Multiple patterns run in one native pass; results are merged and then `offset`/`limit` are applied to the combined match set
|
|
@@ -2,7 +2,7 @@ Finds files using fast pattern matching that works with any codebase size.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Pattern includes the search path: `src/**/*.ts`, `lib/*.json`, `**/*.md`
|
|
5
|
-
- You may provide comma
|
|
5
|
+
- You may provide comma-separated path lists, for example `apps/,packages/,phases/`
|
|
6
6
|
- Simple patterns like `*.ts` automatically search recursively from cwd
|
|
7
7
|
- Includes hidden files by default (use `hidden: false` to exclude)
|
|
8
8
|
- You **SHOULD** perform multiple searches in parallel when potentially useful
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Searches files using powerful regex matching
|
|
1
|
+
Searches files using powerful regex matching.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
|
|
5
|
-
- `path` may be a file, directory, glob path, or comma
|
|
5
|
+
- `path` may be a file, directory, glob path, or comma-separated path list; pair it with `glob` when you need an additional relative file filter
|
|
6
6
|
- Filter files with `glob` (e.g., `*.js`, `**/*.tsx`) or `type` (e.g., `js`, `py`, `rust`)
|
|
7
7
|
- Respects `.gitignore` by default; set `gitignore: false` to include ignored files
|
|
8
8
|
- For cross-line patterns like `struct \\{[\\s\\S]*?field`, set `multiline: true` if needed
|