@oh-my-pi/pi-coding-agent 14.7.0 → 14.7.2
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 +24 -0
- package/package.json +12 -12
- package/src/cli/grep-cli.ts +1 -1
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +108 -22
- package/src/config/settings-schema.ts +46 -1
- package/src/config/settings.ts +71 -1
- package/src/dap/client.ts +1 -0
- package/src/discovery/builtin.ts +34 -9
- package/src/discovery/helpers.ts +4 -3
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/hashline.ts +212 -63
- package/src/eval/py/gateway-coordinator.ts +2 -3
- package/src/eval/py/runtime.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/index.ts +2 -0
- package/src/main.ts +10 -15
- package/src/mcp/discoverable-tool-metadata.ts +24 -202
- package/src/modes/components/extensions/extension-dashboard.ts +26 -2
- package/src/modes/components/extensions/state-manager.ts +41 -0
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +45 -13
- package/src/prompts/system/plan-mode-active.md +7 -3
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/tools/search-tool-bm25.md +14 -14
- package/src/prompts/tools/todo-write.md +1 -0
- package/src/sdk.ts +69 -8
- package/src/session/agent-session.ts +177 -1
- package/src/slash-commands/builtin-registry.ts +13 -2
- package/src/task/index.ts +2 -0
- package/src/task/isolation-backend.ts +22 -0
- package/src/tool-discovery/tool-index.ts +377 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/ast-edit.ts +2 -0
- package/src/tools/ast-grep.ts +2 -0
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +2 -0
- package/src/tools/calculator.ts +2 -0
- package/src/tools/checkpoint.ts +4 -0
- package/src/tools/debug.ts +2 -0
- package/src/tools/eval.ts +2 -0
- package/src/tools/find.ts +2 -0
- package/src/tools/gh.ts +2 -0
- package/src/tools/hindsight-recall.ts +2 -0
- package/src/tools/hindsight-reflect.ts +2 -0
- package/src/tools/hindsight-retain.ts +2 -0
- package/src/tools/index.ts +74 -14
- package/src/tools/inspect-image.ts +2 -0
- package/src/tools/irc.ts +2 -1
- package/src/tools/job.ts +2 -1
- package/src/tools/notebook.ts +2 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/recipe/index.ts +2 -0
- package/src/tools/render-mermaid.ts +2 -0
- package/src/tools/search-tool-bm25.ts +128 -42
- package/src/tools/search.ts +2 -0
- package/src/tools/ssh.ts +2 -0
- package/src/tools/todo-write.ts +2 -1
- package/src/tools/write.ts +2 -0
- package/src/web/search/index.ts +2 -0
- package/src/web/search/providers/searxng.ts +8 -0
package/src/main.ts
CHANGED
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { realpathSync } from "node:fs";
|
|
9
8
|
import * as fs from "node:fs/promises";
|
|
10
9
|
import * as os from "node:os";
|
|
11
10
|
import * as path from "node:path";
|
|
12
11
|
import { createInterface } from "node:readline/promises";
|
|
13
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
14
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
$env,
|
|
15
|
+
getProjectDir,
|
|
16
|
+
logger,
|
|
17
|
+
normalizePathForComparison,
|
|
18
|
+
postmortem,
|
|
19
|
+
setProjectDir,
|
|
20
|
+
VERSION,
|
|
21
|
+
} from "@oh-my-pi/pi-utils";
|
|
15
22
|
import chalk from "chalk";
|
|
16
23
|
import type { Args } from "./cli/args";
|
|
17
24
|
import { processFileArguments } from "./cli/file-processor";
|
|
@@ -216,15 +223,6 @@ async function runInteractiveMode(
|
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
225
|
|
|
219
|
-
function normalizePathForComparison(value: string): string {
|
|
220
|
-
const resolved = path.resolve(value);
|
|
221
|
-
let realPath = resolved;
|
|
222
|
-
try {
|
|
223
|
-
realPath = realpathSync(resolved);
|
|
224
|
-
} catch {}
|
|
225
|
-
return process.platform === "win32" ? realPath.toLowerCase() : realPath;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
226
|
async function promptForkSession(session: SessionInfo): Promise<boolean> {
|
|
229
227
|
if (!process.stdin.isTTY) {
|
|
230
228
|
return false;
|
|
@@ -353,10 +351,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
|
353
351
|
return;
|
|
354
352
|
}
|
|
355
353
|
|
|
356
|
-
const normalizePath =
|
|
357
|
-
const resolved = realpathSync(path.resolve(value));
|
|
358
|
-
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
359
|
-
};
|
|
354
|
+
const normalizePath = normalizePathForComparison;
|
|
360
355
|
|
|
361
356
|
const cwd = normalizePath(getProjectDir());
|
|
362
357
|
const normalizedHome = normalizePath(home);
|
|
@@ -1,202 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 selectDiscoverableMCPToolNamesByServer(
|
|
128
|
-
tools: Iterable<DiscoverableMCPTool>,
|
|
129
|
-
serverNames: ReadonlySet<string>,
|
|
130
|
-
): string[] {
|
|
131
|
-
if (serverNames.size === 0) return [];
|
|
132
|
-
return Array.from(tools)
|
|
133
|
-
.filter(tool => tool.serverName !== undefined && serverNames.has(tool.serverName))
|
|
134
|
-
.map(tool => tool.name);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function summarizeDiscoverableMCPTools(tools: DiscoverableMCPTool[]): DiscoverableMCPToolSummary {
|
|
138
|
-
const serverToolCounts = new Map<string, number>();
|
|
139
|
-
for (const tool of tools) {
|
|
140
|
-
if (!tool.serverName) continue;
|
|
141
|
-
serverToolCounts.set(tool.serverName, (serverToolCounts.get(tool.serverName) ?? 0) + 1);
|
|
142
|
-
}
|
|
143
|
-
const servers = Array.from(serverToolCounts.entries())
|
|
144
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
145
|
-
.map(([name, toolCount]) => ({ name, toolCount }));
|
|
146
|
-
return {
|
|
147
|
-
servers,
|
|
148
|
-
toolCount: tools.length,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function buildDiscoverableMCPSearchIndex(tools: Iterable<DiscoverableMCPTool>): DiscoverableMCPSearchIndex {
|
|
153
|
-
const documents = Array.from(tools, buildSearchDocument);
|
|
154
|
-
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
155
|
-
const documentFrequencies = new Map<string, number>();
|
|
156
|
-
for (const document of documents) {
|
|
157
|
-
for (const token of new Set(document.termFrequencies.keys())) {
|
|
158
|
-
documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
documents,
|
|
163
|
-
averageLength,
|
|
164
|
-
documentFrequencies,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function searchDiscoverableMCPTools(
|
|
169
|
-
index: DiscoverableMCPSearchIndex,
|
|
170
|
-
query: string,
|
|
171
|
-
limit: number,
|
|
172
|
-
): DiscoverableMCPSearchResult[] {
|
|
173
|
-
const queryTokens = tokenize(query);
|
|
174
|
-
if (queryTokens.length === 0) {
|
|
175
|
-
throw new Error("Query must contain at least one letter or number.");
|
|
176
|
-
}
|
|
177
|
-
if (index.documents.length === 0) {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const queryTermCounts = new Map<string, number>();
|
|
182
|
-
for (const token of queryTokens) {
|
|
183
|
-
queryTermCounts.set(token, (queryTermCounts.get(token) ?? 0) + 1);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return index.documents
|
|
187
|
-
.map(document => {
|
|
188
|
-
let score = 0;
|
|
189
|
-
for (const [token, queryTermCount] of queryTermCounts) {
|
|
190
|
-
const termFrequency = document.termFrequencies.get(token) ?? 0;
|
|
191
|
-
if (termFrequency === 0) continue;
|
|
192
|
-
const documentFrequency = index.documentFrequencies.get(token) ?? 0;
|
|
193
|
-
const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
194
|
-
const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
|
|
195
|
-
score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
|
|
196
|
-
}
|
|
197
|
-
return { tool: document.tool, score };
|
|
198
|
-
})
|
|
199
|
-
.filter(result => result.score > 0)
|
|
200
|
-
.sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
|
|
201
|
-
.slice(0, limit);
|
|
202
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Back-compat re-export layer.
|
|
3
|
+
* All types and functions have moved to src/tool-discovery/tool-index.ts.
|
|
4
|
+
* This file exists solely so existing imports continue to compile without changes.
|
|
5
|
+
*/
|
|
6
|
+
export type {
|
|
7
|
+
DiscoverableMCPSearchDocument,
|
|
8
|
+
DiscoverableMCPSearchIndex,
|
|
9
|
+
DiscoverableMCPSearchResult,
|
|
10
|
+
DiscoverableMCPTool,
|
|
11
|
+
DiscoverableMCPToolServerSummary,
|
|
12
|
+
DiscoverableMCPToolSummary,
|
|
13
|
+
} from "../tool-discovery/tool-index";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
buildDiscoverableMCPSearchIndex,
|
|
17
|
+
collectDiscoverableMCPTools,
|
|
18
|
+
formatDiscoverableMCPToolServerSummary,
|
|
19
|
+
getDiscoverableMCPTool,
|
|
20
|
+
isMCPToolName,
|
|
21
|
+
searchDiscoverableMCPTools,
|
|
22
|
+
selectDiscoverableMCPToolNamesByServer,
|
|
23
|
+
summarizeDiscoverableMCPTools,
|
|
24
|
+
} from "../tool-discovery/tool-index";
|
|
@@ -27,15 +27,24 @@ import { theme } from "../../../modes/theme/theme";
|
|
|
27
27
|
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
|
28
28
|
import { ExtensionList } from "./extension-list";
|
|
29
29
|
import { InspectorPanel } from "./inspector-panel";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
applyDisabledExtensionsToState,
|
|
32
|
+
applyFilter,
|
|
33
|
+
createInitialState,
|
|
34
|
+
filterByProvider,
|
|
35
|
+
refreshState,
|
|
36
|
+
toggleProvider,
|
|
37
|
+
} from "./state-manager";
|
|
31
38
|
import type { DashboardState } from "./types";
|
|
32
39
|
|
|
33
40
|
export class ExtensionDashboard extends Container {
|
|
34
41
|
#state!: DashboardState;
|
|
35
42
|
#mainList!: ExtensionList;
|
|
36
43
|
#inspector!: InspectorPanel;
|
|
44
|
+
#refreshToken = 0;
|
|
37
45
|
|
|
38
46
|
onClose?: () => void;
|
|
47
|
+
onRequestRender?: () => void;
|
|
39
48
|
|
|
40
49
|
private constructor(
|
|
41
50
|
private readonly cwd: string,
|
|
@@ -181,16 +190,20 @@ export class ExtensionDashboard extends Container {
|
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
this.#applyDisabledExtensions(disabled);
|
|
184
194
|
void this.#refreshFromState();
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
async #refreshFromState(): Promise<void> {
|
|
198
|
+
const refreshToken = ++this.#refreshToken;
|
|
188
199
|
// Remember current tab ID before refresh
|
|
189
200
|
const currentTabId = this.#state.tabs[this.#state.activeTabIndex]?.id;
|
|
190
201
|
|
|
191
202
|
const sm = this.settings ?? Settings.instance;
|
|
192
203
|
const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
|
|
193
|
-
|
|
204
|
+
const nextState = await refreshState(this.#state, this.cwd, disabledIds);
|
|
205
|
+
if (refreshToken !== this.#refreshToken) return;
|
|
206
|
+
this.#state = nextState;
|
|
194
207
|
|
|
195
208
|
// Find the same tab in the new (re-sorted) list
|
|
196
209
|
if (currentTabId) {
|
|
@@ -208,6 +221,17 @@ export class ExtensionDashboard extends Container {
|
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
this.#buildLayout();
|
|
224
|
+
this.onRequestRender?.();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#applyDisabledExtensions(disabledIds: string[]): void {
|
|
228
|
+
this.#state = applyDisabledExtensionsToState(this.#state, disabledIds);
|
|
229
|
+
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
230
|
+
if (this.#state.selected) {
|
|
231
|
+
this.#inspector.setExtension(this.#state.selected);
|
|
232
|
+
}
|
|
233
|
+
this.#buildLayout();
|
|
234
|
+
this.onRequestRender?.();
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
#switchTab(direction: 1 | -1): void {
|
|
@@ -509,6 +509,47 @@ export function filterByProvider(extensions: Extension[], providerId: string): E
|
|
|
509
509
|
return extensions.filter(ext => ext.source.provider === providerId);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
function isShadowedExtension(ext: Extension): boolean {
|
|
513
|
+
if (ext.shadowedBy) return true;
|
|
514
|
+
return Boolean((ext.raw as { _shadowed?: boolean } | null | undefined)?._shadowed);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Apply setting-backed item disable overrides to an existing dashboard state.
|
|
519
|
+
* This gives the UI immediate feedback while the full capability refresh runs.
|
|
520
|
+
*/
|
|
521
|
+
export function applyDisabledExtensionsToState(state: DashboardState, disabledIds: string[]): DashboardState {
|
|
522
|
+
const disabled = new Set(disabledIds);
|
|
523
|
+
const updateExtension = (ext: Extension): Extension => {
|
|
524
|
+
if (disabled.has(ext.id)) {
|
|
525
|
+
if (ext.state === "disabled" && ext.disabledReason === "item-disabled") return ext;
|
|
526
|
+
return { ...ext, state: "disabled", disabledReason: "item-disabled" };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (ext.state !== "disabled" || ext.disabledReason !== "item-disabled") return ext;
|
|
530
|
+
if (!isProviderEnabled(ext.source.provider)) {
|
|
531
|
+
return { ...ext, state: "disabled", disabledReason: "provider-disabled" };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (isShadowedExtension(ext)) {
|
|
535
|
+
const shadowed: Extension = { ...ext, state: "shadowed", disabledReason: "shadowed" };
|
|
536
|
+
return shadowed;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const enabled: Extension = { ...ext, state: "active" };
|
|
540
|
+
delete enabled.disabledReason;
|
|
541
|
+
return enabled;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
...state,
|
|
546
|
+
extensions: state.extensions.map(updateExtension),
|
|
547
|
+
tabFiltered: state.tabFiltered.map(updateExtension),
|
|
548
|
+
searchFiltered: state.searchFiltered.map(updateExtension),
|
|
549
|
+
selected: state.selected ? updateExtension(state.selected) : null,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
512
553
|
/**
|
|
513
554
|
* Create initial dashboard state.
|
|
514
555
|
*/
|
|
@@ -663,10 +663,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
663
663
|
}
|
|
664
664
|
|
|
665
665
|
finishPendingSubmission(input: SubmittedUserInput): void {
|
|
666
|
-
|
|
666
|
+
const wasPendingSubmission = this.#pendingSubmittedInput === input;
|
|
667
|
+
const pendingSubmissionDispose = this.#pendingSubmissionDispose;
|
|
668
|
+
if (wasPendingSubmission) {
|
|
667
669
|
this.#pendingSubmittedInput = undefined;
|
|
668
670
|
this.#pendingSubmissionDispose = undefined;
|
|
669
671
|
}
|
|
672
|
+
|
|
673
|
+
if (wasPendingSubmission && !this.session.isStreaming && !this.streamingComponent) {
|
|
674
|
+
this.optimisticUserMessageSignature = undefined;
|
|
675
|
+
pendingSubmissionDispose?.();
|
|
676
|
+
this.#pendingWorkingMessage = undefined;
|
|
677
|
+
if (this.loadingAnimation) {
|
|
678
|
+
this.loadingAnimation.stop();
|
|
679
|
+
this.loadingAnimation = undefined;
|
|
680
|
+
this.statusContainer.clear();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
670
683
|
}
|
|
671
684
|
|
|
672
685
|
#computeEditorMaxHeight(): number {
|
|
@@ -857,6 +870,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
857
870
|
/** Restore mode state from session entries on resume (e.g. plan mode). */
|
|
858
871
|
async #restoreModeFromSession(): Promise<void> {
|
|
859
872
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
873
|
+
if (!this.session.settings.get("plan.enabled")) {
|
|
874
|
+
// Clear stale plan/plan_paused mode so re-enabling the setting
|
|
875
|
+
// later doesn't unexpectedly restore an old plan session.
|
|
876
|
+
if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused") {
|
|
877
|
+
this.sessionManager.appendModeChange("none");
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
860
881
|
if (sessionContext.mode === "plan") {
|
|
861
882
|
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
|
|
862
883
|
await this.#enterPlanMode({ planFilePath });
|
|
@@ -1060,7 +1081,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1060
1081
|
|
|
1061
1082
|
async #approvePlan(
|
|
1062
1083
|
planContent: string,
|
|
1063
|
-
options: { planFilePath: string; finalPlanFilePath: string },
|
|
1084
|
+
options: { planFilePath: string; finalPlanFilePath: string; preserveContext?: boolean },
|
|
1064
1085
|
): Promise<void> {
|
|
1065
1086
|
await renameApprovedPlanFile({
|
|
1066
1087
|
planFilePath: options.planFilePath,
|
|
@@ -1070,14 +1091,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1070
1091
|
});
|
|
1071
1092
|
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
1072
1093
|
await this.#exitPlanMode({ silent: true, paused: false });
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1094
|
+
if (!options.preserveContext) {
|
|
1095
|
+
await this.handleClearCommand();
|
|
1096
|
+
// The new session has a fresh local:// root — persist the approved plan there
|
|
1097
|
+
// so `local://<title>.md` resolves correctly in the execution session.
|
|
1098
|
+
const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
|
|
1099
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1100
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1101
|
+
});
|
|
1102
|
+
await Bun.write(newLocalPath, planContent);
|
|
1103
|
+
}
|
|
1081
1104
|
if (previousTools.length > 0) {
|
|
1082
1105
|
await this.session.setActiveToolsByName(previousTools);
|
|
1083
1106
|
}
|
|
@@ -1086,6 +1109,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1086
1109
|
const planModePrompt = prompt.render(planModeApprovedPrompt, {
|
|
1087
1110
|
planContent,
|
|
1088
1111
|
finalPlanFilePath: options.finalPlanFilePath,
|
|
1112
|
+
contextPreserved: options.preserveContext === true,
|
|
1089
1113
|
});
|
|
1090
1114
|
await this.session.prompt(planModePrompt, { synthetic: true });
|
|
1091
1115
|
}
|
|
@@ -1100,6 +1124,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1100
1124
|
await this.#exitPlanMode({ paused: true });
|
|
1101
1125
|
return;
|
|
1102
1126
|
}
|
|
1127
|
+
if (!this.session.settings.get("plan.enabled")) {
|
|
1128
|
+
this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1103
1131
|
await this.#enterPlanMode();
|
|
1104
1132
|
if (initialPrompt && this.onInputCallback) {
|
|
1105
1133
|
this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
|
|
@@ -1129,14 +1157,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1129
1157
|
this.#renderPlanPreview(planContent);
|
|
1130
1158
|
const choice = await this.showHookSelector(
|
|
1131
1159
|
"Plan mode - next step",
|
|
1132
|
-
["Approve and execute", "Refine plan", "Stay in plan mode"],
|
|
1160
|
+
["Approve and execute", "Approve and keep context", "Refine plan", "Stay in plan mode"],
|
|
1133
1161
|
{
|
|
1134
1162
|
helpText: this.#getPlanReviewHelpText(),
|
|
1135
1163
|
onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
|
|
1136
1164
|
},
|
|
1137
1165
|
);
|
|
1138
1166
|
|
|
1139
|
-
if (choice === "Approve and execute") {
|
|
1167
|
+
if (choice === "Approve and execute" || choice === "Approve and keep context") {
|
|
1140
1168
|
const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
|
|
1141
1169
|
try {
|
|
1142
1170
|
const latestPlanContent = await this.#readPlanFile(planFilePath);
|
|
@@ -1144,7 +1172,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1144
1172
|
this.showError(`Plan file not found at ${planFilePath}`);
|
|
1145
1173
|
return;
|
|
1146
1174
|
}
|
|
1147
|
-
await this.#approvePlan(latestPlanContent, {
|
|
1175
|
+
await this.#approvePlan(latestPlanContent, {
|
|
1176
|
+
planFilePath,
|
|
1177
|
+
finalPlanFilePath,
|
|
1178
|
+
preserveContext: choice === "Approve and keep context",
|
|
1179
|
+
});
|
|
1148
1180
|
} catch (error) {
|
|
1149
1181
|
this.showError(
|
|
1150
1182
|
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -6,7 +6,7 @@ You **MUST NOT**:
|
|
|
6
6
|
- Run state-changing commands (git commit, npm install, etc.)
|
|
7
7
|
- Make any system changes
|
|
8
8
|
|
|
9
|
-
To implement: call `{{exitToolName}}` → user approves
|
|
9
|
+
To implement: call `{{exitToolName}}` → user approves an execution option → full write access is restored to execute the plan.
|
|
10
10
|
You **MUST NOT** ask the user to exit plan mode for you; you **MUST** call `{{exitToolName}}` yourself.
|
|
11
11
|
</critical>
|
|
12
12
|
|
|
@@ -21,7 +21,11 @@ You **MUST** create a plan at `{{planFilePath}}`.
|
|
|
21
21
|
You **MUST** use `{{editToolName}}` for incremental updates; use `{{writeToolName}}` only for create/full replace.
|
|
22
22
|
|
|
23
23
|
<caution>
|
|
24
|
-
|
|
24
|
+
The approval selector includes:
|
|
25
|
+
- **Approve and execute**: starts execution in fresh context (session cleared).
|
|
26
|
+
- **Approve and keep context**: starts execution in this session, preserving exploration history.
|
|
27
|
+
|
|
28
|
+
You **MUST** still make the plan file self-contained: include requirements, decisions, key findings, and remaining todos needed to continue without prior session history.
|
|
25
29
|
</caution>
|
|
26
30
|
|
|
27
31
|
{{#if reentry}}
|
|
@@ -100,7 +104,7 @@ You **MUST** ask questions throughout. You **MUST NOT** make large assumptions a
|
|
|
100
104
|
<critical>
|
|
101
105
|
Your turn ends ONLY by:
|
|
102
106
|
1. Using `{{askToolName}}` to gather information, OR
|
|
103
|
-
2. Calling `{{exitToolName}}` when ready — this triggers user approval, then
|
|
107
|
+
2. Calling `{{exitToolName}}` when ready — this triggers user approval, then implementation with full tool access
|
|
104
108
|
|
|
105
109
|
You **MUST NOT** ask plan approval via text or `{{askToolName}}`; you **MUST** use `{{exitToolName}}`.
|
|
106
110
|
You **MUST** keep going until complete.
|
|
@@ -3,6 +3,11 @@ Plan approved. You **MUST** execute it now.
|
|
|
3
3
|
</critical>
|
|
4
4
|
|
|
5
5
|
Finalized plan artifact: `{{finalPlanFilePath}}`
|
|
6
|
+
{{#if contextPreserved}}
|
|
7
|
+
Context was preserved for execution. Use the existing conversation history when it is useful, and treat the finalized plan as the source of truth if it conflicts with earlier exploration.
|
|
8
|
+
{{else}}
|
|
9
|
+
Execution may be running in fresh context. Treat the finalized plan as the source of truth.
|
|
10
|
+
{{/if}}
|
|
6
11
|
|
|
7
12
|
## Plan
|
|
8
13
|
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
Search hidden
|
|
1
|
+
Search hidden tool metadata to discover and activate tools.
|
|
2
2
|
|
|
3
|
-
Use this tool
|
|
3
|
+
Use this tool when you need a capability that is not currently available in your active tool set. It searches all discoverable tools — including MCP tools and built-in tools that are hidden to save tokens.
|
|
4
4
|
|
|
5
5
|
{{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
6
|
-
{{#if discoverableMCPToolCount}}Total discoverable
|
|
6
|
+
{{#if discoverableMCPToolCount}}Total discoverable tools available: {{discoverableMCPToolCount}}.{{/if}}
|
|
7
7
|
Input:
|
|
8
8
|
- `query` — required natural-language or keyword query
|
|
9
9
|
- `limit` — optional maximum number of tools to return and activate (default `8`)
|
|
10
10
|
|
|
11
11
|
Behavior:
|
|
12
|
-
- Searches hidden
|
|
13
|
-
- Matches against
|
|
14
|
-
- Activates the top matching
|
|
15
|
-
- Repeated searches add to the active
|
|
16
|
-
- Newly activated
|
|
12
|
+
- Searches hidden tool metadata using BM25-style relevance ranking
|
|
13
|
+
- Matches against tool name, label, server name, description/summary, and input schema keys
|
|
14
|
+
- Activates the top matching tools for the rest of the current session
|
|
15
|
+
- Repeated searches add to the active tool set; they do not remove earlier selections
|
|
16
|
+
- Newly activated tools become available before the next model call in the same overall turn
|
|
17
17
|
|
|
18
18
|
Notes:
|
|
19
19
|
- If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
|
|
20
|
-
- `query` is matched against
|
|
20
|
+
- `query` is matched against tool metadata fields:
|
|
21
21
|
- `name`
|
|
22
22
|
- `label`
|
|
23
|
-
- `server_name`
|
|
24
|
-
- `mcp_tool_name`
|
|
25
|
-
- `description`
|
|
23
|
+
- `server_name` (MCP tools)
|
|
24
|
+
- `mcp_tool_name` (MCP tools)
|
|
25
|
+
- `description` / `summary`
|
|
26
26
|
- input schema property keys (`schema_keys`)
|
|
27
27
|
|
|
28
|
-
This is not repository search, file search, or code search. Use it only for
|
|
28
|
+
This is not repository search, file search, or code search. Use it only for tool discovery.
|
|
29
29
|
|
|
30
30
|
Returns JSON with:
|
|
31
31
|
- `query`
|
|
32
|
-
- `activated_tools` —
|
|
32
|
+
- `activated_tools` — tools activated by this search call
|
|
33
33
|
- `match_count` — number of ranked matches returned by the search
|
|
34
34
|
- `total_tools`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Manages a phased task list. Pass `ops`: a flat array of operations.
|
|
2
2
|
The next pending task is auto-promoted to `in_progress` after each completion.
|
|
3
|
+
Allowed `op` values are only `init`, `start`, `done`, `drop`, `rm`, `append`, and `note`. `pending` is a task status, not an `op`; leave not-yet-started tasks implicit in `init`/`append` lists.
|
|
3
4
|
|
|
4
5
|
## Operations
|
|
5
6
|
|