diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx
index 9654154dea7..16a338e6b1e 100644
--- a/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx
+++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx
@@ -43,14 +43,7 @@ const AgentSettingsCompactionPage: FC = () => {
[
- organization.id,
- organization.display_name || organization.name,
- ]),
- )
- }
+ organizations={organizations}
modelsError={organizationModels.error ?? organizationModels.partialError}
isLoadingModels={organizationModels.isLoading}
thresholds={thresholdsQuery.data?.thresholds}
diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx
index bdf4e0755d0..5c9ecdd97a4 100644
--- a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx
+++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx
@@ -21,9 +21,7 @@ const baseArgs: AgentSettingsCompactionPageViewProps = {
},
],
providerTypeByID: new Map([["prov-openai", "openai"]]),
- organizationNameByID: new Map([
- [MockDefaultOrganization.id, MockDefaultOrganization.display_name],
- ]),
+ organizations: [MockDefaultOrganization],
modelsError: undefined,
isLoadingModels: false,
thresholds: [
diff --git a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx
index 75ae171be02..61136d266f2 100644
--- a/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx
+++ b/site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx
@@ -6,7 +6,7 @@ import { UserCompactionThresholdSettings } from "./components/UserCompactionThre
export interface AgentSettingsCompactionPageViewProps {
models: readonly TypesGen.ChatModel[] | undefined;
providerTypeByID: ReadonlyMap;
- organizationNameByID: ReadonlyMap;
+ organizations: readonly TypesGen.Organization[];
modelsError: unknown;
isLoadingModels: boolean;
thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined;
@@ -24,7 +24,7 @@ export const AgentSettingsCompactionPageView: FC<
> = ({
models,
providerTypeByID,
- organizationNameByID,
+ organizations,
modelsError,
isLoadingModels,
thresholds,
@@ -42,7 +42,7 @@ export const AgentSettingsCompactionPageView: FC<
([
- [MockChatModel.organization_id, MockDefaultOrganization.display_name],
- ]),
+ organizations: [modelsOrganization],
thresholds: [],
isThresholdsLoading: false,
thresholdsError: undefined,
@@ -116,20 +123,11 @@ export const Default: Story = {
export const EmptyOrganizationDisplayNameFallsBackToName: Story = {
args: {
- organizationNameByID: new Map([
- [
- organizationWithEmptyDisplayName.id,
- organizationWithEmptyDisplayName.display_name ||
- organizationWithEmptyDisplayName.name,
- ],
- ]),
+ organizations: [organizationWithEmptyDisplayName],
thresholds: [{ model_config_id: "model-1", threshold_percent: 90 }],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- expect(
- canvas.getAllByText(organizationWithEmptyDisplayName.name).length,
- ).toBeGreaterThan(0);
expect(
canvas.getByRole("textbox", {
name: `GPT-4o compaction threshold for ${organizationWithEmptyDisplayName.name}`,
@@ -320,6 +318,131 @@ export const PartialSaveFailure: Story = {
},
};
+export const OrganizationFilter: Story = {
+ args: {
+ models: [
+ mockModels[0],
+ {
+ ...mockModels[1],
+ organization_id: MockOrganization2.id,
+ },
+ ],
+ organizations: [modelsOrganization, MockOrganization2],
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const filter = await canvas.findByRole("button", {
+ name: `Organization ${modelsOrganization.display_name}`,
+ });
+
+ expect(canvas.getByText("GPT-4o")).toBeInTheDocument();
+ expect(canvas.queryByText("Claude Sonnet")).not.toBeInTheDocument();
+
+ await userEvent.click(filter);
+ const option = await within(document.body).findByRole("option", {
+ name: MockOrganization2.display_name,
+ });
+ await userEvent.click(option);
+
+ await waitFor(() => {
+ expect(canvas.queryByText("GPT-4o")).not.toBeInTheDocument();
+ expect(canvas.getByText("Claude Sonnet")).toBeInTheDocument();
+ });
+
+ expect(
+ canvas.getByRole("button", {
+ name: `Organization ${MockOrganization2.display_name}`,
+ }),
+ ).toBeInTheDocument();
+ },
+};
+
+export const SingleOrganizationHidesFilter: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await canvas.findByText("GPT-4o");
+ expect(
+ canvas.queryByRole("button", { name: /^Organization / }),
+ ).not.toBeInTheDocument();
+ },
+};
+
+export const OrganizationFilterScopesSaveActions: Story = {
+ args: {
+ models: [
+ mockModels[0],
+ {
+ ...mockModels[1],
+ organization_id: MockOrganization2.id,
+ },
+ ],
+ organizations: [modelsOrganization, MockOrganization2],
+ },
+ play: async ({ canvasElement, args }) => {
+ const canvas = within(canvasElement);
+ const gpt4oInput = await canvas.findByRole("textbox", {
+ name: /GPT-4o compaction threshold/i,
+ });
+ await userEvent.type(gpt4oInput, "95");
+ await canvas.findByRole("button", { name: /Save 1 change/i });
+
+ // Switch to the other organization: the draft belongs to a hidden
+ // row, so the footer must disappear.
+ await userEvent.click(
+ canvas.getByRole("button", {
+ name: `Organization ${modelsOrganization.display_name}`,
+ }),
+ );
+ await userEvent.click(
+ await within(document.body).findByRole("option", {
+ name: MockOrganization2.display_name,
+ }),
+ );
+ await waitFor(() => {
+ expect(canvas.queryByRole("button", { name: /Save/i })).toBeNull();
+ });
+
+ // Editing the visible row saves only that row.
+ const claudeInput = await canvas.findByRole("textbox", {
+ name: /Claude Sonnet compaction threshold/i,
+ });
+ await userEvent.type(claudeInput, "50");
+ await userEvent.click(
+ await canvas.findByRole("button", { name: /Save 1 change/i }),
+ );
+ await waitFor(() => {
+ expect(args.onSaveThreshold).toHaveBeenCalledWith("model-2", 50);
+ expect(args.onSaveThreshold).not.toHaveBeenCalledWith("model-1", 95);
+ });
+
+ // Switching back restores the hidden draft and its footer.
+ await userEvent.click(
+ canvas.getByRole("button", {
+ name: `Organization ${MockOrganization2.display_name}`,
+ }),
+ );
+ await userEvent.click(
+ await within(document.body).findByRole("option", {
+ name: modelsOrganization.display_name,
+ }),
+ );
+ const restoredInput = await canvas.findByRole("textbox", {
+ name: /GPT-4o compaction threshold/i,
+ });
+ expect(restoredInput).toHaveValue("95");
+ // Wait out the temporary "Saved" footer state (2.5s) before the
+ // action buttons reappear.
+ await waitFor(
+ () => {
+ expect(
+ canvas.getByRole("button", { name: /Save 1 change/i }),
+ ).toBeInTheDocument();
+ },
+ { timeout: 5000 },
+ );
+ },
+};
+
export const ErrorState: Story = {
name: "Error",
args: {
@@ -338,9 +461,6 @@ export const PartialModelLoadError: Story = {
expect(
await canvas.findByText("Failed to load models from one organization"),
).toBeVisible();
- expect(
- canvas.getAllByText(MockDefaultOrganization.display_name).length,
- ).toBeGreaterThan(0);
expect(
canvas.getByRole("textbox", {
name: `GPT-4o compaction threshold for ${MockDefaultOrganization.display_name}`,
diff --git a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx
index 4a32d00fe95..a2c9fc5524c 100644
--- a/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx
+++ b/site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx
@@ -5,6 +5,10 @@ import type * as TypesGen from "#/api/typesGenerated";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { Input } from "#/components/Input/Input";
+import {
+ getOrganizationLabel,
+ OrganizationAutocomplete,
+} from "#/components/OrganizationAutocomplete/OrganizationAutocomplete";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Table,
@@ -31,7 +35,7 @@ import { ProviderIcon } from "./ChatModelAdminPanel/ProviderIcon";
interface UserCompactionThresholdSettingsProps {
models: readonly TypesGen.ChatModel[];
providerTypeByID: ReadonlyMap;
- organizationNameByID: ReadonlyMap;
+ organizations: readonly TypesGen.Organization[];
modelsError?: unknown;
isLoadingModels?: boolean;
thresholds: readonly TypesGen.UserChatCompactionThreshold[] | undefined;
@@ -75,7 +79,7 @@ export const UserCompactionThresholdSettings: FC<
> = ({
models,
providerTypeByID,
- organizationNameByID,
+ organizations,
modelsError,
isLoadingModels,
thresholds,
@@ -87,9 +91,32 @@ export const UserCompactionThresholdSettings: FC<
const [drafts, setDrafts] = useState>({});
const [rowErrors, setRowErrors] = useState>({});
const [pendingModels, setPendingModels] = useState>(new Set());
+ const [selectedOrganizationID, setSelectedOrganizationID] = useState<
+ string | null
+ >(null);
const { isSavedVisible, showSavedState } = useTemporarySavedState();
const enabledModels = models.filter((config) => config.enabled);
+ const organizationNameByID = new Map(
+ organizations.map((organization) => [
+ organization.id,
+ organization.display_name || organization.name,
+ ]),
+ );
+ const organizationOptions = organizations.filter((organization) =>
+ enabledModels.some((config) => config.organization_id === organization.id),
+ );
+ const activeOrganization =
+ organizationOptions.find(
+ (organization) => organization.id === selectedOrganizationID,
+ ) ??
+ organizationOptions.find((organization) => organization.is_default) ??
+ organizationOptions[0];
+ const visibleModels = activeOrganization
+ ? enabledModels.filter(
+ (config) => config.organization_id === activeOrganization.id,
+ )
+ : enabledModels;
const overridesByModelID = new Map(
(thresholds ?? []).map(
(threshold: TypesGen.UserChatCompactionThreshold) => [
@@ -152,10 +179,11 @@ export const UserCompactionThresholdSettings: FC<
});
};
- // Compute dirty rows: rows where the user has typed a valid value
- // that differs from the current server-side override.
+ // Save/cancel act only on visible rows; drafts hidden by the org
+ // picker are kept untouched.
+ const visibleModelIDs = new Set(visibleModels.map((config) => config.id));
const dirtyRows: Array<{ modelId: string; value: number }> = [];
- for (const modelConfig of enabledModels) {
+ for (const modelConfig of visibleModels) {
const draft = drafts[modelConfig.id];
if (draft === undefined) continue;
const parsed = parseThresholdDraft(draft);
@@ -197,13 +225,31 @@ export const UserCompactionThresholdSettings: FC<
};
const handleCancelAll = () => {
- setDrafts({});
- setRowErrors({});
+ setDrafts((currentDrafts) =>
+ Object.fromEntries(
+ Object.entries(currentDrafts).filter(
+ ([modelID]) => !visibleModelIDs.has(modelID),
+ ),
+ ),
+ );
+ setRowErrors((currentErrors) =>
+ Object.fromEntries(
+ Object.entries(currentErrors).filter(
+ ([modelID]) => !visibleModelIDs.has(modelID),
+ ),
+ ),
+ );
};
- const hasAnyPending = pendingModels.size > 0;
- const hasAnyErrors = Object.keys(rowErrors).length > 0;
- const hasAnyDrafts = Object.keys(drafts).length > 0;
+ const hasAnyPending = [...pendingModels].some((modelID) =>
+ visibleModelIDs.has(modelID),
+ );
+ const hasAnyErrors = Object.keys(rowErrors).some((modelID) =>
+ visibleModelIDs.has(modelID),
+ );
+ const hasAnyDrafts = Object.keys(drafts).some((modelID) =>
+ visibleModelIDs.has(modelID),
+ );
const shouldShowActions =
hasAnyDrafts || hasAnyErrors || hasAnyPending || dirtyRows.length > 0;
@@ -260,6 +306,26 @@ export const UserCompactionThresholdSettings: FC<
)}
)}
+ {organizationOptions.length > 1 && activeOrganization && (
+
+ {
+ if (!organization) {
+ return;
+ }
+ setSelectedOrganizationID(organization.id);
+ }}
+ />
+
+ )}
@@ -271,7 +337,7 @@ export const UserCompactionThresholdSettings: FC<
- {enabledModels.map((modelConfig) => {
+ {visibleModels.map((modelConfig) => {
const existingOverride = overridesByModelID.get(modelConfig.id);
const hasOverride = overridesByModelID.has(modelConfig.id);
const draftValue =
@@ -300,7 +366,7 @@ export const UserCompactionThresholdSettings: FC<
{modelName}
- {organizationName && (
-
- {organizationName}
-
- )}
{rowError && (
- {hasAnyPending && (
-
- )}
+ {hasAnyPending && }
{hasAnyPending
? "Saving..."
: `Save ${dirtyRows.length} ${dirtyRows.length === 1 ? "change" : "changes"}`}