@oh-my-pi/pi-coding-agent 12.17.2 → 12.18.0
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/package.json +7 -7
- package/src/capability/index.ts +5 -8
- package/src/ipy/executor.ts +42 -32
- package/src/ipy/gateway-coordinator.ts +5 -8
- package/src/ipy/kernel.ts +28 -47
- package/src/main.ts +56 -61
- package/src/modes/controllers/extension-ui-controller.ts +28 -6
- package/src/modes/interactive-mode.ts +15 -18
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/sdk.ts +97 -117
- package/src/system-prompt.ts +14 -36
- package/src/tools/bash-interactive.ts +46 -42
- package/src/tools/gemini-image.ts +2 -11
- package/src/tools/index.ts +18 -27
- package/src/web/search/providers/gemini.ts +2 -24
- package/src/utils/timings.ts +0 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.18.0] - 2026-02-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `overlay` option to custom UI hooks to display components as bottom-centered overlays instead of replacing the editor
|
|
10
|
+
- Added automatic chat transcript rebuild when returning from custom or debug UI to prevent message duplication
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Changed custom UI hook cleanup to conditionally restore editor state only when not using overlay mode
|
|
15
|
+
- Extracted environment variable configuration for non-interactive bash execution into reusable `NO_PAGER_ENV` constant
|
|
16
|
+
- Replaced custom timing instrumentation with logger.timeAsync() and logger.time() from pi-utils for consistent startup profiling
|
|
17
|
+
- Removed PI_DEBUG_STARTUP environment variable in favor of logger.debug() for conditional debug output
|
|
18
|
+
- Consolidated timing calls throughout initialization pipeline to use unified logger-based timing system
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- Deleted utils/timings.ts module - timing functionality now provided by pi-utils logger
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed potential race condition in bash interactive component where output could be appended after the component was closed
|
|
27
|
+
|
|
5
28
|
## [12.17.2] - 2026-02-21
|
|
6
29
|
### Changed
|
|
7
30
|
|
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": "12.
|
|
4
|
+
"version": "12.18.0",
|
|
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 Bölük",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "0.6.0",
|
|
44
|
-
"@oh-my-pi/omp-stats": "12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "12.18.0",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "12.18.0",
|
|
46
|
+
"@oh-my-pi/pi-ai": "12.18.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "12.18.0",
|
|
48
|
+
"@oh-my-pi/pi-tui": "12.18.0",
|
|
49
|
+
"@oh-my-pi/pi-utils": "12.18.0",
|
|
50
50
|
"@sinclair/typebox": "^0.34.48",
|
|
51
51
|
"@xterm/headless": "^6.0.0",
|
|
52
52
|
"ajv": "^8.18.0",
|
package/src/capability/index.ts
CHANGED
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
13
13
|
|
|
14
|
-
/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
|
|
15
|
-
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
16
|
-
|
|
17
14
|
import type { Settings } from "../config/settings";
|
|
18
15
|
import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
|
|
19
16
|
import type {
|
|
@@ -115,12 +112,12 @@ async function loadImpl<T>(
|
|
|
115
112
|
const results = await Promise.all(
|
|
116
113
|
providers.map(async provider => {
|
|
117
114
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
const result = await logger.timeAsync(`capability:${capability.id}:${provider.id}`, () =>
|
|
116
|
+
provider.load(ctx),
|
|
117
|
+
);
|
|
121
118
|
return { provider, result };
|
|
122
119
|
} catch (error) {
|
|
123
|
-
|
|
120
|
+
logger.debug(`capability:${capability.id}:${provider.id}:error`);
|
|
124
121
|
return { provider, error };
|
|
125
122
|
}
|
|
126
123
|
}),
|
package/src/ipy/executor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
4
4
|
import { OutputSink } from "../session/streaming-output";
|
|
5
|
-
import { time } from "../utils/timings";
|
|
6
5
|
import { shutdownSharedGateway } from "./gateway-coordinator";
|
|
7
6
|
import {
|
|
8
7
|
checkPythonKernelAvailability,
|
|
@@ -15,8 +14,6 @@ import {
|
|
|
15
14
|
import { discoverPythonModules } from "./modules";
|
|
16
15
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
17
16
|
|
|
18
|
-
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
19
|
-
|
|
20
17
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
18
|
const MAX_KERNEL_SESSIONS = 4;
|
|
22
19
|
const CLEANUP_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
@@ -228,10 +225,7 @@ export async function warmPythonEnvironment(
|
|
|
228
225
|
const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
|
229
226
|
let cacheState: PreludeCacheState | null = null;
|
|
230
227
|
try {
|
|
231
|
-
|
|
232
|
-
await ensureKernelAvailable(cwd);
|
|
233
|
-
debugStartup("warmPython:ensureKernel:done");
|
|
234
|
-
time("warmPython:ensureKernelAvailable");
|
|
228
|
+
await logger.timeAsync("warmPython:ensureKernelAvailable", () => ensureKernelAvailable(cwd));
|
|
235
229
|
} catch (err: unknown) {
|
|
236
230
|
const reason = err instanceof Error ? err.message : String(err);
|
|
237
231
|
cachedPreludeDocs = [];
|
|
@@ -255,16 +249,15 @@ export async function warmPythonEnvironment(
|
|
|
255
249
|
}
|
|
256
250
|
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
|
257
251
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
const docs = await logger.timeAsync("warmPython:withKernelSession", () =>
|
|
253
|
+
withKernelSession(
|
|
254
|
+
resolvedSessionId,
|
|
255
|
+
cwd,
|
|
256
|
+
async kernel => kernel.introspectPrelude(),
|
|
257
|
+
useSharedGateway,
|
|
258
|
+
sessionFile,
|
|
259
|
+
),
|
|
265
260
|
);
|
|
266
|
-
debugStartup("warmPython:withKernelSession:done");
|
|
267
|
-
time("warmPython:withKernelSession");
|
|
268
261
|
cachedPreludeDocs = docs;
|
|
269
262
|
if (!isTestEnv && docs.length > 0) {
|
|
270
263
|
const state = cacheState ?? (await buildPreludeCacheState(cwd));
|
|
@@ -321,7 +314,6 @@ async function createKernelSession(
|
|
|
321
314
|
artifactsDir?: string,
|
|
322
315
|
isRetry?: boolean,
|
|
323
316
|
): Promise<KernelSession> {
|
|
324
|
-
debugStartup("kernel:createSession:entry");
|
|
325
317
|
const env: Record<string, string> | undefined =
|
|
326
318
|
sessionFile || artifactsDir
|
|
327
319
|
? {
|
|
@@ -332,10 +324,9 @@ async function createKernelSession(
|
|
|
332
324
|
|
|
333
325
|
let kernel: PythonKernel;
|
|
334
326
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
time("createKernelSession:PythonKernel.start");
|
|
327
|
+
kernel = await logger.timeAsync("createKernelSession:PythonKernel.start", () =>
|
|
328
|
+
PythonKernel.start({ cwd, useSharedGateway, env }),
|
|
329
|
+
);
|
|
339
330
|
} catch (err) {
|
|
340
331
|
if (!isRetry && isResourceExhaustionError(err)) {
|
|
341
332
|
await recoverFromResourceExhaustion();
|
|
@@ -412,37 +403,56 @@ async function withKernelSession<T>(
|
|
|
412
403
|
sessionFile?: string,
|
|
413
404
|
artifactsDir?: string,
|
|
414
405
|
): Promise<T> {
|
|
415
|
-
debugStartup("kernel:withSession:entry");
|
|
416
406
|
let session = kernelSessions.get(sessionId);
|
|
417
407
|
if (!session) {
|
|
418
408
|
// Evict oldest session if at capacity
|
|
419
409
|
if (kernelSessions.size >= MAX_KERNEL_SESSIONS) {
|
|
420
410
|
await evictOldestSession();
|
|
421
411
|
}
|
|
422
|
-
session = await
|
|
423
|
-
|
|
412
|
+
session = await logger.timeAsync(
|
|
413
|
+
"kernel:createKernelSession",
|
|
414
|
+
createKernelSession,
|
|
415
|
+
sessionId,
|
|
416
|
+
cwd,
|
|
417
|
+
useSharedGateway,
|
|
418
|
+
sessionFile,
|
|
419
|
+
artifactsDir,
|
|
420
|
+
);
|
|
424
421
|
kernelSessions.set(sessionId, session);
|
|
425
422
|
startCleanupTimer();
|
|
426
423
|
}
|
|
427
424
|
|
|
428
425
|
const run = async (): Promise<T> => {
|
|
429
|
-
debugStartup("kernel:withSession:run");
|
|
430
426
|
session!.lastUsedAt = Date.now();
|
|
431
427
|
if (session!.dead || !session!.kernel.isAlive()) {
|
|
432
|
-
await
|
|
428
|
+
await logger.timeAsync(
|
|
429
|
+
"kernel:restartKernelSession",
|
|
430
|
+
restartKernelSession,
|
|
431
|
+
session!,
|
|
432
|
+
cwd,
|
|
433
|
+
useSharedGateway,
|
|
434
|
+
sessionFile,
|
|
435
|
+
artifactsDir,
|
|
436
|
+
);
|
|
433
437
|
}
|
|
434
438
|
try {
|
|
435
|
-
|
|
436
|
-
const result = await handler(session!.kernel);
|
|
437
|
-
debugStartup("kernel:withSession:handler:done");
|
|
439
|
+
const result = await logger.timeAsync("kernel:withSession:handler", handler, session!.kernel);
|
|
438
440
|
session!.restartCount = 0;
|
|
439
441
|
return result;
|
|
440
442
|
} catch (err) {
|
|
441
443
|
if (!session!.dead && session!.kernel.isAlive()) {
|
|
442
444
|
throw err;
|
|
443
445
|
}
|
|
444
|
-
await
|
|
445
|
-
|
|
446
|
+
await logger.timeAsync(
|
|
447
|
+
"kernel:restartKernelSession",
|
|
448
|
+
restartKernelSession,
|
|
449
|
+
session!,
|
|
450
|
+
cwd,
|
|
451
|
+
useSharedGateway,
|
|
452
|
+
sessionFile,
|
|
453
|
+
artifactsDir,
|
|
454
|
+
);
|
|
455
|
+
const result = await logger.timeAsync("kernel:postRestart:handler", handler, session!.kernel);
|
|
446
456
|
session!.restartCount = 0;
|
|
447
457
|
return result;
|
|
448
458
|
}
|
|
@@ -6,7 +6,6 @@ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
|
|
|
6
6
|
import type { Subprocess } from "bun";
|
|
7
7
|
import { Settings } from "../config/settings";
|
|
8
8
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
9
|
-
import { time } from "../utils/timings";
|
|
10
9
|
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
11
10
|
|
|
12
11
|
const GATEWAY_DIR_NAME = "python-gateway";
|
|
@@ -315,12 +314,9 @@ async function killGateway(pid: number, context: string): Promise<void> {
|
|
|
315
314
|
export async function acquireSharedGateway(cwd: string): Promise<AcquireResult | null> {
|
|
316
315
|
try {
|
|
317
316
|
return await withGatewayLock(async () => {
|
|
318
|
-
|
|
319
|
-
const existingInfo = await readGatewayInfo();
|
|
320
|
-
time("acquireSharedGateway:readInfo");
|
|
317
|
+
const existingInfo = await logger.timeAsync("acquireSharedGateway:readInfo", () => readGatewayInfo());
|
|
321
318
|
if (existingInfo) {
|
|
322
|
-
if (await isGatewayAlive(existingInfo)) {
|
|
323
|
-
time("acquireSharedGateway:isAlive");
|
|
319
|
+
if (await logger.timeAsync("acquireSharedGateway:isAlive", () => isGatewayAlive(existingInfo))) {
|
|
324
320
|
localGatewayUrl = existingInfo.url;
|
|
325
321
|
isCoordinatorInitialized = true;
|
|
326
322
|
logger.debug("Reusing global Python gateway", { url: existingInfo.url });
|
|
@@ -334,8 +330,9 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
|
|
|
334
330
|
await clearGatewayInfo();
|
|
335
331
|
}
|
|
336
332
|
|
|
337
|
-
const { url, pid, pythonPath, venvPath } = await
|
|
338
|
-
|
|
333
|
+
const { url, pid, pythonPath, venvPath } = await logger.timeAsync("acquireSharedGateway:startGateway", () =>
|
|
334
|
+
startGatewayProcess(cwd),
|
|
335
|
+
);
|
|
339
336
|
const info: GatewayInfo = {
|
|
340
337
|
url,
|
|
341
338
|
pid,
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { Settings } from "../config/settings";
|
|
4
|
-
import { time } from "../utils/timings";
|
|
5
4
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
6
5
|
import { acquireSharedGateway, releaseSharedGateway, shutdownSharedGateway } from "./gateway-coordinator";
|
|
7
6
|
import { loadPythonModules } from "./modules";
|
|
@@ -13,8 +12,6 @@ const TEXT_DECODER = new TextDecoder();
|
|
|
13
12
|
const TRACE_IPC = $env.PI_PYTHON_IPC_TRACE === "1";
|
|
14
13
|
const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelude_docs__()))";
|
|
15
14
|
|
|
16
|
-
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
17
|
-
|
|
18
15
|
class SharedGatewayCreateError extends Error {
|
|
19
16
|
constructor(
|
|
20
17
|
readonly status: number,
|
|
@@ -334,10 +331,9 @@ export class PythonKernel {
|
|
|
334
331
|
}
|
|
335
332
|
|
|
336
333
|
static async start(options: KernelStartOptions): Promise<PythonKernel> {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
time("PythonKernel.start:availabilityCheck");
|
|
334
|
+
const availability = await logger.timeAsync("PythonKernel.start:availabilityCheck", () =>
|
|
335
|
+
checkPythonKernelAvailability(options.cwd),
|
|
336
|
+
);
|
|
341
337
|
if (!availability.ok) {
|
|
342
338
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
343
339
|
}
|
|
@@ -353,20 +349,18 @@ export class PythonKernel {
|
|
|
353
349
|
|
|
354
350
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
355
351
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
time("PythonKernel.start:acquireSharedGateway");
|
|
352
|
+
const sharedResult = await logger.timeAsync("PythonKernel.start:acquireSharedGateway", () =>
|
|
353
|
+
acquireSharedGateway(options.cwd),
|
|
354
|
+
);
|
|
360
355
|
if (!sharedResult) {
|
|
361
356
|
throw new Error("Shared Python gateway unavailable");
|
|
362
357
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
time("PythonKernel.start:startWithSharedGateway");
|
|
358
|
+
const kernel = await logger.timeAsync("PythonKernel.start:startWithSharedGateway", () =>
|
|
359
|
+
PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env),
|
|
360
|
+
);
|
|
367
361
|
return kernel;
|
|
368
362
|
} catch (err) {
|
|
369
|
-
|
|
363
|
+
logger.debug("PythonKernel.start:sharedFailed");
|
|
370
364
|
if (attempt === 0 && err instanceof SharedGatewayCreateError && err.status >= 500) {
|
|
371
365
|
logger.warn("Shared gateway kernel creation failed, retrying", {
|
|
372
366
|
status: err.status,
|
|
@@ -436,55 +430,42 @@ export class PythonKernel {
|
|
|
436
430
|
cwd: string,
|
|
437
431
|
env?: Record<string, string | undefined>,
|
|
438
432
|
): Promise<PythonKernel> {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
time("startWithSharedGateway:createKernel");
|
|
433
|
+
const createResponse = await logger.timeAsync("startWithSharedGateway:createKernel", () =>
|
|
434
|
+
fetch(`${gatewayUrl}/api/kernels`, {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: { "Content-Type": "application/json" },
|
|
437
|
+
body: JSON.stringify({ name: "python3" }),
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
447
440
|
|
|
448
441
|
if (!createResponse.ok) {
|
|
449
|
-
|
|
442
|
+
logger.debug(`sharedGateway:fetch:notOk:${createResponse.status}`);
|
|
450
443
|
await shutdownSharedGateway();
|
|
451
|
-
debugStartup("sharedGateway:fetch:shutdown");
|
|
452
444
|
const text = await createResponse.text();
|
|
453
|
-
debugStartup("sharedGateway:fetch:textRead");
|
|
454
445
|
throw new SharedGatewayCreateError(
|
|
455
446
|
createResponse.status,
|
|
456
447
|
`Failed to create kernel on shared gateway: ${text}`,
|
|
457
448
|
);
|
|
458
449
|
}
|
|
459
450
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
451
|
+
const kernelInfo = await logger.timeAsync(
|
|
452
|
+
"startWithSharedGateway:parseJson",
|
|
453
|
+
() => createResponse.json() as Promise<{ id: string }>,
|
|
454
|
+
);
|
|
463
455
|
const kernelId = kernelInfo.id;
|
|
464
456
|
|
|
465
457
|
const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
|
|
466
|
-
debugStartup("sharedGateway:kernelCreated");
|
|
467
458
|
|
|
468
459
|
try {
|
|
469
|
-
|
|
470
|
-
await kernel.#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
await kernel.#initializeKernelEnvironment(cwd, env);
|
|
475
|
-
debugStartup("sharedGateway:initEnv:done");
|
|
476
|
-
time("startWithSharedGateway:initEnv");
|
|
477
|
-
debugStartup("sharedGateway:prelude:start");
|
|
478
|
-
const preludeResult = await kernel.execute(PYTHON_PRELUDE, { silent: true, storeHistory: false });
|
|
479
|
-
debugStartup("sharedGateway:prelude:done");
|
|
480
|
-
time("startWithSharedGateway:prelude");
|
|
460
|
+
await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket());
|
|
461
|
+
await logger.timeAsync("startWithSharedGateway:initEnv", () => kernel.#initializeKernelEnvironment(cwd, env));
|
|
462
|
+
const preludeResult = await logger.timeAsync("startWithSharedGateway:prelude", () =>
|
|
463
|
+
kernel.execute(PYTHON_PRELUDE, { silent: true, storeHistory: false }),
|
|
464
|
+
);
|
|
481
465
|
if (preludeResult.cancelled || preludeResult.status === "error") {
|
|
482
466
|
throw new Error("Failed to initialize Python kernel prelude");
|
|
483
467
|
}
|
|
484
|
-
|
|
485
|
-
await loadPythonModules(kernel, { cwd });
|
|
486
|
-
debugStartup("sharedGateway:loadModules:done");
|
|
487
|
-
time("startWithSharedGateway:loadModules");
|
|
468
|
+
await logger.timeAsync("startWithSharedGateway:loadModules", () => loadPythonModules(kernel, { cwd }));
|
|
488
469
|
return kernel;
|
|
489
470
|
} catch (err: unknown) {
|
|
490
471
|
await kernel.shutdown();
|
package/src/main.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as os from "node:os";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { createInterface } from "node:readline/promises";
|
|
13
13
|
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
14
|
-
import { $env, postmortem } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { $env, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { getProjectDir, setProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
|
|
16
16
|
import chalk from "chalk";
|
|
17
17
|
import type { Args } from "./cli/args";
|
|
@@ -32,10 +32,6 @@ import type { AgentSession } from "./session/agent-session";
|
|
|
32
32
|
import { type SessionInfo, SessionManager } from "./session/session-manager";
|
|
33
33
|
import { resolvePromptInput } from "./system-prompt";
|
|
34
34
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
35
|
-
import { printTimings, time } from "./utils/timings";
|
|
36
|
-
|
|
37
|
-
/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
|
|
38
|
-
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
39
35
|
|
|
40
36
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
41
37
|
try {
|
|
@@ -497,28 +493,25 @@ async function buildSessionOptions(
|
|
|
497
493
|
}
|
|
498
494
|
|
|
499
495
|
export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
|
|
500
|
-
|
|
501
|
-
debugStartup("main:entry");
|
|
496
|
+
logger.startTiming();
|
|
502
497
|
|
|
503
498
|
// Initialize theme early with defaults (CLI commands need symbols)
|
|
504
499
|
// Will be re-initialized with user preferences later
|
|
505
|
-
await initTheme();
|
|
506
|
-
debugStartup("main:initTheme");
|
|
500
|
+
await logger.timeAsync("initTheme:initial", () => initTheme());
|
|
507
501
|
|
|
508
502
|
const parsedArgs = parsed;
|
|
509
|
-
|
|
510
|
-
time("parseArgs");
|
|
511
|
-
await maybeAutoChdir(parsedArgs);
|
|
503
|
+
await logger.timeAsync("maybeAutoChdir", () => maybeAutoChdir(parsedArgs));
|
|
512
504
|
|
|
513
505
|
const notifs: (InteractiveModeNotify | null)[] = [];
|
|
514
506
|
|
|
515
507
|
// Create AuthStorage and ModelRegistry upfront
|
|
516
|
-
const authStorage = await
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
508
|
+
const { authStorage, modelRegistry } = await logger.timeAsync("discoverModels", async () => {
|
|
509
|
+
const authStorage = await discoverAuthStorage();
|
|
510
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
511
|
+
const refreshStrategy = parsedArgs.listModels !== undefined ? "online" : "online-if-uncached";
|
|
512
|
+
await modelRegistry.refresh(refreshStrategy);
|
|
513
|
+
return { authStorage, modelRegistry };
|
|
514
|
+
});
|
|
522
515
|
|
|
523
516
|
if (parsedArgs.version) {
|
|
524
517
|
writeStdout(VERSION);
|
|
@@ -551,26 +544,33 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
551
544
|
}
|
|
552
545
|
|
|
553
546
|
const cwd = getProjectDir();
|
|
554
|
-
await Settings.init({ cwd });
|
|
555
|
-
debugStartup("main:Settings.init");
|
|
556
|
-
time("Settings.init");
|
|
547
|
+
await logger.timeAsync("settings:init", () => Settings.init({ cwd }));
|
|
557
548
|
if (parsedArgs.noPty) {
|
|
558
549
|
settings.override("bash.virtualTerminal", "off");
|
|
559
550
|
Bun.env.PI_NO_PTY = "1";
|
|
560
551
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
552
|
+
const {
|
|
553
|
+
pipedInput,
|
|
554
|
+
initialMessage: initMsg,
|
|
555
|
+
initialImages,
|
|
556
|
+
} = await logger.timeAsync("prepareInitialMessage", async () => {
|
|
557
|
+
const pipedInput = await readPipedInput();
|
|
558
|
+
let { initialMessage, initialImages } = await prepareInitialMessage(
|
|
559
|
+
parsedArgs,
|
|
560
|
+
settings.get("images.autoResize"),
|
|
561
|
+
);
|
|
562
|
+
if (pipedInput) {
|
|
563
|
+
initialMessage = initialMessage ? `${initialMessage}\n${pipedInput}` : pipedInput;
|
|
564
|
+
}
|
|
565
|
+
return { pipedInput, initialMessage, initialImages };
|
|
566
|
+
});
|
|
567
|
+
const initialMessage = initMsg;
|
|
567
568
|
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
568
569
|
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
569
570
|
const mode = parsedArgs.mode || "text";
|
|
570
571
|
|
|
571
572
|
// Initialize discovery system with settings for provider persistence
|
|
572
|
-
initializeWithSettings(settings);
|
|
573
|
-
time("initializeWithSettings");
|
|
573
|
+
logger.time("initializeWithSettings", () => initializeWithSettings(settings));
|
|
574
574
|
|
|
575
575
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
576
576
|
const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
|
|
@@ -584,15 +584,15 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
584
584
|
});
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
-
await initTheme(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
587
|
+
await logger.timeAsync("initTheme:final", () =>
|
|
588
|
+
initTheme(
|
|
589
|
+
isInteractive,
|
|
590
|
+
settings.get("symbolPreset"),
|
|
591
|
+
settings.get("colorBlindMode"),
|
|
592
|
+
settings.get("theme.dark"),
|
|
593
|
+
settings.get("theme.light"),
|
|
594
|
+
),
|
|
593
595
|
);
|
|
594
|
-
debugStartup("main:initTheme2");
|
|
595
|
-
time("initTheme");
|
|
596
596
|
|
|
597
597
|
let scopedModels: ScopedModel[] = [];
|
|
598
598
|
const modelPatterns = parsedArgs.models ?? settings.get("enabledModels");
|
|
@@ -600,25 +600,24 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
600
600
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
601
601
|
};
|
|
602
602
|
if (modelPatterns && modelPatterns.length > 0) {
|
|
603
|
-
scopedModels = await resolveModelScope
|
|
604
|
-
|
|
603
|
+
scopedModels = await logger.timeAsync("resolveModelScope", () =>
|
|
604
|
+
resolveModelScope(modelPatterns, modelRegistry, modelMatchPreferences),
|
|
605
|
+
);
|
|
605
606
|
}
|
|
606
607
|
|
|
607
608
|
// Create session manager based on CLI flags
|
|
608
|
-
let sessionManager = await createSessionManager(parsedArgs, cwd);
|
|
609
|
-
debugStartup("main:createSessionManager");
|
|
610
|
-
time("createSessionManager");
|
|
609
|
+
let sessionManager = await logger.timeAsync("createSessionManager", () => createSessionManager(parsedArgs, cwd));
|
|
611
610
|
|
|
612
611
|
// Handle --resume (no value): show session picker
|
|
613
612
|
if (parsedArgs.resume === true) {
|
|
614
|
-
const sessions = await SessionManager.list
|
|
615
|
-
|
|
613
|
+
const sessions = await logger.timeAsync("SessionManager.list", () =>
|
|
614
|
+
SessionManager.list(cwd, parsedArgs.sessionDir),
|
|
615
|
+
);
|
|
616
616
|
if (sessions.length === 0) {
|
|
617
617
|
writeStdout(chalk.dim("No sessions found"));
|
|
618
618
|
return;
|
|
619
619
|
}
|
|
620
|
-
const selectedPath = await selectSession(sessions);
|
|
621
|
-
time("selectSession");
|
|
620
|
+
const selectedPath = await logger.timeAsync("selectSession", () => selectSession(sessions));
|
|
622
621
|
if (!selectedPath) {
|
|
623
622
|
writeStdout(chalk.dim("No session selected"));
|
|
624
623
|
return;
|
|
@@ -626,13 +625,9 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
626
625
|
sessionManager = await SessionManager.open(selectedPath);
|
|
627
626
|
}
|
|
628
627
|
|
|
629
|
-
const { options: sessionOptions, cliThinkingFromModel } = await buildSessionOptions(
|
|
630
|
-
parsedArgs,
|
|
631
|
-
scopedModels,
|
|
632
|
-
sessionManager,
|
|
633
|
-
modelRegistry,
|
|
628
|
+
const { options: sessionOptions, cliThinkingFromModel } = await logger.timeAsync("buildSessionOptions", () =>
|
|
629
|
+
buildSessionOptions(parsedArgs, scopedModels, sessionManager, modelRegistry),
|
|
634
630
|
);
|
|
635
|
-
debugStartup("main:buildSessionOptions");
|
|
636
631
|
sessionOptions.authStorage = authStorage;
|
|
637
632
|
sessionOptions.modelRegistry = modelRegistry;
|
|
638
633
|
sessionOptions.hasUI = isInteractive;
|
|
@@ -650,11 +645,10 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
650
645
|
}
|
|
651
646
|
}
|
|
652
647
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
time("createAgentSession");
|
|
648
|
+
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await logger.timeAsync(
|
|
649
|
+
"createAgentSession",
|
|
650
|
+
() => createAgentSession(sessionOptions),
|
|
651
|
+
);
|
|
658
652
|
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
659
653
|
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
660
654
|
}
|
|
@@ -692,8 +686,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
692
686
|
}
|
|
693
687
|
}
|
|
694
688
|
}
|
|
695
|
-
time("applyExtensionFlags");
|
|
696
|
-
debugStartup("main:applyExtensionFlags");
|
|
697
689
|
|
|
698
690
|
if (!isInteractive && !session.model) {
|
|
699
691
|
if (modelFallbackMessage) {
|
|
@@ -739,8 +731,11 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
739
731
|
writeStdout(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
|
|
740
732
|
}
|
|
741
733
|
|
|
742
|
-
|
|
743
|
-
|
|
734
|
+
if ($env.PI_TIMING === "1") {
|
|
735
|
+
logger.printTimings();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
logger.endTiming();
|
|
744
739
|
await runInteractiveMode(
|
|
745
740
|
session,
|
|
746
741
|
VERSION,
|