refactor(tui): split theme hooks (#39395)

This commit is contained in:
James Long 2026-07-28 16:25:32 -04:00 committed by GitHub
commit 08b80da931
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 833 additions and 814 deletions

View file

@ -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<string, Tab>,
@ -113,8 +113,8 @@ export function Composer(props: ComposerProps) {
<box
{...SplitBorder}
border={["left"]}
borderColor={themeV2.border.default}
backgroundColor={themeV2.background.default}
borderColor={theme.border.default}
backgroundColor={theme.background.default}
paddingLeft={1}
paddingRight={2}
paddingTop={1}
@ -125,7 +125,7 @@ export function Composer(props: ComposerProps) {
<Show
when={tabList().length > 1}
fallback={
<text fg={themeV2.text.default} attributes={TextAttributes.BOLD}>
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
{tabList()[0]?.label ?? ""}
</text>
}
@ -136,7 +136,7 @@ export function Composer(props: ComposerProps) {
const isActive = createMemo(() => store.active === t.id)
return (
<text
fg={isActive() ? themeV2.text.default : themeV2.text.subdued}
fg={isActive() ? theme.text.default : theme.text.subdued}
attributes={isActive() ? TextAttributes.BOLD : undefined}
>
{t.label}
@ -146,7 +146,7 @@ export function Composer(props: ComposerProps) {
</For>
</box>
</Show>
<text fg={themeV2.text.subdued} onMouseUp={close}>
<text fg={theme.text.subdued} onMouseUp={close}>
esc
</text>
</box>
@ -156,19 +156,19 @@ export function Composer(props: ComposerProps) {
<For each={footerHints()}>
{(hint) => (
<text>
<span style={{ fg: themeV2.text.default }}>
<span style={{ fg: theme.text.default }}>
<b>{hint.label}</b>{" "}
</span>
<span style={{ fg: themeV2.text.subdued }}>{hint.shortcut}</span>
<span style={{ fg: theme.text.subdued }}>{hint.shortcut}</span>
</text>
)}
</For>
<Show when={tabList().length > 1}>
<text>
<span style={{ fg: themeV2.text.default }}>
<span style={{ fg: theme.text.default }}>
<b>tabs</b>{" "}
</span>
<span style={{ fg: themeV2.text.subdued }}>/</span>
<span style={{ fg: theme.text.subdued }}>/</span>
</text>
</Show>
</box>

View file

@ -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 (
<Show when={composer.active("shell")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={themeV2.text.subdued}> No shell commands</text>}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No shell commands</text>}>
<For each={entries()}>
{(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())}
>
<text
fg={active() ? themeV2.text.action.primary.focused : themeV2.text.action.primary.default}
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.default}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
>

View file

@ -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 (
<Show when={composer.active("subagents")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={themeV2.text.subdued}> No subagents</text>}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No subagents</text>}>
<For each={entries()}>
{(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 }) {
<text
fg={
active()
? themeV2.text.action.primary.focused
? theme.text.action.primary.focused
: entry.current
? themeV2.text.action.primary.selected
: themeV2.text.action.primary.default
? theme.text.action.primary.selected
: theme.text.action.primary.default
}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
@ -248,7 +248,7 @@ export function SubagentsTab(props: { sessionID: string }) {
</text>
</box>
<Show when={status()}>
<text fg={active() ? themeV2.text.action.primary.focused : themeV2.text.subdued} wrapMode="none">
<text fg={active() ? theme.text.action.primary.focused : theme.text.subdued} wrapMode="none">
{status()}
</text>
</Show>

View file

@ -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 (
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
<text fg={themeV2.text.subdued}>{directory()}</text>
<text fg={theme.text.subdued}>{directory()}</text>
<box gap={2} flexDirection="row" flexShrink={0}>
<Switch>
<Match when={store.welcome}>
<text fg={themeV2.text.default}>
Get started <span style={{ fg: themeV2.text.subdued }}>/connect</span>
<text fg={theme.text.default}>
Get started <span style={{ fg: theme.text.subdued }}>/connect</span>
</text>
</Match>
<Match when={connected()}>
<Show when={permission.mode !== "auto" && permissions().length > 0}>
<text fg={themeV2.text.feedback.warning.default}>
<span style={{ fg: themeV2.text.feedback.warning.default }}></span> {permissions().length} Permission
<text fg={theme.text.feedback.warning.default}>
<span style={{ fg: theme.text.feedback.warning.default }}></span> {permissions().length} Permission
{permissions().length > 1 ? "s" : ""}
</text>
</Show>
<Show when={mcp()}>
<text fg={themeV2.text.default}>
<text fg={theme.text.default}>
<Switch>
<Match when={mcpError()}>
<span style={{ fg: themeV2.text.feedback.error.default }}> </span>
<span style={{ fg: theme.text.feedback.error.default }}> </span>
</Match>
<Match when={true}>
<span style={{ fg: themeV2.text.feedback.success.default }}> </span>
<span style={{ fg: theme.text.feedback.success.default }}> </span>
</Match>
</Switch>
{mcp()} MCP
</text>
</Show>
<text fg={themeV2.text.subdued}>/status</text>
<text fg={theme.text.subdued}>/status</text>
</Match>
</Switch>
</box>

View file

@ -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 (
<box
backgroundColor={themeV2.background.default}
backgroundColor={theme.background.default}
border={["left"]}
borderColor={themeV2.hue.interactive[themeMode() === "light" ? 800 : 200]}
borderColor={theme.hue.interactive[themeMode() === "light" ? 800 : 200]}
customBorderChars={SplitBorder.customBorderChars}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box paddingLeft={1}>
<text fg={themeV2.text.subdued}>{props.form.title}</text>
<text fg={theme.text.subdued}>{props.form.title}</text>
</box>
<Show when={message()}>
<box paddingLeft={1}>
<text fg={themeV2.text.default}>{message()}</text>
<text fg={theme.text.default}>{message()}</text>
</box>
</Show>
<Show when={!single() && !tabbed()}>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={themeV2.text.subdued}>
<text fg={theme.text.subdued}>
{confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
</text>
<Show when={fields().length > 0}>
<text fg={themeV2.text.subdued}>
<text fg={theme.text.subdued}>
· {answered()}/{fields().length} completed
</text>
</Show>
@ -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 }) {
<text
fg={
isTab()
? themeV2.text.formfield.selected
? theme.text.formfield.selected
: tabHover() === index()
? themeV2.text.formfield.focused
? theme.text.formfield.focused
: isAnswered()
? themeV2.text.default
: themeV2.text.subdued
? theme.text.default
: theme.text.subdued
}
>
{truncate(formLabel(item), 24)}
@ -693,10 +695,10 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<box
backgroundColor={
confirm()
? themeV2.background.formfield.selected
? theme.background.formfield.selected
: tabHover() === "confirm"
? themeV2.background.formfield.focused
: themeV2.background.default
? theme.background.formfield.focused
: theme.background.default
}
onMouseOver={() => setTabHover("confirm")}
onMouseOut={() => setTabHover(null)}
@ -705,7 +707,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
selectTabFromMouse()
}}
>
<text fg={confirm() ? themeV2.text.formfield.selected : themeV2.text.formfield.default}>Confirm</text>
<text fg={confirm() ? theme.text.formfield.selected : theme.text.formfield.default}>Confirm</text>
</box>
</box>
</Show>
@ -714,13 +716,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{(external) => (
<box paddingLeft={1} gap={1}>
<Show when={external().title}>
<text fg={themeV2.text.default}>{external().title}</text>
<text fg={theme.text.default}>{external().title}</text>
</Show>
<Show when={external().description}>
<text fg={themeV2.text.subdued}>{external().description}</text>
<text fg={theme.text.subdued}>{external().description}</text>
</Show>
<text
fg={themeV2.background.action.primary.default}
fg={theme.background.action.primary.default}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
openExternal()
@ -729,9 +731,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{external().url}
</text>
<text
fg={
store.answers[external().key] === true ? themeV2.text.feedback.success.default : themeV2.text.subdued
}
fg={store.answers[external().key] === true ? theme.text.feedback.success.default : theme.text.subdued}
>
{store.answers[external().key] === true
? "✓ Acknowledged"
@ -746,7 +746,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<Show when={!confirm() && answerField()}>
<box paddingLeft={1} gap={1}>
<box>
<text fg={themeV2.text.default}>{answerField()!.description ?? formLabel(answerField()!)}</text>
<text fg={theme.text.default}>{answerField()!.description ?? formLabel(answerField()!)}</text>
</box>
<Show when={textual() ? answerField()!.key : undefined} keyed>
<box paddingLeft={1}>
@ -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}
/>
</box>
</Show>
@ -793,39 +793,35 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box flexDirection="row">
<box
backgroundColor={
active() ? themeV2.background.formfield.focused : themeV2.background.default
}
backgroundColor={active() ? theme.background.formfield.focused : theme.background.default}
paddingRight={1}
>
<text
fg={active() ? themeV2.text.formfield.focused : themeV2.text.formfield.default}
fg={active() ? theme.text.formfield.focused : theme.text.formfield.default}
>{`${i() + 1}.`}</text>
</box>
<box
backgroundColor={
active() ? themeV2.background.formfield.focused : themeV2.background.default
}
backgroundColor={active() ? theme.background.formfield.focused : theme.background.default}
>
<text
fg={
active()
? themeV2.text.formfield.focused
? theme.text.formfield.focused
: picked()
? themeV2.text.formfield.selected
: themeV2.text.formfield.default
? theme.text.formfield.selected
: theme.text.formfield.default
}
>
{multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
</text>
</box>
<Show when={!multi()}>
<text fg={themeV2.text.formfield.selected}>{picked() ? " ✓" : ""}</text>
<text fg={theme.text.formfield.selected}>{picked() ? " ✓" : ""}</text>
</Show>
</box>
<Show when={row.description}>
<box paddingLeft={3}>
<text fg={themeV2.text.subdued}>{row.description}</text>
<text fg={theme.text.subdued}>{row.description}</text>
</box>
</Show>
</box>
@ -843,30 +839,28 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box flexDirection="row">
<box
backgroundColor={other() ? themeV2.background.formfield.focused : themeV2.background.default}
backgroundColor={other() ? theme.background.formfield.focused : theme.background.default}
paddingRight={1}
>
<text fg={other() ? themeV2.text.formfield.focused : themeV2.text.formfield.default}>
<text fg={other() ? theme.text.formfield.focused : theme.text.formfield.default}>
{`${rows().length + 1}.`}
</text>
</box>
<box
backgroundColor={other() ? themeV2.background.formfield.focused : themeV2.background.default}
>
<box backgroundColor={other() ? theme.background.formfield.focused : theme.background.default}>
<text
fg={
other()
? themeV2.text.formfield.focused
? theme.text.formfield.focused
: customPicked()
? themeV2.text.feedback.success.default
: themeV2.text.default
? theme.text.feedback.success.default
: theme.text.default
}
>
{multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
</text>
</box>
<Show when={!multi()}>
<text fg={themeV2.text.feedback.success.default}>{customPicked() ? " ✓" : ""}</text>
<text fg={theme.text.feedback.success.default}>{customPicked() ? " ✓" : ""}</text>
</Show>
</box>
<Show when={store.editing}>
@ -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}
/>
</box>
</Show>
<Show when={!store.editing && input()}>
<box paddingLeft={3}>
<text fg={themeV2.text.subdued}>{input()}</text>
<text fg={theme.text.subdued}>{input()}</text>
</box>
</Show>
</box>
@ -906,7 +900,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<Show when={confirm()}>
<Show when={tabbed()}>
<box paddingLeft={1}>
<text fg={themeV2.text.default}>Review</text>
<text fg={theme.text.default}>Review</text>
</box>
</Show>
<scrollbox
@ -921,12 +915,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: themeV2.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg: acknowledged()
? themeV2.text.feedback.success.default
: themeV2.text.feedback.error.default,
? theme.text.feedback.success.default
: theme.text.feedback.error.default,
}}
>
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
@ -942,15 +936,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: themeV2.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span style={{ fg: theme.text.subdued }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span
style={{
fg:
invalid() || missing()
? themeV2.text.feedback.error.default
? theme.text.feedback.error.default
: answered()
? themeV2.text.default
: themeV2.text.subdued,
? theme.text.default
: theme.text.subdued,
}}
>
{invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
@ -974,41 +968,41 @@ export function FormPrompt(props: { form: FormWithLocation }) {
>
<box flexDirection="row" gap={2}>
<Show when={!single()}>
<text fg={themeV2.text.default}>
{"⇆"} <span style={{ fg: themeV2.text.subdued }}>tab</span>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>tab</span>
</text>
</Show>
<Show when={!confirm() && !textual() && !externalField()}>
<text fg={themeV2.text.default}>
{"↑↓"} <span style={{ fg: themeV2.text.subdued }}>select</span>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>select</span>
</text>
</Show>
<Show when={confirm() && fields().length > 0}>
<text fg={themeV2.text.default}>
{"↑↓"} <span style={{ fg: themeV2.text.subdued }}>scroll</span>
<text fg={theme.text.default}>
{"↑↓"} <span style={{ fg: theme.text.subdued }}>scroll</span>
</text>
</Show>
<text
fg={themeV2.text.default}
fg={theme.text.default}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (confirm()) submit()
if (externalField()) acknowledgeExternal()
}}
>
enter <span style={{ fg: themeV2.text.subdued }}>{actionLabel()}</span>
enter <span style={{ fg: theme.text.subdued }}>{actionLabel()}</span>
</text>
<Show when={externalField() && clipboard.write}>
<text fg={themeV2.text.default} onMouseUp={copyExternal}>
c <span style={{ fg: themeV2.text.subdued }}>copy</span>
<text fg={theme.text.default} onMouseUp={copyExternal}>
c <span style={{ fg: theme.text.subdued }}>copy</span>
</text>
</Show>
<text fg={themeV2.text.default} onMouseUp={cancel}>
esc <span style={{ fg: themeV2.text.subdued }}>dismiss</span>
<text fg={theme.text.default} onMouseUp={cancel}>
esc <span style={{ fg: theme.text.subdued }}>dismiss</span>
</text>
</box>
<Show when={store.error}>
<text fg={themeV2.text.feedback.error.default}>{store.error}</text>
<text fg={theme.text.feedback.error.default}>{store.error}</text>
</Show>
</box>
</box>

View file

@ -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: {
<Show when={Boolean(config.data.debug?.turn_tokens) && steps().length > 0}>
<box paddingLeft={3} flexDirection="column">
<box flexDirection="row">
<text width={INLINE_TOOL_ICON_WIDTH} fg={themeV2.text.subdued}>
<text width={INLINE_TOOL_ICON_WIDTH} fg={theme.text.subdued}>
</text>
<text fg={themeV2.text.subdued} attributes={TextAttributes.BOLD}>
<text fg={theme.text.subdued} attributes={TextAttributes.BOLD}>
Tokens
</text>
</box>
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text fg={themeV2.text.subdued} attributes={TextAttributes.ITALIC}>
<text fg={theme.text.subdued} attributes={TextAttributes.ITALIC}>
{"Step".padEnd(columns().step + 2)}
{"New".padStart(columns().newTokens)}
{" "}
@ -1161,7 +1161,7 @@ function TurnTokenUsage(props: {
<For each={steps()}>
{(item) => (
<box paddingLeft={INLINE_TOOL_ICON_WIDTH} flexDirection="column">
<text fg={verbose() && item.finish === "tool-call" ? undefined : themeV2.text.subdued}>
<text fg={verbose() && item.finish === "tool-call" ? undefined : theme.text.subdued}>
{item.finish.padEnd(columns().step + 2)}
<span style={{ attributes: TextAttributes.BOLD }}>
{item.newTokens.toLocaleString().padStart(columns().newTokens)}
@ -1173,7 +1173,7 @@ function TurnTokenUsage(props: {
</text>
<TurnTokenToolCalls tools={item.tools} />
<Show when={item.reuseDrop !== undefined}>
<text fg={themeV2.text.feedback.warning.default}>
<text fg={theme.text.feedback.warning.default}>
! Likely cache bust: {item.reuseDrop?.toLocaleString()} fewer cached tokens than the previous step
</text>
</Show>
@ -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 (
<Show when={props.tools.length > 0}>
@ -1197,13 +1197,13 @@ function TurnTokenToolCalls(props: { tools: SessionMessageAssistantTool[] }) {
<text
width={nameWidth()}
flexShrink={0}
fg={themeV2.text.subdued}
fg={theme.text.subdued}
attributes={TextAttributes.BOLD}
>
{tool.name}
</text>
<text
fg={themeV2.text.subdued}
fg={theme.text.subdued}
attributes={TextAttributes.DIM}
wrapMode="word"
flexGrow={1}
@ -1234,7 +1234,7 @@ function turnTokenToolSummary(tool: SessionMessageAssistantTool) {
}
function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
const { themeV2 } = useTheme()
const theme = useTheme()
const shortcut = Keymap.useShortcut("session.background")
const visible = createMemo(() => {
const current = props.messages.findLast(
@ -1252,8 +1252,8 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
<Show when={visible() && shortcut()}>
{(value) => (
<box marginTop={1} paddingLeft={3} flexShrink={0}>
<text fg={themeV2.text.subdued}>
Press <span style={{ fg: themeV2.text.default }}>{value()}</span> to move running work to the background
<text fg={theme.text.subdued}>
Press <span style={{ fg: theme.text.default }}>{value()}</span> to move running work to the background
</text>
</box>
)}
@ -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: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={themeV2.raise(themeV2.background.surface.offset)}
borderColor={theme.raise(theme.background.surface.offset)}
paddingLeft={1}
>
<code
@ -1422,7 +1423,7 @@ function SessionReasoningGroupView(props: {
syntaxStyle={syntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={themeV2.text.subdued}
fg={theme.text.subdued}
/>
</box>
</box>
@ -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: {
<Show when={grouped().length > 0}>
<InlineToolRow
icon={props.completed ? "→" : "✱"}
color={hover() ? themeV2.text.default : themeV2.text.subdued}
color={hover() ? theme.text.default : theme.text.subdued}
complete={props.completed}
pending={label()}
spinner={!props.completed}
@ -1506,7 +1507,7 @@ function SessionGroupView(props: {
function AssistantFooter(props: { message: SessionMessageAssistant }) {
const ctx = use()
const local = useLocal()
const { themeV2 } = useTheme().contextual("elevated")
const theme = useThemes().contextual("elevated")
const model = createMemo(
() =>
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}
>
<text fg={themeV2.text.subdued}>{errorMessage(props.message.error)}</text>
<text fg={theme.text.subdued}>{errorMessage(props.message.error)}</text>
</box>
</Show>
<AssistantRetry retry={props.message.retry} />
<box paddingLeft={3} marginTop={props.message.error && !interrupted() ? 1 : 0}>
<text>
<span style={{ fg: props.message.error ? themeV2.text.subdued : local.agent.color(props.message.agent) }}>
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<span style={{ fg: themeV2.text.subdued }}> · {model()}</span>
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
<Show when={duration()}>
<span style={{ fg: themeV2.text.subdued }}> · {Locale.duration(duration())}</span>
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
</Show>
<Show when={interrupted()}>
<span style={{ fg: themeV2.text.subdued }}> · interrupted</span>
<span style={{ fg: theme.text.subdued }}> · interrupted</span>
</Show>
</text>
</box>
@ -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 (
<box paddingLeft={3}>
<text fg={themeV2.text.subdued}>{text()}</text>
<text fg={theme.text.subdued}>{text()}</text>
</box>
)
}
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 (
<Show
when={completion()}
fallback={
<InlineToolRow icon="◈" color={themeV2.text.subdued} pending="Notice" complete={true}>
<InlineToolRow icon="◈" color={theme.text.subdued} pending="Notice" complete={true}>
{text()}
</InlineToolRow>
}
@ -1606,7 +1607,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
<box marginLeft={3}>
<text wrapMode="none">
<span style={{ fg: color() }}>{heading()}</span>
<span style={{ fg: themeV2.text.subdued }}>{suffix()}</span>
<span style={{ fg: theme.text.subdued }}>{suffix()}</span>
</text>
</box>
</Show>
@ -1614,9 +1615,9 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
}
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
const { themeV2 } = useTheme()
const theme = useTheme()
return (
<InlineToolRow icon="→" color={themeV2.text.subdued} pending="Skill" complete={true}>
<InlineToolRow icon="→" color={theme.text.subdued} pending="Skill" complete={true}>
Skill {props.message.name}
</InlineToolRow>
)
@ -1624,14 +1625,15 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
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 (
<box>
<box flexDirection="row" alignItems="center">
@ -1663,8 +1665,8 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
content={content()}
tableOptions={{ style: "grid" }}
conceal={ctx.markdownMode() === "rendered"}
fg={themeV2.markdown.text}
bg={themeV2.background.default}
fg={theme.markdown.text}
bg={theme.background.default}
/>
</box>
</Show>
@ -1673,15 +1675,15 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
}
function CompactionQueued() {
const { themeV2 } = useTheme()
const theme = useTheme()
return (
<box flexDirection="row" alignItems="center">
<box border={["top"]} borderColor={themeV2.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
<text fg={themeV2.text.subdued}></text>
<text fg={themeV2.text.subdued}>Compaction queued</text>
<text fg={theme.text.subdued}></text>
<text fg={theme.text.subdued}>Compaction queued</text>
</box>
<box border={["top"]} borderColor={themeV2.border.default} flexGrow={1} />
<box border={["top"]} borderColor={theme.border.default} flexGrow={1} />
</box>
)
}
@ -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}
>
<box
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={hover() ? themeV2.raise(themeV2.background.default) : themeV2.background.default}
backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default}
>
<text fg={themeV2.text.subdued}>
<text fg={theme.text.subdued}>
{props.count} message{props.count === 1 ? "" : "s"} reverted
</text>
<Show when={props.files.length > 0}>
@ -1743,7 +1745,7 @@ function RevertMessage(props: {
<For each={props.files}>
{(file) => (
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={themeV2.text.subdued}>{statusLabel(file.status)}</text>
<text fg={theme.text.subdued}>{statusLabel(file.status)}</text>
<FilePath
value={file.file}
maxWidth={Math.max(
@ -1753,21 +1755,21 @@ function RevertMessage(props: {
(file.additions > 0 ? stringWidth(`+${file.additions}`) + 1 : 0) -
(file.deletions > 0 ? stringWidth(`-${file.deletions}`) + 1 : 0),
)}
fg={themeV2.text.default}
fg={theme.text.default}
/>
<Show when={file.additions > 0}>
<text fg={themeV2.diff.text.added}>+{file.additions}</text>
<text fg={theme.diff.text.added}>+{file.additions}</text>
</Show>
<Show when={file.deletions > 0}>
<text fg={themeV2.diff.text.removed}>-{file.deletions}</text>
<text fg={theme.diff.text.removed}>-{file.deletions}</text>
</Show>
</box>
)}
</For>
</box>
</Show>
<text fg={themeV2.text.subdued}>
<span style={{ fg: themeV2.text.default }}>{redoKey()}</span> or /redo to restore
<text fg={theme.text.subdued}>
<span style={{ fg: theme.text.default }}>{redoKey()}</span> or /redo to restore
</text>
</box>
</box>
@ -1775,7 +1777,7 @@ function RevertMessage(props: {
}
function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "shell" }> }) {
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<SessionMessageInfo, { type: "she
paddingBottom={1}
paddingLeft={2}
gap={1}
backgroundColor={themeV2.background.default}
backgroundColor={theme.background.default}
customBorderChars={SplitBorder.customBorderChars}
borderColor={themeV2.background.default}
borderColor={theme.background.default}
>
<text fg={themeV2.text.default}>$ {props.message.command}</text>
<text fg={theme.text.default}>$ {props.message.command}</text>
<Show when={output()}>
<text fg={themeV2.text.subdued}>{output()}</text>
<text fg={theme.text.subdued}>{output()}</text>
</Show>
</box>
)
@ -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 }) {
<Show when={props.message.text.trim() || files().length}>
<box
border={["left"]}
borderColor={queued() ? themeV2.border.default : color()}
borderColor={queued() ? theme.border.default : color()}
customBorderChars={SplitBorder.customBorderChars}
>
<box
@ -1839,27 +1843,27 @@ function UserMessage(props: { message: SessionMessageUser }) {
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={hover() ? themeV2.raise(themeV2.background.default) : themeV2.background.default}
backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default}
flexShrink={0}
>
<text fg={themeV2.text.default}>{props.message.text}</text>
<text fg={theme.text.default}>{props.message.text}</text>
<Show when={files().length}>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
{(file) => {
const label = file.mime === "application/x-directory" ? "dir" : "file"
return (
<text fg={themeV2.text.default}>
<text fg={theme.text.default}>
<span
style={{
bg: themeV2.hue.accent[mode() === "light" ? 700 : 200],
fg: themeV2.background.default,
bg: theme.hue.accent[mode() === "light" ? 700 : 200],
fg: theme.background.default,
bold: true,
}}
>
{` ${label} `}
</span>
<span style={{ bg: themeV2.raise(themeV2.background.default), fg: themeV2.text.subdued }}>
<span style={{ bg: theme.raise(theme.background.default), fg: theme.text.subdued }}>
{" "}
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
</span>
@ -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}
>
<text fg={themeV2.text.subdued}>{errorMessage(props.message.error)}</text>
<text fg={theme.text.subdued}>{errorMessage(props.message.error)}</text>
</box>
</Show>
<AssistantRetry retry={props.message.retry} />
@ -1976,12 +1980,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
<Match when={props.last || final() || props.message.error}>
<box paddingLeft={3}>
<text>
<span style={{ fg: props.message.error ? themeV2.text.subdued : local.agent.color(props.message.agent) }}>
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<span style={{ fg: themeV2.text.subdued }}> · {model()}</span>
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
<Show when={duration()}>
<span style={{ fg: themeV2.text.subdued }}> · {Locale.duration(duration())}</span>
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
</Show>
</text>
</box>
@ -1992,12 +1996,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
}
function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
const { themeV2 } = useTheme()
const theme = useTheme()
return (
<Show when={props.retry}>
{(retry) => (
<box paddingLeft={3} marginTop={1}>
<text fg={themeV2.text.subdued}>
<text fg={theme.text.subdued}>
Retry attempt {retry().attempt} scheduled: {retry().error.message} [{retry().error.type}]
</text>
</box>
@ -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
<box flexDirection="column">
<InlineToolRow
icon="✱"
color={themeV2.text.subdued}
color={theme.text.subdued}
complete={!props.active}
pending="Exploring"
spinner={props.active}
@ -2030,7 +2034,7 @@ function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; activ
<For each={props.parts}>
{(part, index) => (
<box paddingLeft={5}>
<text fg={part.state.status === "error" ? themeV2.text.feedback.error.default : themeV2.text.subdued}>
<text fg={part.state.status === "error" ? theme.text.feedback.error.default : theme.text.subdued}>
{index() === props.parts.length - 1 ? "└" : "├"} {label(part)}
</text>
</box>
@ -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: {
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
customBorderChars={SplitBorder.customBorderChars}
borderColor={themeV2.raise(themeV2.background.default)}
borderColor={theme.raise(theme.background.default)}
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
>
<box onMouseUp={toggle}>
@ -2093,7 +2098,7 @@ function ReasoningPart(props: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={themeV2.raise(themeV2.background.default)}
borderColor={theme.raise(theme.background.default)}
paddingLeft={inMinimal() ? 3 : 1}
>
<code
@ -2103,7 +2108,7 @@ function ReasoningPart(props: {
syntaxStyle={syntax()}
content={content()}
conceal={ctx.markdownMode() === "rendered"}
fg={themeV2.text.subdued}
fg={theme.text.subdued}
/>
</box>
</box>
@ -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 (
<Switch>
@ -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 (
<Show when={props.part.text.trim()}>
<box paddingLeft={3} flexShrink={0}>
@ -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}
/>
</box>
</Show>
@ -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) {
<Show when={Object.keys(props.input).length > 0}>
<box gap={1}>
<text>
<span style={{ bg: themeV2.raise(themeV2.background.default), fg: themeV2.text.subdued }}> Input </span>
<span style={{ bg: theme.raise(theme.background.default), fg: theme.text.subdued }}> Input </span>
</text>
<box paddingLeft={1}>
<code
@ -2297,7 +2304,7 @@ function GenericTool(props: ToolProps) {
syntaxStyle={syntax()}
conceal={false}
drawUnstyledText={false}
fg={themeV2.text.default}
fg={theme.text.default}
/>
</box>
</box>
@ -2306,13 +2313,13 @@ function GenericTool(props: ToolProps) {
{(value) => (
<box gap={1}>
<text>
<span style={{ bg: themeV2.raise(themeV2.background.default), fg: themeV2.text.subdued }}>
<span style={{ bg: theme.raise(theme.background.default), fg: theme.text.subdued }}>
{" "}
Output{" "}
</span>
</text>
<box paddingLeft={1}>
<text fg={themeV2.text.default} wrapMode="word">
<text fg={theme.text.default} wrapMode="word">
{value()}
</text>
</box>
@ -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 (
<text flexShrink={0} bg={themeV2.raise(themeV2.background.default)} fg={themeV2.text.subdued}>
<text flexShrink={0} bg={theme.raise(theme.background.default)} fg={theme.text.subdued}>
{" "}
{props.children}{" "}
</text>
@ -2524,13 +2531,13 @@ function BlockTool(props: BlockToolProps) {
const parentTheme = useTheme()
return (
<ThemeContextProvider context="elevated">
<BlockToolContent {...props} borderColor={parentTheme.themeV2.background.default} />
<BlockToolContent {...props} borderColor={parentTheme.background.default} />
</ThemeContextProvider>
)
}
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 }) {
<Show
when={props.spinner}
fallback={
<text fg={permission() ? themeV2.text.feedback.warning.default : themeV2.text.subdued}>
<text fg={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
{title()}
</text>
}
>
<Spinner color={permission() ? themeV2.text.feedback.warning.default : themeV2.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
{title().replace(/^# /, "")}
</Spinner>
</Show>
@ -2579,26 +2586,26 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
<Show
when={props.spinner}
fallback={
<text flexShrink={0} fg={permission() ? themeV2.text.feedback.warning.default : themeV2.text.subdued}>
<text flexShrink={0} fg={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
{path().label}
</text>
}
>
<Spinner color={permission() ? themeV2.text.feedback.warning.default : themeV2.text.subdued}>
<Spinner color={permission() ? theme.text.feedback.warning.default : theme.text.subdued}>
{path().label.replace(/^# /, "")}
</Spinner>
</Show>
<FilePath
value={path().value}
maxWidth={Math.max(2, ctx.width - 4 - stringWidth(path().label) - (props.spinner ? 2 : 0))}
fg={permission() ? themeV2.text.feedback.warning.default : themeV2.text.subdued}
fg={permission() ? theme.text.feedback.warning.default : theme.text.subdued}
/>
</box>
)}
</Show>
{props.children}
<Show when={error()}>
<text fg={themeV2.text.feedback.error.default}>{error()}</text>
<text fg={theme.text.feedback.error.default}>{error()}</text>
</Show>
</box>
)
@ -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" ? (
<Spinner color={color()}>Writing command...</Spinner>
) : (
<text fg={themeV2.text.subdued}>Writing command...</text>
<text fg={theme.text.subdued}>Writing command...</text>
)
}
>
@ -2731,14 +2738,14 @@ function Shell(props: ToolProps) {
when={isRunning()}
fallback={
<text>
<span style={{ fg: themeV2.text.default }}>{limited().slice(0, input().length)}</span>
<span style={{ fg: themeV2.text.subdued }}>{limited().slice(input().length)}</span>
<span style={{ fg: theme.text.default }}>{limited().slice(0, input().length)}</span>
<span style={{ fg: theme.text.subdued }}>{limited().slice(input().length)}</span>
</text>
}
>
<Spinner color={color()}>
<span style={{ fg: themeV2.text.default }}>{limited().slice(0, input().length)}</span>
<span style={{ fg: themeV2.text.subdued }}>{limited().slice(input().length)}</span>
<span style={{ fg: theme.text.default }}>{limited().slice(0, input().length)}</span>
<span style={{ fg: theme.text.subdued }}>{limited().slice(input().length)}</span>
</Spinner>
</Show>
</Show>
@ -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}
>
<line_number fg={themeV2.text.subdued} minWidth={3} paddingRight={1}>
<line_number fg={theme.text.subdued} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={themeV2.text.default}
fg={theme.text.default}
filetype={filetype(stringValue(props.input.path))}
syntaxStyle={syntax()}
content={code()}
@ -2799,7 +2807,7 @@ function Glob(props: ToolProps) {
}
function Read(props: ToolProps) {
const { themeV2 } = useTheme()
const theme = useTheme()
const pathFormatter = usePathFormatter()
const isRunning = createMemo(() => props.part.state.status === "running")
const loaded = createMemo(() => {
@ -2822,7 +2830,7 @@ function Read(props: ToolProps) {
<For each={loaded()}>
{(filepath) => (
<box paddingLeft={3}>
<text paddingLeft={3} fg={themeV2.text.subdued}>
<text paddingLeft={3} fg={theme.text.subdued}>
Loaded {pathFormatter.format(filepath)}
</text>
</box>
@ -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) {
<>
<InlineTool
icon={hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│"}
color={hasRuntimeError() ? themeV2.text.feedback.error.default : undefined}
color={hasRuntimeError() ? theme.text.feedback.error.default : undefined}
spinner={isLoading()}
pending="execute"
complete={true}
@ -2952,7 +2960,7 @@ function Execute(props: ToolProps) {
<box paddingLeft={3}>
<For each={outputPreview().split("\n")}>
{(line, index) => (
<text paddingLeft={3} fg={themeV2.text.feedback.error.default}>
<text paddingLeft={3} fg={theme.text.feedback.error.default}>
{index() === 0 ? "↳ " : " "}
{line}
</text>
@ -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}
/>
</box>
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={stringValue(props.input.path) ?? ""} />
@ -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) {
<Show
when={file.type !== "delete"}
fallback={
<text fg={themeV2.diff.text.removed}>
<text fg={theme.diff.text.removed}>
-{file.deletions} line{file.deletions !== 1 ? "s" : ""}
</text>
}
@ -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}
/>
</box>
</Show>
@ -3114,7 +3124,7 @@ function ApplyPatch(props: ToolProps) {
<FilePath
value={file.resource}
maxWidth={Math.max(2, ctx.width - 3)}
fg={file.type === "delete" ? themeV2.diff.text.removed : themeV2.text.subdued}
fg={file.type === "delete" ? theme.diff.text.removed : theme.text.subdued}
/>
</BlockTool>
)}
@ -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) {
<For each={questions()}>
{(q, i) => (
<box flexDirection="column">
<text fg={themeV2.text.subdued}>{q.question}</text>
<text fg={themeV2.text.default}>{format(answers()?.[i()])}</text>
<text fg={theme.text.subdued}>{q.question}</text>
<text fg={theme.text.default}>{format(answers()?.[i()])}</text>
</box>
)}
</For>
@ -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 }) {
<box>
<For each={errors()}>
{(diagnostic) => (
<text fg={themeV2.text.feedback.error.default}>
<text fg={theme.text.feedback.error.default}>
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
</text>
)}

View file

@ -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}
/>
</scrollbox>
</Show>
@ -76,7 +76,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
when={props.patch}
fallback={
<box paddingLeft={1}>
<text fg={themeV2.text.subdued}>No diff provided</text>
<text fg={theme.text.subdued}>No diff provided</text>
</box>
}
>
@ -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}
/>
</scrollbox>
)}
@ -128,7 +128,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
return { input: undefined, metadata: undefined }
})
const { themeV2 } = useTheme()
const theme = useTheme()
return (
<Switch>
@ -140,7 +140,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
body={
<box paddingLeft={1} gap={1}>
<For each={permissionAlwaysLines(props.request)}>
{(line, index) => <text fg={index() === 0 ? themeV2.text.subdued : themeV2.text.default}>{line}</text>}
{(line, index) => <text fg={index() === 0 ? theme.text.subdued : theme.text.default}>{line}</text>}
</For>
</box>
}
@ -192,9 +192,9 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
) : props.request.action === "external_directory" ? (
<Show when={current.lines.length > 0}>
<box paddingLeft={1} gap={1}>
<text fg={themeV2.text.subdued}>Patterns</text>
<text fg={theme.text.subdued}>Patterns</text>
<box>
<For each={current.lines}>{(line) => <text fg={themeV2.text.default}>{line}</text>}</For>
<For each={current.lines}>{(line) => <text fg={theme.text.default}>{line}</text>}</For>
</box>
</box>
</Show>
@ -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 = () => (
<box flexDirection="column" gap={0}>
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={themeV2.text.feedback.warning.default}>{"△"}</text>
<text fg={themeV2.text.default}>Permission required</text>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>Permission required</text>
</box>
<Show when={props.request.action !== "shell" && current.title}>
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
<text fg={themeV2.text.subdued} flexShrink={0}>
<text fg={theme.text.subdued} flexShrink={0}>
{current.icon}
</text>
<text fg={themeV2.text.default}>{current.title}</text>
<text fg={theme.text.default}>{current.title}</text>
</box>
</Show>
</box>
@ -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}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={themeV2.text.feedback.error.default}>{"△"}</text>
<text fg={themeV2.text.default}>Reject permission</text>
<text fg={theme.text.feedback.error.default}>{"△"}</text>
<text fg={theme.text.default}>Reject permission</text>
</box>
<box paddingLeft={1}>
<text fg={themeV2.text.subdued}>Tell OpenCode what to do differently</text>
<text fg={theme.text.subdued}>Tell OpenCode what to do differently</text>
</box>
</box>
<box
@ -350,7 +350,7 @@ function RejectPrompt(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"}
gap={1}
@ -369,9 +369,9 @@ function RejectPrompt(props: {
val.traits = { status: "REJECT" }
}}
focused
textColor={themeV2.text.default}
focusedTextColor={themeV2.text.default}
cursorColor={themeV2.text.default}
textColor={theme.text.default}
focusedTextColor={theme.text.default}
cursorColor={theme.text.default}
/>
<box
id="session.permission.reject.actions"
@ -394,8 +394,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={() => props.onConfirm(input.plainText)}
>
<text fg={themeV2.text.default}>
enter <span style={{ fg: themeV2.text.subdued }}>confirm</span>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
</text>
</box>
<box
@ -408,8 +408,8 @@ function RejectPrompt(props: {
}))}
onMouseUp={props.onCancel}
>
<text fg={themeV2.text.default}>
esc <span style={{ fg: themeV2.text.subdued }}>cancel</span>
<text fg={theme.text.default}>
esc <span style={{ fg: theme.text.subdued }}>cancel</span>
</text>
</box>
</box>
@ -429,7 +429,7 @@ function Prompt<const T extends Record<string, string>>(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<const T extends Record<string, string>>(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<const T extends Record<string, string>>(props: {
when={props.header}
fallback={
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
<text fg={themeV2.text.feedback.warning.default}>{"△"}</text>
<text fg={themeV2.text.default}>{props.title}</text>
<text fg={theme.text.feedback.warning.default}>{"△"}</text>
<text fg={theme.text.default}>{props.title}</text>
</box>
}
>
@ -573,7 +573,7 @@ function Prompt<const T extends Record<string, string>>(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<const T extends Record<string, string>>(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<const T extends Record<string, string>>(props: {
}}
>
<text
fg={
option === store.selected
? themeV2.text.action.primary.focused
: themeV2.text.action.primary.default
}
fg={option === store.selected ? theme.text.action.primary.focused : theme.text.action.primary.default}
>
{props.options[option]}
</text>
@ -628,15 +624,15 @@ function Prompt<const T extends Record<string, string>>(props: {
</box>
<box flexDirection="row" gap={2} flexShrink={0}>
<Show when={props.fullscreen}>
<text fg={themeV2.text.default}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: themeV2.text.subdued }}>{hint()}</span>
<text fg={theme.text.default}>
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.text.subdued }}>{hint()}</span>
</text>
</Show>
<text fg={themeV2.text.default}>
{"⇆"} <span style={{ fg: themeV2.text.subdued }}>select</span>
<text fg={theme.text.default}>
{"⇆"} <span style={{ fg: theme.text.subdued }}>select</span>
</text>
<text fg={themeV2.text.default}>
enter <span style={{ fg: themeV2.text.subdued }}>confirm</span>
<text fg={theme.text.default}>
enter <span style={{ fg: theme.text.subdued }}>confirm</span>
</text>
</box>
</box>

View file

@ -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 (
<Show when={session()}>
<box
backgroundColor={themeV2.background.default}
backgroundColor={theme.background.default}
width={42}
height="100%"
paddingTop={1}
@ -32,8 +32,8 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: themeV2.background.default,
foregroundColor: themeV2.scrollbar.default,
backgroundColor: theme.background.default,
foregroundColor: theme.scrollbar.default,
},
}}
>
@ -45,11 +45,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
title={session()!.title}
>
<box paddingRight={1}>
<text fg={themeV2.text.default}>
<text fg={theme.text.default}>
<b>{session()!.title}</b>
</text>
<Show when={session()!.location.workspaceID}>
<text fg={themeV2.text.subdued}>{session()!.location.workspaceID}</text>
<text fg={theme.text.subdued}>{session()!.location.workspaceID}</text>
</Show>
</box>
</pluginRuntime.Slot>

View file

@ -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}
>
<box flexDirection="row" justifyContent="space-between" gap={1}>
<box flexDirection="row" gap={1}>
<text fg={themeV2.text.default}>
<text fg={theme.text.default}>
<b>{subagentInfo()}</b>
</text>
<Show when={usage()}>
{(item) => (
<text fg={themeV2.text.subdued} wrapMode="none">
<text fg={theme.text.subdued} wrapMode="none">
{[item().context, item().cost].filter(Boolean).join(" · ")}
</text>
)}
@ -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
}
>
<text fg={themeV2.text.default}>
Parent <span style={{ fg: themeV2.text.subdued }}>{shortcuts.get("session.parent")}</span>
<text fg={theme.text.default}>
Parent <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.parent")}</span>
</text>
</box>
<box
onMouseOver={() => 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}
>
<text fg={themeV2.text.default}>
Prev <span style={{ fg: themeV2.text.subdued }}>{shortcuts.get("session.child.previous")}</span>
<text fg={theme.text.default}>
Prev <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.child.previous")}</span>
</text>
</box>
<box
onMouseOver={() => 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}
>
<text fg={themeV2.text.default}>
Next <span style={{ fg: themeV2.text.subdued }}>{shortcuts.get("session.child.next")}</span>
<text fg={theme.text.default}>
Next <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.child.next")}</span>
</text>
</box>
</box>