Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -35,7 +35,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 @@ -398,9 +397,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
9 changes: 4 additions & 5 deletions src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "../Tooltip/Tooltip";
import { Popover, PopoverTrigger, PopoverContent } from "../Popover/Popover";
import { Checkbox } from "../Checkbox/Checkbox";
import { formatKeybind, KEYBINDS, matchesKeybind } from "@/browser/utils/ui/keybinds";
import {
buildArchiveConfirmDescription,
buildArchiveConfirmWarning,
} from "@/browser/utils/archiveConfirmation";
import { getDevcontainerStatusChip } from "@/browser/utils/runtimeUi";
import { useGitStatus } from "@/browser/stores/GitStatusStore";
import { useRuntimeStatus, useRuntimeStatusStoreRaw } from "@/browser/stores/RuntimeStatusStore";
Expand Down Expand Up @@ -68,11 +72,6 @@ interface WorkspaceMenuBarProps {
onOpenTerminal?: (options?: TerminalSessionCreateOptions) => void;
}

import {
buildArchiveConfirmDescription,
buildArchiveConfirmWarning,
} from "@/browser/utils/archiveConfirmation";

export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
workspaceId,
projectName,
Expand Down
3 changes: 1 addition & 2 deletions src/browser/features/LandingPage/LandingPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from "@/browser/stories/meta.js";
import { createMockORPCClient } from "@/browser/stories/mocks/orpc";
import { createWorkspace, groupWorkspacesByProject } from "@/browser/stories/mocks/workspaces";
import { LEFT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage";

// Integration: stories render full app to test landing page layout with sidebar, analytics, and workspace cards.
export default {
Expand Down Expand Up @@ -168,7 +167,7 @@ export const SidebarCollapsed: AppStory = {
render: () => (
<AppWithMocks
setup={() => {
localStorage.setItem(LEFT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(true));
collapseLeftSidebar();
expandProjects([PROJECT_PATH]);
const client = createMockORPCClient({
projects: groupWorkspacesByProject(WORKSPACES),
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
7 changes: 1 addition & 6 deletions src/common/utils/messages/compactionBoundary.ts
Original file line number Diff line number Diff line change
@@ -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" {
Expand Down
16 changes: 16 additions & 0 deletions src/common/utils/numbers.ts
Original file line number Diff line number Diff line change
@@ -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
);
}
2 changes: 1 addition & 1 deletion src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3985,7 +3985,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,
Expand Down
21 changes: 1 addition & 20 deletions src/node/services/compactionHandler.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -462,10 +451,6 @@ export class CompactionHandler {
await this.deletePersistedPendingStateBestEffort();
}

async ackPendingDiffsConsumed(): Promise<void> {
await this.ackPendingStateConsumed();
}

/**
* Drop pending post-compaction state (e.g., because it caused context_exceeded).
*/
Expand All @@ -490,10 +475,6 @@ export class CompactionHandler {
this.cachedLoadedSkills = [];
}

async discardPendingDiffs(reason: string): Promise<void> {
await this.discardPendingState(reason);
}

private async deletePersistedPendingStateBestEffort(): Promise<void> {
try {
await fsPromises.unlink(this.postCompactionStatePath);
Expand Down
3 changes: 1 addition & 2 deletions src/node/services/heartbeatService.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
13 changes: 1 addition & 12 deletions src/node/services/historyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
73 changes: 17 additions & 56 deletions src/node/services/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -211,22 +212,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;
Expand Down Expand Up @@ -563,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 {
Expand Down Expand Up @@ -3290,18 +3265,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(
Expand Down Expand Up @@ -3354,26 +3323,18 @@ export class WorkspaceService extends EventEmitter {
}

const nextMessage = hasMessageUpdate
? normalizeHeartbeatMessageInput(settings.message)
? sanitizeHeartbeatMessage(settings.message)
: sanitizeHeartbeatMessage(workspaceEntry.heartbeat?.message);
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 ||
Expand Down
Loading
Loading