FN-8711: preserve credential instances in settings actions
Preserve selected credential-instance identity across dashboard authentication actions. - Route named-account OAuth and API-key actions with their instance IDs. - Retain sibling credential accounts during targeted OAuth polling. - Document the default-versus-named credential contract and extend desktop/mobile coverage. Files changed: ...n-8711-settings-credential-instance-contract.md | 7 ++ docs/settings-reference.md | 2 +- .../app/__tests__/settings-moved-keys.test.ts | 2 + .../dashboard/app/components/SettingsModal.tsx | 49 +++++++++++--- .../__tests__/SettingsModal.models-auth.test.tsx | 78 ++++++++++++++++++++++ .../components/__tests__/settings-mobile.test.tsx | 14 ++-- .../settings/sections/AuthenticationSection.tsx | 15 +++-- .../settings-default-descriptions.test.tsx | 19 ++++++ 8 files changed, 165 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-8711 Fusion-Task-Lineage: f43eaa27-4d36-4a3d-acf3-caba54e31857 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep named Authentication credential actions targeted to the selected account.
|
||||
category: fix
|
||||
dev: Settings omits default-instance arguments and preserves explicit credential instance ids for OAuth and API-key actions.
|
||||
@@ -1894,4 +1894,4 @@ already in progress continues on its existing session.
|
||||
|
||||
### Authentication credential instances
|
||||
|
||||
Settings → Authentication can hold multiple named credential accounts for each non-CLI provider. Select **Add another account** to create a client-generated account id, optionally label it, and complete OAuth or save an API key; an abandoned pending account is not stored. The first credential becomes the provider default; later accounts do not change it. Operators can rename, remove, or make an existing account default. Labels are display-only, optional, and need not be unique. CLI-backed provider cards retain their own credential handling and do not support Fusion credential instances.
|
||||
Settings → Authentication can hold multiple named credential accounts for each non-CLI provider. Select **Add another account** to create a client-generated account id, optionally label it, and complete OAuth or save an API key; an abandoned pending account is not stored. The first credential becomes the provider default; later accounts do not change it. Authentication actions without a selected named account target that provider default, while actions on a named account retain its instance id through login, cancellation, logout, save, and clear. Operators can rename, remove, or make an existing account default. Labels are display-only, optional, and need not be unique. CLI-backed provider cards retain their own credential handling and do not support Fusion credential instances.
|
||||
|
||||
@@ -47,6 +47,8 @@ const SURFACE_FILES = [
|
||||
const PRESET_NESTED_KEYS = new Set([
|
||||
"validatorProvider",
|
||||
"validatorModelId",
|
||||
// FNXC:SettingsCredentialInstance 2026-08-01-17:06: Validator instance selection is valid only inside a model preset; the form-read guard remains active for its workflow-owned top-level setting.
|
||||
"validatorCredentialInstanceId",
|
||||
]);
|
||||
|
||||
describe("SettingsModal moved-key removal sweep", () => {
|
||||
|
||||
@@ -2454,6 +2454,10 @@ export function SettingsModal({
|
||||
void copyTextToClipboard(copilotDeviceCode.userCode);
|
||||
}, [deviceCodes]);
|
||||
|
||||
/*
|
||||
FNXC:SettingsCredentialInstance 2026-08-01-17:06:
|
||||
Settings must omit the optional instance argument for a provider's default credential, while every explicit account id follows its OAuth or API-key action unchanged. This preserves the API's default-provider compatibility and prevents UI-local state from conflating default and named account flows.
|
||||
*/
|
||||
const handleLogin = useCallback(async (providerId: string, instanceId?: string, label?: string) => {
|
||||
const stateKey = formatProviderInstanceKey({ providerId, instanceId: instanceId ?? "default" });
|
||||
const provider = authProviders.find((entry) => entry.id === providerId);
|
||||
@@ -2473,7 +2477,11 @@ export function SettingsModal({
|
||||
clearAuthLoginUiState(stateKey);
|
||||
|
||||
try {
|
||||
const { url, instructions, manualCode, deviceCode } = await loginProvider(providerId, instanceId, label);
|
||||
const { url, instructions, manualCode, deviceCode } = instanceId === undefined
|
||||
? await loginProvider(providerId)
|
||||
: label === undefined
|
||||
? await loginProvider(providerId, instanceId)
|
||||
: await loginProvider(providerId, instanceId, label);
|
||||
if (instructions?.trim() && !(providerId === "github-copilot" && deviceCode)) {
|
||||
setLoginInstructions((prev) => ({ ...prev, [stateKey]: instructions }));
|
||||
}
|
||||
@@ -2491,9 +2499,8 @@ export function SettingsModal({
|
||||
pollIntervalRef.current[stateKey] = setInterval(async () => {
|
||||
try {
|
||||
const { providers } = await fetchAuthStatus({ provider: providerId, instance: instanceId });
|
||||
const visibleProviders = filterVisibleOnboardingAndSettingsProviders(providers);
|
||||
setAuthProviders(visibleProviders);
|
||||
const provider = visibleProviders.find((p) => p.id === providerId);
|
||||
const provider = providers.find((candidate) => candidate.id === providerId
|
||||
&& (candidate.instanceId ?? "default") === (instanceId ?? "default"));
|
||||
if (provider?.authenticated) {
|
||||
if (pollIntervalRef.current[stateKey]) {
|
||||
clearInterval(pollIntervalRef.current[stateKey]);
|
||||
@@ -2501,6 +2508,14 @@ export function SettingsModal({
|
||||
}
|
||||
setAuthActionInProgress((prev) => { const next = { ...prev }; delete next[stateKey]; return next; });
|
||||
clearAuthLoginUiState(stateKey);
|
||||
/*
|
||||
FNXC:SettingsCredentialInstance 2026-08-01-17:49:
|
||||
A targeted OAuth poll reports only the requested account's instance rows. Do not
|
||||
replace Authentication's full provider envelope with that scoped response: sibling
|
||||
providers and named accounts must remain visible while the login finishes. Refresh
|
||||
the unscoped status only after this requested instance is terminal.
|
||||
*/
|
||||
await loadAuthStatus().catch(() => {});
|
||||
addToast(t("settings.auth.loginSuccessful", "Login successful"), "success");
|
||||
window.dispatchEvent(new CustomEvent(OAUTH_RELOGIN_SUCCESS_EVENT, { detail: { providerId } }));
|
||||
scrollSettingsToTop();
|
||||
@@ -2544,7 +2559,9 @@ export function SettingsModal({
|
||||
|
||||
setManualCodeSubmitInProgress(stateKey);
|
||||
try {
|
||||
const result = await submitProviderManualCode(providerId, code, instanceId);
|
||||
const result = instanceId === undefined
|
||||
? await submitProviderManualCode(providerId, code)
|
||||
: await submitProviderManualCode(providerId, code, instanceId);
|
||||
if (result.submitted) {
|
||||
setManualCodeInputs((prev) => {
|
||||
if (!(stateKey in prev)) {
|
||||
@@ -2570,7 +2587,11 @@ export function SettingsModal({
|
||||
setAuthActionInProgress((prev) => ({ ...prev, [stateKey]: true }));
|
||||
// Provider status is shared; do not optimistically clear a concurrent instance's login flag.
|
||||
try {
|
||||
await cancelProviderLogin(providerId, instanceId);
|
||||
if (instanceId === undefined) {
|
||||
await cancelProviderLogin(providerId);
|
||||
} else {
|
||||
await cancelProviderLogin(providerId, instanceId);
|
||||
}
|
||||
clearAuthLoginUiState(stateKey);
|
||||
await loadAuthStatus().catch(() => {});
|
||||
addToast(t("settings.auth.loginCancelled", "Login cancelled"), "success");
|
||||
@@ -2590,7 +2611,11 @@ export function SettingsModal({
|
||||
const stateKey = formatProviderInstanceKey({ providerId, instanceId: instanceId ?? "default" });
|
||||
setAuthActionInProgress((prev) => ({ ...prev, [stateKey]: true }));
|
||||
try {
|
||||
await logoutProvider(providerId, instanceId);
|
||||
if (instanceId === undefined) {
|
||||
await logoutProvider(providerId);
|
||||
} else {
|
||||
await logoutProvider(providerId, instanceId);
|
||||
}
|
||||
await loadAuthStatus();
|
||||
addToast(t("settings.auth.loggedOut", "Logged out"), "success");
|
||||
} catch (err) {
|
||||
@@ -2614,7 +2639,9 @@ export function SettingsModal({
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
const saveResult = await saveApiKey(providerId, key, instanceId, label);
|
||||
const saveResult = instanceId === undefined
|
||||
? await saveApiKey(providerId, key)
|
||||
: await saveApiKey(providerId, key, instanceId, label);
|
||||
setApiKeyInputs((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[stateKey];
|
||||
@@ -2677,7 +2704,11 @@ export function SettingsModal({
|
||||
const stateKey = formatProviderInstanceKey({ providerId, instanceId: instanceId ?? "default" });
|
||||
setAuthActionInProgress((prev) => ({ ...prev, [stateKey]: true }));
|
||||
try {
|
||||
await clearApiKey(providerId, instanceId);
|
||||
if (instanceId === undefined) {
|
||||
await clearApiKey(providerId);
|
||||
} else {
|
||||
await clearApiKey(providerId, instanceId);
|
||||
}
|
||||
setApiKeyInputs((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[stateKey];
|
||||
|
||||
@@ -1254,6 +1254,64 @@ describe("SettingsModal", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves sibling providers and named accounts while polling a named OAuth login", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
mockFetchAuthStatus
|
||||
.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{
|
||||
id: "anthropic-subscription",
|
||||
name: "Anthropic Subscription",
|
||||
authenticated: false,
|
||||
type: "oauth",
|
||||
instanceId: "work",
|
||||
instances: [
|
||||
{ instanceId: "work", label: "Work", isDefault: true, authenticated: false, type: "oauth" },
|
||||
{ instanceId: "personal", label: "Personal", isDefault: false, authenticated: false, type: "oauth" },
|
||||
],
|
||||
},
|
||||
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{
|
||||
id: "anthropic-subscription",
|
||||
name: "Anthropic Subscription",
|
||||
authenticated: false,
|
||||
type: "oauth",
|
||||
instanceId: "work",
|
||||
loginInProgress: true,
|
||||
instances: [{ instanceId: "work", label: "Work", isDefault: true, authenticated: false, type: "oauth" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
mockLoginProvider.mockResolvedValueOnce({ url: "https://claude.ai/oauth/authorize" });
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
await settingsModalUser.click(screen.getByRole("button", { name: "Authentication" }));
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const instances = screen.getByTestId("auth-instances-anthropic-subscription");
|
||||
fireEvent.click(within(instances).getAllByRole("button", { name: "Login" })[0]);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
|
||||
expect(mockLoginProvider).toHaveBeenCalledWith("anthropic-subscription", "work");
|
||||
expect(mockFetchAuthStatus).toHaveBeenLastCalledWith({ provider: "anthropic-subscription", instance: "work" });
|
||||
expect(openSpy).toHaveBeenCalledWith("https://claude.ai/oauth/authorize", "_blank");
|
||||
expect(screen.getByText("Personal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-provider-icon-github")).toBeInTheDocument();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows incomplete toast when Anthropic Subscription OAuth stops without authentication", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
const addToast = vi.fn();
|
||||
@@ -1559,6 +1617,26 @@ describe("SettingsModal", () => {
|
||||
expect(mockClearApiKey).toHaveBeenCalledWith("anthropic-api-key");
|
||||
});
|
||||
|
||||
it("routes named credential instances through their matching auth actions", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValue({
|
||||
providers: [
|
||||
{ id: "anthropic-subscription", name: "Anthropic Subscription", authenticated: true, type: "oauth", instanceId: "work" },
|
||||
{ id: "anthropic-api-key", name: "Anthropic API Key", authenticated: true, type: "api_key", instanceId: "billing", keyHint: "sk-•••••work" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={noop} addToast={vi.fn()} />);
|
||||
await settingsModalUser.click(await screen.findByRole("button", { name: "Authentication" }));
|
||||
|
||||
const subscriptionCard = screen.getByTestId("auth-provider-icon-anthropic-subscription").closest(".auth-provider-card") as HTMLElement;
|
||||
const apiKeyCard = screen.getByTestId("auth-provider-icon-anthropic-api-key").closest(".auth-provider-card") as HTMLElement;
|
||||
await settingsModalUser.click(within(subscriptionCard).getByRole("button", { name: "Logout" }));
|
||||
await settingsModalUser.click(within(apiKeyCard).getByRole("button", { name: "Clear" }));
|
||||
|
||||
expect(mockLogoutProvider).toHaveBeenCalledWith("anthropic-subscription", "work");
|
||||
expect(mockClearApiKey).toHaveBeenCalledWith("anthropic-api-key", "billing");
|
||||
});
|
||||
|
||||
it("scrolls settings content to top after API key save succeeds", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" }],
|
||||
|
||||
@@ -54,7 +54,11 @@ vi.mock("../../api", () => ({
|
||||
{ id: "anthropic-subscription", name: "Anthropic Subscription", authenticated: false, type: "oauth" },
|
||||
{ id: "anthropic-api-key", name: "Anthropic API Key", authenticated: false, type: "api_key" },
|
||||
] })),
|
||||
// FNXC:SettingsCredentialInstance 2026-08-01-17:06: Mobile Authentication uses the same default-or-named instance key as desktop so responsive rendering cannot collapse provider action state.
|
||||
formatProviderInstanceKey: ({ providerId, instanceId }: { providerId: string; instanceId: string }) => instanceId === "default" ? providerId : `${providerId}[${instanceId}]`,
|
||||
loginProvider: vi.fn(() => Promise.resolve({ url: "https://auth.example.com/login" })),
|
||||
submitProviderManualCode: vi.fn(() => Promise.resolve({ success: true, submitted: true })),
|
||||
cancelProviderLogin: vi.fn(() => Promise.resolve({ success: true, cancelled: true })),
|
||||
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
|
||||
saveApiKey: vi.fn(() => Promise.resolve({ success: true })),
|
||||
clearApiKey: vi.fn(() => Promise.resolve({ success: true })),
|
||||
@@ -155,7 +159,7 @@ vi.mock("../../hooks/useMemoryBackendStatus", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
import { fetchDashboardHealth, fetchSettings, updateSettings } from "../../api";
|
||||
import { fetchDashboardHealth, fetchSettings, loginProvider, saveApiKey, updateSettings } from "../../api";
|
||||
|
||||
function setDocumentHidden(hidden: boolean): void {
|
||||
Object.defineProperty(document, "hidden", { configurable: true, value: hidden });
|
||||
@@ -724,10 +728,12 @@ describe("SettingsModal mobile adaptations", () => {
|
||||
|
||||
const subscriptionCard = (await findByTestId("auth-provider-icon-anthropic-subscription")).closest(".auth-provider-card") as HTMLElement;
|
||||
const apiKeyCard = (await findByTestId("auth-provider-icon-anthropic-api-key")).closest(".auth-provider-card") as HTMLElement;
|
||||
expect(within(subscriptionCard).getByRole("button", { name: "Login" })).toBeTruthy();
|
||||
await user.click(within(subscriptionCard).getByRole("button", { name: "Login" }));
|
||||
expect(loginProvider).toHaveBeenCalledWith("anthropic-subscription");
|
||||
expect(within(subscriptionCard).queryByPlaceholderText("Enter API key")).toBeNull();
|
||||
expect(within(apiKeyCard).getByPlaceholderText("Enter API key")).toBeTruthy();
|
||||
expect(within(apiKeyCard).getByRole("button", { name: "Save" })).toBeTruthy();
|
||||
await user.type(within(apiKeyCard).getByPlaceholderText("Enter API key"), "sk-mobile");
|
||||
await user.click(within(apiKeyCard).getByRole("button", { name: "Save" }));
|
||||
expect(saveApiKey).toHaveBeenCalledWith("anthropic-api-key", "sk-mobile");
|
||||
});
|
||||
|
||||
it("renders notification provider cards responsively on mobile", async () => {
|
||||
|
||||
@@ -276,7 +276,7 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
|
||||
return <div className="auth-apikey-section">
|
||||
<div className="auth-apikey-input-row">
|
||||
<input type="password" className="auth-apikey-input" placeholder={t("settings.authentication.enterAPIKey", "Enter API key")} value={apiKeyInputs[stateKey] ?? ""} onChange={(e) => setApiKeyInputs((prev) => ({ ...prev, [stateKey]: e.target.value }))} disabled={isAuthActionActive(stateKey)}/>
|
||||
{provider.keyHint && !isPending && !apiKeyInputs[stateKey] ? <button className="btn btn-sm" onClick={() => selectedInstanceId ? handleClearApiKey(provider.id, selectedInstanceId) : handleClearApiKey(provider.id)} disabled={isAuthActionActive(stateKey)}>{t("settings.auth.clearKey", "Clear")}</button> : <button className="btn btn-primary btn-sm" onClick={() => selectedInstanceId ? handleSaveApiKey(provider.id, selectedInstanceId, pendingLabel || undefined) : handleSaveApiKey(provider.id)} disabled={isAuthActionActive(stateKey)}>{t("settings.actions.save", "Save")}</button>}
|
||||
{provider.keyHint && !isPending && !apiKeyInputs[stateKey] ? <button className="btn btn-sm" onClick={() => instanceId ? handleClearApiKey(provider.id, instanceId) : handleClearApiKey(provider.id)} disabled={isAuthActionActive(stateKey)}>{t("settings.auth.clearKey", "Clear")}</button> : <button className="btn btn-primary btn-sm" onClick={() => instanceId ? handleSaveApiKey(provider.id, instanceId, pendingLabel || undefined) : handleSaveApiKey(provider.id)} disabled={isAuthActionActive(stateKey)}>{t("settings.actions.save", "Save")}</button>}
|
||||
</div>
|
||||
{isAuthActionActive(stateKey) && <small className="auth-apikey-progress">{t("settings.auth.savingKey", "Saving…")}</small>}
|
||||
{apiKeyErrors[stateKey] && <small className="auth-apikey-error">{apiKeyErrors[stateKey]}</small>}
|
||||
@@ -284,11 +284,12 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
|
||||
</div>;
|
||||
};
|
||||
const renderAuthenticatedOAuthActions = (provider: AuthProvider, selectedInstanceId?: string) => {
|
||||
const stateKey = formatProviderInstanceKey({ providerId: provider.id, instanceId: selectedInstanceId ?? provider.instanceId ?? "default" });
|
||||
const instanceId = selectedInstanceId ?? provider.instanceId;
|
||||
const stateKey = formatProviderInstanceKey({ providerId: provider.id, instanceId: instanceId ?? "default" });
|
||||
return <div>
|
||||
{isAuthActionActive(stateKey) ? <button className="btn btn-sm" disabled>{t("settings.auth.loggingOut", "Logging out…")}</button>
|
||||
: provider.loginInProgress ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => selectedInstanceId ? handleCancelLogin(provider.id, selectedInstanceId) : handleCancelLogin(provider.id)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: <button className="btn btn-sm" onClick={() => selectedInstanceId ? handleLogout(provider.id, selectedInstanceId) : handleLogout(provider.id)}>{t("settings.auth.logout", "Logout")}</button>}
|
||||
: provider.loginInProgress ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => instanceId ? handleCancelLogin(provider.id, instanceId) : handleCancelLogin(provider.id)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: <button className="btn btn-sm" onClick={() => instanceId ? handleLogout(provider.id, instanceId) : handleLogout(provider.id)}>{t("settings.auth.logout", "Logout")}</button>}
|
||||
</div>;
|
||||
};
|
||||
const renderAvailableOAuthActions = (provider: AuthProvider, selectedInstanceId?: string, pendingLabel?: string) => {
|
||||
@@ -296,9 +297,9 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
|
||||
const stateKey = formatProviderInstanceKey({ providerId: provider.id, instanceId: instanceId ?? "default" });
|
||||
const isActive = provider.loginInProgress || isAuthActionActive(stateKey);
|
||||
return <div>
|
||||
{isAuthActionActive(stateKey) ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => handleCancelLogin(provider.id, selectedInstanceId)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: provider.loginInProgress ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => selectedInstanceId ? handleCancelLogin(provider.id, selectedInstanceId) : handleCancelLogin(provider.id)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: <button className="btn btn-primary btn-sm" onClick={() => selectedInstanceId ? handleLogin(provider.id, selectedInstanceId, pendingLabel) : handleLogin(provider.id)}>{t("settings.auth.login", "Login")}</button>}
|
||||
{isAuthActionActive(stateKey) ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => instanceId ? handleCancelLogin(provider.id, instanceId) : handleCancelLogin(provider.id)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: provider.loginInProgress ? <div className="auth-provider-actions-row"><button className="btn btn-sm" disabled>{t("settings.auth.waitingForLogin", "Waiting for login…")}</button><button className="btn btn-sm" onClick={() => instanceId ? handleCancelLogin(provider.id, instanceId) : handleCancelLogin(provider.id)}>{t("settings.actions.cancel", "Cancel")}</button></div>
|
||||
: <button className="btn btn-primary btn-sm" onClick={() => instanceId ? handleLogin(provider.id, instanceId, pendingLabel) : handleLogin(provider.id)}>{t("settings.auth.login", "Login")}</button>}
|
||||
{provider.id === "github-copilot" && deviceCodes[stateKey] && isActive && <div className="auth-device-code-panel" data-testid={`auth-device-code-${stateKey}`}>
|
||||
<strong>{t("settings.auth.enterCodeOnGitHub", "Enter this code on GitHub")}</strong>
|
||||
<div className="auth-device-code-pill">{deviceCodes[stateKey].userCode}</div>
|
||||
|
||||
@@ -365,6 +365,25 @@ const NOT_SURFACED_ALLOWLIST: Record<string, string> = {
|
||||
validatorFallbackProvider: "moved to workflow settings (U4)",
|
||||
validatorFallbackModelId: "moved to workflow settings (U4)",
|
||||
|
||||
/*
|
||||
FNXC:SettingsCredentialInstance 2026-08-01-17:06:
|
||||
Credential-instance selectors are inline companions to their provider/model pickers. They inherit the provider default when unset, so each has no standalone Settings description while this narrow inventory keeps the default-description census complete.
|
||||
*/
|
||||
defaultCredentialInstanceId: "inline companion for the global default model picker; unset inherits the provider default",
|
||||
fallbackCredentialInstanceId: "inline companion for the global fallback model picker; unset inherits the provider default",
|
||||
executionGlobalCredentialInstanceId: "inline companion for the global execution model picker; unset inherits the provider default",
|
||||
planningGlobalCredentialInstanceId: "inline companion for the global planning model picker; unset inherits the provider default",
|
||||
validatorGlobalCredentialInstanceId: "inline companion for the global validator model picker; unset inherits the provider default",
|
||||
titleSummarizerGlobalCredentialInstanceId: "inline companion for the global title-summarizer model picker; unset inherits the provider default",
|
||||
mergerGlobalCredentialInstanceId: "inline companion for the global merger model picker; unset inherits the provider default",
|
||||
importTranslateGlobalCredentialInstanceId: "inline companion for the global import-translate model picker; unset inherits the provider default",
|
||||
defaultCredentialInstanceIdOverride: "inline companion for the project default model picker; unset inherits the provider default",
|
||||
titleSummarizerCredentialInstanceId: "inline companion for the project title-summarizer model picker; unset inherits the provider default",
|
||||
titleSummarizerFallbackCredentialInstanceId: "inline companion for the project title-summarizer fallback model picker; unset inherits the provider default",
|
||||
importTranslateCredentialInstanceId: "inline companion for the project import-translate model picker; unset inherits the provider default",
|
||||
mergerCredentialInstanceId: "inline companion for the project merger model picker; unset inherits the provider default",
|
||||
mergerFallbackCredentialInstanceId: "inline companion for the project merger fallback model picker; unset inherits the provider default",
|
||||
|
||||
// Internal/engine bookkeeping, session state, or reliability telemetry — not
|
||||
// rendered as a plain user-facing description field anywhere in Settings.
|
||||
globalPause: "engine-managed pause flag, not a plain description field",
|
||||
|
||||
Reference in New Issue
Block a user