authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-28 01:00:30-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-28 01:00:30-07:00
logf276e632c5a7952655e2455aef3b86c08164b988
tree580458cafc6a1e28efd490ae688c72747b108c70
parent5766dfbf55eee88dae89f42290bb86ba6f689600
signature Signed by PGP key B5690EEEBB952194

fix(web): stabilize the provider settings editor (#8472)


4 files changed, 102 insertions(+), 16 deletions(-)

apps/web/src/components/settings/ProviderInstanceCard.test.ts+49-2
......@@ -1,7 +1,14 @@
1import { createElement } from "react";
2import { renderToStaticMarkup } from "react-dom/server";
13import { describe, expect, it } from "vite-plus/test";
2import type { ServerProviderModel } from "@t3tools/contracts";
4import {
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type ServerProvider,
8 type ServerProviderModel,
9} from "@t3tools/contracts";
310
4import { deriveProviderModelsForDisplay } from "./ProviderInstanceCard";
11import { deriveProviderModelsForDisplay, ProviderInstanceCard } from "./ProviderInstanceCard";
512
613describe("deriveProviderModelsForDisplay", () => {
714 it("uses current config custom models instead of stale live custom rows", () => {
......@@ -33,4 +40,44 @@ describe("deriveProviderModelsForDisplay", () => {
3340 }).map((model) => model.slug),
3441 ).toEqual(["server-model", "kept-custom"]);
3542 });
43
44 it("shows a redacted provider email in Configuration", () => {
45 const instanceId = ProviderInstanceId.make("codex");
46 const driver = ProviderDriverKind.make("codex");
47 const liveProvider: ServerProvider = {
48 instanceId,
49 driver,
50 enabled: true,
51 installed: true,
52 version: "1.0.0",
53 status: "ready",
54 auth: { status: "authenticated", email: "developer@example.com" },
55 checkedAt: "2026-08-27T12:00:00.000Z",
56 models: [],
57 slashCommands: [],
58 skills: [],
59 };
60
61 const markup = renderToStaticMarkup(
62 createElement(ProviderInstanceCard, {
63 instanceId,
64 instance: { driver },
65 driverOption: undefined,
66 liveProvider,
67 mode: "editor",
68 onUpdate: () => undefined,
69 hiddenModels: [],
70 favoriteModels: [],
71 modelOrder: [],
72 onHiddenModelsChange: () => undefined,
73 onFavoriteModelsChange: () => undefined,
74 onModelOrderChange: () => undefined,
75 }),
76 );
77
78 expect(markup).toContain("Account email");
79 expect(markup).toContain('aria-label="Toggle account email visibility"');
80 expect(markup).toContain("blur-[2px]");
81 expect(markup).not.toContain("developer@example.com");
82 });
3683});
apps/web/src/components/settings/ProviderInstanceCard.tsx+35-6
......@@ -42,6 +42,7 @@ import { ProviderSettingsForm } from "./ProviderSettingsForm";
4242import { ProviderModelsSection } from "./ProviderModelsSection";
4343import { ProviderInstanceIcon, providerInstanceInitials } from "../chat/ProviderInstanceIcon";
4444import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker";
45import { RedactedSensitiveText } from "./RedactedSensitiveText";
4546import {
4647 getProviderVersionAdvisoryPresentation,
4748 PROVIDER_STATUS_STYLES,
......@@ -149,6 +150,21 @@ export function deriveProviderModelsForDisplay(input: {
149150 return [...serverModels, ...customModels];
150151}
151152
153function ProviderAuthEmail(props: { readonly email: string | undefined }) {
154 const email = props.email?.trim();
155 if (!email) return null;
156
157 return (
158 <RedactedSensitiveText
159 value={email}
160 ariaLabel="Toggle account email visibility"
161 revealTooltip="Click to reveal email"
162 hideTooltip="Click to hide email"
163 className="max-w-full truncate"
164 />
165 );
166}
167
152168function ProviderEnvironmentSection(props: {
153169 readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>;
154170 readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void;
......@@ -420,6 +436,7 @@ export function ProviderInstanceCard({
420436 const statusStyle = PROVIDER_STATUS_STYLES[statusKey];
421437 const rawSummary = getProviderSummary(liveProvider);
422438 const summary = enabled ? rawSummary : { headline: "Disabled", detail: null };
439 const authEmail = liveProvider?.auth.email;
423440 const showEditorStatus = enabled && (statusKey === "warning" || statusKey === "error");
424441 const versionLabel = getProviderVersionLabel(liveProvider?.version);
425442 const versionAdvisory = getProviderVersionAdvisoryPresentation(liveProvider?.versionAdvisory);
......@@ -636,12 +653,12 @@ export function ProviderInstanceCard({
636653 }
637654
638655 return (
639 <div className="min-w-0">
656 <div className="min-w-0 lg:flex lg:h-full lg:min-h-0 lg:flex-col">
640657 <div
641658 inert={readOnly}
642659 aria-disabled={readOnly || undefined}
643660 className={cn(
644 "flex min-h-16 items-center justify-between gap-3 border-b border-border/70 px-4 py-3",
661 "flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/70 px-4 py-3",
645662 readOnly && "opacity-50 select-none",
646663 )}
647664 >
......@@ -755,7 +772,7 @@ export function ProviderInstanceCard({
755772 </div>
756773 </div>
757774
758 <div className="flex h-11 border-b border-border/70 px-1">
775 <div className="flex h-11 shrink-0 border-b border-border/70 px-1">
759776 {driverOption !== undefined ? (
760777 <button
761778 type="button"
......@@ -779,9 +796,21 @@ export function ProviderInstanceCard({
779796 <div
780797 inert={readOnly}
781798 aria-disabled={readOnly || undefined}
782 className={cn("px-4 py-5", readOnly && "opacity-50 select-none")}
799 className={cn("lg:min-h-0 lg:flex-1", readOnly && "opacity-50 select-none")}
783800 >
784 <div className="space-y-5" hidden={visibleTab !== "configuration"}>
801 <div
802 className="space-y-5 px-4 py-5 lg:h-full lg:overflow-y-auto"
803 hidden={visibleTab !== "configuration"}
804 >
805 {authEmail?.trim() ? (
806 <div className="min-w-0">
807 <div className="text-xs font-medium text-foreground">Account email</div>
808 <div className="mt-1.5 flex min-h-8 min-w-0 items-center">
809 <ProviderAuthEmail email={authEmail} />
810 </div>
811 </div>
812 ) : null}
813
785814 <div>
786815 <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block">
787816 <span className="text-xs font-medium text-foreground">Display name</span>
......@@ -838,7 +867,7 @@ export function ProviderInstanceCard({
838867 ) : null}
839868 </div>
840869 {driverOption !== undefined ? (
841 <div hidden={visibleTab !== "models"}>
870 <div className="px-4 py-5 lg:h-full lg:min-h-0" hidden={visibleTab !== "models"}>
842871 <ProviderModelsSection
843872 instanceId={instanceId}
844873 driverKind={driverKind}
apps/web/src/components/settings/ProviderModelsSection.tsx+5-2
......@@ -185,12 +185,15 @@ export function ProviderModelsSection({
185185 };
186186
187187 return (
188 <div>
188 <div className="lg:flex lg:h-full lg:min-h-0 lg:flex-col">
189189 <div className="text-xs font-medium text-foreground">Models</div>
190190 <div className="mt-1 text-xs text-muted-foreground">
191191 {models.length} model{models.length === 1 ? "" : "s"} available.
192192 </div>
193 <div ref={listRef} className="mt-2 max-h-40 overflow-y-auto pb-1">
193 <div
194 ref={listRef}
195 className="mt-2 max-h-40 overflow-y-auto pb-1 lg:min-h-0 lg:max-h-none lg:flex-1"
196 >
194197 {orderedModels.map((model, index) => {
195198 const caps = model.capabilities;
196199 const capLabels: string[] = [];
apps/web/src/components/settings/ProviderSettingsPanel.tsx+13-6
......@@ -837,14 +837,21 @@ export function EnvironmentProviderSettings({
837837 />
838838 ) : null}
839839 <div className="space-y-1">
840 <div className="overflow-hidden rounded-lg border border-border/70 lg:grid lg:grid-cols-[20rem_minmax(0,1fr)]">
841 <div className="border-b border-border/70 lg:border-r lg:border-b-0">
842 <div className="flex min-h-9 items-center justify-between border-b border-border/70 px-3 text-[11px] font-medium text-muted-foreground">
840 <div className="overflow-hidden rounded-lg border border-border/70 lg:grid lg:h-[min(38rem,calc(100dvh-16rem))] lg:min-h-[30rem] lg:grid-cols-[20rem_minmax(0,1fr)]">
841 <div className="border-b border-border/70 lg:flex lg:min-h-0 lg:flex-col lg:border-r lg:border-b-0">
842 <div className="flex min-h-9 shrink-0 items-center justify-between border-b border-border/70 px-3 text-[11px] font-medium text-muted-foreground">
843843 <span>Provider</span>
844844 <span>On</span>
845845 </div>
846 {rows.map((row) => renderProviderInstance(row, "list"))}
847 <div className="flex min-h-10 items-center justify-between px-3">
846 <div className="lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
847 {rows.map((row) => renderProviderInstance(row, "list"))}
848 </div>
849 <div
850 className={cn(
851 "flex min-h-10 shrink-0 items-center justify-between px-3",
852 rows.length > 0 && "border-t border-border/60",
853 )}
854 >
848855 <ProviderLastChecked lastCheckedAt={lastCheckedAt} />
849856 {!readOnly ? (
850857 <Tooltip>
......@@ -871,7 +878,7 @@ export function EnvironmentProviderSettings({
871878 </div>
872879 </div>
873880
874 <div className="min-w-0">
881 <div className="min-w-0 lg:min-h-0">
875882 {selectedRow ? (
876883 renderProviderInstance(selectedRow, "editor")
877884 ) : (