{
+ await Promise.resolve();
+ await Promise.resolve();
+}
+
+describe("KimiSignInControl", () => {
+ beforeEach(() => {
+ hooks.reset();
+ state.atomCalls = [];
+ state.values.clear();
+ commands.signIn.mockReset().mockResolvedValue(undefined);
+ commands.signOut.mockReset().mockResolvedValue(undefined);
+ });
+
+ it("reads only the selected provider instance's sign-in state", () => {
+ state.values.set(`${environmentId}:${personalId}`, {
+ status: "waiting",
+ verificationUri: "https://auth.example/personal",
+ });
+
+ const personal = renderControl(personalId);
+ const work = renderControl(workId);
+
+ expect(state.atomCalls).toEqual([
+ [environmentId, personalId],
+ [environmentId, workId],
+ ]);
+ expect(
+ visitElements(personal, (element) => element.props.href === "https://auth.example/personal"),
+ ).not.toBeNull();
+ const liveStatus = visitElements(personal, (element) => element.props.role === "status");
+ expect(liveStatus?.props["aria-live"]).toBe("polite");
+ expect(visitElements(work, (element) => element.props.href !== undefined)).toBeNull();
+ });
+
+ it("announces sign-in failures as a polite live status", () => {
+ state.values.set(`${environmentId}:${workId}`, {
+ status: "failed",
+ message: "Kimi sign-in failed.",
+ });
+
+ const control = renderControl(workId);
+ const failure = visitElements(
+ control,
+ (element) => element.props.role === "status" && element.props["aria-live"] === "polite",
+ );
+
+ expect(failure).not.toBeNull();
+ expect(failure?.props.children).toBe("Kimi sign-in failed.");
+ });
+
+ it("signs out the authenticated provider instance", async () => {
+ const control = renderControl(workId, true);
+ const button = visitElements(control, (element) => typeof element.props.onClick === "function");
+
+ (button?.props.onClick as (() => void) | undefined)?.();
+ await flushPromises();
+
+ expect(commands.signOut).toHaveBeenCalledWith({
+ environmentId,
+ input: { instanceId: workId },
+ });
+ expect(commands.signIn).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/web/src/components/settings/KimiSignInControl.tsx b/apps/web/src/components/settings/KimiSignInControl.tsx
new file mode 100644
index 000000000000..74c03a6d9513
--- /dev/null
+++ b/apps/web/src/components/settings/KimiSignInControl.tsx
@@ -0,0 +1,125 @@
+import type { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
+import { useAtomValue } from "@effect/atom-react";
+import { CheckIcon, ExternalLinkIcon, LoaderIcon, LogOutIcon } from "lucide-react";
+import { useCallback, useRef, useState } from "react";
+
+import { serverEnvironment } from "../../state/server";
+import { useAtomCommand } from "../../state/use-atom-command";
+import { Button } from "../ui/button";
+
+/**
+ * "Sign in with Kimi" affordance for Kimi provider instances.
+ *
+ * Runs the server-side OAuth device flow (`kimiAuth.signIn`) and renders its
+ * progress inline: a start button, then the verification link and user code
+ * while the server polls for approval, then a brief confirmation. The server
+ * refreshes the provider probe on success, so the surrounding card flips to
+ * authenticated on its own.
+ */
+export function KimiSignInControl({
+ authenticated,
+ environmentId,
+ instanceId,
+}: {
+ readonly authenticated: boolean;
+ readonly environmentId: EnvironmentId;
+ readonly instanceId: ProviderInstanceId;
+}) {
+ const signInState = useAtomValue(
+ serverEnvironment.kimiSignInStateAtom(environmentId, instanceId),
+ );
+ const kimiSignIn = useAtomCommand(serverEnvironment.kimiSignIn, { reportFailure: false });
+ const kimiSignOut = useAtomCommand(serverEnvironment.kimiSignOut, { reportFailure: false });
+ const [isDispatching, setIsDispatching] = useState(false);
+ const dispatchingRef = useRef(false);
+
+ const startSignIn = useCallback(() => {
+ if (dispatchingRef.current) return;
+ dispatchingRef.current = true;
+ setIsDispatching(true);
+ void kimiSignIn({ environmentId, input: { instanceId } }).finally(() => {
+ dispatchingRef.current = false;
+ setIsDispatching(false);
+ });
+ }, [environmentId, instanceId, kimiSignIn]);
+ const startSignOut = useCallback(() => {
+ if (dispatchingRef.current) return;
+ dispatchingRef.current = true;
+ setIsDispatching(true);
+ void kimiSignOut({ environmentId, input: { instanceId } }).finally(() => {
+ dispatchingRef.current = false;
+ setIsDispatching(false);
+ });
+ }, [environmentId, instanceId, kimiSignOut]);
+
+ if (authenticated) {
+ return (
+
+ );
+ }
+
+ if (signInState.status === "waiting") {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {signInState.status === "failed" ? (
+
+ {signInState.message}
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx
index a663aa90990d..2d57374fe2a7 100644
--- a/apps/web/src/components/settings/ProviderInstanceCard.tsx
+++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx
@@ -342,6 +342,11 @@ interface ProviderInstanceCardProps {
* omit it.
*/
readonly headerAction?: ReactNode | undefined;
+ /**
+ * Driver-specific authentication affordance rendered under the auth
+ * summary row (e.g. Kimi's in-app "Sign in with Kimi" device flow).
+ */
+ readonly authAction?: ReactNode | undefined;
readonly hiddenModels: ReadonlyArray;
readonly favoriteModels: ReadonlyArray;
readonly modelOrder: ReadonlyArray;
@@ -384,6 +389,7 @@ export function ProviderInstanceCard({
onUpdate,
onDelete,
headerAction,
+ authAction,
hiddenModels,
favoriteModels,
modelOrder,
@@ -704,6 +710,7 @@ export function ProviderInstanceCard({
{titleTailNode}
{authRowNode}
+ {authAction ? {authAction}
: null}