@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.1
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/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +15 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/commands/completions.ts +1 -1
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/exa/mcp-client.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +22 -4
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +10 -5
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +5 -21
- package/src/session/agent-session.ts +30 -7
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/tiny/text.ts +112 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/title-generator.ts +9 -2
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -116,10 +116,12 @@ function buildResult(
|
|
|
116
116
|
provider,
|
|
117
117
|
providerName,
|
|
118
118
|
};
|
|
119
|
+
const contentText = result.isError ? `Error: ${text}` : text;
|
|
120
|
+
const toolResult: CustomToolResult<MCPToolDetails> = { content: [{ type: "text", text: contentText }], details };
|
|
119
121
|
if (result.isError) {
|
|
120
|
-
|
|
122
|
+
toolResult.isError = true;
|
|
121
123
|
}
|
|
122
|
-
return
|
|
124
|
+
return toolResult;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/** Build an error CustomToolResult from a caught exception. */
|
|
@@ -134,6 +136,7 @@ function buildErrorResult(
|
|
|
134
136
|
return {
|
|
135
137
|
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
136
138
|
details: { serverName, mcpToolName, isError: true, provider, providerName },
|
|
139
|
+
isError: true,
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -217,6 +220,8 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
217
220
|
readonly mcpToolName: string;
|
|
218
221
|
/** Server name */
|
|
219
222
|
readonly mcpServerName: string;
|
|
223
|
+
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
224
|
+
readonly mergeCallAndResult = true;
|
|
220
225
|
|
|
221
226
|
/** Create MCPTool instances for all tools from an MCP server connection */
|
|
222
227
|
static fromTools(connection: MCPServerConnection, tools: MCPToolDefinition[], reconnect?: MCPReconnect): MCPTool[] {
|
|
@@ -300,6 +305,9 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
300
305
|
readonly mcpToolName: string;
|
|
301
306
|
/** Server name */
|
|
302
307
|
readonly mcpServerName: string;
|
|
308
|
+
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
309
|
+
readonly mergeCallAndResult = true;
|
|
310
|
+
|
|
303
311
|
readonly #fallbackProvider: string | undefined;
|
|
304
312
|
readonly #fallbackProviderName: string | undefined;
|
|
305
313
|
|
|
@@ -87,45 +87,62 @@ export class HttpTransport implements MCPTransport {
|
|
|
87
87
|
headers["Mcp-Session-Id"] = this.#sessionId;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
let response: Response;
|
|
90
|
+
let response: Response | null;
|
|
91
91
|
let timedOut = false;
|
|
92
|
+
let startupFinished = false;
|
|
93
|
+
const connection = this.#sseConnection;
|
|
92
94
|
const startupTimeoutMs = resolveSSEConnectTimeoutMs(this.config.timeout);
|
|
93
|
-
const
|
|
95
|
+
const fetchPromise = fetch(this.config.url, {
|
|
96
|
+
method: "GET",
|
|
97
|
+
headers,
|
|
98
|
+
signal: connection.signal,
|
|
99
|
+
});
|
|
100
|
+
const timeoutPromise =
|
|
94
101
|
startupTimeoutMs > 0
|
|
95
|
-
?
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
? new Promise<null>(resolve => {
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
if (!startupFinished) {
|
|
105
|
+
timedOut = true;
|
|
106
|
+
connection.abort();
|
|
107
|
+
}
|
|
108
|
+
resolve(null);
|
|
109
|
+
}, startupTimeoutMs);
|
|
110
|
+
})
|
|
99
111
|
: null;
|
|
100
112
|
try {
|
|
101
|
-
response = await
|
|
102
|
-
method: "GET",
|
|
103
|
-
headers,
|
|
104
|
-
signal: this.#sseConnection.signal,
|
|
105
|
-
});
|
|
113
|
+
response = timeoutPromise === null ? await fetchPromise : await Promise.race([fetchPromise, timeoutPromise]);
|
|
106
114
|
} catch (error) {
|
|
107
|
-
this.#sseConnection = null;
|
|
115
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
108
116
|
if (error instanceof Error && error.name !== "AbortError" && !timedOut) {
|
|
109
117
|
this.onError?.(error);
|
|
110
118
|
}
|
|
111
119
|
return;
|
|
112
120
|
} finally {
|
|
113
|
-
|
|
121
|
+
startupFinished = true;
|
|
122
|
+
}
|
|
123
|
+
if (response === null) {
|
|
124
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
125
|
+
void fetchPromise.then(lateResponse => lateResponse.body?.cancel()).catch(() => {});
|
|
126
|
+
return;
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
if (this.#sseConnection !== connection) {
|
|
130
|
+
await response.body?.cancel();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
116
133
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
117
134
|
await response.body?.cancel();
|
|
118
|
-
this.#sseConnection = null;
|
|
135
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
119
136
|
return;
|
|
120
137
|
}
|
|
121
138
|
|
|
122
139
|
// Connection established — read messages in background.
|
|
123
140
|
// If the stream ends unexpectedly (server restart, network drop),
|
|
124
141
|
// fire onClose so the manager can trigger reconnection.
|
|
125
|
-
const signal =
|
|
142
|
+
const signal = connection.signal;
|
|
126
143
|
void this.#readSSEStream(response.body!, signal).finally(() => {
|
|
127
144
|
const wasConnected = this.#connected;
|
|
128
|
-
this.#sseConnection = null;
|
|
145
|
+
if (this.#sseConnection === connection) this.#sseConnection = null;
|
|
129
146
|
if (wasConnected) this.onClose?.();
|
|
130
147
|
});
|
|
131
148
|
}
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -173,7 +173,7 @@ export class MnemopiSessionState {
|
|
|
173
173
|
return lines.join("\n\n");
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
collectScopedRecallResults(query: string): RecallResult[] {
|
|
176
|
+
async collectScopedRecallResults(query: string): Promise<RecallResult[]> {
|
|
177
177
|
const merged: RecallResult[] = [];
|
|
178
178
|
const byId = new Map<string, number>();
|
|
179
179
|
const byContent = new Map<string, number>();
|
|
@@ -187,7 +187,7 @@ export class MnemopiSessionState {
|
|
|
187
187
|
target.bank === this.scoped.global?.bank && sharedFallbackQuery ? [query, sharedFallbackQuery] : [query];
|
|
188
188
|
try {
|
|
189
189
|
for (const recallQuery of queries) {
|
|
190
|
-
const results = target.memory.recallEnhanced(recallQuery, this.config.recallLimit, {
|
|
190
|
+
const results = await target.memory.recallEnhanced(recallQuery, this.config.recallLimit, {
|
|
191
191
|
includeFacts: true,
|
|
192
192
|
channelId: target.bank,
|
|
193
193
|
});
|
|
@@ -209,7 +209,7 @@ export class MnemopiSessionState {
|
|
|
209
209
|
return merged;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
recallResultsScoped(query: string): RecallResult[] {
|
|
212
|
+
recallResultsScoped(query: string): Promise<RecallResult[]> {
|
|
213
213
|
return this.collectScopedRecallResults(query);
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -242,7 +242,7 @@ export class MnemopiSessionState {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
async recallForContext(query: string): Promise<string | undefined> {
|
|
245
|
-
const results = this.collectScopedRecallResults(query);
|
|
245
|
+
const results = await this.collectScopedRecallResults(query);
|
|
246
246
|
if (results.length === 0) return undefined;
|
|
247
247
|
return formatRecallBlock(results);
|
|
248
248
|
}
|
|
@@ -42,8 +42,9 @@ import {
|
|
|
42
42
|
type SetSessionModeResponse,
|
|
43
43
|
type Usage,
|
|
44
44
|
} from "@agentclientprotocol/sdk";
|
|
45
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
45
46
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
46
|
-
import { logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
47
|
+
import { isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
47
48
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
48
49
|
import { Settings } from "../../config/settings";
|
|
49
50
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -56,10 +57,12 @@ import { runExtensionCompact } from "../../extensibility/extensions/compact-hand
|
|
|
56
57
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
57
58
|
import { buildSkillPromptMessage, getSkillSlashCommandName } from "../../extensibility/skills";
|
|
58
59
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
60
|
+
import { resolveLocalUrlToPath } from "../../internal-urls";
|
|
59
61
|
import { MCPManager } from "../../mcp/manager";
|
|
60
62
|
import type { MCPServerConfig } from "../../mcp/types";
|
|
61
63
|
import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
|
|
62
64
|
import { theme } from "../../modes/theme/theme";
|
|
65
|
+
import { type PlanApprovalDetails, renameApprovedPlanFile, resolvePlanTitle } from "../../plan-mode/approved-plan";
|
|
63
66
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
64
67
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
65
68
|
import {
|
|
@@ -69,6 +72,9 @@ import {
|
|
|
69
72
|
} from "../../session/session-manager";
|
|
70
73
|
import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
71
74
|
import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
|
75
|
+
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
76
|
+
import { runResolveInvocation } from "../../tools/resolve";
|
|
77
|
+
import { ToolError } from "../../tools/tool-errors";
|
|
72
78
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
73
79
|
import {
|
|
74
80
|
buildToolCallStartUpdate,
|
|
@@ -80,6 +86,8 @@ import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
|
80
86
|
const ACP_DEFAULT_MODE_ID = "default";
|
|
81
87
|
const ACP_PLAN_MODE_ID = "plan";
|
|
82
88
|
const DEFAULT_PLAN_FILE_URL = "local://PLAN.md";
|
|
89
|
+
const APPROVE_OPTION = "Approve and execute";
|
|
90
|
+
const REFINE_OPTION = "Refine plan";
|
|
83
91
|
const MODE_CONFIG_ID = "mode";
|
|
84
92
|
const MODEL_CONFIG_ID = "model";
|
|
85
93
|
const THINKING_CONFIG_ID = "thinking";
|
|
@@ -1229,11 +1237,15 @@ export class AcpAgent implements Agent {
|
|
|
1229
1237
|
}
|
|
1230
1238
|
|
|
1231
1239
|
async #pushConfigOptionUpdate(record: ManagedSessionRecord): Promise<void> {
|
|
1240
|
+
await this.#pushConfigOptionUpdateForSession(record.session);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async #pushConfigOptionUpdateForSession(session: AgentSession): Promise<void> {
|
|
1232
1244
|
await this.#connection.sessionUpdate({
|
|
1233
|
-
sessionId:
|
|
1245
|
+
sessionId: session.sessionId,
|
|
1234
1246
|
update: {
|
|
1235
1247
|
sessionUpdate: "config_option_update",
|
|
1236
|
-
configOptions: this.#buildConfigOptions(
|
|
1248
|
+
configOptions: this.#buildConfigOptions(session),
|
|
1237
1249
|
},
|
|
1238
1250
|
});
|
|
1239
1251
|
}
|
|
@@ -1380,11 +1392,164 @@ export class AcpAgent implements Agent {
|
|
|
1380
1392
|
workflow: previous?.workflow ?? "parallel",
|
|
1381
1393
|
reentry: previous !== undefined,
|
|
1382
1394
|
});
|
|
1395
|
+
// Mirror `InteractiveMode.#enterPlanMode`: register the standing resolve
|
|
1396
|
+
// handler that consumes `resolve { action: "apply" }` from plan-mode.
|
|
1397
|
+
// Without this, the agent's resolve call falls through to the "No
|
|
1398
|
+
// pending action to resolve" error (issue #1869).
|
|
1399
|
+
session.setStandingResolveHandler?.(input => this.#runAcpPlanApprovalResolve(session, input));
|
|
1383
1400
|
} else {
|
|
1401
|
+
session.setStandingResolveHandler?.(null);
|
|
1384
1402
|
session.setPlanModeState(undefined);
|
|
1385
1403
|
}
|
|
1386
1404
|
}
|
|
1387
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* Standing resolve handler installed while ACP plan mode is active. The agent
|
|
1408
|
+
* submits the finalized plan via `resolve { action: "apply", extra: { title } }`;
|
|
1409
|
+
* this handler validates the plan file, normalizes the title, asks the ACP
|
|
1410
|
+
* client to confirm (via `unstable_createElicitation` when supported), and on
|
|
1411
|
+
* approval renames the plan to `local://<title>.md`, exits plan mode, and
|
|
1412
|
+
* notifies the client of both mode surfaces so the agent regains full tools.
|
|
1413
|
+
*
|
|
1414
|
+
* Mirrors `InteractiveMode.#runPlanApprovalResolve` for the parts the agent
|
|
1415
|
+
* sees (same `PlanApprovalDetails` shape, same source tool name `plan_approval`).
|
|
1416
|
+
* Clients without form-mode elicitation get an auto-approve so plan mode is
|
|
1417
|
+
* never stranded — the agent always has a way out.
|
|
1418
|
+
*/
|
|
1419
|
+
#runAcpPlanApprovalResolve(session: AgentSession, input: unknown): Promise<AgentToolResult<unknown>> {
|
|
1420
|
+
return runResolveInvocation(input as Parameters<typeof runResolveInvocation>[0], {
|
|
1421
|
+
sourceToolName: "plan_approval",
|
|
1422
|
+
label: "Plan ready for approval",
|
|
1423
|
+
apply: async (_reason, extra) => {
|
|
1424
|
+
const state = session.getPlanModeState();
|
|
1425
|
+
if (!state?.enabled) {
|
|
1426
|
+
throw new ToolError("Plan mode is not active.");
|
|
1427
|
+
}
|
|
1428
|
+
const planFilePath = state.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({
|
|
1436
|
+
suppliedTitle: extra?.title,
|
|
1437
|
+
planContent,
|
|
1438
|
+
planFilePath,
|
|
1439
|
+
});
|
|
1440
|
+
const finalPlanFilePath = `local://${normalized.fileName}`;
|
|
1441
|
+
const approved = await this.#requestAcpPlanApprovalChoice(session.sessionId, normalized.title, planContent);
|
|
1442
|
+
const details: PlanApprovalDetails = {
|
|
1443
|
+
planFilePath,
|
|
1444
|
+
finalPlanFilePath,
|
|
1445
|
+
title: normalized.title,
|
|
1446
|
+
planExists: true,
|
|
1447
|
+
};
|
|
1448
|
+
if (!approved) {
|
|
1449
|
+
// User chose to refine: leave plan mode active so the agent
|
|
1450
|
+
// keeps the read-only toolset and can iterate on the plan file.
|
|
1451
|
+
return {
|
|
1452
|
+
content: [
|
|
1453
|
+
{
|
|
1454
|
+
type: "text" as const,
|
|
1455
|
+
text: 'Plan refinement requested. Update the plan file, then call `resolve { action: "apply" }` again when ready.',
|
|
1456
|
+
},
|
|
1457
|
+
],
|
|
1458
|
+
details,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
// Approved. Rename plan to its titled filename, set the plan
|
|
1462
|
+
// reference so the next turn injects the plan content as
|
|
1463
|
+
// context, then exit plan mode so the agent regains full tools.
|
|
1464
|
+
await renameApprovedPlanFile({
|
|
1465
|
+
planFilePath,
|
|
1466
|
+
finalPlanFilePath,
|
|
1467
|
+
getArtifactsDir: () => session.sessionManager.getArtifactsDir(),
|
|
1468
|
+
getSessionId: () => session.sessionManager.getSessionId(),
|
|
1469
|
+
});
|
|
1470
|
+
session.setPlanReferencePath(finalPlanFilePath);
|
|
1471
|
+
session.setStandingResolveHandler?.(null);
|
|
1472
|
+
session.setPlanModeState(undefined);
|
|
1473
|
+
try {
|
|
1474
|
+
await this.#connection.sessionUpdate({
|
|
1475
|
+
sessionId: session.sessionId,
|
|
1476
|
+
update: this.#buildCurrentModeUpdate(session),
|
|
1477
|
+
});
|
|
1478
|
+
await this.#pushConfigOptionUpdateForSession(session);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
logger.warn("Failed to emit mode updates after plan approval", {
|
|
1481
|
+
sessionId: session.sessionId,
|
|
1482
|
+
error,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
content: [
|
|
1487
|
+
{
|
|
1488
|
+
type: "text" as const,
|
|
1489
|
+
text: `Plan approved at ${finalPlanFilePath}. Plan mode exited; proceed with the implementation.`,
|
|
1490
|
+
},
|
|
1491
|
+
],
|
|
1492
|
+
details,
|
|
1493
|
+
};
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
#resolveAcpPlanFilePath(session: AgentSession, planFilePath: string): string {
|
|
1499
|
+
if (planFilePath.startsWith("local:")) {
|
|
1500
|
+
const normalized = normalizeLocalScheme(planFilePath);
|
|
1501
|
+
return resolveLocalUrlToPath(normalized, {
|
|
1502
|
+
getArtifactsDir: () => session.sessionManager.getArtifactsDir(),
|
|
1503
|
+
getSessionId: () => session.sessionManager.getSessionId(),
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
return path.resolve(session.sessionManager.getCwd(), planFilePath);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async #readAcpPlanFile(session: AgentSession, planFilePath: string): Promise<string | null> {
|
|
1510
|
+
const resolvedPath = this.#resolveAcpPlanFilePath(session, planFilePath);
|
|
1511
|
+
try {
|
|
1512
|
+
return await Bun.file(resolvedPath).text();
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
if (isEnoent(error)) {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
throw error;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Ask the ACP client to confirm plan approval. Returns `true` only on an
|
|
1523
|
+
* explicit `APPROVE_OPTION` selection. Refine, dismissal (`undefined`), or
|
|
1524
|
+
* any unrecognized value falls through to refine semantics — the caller
|
|
1525
|
+
* keeps plan mode active and surfaces guidance text to the agent. Clients
|
|
1526
|
+
* without `elicitation.form` support auto-approve because there is no
|
|
1527
|
+
* confirmation surface available; without that, plan mode would strand
|
|
1528
|
+
* the agent (the bug this method exists to fix).
|
|
1529
|
+
*/
|
|
1530
|
+
async #requestAcpPlanApprovalChoice(sessionId: string, title: string, planContent: string): Promise<boolean> {
|
|
1531
|
+
const supportsForm = this.#clientCapabilities?.elicitation?.form != null;
|
|
1532
|
+
if (!supportsForm) return true;
|
|
1533
|
+
// Include a short preview of the plan so the user has context in the
|
|
1534
|
+
// dialog. Keep the body bounded — Zed renders elicitation messages
|
|
1535
|
+
// inline and a multi-thousand-line plan blows out the dialog.
|
|
1536
|
+
const previewLines = planContent.split("\n").slice(0, 12).join("\n");
|
|
1537
|
+
const ellipsis = planContent.split("\n").length > 12 ? "\n…" : "";
|
|
1538
|
+
const message = `Approve plan "${title}" and start implementation?\n\n${previewLines}${ellipsis}`;
|
|
1539
|
+
const value = await elicitFromAcpClient(
|
|
1540
|
+
this.#connection,
|
|
1541
|
+
sessionId,
|
|
1542
|
+
"select",
|
|
1543
|
+
message,
|
|
1544
|
+
{ type: "string", enum: [APPROVE_OPTION, REFINE_OPTION] },
|
|
1545
|
+
undefined,
|
|
1546
|
+
);
|
|
1547
|
+
// Approve ONLY on the explicit approve selection. Dismissal, cancel,
|
|
1548
|
+
// timeout, or any other non-approve response falls through to refine
|
|
1549
|
+
// semantics so closing the dialog can never grant write access.
|
|
1550
|
+
return value === APPROVE_OPTION;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1388
1553
|
#buildModeState(session: AgentSession): SessionModeState {
|
|
1389
1554
|
return {
|
|
1390
1555
|
availableModes: this.#getAvailableModes(session),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Controls:
|
|
9
9
|
* - Up/Down or j/k: move selection
|
|
10
|
-
* - Tab / Shift+Tab: switch source tab
|
|
10
|
+
* - Tab / Shift+Tab or Left/Right: switch source tab
|
|
11
11
|
* - Space: enable/disable selected agent
|
|
12
12
|
* - Enter: edit model override for selected agent
|
|
13
13
|
* - N: start agent creation flow
|
|
@@ -20,6 +20,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
20
20
|
import {
|
|
21
21
|
type Component,
|
|
22
22
|
Container,
|
|
23
|
+
Editor,
|
|
23
24
|
extractPrintableText,
|
|
24
25
|
fuzzyMatch,
|
|
25
26
|
Input,
|
|
@@ -49,7 +50,7 @@ import { createAgentSession } from "../../sdk";
|
|
|
49
50
|
import { discoverAgents } from "../../task/discovery";
|
|
50
51
|
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
51
52
|
import { shortenPath } from "../../tools/render-utils";
|
|
52
|
-
import { theme } from "../theme/theme";
|
|
53
|
+
import { getEditorTheme, theme } from "../theme/theme";
|
|
53
54
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
54
55
|
import { DynamicBorder } from "./dynamic-border";
|
|
55
56
|
|
|
@@ -97,8 +98,10 @@ const SOURCE_LABEL: Record<AgentSource, string> = {
|
|
|
97
98
|
bundled: "Bundled",
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
+
const LIST_FOOTER =
|
|
102
|
+
" ↑/↓: navigate Space: toggle Enter: model override N: new agent ←/→: source Ctrl+R: reload Esc: close";
|
|
101
103
|
|
|
104
|
+
const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){1,5}$/;
|
|
102
105
|
function joinPatterns(patterns: string[]): string {
|
|
103
106
|
if (patterns.length === 0) return "(session model)";
|
|
104
107
|
return patterns.join(", ");
|
|
@@ -307,7 +310,7 @@ class TwoColumnBody implements Component {
|
|
|
307
310
|
const rightWidth = width - leftWidth - 3;
|
|
308
311
|
const leftLines = this.leftPane.render(leftWidth);
|
|
309
312
|
const rightLines = this.rightPane.render(rightWidth);
|
|
310
|
-
const lineCount =
|
|
313
|
+
const lineCount = this.maxHeight;
|
|
311
314
|
const out: string[] = [];
|
|
312
315
|
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
313
316
|
|
|
@@ -339,11 +342,13 @@ export class AgentDashboard extends Container {
|
|
|
339
342
|
#loading = true;
|
|
340
343
|
#loadError: string | null = null;
|
|
341
344
|
#notice: string | null = null;
|
|
345
|
+
#builtRows = -1;
|
|
346
|
+
#builtCols = -1;
|
|
342
347
|
|
|
343
348
|
#editInput: Input | null = null;
|
|
344
349
|
#editingAgentName: string | null = null;
|
|
345
350
|
|
|
346
|
-
#createInput:
|
|
351
|
+
#createInput: Editor | null = null;
|
|
347
352
|
#createDescription = "";
|
|
348
353
|
#createScope: AgentScope = "project";
|
|
349
354
|
#createGenerating = false;
|
|
@@ -466,8 +471,46 @@ export class AgentDashboard extends Container {
|
|
|
466
471
|
this.#clampSelection();
|
|
467
472
|
}
|
|
468
473
|
|
|
474
|
+
/** Live terminal height so the dashboard tracks resize while open. */
|
|
475
|
+
#terminalRows(): number {
|
|
476
|
+
return process.stdout.rows || this.terminalHeight || 24;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#noticeBlockLines(): number {
|
|
480
|
+
if (!this.#notice) return 0;
|
|
481
|
+
return wrapTextWithAnsi(theme.fg("success", replaceTabs(this.#notice)), this.#uiWidth()).length + 1;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#footerLines(): number {
|
|
485
|
+
return Math.max(1, wrapTextWithAnsi(theme.fg("dim", LIST_FOOTER), this.#uiWidth()).length);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Height budget for the two-column body, sized to the live terminal. */
|
|
489
|
+
#computeBodyHeight(): number {
|
|
490
|
+
// Chrome around the body: top border + title + tab bar + spacer (4),
|
|
491
|
+
// optional notice block, then spacer + footer + bottom border.
|
|
492
|
+
const chrome = 4 + this.#noticeBlockLines() + 1 + this.#footerLines() + 1;
|
|
493
|
+
return Math.max(5, this.#terminalRows() - chrome);
|
|
494
|
+
}
|
|
495
|
+
|
|
469
496
|
#getMaxVisibleItems(): number {
|
|
470
|
-
|
|
497
|
+
// List pane chrome inside the body: search line, blank line, count line.
|
|
498
|
+
return Math.max(3, this.#computeBodyHeight() - 3);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
override render(width: number): string[] {
|
|
502
|
+
// Rebuild when terminal geometry changes so the full-screen overlay
|
|
503
|
+
// re-fits on resize.
|
|
504
|
+
if (this.#terminalRows() !== this.#builtRows || this.#uiWidth() !== this.#builtCols) {
|
|
505
|
+
this.#buildLayout();
|
|
506
|
+
}
|
|
507
|
+
const lines = super.render(width);
|
|
508
|
+
// Pad to the full viewport so every state (list, edit, create) covers the
|
|
509
|
+
// screen as a true full-screen view instead of letting the transcript peek
|
|
510
|
+
// through below it.
|
|
511
|
+
const rows = this.#terminalRows();
|
|
512
|
+
while (lines.length < rows) lines.push("");
|
|
513
|
+
return lines;
|
|
471
514
|
}
|
|
472
515
|
|
|
473
516
|
#clampSelection(): void {
|
|
@@ -557,10 +600,15 @@ export class AgentDashboard extends Container {
|
|
|
557
600
|
this.#createError = null;
|
|
558
601
|
this.#createSpec = null;
|
|
559
602
|
this.#createDescription = "";
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
603
|
+
const editor = new Editor(getEditorTheme());
|
|
604
|
+
editor.setBorderVisible(false);
|
|
605
|
+
editor.setPromptGutter("> ");
|
|
606
|
+
editor.setMaxHeight(Math.max(3, Math.min(8, this.#terminalRows() - 12)));
|
|
607
|
+
editor.disableSubmit = true;
|
|
608
|
+
editor.onChange = value => {
|
|
609
|
+
this.#createDescription = value;
|
|
563
610
|
};
|
|
611
|
+
this.#createInput = editor;
|
|
564
612
|
this.#buildLayout();
|
|
565
613
|
}
|
|
566
614
|
|
|
@@ -578,6 +626,20 @@ export class AgentDashboard extends Container {
|
|
|
578
626
|
this.#buildLayout();
|
|
579
627
|
}
|
|
580
628
|
|
|
629
|
+
#submitCreateDescription(): void {
|
|
630
|
+
if (!this.#createInput || this.#createGenerating) return;
|
|
631
|
+
const description = this.#createInput.getExpandedText();
|
|
632
|
+
this.#createDescription = description;
|
|
633
|
+
void this.#generateAgentFromDescription(description);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#insertCreateNewline(): void {
|
|
637
|
+
if (!this.#createInput || this.#createGenerating) return;
|
|
638
|
+
this.#createInput.handleInput("\n");
|
|
639
|
+
this.#createDescription = this.#createInput.getExpandedText();
|
|
640
|
+
this.#buildLayout();
|
|
641
|
+
}
|
|
642
|
+
|
|
581
643
|
async #generateAgentFromDescription(rawDescription: string): Promise<void> {
|
|
582
644
|
const description = rawDescription.trim();
|
|
583
645
|
this.#createDescription = description;
|
|
@@ -626,7 +688,7 @@ export class AgentDashboard extends Container {
|
|
|
626
688
|
throw new Error("No available model to generate agent specification.");
|
|
627
689
|
}
|
|
628
690
|
|
|
629
|
-
const systemPrompt = prompt.render(agentCreationArchitectPrompt, {
|
|
691
|
+
const systemPrompt = prompt.render(agentCreationArchitectPrompt, {});
|
|
630
692
|
const userPrompt = prompt.render(agentCreationUserPrompt, { request: description });
|
|
631
693
|
|
|
632
694
|
const { session } = await createAgentSession({
|
|
@@ -694,10 +756,14 @@ export class AgentDashboard extends Container {
|
|
|
694
756
|
}
|
|
695
757
|
}
|
|
696
758
|
|
|
697
|
-
const frontmatter = YAML.stringify(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
759
|
+
const frontmatter = YAML.stringify(
|
|
760
|
+
{
|
|
761
|
+
name: spec.identifier,
|
|
762
|
+
description: spec.whenToUse,
|
|
763
|
+
},
|
|
764
|
+
null,
|
|
765
|
+
2,
|
|
766
|
+
).trimEnd();
|
|
701
767
|
const content = `---\n${frontmatter}\n---\n\n${spec.systemPrompt.trim()}\n`;
|
|
702
768
|
await Bun.write(filePath, content);
|
|
703
769
|
await this.#reloadData();
|
|
@@ -789,13 +855,13 @@ export class AgentDashboard extends Container {
|
|
|
789
855
|
}
|
|
790
856
|
return parts.join("");
|
|
791
857
|
}
|
|
792
|
-
|
|
793
858
|
#renderCreateInput(): void {
|
|
794
859
|
this.addChild(new Text(theme.bold(theme.fg("accent", " Create New Agent")), 0, 0));
|
|
795
860
|
this.addChild(new Spacer(1));
|
|
796
861
|
this.addChild(new Text(theme.fg("muted", "Describe what the new agent should do:"), 0, 0));
|
|
797
862
|
this.addChild(new Spacer(1));
|
|
798
863
|
if (this.#createInput) {
|
|
864
|
+
this.#createInput.setMaxHeight(Math.max(3, Math.min(8, this.#terminalRows() - 12)));
|
|
799
865
|
this.addChild(this.#createInput);
|
|
800
866
|
}
|
|
801
867
|
this.addChild(new Spacer(1));
|
|
@@ -805,7 +871,7 @@ export class AgentDashboard extends Container {
|
|
|
805
871
|
this.addChild(new Text(theme.fg("accent", "Generating agent specification..."), 0, 0));
|
|
806
872
|
if (this.#createStreamingText) {
|
|
807
873
|
this.addChild(new Spacer(1));
|
|
808
|
-
const maxPreview = Math.max(3, this
|
|
874
|
+
const maxPreview = Math.max(3, this.#terminalRows() - 18);
|
|
809
875
|
const contentWidth = Math.max(20, this.#uiWidth() - 4);
|
|
810
876
|
const wrappedLines: string[] = [];
|
|
811
877
|
for (const raw of this.#createStreamingText.split("\n")) {
|
|
@@ -826,7 +892,9 @@ export class AgentDashboard extends Container {
|
|
|
826
892
|
this.addChild(new Text(theme.fg("error", replaceTabs(this.#createError)), 0, 0));
|
|
827
893
|
}
|
|
828
894
|
this.addChild(new Spacer(1));
|
|
829
|
-
const hints = this.#createGenerating
|
|
895
|
+
const hints = this.#createGenerating
|
|
896
|
+
? " Generating..."
|
|
897
|
+
: " Ctrl+Enter: generate Enter: newline Tab: toggle scope Esc: cancel";
|
|
830
898
|
this.addChild(new Text(theme.fg("dim", hints), 0, 0));
|
|
831
899
|
}
|
|
832
900
|
|
|
@@ -968,22 +1036,15 @@ export class AgentDashboard extends Container {
|
|
|
968
1036
|
effectivePatterns,
|
|
969
1037
|
effectiveResolution,
|
|
970
1038
|
);
|
|
971
|
-
const bodyHeight =
|
|
1039
|
+
const bodyHeight = this.#computeBodyHeight();
|
|
972
1040
|
this.addChild(new TwoColumnBody(listPane, inspector, bodyHeight));
|
|
973
1041
|
this.addChild(new Spacer(1));
|
|
974
|
-
this.addChild(
|
|
975
|
-
new Text(
|
|
976
|
-
theme.fg(
|
|
977
|
-
"dim",
|
|
978
|
-
" ↑/↓: navigate Space: toggle Enter: model override N: new agent Tab: source Ctrl+R: reload Esc: close",
|
|
979
|
-
),
|
|
980
|
-
0,
|
|
981
|
-
0,
|
|
982
|
-
),
|
|
983
|
-
);
|
|
1042
|
+
this.addChild(new Text(theme.fg("dim", LIST_FOOTER), 0, 0));
|
|
984
1043
|
}
|
|
985
1044
|
|
|
986
1045
|
this.addChild(new DynamicBorder());
|
|
1046
|
+
this.#builtRows = this.#terminalRows();
|
|
1047
|
+
this.#builtCols = this.#uiWidth();
|
|
987
1048
|
}
|
|
988
1049
|
|
|
989
1050
|
handleInput(data: string): void {
|
|
@@ -1024,13 +1085,24 @@ export class AgentDashboard extends Container {
|
|
|
1024
1085
|
}
|
|
1025
1086
|
return;
|
|
1026
1087
|
}
|
|
1088
|
+
if (
|
|
1089
|
+
!this.#createGenerating &&
|
|
1090
|
+
(matchesKey(data, "ctrl+enter") || (data.charCodeAt(0) === 10 && data.length > 1))
|
|
1091
|
+
) {
|
|
1092
|
+
this.#submitCreateDescription();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (!this.#createGenerating && (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n")) {
|
|
1096
|
+
this.#insertCreateNewline();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1027
1099
|
if (!this.#createGenerating && (matchesKey(data, "tab") || matchesKey(data, "shift+tab"))) {
|
|
1028
1100
|
this.#toggleCreateScope();
|
|
1029
1101
|
return;
|
|
1030
1102
|
}
|
|
1031
1103
|
if (!this.#createGenerating && this.#createInput) {
|
|
1032
1104
|
this.#createInput.handleInput(data);
|
|
1033
|
-
this.#createDescription = this.#createInput.
|
|
1105
|
+
this.#createDescription = this.#createInput.getExpandedText();
|
|
1034
1106
|
this.#buildLayout();
|
|
1035
1107
|
}
|
|
1036
1108
|
return;
|
|
@@ -1064,11 +1136,11 @@ export class AgentDashboard extends Container {
|
|
|
1064
1136
|
return;
|
|
1065
1137
|
}
|
|
1066
1138
|
|
|
1067
|
-
if (matchesKey(data, "tab")) {
|
|
1139
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
1068
1140
|
this.#switchTab(1);
|
|
1069
1141
|
return;
|
|
1070
1142
|
}
|
|
1071
|
-
if (matchesKey(data, "shift+tab")) {
|
|
1143
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
1072
1144
|
this.#switchTab(-1);
|
|
1073
1145
|
return;
|
|
1074
1146
|
}
|