@oh-my-pi/pi-coding-agent 13.3.9 → 13.3.12

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 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.9",
4
+ "version": "13.3.12",
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.9",
45
- "@oh-my-pi/pi-agent-core": "13.3.9",
46
- "@oh-my-pi/pi-ai": "13.3.9",
47
- "@oh-my-pi/pi-natives": "13.3.9",
48
- "@oh-my-pi/pi-tui": "13.3.9",
49
- "@oh-my-pi/pi-utils": "13.3.9",
44
+ "@oh-my-pi/omp-stats": "13.3.12",
45
+ "@oh-my-pi/pi-agent-core": "13.3.12",
46
+ "@oh-my-pi/pi-ai": "13.3.12",
47
+ "@oh-my-pi/pi-natives": "13.3.12",
48
+ "@oh-my-pi/pi-tui": "13.3.12",
49
+ "@oh-my-pi/pi-utils": "13.3.12",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -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
- if (!this.ctx.pendingTools.has(content.id)) {
137
- if (content.name === "read") {
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
- const group = this.#getReadGroup();
223
- group.updateArgs(event.args, event.toolCallId);
224
- this.ctx.pendingTools.set(event.toolCallId, group);
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
- const component = this.ctx.pendingTools.get(event.toolCallId);
273
- if (component) {
274
- const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
275
- const isBackgroundRunning = asyncState === "running";
276
- component.updateResult(
277
- { ...event.result, isError: event.isError },
278
- isBackgroundRunning,
279
- event.toolCallId,
280
- );
281
- if (isBackgroundRunning) {
282
- this.#backgroundToolCallIds.add(event.toolCallId);
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.delete(event.toolCallId);
285
- this.#backgroundToolCallIds.delete(event.toolCallId);
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,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import {
4
+ detectMacOSAppearance,
4
5
  type HighlightColors as NativeHighlightColors,
5
6
  highlightCode as nativeHighlightCode,
6
7
  supportsLanguage as nativeSupportsLanguage,
8
+ startMacAppearanceObserver as startNativeMacObserver,
7
9
  } from "@oh-my-pi/pi-natives";
8
10
  import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
9
11
  import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
@@ -1619,11 +1621,21 @@ export async function getThemeByName(name: string): Promise<Theme | undefined> {
1619
1621
  /** Appearance reported by Mode 2031 (terminal DSR), or undefined if not (yet) available. */
1620
1622
  var terminalReportedAppearance: "dark" | "light" | undefined;
1621
1623
 
1624
+ /** Appearance reported by native macOS observer, or undefined if not (yet) available. */
1625
+ var macOSReportedAppearance: "dark" | "light" | undefined;
1626
+
1622
1627
  function detectTerminalBackground(): "dark" | "light" {
1623
1628
  // Prefer terminal-reported appearance from Mode 2031 (CSI ? 997 ; {1,2} n)
1624
1629
  if (terminalReportedAppearance) {
1625
1630
  return terminalReportedAppearance;
1626
1631
  }
1632
+ // macOS: query system appearance via CoreFoundation (native, no shell).
1633
+ // Uses cached observer value, or falls back to CFPreferencesCopyAppValue.
1634
+ // Works on all terminals including Warp which lacks Mode 2031 / OSC 11.
1635
+ const macAppearance = macOSReportedAppearance ?? detectMacOSAppearance();
1636
+ if (macAppearance) {
1637
+ return macAppearance;
1638
+ }
1627
1639
  // Fallback: COLORFGBG environment variable (static, set once at terminal launch)
1628
1640
  const colorfgbg = Bun.env.COLORFGBG || "";
1629
1641
  if (colorfgbg) {
@@ -1763,19 +1775,7 @@ export async function previewTheme(name: string): Promise<{ success: boolean; er
1763
1775
  */
1764
1776
  export function enableAutoTheme(): void {
1765
1777
  autoDetectedTheme = true;
1766
- const resolved = getDefaultTheme();
1767
- if (resolved === currentThemeName) return;
1768
- currentThemeName = resolved;
1769
- loadTheme(resolved, getCurrentThemeOptions())
1770
- .then(loadedTheme => {
1771
- theme = loadedTheme;
1772
- if (onThemeChangeCallback) {
1773
- onThemeChangeCallback();
1774
- }
1775
- })
1776
- .catch(err => {
1777
- logger.debug("Auto theme switch failed", { error: String(err) });
1778
- });
1778
+ reevaluateAutoTheme("enableAutoTheme");
1779
1779
  }
1780
1780
 
1781
1781
  /**
@@ -1785,20 +1785,7 @@ export function enableAutoTheme(): void {
1785
1785
  export function setAutoThemeMapping(mode: "dark" | "light", themeName: string): void {
1786
1786
  if (mode === "dark") autoDarkTheme = themeName;
1787
1787
  else autoLightTheme = themeName;
1788
- if (!autoDetectedTheme) return;
1789
- const resolved = getDefaultTheme();
1790
- if (resolved === currentThemeName) return;
1791
- currentThemeName = resolved;
1792
- loadTheme(resolved, getCurrentThemeOptions())
1793
- .then(loadedTheme => {
1794
- theme = loadedTheme;
1795
- if (onThemeChangeCallback) {
1796
- onThemeChangeCallback();
1797
- }
1798
- })
1799
- .catch(err => {
1800
- logger.debug("Auto theme mapping switch failed", { error: String(err) });
1801
- });
1788
+ reevaluateAutoTheme("setAutoThemeMapping");
1802
1789
  }
1803
1790
 
1804
1791
  /**
@@ -1810,20 +1797,7 @@ export function setAutoThemeMapping(mode: "dark" | "light", themeName: string):
1810
1797
  export function onTerminalAppearanceChange(mode: "dark" | "light"): void {
1811
1798
  if (terminalReportedAppearance === mode) return;
1812
1799
  terminalReportedAppearance = mode;
1813
- if (!autoDetectedTheme) return;
1814
- const resolved = getDefaultTheme();
1815
- if (resolved === currentThemeName) return;
1816
- currentThemeName = resolved;
1817
- loadTheme(resolved, getCurrentThemeOptions())
1818
- .then(loadedTheme => {
1819
- theme = loadedTheme;
1820
- if (onThemeChangeCallback) {
1821
- onThemeChangeCallback();
1822
- }
1823
- })
1824
- .catch(err => {
1825
- logger.debug("Mode 2031 appearance switch failed", { error: String(err) });
1826
- });
1800
+ reevaluateAutoTheme("Mode 2031");
1827
1801
  }
1828
1802
 
1829
1803
  export function setThemeInstance(themeInstance: Theme): void {
@@ -1969,26 +1943,68 @@ async function startThemeWatcher(): Promise<void> {
1969
1943
  }
1970
1944
  }
1971
1945
 
1972
- /** Re-check COLORFGBG on SIGWINCH and switch dark/light when using auto-detected theme. */
1946
+ /**
1947
+ * Shared logic for re-evaluating the auto-detected theme.
1948
+ * Called from SIGWINCH, macOS observer, and Mode 2031 handler.
1949
+ */
1950
+ function reevaluateAutoTheme(debugLabel: string): void {
1951
+ if (!autoDetectedTheme) return;
1952
+ const resolved = getDefaultTheme();
1953
+ if (resolved === currentThemeName) return;
1954
+ currentThemeName = resolved;
1955
+ loadTheme(resolved, getCurrentThemeOptions())
1956
+ .then(loadedTheme => {
1957
+ theme = loadedTheme;
1958
+ if (onThemeChangeCallback) {
1959
+ onThemeChangeCallback();
1960
+ }
1961
+ })
1962
+ .catch(err => {
1963
+ logger.debug(`Theme switch on ${debugLabel} failed`, { error: String(err) });
1964
+ });
1965
+ }
1966
+
1967
+ // ============================================================================
1968
+ // macOS Appearance Observer
1969
+ // ============================================================================
1970
+
1971
+ var macObserver: { stop(): void } | undefined;
1972
+
1973
+ /** Start the native macOS appearance observer (CFDistributedNotificationCenter). */
1974
+ function startMacAppearanceObserver(): void {
1975
+ stopMacAppearanceObserver();
1976
+ if (process.platform !== "darwin") return;
1977
+ try {
1978
+ macObserver = startNativeMacObserver(appearance => {
1979
+ macOSReportedAppearance = appearance;
1980
+ if (!terminalReportedAppearance) reevaluateAutoTheme("macOS observer");
1981
+ });
1982
+ } catch (err) {
1983
+ logger.warn("Failed to start macOS appearance observer", { err });
1984
+ }
1985
+ }
1986
+
1987
+ function stopMacAppearanceObserver(): void {
1988
+ if (macObserver) {
1989
+ macObserver.stop();
1990
+ macObserver = undefined;
1991
+ }
1992
+ macOSReportedAppearance = undefined;
1993
+ }
1994
+
1995
+ // ============================================================================
1996
+ // SIGWINCH Listener
1997
+ // ============================================================================
1998
+
1999
+ /** Re-check appearance on SIGWINCH and switch dark/light when using auto-detected theme. */
1973
2000
  function startSigwinchListener(): void {
1974
2001
  stopSigwinchListener();
1975
2002
  sigwinchHandler = () => {
1976
- if (!autoDetectedTheme) return;
1977
- const resolved = getDefaultTheme();
1978
- if (resolved === currentThemeName) return;
1979
- currentThemeName = resolved;
1980
- loadTheme(resolved, getCurrentThemeOptions())
1981
- .then(loadedTheme => {
1982
- theme = loadedTheme;
1983
- if (onThemeChangeCallback) {
1984
- onThemeChangeCallback();
1985
- }
1986
- })
1987
- .catch(err => {
1988
- logger.debug("Theme switch on SIGWINCH failed", { error: String(err) });
1989
- });
2003
+ reevaluateAutoTheme("SIGWINCH");
1990
2004
  };
1991
2005
  process.on("SIGWINCH", sigwinchHandler);
2006
+ // Start macOS appearance observer alongside SIGWINCH listener.
2007
+ startMacAppearanceObserver();
1992
2008
  }
1993
2009
 
1994
2010
  function stopSigwinchListener(): void {
@@ -1996,6 +2012,7 @@ function stopSigwinchListener(): void {
1996
2012
  process.removeListener("SIGWINCH", sigwinchHandler);
1997
2013
  sigwinchHandler = undefined;
1998
2014
  }
2015
+ stopMacAppearanceObserver();
1999
2016
  }
2000
2017
 
2001
2018
  export function stopThemeWatcher(): void {
@@ -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
- this.ctx.pendingTools.set(content.id, readGroup);
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) {
@@ -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
- if (fileExists(resolved)) {
129
- return resolved;
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
- // Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
151
- const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
152
- if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
153
- return nfdCurlyVariant;
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 { enforceStrictSchema, sanitizeSchemaForStrictMode } from "@oh-my-pi/pi-ai/utils/typebox-helpers";
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
- const strictParameters = enforceStrictSchema(parameters as unknown as Record<string, unknown>);
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 data = (await response.json()) as unknown;
50
- return Array.isArray(data) ? (data as JinaSearchResponse) : [];
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. */