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
3 changes: 1 addition & 2 deletions apps/web/src/components/settings/ProjectDefaultsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,7 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
serverScoped
settingKeys={["fallbackModelSelection"]}
mixed={mixedFallback}
id="fallback-model"
title="Fallback model"
{...searchableSetting("fallback-model")}
description="Used for new threads when the default model's provider is out of usage."
resetAction={
mixedFallback || settings.fallbackModelSelection !== null ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ import { buildSidebarProjectSnapshots } from "../../sidebarProjectGrouping";
import { ProjectSettingsPanel } from "./ProjectSettingsPanel";
import { ProjectsSettings } from "./ProjectsSettings";
import { SettingsScopeProvider } from "./SettingsScopeContext";
import { SettingsScopeNotice } from "./SettingsScopeNotice";
import { validateSettingsScopeSearch, type SettingsScopeSearch } from "./settingsScope";
import { retainSettingsScope, validateSettingsRouteSearch } from "./settingsScopeNavigation";

Expand Down Expand Up @@ -533,6 +534,48 @@ describe("project identity reset", () => {
});
});

it("opens the fallback search target across all environments from a checkout", async () => {
setup();
const root = createRootRoute();
const settings = createRoute({
getParentRoute: () => root,
path: "settings",
validateSearch: validateSettingsRouteSearch,
search: { middlewares: [retainSettingsScope] },
});
const threads = createRoute({
getParentRoute: () => settings,
path: "threads",
component: () => (
<SettingsScopeProvider search={settings.useSearch()} onChange={() => {}}>
<SettingsScopeNotice target="all" targetId="fallback-model">
Choose all environments to edit the fallback model.
</SettingsScopeNotice>
</SettingsScopeProvider>
),
});
const query = new URLSearchParams({
project: projectKey(),
machine: primaryId,
checkout: "primary:/work/primary",
});
const router = createRouter({
routeTree: root.addChildren([settings.addChildren([threads])]),
history: createMemoryHistory({ initialEntries: [`/settings/threads?${query}#fallback-model`] }),
});
await router.load();
// This route needs the real scope-retention middleware used by settings search.
await using _screen = await render(
<RegistryContext.Provider value={appAtomRegistry}>
<RouterProvider router={router} />
</RegistryContext.Provider>,
{ router: false },
);
await userEvent.click(page.getByRole("button", { name: "Open all environments" }));
await vi.waitFor(() => expect(router.state.location.search).toEqual({}));
expect(router.state.location.hash).toBe("fallback-model");
});

describe("project settings scope", () => {
const environmentMenu = () => page.getByRole("button", { name: /^Environment scope:/ });
const primaryOption = () => page.getByRole("menuitemradio", { name: /^primary/ });
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/components/settings/SettingsScopeNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,16 @@ export function SettingsScopeNotice({
)}
onClick={() => {
if (targetId)
void navigate({ to: pathname, search: () => choice.search, hash: targetId });
void navigate({
to: pathname,
search: () => ({
project: undefined,
machine: undefined,
checkout: undefined,
...choice.search,
}),
hash: targetId,
});
else selectScope(choice.search);
}}
>
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/settings/settingsSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ describe("searchSettings", () => {
expect(searchSettings("sharing", ITEMS).map((item) => item.id)).toEqual(["network-access"]);
expect(searchSettings("claude", ITEMS).map((item) => item.id)).toEqual(["providers"]);
expect(searchSettings("long lines", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]);
for (const query of ["fallback", "fallback agent", "fallback model"]) {
expect(searchSettings(query)[0]).toMatchObject({
id: "fallback-model",
to: "/settings/threads",
scope: "environment-defaults",
});
}
});

it("hides the Secrets page from nav and search while the secrets feature is disabled", () => {
Expand Down Expand Up @@ -205,6 +212,11 @@ describe("settings search targets", () => {
expect(isSettingsSearchScopeAvailable(updates.scope, "environment")).toBe(true);
expect(isSettingsSearchScopeAvailable(updates.scope, "all")).toBe(true);
expect(isSettingsSearchScopeAvailable(updates.scope, "project")).toBe(false);
const fallback = getSettingsSearchTargetScope("fallback-model")!;
expect(fallback.scope).toBe("environment-defaults");
expect(isSettingsSearchScopeAvailable(fallback.scope, "environment")).toBe(true);
expect(isSettingsSearchScopeAvailable(fallback.scope, "all")).toBe(true);
expect(isSettingsSearchScopeAvailable(fallback.scope, "project")).toBe(false);
const streaming = getSettingsSearchTargetScope("response-streaming")!;
expect(streaming.scope).toBe("project-defaults");
expect(isSettingsSearchScopeAvailable(streaming.scope, "project")).toBe(true);
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ export const SETTINGS_SEARCH_ITEMS = [
scope: "project-defaults",
searchTerms: ["new thread project provider reasoning effort"],
},
{
id: "fallback-model",
title: "Fallback model",
to: "/settings/threads",
scope: "environment-defaults",
searchTerms: ["fallback agent provider out of usage new threads"],
environmentOnly: true,
},
{
id: "default-permissions",
title: "Permissions",
Expand Down
Loading