1 line
27 KiB
JSON
1 line
27 KiB
JSON
{"repo": "aaron-sequeira/crokcode", "files": {"packages/tui/src/component/dialog-provider.tsx": {"0af532850b2fffb9904f12c00b561248b94a6e86": {"t": 1785596400.3117187, "n": 25865, "h": "a831273f9fa6d661", "c": "import { createMemo, createSignal, onCleanup, onMount, Show } from \"solid-js\"\nimport { useSync } from \"../context/sync\"\nimport { map, pipe, sortBy } from \"remeda\"\nimport { DialogSelect } from \"../ui/dialog-select\"\nimport { useDialog } from \"../ui/dialog\"\nimport { useSDK } from \"../context/sdk\"\nimport { DialogPrompt } from \"../ui/dialog-prompt\"\nimport { Link } from \"../ui/link\"\nimport { useTheme } from \"../context/theme\"\nimport { TextAttributes } from \"@opentui/core\"\nimport type { ProviderAuthAuthorization, ProviderAuthMethod } from \"@crokcode/sdk/v2\"\nimport { DialogModel } from \"./dialog-model\"\nimport { useToast } from \"../ui/toast\"\nimport { isConsoleManagedProvider } from \"../util/provider-origin\"\nimport { useConnected } from \"./use-connected\"\nimport { useBindings } from \"../keymap\"\nimport { useClipboard } from \"../context/clipboard\"\nimport { ollamaProvider, ollamaStatus } from \"../util/local-models\"\n\n// Server providers that still belong in \"Popular\". OpenCode Zen (crokcode) and\n// OpenCode Go (opencode-go) were removed so CrokCode's own plans lead instead.\nconst PROVIDER_PRIORITY: Record<string, number> = {\n openai: 0,\n \"github-copilot\": 1,\n anthropic: 2,\n google: 3,\n}\n\n// CrokCode plans, shown at the top of \"Popular\". All three browser-pair and\n// connect the same `crokapi` gateway provider; the gateway enforces the\n// account's real plan (so a key only works with the plan the user has).\nconst CROK_PLANS = [\n { id: \"crokgo\", title: \"CrokGo\", description: \"$5 first month, then $10/mo — 9 efficient models\" },\n { id: \"crokpro\", title: \"CrokPro\", description: \"$20/mo — all 21 models (Recommended)\" },\n { id: \"crok-as-you-go\", title: \"Crok-as-you-go\", description: \"Pay as you go — all 21 models, no caps\" },\n] as const\n\nconst CUSTOM_PROVIDER_OPTION_VALUE = \"__crokcode_custom_provider__\"\nconst CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/\n\ntype ProviderOptionBase = {\n title: string\n value: string\n description?: string\n category: string\n}\n\ntype ProviderOption =\n | (ProviderOptionBase & {\n type: \"provider\"\n providerID: string\n })\n | (ProviderOptionBase & {\n type: \"custom\"\n })\n | (ProviderOptionBase & {\n type: \"plan\"\n planID: string\n })\n | (ProviderOptionBase & {\n type: \"local\"\n })\n\nexport function providerOptions(list: { id: string; name: string }[]): ProviderOption[] {\n return [\n // CrokCode plans lead the \"Popular\" section.\n ...CROK_PLANS.map((plan) => ({\n type: \"plan\" as const,\n title: plan.title,\n value: `__crok_plan_${plan.id}`,\n description: plan.description,\n category: \"Popular\",\n planID: plan.id,\n })),\n {\n type: \"local\" as const,\n title: \"Local models\",\n value: \"__crok_local__\",\n description: \"Use models installed in Ollama\",\n category: \"Popular\",\n },\n ...pipe(\n list,\n sortBy(\n (x) => PROVIDER_PRIORITY[x.id] ?? 99,\n (x) => x.name.toLowerCase(),\n (x) => x.id,\n ),\n map((provider) => ({\n type: \"provider\" as const,\n title: provider.name,\n value: provider.id,\n providerID: provider.id,\n description: {\n opencode: \"(Recommended)\",\n anthropic: \"(API key)\",\n openai: \"(ChatGPT Plus/Pro or API key)\",\n \"opencode-go\": \"Low cost subscription for everyone\",\n }[provider.id],\n category: provider.id in PROVIDER_PRIORITY ? \"Popular\" : \"Providers\",\n })),\n ),\n {\n type: \"custom\",\n title: \"Other\",\n value: CUSTOM_PROVIDER_OPTION_VALUE,\n description: \"Custom provider\",\n category: \"Providers\",\n },\n ]\n}\n\nexport function normalizeCustomProviderID(value: string) {\n const providerID = value.trim().replace(/^@ai-sdk\\//, \"\")\n if (!CUSTOM_PROVIDER_ID.test(providerID)) return\n return providerID\n}\n\nexport function createDialogProviderOptions() {\n const sync = useSync()\n const dialog = useDialog()\n const sdk = useSDK()\n const toast = useToast()\n const { theme } = useTheme()\n const onboarded = useConnected()\n\n async function promptCustomProviderID(): Promise<string | undefined> {\n const value = await DialogPrompt.show(dialog, \"Other\", {\n placeholder: \"Provider id\",\n description: () => (\n <text fg={theme.textMuted}>\n This only stores a credential. Configure the provider in crokcode.json to use it.\n </text>\n ),\n })\n if (value === null) return\n\n const providerID = normalizeCustomProviderID(value)\n if (providerID) return providerID\n\n toast.show({\n variant: \"error\",\n message:\n \"Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores\",\n })\n return promptCustomProviderID()\n }\n\n const options = createMemo(() => {\n return pipe(\n providerOptions(sync.data.provider_next.all),\n map((provider) => {\n if (provider.type === \"custom\") {\n return {\n title: provider.title,\n value: provider.value,\n description: provider.description,\n category: provider.category,\n async onSelect() {\n const providerID = await promptCustomProviderID()\n if (!providerID) return\n return dialog.replace(() => <ApiMethod providerID={providerID} title=\"API key\" custom />)\n },\n }\n }\n\n if (provider.type === \"plan\") {\n return {\n title: provider.title,\n value: provider.value,\n description: provider.description,\n category: provider.category,\n async onSelect() {\n return dialog.replace(() => <PlanConnect planID={provider.planID} title={provider.title} />)\n },\n }\n }\n\n if (provider.type === \"local\") {\n return {\n title: provider.title,\n value: provider.value,\n description: provider.description,\n category: provider.category,\n async onSelect() {\n const status = await ollamaStatus()\n if (!status.running) {\n toast.show({ variant: \"warning\", message: \"Ollama is not running. Start Ollama and try again.\" })\n return\n }\n if (status.models.length === 0) {\n toast.show({\n variant: \"warning\",\n message: \"No Ollama models are installed. Install one with Ollama, then try again.\",\n })\n return\n }\n try {\n await sdk.client.global.config.update({\n config: { provider: { ollama: ollamaProvider(status.models) } } as any,\n })\n await sdk.client.instance.dispose()\n await sync.bootstrap()\n dialog.replace(() => <DialogModel providerID=\"ollama\" />)\n } catch {\n toast.show({ variant: \"error\", message: \"Could not connect the installed Ollama models.\" })\n dialog.clear()\n }\n },\n }\n }\n\n const providerID = provider.providerID\n const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID)\n const connected = sync.data.provider_next.connected.includes(providerID)\n\n return {\n title: provider.title,\n value: provider.value,\n description: provider.description,\n footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,\n category: provider.category,\n gutter: connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined,\n async onSelect() {\n if (consoleManaged) return\n\n const methods = sync.data.provider_auth[providerID] ?? [\n {\n type: \"api\",\n label: \"API key\",\n },\n ]\n let index: number | null = 0\n if (methods.length > 1) {\n index = await new Promise<number | null>((resolve) => {\n dialog.replace(\n () => (\n <DialogSelect\n title=\"Select auth method\"\n options={methods.map((x, index) => ({\n title: x.label,\n value: index,\n }))}\n onSelect={(option) => resolve(option.value)}\n />\n ),\n () => resolve(null),\n )\n })\n }\n if (index == null) return\n const method = methods[index]\n if (method.type === \"oauth\") {\n let inputs: Record<string, string> | undefined\n if (method.prompts?.length) {\n const value = await PromptsMethod({\n dialog,\n prompts: method.prompts,\n })\n if (!value) return\n inputs = value\n }\n\n const result = await sdk.client.provider.oauth.authorize({\n providerID,\n method: index,\n inputs,\n })\n if (result.error) {\n toast.show({\n variant: \"error\",\n message: JSON.stringify(result.error),\n })\n dialog.clear()\n return\n }\n if (result.data?.method === \"code\") {\n dialog.replace(() => (\n <CodeMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />\n ))\n }\n if (result.data?.method === \"auto\") {\n dialog.replace(() => (\n <AutoMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />\n ))\n }\n }\n if (method.type === \"api\") {\n let metadata: Record<string, string> | undefined\n if (method.prompts?.length) {\n const value = await PromptsMethod({ dialog, prompts: method.prompts })\n if (!value) return\n metadata = value\n }\n return dialog.replace(() => (\n <ApiMethod providerID={providerID} title={method.label} metadata={metadata} />\n ))\n }\n },\n }\n }),\n )\n })\n return options\n}\n\nexport function DialogProvider() {\n const options = createDialogProviderOptions()\n return <DialogSelect title=\"Connect a provider\" options={options()} />\n}\n\ninterface AutoMethodProps {\n index: number\n providerID: string\n title: string\n authorization: ProviderAuthAuthorization\n}\nfunction AutoMethod(props: AutoMethodProps) {\n const { theme } = useTheme()\n const sdk = useSDK()\n const dialog = useDialog()\n const sync = useSync()\n const toast = useToast()\n const clipboard = useClipboard()\n\n useBindings(() => ({\n bindings: [\n {\n key: \"c\",\n desc: \"Copy provider code\",\n group: \"Dialog\",\n cmd: () => {\n const code =\n props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url\n clipboard\n .write?.(code)\n .then(() => toast.show({ message: \"Copied to clipboard\", variant: \"info\" }))\n .catch(toast.error)\n },\n },\n ],\n }))\n\n onMount(async () => {\n const result = await sdk.client.provider.oauth.callback({\n providerID: props.providerID,\n method: props.index,\n })\n if (result.error) {\n toast.show({\n variant: \"error\",\n message:\n \"name\" in result.error && result.error.name === \"ProviderAuthOauthCallbackFailed\"\n ? \"OAuth authorization failed. Try /connect again.\"\n : JSON.stringify(result.error),\n })\n dialog.clear()\n return\n }\n await sdk.client.instance.dispose()\n await sync.bootstrap()\n dialog.replace(() => <DialogModel providerID={props.providerID} />)\n })\n\n return (\n <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>\n <box flexDirection=\"row\" justifyContent=\"space-between\">\n <text attributes={TextAttributes.BOLD} fg={theme.text}>\n {props.title}\n </text>\n <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>\n esc\n </text>\n </box>\n <box gap={1}>\n <Link href={props.authorization.url} fg={theme.primary} />\n <text fg={theme.textMuted}>{props.authorization.instructions}</text>\n </box>\n <text fg={theme.textMuted}>Waiting for authorization...</text>\n <text fg={theme.text}>\n c <span style={{ fg: theme.textMuted }}>copy</span>\n </text>\n </box>\n )\n}\n\ninterface CodeMethodProps {\n index: number\n title: string\n providerID: string\n authorization: ProviderAuthAuthorization\n}\nfunction CodeMethod(props: CodeMethodProps) {\n const { theme } = useTheme()\n const sdk = useSDK()\n const sync = useSync()\n const dialog = useDialog()\n const [error, setError] = createSignal(false)\n\n return (\n <DialogPrompt\n title={props.title}\n placeholder=\"Authorization code\"\n onConfirm={async (value) => {\n const { error } = await sdk.client.provider.oauth.callback({\n providerID: props.providerID,\n method: props.index,\n code: value,\n })\n if (!error) {\n await sdk.client.instance.dispose()\n await sync.bootstrap()\n dialog.replace(() => <DialogModel providerID={props.providerID} />)\n return\n }\n setError(true)\n }}\n description={() => (\n <box gap={1}>\n <text fg={theme.textMuted}>{props.authorization.instructions}</text>\n <Link href={props.authorization.url} fg={theme.primary} />\n <Show when={error()}>\n <text fg={theme.error}>Invalid code</text>\n </Show>\n </box>\n )}\n />\n )\n}\n\ninterface ApiMethodProps {\n providerID: string\n title: string\n metadata?: Record<string, string>\n custom?: boolean\n}\nfunction ApiMethod(props: ApiMethodProps) {\n const dialog = useDialog()\n const sdk = useSDK()\n const sync = useSync()\n const toast = useToast()\n const { theme } = useTheme()\n\n return (\n <DialogPrompt\n title={props.title}\n placeholder=\"API key\"\n description={() =>\n ({\n opencode: (\n <box gap={1}>\n <text fg={theme.textMuted}>\n OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API\n key.\n </text>\n <text fg={theme.text}>\n Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key\n </text>\n </box>\n ),\n \"opencode-go\": (\n <box gap={1}>\n <text fg={theme.textMuted}>\n OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models\n with generous usage limits.\n </text>\n <text fg={theme.text}>\n Go to <span style={{ fg: theme.primary }}>https://opencode.ai/go</span> and enable OpenCode Go\n </text>\n </box>\n ),\n })[props.providerID] ?? undefined\n }\n onConfirm={async (value) => {\n if (!value) return\n await sdk.client.auth.set({\n providerID: props.providerID,\n auth: {\n type: \"api\",\n key: value,\n ...(props.metadata ? { metadata: props.metadata } : {}),\n },\n })\n await sdk.client.instance.dispose()\n await sync.bootstrap()\n if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) {\n toast.show({\n variant: \"info\",\n message: `Saved credential for ${props.providerID}. Configure it in crokcode.json to use it.`,\n })\n dialog.clear()\n return\n }\n dialog.replace(() => <DialogModel providerID={props.providerID} />)\n }}\n />\n )\n}\n\ninterface PromptsMethodProps {\n dialog: ReturnType<typeof useDialog>\n prompts: NonNullable<ProviderAuthMethod[\"prompts\"]>[number][]\n}\nasync function PromptsMethod(props: PromptsMethodProps) {\n const inputs: Record<string, string> = {}\n for (const prompt of props.prompts) {\n if (prompt.when) {\n const value = inputs[prompt.when.key]\n if (value === undefined) continue\n const matches = prompt.when.op === \"eq\" ? value === prompt.when.value : value !== prompt.when.value\n if (!matches) continue\n }\n\n if (prompt.type === \"select\") {\n const value = await new Promise<string | null>((resolve) => {\n props.dialog.replace(\n () => (\n <DialogSelect\n title={prompt.message}\n options={prompt.options.map((x) => ({\n title: x.label,\n value: x.value,\n description: x.hint,\n }))}\n onSelect={(option) => resolve(option.value)}\n />\n ),\n () => resolve(null),\n )\n })\n if (value === null) return null\n inputs[prompt.key] = value\n continue\n }\n\n const value = await new Promise<string | null>((resolve) => {\n props.dialog.replace(\n () => (\n <DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} />\n ),\n () => resolve(null),\n )\n })\n if (value === null) return null\n inputs[prompt.key] = value\n }\n return inputs\n}\n\n// --- CrokCode plan connect (browser device-pairing) ---------------------------\n// Mirrors packages/crokcode/src/cli/cmd/login.ts. All plans connect the same\n// `crokapi` gateway provider; the gateway enforces the account's real plan.\n// ponytail: the model list + config write are duplicated from login.ts. If this\n// diverges, extract a shared helper both the CLI and TUI import.\nconst CROK_AUTH_BASE = \"https://zapkpyjeetjbufuuqwye.supabase.co\"\nconst CROK_CLI_AUTH = `${CROK_AUTH_BASE}/functions/v1/cli-auth`\nconst CROK_GATEWAY = `${CROK_AUTH_BASE}/functions/v1/crokapi/v1`\n\n// `image: true` = accepts image input (from upstream OpenRouter modalities), so\n// the TUI sends attachments instead of stripping them. GLM/DeepSeek are text-only.\nconst CROK_MODELS: Record<string, { name: string; image?: boolean; cost: { input: number; output: number } }> = {\n \"deepseek/deepseek-v4-flash\": { name: \"DeepSeek V4 Flash\", cost: { input: 0.14, output: 0.28 } },\n \"z-ai/glm-4.7-flash\": { name: \"GLM 4.7 Flash\", cost: { input: 0.08, output: 0.56 } },\n \"xiaomi/mimo-v2.5\": { name: \"MiMo V2.5\", image: true, cost: { input: 0.2, output: 0.39 } },\n \"qwen/qwen3-coder-flash\": { name: \"Qwen3 Coder Flash\", cost: { input: 0.28, output: 1.36 } },\n \"deepseek/deepseek-v4-pro\": { name: \"DeepSeek V4 Pro\", cost: { input: 0.6, output: 1.22 } },\n \"xiaomi/mimo-v2.5-pro\": { name: \"MiMo V2.5 Pro\", cost: { input: 0.6, output: 1.22 } },\n \"minimax/minimax-m3\": { name: \"MiniMax M3\", image: true, cost: { input: 0.42, output: 1.68 } },\n \"qwen/qwen3.7-plus\": { name: \"Qwen3.7 Plus\", image: true, cost: { input: 0.45, output: 1.79 } },\n \"z-ai/glm-5.2\": { name: \"GLM 5.2\", cost: { input: 1.11, output: 3.49 } },\n \"moonshotai/kimi-k2.7-code\": { name: \"Kimi K2.7 Code\", image: true, cost: { input: 1.15, output: 5.25 } },\n \"anthropic/claude-haiku-4.5\": { name: \"Claude Haiku 4.5\", image: true, cost: { input: 1.4, output: 7 } },\n \"x-ai/grok-4.5\": { name: \"Grok 4.5\", image: true, cost: { input: 2.8, output: 8.4 } },\n \"google/gemini-3.6-flash\": { name: \"Gemini 3.6 Flash\", image: true, cost: { input: 2.1, output: 10.5 } },\n \"anthropic/claude-sonnet-5\": { name: \"Claude Sonnet 5\", image: true, cost: { input: 2.8, output: 14 } },\n \"google/gemini-3.1-pro-preview\": { name: \"Gemini 3.1 Pro\", image: true, cost: { input: 2.8, output: 16.8 } },\n \"openai/gpt-5.4\": { name: \"GPT-5.4\", image: true, cost: { input: 3.5, output: 21 } },\n \"openai/gpt-5.6-terra\": { name: \"GPT-5.6 Terra\", image: true, cost: { input: 3.5, output: 21 } },\n \"moonshotai/kimi-k3\": { name: \"Kimi K3\", image: true, cost: { input: 4.2, output: 21 } },\n \"anthropic/claude-opus-4.8\": { name: \"Claude Opus 4.8\", image: true, cost: { input: 7, output: 35 } },\n \"openai/gpt-5.6-sol\": { name: \"GPT-5.6 Sol\", image: true, cost: { input: 7, output: 42 } },\n \"anthropic/claude-fable-5\": { name: \"Fable 5\", image: true, cost: { input: 14, output: 70 } },\n}\n\n// Display name + model subset per plan. CrokGo is budget-only; the gateway\n// enforces this too, so these lists must match CROKGO_MODELS in the gateway.\nconst CROK_PLAN_NAME: Record<string, string> = {\n crokgo: \"CrokGo\",\n crokpro: \"CrokPro\",\n \"crok-as-you-go\": \"Crok-as-you-go\",\n}\nconst CROK_PLAN_MODEL_IDS: Record<string, string[]> = {\n crokgo: [\"deepseek/deepseek-v4-flash\", \"z-ai/glm-4.7-flash\", \"xiaomi/mimo-v2.5\", \"qwen/qwen3-coder-flash\", \"deepseek/deepseek-v4-pro\", \"xiaomi/mimo-v2.5-pro\", \"minimax/minimax-m3\", \"qwen/qwen3.7-plus\", \"z-ai/glm-5.2\"],\n crokpro: Object.keys(CROK_MODELS),\n \"crok-as-you-go\": Object.keys(CROK_MODELS),\n}\n// Builds the provider block for a plan: named after the plan, exposing only\n// that plan's models.\nfunction crokProviderBlock(planID: string, apiKey: string) {\n const modelIDs = CROK_PLAN_MODEL_IDS[planID] ?? Object.keys(CROK_MODELS)\n const models: Record<\n string,\n {\n name: string\n reasoning: boolean\n cost: { input: number; output: number }\n modalities: { input: string[]; output: string[] }\n }\n > = {}\n for (const id of modelIDs) {\n const def = CROK_MODELS[id]\n // reasoning enables /effort; cost makes the status line show real dollars.\n if (def)\n models[id] = {\n name: def.name,\n reasoning: true,\n cost: { input: def.cost.input, output: def.cost.output },\n modalities: { input: def.image ? [\"text\", \"image\"] : [\"text\"], output: [\"text\"] },\n }\n }\n return {\n npm: \"@ai-sdk/openai-compatible\",\n name: CROK_PLAN_NAME[planID] ?? \"CrokCode\",\n options: { baseURL: CROK_GATEWAY, apiKey },\n models,\n }\n}\n\ninterface PlanConnectProps {\n planID: string\n title: string\n}\nfunction PlanConnect(props: PlanConnectProps) {\n const { theme } = useTheme()\n const sdk = useSDK()\n const sync = useSync()\n const dialog = useDialog()\n const toast = useToast()\n const [url, setUrl] = createSignal<string>()\n const [code, setCode] = createSignal<string>()\n const [status, setStatus] = createSignal(\"Starting…\")\n let cancelled = false\n onCleanup(() => {\n cancelled = true\n })\n\n async function cliAuth(body: Record<string, unknown>) {\n const res = await fetch(CROK_CLI_AUTH, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n })\n return (await res.json().catch(() => ({}))) as Record<string, any>\n }\n\n onMount(async () => {\n const started = await cliAuth({ action: \"start\" }).catch(() => ({}) as Record<string, any>)\n if (cancelled) return\n if (!started.device_code) {\n toast.show({ variant: \"error\", message: \"Could not reach the CrokCode login service.\" })\n dialog.clear()\n return\n }\n setUrl(started.verification_uri)\n setCode(started.user_code)\n setStatus(\"Waiting for approval in your browser…\")\n\n const interval = Math.max(2, Number(started.interval) || 3) * 1000\n const deadline = Date.now() + (Number(started.expires_in) || 600) * 1000\n while (!cancelled && Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, interval))\n if (cancelled) return\n const polled = await cliAuth({ action: \"poll\", device_code: started.device_code }).catch(\n () => ({}) as Record<string, any>,\n )\n if (polled.api_key) {\n setStatus(\"Approved. Connecting…\")\n try {\n // Persist through the API so the global config cache is invalidated and\n // the provider loads immediately (a raw file write is not picked up\n // mid-session). A fresh key each pairing keeps `changed` true.\n await sdk.client.global.config.update({\n config: { provider: { [props.planID]: crokProviderBlock(props.planID, polled.api_key as string) } } as any,\n })\n } catch {\n toast.show({ variant: \"error\", message: \"Connected, but could not save the provider.\" })\n dialog.clear()\n return\n }\n await sdk.client.instance.dispose()\n await sync.bootstrap()\n if (cancelled) return\n dialog.replace(() => <DialogModel providerID={props.planID} />)\n return\n }\n if (polled.status === \"pending\") continue\n break\n }\n if (!cancelled) {\n toast.show({ variant: \"error\", message: \"Login did not complete. Try again.\" })\n dialog.clear()\n }\n })\n\n return (\n <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>\n <box flexDirection=\"row\" justifyContent=\"space-between\">\n <text attributes={TextAttributes.BOLD} fg={theme.text}>\n Connect {props.title}\n </text>\n <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>\n esc\n </text>\n </box>\n <Show when={code()}>\n <box gap={1}>\n <text fg={theme.textMuted}>Open this link in your browser and approve the code:</text>\n <Link href={url()!} fg={theme.primary} />\n <text attributes={TextAttributes.BOLD} fg={theme.text}>\n {code()}\n </text>\n </box>\n </Show>\n <text fg={theme.textMuted}>{status()}</text>\n </box>\n )\n}\n"}}}} |