@oh-my-pi/pi-coding-agent 12.17.0 → 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/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,
package/src/mcp/config.ts CHANGED
@@ -18,6 +18,8 @@ export interface LoadMCPConfigsOptions {
18
18
  enableProjectConfig?: boolean;
19
19
  /** Whether to filter out Exa MCP servers (default: true) */
20
20
  filterExa?: boolean;
21
+ /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
22
+ filterBrowser?: boolean;
21
23
  }
22
24
 
23
25
  /** Result of loading MCP configs */
@@ -91,6 +93,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
91
93
  export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOptions): Promise<LoadMCPConfigsResult> {
92
94
  const enableProjectConfig = options?.enableProjectConfig ?? true;
93
95
  const filterExa = options?.filterExa ?? true;
96
+ const filterBrowser = options?.filterBrowser ?? false;
94
97
 
95
98
  // Load MCP servers via capability system
96
99
  const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
@@ -103,8 +106,8 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
103
106
  // Load user-level disabled servers list
104
107
  const disabledServers = new Set(await readDisabledServers(getMCPConfigPath("user", cwd)));
105
108
  // Convert to legacy format and preserve source metadata
106
- const configs: Record<string, MCPServerConfig> = {};
107
- const sources: Record<string, SourceMeta> = {};
109
+ let configs: Record<string, MCPServerConfig> = {};
110
+ let sources: Record<string, SourceMeta> = {};
108
111
  for (const server of servers) {
109
112
  const config = convertToLegacyConfig(server);
110
113
  if (config.enabled === false || (server._source.level !== "user" && disabledServers.has(server.name))) {
@@ -114,11 +117,19 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
114
117
  sources[server.name] = server._source;
115
118
  }
116
119
 
117
- const exaApiKeys: string[] = [];
120
+ let exaApiKeys: string[] = [];
118
121
 
119
122
  if (filterExa) {
120
- const filterResult = filterExaMCPServers(configs, sources);
121
- return { configs: filterResult.configs, exaApiKeys: filterResult.exaApiKeys, sources: filterResult.sources };
123
+ const exaResult = filterExaMCPServers(configs, sources);
124
+ configs = exaResult.configs;
125
+ sources = exaResult.sources;
126
+ exaApiKeys = exaResult.exaApiKeys;
127
+ }
128
+
129
+ if (filterBrowser) {
130
+ const browserResult = filterBrowserMCPServers(configs, sources);
131
+ configs = browserResult.configs;
132
+ sources = browserResult.sources;
122
133
  }
123
134
 
124
135
  return { configs, exaApiKeys, sources };
@@ -264,3 +275,89 @@ export function validateServerConfig(name: string, config: MCPServerConfig): str
264
275
 
265
276
  return errors;
266
277
  }
278
+
279
+ /** Known browser automation MCP server names (lowercase) */
280
+ const BROWSER_MCP_NAMES = new Set([
281
+ "puppeteer",
282
+ "playwright",
283
+ "browserbase",
284
+ "browser-tools",
285
+ "browser-use",
286
+ "browser",
287
+ ]);
288
+
289
+ /** Patterns matching browser MCP package names in command/args */
290
+ const BROWSER_MCP_PKG_PATTERN =
291
+ // Official packages
292
+ // - @modelcontextprotocol/server-puppeteer
293
+ // - @playwright/mcp
294
+ // - @browserbasehq/mcp-server-browserbase
295
+ // - @agentdeskai/browser-tools-mcp
296
+ // - @agent-infra/mcp-server-browser
297
+ // Community packages: puppeteer-mcp-server, playwright-mcp, pptr-mcp, etc.
298
+ /(?:@modelcontextprotocol\/server-puppeteer|@playwright\/mcp|@browserbasehq\/mcp-server-browserbase|@agentdeskai\/browser-tools-mcp|@agent-infra\/mcp-server-browser|puppeteer-mcp|playwright-mcp|pptr-mcp|browser-use-mcp|mcp-browser-use)/i;
299
+
300
+ /** URL patterns for hosted browser MCP services */
301
+ const BROWSER_MCP_URL_PATTERN = /browserbase\.com|browser-use\.com/i;
302
+
303
+ /**
304
+ * Check if a server config is a browser automation MCP server.
305
+ */
306
+ export function isBrowserMCPServer(name: string, config: MCPServerConfig): boolean {
307
+ // Check by server name
308
+ if (BROWSER_MCP_NAMES.has(name.toLowerCase())) {
309
+ return true;
310
+ }
311
+
312
+ // Check by URL for HTTP/SSE servers
313
+ if (config.type === "http" || config.type === "sse") {
314
+ const httpConfig = config as { url?: string };
315
+ if (httpConfig.url && BROWSER_MCP_URL_PATTERN.test(httpConfig.url)) {
316
+ return true;
317
+ }
318
+ }
319
+
320
+ // Check by command/args for stdio servers
321
+ if (!config.type || config.type === "stdio") {
322
+ const stdioConfig = config as { command?: string; args?: string[] };
323
+ if (stdioConfig.command && BROWSER_MCP_PKG_PATTERN.test(stdioConfig.command)) {
324
+ return true;
325
+ }
326
+ if (stdioConfig.args?.some(arg => BROWSER_MCP_PKG_PATTERN.test(arg))) {
327
+ return true;
328
+ }
329
+ }
330
+
331
+ return false;
332
+ }
333
+
334
+ /** Result of filtering browser MCP servers */
335
+ export interface BrowserFilterResult {
336
+ /** Configs with browser servers removed */
337
+ configs: Record<string, MCPServerConfig>;
338
+ /** Source metadata for remaining servers */
339
+ sources: Record<string, SourceMeta>;
340
+ }
341
+
342
+ /**
343
+ * Filter out browser automation MCP servers.
344
+ * Since we have a native browser tool, we don't need these MCP servers.
345
+ */
346
+ export function filterBrowserMCPServers(
347
+ configs: Record<string, MCPServerConfig>,
348
+ sources: Record<string, SourceMeta>,
349
+ ): BrowserFilterResult {
350
+ const filtered: Record<string, MCPServerConfig> = {};
351
+ const filteredSources: Record<string, SourceMeta> = {};
352
+
353
+ for (const [name, config] of Object.entries(configs)) {
354
+ if (!isBrowserMCPServer(name, config)) {
355
+ filtered[name] = config;
356
+ if (sources[name]) {
357
+ filteredSources[name] = sources[name];
358
+ }
359
+ }
360
+ }
361
+
362
+ return { configs: filtered, sources: filteredSources };
363
+ }
package/src/mcp/index.ts CHANGED
@@ -8,10 +8,12 @@
8
8
  // Client
9
9
  export { callTool, connectToServer, disconnectServer, listTools, serverSupportsTools } from "./client";
10
10
  // Config
11
- export type { ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config";
11
+ export type { BrowserFilterResult, ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config";
12
12
  export {
13
13
  extractExaApiKey,
14
+ filterBrowserMCPServers,
14
15
  filterExaMCPServers,
16
+ isBrowserMCPServer,
15
17
  isExaMCPServer,
16
18
  loadAllMCPConfigs,
17
19
  validateServerConfig,
package/src/mcp/loader.ts CHANGED
@@ -32,6 +32,8 @@ export interface MCPToolsLoadOptions {
32
32
  enableProjectConfig?: boolean;
33
33
  /** Whether to filter out Exa MCP servers (default: true) */
34
34
  filterExa?: boolean;
35
+ /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
36
+ filterBrowser?: boolean;
35
37
  /** SQLite storage for MCP tool cache (null disables cache) */
36
38
  cacheStorage?: AgentStorage | null;
37
39
  /** Auth storage used to resolve OAuth credentials before initial MCP connect */
@@ -69,6 +71,7 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
69
71
  onConnecting: options?.onConnecting,
70
72
  enableProjectConfig: options?.enableProjectConfig,
71
73
  filterExa: options?.filterExa,
74
+ filterBrowser: options?.filterBrowser,
72
75
  });
73
76
  } catch (error) {
74
77
  // If discovery fails entirely, return empty result
@@ -68,6 +68,8 @@ export interface MCPDiscoverOptions {
68
68
  enableProjectConfig?: boolean;
69
69
  /** Whether to filter out Exa MCP servers (default: true) */
70
70
  filterExa?: boolean;
71
+ /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
72
+ filterBrowser?: boolean;
71
73
  /** Called when starting to connect to servers */
72
74
  onConnecting?: (serverNames: string[]) => void;
73
75
  }
@@ -105,6 +107,7 @@ export class MCPManager {
105
107
  const { configs, exaApiKeys, sources } = await loadAllMCPConfigs(this.cwd, {
106
108
  enableProjectConfig: options?.enableProjectConfig,
107
109
  filterExa: options?.filterExa,
110
+ filterBrowser: options?.filterBrowser,
108
111
  });
109
112
  const result = await this.connectServers(configs, sources, options?.onConnecting);
110
113
  result.exaApiKeys = exaApiKeys;
@@ -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, {