From 621a5b07068b97da51541a2983bdc3d3782e1457 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:45:59 -0500 Subject: [PATCH] feat(app): show ChatGPT token-sharing guidance (#52160) --- .../connect/chatgpt-welcome.stories.tsx | 23 ++++++ .../src/providers/connect/chatgpt-welcome.tsx | 37 +++++++++ packages/app/src/providers/connect/dialog.tsx | 35 +++++++-- .../models/select-dialog.stories.tsx | 78 +++++++++++++++++++ .../src/providers/models/select-dialog.tsx | 36 ++++++++- packages/app/src/runtime/i18n/en.ts | 6 ++ 6 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 packages/app/src/providers/connect/chatgpt-welcome.stories.tsx create mode 100644 packages/app/src/providers/connect/chatgpt-welcome.tsx create mode 100644 packages/app/src/providers/models/select-dialog.stories.tsx diff --git a/packages/app/src/providers/connect/chatgpt-welcome.stories.tsx b/packages/app/src/providers/connect/chatgpt-welcome.stories.tsx new file mode 100644 index 00000000000..a33cd10bf65 --- /dev/null +++ b/packages/app/src/providers/connect/chatgpt-welcome.stories.tsx @@ -0,0 +1,23 @@ +import { Button } from "@opencode/ui/button" +import { useDialog } from "@opencode/ui/context/dialog" +import { onMount } from "solid-js" +import { DialogChatGPTPlanWelcome } from "./chatgpt-welcome" + +function WelcomeStory() { + const dialog = useDialog() + const open = () => dialog.show(() => ) + onMount(open) + + return ( + + ) +} + +export default { + title: "App/Dialogs/Connect Provider", + id: "app-dialog-chatgpt-welcome", +} + +export const ChatGPTPlanWelcome = { render: () => } diff --git a/packages/app/src/providers/connect/chatgpt-welcome.tsx b/packages/app/src/providers/connect/chatgpt-welcome.tsx new file mode 100644 index 00000000000..14d751eb42b --- /dev/null +++ b/packages/app/src/providers/connect/chatgpt-welcome.tsx @@ -0,0 +1,37 @@ +import { Button } from "@opencode/ui/button" +import { useDialog } from "@opencode/ui/context/dialog" +import { Dialog, DialogTitle } from "@opencode/ui/dialog" +import { ProviderIcon } from "@opencode/ui/provider-icon" +import { useLanguage } from "@/runtime/i18n/language" +import { ExternalLink } from "@/runtime/platform/external-link" + +export function DialogChatGPTPlanWelcome() { + const dialog = useDialog() + const language = useLanguage() + + return ( + +
+
+
+ ) +} diff --git a/packages/app/src/providers/connect/dialog.tsx b/packages/app/src/providers/connect/dialog.tsx index c7bbaf28d54..a992686d20c 100644 --- a/packages/app/src/providers/connect/dialog.tsx +++ b/packages/app/src/providers/connect/dialog.tsx @@ -39,6 +39,9 @@ import { OpenCodeLogo } from "@/providers/opencode-logo" import { decode64 } from "@/runtime/persistence/base64" import { SettingsList } from "@/settings/list" import { useTabs } from "@/shell/tabs/tabs" +import { Persist, persisted } from "@/runtime/persistence/storage" +import { Persistence } from "@/runtime/persistence/schema" +import { Schema } from "effect" import { CONSOLE_INTEGRATION, CONSOLE_PROVIDERS, @@ -48,6 +51,7 @@ import { type ProviderConnectMethod, } from "./controller" import { ConsoleAuthorization } from "./console" +import { DialogChatGPTPlanWelcome } from "./chatgpt-welcome" import { authServerName, RemoteAuthNotice } from "./remote" import "./models.css" @@ -81,8 +85,15 @@ export const DialogConnectProvider: Component<{ completed: false, modelProvider: undefined as { id: string; name: string } | undefined, authorization: false, + chatgptWelcome: false, }) const language = useLanguage() + const dialog = useDialog() + const [welcome, setWelcome, , welcomeReady] = persisted( + Persist.global("chatgpt-plan-welcome.v1"), + Persistence.struct({ seen: Schema.Boolean }), + { seen: false }, + ) const reset = controller.reset const back = { current: reset } const consoleSelected = () => CONSOLE_PROVIDERS.has(controller.selected() ?? "") @@ -112,7 +123,11 @@ export const DialogConnectProvider: Component<{ setBack={(handler) => (back.current = handler)} selection={props.selection} onDone={props.onDone ? () => setState("completed", true) : undefined} - onConnected={() => props.onConnected?.(provider)} + onConnected={(methodID) => { + props.onConnected?.(provider) + if (provider === "openai" && methodID === "chatgpt-token-sharing") + setState("chatgptWelcome", true) + }} onFirstConnection={(provider) => setState("modelProvider", provider)} onAuthorization={(authorization) => setState("authorization", authorization)} /> @@ -136,9 +151,16 @@ export const DialogConnectProvider: Component<{ : "!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]" } onCloseAutoFocus={(event) => { - if (!state.completed || !props.onDone) return - event.preventDefault() - props.onDone() + if (state.completed && props.onDone) { + event.preventDefault() + props.onDone() + } + if (!state.chatgptWelcome) return + void Promise.resolve(welcomeReady.promise).then(() => { + if (welcome.seen) return + setWelcome("seen", true) + void dialog.show(() => ) + }) }} class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]" classList={{ @@ -354,7 +376,7 @@ function ProviderConnection(props: { setBack: (handler: () => void) => void selection?: ModelSelection onDone?: () => void - onConnected?: () => void + onConnected?: (methodID?: string) => void onFirstConnection: (provider: { id: string; name: string }) => void onAuthorization: (authorization: boolean) => void }) { @@ -401,7 +423,8 @@ function ProviderConnection(props: { prepare: isConsole ? prepareConsoleCatalog : undefined, pollInterval: isConsole ? 500 : undefined, onComplete: () => { - props.onConnected?.() + const method = controller.currentMethod() + props.onConnected?.(method?.type === "oauth" ? method.id : undefined) // The picker only lists the newest model per family by default, which hides most of // what a new connection just unlocked. Show everything the connected integration offers. global.models.show( diff --git a/packages/app/src/providers/models/select-dialog.stories.tsx b/packages/app/src/providers/models/select-dialog.stories.tsx new file mode 100644 index 00000000000..25ac1c3cad7 --- /dev/null +++ b/packages/app/src/providers/models/select-dialog.stories.tsx @@ -0,0 +1,78 @@ +import { Button } from "@opencode/ui/button" +import { createSignal } from "solid-js" +import type { ModelSelection } from "./selection" +import { ModelSelectorPopoverView } from "./select-dialog" + +const chatgpt = { + id: "gpt-5.6-sol", + providerID: "openai", + api: { id: "gpt-5.6-sol", url: "https://api.openai.com/v1", npm: "@opencode/ai/providers/openai" }, + name: "GPT-5.6 Sol", + family: "gpt", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: true, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 400_000, output: 64_000 }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-09-01", + variants: {}, + provider: { + id: "openai", + name: "OpenAI", + source: "custom", + env: [], + options: {}, + models: {}, + }, + latest: true, +} satisfies NonNullable> + +const models = [ + chatgpt, + { + ...chatgpt, + id: "gpt-5.6-terra", + api: { ...chatgpt.api, id: "gpt-5.6-terra" }, + name: "GPT-5.6 Terra", + latest: false, + }, +] + +function SelectorStory(props: { plan: boolean }) { + const [current, setCurrent] = createSignal(models[0].id) + return ( +
+ ( + + )} + models={(search) => models.filter((model) => model.name.toLowerCase().includes(search.toLowerCase()))} + groups={(items) => [{ category: "openai", items }]} + current={`openai:${current()}`} + chatgptPlan={props.plan} + select={(item) => setCurrent(item.id)} + onManage={() => undefined} + onClose={() => undefined} + /> +
+ ) +} + +export default { + title: "App/Dialogs/Model Selector", + id: "app-dialog-model-selector", +} + +export const ChatGPTPlan = { render: () => } +export const ApiKey = { render: () => } diff --git a/packages/app/src/providers/models/select-dialog.tsx b/packages/app/src/providers/models/select-dialog.tsx index 524d37f9600..c7090c7c28d 100644 --- a/packages/app/src/providers/models/select-dialog.tsx +++ b/packages/app/src/providers/models/select-dialog.tsx @@ -15,13 +15,16 @@ import { Menu } from "@opencode/ui/menu" import { TextInput } from "@opencode/ui/text-input" import { ModelTooltip } from "./tooltip" import { useLanguage } from "@/runtime/i18n/language" +import { ExternalLink } from "@/runtime/platform/external-link" +import { useData } from "@/runtime/server/current" +import { useWorkspaceLocation } from "@/workspaces/location" import { decode64 } from "@/runtime/persistence/base64" import { handleDocumentSearchKeydown } from "@/shell/commands/search-keydown" import { createMenuDismissController } from "@/shell/commands/menu-dismiss" import { createEventListener } from "@solid-primitives/event-listener" import { matchesModelSearch } from "./search" import { SettingsList } from "@/settings/list" -import { CONSOLE_GROUP_KEY, consoleModelGroup, ProviderModelSections } from "@/providers/models/provider-group" +import { CONSOLE_GROUP_KEY, consoleModelGroup, ProviderModelIcon, ProviderModelSections } from "@/providers/models/provider-group" import "@/settings/settings.css" const isFree = (provider: string, cost: { input: number } | undefined) => @@ -218,11 +221,21 @@ export function ModelSelectorPopover(props: { onClose?: () => void }) { const dialog = useDialog() + const data = useData() + const location = useWorkspaceLocation() const controller = createModelSelectorController({ model: props.model, provider: () => props.provider, onSelect: () => props.onClose?.(), }) + const chatgptPlan = () => { + if (!controller.current()?.startsWith("openai:")) return false + const connection = data.location.integration + .list(location().ref) + ?.find((integration) => integration.id === "openai") + ?.connections[0] + return connection?.type === "credential" && connection.method === "oauth" + } return ( { void import("./manage").then((module) => { @@ -281,11 +295,12 @@ function createModelSelectorController(input: { } } -function ModelSelectorPopoverView(props: { +export function ModelSelectorPopoverView(props: { trigger: ModelSelectorTrigger models: (search: string) => ModelItem[] groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[] current: string | undefined + chatgptPlan?: boolean select: (item: ModelItem) => void onManage: () => void onClose: () => void @@ -368,7 +383,8 @@ function ModelSelectorPopoverView(props: { (contentRef = element)} - class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none" + class="w-[284px] max-w-[calc(100vw-16px)] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none" + classList={{ "!w-[320px]": props.chatgptPlan }} onPointerDownOutside={dismiss.preventTriggerRestore} onFocusOutside={dismiss.preventTriggerRestore} onCloseAutoFocus={dismiss.onCloseAutoFocus} @@ -505,6 +521,20 @@ function ModelSelectorPopoverView(props: { {language.t("dialog.model.manage")} + +
+
+ + {language.t("dialog.model.chatgptPlan")} + + {language.t("dialog.model.chatgptManageUsage")} + + +
+ diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index ff4ef6504ec..eab7419d22f 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -186,6 +186,8 @@ export const dict = { "dialog.model.empty": "No model results", "dialog.model.manage": "Manage models", "dialog.model.manage.description": "Customize which models appear in the model selector.", + "dialog.model.chatgptPlan": "Using ChatGPT plan", + "dialog.model.chatgptManageUsage": "Manage usage", "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models", "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", @@ -217,6 +219,10 @@ export const dict = { "provider.connect.models.title": "Connected to {{provider}}", "provider.connect.models.description": "Choose a model to start with. You can switch models anytime.", "provider.connect.models.available": "Available models", + "provider.connect.chatgptWelcome.title": "You're using your ChatGPT plan", + "provider.connect.chatgptWelcome.description": "Eligible requests in OpenCode use your ChatGPT plan.", + "provider.connect.chatgptWelcome.usage": "Manage usage in ChatGPT settings", + "provider.connect.chatgptWelcome.confirm": "Got it", "provider.connect.models.list": "Models available from {{provider}}", "provider.connect.chatgptUsageLimit.title": "ChatGPT usage limit reached", "provider.connect.chatgptUsageLimit.description": "Review your usage settings in ChatGPT.",