coding-agent: fix macOS screenshot filenames with unicode spaces (#181)

This commit is contained in:
Nico Bailon 2025-12-13 13:04:01 -08:00 committed by GitHub
parent 5c0a84b2d8
commit 9a7bbb2839
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 70 additions and 108 deletions

View file

@ -5,8 +5,8 @@
import type { Attachment } from "@mariozechner/pi-agent-core"; import type { Attachment } from "@mariozechner/pi-agent-core";
import chalk from "chalk"; import chalk from "chalk";
import { existsSync, readFileSync, statSync } from "fs"; import { existsSync, readFileSync, statSync } from "fs";
import { homedir } from "os";
import { extname, resolve } from "path"; import { extname, resolve } from "path";
import { resolveReadPath } from "../core/tools/path-utils.js";
/** Map of file extensions to MIME types for common image formats */ /** Map of file extensions to MIME types for common image formats */
const IMAGE_MIME_TYPES: Record<string, string> = { const IMAGE_MIME_TYPES: Record<string, string> = {
@ -23,17 +23,6 @@ function isImageFile(filePath: string): string | null {
return IMAGE_MIME_TYPES[ext] || null; return IMAGE_MIME_TYPES[ext] || null;
} }
/** Expand ~ to home directory */
function expandPath(filePath: string): string {
if (filePath === "~") {
return homedir();
}
if (filePath.startsWith("~/")) {
return homedir() + filePath.slice(1);
}
return filePath;
}
export interface ProcessedFiles { export interface ProcessedFiles {
textContent: string; textContent: string;
imageAttachments: Attachment[]; imageAttachments: Attachment[];
@ -45,9 +34,8 @@ export function processFileArguments(fileArgs: string[]): ProcessedFiles {
const imageAttachments: Attachment[] = []; const imageAttachments: Attachment[] = [];
for (const fileArg of fileArgs) { for (const fileArg of fileArgs) {
// Expand and resolve path // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
const expandedPath = expandPath(fileArg); const absolutePath = resolve(resolveReadPath(fileArg));
const absolutePath = resolve(expandedPath);
// Check if file exists // Check if file exists
if (!existsSync(absolutePath)) { if (!existsSync(absolutePath)) {

View file

@ -44,17 +44,21 @@ export interface LoadHooksResult {
errors: Array<{ path: string; error: string }>; errors: Array<{ path: string; error: string }>;
} }
/** const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
* Expand path with ~ support.
*/ function normalizeUnicodeSpaces(str: string): string {
return str.replace(UNICODE_SPACES, " ");
}
function expandPath(p: string): string { function expandPath(p: string): string {
if (p.startsWith("~/")) { const normalized = normalizeUnicodeSpaces(p);
return path.join(os.homedir(), p.slice(2)); if (normalized.startsWith("~/")) {
return path.join(os.homedir(), normalized.slice(2));
} }
if (p.startsWith("~")) { if (normalized.startsWith("~")) {
return path.join(os.homedir(), p.slice(1)); return path.join(os.homedir(), normalized.slice(1));
} }
return p; return normalized;
} }
/** /**

View file

@ -1,23 +1,10 @@
import * as os from "node:os";
import type { AgentTool } from "@mariozechner/pi-ai"; import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import * as Diff from "diff"; import * as Diff from "diff";
import { constants } from "fs"; import { constants } from "fs";
import { access, readFile, writeFile } from "fs/promises"; import { access, readFile, writeFile } from "fs/promises";
import { resolve as resolvePath } from "path"; import { resolve as resolvePath } from "path";
import { expandPath } from "./path-utils.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return os.homedir();
}
if (filePath.startsWith("~/")) {
return os.homedir() + filePath.slice(1);
}
return filePath;
}
/** /**
* Generate a unified diff string with line numbers and context * Generate a unified diff string with line numbers and context

View file

@ -3,24 +3,11 @@ import { Type } from "@sinclair/typebox";
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { globSync } from "glob"; import { globSync } from "glob";
import { homedir } from "os";
import path from "path"; import path from "path";
import { ensureTool } from "../../utils/tools-manager.js"; import { ensureTool } from "../../utils/tools-manager.js";
import { expandPath } from "./path-utils.js";
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return homedir();
}
if (filePath.startsWith("~/")) {
return homedir() + filePath.slice(1);
}
return filePath;
}
const findSchema = Type.Object({ const findSchema = Type.Object({
pattern: Type.String({ pattern: Type.String({
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'", description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",

View file

@ -3,9 +3,9 @@ import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { spawn } from "child_process"; import { spawn } from "child_process";
import { readFileSync, type Stats, statSync } from "fs"; import { readFileSync, type Stats, statSync } from "fs";
import { homedir } from "os";
import path from "path"; import path from "path";
import { ensureTool } from "../../utils/tools-manager.js"; import { ensureTool } from "../../utils/tools-manager.js";
import { expandPath } from "./path-utils.js";
import { import {
DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES,
formatSize, formatSize,
@ -15,19 +15,6 @@ import {
truncateLine, truncateLine,
} from "./truncate.js"; } from "./truncate.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return homedir();
}
if (filePath.startsWith("~/")) {
return homedir() + filePath.slice(1);
}
return filePath;
}
const grepSchema = Type.Object({ const grepSchema = Type.Object({
pattern: Type.String({ description: "Search pattern (regex or literal string)" }), pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })), path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),

View file

@ -1,23 +1,10 @@
import type { AgentTool } from "@mariozechner/pi-ai"; import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { existsSync, readdirSync, statSync } from "fs"; import { existsSync, readdirSync, statSync } from "fs";
import { homedir } from "os";
import nodePath from "path"; import nodePath from "path";
import { expandPath } from "./path-utils.js";
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return homedir();
}
if (filePath.startsWith("~/")) {
return homedir() + filePath.slice(1);
}
return filePath;
}
const lsSchema = Type.Object({ const lsSchema = Type.Object({
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })), path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })), limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),

View file

@ -0,0 +1,48 @@
import { accessSync, constants } from "node:fs";
import * as os from "node:os";
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
const NARROW_NO_BREAK_SPACE = "\u202F";
function normalizeUnicodeSpaces(str: string): string {
return str.replace(UNICODE_SPACES, " ");
}
function tryMacOSScreenshotPath(filePath: string): string {
return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
}
function fileExists(filePath: string): boolean {
try {
accessSync(filePath, constants.F_OK);
return true;
} catch {
return false;
}
}
export function expandPath(filePath: string): string {
const normalized = normalizeUnicodeSpaces(filePath);
if (normalized === "~") {
return os.homedir();
}
if (normalized.startsWith("~/")) {
return os.homedir() + normalized.slice(1);
}
return normalized;
}
export function resolveReadPath(filePath: string): string {
const expanded = expandPath(filePath);
if (fileExists(expanded)) {
return expanded;
}
const macOSVariant = tryMacOSScreenshotPath(expanded);
if (macOSVariant !== expanded && fileExists(macOSVariant)) {
return macOSVariant;
}
return expanded;
}

View file

@ -1,24 +1,11 @@
import * as os from "node:os";
import type { AgentTool, ImageContent, TextContent } from "@mariozechner/pi-ai"; import type { AgentTool, ImageContent, TextContent } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { constants } from "fs"; import { constants } from "fs";
import { access, readFile } from "fs/promises"; import { access, readFile } from "fs/promises";
import { extname, resolve as resolvePath } from "path"; import { extname, resolve as resolvePath } from "path";
import { resolveReadPath } from "./path-utils.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return os.homedir();
}
if (filePath.startsWith("~/")) {
return os.homedir() + filePath.slice(1);
}
return filePath;
}
/** /**
* Map of file extensions to MIME types for common image formats * Map of file extensions to MIME types for common image formats
*/ */
@ -58,7 +45,7 @@ export const readTool: AgentTool<typeof readSchema> = {
{ path, offset, limit }: { path: string; offset?: number; limit?: number }, { path, offset, limit }: { path: string; offset?: number; limit?: number },
signal?: AbortSignal, signal?: AbortSignal,
) => { ) => {
const absolutePath = resolvePath(expandPath(path)); const absolutePath = resolvePath(resolveReadPath(path));
const mimeType = isImageFile(absolutePath); const mimeType = isImageFile(absolutePath);
return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>( return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(

View file

@ -1,21 +1,8 @@
import * as os from "node:os";
import type { AgentTool } from "@mariozechner/pi-ai"; import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { mkdir, writeFile } from "fs/promises"; import { mkdir, writeFile } from "fs/promises";
import { dirname, resolve as resolvePath } from "path"; import { dirname, resolve as resolvePath } from "path";
import { expandPath } from "./path-utils.js";
/**
* Expand ~ to home directory
*/
function expandPath(filePath: string): string {
if (filePath === "~") {
return os.homedir();
}
if (filePath.startsWith("~/")) {
return os.homedir() + filePath.slice(1);
}
return filePath;
}
const writeSchema = Type.Object({ const writeSchema = Type.Object({
path: Type.String({ description: "Path to the file to write (relative or absolute)" }), path: Type.String({ description: "Path to the file to write (relative or absolute)" }),