Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/opencode/src/server/amicode/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -152,15 +152,15 @@ 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",
kind: "built-in",
name: "Google Drive",
icon: { kind: "svg", svg: CONNECTION_ICONS["google-drive"] },
validator: "google-drive",
authShape: "token-only",
authShape: "browser",
},
]

Expand Down
69 changes: 51 additions & 18 deletions packages/ui/src/amicode/connection-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function ConnectionPicker(props: {
catalog: CatalogEntry[]
onAddCustom: (payload: { name: string; token: string; url?: string }) => Promise<void>
onSubmitToken: (id: string, token: string) => Promise<void>
onStartBrowser?: (id: string) => void
onClose: () => void
}) {
const [picked, setPicked] = createSignal<string | undefined>(undefined)
Expand All @@ -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())
Expand All @@ -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"

Expand Down Expand Up @@ -73,6 +88,9 @@ export function ConnectionPicker(props: {
>
<span class="w-[18px] h-[18px] flex items-center justify-center text-text-base" innerHTML={entry.icon} />
<span class="text-12-regular text-text-base">{entry.name}</span>
<Show when={entry.authShape === "browser"}>
<span class="ml-auto text-10-regular text-text-weaker border border-border-weak-base rounded px-1 py-0">Browser</span>
</Show>
</button>
)}
</For>
Expand Down Expand Up @@ -120,25 +138,40 @@ export function ConnectionPicker(props: {
</Show>

<Show when={isBuiltIn()}>
<form class="flex flex-col gap-1.5" onSubmit={submitToken} data-slot="amicode-picker-token-form">
<span class="text-12-regular text-text-base">{picked()}</span>
<input
type="password"
placeholder="Token"
aria-label="Token"
value={token()}
onInput={(e) => setToken(e.currentTarget.value)}
class="amc-input amc-input--compact"
/>
<div class="flex gap-2">
<Button type="submit" variant="primary" size="small">
Connect
</Button>
<Button type="button" variant="ghost" size="small" onClick={() => setPicked(undefined)}>
Back
</Button>
<Show when={isBrowserEntry()} fallback={
<form class="flex flex-col gap-1.5" onSubmit={submitToken} data-slot="amicode-picker-token-form">
<span class="text-12-regular text-text-base">{picked()}</span>
<input
type="password"
placeholder="Token"
aria-label="Token"
value={token()}
onInput={(e) => setToken(e.currentTarget.value)}
class="amc-input amc-input--compact"
/>
<div class="flex gap-2">
<Button type="submit" variant="primary" size="small">
Connect
</Button>
<Button type="button" variant="ghost" size="small" onClick={() => setPicked(undefined)}>
Back
</Button>
</div>
</form>
}>
<div class="flex flex-col gap-1.5" data-slot="amicode-picker-browser-form">
<span class="text-12-regular text-text-base">{picked()}</span>
<span class="text-11-regular text-text-weaker">Sign in with your browser to connect {pickedEntry()?.name ?? picked()}.</span>
<div class="flex gap-2">
<Button type="button" variant="primary" size="small" onClick={startBrowser} data-slot="amicode-picker-browser-start">
Sign in with browser
</Button>
<Button type="button" variant="ghost" size="small" onClick={() => setPicked(undefined)}>
Back
</Button>
</div>
</div>
</form>
</Show>
</Show>
</div>
)
Expand Down
13 changes: 13 additions & 0 deletions packages/ui/src/amicode/connections-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
/>
</Show>
Expand Down Expand Up @@ -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)}
/>
</div>
Expand Down
7 changes: 4 additions & 3 deletions packages/ui/src/amicode/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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)
Expand Down
Loading