@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli.ts +1 -0
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +31 -0
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +6 -6
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +17 -1
- package/src/task/render.ts +6 -3
- package/src/tools/bash.ts +176 -2
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP-side `ClientBridge` implementation. Wraps `AgentSideConnection` so the
|
|
3
|
+
* `read`/`write`/`bash`/`edit` tools (and the permission gate in
|
|
4
|
+
* `AgentSession`) can route through the client when it advertises the
|
|
5
|
+
* relevant capabilities at `initialize` time.
|
|
6
|
+
*/
|
|
7
|
+
import type {
|
|
8
|
+
PermissionOption as AcpPermissionOption,
|
|
9
|
+
TerminalHandle as AcpTerminalHandle,
|
|
10
|
+
AgentSideConnection,
|
|
11
|
+
ClientCapabilities,
|
|
12
|
+
RequestPermissionRequest,
|
|
13
|
+
ToolCallUpdate,
|
|
14
|
+
} from "@agentclientprotocol/sdk";
|
|
15
|
+
import type {
|
|
16
|
+
ClientBridge,
|
|
17
|
+
ClientBridgeCapabilities,
|
|
18
|
+
ClientBridgeCreateTerminalParams,
|
|
19
|
+
ClientBridgePermissionOption,
|
|
20
|
+
ClientBridgePermissionOutcome,
|
|
21
|
+
ClientBridgePermissionToolCall,
|
|
22
|
+
ClientBridgeTerminalHandle,
|
|
23
|
+
} from "../../session/client-bridge";
|
|
24
|
+
|
|
25
|
+
export function createAcpClientBridge(
|
|
26
|
+
connection: AgentSideConnection,
|
|
27
|
+
sessionId: string,
|
|
28
|
+
clientCapabilities: ClientCapabilities | undefined,
|
|
29
|
+
): ClientBridge {
|
|
30
|
+
const capabilities: ClientBridgeCapabilities = {
|
|
31
|
+
readTextFile: clientCapabilities?.fs?.readTextFile === true,
|
|
32
|
+
writeTextFile: clientCapabilities?.fs?.writeTextFile === true,
|
|
33
|
+
terminal: clientCapabilities?.terminal === true,
|
|
34
|
+
// Permission requests are always usable on the connection; gating is
|
|
35
|
+
// the agent's policy choice rather than a client capability.
|
|
36
|
+
requestPermission: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const bridge: ClientBridge = { capabilities };
|
|
40
|
+
|
|
41
|
+
if (capabilities.readTextFile) {
|
|
42
|
+
bridge.readTextFile = async params => {
|
|
43
|
+
const response = await connection.readTextFile({
|
|
44
|
+
sessionId,
|
|
45
|
+
path: params.path,
|
|
46
|
+
...(typeof params.line === "number" ? { line: params.line } : {}),
|
|
47
|
+
...(typeof params.limit === "number" ? { limit: params.limit } : {}),
|
|
48
|
+
});
|
|
49
|
+
return response.content;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (capabilities.writeTextFile) {
|
|
54
|
+
bridge.writeTextFile = async params => {
|
|
55
|
+
await connection.writeTextFile({
|
|
56
|
+
sessionId,
|
|
57
|
+
path: params.path,
|
|
58
|
+
content: params.content,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (capabilities.terminal) {
|
|
64
|
+
bridge.createTerminal = (params: ClientBridgeCreateTerminalParams) =>
|
|
65
|
+
createTerminalHandle(connection, sessionId, params);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bridge.requestPermission = (toolCall, options, signal) =>
|
|
69
|
+
requestPermission(connection, sessionId, toolCall, options, signal);
|
|
70
|
+
|
|
71
|
+
return bridge;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function createTerminalHandle(
|
|
75
|
+
connection: AgentSideConnection,
|
|
76
|
+
sessionId: string,
|
|
77
|
+
params: ClientBridgeCreateTerminalParams,
|
|
78
|
+
): Promise<ClientBridgeTerminalHandle> {
|
|
79
|
+
const handle = await connection.createTerminal({
|
|
80
|
+
sessionId,
|
|
81
|
+
command: params.command,
|
|
82
|
+
...(params.args ? { args: params.args } : {}),
|
|
83
|
+
...(params.env ? { env: params.env } : {}),
|
|
84
|
+
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
85
|
+
...(typeof params.outputByteLimit === "number" ? { outputByteLimit: params.outputByteLimit } : {}),
|
|
86
|
+
});
|
|
87
|
+
return wrapTerminalHandle(handle);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapTerminalHandle(handle: AcpTerminalHandle): ClientBridgeTerminalHandle {
|
|
91
|
+
return {
|
|
92
|
+
terminalId: handle.id,
|
|
93
|
+
async currentOutput() {
|
|
94
|
+
const out = await handle.currentOutput();
|
|
95
|
+
return {
|
|
96
|
+
output: out.output,
|
|
97
|
+
truncated: out.truncated,
|
|
98
|
+
exitStatus: out.exitStatus ?? null,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
async waitForExit() {
|
|
102
|
+
const status = await handle.waitForExit();
|
|
103
|
+
return { exitCode: status.exitCode ?? null, signal: status.signal ?? null };
|
|
104
|
+
},
|
|
105
|
+
async kill() {
|
|
106
|
+
await handle.kill();
|
|
107
|
+
},
|
|
108
|
+
async release() {
|
|
109
|
+
await handle.release();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function requestPermission(
|
|
115
|
+
connection: AgentSideConnection,
|
|
116
|
+
sessionId: string,
|
|
117
|
+
toolCall: ClientBridgePermissionToolCall,
|
|
118
|
+
options: ClientBridgePermissionOption[],
|
|
119
|
+
signal: AbortSignal | undefined,
|
|
120
|
+
): Promise<ClientBridgePermissionOutcome> {
|
|
121
|
+
const update: ToolCallUpdate = {
|
|
122
|
+
toolCallId: toolCall.toolCallId,
|
|
123
|
+
title: toolCall.title,
|
|
124
|
+
...(toolCall.kind ? { kind: toolCall.kind as ToolCallUpdate["kind"] } : {}),
|
|
125
|
+
...(toolCall.rawInput !== undefined ? { rawInput: toolCall.rawInput } : {}),
|
|
126
|
+
...(toolCall.locations ? { locations: toolCall.locations } : {}),
|
|
127
|
+
};
|
|
128
|
+
const acpOptions: AcpPermissionOption[] = options.map(option => ({
|
|
129
|
+
optionId: option.optionId,
|
|
130
|
+
name: option.name,
|
|
131
|
+
kind: option.kind,
|
|
132
|
+
}));
|
|
133
|
+
const request: RequestPermissionRequest = {
|
|
134
|
+
sessionId,
|
|
135
|
+
toolCall: update,
|
|
136
|
+
options: acpOptions,
|
|
137
|
+
};
|
|
138
|
+
if (signal?.aborted) {
|
|
139
|
+
return { outcome: "cancelled" };
|
|
140
|
+
}
|
|
141
|
+
const response = await connection.requestPermission(request);
|
|
142
|
+
const outcome = response.outcome;
|
|
143
|
+
if (outcome.outcome === "cancelled") {
|
|
144
|
+
return { outcome: "cancelled" };
|
|
145
|
+
}
|
|
146
|
+
const matched = options.find(option => option.optionId === outcome.optionId);
|
|
147
|
+
return {
|
|
148
|
+
outcome: "selected",
|
|
149
|
+
optionId: outcome.optionId,
|
|
150
|
+
...(matched ? { kind: matched.kind } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -6,10 +6,24 @@ import type {
|
|
|
6
6
|
ToolKind,
|
|
7
7
|
} from "@agentclientprotocol/sdk";
|
|
8
8
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
9
|
+
import { resolveToCwd } from "../../tools/path-utils";
|
|
9
10
|
import type { TodoStatus } from "../../tools/todo-write";
|
|
10
11
|
|
|
12
|
+
interface MessageProgress {
|
|
13
|
+
textEmitted: boolean;
|
|
14
|
+
thoughtEmitted: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
interface AcpEventMapperOptions {
|
|
12
18
|
getMessageId?: (message: unknown) => string | undefined;
|
|
19
|
+
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
22
|
+
* (the editor host needs them to open or focus files). When provided,
|
|
23
|
+
* the mapper resolves raw `path`/`file`/etc. args against this cwd
|
|
24
|
+
* before emitting `ToolCallLocation` entries.
|
|
25
|
+
*/
|
|
26
|
+
cwd?: string;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
interface ContentArrayContainer {
|
|
@@ -127,6 +141,8 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
127
141
|
switch (event.type) {
|
|
128
142
|
case "message_update":
|
|
129
143
|
return mapAssistantMessageUpdate(event, sessionId, options);
|
|
144
|
+
case "message_end":
|
|
145
|
+
return mapAssistantMessageEnd(event, sessionId, options);
|
|
130
146
|
case "tool_execution_start": {
|
|
131
147
|
const update: SessionUpdate = {
|
|
132
148
|
sessionUpdate: "tool_call",
|
|
@@ -136,14 +152,16 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
136
152
|
status: "pending",
|
|
137
153
|
rawInput: event.args,
|
|
138
154
|
};
|
|
139
|
-
const locations = extractToolLocations(event.args);
|
|
155
|
+
const locations = extractToolLocations(event.args, options.cwd);
|
|
140
156
|
if (locations.length > 0) {
|
|
141
157
|
update.locations = locations;
|
|
142
158
|
}
|
|
143
159
|
return [toSessionNotification(sessionId, update)];
|
|
144
160
|
}
|
|
145
161
|
case "tool_execution_update": {
|
|
146
|
-
const
|
|
162
|
+
const terminalContent = extractTerminalToolCallContent(event.partialResult);
|
|
163
|
+
const otherContent = terminalContent.length > 0 ? [] : extractToolCallContent(event.partialResult);
|
|
164
|
+
const content = [...terminalContent, ...otherContent];
|
|
147
165
|
const update: SessionUpdate = {
|
|
148
166
|
sessionUpdate: "tool_call_update",
|
|
149
167
|
toolCallId: event.toolCallId,
|
|
@@ -153,10 +171,17 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
153
171
|
if (content.length > 0) {
|
|
154
172
|
update.content = content;
|
|
155
173
|
}
|
|
174
|
+
const locations = extractToolLocations(event.args, options.cwd);
|
|
175
|
+
if (locations.length > 0) {
|
|
176
|
+
update.locations = locations;
|
|
177
|
+
}
|
|
156
178
|
return [toSessionNotification(sessionId, update)];
|
|
157
179
|
}
|
|
158
180
|
case "tool_execution_end": {
|
|
159
|
-
const
|
|
181
|
+
const diffContent = extractDiffToolCallContent(event.result);
|
|
182
|
+
const terminalContent = extractTerminalToolCallContent(event.result);
|
|
183
|
+
const otherContent = extractToolCallContent(event.result);
|
|
184
|
+
const content = [...diffContent, ...terminalContent, ...otherContent];
|
|
160
185
|
const update: SessionUpdate = {
|
|
161
186
|
sessionUpdate: "tool_call_update",
|
|
162
187
|
toolCallId: event.toolCallId,
|
|
@@ -166,6 +191,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
166
191
|
if (content.length > 0) {
|
|
167
192
|
update.content = content;
|
|
168
193
|
}
|
|
194
|
+
const locations = extractToolLocationsFromResult(event.result, options.cwd);
|
|
195
|
+
if (locations.length > 0) {
|
|
196
|
+
update.locations = locations;
|
|
197
|
+
}
|
|
169
198
|
return [toSessionNotification(sessionId, update)];
|
|
170
199
|
}
|
|
171
200
|
case "todo_reminder": {
|
|
@@ -194,14 +223,31 @@ function mapAssistantMessageUpdate(
|
|
|
194
223
|
|
|
195
224
|
let sessionUpdate: "agent_message_chunk" | "agent_thought_chunk";
|
|
196
225
|
let text: string;
|
|
226
|
+
const progress = options.getMessageProgress?.(event.message);
|
|
197
227
|
switch (event.assistantMessageEvent.type) {
|
|
198
228
|
case "text_delta":
|
|
199
229
|
sessionUpdate = "agent_message_chunk";
|
|
200
230
|
text = event.assistantMessageEvent.delta;
|
|
231
|
+
if (text.length > 0 && progress) {
|
|
232
|
+
progress.textEmitted = true;
|
|
233
|
+
}
|
|
201
234
|
break;
|
|
202
235
|
case "thinking_delta":
|
|
203
236
|
sessionUpdate = "agent_thought_chunk";
|
|
204
237
|
text = event.assistantMessageEvent.delta;
|
|
238
|
+
if (text.length > 0 && progress) {
|
|
239
|
+
progress.thoughtEmitted = true;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
case "done":
|
|
243
|
+
if (progress?.textEmitted) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
sessionUpdate = "agent_message_chunk";
|
|
247
|
+
text = extractAssistantMessageText(event.assistantMessageEvent.message);
|
|
248
|
+
if (text.length > 0 && progress) {
|
|
249
|
+
progress.textEmitted = true;
|
|
250
|
+
}
|
|
205
251
|
break;
|
|
206
252
|
case "error":
|
|
207
253
|
sessionUpdate = "agent_message_chunk";
|
|
@@ -224,6 +270,33 @@ function mapAssistantMessageUpdate(
|
|
|
224
270
|
];
|
|
225
271
|
}
|
|
226
272
|
|
|
273
|
+
function mapAssistantMessageEnd(
|
|
274
|
+
event: Extract<AgentSessionEvent, { type: "message_end" }>,
|
|
275
|
+
sessionId: string,
|
|
276
|
+
options: AcpEventMapperOptions,
|
|
277
|
+
): SessionNotification[] {
|
|
278
|
+
if (!isAssistantMessage(event.message)) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
const progress = options.getMessageProgress?.(event.message);
|
|
282
|
+
if (!progress || progress.textEmitted) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
const text = extractAssistantMessageText(event.message);
|
|
286
|
+
if (text.length === 0) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
progress.textEmitted = true;
|
|
290
|
+
const messageId = options.getMessageId?.(event.message);
|
|
291
|
+
return [
|
|
292
|
+
toSessionNotification(sessionId, {
|
|
293
|
+
sessionUpdate: "agent_message_chunk",
|
|
294
|
+
content: { type: "text", text },
|
|
295
|
+
messageId,
|
|
296
|
+
}),
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
|
|
227
300
|
function toSessionNotification(sessionId: string, update: SessionUpdate): SessionNotification {
|
|
228
301
|
return { sessionId, update };
|
|
229
302
|
}
|
|
@@ -257,26 +330,104 @@ function buildToolTitle(toolName: string, args: unknown, intent: string | undefi
|
|
|
257
330
|
return toolName;
|
|
258
331
|
}
|
|
259
332
|
|
|
260
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Resolve a single raw path against cwd for an ACP location. When `cwd` is
|
|
335
|
+
* omitted we pass the value through unchanged (callers without session
|
|
336
|
+
* context, e.g. some legacy entry points and tests); the ACP-side caller
|
|
337
|
+
* always supplies cwd so notifications carry absolute paths.
|
|
338
|
+
*/
|
|
339
|
+
function toAcpLocationPath(value: string, cwd?: string): string {
|
|
340
|
+
if (!cwd) return value;
|
|
341
|
+
try {
|
|
342
|
+
return resolveToCwd(value, cwd);
|
|
343
|
+
} catch {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function extractToolLocations(args: unknown, cwd?: string): ToolCallLocation[] {
|
|
261
349
|
const locations: ToolCallLocation[] = [];
|
|
262
|
-
const
|
|
263
|
-
|
|
350
|
+
const seen = new Set<string>();
|
|
351
|
+
const pushPath = (raw: string | undefined) => {
|
|
352
|
+
if (!raw) return;
|
|
353
|
+
const path = toAcpLocationPath(raw, cwd);
|
|
354
|
+
if (seen.has(path)) return;
|
|
355
|
+
seen.add(path);
|
|
264
356
|
locations.push({ path });
|
|
265
|
-
}
|
|
357
|
+
};
|
|
266
358
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
359
|
+
pushPath(extractStringProperty<PathContainer>(args, "path"));
|
|
360
|
+
pushPath(extractStringProperty<OldPathContainer>(args, "oldPath"));
|
|
361
|
+
pushPath(extractStringProperty<NewPathContainer>(args, "newPath"));
|
|
271
362
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
locations.push({ path: newPath });
|
|
275
|
-
}
|
|
363
|
+
return locations;
|
|
364
|
+
}
|
|
276
365
|
|
|
366
|
+
/** Pull locations from a tool result's details (e.g. EditToolDetails.perFileResults[].path). */
|
|
367
|
+
function extractToolLocationsFromResult(result: unknown, cwd?: string): ToolCallLocation[] {
|
|
368
|
+
if (typeof result !== "object" || result === null) return [];
|
|
369
|
+
const details = (result as { details?: unknown }).details;
|
|
370
|
+
if (typeof details !== "object" || details === null) return [];
|
|
371
|
+
const direct = extractToolLocations(details, cwd);
|
|
372
|
+
const perFile = (details as { perFileResults?: unknown }).perFileResults;
|
|
373
|
+
if (!Array.isArray(perFile)) {
|
|
374
|
+
return direct;
|
|
375
|
+
}
|
|
376
|
+
const seen = new Set(direct.map(loc => loc.path));
|
|
377
|
+
const locations = [...direct];
|
|
378
|
+
for (const entry of perFile) {
|
|
379
|
+
const raw = extractStringProperty<PathContainer>(entry, "path");
|
|
380
|
+
if (!raw) continue;
|
|
381
|
+
const path = toAcpLocationPath(raw, cwd);
|
|
382
|
+
if (seen.has(path)) continue;
|
|
383
|
+
seen.add(path);
|
|
384
|
+
locations.push({ path });
|
|
385
|
+
}
|
|
277
386
|
return locations;
|
|
278
387
|
}
|
|
279
388
|
|
|
389
|
+
/** Emit a `diff` ToolCallContent for each per-file edit result that carries oldText/newText. */
|
|
390
|
+
function extractDiffToolCallContent(result: unknown): ToolCallContent[] {
|
|
391
|
+
if (typeof result !== "object" || result === null) return [];
|
|
392
|
+
const details = (result as { details?: unknown }).details;
|
|
393
|
+
if (typeof details !== "object" || details === null) return [];
|
|
394
|
+
const blocks: ToolCallContent[] = [];
|
|
395
|
+
const perFile = (details as { perFileResults?: unknown }).perFileResults;
|
|
396
|
+
const entries: unknown[] = Array.isArray(perFile) ? perFile : [details];
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const block = buildDiffContent(entry);
|
|
399
|
+
if (block) blocks.push(block);
|
|
400
|
+
}
|
|
401
|
+
return blocks;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildDiffContent(entry: unknown): ToolCallContent | undefined {
|
|
405
|
+
if (typeof entry !== "object" || entry === null) return undefined;
|
|
406
|
+
const candidate = entry as { path?: unknown; oldText?: unknown; newText?: unknown; isError?: unknown };
|
|
407
|
+
if (candidate.isError === true) return undefined;
|
|
408
|
+
const path = typeof candidate.path === "string" && candidate.path.length > 0 ? candidate.path : undefined;
|
|
409
|
+
if (!path) return undefined;
|
|
410
|
+
const oldText = typeof candidate.oldText === "string" ? candidate.oldText : undefined;
|
|
411
|
+
const newText = typeof candidate.newText === "string" ? candidate.newText : undefined;
|
|
412
|
+
if (oldText === undefined && newText === undefined) return undefined;
|
|
413
|
+
return {
|
|
414
|
+
type: "diff",
|
|
415
|
+
path,
|
|
416
|
+
oldText: oldText ?? null,
|
|
417
|
+
newText: newText ?? "",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Emit a `terminal` ToolCallContent when a tool result carries a `details.terminalId` (e.g. bash routed through ACP terminal/*). */
|
|
422
|
+
function extractTerminalToolCallContent(result: unknown): ToolCallContent[] {
|
|
423
|
+
if (typeof result !== "object" || result === null) return [];
|
|
424
|
+
const details = (result as { details?: unknown }).details;
|
|
425
|
+
if (typeof details !== "object" || details === null) return [];
|
|
426
|
+
const terminalId = (details as { terminalId?: unknown }).terminalId;
|
|
427
|
+
if (typeof terminalId !== "string" || terminalId.length === 0) return [];
|
|
428
|
+
return [{ type: "terminal", terminalId }];
|
|
429
|
+
}
|
|
430
|
+
|
|
280
431
|
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
281
432
|
const richContent = extractStructuredToolCallContent(value);
|
|
282
433
|
const fallbackText = extractReadableText(value);
|
|
@@ -479,6 +630,20 @@ function extractReadableText(value: unknown): string | undefined {
|
|
|
479
630
|
return normalizeText(serialized);
|
|
480
631
|
}
|
|
481
632
|
|
|
633
|
+
function extractAssistantMessageText(value: unknown): string {
|
|
634
|
+
if (typeof value !== "object" || value === null || !("content" in value)) {
|
|
635
|
+
return "";
|
|
636
|
+
}
|
|
637
|
+
const content = (value as ContentArrayContainer).content;
|
|
638
|
+
if (!Array.isArray(content)) {
|
|
639
|
+
return "";
|
|
640
|
+
}
|
|
641
|
+
return content
|
|
642
|
+
.map(block => extractStructuredText(block))
|
|
643
|
+
.filter((chunk): chunk is string => typeof chunk === "string" && chunk.length > 0)
|
|
644
|
+
.join("\n");
|
|
645
|
+
}
|
|
646
|
+
|
|
482
647
|
function extractStructuredText(value: unknown): string | undefined {
|
|
483
648
|
const text = extractStringProperty<TextLikeContent>(value, "text");
|
|
484
649
|
if (!text) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const ACP_TERMINAL_AUTH_FLAG = "--acp-terminal-auth";
|
|
2
|
+
|
|
3
|
+
export interface AcpTerminalAuthArgs {
|
|
4
|
+
args: string[];
|
|
5
|
+
terminalAuth: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function prepareAcpTerminalAuthArgs(rawArgs: readonly string[]): AcpTerminalAuthArgs {
|
|
9
|
+
const withoutAuthFlag: string[] = [];
|
|
10
|
+
let terminalAuth = false;
|
|
11
|
+
for (const arg of rawArgs) {
|
|
12
|
+
if (arg === ACP_TERMINAL_AUTH_FLAG) {
|
|
13
|
+
terminalAuth = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
withoutAuthFlag.push(arg);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!terminalAuth) {
|
|
20
|
+
return { args: withoutAuthFlag, terminalAuth: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const args: string[] = [];
|
|
24
|
+
for (let i = 0; i < withoutAuthFlag.length; i++) {
|
|
25
|
+
const arg = withoutAuthFlag[i];
|
|
26
|
+
if (arg === "--mode") {
|
|
27
|
+
i++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg.startsWith("--mode=")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
args.push(arg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { args, terminalAuth: true };
|
|
37
|
+
}
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { InternalUrlRouter } from "../../internal-urls";
|
|
3
4
|
import { getLanguageFromPath, theme } from "../../modes/theme/theme";
|
|
5
|
+
import { splitPathAndSel } from "../../tools/path-utils";
|
|
4
6
|
import { PREVIEW_LIMITS, shortenPath } from "../../tools/render-utils";
|
|
5
7
|
import { renderCodeCell } from "../../tui";
|
|
6
8
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Read calls whose target is resolved through {@link InternalUrlRouter} are
|
|
12
|
+
* rendered as full tool executions (not collapsed into the read group) so the
|
|
13
|
+
* resolved content is visible. `path` is the canonical arg; `file_path` is the
|
|
14
|
+
* legacy alias still tolerated by the read tool schema.
|
|
15
|
+
*/
|
|
16
|
+
function readArgsTarget(args: unknown): string | undefined {
|
|
17
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
18
|
+
const record = args as Record<string, unknown>;
|
|
19
|
+
return typeof record.path === "string"
|
|
20
|
+
? record.path
|
|
21
|
+
: typeof record.file_path === "string"
|
|
22
|
+
? record.file_path
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readArgsHaveTarget(args: unknown): boolean {
|
|
27
|
+
return readArgsTarget(args) !== undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readArgsTargetInternalUrl(args: unknown): boolean {
|
|
31
|
+
const target = readArgsTarget(args);
|
|
32
|
+
if (!target) return false;
|
|
33
|
+
return InternalUrlRouter.instance().canHandle(target);
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
type ReadRenderArgs = {
|
|
9
37
|
path?: string;
|
|
10
38
|
file_path?: string;
|
|
@@ -174,7 +202,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
174
202
|
* When expanded: shows full content.
|
|
175
203
|
*/
|
|
176
204
|
#addContentPreview(entry: ReadEntry): void {
|
|
177
|
-
const lang = getLanguageFromPath(entry.path);
|
|
205
|
+
const lang = getLanguageFromPath(splitPathAndSel(entry.path).path);
|
|
178
206
|
const filePath = shortenPath(entry.path);
|
|
179
207
|
const correctionSuffix = entry.correctedFrom ? ` (corrected from ${shortenPath(entry.correctedFrom)})` : "";
|
|
180
208
|
const title = filePath ? `Read ${filePath}${correctionSuffix}` : "Read";
|
|
@@ -37,6 +37,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
|
37
37
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
38
38
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
39
39
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
40
|
+
import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
|
|
40
41
|
import type { NewSessionOptions } from "../../session/session-manager";
|
|
41
42
|
import { outputMeta } from "../../tools/output-meta";
|
|
42
43
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
@@ -1071,16 +1072,16 @@ export class CommandController {
|
|
|
1071
1072
|
this.ctx.ui.requestRender();
|
|
1072
1073
|
}
|
|
1073
1074
|
|
|
1074
|
-
async handleCompactCommand(customInstructions?: string): Promise<
|
|
1075
|
+
async handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
|
|
1075
1076
|
const entries = this.ctx.sessionManager.getEntries();
|
|
1076
1077
|
const messageCount = entries.filter(e => e.type === "message").length;
|
|
1077
1078
|
|
|
1078
1079
|
if (messageCount < 2) {
|
|
1079
1080
|
this.ctx.showWarning("Nothing to compact (no messages yet)");
|
|
1080
|
-
return;
|
|
1081
|
+
return "ok";
|
|
1081
1082
|
}
|
|
1082
1083
|
|
|
1083
|
-
|
|
1084
|
+
return this.executeCompaction(customInstructions, false);
|
|
1084
1085
|
}
|
|
1085
1086
|
|
|
1086
1087
|
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
@@ -1098,7 +1099,10 @@ export class CommandController {
|
|
|
1098
1099
|
}
|
|
1099
1100
|
}
|
|
1100
1101
|
|
|
1101
|
-
async executeCompaction(
|
|
1102
|
+
async executeCompaction(
|
|
1103
|
+
customInstructionsOrOptions?: string | CompactOptions,
|
|
1104
|
+
isAuto = false,
|
|
1105
|
+
): Promise<CompactionOutcome> {
|
|
1102
1106
|
if (this.ctx.loadingAnimation) {
|
|
1103
1107
|
this.ctx.loadingAnimation.stop();
|
|
1104
1108
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -1122,6 +1126,7 @@ export class CommandController {
|
|
|
1122
1126
|
this.ctx.statusContainer.addChild(compactingLoader);
|
|
1123
1127
|
this.ctx.ui.requestRender();
|
|
1124
1128
|
|
|
1129
|
+
let outcome: CompactionOutcome = "ok";
|
|
1125
1130
|
try {
|
|
1126
1131
|
const instructions = typeof customInstructionsOrOptions === "string" ? customInstructionsOrOptions : undefined;
|
|
1127
1132
|
const options =
|
|
@@ -1135,10 +1140,12 @@ export class CommandController {
|
|
|
1135
1140
|
this.ctx.statusLine.invalidate();
|
|
1136
1141
|
this.ctx.updateEditorTopBorder();
|
|
1137
1142
|
} catch (error) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1143
|
+
if (error instanceof CompactionCancelledError) {
|
|
1144
|
+
outcome = "cancelled";
|
|
1140
1145
|
this.ctx.showError("Compaction cancelled");
|
|
1141
1146
|
} else {
|
|
1147
|
+
outcome = "failed";
|
|
1148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1142
1149
|
this.ctx.showError(`Compaction failed: ${message}`);
|
|
1143
1150
|
}
|
|
1144
1151
|
} finally {
|
|
@@ -1147,6 +1154,7 @@ export class CommandController {
|
|
|
1147
1154
|
this.ctx.editor.onEscape = originalOnEscape;
|
|
1148
1155
|
}
|
|
1149
1156
|
await this.ctx.flushCompactionQueue({ willRetry: false });
|
|
1157
|
+
return outcome;
|
|
1150
1158
|
}
|
|
1151
1159
|
|
|
1152
1160
|
async handleHandoffCommand(customInstructions?: string): Promise<void> {
|
|
@@ -3,7 +3,11 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ReadToolGroupComponent,
|
|
8
|
+
readArgsHaveTarget,
|
|
9
|
+
readArgsTargetInternalUrl,
|
|
10
|
+
} from "../../modes/components/read-tool-group";
|
|
7
11
|
import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
8
12
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
9
13
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
@@ -274,16 +278,25 @@ export class EventController {
|
|
|
274
278
|
for (const content of this.ctx.streamingMessage.content) {
|
|
275
279
|
if (content.type !== "toolCall") continue;
|
|
276
280
|
if (content.name === "read") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
component
|
|
281
|
-
|
|
282
|
-
const group = this.#getReadGroup();
|
|
283
|
-
group.updateArgs(content.arguments, content.id);
|
|
284
|
-
this.ctx.pendingTools.set(content.id, group);
|
|
281
|
+
if (!readArgsHaveTarget(content.arguments)) {
|
|
282
|
+
// Args still streaming — defer until path is parseable so we can route to the
|
|
283
|
+
// read group (regular files) vs ToolExecutionComponent (internal URLs).
|
|
284
|
+
// Creating either component now would lock the read into the wrong shape.
|
|
285
|
+
continue;
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
+
if (!readArgsTargetInternalUrl(content.arguments)) {
|
|
288
|
+
this.#trackReadToolCall(content.id, content.arguments);
|
|
289
|
+
const component = this.ctx.pendingTools.get(content.id);
|
|
290
|
+
if (component) {
|
|
291
|
+
component.updateArgs(content.arguments, content.id);
|
|
292
|
+
} else {
|
|
293
|
+
const group = this.#getReadGroup();
|
|
294
|
+
group.updateArgs(content.arguments, content.id);
|
|
295
|
+
this.ctx.pendingTools.set(content.id, group);
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Internal URL read falls through to ToolExecutionComponent below.
|
|
287
300
|
}
|
|
288
301
|
|
|
289
302
|
// Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
|
|
@@ -384,7 +397,7 @@ export class EventController {
|
|
|
384
397
|
async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
|
385
398
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
386
399
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
387
|
-
if (event.toolName === "read") {
|
|
400
|
+
if (event.toolName === "read" && readArgsHaveTarget(event.args) && !readArgsTargetInternalUrl(event.args)) {
|
|
388
401
|
this.#trackReadToolCall(event.toolCallId, event.args);
|
|
389
402
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
390
403
|
if (component) {
|
|
@@ -119,7 +119,10 @@ export class ExtensionUiController {
|
|
|
119
119
|
abort: () => this.ctx.session.abort(),
|
|
120
120
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
121
121
|
shutdown: () => {
|
|
122
|
-
//
|
|
122
|
+
// Defer the actual teardown to the main loop, which calls
|
|
123
|
+
// `checkShutdownRequested()` at idle boundaries so any queued
|
|
124
|
+
// steering / follow-up messages drain first (see issue #1020).
|
|
125
|
+
this.ctx.shutdownRequested = true;
|
|
123
126
|
},
|
|
124
127
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
125
128
|
compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
|
|
@@ -356,7 +359,10 @@ export class ExtensionUiController {
|
|
|
356
359
|
abort: () => this.ctx.session.abort(),
|
|
357
360
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
358
361
|
shutdown: () => {
|
|
359
|
-
//
|
|
362
|
+
// Defer the actual teardown to the main loop, which calls
|
|
363
|
+
// `checkShutdownRequested()` at idle boundaries so any queued
|
|
364
|
+
// steering / follow-up messages drain first (see issue #1020).
|
|
365
|
+
this.ctx.shutdownRequested = true;
|
|
360
366
|
},
|
|
361
367
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
362
368
|
compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
|