Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
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