diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx
index 73a44854d17..3cbe50a49d1 100644
--- a/packages/tui/src/app.tsx
+++ b/packages/tui/src/app.tsx
@@ -66,7 +66,7 @@ import { DialogHelp } from "./ui/dialog-help"
import { DialogAgent } from "./component/dialog-agent"
import { DialogSessionList } from "./component/dialog-session-list"
import { ThemeErrorToast } from "./component/theme-error-toast"
-import { ThemeProvider, useTheme } from "./context/theme"
+import { ThemeProvider, useTheme, useThemes } from "./context/theme"
import { Home } from "./routes/home"
import { Session } from "./routes/session"
import { PromptHistoryProvider } from "./component/prompt/history"
@@ -423,8 +423,8 @@ function App(props: { pair?: DialogPairCredentials }) {
const event = useEvent()
const client = useClient()
const toast = useToast()
- const themeState = useTheme()
- const { themeV2, mode, supports, setMode, locked, lock, unlock } = themeState
+ const theme = useTheme()
+ const { mode, supports, setMode, locked, lock, unlock } = useThemes()
const data = useData()
const location = useLocation()
const exit = useExit()
@@ -1090,7 +1090,7 @@ function App(props: { pair?: DialogPairCredentials }) {
width={dimensions().width}
height={dimensions().height}
flexDirection="column"
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
onMouseDown={(evt) => {
if (copyOnSelectEnabled()) return
if (evt.button !== MouseButton.RIGHT) return
diff --git a/packages/tui/src/component/bg-pulse.tsx b/packages/tui/src/component/bg-pulse.tsx
index 064cc314f93..b4635a27906 100644
--- a/packages/tui/src/component/bg-pulse.tsx
+++ b/packages/tui/src/component/bg-pulse.tsx
@@ -7,7 +7,7 @@ import {
} from "@opentui/core"
import { extend, useRenderer } from "@opentui/solid"
import { onCleanup, onMount } from "solid-js"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { tint } from "../theme/color"
import { GoUpsellArtPainter } from "./bg-pulse-render"
@@ -70,7 +70,9 @@ declare module "@opentui/solid" {
extend({ go_upsell_art: GoUpsellArtRenderable })
export function BgPulse() {
- const { themeV2, mode } = useTheme().contextual("elevated")
+ const themes = useThemes()
+ const theme = themes.contextual("elevated")
+ const mode = themes.mode
const renderer = useRenderer()
let targetFps = renderer.targetFps
let maxFps = renderer.maxFps
@@ -91,9 +93,9 @@ export function BgPulse() {
)
diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx
index 8fa55144e1b..abd9525a773 100644
--- a/packages/tui/src/component/devtools-bar.tsx
+++ b/packages/tui/src/component/devtools-bar.tsx
@@ -11,7 +11,7 @@ import { useData } from "../context/data"
import { useLocation } from "../context/location"
import { useRoute } from "../context/route"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useTheme, useThemes } from "../context/theme"
import { DevTools } from "../devtools"
import { usePlugin } from "../plugin/context"
import { errorMessage } from "../util/error"
@@ -31,12 +31,12 @@ export function DevToolsBar() {
const location = useLocation()
const route = useRoute()
const plugins = usePlugin()
- const theme = useTheme()
+ const themes = useThemes()
const keymap = Keymap.use()
const renderer = useRenderer()
const dimensions = useTerminalDimensions()
- const { themeV2, mode, supports, setMode } = theme
- const elevatedTheme = theme.contextual("elevated").themeV2
+ const { current: theme, mode, supports, setMode } = themes
+ const elevatedTheme = themes.contextual("elevated")
const [panel, setPanel] = createSignal()
const [dumping, setDumping] = createSignal(false)
const [dumpPath, setDumpPath] = createSignal()
@@ -196,8 +196,8 @@ export function DevToolsBar() {
})),
},
theme: {
- name: theme.selected,
- mode: theme.mode(),
+ name: themes.selected,
+ mode: themes.mode(),
},
},
null,
@@ -213,7 +213,7 @@ export function DevToolsBar() {
}
return (
-
+
{serverIndicator().icon}
@@ -243,10 +243,10 @@ export function DevToolsBar() {
{" "}
@@ -279,10 +279,10 @@ export function DevToolsBar() {
{statusIcon(runtime())}
@@ -290,10 +290,10 @@ export function DevToolsBar() {
{" "}
@@ -320,11 +320,11 @@ export function DevToolsBar() {
toggle("theme")}>
- Theme
+ Theme
Theme
-
+
{(entry) =>
}
@@ -336,7 +336,7 @@ export function DevToolsBar() {
toggle("tools")}>
- Tools
+ Tools
Tools
@@ -406,14 +406,14 @@ export function DevToolsBar() {
-
+
)
}
function BarItem(props: ParentProps<{ active: boolean; onClick: () => void }>) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const renderer = useRenderer()
return (
void }>) {
flexDirection="row"
paddingLeft={1}
paddingRight={1}
- backgroundColor={props.active ? themeV2.background.action.primary.focused : undefined}
+ backgroundColor={props.active ? theme.background.action.primary.focused : undefined}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
props.onClick()
@@ -435,7 +435,7 @@ function BarItem(props: ParentProps<{ active: boolean; onClick: () => void }>) {
}
function PanelBox(props: ParentProps) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const renderer = useRenderer()
return (
{
if (renderer.getSelection()?.getSelectedText()) return
@@ -461,32 +461,32 @@ function PanelBox(props: ParentProps) {
}
function PanelTitle(props: ParentProps) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
return (
-
+
{props.children}
)
}
function Row(props: { label: string; value: string }) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
return (
- {props.label}
+ {props.label}
- {props.value}
+ {props.value}
)
}
function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; hoverBackground?: boolean }>) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [hovered, setHovered] = createSignal(false)
return (
setHovered(true)}
onMouseOut={() => setHovered(false)}
@@ -495,7 +495,7 @@ function Action(props: ParentProps<{ onClick: () => void; disabled?: boolean; ho
if (!props.disabled) props.onClick()
}}
>
- {props.children}
+ {props.children}
)
}
@@ -506,7 +506,7 @@ function cpuPercent(microseconds: number, milliseconds: number) {
}
function ProcessStat(props: { label: string; values: readonly number[]; unit: string; decimals?: number }) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const value = () => {
const value = props.values.at(-1)
if (value === undefined) return "--"
@@ -515,13 +515,13 @@ function ProcessStat(props: { label: string; values: readonly number[]; unit: st
return (
- {props.label}
+ {props.label}
- {brailleGraph(props.values)}
+ {brailleGraph(props.values)}
- {value()}
+ {value()}
)
diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx
index 49d5f93650e..e62ce18d248 100644
--- a/packages/tui/src/component/dialog-config.tsx
+++ b/packages/tui/src/component/dialog-config.tsx
@@ -1,6 +1,6 @@
import { createMemo, createSignal } from "solid-js"
import { useConfig } from "../config"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { DialogSelect } from "../ui/dialog-select"
import { useToast } from "../ui/toast"
@@ -267,7 +267,7 @@ export function settingID(setting: Setting) {
export function DialogConfig(props: { current?: string }) {
const config = useConfig()
const toast = useToast()
- const themeState = useTheme()
+ const themes = useThemes()
const current = Math.max(
0,
settings.findIndex((setting) => settingID(setting) === props.current),
@@ -280,12 +280,12 @@ export function DialogConfig(props: { current?: string }) {
if (!result || typeof result !== "object") return undefined
return (result as Record)[key]
}, config.data)
- if (setting.path.join(".") === "theme.name") return current ?? themeState.selected
+ if (setting.path.join(".") === "theme.name") return current ?? themes.selected
return current ?? setting.default
}
const values = (setting: Setting) =>
setting.path.join(".") === "theme.name"
- ? Object.keys(themeState.all()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
+ ? Object.keys(themes.all()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
: setting.values
const display = (setting: Setting) => {
const current = value(setting)
diff --git a/packages/tui/src/component/dialog-debug.tsx b/packages/tui/src/component/dialog-debug.tsx
index ed13a8ffe05..fcf2673187f 100644
--- a/packages/tui/src/component/dialog-debug.tsx
+++ b/packages/tui/src/component/dialog-debug.tsx
@@ -11,7 +11,7 @@ import { describeOS, describeTerminal } from "../util/system"
import { useTuiApp } from "../context/runtime"
export function DialogDebug() {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const dialog = useDialog()
const route = useRoute()
const local = useLocal()
@@ -55,10 +55,10 @@ export function DialogDebug() {
return (
-
+
Debug
- dialog.clear()}>
+ dialog.clear()}>
esc
@@ -68,10 +68,10 @@ export function DialogDebug() {
{(entry) => (
-
+
{entry.label.padEnd(10)}
-
+
{entry.value}
@@ -79,12 +79,12 @@ export function DialogDebug() {
- Share this when reporting an issue.
+ Share this when reporting an issue.
-
+
{copied() ? "✓ copied" : "copy"}{" "}
- enter
+ enter
diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx
index 8a168b457d3..5a32abe2861 100644
--- a/packages/tui/src/component/dialog-integration.tsx
+++ b/packages/tui/src/component/dialog-integration.tsx
@@ -11,7 +11,7 @@ import { useClipboard } from "../context/clipboard"
import { useData } from "../context/data"
import { useClient } from "../context/client"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { DialogPrompt } from "../ui/dialog-prompt"
import { DialogSelect } from "../ui/dialog-select"
@@ -64,7 +64,7 @@ export function DialogIntegration(
) {
const data = useData()
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const options = createMemo(() => {
const providers = data.location.websearch.list() ?? []
const providersByID = new Map(providers.map((provider) => [provider.id, provider]))
@@ -87,7 +87,7 @@ export function DialogIntegration(
disabled: methods.length === 0 && credentials.length === 0,
gutter:
integration.connections.length > 0
- ? () => ✓
+ ? () => ✓
: undefined,
onSelect: () => {
if (credentials.length) return manageConnections(integration, methods, dialog, props.onConnected)
@@ -103,12 +103,12 @@ export function DialogIntegration(
options={options()}
emptyView={
- No integrations available
+ No integrations available
}
noMatchView={
- No integrations found
+ No integrations found
}
/>
@@ -303,16 +303,16 @@ function CommandPending(props: {
function CommandView(props: { title: string; output: string; message: string }) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
- const { themeV2: overlayTheme } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("elevated")
+ const overlayTheme = useThemes().contextual("overlay")
onMount(() => dialog.setSize("large"))
return (
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc close
@@ -326,7 +326,7 @@ function CommandView(props: { title: string; output: string; message: string })
{props.output.trim()}
- {props.message}
+ {props.message}
)
@@ -341,7 +341,7 @@ function KeyMethod(props: {
const dialog = useDialog()
const client = useClient()
const toast = useToast()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [error, setError] = createSignal()
return (
@@ -360,7 +360,7 @@ function KeyMethod(props: {
.catch((cause) => setError(message(cause)))
}}
description={() => (
- {(value) => {value()}}
+ {(value) => {value()}}
)}
/>
)
@@ -516,7 +516,7 @@ function OAuthCode(props: {
const dialog = useDialog()
const client = useClient()
const toast = useToast()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [error, setError] = createSignal()
let settled = false
@@ -550,9 +550,9 @@ function OAuthCode(props: {
}}
description={() => (
- {props.attempt.instructions}
-
- {(value) => {value()}}
+ {props.attempt.instructions}
+
+ {(value) => {value()}}
)}
/>
@@ -561,31 +561,31 @@ function OAuthCode(props: {
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
return (
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc
{(url) => (
-
+
- {(instructions) => {instructions()}}
+ {(instructions) => {instructions()}}
)}
- {props.message}
+ {props.message}
-
- c copy
+
+ c copy
diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx
index e1ecd011f49..31fafd7ef88 100644
--- a/packages/tui/src/component/dialog-mcp.tsx
+++ b/packages/tui/src/component/dialog-mcp.tsx
@@ -5,7 +5,7 @@ import { Keymap } from "../context/keymap"
import { pipe, sortBy } from "remeda"
import { DialogSelect } from "../ui/dialog-select"
import { useDialog } from "../ui/dialog"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
import type { McpServer } from "@opencode-ai/client"
import { useClipboard } from "../context/clipboard"
@@ -20,12 +20,12 @@ function statusError(status: McpServer["status"]) {
}
function Status(props: { enabled: boolean; loading: boolean }) {
- const { themeV2 } = useTheme().contextual("elevated")
- if (props.loading) return ⋯ Loading
+ const theme = useThemes().contextual("elevated")
+ if (props.loading) return ⋯ Loading
if (props.enabled) {
- return ✓ Enabled
+ return ✓ Enabled
}
- return ○ Disabled
+ return ○ Disabled
}
export function DialogMcp() {
@@ -33,7 +33,7 @@ export function DialogMcp() {
const dialog = useDialog()
const client = useClient()
const toast = useToast()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [focused, setFocused] = createSignal()
const [detail, setDetail] = createSignal()
const [loading, setLoading] = createSignal(null)
@@ -110,7 +110,7 @@ export function DialogMcp() {
]}
footer={
- enter to view error
+ enter to view error
}
/>
@@ -134,8 +134,8 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
const dialog = useDialog()
const clipboard = useClipboard()
const toast = useToast()
- const { themeV2 } = useTheme().contextual("elevated")
- const { themeV2: overlayTheme } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("elevated")
+ const overlayTheme = useThemes().contextual("overlay")
const dimensions = useTerminalDimensions()
const config = useConfig().data
const [copied, setCopied] = createSignal(false)
@@ -171,14 +171,14 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
return (
-
+
MCP server: {props.server.name}
-
+
esc back
- ✗ Failed
+ ✗ Failed
void }) {
- ↑↓ scroll
-
+ ↑↓ scroll
+
{copied() ? "✓ copied" : "c copy details"}
diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx
index 6f2f3e6ba60..3a1171acf8e 100644
--- a/packages/tui/src/component/dialog-move-session.tsx
+++ b/packages/tui/src/component/dialog-move-session.tsx
@@ -6,7 +6,7 @@ import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
import { useDialog } from "../ui/dialog"
import { useClient } from "../context/client"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useData } from "../context/data"
import { abbreviateHome } from "../runtime"
import { useTuiPaths } from "../context/runtime"
@@ -38,7 +38,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
const dialog = useDialog()
const client = useClient()
const dimensions = useTerminalDimensions()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const sessionData = useData()
const route = useRoute()
const toast = useToast()
@@ -172,18 +172,18 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
return {
title,
titleView: isRemoving ? (
- Deleting {item.location}
+ Deleting {item.location}
) : deleting ? (
-
+
Press {shortcuts.get("dialog.move_session.delete")} again to confirm
) : suffix ? (
<>
{visible.slice(0, split)}
- {visible.slice(split)}
+ {visible.slice(split)}
>
) : undefined,
- bg: deleting ? themeV2.background.action.destructive.default : undefined,
+ bg: deleting ? theme.background.action.destructive.default : undefined,
value: {
type: "directory",
directory: item.location,
@@ -316,7 +316,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
title="Move session"
titleView={
-
+
Move session
@@ -329,25 +329,25 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
emptyView={
showError() ? (
-
+
Could not load project directories
- {errorMessage(loadError())}
- Close and reopen Move session to try again.
+ {errorMessage(loadError())}
+ Close and reopen Move session to try again.
) : directories.loading || loadedProject.loading ? (
- Loading project directories…
+ Loading project directories…
) : (
- No project directories available
+ No project directories available
)
}
noMatchView={
- No project directories found
+ No project directories found
}
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
diff --git a/packages/tui/src/component/dialog-pair.tsx b/packages/tui/src/component/dialog-pair.tsx
index 0a5cb0d75d5..05565dd0df3 100644
--- a/packages/tui/src/component/dialog-pair.tsx
+++ b/packages/tui/src/component/dialog-pair.tsx
@@ -3,7 +3,7 @@ import { useTerminalDimensions } from "@opentui/solid"
import { createMemo, createResource, createSignal, For, Show } from "solid-js"
import { renderUnicodeCompact } from "uqr"
import { useClient } from "../context/client"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { errorMessage } from "../util/error"
@@ -16,7 +16,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
const client = useClient()
const dialog = useDialog()
const dimensions = useTerminalDimensions()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [loadError, setLoadError] = createSignal()
const [showPassword, setShowPassword] = createSignal(false)
const [passwordHover, setPasswordHover] = createSignal(false)
@@ -47,17 +47,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
- URLs
- {(url) => {url}}
+ URLs
+ {(url) => {url}}
- Username
- {value.username}
+ Username
+ {value.username}
- Password
+ Password
setPasswordHover(true)}
onMouseOut={() => setPasswordHover(false)}
@@ -67,7 +67,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname))}>
-
+
Run `opencode service set hostname 0.0.0.0` to access the service remotely.
@@ -78,7 +78,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
flexShrink={0}
alignItems={horizontal() ? "flex-end" : "center"}
>
- {renderUnicodeCompact(JSON.stringify(value), { border: 1 })}
+ {renderUnicodeCompact(JSON.stringify(value), { border: 1 })}
)
@@ -87,17 +87,17 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
return (
-
+
Pair
- dialog.clear()}>
+ dialog.clear()}>
esc
Loading server information…}>
+ Loading server information…}>
= 36}
fallback={
@@ -116,11 +116,11 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
>
{(error) => (
-
+
Could not load server information
- {errorMessage(error())}
- Close and reopen Pair to try again.
+ {errorMessage(error())}
+ Close and reopen Pair to try again.
)}
diff --git a/packages/tui/src/component/dialog-project-copy-name.tsx b/packages/tui/src/component/dialog-project-copy-name.tsx
index 63a15bcace0..d172b224563 100644
--- a/packages/tui/src/component/dialog-project-copy-name.tsx
+++ b/packages/tui/src/component/dialog-project-copy-name.tsx
@@ -2,12 +2,12 @@ import { InputRenderable, TextAttributes } from "@opentui/core"
import { Slug } from "@opencode-ai/core/util/slug"
import { createSignal, onMount } from "solid-js"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "../ui/dialog"
export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
const [inputTarget, setInputTarget] = createSignal()
let input: InputRenderable
@@ -47,10 +47,10 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
return (
-
+
Name project copy
- dialog.clear()}>
+ dialog.clear()}>
esc
@@ -61,17 +61,17 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
}}
onSubmit={confirm}
placeholder="Project copy name"
- placeholderColor={themeV2.text.subdued}
- textColor={themeV2.text.formfield.default}
- focusedTextColor={themeV2.text.formfield.default}
- cursorColor={themeV2.text.formfield.default}
+ placeholderColor={theme.text.subdued}
+ textColor={theme.text.formfield.default}
+ focusedTextColor={theme.text.formfield.default}
+ cursorColor={theme.text.formfield.default}
/>
-
- enter submit
+
+ enter submit
-
- {shortcuts.get("dialog.project_copy.generate")} generate one
+
+ {shortcuts.get("dialog.project_copy.generate")} generate one
diff --git a/packages/tui/src/component/dialog-retry-action.tsx b/packages/tui/src/component/dialog-retry-action.tsx
index 24c4b7cdcf8..b98ba9580c0 100644
--- a/packages/tui/src/component/dialog-retry-action.tsx
+++ b/packages/tui/src/component/dialog-retry-action.tsx
@@ -2,7 +2,7 @@ import { RGBA, TextAttributes } from "@opentui/core"
import open from "open"
import { createSignal } from "solid-js"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "../ui/dialog"
import { Link } from "../ui/link"
import { BgPulse } from "./bg-pulse"
@@ -38,9 +38,9 @@ function panelOverlay(color: RGBA) {
export function DialogRetryAction(props: DialogRetryActionProps) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const showGoTreatment = () => props.link === GO_URL
- const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background.default) : undefined)
+ const textBg = () => (showGoTreatment() ? panelOverlay(theme.background.default) : undefined)
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
Keymap.createLayer(() => ({
@@ -85,26 +85,26 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
) : null}
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc
-
+
{props.message}
{props.link ? (
showGoTreatment() ? (
-
+
) : (
-
+
)
) : (
@@ -115,13 +115,13 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
paddingLeft={2}
paddingRight={2}
backgroundColor={
- selected() === "dismiss" ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
+ selected() === "dismiss" ? theme.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
}
onMouseOver={() => setSelected("dismiss")}
onMouseUp={() => dismiss(props, dialog)}
>
@@ -132,13 +132,13 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
paddingLeft={2}
paddingRight={2}
backgroundColor={
- selected() === "action" ? themeV2.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
+ selected() === "action" ? theme.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
}
onMouseOver={() => setSelected("action")}
onMouseUp={() => runAction(props, dialog)}
>
diff --git a/packages/tui/src/component/dialog-session-delete-failed.tsx b/packages/tui/src/component/dialog-session-delete-failed.tsx
index 1f671218044..d11b113a7f9 100644
--- a/packages/tui/src/component/dialog-session-delete-failed.tsx
+++ b/packages/tui/src/component/dialog-session-delete-failed.tsx
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { createStore } from "solid-js/store"
import { For } from "solid-js"
@@ -13,7 +13,7 @@ export function DialogSessionDeleteFailed(props: {
onDone?: () => void
}) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [store, setStore] = createStore({
active: "delete" as "delete" | "restore",
})
@@ -64,17 +64,17 @@ export function DialogSessionDeleteFailed(props: {
return (
-
+
Failed to Delete Session
- dialog.clear()}>
+ dialog.clear()}>
esc
-
+
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
-
+
Choose how you want to recover this broken workspace session.
@@ -86,7 +86,7 @@ export function DialogSessionDeleteFailed(props: {
paddingRight={1}
paddingTop={1}
paddingBottom={1}
- backgroundColor={item.id === store.active ? themeV2.background.action.primary.focused : undefined}
+ backgroundColor={item.id === store.active ? theme.background.action.primary.focused : undefined}
onMouseUp={() => {
setStore("active", item.id)
void confirm()
@@ -94,12 +94,12 @@ export function DialogSessionDeleteFailed(props: {
>
{item.title}
{item.description}
diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx
index b1067fa8598..349d216822a 100644
--- a/packages/tui/src/component/dialog-session-list.tsx
+++ b/packages/tui/src/component/dialog-session-list.tsx
@@ -7,7 +7,7 @@ import { useRoute } from "../context/route"
import { useData } from "../context/data"
import { Keymap } from "../context/keymap"
import { Locale } from "../util/locale"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useClient } from "../context/client"
import { useLocal } from "../context/local"
import { createDebouncedSignal } from "../util/signal"
@@ -20,7 +20,9 @@ export function DialogSessionList() {
const dialog = useDialog()
const route = useRoute()
const data = useData()
- const { themeV2, mode } = useTheme().contextual("elevated")
+ const themes = useThemes()
+ const theme = themes.contextual("elevated")
+ const mode = themes.mode
const client = useClient()
const local = useLocal()
const toast = useToast()
@@ -109,13 +111,13 @@ export function DialogSessionList() {
value: session.id,
category,
footer,
- bg: deleting ? themeV2.background.action.destructive.focused : undefined,
- fg: deleting ? themeV2.text.action.destructive.focused : undefined,
+ bg: deleting ? theme.background.action.destructive.focused : undefined,
+ fg: deleting ? theme.text.action.destructive.focused : undefined,
gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running")
? () =>
: slot === undefined
? undefined
- : () => {slot},
+ : () => {slot},
}
}
@@ -143,12 +145,12 @@ export function DialogSessionList() {
}}
emptyView={
- No sessions available
+ No sessions available
}
noMatchView={
-
+
{searchState().message}
diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx
index d853e55fffd..3d2e31260ce 100644
--- a/packages/tui/src/component/dialog-skill.tsx
+++ b/packages/tui/src/component/dialog-skill.tsx
@@ -15,7 +15,7 @@ export type DialogSkillProps = {
export function DialogSkill(props: DialogSkillProps) {
const dialog = useDialog()
const data = useData()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
dialog.setSize("large")
const [loadError, setLoadError] = createSignal()
@@ -63,29 +63,29 @@ export function DialogSkill(props: DialogSkillProps) {
- No skills available
+ No skills available
}
>
-
+
Could not load skills
- {errorMessage(loadError())}
- Close and reopen Skills to try again.
+ {errorMessage(loadError())}
+ Close and reopen Skills to try again.
- Loading skills…
+ Loading skills…
}
noMatchView={
- No skills found
+ No skills found
}
/>
diff --git a/packages/tui/src/component/dialog-stash.tsx b/packages/tui/src/component/dialog-stash.tsx
index 80aa75250cb..90c2dc8e534 100644
--- a/packages/tui/src/component/dialog-stash.tsx
+++ b/packages/tui/src/component/dialog-stash.tsx
@@ -3,7 +3,7 @@ import { DialogSelect } from "../ui/dialog-select"
import { createMemo, createSignal } from "solid-js"
import { Locale } from "../util/locale"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { usePromptStash, type StashEntry } from "./prompt/stash"
function getRelativeTime(timestamp: number): string {
@@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string {
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
const dialog = useDialog()
const stash = usePromptStash()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
const [toDelete, setToDelete] = createSignal()
@@ -45,8 +45,8 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
title: isDeleting
? `Press ${shortcuts.get("stash.delete")} again to confirm`
: getStashPreview(entry.prompt.text),
- bg: isDeleting ? themeV2.background.action.destructive.focused : undefined,
- fg: isDeleting ? themeV2.text.action.destructive.focused : undefined,
+ bg: isDeleting ? theme.background.action.destructive.focused : undefined,
+ fg: isDeleting ? theme.text.action.destructive.focused : undefined,
value: index,
description: getRelativeTime(entry.timestamp),
footer: lineCount > 1 ? `~${lineCount} lines` : undefined,
diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx
index 2e983465beb..2e9675c807a 100644
--- a/packages/tui/src/component/dialog-status.tsx
+++ b/packages/tui/src/component/dialog-status.tsx
@@ -1,5 +1,5 @@
import { TextAttributes } from "@opentui/core"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { useData } from "../context/data"
import { For, Match, Switch, Show, createMemo } from "solid-js"
@@ -8,30 +8,30 @@ export type DialogStatusProps = {}
export function DialogStatus() {
const data = useData()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().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.default
- if (status === "failed") return themeV2.text.feedback.error.default
- if (status === "needs_auth") return themeV2.text.feedback.warning.default
- if (status === "needs_client_registration") return themeV2.text.feedback.error.default
- return themeV2.text.subdued
+ if (status === "connected") return theme.text.feedback.success.default
+ if (status === "failed") return theme.text.feedback.error.default
+ if (status === "needs_auth") return theme.text.feedback.warning.default
+ if (status === "needs_client_registration") return theme.text.feedback.error.default
+ return theme.text.subdued
}
return (
-
+
Status
- dialog.clear()}>
+ dialog.clear()}>
esc
- 0} fallback={No MCP servers}>
+ 0} fallback={No MCP servers}>
-
+
{mcp().length} MCP server{mcp().length === 1 ? "" : "s"}
@@ -40,9 +40,9 @@ export function DialogStatus() {
•
-
+
{item.name}{" "}
-
+
Connected
{(val) => val().error}
diff --git a/packages/tui/src/component/dialog-theme-list.tsx b/packages/tui/src/component/dialog-theme-list.tsx
index 6cf3539ad94..f1a8cda376a 100644
--- a/packages/tui/src/component/dialog-theme-list.tsx
+++ b/packages/tui/src/component/dialog-theme-list.tsx
@@ -1,11 +1,11 @@
import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { onCleanup } from "solid-js"
export function DialogThemeList() {
- const theme = useTheme()
- const options = Object.keys(theme.all())
+ const themes = useThemes()
+ const options = Object.keys(themes.all())
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
.map((value) => ({
title: value,
@@ -14,10 +14,10 @@ export function DialogThemeList() {
const dialog = useDialog()
let confirmed = false
let ref: DialogSelectRef
- const initial = theme.selected
+ const initial = themes.selected
onCleanup(() => {
- if (!confirmed) theme.set(initial)
+ if (!confirmed) themes.set(initial)
})
return (
@@ -26,10 +26,10 @@ export function DialogThemeList() {
options={options}
current={initial}
onMove={(opt) => {
- theme.set(opt.value)
+ themes.set(opt.value)
}}
onSelect={(opt) => {
- theme.set(opt.value)
+ themes.set(opt.value)
confirmed = true
dialog.clear()
}}
@@ -38,12 +38,12 @@ export function DialogThemeList() {
}}
onFilter={(query) => {
if (query.length === 0) {
- theme.set(initial)
+ themes.set(initial)
return
}
const first = ref.filtered[0]
- if (first) theme.set(first.value)
+ if (first) themes.set(first.value)
}}
/>
)
diff --git a/packages/tui/src/component/dialog-workspace-file-changes.tsx b/packages/tui/src/component/dialog-workspace-file-changes.tsx
index 4d79dd26c6c..d349923e05e 100644
--- a/packages/tui/src/component/dialog-workspace-file-changes.tsx
+++ b/packages/tui/src/component/dialog-workspace-file-changes.tsx
@@ -4,7 +4,7 @@ import type { VcsFileStatus } from "@opencode-ai/client"
import { createMemo, For } from "solid-js"
import { createStore } from "solid-js/store"
import { FilePath } from "../ui/file-path"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useConfig } from "../config"
import { useDialog, type DialogContext } from "../ui/dialog"
import { getScrollAcceleration } from "../util/scroll"
@@ -31,8 +31,8 @@ export function DialogWorkspaceFileChanges(props: {
message?: string
}) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
- const { themeV2: overlayTheme } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("elevated")
+ const overlayTheme = useThemes().contextual("overlay")
const config = useConfig().data
const dimensions = useTerminalDimensions()
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
@@ -72,15 +72,15 @@ export function DialogWorkspaceFileChanges(props: {
return (
-
+
{props.title ?? "File Changes Found"}
- dialog.clear()}>
+ dialog.clear()}>
esc
-
+
{props.message ?? "Do you want to move these changes with the session?"}
@@ -118,16 +118,14 @@ export function DialogWorkspaceFileChanges(props: {
{
setStore("active", item)
props.onSelect(item)
dialog.clear()
}}
>
-
- {item}
-
+ {item}
)}
diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx
index 21465820594..adf6eb7d6db 100644
--- a/packages/tui/src/component/logo.tsx
+++ b/packages/tui/src/component/logo.tsx
@@ -5,10 +5,10 @@ import { tint } from "../theme/color"
import { logo } from "../logo"
export function Logo() {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
- const shadow = tint(themeV2.background.default, fg, 0.25)
+ const shadow = tint(theme.background.default, fg, 0.25)
const attrs = bold ? TextAttributes.BOLD : undefined
return Array.from(line).map((char) => {
if (char === "_") {
@@ -52,8 +52,8 @@ export function Logo() {
{(line, index) => (
- {renderLine(line, themeV2.text.subdued, false)}
- {renderLine(logo.right[index()], themeV2.text.default, true)}
+ {renderLine(line, theme.text.subdued, false)}
+ {renderLine(logo.right[index()], theme.text.default, true)}
)}
diff --git a/packages/tui/src/component/plugin-route-missing.tsx b/packages/tui/src/component/plugin-route-missing.tsx
index 5b36dcbdaf6..d9c2ff79477 100644
--- a/packages/tui/src/component/plugin-route-missing.tsx
+++ b/packages/tui/src/component/plugin-route-missing.tsx
@@ -1,20 +1,20 @@
import { useTheme } from "../context/theme"
export function PluginRouteMissing(props: { id: string; name: string; onHome: () => void }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
return (
-
+
Unknown plugin route: {props.id}/{props.name}
- go home
+ go home
)
diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx
index 921d096819c..10c1f037dda 100644
--- a/packages/tui/src/component/prompt/autocomplete.tsx
+++ b/packages/tui/src/component/prompt/autocomplete.tsx
@@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll"
import { useTuiPaths } from "../../context/runtime"
import { useConfig } from "../../config"
import { useLocation } from "../../context/location"
-import { useTheme } from "../../context/theme"
+import { useThemes } from "../../context/theme"
import { SplitBorder } from "../../ui/border"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "../../util/locale"
@@ -57,7 +57,7 @@ export function Autocomplete(props: {
const data = useData()
const keymap = Keymap.use()
const keymapCommands = Keymap.useCommands()
- const { themeV2 } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("overlay")
const dimensions = useTerminalDimensions()
const frecency = useFrecency()
const config = useConfig().data
@@ -698,11 +698,11 @@ export function Autocomplete(props: {
width={position().width}
zIndex={100}
{...SplitBorder}
- borderColor={themeV2.border.default}
+ borderColor={theme.border.default}
>
(scroll = r)}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
height={height()}
scrollbarOptions={{ visible: false }}
scrollAcceleration={scrollAcceleration()}
@@ -711,9 +711,7 @@ export function Autocomplete(props: {
each={options()}
fallback={
-
- {emptyMessage()}
-
+ {emptyMessage()}
}
>
@@ -721,7 +719,7 @@ export function Autocomplete(props: {
{
setStore("input", "mouse")
@@ -737,14 +735,14 @@ export function Autocomplete(props: {
onMouseUp={() => select()}
>
{option().display}
{" " + option().description?.trimStart()}
diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
index ff4b516a889..44e98f92d5d 100644
--- a/packages/tui/src/component/prompt/index.tsx
+++ b/packages/tui/src/component/prompt/index.tsx
@@ -12,7 +12,7 @@ import { registerOpencodeSpinner } from "../register-spinner"
import path from "path"
import { fileURLToPath } from "url"
import { useLocal } from "../../context/local"
-import { useTheme } from "../../context/theme"
+import { useTheme, useThemes } from "../../context/theme"
import { tint } from "../../theme/color"
import { EmptyBorder, SplitBorder } from "../../ui/border"
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
@@ -189,7 +189,8 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer()
const exit = useExit()
const dimensions = useTerminalDimensions()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const animationsEnabled = createMemo(() => config.animations ?? true)
const list = createMemo(() => props.placeholders?.normal ?? [])
const shell = createMemo(() => props.placeholders?.shell ?? [])
@@ -296,8 +297,8 @@ export function Prompt(props: PromptProps) {
createEffect(() => {
if (!input || input.isDestroyed) return
- if (props.disabled) input.cursorColor = themeV2.background.surface.offset
- if (!props.disabled) input.cursorColor = themeV2.text.default
+ if (props.disabled) input.cursorColor = theme.background.surface.offset
+ if (!props.disabled) input.cursorColor = theme.text.default
})
const usage = createMemo(() => {
@@ -1299,10 +1300,10 @@ export function Prompt(props: PromptProps) {
}
const highlight = createMemo(() => {
- if (leader()) return themeV2.border.default
- if (store.mode === "shell") return themeV2.text.action.primary.selected
+ if (leader()) return theme.border.default
+ if (store.mode === "shell") return theme.text.action.primary.selected
const agent = local.agent.current()
- if (!agent) return themeV2.border.default
+ if (!agent) return theme.border.default
return local.agent.color(agent.id)
})
const agentLabel = createMemo(() => {
@@ -1324,7 +1325,7 @@ export function Prompt(props: PromptProps) {
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
animationsEnabled,
)
- const borderHighlight = createMemo(() => tint(themeV2.border.default, highlight(), agentMetaAlpha()))
+ const borderHighlight = createMemo(() => tint(theme.border.default, highlight(), agentMetaAlpha()))
const placeholderText = createMemo(() => {
if (props.showPlaceholder === false) return undefined
@@ -1344,7 +1345,7 @@ export function Prompt(props: PromptProps) {
const spinnerDef = createMemo(() => {
const agent = status() === "running" ? local.agent.current() : local.agent.current()
- const color = agent ? local.agent.color(agent.id) : themeV2.border.default
+ const color = agent ? local.agent.color(agent.id) : theme.border.default
return {
frames: createFrames({
color,
@@ -1364,7 +1365,7 @@ export function Prompt(props: PromptProps) {
})
const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3)))
- const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset))
+ const promptBg = createMemo(() => theme.raise(theme.background.surface.offset))
return (
<>
@@ -1390,9 +1391,9 @@ export function Prompt(props: PromptProps) {
}>
0 ? themeV2.background.action.primary.default : themeV2.text.default}
+ fg={store.interrupt > 0 ? theme.background.action.primary.default : theme.text.default}
wrapMode="none"
truncate
flexShrink={1}
@@ -1554,7 +1555,7 @@ export function Prompt(props: PromptProps) {
esc{" "}
0 ? themeV2.background.action.primary.default : themeV2.text.subdued,
+ fg: store.interrupt > 0 ? theme.background.action.primary.default : theme.text.subdued,
}}
>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
@@ -1565,16 +1566,16 @@ export function Prompt(props: PromptProps) {
{(progress) => (
-
+
{progress()}
- {".".repeat(move.creatingDots())}
+ {".".repeat(move.creatingDots())}
)}
-
+
(new working copy)
@@ -1582,7 +1583,7 @@ export function Prompt(props: PromptProps) {
}>
{(location) => (
-
+
{location()}
)}
@@ -1596,7 +1597,7 @@ export function Prompt(props: PromptProps) {
wrapMode="none"
truncate
flexShrink={1}
- fg={editorContextLabelState() === "pending" ? themeV2.hue.accent[500] : themeV2.text.subdued}
+ fg={editorContextLabelState() === "pending" ? theme.hue.accent[500] : theme.text.subdued}
>
{file()}
@@ -1606,40 +1607,40 @@ export function Prompt(props: PromptProps) {
0}>
-
+
- {(shortcut) => {shortcut()} }
+ {(shortcut) => {shortcut()} }
- {(label) => {label()}}
+ {(label) => {label()}}
- ·
+ ·
- {(label) => {label()}}
+ {(label) => {label()}}
0}>
- ·
+ ·
0}>
- {statusItems().join(" · ")}
+ {statusItems().join(" · ")}
-
- {agentShortcut()} agents
+
+ {agentShortcut()} agents
-
- {paletteShortcut()} commands
+
+ {paletteShortcut()} commands
-
- esc exit shell mode
+
+ esc exit shell mode
diff --git a/packages/tui/src/component/reconnecting.tsx b/packages/tui/src/component/reconnecting.tsx
index 927c42519c7..a61e319bd2b 100644
--- a/packages/tui/src/component/reconnecting.tsx
+++ b/packages/tui/src/component/reconnecting.tsx
@@ -1,9 +1,9 @@
import { RGBA } from "@opentui/core"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { Spinner } from "./spinner"
export function Reconnecting() {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
return (
- Restarting service...
- Your session will resume automatically.
+ Restarting service...
+ Your session will resume automatically.
)
diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx
index 85469c48844..1cca2f4bcff 100644
--- a/packages/tui/src/component/spinner.tsx
+++ b/packages/tui/src/component/spinner.tsx
@@ -11,9 +11,9 @@ export { SPINNER_FRAMES } from "./spinner-frames"
registerOpencodeSpinner()
export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const config = useConfig().data
- const color = () => props.color ?? themeV2.text.subdued
+ const color = () => props.color ?? theme.text.subdued
return (
boolean }) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().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 (
-
- {text()}
+
+ {text()}
diff --git a/packages/tui/src/component/theme-error-toast.tsx b/packages/tui/src/component/theme-error-toast.tsx
index f10a11bdc94..4daab0e547c 100644
--- a/packages/tui/src/component/theme-error-toast.tsx
+++ b/packages/tui/src/component/theme-error-toast.tsx
@@ -1,13 +1,13 @@
import { onCleanup } from "solid-js"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useToast } from "../ui/toast"
export function ThemeErrorToast() {
- const theme = useTheme()
+ const themes = useThemes()
const toast = useToast()
onCleanup(
- theme.onError(({ name, error }) =>
+ themes.onError(({ name, error }) =>
toast.show({
variant: "error",
title: `Failed to load theme: ${name}`,
diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx
index 98faea2f9d0..96b01519318 100644
--- a/packages/tui/src/context/local.tsx
+++ b/packages/tui/src/context/local.tsx
@@ -17,7 +17,7 @@ import {
type ModelPreference,
type ModelPreferenceModel,
} from "../model-preference"
-import { useTheme } from "./theme"
+import { useTheme, useThemes } from "./theme"
import { useToast } from "../ui/toast"
import { useRoute } from "./route"
import { useData } from "./data"
@@ -50,7 +50,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const data = useData()
const client = useClient()
const toast = useToast()
- const { themeV2, mode } = useTheme()
+ const theme = useTheme()
+ const { mode } = useThemes()
const route = useRoute()
const paths = useTuiPaths()
const args = useArgs()
@@ -82,7 +83,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const colors = createMemo(() => {
const step = mode() === "light" ? 800 : 200
return dedupeWith(
- themeV2.categorical.map((scale) => scale[step]),
+ theme.categorical.map((scale) => scale[step]),
(first, second) => first.equals(second),
)
})
diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx
index 699653a51a9..a84826f36a4 100644
--- a/packages/tui/src/context/theme.tsx
+++ b/packages/tui/src/context/theme.tsx
@@ -96,13 +96,13 @@ type State = {
}
type ContextName = "elevated" | "overlay"
-type ThemeService = {
- themeV2: ComponentTheme
- contextual(context: ContextName): ThemeService
+type Themes = {
+ current: ComponentTheme
+ contextual(context: ContextName): ComponentTheme
readonly selected: string
all: typeof allThemes
has: typeof hasTheme
- syntax: Accessor
+ currentSyntax: Accessor
mode: Accessor<"dark" | "light">
modes: Accessor
supports(mode: "dark" | "light"): boolean
@@ -308,7 +308,7 @@ const themeContext = createSimpleContext({
const valuesV2 = () => selected().theme
valuesV2()
themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`)
- const themeV2 = createComponentTheme(valuesV2, mode)
+ const current = createComponentTheme(valuesV2, mode)
const contextsV2 = {
elevated: createComponentTheme(() => valuesV2().contexts["@context:elevated"] ?? valuesV2(), mode),
overlay: createComponentTheme(() => valuesV2().contexts["@context:overlay"] ?? valuesV2(), mode),
@@ -316,19 +316,19 @@ const themeContext = createSimpleContext({
createEffect(() => renderer.setBackgroundColor(valuesV2().background.default))
- const syntax = createSyntaxStyleMemo(() => generateSyntax(valuesV2(), mode()))
+ const currentSyntax = createSyntaxStyleMemo(() => generateSyntax(valuesV2(), mode()))
function contextual(context: ContextName) {
- return contextualServices[context]
+ return contextsV2[context]
}
- const service: ThemeService = {
- themeV2,
+ const service: Themes = {
+ current,
+ currentSyntax,
contextual,
get selected() {
return store.active
},
all: allThemes,
has: hasTheme,
- syntax,
mode,
modes,
supports: (requested) => modes().includes(requested),
@@ -355,21 +355,28 @@ const themeContext = createSimpleContext({
return store.ready
},
}
- const contextualServices = {
- elevated: Object.assign(Object.create(service) as ThemeService, { themeV2: contextsV2.elevated }),
- overlay: Object.assign(Object.create(service) as ThemeService, { themeV2: contextsV2.overlay }),
+ return {
+ current,
+ themes: service,
+ get ready() {
+ return service.ready
+ },
}
- return service
},
})
-export const useTheme = themeContext.use
+export function useThemes() {
+ return themeContext.use().themes
+}
+export function useTheme() {
+ return themeContext.use().current
+}
export const ThemeProvider = themeContext.provider
export function ThemeContextProvider(props: ParentProps<{ context: ContextName }>) {
- const theme = useTheme()
+ const themes = useThemes()
return (
-
+
{props.children}
)
diff --git a/packages/tui/src/plugin/context.tsx b/packages/tui/src/plugin/context.tsx
index b4e37e6946b..ad6c394ecff 100644
--- a/packages/tui/src/plugin/context.tsx
+++ b/packages/tui/src/plugin/context.tsx
@@ -23,7 +23,7 @@ import { Keymap } from "../context/keymap"
import { useRoute } from "../context/route"
import { useTuiApp, useTuiLifecycle, useTuiPaths } from "../context/runtime"
import { useLocation } from "../context/location"
-import { useTheme } from "../context/theme"
+import { useTheme, useThemes } from "../context/theme"
import { DialogAlert } from "../ui/dialog-alert"
import { DialogConfirm } from "../ui/dialog-confirm"
import { DialogPrompt } from "../ui/dialog-prompt"
@@ -80,7 +80,8 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
const paths = useTuiPaths()
const location = useLocation()
const theme = useTheme()
- const pluginTheme = createPluginTheme(theme)
+ const themes = useThemes()
+ const pluginTheme = createPluginTheme(theme, themes)
const dialog = useDialog()
const toast = useToast()
const attention = useAttention()
@@ -528,19 +529,20 @@ function isPlugin(value: unknown): value is Plugin.Definition {
)
}
-type PluginTheme = ReturnType["themeV2"] & {
+type PluginTheme = ReturnType & {
contextual(context: "elevated" | "overlay"): PluginTheme
- syntaxStyle(): ReturnType["syntax"]>
+ syntaxStyle(): ReturnType["currentSyntax"]>
}
-export function createPluginTheme(theme: ReturnType): PluginTheme {
- return new Proxy(theme.themeV2 as PluginTheme, {
+export function createPluginTheme(theme: ReturnType, themes: ReturnType): PluginTheme {
+ return new Proxy(theme as PluginTheme, {
get(target, property, receiver) {
- if (property === "contextual")
- return (context: "elevated" | "overlay") => createPluginTheme(theme.contextual(context))
- if (property === "syntaxStyle") return theme.syntax
+ if (property === "contextual") {
+ return (context: "elevated" | "overlay") => createPluginTheme(themes.contextual(context), themes)
+ }
+ if (property === "syntaxStyle") return themes.currentSyntax
if (Reflect.has(target, property)) return Reflect.get(target, property, receiver)
- return Reflect.get(theme, property, theme)
+ return Reflect.get(themes, property, themes)
},
})
}
diff --git a/packages/tui/src/routes/session/composer/index.tsx b/packages/tui/src/routes/session/composer/index.tsx
index 4c53fe9043f..3bc3b96d33c 100644
--- a/packages/tui/src/routes/session/composer/index.tsx
+++ b/packages/tui/src/routes/session/composer/index.tsx
@@ -1,7 +1,7 @@
import { createEffect, createMemo, For, onCleanup, Show, useContext, createContext } from "solid-js"
import { createStore } from "solid-js/store"
import { TextAttributes } from "@opentui/core"
-import { useTheme } from "../../../context/theme"
+import { useThemes } from "../../../context/theme"
import { SplitBorder } from "../../../ui/border"
import { Keymap } from "../../../context/keymap"
import { SubagentsTab } from "./subagents-tab"
@@ -39,7 +39,7 @@ export type ComposerProps = {
}
export function Composer(props: ComposerProps) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [store, setStore] = createStore({
tabs: {} as Record,
@@ -113,8 +113,8 @@ export function Composer(props: ComposerProps) {
1}
fallback={
-
+
{tabList()[0]?.label ?? ""}
}
@@ -136,7 +136,7 @@ export function Composer(props: ComposerProps) {
const isActive = createMemo(() => store.active === t.id)
return (
{t.label}
@@ -146,7 +146,7 @@ export function Composer(props: ComposerProps) {
-
+
esc
@@ -156,19 +156,19 @@ export function Composer(props: ComposerProps) {
{(hint) => (
-
+
{hint.label}{" "}
- {hint.shortcut}
+ {hint.shortcut}
)}
1}>
-
+
tabs{" "}
- ←/→
+ ←/→
diff --git a/packages/tui/src/routes/session/composer/shell-tab.tsx b/packages/tui/src/routes/session/composer/shell-tab.tsx
index b945292de91..6759852bf72 100644
--- a/packages/tui/src/routes/session/composer/shell-tab.tsx
+++ b/packages/tui/src/routes/session/composer/shell-tab.tsx
@@ -12,7 +12,7 @@ export function ShellTab(props: { sessionID: string }) {
const data = useData()
const location = useLocation()
const client = useClient()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const composer = useComposerTab()
const shortcuts = Keymap.useShortcuts()
@@ -98,7 +98,7 @@ export function ShellTab(props: { sessionID: string }) {
return (
(scroll = r)}>
- 0} fallback={ No shell commands}>
+ 0} fallback={ No shell commands}>
{(shell, index) => {
const active = createMemo(() => index() === store.selected)
@@ -108,12 +108,12 @@ export function ShellTab(props: { sessionID: string }) {
paddingLeft={1}
paddingRight={1}
backgroundColor={
- active() ? themeV2.background.action.primary.focused : themeV2.background.action.primary.default
+ active() ? theme.background.action.primary.focused : theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", index())}
>
diff --git a/packages/tui/src/routes/session/composer/subagents-tab.tsx b/packages/tui/src/routes/session/composer/subagents-tab.tsx
index 8a44b9f9ee3..0db07477ec4 100644
--- a/packages/tui/src/routes/session/composer/subagents-tab.tsx
+++ b/packages/tui/src/routes/session/composer/subagents-tab.tsx
@@ -21,7 +21,7 @@ export function SubagentsTab(props: { sessionID: string }) {
const route = useRouteData("session")
const data = useData()
const client = useClient()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const navigate = useRoute().navigate
const composer = useComposerTab()
const shortcuts = Keymap.useShortcuts()
@@ -206,7 +206,7 @@ export function SubagentsTab(props: { sessionID: string }) {
return (
(scroll = r)}>
- 0} fallback={ No subagents}>
+ 0} fallback={ No subagents}>
{(entry, index) => {
const active = createMemo(() => index() === selected())
@@ -221,10 +221,10 @@ export function SubagentsTab(props: { sessionID: string }) {
paddingRight={1}
backgroundColor={
active()
- ? themeV2.background.action.primary.focused
+ ? theme.background.action.primary.focused
: entry.current
- ? themeV2.background.action.primary.selected
- : themeV2.background.action.primary.default
+ ? theme.background.action.primary.selected
+ : theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", index())}
onMouseUp={() => {
@@ -236,10 +236,10 @@ export function SubagentsTab(props: { sessionID: string }) {
-
+
{status()}
diff --git a/packages/tui/src/routes/session/footer.tsx b/packages/tui/src/routes/session/footer.tsx
index 401db265c9e..e58e66f2a08 100644
--- a/packages/tui/src/routes/session/footer.tsx
+++ b/packages/tui/src/routes/session/footer.tsx
@@ -8,7 +8,7 @@ import { useRoute } from "../../context/route"
import { usePermission } from "../../context/permission"
export function Footer() {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const data = useData()
const route = useRoute()
const permission = usePermission()
@@ -54,35 +54,35 @@ export function Footer() {
return (
- {directory()}
+ {directory()}
-
- Get started /connect
+
+ Get started /connect
0}>
-
- △ {permissions().length} Permission
+
+ △ {permissions().length} Permission
{permissions().length > 1 ? "s" : ""}
-
+
- ⊙
+ ⊙
- ⊙
+ ⊙
{mcp()} MCP
- /status
+ /status
diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx
index 5cad287f612..976fca48246 100644
--- a/packages/tui/src/routes/session/form.tsx
+++ b/packages/tui/src/routes/session/form.tsx
@@ -3,7 +3,7 @@ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-j
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"
import open from "open"
-import { useTheme } from "../../context/theme"
+import { useThemes } from "../../context/theme"
import type { FormField, FormValue } from "@opencode-ai/client"
import type { FormWithLocation } from "../../context/data"
import { useClient } from "../../context/client"
@@ -44,7 +44,9 @@ function requestOptions(form: FormWithLocation) {
export function FormPrompt(props: { form: FormWithLocation }) {
const client = useClient()
- const { themeV2, mode: themeMode } = useTheme().contextual("elevated")
+ const themes = useThemes()
+ const theme = themes.contextual("elevated")
+ const themeMode = themes.mode
const renderer = useRenderer()
const dimensions = useTerminalDimensions()
const keymap = Keymap.use()
@@ -624,27 +626,27 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
- {props.form.title}
+ {props.form.title}
- {message()}
+ {message()}
-
+
{confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
0}>
-
+
· {answered()}/{fields().length} completed
@@ -661,10 +663,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
paddingRight={2}
backgroundColor={
isTab()
- ? themeV2.background.formfield.selected
+ ? theme.background.formfield.selected
: tabHover() === index()
- ? themeV2.background.formfield.focused
- : themeV2.background.default
+ ? theme.background.formfield.focused
+ : theme.background.default
}
onMouseOver={() => setTabHover(index())}
onMouseOut={() => setTabHover(null)}
@@ -676,12 +678,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{truncate(formLabel(item), 24)}
@@ -693,10 +695,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
setTabHover("confirm")}
onMouseOut={() => setTabHover(null)}
@@ -705,7 +707,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
selectTabFromMouse()
}}
>
- Confirm
+ Confirm
@@ -714,13 +716,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{(external) => (
- {external().title}
+ {external().title}
- {external().description}
+ {external().description}
{
if (renderer.getSelection()?.getSelectedText()) return
openExternal()
@@ -729,9 +731,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{external().url}
{store.answers[external().key] === true
? "✓ Acknowledged"
@@ -746,7 +746,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
- {answerField()!.description ?? formLabel(answerField()!)}
+ {answerField()!.description ?? formLabel(answerField()!)}
@@ -763,12 +763,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
input() || formDisplayValue(answerField()!, store.answers[answerField()!.key], "(none)")
}
placeholder={placeholder()}
- placeholderColor={themeV2.text.subdued}
+ placeholderColor={theme.text.subdued}
minHeight={1}
maxHeight={6}
- textColor={themeV2.text.default}
- focusedTextColor={themeV2.text.default}
- cursorColor={themeV2.text.default}
+ textColor={theme.text.default}
+ focusedTextColor={theme.text.default}
+ cursorColor={theme.text.default}
/>
@@ -793,39 +793,35 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
{`${i() + 1}.`}
{multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
- {picked() ? " ✓" : ""}
+ {picked() ? " ✓" : ""}
- {row.description}
+ {row.description}
@@ -843,30 +839,28 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
-
+
{`${rows().length + 1}.`}
-
+
{multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
- {customPicked() ? " ✓" : ""}
+ {customPicked() ? " ✓" : ""}
@@ -882,18 +876,18 @@ export function FormPrompt(props: { form: FormWithLocation }) {
}}
initialValue={input()}
placeholder="Type your own answer"
- placeholderColor={themeV2.text.subdued}
+ placeholderColor={theme.text.subdued}
minHeight={1}
maxHeight={6}
- textColor={themeV2.text.default}
- focusedTextColor={themeV2.text.default}
- cursorColor={themeV2.text.default}
+ textColor={theme.text.default}
+ focusedTextColor={theme.text.default}
+ cursorColor={theme.text.default}
/>
- {input()}
+ {input()}
@@ -906,7 +900,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
- Review
+ Review
- {truncate(formLabel(item), 40)}:{" "}
+ {truncate(formLabel(item), 40)}:{" "}
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
@@ -942,15 +936,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
- {truncate(formLabel(item), 40)}:{" "}
+ {truncate(formLabel(item), 40)}:{" "}
{invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
@@ -974,41 +968,41 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
-
- {"⇆"} tab
+
+ {"⇆"} tab
-
- {"↑↓"} select
+
+ {"↑↓"} select
0}>
-
- {"↑↓"} scroll
+
+ {"↑↓"} scroll
{
if (renderer.getSelection()?.getSelectedText()) return
if (confirm()) submit()
if (externalField()) acknowledgeExternal()
}}
>
- enter {actionLabel()}
+ enter {actionLabel()}
-
- c copy
+
+ c copy
-
- esc dismiss
+
+ esc dismiss
- {store.error}
+ {store.error}
diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx
index 13683ff08e8..38149f13481 100644
--- a/packages/tui/src/routes/session/index.tsx
+++ b/packages/tui/src/routes/session/index.tsx
@@ -22,7 +22,7 @@ import { useData } from "../../context/data"
import { SplitBorder } from "../../ui/border"
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
-import { ThemeContextProvider, useTheme } from "../../context/theme"
+import { ThemeContextProvider, useTheme, useThemes } from "../../context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { Prompt, type PromptRef } from "../../component/prompt"
import type {
@@ -133,7 +133,7 @@ export function Session() {
const paths = useTuiPaths()
const configState = useConfig()
const config = configState.data
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const promptRef = usePromptRef()
const session = createMemo(() => data.session.get(route.sessionID))
const messages = () => data.session.message.list(route.sessionID)
@@ -925,8 +925,8 @@ export function Session() {
paddingLeft: 1,
visible: showScrollbar(),
trackOptions: {
- backgroundColor: themeV2.raise(themeV2.background.surface.offset),
- foregroundColor: themeV2.border.default,
+ backgroundColor: theme.raise(theme.background.surface.offset),
+ foregroundColor: theme.border.default,
},
}}
stickyScroll={!navigationMessage()}
@@ -1101,7 +1101,7 @@ function TurnTokenUsage(props: {
message: (messageID: string) => SessionMessageInfo | undefined
}) {
const config = useConfig()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const verbose = () => config.data.debug?.turn_tokens === "verbose"
const steps = createMemo(() => {
let previousCache = props.previousCache
@@ -1141,15 +1141,15 @@ function TurnTokenUsage(props: {
0}>
-
+
◈
-
+
Tokens
-
+
{"Step".padEnd(columns().step + 2)}
{"New".padStart(columns().newTokens)}
{" "}
@@ -1161,7 +1161,7 @@ function TurnTokenUsage(props: {
{(item) => (
-
+
{item.finish.padEnd(columns().step + 2)}
{item.newTokens.toLocaleString().padStart(columns().newTokens)}
@@ -1173,7 +1173,7 @@ function TurnTokenUsage(props: {
-
+
! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
@@ -1186,7 +1186,7 @@ function TurnTokenUsage(props: {
}
function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const nameWidth = () => Math.max(0, ...props.tools.map((tool) => tool.name.length)) + 2
return (
0}>
@@ -1197,13 +1197,13 @@ function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
{tool.name}
{
const current = props.messages.findLast(
@@ -1252,8 +1252,8 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
{(value) => (
-
- Press {value()} to move running work to the background
+
+ Press {value()} to move running work to the background
)}
@@ -1323,7 +1323,8 @@ function SessionReasoningGroupView(props: {
message: (messageID: string) => SessionMessageInfo | undefined
}) {
const ctx = use()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
@@ -1363,13 +1364,13 @@ function SessionReasoningGroupView(props: {
icon={expanded() ? "-" : "+"}
color={
!props.completed
- ? themeV2.text.default
+ ? theme.text.default
: hover() || expanded()
- ? themeV2.text.feedback.warning.default
+ ? theme.text.feedback.warning.default
: RGBA.fromValues(
- themeV2.text.feedback.warning.default.r,
- themeV2.text.feedback.warning.default.g,
- themeV2.text.feedback.warning.default.b,
+ theme.text.feedback.warning.default.r,
+ theme.text.feedback.warning.default.g,
+ theme.text.feedback.warning.default.b,
0.6,
)
}
@@ -1412,7 +1413,7 @@ function SessionReasoningGroupView(props: {
@@ -1444,7 +1445,7 @@ function SessionGroupView(props: {
completed: boolean
message: (messageID: string) => SessionMessageInfo | undefined
}) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const ctx = use()
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
@@ -1480,7 +1481,7 @@ function SessionGroupView(props: {
0}>
ctx
@@ -1526,25 +1527,25 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
customBorderChars={SplitBorder.customBorderChars}
- borderColor={themeV2.text.feedback.error.default}
+ borderColor={theme.text.feedback.error.default}
>
- {errorMessage(props.message.error)}
+ {errorMessage(props.message.error)}
-
+
{Locale.titlecase(props.message.agent)}
- · {model()}
+ · {model()}
- · {Locale.duration(duration())}
+ · {Locale.duration(duration())}
- · interrupted
+ · interrupted
@@ -1554,7 +1555,7 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
const ctx = use()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const text = () => {
if (props.message.type === "agent-switched") return `Switched agent to ${props.message.agent}`
if (props.message.type === "model-switched")
@@ -1563,14 +1564,14 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
}
return (
- {text()}
+ {text()}
)
}
function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
const ctx = use()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const metadata = () => (props.message.type === "synthetic" ? props.message.metadata : undefined)
const source = () => stringValue(metadata()?.source)
const completion = () => source() === "subagent" || source() === "shell"
@@ -1590,15 +1591,15 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
const suffix = () => Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())))
const color = () => {
- if (state() === "error") return themeV2.text.feedback.error.default
- if (state() === "cancelled") return themeV2.text.feedback.warning.default
- return themeV2.text.feedback.info.default
+ if (state() === "error") return theme.text.feedback.error.default
+ if (state() === "cancelled") return theme.text.feedback.warning.default
+ return theme.text.feedback.info.default
}
return (
+
{text()}
}
@@ -1606,7 +1607,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
{heading()}
- {suffix()}
+ {suffix()}
@@ -1614,9 +1615,9 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
}
function SessionSkillMessage(props: { message: Extract }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
return (
-
+
Skill {props.message.name}
)
@@ -1624,14 +1625,15 @@ function SessionSkillMessage(props: { message: Extract }) {
const ctx = use()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const status = () => props.message.status
const cancelled = () => props.message.status === "failed" && props.message.error.type === "aborted"
const text = () =>
props.message.status === "failed" ? (cancelled() ? "" : props.message.error.message) : props.message.summary
const content = createMemo(() => text().trim())
const color = () =>
- status() === "failed" && !cancelled() ? themeV2.text.feedback.error.default : themeV2.text.subdued
+ status() === "failed" && !cancelled() ? theme.text.feedback.error.default : theme.text.subdued
return (
@@ -1663,8 +1665,8 @@ function CompactionMessage(props: { message: Extract
@@ -1673,15 +1675,15 @@ function CompactionMessage(props: { message: Extract
-
+
- ◇
- Compaction queued
+ ◇
+ Compaction queued
-
+
)
}
@@ -1702,7 +1704,7 @@ function RevertMessage(props: {
}>
}) {
const ctx = use()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const route = useRouteData("session")
const client = useClient()
const toast = useToast()
@@ -1727,15 +1729,15 @@ function RevertMessage(props: {
marginTop={1}
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
- borderColor={themeV2.background.default}
+ borderColor={theme.background.default}
>
-
+
{props.count} message{props.count === 1 ? "" : "s"} reverted
0}>
@@ -1743,7 +1745,7 @@ function RevertMessage(props: {
{(file) => (
- {statusLabel(file.status)}
+ {statusLabel(file.status)}
0 ? stringWidth(`+${file.additions}`) + 1 : 0) -
(file.deletions > 0 ? stringWidth(`-${file.deletions}`) + 1 : 0),
)}
- fg={themeV2.text.default}
+ fg={theme.text.default}
/>
0}>
- +{file.additions}
+ +{file.additions}
0}>
- -{file.deletions}
+ -{file.deletions}
)}
-
- {redoKey()} or /redo to restore
+
+ {redoKey()} or /redo to restore
@@ -1775,7 +1777,7 @@ function RevertMessage(props: {
}
function ShellMessage(props: { message: Extract }) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
return (
@@ -1785,13 +1787,13 @@ function ShellMessage(props: { message: Extract
- $ {props.message.command}
+ $ {props.message.command}
- {output()}
+ {output()}
)
@@ -1802,7 +1804,9 @@ function UserMessage(props: { message: SessionMessageUser }) {
const data = useData()
const local = useLocal()
const files = createMemo(() => props.message.files ?? [])
- const { themeV2, mode } = useTheme().contextual("elevated")
+ const themes = useThemes()
+ const theme = themes.contextual("elevated")
+ const mode = themes.mode
const [hover, setHover] = createSignal(false)
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
const queued = createMemo(
@@ -1816,7 +1820,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
- {props.message.text}
+ {props.message.text}
{(file) => {
const label = file.mime === "application/x-directory" ? "dir" : "file"
return (
-
+
{` ${label} `}
-
+
{" "}
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
@@ -1878,7 +1882,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
function AssistantMessage(props: { message: SessionMessageAssistant; last: boolean }) {
const ctx = use()
const local = useLocal()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const model = createMemo(
() =>
ctx
@@ -1964,11 +1968,11 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
customBorderChars={SplitBorder.customBorderChars}
- borderColor={themeV2.text.feedback.error.default}
+ borderColor={theme.text.feedback.error.default}
>
- {errorMessage(props.message.error)}
+ {errorMessage(props.message.error)}
@@ -1976,12 +1980,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
-
+
{Locale.titlecase(props.message.agent)}
- · {model()}
+ · {model()}
- · {Locale.duration(duration())}
+ · {Locale.duration(duration())}
@@ -1992,12 +1996,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
}
function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
return (
{(retry) => (
-
+
Retry attempt {retry().attempt} scheduled: {retry().error.message} [{retry().error.type}]
@@ -2007,7 +2011,7 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
}
function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; active: boolean }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const pathFormatter = usePathFormatter()
const label = (part: SessionMessageAssistantTool) => {
const input = typeof part.state.input === "string" ? {} : part.state.input
@@ -2020,7 +2024,7 @@ function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; activ
{(part, index) => (
-
+
{index() === props.parts.length - 1 ? "└" : "├"} {label(part)}
@@ -2047,7 +2051,8 @@ function ReasoningPart(props: {
part: SessionMessageAssistantReasoning
message: SessionMessageAssistant
}) {
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const ctx = use()
// Collapsed by default in hide mode: a single line throughout, so the
// layout never shifts. Click to open the full markdown block, click to close.
@@ -2075,7 +2080,7 @@ function ReasoningPart(props: {
@@ -2093,7 +2098,7 @@ function ReasoningPart(props: {
@@ -2125,16 +2130,16 @@ function ReasoningHeader(props: {
title: string | null
duration?: string
}) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const fg = () =>
props.open
? RGBA.fromValues(
- themeV2.text.feedback.warning.default.r,
- themeV2.text.feedback.warning.default.g,
- themeV2.text.feedback.warning.default.b,
+ theme.text.feedback.warning.default.r,
+ theme.text.feedback.warning.default.g,
+ theme.text.feedback.warning.default.b,
0.6,
)
- : themeV2.text.feedback.warning.default
+ : theme.text.feedback.warning.default
return (
@@ -2169,7 +2174,8 @@ function ReasoningHeader(props: {
function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
const ctx = use()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
return (
@@ -2180,8 +2186,8 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
content={props.part.text.trim()}
tableOptions={{ style: "grid" }}
conceal={ctx.markdownMode() === "rendered"}
- fg={themeV2.markdown.text}
- bg={themeV2.background.default}
+ fg={theme.markdown.text}
+ bg={theme.background.default}
/>
@@ -2270,7 +2276,8 @@ type ToolProps = {
part: SessionMessageAssistantTool
}
function GenericTool(props: ToolProps) {
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const output = createMemo(() => props.output?.trim() ?? "")
const args = createMemo(() => JSON.stringify(props.input, null, 2))
const [expanded, setExpanded] = createSignal(false)
@@ -2288,7 +2295,7 @@ function GenericTool(props: ToolProps) {
0}>
- Input
+ Input
@@ -2306,13 +2313,13 @@ function GenericTool(props: ToolProps) {
{(value) => (
-
+
{" "}
Output{" "}
-
+
{value()}
@@ -2349,7 +2356,7 @@ function InlineTool(props: {
part: SessionMessageAssistantTool
onClick?: () => void
}) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
const [errorExpanded, setErrorExpanded] = createSignal(false)
@@ -2369,10 +2376,10 @@ function InlineTool(props: {
const clickable = createMemo(() => Boolean(props.onClick || failed()))
const fg = createMemo(() => {
if (props.color) return props.color
- if (permission()) return themeV2.text.feedback.warning.default
- if (failed()) return themeV2.text.feedback.error.default
- if (hover() && props.onClick) return themeV2.text.default
- return themeV2.text.subdued
+ if (permission()) return theme.text.feedback.warning.default
+ if (failed()) return theme.text.feedback.error.default
+ if (hover() && props.onClick) return theme.text.default
+ return theme.text.subdued
})
return (
@@ -2380,7 +2387,7 @@ function InlineTool(props: {
icon={props.icon}
iconColor={props.iconColor}
color={fg()}
- errorColor={themeV2.text.feedback.error.default}
+ errorColor={theme.text.feedback.error.default}
failed={failed()}
denied={Boolean(denied())}
error={error()}
@@ -2502,9 +2509,9 @@ function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.El
}
function StatusBadge(props: { children: string }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
return (
-
+
{" "}
{props.children}{" "}
@@ -2524,13 +2531,13 @@ function BlockTool(props: BlockToolProps) {
const parentTheme = useTheme()
return (
-
+
)
}
function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const ctx = use()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
@@ -2543,7 +2550,7 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
paddingBottom={1}
paddingLeft={2}
gap={1}
- backgroundColor={hover() ? themeV2.raise(themeV2.background.default) : themeV2.background.default}
+ backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default}
customBorderChars={SplitBorder.customBorderChars}
borderColor={props.borderColor}
onMouseOver={() => props.onClick && setHover(true)}
@@ -2561,12 +2568,12 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
+
{title()}
}
>
-
+
{title().replace(/^# /, "")}
@@ -2579,26 +2586,26 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
+
{path().label}
}
>
-
+
{path().label.replace(/^# /, "")}
)}
{props.children}
- {error()}
+ {error()}
)
@@ -2607,13 +2614,13 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
const SHELL_DISPLAY_LIMIT = 1024 * 1024
function Shell(props: ToolProps) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const ctx = use()
const client = useClient()
const data = useData()
const pathFormatter = usePathFormatter()
const permission = useToolPermission(() => props.part)
- const color = createMemo(() => (permission() ? themeV2.text.feedback.warning.default : themeV2.text.default))
+ const color = createMemo(() => (permission() ? theme.text.feedback.warning.default : theme.text.default))
const shellID = createMemo(() => stringValue(props.metadata.shellID))
const background = createMemo(() => Boolean(shellID()) && props.part.state.status !== "running")
const backgroundRunning = createMemo(() => {
@@ -2723,7 +2730,7 @@ function Shell(props: ToolProps) {
isRunning() || props.part.state.status === "streaming" ? (
Writing command...
) : (
- Writing command...
+ Writing command...
)
}
>
@@ -2731,14 +2738,14 @@ function Shell(props: ToolProps) {
when={isRunning()}
fallback={
- {limited().slice(0, input().length)}
- {limited().slice(input().length)}
+ {limited().slice(0, input().length)}
+ {limited().slice(input().length)}
}
>
- {limited().slice(0, input().length)}
- {limited().slice(input().length)}
+ {limited().slice(0, input().length)}
+ {limited().slice(input().length)}
@@ -2751,7 +2758,8 @@ function Shell(props: ToolProps) {
}
function Write(props: ToolProps) {
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const pathFormatter = usePathFormatter()
const code = createMemo(() => {
return stringValue(props.input.content) ?? ""
@@ -2764,10 +2772,10 @@ function Write(props: ToolProps) {
path={{ label: "# Wrote", value: pathFormatter.format(stringValue(props.input.path)) }}
part={props.part}
>
-
+
props.part.state.status === "running")
const loaded = createMemo(() => {
@@ -2822,7 +2830,7 @@ function Read(props: ToolProps) {
{(filepath) => (
-
+
↳ Loaded {pathFormatter.format(filepath)}
@@ -2920,7 +2928,7 @@ function executeCalls(value: unknown): ExecuteCall[] {
// The `execute` tool streams child tool calls through metadata, not a child session like Task.
function Execute(props: ToolProps) {
const ctx = use()
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const isLoading = createMemo(() => props.part.state.status === "streaming" || props.part.state.status === "running")
const calls = createMemo(() => executeCalls(props.metadata.toolCalls))
const output = createMemo(() => stripAnsi(props.output?.trim() ?? ""))
@@ -2940,7 +2948,7 @@ function Execute(props: ToolProps) {
<>
{(line, index) => (
-
+
{index() === 0 ? "↳ " : " "}
{line}
@@ -2966,7 +2974,8 @@ function Execute(props: ToolProps) {
function Edit(props: ToolProps) {
const ctx = use()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const pathFormatter = usePathFormatter()
const view = createMemo(() => {
@@ -2994,16 +3003,16 @@ function Edit(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
- fg={themeV2.text.default}
- addedBg={themeV2.diff.background.added}
- removedBg={themeV2.diff.background.removed}
- contextBg={themeV2.diff.background.context}
- addedSignColor={themeV2.diff.highlight.added}
- removedSignColor={themeV2.diff.highlight.removed}
- lineNumberFg={themeV2.diff.lineNumber.text}
- lineNumberBg={themeV2.diff.background.context}
- addedLineNumberBg={themeV2.diff.lineNumber.background.added}
- removedLineNumberBg={themeV2.diff.lineNumber.background.removed}
+ fg={theme.text.default}
+ addedBg={theme.diff.background.added}
+ removedBg={theme.diff.background.removed}
+ contextBg={theme.diff.background.context}
+ addedSignColor={theme.diff.highlight.added}
+ removedSignColor={theme.diff.highlight.removed}
+ lineNumberFg={theme.diff.lineNumber.text}
+ lineNumberBg={theme.diff.background.context}
+ addedLineNumberBg={theme.diff.lineNumber.background.added}
+ removedLineNumberBg={theme.diff.lineNumber.background.removed}
/>
@@ -3028,7 +3037,8 @@ function Edit(props: ToolProps) {
function ApplyPatch(props: ToolProps) {
const ctx = use()
- const { themeV2, syntax } = useTheme()
+ const theme = useTheme()
+ const { currentSyntax: syntax } = useThemes()
const pathFormatter = usePathFormatter()
const files = createMemo(() => parseApplyPatchFiles(props.metadata.files))
const targets = createMemo(() => {
@@ -3068,7 +3078,7 @@ function ApplyPatch(props: ToolProps) {
+
-{file.deletions} line{file.deletions !== 1 ? "s" : ""}
}
@@ -3082,16 +3092,16 @@ function ApplyPatch(props: ToolProps) {
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
- fg={themeV2.text.default}
- addedBg={themeV2.diff.background.added}
- removedBg={themeV2.diff.background.removed}
- contextBg={themeV2.diff.background.context}
- addedSignColor={themeV2.diff.highlight.added}
- removedSignColor={themeV2.diff.highlight.removed}
- lineNumberFg={themeV2.diff.lineNumber.text}
- lineNumberBg={themeV2.diff.background.context}
- addedLineNumberBg={themeV2.diff.lineNumber.background.added}
- removedLineNumberBg={themeV2.diff.lineNumber.background.removed}
+ fg={theme.text.default}
+ addedBg={theme.diff.background.added}
+ removedBg={theme.diff.background.removed}
+ contextBg={theme.diff.background.context}
+ addedSignColor={theme.diff.highlight.added}
+ removedSignColor={theme.diff.highlight.removed}
+ lineNumberFg={theme.diff.lineNumber.text}
+ lineNumberBg={theme.diff.background.context}
+ addedLineNumberBg={theme.diff.lineNumber.background.added}
+ removedLineNumberBg={theme.diff.lineNumber.background.removed}
/>
@@ -3114,7 +3124,7 @@ function ApplyPatch(props: ToolProps) {
)}
@@ -3143,7 +3153,7 @@ function ApplyPatch(props: ToolProps) {
}
function Question(props: ToolProps) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const questions = createMemo(() => parseQuestions(props.input.questions))
const answers = createMemo(() => parseQuestionAnswers(props.metadata.answers))
const count = createMemo(() => questions().length)
@@ -3161,8 +3171,8 @@ function Question(props: ToolProps) {
{(q, i) => (
- {q.question}
- {format(answers()?.[i()])}
+ {q.question}
+ {format(answers()?.[i()])}
)}
@@ -3188,7 +3198,7 @@ function Skill(props: ToolProps) {
}
function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
- const { themeV2 } = useTheme()
+ const theme = useTheme()
const terminalEnvironment = useTuiTerminalEnvironment()
const errors = createMemo(() => {
const normalized = normalizePath(
@@ -3203,7 +3213,7 @@ function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
{(diagnostic) => (
-
+
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
)}
diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx
index e70c2917f02..663c4edf01f 100644
--- a/packages/tui/src/routes/session/permission.tsx
+++ b/packages/tui/src/routes/session/permission.tsx
@@ -2,7 +2,7 @@ import { createStore } from "solid-js/store"
import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core"
-import { useTheme } from "../../context/theme"
+import { useTheme, useThemes } from "../../context/theme"
import type { PermissionRequest } from "@opencode-ai/client"
import { useClient } from "../../context/client"
import { SplitBorder } from "../../ui/border"
@@ -18,9 +18,9 @@ import { SimulationSemantics } from "../../simulation/semantics"
type PermissionStage = "permission" | "always" | "reject"
function EditBody(props: { file?: string; diff?: string; patch?: string }) {
- const themeState = useTheme()
- const themeV2 = themeState.themeV2
- const syntax = themeState.syntax
+ const theme = useTheme()
+ const themes = useThemes()
+ const syntax = themes.currentSyntax
const config = useConfig().data
const dimensions = useTerminalDimensions()
@@ -45,8 +45,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
- backgroundColor: themeV2.background.default,
- foregroundColor: themeV2.scrollbar.default,
+ backgroundColor: theme.background.default,
+ foregroundColor: theme.scrollbar.default,
},
}}
>
@@ -58,16 +58,16 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
showLineNumbers={true}
width="100%"
wrapMode="word"
- fg={themeV2.text.default}
- addedBg={themeV2.diff.background.added}
- removedBg={themeV2.diff.background.removed}
- contextBg={themeV2.diff.background.context}
- addedSignColor={themeV2.diff.highlight.added}
- removedSignColor={themeV2.diff.highlight.removed}
- lineNumberFg={themeV2.diff.lineNumber.text}
- lineNumberBg={themeV2.diff.background.context}
- addedLineNumberBg={themeV2.diff.lineNumber.background.added}
- removedLineNumberBg={themeV2.diff.lineNumber.background.removed}
+ fg={theme.text.default}
+ addedBg={theme.diff.background.added}
+ removedBg={theme.diff.background.removed}
+ contextBg={theme.diff.background.context}
+ addedSignColor={theme.diff.highlight.added}
+ removedSignColor={theme.diff.highlight.removed}
+ lineNumberFg={theme.diff.lineNumber.text}
+ lineNumberBg={theme.diff.background.context}
+ addedLineNumberBg={theme.diff.lineNumber.background.added}
+ removedLineNumberBg={theme.diff.lineNumber.background.removed}
/>
@@ -76,7 +76,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
when={props.patch}
fallback={
- No diff provided
+ No diff provided
}
>
@@ -86,8 +86,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
- backgroundColor: themeV2.background.default,
- foregroundColor: themeV2.scrollbar.default,
+ backgroundColor: theme.background.default,
+ foregroundColor: theme.scrollbar.default,
},
}}
>
@@ -97,7 +97,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
streaming={true}
syntaxStyle={syntax()}
content={patch()}
- fg={themeV2.text.subdued}
+ fg={theme.text.subdued}
/>
)}
@@ -128,7 +128,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
return { input: undefined, metadata: undefined }
})
- const { themeV2 } = useTheme()
+ const theme = useTheme()
return (
@@ -140,7 +140,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
body={
- {(line, index) => {line}}
+ {(line, index) => {line}}
}
@@ -192,9 +192,9 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
) : props.request.action === "external_directory" ? (
0}>
- Patterns
+ Patterns
- {(line) => {line}}
+ {(line) => {line}}
@@ -207,8 +207,8 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
props.request.action === "shell" ||
props.request.action === "subagent" ||
props.request.action === "task"
- ? themeV2.text.default
- : themeV2.text.subdued
+ ? theme.text.default
+ : theme.text.subdued
}
>
{line}
@@ -221,15 +221,15 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
const header = () => (
- {"△"}
- Permission required
+ {"△"}
+ Permission required
-
+
{current.icon}
- {current.title}
+ {current.title}
@@ -297,7 +297,7 @@ function RejectPrompt(props: {
onCancel: () => void
}) {
let input: TextareaRenderable
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
Keymap.createLayer(() => ({
@@ -329,18 +329,18 @@ function RejectPrompt(props: {
role: "dialog",
label: `Reject permission: ${props.action}`,
}))}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
border={["left"]}
- borderColor={themeV2.text.feedback.error.default}
+ borderColor={theme.text.feedback.error.default}
customBorderChars={SplitBorder.customBorderChars}
>
- {"△"}
- Reject permission
+ {"△"}
+ Reject permission
- Tell OpenCode what to do differently
+ Tell OpenCode what to do differently
props.onConfirm(input.plainText)}
>
-
- enter confirm
+
+ enter confirm
-
- esc cancel
+
+ esc cancel
@@ -429,7 +429,7 @@ function Prompt>(props: {
fullscreen?: boolean
onSelect: (option: keyof T) => void
}) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const dimensions = useTerminalDimensions()
const keys = Object.keys(props.options) as (keyof T)[]
const [store, setStore] = createStore({
@@ -534,9 +534,9 @@ function Prompt>(props: {
label: props.semanticLabel ?? props.title,
expanded: store.expanded,
}))}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
border={["left"]}
- borderColor={themeV2.background.action.primary.focused}
+ borderColor={theme.background.action.primary.focused}
customBorderChars={SplitBorder.customBorderChars}
{...(store.expanded
? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
@@ -554,8 +554,8 @@ function Prompt>(props: {
when={props.header}
fallback={
- {"△"}
- {props.title}
+ {"△"}
+ {props.title}
}
>
@@ -573,7 +573,7 @@ function Prompt>(props: {
paddingLeft={2}
paddingRight={3}
paddingBottom={1}
- backgroundColor={themeV2.raise(themeV2.background.default)}
+ backgroundColor={theme.raise(theme.background.default)}
justifyContent={narrow() ? "flex-start" : "space-between"}
alignItems={narrow() ? "flex-start" : "center"}
>
@@ -604,8 +604,8 @@ function Prompt>(props: {
paddingRight={1}
backgroundColor={
option === store.selected
- ? themeV2.background.action.primary.focused
- : themeV2.background.action.primary.default
+ ? theme.background.action.primary.focused
+ : theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", option)}
onMouseUp={() => {
@@ -614,11 +614,7 @@ function Prompt>(props: {
}}
>
{props.options[option]}
@@ -628,15 +624,15 @@ function Prompt>(props: {
-
- {shortcuts.get("permission.prompt.fullscreen")} {hint()}
+
+ {shortcuts.get("permission.prompt.fullscreen")} {hint()}
-
- {"⇆"} select
+
+ {"⇆"} select
-
- enter confirm
+
+ enter confirm
diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx
index 9cdd57c242c..338a8c1ea22 100644
--- a/packages/tui/src/routes/session/sidebar.tsx
+++ b/packages/tui/src/routes/session/sidebar.tsx
@@ -1,6 +1,6 @@
import { useData } from "../../context/data"
import { createMemo, Show } from "solid-js"
-import { useTheme } from "../../context/theme"
+import { useThemes } from "../../context/theme"
import { useConfig } from "../../config"
import { usePluginRuntime } from "../../plugin/runtime"
import { PluginSlot } from "../../plugin/context"
@@ -10,7 +10,7 @@ import { getScrollAcceleration } from "../../util/scroll"
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const pluginRuntime = usePluginRuntime()
const data = useData()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const config = useConfig().data
const session = createMemo(() => data.session.get(props.sessionID))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
@@ -18,7 +18,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
return (
@@ -45,11 +45,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
title={session()!.title}
>
-
+
{session()!.title}
- {session()!.location.workspaceID}
+ {session()!.location.workspaceID}
diff --git a/packages/tui/src/routes/session/subagent-footer.tsx b/packages/tui/src/routes/session/subagent-footer.tsx
index f4654f200e8..a0ba4971f49 100644
--- a/packages/tui/src/routes/session/subagent-footer.tsx
+++ b/packages/tui/src/routes/session/subagent-footer.tsx
@@ -1,7 +1,7 @@
import { createMemo, createSignal, Show } from "solid-js"
import { useRouteData } from "../../context/route"
import { useData } from "../../context/data"
-import { useTheme } from "../../context/theme"
+import { useThemes } from "../../context/theme"
import { SplitBorder } from "../../ui/border"
import { Locale } from "../../util/locale"
import { useTerminalDimensions } from "@opentui/solid"
@@ -42,7 +42,7 @@ export function SubagentFooter() {
}
})
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const keymap = Keymap.use()
const shortcuts = Keymap.useShortcuts()
const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
@@ -57,18 +57,18 @@ export function SubagentFooter() {
paddingRight={1}
{...SplitBorder}
border={["left"]}
- borderColor={themeV2.border.default}
+ borderColor={theme.border.default}
flexShrink={0}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
>
-
+
{subagentInfo()}
{(item) => (
-
+
{[item().context, item().cost].filter(Boolean).join(" · ")}
)}
@@ -80,35 +80,31 @@ export function SubagentFooter() {
onMouseOut={() => setHover(null)}
onMouseUp={() => keymap.dispatch("session.parent")}
backgroundColor={
- hover() === "parent" ? themeV2.background.action.primary.hovered : themeV2.background.default
+ hover() === "parent" ? theme.background.action.primary.hovered : theme.background.default
}
>
-
- Parent {shortcuts.get("session.parent")}
+
+ Parent {shortcuts.get("session.parent")}
setHover("prev")}
onMouseOut={() => setHover(null)}
onMouseUp={() => keymap.dispatch("session.child.previous")}
- backgroundColor={
- hover() === "prev" ? themeV2.background.action.primary.hovered : themeV2.background.default
- }
+ backgroundColor={hover() === "prev" ? theme.background.action.primary.hovered : theme.background.default}
>
-
- Prev {shortcuts.get("session.child.previous")}
+
+ Prev {shortcuts.get("session.child.previous")}
setHover("next")}
onMouseOut={() => setHover(null)}
onMouseUp={() => keymap.dispatch("session.child.next")}
- backgroundColor={
- hover() === "next" ? themeV2.background.action.primary.hovered : themeV2.background.default
- }
+ backgroundColor={hover() === "next" ? theme.background.action.primary.hovered : theme.background.default}
>
-
- Next {shortcuts.get("session.child.next")}
+
+ Next {shortcuts.get("session.child.next")}
diff --git a/packages/tui/src/ui/dialog-alert.tsx b/packages/tui/src/ui/dialog-alert.tsx
index 87d3d645acc..f7c231a07f2 100644
--- a/packages/tui/src/ui/dialog-alert.tsx
+++ b/packages/tui/src/ui/dialog-alert.tsx
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
export type DialogAlertProps = {
@@ -11,7 +11,7 @@ export type DialogAlertProps = {
export function DialogAlert(props: DialogAlertProps) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
Keymap.createLayer(() => ({
mode: "modal",
@@ -30,27 +30,27 @@ export function DialogAlert(props: DialogAlertProps) {
return (
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc
- {props.message}
+ {props.message}
{
props.onConfirm?.()
dialog.clear()
}}
>
- ok
+ ok
diff --git a/packages/tui/src/ui/dialog-confirm.tsx b/packages/tui/src/ui/dialog-confirm.tsx
index 1541c4c992f..5adbb0333e2 100644
--- a/packages/tui/src/ui/dialog-confirm.tsx
+++ b/packages/tui/src/ui/dialog-confirm.tsx
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { For } from "solid-js"
@@ -21,7 +21,7 @@ export type DialogConfirmResult = boolean | undefined
export function DialogConfirm(props: DialogConfirmProps) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const [store, setStore] = createStore({
active: "confirm" as "confirm" | "cancel",
})
@@ -60,15 +60,15 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc
- {props.message}
+ {props.message}
@@ -76,14 +76,14 @@ export function DialogConfirm(props: DialogConfirmProps) {
{
if (key === "confirm") props.onConfirm?.()
if (key === "cancel") props.onCancel?.()
dialog.clear()
}}
>
-
+
{Locale.titlecase(props.label?.[key] ?? key)}
diff --git a/packages/tui/src/ui/dialog-export-options.tsx b/packages/tui/src/ui/dialog-export-options.tsx
index 13d67d58086..dc9049a525c 100644
--- a/packages/tui/src/ui/dialog-export-options.tsx
+++ b/packages/tui/src/ui/dialog-export-options.tsx
@@ -1,6 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { For, Show } from "solid-js"
@@ -17,8 +17,8 @@ type Active = ExportFormat | "thinking" | "copy" | "export"
export function DialogExportOptions(props: DialogExportOptionsProps) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
- const { themeV2: overlayTheme } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("elevated")
+ const overlayTheme = useThemes().contextual("overlay")
const [store, setStore] = createStore({
format: "markdown" as ExportFormat,
thinking: props.defaultThinking,
@@ -73,15 +73,15 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
return (
-
+
Export session
- dialog.clear()}>
+ dialog.clear()}>
esc
- Export as:
+ Export as:
{(format) => (
@@ -90,20 +90,20 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
paddingRight={1}
backgroundColor={
store.active === format
- ? themeV2.background.formfield.focused
+ ? theme.background.formfield.focused
: store.format === format
- ? themeV2.background.formfield.selected
- : themeV2.background.formfield.default
+ ? theme.background.formfield.selected
+ : theme.background.formfield.default
}
onMouseUp={() => selectFormat(format)}
>
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
@@ -119,10 +119,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
gap={1}
backgroundColor={
store.active === "thinking"
- ? themeV2.background.formfield.focused
+ ? theme.background.formfield.focused
: store.thinking
- ? themeV2.background.formfield.selected
- : themeV2.background.formfield.default
+ ? theme.background.formfield.selected
+ : theme.background.formfield.default
}
onMouseUp={() => {
setStore("active", "thinking")
@@ -132,10 +132,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
{store.thinking ? "[x]" : "[ ]"}
@@ -143,10 +143,10 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
Include thinking
@@ -167,14 +167,12 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
paddingRight={4}
backgroundColor={
store.active === "export"
- ? themeV2.background.action.primary.focused
- : themeV2.background.action.primary.default
+ ? theme.background.action.primary.focused
+ : theme.background.action.primary.default
}
onMouseUp={() => confirm("export")}
>
-
+
Export
diff --git a/packages/tui/src/ui/dialog-export-result.tsx b/packages/tui/src/ui/dialog-export-result.tsx
index 6013b6580f3..c43b7ef2e13 100644
--- a/packages/tui/src/ui/dialog-export-result.tsx
+++ b/packages/tui/src/ui/dialog-export-result.tsx
@@ -1,11 +1,11 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
export function DialogExportResult(props: { path: string; onClose?: () => void }) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const close = () => {
props.onClose?.()
@@ -27,24 +27,24 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
return (
-
+
Session exported
-
+
esc
- {props.path}
+ {props.path}
- Close
+ Close
diff --git a/packages/tui/src/ui/dialog-help.tsx b/packages/tui/src/ui/dialog-help.tsx
index 20b0c151e0e..9217d71f6a8 100644
--- a/packages/tui/src/ui/dialog-help.tsx
+++ b/packages/tui/src/ui/dialog-help.tsx
@@ -1,11 +1,11 @@
import { TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog } from "./dialog"
export function DialogHelp() {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
Keymap.createLayer(() => ({
@@ -19,15 +19,15 @@ export function DialogHelp() {
return (
-
+
Help
- dialog.clear()}>
+ dialog.clear()}>
esc/enter
-
+
Press {shortcuts.get("command.palette.show")} to see all available actions and commands in any context.
@@ -35,10 +35,10 @@ export function DialogHelp() {
dialog.clear()}
>
- ok
+ ok
diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx
index 54d5e8b995a..acb1700d26e 100644
--- a/packages/tui/src/ui/dialog-prompt.tsx
+++ b/packages/tui/src/ui/dialog-prompt.tsx
@@ -1,6 +1,6 @@
import { TextareaRenderable, TextAttributes } from "@opentui/core"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
import { Spinner } from "../component/spinner"
@@ -18,7 +18,7 @@ export type DialogPromptProps = {
export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const shortcuts = Keymap.useShortcuts()
const [textareaTarget, setTextareaTarget] = createSignal()
let textarea: TextareaRenderable
@@ -74,10 +74,10 @@ export function DialogPrompt(props: DialogPromptProps) {
return (
-
+
{props.title}
- dialog.clear()}>
+ dialog.clear()}>
esc
@@ -91,20 +91,20 @@ export function DialogPrompt(props: DialogPromptProps) {
}}
initialValue={props.value}
placeholder={props.placeholder ?? "Enter text"}
- placeholderColor={themeV2.text.subdued}
- textColor={props.busy ? themeV2.text.formfield.disabled : themeV2.text.formfield.default}
- focusedTextColor={props.busy ? themeV2.text.formfield.disabled : themeV2.text.formfield.default}
- cursorColor={props.busy ? themeV2.background.formfield.disabled : themeV2.text.default}
+ placeholderColor={theme.text.subdued}
+ textColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.default}
+ focusedTextColor={props.busy ? theme.text.formfield.disabled : theme.text.formfield.default}
+ cursorColor={props.busy ? theme.background.formfield.disabled : theme.text.default}
/>
- {props.busyText ?? "Working..."}
+ {props.busyText ?? "Working..."}
- processing...}>
+ processing...}>
-
- {shortcuts.get("dialog.prompt.submit")} submit
+
+ {shortcuts.get("dialog.prompt.submit")} submit
diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx
index be97539db59..7b2d27ffe09 100644
--- a/packages/tui/src/ui/dialog-select.tsx
+++ b/packages/tui/src/ui/dialog-select.tsx
@@ -1,6 +1,6 @@
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
import { Keymap, type KeymapCommand } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { entries, filter, flatMap, groupBy, pipe } from "remeda"
import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
@@ -95,7 +95,9 @@ export function DialogSelect(props: DialogSelectProps) {
type VisibleAction = (Action & { label: string }) | FooterHint
const dialog = useDialog()
- const { themeV2, mode } = useTheme().contextual("elevated")
+ const themes = useThemes()
+ const theme = themes.contextual("elevated")
+ const mode = themes.mode
const config = useConfig().data
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
@@ -522,10 +524,10 @@ export function DialogSelect(props: DialogSelectProps) {
if (!isActionItem(action.item))
return (
-
+
{action.item.title}{" "}
- {action.item.label}
+ {action.item.label}
)
const item = action.item
@@ -534,16 +536,16 @@ export function DialogSelect(props: DialogSelectProps) {
return (
trigger(item)}
>
@@ -552,10 +554,10 @@ export function DialogSelect(props: DialogSelectProps) {
{" " + item.label}
@@ -569,11 +571,11 @@ export function DialogSelect(props: DialogSelectProps) {
{props.titleView ?? (
-
+
{props.title}
)}
- dialog.clear()}>
+ dialog.clear()}>
esc
@@ -587,9 +589,9 @@ export function DialogSelect(props: DialogSelectProps) {
props.onFilter?.(e)
})
}}
- focusedBackgroundColor={themeV2.background.formfield.focused}
- cursorColor={themeV2.text.formfield.focused}
- focusedTextColor={themeV2.text.formfield.focused}
+ focusedBackgroundColor={theme.background.formfield.focused}
+ cursorColor={theme.text.formfield.focused}
+ focusedTextColor={theme.text.formfield.focused}
ref={(r) => {
input = r
input.traits = { status: "FILTER" }
@@ -600,7 +602,7 @@ export function DialogSelect(props: DialogSelectProps) {
}, 1)
}}
placeholder={props.placeholder ?? "Search"}
- placeholderColor={themeV2.text.subdued}
+ placeholderColor={theme.text.subdued}
/>
@@ -614,14 +616,14 @@ export function DialogSelect(props: DialogSelectProps) {
fallback={
props.emptyView ?? (
- No items available
+ No items available
)
}
>
{props.noMatchView ?? (
- No results found
+ No results found
)}
@@ -643,10 +645,7 @@ export function DialogSelect(props: DialogSelectProps) {
+
{category}
}
@@ -695,8 +694,8 @@ export function DialogSelect(props: DialogSelectProps) {
backgroundColor={
active()
? actionFocused()
- ? themeV2.background.surface.overlay
- : (option.bg ?? themeV2.background.action.primary.focused)
+ ? theme.background.surface.overlay
+ : (option.bg ?? theme.background.action.primary.focused)
: RGBA.fromInts(0, 0, 0, 0)
}
>
@@ -725,7 +724,7 @@ export function DialogSelect(props: DialogSelectProps) {
{(detail) => (
{option.detailsWrap
@@ -774,12 +773,12 @@ function Option(props: {
activeColor?: RGBA
onMouseOver?: () => void
}) {
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const text = createMemo(() => {
- if (props.active && !props.muted) return props.activeColor ?? themeV2.text.action.primary.focused
- if (props.muted && (props.active || props.current)) return themeV2.text.subdued
- if (props.current) return themeV2.text.formfield.selected
- return themeV2.text.default
+ if (props.active && !props.muted) return props.activeColor ?? theme.text.action.primary.focused
+ if (props.muted && (props.active || props.current)) return theme.text.subdued
+ if (props.current) return theme.text.formfield.selected
+ return theme.text.default
})
return (
@@ -809,14 +808,14 @@ function Option(props: {
? Locale.truncateLeft(props.title, props.titleWidth ?? 61)
: Locale.truncate(props.title, props.titleWidth ?? 61))}
-
+
{" " + props.description}
- {props.footer}
+ {props.footer}
>
diff --git a/packages/tui/src/ui/dialog.tsx b/packages/tui/src/ui/dialog.tsx
index 8ab8a2269aa..b0d885d50c3 100644
--- a/packages/tui/src/ui/dialog.tsx
+++ b/packages/tui/src/ui/dialog.tsx
@@ -1,7 +1,7 @@
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
import { batch, createContext, createEffect, onCleanup, Show, useContext, type JSX, type ParentProps } from "solid-js"
import { Keymap } from "../context/keymap"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { MouseButton, Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useToast } from "./toast"
@@ -16,7 +16,7 @@ export function Dialog(
}>,
) {
const dimensions = useTerminalDimensions()
- const { themeV2 } = useTheme().contextual("elevated")
+ const theme = useThemes().contextual("elevated")
const renderer = useRenderer()
let dismiss = false
@@ -59,7 +59,7 @@ export function Dialog(
}}
width={width()}
maxWidth={dimensions().width - 2}
- backgroundColor={themeV2.background.default}
+ backgroundColor={theme.background.default}
paddingTop={1}
>
{props.children}
diff --git a/packages/tui/src/ui/toast.tsx b/packages/tui/src/ui/toast.tsx
index 48e499cfc27..821e6ae12bd 100644
--- a/packages/tui/src/ui/toast.tsx
+++ b/packages/tui/src/ui/toast.tsx
@@ -1,6 +1,6 @@
import { createContext, useContext, type ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
-import { useTheme } from "../context/theme"
+import { useThemes } from "../context/theme"
import { useTerminalDimensions } from "@opentui/solid"
import { SplitBorder } from "./border"
import { TextAttributes } from "@opentui/core"
@@ -14,7 +14,7 @@ type ToastInput = Omit & { duration?: number }
export function Toast() {
const toast = useToast()
- const { themeV2 } = useTheme().contextual("overlay")
+ const theme = useThemes().contextual("overlay")
const dimensions = useTerminalDimensions()
return (
@@ -31,17 +31,17 @@ export function Toast() {
paddingRight={2}
paddingTop={1}
paddingBottom={1}
- backgroundColor={themeV2.background.default}
- borderColor={themeV2.text.feedback[current().variant].default}
+ backgroundColor={theme.background.default}
+ borderColor={theme.text.feedback[current().variant].default}
border={["left", "right"]}
customBorderChars={SplitBorder.customBorderChars}
>
-
+
{current().title}
-
+
{current().message}
diff --git a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx
index f85f5ddbb7b..aa930029f55 100644
--- a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx
+++ b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx
@@ -4,7 +4,7 @@ import { testRender } from "@opentui/solid"
import type { JSX } from "solid-js"
import { onMount, type ParentProps } from "solid-js"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
-import { ThemeProvider, useTheme } from "../../../src/context/theme"
+import { ThemeProvider, useTheme, useThemes } from "../../../src/context/theme"
import type { Plugin } from "@opencode-ai/plugin/tui"
import { ConfigProvider } from "../../../src/config"
import {
@@ -130,7 +130,7 @@ describe("DiffViewerFileTree", () => {
})
function ThemedDiffViewerFileTree(props: Omit) {
- return
+ return
}
async function renderFrame(component: () => JSX.Element) {
diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx
index f640310b52c..04758facb9d 100644
--- a/packages/tui/test/cli/tui/diff-viewer.test.tsx
+++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx
@@ -11,7 +11,7 @@ import type {
Route,
Slot,
} from "@opencode-ai/plugin/tui/context"
-import { ThemeProvider, useTheme } from "../../../src/context/theme"
+import { ThemeProvider, useTheme, useThemes } from "../../../src/context/theme"
import { ConfigProvider } from "../../../src/config"
import { TuiKeybind } from "../../../src/config/keybind"
import { Keymap } from "../../../src/context/keymap"
@@ -207,7 +207,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
void diffViewerPlugin.setup(context)
function Content() {
- theme = createPluginTheme(useTheme())
+ theme = createPluginTheme(useTheme(), useThemes())
const commandView = renderCommands?.({})
if (current.type !== "plugin") commands.get("diff.open")?.run()
return (
diff --git a/packages/tui/test/cli/tui/theme-mode.test.tsx b/packages/tui/test/cli/tui/theme-mode.test.tsx
index 34aa59ffd73..7e59846bfb2 100644
--- a/packages/tui/test/cli/tui/theme-mode.test.tsx
+++ b/packages/tui/test/cli/tui/theme-mode.test.tsx
@@ -6,7 +6,7 @@ import { DEFAULT_THEME, selectTheme } from "@opencode-ai/theme/tui"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { DEFAULT_THEMES } from "../../../src/theme"
import { ConfigProvider } from "../../../src/config"
-import { ThemeProvider, useTheme, type ThemeError } from "../../../src/context/theme"
+import { ThemeContextProvider, ThemeProvider, useTheme, useThemes, type ThemeError } from "../../../src/context/theme"
async function wait(fn: () => boolean) {
const started = Date.now()
@@ -27,17 +27,17 @@ test("uses an available mode while retaining the pinned preference", async () =>
darkOnly.theme.background = "#111111"
darkOnly.theme.text = "#eeeeee"
const native = { version: 2, dark: { text: { default: "#abcdef" } } } as const
- let theme: ReturnType | undefined
+ let themes: ReturnType | undefined
function Probe() {
- const value = useTheme()
- theme = value
+ const value = useThemes()
+ themes = value
return {value.mode()}
}
function current() {
- if (!theme) throw new Error("Theme provider is not mounted")
- return theme
+ if (!themes) throw new Error("Theme provider is not mounted")
+ return themes
}
const app = await testRender(
@@ -56,7 +56,7 @@ test("uses an available mode while retaining the pinned preference", async () =>
app.renderer.start()
try {
- await wait(() => theme?.ready === true)
+ await wait(() => themes?.ready === true)
expect(current().mode()).toBe("light")
expect(current().modes()).toEqual(["light"])
expect(current().supports("dark")).toBeFalse()
@@ -72,7 +72,7 @@ test("uses an available mode while retaining the pinned preference", async () =>
expect(current().set("native")).toBeTrue()
await wait(() => current().selected === "native")
expect(current().modes()).toEqual(["dark"])
- expect(current().themeV2.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
+ expect(current().current.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
} finally {
app.renderer.destroy()
}
@@ -83,13 +83,13 @@ test.each([
["mode merging", { version: 2, light: { mergeMode: true } }],
["token reference", { version: 2, light: { text: { default: "$missing" } } }],
] as const)("falls back to OpenCode when configured V2 theme %s is invalid", async (_label, source) => {
- let theme: ReturnType | undefined
+ let themes: ReturnType | undefined
let failure: ThemeError | undefined
let unsubscribe: (() => void) | undefined
function Probe() {
- const value = useTheme()
- theme = value
+ const value = useThemes()
+ themes = value
unsubscribe = value.onError((error) => (failure = error))
return {value.selected}
}
@@ -107,8 +107,8 @@ test.each([
app.renderer.start()
try {
- await wait(() => theme?.ready === true)
- expect(theme?.selected).toBe("opencode")
+ await wait(() => themes?.ready === true)
+ expect(themes?.selected).toBe("opencode")
expect(failure?.name).toBe("invalid")
expect(failure?.error).toBeInstanceOf(Error)
expect(failure?.error.message.length).toBeGreaterThan(0)
@@ -118,17 +118,30 @@ test.each([
}
})
-test("contextual themes fall back to a standalone theme's base view", async () => {
+test("contextual hooks resolve overrides and fall back to a standalone theme's base view", async () => {
const standalone = {
version: 2,
standalone: true,
- dark: { hue: selectTheme(DEFAULT_THEME, "dark").hue },
+ dark: {
+ hue: selectTheme(DEFAULT_THEME, "dark").hue,
+ "@context:elevated": { text: { default: "#abcdef" } },
+ },
} as const
+ let themes: ReturnType | undefined
let theme: ReturnType | undefined
- function Probe() {
+ function ContextProbe() {
theme = useTheme()
- return {theme.selected}
+ return {theme.text.default.toString()}
+ }
+
+ function Probe() {
+ themes = useThemes()
+ return (
+
+
+
+ )
}
const app = await testRender(
@@ -144,10 +157,12 @@ test("contextual themes fall back to a standalone theme's base view", async () =
app.renderer.start()
try {
- await wait(() => theme?.ready === true)
- if (!theme) throw new Error("Theme provider is not mounted")
- expect(theme.contextual("elevated").themeV2.text.default).toBe(theme.themeV2.text.default)
- expect(theme.contextual("overlay").themeV2.background.default).toBe(theme.themeV2.background.default)
+ await wait(() => themes?.ready === true)
+ if (!themes) throw new Error("Theme provider is not mounted")
+ if (!theme) throw new Error("Contextual theme is not mounted")
+ expect(theme.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue()
+ expect(theme.text.default).toBe(themes.contextual("elevated").text.default)
+ expect(themes.contextual("overlay").background.default).toBe(themes.current.background.default)
} finally {
app.renderer.destroy()
}