diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index b042205cf4d..adb2e5fbe8a 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -181,11 +181,24 @@ export function DialogConnectProvider(props: { provider: string }) { async function complete() { await globalSDK.client.global.dispose() dialog.close() + + // Fetch identity to show which account was connected + let email: string | undefined + try { + const response = await fetch("/auth/identity") + if (response.ok) { + const identities = (await response.json()) as Record + email = identities[props.provider] + } + } catch {} + showToast({ variant: "success", icon: "circle-check", title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), - description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), + description: email + ? language.t("provider.connect.toast.connected.description", { provider: provider().name }) + ` (${email})` + : language.t("provider.connect.toast.connected.description", { provider: provider().name }), }) } diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index cc69327f80d..4c97a547ebc 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -4,7 +4,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tag } from "@opencode-ai/ui/tag" import { showToast } from "@opencode-ai/ui/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" +import { createMemo, createSignal, onMount, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" @@ -34,6 +34,17 @@ export const SettingsProviders: Component = () => { const globalSync = useGlobalSync() const providers = useProviders() + const [identity, setIdentity] = createSignal>({}) + + onMount(async () => { + try { + const response = await fetch("/auth/identity") + if (response.ok) { + setIdentity(await response.json()) + } + } catch {} + }) + const connected = createMemo(() => { return providers .connected() @@ -153,6 +164,11 @@ export const SettingsProviders: Component = () => { {item.name} {type(item)} + + {(email) => ( + {email()} + )} + ("OAuth")({ expires: Schema.Number, accountId: Schema.optional(Schema.String), enterpriseUrl: Schema.optional(Schema.String), + email: Schema.optional(Schema.String), }) {} export class Api extends Schema.Class("ApiAuth")({ diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 6f588e93751..f68c2272430 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -18,6 +18,7 @@ export namespace Auth { expires: z.number(), accountId: z.string().optional(), enterpriseUrl: z.string().optional(), + email: z.string().optional(), }) .meta({ ref: "OAuth" }) diff --git a/packages/opencode/src/plugin/copilot.ts b/packages/opencode/src/plugin/copilot.ts index 44c5289dd15..02a31158a3e 100644 --- a/packages/opencode/src/plugin/copilot.ts +++ b/packages/opencode/src/plugin/copilot.ts @@ -253,6 +253,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { expires: number provider?: string enterpriseUrl?: string + email?: string } = { type: "success", refresh: data.access_token, @@ -264,6 +265,24 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { result.enterpriseUrl = domain } + // Fetch GitHub user identity (best-effort) + try { + const apiBase = domain === "github.com" + ? "https://api.github.com" + : `https://${domain}/api/v3` + const userResponse = await fetch(`${apiBase}/user`, { + headers: { + Authorization: `Bearer ${data.access_token}`, + Accept: "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + }, + }) + if (userResponse.ok) { + const user = (await userResponse.json()) as { login?: string; email?: string } + result.email = user.login ?? user.email + } + } catch {} + return result } diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 5204b5fb8d3..7d0dfe84ad6 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -205,6 +205,7 @@ export namespace ProviderAuth { refresh: result.refresh, expires: result.expires, ...(result.accountId ? { accountId: result.accountId } : {}), + ...(result.email ? { email: result.email } : {}), }) } }) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c485654fdf8..9931ee2e819 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -192,6 +192,34 @@ export namespace Server { return c.json(true) }, ) + .get( + "/auth/identity", + describeRoute({ + summary: "Get provider identities", + description: "Get the email or username associated with each authenticated provider.", + operationId: "auth.identity", + responses: { + 200: { + description: "Map of provider IDs to identity strings", + content: { + "application/json": { + schema: resolver(z.record(z.string(), z.string())), + }, + }, + }, + }, + }), + async (c) => { + const all = await Auth.all() + const identity: Record = {} + for (const [providerID, info] of Object.entries(all)) { + if (info.type === "oauth" && "email" in info && info.email) { + identity[providerID] = info.email + } + } + return c.json(identity) + }, + ) .use(async (c, next) => { if (c.req.path === "/log") return next() const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace") diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 7e5ae7a6ec5..e4c528ab89b 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -123,6 +123,7 @@ export type AuthOuathResult = { url: string; instructions: string } & ( | ({ type: "success" provider?: string + email?: string } & ( | { refresh: string @@ -143,6 +144,7 @@ export type AuthOuathResult = { url: string; instructions: string } & ( | ({ type: "success" provider?: string + email?: string } & ( | { refresh: string