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
parent bce9c639d1
commit 677526d72e
10 changed files with 86 additions and 81 deletions
+15 -15
View File
@@ -16,7 +16,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
const client = useClient()
const dialog = useDialog()
const dimensions = useTerminalDimensions()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const [loadError, setLoadError] = createSignal<unknown>()
const [showPassword, setShowPassword] = 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 width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
<box>
<text fg={theme.textMuted}>URLs</text>
<For each={value.urls}>{(url) => <text fg={theme.text}>{url}</text>}</For>
<text fg={themeV2.text.subdued()}>URLs</text>
<For each={value.urls}>{(url) => <text fg={themeV2.text()}>{url}</text>}</For>
</box>
<box>
<text fg={theme.textMuted}>Username</text>
<text fg={theme.text}>{value.username}</text>
<text fg={themeV2.text.subdued()}>Username</text>
<text fg={themeV2.text()}>{value.username}</text>
</box>
<box>
<text fg={theme.textMuted}>Password</text>
<text fg={themeV2.text.subdued()}>Password</text>
<text
fg={passwordHover() ? theme.text : theme.textMuted}
fg={passwordHover() ? themeV2.text() : themeV2.text.subdued()}
wrapMode="word"
onMouseOver={() => setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)}
@@ -67,7 +67,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
</text>
</box>
<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.
</text>
</Show>
@@ -78,7 +78,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0}
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>
)
@@ -87,17 +87,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
return (
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
Pair
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show
when={loadError()}
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
when={dimensions().height >= 36}
fallback={
@@ -116,11 +116,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
>
{(error) => (
<box>
<text fg={theme.error} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
Could not load server information
</text>
<text fg={theme.textMuted}>{errorMessage(error())}</text>
<text fg={theme.textMuted}>Close and reopen Pair to try again.</text>
<text fg={themeV2.text.subdued()}>{errorMessage(error())}</text>
<text fg={themeV2.text.subdued()}>Close and reopen Pair to try again.</text>
</box>
)}
</Show>
@@ -7,7 +7,7 @@ import { useDialog, type DialogContext } from "../ui/dialog"
export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) {
const dialog = useDialog()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
let input: InputRenderable
@@ -47,10 +47,10 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Name project copy
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
@@ -61,17 +61,18 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
}}
onSubmit={confirm}
placeholder="Project copy name"
placeholderColor={theme.textMuted}
textColor={theme.text}
focusedTextColor={theme.text}
cursorColor={theme.text}
placeholderColor={themeV2.text.subdued()}
textColor={themeV2.text.formfield()}
focusedTextColor={themeV2.text.formfield()}
cursorColor={themeV2.text.formfield()}
/>
<box paddingBottom={1} flexDirection="row" gap={2}>
<text fg={theme.text}>
enter <span style={{ fg: theme.textMuted }}>submit</span>
<text fg={themeV2.text()}>
enter <span style={{ fg: themeV2.text.subdued() }}>submit</span>
</text>
<text fg={theme.text}>
{shortcuts.get("dialog.project_copy.generate")} <span style={{ fg: theme.textMuted }}>generate one</span>
<text fg={themeV2.text()}>
{shortcuts.get("dialog.project_copy.generate")}{" "}
<span style={{ fg: themeV2.text.subdued() }}>generate one</span>
</text>
</box>
</box>
+15 -21
View File
@@ -8,47 +8,41 @@ export type DialogStatusProps = {}
export function DialogStatus() {
const data = useData()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const dialog = useDialog()
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 (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc
</text>
</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>
<text fg={theme.text}>
<text fg={themeV2.text()}>
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
</text>
<For each={mcp()}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
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 flexShrink={0} style={{ fg: color(item.status.status) }}>
</text>
<text fg={theme.text} wrapMode="word">
<text fg={themeV2.text()} wrapMode="word">
<b>{item.name}</b>{" "}
<span style={{ fg: theme.textMuted }}>
<span style={{ fg: themeV2.text.subdued() }}>
<Switch fallback={item.status.status}>
<Match when={item.status.status === "connected"}>Connected</Match>
<Match when={item.status.status === "failed" && item.status}>{(val) => val().error}</Match>
@@ -1,15 +1,20 @@
import { useTheme } from "../context/theme"
export function PluginRouteMissing(props: { id: string; name: string; onHome: () => void }) {
const { theme } = useTheme()
const { themeV2 } = useTheme()
return (
<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}
</text>
<box onMouseUp={props.onHome} backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
<text fg={theme.text}>go home</text>
<box
onMouseUp={props.onHome}
backgroundColor={themeV2.background.action("hovered")}
paddingLeft={1}
paddingRight={1}
>
<text fg={themeV2.text.action("hovered")}>go home</text>
</box>
</box>
)
+3 -3
View File
@@ -2,7 +2,7 @@ import { useTheme } from "../context/theme"
import { Spinner } from "./spinner"
export function Reconnecting() {
const theme = useTheme().theme
const { themeV2 } = useTheme()
return (
<box
@@ -12,12 +12,12 @@ export function Reconnecting() {
right={0}
bottom={0}
left={0}
backgroundColor={theme.background}
backgroundColor={themeV2.background()}
alignItems="center"
justifyContent="center"
>
<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>
)
@@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
import { Spinner } from "./spinner"
export function StartupLoading(props: { ready: () => boolean }) {
const theme = useTheme().theme
const { themeV2 } = useTheme().contextual("elevated")
const [show, setShow] = createSignal(false)
const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins..."))
let wait: NodeJS.Timeout | undefined
@@ -54,8 +54,8 @@ export function StartupLoading(props: { ready: () => boolean }) {
return (
<Show when={show()}>
<box position="absolute" zIndex={5000} left={0} right={0} bottom={1} justifyContent="center" alignItems="center">
<box backgroundColor={theme.backgroundPanel} paddingLeft={1} paddingRight={1}>
<Spinner color={theme.textMuted}>{text()}</Spinner>
<box backgroundColor={themeV2.background()} paddingLeft={1} paddingRight={1}>
<Spinner color={themeV2.text.subdued()}>{text()}</Spinner>
</box>
</box>
</Show>
+6 -6
View File
@@ -11,7 +11,7 @@ export type DialogAlertProps = {
export function DialogAlert(props: DialogAlertProps) {
const dialog = useDialog()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
Keymap.createLayer(() => ({
mode: "modal",
@@ -30,27 +30,27 @@ export function DialogAlert(props: DialogAlertProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
{props.title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text>
<text fg={themeV2.text.subdued()}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box
paddingLeft={3}
paddingRight={3}
backgroundColor={theme.primary}
backgroundColor={themeV2.background.action("focused")}
onMouseUp={() => {
props.onConfirm?.()
dialog.clear()
}}
>
<text fg={theme.selectedListItemText}>ok</text>
<text fg={themeV2.text.action("focused")}>ok</text>
</box>
</box>
</box>
+6 -6
View File
@@ -18,7 +18,7 @@ export type DialogConfirmResult = boolean | undefined
export function DialogConfirm(props: DialogConfirmProps) {
const dialog = useDialog()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const [store, setStore] = createStore({
active: "confirm" as "confirm" | "cancel",
})
@@ -57,15 +57,15 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
{props.title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>{props.message}</text>
<text fg={themeV2.text.subdued()}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<For each={["cancel", "confirm"] as const}>
@@ -73,14 +73,14 @@ export function DialogConfirm(props: DialogConfirmProps) {
<box
paddingLeft={1}
paddingRight={1}
backgroundColor={key === store.active ? theme.primary : undefined}
backgroundColor={key === store.active ? themeV2.background.action("focused") : undefined}
onMouseUp={() => {
if (key === "confirm") props.onConfirm?.()
if (key === "cancel") props.onCancel?.()
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)}
</text>
</box>
+6 -6
View File
@@ -5,7 +5,7 @@ import { useDialog, type DialogContext } from "./dialog"
export function DialogExportResult(props: { path: string; onClose?: () => void }) {
const dialog = useDialog()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const close = () => {
props.onClose?.()
@@ -27,19 +27,19 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Session exported
</text>
<text fg={theme.textMuted} onMouseUp={close}>
<text fg={themeV2.text.subdued()} onMouseUp={close}>
esc
</text>
</box>
<box>
<text fg={theme.text}>{props.path}</text>
<text fg={themeV2.text()}>{props.path}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={close}>
<text fg={theme.selectedListItemText}>Close</text>
<box paddingLeft={3} paddingRight={3} backgroundColor={themeV2.background.action("focused")} onMouseUp={close}>
<text fg={themeV2.text.action("focused")}>Close</text>
</box>
</box>
</box>
+11 -6
View File
@@ -5,7 +5,7 @@ import { useDialog } from "./dialog"
export function DialogHelp() {
const dialog = useDialog()
const { theme } = useTheme()
const { themeV2 } = useTheme().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
Keymap.createLayer(() => ({
@@ -19,21 +19,26 @@ export function DialogHelp() {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
Help
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
esc/enter
</text>
</box>
<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.
</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
<text fg={theme.selectedListItemText}>ok</text>
<box
paddingLeft={3}
paddingRight={3}
backgroundColor={themeV2.background.action("focused")}
onMouseUp={() => dialog.clear()}
>
<text fg={themeV2.text.action("focused")}>ok</text>
</box>
</box>
</box>