From 2ecf442c94cc30acbc7fe4fb65904f70accf5540 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 04:58:38 +0000 Subject: [PATCH 1/9] refactor: use shared isAbortError utility in AuthTokenModal Replace two inline `error instanceof DOMException && error.name === "AbortError"` checks with the existing shared `isAbortError()` utility from `@/browser/utils/isAbortError`. The shared utility is a superset that handles any Error with name "AbortError" (including DOMException), so behavior is preserved. --- src/browser/components/AuthTokenModal/AuthTokenModal.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/browser/components/AuthTokenModal/AuthTokenModal.tsx b/src/browser/components/AuthTokenModal/AuthTokenModal.tsx index 19fc00c897..9445b52e8b 100644 --- a/src/browser/components/AuthTokenModal/AuthTokenModal.tsx +++ b/src/browser/components/AuthTokenModal/AuthTokenModal.tsx @@ -11,6 +11,7 @@ import { import { Button } from "@/browser/components/Button/Button"; import { getBrowserBackendBaseUrl } from "@/browser/utils/backendBaseUrl"; import { getErrorMessage } from "@/common/utils/errors"; +import { isAbortError } from "@/browser/utils/isAbortError"; interface AuthTokenModalProps { isOpen: boolean; @@ -136,8 +137,7 @@ export function AuthTokenModal(props: AuthTokenModalProps) { return; } - const isAbortError = error instanceof DOMException && error.name === "AbortError"; - if (!isAbortError) { + if (!isAbortError(error)) { setGithubDeviceFlowEnabled(false); setGithubOptionsLoading(false); } @@ -218,8 +218,7 @@ export function AuthTokenModal(props: AuthTokenModalProps) { clearStoredAuthToken(); props.onSessionAuthenticated?.(); } catch (error) { - const isAbortError = error instanceof DOMException && error.name === "AbortError"; - if (isAbortError) { + if (isAbortError(error)) { return; } From 2cead1cf9bbf9f47d85e35af1aadf7c8e49c884e Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:35:13 +0000 Subject: [PATCH 2/9] refactor: extract extractChunkDeltaText helper to deduplicate advisor chunk parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text-delta and reasoning-delta cases in onAdvisorChunk both cast the chunk to the same shape and probe three field names for a string value, differing only in priority order. Extract a shared helper that takes the chunk and an ordered list of field names, returning the first string found. Behavior is preserved — field lookup order is unchanged. --- src/node/services/aiService.ts | 55 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 6fbdb4a8e1..f50d5dc83f 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -193,6 +193,25 @@ interface ToolExecutionContext { abortSignal?: AbortSignal; } +/** + * Extract the first string-typed value from a chunk object by trying field names + * in priority order. Different AI SDK chunk types (`text-delta`, `reasoning-delta`) + * surface the delta text under varying field names; this avoids duplicating the + * probe logic for each case. + */ +function extractChunkDeltaText( + chunk: Record, + fieldPriority: readonly string[] +): string { + for (const field of fieldPriority) { + const value = chunk[field]; + if (typeof value === "string") { + return value; + } + } + return ""; +} + function isToolExecutionContext(value: unknown): value is ToolExecutionContext { if (typeof value !== "object" || value == null || Array.isArray(value)) { return false; @@ -1307,40 +1326,24 @@ export class AIService extends EventEmitter { const onAdvisorChunk: StreamTextOnChunk = ({ chunk }) => { switch (chunk.type) { case "text-delta": { - const textDeltaChunk = chunk as { - text?: unknown; - delta?: unknown; - textDelta?: unknown; - }; // Providers/SDKs can stream advisor text deltas under different field names. - const chunkText = - typeof textDeltaChunk.textDelta === "string" - ? textDeltaChunk.textDelta - : typeof textDeltaChunk.delta === "string" - ? textDeltaChunk.delta - : typeof textDeltaChunk.text === "string" - ? textDeltaChunk.text - : ""; + const chunkText = extractChunkDeltaText(chunk as Record, [ + "textDelta", + "delta", + "text", + ]); if (chunkText.length > 0) { advisorStepCaptureRef.currentStepText += chunkText; } return; } case "reasoning-delta": { - const reasoningChunk = chunk as { - text?: unknown; - textDelta?: unknown; - delta?: unknown; - }; // Anthropic signature updates can arrive as reasoning deltas without text. - const chunkText = - typeof reasoningChunk.text === "string" - ? reasoningChunk.text - : typeof reasoningChunk.textDelta === "string" - ? reasoningChunk.textDelta - : typeof reasoningChunk.delta === "string" - ? reasoningChunk.delta - : ""; + const chunkText = extractChunkDeltaText(chunk as Record, [ + "text", + "textDelta", + "delta", + ]); if (chunkText.length > 0) { advisorStepCaptureRef.currentStepReasoning += chunkText; } From 631b9010fbd4d114852448dc9472489ff20682bc Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:21:45 +0000 Subject: [PATCH 3/9] refactor: remove unnecessary exports from skillFileUtils resolveSkillFilePath and lstatIfExists are only used within skillFileUtils.ts itself. Remove the export keyword to narrow their visibility to file-internal, making the module's public API surface clearer. --- src/node/services/tools/skillFileUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/services/tools/skillFileUtils.ts b/src/node/services/tools/skillFileUtils.ts index 9ca9c57ccf..ed63e7c3f5 100644 --- a/src/node/services/tools/skillFileUtils.ts +++ b/src/node/services/tools/skillFileUtils.ts @@ -27,7 +27,7 @@ export function isAbsolutePathAny(filePath: string): boolean { return /^[A-Za-z]:[\\/]/.test(filePath); } -export function resolveSkillFilePath( +function resolveSkillFilePath( skillDir: string, filePath: string ): { @@ -63,7 +63,7 @@ export function resolveSkillFilePath( }; } -export async function lstatIfExists(targetPath: string): Promise { +async function lstatIfExists(targetPath: string): Promise { try { return await fsPromises.lstat(targetPath); } catch (error) { From 453c7acd15dc189ca38318f6efb78445d25bb578 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:27:59 +0000 Subject: [PATCH 4/9] refactor: remove dead getCancelledCompactionKey storage helper The getCancelledCompactionKey function and its entry in the EPHEMERAL_WORKSPACE_KEY_FUNCTIONS array became dead code when useResumeManager.ts (its only consumer) was deleted. Remove both the function definition and the ephemeral-keys array reference. --- src/common/constants/storage.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index c772ea3cf1..84f4a4eec1 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -248,14 +248,6 @@ export function getAutoRetryKey(workspaceId: string): string { return `${workspaceId}-autoRetry`; } -/** - * Get storage key for cancelled compaction tracking. - * Stores compaction-request user message ID to verify freshness across reloads. - */ -export function getCancelledCompactionKey(workspaceId: string): string { - return `workspace:${workspaceId}:cancelled-compaction`; -} - /** * Get the localStorage key for the selected agent definition id for a scope. * Format: "agentId:{scopeId}" @@ -673,7 +665,6 @@ export function getPostCompactionStateKey(workspaceId: string): string { * Additional ephemeral keys to delete on workspace removal (not copied on fork) */ const EPHEMERAL_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> = [ - getCancelledCompactionKey, getPendingWorkspaceSendErrorKey, getPlanContentKey, // Cache only, no need to preserve on fork getPostCompactionStateKey, // Cache only, no need to preserve on fork From 6b44193f3a0136f26045c6d2313fce1257ccbe7a Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 04:59:57 +0000 Subject: [PATCH 5/9] refactor: deduplicate hasErrorCode in submoduleSync Remove the private hasErrorCode function from submoduleSync.ts and import the identical exported version from skillFileUtils.ts. Both implementations check whether an unknown error has a specific Node.js error code; the private copy was a verbatim duplicate. --- src/node/runtime/submoduleSync.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/runtime/submoduleSync.ts b/src/node/runtime/submoduleSync.ts index ff29e1e156..cb88cc1625 100644 --- a/src/node/runtime/submoduleSync.ts +++ b/src/node/runtime/submoduleSync.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import { NON_INTERACTIVE_ENV_VARS } from "@/common/constants/env"; import { getErrorMessage } from "@/common/utils/errors"; +import { hasErrorCode } from "@/node/services/tools/skillFileUtils"; import { GIT_NO_HOOKS_ENV } from "@/node/utils/gitNoHooksEnv"; import { execBuffered } from "@/node/utils/runtime/helpers"; @@ -50,10 +51,6 @@ function formatGitmodulesProbeError(error: unknown): Error { return new Error(`Failed to probe .gitmodules before submodule sync: ${getErrorMessage(error)}`); } -function hasErrorCode(error: unknown, code: string): boolean { - return Boolean(error && typeof error === "object" && "code" in error && error.code === code); -} - async function runSubmoduleCommand(args: { runtime: Runtime; workspacePath: string; From b861c09919619902014dee0ecac6711c9ee12aca Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:25:28 +0000 Subject: [PATCH 6/9] refactor: remove dead quickReviewNotes module The buildQuickLineReviewNote and buildQuickHunkReviewNote functions in src/browser/utils/review/quickReviewNotes.ts were never imported by any production code since their introduction in PR #2448 (Feb 2026). Remove the module and its test file (482 lines of dead code). --- .../utils/review/quickReviewNotes.test.ts | 246 ------------------ src/browser/utils/review/quickReviewNotes.ts | 236 ----------------- 2 files changed, 482 deletions(-) delete mode 100644 src/browser/utils/review/quickReviewNotes.test.ts delete mode 100644 src/browser/utils/review/quickReviewNotes.ts diff --git a/src/browser/utils/review/quickReviewNotes.test.ts b/src/browser/utils/review/quickReviewNotes.test.ts deleted file mode 100644 index cc0809edb1..0000000000 --- a/src/browser/utils/review/quickReviewNotes.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { describe, test, expect } from "bun:test"; -import type { DiffHunk } from "@/common/types/review"; -import { buildQuickHunkReviewNote, buildQuickLineReviewNote } from "./quickReviewNotes"; - -function makeHunk(overrides: Partial = {}): DiffHunk { - return { - id: "hunk-1", - filePath: "src/example.ts", - oldStart: 10, - oldLines: 3, - newStart: 10, - newLines: 3, - content: "-const a = 1;\n+const a = 2;\n console.log(a);", - header: "@@ -10,3 +10,3 @@", - ...overrides, - }; -} - -describe("buildQuickHunkReviewNote", () => { - test("returns correct filePath and userNote", () => { - const hunk = makeHunk(); - - const note = buildQuickHunkReviewNote({ - hunk, - userNote: "Looks good", - }); - - expect(note.filePath).toBe("src/example.ts"); - expect(note.userNote).toBe("Looks good"); - }); - - test("builds correct lineRange from hunk coordinates", () => { - const hunk = makeHunk({ - oldStart: 12, - oldLines: 4, - newStart: 20, - newLines: 5, - header: "@@ -12,4 +20,5 @@", - }); - - const note = buildQuickHunkReviewNote({ - hunk, - userNote: "Coordinate check", - }); - - expect(note.lineRange).toBe("-12-15 +20-24"); - }); - - test("includes selectedDiff matching hunk.content", () => { - const hunk = makeHunk({ - content: "-old line\n+new line\n unchanged", - }); - - const note = buildQuickHunkReviewNote({ - hunk, - userNote: "Diff included", - }); - - expect(note.selectedDiff).toBe(hunk.content); - }); - - test("handles small hunks by including all lines in selectedCode", () => { - const hunk = makeHunk({ - oldStart: 40, - oldLines: 5, - newStart: 40, - newLines: 5, - header: "@@ -40,5 +40,5 @@", - content: [ - "-const a = 1;", - "+const a = 2;", - " const b = 3;", - "-console.log(a);", - "+console.log(a, b);", - ].join("\n"), - }); - - const note = buildQuickHunkReviewNote({ - hunk, - userNote: "Small hunk", - }); - - const selectedLines = note.selectedCode.split("\n"); - - expect(selectedLines).toHaveLength(5); - expect(note.selectedCode).toContain("const a = 1;"); - expect(note.selectedCode).toContain("const a = 2;"); - expect(note.selectedCode).toContain("const b = 3;"); - expect(note.selectedCode).toContain("console.log(a);"); - expect(note.selectedCode).toContain("console.log(a, b);"); - expect(note.selectedCode).not.toContain("lines omitted"); - }); - - test("handles large hunks by eliding middle lines when over 20 lines", () => { - const content = Array.from( - { length: 25 }, - (_, index) => `+const line${index + 1} = ${index + 1};` - ).join("\n"); - - const hunk = makeHunk({ - oldStart: 100, - oldLines: 25, - newStart: 200, - newLines: 25, - header: "@@ -100,25 +200,25 @@", - content, - }); - - const note = buildQuickHunkReviewNote({ - hunk, - userNote: "Large hunk", - }); - - const selectedLines = note.selectedCode.split("\n"); - - expect(selectedLines).toHaveLength(21); - expect(note.selectedCode).toContain("(5 lines omitted)"); - expect(note.selectedCode).toContain("const line1 = 1;"); - expect(note.selectedCode).toContain("const line10 = 10;"); - expect(note.selectedCode).toContain("const line16 = 16;"); - expect(note.selectedCode).toContain("const line25 = 25;"); - expect(note.selectedCode).not.toContain("const line11 = 11;"); - expect(note.selectedCode).not.toContain("const line15 = 15;"); - }); -}); - -describe("buildQuickLineReviewNote", () => { - test("builds note data for a single selected line", () => { - const hunk = makeHunk({ - content: "-const a = 1;\n+const a = 2;\n const b = a;", - }); - - const note = buildQuickLineReviewNote({ - hunk, - startIndex: 1, - endIndex: 1, - userNote: "Use a constant here", - }); - - expect(note.lineRange).toBe("+10"); - expect(note.selectedDiff).toBe("+const a = 2;"); - expect(note.selectedCode).toContain("+ const a = 2;"); - expect(note.oldStart).toBe(1); - expect(note.newStart).toBe(10); - expect(note.userNote).toBe("Use a constant here"); - }); - - test("builds ranges from selected line span", () => { - const hunk = makeHunk({ - oldStart: 50, - oldLines: 4, - newStart: 50, - newLines: 4, - content: "-const a = 1;\n+const a = 2;\n const b = 3;\n-console.log(a);\n+console.log(a, b);", - header: "@@ -50,4 +50,4 @@", - }); - - const note = buildQuickLineReviewNote({ - hunk, - startIndex: 0, - endIndex: 2, - userNote: "Please revisit this block", - }); - - expect(note.lineRange).toBe("-50-51 +50-51"); - expect(note.selectedDiff).toBe("-const a = 1;\n+const a = 2;\n const b = 3;"); - expect(note.oldStart).toBe(50); - expect(note.newStart).toBe(50); - }); - - test("keeps old/new coordinates for context-only selections", () => { - const hunk = makeHunk({ - oldStart: 30, - oldLines: 3, - newStart: 40, - newLines: 3, - content: - "-const removed = 1;\n+const added = 1;\n const keepOne = added;\n const keepTwo = keepOne;", - header: "@@ -30,3 +40,3 @@", - }); - - const note = buildQuickLineReviewNote({ - hunk, - startIndex: 2, - endIndex: 3, - userNote: "Context-only selection", - }); - - expect(note.lineRange).toBe("-31-32 +41-42"); - expect(note.selectedDiff).toBe(" const keepOne = added;\n const keepTwo = keepOne;"); - expect(note.oldStart).toBe(31); - expect(note.newStart).toBe(41); - }); - - test("clamps out-of-bounds selection indices", () => { - const hunk = makeHunk({ - content: "-old\n+new\n context", - oldStart: 7, - oldLines: 2, - newStart: 7, - newLines: 2, - }); - - const note = buildQuickLineReviewNote({ - hunk, - startIndex: -50, - endIndex: 99, - userNote: "Clamp selection", - }); - - expect(note.lineRange).toBe("-7-8 +7-8"); - expect(note.selectedDiff).toBe("-old\n+new\n context"); - }); - - test("elides selectedCode for ranges longer than 20 lines", () => { - const content = Array.from( - { length: 30 }, - (_, index) => `+const line${index + 1} = ${index + 1};` - ).join("\n"); - - const hunk = makeHunk({ - oldStart: 1, - oldLines: 30, - newStart: 100, - newLines: 30, - content, - header: "@@ -1,30 +100,30 @@", - }); - - const note = buildQuickLineReviewNote({ - hunk, - startIndex: 0, - endIndex: 29, - userNote: "Large range", - }); - - const selectedLines = note.selectedCode.split("\n"); - expect(selectedLines).toHaveLength(21); - expect(note.selectedCode).toContain("(10 lines omitted)"); - expect(note.selectedCode).toContain("const line1 = 1;"); - expect(note.selectedCode).toContain("const line10 = 10;"); - expect(note.selectedCode).toContain("const line21 = 21;"); - expect(note.selectedCode).toContain("const line30 = 30;"); - expect(note.selectedCode).not.toContain("const line11 = 11;"); - }); -}); diff --git a/src/browser/utils/review/quickReviewNotes.ts b/src/browser/utils/review/quickReviewNotes.ts deleted file mode 100644 index 88f040f23a..0000000000 --- a/src/browser/utils/review/quickReviewNotes.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Utilities for building quick review notes from hunks in immersive mode. - * - * - buildQuickHunkReviewNote creates note data for an entire hunk. - * - buildQuickLineReviewNote creates note data for a selected line range in a hunk. - */ - -import type { DiffHunk, ReviewNoteData } from "@/common/types/review"; - -const CONTEXT_LINES = 10; -const MAX_FULL_LINES = CONTEXT_LINES * 2; - -interface QuickReviewLineData { - raw: string; - oldLineNum: number | null; - newLineNum: number | null; -} - -function splitDiffLines(content: string): string[] { - const lines = content.split(/\r?\n/); - if (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop(); - } - return lines; -} - -function formatRange(nums: number[]): string | null { - if (nums.length === 0) { - return null; - } - - const min = Math.min(...nums); - const max = Math.max(...nums); - return min === max ? `${min}` : `${min}-${max}`; -} - -function buildLineDataForHunk(hunk: DiffHunk): QuickReviewLineData[] { - const lines = splitDiffLines(hunk.content); - const lineData: QuickReviewLineData[] = []; - - let oldNum = hunk.oldStart; - let newNum = hunk.newStart; - - for (const line of lines) { - if (line.startsWith("@@")) { - const headerMatch = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/.exec(line); - if (headerMatch) { - oldNum = Number.parseInt(headerMatch[1], 10); - newNum = Number.parseInt(headerMatch[2], 10); - } - continue; - } - - const indicator = line[0] ?? " "; - if (indicator === "+") { - lineData.push({ raw: line, oldLineNum: null, newLineNum: newNum }); - newNum += 1; - continue; - } - - if (indicator === "-") { - lineData.push({ raw: line, oldLineNum: oldNum, newLineNum: null }); - oldNum += 1; - continue; - } - - lineData.push({ raw: line, oldLineNum: oldNum, newLineNum: newNum }); - oldNum += 1; - newNum += 1; - } - - return lineData; -} - -function buildRangeForSelectedLines(selectedLineData: QuickReviewLineData[]): string { - const oldLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.oldLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - const newLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.newLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - - const oldRange = formatRange(oldLineNumbers); - const newRange = formatRange(newLineNumbers); - - return [oldRange ? `-${oldRange}` : null, newRange ? `+${newRange}` : null] - .filter((part): part is string => part !== null) - .join(" "); -} - -function formatSelectedCode(selectedLineData: QuickReviewLineData[]): string { - const oldLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.oldLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - const newLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.newLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - - const oldWidth = Math.max(1, ...oldLineNumbers.map((lineNum) => String(lineNum).length)); - const newWidth = Math.max(1, ...newLineNumbers.map((lineNum) => String(lineNum).length)); - - const allLines = selectedLineData.map((lineInfo) => { - const indicator = lineInfo.raw[0] ?? " "; - const content = lineInfo.raw.slice(1); - const oldStr = lineInfo.oldLineNum === null ? "" : String(lineInfo.oldLineNum); - const newStr = lineInfo.newLineNum === null ? "" : String(lineInfo.newLineNum); - - return `${oldStr.padStart(oldWidth)} ${newStr.padStart(newWidth)} ${indicator} ${content}`; - }); - - if (allLines.length <= MAX_FULL_LINES) { - return allLines.join("\n"); - } - - const omittedCount = allLines.length - MAX_FULL_LINES; - return [ - ...allLines.slice(0, CONTEXT_LINES), - ` (${omittedCount} lines omitted)`, - ...allLines.slice(-CONTEXT_LINES), - ].join("\n"); -} - -/** - * Build a ReviewNoteData from a selected line range in a hunk. - * Mirrors ReviewNoteInput formatting in DiffRenderer for consistent payloads. - */ -export function buildQuickLineReviewNote(params: { - hunk: DiffHunk; - startIndex: number; - endIndex: number; - userNote: string; -}): ReviewNoteData { - const { hunk, startIndex, endIndex, userNote } = params; - const lineData = buildLineDataForHunk(hunk); - - if (lineData.length === 0) { - return buildQuickHunkReviewNote({ hunk, userNote }); - } - - const requestedStart = Math.min(startIndex, endIndex); - const requestedEnd = Math.max(startIndex, endIndex); - const clampedStart = Math.max(0, Math.min(requestedStart, lineData.length - 1)); - const clampedEnd = Math.max(clampedStart, Math.min(requestedEnd, lineData.length - 1)); - const selectedLineData = lineData.slice(clampedStart, clampedEnd + 1); - - const oldLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.oldLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - const newLineNumbers = selectedLineData - .map((lineInfo) => lineInfo.newLineNum) - .filter((lineNum): lineNum is number => lineNum !== null); - - return { - filePath: hunk.filePath, - lineRange: buildRangeForSelectedLines(selectedLineData), - selectedCode: formatSelectedCode(selectedLineData), - selectedDiff: selectedLineData.map((lineInfo) => lineInfo.raw).join("\n"), - oldStart: oldLineNumbers.length > 0 ? Math.min(...oldLineNumbers) : 1, - newStart: newLineNumbers.length > 0 ? Math.min(...newLineNumbers) : 1, - userNote, - }; -} - -/** - * Build a ReviewNoteData for the entire hunk with a prefilled user note. - * Used by the quick feedback actions in immersive review mode. - */ -export function buildQuickHunkReviewNote(params: { - hunk: DiffHunk; - userNote: string; -}): ReviewNoteData { - const { hunk, userNote } = params; - - const lines = hunk.content.split("\n").filter((line) => line.length > 0); - - // Compute line number ranges, omitting segments for pure additions/deletions - const oldRange = - hunk.oldLines > 0 ? `-${hunk.oldStart}-${hunk.oldStart + hunk.oldLines - 1}` : null; - const newRange = - hunk.newLines > 0 ? `+${hunk.newStart}-${hunk.newStart + hunk.newLines - 1}` : null; - const lineRange = [oldRange, newRange].filter(Boolean).join(" "); - - const oldEnd = hunk.oldLines > 0 ? hunk.oldStart + hunk.oldLines - 1 : hunk.oldStart; - const newEnd = hunk.newLines > 0 ? hunk.newStart + hunk.newLines - 1 : hunk.newStart; - - // Build selectedCode with line numbers (matching DiffRenderer format) - const oldWidth = Math.max(1, String(oldEnd).length); - const newWidth = Math.max(1, String(newEnd).length); - - let oldNum = hunk.oldStart; - let newNum = hunk.newStart; - const codeLines = lines.map((line) => { - const indicator = line[0] ?? " "; - const content = line.slice(1); - let oldStr = ""; - let newStr = ""; - - if (indicator === "+") { - newStr = String(newNum); - newNum++; - } else if (indicator === "-") { - oldStr = String(oldNum); - oldNum++; - } else { - oldStr = String(oldNum); - newStr = String(newNum); - oldNum++; - newNum++; - } - - return `${oldStr.padStart(oldWidth)} ${newStr.padStart(newWidth)} ${indicator} ${content}`; - }); - - // Elide middle lines if more than 20 - let selectedCode: string; - if (codeLines.length <= MAX_FULL_LINES) { - selectedCode = codeLines.join("\n"); - } else { - const omittedCount = codeLines.length - MAX_FULL_LINES; - selectedCode = [ - ...codeLines.slice(0, CONTEXT_LINES), - ` (${omittedCount} lines omitted)`, - ...codeLines.slice(-CONTEXT_LINES), - ].join("\n"); - } - - return { - filePath: hunk.filePath, - lineRange, - selectedCode, - selectedDiff: hunk.content, - oldStart: hunk.oldStart, - newStart: hunk.newStart, - userNote, - }; -} From bae4d31a4fca6b7305eba4caffc89929d13fe71a Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:22:41 +0000 Subject: [PATCH 7/9] refactor: un-export isBashOutputTool in messageUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the `export` keyword from `isBashOutputTool` in `src/browser/utils/messages/messageUtils.ts` — the type guard is only used within the same file by `computeBashOutputGroupInfos`, so the export was unnecessary. --- src/browser/utils/messages/messageUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/utils/messages/messageUtils.ts b/src/browser/utils/messages/messageUtils.ts index a32da685bd..fa44235ae2 100644 --- a/src/browser/utils/messages/messageUtils.ts +++ b/src/browser/utils/messages/messageUtils.ts @@ -32,7 +32,7 @@ export function getEditableUserMessageText( /** * Type guard to check if a message is a bash_output tool call with valid args */ -export function isBashOutputTool( +function isBashOutputTool( msg: DisplayedMessage ): msg is DisplayedMessage & { type: "tool"; toolName: "bash_output"; args: BashOutputToolArgs } { if (msg.type !== "tool" || msg.toolName !== "bash_output") { From 02764cf931a4204eb9a3365d0125205e801c82ce Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:36:25 +0000 Subject: [PATCH 8/9] refactor: simplify hasCompletedDescendants to reuse listCompletedDescendantAgentTaskIds The method duplicated the DFS tree walk that listCompletedDescendantAgentTaskIds already performs. Delegate to the existing helper instead of reimplementing the traversal inline. --- src/node/services/taskService.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/node/services/taskService.ts b/src/node/services/taskService.ts index 9d8607ca79..38d9a36fc0 100644 --- a/src/node/services/taskService.ts +++ b/src/node/services/taskService.ts @@ -2204,19 +2204,7 @@ export class TaskService { const cfg = this.config.loadConfigOrDefault(); const index = this.buildAgentTaskIndex(cfg); - const stack: string[] = [...(index.childrenByParent.get(workspaceId) ?? [])]; - while (stack.length > 0) { - const next = stack.pop()!; - const entry = index.byId.get(next); - if (entry && hasCompletedAgentReport(entry)) { - return true; - } - const children = index.childrenByParent.get(next); - if (children) { - stack.push(...children); - } - } - return false; + return this.listCompletedDescendantAgentTaskIds(index, workspaceId).length > 0; } listActiveDescendantAgentTaskIds(workspaceId: string): string[] { From 504c4b97c39aa53964fef8c19cf51c2d316d45c5 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:19:52 +0000 Subject: [PATCH 9/9] refactor: reuse anthropicSupportsNativeXhigh in Anthropic fetch wrapper Replace the duplicated Opus 4.7+ regex inside wrapFetchWithAnthropicCacheControl with a call to the existing anthropicSupportsNativeXhigh helper from src/common/types/thinking.ts. The helper already performs the same regex check plus provider-prefix normalization (e.g., anthropic/claude-opus-4-7 via the ai-model-id gateway header), keeping the wire-level detection and the policy-level detection in one place. --- src/node/services/providerModelFactory.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index d03d218bf9..abdad0c76e 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -2,7 +2,7 @@ import assert from "node:assert"; import type { XaiProviderOptions } from "@ai-sdk/xai"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import { wrapLanguageModel, type LanguageModel } from "ai"; -import type { ThinkingLevel } from "@/common/types/thinking"; +import { anthropicSupportsNativeXhigh, type ThinkingLevel } from "@/common/types/thinking"; import { Ok, Err } from "@/common/types/result"; import type { Result } from "@/common/types/result"; import type { SendMessageError } from "@/common/types/errors"; @@ -324,9 +324,10 @@ export function wrapFetchWithAnthropicCacheControl( // with the model exposed via the ai-model-id header. const directModel = typeof json.model === "string" ? json.model : ""; const headerModelId = incomingHeaders.get("ai-model-id") ?? ""; - const targetsOpus47OrNewer = [directModel, headerModelId].some((candidate) => - /claude-opus-(?:4-(?:[7-9]|\d{2,})|[5-9]|\d{2,})/i.test(candidate) - ); + // Reuse the shared Opus 4.7+ detector so the wire-level regex stays in + // one place (src/common/types/thinking.ts) — it also normalizes provider + // prefixes (e.g., `anthropic/claude-opus-4-7`). + const targetsOpus47OrNewer = [directModel, headerModelId].some(anthropicSupportsNativeXhigh); const directThinking = isRecord(json.thinking) ? json.thinking : undefined; const providerOpts = isRecord(json.providerOptions) ? json.providerOptions : undefined;