refactor(connections): move provider auth out of app shell

This commit is contained in:
Benjamin Shafii
2026-03-28 20:46:09 -07:00
parent 88cf83aabc
commit 36e4587866
6 changed files with 612 additions and 576 deletions

View File

@@ -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(),

View File

@@ -0,0 +1,3 @@
export { createProvidersStore } from "./store";
export type { ProviderAuthMethod, ProviderOAuthStartResult } from "./store";
export { default as ProviderAuthModal } from "./provider-auth-modal";

View File

@@ -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) => (

View 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,
};
}

View File

@@ -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";

View File

@@ -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 {