refactor(tui): migrate dialog views to V2 themes (#37516)

This commit is contained in:
James Long 2026-07-17 12:17:11 -04:00 committed by GitHub
commit 677526d72e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 86 additions and 81 deletions

View file

@ -16,7 +16,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
const client = useClient() const client = useClient()
const dialog = useDialog() const dialog = useDialog()
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const [loadError, setLoadError] = createSignal<unknown>() const [loadError, setLoadError] = createSignal<unknown>()
const [showPassword, setShowPassword] = createSignal(false) const [showPassword, setShowPassword] = createSignal(false)
const [passwordHover, setPasswordHover] = createSignal(false) const [passwordHover, setPasswordHover] = createSignal(false)
@ -47,17 +47,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
<box flexDirection={horizontal() ? "row" : "column"} alignItems={horizontal() ? "flex-start" : "center"} gap={2}> <box flexDirection={horizontal() ? "row" : "column"} alignItems={horizontal() ? "flex-start" : "center"} gap={2}>
<box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}> <box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
<box> <box>
<text fg={theme.textMuted}>URLs</text> <text fg={themeV2.text.subdued()}>URLs</text>
<For each={value.urls}>{(url) => <text fg={theme.text}>{url}</text>}</For> <For each={value.urls}>{(url) => <text fg={themeV2.text()}>{url}</text>}</For>
</box> </box>
<box> <box>
<text fg={theme.textMuted}>Username</text> <text fg={themeV2.text.subdued()}>Username</text>
<text fg={theme.text}>{value.username}</text> <text fg={themeV2.text()}>{value.username}</text>
</box> </box>
<box> <box>
<text fg={theme.textMuted}>Password</text> <text fg={themeV2.text.subdued()}>Password</text>
<text <text
fg={passwordHover() ? theme.text : theme.textMuted} fg={passwordHover() ? themeV2.text() : themeV2.text.subdued()}
wrapMode="word" wrapMode="word"
onMouseOver={() => setPasswordHover(true)} onMouseOver={() => setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)} onMouseOut={() => setPasswordHover(false)}
@ -67,7 +67,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
</text> </text>
</box> </box>
<Show when={value.urls.some((url) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}> <Show when={value.urls.some((url) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}>
<text fg={theme.textMuted} wrapMode="word"> <text fg={themeV2.text.subdued()} wrapMode="word">
Run `opencode service set hostname 0.0.0.0` to access the service remotely. Run `opencode service set hostname 0.0.0.0` to access the service remotely.
</text> </text>
</Show> </Show>
@ -78,7 +78,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0} flexShrink={0}
alignItems={horizontal() ? "flex-end" : "center"} alignItems={horizontal() ? "flex-end" : "center"}
> >
<text fg={theme.text}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text> <text fg={themeV2.text()}>{renderUnicodeCompact(JSON.stringify(value), { border: 1 })}</text>
</box> </box>
</box> </box>
) )
@ -87,17 +87,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
return ( return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}> <box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}> <text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
Pair Pair
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc esc
</text> </text>
</box> </box>
<Show <Show
when={loadError()} when={loadError()}
fallback={ fallback={
<Show when={info()} fallback={<text fg={theme.textMuted}>Loading server information</text>}> <Show when={info()} fallback={<text fg={themeV2.text.subdued()}>Loading server information</text>}>
<Show <Show
when={dimensions().height >= 36} when={dimensions().height >= 36}
fallback={ fallback={
@ -116,11 +116,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
> >
{(error) => ( {(error) => (
<box> <box>
<text fg={theme.error} attributes={TextAttributes.BOLD}> <text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
Could not load server information Could not load server information
</text> </text>
<text fg={theme.textMuted}>{errorMessage(error())}</text> <text fg={themeV2.text.subdued()}>{errorMessage(error())}</text>
<text fg={theme.textMuted}>Close and reopen Pair to try again.</text> <text fg={themeV2.text.subdued()}>Close and reopen Pair to try again.</text>
</box> </box>
)} )}
</Show> </Show>

View file

@ -7,7 +7,7 @@ import { useDialog, type DialogContext } from "../ui/dialog"
export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) { export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) {
const dialog = useDialog() const dialog = useDialog()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const shortcuts = Keymap.useShortcuts() const shortcuts = Keymap.useShortcuts()
const [inputTarget, setInputTarget] = createSignal<InputRenderable>() const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
let input: InputRenderable let input: InputRenderable
@ -47,10 +47,10 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1}> <box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}> <text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Name project copy Name project copy
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc esc
</text> </text>
</box> </box>
@ -61,17 +61,18 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
}} }}
onSubmit={confirm} onSubmit={confirm}
placeholder="Project copy name" placeholder="Project copy name"
placeholderColor={theme.textMuted} placeholderColor={themeV2.text.subdued()}
textColor={theme.text} textColor={themeV2.text.formfield()}
focusedTextColor={theme.text} focusedTextColor={themeV2.text.formfield()}
cursorColor={theme.text} cursorColor={themeV2.text.formfield()}
/> />
<box paddingBottom={1} flexDirection="row" gap={2}> <box paddingBottom={1} flexDirection="row" gap={2}>
<text fg={theme.text}> <text fg={themeV2.text()}>
enter <span style={{ fg: theme.textMuted }}>submit</span> enter <span style={{ fg: themeV2.text.subdued() }}>submit</span>
</text> </text>
<text fg={theme.text}> <text fg={themeV2.text()}>
{shortcuts.get("dialog.project_copy.generate")} <span style={{ fg: theme.textMuted }}>generate one</span> {shortcuts.get("dialog.project_copy.generate")}{" "}
<span style={{ fg: themeV2.text.subdued() }}>generate one</span>
</text> </text>
</box> </box>
</box> </box>

View file

@ -8,47 +8,41 @@ export type DialogStatusProps = {}
export function DialogStatus() { export function DialogStatus() {
const data = useData() const data = useData()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const dialog = useDialog() const dialog = useDialog()
const mcp = createMemo(() => data.location.mcp.server.list() ?? []) const mcp = createMemo(() => data.location.mcp.server.list() ?? [])
const color = (status: string) => {
if (status === "connected") return themeV2.text.feedback.success()
if (status === "failed") return themeV2.text.feedback.error()
if (status === "needs_auth") return themeV2.text.feedback.warning()
if (status === "needs_client_registration") return themeV2.text.feedback.error()
return themeV2.text.subdued()
}
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}> <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}> <text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
Status Status
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc esc
</text> </text>
</box> </box>
<Show when={mcp().length > 0} fallback={<text fg={theme.text}>No MCP servers</text>}> <Show when={mcp().length > 0} fallback={<text fg={themeV2.text()}>No MCP servers</text>}>
<box> <box>
<text fg={theme.text}> <text fg={themeV2.text()}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"} {mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text> </text>
<For each={mcp()}> <For each={mcp()}>
{(item) => ( {(item) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text <text flexShrink={0} style={{ fg: color(item.status.status) }}>
flexShrink={0}
style={{
fg: (
{
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
needs_auth: theme.warning,
needs_client_registration: theme.error,
} as Record<string, typeof theme.success>
)[item.status.status],
}}
>
</text> </text>
<text fg={theme.text} wrapMode="word"> <text fg={themeV2.text()} wrapMode="word">
<b>{item.name}</b>{" "} <b>{item.name}</b>{" "}
<span style={{ fg: theme.textMuted }}> <span style={{ fg: themeV2.text.subdued() }}>
<Switch fallback={item.status.status}> <Switch fallback={item.status.status}>
<Match when={item.status.status === "connected"}>Connected</Match> <Match when={item.status.status === "connected"}>Connected</Match>
<Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match> <Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>

View file

@ -1,15 +1,20 @@
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
export function PluginRouteMissing(props: { id: string; name: string; onHome: () => void }) { export function PluginRouteMissing(props: { id: string; name: string; onHome: () => void }) {
const { theme } = useTheme() const { themeV2 } = useTheme()
return ( return (
<box width="100%" height="100%" alignItems="center" justifyContent="center" flexDirection="column" gap={1}> <box width="100%" height="100%" alignItems="center" justifyContent="center" flexDirection="column" gap={1}>
<text fg={theme.warning}> <text fg={themeV2.text.feedback.warning()}>
Unknown plugin route: {props.id}/{props.name} Unknown plugin route: {props.id}/{props.name}
</text> </text>
<box onMouseUp={props.onHome} backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}> <box
<text fg={theme.text}>go home</text> onMouseUp={props.onHome}
backgroundColor={themeV2.background.action("hovered")}
paddingLeft={1}
paddingRight={1}
>
<text fg={themeV2.text.action("hovered")}>go home</text>
</box> </box>
</box> </box>
) )

View file

@ -2,7 +2,7 @@ import { useTheme } from "../context/theme"
import { Spinner } from "./spinner" import { Spinner } from "./spinner"
export function Reconnecting() { export function Reconnecting() {
const theme = useTheme().theme const { themeV2 } = useTheme()
return ( return (
<box <box
@ -12,12 +12,12 @@ export function Reconnecting() {
right={0} right={0}
bottom={0} bottom={0}
left={0} left={0}
backgroundColor={theme.background} backgroundColor={themeV2.background()}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
> >
<box width={62} maxWidth="90%" flexDirection="column" alignItems="center" gap={1}> <box width={62} maxWidth="90%" flexDirection="column" alignItems="center" gap={1}>
<Spinner color={theme.textMuted}>Waiting for background service...</Spinner> <Spinner color={themeV2.text.subdued()}>Waiting for background service...</Spinner>
</box> </box>
</box> </box>
) )

View file

@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
import { Spinner } from "./spinner" import { Spinner } from "./spinner"
export function StartupLoading(props: { ready: () => boolean }) { export function StartupLoading(props: { ready: () => boolean }) {
const theme = useTheme().theme const { themeV2 } = useTheme().contextual("elevated")
const [show, setShow] = createSignal(false) const [show, setShow] = createSignal(false)
const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins...")) const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins..."))
let wait: NodeJS.Timeout | undefined let wait: NodeJS.Timeout | undefined
@ -54,8 +54,8 @@ export function StartupLoading(props: { ready: () => boolean }) {
return ( return (
<Show when={show()}> <Show when={show()}>
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center"> <box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
<box backgroundColor={theme.backgroundPanel} paddingLeft={1} paddingRight={1}> <box backgroundColor={themeV2.background()} paddingLeft={1} paddingRight={1}>
<Spinner color={theme.textMuted}>{text()}</Spinner> <Spinner color={themeV2.text.subdued()}>{text()}</Spinner>
</box> </box>
</box> </box>
</Show> </Show>

View file

@ -11,7 +11,7 @@ export type DialogAlertProps = {
export function DialogAlert(props: DialogAlertProps) { export function DialogAlert(props: DialogAlertProps) {
const dialog = useDialog() const dialog = useDialog()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
Keymap.createLayer(() => ({ Keymap.createLayer(() => ({
mode: "modal", mode: "modal",
@ -30,27 +30,27 @@ export function DialogAlert(props: DialogAlertProps) {
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1}> <box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}> <text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
{props.title} {props.title}
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc esc
</text> </text>
</box> </box>
<box paddingBottom={1}> <box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text> <text fg={themeV2.text.subdued()}>{props.message}</text>
</box> </box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}> <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box <box
paddingLeft={3} paddingLeft={3}
paddingRight={3} paddingRight={3}
backgroundColor={theme.primary} backgroundColor={themeV2.background.action("focused")}
onMouseUp={() => { onMouseUp={() => {
props.onConfirm?.() props.onConfirm?.()
dialog.clear() dialog.clear()
}} }}
> >
<text fg={theme.selectedListItemText}>ok</text> <text fg={themeV2.text.action("focused")}>ok</text>
</box> </box>
</box> </box>
</box> </box>

View file

@ -18,7 +18,7 @@ export type DialogConfirmResult = boolean | undefined
export function DialogConfirm(props: DialogConfirmProps) { export function DialogConfirm(props: DialogConfirmProps) {
const dialog = useDialog() const dialog = useDialog()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const [store, setStore] = createStore({ const [store, setStore] = createStore({
active: "confirm" as "confirm" | "cancel", active: "confirm" as "confirm" | "cancel",
}) })
@ -57,15 +57,15 @@ export function DialogConfirm(props: DialogConfirmProps) {
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1}> <box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}> <text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
{props.title} {props.title}
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc esc
</text> </text>
</box> </box>
<box paddingBottom={1}> <box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text> <text fg={themeV2.text.subdued()}>{props.message}</text>
</box> </box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}> <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<For each={["cancel", "confirm"] as const}> <For each={["cancel", "confirm"] as const}>
@ -73,14 +73,14 @@ export function DialogConfirm(props: DialogConfirmProps) {
<box <box
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={key === store.active ? theme.primary : undefined} backgroundColor={key === store.active ? themeV2.background.action("focused") : undefined}
onMouseUp={() => { onMouseUp={() => {
if (key === "confirm") props.onConfirm?.() if (key === "confirm") props.onConfirm?.()
if (key === "cancel") props.onCancel?.() if (key === "cancel") props.onCancel?.()
dialog.clear() dialog.clear()
}} }}
> >
<text fg={key === store.active ? theme.selectedListItemText : theme.textMuted}> <text fg={key === store.active ? themeV2.text.action("focused") : themeV2.text.subdued()}>
{Locale.titlecase(key === "cancel" ? (props.label ?? key) : key)} {Locale.titlecase(key === "cancel" ? (props.label ?? key) : key)}
</text> </text>
</box> </box>

View file

@ -5,7 +5,7 @@ import { useDialog, type DialogContext } from "./dialog"
export function DialogExportResult(props: { path: string; onClose?: () => void }) { export function DialogExportResult(props: { path: string; onClose?: () => void }) {
const dialog = useDialog() const dialog = useDialog()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const close = () => { const close = () => {
props.onClose?.() props.onClose?.()
@ -27,19 +27,19 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1}> <box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}> <text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Session exported Session exported
</text> </text>
<text fg={theme.textMuted} onMouseUp={close}> <text fg={themeV2.text.subdued()} onMouseUp={close}>
esc esc
</text> </text>
</box> </box>
<box> <box>
<text fg={theme.text}>{props.path}</text> <text fg={themeV2.text()}>{props.path}</text>
</box> </box>
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}> <box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={close}> <box paddingLeft={3} paddingRight={3} backgroundColor={themeV2.background.action("focused")} onMouseUp={close}>
<text fg={theme.selectedListItemText}>Close</text> <text fg={themeV2.text.action("focused")}>Close</text>
</box> </box>
</box> </box>
</box> </box>

View file

@ -5,7 +5,7 @@ import { useDialog } from "./dialog"
export function DialogHelp() { export function DialogHelp() {
const dialog = useDialog() const dialog = useDialog()
const { theme } = useTheme() const { themeV2 } = useTheme().contextual("elevated")
const shortcuts = Keymap.useShortcuts() const shortcuts = Keymap.useShortcuts()
Keymap.createLayer(() => ({ Keymap.createLayer(() => ({
@ -19,21 +19,26 @@ export function DialogHelp() {
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1}> <box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}> <text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Help Help
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc/enter esc/enter
</text> </text>
</box> </box>
<box paddingBottom={1}> <box paddingBottom={1}>
<text fg={theme.textMuted}> <text fg={themeV2.text.subdued()}>
Press {shortcuts.get("command.palette.show")} to see all available actions and commands in any context. Press {shortcuts.get("command.palette.show")} to see all available actions and commands in any context.
</text> </text>
</box> </box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}> <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}> <box
<text fg={theme.selectedListItemText}>ok</text> paddingLeft={3}
paddingRight={3}
backgroundColor={themeV2.background.action("focused")}
onMouseUp={() => dialog.clear()}
>
<text fg={themeV2.text.action("focused")}>ok</text>
</box> </box>
</box> </box>
</box> </box>