Skip to content
Merged
16 changes: 8 additions & 8 deletions docs/hooks/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,9 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
| Env var | JSON path | Type | Description |
| ------------------------------ | --------------- | ------ | --------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_CONTENT` | `content` | string | The content to insert |
| `MUX_TOOL_INPUT_FILE_PATH` | `file_path` | string | Path to the file to edit (absolute or relative to the current workspace) |
| `MUX_TOOL_INPUT_INSERT_AFTER` | `insert_after` | string | Anchor text to insert after. Content will be placed immediately after this substring. |
| `MUX_TOOL_INPUT_INSERT_BEFORE` | `insert_before` | string | Anchor text to insert before. Content will be placed immediately before this substring. |
| `MUX_TOOL_INPUT_PATH` | `path` | string | Path to the file to edit (absolute or relative to the current workspace) |

</details>

Expand All @@ -326,9 +326,9 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
| `MUX_TOOL_INPUT_END_LINE` | `end_line` | number | 1-indexed end line (inclusive) to replace |
| `MUX_TOOL_INPUT_EXPECTED_LINES_<INDEX>` | `expected_lines[<INDEX>]` | string | Optional safety check. When provided, the current lines in the specified range must match exactly. |
| `MUX_TOOL_INPUT_EXPECTED_LINES_COUNT` | `expected_lines.length` | number | Number of elements in expected_lines (Optional safety check. When provided, the current lines in the specified range must match exactly.) |
| `MUX_TOOL_INPUT_FILE_PATH` | `file_path` | string | Path to the file to edit (absolute or relative to the current workspace) |
| `MUX_TOOL_INPUT_NEW_LINES_<INDEX>` | `new_lines[<INDEX>]` | string | Replacement lines. Provide an empty array to delete the specified range. |
| `MUX_TOOL_INPUT_NEW_LINES_COUNT` | `new_lines.length` | number | Number of elements in new_lines (Replacement lines. Provide an empty array to delete the specified range.) |
| `MUX_TOOL_INPUT_PATH` | `path` | string | Path to the file to edit (absolute or relative to the current workspace) |
| `MUX_TOOL_INPUT_START_LINE` | `start_line` | number | 1-indexed start line (inclusive) to replace |

</details>
Expand All @@ -338,21 +338,21 @@ If a value is too large for the environment, it may be omitted (not set). Mux al

| Env var | JSON path | Type | Description |
| ------------------------------ | --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_FILE_PATH` | `file_path` | string | Path to the file to edit (absolute or relative to the current workspace) |
| `MUX_TOOL_INPUT_NEW_STRING` | `new_string` | string | The replacement text |
| `MUX_TOOL_INPUT_OLD_STRING` | `old_string` | string | The exact text to replace (must be unique in file if replace_count is 1). Include enough context (indentation, surrounding lines) to make it unique. |
| `MUX_TOOL_INPUT_PATH` | `path` | string | Path to the file to edit (absolute or relative to the current workspace) |
| `MUX_TOOL_INPUT_REPLACE_COUNT` | `replace_count` | number | Number of occurrences to replace (default: 1). Use -1 to replace all occurrences. If 1, old_string must be unique in the file. |

</details>

<details>
<summary>file_read (3)</summary>

| Env var | JSON path | Type | Description |
| -------------------------- | ----------- | ------ | ------------------------------------------------------------------------------ |
| `MUX_TOOL_INPUT_FILE_PATH` | `file_path` | string | The path to the file to read (absolute or relative) |
| `MUX_TOOL_INPUT_LIMIT` | `limit` | number | Number of lines to return from offset (optional, returns all if not specified) |
| `MUX_TOOL_INPUT_OFFSET` | `offset` | number | 1-based starting line number (optional, defaults to 1) |
| Env var | JSON path | Type | Description |
| ----------------------- | --------- | ------ | ------------------------------------------------------------------------------ |
| `MUX_TOOL_INPUT_LIMIT` | `limit` | number | Number of lines to return from offset (optional, returns all if not specified) |
| `MUX_TOOL_INPUT_OFFSET` | `offset` | number | 1-based starting line number (optional, defaults to 1) |
| `MUX_TOOL_INPUT_PATH` | `path` | string | The path to the file to read (absolute or relative) |

</details>

Expand Down
3 changes: 2 additions & 1 deletion src/browser/components/tools/FileEditToolCall.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { FileIcon } from "@/browser/components/FileIcon";
import { parsePatch } from "diff";
import { extractToolFilePath } from "@/common/utils/tools/toolInputFilePath";
import type {
FileEditInsertToolArgs,
FileEditInsertToolResult,
Expand Down Expand Up @@ -109,7 +110,7 @@ export const FileEditToolCall: React.FC<FileEditToolCallProps> = ({

const uiOnlyDiff = getToolOutputUiOnly(result)?.file_edit?.diff;
const diff = result && result.success ? (uiOnlyDiff ?? result.diff) : undefined;
const filePath = "file_path" in args ? args.file_path : undefined;
const filePath = extractToolFilePath(args);

// Copy to clipboard with feedback
const { copied, copyToClipboard } = useCopyToClipboard();
Expand Down
5 changes: 2 additions & 3 deletions src/browser/components/tools/FileReadToolCall.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { FileIcon } from "@/browser/components/FileIcon";
import { extractToolFilePath } from "@/common/utils/tools/toolInputFilePath";
import type { FileReadToolArgs, FileReadToolResult } from "@/common/types/tools";
import {
ToolContainer,
Expand Down Expand Up @@ -70,9 +71,7 @@ export const FileReadToolCall: React.FC<FileReadToolCallProps> = ({
}) => {
const { expanded, toggleExpanded } = useToolExpansion();

// Support both new (file_path) and legacy (filePath) property names for backwards compatibility
const filePath =
"file_path" in args ? args.file_path : ((args as { filePath?: string }).filePath ?? "");
const filePath = extractToolFilePath(args) ?? "";

// Parse the file content to extract line numbers and actual content
const parsedContent = result?.success && result.content ? parseFileContent(result.content) : null;
Expand Down
2 changes: 1 addition & 1 deletion src/browser/stories/App.chat.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ export const Streaming: AppStory = {
pendingTool: {
toolCallId: "call-1",
toolName: "file_read",
args: { file_path: "src/db/connection.ts" },
args: { path: "src/db/connection.ts" },
},
gitStatus: { dirty: 1 },
})
Expand Down
24 changes: 12 additions & 12 deletions src/browser/stories/App.codeExecution.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export default {
};

const SAMPLE_CODE = `// Read a file and make an edit
const content = await mux.file_read({ file_path: "src/config.ts" });
const content = await mux.file_read({ path: "src/config.ts" });
console.log("Read file with", content.lines_read, "lines");

await mux.file_edit_replace_string({
file_path: "src/config.ts",
path: "src/config.ts",
old_string: "debug: false",
new_string: "debug: true"
});
Expand Down Expand Up @@ -63,14 +63,14 @@ export const Completed: AppStory = {
toolCalls: [
{
toolName: "file_read",
args: { file_path: "src/config.ts" },
args: { path: "src/config.ts" },
result: { success: true, lines_read: 42 },
duration_ms: 15,
},
{
toolName: "file_edit_replace_string",
args: {
file_path: "src/config.ts",
path: "src/config.ts",
old_string: "debug: false",
new_string: "debug: true",
},
Expand Down Expand Up @@ -112,7 +112,7 @@ export const Completed: AppStory = {
{
toolCallId: "nested-1",
toolName: "file_read",
input: { file_path: "src/config.ts" },
input: { path: "src/config.ts" },
output: {
success: true,
lines_read: 4,
Expand All @@ -125,7 +125,7 @@ export const Completed: AppStory = {
toolCallId: "nested-2",
toolName: "file_edit_replace_string",
input: {
file_path: "src/config.ts",
path: "src/config.ts",
old_string: "debug: false",
new_string: "debug: true",
},
Expand Down Expand Up @@ -192,7 +192,7 @@ export const Executing: AppStory = {
{
toolCallId: "nested-1",
toolName: "file_read",
input: { file_path: "src/config.ts" },
input: { path: "src/config.ts" },
output: {
success: true,
file_size: 1024,
Expand All @@ -205,7 +205,7 @@ export const Executing: AppStory = {
toolCallId: "nested-2",
toolName: "file_edit_replace_string",
input: {
file_path: "src/config.ts",
path: "src/config.ts",
old_string: "debug: false",
new_string: "debug: true",
},
Expand Down Expand Up @@ -367,7 +367,7 @@ export const NestedToolError: AppStory = {
toolCalls: [
createCodeExecutionTool(
"call-1",
`const result = mux.file_read({ file_path: "nonexistent.ts" });
`const result = mux.file_read({ path: "nonexistent.ts" });
return result;`,
{
success: false,
Expand All @@ -377,7 +377,7 @@ return result;`,
toolCalls: [
{
toolName: "file_read",
args: { file_path: "nonexistent.ts" },
args: { path: "nonexistent.ts" },
// Error-only output shape (no success field) - tool threw
result: { error: "ENOENT: no such file or directory" },
duration_ms: 5,
Expand Down Expand Up @@ -414,7 +414,7 @@ export const Interrupted: AppStory = {
{
toolCallId: "nested-1",
toolName: "file_read",
input: { file_path: "src/config.ts" },
input: { path: "src/config.ts" },
output: {
success: true,
file_size: 1024,
Expand Down Expand Up @@ -462,7 +462,7 @@ export const ShowCodeView: AppStory = {
createCodeExecutionTool(
"call-1",
`// Analysis script with various syntax elements
const data = mux.file_read({ file_path: "data.json" });
const data = mux.file_read({ path: "data.json" });
const parsed = JSON.parse(data.content);

function analyze(items) {
Expand Down
2 changes: 1 addition & 1 deletion src/browser/stories/App.demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export const Comprehensive: AppStory = {
pendingTool: {
toolCallId: "call-s1",
toolName: "file_read",
args: { file_path: "src/db/connection.ts" },
args: { path: "src/db/connection.ts" },
},
}),
],
Expand Down
4 changes: 2 additions & 2 deletions src/browser/stories/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export function createFileReadTool(toolCallId: string, filePath: string, content
toolCallId,
toolName: "file_read",
state: "output-available",
input: { file_path: filePath },
input: { path: filePath },
output: { success: true, content },
};
}
Expand All @@ -289,7 +289,7 @@ export function createFileEditTool(toolCallId: string, filePath: string, diff: s
toolCallId,
toolName: "file_edit_replace_string",
state: "output-available",
input: { file_path: filePath, old_string: "...", new_string: "..." },
input: { path: filePath, old_string: "...", new_string: "..." },
output: { success: true, diff, edits_applied: 1 },
};
}
Expand Down
18 changes: 11 additions & 7 deletions src/cli/toolFormatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
* with emoji prefixes and structured output similar to the frontend UI.
*/

import { extractToolFilePath } from "@/common/utils/tools/toolInputFilePath";
import chalk from "chalk";
import type { ToolCallStartEvent, ToolCallEndEvent } from "@/common/types/stream";
import type {
BashToolArgs,
BashToolResult,
FileReadToolArgs,
FileReadToolResult,
FileEditReplaceStringToolArgs,
FileEditReplaceStringToolResult,
FileEditInsertToolArgs,
FileEditInsertToolResult,
TaskToolArgs,
TaskToolResult,
Expand Down Expand Up @@ -47,6 +46,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
}

function extractFilePathArg(args: unknown): string | undefined {
return extractToolFilePath(args);
}

function formatFilePath(filePath: string): string {
return chalk.cyan(filePath);
}
Expand Down Expand Up @@ -109,15 +112,16 @@ function renderUnknown(value: unknown): string {
// ============================================================================

function formatFileEditStart(_toolName: string, args: unknown): string | null {
const editArgs = args as FileEditReplaceStringToolArgs | FileEditInsertToolArgs;
if (!editArgs?.file_path) return null;
const filePath = extractFilePathArg(args);
if (!filePath) return null;

return `✏️ ${formatFilePath(editArgs.file_path)}`;
return `✏️ ${formatFilePath(filePath)}`;
}

function formatFileReadStart(_toolName: string, args: unknown): string | null {
const readArgs = args as FileReadToolArgs;
if (!readArgs?.file_path) return null;
const filePath = extractFilePathArg(args);
if (!filePath) return null;

let suffix = "";
if (readArgs.offset != null || readArgs.limit != null) {
Expand All @@ -127,7 +131,7 @@ function formatFileReadStart(_toolName: string, args: unknown): string | null {
suffix = chalk.dim(` (${parts.join(", ")})`);
}

return `📖 ${formatFilePath(readArgs.file_path)}${suffix}`;
return `📖 ${formatFilePath(filePath)}${suffix}`;
}

function formatBashStart(_toolName: string, args: unknown): string | null {
Expand Down
41 changes: 40 additions & 1 deletion src/common/utils/messages/extractEditedFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function createAssistantMessage(
diff: string;
uiOnlyDiff?: string;
success?: boolean;
inputPathKey?: "path" | "file_path";
}>
): MuxMessage {
return {
Expand All @@ -24,7 +25,7 @@ function createAssistantMessage(
toolCallId: `tc-${Math.random().toString(36).slice(2)}`,
toolName: tc.toolName,
state: "output-available" as const,
input: { file_path: tc.filePath },
input: tc.inputPathKey === "file_path" ? { file_path: tc.filePath } : { path: tc.filePath },
output: {
success: tc.success ?? true,
diff: tc.diff,
Expand Down Expand Up @@ -72,6 +73,22 @@ describe("extractEditedFilePaths", () => {
expect(paths).toEqual(["/path/to/file2.ts", "/path/to/file1.ts"]);
});

it("should extract file paths from legacy path alias inputs", () => {
const messages: MuxMessage[] = [
createAssistantMessage([
{
toolName: "file_edit_replace_string",
filePath: "/path/to/legacy.ts",
diff: makeDiff("/path/to/legacy.ts", "old", "new"),
inputPathKey: "path",
},
]),
];

const paths = extractEditedFilePaths(messages);
expect(paths).toEqual(["/path/to/legacy.ts"]);
});

it("should ignore failed edits", () => {
const messages: MuxMessage[] = [
createAssistantMessage([
Expand Down Expand Up @@ -143,6 +160,28 @@ describe("extractEditedFileDiffs", () => {
expect(result[0].diff).toBe(diff);
});

it("should extract diffs when input uses legacy path alias", () => {
const originalContent = "line1\nline2\nline3";
const newContent = "line1\nupdated\nline3";
const diff = makeDiff("/path/to/legacy.ts", originalContent, newContent);

const messages: MuxMessage[] = [
createAssistantMessage([
{
toolName: "file_edit_replace_string",
filePath: "/path/to/legacy.ts",
diff,
inputPathKey: "path",
},
]),
];

const result = extractEditedFileDiffs(messages);
expect(result).toHaveLength(1);
expect(result[0].path).toBe("/path/to/legacy.ts");
expect(result[0].diff).toBe(diff);
});

it("should prefer ui_only diffs when present", () => {
const originalContent = "line1\nline2\nline3";
const newContent = "line1\nmodified\nline3";
Expand Down
19 changes: 5 additions & 14 deletions src/common/utils/messages/extractEditedFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ import { getToolOutputUiOnly } from "@/common/utils/tools/toolOutputUiOnly";
import { FILE_EDIT_TOOL_NAMES } from "@/common/types/tools";
import { MAX_EDITED_FILES, MAX_FILE_CONTENT_SIZE } from "@/common/constants/attachments";
import { applyPatch, createPatch, parsePatch } from "diff";

/**
* Input shape for file edit tools.
* All file edit tools have a file_path field.
*/
interface FileEditToolInput {
file_path?: string;
}
import { extractToolFilePath } from "@/common/utils/tools/toolInputFilePath";

/**
* Output shape for file edit tools.
Expand Down Expand Up @@ -60,9 +53,8 @@ export function extractEditedFilePaths(messages: MuxMessage[]): string[] {
if (!output?.success) continue;

// Extract file path from input
const input = part.input as FileEditToolInput | undefined;
const filePath = input?.file_path;
if (filePath && typeof filePath === "string" && !seen.has(filePath)) {
const filePath = extractToolFilePath(part.input);
if (filePath && !seen.has(filePath)) {
seen.add(filePath);
editedFiles.push(filePath);
}
Expand Down Expand Up @@ -205,9 +197,8 @@ export function extractEditedFileDiffs(messages: MuxMessage[]): FileEditDiff[] {
const diff = uiOnly?.file_edit?.diff ?? output.diff;
if (!diff) continue;

const input = part.input as FileEditToolInput | undefined;
const filePath = input?.file_path;
if (!filePath || typeof filePath !== "string") continue;
const filePath = extractToolFilePath(part.input);
if (!filePath) continue;

// Add diff to this file's list
if (!diffsByPath.has(filePath)) {
Expand Down
Loading
Loading