From 7430ffa3578c2f9d1bde329b0d5a200109f01393 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:28:17 +0000 Subject: [PATCH 1/5] refactor: remove dead compareSets utility function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compareSets was exported from useStableReference.ts but never called anywhere in the codebase — not in production code, not in tests. Its sibling comparators (compareMaps, compareRecords, compareArrays) are all actively used and tested; compareSets was speculatively added but never wired up. --- src/browser/hooks/useStableReference.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/browser/hooks/useStableReference.ts b/src/browser/hooks/useStableReference.ts index 71fb6915a8..2bcbefbc9d 100644 --- a/src/browser/hooks/useStableReference.ts +++ b/src/browser/hooks/useStableReference.ts @@ -51,21 +51,6 @@ export function compareRecords( return true; } -/** - * Compare two Sets for equality (same size and values). - * - * @param prev Previous Set - * @param next Next Set - * @returns true if Sets are equal, false otherwise - */ -export function compareSets(a: Set, b: Set): boolean { - if (a.size !== b.size) return false; - for (const item of a) { - if (!b.has(item)) return false; - } - return true; -} - /** * Compare two Arrays for deep equality (same length and values). * Uses === for value comparison by default. From f9d2a0efa311023c9b09dcc2563cd9a9b68696d3 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:11:10 +0000 Subject: [PATCH 2/5] refactor: remove dead asyncEventIterator export The asyncEventIterator async generator function was exported but never imported anywhere in the codebase. Only its sibling createAsyncEventQueue is actively used (by router.ts and withQueueHeartbeat.test.ts). Remove the dead function to reduce surface area. --- src/common/utils/asyncEventIterator.ts | 68 -------------------------- 1 file changed, 68 deletions(-) diff --git a/src/common/utils/asyncEventIterator.ts b/src/common/utils/asyncEventIterator.ts index 068075f848..8a7c05e5c0 100644 --- a/src/common/utils/asyncEventIterator.ts +++ b/src/common/utils/asyncEventIterator.ts @@ -1,71 +1,3 @@ -/** - * Convert event emitter subscription to async iterator. - * - * Handles the common pattern of: - * 1. Subscribe to events - * 2. Yield events as async iterator - * 3. Unsubscribe on cleanup - * - * Usage: - * ```ts - * yield* asyncEventIterator( - * (handler) => emitter.on('event', handler), - * (handler) => emitter.off('event', handler) - * ); - * ``` - * - * Or with initialValue for immediate first yield: - * ```ts - * yield* asyncEventIterator( - * (handler) => service.onChange(handler), - * (handler) => service.offChange(handler), - * { initialValue: await service.getState() } - * ); - * ``` - */ -export async function* asyncEventIterator( - subscribe: (handler: (value: T) => void) => void, - unsubscribe: (handler: (value: T) => void) => void, - options?: { initialValue?: T } -): AsyncGenerator { - const queue: T[] = []; - let resolveNext: ((value: T) => void) | null = null; - let ended = false; - - const handler = (value: T) => { - if (ended) return; - if (resolveNext) { - const resolve = resolveNext; - resolveNext = null; - resolve(value); - } else { - queue.push(value); - } - }; - - subscribe(handler); - - try { - // Yield initial value if provided - if (options?.initialValue !== undefined) { - yield options.initialValue; - } - - while (!ended) { - if (queue.length > 0) { - yield queue.shift()!; - } else { - yield await new Promise((resolve) => { - resolveNext = resolve; - }); - } - } - } finally { - ended = true; - unsubscribe(handler); - } -} - /** * Create an async event queue that can be pushed to from event handlers. * From cd6695ecf1e05208288f5a14be7e557896846fa7 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:13:34 +0000 Subject: [PATCH 3/5] refactor: deduplicate isPathInsideDir into shared pathUtils Four files (router.ts, workspaceService.ts, task_apply_git_patch.ts, AgentBrowserSessionDiscoveryService.ts) each defined an identical isPathInsideDir function. Extract it into src/node/utils/pathUtils.ts and replace the copies with imports. --- src/node/orpc/router.ts | 10 +--------- .../AgentBrowserSessionDiscoveryService.ts | 6 +----- .../services/tools/task_apply_git_patch.ts | 9 +-------- src/node/services/workspaceService.ts | 9 +-------- src/node/utils/pathUtils.ts | 18 ++++++++++++++++++ 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index b7dcb5cf61..200de3aea3 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -9,7 +9,7 @@ import { } from "@/common/constants/muxGatewayOAuth"; import { Err, Ok } from "@/common/types/result"; import { resolveProviderCredentials } from "@/node/utils/providerRequirements"; -import { stripTrailingSlashes } from "@/node/utils/pathUtils"; +import { isPathInsideDir, stripTrailingSlashes } from "@/node/utils/pathUtils"; import { generateWorkspaceIdentity } from "@/node/services/workspaceTitleGenerator"; import type { UpdateStatus, @@ -148,14 +148,6 @@ function isErrnoWithCode(error: unknown, code: string): boolean { return Boolean(error && typeof error === "object" && "code" in error && error.code === code); } -function isPathInsideDir(dirPath: string, filePath: string): boolean { - const resolvedDir = path.resolve(dirPath); - const resolvedFile = path.resolve(filePath); - const relative = path.relative(resolvedDir, resolvedFile); - - return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); -} - function isTrustedProjectPath(context: ORPCContext, projectPath?: string | null): boolean { return isProjectTrusted(context.config, projectPath); } diff --git a/src/node/services/browser/AgentBrowserSessionDiscoveryService.ts b/src/node/services/browser/AgentBrowserSessionDiscoveryService.ts index 96e522c952..4437147243 100644 --- a/src/node/services/browser/AgentBrowserSessionDiscoveryService.ts +++ b/src/node/services/browser/AgentBrowserSessionDiscoveryService.ts @@ -6,6 +6,7 @@ import * as path from "node:path"; import { getErrorMessage } from "@/common/utils/errors"; import { log } from "@/node/services/log"; import { DisposableProcess } from "@/node/utils/disposableExec"; +import { isPathInsideDir } from "@/node/utils/pathUtils"; const CLI_TIMEOUT_MS = 30_000; const PROCESS_CWD_TIMEOUT_MS = 5_000; @@ -432,11 +433,6 @@ function normalizeComparablePath(filePath: string): string { return process.platform === "win32" ? normalized.toLowerCase() : normalized; } -function isPathInsideDir(dirPath: string, filePath: string): boolean { - const relative = path.relative(dirPath, filePath); - return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); -} - async function readPositiveIntegerFile( readFileFn: typeof fsPromises.readFile, filePath: string diff --git a/src/node/services/tools/task_apply_git_patch.ts b/src/node/services/tools/task_apply_git_patch.ts index 41fa9008e5..1d5bd760fc 100644 --- a/src/node/services/tools/task_apply_git_patch.ts +++ b/src/node/services/tools/task_apply_git_patch.ts @@ -12,6 +12,7 @@ import { import { shellQuote } from "@/common/utils/shell"; import { execBuffered } from "@/node/utils/runtime/helpers"; import { gitNoHooksPrefix } from "@/node/utils/gitNoHooksEnv"; +import { isPathInsideDir } from "@/node/utils/pathUtils"; import { getSubagentGitPatchMboxPath, markSubagentGitPatchArtifactApplied, @@ -77,14 +78,6 @@ function mergeNotes(...notes: Array): string | undefined { return parts.length > 0 ? parts.join("\n") : undefined; } -function isPathInsideDir(dirPath: string, filePath: string): boolean { - const resolvedDir = path.resolve(dirPath); - const resolvedFile = path.resolve(filePath); - const relative = path.relative(resolvedDir, resolvedFile); - - return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); -} - async function tryRevParseHead(params: { runtime: ToolConfiguration["runtime"]; cwd: string; diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 1625e02cc0..2bb2b2dde6 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -12,6 +12,7 @@ import { Ok, Err } from "@/common/types/result"; import { askUserQuestionManager } from "@/node/services/askUserQuestionManager"; import { delegatedToolCallManager } from "@/node/services/delegatedToolCallManager"; import { log } from "@/node/services/log"; +import { isPathInsideDir } from "@/node/utils/pathUtils"; import { AgentSession } from "@/node/services/agentSession"; import type { HistoryService } from "@/node/services/historyService"; import type { AIService } from "@/node/services/aiService"; @@ -401,14 +402,6 @@ async function resetForkedSessionUsage( ); } -function isPathInsideDir(dirPath: string, filePath: string): boolean { - const resolvedDir = path.resolve(dirPath); - const resolvedFile = path.resolve(filePath); - const relative = path.relative(resolvedDir, resolvedFile); - - return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); -} - function isPositiveInteger(value: unknown): value is number { return ( typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 diff --git a/src/node/utils/pathUtils.ts b/src/node/utils/pathUtils.ts index 236f2239af..4ec464da4c 100644 --- a/src/node/utils/pathUtils.ts +++ b/src/node/utils/pathUtils.ts @@ -106,3 +106,21 @@ export async function isGitRepository(projectPath: string): Promise { return false; } } + +/** + * Check whether `filePath` is equal to or nested inside `dirPath`. + * + * Both paths are resolved to absolute form first, so relative segments + * and missing trailing slashes are handled automatically. + * + * @example + * isPathInsideDir("/home/user/project", "/home/user/project/src/index.ts") // true + * isPathInsideDir("/home/user/project", "/home/user/other/file.ts") // false + */ +export function isPathInsideDir(dirPath: string, filePath: string): boolean { + const resolvedDir = path.resolve(dirPath); + const resolvedFile = path.resolve(filePath); + const relative = path.relative(resolvedDir, resolvedFile); + + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} From b700d042ec05e2218f37558619529bbf9bf61d32 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:09:38 +0000 Subject: [PATCH 4/5] refactor: extract shared capitalize utility Two files (modelDisplay.ts, DelegationChart.tsx) each defined an identical capitalize(str) function. Extracted into src/common/utils/capitalize.ts and replaced both copies with imports. --- src/browser/features/Analytics/DelegationChart.tsx | 10 +--------- src/common/utils/ai/modelDisplay.ts | 10 ++-------- src/common/utils/capitalize.ts | 7 +++++++ 3 files changed, 10 insertions(+), 17 deletions(-) create mode 100644 src/common/utils/capitalize.ts diff --git a/src/browser/features/Analytics/DelegationChart.tsx b/src/browser/features/Analytics/DelegationChart.tsx index 9cb5f9c760..5a07a9cf50 100644 --- a/src/browser/features/Analytics/DelegationChart.tsx +++ b/src/browser/features/Analytics/DelegationChart.tsx @@ -8,7 +8,7 @@ import { XAxis, YAxis, } from "recharts"; -import assert from "@/common/utils/assert"; +import { capitalize } from "@/common/utils/capitalize"; import { Skeleton } from "@/browser/components/Skeleton/Skeleton"; import type { DelegationSummary } from "@/browser/hooks/useAnalytics"; import { @@ -51,14 +51,6 @@ const DELEGATION_TOKEN_CATEGORIES: Array<{ { key: "reasoningTokens", label: "Reasoning", color: TOKEN_CATEGORY_COLORS.reasoningTokens }, ]; -function capitalize(s: string): string { - assert(typeof s === "string", "capitalize expects a string"); - if (s.length === 0) { - return s; - } - return s.charAt(0).toUpperCase() + s.slice(1); -} - function formatCompressionRatio(compressionRatio: number): string { if (!Number.isFinite(compressionRatio) || compressionRatio <= 0) { return "N/A"; diff --git a/src/common/utils/ai/modelDisplay.ts b/src/common/utils/ai/modelDisplay.ts index e89886378b..8ad9d3f9ab 100644 --- a/src/common/utils/ai/modelDisplay.ts +++ b/src/common/utils/ai/modelDisplay.ts @@ -2,6 +2,8 @@ * Formatting utilities for model display names */ +import { capitalize } from "../capitalize"; + /** * Format a model name for display with proper capitalization and spacing. * @@ -170,14 +172,6 @@ export function formatModelDisplayName(modelName: string): string { return modelName.split("-").map(capitalize).join(" "); } -/** - * Capitalize the first letter of a string - */ -function capitalize(str: string): string { - if (!str) return str; - return str.charAt(0).toUpperCase() + str.slice(1); -} - /** * Format version numbers: ["4", "5"] -> "4.5" */ diff --git a/src/common/utils/capitalize.ts b/src/common/utils/capitalize.ts new file mode 100644 index 0000000000..6b18cd21c9 --- /dev/null +++ b/src/common/utils/capitalize.ts @@ -0,0 +1,7 @@ +/** + * Capitalize the first letter of a string. + */ +export function capitalize(str: string): string { + if (!str) return str; + return str.charAt(0).toUpperCase() + str.slice(1); +} From 46ecc18e4f898186619db7395f3bcbfd1a42a36f Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:25:30 +0000 Subject: [PATCH 5/5] refactor: extract shared formatRepoCount helper Both MultiProjectGitStatusIndicator.tsx and MultiProjectDivergenceDialog.tsx defined identical formatRepoCount(count) functions. Extract to a shared gitStatusFormatters.ts module in the same directory. --- .../GitStatusIndicator/MultiProjectDivergenceDialog.tsx | 5 +---- .../GitStatusIndicator/MultiProjectGitStatusIndicator.tsx | 5 +---- .../components/GitStatusIndicator/gitStatusFormatters.ts | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 src/browser/components/GitStatusIndicator/gitStatusFormatters.ts diff --git a/src/browser/components/GitStatusIndicator/MultiProjectDivergenceDialog.tsx b/src/browser/components/GitStatusIndicator/MultiProjectDivergenceDialog.tsx index 19a7b17042..c372123163 100644 --- a/src/browser/components/GitStatusIndicator/MultiProjectDivergenceDialog.tsx +++ b/src/browser/components/GitStatusIndicator/MultiProjectDivergenceDialog.tsx @@ -10,6 +10,7 @@ import { cn } from "@/common/lib/utils"; import assert from "@/common/utils/assert"; import type { GitStatus } from "@/common/types/workspace"; import type { MultiProjectGitSummary } from "@/browser/stores/GitStatusStore"; +import { formatRepoCount } from "./gitStatusFormatters"; interface MultiProjectDivergenceDialogProps { isOpen: boolean; @@ -18,10 +19,6 @@ interface MultiProjectDivergenceDialogProps { isRefreshing: boolean; } -function formatRepoCount(count: number): string { - return `${count} ${count === 1 ? "repo" : "repos"}`; -} - function formatLineDelta(additions: number, deletions: number): React.ReactNode { if (additions === 0 && deletions === 0) { return 0; diff --git a/src/browser/components/GitStatusIndicator/MultiProjectGitStatusIndicator.tsx b/src/browser/components/GitStatusIndicator/MultiProjectGitStatusIndicator.tsx index 937b5eab3a..59c4904280 100644 --- a/src/browser/components/GitStatusIndicator/MultiProjectGitStatusIndicator.tsx +++ b/src/browser/components/GitStatusIndicator/MultiProjectGitStatusIndicator.tsx @@ -10,6 +10,7 @@ import { stopKeyboardPropagation } from "@/browser/utils/events"; import { cn } from "@/common/lib/utils"; import assert from "@/common/utils/assert"; import { Tooltip, TooltipContent, TooltipTrigger } from "../Tooltip/Tooltip"; +import { formatRepoCount } from "./gitStatusFormatters"; interface MultiProjectGitStatusIndicatorProps { workspaceId: string; @@ -24,10 +25,6 @@ interface ChipPresentation { className: string; } -function formatRepoCount(count: number): string { - return `${count} ${count === 1 ? "repo" : "repos"}`; -} - function formatCategoryCount(count: number, noun: string): string { return `${count} ${noun}`; } diff --git a/src/browser/components/GitStatusIndicator/gitStatusFormatters.ts b/src/browser/components/GitStatusIndicator/gitStatusFormatters.ts new file mode 100644 index 0000000000..8ec03ba6cd --- /dev/null +++ b/src/browser/components/GitStatusIndicator/gitStatusFormatters.ts @@ -0,0 +1,4 @@ +/** Pluralize a repo count: "1 repo" / "3 repos". */ +export function formatRepoCount(count: number): string { + return `${count} ${count === 1 ? "repo" : "repos"}`; +}