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
12 changes: 6 additions & 6 deletions src/browser/features/Settings/Sections/GoalsSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {

type GoalDefaultsUpdate = Parameters<APIClient["config"]["updateGoalDefaults"]>[0];

import { GoalsSection } from "./GoalsSection";
import { GoalDefaultsControls } from "./GoalsSection";

function renderGoalsSection(config: { goalDefaults?: GoalDefaults } = {}) {
function renderGoalDefaultsControls(config: { goalDefaults?: GoalDefaults } = {}) {
const current: Partial<TestClientConfig> = { ...config };
const updateGoalDefaults = mock(({ goalDefaults }: GoalDefaultsUpdate) => {
// The real input is partial; the backend stores it normalized, as getConfig returns it.
Expand All @@ -36,15 +36,15 @@ function renderGoalsSection(config: { goalDefaults?: GoalDefaults } = {}) {
const view = render(
<APIProvider client={createTestApiClient(mockApi)}>
<ThemeProvider forcedTheme="dark">
<GoalsSection />
<GoalDefaultsControls />
</ThemeProvider>
</APIProvider>
);

return { view, updateGoalDefaults };
}

describe("GoalsSection", () => {
describe("GoalDefaultsControls", () => {
let cleanupDom: (() => void) | null = null;

beforeEach(() => {
Expand All @@ -59,7 +59,7 @@ describe("GoalsSection", () => {
});

test("renders default goal settings", async () => {
const { view } = renderGoalsSection();
const { view } = renderGoalDefaultsControls();

const budgetInput = (await waitFor(() =>
view.getByLabelText("Default goal budget in dollars")
Expand All @@ -73,7 +73,7 @@ describe("GoalsSection", () => {
});

test("persists edited goal defaults", async () => {
const { view, updateGoalDefaults } = renderGoalsSection({
const { view, updateGoalDefaults } = renderGoalDefaultsControls({
goalDefaults: { ...DEFAULT_GOAL_DEFAULTS, defaultBudgetCents: 300 },
});

Expand Down
21 changes: 0 additions & 21 deletions src/browser/features/Settings/Sections/GoalsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Target } from "lucide-react";
import { useEffect, useState } from "react";
import { Input } from "@/browser/components/Input/Input";
import { useAPI } from "@/browser/contexts/API";
Expand Down Expand Up @@ -169,23 +168,3 @@ export function GoalDefaultsControls(props: GoalDefaultsControlsProps) {
</div>
);
}

export function GoalsSection() {
return (
<div className="space-y-6">
<div>
<h2 className="text-foreground mb-2 text-lg font-semibold">Goals</h2>
<p className="text-muted text-sm">
Workspace goals let you pin an objective to a workspace and track progress across turns.
</p>
</div>
<div className="border-border-light bg-surface-secondary rounded-lg border p-4">
<div className="mb-4 flex items-center gap-2 text-sm font-medium">
<Target className="h-4 w-4" aria-hidden="true" />
Goal defaults
</div>
<GoalDefaultsControls />
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/browser/hooks/useModelsFromSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import * as RoutingModule from "@/browser/hooks/useRouting";
import {
filterHiddenModels,
getDefaultModel,
getSuggestedModels,
useModelsFromSettings,
} from "./useModelsFromSettings";
import { KNOWN_MODELS } from "@/common/constants/knownModels";
import { getSuggestedModels } from "@/common/utils/ai/selectableModels";
import type {
ProviderConfigInfo,
ProviderModelEntry,
Expand Down
5 changes: 2 additions & 3 deletions src/browser/hooks/useModelsFromSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import {
dedupeKeepFirst,
filterHiddenModels,
getCustomModels,
getSuggestedModels,
isAuthoritativeProviderModelAccessible as isAuthoritativeProviderModelAccessibleIn,
isProviderConfigured,
} from "@/common/utils/ai/selectableModels";
import { getProviderModelEntryId } from "@/common/utils/providers/modelEntries";

// The selector pipeline lives in @/common (shared with the backend models_list
// tool); these two stay exported for existing importers/tests.
export { filterHiddenModels, getSuggestedModels };
// tool); this one stays exported for existing importers.
export { filterHiddenModels };

const BUILT_IN_MODEL_SET = new Set<string>(BUILT_IN_MODELS);
// Module-level so the default keeps a stable identity across renders.
Expand Down
19 changes: 0 additions & 19 deletions src/browser/stories/helpers/contrast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,6 @@ export function textContrast(element: HTMLElement): number {
return contrastRatio(over(toRgba(getComputedStyle(element).color), background), background);
}

/**
* WCAG 2.x contrast of an element's text on its own (often translucent) background drawn over
* the opaque color `base`, a computed color string. Lets a play check a tinted chip on every
* background its row can have (for example the selected row color), not only the current one.
*/
export function textContrastOnBase(element: HTMLElement, base: string): number {
const style = getComputedStyle(element);
const background = over(toRgba(style.backgroundColor), toRgba(base));
return contrastRatio(over(toRgba(style.color), background), background);
}

/** The nearest ancestor of `element` (or itself) whose background is opaque. */
export function opaqueBackgroundOwner(element: HTMLElement): HTMLElement | null {
for (let node: HTMLElement | null = element; node; node = node.parentElement) {
if (toRgba(getComputedStyle(node).backgroundColor)[3] === 1) return node;
}
return null;
}

/**
* WCAG 2.x contrast of the CSS color `color` drawn on the background behind `element`. Lets a
* play check a color value (for example a replacement in a syntax theme) on the backgrounds the
Expand Down
11 changes: 9 additions & 2 deletions src/browser/utils/highlighting/highlightDiffChunk.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
highlightDiffChunk,
highlightDiffChunks,
isWithinDiffHighlightSyncBudget,
type HighlightedChunk,
} from "./highlightDiffChunk";
import type { DiffChunk } from "./diffChunking";

Expand All @@ -11,7 +11,14 @@ import type { DiffChunk } from "./diffChunking";
* WASM loads on first test (~100ms), then cached for subsequent tests
*/

describe("highlightDiffChunk", () => {
// One chunk through the batch API that DiffRenderer calls.
async function highlightDiffChunk(chunk: DiffChunk, language: string): Promise<HighlightedChunk> {
const highlighted = await highlightDiffChunks([chunk], language);
expect(highlighted).toHaveLength(1);
return highlighted[0];
}

describe("highlightDiffChunks", () => {
const mockChunk: DiffChunk = {
type: "add",
lines: ["const x = 1;", "const y = 2;"],
Expand Down
13 changes: 0 additions & 13 deletions src/browser/utils/highlighting/highlightDiffChunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,6 @@ export interface HighlightedChunk {
usedFallback: boolean; // True if highlighting failed
}

/**
* Highlight a chunk of code using Shiki.
* Falls back to plain text on error, except a worker start timeout, which rejects.
*/
export async function highlightDiffChunk(
chunk: DiffChunk,
language: string,
themeMode: ThemeMode = "dark"
): Promise<HighlightedChunk> {
const [highlighted] = await highlightDiffChunks([chunk], language, themeMode);
return highlighted ?? createFallbackChunk(chunk);
}

interface DiffLineRef {
chunkIndex: number;
lineIndex: number;
Expand Down
2 changes: 1 addition & 1 deletion src/browser/utils/highlighting/highlightsEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Shiki only for the languages Highlights does not know (ini, nginx, prisma, ...).
*
* Output contract: the same HTML shape Shiki's `codeToHtml` produces, so every caller
* (`extractShikiLines`, `highlightDiffChunk`, the contrast stories) works unchanged:
* (`extractShikiLines`, `highlightDiffChunks`, the contrast stories) works unchanged:
* <pre class="shiki"><code><span class="line">TOKENS</span>\n<span class="line">…</span></code></pre>
* Each token is `<span style="color:#rrggbb[;font-style:italic][;font-weight:bold]">TEXT</span>`.
* We do not use Highlights' own `codeToHtml()`: it has no per-line spans, and one span can
Expand Down
2 changes: 1 addition & 1 deletion src/browser/utils/messages/StreamingTPSCalculator.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect } from "bun:test";
import { calculateTPS, calculateTokenCount, type DeltaRecord } from "./StreamingTPSCalculator";
import { calculateTPS, calculateTokenCount, type DeltaRecord } from "@/common/utils/tokens/tps";

describe("StreamingTPSCalculator", () => {
describe("calculateTokenCount", () => {
Expand Down
12 changes: 3 additions & 9 deletions src/browser/utils/messages/StreamingTPSCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@
* Designed to be independently testable from the main aggregator.
*/

import {
calculateTPS,
calculateTokenCount,
createDeltaStorage,
type DeltaRecord,
type DeltaRecordStorage,
} from "@/common/utils/tokens/tps";
import { createDeltaStorage, type DeltaRecordStorage } from "@/common/utils/tokens/tps";

export { calculateTPS, calculateTokenCount, createDeltaStorage };
export type { DeltaRecord, DeltaRecordStorage };
export { createDeltaStorage };
export type { DeltaRecordStorage };

/**
* Maximum reasonable TPS for sanity checking.
Expand Down
6 changes: 4 additions & 2 deletions src/browser/utils/workflowRunMessages.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { describe, expect, test } from "bun:test";

import { buildWorkflowResultContextMessage } from "@/common/utils/workflowRunMessages";
import {
buildWorkflowRunCardMessage,
buildWorkflowResultContextMessage,
filterWorkflowDisplayOnlyMessages,
} from "@/common/utils/workflowRunMessages";
import {
buildWorkflowRunCardMessage,
getWorkflowRunCardProjection,
hasWorkflowRunToolCallMessage,
} from "./workflowRunMessages";
Expand Down
3 changes: 1 addition & 2 deletions src/browser/utils/workflowRunMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import {
WORKFLOW_RUN_CARD_DISPLAY_METADATA_TYPE,
WORKFLOW_TRIGGER_DISPLAY_METADATA_TYPE,
buildWorkflowRunCardMessage,
filterWorkflowDisplayOnlyMessages,
isWorkflowRunEmittingToolName,
type WorkflowRunCardInput,
type WorkflowRunCardResult,
} from "@/common/utils/workflowRunMessages";

export { buildWorkflowRunCardMessage, filterWorkflowDisplayOnlyMessages };
export { buildWorkflowRunCardMessage };

function getLatestWorkflowResult(run: WorkflowRunRecord): unknown {
return run.events.findLast((event) => event.type === "result")?.result ?? null;
Expand Down
53 changes: 1 addition & 52 deletions src/browser/utils/workspaceStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { restoreDomGlobals, saveDomGlobals } from "../../../tests/ui/domGlobals";
import { QuotaLimitedStorage } from "../../../tests/ui/quotaLimitedStorage";

import { copyWorkspaceStorage, deleteWorkspaceStorage } from "@/browser/utils/workspaceStorage";
import {
copyWorkspaceStorage,
deleteWorkspaceStorage,
migrateWorkspaceStorage,
} from "@/browser/utils/workspaceStorage";
import {
getAutoExpandPrefsKey,
getReviewsKey,
getPersistedKeyRegistration,
getDesktopPopoutKey,
getMCPTestResultsKey,
getPinnedTodoExpandedKey,
Expand Down Expand Up @@ -78,50 +71,6 @@ describe("deleteWorkspaceStorage", () => {
});
});

describe("migrateWorkspaceStorage", () => {
beforeEach(() => {
saveDomGlobals();
});

afterEach(() => {
restoreDomGlobals();
});

// The copy no longer throws when a write fails; deleting the source anyway would erase the
// only persisted copy of the scope's model/review/name state.
test("keeps the source keys when a destination write fails", () => {
const storage = new QuotaLimitedStorage(60);
const domWindow = new GlobalWindow() as unknown as Window & typeof globalThis;
Object.defineProperty(domWindow, "localStorage", { value: storage, configurable: true });
globalThis.window = domWindow;
globalThis.document = domWindow.document;
globalThis.localStorage = storage;
const sourceKey = getAutoExpandPrefsKey("__pending__/repo");
storage.setItem(sourceKey, JSON.stringify("anthropic:claude-opus"));

migrateWorkspaceStorage("__pending__/repo", "ws-destination");

expect(storage.getItem(sourceKey)).toBe(JSON.stringify("anthropic:claude-opus"));
});

// A value over the destination's budget can only live there in memory, so without an owner bound
// the source must stay the durable copy.
test("keeps a source value without an owner bound that is over its budget at the destination", () => {
const domWindow = new GlobalWindow() as unknown as Window & typeof globalThis;
globalThis.window = domWindow;
globalThis.document = domWindow.document;
globalThis.localStorage = domWindow.localStorage;
const sourceKey = getAutoExpandPrefsKey("__pending__/repo2");
const budget = getPersistedKeyRegistration(sourceKey)!.maxValueChars;
const legacyValue = JSON.stringify("m".repeat(budget));
localStorage.setItem(sourceKey, legacyValue);

migrateWorkspaceStorage("__pending__/repo2", "ws-destination2");

expect(localStorage.getItem(sourceKey)).toBe(legacyValue);
});
});

describe("copyWorkspaceStorage", () => {
beforeEach(() => {
saveDomGlobals();
Expand Down
11 changes: 0 additions & 11 deletions src/browser/utils/workspaceStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,3 @@ export function deleteWorkspaceStorage(workspaceId: string): void {
export function listWorkspaceStorageGcCandidateKeys(): string[] {
return listPersistedStateKeys([""]).filter(isWorkspaceStorageGcCandidateKey);
}

/**
* Migrate all workspace-specific localStorage keys from old to new workspace ID
* Should be called when a workspace is renamed to preserve settings.
* If any copy fails (e.g. the quota is full), the old keys are kept: the write helpers report
* failure instead of throwing, and deleting anyway would erase the only persisted copy.
*/
export function migrateWorkspaceStorage(oldWorkspaceId: string, newWorkspaceId: string): void {
if (!copyWorkspaceStorage(oldWorkspaceId, newWorkspaceId)) return;
deleteWorkspaceStorage(oldWorkspaceId);
}
2 changes: 1 addition & 1 deletion src/common/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ export interface WorkspaceKeyRegistration {
scope: "workspaceId";
getKey: (scopeId: string) => string;
kind: PersistedKeyKind;
/** Copied to the new workspace on fork (and to the new scope on migrateWorkspaceStorage). */
/** Copied to the new workspace on fork. */
copyOnFork: boolean;
/** Longest accepted serialized value (UTF-16 code units); 0 refuses every write. */
maxValueChars: number;
Expand Down
Loading