Q172: disconnecting a provider shows what it stops, then the outcome

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 04:25:31 -04:00
1 parent 059970e7e1
commit 538d834647
12 files changed
+170 -1

No files matched your search

+9
View File
@@ -695,4 +695,13 @@ export const zh: Record<string, string> = {
"Zero data retention switched background off.": "零数据保留已关闭后台执行。",
"Resets {date} (UTC)": "{date} 重置(UTC)",
"Unsaved": "未保存",
"Disconnect {label}?": "断开 {label}?",
"Checking what this affects…": "正在检查影响范围…",
"Agents on this provider stop working until it is reconnected or they move to another provider: {names}.": "使用该 Provider 的 Agent 将停止工作,直到重新连接或改用其他 Provider:{names}。",
"No agent uses this provider.": "没有 Agent 使用该 Provider。",
"Open tasks assigned to them: {count}.": "分配给它们的未完成任务:{count}。",
"Running sessions that stop now: {count}.": "立即停止的运行中会话:{count}。",
"Queued sessions that will fail when they start: {count}.": "开始时将失败的排队会话:{count}。",
"Disconnecting…": "正在断开…",
"{label} is disconnected.": "{label} 已断开。",
};
@@ -0,0 +1,66 @@
import { useEffect, useState } from "react";
import type { ProviderDisconnectImpact } from "../../../../shared/commands";
import type { ProviderRecord } from "../../../../shared/domain";
import type { SnapshotActions } from "../../app/useSnapshot";
import { Dialog } from "../../shared/Dialog";
import { t } from "../../i18n/i18n";
/** Shows what a disconnect stops before it happens, then its outcome, including a provider that refused to revoke. */
export function DisconnectProviderDialog({ orgId, provider, revokesRemotely, actions, onClose }: {
orgId: string;
provider: ProviderRecord;
revokesRemotely: boolean;
actions: SnapshotActions;
onClose: () => void;
}) {
const [impact, setImpact] = useState<ProviderDisconnectImpact | null>(null);
const [state, setState] = useState<{ kind: "idle" | "busy" | "done" } | { kind: "error"; message: string }>({ kind: "idle" });
useEffect(() => {
let live = true;
void window.agentCompany.providerDisconnectImpact({ orgId, providerId: provider.id }).then((result) => {
if (!live) return;
if (result.ok) setImpact(result.data);
else setState({ kind: "error", message: result.error.message });
});
return () => { live = false; };
}, [orgId, provider.id]);
const disconnect = () => {
setState({ kind: "busy" });
actions.run(() => window.agentCompany.revokeProviderCredential({ orgId, providerId: provider.id }))
.then(() => setState({ kind: "done" }))
.catch((failure: unknown) => setState({ kind: "error", message: failure instanceof Error ? failure.message : String(failure) }));
};
const finished = state.kind === "done";
return (
<Dialog
title={t("Disconnect {label}?", { label: provider.label })}
open
submitLabel={state.kind === "busy" ? t("Disconnecting…") : t("Disconnect")}
hideSubmit={finished || impact === null}
onClose={onClose}
onSubmit={(event) => { event.preventDefault(); if (state.kind !== "busy") disconnect(); }}
>
{finished ? (
<p role="status" style={{ margin: 0 }}>{t("{label} is disconnected.", { label: provider.label })}</p>
) : impact === null ? (
state.kind !== "error" && <p style={{ margin: 0, color: "var(--text-3)" }}>{t("Checking what this affects…")}</p>
) : (
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: 13 }}>
<li>{impact.agents.length
? t("Agents on this provider stop working until it is reconnected or they move to another provider: {names}.", { names: impact.agents.join(", ") })
: t("No agent uses this provider.")}</li>
{impact.agents.length > 0 && <li>{t("Open tasks assigned to them: {count}.", { count: impact.openTasks })}</li>}
<li>{t("Running sessions that stop now: {count}.", { count: impact.runningRuns })}</li>
<li>{t("Queued sessions that will fail when they start: {count}.", { count: impact.queuedRuns })}</li>
<li>{revokesRemotely
? t("Revokes the sign-in at {provider} and erases it from this computer.", { provider: provider.label })
: t("Erases the key from this computer. {provider} keeps accepting it until you revoke it in your account.", { provider: provider.label })}</li>
</ul>
)}
{state.kind === "error" && <div className="form-error" role="alert">{state.message}</div>}
</Dialog>
);
}
@@ -15,6 +15,7 @@ import { ProviderLogo } from "../../shared/ProviderLogo";
import { ModelPicker, ProviderConnectFields, ProviderOAuthFields } from "./ProviderConnectionDialog";
import { composeSecret } from "./provider-secret";
import { ProviderPolicyTable } from "./ProviderPolicyTable";
import { DisconnectProviderDialog } from "./DisconnectProviderDialog";
import type { SnapshotActions } from "../../app/useSnapshot";
interface ProvidersPanelProps {
@@ -35,6 +36,7 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
const [active, setActive] = useState<ProviderRecord | null>(null);
const [modelFor, setModelFor] = useState<ProviderRecord | null>(null);
const [modelError, setModelError] = useState<string | null>(null);
const [disconnecting, setDisconnecting] = useState<ProviderRecord | null>(null);
const openModel = (provider: ProviderRecord | null) => { setModelError(null); setModelFor(provider); };
const modelForEntry = modelFor ? requireCatalogEntry(catalog, modelFor.providerKey) : null;
const connected = providers.filter((provider) => provider.enabled);
@@ -88,7 +90,7 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
onCheck={() => void actions.run(() => window.agentCompany.checkProviderHealth({ orgId, providerId: provider.id }))}
onManage={() => setActive(provider)}
onChangeModel={provider.providerKey === "azure-openai" ? null : () => openModel(provider)}
onRevoke={() => void actions.run(() => window.agentCompany.revokeProviderCredential({ orgId, providerId: provider.id }))}
onRevoke={() => setDisconnecting(provider)}
revokesRemotely={requireCatalogEntry(catalog, provider.providerKey).revokesRemotely}
/>
))}
@@ -142,6 +144,17 @@ export function ProvidersPanel({ orgId, companyName, employeeId, providers, auth
)}
</Dialog>
{disconnecting && (
<DisconnectProviderDialog
key={disconnecting.id}
orgId={orgId}
provider={disconnecting}
revokesRemotely={requireCatalogEntry(catalog, disconnecting.providerKey).revokesRemotely}
actions={actions}
onClose={() => setDisconnecting(null)}
/>
)}
{/* Only the model changes here: the credential and its sign-in stay as they are. */}
<Dialog
key={modelFor?.id ?? "none"}