@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +6 -2
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +164 -109
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +2 -3
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
package/src/lsp/client.ts
CHANGED
|
@@ -946,18 +946,28 @@ export async function shutdownClient(key: string): Promise<void> {
|
|
|
946
946
|
// LSP Protocol Methods
|
|
947
947
|
// =============================================================================
|
|
948
948
|
|
|
949
|
-
/** Default timeout for LSP requests (30 seconds) */
|
|
949
|
+
/** Default timeout for LSP requests when no abort signal is provided (30 seconds) */
|
|
950
950
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
951
951
|
|
|
952
952
|
/**
|
|
953
953
|
* Send an LSP request and wait for response.
|
|
954
|
+
*
|
|
955
|
+
* Timeout policy:
|
|
956
|
+
* - If `timeoutMs` is explicitly provided, that value is used.
|
|
957
|
+
* - Else, if `signal` is provided, no internal timer is installed (the caller
|
|
958
|
+
* owns the deadline via the signal — typically a wall-clock `AbortSignal.timeout`
|
|
959
|
+
* from the LSP tool). Installing a second hard-coded 30s timer here used to
|
|
960
|
+
* cause "timed out after 30000ms" errors even when the caller had requested
|
|
961
|
+
* `timeout: 60`.
|
|
962
|
+
* - Else (no signal, no explicit timeout), fall back to `DEFAULT_REQUEST_TIMEOUT_MS`
|
|
963
|
+
* to avoid leaking pending requests forever.
|
|
954
964
|
*/
|
|
955
965
|
export async function sendRequest(
|
|
956
966
|
client: LspClient,
|
|
957
967
|
method: string,
|
|
958
968
|
params: unknown,
|
|
959
969
|
signal?: AbortSignal,
|
|
960
|
-
timeoutMs
|
|
970
|
+
timeoutMs?: number,
|
|
961
971
|
): Promise<unknown> {
|
|
962
972
|
// Atomically increment and capture request ID
|
|
963
973
|
const id = ++client.requestId;
|
|
@@ -993,15 +1003,17 @@ export async function sendRequest(
|
|
|
993
1003
|
reject(reason);
|
|
994
1004
|
};
|
|
995
1005
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
client.pendingRequests.
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1006
|
+
const effectiveTimeoutMs = timeoutMs ?? (signal ? undefined : DEFAULT_REQUEST_TIMEOUT_MS);
|
|
1007
|
+
if (effectiveTimeoutMs !== undefined) {
|
|
1008
|
+
timeout = setTimeout(() => {
|
|
1009
|
+
if (client.pendingRequests.has(id)) {
|
|
1010
|
+
client.pendingRequests.delete(id);
|
|
1011
|
+
const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
|
|
1012
|
+
cleanup();
|
|
1013
|
+
reject(err);
|
|
1014
|
+
}
|
|
1015
|
+
}, effectiveTimeoutMs);
|
|
1016
|
+
}
|
|
1005
1017
|
if (signal) {
|
|
1006
1018
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1007
1019
|
if (signal.aborted) {
|
package/src/lsp/config.ts
CHANGED
|
@@ -450,13 +450,23 @@ export function loadConfig(cwd: string): LspConfig {
|
|
|
450
450
|
*/
|
|
451
451
|
export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
|
|
452
452
|
const ext = path.extname(filePath).toLowerCase();
|
|
453
|
+
const extNoDot = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
453
454
|
const fileName = path.basename(filePath).toLowerCase();
|
|
454
455
|
const matches: Array<[string, ServerConfig]> = [];
|
|
455
456
|
|
|
456
457
|
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
457
458
|
const supportsFile = serverConfig.fileTypes.some(fileType => {
|
|
459
|
+
// Accept both `.ts` and `ts` forms in user config / fixtures so a
|
|
460
|
+
// missing dot in `fileTypes` doesn't silently exclude the server
|
|
461
|
+
// from extension-based routing (e.g. rename_file's relevance filter).
|
|
458
462
|
const normalized = fileType.toLowerCase();
|
|
459
|
-
|
|
463
|
+
const normalizedNoDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
|
|
464
|
+
return (
|
|
465
|
+
normalized === ext ||
|
|
466
|
+
normalized === fileName ||
|
|
467
|
+
normalizedNoDot === extNoDot ||
|
|
468
|
+
normalizedNoDot === fileName
|
|
469
|
+
);
|
|
460
470
|
});
|
|
461
471
|
|
|
462
472
|
if (supportsFile) {
|
package/src/lsp/index.ts
CHANGED
|
@@ -1261,7 +1261,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1261
1261
|
|
|
1262
1262
|
// Status action doesn't need a file
|
|
1263
1263
|
if (action === "status") {
|
|
1264
|
-
const
|
|
1264
|
+
const configuredNames = Object.keys(config.servers);
|
|
1265
1265
|
const lspmuxState = await detectLspmux();
|
|
1266
1266
|
const lspmuxStatus = lspmuxState.available
|
|
1267
1267
|
? lspmuxState.running
|
|
@@ -1269,14 +1269,40 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1269
1269
|
: "lspmux: installed but server not running"
|
|
1270
1270
|
: "";
|
|
1271
1271
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1272
|
+
// `Object.keys(config.servers)` reflects what is *configured & resolvable
|
|
1273
|
+
// on PATH* — it does NOT prove the server actually starts. A wrapper
|
|
1274
|
+
// binary that exits immediately (e.g. rustup without the rust-analyzer
|
|
1275
|
+
// component) still appears here. Distinguish "configured" from
|
|
1276
|
+
// "started" (have a live in-process client) so callers cannot mistake
|
|
1277
|
+
// presence-on-PATH for a working server.
|
|
1278
|
+
const startedClients = getActiveClients();
|
|
1279
|
+
const startedByConfigName = new Map<string, LspServerStatus>();
|
|
1280
|
+
// getActiveClients() reports `name = client.config.command` (the
|
|
1281
|
+
// unresolved binary name from defaults.json), so match against
|
|
1282
|
+
// `serverConfig.command`, not the resolved path.
|
|
1283
|
+
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
1284
|
+
const matched = startedClients.find(c => c.name === serverConfig.command);
|
|
1285
|
+
if (matched) startedByConfigName.set(name, matched);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const lines: string[] = [];
|
|
1289
|
+
if (configuredNames.length === 0) {
|
|
1290
|
+
lines.push("No language servers configured for this project");
|
|
1291
|
+
} else {
|
|
1292
|
+
const labelled = configuredNames.map(name => {
|
|
1293
|
+
const started = startedByConfigName.get(name);
|
|
1294
|
+
if (!started) return `${name} (configured, not started)`;
|
|
1295
|
+
return `${name} (${started.status})`;
|
|
1296
|
+
});
|
|
1297
|
+
lines.push(`Language servers: ${labelled.join(", ")}`);
|
|
1298
|
+
lines.push(
|
|
1299
|
+
" note: 'configured, not started' means the binary resolves on PATH but no request has spawned it yet; 'ready' means a client process is live for this cwd.",
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (lspmuxStatus) lines.push(lspmuxStatus);
|
|
1276
1303
|
|
|
1277
|
-
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
1278
1304
|
return {
|
|
1279
|
-
content: [{ type: "text", text:
|
|
1305
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1280
1306
|
details: { action, success: true, request: params },
|
|
1281
1307
|
};
|
|
1282
1308
|
}
|
|
@@ -1505,7 +1531,26 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1505
1531
|
}
|
|
1506
1532
|
|
|
1507
1533
|
const lspParams = { files: pairs };
|
|
1508
|
-
|
|
1534
|
+
// Filter to servers whose fileTypes match either the source or any
|
|
1535
|
+
// destination path. Asking every configured server about a .md/.sql/.txt
|
|
1536
|
+
// rename used to stack up willRenameFiles requests against irrelevant
|
|
1537
|
+
// language servers and hit the wall-clock timeout. A server only has
|
|
1538
|
+
// something useful to say about a rename if it understands one of the
|
|
1539
|
+
// affected file extensions.
|
|
1540
|
+
const allLspServers = getLspServers(config);
|
|
1541
|
+
const relevantNames = new Set<string>();
|
|
1542
|
+
const collectRelevant = (filePath: string) => {
|
|
1543
|
+
for (const [name] of getLspServersForFile(config, filePath)) {
|
|
1544
|
+
relevantNames.add(name);
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
collectRelevant(source);
|
|
1548
|
+
collectRelevant(dest);
|
|
1549
|
+
for (const pair of pairs) {
|
|
1550
|
+
collectRelevant(uriToFile(pair.oldUri));
|
|
1551
|
+
collectRelevant(uriToFile(pair.newUri));
|
|
1552
|
+
}
|
|
1553
|
+
const servers = allLspServers.filter(([name]) => relevantNames.has(name));
|
|
1509
1554
|
const respondingServers = new Set<string>();
|
|
1510
1555
|
const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
|
|
1511
1556
|
const serverNotes: string[] = [];
|
|
@@ -1829,8 +1874,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1829
1874
|
throw new ToolAbortError();
|
|
1830
1875
|
}
|
|
1831
1876
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1877
|
+
// Echo a (truncated) preview of the params we sent so the caller can
|
|
1878
|
+
// tell parse / shape errors (e.g. nested args dropped, missing field)
|
|
1879
|
+
// apart from genuine server errors without spinning up another debug call.
|
|
1880
|
+
const previewRaw = JSON.stringify(requestParams ?? null);
|
|
1881
|
+
const preview = previewRaw.length > 400 ? `${previewRaw.slice(0, 397)}...` : previewRaw;
|
|
1832
1882
|
return {
|
|
1833
|
-
content: [
|
|
1883
|
+
content: [
|
|
1884
|
+
{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}\n params: ${preview}` },
|
|
1885
|
+
],
|
|
1834
1886
|
details: { action, serverName: chosenName, success: false, request: params },
|
|
1835
1887
|
};
|
|
1836
1888
|
}
|
package/src/lsp/utils.ts
CHANGED
|
@@ -153,9 +153,10 @@ export function formatDiagnostic(diagnostic: Diagnostic, filePath: string): stri
|
|
|
153
153
|
const DIAG_PATH_RE = /^(.+?):(\d+:\d+\s+.*)$/;
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* Reformat pre-formatted diagnostic messages into
|
|
156
|
+
* Reformat pre-formatted diagnostic messages into a multi-level, prefix-folded
|
|
157
|
+
* directory/file grouping (see `formatGroupedFiles`).
|
|
157
158
|
* Input: ["path:line:col [sev] msg", ...]
|
|
158
|
-
* Output: "#
|
|
159
|
+
* Output: "# pkg/src/\n## file.ts\n line:col [sev] msg"
|
|
159
160
|
*
|
|
160
161
|
* Messages that don't match the expected format are appended ungrouped at the end.
|
|
161
162
|
*/
|
package/src/main.ts
CHANGED
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* This file handles CLI argument parsing and translates them into
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as fsSync from "node:fs";
|
|
9
8
|
import * as os from "node:os";
|
|
10
|
-
import * as path from "node:path";
|
|
11
9
|
import { createInterface } from "node:readline/promises";
|
|
12
10
|
import { EventLoopKeepalive } from "@oh-my-pi/pi-agent-core";
|
|
13
11
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
@@ -28,9 +26,16 @@ import { processFileArguments } from "./cli/file-processor";
|
|
|
28
26
|
import { buildInitialMessage } from "./cli/initial-message";
|
|
29
27
|
import { runListModelsCommand } from "./cli/list-models";
|
|
30
28
|
import { selectSession } from "./cli/session-picker";
|
|
29
|
+
import { applyStartupCwd } from "./cli/startup-cwd";
|
|
31
30
|
import { findConfigFile } from "./config";
|
|
32
31
|
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
getModelMatchPreferences,
|
|
34
|
+
resolveCliModel,
|
|
35
|
+
resolveModelRoleValue,
|
|
36
|
+
resolveModelScope,
|
|
37
|
+
type ScopedModel,
|
|
38
|
+
} from "./config/model-resolver";
|
|
34
39
|
import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
|
|
35
40
|
import { initializeWithSettings } from "./discovery";
|
|
36
41
|
import {
|
|
@@ -278,7 +283,10 @@ async function runInteractiveMode(
|
|
|
278
283
|
})
|
|
279
284
|
: [];
|
|
280
285
|
|
|
281
|
-
await mode.init({
|
|
286
|
+
await mode.init({
|
|
287
|
+
suppressWelcomeIntro: resuming || setupScenes.length > 0,
|
|
288
|
+
clearInitialTerminalHistory: true,
|
|
289
|
+
});
|
|
282
290
|
|
|
283
291
|
if (setupWizard && setupScenes.length > 0) {
|
|
284
292
|
await setupWizard.runSetupWizard(mode, setupScenes);
|
|
@@ -295,12 +303,11 @@ async function runInteractiveMode(
|
|
|
295
303
|
})
|
|
296
304
|
.catch(() => {});
|
|
297
305
|
|
|
298
|
-
// Cold-launch cleanup:
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// the
|
|
302
|
-
//
|
|
303
|
-
// launch is the lone path that did not.
|
|
306
|
+
// Cold-launch cleanup: the first paint already clears native history, and this
|
|
307
|
+
// replay replaces the welcome/startup frame with the resumed/new transcript.
|
|
308
|
+
// Every in-process session load also uses `clearTerminalHistory`; cold launch
|
|
309
|
+
// follows the same clean-cutover path instead of preserving a previous run's
|
|
310
|
+
// transcript above the fresh one.
|
|
304
311
|
mode.renderInitialMessages(undefined, { preserveExistingChat: true, clearTerminalHistory: true });
|
|
305
312
|
|
|
306
313
|
for (const notify of notifs) {
|
|
@@ -342,11 +349,11 @@ async function runInteractiveMode(
|
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
|
|
345
|
-
type
|
|
352
|
+
type SessionPromptResult = "accepted" | "declined" | "unavailable";
|
|
346
353
|
|
|
347
|
-
type
|
|
354
|
+
type SessionPrompt = (session: SessionInfo) => Promise<SessionPromptResult>;
|
|
348
355
|
|
|
349
|
-
async function promptForkSession(session: SessionInfo): Promise<
|
|
356
|
+
async function promptForkSession(session: SessionInfo): Promise<SessionPromptResult> {
|
|
350
357
|
if (!process.stdin.isTTY) {
|
|
351
358
|
return "unavailable";
|
|
352
359
|
}
|
|
@@ -360,6 +367,52 @@ async function promptForkSession(session: SessionInfo): Promise<ForkSessionPromp
|
|
|
360
367
|
}
|
|
361
368
|
}
|
|
362
369
|
|
|
370
|
+
async function promptMoveSession(session: SessionInfo): Promise<SessionPromptResult> {
|
|
371
|
+
if (!process.stdin.isTTY) {
|
|
372
|
+
return "unavailable";
|
|
373
|
+
}
|
|
374
|
+
const message = `Session's directory no longer exists (${session.cwd}). Move (re-root) it into the current directory? [Y/n] `;
|
|
375
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
376
|
+
try {
|
|
377
|
+
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
378
|
+
return answer === "" || answer === "y" || answer === "yes" ? "accepted" : "declined";
|
|
379
|
+
} finally {
|
|
380
|
+
rl.close();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
type MissingCwdMoveResult =
|
|
385
|
+
| { status: "not-needed" }
|
|
386
|
+
| { status: "declined" }
|
|
387
|
+
| { status: "moved"; manager: SessionManager };
|
|
388
|
+
|
|
389
|
+
async function moveMissingCwdSessionIfNeeded(
|
|
390
|
+
sessionArg: string,
|
|
391
|
+
session: SessionInfo,
|
|
392
|
+
cwd: string,
|
|
393
|
+
sessionDir: string | undefined,
|
|
394
|
+
askToMoveSession: SessionPrompt,
|
|
395
|
+
): Promise<MissingCwdMoveResult> {
|
|
396
|
+
const sourceCwd = session.cwd;
|
|
397
|
+
if (!sourceCwd || fsSync.existsSync(sourceCwd)) {
|
|
398
|
+
return { status: "not-needed" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const movePromptResult = await askToMoveSession(session);
|
|
402
|
+
if (movePromptResult === "unavailable") {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Session "${sessionArg}" belongs to a directory that no longer exists (${sourceCwd}); run interactively to move it into the current project.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (movePromptResult === "declined") {
|
|
408
|
+
return { status: "declined" };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const manager = await SessionManager.open(session.path, sessionDir);
|
|
412
|
+
await manager.moveTo(cwd, sessionDir);
|
|
413
|
+
return { status: "moved", manager };
|
|
414
|
+
}
|
|
415
|
+
|
|
363
416
|
async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
|
|
364
417
|
if (parsed.continue || parsed.resume) {
|
|
365
418
|
return undefined;
|
|
@@ -405,7 +458,8 @@ export async function createSessionManager(
|
|
|
405
458
|
parsed: Args,
|
|
406
459
|
cwd: string,
|
|
407
460
|
activeSettings: Settings = settings,
|
|
408
|
-
askToForkSession:
|
|
461
|
+
askToForkSession: SessionPrompt = promptForkSession,
|
|
462
|
+
askToMoveSession: SessionPrompt = promptMoveSession,
|
|
409
463
|
): Promise<SessionManager | undefined> {
|
|
410
464
|
if (parsed.fork) {
|
|
411
465
|
if (parsed.noSession) {
|
|
@@ -434,10 +488,38 @@ export async function createSessionManager(
|
|
|
434
488
|
if (!match) {
|
|
435
489
|
throw new Error(`Session "${sessionArg}" not found.`);
|
|
436
490
|
}
|
|
491
|
+
if (match.scope === "local") {
|
|
492
|
+
const moveResult = await moveMissingCwdSessionIfNeeded(
|
|
493
|
+
sessionArg,
|
|
494
|
+
match.session,
|
|
495
|
+
cwd,
|
|
496
|
+
parsed.sessionDir,
|
|
497
|
+
askToMoveSession,
|
|
498
|
+
);
|
|
499
|
+
if (moveResult.status === "moved") {
|
|
500
|
+
return moveResult.manager;
|
|
501
|
+
}
|
|
502
|
+
if (moveResult.status === "declined") {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
437
506
|
if (match.scope === "global") {
|
|
438
507
|
const normalizedCwd = normalizePathForComparison(cwd);
|
|
439
508
|
const normalizedMatchCwd = normalizePathForComparison(match.session.cwd || cwd);
|
|
440
509
|
if (normalizedCwd !== normalizedMatchCwd) {
|
|
510
|
+
const moveResult = await moveMissingCwdSessionIfNeeded(
|
|
511
|
+
sessionArg,
|
|
512
|
+
match.session,
|
|
513
|
+
cwd,
|
|
514
|
+
parsed.sessionDir,
|
|
515
|
+
askToMoveSession,
|
|
516
|
+
);
|
|
517
|
+
if (moveResult.status === "moved") {
|
|
518
|
+
return moveResult.manager;
|
|
519
|
+
}
|
|
520
|
+
if (moveResult.status === "declined") {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
441
523
|
const forkPromptResult = await askToForkSession(match.session);
|
|
442
524
|
if (forkPromptResult === "unavailable") {
|
|
443
525
|
throw new Error(
|
|
@@ -478,56 +560,6 @@ export async function createSessionManager(
|
|
|
478
560
|
return undefined;
|
|
479
561
|
}
|
|
480
562
|
|
|
481
|
-
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
482
|
-
if (parsed.allowHome || parsed.cwd) {
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const home = os.homedir();
|
|
487
|
-
if (!home) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const normalizePath = normalizePathForComparison;
|
|
492
|
-
|
|
493
|
-
const cwd = normalizePath(getProjectDir());
|
|
494
|
-
const normalizedHome = normalizePath(home);
|
|
495
|
-
if (cwd !== normalizedHome) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const isDirectory = async (p: string) => {
|
|
500
|
-
try {
|
|
501
|
-
const s = await fs.stat(p);
|
|
502
|
-
return s.isDirectory();
|
|
503
|
-
} catch {
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
|
|
509
|
-
for (const candidate of candidates) {
|
|
510
|
-
try {
|
|
511
|
-
if (!(await isDirectory(candidate))) {
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
setProjectDir(candidate);
|
|
515
|
-
return;
|
|
516
|
-
} catch {
|
|
517
|
-
// Try next candidate.
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
const fallback = os.tmpdir();
|
|
523
|
-
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
|
524
|
-
setProjectDir(fallback);
|
|
525
|
-
}
|
|
526
|
-
} catch {
|
|
527
|
-
// Ignore fallback errors.
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
563
|
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
|
532
564
|
function discoverSystemPromptFile(): string | undefined {
|
|
533
565
|
// Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
|
|
@@ -584,9 +616,7 @@ async function buildSessionOptions(
|
|
|
584
616
|
// Model from CLI
|
|
585
617
|
// - supports --provider <name> --model <pattern>
|
|
586
618
|
// - supports --model <provider>/<pattern>
|
|
587
|
-
const modelMatchPreferences =
|
|
588
|
-
usageOrder: activeSettings.getStorage()?.getModelUsageOrder(),
|
|
589
|
-
};
|
|
619
|
+
const modelMatchPreferences = getModelMatchPreferences(activeSettings);
|
|
590
620
|
if (parsed.model) {
|
|
591
621
|
const resolved = resolveCliModel({
|
|
592
622
|
cliProvider: parsed.provider,
|
|
@@ -743,7 +773,7 @@ export async function runRootCommand(
|
|
|
743
773
|
await logger.time("initTheme:initial", initTheme);
|
|
744
774
|
|
|
745
775
|
const parsedArgs = parsed;
|
|
746
|
-
await logger.time("
|
|
776
|
+
await logger.time("applyStartupCwd", applyStartupCwd, parsedArgs);
|
|
747
777
|
|
|
748
778
|
const notifs: (InteractiveModeNotify | null)[] = [];
|
|
749
779
|
|
|
@@ -878,9 +908,7 @@ export async function runRootCommand(
|
|
|
878
908
|
|
|
879
909
|
let scopedModels: ScopedModel[] = [];
|
|
880
910
|
const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
|
|
881
|
-
const modelMatchPreferences =
|
|
882
|
-
usageOrder: settingsInstance.getStorage()?.getModelUsageOrder(),
|
|
883
|
-
};
|
|
911
|
+
const modelMatchPreferences = getModelMatchPreferences(settingsInstance);
|
|
884
912
|
if (modelPatterns && modelPatterns.length > 0) {
|
|
885
913
|
scopedModels = await logger.time(
|
|
886
914
|
"resolveModelScope",
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -220,6 +220,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
220
220
|
readonly mcpToolName: string;
|
|
221
221
|
/** Server name */
|
|
222
222
|
readonly mcpServerName: string;
|
|
223
|
+
readonly approval = "write" as const;
|
|
223
224
|
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
224
225
|
readonly mergeCallAndResult = true;
|
|
225
226
|
|
|
@@ -305,6 +306,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
305
306
|
readonly mcpToolName: string;
|
|
306
307
|
/** Server name */
|
|
307
308
|
readonly mcpServerName: string;
|
|
309
|
+
readonly approval = "write" as const;
|
|
308
310
|
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
309
311
|
readonly mergeCallAndResult = true;
|
|
310
312
|
|
package/src/memories/index.ts
CHANGED
|
@@ -3,10 +3,11 @@ import type * as fsNode from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
|
-
import { clampThinkingLevelForModel, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { type ApiKey, clampThinkingLevelForModel, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
|
|
8
|
+
|
|
8
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
9
|
-
import { resolveModelRoleValue } from "../config/model-resolver";
|
|
10
|
+
import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
|
|
10
11
|
import type { Settings } from "../config/settings";
|
|
11
12
|
import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
|
|
12
13
|
import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
|
|
@@ -271,7 +272,10 @@ async function runPhase1(options: {
|
|
|
271
272
|
const result = await runStage1Job({
|
|
272
273
|
claim,
|
|
273
274
|
model: phase1Model,
|
|
274
|
-
apiKey:
|
|
275
|
+
apiKey: modelRegistry.resolver(phase1Model.provider, {
|
|
276
|
+
sessionId: session.sessionId,
|
|
277
|
+
baseUrl: phase1Model.baseUrl,
|
|
278
|
+
}),
|
|
275
279
|
modelMaxTokens: computeModelTokenBudget(phase1Model, config),
|
|
276
280
|
config,
|
|
277
281
|
metadata: session.agent?.metadataForProvider(phase1Model.provider),
|
|
@@ -428,7 +432,10 @@ async function runPhase2(options: {
|
|
|
428
432
|
const consolidated = await runConsolidationModel({
|
|
429
433
|
memoryRoot,
|
|
430
434
|
model: phase2Model,
|
|
431
|
-
apiKey:
|
|
435
|
+
apiKey: modelRegistry.resolver(phase2Model.provider, {
|
|
436
|
+
sessionId: session.sessionId,
|
|
437
|
+
baseUrl: phase2Model.baseUrl,
|
|
438
|
+
}),
|
|
432
439
|
metadata: session.agent?.metadataForProvider(phase2Model.provider),
|
|
433
440
|
});
|
|
434
441
|
await applyConsolidation(memoryRoot, consolidated);
|
|
@@ -574,7 +581,7 @@ function extractPersistableMessages(payload: string): AgentMessage[] {
|
|
|
574
581
|
async function runStage1Job(options: {
|
|
575
582
|
claim: Stage1Claim;
|
|
576
583
|
model: Model;
|
|
577
|
-
apiKey:
|
|
584
|
+
apiKey: ApiKey;
|
|
578
585
|
modelMaxTokens: number;
|
|
579
586
|
config: MemoryRuntimeConfig;
|
|
580
587
|
metadata?: Record<string, unknown>;
|
|
@@ -718,7 +725,7 @@ async function readRolloutSummaries(memoryRoot: string): Promise<string> {
|
|
|
718
725
|
async function runConsolidationModel(options: {
|
|
719
726
|
memoryRoot: string;
|
|
720
727
|
model: Model;
|
|
721
|
-
apiKey:
|
|
728
|
+
apiKey: ApiKey;
|
|
722
729
|
metadata?: Record<string, unknown>;
|
|
723
730
|
}): Promise<{
|
|
724
731
|
memoryMd: string;
|
|
@@ -1081,7 +1088,7 @@ async function resolveMemoryModel(options: {
|
|
|
1081
1088
|
if (requestedModel) {
|
|
1082
1089
|
const resolved = resolveModelRoleValue(requestedModel, modelRegistry.getAll(), {
|
|
1083
1090
|
settings: session.settings,
|
|
1084
|
-
matchPreferences:
|
|
1091
|
+
matchPreferences: getModelMatchPreferences(session.settings),
|
|
1085
1092
|
modelRegistry,
|
|
1086
1093
|
});
|
|
1087
1094
|
if (resolved.model) return resolved.model;
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Mnemopi } from "@oh-my-pi/pi-mnemopi";
|
|
|
5
5
|
import { BankManager } from "@oh-my-pi/pi-mnemopi/core";
|
|
6
6
|
import { type DiagnosticSummary, inspectDatabase } from "@oh-my-pi/pi-mnemopi/diagnose";
|
|
7
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
|
+
|
|
8
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
9
10
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
10
11
|
import type { MemoryBackend, MemoryBackendStartOptions } from "../memory-backend/types";
|
|
@@ -334,7 +335,10 @@ async function resolveMnemopiProviderOptions(
|
|
|
334
335
|
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
335
336
|
},
|
|
336
337
|
{
|
|
337
|
-
apiKey,
|
|
338
|
+
apiKey: modelRegistry.resolver(model.provider, {
|
|
339
|
+
sessionId,
|
|
340
|
+
baseUrl: model.baseUrl,
|
|
341
|
+
}),
|
|
338
342
|
maxTokens: opts?.maxTokens,
|
|
339
343
|
temperature: opts?.temperature,
|
|
340
344
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
type Agent,
|
|
@@ -62,7 +63,7 @@ import { MCPManager } from "../../mcp/manager";
|
|
|
62
63
|
import type { MCPServerConfig } from "../../mcp/types";
|
|
63
64
|
import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
|
|
64
65
|
import { theme } from "../../modes/theme/theme";
|
|
65
|
-
import { type PlanApprovalDetails,
|
|
66
|
+
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
66
67
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
67
68
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
68
69
|
import {
|
|
@@ -1425,24 +1426,16 @@ export class AcpAgent implements Agent {
|
|
|
1425
1426
|
if (!state?.enabled) {
|
|
1426
1427
|
throw new ToolError("Plan mode is not active.");
|
|
1427
1428
|
}
|
|
1428
|
-
const planFilePath =
|
|
1429
|
-
const planContent = await this.#readAcpPlanFile(session, planFilePath);
|
|
1430
|
-
if (planContent === null) {
|
|
1431
|
-
throw new ToolError(
|
|
1432
|
-
`Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
const normalized = resolvePlanTitle({
|
|
1429
|
+
const { planFilePath, planContent, title } = await resolveApprovedPlan({
|
|
1436
1430
|
suppliedTitle: extra?.title,
|
|
1437
|
-
|
|
1438
|
-
|
|
1431
|
+
statePlanFilePath: state.planFilePath,
|
|
1432
|
+
readPlan: url => this.#readAcpPlanFile(session, url),
|
|
1433
|
+
listPlanFiles: () => this.#listAcpLocalPlanFiles(session),
|
|
1439
1434
|
});
|
|
1440
|
-
const
|
|
1441
|
-
const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, normalized.title, planContent);
|
|
1435
|
+
const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, title, planContent);
|
|
1442
1436
|
const details: PlanApprovalDetails = {
|
|
1443
1437
|
planFilePath,
|
|
1444
|
-
|
|
1445
|
-
title: normalized.title,
|
|
1438
|
+
title,
|
|
1446
1439
|
planExists: true,
|
|
1447
1440
|
};
|
|
1448
1441
|
if (!approved) {
|
|
@@ -1458,16 +1451,10 @@ export class AcpAgent implements Agent {
|
|
|
1458
1451
|
details,
|
|
1459
1452
|
};
|
|
1460
1453
|
}
|
|
1461
|
-
// Approved.
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
|
|
1465
|
-
planFilePath,
|
|
1466
|
-
finalPlanFilePath,
|
|
1467
|
-
getArtifactsDir: () => session.sessionManager.getArtifactsDir(),
|
|
1468
|
-
getSessionId: () => session.sessionManager.getSessionId(),
|
|
1469
|
-
});
|
|
1470
|
-
session.setPlanReferencePath(finalPlanFilePath);
|
|
1454
|
+
// Approved. Set the plan reference so the next turn injects the plan
|
|
1455
|
+
// content as context (the file keeps its agent-chosen name — no
|
|
1456
|
+
// rename), then exit plan mode so the agent regains full tools.
|
|
1457
|
+
session.setPlanReferencePath(planFilePath);
|
|
1471
1458
|
session.setStandingResolveHandler?.(null);
|
|
1472
1459
|
session.setPlanModeState(undefined);
|
|
1473
1460
|
try {
|
|
@@ -1486,7 +1473,7 @@ export class AcpAgent implements Agent {
|
|
|
1486
1473
|
content: [
|
|
1487
1474
|
{
|
|
1488
1475
|
type: "text" as const,
|
|
1489
|
-
text: `Plan approved at ${
|
|
1476
|
+
text: `Plan approved at ${planFilePath}. Plan mode exited; proceed with the implementation.`,
|
|
1490
1477
|
},
|
|
1491
1478
|
],
|
|
1492
1479
|
details,
|
|
@@ -1518,6 +1505,26 @@ export class AcpAgent implements Agent {
|
|
|
1518
1505
|
}
|
|
1519
1506
|
}
|
|
1520
1507
|
|
|
1508
|
+
/** `local://` URLs of plan files in the session-local root, newest first —
|
|
1509
|
+
* the `resolveApprovedPlan` fallback for a dropped `extra.title`. */
|
|
1510
|
+
async #listAcpLocalPlanFiles(session: AgentSession): Promise<string[]> {
|
|
1511
|
+
const localRoot = this.#resolveAcpPlanFilePath(session, "local://");
|
|
1512
|
+
try {
|
|
1513
|
+
const entries = await fs.readdir(localRoot, { withFileTypes: true });
|
|
1514
|
+
const plans = await Promise.all(
|
|
1515
|
+
entries
|
|
1516
|
+
.filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
|
|
1517
|
+
.map(async entry => {
|
|
1518
|
+
const stat = await fs.stat(path.join(localRoot, entry.name)).catch(() => null);
|
|
1519
|
+
return { url: `local://${entry.name}`, mtime: stat?.mtimeMs ?? 0 };
|
|
1520
|
+
}),
|
|
1521
|
+
);
|
|
1522
|
+
return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
|
|
1523
|
+
} catch {
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1521
1528
|
/**
|
|
1522
1529
|
* Ask the ACP client to confirm plan approval. Returns `true` only on an
|
|
1523
1530
|
* explicit `APPROVE_OPTION` selection. Refine, dismissal (`undefined`), or
|