| author | |
| committer | |
| log | f276e632c5a7952655e2455aef3b86c08164b988 |
| tree | 580458cafc6a1e28efd490ae688c72747b108c70 |
| parent | 5766dfbf55eee88dae89f42290bb86ba6f689600 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 102 insertions(+), 16 deletions(-)
apps/web/src/components/settings/ProviderInstanceCard.test.ts+49-2| ... | ... | @@ -1,7 +1,14 @@ |
| 1 | import { createElement } from "react"; | |
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | |
| 1 | 3 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import type { ServerProviderModel } from "@t3tools/contracts"; | |
| 4 | import { | |
| 5 | ProviderDriverKind, | |
| 6 | ProviderInstanceId, | |
| 7 | type ServerProvider, | |
| 8 | type ServerProviderModel, | |
| 9 | } from "@t3tools/contracts"; | |
| 3 | 10 | |
| 4 | import { deriveProviderModelsForDisplay } from "./ProviderInstanceCard"; | |
| 11 | import { deriveProviderModelsForDisplay, ProviderInstanceCard } from "./ProviderInstanceCard"; | |
| 5 | 12 | |
| 6 | 13 | describe("deriveProviderModelsForDisplay", () => { |
| 7 | 14 | it("uses current config custom models instead of stale live custom rows", () => { |
| ... | ... | @@ -33,4 +40,44 @@ describe("deriveProviderModelsForDisplay", () => { |
| 33 | 40 | }).map((model) => model.slug), |
| 34 | 41 | ).toEqual(["server-model", "kept-custom"]); |
| 35 | 42 | }); |
| 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 | }); | |
| 36 | 83 | }); |
apps/web/src/components/settings/ProviderInstanceCard.tsx+35-6| ... | ... | @@ -42,6 +42,7 @@ import { ProviderSettingsForm } from "./ProviderSettingsForm"; |
| 42 | 42 | import { ProviderModelsSection } from "./ProviderModelsSection"; |
| 43 | 43 | import { ProviderInstanceIcon, providerInstanceInitials } from "../chat/ProviderInstanceIcon"; |
| 44 | 44 | import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker"; |
| 45 | import { RedactedSensitiveText } from "./RedactedSensitiveText"; | |
| 45 | 46 | import { |
| 46 | 47 | getProviderVersionAdvisoryPresentation, |
| 47 | 48 | PROVIDER_STATUS_STYLES, |
| ... | ... | @@ -149,6 +150,21 @@ export function deriveProviderModelsForDisplay(input: { |
| 149 | 150 | return [...serverModels, ...customModels]; |
| 150 | 151 | } |
| 151 | 152 | |
| 153 | function 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 | ||
| 152 | 168 | function ProviderEnvironmentSection(props: { |
| 153 | 169 | readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>; |
| 154 | 170 | readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void; |
| ... | ... | @@ -420,6 +436,7 @@ export function ProviderInstanceCard({ |
| 420 | 436 | const statusStyle = PROVIDER_STATUS_STYLES[statusKey]; |
| 421 | 437 | const rawSummary = getProviderSummary(liveProvider); |
| 422 | 438 | const summary = enabled ? rawSummary : { headline: "Disabled", detail: null }; |
| 439 | const authEmail = liveProvider?.auth.email; | |
| 423 | 440 | const showEditorStatus = enabled && (statusKey === "warning" || statusKey === "error"); |
| 424 | 441 | const versionLabel = getProviderVersionLabel(liveProvider?.version); |
| 425 | 442 | const versionAdvisory = getProviderVersionAdvisoryPresentation(liveProvider?.versionAdvisory); |
| ... | ... | @@ -636,12 +653,12 @@ export function ProviderInstanceCard({ |
| 636 | 653 | } |
| 637 | 654 | |
| 638 | 655 | 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"> | |
| 640 | 657 | <div |
| 641 | 658 | inert={readOnly} |
| 642 | 659 | aria-disabled={readOnly || undefined} |
| 643 | 660 | 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", | |
| 645 | 662 | readOnly && "opacity-50 select-none", |
| 646 | 663 | )} |
| 647 | 664 | > |
| ... | ... | @@ -755,7 +772,7 @@ export function ProviderInstanceCard({ |
| 755 | 772 | </div> |
| 756 | 773 | </div> |
| 757 | 774 | |
| 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"> | |
| 759 | 776 | {driverOption !== undefined ? ( |
| 760 | 777 | <button |
| 761 | 778 | type="button" |
| ... | ... | @@ -779,9 +796,21 @@ export function ProviderInstanceCard({ |
| 779 | 796 | <div |
| 780 | 797 | inert={readOnly} |
| 781 | 798 | 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")} | |
| 783 | 800 | > |
| 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 | ||
| 785 | 814 | <div> |
| 786 | 815 | <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block"> |
| 787 | 816 | <span className="text-xs font-medium text-foreground">Display name</span> |
| ... | ... | @@ -838,7 +867,7 @@ export function ProviderInstanceCard({ |
| 838 | 867 | ) : null} |
| 839 | 868 | </div> |
| 840 | 869 | {driverOption !== undefined ? ( |
| 841 | <div hidden={visibleTab !== "models"}> | |
| 870 | <div className="px-4 py-5 lg:h-full lg:min-h-0" hidden={visibleTab !== "models"}> | |
| 842 | 871 | <ProviderModelsSection |
| 843 | 872 | instanceId={instanceId} |
| 844 | 873 | driverKind={driverKind} |
apps/web/src/components/settings/ProviderModelsSection.tsx+5-2| ... | ... | @@ -185,12 +185,15 @@ export function ProviderModelsSection({ |
| 185 | 185 | }; |
| 186 | 186 | |
| 187 | 187 | return ( |
| 188 | <div> | |
| 188 | <div className="lg:flex lg:h-full lg:min-h-0 lg:flex-col"> | |
| 189 | 189 | <div className="text-xs font-medium text-foreground">Models</div> |
| 190 | 190 | <div className="mt-1 text-xs text-muted-foreground"> |
| 191 | 191 | {models.length} model{models.length === 1 ? "" : "s"} available. |
| 192 | 192 | </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 | > | |
| 194 | 197 | {orderedModels.map((model, index) => { |
| 195 | 198 | const caps = model.capabilities; |
| 196 | 199 | const capLabels: string[] = []; |
apps/web/src/components/settings/ProviderSettingsPanel.tsx+13-6| ... | ... | @@ -837,14 +837,21 @@ export function EnvironmentProviderSettings({ |
| 837 | 837 | /> |
| 838 | 838 | ) : null} |
| 839 | 839 | <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"> | |
| 843 | 843 | <span>Provider</span> |
| 844 | 844 | <span>On</span> |
| 845 | 845 | </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 | > | |
| 848 | 855 | <ProviderLastChecked lastCheckedAt={lastCheckedAt} /> |
| 849 | 856 | {!readOnly ? ( |
| 850 | 857 | <Tooltip> |
| ... | ... | @@ -871,7 +878,7 @@ export function EnvironmentProviderSettings({ |
| 871 | 878 | </div> |
| 872 | 879 | </div> |
| 873 | 880 | |
| 874 | <div className="min-w-0"> | |
| 881 | <div className="min-w-0 lg:min-h-0"> | |
| 875 | 882 | {selectedRow ? ( |
| 876 | 883 | renderProviderInstance(selectedRow, "editor") |
| 877 | 884 | ) : ( |