tui: go plan payg msg (#26248)

This commit is contained in:
Aiden Cline
2026-05-07 16:53:24 -05:00
committed by GitHub
parent 22e64cac67
commit f5d0371efe
10 changed files with 285 additions and 83 deletions

View File

@@ -8,32 +8,36 @@ import { GoLogo } from "./logo"
import { BgPulse, type BgPulseMask } from "./bg-pulse"
import { useBindings } from "../keymap"
const GO_URL = "https://opencode.ai/go"
const PAD_X = 3
const PAD_TOP_OUTER = 1
export type DialogGoUpsellProps = {
export type DialogRetryActionProps = {
title: string
message: string
label: string
link?: string
onClose?: (dontShowAgain?: boolean) => void
}
function subscribe(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
open(GO_URL).catch(() => {})
function runAction(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
if (props.link) open(props.link).catch(() => {})
props.onClose?.()
dialog.clear()
}
function dismiss(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
function dismiss(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
props.onClose?.(true)
dialog.clear()
}
export function DialogGoUpsell(props: DialogGoUpsellProps) {
export function DialogRetryAction(props: DialogRetryActionProps) {
const dialog = useDialog()
const { theme } = useTheme()
const fg = selectedForeground(theme)
const [selected, setSelected] = createSignal<"dismiss" | "subscribe">("subscribe")
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
const [center, setCenter] = createSignal<{ x: number; y: number } | undefined>()
const [masks, setMasks] = createSignal<BgPulseMask[]>([])
const showGoTreatment = () => props.link === "https://opencode.ai/go"
let content: BoxRenderable | undefined
let logoBox: BoxRenderable | undefined
let headingBox: BoxRenderable | undefined
@@ -41,11 +45,13 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
let buttonsBox: BoxRenderable | undefined
const sync = () => {
if (!content || !logoBox) return
setCenter({
x: logoBox.x - content.x + logoBox.width / 2,
y: logoBox.y - content.y + logoBox.height / 2 + PAD_TOP_OUTER,
})
if (!content) return
if (logoBox) {
setCenter({
x: logoBox.x - content.x + logoBox.width / 2,
y: logoBox.y - content.y + logoBox.height / 2 + PAD_TOP_OUTER,
})
}
const next: BgPulseMask[] = []
const baseY = PAD_TOP_OUTER
for (const b of [headingBox, descBox, buttonsBox]) {
@@ -75,20 +81,20 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
bindings: [
{
key: "left",
cmd: () => setSelected((value) => (value === "subscribe" ? "dismiss" : "subscribe")),
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
},
{
key: "right",
cmd: () => setSelected((value) => (value === "subscribe" ? "dismiss" : "subscribe")),
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
},
{
key: "tab",
cmd: () => setSelected((value) => (value === "subscribe" ? "dismiss" : "subscribe")),
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
},
{
key: "return",
cmd: () => {
if (selected() === "subscribe") subscribe(props, dialog)
if (selected() === "action") runAction(props, dialog)
else dismiss(props, dialog)
},
},
@@ -97,33 +103,34 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
return (
<box ref={(item: BoxRenderable) => (content = item)}>
<box position="absolute" top={-PAD_TOP_OUTER} left={0} right={0} bottom={0} zIndex={0}>
<BgPulse centerX={center()?.x} centerY={center()?.y} masks={masks()} />
</box>
<box paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
{showGoTreatment() ? (
<box position="absolute" top={-PAD_TOP_OUTER} left={0} right={0} bottom={0} zIndex={0}>
<BgPulse centerX={center()?.x} centerY={center()?.y} masks={masks()} />
</box>
) : null}
<box paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1} zIndex={1}>
<box ref={(item: BoxRenderable) => (headingBox = item)} flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Free limit reached
{props.title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box ref={(item: BoxRenderable) => (descBox = item)} gap={0}>
<box flexDirection="row">
<text fg={theme.textMuted}>Subscribe to </text>
<text attributes={TextAttributes.BOLD} fg={theme.textMuted}>
OpenCode Go
</text>
<text fg={theme.textMuted}> for reliable access to the</text>
</box>
<text fg={theme.textMuted}>best open-source models, starting at $5/month.</text>
<text fg={theme.textMuted}>{props.message}</text>
</box>
<box alignItems="center" gap={1} paddingBottom={1}>
<box ref={(item: BoxRenderable) => (logoBox = item)}>
<GoLogo />
</box>
<Link href={GO_URL} fg={theme.primary} />
<box gap={1} paddingBottom={1}>
{showGoTreatment() ? (
<box ref={(item: BoxRenderable) => (logoBox = item)} alignItems="center">
<GoLogo />
</box>
) : null}
{props.link ? (
<box width="100%" flexDirection="row" justifyContent="center">
<Link href={props.link} fg={theme.primary} wrapMode="none" />
</box>
) : null}
</box>
<box ref={(item: BoxRenderable) => (buttonsBox = item)} flexDirection="row" justifyContent="space-between">
<box
@@ -143,15 +150,15 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
<box
paddingLeft={2}
paddingRight={2}
backgroundColor={selected() === "subscribe" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
onMouseOver={() => setSelected("subscribe")}
onMouseUp={() => subscribe(props, dialog)}
backgroundColor={selected() === "action" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
onMouseOver={() => setSelected("action")}
onMouseUp={() => runAction(props, dialog)}
>
<text
fg={selected() === "subscribe" ? fg : theme.text}
attributes={selected() === "subscribe" ? TextAttributes.BOLD : undefined}
fg={selected() === "action" ? fg : theme.text}
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
>
subscribe
{props.label}
</text>
</box>
</box>
@@ -160,10 +167,13 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
)
}
DialogGoUpsell.show = (dialog: DialogContext) => {
DialogRetryAction.show = (
dialog: DialogContext,
props: Pick<DialogRetryActionProps, "title" | "message" | "label" | "link">,
) => {
return new Promise<boolean>((resolve) => {
dialog.replace(
() => <DialogGoUpsell onClose={(dontShow) => resolve(dontShow ?? false)} />,
() => <DialogRetryAction {...props} onClose={(dontShow) => resolve(dontShow ?? false)} />,
() => resolve(false),
)
})