@oh-my-pi/pi-coding-agent 12.17.2 → 12.18.1

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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.18.1] - 2026-02-21
6
+ ### Added
7
+
8
+ - Added Buffer.toBase64() polyfill for Bun compatibility to enable base64 encoding of buffers
9
+
10
+ ## [12.18.0] - 2026-02-21
11
+
12
+ ### Added
13
+
14
+ - Added `overlay` option to custom UI hooks to display components as bottom-centered overlays instead of replacing the editor
15
+ - Added automatic chat transcript rebuild when returning from custom or debug UI to prevent message duplication
16
+
17
+ ### Changed
18
+
19
+ - Changed custom UI hook cleanup to conditionally restore editor state only when not using overlay mode
20
+ - Extracted environment variable configuration for non-interactive bash execution into reusable `NO_PAGER_ENV` constant
21
+ - Replaced custom timing instrumentation with logger.timeAsync() and logger.time() from pi-utils for consistent startup profiling
22
+ - Removed PI_DEBUG_STARTUP environment variable in favor of logger.debug() for conditional debug output
23
+ - Consolidated timing calls throughout initialization pipeline to use unified logger-based timing system
24
+
25
+ ### Removed
26
+
27
+ - Deleted utils/timings.ts module - timing functionality now provided by pi-utils logger
28
+
29
+ ### Fixed
30
+
31
+ - Fixed potential race condition in bash interactive component where output could be appended after the component was closed
32
+
5
33
  ## [12.17.2] - 2026-02-21
6
34
  ### Changed
7
35
 
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.17.2",
4
+ "version": "12.18.1",
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.17.2",
45
- "@oh-my-pi/pi-agent-core": "12.17.2",
46
- "@oh-my-pi/pi-ai": "12.17.2",
47
- "@oh-my-pi/pi-natives": "12.17.2",
48
- "@oh-my-pi/pi-tui": "12.17.2",
49
- "@oh-my-pi/pi-utils": "12.17.2",
44
+ "@oh-my-pi/omp-stats": "12.18.1",
45
+ "@oh-my-pi/pi-agent-core": "12.18.1",
46
+ "@oh-my-pi/pi-ai": "12.18.1",
47
+ "@oh-my-pi/pi-natives": "12.18.1",
48
+ "@oh-my-pi/pi-tui": "12.18.1",
49
+ "@oh-my-pi/pi-utils": "12.18.1",
50
50
  "@sinclair/typebox": "^0.34.48",
51
51
  "@xterm/headless": "^6.0.0",
52
52
  "ajv": "^8.18.0",
@@ -8,12 +8,9 @@
8
8
  */
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
- import { $env } from "@oh-my-pi/pi-utils";
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
- debugStartup(`capability:${capability.id}:${provider.id}:start`);
119
- const result = await provider.load(ctx);
120
- debugStartup(`capability:${capability.id}:${provider.id}:done`);
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
- debugStartup(`capability:${capability.id}:${provider.id}:error`);
120
+ logger.debug(`capability:${capability.id}:${provider.id}:error`);
124
121
  return { provider, error };
125
122
  }
126
123
  }),
@@ -1,8 +1,7 @@
1
1
  import * as path from "node:path";
2
- import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
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
- debugStartup("warmPython:ensureKernel:start");
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
- debugStartup("warmPython:withKernelSession:start");
259
- const docs = await withKernelSession(
260
- resolvedSessionId,
261
- cwd,
262
- async kernel => kernel.introspectPrelude(),
263
- useSharedGateway,
264
- sessionFile,
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
- debugStartup("kernel:PythonKernel.start:start");
336
- kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
337
- debugStartup("kernel:PythonKernel.start:done");
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 createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, artifactsDir);
423
- debugStartup("kernel:withSession:created");
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 restartKernelSession(session!, cwd, useSharedGateway, sessionFile, artifactsDir);
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
- debugStartup("kernel:withSession:handler:start");
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 restartKernelSession(session!, cwd, useSharedGateway, sessionFile, artifactsDir);
445
- const result = await handler(session!.kernel);
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
- time("acquireSharedGateway:lockAcquired");
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 startGatewayProcess(cwd);
338
- time("acquireSharedGateway:startGateway");
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
- debugStartup("PythonKernel.start:entry");
338
- const availability = await checkPythonKernelAvailability(options.cwd);
339
- debugStartup("PythonKernel.start:availCheck");
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
- debugStartup("PythonKernel.start:acquireShared:start");
357
- const sharedResult = await acquireSharedGateway(options.cwd);
358
- debugStartup("PythonKernel.start:acquireShared:done");
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
- debugStartup("PythonKernel.start:startShared:start");
364
- const kernel = await PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env);
365
- debugStartup("PythonKernel.start:startShared:done");
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
- debugStartup("PythonKernel.start:sharedFailed");
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
- debugStartup("sharedGateway:fetch:start");
440
- const createResponse = await fetch(`${gatewayUrl}/api/kernels`, {
441
- method: "POST",
442
- headers: { "Content-Type": "application/json" },
443
- body: JSON.stringify({ name: "python3" }),
444
- });
445
- debugStartup("sharedGateway:fetch:done");
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
- debugStartup(`sharedGateway:fetch:notOk:${createResponse.status}`);
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
- debugStartup("sharedGateway:json:start");
461
- const kernelInfo = (await createResponse.json()) as { id: string };
462
- debugStartup("sharedGateway:json:done");
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
- debugStartup("sharedGateway:connectWS:start");
470
- await kernel.#connectWebSocket();
471
- debugStartup("sharedGateway:connectWS:done");
472
- time("startWithSharedGateway:connectWS");
473
- debugStartup("sharedGateway:initEnv:start");
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
- debugStartup("sharedGateway:loadModules:start");
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
- time("start");
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
- debugStartup("main:parseArgs");
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 discoverAuthStorage();
517
- const modelRegistry = new ModelRegistry(authStorage);
518
- const refreshStrategy = parsedArgs.listModels !== undefined ? "online" : "online-if-uncached";
519
- await modelRegistry.refresh(refreshStrategy);
520
- debugStartup("main:discoverModels");
521
- time("discoverModels");
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 pipedInput = await readPipedInput();
562
- let { initialMessage, initialImages } = await prepareInitialMessage(parsedArgs, settings.get("images.autoResize"));
563
- if (pipedInput) {
564
- initialMessage = initialMessage ? `${initialMessage}\n${pipedInput}` : pipedInput;
565
- }
566
- time("prepareInitialMessage");
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
- isInteractive,
589
- settings.get("symbolPreset"),
590
- settings.get("colorBlindMode"),
591
- settings.get("theme.dark"),
592
- settings.get("theme.light"),
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(modelPatterns, modelRegistry, modelMatchPreferences);
604
- time("resolveModelScope");
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(cwd, parsedArgs.sessionDir);
615
- time("SessionManager.list");
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
- time("buildSessionOptions");
654
- const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } =
655
- await createAgentSession(sessionOptions);
656
- debugStartup("main:createAgentSession");
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
- printTimings();
743
- debugStartup("main:runInteractiveMode:start");
734
+ if ($env.PI_TIMING === "1") {
735
+ logger.printTimings();
736
+ }
737
+
738
+ logger.endTiming();
744
739
  await runInteractiveMode(
745
740
  session,
746
741
  VERSION,