mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
refactor(connections): move provider auth out of app shell
This commit is contained in:
@@ -15,7 +15,6 @@ import { useLocation, useNavigate } from "@solidjs/router";
|
|||||||
import type {
|
import type {
|
||||||
Agent,
|
Agent,
|
||||||
Part,
|
Part,
|
||||||
ProviderAuthAuthorization,
|
|
||||||
Session,
|
Session,
|
||||||
TextPartInput,
|
TextPartInput,
|
||||||
FilePartInput,
|
FilePartInput,
|
||||||
@@ -41,7 +40,7 @@ import ReloadWorkspaceToast from "./components/reload-workspace-toast";
|
|||||||
import StatusToast from "./components/status-toast";
|
import StatusToast from "./components/status-toast";
|
||||||
import DashboardView from "./pages/dashboard";
|
import DashboardView from "./pages/dashboard";
|
||||||
import SessionView from "./pages/session";
|
import SessionView from "./pages/session";
|
||||||
import { createClient, unwrap, waitForHealthy, type OpencodeAuth } from "./lib/opencode";
|
import { createClient, unwrap } from "./lib/opencode";
|
||||||
import { createDenClient, writeDenSettings } from "./lib/den";
|
import { createDenClient, writeDenSettings } from "./lib/den";
|
||||||
import {
|
import {
|
||||||
abortSession as abortSessionTyped,
|
abortSession as abortSessionTyped,
|
||||||
@@ -71,12 +70,7 @@ import {
|
|||||||
usesChromeDevtoolsAutoConnect,
|
usesChromeDevtoolsAutoConnect,
|
||||||
validateMcpServerName,
|
validateMcpServerName,
|
||||||
} from "./mcp";
|
} from "./mcp";
|
||||||
import {
|
import { compareProviders, providerPriorityRank } from "./utils/providers";
|
||||||
compareProviders,
|
|
||||||
filterProviderList,
|
|
||||||
mapConfigProvidersToList,
|
|
||||||
providerPriorityRank,
|
|
||||||
} from "./utils/providers";
|
|
||||||
import {
|
import {
|
||||||
blueprintMaterializedSessions,
|
blueprintMaterializedSessions,
|
||||||
blueprintSessions,
|
blueprintSessions,
|
||||||
@@ -96,10 +90,7 @@ import type {
|
|||||||
PluginScope,
|
PluginScope,
|
||||||
ReloadReason,
|
ReloadReason,
|
||||||
ReloadTrigger,
|
ReloadTrigger,
|
||||||
ResetOpenworkMode,
|
|
||||||
SettingsTab,
|
SettingsTab,
|
||||||
SkillCard,
|
|
||||||
SidebarSessionItem,
|
|
||||||
TodoItem,
|
TodoItem,
|
||||||
View,
|
View,
|
||||||
WorkspaceSessionGroup,
|
WorkspaceSessionGroup,
|
||||||
@@ -111,26 +102,22 @@ import type {
|
|||||||
ComposerPart,
|
ComposerPart,
|
||||||
ProviderListItem,
|
ProviderListItem,
|
||||||
SessionErrorTurn,
|
SessionErrorTurn,
|
||||||
UpdateHandle,
|
|
||||||
OpencodeConnectStatus,
|
OpencodeConnectStatus,
|
||||||
ScheduledJob,
|
|
||||||
WorkspacePreset,
|
WorkspacePreset,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
clearStartupPreference,
|
clearStartupPreference,
|
||||||
deriveArtifacts,
|
deriveArtifacts,
|
||||||
deriveWorkingFiles,
|
deriveWorkingFiles,
|
||||||
formatBytes,
|
|
||||||
formatModelLabel,
|
formatModelLabel,
|
||||||
formatModelRef,
|
formatModelRef,
|
||||||
formatRelativeTime,
|
|
||||||
isVisibleTextPart,
|
isVisibleTextPart,
|
||||||
isTauriRuntime,
|
isTauriRuntime,
|
||||||
modelEquals,
|
modelEquals,
|
||||||
normalizeDirectoryQueryPath,
|
normalizeDirectoryQueryPath,
|
||||||
normalizeDirectoryPath,
|
normalizeDirectoryPath,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { currentLocale, setLocale, t, type Language } from "../i18n";
|
import { currentLocale, setLocale, t } from "../i18n";
|
||||||
import {
|
import {
|
||||||
isWindowsPlatform,
|
isWindowsPlatform,
|
||||||
lastUserModelFromMessages,
|
lastUserModelFromMessages,
|
||||||
@@ -150,6 +137,7 @@ import {
|
|||||||
import { createSystemState } from "./system-state";
|
import { createSystemState } from "./system-state";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
import { createSessionStore } from "./context/session";
|
import { createSessionStore } from "./context/session";
|
||||||
|
import { createProvidersStore } from "./context/providers";
|
||||||
import {
|
import {
|
||||||
formatGenericBehaviorLabel,
|
formatGenericBehaviorLabel,
|
||||||
getModelBehaviorSummary,
|
getModelBehaviorSummary,
|
||||||
@@ -158,7 +146,6 @@ import {
|
|||||||
} from "./lib/model-behavior";
|
} from "./lib/model-behavior";
|
||||||
import {
|
import {
|
||||||
describeDirectoryScope,
|
describeDirectoryScope,
|
||||||
shouldApplyScopedSessionLoad,
|
|
||||||
shouldRedirectMissingSessionAfterScopedLoad,
|
shouldRedirectMissingSessionAfterScopedLoad,
|
||||||
toSessionTransportDirectory,
|
toSessionTransportDirectory,
|
||||||
} from "./lib/session-scope";
|
} from "./lib/session-scope";
|
||||||
@@ -238,7 +225,6 @@ import {
|
|||||||
type DenAuthDeepLink,
|
type DenAuthDeepLink,
|
||||||
type RemoteWorkspaceDefaults,
|
type RemoteWorkspaceDefaults,
|
||||||
type SharedBundleDeepLink,
|
type SharedBundleDeepLink,
|
||||||
type SharedBundleImportIntent,
|
|
||||||
type SharedBundleV1,
|
type SharedBundleV1,
|
||||||
type SharedSkillBundleV1,
|
type SharedSkillBundleV1,
|
||||||
type SharedWorkspaceProfileBundleV1,
|
type SharedWorkspaceProfileBundleV1,
|
||||||
@@ -311,16 +297,6 @@ export default function App() {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
type ProviderAuthMethod = {
|
|
||||||
type: "oauth" | "api";
|
|
||||||
label: string;
|
|
||||||
methodIndex?: number;
|
|
||||||
};
|
|
||||||
type ProviderOAuthStartResult = {
|
|
||||||
methodIndex: number;
|
|
||||||
authorization: ProviderAuthAuthorization;
|
|
||||||
};
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -831,7 +807,6 @@ export default function App() {
|
|||||||
const [error, setError] = createSignal<string | null>(null);
|
const [error, setError] = createSignal<string | null>(null);
|
||||||
const [opencodeConnectStatus, setOpencodeConnectStatus] = createSignal<OpencodeConnectStatus | null>(null);
|
const [opencodeConnectStatus, setOpencodeConnectStatus] = createSignal<OpencodeConnectStatus | null>(null);
|
||||||
const [booting, setBooting] = createSignal(true);
|
const [booting, setBooting] = createSignal(true);
|
||||||
const mountTime = Date.now();
|
|
||||||
const [lastKnownConfigSnapshot, setLastKnownConfigSnapshot] = createSignal("");
|
const [lastKnownConfigSnapshot, setLastKnownConfigSnapshot] = createSignal("");
|
||||||
const [developerMode, setDeveloperMode] = createSignal(false);
|
const [developerMode, setDeveloperMode] = createSignal(false);
|
||||||
const [documentVisible, setDocumentVisible] = createSignal(true);
|
const [documentVisible, setDocumentVisible] = createSignal(true);
|
||||||
@@ -891,13 +866,6 @@ export default function App() {
|
|||||||
type PromptFocusReturnTarget = "none" | "composer";
|
type PromptFocusReturnTarget = "none" | "composer";
|
||||||
|
|
||||||
const [sessionAgentById, setSessionAgentById] = createSignal<Record<string, string>>({});
|
const [sessionAgentById, setSessionAgentById] = createSignal<Record<string, string>>({});
|
||||||
const [providerAuthModalOpen, setProviderAuthModalOpen] = createSignal(false);
|
|
||||||
const [providerAuthBusy, setProviderAuthBusy] = createSignal(false);
|
|
||||||
const [providerAuthError, setProviderAuthError] = createSignal<string | null>(null);
|
|
||||||
const [providerAuthMethods, setProviderAuthMethods] = createSignal<Record<string, ProviderAuthMethod[]>>({});
|
|
||||||
const [providerAuthPreferredProviderId, setProviderAuthPreferredProviderId] = createSignal<string | null>(null);
|
|
||||||
const [providerAuthReturnFocusTarget, setProviderAuthReturnFocusTarget] =
|
|
||||||
createSignal<PromptFocusReturnTarget>("none");
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const view = currentView();
|
const view = currentView();
|
||||||
@@ -1025,21 +993,11 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
const activeSessions = createMemo(() => sessions());
|
const activeSessions = createMemo(() => sessions());
|
||||||
const activeSessionStatusById = createMemo(() => sessionStatusById());
|
const activeSessionStatusById = createMemo(() => sessionStatusById());
|
||||||
const activeMessages = createMemo(() => messages());
|
|
||||||
const activeTodos = createMemo(() => todos());
|
const activeTodos = createMemo(() => todos());
|
||||||
const activeWorkingFiles = createMemo(() => workingFiles());
|
const activeWorkingFiles = createMemo(() => workingFiles());
|
||||||
|
|
||||||
const sessionActivity = (session: Session) =>
|
const sessionActivity = (session: Session) =>
|
||||||
session.time?.updated ?? session.time?.created ?? 0;
|
session.time?.updated ?? session.time?.created ?? 0;
|
||||||
const sortSessionsByActivity = (list: Session[]) =>
|
|
||||||
list
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => {
|
|
||||||
const delta = sessionActivity(b) - sessionActivity(a);
|
|
||||||
if (delta !== 0) return delta;
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [sessionsLoaded, setSessionsLoaded] = createSignal(false);
|
const [sessionsLoaded, setSessionsLoaded] = createSignal(false);
|
||||||
const loadSessionsWithReady = async (scopeRoot?: string) => {
|
const loadSessionsWithReady = async (scopeRoot?: string) => {
|
||||||
await loadSessions(scopeRoot);
|
await loadSessions(scopeRoot);
|
||||||
@@ -1832,369 +1790,6 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildProviderAuthMethods = (
|
|
||||||
methods: Record<string, ProviderAuthMethod[]>,
|
|
||||||
availableProviders: ProviderListItem[],
|
|
||||||
workerType: "local" | "remote",
|
|
||||||
) => {
|
|
||||||
const merged = Object.fromEntries(
|
|
||||||
Object.entries(methods ?? {}).map(([id, providerMethods]) => [
|
|
||||||
id,
|
|
||||||
(providerMethods ?? []).map((method, methodIndex) => ({
|
|
||||||
...method,
|
|
||||||
methodIndex,
|
|
||||||
})),
|
|
||||||
]),
|
|
||||||
) as Record<string, ProviderAuthMethod[]>;
|
|
||||||
for (const provider of availableProviders ?? []) {
|
|
||||||
const id = provider.id?.trim();
|
|
||||||
if (!id || id === "opencode") continue;
|
|
||||||
if (!Array.isArray(provider.env) || provider.env.length === 0) continue;
|
|
||||||
const existing = merged[id] ?? [];
|
|
||||||
if (existing.some((method) => method.type === "api")) continue;
|
|
||||||
merged[id] = [...existing, { type: "api", label: "API key" }];
|
|
||||||
}
|
|
||||||
for (const [id, providerMethods] of Object.entries(merged)) {
|
|
||||||
const provider = availableProviders.find((item) => item.id === id);
|
|
||||||
const normalizedId = id.trim().toLowerCase();
|
|
||||||
const normalizedName = provider?.name?.trim().toLowerCase() ?? "";
|
|
||||||
const isOpenAiProvider = normalizedId === "openai" || normalizedName === "openai";
|
|
||||||
if (!isOpenAiProvider) continue;
|
|
||||||
merged[id] = providerMethods.filter((method) => {
|
|
||||||
if (method.type !== "oauth") return true;
|
|
||||||
const label = method.label.toLowerCase();
|
|
||||||
const isHeadless = label.includes("headless") || label.includes("device");
|
|
||||||
return workerType === "remote" ? isHeadless : !isHeadless;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadProviderAuthMethods = async (workerType: "local" | "remote") => {
|
|
||||||
const c = client();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Not connected to a server");
|
|
||||||
}
|
|
||||||
const methods = unwrap(await c.provider.auth());
|
|
||||||
return buildProviderAuthMethods(
|
|
||||||
methods as Record<string, ProviderAuthMethod[]>,
|
|
||||||
providers(),
|
|
||||||
workerType,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function startProviderAuth(
|
|
||||||
providerId?: string,
|
|
||||||
methodIndex?: number,
|
|
||||||
): Promise<ProviderOAuthStartResult> {
|
|
||||||
setProviderAuthError(null);
|
|
||||||
const c = client();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Not connected to a server");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const cachedMethods = providerAuthMethods();
|
|
||||||
const workerType = selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local";
|
|
||||||
const authMethods = Object.keys(cachedMethods).length
|
|
||||||
? cachedMethods
|
|
||||||
: await loadProviderAuthMethods(workerType);
|
|
||||||
const providerIds = Object.keys(authMethods).sort();
|
|
||||||
if (!providerIds.length) {
|
|
||||||
throw new Error("No providers available");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = providerId?.trim() ?? "";
|
|
||||||
if (!resolved) {
|
|
||||||
throw new Error("Provider ID is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const methods = authMethods[resolved];
|
|
||||||
if (!methods || !methods.length) {
|
|
||||||
throw new Error(`Unknown provider: ${resolved}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const oauthIndex =
|
|
||||||
methodIndex !== undefined
|
|
||||||
? methodIndex
|
|
||||||
: methods.find((method) => method.type === "oauth")?.methodIndex ?? -1;
|
|
||||||
if (oauthIndex === -1) {
|
|
||||||
throw new Error(`No OAuth flow available for ${resolved}. Use an API key instead.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedMethod = methods.find((method) => method.methodIndex === oauthIndex);
|
|
||||||
if (!selectedMethod || selectedMethod.type !== "oauth") {
|
|
||||||
throw new Error(`Selected auth method is not an OAuth flow for ${resolved}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth = unwrap(await c.provider.oauth.authorize({ providerID: resolved, method: oauthIndex }));
|
|
||||||
return {
|
|
||||||
methodIndex: oauthIndex,
|
|
||||||
authorization: auth,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const message = describeProviderError(error, "Failed to connect provider");
|
|
||||||
setProviderAuthError(message);
|
|
||||||
throw error instanceof Error ? error : new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshProviders(options?: { dispose?: boolean }) {
|
|
||||||
const c = client();
|
|
||||||
if (!c) return null;
|
|
||||||
|
|
||||||
if (options?.dispose) {
|
|
||||||
try {
|
|
||||||
unwrap(await c.instance.dispose());
|
|
||||||
} catch {
|
|
||||||
// ignore dispose failures and try reading current state anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await waitForHealthy(client() ?? c, { timeoutMs: 8_000, pollMs: 250 });
|
|
||||||
} catch {
|
|
||||||
// ignore health wait failures and still attempt provider reads
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeClient = client() ?? c;
|
|
||||||
let disabledProviders = globalSync.data.config.disabled_providers ?? [];
|
|
||||||
try {
|
|
||||||
const config = unwrap(await activeClient.config.get());
|
|
||||||
disabledProviders = Array.isArray(config.disabled_providers) ? config.disabled_providers : [];
|
|
||||||
} catch {
|
|
||||||
// ignore config read failures and continue with current store state
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const updated = filterProviderList(
|
|
||||||
unwrap(await activeClient.provider.list()),
|
|
||||||
disabledProviders,
|
|
||||||
);
|
|
||||||
globalSync.set("provider", updated);
|
|
||||||
return updated;
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
const fallback = unwrap(await activeClient.config.providers());
|
|
||||||
const mapped = mapConfigProvidersToList(fallback.providers);
|
|
||||||
const previousConnected = providerConnectedIds();
|
|
||||||
const next = filterProviderList(
|
|
||||||
{
|
|
||||||
all: mapped,
|
|
||||||
connected: previousConnected.filter((id) => mapped.some((provider) => provider.id === id)),
|
|
||||||
default: fallback.default,
|
|
||||||
},
|
|
||||||
disabledProviders,
|
|
||||||
);
|
|
||||||
globalSync.set("provider", next);
|
|
||||||
return next;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function completeProviderAuthOAuth(providerId: string, methodIndex: number, code?: string) {
|
|
||||||
setProviderAuthError(null);
|
|
||||||
const c = client();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Not connected to a server");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = providerId?.trim();
|
|
||||||
if (!resolved) {
|
|
||||||
throw new Error("Provider ID is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isInteger(methodIndex) || methodIndex < 0) {
|
|
||||||
throw new Error("OAuth method is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitForProviderConnection = async (timeoutMs = 15_000, pollMs = 2_000) => {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
|
||||||
try {
|
|
||||||
const updated = await refreshProviders({ dispose: true });
|
|
||||||
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore and retry
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPendingOauthError = (error: unknown) => {
|
|
||||||
const text = error instanceof Error ? error.message : String(error ?? "");
|
|
||||||
return /request timed out/i.test(text) || /ProviderAuthOauthMissing/i.test(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const trimmedCode = code?.trim();
|
|
||||||
const result = await c.provider.oauth.callback({
|
|
||||||
providerID: resolved,
|
|
||||||
method: methodIndex,
|
|
||||||
code: trimmedCode || undefined,
|
|
||||||
});
|
|
||||||
assertNoClientError(result);
|
|
||||||
const updated = await refreshProviders({ dispose: true });
|
|
||||||
const connectedNow = Array.isArray(updated?.connected) && updated.connected.includes(resolved);
|
|
||||||
if (connectedNow) {
|
|
||||||
return { connected: true, message: `Connected ${resolved}` };
|
|
||||||
}
|
|
||||||
const connected = await waitForProviderConnection();
|
|
||||||
if (connected) {
|
|
||||||
return { connected: true, message: `Connected ${resolved}` };
|
|
||||||
}
|
|
||||||
return { connected: false, pending: true };
|
|
||||||
} catch (error) {
|
|
||||||
if (isPendingOauthError(error)) {
|
|
||||||
const updated = await refreshProviders({ dispose: true });
|
|
||||||
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
|
||||||
return { connected: true, message: `Connected ${resolved}` };
|
|
||||||
}
|
|
||||||
const connected = await waitForProviderConnection();
|
|
||||||
if (connected) {
|
|
||||||
return { connected: true, message: `Connected ${resolved}` };
|
|
||||||
}
|
|
||||||
return { connected: false, pending: true };
|
|
||||||
}
|
|
||||||
const message = describeProviderError(error, "Failed to complete OAuth");
|
|
||||||
setProviderAuthError(message);
|
|
||||||
throw error instanceof Error ? error : new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitProviderApiKey(providerId: string, apiKey: string) {
|
|
||||||
setProviderAuthError(null);
|
|
||||||
const c = client();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Not connected to a server");
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = apiKey.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
throw new Error("API key is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await c.auth.set({
|
|
||||||
providerID: providerId,
|
|
||||||
auth: { type: "api", key: trimmed },
|
|
||||||
});
|
|
||||||
await refreshProviders({ dispose: true });
|
|
||||||
return `Connected ${providerId}`;
|
|
||||||
} catch (error) {
|
|
||||||
const message = describeProviderError(error, "Failed to save API key");
|
|
||||||
setProviderAuthError(message);
|
|
||||||
throw error instanceof Error ? error : new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function disconnectProvider(providerId: string) {
|
|
||||||
setProviderAuthError(null);
|
|
||||||
const c = client();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Not connected to a server");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolved = providerId.trim();
|
|
||||||
if (!resolved) {
|
|
||||||
throw new Error("Provider ID is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = providers().find((entry) => entry.id === resolved) as
|
|
||||||
| (ProviderListItem & { source?: string })
|
|
||||||
| undefined;
|
|
||||||
const canDisableProvider =
|
|
||||||
provider?.source === "config" || provider?.source === "custom";
|
|
||||||
|
|
||||||
const removeProviderAuth = async () => {
|
|
||||||
const authClient = c.auth as unknown as {
|
|
||||||
remove?: (options: { providerID: string }) => Promise<unknown>;
|
|
||||||
set?: (options: { providerID: string; auth: unknown }) => Promise<unknown>;
|
|
||||||
};
|
|
||||||
if (typeof authClient.remove === "function") {
|
|
||||||
const result = await authClient.remove({ providerID: resolved });
|
|
||||||
assertNoClientError(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawClient = (c as unknown as { client?: { delete?: (options: { url: string }) => Promise<unknown> } })
|
|
||||||
.client;
|
|
||||||
if (rawClient?.delete) {
|
|
||||||
await rawClient.delete({ url: `/auth/${encodeURIComponent(resolved)}` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof authClient.set === "function") {
|
|
||||||
const result = await authClient.set({ providerID: resolved, auth: null });
|
|
||||||
assertNoClientError(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Provider auth removal is not supported by this client.");
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableProvider = async () => {
|
|
||||||
const config = unwrap(await c.config.get());
|
|
||||||
const disabledProviders = Array.isArray(config.disabled_providers)
|
|
||||||
? config.disabled_providers
|
|
||||||
: [];
|
|
||||||
if (disabledProviders.includes(resolved)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = [...disabledProviders, resolved];
|
|
||||||
globalSync.set("config", "disabled_providers", next);
|
|
||||||
try {
|
|
||||||
const result = await c.config.update({
|
|
||||||
config: {
|
|
||||||
...config,
|
|
||||||
disabled_providers: next,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assertNoClientError(result);
|
|
||||||
markOpencodeConfigReloadRequired();
|
|
||||||
} catch (error) {
|
|
||||||
globalSync.set("config", "disabled_providers", disabledProviders);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeProviderAuth();
|
|
||||||
let updated = await refreshProviders({ dispose: true });
|
|
||||||
if (
|
|
||||||
canDisableProvider &&
|
|
||||||
Array.isArray(updated?.connected) &&
|
|
||||||
updated.connected.includes(resolved)
|
|
||||||
) {
|
|
||||||
const disabled = await disableProvider();
|
|
||||||
if (disabled) {
|
|
||||||
updated = filterProviderList(updated, globalSync.data.config.disabled_providers ?? []);
|
|
||||||
globalSync.set("provider", updated);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(updated?.connected) || !updated.connected.includes(resolved)) {
|
|
||||||
return disabled
|
|
||||||
? `Disconnected ${resolved} and disabled it in OpenCode config.`
|
|
||||||
: `Disconnected ${resolved}.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
|
||||||
return `Removed stored credentials for ${resolved}, but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.`;
|
|
||||||
}
|
|
||||||
removeProviderFromState(resolved);
|
|
||||||
return `Disconnected ${resolved}`;
|
|
||||||
} catch (error) {
|
|
||||||
const message = describeProviderError(error, "Failed to disconnect provider");
|
|
||||||
setProviderAuthError(message);
|
|
||||||
throw error instanceof Error ? error : new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusSessionPromptSoon() {
|
function focusSessionPromptSoon() {
|
||||||
if (typeof window === "undefined" || currentView() !== "session") return;
|
if (typeof window === "undefined" || currentView() !== "session") return;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -2204,43 +1799,6 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openProviderAuthModal(options?: {
|
|
||||||
returnFocusTarget?: PromptFocusReturnTarget;
|
|
||||||
preferredProviderId?: string;
|
|
||||||
}) {
|
|
||||||
const workerType = selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local";
|
|
||||||
setProviderAuthReturnFocusTarget(options?.returnFocusTarget ?? "none");
|
|
||||||
setProviderAuthPreferredProviderId(options?.preferredProviderId?.trim() || null);
|
|
||||||
setProviderAuthBusy(true);
|
|
||||||
setProviderAuthError(null);
|
|
||||||
try {
|
|
||||||
const methods = await loadProviderAuthMethods(workerType);
|
|
||||||
setProviderAuthMethods(methods);
|
|
||||||
setProviderAuthModalOpen(true);
|
|
||||||
} catch (error) {
|
|
||||||
setProviderAuthPreferredProviderId(null);
|
|
||||||
setProviderAuthReturnFocusTarget("none");
|
|
||||||
const message = describeProviderError(error, "Failed to load providers");
|
|
||||||
setProviderAuthError(message);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
setProviderAuthBusy(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeProviderAuthModal(options?: { restorePromptFocus?: boolean }) {
|
|
||||||
const shouldFocusPrompt =
|
|
||||||
options?.restorePromptFocus ??
|
|
||||||
providerAuthReturnFocusTarget() === "composer";
|
|
||||||
setProviderAuthModalOpen(false);
|
|
||||||
setProviderAuthError(null);
|
|
||||||
setProviderAuthPreferredProviderId(null);
|
|
||||||
setProviderAuthReturnFocusTarget("none");
|
|
||||||
if (shouldFocusPrompt) {
|
|
||||||
focusSessionPromptSoon();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSessionExport(sessionID: string) {
|
async function saveSessionExport(sessionID: string) {
|
||||||
const c = client();
|
const c = client();
|
||||||
if (!c) {
|
if (!c) {
|
||||||
@@ -2400,16 +1958,6 @@ export default function App() {
|
|||||||
globalSync.set("provider", "connected", value);
|
globalSync.set("provider", "connected", value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeProviderFromState = (providerId: string) => {
|
|
||||||
const resolved = providerId.trim();
|
|
||||||
if (!resolved) return;
|
|
||||||
setProviders(providers().filter((provider) => provider.id !== resolved));
|
|
||||||
setProviderConnectedIds(providerConnectedIds().filter((id) => id !== resolved));
|
|
||||||
setProviderDefaults(
|
|
||||||
Object.fromEntries(Object.entries(providerDefaults()).filter(([id]) => id !== resolved)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [defaultModel, setDefaultModel] = createSignal<ModelRef>(DEFAULT_MODEL);
|
const [defaultModel, setDefaultModel] = createSignal<ModelRef>(DEFAULT_MODEL);
|
||||||
const sessionModelOverridesKey = (workspaceId: string) =>
|
const sessionModelOverridesKey = (workspaceId: string) =>
|
||||||
`${SESSION_MODEL_PREF_KEY}.${workspaceId}`;
|
`${SESSION_MODEL_PREF_KEY}.${workspaceId}`;
|
||||||
@@ -2725,6 +2273,35 @@ export default function App() {
|
|||||||
setPendingInitialSessionSelection,
|
setPendingInitialSessionSelection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
providerAuthModalOpen,
|
||||||
|
providerAuthBusy,
|
||||||
|
providerAuthError,
|
||||||
|
providerAuthMethods,
|
||||||
|
providerAuthPreferredProviderId,
|
||||||
|
providerAuthWorkerType,
|
||||||
|
startProviderAuth,
|
||||||
|
refreshProviders,
|
||||||
|
completeProviderAuthOAuth,
|
||||||
|
submitProviderApiKey,
|
||||||
|
disconnectProvider,
|
||||||
|
openProviderAuthModal,
|
||||||
|
closeProviderAuthModal,
|
||||||
|
} = createProvidersStore({
|
||||||
|
client,
|
||||||
|
providers,
|
||||||
|
providerDefaults,
|
||||||
|
providerConnectedIds,
|
||||||
|
disabledProviders: () => globalSync.data.config.disabled_providers ?? [],
|
||||||
|
selectedWorkspaceDisplay: () => workspaceStore.selectedWorkspaceDisplay(),
|
||||||
|
setProviders,
|
||||||
|
setProviderDefaults,
|
||||||
|
setProviderConnectedIds,
|
||||||
|
setDisabledProviders: (value) => globalSync.set("config", "disabled_providers", value),
|
||||||
|
markOpencodeConfigReloadRequired: () => markOpencodeConfigReloadRequired(),
|
||||||
|
focusPromptSoon: focusSessionPromptSoon,
|
||||||
|
});
|
||||||
|
|
||||||
const runtimeWorkspaceId = createMemo(() => workspaceStore.runtimeWorkspaceId());
|
const runtimeWorkspaceId = createMemo(() => workspaceStore.runtimeWorkspaceId());
|
||||||
const activeWorkspaceServerConfig = createMemo(() => workspaceStore.runtimeWorkspaceConfig());
|
const activeWorkspaceServerConfig = createMemo(() => workspaceStore.runtimeWorkspaceConfig());
|
||||||
|
|
||||||
@@ -6764,103 +6341,9 @@ export default function App() {
|
|||||||
return seconds > 0 ? `${label} · ${seconds}s` : label;
|
return seconds > 0 ? `${label} · ${seconds}s` : label;
|
||||||
});
|
});
|
||||||
|
|
||||||
const localHostLabel = createMemo(() => {
|
|
||||||
const info = engine();
|
|
||||||
if (info?.hostname && info?.port) {
|
|
||||||
return `${info.hostname}:${info.port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new URL(baseUrl()).host;
|
|
||||||
} catch {
|
|
||||||
return "localhost:4096";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onboardingProps = () => ({
|
|
||||||
startupPreference: startupPreference(),
|
|
||||||
onboardingStep: onboardingStep(),
|
|
||||||
rememberStartupChoice: rememberStartupChoice(),
|
|
||||||
busy: busy(),
|
|
||||||
clientDirectory: clientDirectory(),
|
|
||||||
openworkHostUrl: openworkServerSettings().urlOverride ?? "",
|
|
||||||
openworkToken: openworkServerSettings().token ?? "",
|
|
||||||
newAuthorizedDir: newAuthorizedDir(),
|
|
||||||
authorizedDirs: workspaceStore.authorizedDirs(),
|
|
||||||
selectedWorkspacePath: workspaceStore.selectedWorkspacePath(),
|
|
||||||
workspaces: workspaceStore.workspaces(),
|
|
||||||
localHostLabel: localHostLabel(),
|
|
||||||
engineRunning: Boolean(engine()?.running),
|
|
||||||
developerMode: developerMode(),
|
|
||||||
engineBaseUrl: engine()?.baseUrl ?? null,
|
|
||||||
engineDoctorFound: engineDoctorResult()?.found ?? null,
|
|
||||||
engineDoctorSupportsServe: engineDoctorResult()?.supportsServe ?? null,
|
|
||||||
engineDoctorVersion: engineDoctorResult()?.version ?? null,
|
|
||||||
engineDoctorResolvedPath: engineDoctorResult()?.resolvedPath ?? null,
|
|
||||||
engineDoctorNotes: engineDoctorResult()?.notes ?? [],
|
|
||||||
engineDoctorServeHelpStdout: engineDoctorResult()?.serveHelpStdout ?? null,
|
|
||||||
engineDoctorServeHelpStderr: engineDoctorResult()?.serveHelpStderr ?? null,
|
|
||||||
engineDoctorCheckedAt: engineDoctorCheckedAt(),
|
|
||||||
engineInstallLogs: engineInstallLogs(),
|
|
||||||
error: error(),
|
|
||||||
canRepairMigration: workspaceStore.canRepairOpencodeMigration(),
|
|
||||||
migrationRepairUnavailableReason: migrationRepairUnavailableReason(),
|
|
||||||
migrationRepairBusy: workspaceStore.migrationRepairBusy(),
|
|
||||||
migrationRepairResult: workspaceStore.migrationRepairResult(),
|
|
||||||
isWindows: isWindowsPlatform(),
|
|
||||||
onClientDirectoryChange: setClientDirectory,
|
|
||||||
onOpenworkHostUrlChange: (value: string) =>
|
|
||||||
updateOpenworkServerSettings({
|
|
||||||
...openworkServerSettings(),
|
|
||||||
urlOverride: value,
|
|
||||||
}),
|
|
||||||
onOpenworkTokenChange: (value: string) =>
|
|
||||||
updateOpenworkServerSettings({
|
|
||||||
...openworkServerSettings(),
|
|
||||||
token: value,
|
|
||||||
}),
|
|
||||||
onSelectStartup: workspaceStore.onSelectStartup,
|
|
||||||
onRememberStartupToggle: workspaceStore.onRememberStartupToggle,
|
|
||||||
onStartHost: workspaceStore.onStartHost,
|
|
||||||
onRepairMigration: workspaceStore.onRepairOpencodeMigration,
|
|
||||||
onCreateWorkspace: workspaceStore.createWorkspaceFlow,
|
|
||||||
onPickWorkspaceFolder: workspaceStore.pickWorkspaceFolder,
|
|
||||||
onImportWorkspaceConfig: workspaceStore.importWorkspaceConfig,
|
|
||||||
importingWorkspaceConfig: workspaceStore.importingWorkspaceConfig(),
|
|
||||||
onAttachHost: workspaceStore.onAttachHost,
|
|
||||||
onConnectClient: workspaceStore.onConnectClient,
|
|
||||||
onBackToWelcome: workspaceStore.onBackToWelcome,
|
|
||||||
onSetAuthorizedDir: workspaceStore.setNewAuthorizedDir,
|
|
||||||
onAddAuthorizedDir: workspaceStore.addAuthorizedDir,
|
|
||||||
onAddAuthorizedDirFromPicker: () =>
|
|
||||||
workspaceStore.addAuthorizedDirFromPicker({ persistToWorkspace: true }),
|
|
||||||
onRemoveAuthorizedDir: workspaceStore.removeAuthorizedDirAtIndex,
|
|
||||||
onRefreshEngineDoctor: async () => {
|
|
||||||
workspaceStore.setEngineInstallLogs(null);
|
|
||||||
await workspaceStore.refreshEngineDoctor();
|
|
||||||
},
|
|
||||||
onInstallEngine: workspaceStore.onInstallEngine,
|
|
||||||
onShowSearchNotes: () => {
|
|
||||||
const notes =
|
|
||||||
workspaceStore.engineDoctorResult()?.notes?.join("\n") ?? "";
|
|
||||||
workspaceStore.setEngineInstallLogs(notes || null);
|
|
||||||
},
|
|
||||||
onOpenSettings: () => {
|
|
||||||
setTab("settings");
|
|
||||||
setView("dashboard");
|
|
||||||
},
|
|
||||||
onOpenAdvancedSettings: () => {
|
|
||||||
setTab("config");
|
|
||||||
setView("dashboard");
|
|
||||||
},
|
|
||||||
themeMode: themeMode(),
|
|
||||||
setThemeMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dashboardProps = () => {
|
const dashboardProps = () => {
|
||||||
const workspaceType = selectedWorkspaceDisplay().workspaceType;
|
const workspaceType = selectedWorkspaceDisplay().workspaceType;
|
||||||
const isRemoteWorkspace = workspaceType === "remote";
|
const isRemoteWorkspace = workspaceType === "remote";
|
||||||
const providerAuthWorkerType: "local" | "remote" = isRemoteWorkspace ? "remote" : "local";
|
|
||||||
const openworkStatus = openworkServerStatus();
|
const openworkStatus = openworkServerStatus();
|
||||||
const canUseDesktopTools = isTauriRuntime() && !isRemoteWorkspace;
|
const canUseDesktopTools = isTauriRuntime() && !isRemoteWorkspace;
|
||||||
const canInstallSkillCreator = isRemoteWorkspace
|
const canInstallSkillCreator = isRemoteWorkspace
|
||||||
@@ -6901,7 +6384,7 @@ export default function App() {
|
|||||||
providerAuthError: providerAuthError(),
|
providerAuthError: providerAuthError(),
|
||||||
providerAuthMethods: providerAuthMethods(),
|
providerAuthMethods: providerAuthMethods(),
|
||||||
providerAuthPreferredProviderId: providerAuthPreferredProviderId(),
|
providerAuthPreferredProviderId: providerAuthPreferredProviderId(),
|
||||||
providerAuthWorkerType,
|
providerAuthWorkerType: providerAuthWorkerType(),
|
||||||
openProviderAuthModal,
|
openProviderAuthModal,
|
||||||
disconnectProvider,
|
disconnectProvider,
|
||||||
closeProviderAuthModal,
|
closeProviderAuthModal,
|
||||||
@@ -7176,9 +6659,7 @@ export default function App() {
|
|||||||
|
|
||||||
const sessionProps = () => ({
|
const sessionProps = () => ({
|
||||||
booting: booting(),
|
booting: booting(),
|
||||||
providerAuthWorkerType: (selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local") as
|
providerAuthWorkerType: providerAuthWorkerType(),
|
||||||
| "remote"
|
|
||||||
| "local",
|
|
||||||
selectedSessionId: activeSessionId(),
|
selectedSessionId: activeSessionId(),
|
||||||
setView,
|
setView,
|
||||||
tab: tab(),
|
tab: tab(),
|
||||||
|
|||||||
3
apps/app/src/app/context/providers/index.ts
Normal file
3
apps/app/src/app/context/providers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { createProvidersStore } from "./store";
|
||||||
|
export type { ProviderAuthMethod, ProviderOAuthStartResult } from "./store";
|
||||||
|
export { default as ProviderAuthModal } from "./provider-auth-modal";
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client";
|
|
||||||
import { CheckCircle2, Loader2, X, Search, ChevronRight } from "lucide-solid";
|
import { CheckCircle2, Loader2, X, Search, ChevronRight } from "lucide-solid";
|
||||||
import type { ProviderListItem } from "../types";
|
|
||||||
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
||||||
import { isTauriRuntime } from "../utils";
|
|
||||||
import { compareProviders } from "../utils/providers";
|
|
||||||
|
|
||||||
import Button from "./button";
|
import type { ProviderListItem } from "../../types";
|
||||||
import ProviderIcon from "./provider-icon";
|
import { isTauriRuntime } from "../../utils";
|
||||||
import TextInput from "./text-input";
|
import { compareProviders } from "../../utils/providers";
|
||||||
|
import Button from "../../components/button";
|
||||||
|
import ProviderIcon from "../../components/provider-icon";
|
||||||
|
import TextInput from "../../components/text-input";
|
||||||
|
import type { ProviderAuthMethod, ProviderOAuthStartResult } from "./store";
|
||||||
|
|
||||||
export type ProviderAuthMethod = {
|
|
||||||
type: "oauth" | "api";
|
|
||||||
label: string;
|
|
||||||
methodIndex?: number;
|
|
||||||
};
|
|
||||||
type ProviderAuthEntry = {
|
type ProviderAuthEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -22,11 +17,6 @@ type ProviderAuthEntry = {
|
|||||||
env: string[];
|
env: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderOAuthStartResult = {
|
|
||||||
methodIndex: number;
|
|
||||||
authorization: ProviderAuthAuthorization;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProviderOAuthSession = ProviderOAuthStartResult & {
|
type ProviderOAuthSession = ProviderOAuthStartResult & {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
methodLabel: string;
|
methodLabel: string;
|
||||||
@@ -706,7 +696,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-gray-9 font-mono truncate mt-0.5 opacity-60 group-hover:opacity-80 transition-opacity">{entry.id}</div>
|
<div class="text-[11px] text-gray-9 font-mono truncate mt-0.5 opacity-60 group-hover:opacity-80 transition-opacity">{entry.id}</div>
|
||||||
|
|
||||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||||
<For each={entry.methods}>
|
<For each={entry.methods}>
|
||||||
{(method) => (
|
{(method) => (
|
||||||
562
apps/app/src/app/context/providers/store.ts
Normal file
562
apps/app/src/app/context/providers/store.ts
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
import { createMemo, createSignal, type Accessor } from "solid-js";
|
||||||
|
|
||||||
|
import type { ProviderAuthAuthorization, ProviderListResponse } from "@opencode-ai/sdk/v2/client";
|
||||||
|
|
||||||
|
import { unwrap, waitForHealthy } from "../../lib/opencode";
|
||||||
|
import type { Client, ProviderListItem, WorkspaceDisplay } from "../../types";
|
||||||
|
import { safeStringify } from "../../utils";
|
||||||
|
import { filterProviderList, mapConfigProvidersToList } from "../../utils/providers";
|
||||||
|
|
||||||
|
type ProviderReturnFocusTarget = "none" | "composer";
|
||||||
|
|
||||||
|
export type ProviderAuthMethod = {
|
||||||
|
type: "oauth" | "api";
|
||||||
|
label: string;
|
||||||
|
methodIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProviderOAuthStartResult = {
|
||||||
|
methodIndex: number;
|
||||||
|
authorization: ProviderAuthAuthorization;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateProvidersStoreOptions = {
|
||||||
|
client: Accessor<Client | null>;
|
||||||
|
providers: Accessor<ProviderListItem[]>;
|
||||||
|
providerDefaults: Accessor<Record<string, string>>;
|
||||||
|
providerConnectedIds: Accessor<string[]>;
|
||||||
|
disabledProviders: Accessor<string[]>;
|
||||||
|
selectedWorkspaceDisplay: Accessor<WorkspaceDisplay>;
|
||||||
|
setProviders: (value: ProviderListItem[]) => void;
|
||||||
|
setProviderDefaults: (value: Record<string, string>) => void;
|
||||||
|
setProviderConnectedIds: (value: string[]) => void;
|
||||||
|
setDisabledProviders: (value: string[]) => void;
|
||||||
|
markOpencodeConfigReloadRequired: () => void;
|
||||||
|
focusPromptSoon?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createProvidersStore(options: CreateProvidersStoreOptions) {
|
||||||
|
const [providerAuthModalOpen, setProviderAuthModalOpen] = createSignal(false);
|
||||||
|
const [providerAuthBusy, setProviderAuthBusy] = createSignal(false);
|
||||||
|
const [providerAuthError, setProviderAuthError] = createSignal<string | null>(null);
|
||||||
|
const [providerAuthMethods, setProviderAuthMethods] = createSignal<Record<string, ProviderAuthMethod[]>>({});
|
||||||
|
const [providerAuthPreferredProviderId, setProviderAuthPreferredProviderId] = createSignal<string | null>(null);
|
||||||
|
const [providerAuthReturnFocusTarget, setProviderAuthReturnFocusTarget] =
|
||||||
|
createSignal<ProviderReturnFocusTarget>("none");
|
||||||
|
|
||||||
|
const providerAuthWorkerType = createMemo<"local" | "remote">(() =>
|
||||||
|
options.selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local",
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyProviderListState = (value: ProviderListResponse) => {
|
||||||
|
options.setProviders(value.all ?? []);
|
||||||
|
options.setProviderDefaults(value.default ?? {});
|
||||||
|
options.setProviderConnectedIds(value.connected ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeProviderFromState = (providerId: string) => {
|
||||||
|
const resolved = providerId.trim();
|
||||||
|
if (!resolved) return;
|
||||||
|
options.setProviders(options.providers().filter((provider) => provider.id !== resolved));
|
||||||
|
options.setProviderConnectedIds(options.providerConnectedIds().filter((id) => id !== resolved));
|
||||||
|
options.setProviderDefaults(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(options.providerDefaults()).filter(([id]) => id !== resolved),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNoClientError = (result: unknown) => {
|
||||||
|
const maybe = result as { error?: unknown } | null | undefined;
|
||||||
|
if (!maybe || maybe.error === undefined) return;
|
||||||
|
throw new Error(describeProviderError(maybe.error, "Request failed"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const describeProviderError = (error: unknown, fallback: string) => {
|
||||||
|
const readString = (value: unknown, max = 700) => {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
if (trimmed.length <= max) return trimmed;
|
||||||
|
return `${trimmed.slice(0, Math.max(0, max - 3))}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const records: Record<string, unknown>[] = [];
|
||||||
|
const root = error && typeof error === "object" ? (error as Record<string, unknown>) : null;
|
||||||
|
if (root) {
|
||||||
|
records.push(root);
|
||||||
|
if (root.data && typeof root.data === "object") records.push(root.data as Record<string, unknown>);
|
||||||
|
if (root.cause && typeof root.cause === "object") {
|
||||||
|
const cause = root.cause as Record<string, unknown>;
|
||||||
|
records.push(cause);
|
||||||
|
if (cause.data && typeof cause.data === "object") records.push(cause.data as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstString = (keys: string[]) => {
|
||||||
|
for (const record of records) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = readString(record[key]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstNumber = (keys: string[]) => {
|
||||||
|
for (const record of records) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = firstNumber(["statusCode", "status"]);
|
||||||
|
const provider = firstString(["providerID", "providerId", "provider"]);
|
||||||
|
const code = firstString(["code", "errorCode"]);
|
||||||
|
const response = firstString(["responseBody", "body", "response"]);
|
||||||
|
const raw =
|
||||||
|
(error instanceof Error ? readString(error.message) : null) ||
|
||||||
|
firstString(["message", "detail", "reason", "error"]) ||
|
||||||
|
(typeof error === "string" ? readString(error) : null);
|
||||||
|
|
||||||
|
const generic = raw && /^unknown\s+error$/i.test(raw);
|
||||||
|
const heading = (() => {
|
||||||
|
if (status === 401 || status === 403) return "Authentication failed";
|
||||||
|
if (status === 429) return "Rate limit exceeded";
|
||||||
|
if (provider) return `Provider error (${provider})`;
|
||||||
|
return fallback;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const lines = [heading];
|
||||||
|
if (raw && !generic && raw !== heading) lines.push(raw);
|
||||||
|
if (status && !heading.includes(String(status))) lines.push(`Status: ${status}`);
|
||||||
|
if (provider && !heading.includes(provider)) lines.push(`Provider: ${provider}`);
|
||||||
|
if (code) lines.push(`Code: ${code}`);
|
||||||
|
if (response) lines.push(`Response: ${response}`);
|
||||||
|
if (lines.length > 1) return lines.join("\n");
|
||||||
|
|
||||||
|
if (raw && !generic) return raw;
|
||||||
|
if (error && typeof error === "object") {
|
||||||
|
const serialized = safeStringify(error);
|
||||||
|
if (serialized && serialized !== "{}") return serialized;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildProviderAuthMethods = (
|
||||||
|
methods: Record<string, ProviderAuthMethod[]>,
|
||||||
|
availableProviders: ProviderListItem[],
|
||||||
|
workerType: "local" | "remote",
|
||||||
|
) => {
|
||||||
|
const merged = Object.fromEntries(
|
||||||
|
Object.entries(methods ?? {}).map(([id, providerMethods]) => [
|
||||||
|
id,
|
||||||
|
(providerMethods ?? []).map((method, methodIndex) => ({
|
||||||
|
...method,
|
||||||
|
methodIndex,
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
) as Record<string, ProviderAuthMethod[]>;
|
||||||
|
for (const provider of availableProviders ?? []) {
|
||||||
|
const id = provider.id?.trim();
|
||||||
|
if (!id || id === "opencode") continue;
|
||||||
|
if (!Array.isArray(provider.env) || provider.env.length === 0) continue;
|
||||||
|
const existing = merged[id] ?? [];
|
||||||
|
if (existing.some((method) => method.type === "api")) continue;
|
||||||
|
merged[id] = [...existing, { type: "api", label: "API key" }];
|
||||||
|
}
|
||||||
|
for (const [id, providerMethods] of Object.entries(merged)) {
|
||||||
|
const provider = availableProviders.find((item) => item.id === id);
|
||||||
|
const normalizedId = id.trim().toLowerCase();
|
||||||
|
const normalizedName = provider?.name?.trim().toLowerCase() ?? "";
|
||||||
|
const isOpenAiProvider = normalizedId === "openai" || normalizedName === "openai";
|
||||||
|
if (!isOpenAiProvider) continue;
|
||||||
|
merged[id] = providerMethods.filter((method) => {
|
||||||
|
if (method.type !== "oauth") return true;
|
||||||
|
const label = method.label.toLowerCase();
|
||||||
|
const isHeadless = label.includes("headless") || label.includes("device");
|
||||||
|
return workerType === "remote" ? isHeadless : !isHeadless;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProviderAuthMethods = async (workerType: "local" | "remote") => {
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Not connected to a server");
|
||||||
|
}
|
||||||
|
const methods = unwrap(await c.provider.auth());
|
||||||
|
return buildProviderAuthMethods(
|
||||||
|
methods as Record<string, ProviderAuthMethod[]>,
|
||||||
|
options.providers(),
|
||||||
|
workerType,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function startProviderAuth(
|
||||||
|
providerId?: string,
|
||||||
|
methodIndex?: number,
|
||||||
|
): Promise<ProviderOAuthStartResult> {
|
||||||
|
setProviderAuthError(null);
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Not connected to a server");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cachedMethods = providerAuthMethods();
|
||||||
|
const authMethods = Object.keys(cachedMethods).length
|
||||||
|
? cachedMethods
|
||||||
|
: await loadProviderAuthMethods(providerAuthWorkerType());
|
||||||
|
const providerIds = Object.keys(authMethods).sort();
|
||||||
|
if (!providerIds.length) {
|
||||||
|
throw new Error("No providers available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = providerId?.trim() ?? "";
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error("Provider ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods = authMethods[resolved];
|
||||||
|
if (!methods || !methods.length) {
|
||||||
|
throw new Error(`Unknown provider: ${resolved}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthIndex =
|
||||||
|
methodIndex !== undefined
|
||||||
|
? methodIndex
|
||||||
|
: methods.find((method) => method.type === "oauth")?.methodIndex ?? -1;
|
||||||
|
if (oauthIndex === -1) {
|
||||||
|
throw new Error(`No OAuth flow available for ${resolved}. Use an API key instead.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedMethod = methods.find((method) => method.methodIndex === oauthIndex);
|
||||||
|
if (!selectedMethod || selectedMethod.type !== "oauth") {
|
||||||
|
throw new Error(`Selected auth method is not an OAuth flow for ${resolved}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = unwrap(await c.provider.oauth.authorize({ providerID: resolved, method: oauthIndex }));
|
||||||
|
return {
|
||||||
|
methodIndex: oauthIndex,
|
||||||
|
authorization: auth,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = describeProviderError(error, "Failed to connect provider");
|
||||||
|
setProviderAuthError(message);
|
||||||
|
throw error instanceof Error ? error : new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshProviders(optionsArg?: { dispose?: boolean }) {
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) return null;
|
||||||
|
|
||||||
|
if (optionsArg?.dispose) {
|
||||||
|
try {
|
||||||
|
unwrap(await c.instance.dispose());
|
||||||
|
} catch {
|
||||||
|
// ignore dispose failures and try reading current state anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForHealthy(options.client() ?? c, { timeoutMs: 8_000, pollMs: 250 });
|
||||||
|
} catch {
|
||||||
|
// ignore health wait failures and still attempt provider reads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeClient = options.client() ?? c;
|
||||||
|
let disabledProviders = options.disabledProviders() ?? [];
|
||||||
|
try {
|
||||||
|
const config = unwrap(await activeClient.config.get());
|
||||||
|
disabledProviders = Array.isArray(config.disabled_providers) ? config.disabled_providers : [];
|
||||||
|
options.setDisabledProviders(disabledProviders);
|
||||||
|
} catch {
|
||||||
|
// ignore config read failures and continue with current store state
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const updated = filterProviderList(
|
||||||
|
unwrap(await activeClient.provider.list()),
|
||||||
|
disabledProviders,
|
||||||
|
);
|
||||||
|
applyProviderListState(updated);
|
||||||
|
return updated;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const fallback = unwrap(await activeClient.config.providers());
|
||||||
|
const mapped = mapConfigProvidersToList(fallback.providers);
|
||||||
|
const next = filterProviderList(
|
||||||
|
{
|
||||||
|
all: mapped,
|
||||||
|
connected: options.providerConnectedIds().filter((id) => mapped.some((provider) => provider.id === id)),
|
||||||
|
default: fallback.default,
|
||||||
|
},
|
||||||
|
disabledProviders,
|
||||||
|
);
|
||||||
|
applyProviderListState(next);
|
||||||
|
return next;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeProviderAuthOAuth(providerId: string, methodIndex: number, code?: string) {
|
||||||
|
setProviderAuthError(null);
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Not connected to a server");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = providerId?.trim();
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error("Provider ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(methodIndex) || methodIndex < 0) {
|
||||||
|
throw new Error("OAuth method is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForProviderConnection = async (timeoutMs = 15_000, pollMs = 2_000) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
try {
|
||||||
|
const updated = await refreshProviders({ dispose: true });
|
||||||
|
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore and retry
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPendingOauthError = (error: unknown) => {
|
||||||
|
const text = error instanceof Error ? error.message : String(error ?? "");
|
||||||
|
return /request timed out/i.test(text) || /ProviderAuthOauthMissing/i.test(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const trimmedCode = code?.trim();
|
||||||
|
const result = await c.provider.oauth.callback({
|
||||||
|
providerID: resolved,
|
||||||
|
method: methodIndex,
|
||||||
|
code: trimmedCode || undefined,
|
||||||
|
});
|
||||||
|
assertNoClientError(result);
|
||||||
|
const updated = await refreshProviders({ dispose: true });
|
||||||
|
const connectedNow = Array.isArray(updated?.connected) && updated.connected.includes(resolved);
|
||||||
|
if (connectedNow) {
|
||||||
|
return { connected: true, message: `Connected ${resolved}` };
|
||||||
|
}
|
||||||
|
const connected = await waitForProviderConnection();
|
||||||
|
if (connected) {
|
||||||
|
return { connected: true, message: `Connected ${resolved}` };
|
||||||
|
}
|
||||||
|
return { connected: false, pending: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (isPendingOauthError(error)) {
|
||||||
|
const updated = await refreshProviders({ dispose: true });
|
||||||
|
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
||||||
|
return { connected: true, message: `Connected ${resolved}` };
|
||||||
|
}
|
||||||
|
const connected = await waitForProviderConnection();
|
||||||
|
if (connected) {
|
||||||
|
return { connected: true, message: `Connected ${resolved}` };
|
||||||
|
}
|
||||||
|
return { connected: false, pending: true };
|
||||||
|
}
|
||||||
|
const message = describeProviderError(error, "Failed to complete OAuth");
|
||||||
|
setProviderAuthError(message);
|
||||||
|
throw error instanceof Error ? error : new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitProviderApiKey(providerId: string, apiKey: string) {
|
||||||
|
setProviderAuthError(null);
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Not connected to a server");
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = apiKey.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error("API key is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await c.auth.set({
|
||||||
|
providerID: providerId,
|
||||||
|
auth: { type: "api", key: trimmed },
|
||||||
|
});
|
||||||
|
await refreshProviders({ dispose: true });
|
||||||
|
return `Connected ${providerId}`;
|
||||||
|
} catch (error) {
|
||||||
|
const message = describeProviderError(error, "Failed to save API key");
|
||||||
|
setProviderAuthError(message);
|
||||||
|
throw error instanceof Error ? error : new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectProvider(providerId: string) {
|
||||||
|
setProviderAuthError(null);
|
||||||
|
const c = options.client();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Not connected to a server");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = providerId.trim();
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error("Provider ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = options.providers().find((entry) => entry.id === resolved) as
|
||||||
|
| (ProviderListItem & { source?: string })
|
||||||
|
| undefined;
|
||||||
|
const canDisableProvider =
|
||||||
|
provider?.source === "config" || provider?.source === "custom";
|
||||||
|
|
||||||
|
const removeProviderAuth = async () => {
|
||||||
|
const authClient = c.auth as unknown as {
|
||||||
|
remove?: (options: { providerID: string }) => Promise<unknown>;
|
||||||
|
set?: (options: { providerID: string; auth: unknown }) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
if (typeof authClient.remove === "function") {
|
||||||
|
const result = await authClient.remove({ providerID: resolved });
|
||||||
|
assertNoClientError(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawClient = (c as unknown as { client?: { delete?: (options: { url: string }) => Promise<unknown> } })
|
||||||
|
.client;
|
||||||
|
if (rawClient?.delete) {
|
||||||
|
await rawClient.delete({ url: `/auth/${encodeURIComponent(resolved)}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof authClient.set === "function") {
|
||||||
|
const result = await authClient.set({ providerID: resolved, auth: null });
|
||||||
|
assertNoClientError(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Provider auth removal is not supported by this client.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableProvider = async () => {
|
||||||
|
const config = unwrap(await c.config.get());
|
||||||
|
const disabledProviders = Array.isArray(config.disabled_providers)
|
||||||
|
? config.disabled_providers
|
||||||
|
: [];
|
||||||
|
if (disabledProviders.includes(resolved)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = [...disabledProviders, resolved];
|
||||||
|
options.setDisabledProviders(next);
|
||||||
|
try {
|
||||||
|
const result = await c.config.update({
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
disabled_providers: next,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assertNoClientError(result);
|
||||||
|
options.markOpencodeConfigReloadRequired();
|
||||||
|
} catch (error) {
|
||||||
|
options.setDisabledProviders(disabledProviders);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeProviderAuth();
|
||||||
|
let updated = await refreshProviders({ dispose: true });
|
||||||
|
if (
|
||||||
|
canDisableProvider &&
|
||||||
|
Array.isArray(updated?.connected) &&
|
||||||
|
updated.connected.includes(resolved)
|
||||||
|
) {
|
||||||
|
const disabled = await disableProvider();
|
||||||
|
if (disabled && updated) {
|
||||||
|
updated = filterProviderList(updated, options.disabledProviders() ?? []);
|
||||||
|
applyProviderListState(updated);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(updated?.connected) || !updated.connected.includes(resolved)) {
|
||||||
|
return disabled
|
||||||
|
? `Disconnected ${resolved} and disabled it in OpenCode config.`
|
||||||
|
: `Disconnected ${resolved}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
|
||||||
|
return `Removed stored credentials for ${resolved}, but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.`;
|
||||||
|
}
|
||||||
|
removeProviderFromState(resolved);
|
||||||
|
return `Disconnected ${resolved}`;
|
||||||
|
} catch (error) {
|
||||||
|
const message = describeProviderError(error, "Failed to disconnect provider");
|
||||||
|
setProviderAuthError(message);
|
||||||
|
throw error instanceof Error ? error : new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openProviderAuthModal(optionsArg?: {
|
||||||
|
returnFocusTarget?: ProviderReturnFocusTarget;
|
||||||
|
preferredProviderId?: string;
|
||||||
|
}) {
|
||||||
|
setProviderAuthReturnFocusTarget(optionsArg?.returnFocusTarget ?? "none");
|
||||||
|
setProviderAuthPreferredProviderId(optionsArg?.preferredProviderId?.trim() || null);
|
||||||
|
setProviderAuthBusy(true);
|
||||||
|
setProviderAuthError(null);
|
||||||
|
try {
|
||||||
|
const methods = await loadProviderAuthMethods(providerAuthWorkerType());
|
||||||
|
setProviderAuthMethods(methods);
|
||||||
|
setProviderAuthModalOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
setProviderAuthPreferredProviderId(null);
|
||||||
|
setProviderAuthReturnFocusTarget("none");
|
||||||
|
const message = describeProviderError(error, "Failed to load providers");
|
||||||
|
setProviderAuthError(message);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setProviderAuthBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProviderAuthModal(optionsArg?: { restorePromptFocus?: boolean }) {
|
||||||
|
const shouldFocusPrompt =
|
||||||
|
optionsArg?.restorePromptFocus ??
|
||||||
|
providerAuthReturnFocusTarget() === "composer";
|
||||||
|
setProviderAuthModalOpen(false);
|
||||||
|
setProviderAuthError(null);
|
||||||
|
setProviderAuthPreferredProviderId(null);
|
||||||
|
setProviderAuthReturnFocusTarget("none");
|
||||||
|
if (shouldFocusPrompt) {
|
||||||
|
options.focusPromptSoon?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
providerAuthModalOpen,
|
||||||
|
providerAuthBusy,
|
||||||
|
providerAuthError,
|
||||||
|
providerAuthMethods,
|
||||||
|
providerAuthPreferredProviderId,
|
||||||
|
providerAuthWorkerType,
|
||||||
|
startProviderAuth,
|
||||||
|
refreshProviders,
|
||||||
|
completeProviderAuthOAuth,
|
||||||
|
submitProviderApiKey,
|
||||||
|
disconnectProvider,
|
||||||
|
openProviderAuthModal,
|
||||||
|
closeProviderAuthModal,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -50,10 +50,10 @@ import Button from "../components/button";
|
|||||||
import ConfigView from "./config";
|
import ConfigView from "./config";
|
||||||
import SettingsView from "./settings";
|
import SettingsView from "./settings";
|
||||||
import StatusBar from "../components/status-bar";
|
import StatusBar from "../components/status-bar";
|
||||||
import ProviderAuthModal, {
|
import { ProviderAuthModal,
|
||||||
type ProviderAuthMethod,
|
type ProviderAuthMethod,
|
||||||
type ProviderOAuthStartResult,
|
type ProviderOAuthStartResult,
|
||||||
} from "../components/provider-auth-modal";
|
} from "../context/providers";
|
||||||
import ShareWorkspaceModal from "../components/share-workspace-modal";
|
import ShareWorkspaceModal from "../components/share-workspace-modal";
|
||||||
import WorkspaceSessionList from "../components/session/workspace-session-list";
|
import WorkspaceSessionList from "../components/session/workspace-session-list";
|
||||||
import WorkspaceToolsPanel from "./workspace-tools-panel";
|
import WorkspaceToolsPanel from "./workspace-tools-panel";
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ import {
|
|||||||
import Button from "../components/button";
|
import Button from "../components/button";
|
||||||
import ConfirmModal from "../components/confirm-modal";
|
import ConfirmModal from "../components/confirm-modal";
|
||||||
import RenameSessionModal from "../components/rename-session-modal";
|
import RenameSessionModal from "../components/rename-session-modal";
|
||||||
import ProviderAuthModal, {
|
import { ProviderAuthModal,
|
||||||
type ProviderAuthMethod,
|
type ProviderAuthMethod,
|
||||||
type ProviderOAuthStartResult,
|
type ProviderOAuthStartResult,
|
||||||
} from "../components/provider-auth-modal";
|
} from "../context/providers";
|
||||||
import ShareWorkspaceModal from "../components/share-workspace-modal";
|
import ShareWorkspaceModal from "../components/share-workspace-modal";
|
||||||
import StatusBar from "../components/status-bar";
|
import StatusBar from "../components/status-bar";
|
||||||
import {
|
import {
|
||||||
|
|||||||
Reference in New Issue
Block a user