@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/CHANGELOG.md +42 -0
- package/package.json +414 -92
- package/src/capability/index.ts +5 -8
- package/src/cli/stats-cli.ts +3 -3
- package/src/exec/bash-executor.ts +92 -58
- 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/mcp/config.ts +102 -5
- package/src/mcp/index.ts +3 -1
- package/src/mcp/loader.ts +3 -0
- package/src/mcp/manager.ts +3 -0
- 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 -115
- package/src/system-prompt.ts +15 -36
- package/src/tools/bash-interactive.ts +46 -42
- package/src/tools/bash.ts +6 -7
- package/src/tools/gemini-image.ts +2 -11
- package/src/tools/index.ts +18 -27
- package/src/utils/shell-snapshot.ts +18 -3
- package/src/web/search/providers/gemini.ts +2 -24
- package/src/utils/timings.ts +0 -26
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,
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
120
|
+
let exaApiKeys: string[] = [];
|
|
118
121
|
|
|
119
122
|
if (filterExa) {
|
|
120
|
-
const
|
|
121
|
-
|
|
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
|
package/src/mcp/manager.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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 {
|
|
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
|
|
271
|
-
|
|
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 =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 {
|
package/src/modes/types.ts
CHANGED
|
@@ -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, {
|