@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.
@@ -48,7 +48,7 @@ export class ExtensionUiController {
48
48
  setWorkingMessage: message => this.ctx.setWorkingMessage(message),
49
49
  setWidget: (key, content) => this.setHookWidget(key, content),
50
50
  setTitle: title => setTerminalTitle(title),
51
- custom: (factory, _options) => this.showHookCustom(factory),
51
+ custom: (factory, options) => this.showHookCustom(factory, options),
52
52
  setEditorText: text => this.ctx.editor.setText(text),
53
53
  pasteToEditor: text => {
54
54
  this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
@@ -702,25 +702,47 @@ export class ExtensionUiController {
702
702
  keybindings: KeybindingsManager,
703
703
  done: (result: T) => void,
704
704
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
705
+ options?: { overlay?: boolean },
705
706
  ): Promise<T> {
706
707
  const savedText = this.ctx.editor.getText();
707
708
  const keybindings = KeybindingsManager.inMemory();
708
709
 
709
710
  const { promise, resolve } = Promise.withResolvers<T>();
710
- let component: Component & { dispose?(): void };
711
+ let component: (Component & { dispose?(): void }) | undefined;
712
+ let overlayHandle: OverlayHandle | undefined;
713
+ let closed = false;
711
714
 
712
715
  const close = (result: T) => {
713
- component.dispose?.();
714
- this.ctx.editorContainer.clear();
715
- this.ctx.editorContainer.addChild(this.ctx.editor);
716
- this.ctx.editor.setText(savedText);
716
+ if (closed) return;
717
+ closed = true;
718
+ component?.dispose?.();
719
+ overlayHandle?.hide();
720
+ overlayHandle = undefined;
721
+ if (!options?.overlay) {
722
+ this.ctx.editorContainer.clear();
723
+ this.ctx.editorContainer.addChild(this.ctx.editor);
724
+ this.ctx.editor.setText(savedText);
725
+ }
717
726
  this.ctx.ui.setFocus(this.ctx.editor);
718
727
  this.ctx.ui.requestRender();
719
728
  resolve(result);
720
729
  };
721
730
 
722
731
  Promise.try(() => factory(this.ctx.ui, theme, keybindings, close)).then(c => {
732
+ if (closed) {
733
+ c.dispose?.();
734
+ return;
735
+ }
723
736
  component = c;
737
+ if (options?.overlay) {
738
+ overlayHandle = this.ctx.ui.showOverlay(component, {
739
+ anchor: "bottom-center",
740
+ width: "100%",
741
+ maxHeight: "100%",
742
+ margin: 0,
743
+ });
744
+ return;
745
+ }
724
746
  this.ctx.editorContainer.clear();
725
747
  this.ctx.editorContainer.addChild(component);
726
748
  this.ctx.ui.setFocus(component);
@@ -15,7 +15,7 @@ import {
15
15
  Text,
16
16
  TUI,
17
17
  } from "@oh-my-pi/pi-tui";
18
- import { $env, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
18
+ import { hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
19
19
  import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
20
20
  import chalk from "chalk";
21
21
  import { KeybindingsManager } from "../config/keybindings";
@@ -57,9 +57,6 @@ import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/
57
57
  import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types";
58
58
  import { UiHelpers } from "./utils/ui-helpers";
59
59
 
60
- /** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
61
- const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
62
-
63
60
  const TODO_FILE_NAME = "todos.json";
64
61
  const EDITOR_MAX_HEIGHT_MIN = 6;
65
62
  const EDITOR_MAX_HEIGHT_MAX = 18;
@@ -258,28 +255,29 @@ export class InteractiveMode implements InteractiveModeContext {
258
255
 
259
256
  async init(): Promise<void> {
260
257
  if (this.isInitialized) return;
261
- debugStartup("InteractiveMode.init:entry");
262
258
 
263
- this.keybindings = await KeybindingsManager.create();
264
- debugStartup("InteractiveMode.init:keybindings");
259
+ this.keybindings = await logger.timeAsync("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
265
260
 
266
261
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
267
262
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
268
- debugStartup("InteractiveMode.init:cleanupRegistered");
269
263
 
270
- await this.refreshSlashCommandState(getProjectDir());
271
- debugStartup("InteractiveMode.init:slashCommands");
264
+ await logger.timeAsync("InteractiveMode.init:slashCommands", () =>
265
+ this.refreshSlashCommandState(getProjectDir()),
266
+ );
272
267
 
273
268
  // Get current model info for welcome screen
274
269
  const modelName = this.session.model?.name ?? "Unknown";
275
270
  const providerName = this.session.model?.provider ?? "Unknown";
276
271
 
277
272
  // Get recent sessions
278
- const recentSessions = (await getRecentSessions(this.sessionManager.getSessionDir())).map(s => ({
279
- name: s.name,
280
- timeAgo: s.timeAgo,
281
- }));
282
- debugStartup("InteractiveMode.init:recentSessions");
273
+ const recentSessions = await logger.timeAsync("InteractiveMode.init:recentSessions", () =>
274
+ getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
275
+ sessions.map(s => ({
276
+ name: s.name,
277
+ timeAgo: s.timeAgo,
278
+ })),
279
+ ),
280
+ );
283
281
 
284
282
  // Convert LSP servers to welcome format
285
283
  const lspServerInfo =
@@ -293,9 +291,7 @@ export class InteractiveMode implements InteractiveModeContext {
293
291
 
294
292
  if (!startupQuiet) {
295
293
  // Add welcome header
296
- debugStartup("InteractiveMode.init:welcomeComponent:start");
297
294
  const welcome = new WelcomeComponent(this.#version, modelName, providerName, recentSessions, lspServerInfo);
298
- debugStartup("InteractiveMode.init:welcomeComponent:created");
299
295
 
300
296
  // Setup UI layout
301
297
  this.ui.addChild(new Spacer(1));
@@ -1248,8 +1244,9 @@ export class InteractiveMode implements InteractiveModeContext {
1248
1244
  keybindings: KeybindingsManager,
1249
1245
  done: (result: T) => void,
1250
1246
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
1247
+ options?: { overlay?: boolean },
1251
1248
  ): Promise<T> {
1252
- return this.#extensionUiController.showHookCustom(factory);
1249
+ return this.#extensionUiController.showHookCustom(factory, options);
1253
1250
  }
1254
1251
 
1255
1252
  showExtensionError(extensionPath: string, error: string): void {
@@ -215,6 +215,7 @@ export interface InteractiveModeContext {
215
215
  keybindings: KeybindingsManager,
216
216
  done: (result: T) => void,
217
217
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
218
+ options?: { overlay?: boolean },
218
219
  ): Promise<T>;
219
220
  showExtensionError(extensionPath: string, error: string): void;
220
221
  showToolError(toolName: string, error: string): void;
@@ -278,6 +278,13 @@ export class UiHelpers {
278
278
  }
279
279
 
280
280
  renderInitialMessages(): void {
281
+ // This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI).
282
+ // Clear existing rendered chat first to avoid duplicating the full session in the container.
283
+ this.ctx.chatContainer.clear();
284
+ this.ctx.pendingMessagesContainer.clear();
285
+ this.ctx.pendingBashComponents = [];
286
+ this.ctx.pendingPythonComponents = [];
287
+
281
288
  // Get aligned messages and entries from session context
282
289
  const context = this.ctx.sessionManager.buildSessionContext();
283
290
  this.ctx.renderSessionContext(context, {
package/src/sdk.ts CHANGED
@@ -83,10 +83,6 @@ import {
83
83
  import { ToolContextStore } from "./tools/context";
84
84
  import { getGeminiImageTools } from "./tools/gemini-image";
85
85
  import { EventBus } from "./utils/event-bus";
86
- import { time } from "./utils/timings";
87
-
88
- /** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
89
- const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
90
86
 
91
87
  // Types
92
88
  export interface CreateAgentSessionOptions {
@@ -540,7 +536,6 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
540
536
  * ```
541
537
  */
542
538
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
543
- debugStartup("sdk:createAgentSession:entry");
544
539
  const cwd = options.cwd ?? getProjectDir();
545
540
  const agentDir = options.agentDir ?? getDefaultAgentDir();
546
541
  const eventBus = options.eventBus ?? new EventBus();
@@ -549,17 +544,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
549
544
  registerPythonCleanup();
550
545
 
551
546
  // Use provided or create AuthStorage and ModelRegistry
552
- const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
553
- const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
554
- if (!options.modelRegistry) {
555
- await modelRegistry.refresh();
556
- }
557
- time("discoverModels");
547
+ const { authStorage, modelRegistry } = await logger.timeAsync("discoverModels", async () => {
548
+ const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
549
+ const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
550
+ if (!options.modelRegistry) {
551
+ await modelRegistry.refresh();
552
+ }
553
+ return { authStorage, modelRegistry };
554
+ });
558
555
 
559
- const settings = options.settings ?? (await Settings.init({ cwd, agentDir }));
560
- time("settings");
561
- initializeWithSettings(settings);
562
- time("initializeWithSettings");
556
+ const settings = await logger.timeAsync(
557
+ "settings",
558
+ async () => options.settings ?? (await Settings.init({ cwd, agentDir })),
559
+ );
560
+ logger.time("initializeWithSettings", initializeWithSettings, settings);
563
561
  const skillsSettings = settings.getGroup("skills") as SkillsSettings;
564
562
  const discoveredSkillsPromise =
565
563
  options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
@@ -568,8 +566,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
568
566
  setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
569
567
  setPreferredImageProvider(settings.get("providers.image") ?? "auto");
570
568
 
571
- const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
572
- time("sessionManager");
569
+ const sessionManager = options.sessionManager ?? logger.time("sessionManager", SessionManager.create, cwd);
573
570
  const sessionId = sessionManager.getSessionId();
574
571
  const modelApiKeyAvailability = new Map<string, boolean>();
575
572
  const getModelAvailabilityKey = (candidate: Model): string =>
@@ -587,8 +584,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
587
584
  };
588
585
 
589
586
  // Check if session has existing data to restore
590
- const existingSession = sessionManager.buildSessionContext();
591
- time("loadSession");
587
+ const existingSession = logger.time("loadSession", () => sessionManager.buildSessionContext());
592
588
  const hasExistingSession = existingSession.messages.length > 0;
593
589
  const hasThinkingEntry = sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
594
590
 
@@ -667,46 +663,52 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
667
663
  skills = options.skills;
668
664
  skillWarnings = [];
669
665
  } else {
670
- const discovered = discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] };
671
- time("discoverSkills");
666
+ const discovered = await logger.timeAsync("discoverSkills", async () =>
667
+ discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] },
668
+ );
672
669
  skills = discovered.skills;
673
670
  skillWarnings = discovered.warnings;
674
671
  }
675
672
 
676
- debugStartup("sdk:discoverSkills");
677
-
678
673
  // Discover rules
679
- const ttsrSettings = settings.getGroup("ttsr");
680
- const ttsrManager = new TtsrManager(ttsrSettings);
681
- const rulesResult =
682
- options.rules !== undefined
683
- ? { items: options.rules, warnings: undefined }
684
- : await loadCapability<Rule>(ruleCapability.id, { cwd });
685
- const registeredTtsrRuleNames = new Set<string>();
686
- for (const rule of rulesResult.items) {
687
- if (rule.condition && rule.condition.length > 0) {
688
- if (ttsrManager.addRule(rule)) {
689
- registeredTtsrRuleNames.add(rule.name);
674
+ const { ttsrManager, rulesResult, registeredTtsrRuleNames } = await logger.timeAsync(
675
+ "discoverTtsrRules",
676
+ async () => {
677
+ const ttsrSettings = settings.getGroup("ttsr");
678
+ const ttsrManager = new TtsrManager(ttsrSettings);
679
+ const rulesResult =
680
+ options.rules !== undefined
681
+ ? { items: options.rules, warnings: undefined }
682
+ : await loadCapability<Rule>(ruleCapability.id, { cwd });
683
+ const registeredTtsrRuleNames = new Set<string>();
684
+ for (const rule of rulesResult.items) {
685
+ if (rule.condition && rule.condition.length > 0) {
686
+ if (ttsrManager.addRule(rule)) {
687
+ registeredTtsrRuleNames.add(rule.name);
688
+ }
689
+ }
690
690
  }
691
- }
692
- }
693
- if (existingSession.injectedTtsrRules.length > 0) {
694
- ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
695
- }
696
- time("discoverTtsrRules");
691
+ if (existingSession.injectedTtsrRules.length > 0) {
692
+ ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
693
+ }
694
+ return { ttsrManager, rulesResult, registeredTtsrRuleNames };
695
+ },
696
+ );
697
697
 
698
698
  // Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
699
- const rulebookRules = rulesResult.items.filter((rule: Rule) => {
700
- if (registeredTtsrRuleNames.has(rule.name)) return false;
701
- if (rule.alwaysApply) return false;
702
- if (!rule.description) return false;
703
- return true;
704
- });
705
- time("filterRulebookRules");
699
+ const rulebookRules = logger.time("filterRulebookRules", () =>
700
+ rulesResult.items.filter((rule: Rule) => {
701
+ if (registeredTtsrRuleNames.has(rule.name)) return false;
702
+ if (rule.alwaysApply) return false;
703
+ if (!rule.description) return false;
704
+ return true;
705
+ }),
706
+ );
706
707
 
707
- const contextFiles = options.contextFiles ?? (await discoverContextFiles(cwd, agentDir));
708
- debugStartup("sdk:discoverContextFiles");
709
- time("discoverContextFiles");
708
+ const contextFiles = await logger.timeAsync(
709
+ "discoverContextFiles",
710
+ async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
711
+ );
710
712
 
711
713
  let agent: Agent;
712
714
  let session: AgentSession;
@@ -782,37 +784,30 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
782
784
  options.parentTaskPrefix ? { parentPrefix: options.parentTaskPrefix } : undefined,
783
785
  );
784
786
 
785
- debugStartup("sdk:createTools:start");
786
787
  // Create built-in tools (already wrapped with meta notice formatting)
787
- const builtinTools = await createTools(toolSession, options.toolNames);
788
- debugStartup("sdk:createTools");
789
- time("createAllTools");
788
+ const builtinTools = await logger.timeAsync("createAllTools", () => createTools(toolSession, options.toolNames));
790
789
 
791
- debugStartup("sdk:discoverMCP:start");
792
790
  // Discover MCP tools from .mcp.json files
793
791
  let mcpManager: MCPManager | undefined;
794
792
  const enableMCP = options.enableMCP ?? true;
795
793
  const customTools: CustomTool[] = [];
796
794
  if (enableMCP) {
797
- const mcpResult = await discoverAndLoadMCPTools(cwd, {
798
- onConnecting: serverNames => {
799
- if (options.hasUI && serverNames.length > 0) {
800
- process.stderr.write(
801
- chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}
802
- `),
803
- );
804
- }
805
- },
806
- enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
807
- // Always filter Exa - we have native integration
808
- filterExa: true,
809
- // Filter browser MCP servers when builtin browser tool is active
810
- filterBrowser: (settings.get("browser.enabled") as boolean) ?? false,
811
- cacheStorage: settings.getStorage(),
812
- authStorage,
813
- });
814
- time("discoverAndLoadMCPTools");
815
- debugStartup("sdk:discoverAndLoadMCPTools");
795
+ const mcpResult = await logger.timeAsync("discoverAndLoadMCPTools", () =>
796
+ discoverAndLoadMCPTools(cwd, {
797
+ onConnecting: serverNames => {
798
+ if (options.hasUI && serverNames.length > 0) {
799
+ process.stderr.write(`${chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…`)}\n`);
800
+ }
801
+ },
802
+ enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
803
+ // Always filter Exa - we have native integration
804
+ filterExa: true,
805
+ // Filter browser MCP servers when builtin browser tool is active
806
+ filterBrowser: (settings.get("browser.enabled") as boolean) ?? false,
807
+ cacheStorage: settings.getStorage(),
808
+ authStorage,
809
+ }),
810
+ );
816
811
  mcpManager = mcpResult.manager;
817
812
  toolSession.mcpManager = mcpManager;
818
813
 
@@ -832,18 +827,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
832
827
  }
833
828
  }
834
829
 
835
- debugStartup("sdk:geminiImageTools:start");
836
830
  // Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
837
- const geminiImageTools = await getGeminiImageTools();
831
+ const geminiImageTools = await logger.timeAsync("getGeminiImageTools", getGeminiImageTools);
838
832
  if (geminiImageTools.length > 0) {
839
833
  customTools.push(...(geminiImageTools as unknown as CustomTool[]));
840
834
  }
841
- time("getGeminiImageTools");
842
835
 
843
836
  // Add specialized Exa web search tools if EXA_API_KEY is available
844
837
  const exaSettings = settings.getGroup("exa");
845
838
  if (exaSettings.enabled && exaSettings.enableSearch) {
846
- const exaSearchTools = await getSearchTools({
839
+ const exaSearchTools = await logger.timeAsync("getSearchTools", getSearchTools, {
847
840
  enableLinkedin: exaSettings.enableLinkedin as boolean,
848
841
  enableCompany: exaSettings.enableCompany as boolean,
849
842
  });
@@ -852,34 +845,34 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
852
845
  if (specializedTools.length > 0) {
853
846
  customTools.push(...specializedTools);
854
847
  }
855
- time("getSearchTools");
856
848
  }
857
849
 
858
- debugStartup("sdk:discoverCustomTools:start");
859
850
  // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
860
851
  const builtInToolNames = builtinTools.map(t => t.name);
861
- const discoveredCustomTools = await discoverAndLoadCustomTools([], cwd, builtInToolNames);
852
+ const discoveredCustomTools = await logger.timeAsync(
853
+ "discoverAndLoadCustomTools",
854
+ discoverAndLoadCustomTools,
855
+ [],
856
+ cwd,
857
+ builtInToolNames,
858
+ );
862
859
  for (const { path, error } of discoveredCustomTools.errors) {
863
860
  logger.error("Custom tool load failed", { path, error });
864
861
  }
865
862
  if (discoveredCustomTools.tools.length > 0) {
866
863
  customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool));
867
864
  }
868
- time("discoverAndLoadCustomTools");
869
- debugStartup("sdk:discoverCustomTools:done");
870
865
 
871
866
  const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
872
867
  if (customTools.length > 0) {
873
868
  inlineExtensions.push(createCustomToolsExtension(customTools));
874
869
  }
875
870
 
876
- debugStartup("sdk:loadExtensions:start");
877
871
  // Load extensions (discovers from standard locations + configured paths)
878
872
  let extensionsResult: LoadExtensionsResult;
879
873
  if (options.disableExtensionDiscovery) {
880
874
  const configuredPaths = options.additionalExtensionPaths ?? [];
881
- extensionsResult = await loadExtensions(configuredPaths, cwd, eventBus);
882
- time("loadExtensions");
875
+ extensionsResult = await logger.timeAsync("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
883
876
  for (const { path, error } of extensionsResult.errors) {
884
877
  logger.error("Failed to load extension", { path, error });
885
878
  }
@@ -891,14 +884,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
891
884
  ...(options.additionalExtensionPaths ?? []),
892
885
  ...((settings.get("extensions") as string[]) ?? []),
893
886
  ];
894
- extensionsResult = await discoverAndLoadExtensions(
887
+ extensionsResult = await logger.timeAsync(
888
+ "discoverAndLoadExtensions",
889
+ discoverAndLoadExtensions,
895
890
  configuredPaths,
896
891
  cwd,
897
892
  eventBus,
898
- (settings.get("disabledExtensions") as string[]) ?? [],
899
893
  );
900
- time("discoverAndLoadExtensions");
901
- debugStartup("sdk:discoverAndLoadExtensions");
902
894
  for (const { path, error } of extensionsResult.errors) {
903
895
  logger.error("Failed to load extension", { path, error });
904
896
  }
@@ -959,7 +951,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
959
951
  break;
960
952
  }
961
953
  }
962
- time("findAvailableModel");
963
954
  if (model) {
964
955
  if (modelFallbackMessage) {
965
956
  modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
@@ -970,13 +961,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
970
961
  }
971
962
  }
972
963
 
973
- time("findModel");
974
964
  // Discover custom commands (TypeScript slash commands)
975
965
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
976
966
  ? { commands: [], errors: [] }
977
- : await loadCustomCommandsInternal({ cwd, agentDir });
967
+ : await logger.timeAsync("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
978
968
  if (!options.disableExtensionDiscovery) {
979
- time("discoverCustomCommands");
980
969
  for (const { path, error } of customCommandsResult.errors) {
981
970
  logger.error("Failed to load custom command", { path, error });
982
971
  }
@@ -1050,7 +1039,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1050
1039
  if (model?.provider === "cursor") {
1051
1040
  toolRegistry.delete("edit");
1052
1041
  }
1053
- time("combineTools");
1054
1042
 
1055
1043
  let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
1056
1044
  const cursorExecHandlers = new CursorExecHandlers({
@@ -1117,16 +1105,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1117
1105
  }
1118
1106
  }
1119
1107
 
1120
- const systemPrompt = await rebuildSystemPrompt(initialToolNames, toolRegistry);
1121
- debugStartup("sdk:buildSystemPrompt:done");
1122
- time("buildSystemPrompt");
1108
+ const systemPrompt = await logger.timeAsync(
1109
+ "buildSystemPrompt",
1110
+ rebuildSystemPrompt,
1111
+ initialToolNames,
1112
+ toolRegistry,
1113
+ );
1123
1114
 
1124
- const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
1125
- time("discoverPromptTemplates");
1115
+ const promptTemplates =
1116
+ options.promptTemplates ??
1117
+ (await logger.timeAsync("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1126
1118
  toolSession.promptTemplates = promptTemplates;
1127
1119
 
1128
- const slashCommands = options.slashCommands ?? (await discoverSlashCommands(cwd));
1129
- time("discoverSlashCommands");
1120
+ const slashCommands =
1121
+ options.slashCommands ?? (await logger.timeAsync("discoverSlashCommands", discoverSlashCommands, cwd));
1130
1122
 
1131
1123
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1132
1124
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
@@ -1166,13 +1158,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1166
1158
  // Load and create secret obfuscator if secrets are enabled
1167
1159
  let obfuscator: SecretObfuscator | undefined;
1168
1160
  if (settings.get("secrets.enabled")) {
1169
- const fileEntries = await loadSecrets(cwd, agentDir);
1161
+ const fileEntries = await logger.timeAsync("loadSecrets", loadSecrets, cwd, agentDir);
1170
1162
  const envEntries = collectEnvSecrets();
1171
1163
  const allEntries = [...envEntries, ...fileEntries];
1172
1164
  if (allEntries.length > 0) {
1173
1165
  obfuscator = new SecretObfuscator(allEntries);
1174
1166
  }
1175
- time("loadSecrets");
1176
1167
  }
1177
1168
 
1178
1169
  // Final convertToLlm: chain block-images filter with secret obfuscation
@@ -1230,8 +1221,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1230
1221
  intentTracing: settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1",
1231
1222
  });
1232
1223
  cursorEventEmitter = event => agent.emitExternalEvent(event);
1233
- debugStartup("sdk:createAgent");
1234
- time("createAgent");
1235
1224
 
1236
1225
  // Restore messages if session has existing data
1237
1226
  if (hasExistingSession) {
@@ -1266,20 +1255,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1266
1255
  forceCopilotAgentInitiator,
1267
1256
  obfuscator,
1268
1257
  });
1269
- debugStartup("sdk:createAgentSession");
1270
- time("createAgentSession");
1271
1258
 
1272
1259
  if (model?.api === "openai-codex-responses") {
1273
1260
  try {
1274
- debugStartup("sdk:prewarmCodexWebsocket:start");
1275
- await prewarmOpenAICodexResponses(model, {
1261
+ await logger.timeAsync("prewarmCodexWebsocket", prewarmOpenAICodexResponses, model, {
1276
1262
  apiKey: await modelRegistry.getApiKey(model, sessionId),
1277
1263
  sessionId,
1278
1264
  preferWebsockets: preferOpenAICodexWebsockets,
1279
1265
  providerSessionState: session.providerSessionState,
1280
1266
  });
1281
- debugStartup("sdk:prewarmCodexWebsocket:done");
1282
- time("prewarmCodexWebsocket");
1283
1267
  } catch (error) {
1284
1268
  logger.debug("Codex websocket prewarm failed", {
1285
1269
  error: error instanceof Error ? error.message : String(error),
@@ -1293,17 +1277,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1293
1277
  let lspServers: CreateAgentSessionResult["lspServers"];
1294
1278
  if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1295
1279
  try {
1296
- debugStartup("sdk:warmupLspServers:start");
1297
- const result = await warmupLspServers(cwd, {
1280
+ const result = await logger.timeAsync("warmupLspServers", warmupLspServers, cwd, {
1298
1281
  onConnecting: serverNames => {
1299
1282
  if (options.hasUI && serverNames.length > 0) {
1300
1283
  process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}…\n`));
1301
1284
  }
1302
1285
  },
1303
1286
  });
1304
- debugStartup("sdk:warmupLspServers:done");
1305
1287
  lspServers = result.servers;
1306
- time("warmupLspServers");
1307
1288
  } catch (error) {
1308
1289
  logger.warn("LSP server warmup failed", { cwd, error: String(error) });
1309
1290
  }
@@ -1317,7 +1298,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1317
1298
  taskDepth,
1318
1299
  });
1319
1300
 
1320
- debugStartup("sdk:return");
1321
1301
  return {
1322
1302
  session,
1323
1303
  extensionsResult,