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

View file

@ -44,17 +44,21 @@ export interface LoadHooksResult {
errors: Array<{ path: string; error: string }>;
}
/**
* Expand path with ~ support.
*/
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
function normalizeUnicodeSpaces(str: string): string {
return str.replace(UNICODE_SPACES, " ");
}
function expandPath(p: string): string {
if (p.startsWith("~/")) {
return path.join(os.homedir(), p.slice(2));
const normalized = normalizeUnicodeSpaces(p);
if (normalized.startsWith("~/")) {
return path.join(os.homedir(), normalized.slice(2));
}
if (p.startsWith("~")) {
return path.join(os.homedir(), p.slice(1));
if (normalized.startsWith("~")) {
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 } from "@sinclair/typebox";
import * as Diff from "diff";
import { constants } from "fs";
import { access, readFile, writeFile } from "fs/promises";
import { resolve as resolvePath } from "path";
/**
* 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;
}
import { expandPath } from "./path-utils.js";
/**
* 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 { existsSync } from "fs";
import { globSync } from "glob";
import { homedir } from "os";
import path from "path";
import { ensureTool } from "../../utils/tools-manager.js";
import { expandPath } from "./path-utils.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({
pattern: Type.String({
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 { spawn } from "child_process";
import { readFileSync, type Stats, statSync } from "fs";
import { homedir } from "os";
import path from "path";
import { ensureTool } from "../../utils/tools-manager.js";
import { expandPath } from "./path-utils.js";
import {
DEFAULT_MAX_BYTES,
formatSize,
@ -15,19 +15,6 @@ import {
truncateLine,
} 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({
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
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 } from "@sinclair/typebox";
import { existsSync, readdirSync, statSync } from "fs";
import { homedir } from "os";
import nodePath from "path";
import { expandPath } from "./path-utils.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({
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)" })),

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 } from "@sinclair/typebox";
import { constants } from "fs";
import { access, readFile } from "fs/promises";
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";
/**
* 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
*/
@ -58,7 +45,7 @@ export const readTool: AgentTool<typeof readSchema> = {
{ path, offset, limit }: { path: string; offset?: number; limit?: number },
signal?: AbortSignal,
) => {
const absolutePath = resolvePath(expandPath(path));
const absolutePath = resolvePath(resolveReadPath(path));
const mimeType = isImageFile(absolutePath);
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 } from "@sinclair/typebox";
import { mkdir, writeFile } from "fs/promises";
import { dirname, resolve as resolvePath } from "path";
/**
* 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;
}
import { expandPath } from "./path-utils.js";
const writeSchema = Type.Object({
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),