@oh-my-pi/pi-coding-agent 13.14.0 → 13.15.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 +140 -0
- package/package.json +10 -8
- package/src/autoresearch/command-initialize.md +34 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +332 -0
- package/src/autoresearch/dashboard.ts +447 -0
- package/src/autoresearch/git.ts +243 -0
- package/src/autoresearch/helpers.ts +458 -0
- package/src/autoresearch/index.ts +693 -0
- package/src/autoresearch/prompt.md +227 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +386 -0
- package/src/autoresearch/tools/init-experiment.ts +310 -0
- package/src/autoresearch/tools/log-experiment.ts +833 -0
- package/src/autoresearch/tools/run-experiment.ts +640 -0
- package/src/autoresearch/types.ts +218 -0
- package/src/cli/args.ts +8 -2
- package/src/cli/initial-message.ts +58 -0
- package/src/config/keybindings.ts +417 -212
- package/src/config/model-registry.ts +1 -0
- package/src/config/model-resolver.ts +57 -9
- package/src/config/settings-schema.ts +38 -10
- package/src/config/settings.ts +1 -4
- package/src/exec/bash-executor.ts +7 -5
- package/src/export/html/template.css +43 -13
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.html +1 -0
- package/src/export/html/template.js +107 -0
- package/src/extensibility/extensions/types.ts +31 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +44 -44
- package/src/mcp/oauth-discovery.ts +1 -1
- package/src/modes/acp/acp-agent.ts +957 -0
- package/src/modes/acp/acp-event-mapper.ts +531 -0
- package/src/modes/acp/acp-mode.ts +13 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +5 -4
- package/src/modes/components/bash-execution.ts +40 -11
- package/src/modes/components/custom-editor.ts +47 -47
- package/src/modes/components/extensions/extension-dashboard.ts +2 -1
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +2 -1
- package/src/modes/components/hook-input.ts +8 -7
- package/src/modes/components/hook-selector.ts +15 -10
- package/src/modes/components/keybinding-hints.ts +9 -9
- package/src/modes/components/login-dialog.ts +3 -3
- package/src/modes/components/mcp-add-wizard.ts +2 -1
- package/src/modes/components/model-selector.ts +14 -3
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/python-execution.ts +2 -3
- package/src/modes/components/session-selector.ts +2 -1
- package/src/modes/components/settings-selector.ts +2 -1
- package/src/modes/components/status-line-segment-editor.ts +2 -1
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/user-message-selector.ts +3 -8
- package/src/modes/components/user-message.ts +16 -0
- package/src/modes/controllers/command-controller.ts +0 -2
- package/src/modes/controllers/extension-ui-controller.ts +89 -4
- package/src/modes/controllers/input-controller.ts +29 -23
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/prompt-action-autocomplete.ts +7 -7
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/theme/theme.ts +53 -44
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/hotkeys-markdown.ts +19 -19
- package/src/modes/utils/keybinding-matchers.ts +21 -0
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/hashline.ts +139 -127
- package/src/patch/index.ts +77 -59
- package/src/patch/shared.ts +19 -11
- package/src/prompts/tools/hashline.md +43 -116
- package/src/sdk.ts +34 -17
- package/src/session/agent-session.ts +123 -30
- package/src/session/session-manager.ts +32 -31
- package/src/session/streaming-output.ts +87 -37
- package/src/tools/ask.ts +56 -30
- package/src/tools/bash-interactive.ts +2 -6
- package/src/tools/bash-interceptor.ts +1 -39
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/gemini-image.ts +1 -1
- package/src/tools/python.ts +2 -2
- package/src/tools/resolve.ts +1 -1
- package/src/utils/child-process.ts +88 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionNotification,
|
|
3
|
+
SessionUpdate,
|
|
4
|
+
ToolCallContent,
|
|
5
|
+
ToolCallLocation,
|
|
6
|
+
ToolKind,
|
|
7
|
+
} from "@agentclientprotocol/sdk";
|
|
8
|
+
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
9
|
+
import type { TodoStatus } from "../../tools/todo-write";
|
|
10
|
+
|
|
11
|
+
interface ContentArrayContainer {
|
|
12
|
+
content?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TypedValue {
|
|
16
|
+
type?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TextLikeContent extends TypedValue {
|
|
20
|
+
text?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BinaryLikeContent extends TypedValue {
|
|
24
|
+
data?: unknown;
|
|
25
|
+
mimeType?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PathContainer {
|
|
29
|
+
path?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface OldPathContainer {
|
|
33
|
+
oldPath?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface NewPathContainer {
|
|
37
|
+
newPath?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CommandContainer {
|
|
41
|
+
command?: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PatternContainer {
|
|
45
|
+
pattern?: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface QueryContainer {
|
|
49
|
+
query?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ErrorMessageContainer {
|
|
53
|
+
errorMessage?: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MessageContainer {
|
|
57
|
+
message?: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ResourceLinkLikeContent extends TypedValue {
|
|
61
|
+
uri?: unknown;
|
|
62
|
+
name?: unknown;
|
|
63
|
+
title?: unknown;
|
|
64
|
+
description?: unknown;
|
|
65
|
+
mimeType?: unknown;
|
|
66
|
+
size?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface BlobResourceLike {
|
|
70
|
+
uri?: unknown;
|
|
71
|
+
blob?: unknown;
|
|
72
|
+
mimeType?: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface TextResourceLike {
|
|
76
|
+
uri?: unknown;
|
|
77
|
+
text?: unknown;
|
|
78
|
+
mimeType?: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface EmbeddedResourceLikeContent extends TypedValue {
|
|
82
|
+
resource?: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface TextMessageLike {
|
|
86
|
+
role?: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ACP_TEXT_LIMIT = 4_000;
|
|
90
|
+
|
|
91
|
+
export function mapToolKind(toolName: string): ToolKind {
|
|
92
|
+
switch (toolName) {
|
|
93
|
+
case "read":
|
|
94
|
+
return "read";
|
|
95
|
+
case "write":
|
|
96
|
+
case "edit":
|
|
97
|
+
return "edit";
|
|
98
|
+
case "delete":
|
|
99
|
+
return "delete";
|
|
100
|
+
case "move":
|
|
101
|
+
return "move";
|
|
102
|
+
case "bash":
|
|
103
|
+
case "python":
|
|
104
|
+
return "execute";
|
|
105
|
+
case "grep":
|
|
106
|
+
case "find":
|
|
107
|
+
case "ast_grep":
|
|
108
|
+
return "search";
|
|
109
|
+
case "fetch":
|
|
110
|
+
case "web_search":
|
|
111
|
+
return "fetch";
|
|
112
|
+
case "todo_write":
|
|
113
|
+
return "think";
|
|
114
|
+
default:
|
|
115
|
+
return "other";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function mapAgentSessionEventToAcpSessionUpdates(
|
|
120
|
+
event: AgentSessionEvent,
|
|
121
|
+
sessionId: string,
|
|
122
|
+
): SessionNotification[] {
|
|
123
|
+
switch (event.type) {
|
|
124
|
+
case "message_update":
|
|
125
|
+
return mapAssistantMessageUpdate(event, sessionId);
|
|
126
|
+
case "tool_execution_start": {
|
|
127
|
+
const update: SessionUpdate = {
|
|
128
|
+
sessionUpdate: "tool_call",
|
|
129
|
+
toolCallId: event.toolCallId,
|
|
130
|
+
title: buildToolTitle(event.toolName, event.args, event.intent),
|
|
131
|
+
kind: mapToolKind(event.toolName),
|
|
132
|
+
status: "pending",
|
|
133
|
+
rawInput: event.args,
|
|
134
|
+
};
|
|
135
|
+
const locations = extractToolLocations(event.args);
|
|
136
|
+
if (locations.length > 0) {
|
|
137
|
+
update.locations = locations;
|
|
138
|
+
}
|
|
139
|
+
return [toSessionNotification(sessionId, update)];
|
|
140
|
+
}
|
|
141
|
+
case "tool_execution_update": {
|
|
142
|
+
const content = extractToolCallContent(event.partialResult);
|
|
143
|
+
const update: SessionUpdate = {
|
|
144
|
+
sessionUpdate: "tool_call_update",
|
|
145
|
+
toolCallId: event.toolCallId,
|
|
146
|
+
status: "in_progress",
|
|
147
|
+
rawOutput: event.partialResult,
|
|
148
|
+
};
|
|
149
|
+
if (content.length > 0) {
|
|
150
|
+
update.content = content;
|
|
151
|
+
}
|
|
152
|
+
return [toSessionNotification(sessionId, update)];
|
|
153
|
+
}
|
|
154
|
+
case "tool_execution_end": {
|
|
155
|
+
const content = extractToolCallContent(event.result);
|
|
156
|
+
const update: SessionUpdate = {
|
|
157
|
+
sessionUpdate: "tool_call_update",
|
|
158
|
+
toolCallId: event.toolCallId,
|
|
159
|
+
status: event.isError ? "failed" : "completed",
|
|
160
|
+
rawOutput: event.result,
|
|
161
|
+
};
|
|
162
|
+
if (content.length > 0) {
|
|
163
|
+
update.content = content;
|
|
164
|
+
}
|
|
165
|
+
return [toSessionNotification(sessionId, update)];
|
|
166
|
+
}
|
|
167
|
+
case "todo_reminder": {
|
|
168
|
+
const entries = event.todos.map(todo => ({
|
|
169
|
+
content: todo.content,
|
|
170
|
+
priority: "medium" as const,
|
|
171
|
+
status: mapTodoStatus(todo.status),
|
|
172
|
+
}));
|
|
173
|
+
return [toSessionNotification(sessionId, { sessionUpdate: "plan", entries })];
|
|
174
|
+
}
|
|
175
|
+
case "todo_auto_clear":
|
|
176
|
+
return [toSessionNotification(sessionId, { sessionUpdate: "plan", entries: [] })];
|
|
177
|
+
default:
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function mapAssistantMessageUpdate(
|
|
183
|
+
event: Extract<AgentSessionEvent, { type: "message_update" }>,
|
|
184
|
+
sessionId: string,
|
|
185
|
+
): SessionNotification[] {
|
|
186
|
+
if (!isAssistantMessage(event.message)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let sessionUpdate: "agent_message_chunk" | "agent_thought_chunk";
|
|
191
|
+
let text: string;
|
|
192
|
+
switch (event.assistantMessageEvent.type) {
|
|
193
|
+
case "text_delta":
|
|
194
|
+
sessionUpdate = "agent_message_chunk";
|
|
195
|
+
text = event.assistantMessageEvent.delta;
|
|
196
|
+
break;
|
|
197
|
+
case "thinking_delta":
|
|
198
|
+
sessionUpdate = "agent_thought_chunk";
|
|
199
|
+
text = event.assistantMessageEvent.delta;
|
|
200
|
+
break;
|
|
201
|
+
case "error":
|
|
202
|
+
sessionUpdate = "agent_message_chunk";
|
|
203
|
+
text = event.assistantMessageEvent.error.errorMessage ?? "Unknown error";
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
if (text.length === 0) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
toSessionNotification(sessionId, {
|
|
214
|
+
sessionUpdate,
|
|
215
|
+
content: { type: "text", text },
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toSessionNotification(sessionId: string, update: SessionUpdate): SessionNotification {
|
|
221
|
+
return { sessionId, update };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const todoStatusMap: Record<TodoStatus, "pending" | "in_progress" | "completed"> = {
|
|
225
|
+
pending: "pending",
|
|
226
|
+
in_progress: "in_progress",
|
|
227
|
+
completed: "completed",
|
|
228
|
+
abandoned: "completed",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "completed" {
|
|
232
|
+
return todoStatusMap[status];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string {
|
|
236
|
+
const trimmedIntent = intent?.trim();
|
|
237
|
+
if (trimmedIntent) {
|
|
238
|
+
return trimmedIntent;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const subject =
|
|
242
|
+
extractStringProperty<PathContainer>(args, "path") ??
|
|
243
|
+
extractStringProperty<CommandContainer>(args, "command") ??
|
|
244
|
+
extractStringProperty<PatternContainer>(args, "pattern") ??
|
|
245
|
+
extractStringProperty<QueryContainer>(args, "query");
|
|
246
|
+
if (subject) {
|
|
247
|
+
return `${toolName}: ${subject}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return toolName;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractToolLocations(args: unknown): ToolCallLocation[] {
|
|
254
|
+
const locations: ToolCallLocation[] = [];
|
|
255
|
+
const path = extractStringProperty<PathContainer>(args, "path");
|
|
256
|
+
if (path) {
|
|
257
|
+
locations.push({ path });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const oldPath = extractStringProperty<OldPathContainer>(args, "oldPath");
|
|
261
|
+
if (oldPath && oldPath !== path) {
|
|
262
|
+
locations.push({ path: oldPath });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const newPath = extractStringProperty<NewPathContainer>(args, "newPath");
|
|
266
|
+
if (newPath && newPath !== path && newPath !== oldPath) {
|
|
267
|
+
locations.push({ path: newPath });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return locations;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
274
|
+
const richContent = extractStructuredToolCallContent(value);
|
|
275
|
+
const fallbackText = extractReadableText(value);
|
|
276
|
+
if (!fallbackText) {
|
|
277
|
+
return richContent;
|
|
278
|
+
}
|
|
279
|
+
if (hasEquivalentTextContent(richContent, fallbackText)) {
|
|
280
|
+
return richContent;
|
|
281
|
+
}
|
|
282
|
+
return [...richContent, textToolCallContent(fallbackText)];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
286
|
+
const blocks = getContentBlocks(value);
|
|
287
|
+
if (!blocks) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const content: ToolCallContent[] = [];
|
|
292
|
+
for (const block of blocks) {
|
|
293
|
+
const toolCallContent = toToolCallContent(block);
|
|
294
|
+
if (toolCallContent) {
|
|
295
|
+
content.push(toolCallContent);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return content;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getContentBlocks(value: unknown): unknown[] | undefined {
|
|
302
|
+
if (Array.isArray(value)) {
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
if (typeof value !== "object" || value === null || !("content" in value)) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
const content = (value as ContentArrayContainer).content;
|
|
309
|
+
return Array.isArray(content) ? content : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
313
|
+
const type = getContentType(value);
|
|
314
|
+
if (!type) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
switch (type) {
|
|
319
|
+
case "text": {
|
|
320
|
+
const text = extractStructuredText(value);
|
|
321
|
+
return text ? textToolCallContent(text) : undefined;
|
|
322
|
+
}
|
|
323
|
+
case "image":
|
|
324
|
+
case "audio": {
|
|
325
|
+
const data = extractStringProperty<BinaryLikeContent>(value, "data");
|
|
326
|
+
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
327
|
+
if (!data || !mimeType) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
type: "content",
|
|
332
|
+
content: {
|
|
333
|
+
type,
|
|
334
|
+
data,
|
|
335
|
+
mimeType,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
case "resource_link": {
|
|
340
|
+
const uri = extractStringProperty<ResourceLinkLikeContent>(value, "uri");
|
|
341
|
+
const name = extractStringProperty<ResourceLinkLikeContent>(value, "name");
|
|
342
|
+
if (!uri || !name) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
const resourceLinkContent: {
|
|
346
|
+
type: "resource_link";
|
|
347
|
+
uri: string;
|
|
348
|
+
name: string;
|
|
349
|
+
title?: string;
|
|
350
|
+
description?: string;
|
|
351
|
+
mimeType?: string;
|
|
352
|
+
size?: number;
|
|
353
|
+
} = {
|
|
354
|
+
type: "resource_link",
|
|
355
|
+
uri,
|
|
356
|
+
name,
|
|
357
|
+
};
|
|
358
|
+
const title = extractStringProperty<ResourceLinkLikeContent>(value, "title");
|
|
359
|
+
if (title) {
|
|
360
|
+
resourceLinkContent.title = title;
|
|
361
|
+
}
|
|
362
|
+
const description = extractStringProperty<ResourceLinkLikeContent>(value, "description");
|
|
363
|
+
if (description) {
|
|
364
|
+
resourceLinkContent.description = description;
|
|
365
|
+
}
|
|
366
|
+
const mimeType = extractStringProperty<ResourceLinkLikeContent>(value, "mimeType");
|
|
367
|
+
if (mimeType) {
|
|
368
|
+
resourceLinkContent.mimeType = mimeType;
|
|
369
|
+
}
|
|
370
|
+
const size = extractNumberProperty<ResourceLinkLikeContent>(value, "size");
|
|
371
|
+
if (size !== undefined) {
|
|
372
|
+
resourceLinkContent.size = size;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
type: "content",
|
|
376
|
+
content: resourceLinkContent,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case "resource": {
|
|
380
|
+
const resource = extractEmbeddedResource(value);
|
|
381
|
+
return resource
|
|
382
|
+
? {
|
|
383
|
+
type: "content",
|
|
384
|
+
content: {
|
|
385
|
+
type: "resource",
|
|
386
|
+
resource,
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
: undefined;
|
|
390
|
+
}
|
|
391
|
+
default:
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function extractEmbeddedResource(
|
|
397
|
+
value: unknown,
|
|
398
|
+
): { uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } | undefined {
|
|
399
|
+
if (typeof value !== "object" || value === null || !("resource" in value)) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const resource = (value as EmbeddedResourceLikeContent).resource;
|
|
404
|
+
if (typeof resource !== "object" || resource === null) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const uri = extractStringProperty<TextResourceLike>(resource, "uri");
|
|
409
|
+
if (!uri) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const text = extractStringProperty<TextResourceLike>(resource, "text");
|
|
414
|
+
if (text) {
|
|
415
|
+
const mimeType = extractStringProperty<TextResourceLike>(resource, "mimeType");
|
|
416
|
+
return mimeType ? { uri, text, mimeType } : { uri, text };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const blob = extractStringProperty<BlobResourceLike>(resource, "blob");
|
|
420
|
+
if (!blob) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const mimeType = extractStringProperty<BlobResourceLike>(resource, "mimeType");
|
|
424
|
+
return mimeType ? { uri, blob, mimeType } : { uri, blob };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function textToolCallContent(text: string): ToolCallContent {
|
|
428
|
+
return {
|
|
429
|
+
type: "content",
|
|
430
|
+
content: {
|
|
431
|
+
type: "text",
|
|
432
|
+
text,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function hasEquivalentTextContent(content: ToolCallContent[], text: string): boolean {
|
|
438
|
+
return content.some(item => item.type === "content" && item.content.type === "text" && item.content.text === text);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function extractReadableText(value: unknown): string | undefined {
|
|
442
|
+
if (typeof value === "string") {
|
|
443
|
+
return normalizeText(value);
|
|
444
|
+
}
|
|
445
|
+
if (value instanceof Error) {
|
|
446
|
+
return normalizeText(value.message);
|
|
447
|
+
}
|
|
448
|
+
if (typeof value !== "object" || value === null) {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const directText =
|
|
453
|
+
extractStringProperty<TextLikeContent>(value, "text") ??
|
|
454
|
+
extractStringProperty<ErrorMessageContainer>(value, "errorMessage") ??
|
|
455
|
+
extractStringProperty<MessageContainer>(value, "message");
|
|
456
|
+
if (directText) {
|
|
457
|
+
return normalizeText(directText);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const contentBlocks = getContentBlocks(value);
|
|
461
|
+
if (contentBlocks) {
|
|
462
|
+
const text = contentBlocks
|
|
463
|
+
.map(block => extractStructuredText(block))
|
|
464
|
+
.filter((chunk): chunk is string => typeof chunk === "string" && chunk.length > 0)
|
|
465
|
+
.join("\n");
|
|
466
|
+
if (text.length > 0) {
|
|
467
|
+
return normalizeText(text);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const serialized = safeJsonStringify(value);
|
|
472
|
+
return normalizeText(serialized);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function extractStructuredText(value: unknown): string | undefined {
|
|
476
|
+
const text = extractStringProperty<TextLikeContent>(value, "text");
|
|
477
|
+
if (!text) {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
return limitText(text);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function getContentType(value: unknown): string | undefined {
|
|
484
|
+
if (typeof value !== "object" || value === null || !("type" in value)) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
const type = (value as TypedValue).type;
|
|
488
|
+
return typeof type === "string" ? type : undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function extractStringProperty<T extends object>(value: unknown, key: keyof T): string | undefined {
|
|
492
|
+
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
const property = (value as T)[key];
|
|
496
|
+
return typeof property === "string" && property.length > 0 ? property : undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function extractNumberProperty<T extends object>(value: unknown, key: keyof T): number | undefined {
|
|
500
|
+
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
const property = (value as T)[key];
|
|
504
|
+
return typeof property === "number" && Number.isFinite(property) ? property : undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function isAssistantMessage(value: unknown): boolean {
|
|
508
|
+
return (
|
|
509
|
+
typeof value === "object" && value !== null && "role" in value && (value as TextMessageLike).role === "assistant"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function normalizeText(text: string | undefined): string | undefined {
|
|
514
|
+
if (!text) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
const normalized = text.trim();
|
|
518
|
+
return normalized.length > 0 ? limitText(normalized) : undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function limitText(text: string): string {
|
|
522
|
+
return text.length > ACP_TEXT_LIMIT ? `${text.slice(0, ACP_TEXT_LIMIT - 1)}…` : text;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function safeJsonStringify(value: unknown): string | undefined {
|
|
526
|
+
try {
|
|
527
|
+
return JSON.stringify(value);
|
|
528
|
+
} catch {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as stream from "node:stream";
|
|
2
|
+
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
3
|
+
import type { AgentSession } from "../../session/agent-session";
|
|
4
|
+
import { AcpAgent } from "./acp-agent";
|
|
5
|
+
|
|
6
|
+
export async function runAcpMode(session: AgentSession): Promise<never> {
|
|
7
|
+
const input = stream.Writable.toWeb(process.stdout);
|
|
8
|
+
const output = stream.Readable.toWeb(process.stdin);
|
|
9
|
+
const transport = ndJsonStream(input, output);
|
|
10
|
+
const connection = new AgentSideConnection(conn => new AcpAgent(conn, session), transport);
|
|
11
|
+
await connection.closed;
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
@@ -50,6 +50,7 @@ import { discoverAgents } from "../../task/discovery";
|
|
|
50
50
|
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
51
51
|
import { shortenPath } from "../../tools/render-utils";
|
|
52
52
|
import { theme } from "../theme/theme";
|
|
53
|
+
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
53
54
|
import { DynamicBorder } from "./dynamic-border";
|
|
54
55
|
|
|
55
56
|
type SourceTabId = "all" | AgentSource;
|
|
@@ -993,7 +994,7 @@ export class AgentDashboard extends Container {
|
|
|
993
994
|
}
|
|
994
995
|
|
|
995
996
|
if (this.#createSpec) {
|
|
996
|
-
if (
|
|
997
|
+
if (matchesAppInterrupt(data)) {
|
|
997
998
|
this.#clearCreateFlow();
|
|
998
999
|
this.#buildLayout();
|
|
999
1000
|
return;
|
|
@@ -1017,7 +1018,7 @@ export class AgentDashboard extends Container {
|
|
|
1017
1018
|
}
|
|
1018
1019
|
|
|
1019
1020
|
if (this.#createInput || this.#createGenerating) {
|
|
1020
|
-
if (
|
|
1021
|
+
if (matchesAppInterrupt(data)) {
|
|
1021
1022
|
if (!this.#createGenerating) {
|
|
1022
1023
|
this.#clearCreateFlow();
|
|
1023
1024
|
this.#buildLayout();
|
|
@@ -1037,7 +1038,7 @@ export class AgentDashboard extends Container {
|
|
|
1037
1038
|
}
|
|
1038
1039
|
|
|
1039
1040
|
if (this.#editInput) {
|
|
1040
|
-
if (
|
|
1041
|
+
if (matchesAppInterrupt(data)) {
|
|
1041
1042
|
this.#cancelModelEdit();
|
|
1042
1043
|
return;
|
|
1043
1044
|
}
|
|
@@ -1048,7 +1049,7 @@ export class AgentDashboard extends Container {
|
|
|
1048
1049
|
return;
|
|
1049
1050
|
}
|
|
1050
1051
|
|
|
1051
|
-
if (
|
|
1052
|
+
if (matchesAppInterrupt(data)) {
|
|
1052
1053
|
if (this.#searchQuery.length > 0) {
|
|
1053
1054
|
this.#searchQuery = "";
|
|
1054
1055
|
this.#applyFilters();
|
|
@@ -6,13 +6,17 @@ import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
|
6
6
|
import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
8
8
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
9
|
-
import { getSixelLineMask, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
9
|
+
import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
10
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
11
11
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
12
12
|
|
|
13
13
|
// Preview line limit when not expanded (matches tool execution behavior)
|
|
14
14
|
const PREVIEW_LINES = 20;
|
|
15
|
+
const STREAMING_LINE_CAP = PREVIEW_LINES * 5;
|
|
15
16
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
17
|
+
// Minimum interval between processing incoming chunks for display (ms).
|
|
18
|
+
// Chunks arriving faster than this are accumulated and processed in one batch.
|
|
19
|
+
const CHUNK_THROTTLE_MS = 50;
|
|
16
20
|
|
|
17
21
|
export class BashExecutionComponent extends Container {
|
|
18
22
|
#outputLines: string[] = [];
|
|
@@ -21,7 +25,10 @@ export class BashExecutionComponent extends Container {
|
|
|
21
25
|
#loader: Loader;
|
|
22
26
|
#truncation?: TruncationMeta;
|
|
23
27
|
#expanded = false;
|
|
28
|
+
#displayDirty = false;
|
|
29
|
+
#chunkGate = false;
|
|
24
30
|
#contentContainer: Container;
|
|
31
|
+
#headerText: Text;
|
|
25
32
|
|
|
26
33
|
constructor(
|
|
27
34
|
private readonly command: string,
|
|
@@ -45,8 +52,8 @@ export class BashExecutionComponent extends Container {
|
|
|
45
52
|
this.addChild(this.#contentContainer);
|
|
46
53
|
|
|
47
54
|
// Command header
|
|
48
|
-
|
|
49
|
-
this.#contentContainer.addChild(
|
|
55
|
+
this.#headerText = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);
|
|
56
|
+
this.#contentContainer.addChild(this.#headerText);
|
|
50
57
|
|
|
51
58
|
// Loader
|
|
52
59
|
this.#loader = new Loader(
|
|
@@ -72,14 +79,22 @@ export class BashExecutionComponent extends Container {
|
|
|
72
79
|
|
|
73
80
|
override invalidate(): void {
|
|
74
81
|
super.invalidate();
|
|
82
|
+
this.#displayDirty = false;
|
|
75
83
|
this.#updateDisplay();
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
appendOutput(chunk: string): void {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
87
|
+
// During high-throughput output (e.g. seq 1 500M), processing every
|
|
88
|
+
// chunk would saturate the event loop. Instead, accept one chunk per
|
|
89
|
+
// throttle window and drop the rest — the OutputSink captures everything
|
|
90
|
+
// for the artifact, and setComplete() replaces with the final output.
|
|
91
|
+
if (this.#chunkGate) return;
|
|
92
|
+
this.#chunkGate = true;
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
this.#chunkGate = false;
|
|
95
|
+
}, CHUNK_THROTTLE_MS);
|
|
96
|
+
|
|
97
|
+
const incomingLines = chunk.split("\n");
|
|
83
98
|
if (this.#outputLines.length > 0 && incomingLines.length > 0) {
|
|
84
99
|
const lastIndex = this.#outputLines.length - 1;
|
|
85
100
|
const mergedLines = [`${this.#outputLines[lastIndex]}${incomingLines[0]}`, ...incomingLines.slice(1)];
|
|
@@ -90,7 +105,12 @@ export class BashExecutionComponent extends Container {
|
|
|
90
105
|
this.#outputLines.push(...this.#clampLinesPreservingSixel(incomingLines));
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
// Cap stored lines during streaming to avoid unbounded memory growth
|
|
109
|
+
if (this.#outputLines.length > STREAMING_LINE_CAP) {
|
|
110
|
+
this.#outputLines = this.#outputLines.slice(-STREAMING_LINE_CAP);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.#displayDirty = true;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
setComplete(
|
|
@@ -115,6 +135,14 @@ export class BashExecutionComponent extends Container {
|
|
|
115
135
|
this.#updateDisplay();
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
override render(width: number): string[] {
|
|
139
|
+
if (this.#displayDirty) {
|
|
140
|
+
this.#displayDirty = false;
|
|
141
|
+
this.#updateDisplay();
|
|
142
|
+
}
|
|
143
|
+
return super.render(width);
|
|
144
|
+
}
|
|
145
|
+
|
|
118
146
|
#updateDisplay(): void {
|
|
119
147
|
const availableLines = this.#outputLines;
|
|
120
148
|
|
|
@@ -122,15 +150,16 @@ export class BashExecutionComponent extends Container {
|
|
|
122
150
|
const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
|
|
123
151
|
const hiddenLineCount = availableLines.length - previewLogicalLines.length;
|
|
124
152
|
const sixelLineMask =
|
|
125
|
-
TERMINAL.imageProtocol === ImageProtocol.Sixel
|
|
153
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled()
|
|
154
|
+
? getSixelLineMask(availableLines)
|
|
155
|
+
: undefined;
|
|
126
156
|
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
127
157
|
|
|
128
158
|
// Rebuild content container
|
|
129
159
|
this.#contentContainer.clear();
|
|
130
160
|
|
|
131
161
|
// Command header
|
|
132
|
-
|
|
133
|
-
this.#contentContainer.addChild(header);
|
|
162
|
+
this.#contentContainer.addChild(this.#headerText);
|
|
134
163
|
|
|
135
164
|
// Output
|
|
136
165
|
if (availableLines.length > 0) {
|