Skip to content
Merged
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
23 changes: 9 additions & 14 deletions src/browser/components/RightSidebar/CostsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSett
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
import { PostCompactionSection } from "./PostCompactionSection";
import { usePostCompactionState } from "@/browser/hooks/usePostCompactionState";
import { useExperimentValue } from "@/browser/contexts/ExperimentsContext";
import { EXPERIMENT_IDS } from "@/common/constants/experiments";
import { useOptionalWorkspaceContext } from "@/browser/contexts/WorkspaceContext";

/**
Expand Down Expand Up @@ -59,8 +57,7 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
const { options } = useProviderOptions();
const use1M = options.anthropic?.use1MContext ?? false;

// Post-compaction context state for UI display (gated by experiment)
const postCompactionEnabled = useExperimentValue(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
// Post-compaction context state for UI display
const postCompactionState = usePostCompactionState(workspaceId);

// Get runtimeConfig for SSH-aware editor opening
Expand Down Expand Up @@ -136,16 +133,14 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
);
})()}
</div>
{postCompactionEnabled && (
<PostCompactionSection
workspaceId={workspaceId}
planPath={postCompactionState.planPath}
trackedFilePaths={postCompactionState.trackedFilePaths}
excludedItems={postCompactionState.excludedItems}
onToggleExclusion={postCompactionState.toggleExclusion}
runtimeConfig={runtimeConfig}
/>
)}
<PostCompactionSection
workspaceId={workspaceId}
planPath={postCompactionState.planPath}
trackedFilePaths={postCompactionState.trackedFilePaths}
excludedItems={postCompactionState.excludedItems}
onToggleExclusion={postCompactionState.toggleExclusion}
runtimeConfig={runtimeConfig}
/>
</div>
)}

Expand Down
17 changes: 3 additions & 14 deletions src/browser/components/Settings/sections/ExperimentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import type { ApiServerStatus } from "@/common/orpc/types";
import { Input } from "@/browser/components/ui/input";
import { useAPI } from "@/browser/contexts/API";
import { useFeatureFlags } from "@/browser/contexts/FeatureFlagsContext";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useTelemetry } from "@/browser/hooks/useTelemetry";

interface ExperimentRowProps {
Expand Down Expand Up @@ -485,7 +484,6 @@ function StatsTabRow() {

export function ExperimentsSection() {
const allExperiments = getExperimentList();
const { refreshWorkspaceMetadata } = useWorkspaceContext();
const { api } = useAPI();

// Only show user-overridable experiments (non-overridable ones are hidden since users can't change them)
Expand All @@ -495,13 +493,6 @@ export function ExperimentsSection() {
[allExperiments]
);

// When post-compaction experiment is toggled, refresh metadata to fetch/clear bundled state
const handlePostCompactionToggle = useCallback(() => {
refreshWorkspaceMetadata().catch(() => {
// ignore
});
}, [refreshWorkspaceMetadata]);

const handleConfigurableBindUrlToggle = useCallback(
(enabled: boolean) => {
if (enabled) {
Expand Down Expand Up @@ -531,11 +522,9 @@ export function ExperimentsSection() {
name={exp.name}
description={exp.description}
onToggle={
exp.id === EXPERIMENT_IDS.POST_COMPACTION_CONTEXT
? handlePostCompactionToggle
: exp.id === EXPERIMENT_IDS.CONFIGURABLE_BIND_URL
? handleConfigurableBindUrlToggle
: undefined
exp.id === EXPERIMENT_IDS.CONFIGURABLE_BIND_URL
? handleConfigurableBindUrlToggle
: undefined
}
/>
{exp.id === EXPERIMENT_IDS.CONFIGURABLE_BIND_URL && <ConfigurableBindUrlControls />}
Expand Down
6 changes: 3 additions & 3 deletions src/browser/contexts/ExperimentsContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ describe("ExperimentsProvider", () => {

if (callCount === 1) {
return Promise.resolve({
[EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: { value: null, source: "cache" },
[EXPERIMENT_IDS.SYSTEM_1]: { value: null, source: "cache" },
} satisfies Record<string, ExperimentValue>);
}

return Promise.resolve({
[EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: { value: "test", source: "posthog" },
[EXPERIMENT_IDS.SYSTEM_1]: { value: "test", source: "posthog" },
} satisfies Record<string, ExperimentValue>);
});

Expand All @@ -56,7 +56,7 @@ describe("ExperimentsProvider", () => {
};

function Observer() {
const enabled = useExperimentValue(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const enabled = useExperimentValue(EXPERIMENT_IDS.SYSTEM_1);
return <div data-testid="enabled">{String(enabled)}</div>;
}

Expand Down
16 changes: 8 additions & 8 deletions src/browser/hooks/useExperiments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,30 @@ describe("isExperimentEnabled", () => {
});

test("returns undefined when no local override exists for a user-overridable experiment", () => {
expect(isExperimentEnabled(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT)).toBeUndefined();
expect(isExperimentEnabled(EXPERIMENT_IDS.SYSTEM_1)).toBeUndefined();
});

test("returns boolean when local override exists", () => {
const key = getExperimentKey(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const key = getExperimentKey(EXPERIMENT_IDS.SYSTEM_1);

globalThis.window.localStorage.setItem(key, JSON.stringify(true));
expect(isExperimentEnabled(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT)).toBe(true);
expect(isExperimentEnabled(EXPERIMENT_IDS.SYSTEM_1)).toBe(true);

globalThis.window.localStorage.setItem(key, JSON.stringify(false));
expect(isExperimentEnabled(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT)).toBe(false);
expect(isExperimentEnabled(EXPERIMENT_IDS.SYSTEM_1)).toBe(false);
});

test('treats literal "undefined" as no override', () => {
const key = getExperimentKey(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const key = getExperimentKey(EXPERIMENT_IDS.SYSTEM_1);

globalThis.window.localStorage.setItem(key, "undefined");
expect(isExperimentEnabled(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT)).toBeUndefined();
expect(isExperimentEnabled(EXPERIMENT_IDS.SYSTEM_1)).toBeUndefined();
});

test("treats non-boolean stored value as no override", () => {
const key = getExperimentKey(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const key = getExperimentKey(EXPERIMENT_IDS.SYSTEM_1);

globalThis.window.localStorage.setItem(key, JSON.stringify("test"));
expect(isExperimentEnabled(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT)).toBeUndefined();
expect(isExperimentEnabled(EXPERIMENT_IDS.SYSTEM_1)).toBeUndefined();
});
});
17 changes: 7 additions & 10 deletions src/browser/hooks/usePostCompactionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { useEffect, useState, useCallback, useRef } from "react";
import { useAPI } from "@/browser/contexts/API";
import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState";
import { getPostCompactionStateKey } from "@/common/constants/storage";
import { useExperimentValue } from "@/browser/hooks/useExperiments";
import { EXPERIMENT_IDS } from "@/common/constants/experiments";

interface PostCompactionState {
planPath: string | null;
Expand Down Expand Up @@ -35,12 +33,11 @@ function loadFromCache(wsId: string) {
* Hook to get post-compaction context state for a workspace.
* Fetches lazily from the backend API and caches in localStorage.
* This avoids the expensive runtime.stat calls during workspace.list().
* Only fetches when the POST_COMPACTION_CONTEXT experiment is enabled.
*
* Always enabled: post-compaction context is a stable feature (not an experiment).
*/
export function usePostCompactionState(workspaceId: string): PostCompactionState {
const { api } = useAPI();
const experimentEnabled = useExperimentValue(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);

const [state, setState] = useState(() => loadFromCache(workspaceId));

// Track which workspaceId the current state belongs to.
Expand All @@ -51,9 +48,9 @@ export function usePostCompactionState(workspaceId: string): PostCompactionState
setState(loadFromCache(workspaceId));
}

// Fetch fresh data when workspaceId changes (only if experiment enabled)
// Fetch fresh data when workspaceId changes
useEffect(() => {
if (!api || !experimentEnabled) return;
if (!api) return;

let cancelled = false;
const fetchState = async () => {
Expand Down Expand Up @@ -84,11 +81,11 @@ export function usePostCompactionState(workspaceId: string): PostCompactionState
return () => {
cancelled = true;
};
}, [api, workspaceId, experimentEnabled]);
}, [api, workspaceId]);

const toggleExclusion = useCallback(
async (itemId: string) => {
if (!api || !experimentEnabled) return;
if (!api) return;
const isCurrentlyExcluded = state.excludedItems.has(itemId);
const result = await api.workspace.setPostCompactionExclusion({
workspaceId,
Expand Down Expand Up @@ -117,7 +114,7 @@ export function usePostCompactionState(workspaceId: string): PostCompactionState
});
}
},
[api, workspaceId, state.excludedItems, experimentEnabled]
[api, workspaceId, state.excludedItems]
);

return { ...state, toggleExclusion };
Expand Down
5 changes: 1 addition & 4 deletions src/browser/hooks/useSendMessageOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ function applyGatewayTransform(modelId: string, gateway: GatewayState): string {
}

interface ExperimentValues {
postCompactionContext: boolean | undefined;
programmaticToolCalling: boolean | undefined;
programmaticToolCallingExclusive: boolean | undefined;
system1: boolean | undefined;
Expand Down Expand Up @@ -93,7 +92,6 @@ function constructSendMessageOptions(
// toolPolicy is computed by backend from agent definitions (resolveToolPolicyForAgent)
providerOptions,
experiments: {
postCompactionContext: experimentValues.postCompactionContext,
programmaticToolCalling: experimentValues.programmaticToolCalling,
programmaticToolCallingExclusive: experimentValues.programmaticToolCallingExclusive,
system1: experimentValues.system1,
Expand Down Expand Up @@ -139,7 +137,6 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptionsWi

// Subscribe to local override state so toggles apply immediately.
// If undefined, the backend will apply the PostHog assignment.
const postCompactionContext = useExperimentOverrideValue(EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const programmaticToolCalling = useExperimentOverrideValue(
EXPERIMENT_IDS.PROGRAMMATIC_TOOL_CALLING
);
Expand Down Expand Up @@ -177,7 +174,7 @@ export function useSendMessageOptions(workspaceId: string): SendMessageOptionsWi
providerOptions,
defaultModel,
gateway,
{ postCompactionContext, programmaticToolCalling, programmaticToolCallingExclusive, system1 },
{ programmaticToolCalling, programmaticToolCallingExclusive, system1 },
system1Model,
system1ThinkingLevel
);
Expand Down
10 changes: 7 additions & 3 deletions src/browser/stories/App.settings.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import { createWorkspace, groupWorkspacesByProject } from "./mockFactory";
import { selectWorkspace } from "./storyHelpers";
import { createMockORPCClient } from "@/browser/stories/mocks/orpc";
import { within, userEvent, waitFor } from "@storybook/test";
import { getExperimentKey, EXPERIMENT_IDS } from "@/common/constants/experiments";
import {
getExperimentKey,
EXPERIMENT_IDS,
type ExperimentId,
} from "@/common/constants/experiments";
import type { AgentAiDefaults } from "@/common/types/agentAiDefaults";
import type { TaskSettings } from "@/common/types/tasks";
import type { LayoutPresetsConfig } from "@/common/types/uiLayouts";
Expand Down Expand Up @@ -54,7 +58,7 @@ function setupSettingsStory(options: {
// Pre-set experiment states if provided
if (options.experiments) {
for (const [experimentId, enabled] of Object.entries(options.experiments)) {
const key = getExperimentKey(experimentId as typeof EXPERIMENT_IDS.POST_COMPACTION_CONTEXT);
const key = getExperimentKey(experimentId as ExperimentId);
window.localStorage.setItem(key, JSON.stringify(enabled));
}
}
Expand Down Expand Up @@ -483,7 +487,7 @@ export const ExperimentsToggleOn: AppStory = {
<AppWithMocks
setup={() =>
setupSettingsStory({
experiments: { [EXPERIMENT_IDS.POST_COMPACTION_CONTEXT]: true },
experiments: { [EXPERIMENT_IDS.SYSTEM_1]: true },
})
}
/>
Expand Down
42 changes: 40 additions & 2 deletions src/browser/utils/messages/attachmentRenderer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { describe, it, expect } from "@jest/globals";
import { renderAttachmentToContent } from "./attachmentRenderer";
import type { TodoListAttachment } from "@/common/types/attachment";
import {
renderAttachmentToContent,
renderAttachmentsToContentWithBudget,
} from "./attachmentRenderer";
import type {
TodoListAttachment,
PlanFileReferenceAttachment,
EditedFilesReferenceAttachment,
} from "@/common/types/attachment";

describe("attachmentRenderer", () => {
it("renders todo list inline and mentions todo_read", () => {
Expand All @@ -27,4 +34,35 @@ describe("attachmentRenderer", () => {
expect(content).not.toContain("todos.json");
expect(content).not.toContain("~/.mux");
});

it("respects a maxChars budget and truncates oversized plan content", () => {
const attachment: PlanFileReferenceAttachment = {
type: "plan_file_reference",
planFilePath: "~/.mux/plans/cmux/ws.md",
planContent: "a".repeat(10_000),
};

const content = renderAttachmentsToContentWithBudget([attachment], { maxChars: 400 });

expect(content.length).toBeLessThanOrEqual(400);
expect(content).toContain("Plan contents");
expect(content).toContain("...(truncated)");
expect(content).toContain("<system-update>");
});

it("emits an omitted-file-diffs note when edited file diffs do not fit", () => {
const attachment: EditedFilesReferenceAttachment = {
type: "edited_files_reference",
files: [
{ path: "src/a.ts", diff: "a".repeat(2000), truncated: false },
{ path: "src/b.ts", diff: "b".repeat(2000), truncated: false },
],
};

const content = renderAttachmentsToContentWithBudget([attachment], { maxChars: 120 });

expect(content.length).toBeLessThanOrEqual(120);
expect(content).toContain("omitted 2 file diffs");
expect(content).toContain("<system-update>");
});
});
Loading
Loading