Skip to content
22 changes: 22 additions & 0 deletions apps/web/src/components/ConnectionStatusDot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";

import { cn } from "~/lib/utils";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";

/** Canonical connection-phase → dot color mapping shared by every status dot. */
export function connectionPhaseDotClassName(phase: EnvironmentConnectionPhase): string {
switch (phase) {
case "connected":
return "bg-success";
case "connecting":
case "reconnecting":
return "bg-warning";
case "error":
return "bg-destructive";
default:
return "bg-muted-foreground/40";
}
}

/** Ping halo for transitional phases; null renders no ping. */
export function connectionPhasePingClassName(phase: EnvironmentConnectionPhase): string | null {
return phase === "connecting" || phase === "reconnecting" ? "bg-warning/60 duration-2000" : null;
}

type ConnectionStatusDotProps = {
tooltipText?: string | null;
dotClassName: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { EnvironmentId } from "@t3tools/contracts";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { reactHookHarness as hooks } from "../../test/reactHookHarness";

const settingsHooks = vi.hoisted(() => ({
read: vi.fn(() => ({ providerInstances: {} })),
update: vi.fn(() => vi.fn()),
}));

vi.mock("react", async (importOriginal) => {
const actual = await importOriginal<typeof import("react")>();
const { reactHookHarness } = await import("../../test/reactHookHarness");
return {
...actual,
useMemo: reactHookHarness.useMemo,
useState: reactHookHarness.useState,
};
});

vi.mock("react/compiler-runtime", async () => {
const { reactHookHarness } = await import("../../test/reactHookHarness");
return { c: reactHookHarness.useMemoCache };
});

vi.mock("../../hooks/useSettings", () => ({
useEnvironmentSettings: settingsHooks.read,
useUpdateEnvironmentSettings: settingsHooks.update,
}));

import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";

const remoteEnvironmentId = EnvironmentId.make("remote-device");

describe("AddProviderInstanceDialog environment routing", () => {
beforeEach(() => {
hooks.reset();
settingsHooks.read.mockClear();
settingsHooks.update.mockClear();
});

it("reads and writes settings through the supplied environment", () => {
hooks.beginRender();
AddProviderInstanceDialog({
open: true,
environmentId: remoteEnvironmentId,
environmentLabel: "Remote device",
onOpenChange: vi.fn(),
});

expect(settingsHooks.read).toHaveBeenCalledWith(remoteEnvironmentId);
expect(settingsHooks.update).toHaveBeenCalledWith(remoteEnvironmentId);
});
});
24 changes: 16 additions & 8 deletions apps/web/src/components/settings/AddProviderInstanceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { useMemo, useState } from "react";
import {
ProviderInstanceId,
ProviderDriverKind,
type EnvironmentId,
type ProviderInstanceConfig,
} from "@t3tools/contracts";

import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";
import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings";
import { cn } from "../../lib/utils";
import { normalizeProviderAccentColor } from "../../providerInstances";
import { Button } from "../ui/button";
Expand Down Expand Up @@ -115,13 +116,20 @@ function validateInstanceId(id: string, existing: ReadonlySet<string>): string |
}

interface AddProviderInstanceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
readonly open: boolean;
readonly environmentId: EnvironmentId;
readonly environmentLabel: string;
readonly onOpenChange: (open: boolean) => void;
}

export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) {
const settings = usePrimarySettings();
const updateSettings = useUpdatePrimarySettings();
export function AddProviderInstanceDialog({
open,
environmentId,
environmentLabel,
onOpenChange,
}: AddProviderInstanceDialogProps) {
const settings = useEnvironmentSettings(environmentId);
const updateSettings = useUpdateEnvironmentSettings(environmentId);

const [wizardStep, setWizardStep] = useState(0);
const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND);
Expand Down Expand Up @@ -227,8 +235,8 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
<DialogHeader>
<DialogTitle>Add provider instance</DialogTitle>
<DialogDescription>
Configure an additional provider instance — for example, a second Codex install
pointed at a different workspace.
Configure an additional provider instance on {environmentLabel} — for example, a
second Codex install pointed at a different workspace.
</DialogDescription>
<AddProviderInstanceWizardSteps
currentStep={wizardStep}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import type { ReactElement } from "react";
import {
DEFAULT_UNIFIED_SETTINGS,
EnvironmentId,
ProviderDriverKind,
ProviderInstanceId,
type ServerProvider,
type UnifiedSettings,
} from "@t3tools/contracts";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { visitElements } from "../../test/reactElementTree";
import { reactHookHarness as hooks } from "../../test/reactHookHarness";

const atoms = vi.hoisted(() => ({
providers: null as ReadonlyArray<ServerProvider> | null,
providersAtom: Symbol("providers"),
refreshProviders: Symbol("refreshProviders"),
updateProvider: Symbol("updateProvider"),
}));

const commands = vi.hoisted(() => ({
refresh: vi.fn(),
updateProvider: vi.fn(),
}));

const settingsState = vi.hoisted(() => ({
value: null as UnifiedSettings | null,
readEnvironmentIds: [] as EnvironmentId[],
updateEnvironmentIds: [] as EnvironmentId[],
updateSettings: vi.fn(),
}));

vi.mock("react", async (importOriginal) => {
const actual = await importOriginal<typeof import("react")>();
const { reactHookHarness } = await import("../../test/reactHookHarness");
return {
...actual,
useCallback: reactHookHarness.useCallback,
useMemo: reactHookHarness.useMemo,
useRef: reactHookHarness.useRef,
useState: reactHookHarness.useState,
};
});

vi.mock("react/compiler-runtime", async () => {
const { reactHookHarness } = await import("../../test/reactHookHarness");
return { c: reactHookHarness.useMemoCache };
});

vi.mock("@effect/atom-react", () => ({
useAtomValue: () => atoms.providers,
}));

vi.mock("../../state/server", () => ({
EMPTY_SERVER_PROVIDERS: [],
serverEnvironment: {
providersValueAtom: () => atoms.providersAtom,
refreshProviders: atoms.refreshProviders,
updateProvider: atoms.updateProvider,
},
}));

vi.mock("../../state/use-atom-command", () => ({
useAtomCommand: (atom: symbol) =>
atom === atoms.refreshProviders ? commands.refresh : commands.updateProvider,
}));

vi.mock("../../hooks/useSettings", () => ({
useEnvironmentSettings: (environmentId: EnvironmentId) => {
settingsState.readEnvironmentIds.push(environmentId);
return settingsState.value;
},
useUpdateEnvironmentSettings: (environmentId: EnvironmentId) => {
settingsState.updateEnvironmentIds.push(environmentId);
return settingsState.updateSettings;
},
}));

vi.mock("../../environments/primary", () => ({
usePrimarySessionState: () => ({ data: null, error: null, isPending: false, refresh: vi.fn() }),
}));

vi.mock("../../state/session", () => ({
useEnvironmentSessionState: () => ({ data: null, hasError: false, isPending: true }),
}));

import { EnvironmentProviderSettings } from "./ProviderSettingsPanel";

const environmentId = EnvironmentId.make("remote-device");
const codexId = ProviderInstanceId.make("codex");
const customId = ProviderInstanceId.make("codex_work");

function provider(): ServerProvider {
return {
instanceId: codexId,
driver: ProviderDriverKind.make("codex"),
enabled: true,
installed: true,
version: "1.0.0",
status: "ready",
auth: { status: "authenticated" },
checkedAt: "2026-07-24T12:00:00.000Z",
models: [],
slashCommands: [],
skills: [],
versionAdvisory: {
status: "behind_latest",
currentVersion: "1.0.0",
latestVersion: "1.1.0",
updateCommand: "pnpm add -g @openai/codex@latest",
canUpdate: true,
checkedAt: "2026-07-24T12:00:00.000Z",
message: "Update available.",
},
};
}

function renderPanel(options?: {
readonly readOnly?: boolean;
}): ReactElement<Record<string, unknown>> {
hooks.beginRender();
return EnvironmentProviderSettings({
environmentId,
environmentLabel: "Remote device",
...(options?.readOnly === undefined ? {} : { readOnly: options.readOnly }),
}) as ReactElement<Record<string, unknown>>;
}

async function flushPromises(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}

describe("EnvironmentProviderSettings routing", () => {
beforeEach(() => {
hooks.reset();
atoms.providers = null;
settingsState.value = DEFAULT_UNIFIED_SETTINGS;
settingsState.readEnvironmentIds = [];
settingsState.updateEnvironmentIds = [];
settingsState.updateSettings.mockReset();
commands.refresh.mockReset().mockResolvedValue({ _tag: "Success" });
commands.updateProvider.mockReset().mockResolvedValue({ _tag: "Success" });
});

it("coalesces a nullable provider snapshot before rendering array-backed UI", () => {
expect(() => renderPanel()).not.toThrow();
expect(settingsState.readEnvironmentIds).toEqual([environmentId]);
expect(settingsState.updateEnvironmentIds).toEqual([environmentId]);
});

it("routes refresh and provider update commands to the selected environment", async () => {
atoms.providers = [provider()];
const panel = renderPanel();
const refreshButton = visitElements(
panel,
(element) => element.props["aria-label"] === "Refresh provider status",
);
expect(refreshButton).not.toBeNull();
(refreshButton?.props.onClick as (() => void) | undefined)?.();
await flushPromises();

expect(commands.refresh).toHaveBeenCalledWith({ environmentId, input: {} });

const providerCard = visitElements(
panel,
(element) =>
element.props.instanceId === codexId && typeof element.props.onRunUpdate === "function",
);
expect(providerCard).not.toBeNull();
(providerCard?.props.onRunUpdate as (() => void) | undefined)?.();
await flushPromises();

expect(commands.updateProvider).toHaveBeenCalledWith({
environmentId,
input: { provider: ProviderDriverKind.make("codex"), instanceId: codexId },
});
});

it("renders the provider layout inert with a limited-permissions notice when read only", () => {
atoms.providers = [provider()];
const panel = renderPanel({ readOnly: true });

const inertWrapper = visitElements(panel, (element) => element.props.inert === true);
expect(inertWrapper).not.toBeNull();
const providerCard = visitElements(panel, (element) => element.props.instanceId === codexId);
expect(providerCard).not.toBeNull();

const notice = visitElements(panel, (element) => element.props.title === "Limited permissions");
expect(notice).not.toBeNull();

expect(
visitElements(panel, (element) => element.props["aria-label"] === "Add provider instance"),
).toBeNull();
expect(
visitElements(panel, (element) => element.props["aria-label"] === "Refresh provider status"),
).toBeNull();
});

it("keeps the editable layout interactive when not read only", () => {
atoms.providers = [provider()];
const panel = renderPanel();
expect(visitElements(panel, (element) => element.props.inert === true)).toBeNull();
expect(
visitElements(panel, (element) => element.props.title === "Limited permissions"),
).toBeNull();
});

it("deletes and resets provider configuration without erasing shared preferences", () => {
settingsState.value = {
...DEFAULT_UNIFIED_SETTINGS,
providerInstances: {
[codexId]: {
driver: ProviderDriverKind.make("codex"),
enabled: false,
},
[customId]: {
driver: ProviderDriverKind.make("codex"),
enabled: true,
},
},
providerModelPreferences: {
[customId]: { hiddenModels: ["hidden"], modelOrder: ["model"] },
},
favorites: [{ provider: customId, model: "favorite" }],
};
const panel = renderPanel();
const customCard = visitElements(panel, (element) => element.props.instanceId === customId);
expect(customCard).not.toBeNull();
(customCard?.props.onDelete as (() => void) | undefined)?.();

expect(settingsState.updateSettings).toHaveBeenLastCalledWith({
providerInstances: {
[codexId]: settingsState.value.providerInstances?.[codexId],
},
});

settingsState.updateSettings.mockClear();
const defaultCard = visitElements(panel, (element) => element.props.instanceId === codexId);
const resetAction = defaultCard?.props.headerAction;
const resetButton = visitElements(
resetAction,
(element) => typeof element.props.onClick === "function",
);
expect(resetButton).not.toBeNull();
(resetButton?.props.onClick as (() => void) | undefined)?.();

const resetPatch = settingsState.updateSettings.mock.lastCall?.[0] as
| Record<string, unknown>
| undefined;
expect(Object.keys(resetPatch ?? {}).sort()).toEqual(["providerInstances", "providers"]);
expect(resetPatch).not.toHaveProperty("favorites");
expect(resetPatch).not.toHaveProperty("providerModelPreferences");
});
});
Loading
Loading