@oh-my-pi/pi-coding-agent 13.3.10 → 13.3.13
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 +7 -0
- package/package.json +7 -7
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/modes/components/assistant-message.ts +40 -2
- package/src/modes/controllers/event-controller.ts +120 -22
- package/src/modes/utils/ui-helpers.ts +54 -8
- package/src/tools/path-utils.ts +35 -24
- package/src/tools/submit-result.ts +2 -3
- package/src/web/search/providers/jina.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.3.11] - 2026-02-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Restored inline rendering for `read` tool image results in assistant transcript components, including streaming and rebuilt session history paths.
|
|
10
|
+
- Fixed shell-escaped read paths (for example, pasted `\ `-escaped screenshot filenames) by resolving unescaped fallback candidates before macOS filename normalization variants.
|
|
11
|
+
|
|
5
12
|
## [13.3.8] - 2026-02-28
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.3.
|
|
4
|
+
"version": "13.3.13",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.3.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.3.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.3.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.3.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.3.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.3.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.3.13",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.3.13",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.3.13",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.3.13",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.3.13",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.3.13",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -110,7 +110,7 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
|
|
|
110
110
|
while (true) {
|
|
111
111
|
let fd: fs.promises.FileHandle | undefined;
|
|
112
112
|
try {
|
|
113
|
-
fd = await fs.promises.open(lockPath,
|
|
113
|
+
fd = await fs.promises.open(lockPath, "wx");
|
|
114
114
|
let heartbeatRunning = true;
|
|
115
115
|
const heartbeat = (async () => {
|
|
116
116
|
while (heartbeatRunning) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { Container, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache";
|
|
5
5
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
@@ -11,6 +11,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
11
11
|
#contentContainer: Container;
|
|
12
12
|
#lastMessage?: AssistantMessage;
|
|
13
13
|
#prerenderInFlight = false;
|
|
14
|
+
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
14
15
|
|
|
15
16
|
constructor(
|
|
16
17
|
message?: AssistantMessage,
|
|
@@ -38,6 +39,42 @@ export class AssistantMessageComponent extends Container {
|
|
|
38
39
|
this.hideThinkingBlock = hide;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
43
|
+
if (!toolCallId) return;
|
|
44
|
+
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
45
|
+
if (validImages.length === 0) {
|
|
46
|
+
this.#toolImagesByCallId.delete(toolCallId);
|
|
47
|
+
} else {
|
|
48
|
+
this.#toolImagesByCallId.set(toolCallId, validImages);
|
|
49
|
+
}
|
|
50
|
+
if (this.#lastMessage) {
|
|
51
|
+
this.updateContent(this.#lastMessage);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#renderToolImages(): void {
|
|
56
|
+
const images = Array.from(this.#toolImagesByCallId.values()).flat();
|
|
57
|
+
if (images.length === 0) return;
|
|
58
|
+
|
|
59
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
60
|
+
for (const image of images) {
|
|
61
|
+
if (
|
|
62
|
+
TERMINAL.imageProtocol &&
|
|
63
|
+
(TERMINAL.imageProtocol !== ImageProtocol.Kitty || image.mimeType === "image/png")
|
|
64
|
+
) {
|
|
65
|
+
this.#contentContainer.addChild(
|
|
66
|
+
new Image(
|
|
67
|
+
image.data,
|
|
68
|
+
image.mimeType,
|
|
69
|
+
{ fallbackColor: (text: string) => theme.fg("toolOutput", text) },
|
|
70
|
+
{ maxWidthCells: 60 },
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
41
78
|
#triggerMermaidPrerender(message: AssistantMessage): void {
|
|
42
79
|
if (!TERMINAL.imageProtocol || this.#prerenderInFlight) return;
|
|
43
80
|
|
|
@@ -119,6 +156,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
119
156
|
}
|
|
120
157
|
}
|
|
121
158
|
|
|
159
|
+
this.#renderToolImages();
|
|
122
160
|
// Check if aborted - show after partial content
|
|
123
161
|
// But only if there are no tool calls (tool execution components will show the error)
|
|
124
162
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
3
|
import { Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { settings } from "../../config/settings";
|
|
4
5
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
@@ -17,7 +18,9 @@ export class EventController {
|
|
|
17
18
|
#renderedCustomMessages = new Set<string>();
|
|
18
19
|
#lastIntent: string | undefined = undefined;
|
|
19
20
|
#backgroundToolCallIds = new Set<string>();
|
|
20
|
-
|
|
21
|
+
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
22
|
+
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
23
|
+
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
21
24
|
constructor(private ctx: InteractiveModeContext) {}
|
|
22
25
|
|
|
23
26
|
#resetReadGroup(): void {
|
|
@@ -35,6 +38,39 @@ export class EventController {
|
|
|
35
38
|
return this.#lastReadGroup;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
#trackReadToolCall(toolCallId: string, args: unknown): void {
|
|
42
|
+
if (!toolCallId) return;
|
|
43
|
+
const normalizedArgs =
|
|
44
|
+
args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
|
|
45
|
+
this.#readToolCallArgs.set(toolCallId, normalizedArgs);
|
|
46
|
+
const assistantComponent = this.ctx.streamingComponent ?? this.#lastAssistantComponent;
|
|
47
|
+
if (assistantComponent) {
|
|
48
|
+
this.#readToolCallAssistantComponents.set(toolCallId, assistantComponent);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#clearReadToolCall(toolCallId: string): void {
|
|
53
|
+
this.#readToolCallArgs.delete(toolCallId);
|
|
54
|
+
this.#readToolCallAssistantComponents.delete(toolCallId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#inlineReadToolImages(
|
|
58
|
+
toolCallId: string,
|
|
59
|
+
result: { content: Array<{ type: string; data?: string; mimeType?: string }> },
|
|
60
|
+
): boolean {
|
|
61
|
+
if (!settings.get("terminal.showImages")) return false;
|
|
62
|
+
const assistantComponent = this.#readToolCallAssistantComponents.get(toolCallId);
|
|
63
|
+
if (!assistantComponent) return false;
|
|
64
|
+
const images: ImageContent[] = result.content
|
|
65
|
+
.filter(
|
|
66
|
+
(content): content is ImageContent =>
|
|
67
|
+
content.type === "image" && typeof content.data === "string" && typeof content.mimeType === "string",
|
|
68
|
+
)
|
|
69
|
+
.map(content => ({ type: "image", data: content.data, mimeType: content.mimeType }));
|
|
70
|
+
if (images.length === 0) return false;
|
|
71
|
+
assistantComponent.setToolResultImages(toolCallId, images);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
38
74
|
#updateWorkingMessageFromIntent(intent: string | undefined): void {
|
|
39
75
|
const trimmed = intent?.trim();
|
|
40
76
|
if (!trimmed || trimmed === this.#lastIntent) return;
|
|
@@ -59,6 +95,9 @@ export class EventController {
|
|
|
59
95
|
switch (event.type) {
|
|
60
96
|
case "agent_start":
|
|
61
97
|
this.#lastIntent = undefined;
|
|
98
|
+
this.#readToolCallArgs.clear();
|
|
99
|
+
this.#readToolCallAssistantComponents.clear();
|
|
100
|
+
this.#lastAssistantComponent = undefined;
|
|
62
101
|
if (this.ctx.retryEscapeHandler) {
|
|
63
102
|
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
|
64
103
|
this.ctx.retryEscapeHandler = undefined;
|
|
@@ -132,15 +171,20 @@ export class EventController {
|
|
|
132
171
|
|
|
133
172
|
for (const content of this.ctx.streamingMessage.content) {
|
|
134
173
|
if (content.type !== "toolCall") continue;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
174
|
+
if (content.name === "read") {
|
|
175
|
+
this.#trackReadToolCall(content.id, content.arguments);
|
|
176
|
+
const component = this.ctx.pendingTools.get(content.id);
|
|
177
|
+
if (component) {
|
|
178
|
+
component.updateArgs(content.arguments, content.id);
|
|
179
|
+
} else {
|
|
138
180
|
const group = this.#getReadGroup();
|
|
139
181
|
group.updateArgs(content.arguments, content.id);
|
|
140
182
|
this.ctx.pendingTools.set(content.id, group);
|
|
141
|
-
continue;
|
|
142
183
|
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
143
186
|
|
|
187
|
+
if (!this.ctx.pendingTools.has(content.id)) {
|
|
144
188
|
this.#resetReadGroup();
|
|
145
189
|
this.ctx.chatContainer.addChild(new Text("", 0, 0));
|
|
146
190
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
@@ -207,6 +251,7 @@ export class EventController {
|
|
|
207
251
|
component.setArgsComplete(toolCallId);
|
|
208
252
|
}
|
|
209
253
|
}
|
|
254
|
+
this.#lastAssistantComponent = this.ctx.streamingComponent;
|
|
210
255
|
this.ctx.streamingComponent = undefined;
|
|
211
256
|
this.ctx.streamingMessage = undefined;
|
|
212
257
|
this.ctx.statusLine.invalidate();
|
|
@@ -219,9 +264,15 @@ export class EventController {
|
|
|
219
264
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
220
265
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
221
266
|
if (event.toolName === "read") {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
this.#trackReadToolCall(event.toolCallId, event.args);
|
|
268
|
+
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
269
|
+
if (component) {
|
|
270
|
+
component.updateArgs(event.args, event.toolCallId);
|
|
271
|
+
} else {
|
|
272
|
+
const group = this.#getReadGroup();
|
|
273
|
+
group.updateArgs(event.args, event.toolCallId);
|
|
274
|
+
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
275
|
+
}
|
|
225
276
|
this.ctx.ui.requestRender();
|
|
226
277
|
break;
|
|
227
278
|
}
|
|
@@ -269,22 +320,66 @@ export class EventController {
|
|
|
269
320
|
}
|
|
270
321
|
|
|
271
322
|
case "tool_execution_end": {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
event.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
if (event.toolName === "read") {
|
|
324
|
+
if (this.#inlineReadToolImages(event.toolCallId, event.result)) {
|
|
325
|
+
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
326
|
+
if (component) {
|
|
327
|
+
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
328
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
329
|
+
}
|
|
330
|
+
const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
331
|
+
if (asyncState === "running") {
|
|
332
|
+
this.#backgroundToolCallIds.add(event.toolCallId);
|
|
333
|
+
} else {
|
|
334
|
+
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
335
|
+
this.#clearReadToolCall(event.toolCallId);
|
|
336
|
+
}
|
|
337
|
+
this.ctx.ui.requestRender();
|
|
283
338
|
} else {
|
|
284
|
-
this.ctx.pendingTools.
|
|
285
|
-
|
|
339
|
+
let component = this.ctx.pendingTools.get(event.toolCallId);
|
|
340
|
+
if (!component) {
|
|
341
|
+
const group = this.#getReadGroup();
|
|
342
|
+
const args = this.#readToolCallArgs.get(event.toolCallId);
|
|
343
|
+
if (args) {
|
|
344
|
+
group.updateArgs(args, event.toolCallId);
|
|
345
|
+
}
|
|
346
|
+
component = group;
|
|
347
|
+
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
348
|
+
}
|
|
349
|
+
const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
350
|
+
const isBackgroundRunning = asyncState === "running";
|
|
351
|
+
component.updateResult(
|
|
352
|
+
{ ...event.result, isError: event.isError },
|
|
353
|
+
isBackgroundRunning,
|
|
354
|
+
event.toolCallId,
|
|
355
|
+
);
|
|
356
|
+
if (isBackgroundRunning) {
|
|
357
|
+
this.#backgroundToolCallIds.add(event.toolCallId);
|
|
358
|
+
} else {
|
|
359
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
360
|
+
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
361
|
+
this.#clearReadToolCall(event.toolCallId);
|
|
362
|
+
}
|
|
363
|
+
this.ctx.ui.requestRender();
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
367
|
+
if (component) {
|
|
368
|
+
const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
369
|
+
const isBackgroundRunning = asyncState === "running";
|
|
370
|
+
component.updateResult(
|
|
371
|
+
{ ...event.result, isError: event.isError },
|
|
372
|
+
isBackgroundRunning,
|
|
373
|
+
event.toolCallId,
|
|
374
|
+
);
|
|
375
|
+
if (isBackgroundRunning) {
|
|
376
|
+
this.#backgroundToolCallIds.add(event.toolCallId);
|
|
377
|
+
} else {
|
|
378
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
379
|
+
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
380
|
+
}
|
|
381
|
+
this.ctx.ui.requestRender();
|
|
286
382
|
}
|
|
287
|
-
this.ctx.ui.requestRender();
|
|
288
383
|
}
|
|
289
384
|
// Update todo display when todo_write tool completes
|
|
290
385
|
if (event.toolName === "todo_write" && !event.isError) {
|
|
@@ -329,6 +424,9 @@ export class EventController {
|
|
|
329
424
|
this.#backgroundToolCallIds = new Set(
|
|
330
425
|
Array.from(this.#backgroundToolCallIds).filter(toolCallId => this.ctx.pendingTools.has(toolCallId)),
|
|
331
426
|
);
|
|
427
|
+
this.#readToolCallArgs.clear();
|
|
428
|
+
this.#readToolCallAssistantComponents.clear();
|
|
429
|
+
this.#lastAssistantComponent = undefined;
|
|
332
430
|
this.ctx.ui.requestRender();
|
|
333
431
|
this.sendCompletionNotification();
|
|
334
432
|
break;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { AssistantMessage, Message } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
@@ -217,10 +217,14 @@ export class UiHelpers {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
let readGroup: ReadToolGroupComponent | null = null;
|
|
220
|
+
const readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
221
|
+
const readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
220
222
|
for (const message of sessionContext.messages) {
|
|
221
223
|
// Assistant messages need special handling for tool calls
|
|
222
224
|
if (message.role === "assistant") {
|
|
223
225
|
this.ctx.addMessageToChat(message);
|
|
226
|
+
const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
|
|
227
|
+
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
|
224
228
|
readGroup = null;
|
|
225
229
|
const hasErrorStop = message.stopReason === "aborted" || message.stopReason === "error";
|
|
226
230
|
const errorMessage = hasErrorStop
|
|
@@ -241,20 +245,27 @@ export class UiHelpers {
|
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
if (content.name === "read") {
|
|
244
|
-
if (!readGroup) {
|
|
245
|
-
readGroup = new ReadToolGroupComponent();
|
|
246
|
-
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
247
|
-
this.ctx.chatContainer.addChild(readGroup);
|
|
248
|
-
}
|
|
249
|
-
readGroup.updateArgs(content.arguments, content.id);
|
|
250
248
|
if (hasErrorStop && errorMessage) {
|
|
249
|
+
if (!readGroup) {
|
|
250
|
+
readGroup = new ReadToolGroupComponent();
|
|
251
|
+
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
252
|
+
this.ctx.chatContainer.addChild(readGroup);
|
|
253
|
+
}
|
|
254
|
+
readGroup.updateArgs(content.arguments, content.id);
|
|
251
255
|
readGroup.updateResult(
|
|
252
256
|
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
253
257
|
false,
|
|
254
258
|
content.id,
|
|
255
259
|
);
|
|
256
260
|
} else {
|
|
257
|
-
|
|
261
|
+
const normalizedArgs =
|
|
262
|
+
content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
|
|
263
|
+
? (content.arguments as Record<string, unknown>)
|
|
264
|
+
: {};
|
|
265
|
+
readToolCallArgs.set(content.id, normalizedArgs);
|
|
266
|
+
if (assistantComponent) {
|
|
267
|
+
readToolCallAssistantComponents.set(content.id, assistantComponent);
|
|
268
|
+
}
|
|
258
269
|
}
|
|
259
270
|
continue;
|
|
260
271
|
}
|
|
@@ -287,6 +298,41 @@ export class UiHelpers {
|
|
|
287
298
|
}
|
|
288
299
|
}
|
|
289
300
|
} else if (message.role === "toolResult") {
|
|
301
|
+
if (message.toolName === "read") {
|
|
302
|
+
const assistantComponent = readToolCallAssistantComponents.get(message.toolCallId);
|
|
303
|
+
const images: ImageContent[] = message.content.filter(
|
|
304
|
+
(content): content is ImageContent => content.type === "image",
|
|
305
|
+
);
|
|
306
|
+
if (images.length > 0 && assistantComponent && settings.get("terminal.showImages")) {
|
|
307
|
+
assistantComponent.setToolResultImages(message.toolCallId, images);
|
|
308
|
+
const hasText = message.content.some(c => c.type === "text");
|
|
309
|
+
if (!hasText) {
|
|
310
|
+
readToolCallArgs.delete(message.toolCallId);
|
|
311
|
+
readToolCallAssistantComponents.delete(message.toolCallId);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
let component = this.ctx.pendingTools.get(message.toolCallId);
|
|
316
|
+
if (!component) {
|
|
317
|
+
if (!readGroup) {
|
|
318
|
+
readGroup = new ReadToolGroupComponent();
|
|
319
|
+
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
320
|
+
this.ctx.chatContainer.addChild(readGroup);
|
|
321
|
+
}
|
|
322
|
+
const args = readToolCallArgs.get(message.toolCallId);
|
|
323
|
+
if (args) {
|
|
324
|
+
readGroup.updateArgs(args, message.toolCallId);
|
|
325
|
+
}
|
|
326
|
+
component = readGroup;
|
|
327
|
+
this.ctx.pendingTools.set(message.toolCallId, readGroup);
|
|
328
|
+
}
|
|
329
|
+
component.updateResult(message, false, message.toolCallId);
|
|
330
|
+
this.ctx.pendingTools.delete(message.toolCallId);
|
|
331
|
+
readToolCallArgs.delete(message.toolCallId);
|
|
332
|
+
readToolCallAssistantComponents.delete(message.toolCallId);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
// Match tool results to pending tool components
|
|
291
337
|
const component = this.ctx.pendingTools.get(message.toolCallId);
|
|
292
338
|
if (component) {
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -24,6 +24,11 @@ function tryCurlyQuoteVariant(filePath: string): string {
|
|
|
24
24
|
return filePath.replace(/'/g, "\u2019");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function tryShellEscapedPath(filePath: string): string {
|
|
28
|
+
if (!filePath.includes("\\") || !filePath.includes("/")) return filePath;
|
|
29
|
+
return filePath.replace(/\\([ \t"'(){}[\]])/g, "$1");
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
function fileExists(filePath: string): boolean {
|
|
28
33
|
try {
|
|
29
34
|
fs.accessSync(filePath, fs.constants.F_OK);
|
|
@@ -124,33 +129,39 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
|
124
129
|
|
|
125
130
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
126
131
|
const resolved = resolveToCwd(filePath, cwd);
|
|
132
|
+
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
|
133
|
+
const baseCandidates = shellEscapedVariant !== resolved ? [resolved, shellEscapedVariant] : [resolved];
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Try macOS AM/PM variant (narrow no-break space before AM/PM)
|
|
133
|
-
const amPmVariant = tryMacOSScreenshotPath(resolved);
|
|
134
|
-
if (amPmVariant !== resolved && fileExists(amPmVariant)) {
|
|
135
|
-
return amPmVariant;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Try NFD variant (macOS stores filenames in NFD form)
|
|
139
|
-
const nfdVariant = tryNFDVariant(resolved);
|
|
140
|
-
if (nfdVariant !== resolved && fileExists(nfdVariant)) {
|
|
141
|
-
return nfdVariant;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Try curly quote variant (macOS uses U+2019 in screenshot names)
|
|
145
|
-
const curlyVariant = tryCurlyQuoteVariant(resolved);
|
|
146
|
-
if (curlyVariant !== resolved && fileExists(curlyVariant)) {
|
|
147
|
-
return curlyVariant;
|
|
135
|
+
for (const baseCandidate of baseCandidates) {
|
|
136
|
+
if (fileExists(baseCandidate)) {
|
|
137
|
+
return baseCandidate;
|
|
138
|
+
}
|
|
148
139
|
}
|
|
149
140
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
for (const baseCandidate of baseCandidates) {
|
|
142
|
+
// Try macOS AM/PM variant (narrow no-break space before AM/PM)
|
|
143
|
+
const amPmVariant = tryMacOSScreenshotPath(baseCandidate);
|
|
144
|
+
if (amPmVariant !== baseCandidate && fileExists(amPmVariant)) {
|
|
145
|
+
return amPmVariant;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Try NFD variant (macOS stores filenames in NFD form)
|
|
149
|
+
const nfdVariant = tryNFDVariant(baseCandidate);
|
|
150
|
+
if (nfdVariant !== baseCandidate && fileExists(nfdVariant)) {
|
|
151
|
+
return nfdVariant;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try curly quote variant (macOS uses U+2019 in screenshot names)
|
|
155
|
+
const curlyVariant = tryCurlyQuoteVariant(baseCandidate);
|
|
156
|
+
if (curlyVariant !== baseCandidate && fileExists(curlyVariant)) {
|
|
157
|
+
return curlyVariant;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
|
|
161
|
+
const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
|
|
162
|
+
if (nfdCurlyVariant !== baseCandidate && fileExists(nfdCurlyVariant)) {
|
|
163
|
+
return nfdCurlyVariant;
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
return resolved;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Subagents must call this tool to finish and return structured JSON output.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
|
-
import {
|
|
7
|
+
import { sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/typebox-helpers";
|
|
8
8
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
@@ -179,8 +179,7 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
parameters = createParameters(dataSchema);
|
|
182
|
-
|
|
183
|
-
JSON.stringify(strictParameters);
|
|
182
|
+
JSON.stringify(parameters);
|
|
184
183
|
// Verify the final parameters compile with AJV (catches unresolved $ref, etc.)
|
|
185
184
|
ajv.compile(parameters as Record<string, unknown>);
|
|
186
185
|
} catch (err) {
|
|
@@ -46,8 +46,8 @@ async function callJinaSearch(apiKey: string, query: string): Promise<JinaSearch
|
|
|
46
46
|
throw new SearchProviderError("jina", `Jina API error (${response.status}): ${errorText}`, response.status);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
50
|
-
return Array.isArray(data) ?
|
|
49
|
+
const payload = (await response.json()) as { data?: JinaSearchResponse } | null;
|
|
50
|
+
return Array.isArray(payload?.data) ? payload.data : [];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Execute Jina web search. */
|