diff --git a/packages/opencode/src/server/amicode/connections.ts b/packages/opencode/src/server/amicode/connections.ts index ff344d807..217cb3f93 100644 --- a/packages/opencode/src/server/amicode/connections.ts +++ b/packages/opencode/src/server/amicode/connections.ts @@ -101,7 +101,7 @@ export interface ConnectionEntry { name: string icon: { kind: "svg"; svg: string } | { kind: "letter"; letter: string } validator: "company-compute" | "pasqal" | "slack" | "github" | "linear" | "google" | "google-drive" | "none" - authShape: "base-url-token" | "token-only" | "pasqal-credentials" + authShape: "base-url-token" | "token-only" | "pasqal-credentials" | "browser" url?: string } @@ -152,7 +152,7 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [ name: "Google", icon: { kind: "svg", svg: CONNECTION_ICONS["google"] }, validator: "google", - authShape: "token-only", + authShape: "browser", }, { id: "google-drive", @@ -160,7 +160,7 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [ name: "Google Drive", icon: { kind: "svg", svg: CONNECTION_ICONS["google-drive"] }, validator: "google-drive", - authShape: "token-only", + authShape: "browser", }, ] diff --git a/packages/ui/src/amicode/connection-picker.tsx b/packages/ui/src/amicode/connection-picker.tsx index 1f1b82c15..15c961d22 100644 --- a/packages/ui/src/amicode/connection-picker.tsx +++ b/packages/ui/src/amicode/connection-picker.tsx @@ -9,6 +9,7 @@ export function ConnectionPicker(props: { catalog: CatalogEntry[] onAddCustom: (payload: { name: string; token: string; url?: string }) => Promise onSubmitToken: (id: string, token: string) => Promise + onStartBrowser?: (id: string) => void onClose: () => void }) { const [picked, setPicked] = createSignal(undefined) @@ -17,6 +18,9 @@ export function ConnectionPicker(props: { const [customUrl, setCustomUrl] = createSignal("") const [token, setToken] = createSignal("") + const pickedEntry = () => props.catalog.find((e) => e.id === picked()) + const isBrowserEntry = () => pickedEntry()?.authShape === "browser" + const submitCustom = async (e: Event) => { e.preventDefault() const payload = customConnectionPayload(customName(), customToken(), customUrl()) @@ -39,6 +43,17 @@ export function ConnectionPicker(props: { setPicked(undefined) } + const startBrowser = (e: Event) => { + e.preventDefault() + const id = picked() + if (!id) return + if (props.onStartBrowser) props.onStartBrowser(id) + else { + // Fallback: if no browser handler, treat as token flow for backwards compat + } + setPicked(undefined) + } + const isCustom = () => picked() === "custom" const isBuiltIn = () => picked() !== undefined && picked() !== "custom" @@ -73,6 +88,9 @@ export function ConnectionPicker(props: { > {entry.name} + + Browser + )} @@ -120,25 +138,40 @@ export function ConnectionPicker(props: { -
- {picked()} - setToken(e.currentTarget.value)} - class="amc-input amc-input--compact" - /> -
- - + + {picked()} + setToken(e.currentTarget.value)} + class="amc-input amc-input--compact" + /> +
+ + +
+ + }> +
+ {picked()} + Sign in with your browser to connect {pickedEntry()?.name ?? picked()}. +
+ + +
- +
) diff --git a/packages/ui/src/amicode/connections-tab.tsx b/packages/ui/src/amicode/connections-tab.tsx index 1bac7a8db..5c80e3d74 100644 --- a/packages/ui/src/amicode/connections-tab.tsx +++ b/packages/ui/src/amicode/connections-tab.tsx @@ -132,6 +132,13 @@ export function AmicodeConnectionsTab(props: { if (payload) await props.onSubmit(payload as CredentialSubmitPayload) setShowPicker(false) }} + onStartBrowser={(id) => { + // Browser login for google/google-drive: start OAuth in browser + const fakeView = { id, state: "needs-key" as const, rawState: "needs-key", validatedAt: "—", stale: false, authMethods: ["browser" as const] } + const payload = startAuthPayload(fakeView as any, "browser") + if (payload) props.onStartAuth?.(payload) + setShowPicker(false) + }} onClose={() => setShowPicker(false)} />
@@ -172,6 +179,12 @@ export function AmicodeConnectionsTab(props: { if (payload) await props.onSubmit(payload as CredentialSubmitPayload) setShowPicker(false) }} + onStartBrowser={(id) => { + const fakeView = { id, state: "needs-key" as const, rawState: "needs-key", validatedAt: "—", stale: false, authMethods: ["browser" as const] } + const payload = startAuthPayload(fakeView as any, "browser") + if (payload) props.onStartAuth?.(payload) + setShowPicker(false) + }} onClose={() => setShowPicker(false)} /> diff --git a/packages/ui/src/amicode/connections.ts b/packages/ui/src/amicode/connections.ts index b5fa28478..f7df41da5 100644 --- a/packages/ui/src/amicode/connections.ts +++ b/packages/ui/src/amicode/connections.ts @@ -439,11 +439,12 @@ export type CredentialSubmitPayload = BaseUrlTokenPayload | PasqalCredentialsPay /** Which credential fields a card's form collects (169): pasqal-cloud takes * username/password/project_id; every other id keeps base_url + token. * #327: slack/github/linear take token-only; custom takes name+token+url. */ -export type ConnectionFormKind = "base-url-token" | "pasqal-credentials" | "token-only" | "custom" +export type ConnectionFormKind = "base-url-token" | "pasqal-credentials" | "token-only" | "custom" | "browser" export function connectionFormKind(id: string): ConnectionFormKind { if (id === PASQAL_ID) return "pasqal-credentials" - if (id === SLACK_ID || id === GITHUB_ID || id === LINEAR_ID || id === GOOGLE_ID || id === GOOGLE_DRIVE_ID) return "token-only" + if (id === GOOGLE_ID || id === GOOGLE_DRIVE_ID) return "browser" + if (id === SLACK_ID || id === GITHUB_ID || id === LINEAR_ID) return "token-only" if (isCustomConnectionId(id)) return "custom" return "base-url-token" } @@ -484,7 +485,7 @@ export function methodEntryKind(id: string, method: ConnectionAuthMethod): Metho if (method === "credentials") return connectionFormKind(id) if (method === "token") { const kind = connectionFormKind(id) - if (kind === "token-only" || kind === "custom") return kind + if (kind === "token-only" || kind === "custom" || kind === "browser") return kind return id === PASQAL_ID ? "pasqal-token" : "base-url-token" } return connectionFormKind(id)