From 03bcf8d66c5e723eadf22f1b678bceb76bb0c978 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:18:42 +0000 Subject: [PATCH 01/15] refactor: extract buildUntrackedConfirmationErr helper in archive snapshot service Deduplicate two identical 14-line blocks in captureSnapshotForArchive that fetch latest untracked paths and return confirmation errors. Both code paths (new-untracked-after-acknowledgement and first-time-untracked) now call the same private helper method. Behavior-preserving: no logic change, same call sequence and return values. --- .../worktreeArchiveSnapshotService.ts | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/node/services/worktreeArchiveSnapshotService.ts b/src/node/services/worktreeArchiveSnapshotService.ts index 2000a53b06..ccfcdbaa7d 100644 --- a/src/node/services/worktreeArchiveSnapshotService.ts +++ b/src/node/services/worktreeArchiveSnapshotService.ts @@ -291,38 +291,16 @@ export class WorktreeArchiveSnapshotService { const acknowledgedSet = new Set(args.acknowledgedUntrackedPaths); const newPaths = currentUntracked.filter((p) => !acknowledgedSet.has(p)); if (newPaths.length > 0) { - const latestUntrackedResult = await this.getUnsupportedUntrackedPaths({ + return this.buildUntrackedConfirmationErr({ workspaceId: args.workspaceId, workspaceMetadata: args.workspaceMetadata, }); - if (!latestUntrackedResult.success) { - return Err(latestUntrackedResult.error); - } - assert( - latestUntrackedResult.data.length > 0, - "captureSnapshotForArchive: expected current untracked paths when confirmation is required" - ); - return Err({ - kind: "confirm-lossy-untracked-files", - paths: latestUntrackedResult.data, - }); } } else if (currentUntracked.length > 0) { - const latestUntrackedResult = await this.getUnsupportedUntrackedPaths({ + return this.buildUntrackedConfirmationErr({ workspaceId: args.workspaceId, workspaceMetadata: args.workspaceMetadata, }); - if (!latestUntrackedResult.success) { - return Err(latestUntrackedResult.error); - } - assert( - latestUntrackedResult.data.length > 0, - "captureSnapshotForArchive: expected current untracked paths when confirmation is required" - ); - return Err({ - kind: "confirm-lossy-untracked-files", - paths: latestUntrackedResult.data, - }); } await this.ensureNoDirtySubmodules(projectRepo.repoCwd); @@ -680,6 +658,32 @@ export class WorktreeArchiveSnapshotService { return detectDefaultTrunkBranch(args.projectPath); } + /** + * Fetch the current untracked-file set for a workspace and return an + * `Err` asking the user to re-confirm. Used by `captureSnapshotForArchive` + * when untracked files are detected that the user hasn't acknowledged. + */ + private async buildUntrackedConfirmationErr(args: { + workspaceId: string; + workspaceMetadata: WorkspaceMetadata; + }): Promise> { + const latestUntrackedResult = await this.getUnsupportedUntrackedPaths({ + workspaceId: args.workspaceId, + workspaceMetadata: args.workspaceMetadata, + }); + if (!latestUntrackedResult.success) { + return Err(latestUntrackedResult.error); + } + assert( + latestUntrackedResult.data.length > 0, + "captureSnapshotForArchive: expected current untracked paths when confirmation is required" + ); + return Err({ + kind: "confirm-lossy-untracked-files", + paths: latestUntrackedResult.data, + }); + } + /** * List untracked files/directories in a repo that archive snapshots cannot preserve. * Returns a sorted, normalized array of relative paths. From 12cc48e1c46887b49e1f1373dccfeb899a7e3ef2 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:26:39 +0000 Subject: [PATCH 02/15] refactor: combine duplicate workspace type imports in heartbeatService Merged two separate `import type` statements from the same `@/common/types/workspace` module into a single import line, consistent with how other files in the codebase handle multiple type imports from the same module. --- src/node/services/heartbeatService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/services/heartbeatService.ts b/src/node/services/heartbeatService.ts index 17a55c9187..57dbcf0c57 100644 --- a/src/node/services/heartbeatService.ts +++ b/src/node/services/heartbeatService.ts @@ -1,8 +1,7 @@ import assert from "@/common/utils/assert"; import type { MuxMessage } from "@/common/types/message"; import type { ProjectsConfig, Workspace } from "@/common/types/project"; -import type { WorkspaceActivitySnapshot } from "@/common/types/workspace"; -import type { WorkspaceMetadata } from "@/common/types/workspace"; +import type { WorkspaceActivitySnapshot, WorkspaceMetadata } from "@/common/types/workspace"; import { isWorkspaceArchived } from "@/common/utils/archive"; import { HEARTBEAT_DEFAULT_INTERVAL_MS, From e0993b80645c11b22efbf78a2ecdcff34c59962f Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:28:42 +0000 Subject: [PATCH 03/15] refactor: deduplicate heartbeat settings object construction Both getHeartbeatSettings and setHeartbeatSettings had nearly identical object literals duplicated across two ternary branches, differing only by the optional message field. Use conditional spread to build each object once, removing the duplicated comment in setHeartbeatSettings. --- src/node/services/workspaceService.ts | 40 +++++++++------------------ 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 98df58ecf3..6694a18189 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -3292,18 +3292,12 @@ export class WorkspaceService extends EventEmitter { const message = sanitizeHeartbeatMessage(workspaceEntry.heartbeat.message); const contextMode = sanitizeHeartbeatContextMode(workspaceEntry.heartbeat.contextMode); - return message == null - ? { - enabled: workspaceEntry.heartbeat.enabled, - intervalMs: workspaceEntry.heartbeat.intervalMs, - contextMode, - } - : { - enabled: workspaceEntry.heartbeat.enabled, - intervalMs: workspaceEntry.heartbeat.intervalMs, - message, - contextMode, - }; + return { + enabled: workspaceEntry.heartbeat.enabled, + intervalMs: workspaceEntry.heartbeat.intervalMs, + contextMode, + ...(message != null ? { message } : {}), + }; } async setHeartbeatSettings( @@ -3361,21 +3355,13 @@ export class WorkspaceService extends EventEmitter { const nextContextMode = hasContextModeUpdate ? sanitizeHeartbeatContextMode(settings.contextMode) : sanitizeHeartbeatContextMode(workspaceEntry.heartbeat?.contextMode); - const nextSettings: WorkspaceHeartbeatSettings = - nextMessage == null - ? { - enabled: settings.enabled, - // Keep the interval on disk even when disabled so re-enabling restores the user's choice. - intervalMs: settings.intervalMs, - contextMode: nextContextMode, - } - : { - enabled: settings.enabled, - // Keep the interval on disk even when disabled so re-enabling restores the user's choice. - intervalMs: settings.intervalMs, - message: nextMessage, - contextMode: nextContextMode, - }; + // Keep the interval on disk even when disabled so re-enabling restores the user's choice. + const nextSettings: WorkspaceHeartbeatSettings = { + enabled: settings.enabled, + intervalMs: settings.intervalMs, + contextMode: nextContextMode, + ...(nextMessage != null ? { message: nextMessage } : {}), + }; const changed = workspaceEntry.heartbeat?.enabled !== nextSettings.enabled || From 2614073bd1ae517667fc93db2136639b20b8f162 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:23:49 +0000 Subject: [PATCH 04/15] refactor: remove redundant normalizeHeartbeatMessageInput After the HEARTBEAT_MAX_MESSAGE_LENGTH removal (5f002d8a), normalizeHeartbeatMessageInput became functionally identical to sanitizeHeartbeatMessage for its string | undefined input type. The only distinguishing logic was the max-length assert which no longer exists. Consolidate both callsites to use the single sanitizeHeartbeatMessage function, which safely handles both user input and persisted config values. --- src/node/services/workspaceService.ts | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 6694a18189..d859dbe856 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -211,22 +211,8 @@ type WorktreeArchiveSnapshotLifecycleService = Pick< | "restoreSnapshotAfterUnarchive" | "getUnsupportedUntrackedPaths" >; -function normalizeHeartbeatMessageInput(message: string | undefined): string | undefined { - if (message == null) { - return undefined; - } - - assert(typeof message === "string", "Heartbeat message must be a string when provided"); - const trimmedMessage = message.trim(); - if (trimmedMessage.length === 0) { - return undefined; - } - - return trimmedMessage; -} - -// Persisted workspace config can contain non-string or whitespace-only values; normalize the -// message on read so an invalid override never bricks heartbeat execution. +// Trim and normalize a heartbeat message for storage. Accepts `unknown` so it safely handles +// both user input (string | undefined) and persisted config values that may have been corrupted. function sanitizeHeartbeatMessage(message: unknown): string | undefined { if (typeof message !== "string") { return undefined; @@ -3350,7 +3336,7 @@ export class WorkspaceService extends EventEmitter { } const nextMessage = hasMessageUpdate - ? normalizeHeartbeatMessageInput(settings.message) + ? sanitizeHeartbeatMessage(settings.message) : sanitizeHeartbeatMessage(workspaceEntry.heartbeat?.message); const nextContextMode = hasContextModeUpdate ? sanitizeHeartbeatContextMode(settings.contextMode) From 3eb6851c9c5467fadd0e1b3ed951405725d45297 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:22:50 +0000 Subject: [PATCH 05/15] refactor: remove dead AddSectionButton component The AddSectionButton component was removed from ProjectSidebar's imports in #3099 but the component file and its test mock were left behind. Remove both since nothing references them anymore. --- .../AddSectionButton/AddSectionButton.tsx | 63 ------------------- .../ProjectSidebar/ProjectSidebar.test.tsx | 4 -- 2 files changed, 67 deletions(-) delete mode 100644 src/browser/components/AddSectionButton/AddSectionButton.tsx diff --git a/src/browser/components/AddSectionButton/AddSectionButton.tsx b/src/browser/components/AddSectionButton/AddSectionButton.tsx deleted file mode 100644 index beff820652..0000000000 --- a/src/browser/components/AddSectionButton/AddSectionButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState, useRef, useEffect } from "react"; -import { Plus } from "lucide-react"; -// import { Tooltip, TooltipTrigger, TooltipContent } from "../Tooltip/Tooltip"; - -interface AddSectionButtonProps { - onCreateSection: (name: string) => void; -} - -export const AddSectionButton: React.FC = ({ onCreateSection }) => { - const [isCreating, setIsCreating] = useState(false); - const [name, setName] = useState(""); - const inputRef = useRef(null); - - useEffect(() => { - if (isCreating && inputRef.current) { - inputRef.current.focus(); - } - }, [isCreating]); - - const handleSubmit = () => { - const trimmed = name.trim(); - if (trimmed) { - onCreateSection(trimmed); - } - setName(""); - setIsCreating(false); - }; - - if (isCreating) { - return ( -
- setName(e.target.value)} - onBlur={handleSubmit} - onKeyDown={(e) => { - if (e.key === "Enter") handleSubmit(); - if (e.key === "Escape") { - setName(""); - setIsCreating(false); - } - }} - placeholder="Section name..." - data-testid="add-section-input" - className="bg-background/50 text-foreground ml-6 min-w-0 flex-1 rounded border border-white/20 px-1.5 py-0.5 text-[11px] outline-none select-text" - /> -
- ); - } - - return ( - - ); -}; diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx index e0847ac919..c774b168e0 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx @@ -33,7 +33,6 @@ import * as ProjectDeleteConfirmationModalModule from "../ProjectDeleteConfirmat import * as WorkspaceStatusIndicatorModule from "../WorkspaceStatusIndicator/WorkspaceStatusIndicator"; import * as PopoverErrorModule from "../PopoverError/PopoverError"; import * as SectionHeaderModule from "../SectionHeader/SectionHeader"; -import * as AddSectionButtonModule from "../AddSectionButton/AddSectionButton"; import * as WorkspaceSectionDropZoneModule from "../WorkspaceSectionDropZone/WorkspaceSectionDropZone"; import * as WorkspaceDragLayerModule from "../WorkspaceDragLayer/WorkspaceDragLayer"; import * as SectionDragLayerModule from "../SectionDragLayer/SectionDragLayer"; @@ -488,9 +487,6 @@ function installProjectSidebarTestDoubles() { spyOn(SectionHeaderModule, "SectionHeader").mockImplementation( (() => null) as unknown as typeof SectionHeaderModule.SectionHeader ); - spyOn(AddSectionButtonModule, "AddSectionButton").mockImplementation( - (() => null) as unknown as typeof AddSectionButtonModule.AddSectionButton - ); spyOn(WorkspaceSectionDropZoneModule, "WorkspaceSectionDropZone").mockImplementation( TestWrapper as unknown as typeof WorkspaceSectionDropZoneModule.WorkspaceSectionDropZone ); From 108628334508cd96e706afa1931d92151c9b9046 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:14:16 +0000 Subject: [PATCH 06/15] refactor: remove dead ackPendingDiffsConsumed/discardPendingDiffs wrappers After the compaction handler refactor (710bda55), the production callers were migrated to use the new *State methods directly. The old *Diffs wrappers had zero remaining callers: - discardPendingDiffs: no callers in production code or tests - ackPendingDiffsConsumed: one caller in agentSession.ts, now updated to call ackPendingStateConsumed directly peekPendingDiffs is retained as it's still used by tests as a convenience accessor. --- src/node/services/agentSession.ts | 2 +- src/node/services/compactionHandler.ts | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/node/services/agentSession.ts b/src/node/services/agentSession.ts index 0f5bbcacc4..7eefd50adf 100644 --- a/src/node/services/agentSession.ts +++ b/src/node/services/agentSession.ts @@ -4021,7 +4021,7 @@ export class AgentSession { if (this.ackPendingPostCompactionStateOnStreamEnd) { this.ackPendingPostCompactionStateOnStreamEnd = false; try { - await this.compactionHandler.ackPendingDiffsConsumed(); + await this.compactionHandler.ackPendingStateConsumed(); } catch (error) { log.warn("Failed to ack pending post-compaction state", { workspaceId: this.workspaceId, diff --git a/src/node/services/compactionHandler.ts b/src/node/services/compactionHandler.ts index c82dd4861b..4dfea45df4 100644 --- a/src/node/services/compactionHandler.ts +++ b/src/node/services/compactionHandler.ts @@ -462,10 +462,6 @@ export class CompactionHandler { await this.deletePersistedPendingStateBestEffort(); } - async ackPendingDiffsConsumed(): Promise { - await this.ackPendingStateConsumed(); - } - /** * Drop pending post-compaction state (e.g., because it caused context_exceeded). */ @@ -490,10 +486,6 @@ export class CompactionHandler { this.cachedLoadedSkills = []; } - async discardPendingDiffs(reason: string): Promise { - await this.discardPendingState(reason); - } - private async deletePersistedPendingStateBestEffort(): Promise { try { await fsPromises.unlink(this.postCompactionStatePath); From 359a4be2b2c9d6f3046a8bc34582fc33ed7517f2 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:13:32 +0000 Subject: [PATCH 07/15] refactor: remove dead --color-scrollbar-* CSS variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3111 migrated scrollbar styling to use --color-surface-quaternary and standard scrollbar-color, but left the old --color-scrollbar-track, --color-scrollbar-thumb, and --color-scrollbar-thumb-hover variable definitions in all four themes. These variables are no longer referenced anywhere — remove the 12 dead lines. --- src/browser/styles/globals.css | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index 6f6d91bf80..98816a1c95 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -233,11 +233,6 @@ --color-input-border: hsl(207 51% 59%); --color-input-border-focus: hsl(193 91% 64%); - /* Scrollbar */ - --color-scrollbar-track: hsl(0 0% 18%); - --color-scrollbar-thumb: hsl(0 0% 32%); - --color-scrollbar-thumb-hover: hsl(0 0% 42%); - /* Additional Semantic Colors */ --color-muted: hsl(0 0% 53%); /* #888 - muted text */ --color-muted-light: hsl(0 0% 50%); /* #808080 - muted light */ @@ -515,10 +510,6 @@ --color-input-border: hsl(207 75% 52%); --color-input-border-focus: hsl(193 85% 56%); - --color-scrollbar-track: hsl(210 38% 95%); - --color-scrollbar-thumb: hsl(210 18% 78%); - --color-scrollbar-thumb-hover: hsl(210 18% 70%); - --color-muted: hsl(210 14% 52%); --color-muted-light: hsl(210 20% 60%); --color-muted-dark: hsl(210 12% 42%); @@ -764,10 +755,6 @@ --color-input-border: #205ea6; --color-input-border-focus: color-mix(in srgb, var(--color-input-border), white 30%); - --color-scrollbar-track: #f2f0e5; - --color-scrollbar-thumb: #dad8ce; - --color-scrollbar-thumb-hover: #cecdc3; - --color-muted: #6f6e69; --color-muted-light: #6f6e69; --color-muted-dark: #6f6e69; @@ -997,10 +984,6 @@ --color-input-border: #4385be; --color-input-border-focus: color-mix(in srgb, var(--color-input-border), white 22%); - --color-scrollbar-track: #1c1b1a; - --color-scrollbar-thumb: #343331; - --color-scrollbar-thumb-hover: #403e3c; - --color-muted: #878580; --color-muted-light: #cecdc3; --color-muted-dark: #575653; From 7af5640154e5d67562acfae1d9e8b6bb4fadb610 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:13:34 +0000 Subject: [PATCH 08/15] refactor: deduplicate isPositiveInteger/isNonNegativeInteger into shared utility Extract identical number type guard functions that were duplicated across compactionBoundary.ts, compactionHandler.ts, workspaceService.ts, and historyService.ts into a shared src/common/utils/numbers.ts module. --- src/common/utils/messages/compactionBoundary.ts | 7 +------ src/common/utils/numbers.ts | 16 ++++++++++++++++ src/node/services/compactionHandler.ts | 13 +------------ src/node/services/historyService.ts | 13 +------------ src/node/services/workspaceService.ts | 13 +------------ 5 files changed, 20 insertions(+), 42 deletions(-) create mode 100644 src/common/utils/numbers.ts diff --git a/src/common/utils/messages/compactionBoundary.ts b/src/common/utils/messages/compactionBoundary.ts index bab5a9b50e..8c218932d6 100644 --- a/src/common/utils/messages/compactionBoundary.ts +++ b/src/common/utils/messages/compactionBoundary.ts @@ -1,13 +1,8 @@ import assert from "@/common/utils/assert"; +import { isPositiveInteger } from "@/common/utils/numbers"; import type { MuxMessage } from "@/common/types/message"; -function isPositiveInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 - ); -} - export function isDurableCompactedMarker( value: unknown ): value is true | "user" | "idle" | "heartbeat" { diff --git a/src/common/utils/numbers.ts b/src/common/utils/numbers.ts new file mode 100644 index 0000000000..1749b5efe4 --- /dev/null +++ b/src/common/utils/numbers.ts @@ -0,0 +1,16 @@ +/** + * Number type guards used across compaction, history, and workspace services + * to validate persisted metadata fields. + */ + +export function isPositiveInteger(value: unknown): value is number { + return ( + typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 + ); +} + +export function isNonNegativeInteger(value: unknown): value is number { + return ( + typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 + ); +} diff --git a/src/node/services/compactionHandler.ts b/src/node/services/compactionHandler.ts index 4dfea45df4..de7900a063 100644 --- a/src/node/services/compactionHandler.ts +++ b/src/node/services/compactionHandler.ts @@ -1,6 +1,7 @@ import type { EventEmitter } from "events"; import * as fsPromises from "fs/promises"; import assert from "@/common/utils/assert"; +import { isNonNegativeInteger, isPositiveInteger } from "@/common/utils/numbers"; import * as path from "path"; import type { HistoryService } from "./historyService"; @@ -247,18 +248,6 @@ function isCompactedSummaryMessage(message: MuxMessage): boolean { return isDurableCompactedMarker(message.metadata?.compacted); } -function isPositiveInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 - ); -} - -function isNonNegativeInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 - ); -} - function getNextCompactionEpoch(messages: MuxMessage[]): number { let epochCursor = 0; diff --git a/src/node/services/historyService.ts b/src/node/services/historyService.ts index 00fa05db99..c50db80015 100644 --- a/src/node/services/historyService.ts +++ b/src/node/services/historyService.ts @@ -22,18 +22,7 @@ import { isDurableCompactionBoundaryMarker, } from "@/common/utils/messages/compactionBoundary"; import { getErrorMessage } from "@/common/utils/errors"; - -function isPositiveInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 - ); -} - -function isNonNegativeInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 - ); -} +import { isNonNegativeInteger, isPositiveInteger } from "@/common/utils/numbers"; function hasDurableCompactionBoundary(metadata: MuxMetadata | undefined): boolean { if (metadata?.compactionBoundary !== true) { diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index d859dbe856..4b284e3786 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -54,6 +54,7 @@ import { shellQuote } from "@/node/runtime/backgroundCommands"; import { extractEditedFilePaths } from "@/common/utils/messages/extractEditedFiles"; import { buildCompactionMessageText } from "@/common/utils/compaction/compactionPrompt"; import { isDurableCompactedMarker } from "@/common/utils/messages/compactionBoundary"; +import { isNonNegativeInteger, isPositiveInteger } from "@/common/utils/numbers"; import { deriveTodoStatus } from "@/common/utils/todoList"; import { fileExists } from "@/node/utils/runtime/fileExists"; import { orchestrateFork } from "@/node/services/utils/forkOrchestrator"; @@ -549,18 +550,6 @@ async function resetForkedSessionUsage( ); } -function isPositiveInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0 - ); -} - -function isNonNegativeInteger(value: unknown): value is number { - return ( - typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 - ); -} - function getOldestSequencedMessage( messages: readonly MuxMessage[] ): { message: MuxMessage; historySequence: number } | null { From 429c3b45d1aa880984bd462d86004e305faa5b33 Mon Sep 17 00:00:00 2001 From: "mux-bot[bot]" <264182336+mux-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:13:48 +0000 Subject: [PATCH 09/15] refactor: extract PROJECT_TOGGLE_BUTTON_CLASSES constant in ProjectSidebar Deduplicate the identical className string used by both the multi-project section toggle button and per-project expand/collapse buttons into a shared constant, following the existing pattern of PROJECT_ITEM_BASE_CLASS. --- src/browser/components/ProjectSidebar/ProjectSidebar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.tsx index f89e4bd8e5..f78cb7d3ee 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.tsx @@ -250,6 +250,10 @@ function useWorkspaceAttentionSubscription( const PROJECT_ITEM_BASE_CLASS = "group sticky top-0 z-30 py-2 pl-2 pr-1 flex select-none items-center border-l-transparent bg-surface-primary transition-colors duration-150"; +// Shared classes for the chevron toggle buttons on project/section headers. +const PROJECT_TOGGLE_BUTTON_CLASSES = + "text-secondary hover:bg-hover hover:border-border-light mr-1.5 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded border border-transparent bg-transparent p-0 transition-all duration-200"; + function getProjectFallbackLabel(projectPath: string): string { const abbreviatedPath = PlatformPaths.abbreviate(projectPath); const { basename } = PlatformPaths.splitAbbreviated(abbreviatedPath); @@ -1696,7 +1700,7 @@ const ProjectSidebarInner: React.FC = ({