Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d67db67
refactor: extract buildUntrackedConfirmationErr helper in archive sna…
mux-bot[bot] Mar 31, 2026
46eabe0
refactor: combine duplicate workspace type imports in heartbeatService
mux-bot[bot] Mar 31, 2026
1877dcf
refactor: deduplicate heartbeat settings object construction
mux-bot[bot] Apr 1, 2026
02b9c32
refactor: remove redundant normalizeHeartbeatMessageInput
mux-bot[bot] Apr 1, 2026
fa4c061
refactor: remove dead AddSectionButton component
mux-bot[bot] Apr 2, 2026
20fa4f4
refactor: remove dead ackPendingDiffsConsumed/discardPendingDiffs wra…
mux-bot[bot] Apr 2, 2026
26b756c
refactor: remove dead --color-scrollbar-* CSS variables
mux-bot[bot] Apr 3, 2026
5bceaef
refactor: deduplicate isPositiveInteger/isNonNegativeInteger into sha…
mux-bot[bot] Apr 3, 2026
4c5c145
refactor: extract PROJECT_TOGGLE_BUTTON_CLASSES constant in ProjectSi…
mux-bot[bot] Apr 4, 2026
defad7a
refactor: remove tombstone comment and dead `relative` class from wor…
mux-bot[bot] Apr 5, 2026
e585401
refactor: remove redundant inputWorkspaces alias in mock oRPC client
mux-bot[bot] Apr 6, 2026
2bb939d
refactor: remove redundant expandHookPath import alias in SSHRuntime
mux-bot[bot] Apr 7, 2026
48793f6
refactor: remove dead isCopilotRoutableModel guard from gatewayModelC…
mux-bot[bot] Apr 7, 2026
96ae7d4
refactor: extract shared heartbeat interval-minute helpers
mux-bot[bot] Apr 7, 2026
d0490b6
refactor: remove stale LandingPage entry from storybook migration list
mux-bot[bot] Apr 7, 2026
d2a7e42
refactor: deduplicate sleepWithAbort into shared abort utility
mux-bot[bot] Apr 8, 2026
48a4fff
refactor: remove dead WRITE_DENIED_PREFIX constant
mux-bot[bot] Apr 9, 2026
61014c4
refactor: remove dead computeBashOutputGroupInfo export from messageU…
mux-bot[bot] Apr 9, 2026
eaadfb6
refactor: extract errorMessageText helper in SSH2ConnectionPool
mux-bot[bot] Apr 10, 2026
cc15417
refactor: deduplicate streamToString into shared streamUtils
mux-bot[bot] Apr 10, 2026
829ae30
refactor: remove redundant advisor variable aliases in aiService
mux-bot[bot] Apr 11, 2026
d1eda1a
refactor: remove dead rendererClient tokenizer module
mux-bot[bot] Apr 12, 2026
5b3a81b
refactor: use isAdvisorPhaseEvent type guard in WorkspaceStore
mux-bot[bot] Apr 12, 2026
67bb003
refactor: remove dead safeClone JSON fallback
mux-bot[bot] Apr 12, 2026
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
63 changes: 0 additions & 63 deletions src/browser/components/AddSectionButton/AddSectionButton.tsx

This file was deleted.

4 changes: 0 additions & 4 deletions src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
);
Expand Down
13 changes: 7 additions & 6 deletions src/browser/components/ProjectSidebar/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1696,7 +1700,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
<button
onClick={() => toggleProject(MULTI_PROJECT_SIDEBAR_SECTION_ID)}
aria-label={`${isMultiProjectSectionExpanded ? "Collapse" : "Expand"} multi-project workspaces`}
className="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"
className={PROJECT_TOGGLE_BUTTON_CLASSES}
>
<span className="relative flex h-4 w-4 items-center justify-center">
<ChevronRight
Expand Down Expand Up @@ -1839,7 +1843,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
}}
aria-label={`${isExpanded ? "Collapse" : "Expand"} project ${projectName}`}
data-project-path={projectPath}
className="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"
className={PROJECT_TOGGLE_BUTTON_CLASSES}
>
<span className="relative flex h-4 w-4 items-center justify-center">
<ChevronRight
Expand Down Expand Up @@ -1983,11 +1987,8 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
id={workspaceListId}
role="region"
aria-label={`Workspaces for ${projectName}`}
className="relative pt-1"
className="pt-1"
>
{/* Vertical connector line removed — workspace status dots now
align directly with the project folder icon, so the tree
connector is no longer needed. */}
{(() => {
// Archived workspaces are excluded from workspaceMetadata so won't appear here

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,22 @@ import { Input } from "@/browser/components/Input/Input";
import { Switch } from "@/browser/components/Switch/Switch";
import { useWorkspaceHeartbeat } from "@/browser/hooks/useWorkspaceHeartbeat";
import assert from "@/common/utils/assert";
import {
clampIntervalMinutes,
formatIntervalMinutes,
HEARTBEAT_DEFAULT_INTERVAL_MINUTES,
HEARTBEAT_MAX_INTERVAL_MINUTES,
HEARTBEAT_MIN_INTERVAL_MINUTES,
intervalMinutesToMs,
parseIntervalMinutes,
} from "@/browser/utils/heartbeatIntervalMinutes";
import {
HEARTBEAT_DEFAULT_CONTEXT_MODE,
HEARTBEAT_DEFAULT_INTERVAL_MS,
HEARTBEAT_DEFAULT_MESSAGE_BODY,
HEARTBEAT_MAX_INTERVAL_MS,
HEARTBEAT_MIN_INTERVAL_MS,
type HeartbeatContextMode,
} from "@/constants/heartbeat";

const MS_PER_MINUTE = 60_000;
const HEARTBEAT_MIN_INTERVAL_MINUTES = HEARTBEAT_MIN_INTERVAL_MS / MS_PER_MINUTE;
const HEARTBEAT_MAX_INTERVAL_MINUTES = HEARTBEAT_MAX_INTERVAL_MS / MS_PER_MINUTE;
const HEARTBEAT_DEFAULT_INTERVAL_MINUTES = HEARTBEAT_DEFAULT_INTERVAL_MS / MS_PER_MINUTE;

assert(
Number.isInteger(HEARTBEAT_MIN_INTERVAL_MINUTES),
"Workspace heartbeat minimum interval must be a whole number of minutes"
);
assert(
Number.isInteger(HEARTBEAT_MAX_INTERVAL_MINUTES),
"Workspace heartbeat maximum interval must be a whole number of minutes"
);
assert(
Number.isInteger(HEARTBEAT_DEFAULT_INTERVAL_MINUTES),
"Workspace heartbeat default interval must be a whole number of minutes"
);

const HEARTBEAT_CONTEXT_MODE_OPTIONS: Array<{
value: HeartbeatContextMode;
label: string;
Expand Down Expand Up @@ -74,33 +63,6 @@ interface WorkspaceHeartbeatModalProps {
onOpenChange: (open: boolean) => void;
}

function formatIntervalMinutes(intervalMs: number): string {
if (!Number.isFinite(intervalMs)) {
return String(HEARTBEAT_DEFAULT_INTERVAL_MINUTES);
}

const roundedMinutes = Math.round(intervalMs / MS_PER_MINUTE);
return String(clampIntervalMinutes(roundedMinutes));
}

function parseIntervalMinutes(value: string): number | null {
const trimmedValue = value.trim();
if (trimmedValue.length === 0 || !/^\d+$/.test(trimmedValue)) {
return null;
}

const minutes = Number.parseInt(trimmedValue, 10);
return Number.isInteger(minutes) ? minutes : null;
}

function clampIntervalMinutes(minutes: number): number {
assert(Number.isInteger(minutes), "Workspace heartbeat minutes must be a whole number");
return Math.min(
HEARTBEAT_MAX_INTERVAL_MINUTES,
Math.max(HEARTBEAT_MIN_INTERVAL_MINUTES, minutes)
);
}

function getValidationErrorMessage(value: string): string | null {
const minutes = parseIntervalMinutes(value);
if (minutes == null) {
Expand Down Expand Up @@ -221,7 +183,7 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {

const didSave = await save({
enabled: draftEnabled,
intervalMs: parsedMinutes * MS_PER_MINUTE,
intervalMs: intervalMinutesToMs(parsedMinutes),
contextMode: draftContextMode,
// Read directly from the textarea on save so the final keystroke is preserved even if the
// click lands before React finishes flushing the last state update.
Expand Down
58 changes: 9 additions & 49 deletions src/browser/features/Settings/Sections/HeartbeatSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,19 @@ import React, { useCallback, useEffect, useRef, useState } from "react";

import { Input } from "@/browser/components/Input/Input";
import { useAPI } from "@/browser/contexts/API";
import assert from "@/common/utils/assert";
import {
clampIntervalMinutes,
formatIntervalMinutes,
HEARTBEAT_MAX_INTERVAL_MINUTES,
HEARTBEAT_MIN_INTERVAL_MINUTES,
intervalMinutesToMs,
parseIntervalMinutes,
} from "@/browser/utils/heartbeatIntervalMinutes";
import {
HEARTBEAT_DEFAULT_INTERVAL_MS,
HEARTBEAT_DEFAULT_MESSAGE_BODY,
HEARTBEAT_MAX_INTERVAL_MS,
HEARTBEAT_MIN_INTERVAL_MS,
} from "@/constants/heartbeat";

const MS_PER_MINUTE = 60_000;
const HEARTBEAT_MIN_INTERVAL_MINUTES = HEARTBEAT_MIN_INTERVAL_MS / MS_PER_MINUTE;
const HEARTBEAT_MAX_INTERVAL_MINUTES = HEARTBEAT_MAX_INTERVAL_MS / MS_PER_MINUTE;
const HEARTBEAT_DEFAULT_INTERVAL_MINUTES = HEARTBEAT_DEFAULT_INTERVAL_MS / MS_PER_MINUTE;

assert(
Number.isInteger(HEARTBEAT_MIN_INTERVAL_MINUTES),
"Heartbeat minimum interval must be a whole number of minutes"
);
assert(
Number.isInteger(HEARTBEAT_MAX_INTERVAL_MINUTES),
"Heartbeat maximum interval must be a whole number of minutes"
);
assert(
Number.isInteger(HEARTBEAT_DEFAULT_INTERVAL_MINUTES),
"Heartbeat default interval must be a whole number of minutes"
);

function formatIntervalMinutes(intervalMs: number | undefined): string {
if (intervalMs == null || !Number.isFinite(intervalMs)) {
return String(HEARTBEAT_DEFAULT_INTERVAL_MINUTES);
}

const roundedMinutes = Math.round(intervalMs / MS_PER_MINUTE);
return String(clampIntervalMinutes(roundedMinutes));
}

function parseIntervalMinutes(value: string): number | null {
const trimmedValue = value.trim();
if (trimmedValue.length === 0 || !/^\d+$/.test(trimmedValue)) {
return null;
}

const minutes = Number.parseInt(trimmedValue, 10);
return Number.isInteger(minutes) ? minutes : null;
}

function clampIntervalMinutes(minutes: number): number {
assert(Number.isInteger(minutes), "Heartbeat minutes must be a whole number");
return Math.min(
HEARTBEAT_MAX_INTERVAL_MINUTES,
Math.max(HEARTBEAT_MIN_INTERVAL_MINUTES, minutes)
);
}

export function HeartbeatSection() {
const { api } = useAPI();
const [heartbeatDefaultPrompt, setHeartbeatDefaultPrompt] = useState("");
Expand Down Expand Up @@ -192,7 +152,7 @@ export function HeartbeatSection() {
})
.then(() =>
api.config.updateHeartbeatDefaultIntervalMs({
intervalMs: clampedMinutes * MS_PER_MINUTE,
intervalMs: intervalMinutesToMs(clampedMinutes),
})
)
.then(() => {
Expand Down
14 changes: 7 additions & 7 deletions src/browser/stores/WorkspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
isInitEnd,
isInitOutput,
isInitStart,
isAdvisorPhaseEvent,
isBashOutputEvent,
isTaskCreatedEvent,
isMuxMessage,
Expand Down Expand Up @@ -3690,17 +3691,16 @@ export class WorkspaceStore {
return;
}

if ("type" in data && data.type === "advisor-phase") {
const advisorPhase: AdvisorPhaseEvent = data;
if (isAdvisorPhaseEvent(data)) {
const transient = this.assertChatTransientState(workspaceId);
const prev = transient.liveAdvisorPhase.get(advisorPhase.toolCallId);
const prev = transient.liveAdvisorPhase.get(data.toolCallId);

// Avoid unnecessary re-renders if the phase is unchanged.
if (prev?.phase === advisorPhase.phase) return;
if (prev?.phase === data.phase) return;

transient.liveAdvisorPhase.set(advisorPhase.toolCallId, {
phase: advisorPhase.phase,
timestamp: advisorPhase.timestamp,
transient.liveAdvisorPhase.set(data.toolCallId, {
phase: data.phase,
timestamp: data.timestamp,
});

// Low-frequency: bump immediately so advisor progress updates feel responsive.
Expand Down
3 changes: 1 addition & 2 deletions src/browser/stories/mocks/orpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ type MockMcpTestResult = { success: true; tools: string[] } | { success: false;
export function createMockORPCClient(options: MockORPCClientOptions = {}): APIClient {
const {
projects: providedProjects = new Map<string, ProjectConfig>(),
workspaces: inputWorkspaces = [],
workspaces = [],
projectGitStatusesByWorkspace = new Map<string, ApiProjectGitStatusResult[]>(),
workspaceActivitySnapshots = {},
onChat,
Expand Down Expand Up @@ -383,7 +383,6 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
runtimeStatuses = new Map<string, "running" | "stopped" | "unknown" | "unsupported">(),
} = options;

const workspaces = inputWorkspaces;
const projects = new Map(providedProjects);
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));

Expand Down
17 changes: 0 additions & 17 deletions src/browser/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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%);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading