Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ description: Agent instructions for AI assistants working on the Mux codebase
- **Avoid `setTimeout` for component coordination** - racy and fragile; use callbacks or effects.
- **Keyboard event propagation** - React's `e.stopPropagation()` only stops synthetic event bubbling; native `window` listeners still fire. Use `stopKeyboardPropagation(e)` from `@/browser/utils/events` to stop both React and native propagation when blocking global handlers (like stream interrupt on Escape).


## Tool Schema Conventions

- **Use `.nullish()` for optional tool input parameters** β€” never `.optional()` alone. OpenAI's Responses API normalizes tool schemas into strict mode, which forces every field into `required` and expects optional fields to accept `null`. `.nullish()` (= `.optional().nullable()`) satisfies strict-mode providers (OpenAI) while remaining compatible with non-strict providers (Anthropic, Google). See the module doc comment in `src/common/utils/tools/toolDefinitions.ts` for details.
- Implementation handlers should use `!= null` (loose equality) instead of `!== undefined` to treat both `null` and `undefined` as "not provided".
- This applies only to tool **input** schemas (parameters the model provides), not to tool **output/result** schemas (constructed by our backend).

## Component State & Storage

- Prefer **self-contained components** over utility functions + hook proliferation. A component that takes `workspaceId` and computes everything internally is better than one that requires 10 props drilled from parent hooks.
Expand Down
2 changes: 1 addition & 1 deletion docs/config/notifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ notify: {
message: z
.string()
.max(200)
.optional()
.nullish()
.describe(
"Optional notification body with more details (max 200 chars). " +
"Keep it brief - users may only see a preview."
Expand Down
4 changes: 2 additions & 2 deletions src/browser/components/tools/AgentSkillReadFileToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ export const AgentSkillReadFileToolCall: React.FC<AgentSkillReadFileToolCallProp
<span className="text-secondary font-medium">File:</span>
<span className="text-text font-monospace break-all">{args.filePath}</span>
</div>
{args.offset !== undefined && (
{args.offset != null && (
<div className="flex items-baseline gap-1.5">
<span className="text-secondary font-medium">Offset:</span>
<span className="text-text font-monospace break-all">line {args.offset}</span>
</div>
)}
{args.limit !== undefined && (
{args.limit != null && (
<div className="flex items-baseline gap-1.5">
<span className="text-secondary font-medium">Limit:</span>
<span className="text-text font-monospace break-all">{args.limit} lines</span>
Expand Down
4 changes: 2 additions & 2 deletions src/browser/components/tools/FileReadToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ export const FileReadToolCall: React.FC<FileReadToolCallProps> = ({
<span className="text-secondary font-medium">Path:</span>
<span className="text-text font-monospace break-all">{filePath}</span>
</div>
{args.offset !== undefined && (
{args.offset != null && (
<div className="flex gap-1.5">
<span className="text-secondary font-medium">Offset:</span>
<span className="text-text font-monospace break-all">line {args.offset}</span>
</div>
)}
{args.limit !== undefined && (
{args.limit != null && (
<div className="flex gap-1.5">
<span className="text-secondary font-medium">Limit:</span>
<span className="text-text font-monospace break-all">{args.limit} lines</span>
Expand Down
11 changes: 4 additions & 7 deletions src/browser/components/tools/TaskToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,7 @@ export const TaskAwaitToolCall: React.FC<TaskAwaitToolCallProps> = ({
const suppressReportInAwaitTaskIds = taskReportLinking?.suppressReportInAwaitTaskIds;

const showConfigInfo =
taskIds !== undefined ||
timeoutSecs !== undefined ||
args.filter !== undefined ||
args.filter_exclude === true;
taskIds != null || timeoutSecs != null || args.filter != null || args.filter_exclude === true;

// Summary for header
const completedCount = results.filter((r) => r.status === "completed").length;
Expand Down Expand Up @@ -562,9 +559,9 @@ export const TaskAwaitToolCall: React.FC<TaskAwaitToolCallProps> = ({
{/* Config info */}
{showConfigInfo && (
<div className="task-divider text-muted mb-2 flex flex-wrap gap-2 border-b pb-2 text-[10px]">
{taskIds !== undefined && <span>Waiting for: {taskIds.length} task(s)</span>}
{timeoutSecs !== undefined && <span>Timeout: {timeoutSecs}s</span>}
{args.filter !== undefined && <span>Filter: {args.filter}</span>}
{taskIds != null && <span>Waiting for: {taskIds.length} task(s)</span>}
{timeoutSecs != null && <span>Timeout: {timeoutSecs}s</span>}
{args.filter != null && <span>Filter: {args.filter}</span>}
{args.filter_exclude === true && <span>Exclude: true</span>}
</div>
)}
Expand Down
6 changes: 3 additions & 3 deletions src/cli/toolFormatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ function formatFileReadStart(_toolName: string, args: unknown): string | null {
if (!readArgs?.file_path) return null;

let suffix = "";
if (readArgs.offset !== undefined || readArgs.limit !== undefined) {
if (readArgs.offset != null || readArgs.limit != null) {
const parts: string[] = [];
if (readArgs.offset !== undefined) parts.push(`L${readArgs.offset}`);
if (readArgs.limit !== undefined) parts.push(`+${readArgs.limit}`);
if (readArgs.offset != null) parts.push(`L${readArgs.offset}`);
if (readArgs.limit != null) parts.push(`+${readArgs.limit}`);
suffix = chalk.dim(` (${parts.join(", ")})`);
}

Expand Down
91 changes: 27 additions & 64 deletions src/common/types/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,14 @@ import type {
WebFetchToolResultSchema,
} from "@/common/utils/tools/toolDefinitions";

// Bash Tool Types
export interface BashToolArgs {
script: string;
timeout_secs: number; // Required - defaults should be applied by producers
run_in_background?: boolean; // Run without blocking (for long-running processes)
display_name: string; // Required - used as process identifier if sent to background
}
// Bash Tool Types β€” derived from schema (avoid drift)
export type BashToolArgs = z.infer<typeof TOOL_DEFINITIONS.bash.schema>;

// BashToolResult derived from Zod schema (single source of truth)
export type BashToolResult = z.infer<typeof BashToolResultSchema>;

// File Read Tool Types
export interface FileReadToolArgs {
file_path: string;
offset?: number; // 1-based starting line number (optional)
limit?: number; // number of lines to return from offset (optional)
}
// File Read Tool Types β€” derived from schema (avoid drift)
export type FileReadToolArgs = z.infer<typeof TOOL_DEFINITIONS.file_read.schema>;

// Agent Skill Tool Types
// Args derived from schema (avoid drift)
Expand Down Expand Up @@ -110,35 +101,24 @@ export interface FileEditErrorResult extends ToolOutputUiOnlyFields {
note?: string; // Agent-only message (not displayed in UI)
}

export interface FileEditInsertToolArgs {
file_path: string;
/** Anchor text to insert before. Content will be placed immediately before this substring. */
insert_before?: string;
/** Anchor text to insert after. Content will be placed immediately after this substring. */
insert_after?: string;
content: string;
}
// FileEditInsertToolArgs derived from schema (avoid drift)
export type FileEditInsertToolArgs = z.infer<typeof TOOL_DEFINITIONS.file_edit_insert.schema>;

// FileEditInsertToolResult derived from Zod schema (single source of truth)
export type FileEditInsertToolResult = z.infer<typeof FileEditInsertToolResultSchema>;

export interface FileEditReplaceStringToolArgs {
file_path: string;
old_string: string;
new_string: string;
replace_count?: number;
}
// FileEditReplaceStringToolArgs derived from schema (avoid drift)
export type FileEditReplaceStringToolArgs = z.infer<
typeof TOOL_DEFINITIONS.file_edit_replace_string.schema
>;

// FileEditReplaceStringToolResult derived from Zod schema (single source of truth)
export type FileEditReplaceStringToolResult = z.infer<typeof FileEditReplaceStringToolResultSchema>;

export interface FileEditReplaceLinesToolArgs {
file_path: string;
start_line: number;
end_line: number;
new_lines: string[];
expected_lines?: string[];
}
// FileEditReplaceLinesToolArgs derived from schema (avoid drift)
export type FileEditReplaceLinesToolArgs = z.infer<
typeof TOOL_DEFINITIONS.file_edit_replace_lines.schema
>;

export type FileEditReplaceLinesToolResult =
| (FileEditDiffSuccessBase & {
Expand Down Expand Up @@ -288,43 +268,28 @@ export interface LegacyProposePlanToolResult {
message: string;
}

// Todo Tool Types
export interface TodoItem {
content: string;
status: "pending" | "in_progress" | "completed";
}

export interface TodoWriteToolArgs {
todos: TodoItem[];
}
// Todo Tool Types β€” derived from schema (avoid drift)
export type TodoWriteToolArgs = z.infer<typeof TOOL_DEFINITIONS.todo_write.schema>;
export type TodoItem = TodoWriteToolArgs["todos"][number];

export interface TodoWriteToolResult {
success: true;
count: number;
}

// Status Set Tool Types
export interface StatusSetToolArgs {
emoji: string;
message: string;
url?: string;
}
// Status Set Tool Types β€” derived from schema (avoid drift)
export type StatusSetToolArgs = z.infer<typeof TOOL_DEFINITIONS.status_set.schema>;

// Bash Output Tool Types (read incremental output from background processes)
export interface BashOutputToolArgs {
process_id: string;
filter?: string;
filter_exclude?: boolean;
timeout_secs: number;
}
// Bash Output Tool Types β€” derived from schema (avoid drift)
export type BashOutputToolArgs = z.infer<typeof TOOL_DEFINITIONS.bash_output.schema>;

// BashOutputToolResult derived from Zod schema (single source of truth)
export type BashOutputToolResult = z.infer<typeof BashOutputToolResultSchema>;

// Bash Background Tool Types
export interface BashBackgroundTerminateArgs {
process_id: string;
}
// Bash Background Tool Types β€” derived from schema (avoid drift)
export type BashBackgroundTerminateArgs = z.infer<
typeof TOOL_DEFINITIONS.bash_background_terminate.schema
>;

// BashBackgroundTerminateResult derived from Zod schema (single source of truth)
export type BashBackgroundTerminateResult = z.infer<typeof BashBackgroundTerminateResultSchema>;
Expand Down Expand Up @@ -353,10 +318,8 @@ export type StatusSetToolResult =
error: string;
};

// Web Fetch Tool Types
export interface WebFetchToolArgs {
url: string;
}
// Web Fetch Tool Types β€” derived from schema (avoid drift)
export type WebFetchToolArgs = z.infer<typeof TOOL_DEFINITIONS.web_fetch.schema>;

// WebFetchToolResult derived from Zod schema (single source of truth)
export type WebFetchToolResult = z.infer<typeof WebFetchToolResultSchema>;
Expand Down
Loading
Loading