@oh-my-pi/pi-coding-agent 13.12.6 → 13.12.8
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 +39 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +1 -4
- package/src/config/settings-schema.ts +10 -3
- package/src/discovery/claude.ts +1 -0
- package/src/discovery/codex.ts +4 -0
- package/src/discovery/cursor.ts +1 -0
- package/src/discovery/gemini.ts +1 -0
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +1 -0
- package/src/mcp/discoverable-tool-metadata.ts +192 -0
- package/src/modes/components/status-line/segments.ts +13 -5
- package/src/modes/controllers/command-controller.ts +8 -46
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +19 -3
- package/src/modes/utils/hotkeys-markdown.ts +57 -0
- package/src/prompts/system/system-prompt.md +7 -0
- package/src/prompts/tools/search-tool-bm25.md +34 -0
- package/src/sdk.ts +47 -7
- package/src/session/agent-session.ts +103 -14
- package/src/session/session-manager.ts +64 -45
- package/src/system-prompt.ts +36 -1
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +13 -3
- package/src/tools/find.ts +2 -2
- package/src/tools/grep.ts +3 -2
- package/src/tools/index.ts +17 -0
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/search-tool-bm25.ts +278 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.12.8] - 2026-03-16
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Changed `SessionManager.create()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
10
|
+
- Changed `SessionManager.continueRecent()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
11
|
+
- Changed `SessionManager.forkFrom()` to require explicit `sessionDir` parameter instead of optional—callers must now pass `SessionManager.getDefaultSessionDir(cwd)` to use default behavior
|
|
12
|
+
- Changed `SessionManager.list()` signature to accept only `sessionDir` parameter instead of `cwd` and optional `sessionDir`—callers must now compute and pass the session directory explicitly
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added `SessionManager.getDefaultSessionDir()` static method to explicitly resolve the canonical default session directory for a working directory
|
|
17
|
+
- Added support for quoted paths in grep, ast_grep, and find tools to handle directory names with spaces
|
|
18
|
+
- Added `normalizePathLikeInput` utility function to consistently handle quoted and whitespace-trimmed path inputs
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Made `sessionDir` parameter optional in `SessionManager.create()`, `SessionManager.continueRecent()`, and `SessionManager.forkFrom()`—callers can now omit it to use the default session directory
|
|
23
|
+
- Changed `SessionManager.list()` signature to accept `cwd` as the first parameter instead of requiring an explicit `sessionDir`—callers can now omit `sessionDir` to use the default for the given working directory
|
|
24
|
+
- Updated `SessionManager.getDefaultSessionDir()` to accept optional `agentDir` parameter for computing session directories within a custom agent root
|
|
25
|
+
- Improved status line path display to strip display roots using canonical path resolution, correctly handling symlink aliases to home and Projects directories
|
|
26
|
+
- Improved error messaging in ast_grep when no matches are found with parse errors, now suggests narrowing `path`/`glob` or setting `lang` to resolve mis-scoped queries
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed SDK-created default sessions to honor the configured `agentDir` for session storage, preventing tests from writing stray session directories into the real `~/.omp/agent/sessions` root
|
|
31
|
+
- Fixed session directory resolution to correctly handle symlink-equivalent paths, ensuring aliased home and temp directories resolve to the same session storage location as their real targets
|
|
32
|
+
|
|
33
|
+
## [13.12.7] - 2026-03-16
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Modified `getSelectedMCPToolNames()` to return only active MCP tools in non-discovery sessions, filtering by tool registry availability
|
|
37
|
+
- Updated `search_tool_bm25` tool instantiation to conditionally create the tool only when MCP discovery mode is enabled and execution hooks are available
|
|
38
|
+
- Changed search results to exclude already-selected MCP tools before applying the limit parameter, allowing discovery of additional tools in subsequent searches
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Fixed MCP tool selection tracking to properly distinguish between discovery-enabled and non-discovery sessions, preventing orphaned tool selections after manual deactivation
|
|
43
|
+
|
|
5
44
|
## [13.12.6] - 2026-03-15
|
|
6
45
|
### Changed
|
|
7
46
|
|
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": "13.12.
|
|
4
|
+
"version": "13.12.8",
|
|
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 Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.12.8",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.12.8",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.12.8",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.12.8",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.12.8",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.12.8",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -264,10 +264,7 @@ export class KeybindingsManager {
|
|
|
264
264
|
* Get display string for an action.
|
|
265
265
|
*/
|
|
266
266
|
getDisplayString(action: AppAction): string {
|
|
267
|
-
|
|
268
|
-
if (keys.length === 0) return "";
|
|
269
|
-
if (keys.length === 1) return keys[0]!;
|
|
270
|
-
return keys.join("/");
|
|
267
|
+
return formatKeyHints(this.getKeys(action));
|
|
271
268
|
}
|
|
272
269
|
|
|
273
270
|
/**
|
|
@@ -1188,6 +1188,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1188
1188
|
ui: { tab: "tools", label: "MCP Project Config", description: "Load .mcp.json/mcp.json from project root" },
|
|
1189
1189
|
},
|
|
1190
1190
|
|
|
1191
|
+
"mcp.discoveryMode": {
|
|
1192
|
+
type: "boolean",
|
|
1193
|
+
default: false,
|
|
1194
|
+
ui: {
|
|
1195
|
+
tab: "tools",
|
|
1196
|
+
label: "MCP Tool Discovery",
|
|
1197
|
+
description: "Hide MCP tools by default and expose them through a tool discovery tool",
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1191
1201
|
"mcp.notifications": {
|
|
1192
1202
|
type: "boolean",
|
|
1193
1203
|
default: false,
|
|
@@ -1463,9 +1473,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
1463
1473
|
ui: { tab: "providers", label: "Exa Websets", description: "Webset management and enrichment tools" },
|
|
1464
1474
|
},
|
|
1465
1475
|
|
|
1466
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
1467
|
-
// Advanced settings (no UI)
|
|
1468
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
1469
1476
|
"commit.mapReduceEnabled": { type: "boolean", default: true },
|
|
1470
1477
|
|
|
1471
1478
|
"commit.mapReduceMinFiles": { type: "number", default: 4 },
|
package/src/discovery/claude.ts
CHANGED
|
@@ -89,6 +89,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
89
89
|
const serverConfig = config as Record<string, unknown>;
|
|
90
90
|
return {
|
|
91
91
|
name,
|
|
92
|
+
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
92
93
|
command: serverConfig.command as string | undefined,
|
|
93
94
|
args: serverConfig.args as string[] | undefined,
|
|
94
95
|
env: serverConfig.env as Record<string, string> | undefined,
|
package/src/discovery/codex.ts
CHANGED
|
@@ -197,6 +197,10 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
|
|
|
197
197
|
}
|
|
198
198
|
// Note: validation of transport vs endpoint is handled by mcpCapability.validate()
|
|
199
199
|
|
|
200
|
+
// Map Codex tool_timeout_sec (seconds) to MCPServer timeout (milliseconds)
|
|
201
|
+
if (typeof config.tool_timeout_sec === "number" && config.tool_timeout_sec > 0) {
|
|
202
|
+
server.timeout = config.tool_timeout_sec * 1000;
|
|
203
|
+
}
|
|
200
204
|
result[name] = server;
|
|
201
205
|
}
|
|
202
206
|
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -65,6 +65,7 @@ function parseMCPServers(
|
|
|
65
65
|
transport: ["stdio", "sse", "http"].includes(serverConfig.type as string)
|
|
66
66
|
? (serverConfig.type as "stdio" | "sse" | "http")
|
|
67
67
|
: undefined,
|
|
68
|
+
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
68
69
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
69
70
|
});
|
|
70
71
|
}
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -110,6 +110,7 @@ async function loadMCPFromSettings(
|
|
|
110
110
|
transport: ["stdio", "sse", "http"].includes(raw.type as string)
|
|
111
111
|
? (raw.type as "stdio" | "sse" | "http")
|
|
112
112
|
: undefined,
|
|
113
|
+
timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
|
|
113
114
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
114
115
|
} as MCPServer);
|
|
115
116
|
}
|
package/src/discovery/vscode.ts
CHANGED
|
@@ -94,6 +94,7 @@ async function loadMCPConfig(
|
|
|
94
94
|
transport: ["stdio", "sse", "http"].includes(expanded.transport as string)
|
|
95
95
|
? (expanded.transport as "stdio" | "sse" | "http")
|
|
96
96
|
: undefined,
|
|
97
|
+
timeout: typeof expanded.timeout === "number" ? expanded.timeout : undefined,
|
|
97
98
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
98
99
|
};
|
|
99
100
|
|
|
@@ -54,6 +54,7 @@ function parseServerConfig(
|
|
|
54
54
|
url: server.url as string | undefined,
|
|
55
55
|
headers: server.headers as Record<string, string> | undefined,
|
|
56
56
|
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
57
|
+
timeout: typeof server.timeout === "number" ? server.timeout : undefined,
|
|
57
58
|
_source: createSourceMeta(PROVIDER_ID, path, scope),
|
|
58
59
|
},
|
|
59
60
|
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export interface DiscoverableMCPTool {
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
serverName?: string;
|
|
8
|
+
mcpToolName?: string;
|
|
9
|
+
schemaKeys: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DiscoverableMCPToolServerSummary {
|
|
13
|
+
name: string;
|
|
14
|
+
toolCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoverableMCPToolSummary {
|
|
18
|
+
servers: DiscoverableMCPToolServerSummary[];
|
|
19
|
+
toolCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDiscoverableMCPToolServerSummary(server: DiscoverableMCPToolServerSummary): string {
|
|
23
|
+
const toolLabel = server.toolCount === 1 ? "tool" : "tools";
|
|
24
|
+
return `${server.name} (${server.toolCount} ${toolLabel})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DiscoverableMCPSearchDocument {
|
|
28
|
+
tool: DiscoverableMCPTool;
|
|
29
|
+
termFrequencies: Map<string, number>;
|
|
30
|
+
length: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiscoverableMCPSearchIndex {
|
|
34
|
+
documents: DiscoverableMCPSearchDocument[];
|
|
35
|
+
averageLength: number;
|
|
36
|
+
documentFrequencies: Map<string, number>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DiscoverableMCPSearchResult {
|
|
40
|
+
tool: DiscoverableMCPTool;
|
|
41
|
+
score: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const BM25_K1 = 1.2;
|
|
45
|
+
const BM25_B = 0.75;
|
|
46
|
+
const FIELD_WEIGHTS = {
|
|
47
|
+
name: 6,
|
|
48
|
+
label: 4,
|
|
49
|
+
serverName: 2,
|
|
50
|
+
mcpToolName: 4,
|
|
51
|
+
description: 2,
|
|
52
|
+
schemaKey: 1,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export function isMCPToolName(name: string): boolean {
|
|
56
|
+
return name.startsWith("mcp_");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSchemaPropertyKeys(parameters: unknown): string[] {
|
|
60
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return [];
|
|
61
|
+
const properties = (parameters as { properties?: unknown }).properties;
|
|
62
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties)) return [];
|
|
63
|
+
return Object.keys(properties as Record<string, unknown>).sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tokenize(value: string): string[] {
|
|
67
|
+
return value
|
|
68
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
69
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.trim()
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter(token => token.length > 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
|
|
77
|
+
if (!value) return;
|
|
78
|
+
for (const token of tokenize(value)) {
|
|
79
|
+
termFrequencies.set(token, (termFrequencies.get(token) ?? 0) + weight);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildSearchDocument(tool: DiscoverableMCPTool): DiscoverableMCPSearchDocument {
|
|
84
|
+
const termFrequencies = new Map<string, number>();
|
|
85
|
+
addWeightedTokens(termFrequencies, tool.name, FIELD_WEIGHTS.name);
|
|
86
|
+
addWeightedTokens(termFrequencies, tool.label, FIELD_WEIGHTS.label);
|
|
87
|
+
addWeightedTokens(termFrequencies, tool.serverName, FIELD_WEIGHTS.serverName);
|
|
88
|
+
addWeightedTokens(termFrequencies, tool.mcpToolName, FIELD_WEIGHTS.mcpToolName);
|
|
89
|
+
addWeightedTokens(termFrequencies, tool.description, FIELD_WEIGHTS.description);
|
|
90
|
+
for (const schemaKey of tool.schemaKeys) {
|
|
91
|
+
addWeightedTokens(termFrequencies, schemaKey, FIELD_WEIGHTS.schemaKey);
|
|
92
|
+
}
|
|
93
|
+
const length = Array.from(termFrequencies.values()).reduce((sum, value) => sum + value, 0);
|
|
94
|
+
return { tool, termFrequencies, length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | null {
|
|
98
|
+
if (!isMCPToolName(tool.name)) return null;
|
|
99
|
+
const toolRecord = tool as AgentTool & {
|
|
100
|
+
label?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
mcpServerName?: string;
|
|
103
|
+
mcpToolName?: string;
|
|
104
|
+
parameters?: unknown;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
name: tool.name,
|
|
108
|
+
label: typeof toolRecord.label === "string" ? toolRecord.label : tool.name,
|
|
109
|
+
description: typeof toolRecord.description === "string" ? toolRecord.description : "",
|
|
110
|
+
serverName: typeof toolRecord.mcpServerName === "string" ? toolRecord.mcpServerName : undefined,
|
|
111
|
+
mcpToolName: typeof toolRecord.mcpToolName === "string" ? toolRecord.mcpToolName : undefined,
|
|
112
|
+
schemaKeys: getSchemaPropertyKeys(toolRecord.parameters),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function collectDiscoverableMCPTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
|
|
117
|
+
const discoverable: DiscoverableMCPTool[] = [];
|
|
118
|
+
for (const tool of tools) {
|
|
119
|
+
const metadata = getDiscoverableMCPTool(tool);
|
|
120
|
+
if (metadata) {
|
|
121
|
+
discoverable.push(metadata);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return discoverable;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function summarizeDiscoverableMCPTools(tools: DiscoverableMCPTool[]): DiscoverableMCPToolSummary {
|
|
128
|
+
const serverToolCounts = new Map<string, number>();
|
|
129
|
+
for (const tool of tools) {
|
|
130
|
+
if (!tool.serverName) continue;
|
|
131
|
+
serverToolCounts.set(tool.serverName, (serverToolCounts.get(tool.serverName) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
const servers = Array.from(serverToolCounts.entries())
|
|
134
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
135
|
+
.map(([name, toolCount]) => ({ name, toolCount }));
|
|
136
|
+
return {
|
|
137
|
+
servers,
|
|
138
|
+
toolCount: tools.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildDiscoverableMCPSearchIndex(tools: Iterable<DiscoverableMCPTool>): DiscoverableMCPSearchIndex {
|
|
143
|
+
const documents = Array.from(tools, buildSearchDocument);
|
|
144
|
+
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
145
|
+
const documentFrequencies = new Map<string, number>();
|
|
146
|
+
for (const document of documents) {
|
|
147
|
+
for (const token of new Set(document.termFrequencies.keys())) {
|
|
148
|
+
documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
documents,
|
|
153
|
+
averageLength,
|
|
154
|
+
documentFrequencies,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function searchDiscoverableMCPTools(
|
|
159
|
+
index: DiscoverableMCPSearchIndex,
|
|
160
|
+
query: string,
|
|
161
|
+
limit: number,
|
|
162
|
+
): DiscoverableMCPSearchResult[] {
|
|
163
|
+
const queryTokens = tokenize(query);
|
|
164
|
+
if (queryTokens.length === 0) {
|
|
165
|
+
throw new Error("Query must contain at least one letter or number.");
|
|
166
|
+
}
|
|
167
|
+
if (index.documents.length === 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const queryTermCounts = new Map<string, number>();
|
|
172
|
+
for (const token of queryTokens) {
|
|
173
|
+
queryTermCounts.set(token, (queryTermCounts.get(token) ?? 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return index.documents
|
|
177
|
+
.map(document => {
|
|
178
|
+
let score = 0;
|
|
179
|
+
for (const [token, queryTermCount] of queryTermCounts) {
|
|
180
|
+
const termFrequency = document.termFrequencies.get(token) ?? 0;
|
|
181
|
+
if (termFrequency === 0) continue;
|
|
182
|
+
const documentFrequency = index.documentFrequencies.get(token) ?? 0;
|
|
183
|
+
const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
184
|
+
const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
|
|
185
|
+
score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
|
|
186
|
+
}
|
|
187
|
+
return { tool: document.tool, score };
|
|
188
|
+
})
|
|
189
|
+
.filter(result => result.score > 0)
|
|
190
|
+
.sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
|
|
191
|
+
.slice(0, limit);
|
|
192
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { formatDuration, formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { theme } from "../../../modes/theme/theme";
|
|
6
7
|
import { shortenPath } from "../../../tools/render-utils";
|
|
7
8
|
import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
|
|
@@ -17,6 +18,14 @@ function withIcon(icon: string, text: string): string {
|
|
|
17
18
|
return icon ? `${icon} ${text}` : text;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function stripDisplayRoot(pwd: string): string {
|
|
22
|
+
for (const root of ["/work", path.join(os.homedir(), "Projects")]) {
|
|
23
|
+
const relative = relativePathWithinRoot(root, pwd);
|
|
24
|
+
if (relative) return relative;
|
|
25
|
+
}
|
|
26
|
+
return pwd;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
function normalizePremiumRequests(value: number): number {
|
|
21
30
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
22
31
|
}
|
|
@@ -87,13 +96,12 @@ const pathSegment: StatusLineSegment = {
|
|
|
87
96
|
|
|
88
97
|
let pwd = getProjectDir();
|
|
89
98
|
|
|
99
|
+
if (opts.stripWorkPrefix !== false) {
|
|
100
|
+
pwd = stripDisplayRoot(pwd);
|
|
101
|
+
}
|
|
90
102
|
if (opts.abbreviate !== false) {
|
|
91
103
|
pwd = shortenPath(pwd);
|
|
92
104
|
}
|
|
93
|
-
if (opts.stripWorkPrefix !== false) {
|
|
94
|
-
if (pwd.startsWith("/work/")) pwd = pwd.slice(6);
|
|
95
|
-
else if (pwd.startsWith("~/Projects/")) pwd = pwd.slice(11);
|
|
96
|
-
}
|
|
97
105
|
|
|
98
106
|
const maxLen = opts.maxLength ?? 40;
|
|
99
107
|
if (pwd.length > maxLen) {
|
|
@@ -24,6 +24,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
|
24
24
|
import { PythonExecutionComponent } from "../../modes/components/python-execution";
|
|
25
25
|
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
26
26
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
27
|
+
import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
27
28
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
28
29
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
29
30
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -509,52 +510,13 @@ export class CommandController {
|
|
|
509
510
|
const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
|
|
510
511
|
const copyLineKey = this.ctx.keybindings.getDisplayString("copyLine") || "Alt+Shift+L";
|
|
511
512
|
const copyPromptKey = this.ctx.keybindings.getDisplayString("copyPrompt") || "Alt+Shift+C";
|
|
512
|
-
const hotkeys =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
520
|
-
|
|
521
|
-
**Editing**
|
|
522
|
-
| Key | Action |
|
|
523
|
-
|-----|--------|
|
|
524
|
-
| \`Enter\` | Send message |
|
|
525
|
-
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
526
|
-
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
527
|
-
| \`Ctrl+U\` | Delete to start of line |
|
|
528
|
-
| \`Ctrl+K\` | Delete to end of line |
|
|
529
|
-
| \`${copyLineKey}\` | Copy current line |
|
|
530
|
-
| \`${copyPromptKey}\` | Copy whole prompt |
|
|
531
|
-
|
|
532
|
-
**Other**
|
|
533
|
-
| Key | Action |
|
|
534
|
-
|-----|--------|
|
|
535
|
-
| \`Tab\` | Path completion / accept autocomplete |
|
|
536
|
-
| \`Escape\` | Cancel autocomplete / abort streaming |
|
|
537
|
-
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
|
538
|
-
| \`Ctrl+D\` | Exit (when editor is empty) |
|
|
539
|
-
| \`Ctrl+Z\` | Suspend to background |
|
|
540
|
-
| \`Shift+Tab\` | Cycle thinking level |
|
|
541
|
-
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
542
|
-
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
543
|
-
| \`Alt+P\` | Select model (temporary) |
|
|
544
|
-
| \`Ctrl+L\` | Select model (set roles) |
|
|
545
|
-
| \`${planModeKey}\` | Toggle plan mode |
|
|
546
|
-
| \`Ctrl+R\` | Search prompt history |
|
|
547
|
-
| \`${expandToolsKey}\` | Toggle tool output expansion |
|
|
548
|
-
| \`Ctrl+T\` | Toggle todo list expansion |
|
|
549
|
-
| \`Ctrl+G\` | Edit message in external editor |
|
|
550
|
-
| \`${sttKey}\` | Toggle speech-to-text recording |
|
|
551
|
-
| \`#\` | Open prompt actions |
|
|
552
|
-
| \`/\` | Slash commands |
|
|
553
|
-
| \`!\` | Run bash command |
|
|
554
|
-
| \`!!\` | Run bash command (excluded from context) |
|
|
555
|
-
| \`$\` | Run Python in shared kernel |
|
|
556
|
-
| \`$$\` | Run Python (excluded from context) |
|
|
557
|
-
`;
|
|
513
|
+
const hotkeys = buildHotkeysMarkdown({
|
|
514
|
+
expandToolsKey,
|
|
515
|
+
planModeKey,
|
|
516
|
+
sttKey,
|
|
517
|
+
copyLineKey,
|
|
518
|
+
copyPromptKey,
|
|
519
|
+
});
|
|
558
520
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
559
521
|
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
560
522
|
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
@@ -535,6 +535,7 @@ export class InputController {
|
|
|
535
535
|
keybindings: this.ctx.keybindings,
|
|
536
536
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
537
537
|
copyPrompt: () => this.handleCopyPrompt(),
|
|
538
|
+
undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
|
|
538
539
|
moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
|
|
539
540
|
moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
|
|
540
541
|
moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
|
|
@@ -12,12 +12,12 @@ interface PromptActionDefinition {
|
|
|
12
12
|
label: string;
|
|
13
13
|
description: string;
|
|
14
14
|
keywords: string[];
|
|
15
|
-
execute: () => void;
|
|
15
|
+
execute: (prefix: string) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface PromptActionAutocompleteItem extends AutocompleteItem {
|
|
19
19
|
actionId: string;
|
|
20
|
-
execute: () => void;
|
|
20
|
+
execute: (prefix: string) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface PromptActionAutocompleteOptions {
|
|
@@ -26,6 +26,7 @@ interface PromptActionAutocompleteOptions {
|
|
|
26
26
|
keybindings: KeybindingsManager;
|
|
27
27
|
copyCurrentLine: () => void;
|
|
28
28
|
copyPrompt: () => void;
|
|
29
|
+
undo: (prefix: string) => void;
|
|
29
30
|
moveCursorToMessageEnd: () => void;
|
|
30
31
|
moveCursorToMessageStart: () => void;
|
|
31
32
|
moveCursorToLineStart: () => void;
|
|
@@ -141,6 +142,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
141
142
|
onApplied?: () => void;
|
|
142
143
|
} {
|
|
143
144
|
if (prefix.startsWith("#") && isPromptActionItem(item)) {
|
|
145
|
+
if (item.actionId === "undo") {
|
|
146
|
+
return {
|
|
147
|
+
lines,
|
|
148
|
+
cursorLine,
|
|
149
|
+
cursorCol,
|
|
150
|
+
onApplied: () => item.execute(prefix),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
144
153
|
const currentLine = lines[cursorLine] || "";
|
|
145
154
|
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
146
155
|
const afterCursor = currentLine.slice(cursorCol);
|
|
@@ -150,7 +159,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
150
159
|
lines: newLines,
|
|
151
160
|
cursorLine,
|
|
152
161
|
cursorCol: beforePrefix.length,
|
|
153
|
-
onApplied: item.execute,
|
|
162
|
+
onApplied: () => item.execute(prefix),
|
|
154
163
|
};
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -181,6 +190,13 @@ export function createPromptActionAutocompleteProvider(
|
|
|
181
190
|
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
182
191
|
execute: options.copyPrompt,
|
|
183
192
|
},
|
|
193
|
+
{
|
|
194
|
+
id: "undo",
|
|
195
|
+
label: "Undo",
|
|
196
|
+
description: formatKeyHints(editorKeybindings.getKeys("undo")),
|
|
197
|
+
keywords: ["undo", "revert", "edit", "history"],
|
|
198
|
+
execute: options.undo,
|
|
199
|
+
},
|
|
184
200
|
{
|
|
185
201
|
id: "cursor-message-end",
|
|
186
202
|
label: "Move cursor to end of message",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface HotkeysMarkdownBindings {
|
|
2
|
+
expandToolsKey: string;
|
|
3
|
+
planModeKey: string;
|
|
4
|
+
sttKey: string;
|
|
5
|
+
copyLineKey: string;
|
|
6
|
+
copyPromptKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
|
|
10
|
+
const { expandToolsKey, planModeKey, sttKey, copyLineKey, copyPromptKey } = bindings;
|
|
11
|
+
return [
|
|
12
|
+
"**Navigation**",
|
|
13
|
+
"| Key | Action |",
|
|
14
|
+
"|-----|--------|",
|
|
15
|
+
"| `Arrow keys` | Move cursor / browse history (Up when empty) |",
|
|
16
|
+
"| `Option+Left/Right` | Move by word |",
|
|
17
|
+
"| `Ctrl+A` / `Home` / `Cmd+Left` | Start of line |",
|
|
18
|
+
"| `Ctrl+E` / `End` / `Cmd+Right` | End of line |",
|
|
19
|
+
"",
|
|
20
|
+
"**Editing**",
|
|
21
|
+
"| Key | Action |",
|
|
22
|
+
"|-----|--------|",
|
|
23
|
+
"| `Enter` | Send message |",
|
|
24
|
+
"| `Shift+Enter` / `Alt+Enter` | New line |",
|
|
25
|
+
"| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
|
|
26
|
+
"| `Ctrl+U` | Delete to start of line |",
|
|
27
|
+
"| `Ctrl+K` | Delete to end of line |",
|
|
28
|
+
`| \`${copyLineKey}\` | Copy current line |`,
|
|
29
|
+
`| \`${copyPromptKey}\` | Copy whole prompt |`,
|
|
30
|
+
"",
|
|
31
|
+
"**Other**",
|
|
32
|
+
"| Key | Action |",
|
|
33
|
+
"|-----|--------|",
|
|
34
|
+
"| `Tab` | Path completion / accept autocomplete |",
|
|
35
|
+
"| `Escape` | Cancel autocomplete / abort streaming |",
|
|
36
|
+
"| `Ctrl+C` | Clear editor (first) / exit (second) |",
|
|
37
|
+
"| `Ctrl+D` | Exit (when editor is empty) |",
|
|
38
|
+
"| `Ctrl+Z` | Suspend to background |",
|
|
39
|
+
"| `Shift+Tab` | Cycle thinking level |",
|
|
40
|
+
"| `Ctrl+P` | Cycle role models (slow/default/smol) |",
|
|
41
|
+
"| `Shift+Ctrl+P` | Cycle role models (temporary) |",
|
|
42
|
+
"| `Alt+P` | Select model (temporary) |",
|
|
43
|
+
"| `Ctrl+L` | Select model (set roles) |",
|
|
44
|
+
`| \`${planModeKey}\` | Toggle plan mode |`,
|
|
45
|
+
"| `Ctrl+R` | Search prompt history |",
|
|
46
|
+
`| \`${expandToolsKey}\` | Toggle tool output expansion |`,
|
|
47
|
+
"| `Ctrl+T` | Toggle todo list expansion |",
|
|
48
|
+
"| `Ctrl+G` | Edit message in external editor |",
|
|
49
|
+
`| \`${sttKey}\` | Toggle speech-to-text recording |`,
|
|
50
|
+
"| `#` | Open prompt actions |",
|
|
51
|
+
"| `/` | Slash commands |",
|
|
52
|
+
"| `!` | Run bash command |",
|
|
53
|
+
"| `!!` | Run bash command (excluded from context) |",
|
|
54
|
+
"| `$` | Run Python in shared kernel |",
|
|
55
|
+
"| `$$` | Run Python (excluded from context) |",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
@@ -155,6 +155,13 @@ You **MUST** use the following tools, as effectively as possible, to complete th
|
|
|
155
155
|
{{/each}}
|
|
156
156
|
{{/if}}
|
|
157
157
|
|
|
158
|
+
{{#if mcpDiscoveryMode}}
|
|
159
|
+
### MCP tool discovery
|
|
160
|
+
|
|
161
|
+
Some MCP tools are intentionally hidden from the initial tool list.
|
|
162
|
+
{{#if hasMCPDiscoveryServers}}Discoverable MCP servers in this session: {{#list mcpDiscoveryServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
163
|
+
If the task may involve external systems, SaaS APIs, chat, tickets, databases, deployments, or other non-local integrations, you **SHOULD** call `search_tool_bm25` before concluding no such tool exists.
|
|
164
|
+
{{/if}}
|
|
158
165
|
## Precedence
|
|
159
166
|
{{#ifAny (includes tools "python") (includes tools "bash")}}
|
|
160
167
|
Pick the right tool for the job:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Search hidden MCP tool metadata when MCP tool discovery is enabled.
|
|
2
|
+
|
|
3
|
+
Use this tool to discover MCP tools that are loaded into the session but not exposed to the model by default.
|
|
4
|
+
|
|
5
|
+
{{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
6
|
+
{{#if discoverableMCPToolCount}}Total discoverable MCP tools loaded: {{discoverableMCPToolCount}}.{{/if}}
|
|
7
|
+
Input:
|
|
8
|
+
- `query` — required natural-language or keyword query
|
|
9
|
+
- `limit` — optional maximum number of tools to return and activate (default `8`)
|
|
10
|
+
|
|
11
|
+
Behavior:
|
|
12
|
+
- Searches hidden MCP tool metadata using BM25-style relevance ranking
|
|
13
|
+
- Matches against MCP tool name, server name, description, and input schema keys
|
|
14
|
+
- Activates the top matching MCP tools for the rest of the current session
|
|
15
|
+
- Repeated searches add to the active MCP tool set; they do not remove earlier selections
|
|
16
|
+
- Newly activated MCP tools become available before the next model call in the same overall turn
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
- If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
|
|
20
|
+
- `query` is matched against MCP tool metadata fields:
|
|
21
|
+
- `name`
|
|
22
|
+
- `label`
|
|
23
|
+
- `server_name`
|
|
24
|
+
- `mcp_tool_name`
|
|
25
|
+
- `description`
|
|
26
|
+
- input schema property keys (`schema_keys`)
|
|
27
|
+
|
|
28
|
+
This is not repository search, file search, or code search. Use it only for MCP tool discovery.
|
|
29
|
+
|
|
30
|
+
Returns JSON with:
|
|
31
|
+
- `query`
|
|
32
|
+
- `activated_tools` — MCP tools activated by this search call
|
|
33
|
+
- `match_count` — number of ranked matches returned by the search
|
|
34
|
+
- `total_tools`
|