Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/tui/src/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function Prompt(props: PromptProps) {
const renderer = useRenderer()
const exit = useExit()
const dimensions = useTerminalDimensions()
const { themeV2, syntax, mode } = useTheme()
const { themeV2, syntax } = useTheme()
const animationsEnabled = createMemo(() => config.animations ?? true)
const list = createMemo(() => props.placeholders?.normal ?? [])
const shell = createMemo(() => props.placeholders?.shell ?? [])
Expand Down Expand Up @@ -1379,11 +1379,7 @@ export function Prompt(props: PromptProps) {
})
const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3)))

const promptBg = createMemo(() =>
mode() === "light"
? themeV2.increase(themeV2.background.surface.offset(), 1)
: themeV2.decrease(themeV2.background.surface.offset(), 1),
)
const promptBg = createMemo(() => themeV2.raise(themeV2.background.surface.offset()))

return (
<>
Expand Down
7 changes: 4 additions & 3 deletions packages/tui/src/context/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,18 +297,19 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
themePerformance.set("Resolve final theme", duration(performance.now() - resolveStarted))
return result
})
const themeV2 = createComponentTheme(valuesV2)
const mode = () => store.mode
const themeV2 = createComponentTheme(valuesV2, mode)
const contextsV2 = {
elevated: createComponentTheme(() => {
const theme = valuesV2().contexts["@context:elevated"]
if (!theme) throw new Error("Theme context is not defined: elevated")
return theme
}),
}, mode),
overlay: createComponentTheme(() => {
const theme = valuesV2().contexts["@context:overlay"]
if (!theme) throw new Error("Theme context is not defined: overlay")
return theme
}),
}, mode),
}

createEffect(() => renderer.setBackgroundColor(values().background))
Expand Down
44 changes: 19 additions & 25 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import { useLocation } from "../../context/location"
import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./rows"
import { switchLabel } from "../../util/model"
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
import type { ComponentTheme } from "../../theme/v2/component"
import { stringWidth } from "../../util/string-width"

addDefaultParsers(parsers.parsers)
Expand All @@ -100,11 +99,6 @@ function use() {
return ctx
}

function offsetBackground(themeV2: ComponentTheme, mode: "light" | "dark") {
if (mode === "light") return themeV2.increase(themeV2.background.surface.offset(), 1)
return themeV2.decrease(themeV2.background.surface.offset(), 1)
}

export function Session() {
const setEpilogue = useEpilogue()
const clipboard = useClipboard()
Expand All @@ -119,7 +113,7 @@ export function Session() {
const paths = useTuiPaths()
const configState = useConfig()
const config = configState.data
const { themeV2, mode } = useTheme()
const { themeV2 } = useTheme()
const promptRef = usePromptRef()
const session = createMemo(() => data.session.get(route.sessionID))
const messages = () => data.session.message.list(route.sessionID)
Expand Down Expand Up @@ -898,7 +892,7 @@ export function Session() {
paddingLeft: 1,
visible: showScrollbar(),
trackOptions: {
backgroundColor: offsetBackground(themeV2, mode()),
backgroundColor: themeV2.raise(themeV2.background.surface.offset()),
foregroundColor: themeV2.border(),
},
}}
Expand Down Expand Up @@ -1140,7 +1134,7 @@ function SessionReasoningGroupView(props: {
message: (messageID: string) => SessionMessageInfo | undefined
}) {
const ctx = use()
const { themeV2, syntax, mode } = useTheme()
const { themeV2, syntax } = useTheme()
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
Expand Down Expand Up @@ -1229,7 +1223,7 @@ function SessionReasoningGroupView(props: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={offsetBackground(themeV2, mode())}
borderColor={themeV2.raise(themeV2.background.surface.offset())}
paddingLeft={1}
>
<code
Expand Down Expand Up @@ -1513,7 +1507,7 @@ function RevertMessage(props: {
}>
}) {
const ctx = use()
const { themeV2, mode } = useTheme().contextual("elevated")
const { themeV2 } = useTheme().contextual("elevated")
const route = useRouteData("session")
const client = useClient()
const toast = useToast()
Expand Down Expand Up @@ -1544,7 +1538,7 @@ function RevertMessage(props: {
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={hover() ? offsetBackground(themeV2, mode()) : themeV2.background()}
backgroundColor={hover() ? themeV2.raise(themeV2.background()) : themeV2.background()}
>
<text fg={themeV2.text.subdued()}>
{props.count} message{props.count === 1 ? "" : "s"} reverted
Expand Down Expand Up @@ -1613,7 +1607,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
const data = useData()
const local = useLocal()
const files = createMemo(() => props.message.files ?? [])
const { themeV2, mode } = useTheme().contextual("elevated")
const { themeV2 } = useTheme().contextual("elevated")
const [hover, setHover] = createSignal(false)
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
const queued = createMemo(
Expand Down Expand Up @@ -1650,7 +1644,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={hover() ? offsetBackground(themeV2, mode()) : themeV2.background()}
backgroundColor={hover() ? themeV2.raise(themeV2.background()) : themeV2.background()}
flexShrink={0}
>
<text fg={themeV2.text()}>{props.message.text}</text>
Expand All @@ -1670,7 +1664,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
>
{` ${label} `}
</span>
<span style={{ bg: offsetBackground(themeV2, mode()), fg: themeV2.text.subdued() }}>
<span style={{ bg: themeV2.raise(themeV2.background()), fg: themeV2.text.subdued() }}>
{" "}
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
</span>
Expand Down Expand Up @@ -1858,7 +1852,7 @@ function ReasoningPart(props: {
part: SessionMessageAssistantReasoning
message: SessionMessageAssistant
}) {
const { themeV2, syntax, mode } = useTheme()
const { themeV2, syntax } = useTheme()
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.
Expand Down Expand Up @@ -1886,7 +1880,7 @@ function ReasoningPart(props: {
<box
border={!inMinimal() || expanded() ? ["left"] : undefined}
customBorderChars={SplitBorder.customBorderChars}
borderColor={offsetBackground(themeV2, mode())}
borderColor={themeV2.raise(themeV2.background())}
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
>
<box onMouseUp={toggle}>
Expand All @@ -1904,7 +1898,7 @@ function ReasoningPart(props: {
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={offsetBackground(themeV2, mode())}
borderColor={themeV2.raise(themeV2.background())}
paddingLeft={inMinimal() ? 3 : 1}
>
<code
Expand Down Expand Up @@ -2081,7 +2075,7 @@ type ToolProps = {
part: SessionMessageAssistantTool
}
function GenericTool(props: ToolProps) {
const { themeV2, syntax, mode } = useTheme()
const { themeV2, syntax } = useTheme()
const output = createMemo(() => props.output?.trim() ?? "")
const args = createMemo(() => JSON.stringify(props.input, null, 2))
const [expanded, setExpanded] = createSignal(false)
Expand All @@ -2099,7 +2093,7 @@ function GenericTool(props: ToolProps) {
<Show when={Object.keys(props.input).length > 0}>
<box gap={1}>
<text>
<span style={{ bg: offsetBackground(themeV2, mode()), fg: themeV2.text.subdued() }}> Input </span>
<span style={{ bg: themeV2.raise(themeV2.background()), fg: themeV2.text.subdued() }}> Input </span>
</text>
<box paddingLeft={1}>
<code
Expand All @@ -2117,7 +2111,7 @@ function GenericTool(props: ToolProps) {
{(value) => (
<box gap={1}>
<text>
<span style={{ bg: offsetBackground(themeV2, mode()), fg: themeV2.text.subdued() }}> Output </span>
<span style={{ bg: themeV2.raise(themeV2.background()), fg: themeV2.text.subdued() }}> Output </span>
</text>
<box paddingLeft={1}>
<text fg={themeV2.text()} wrapMode="word">
Expand Down Expand Up @@ -2305,9 +2299,9 @@ function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.El
}

function StatusBadge(props: { children: string }) {
const { themeV2, mode } = useTheme()
const { themeV2 } = useTheme()
return (
<text flexShrink={0} bg={offsetBackground(themeV2, mode())} fg={themeV2.text.subdued()}>
<text flexShrink={0} bg={themeV2.raise(themeV2.background())} fg={themeV2.text.subdued()}>
{" "}
{props.children}{" "}
</text>
Expand All @@ -2322,7 +2316,7 @@ function BlockTool(props: {
part?: SessionMessageAssistantTool
spinner?: boolean
}) {
const { themeV2, mode } = useTheme().contextual("elevated")
const { themeV2 } = useTheme().contextual("elevated")
const ctx = use()
const data = useData()
const renderer = useRenderer()
Expand All @@ -2341,7 +2335,7 @@ function BlockTool(props: {
paddingLeft={2}
gap={1}
backgroundColor={
hover() ? offsetBackground(themeV2, mode()) : themeV2.background()
hover() ? themeV2.raise(themeV2.background()) : themeV2.background()
}
customBorderChars={SplitBorder.customBorderChars}
borderColor={themeV2.background()}
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/routes/session/permission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
paddingLeft={2}
paddingRight={3}
paddingBottom={1}
backgroundColor={themeV2.background.action("focused")}
backgroundColor={themeV2.raise(themeV2.background())}
justifyContent={narrow() ? "flex-start" : "space-between"}
alignItems={narrow() ? "flex-start" : "center"}
gap={1}
Expand Down Expand Up @@ -693,7 +693,7 @@ function Prompt<const T extends Record<string, string>>(props: {
paddingLeft={2}
paddingRight={3}
paddingBottom={1}
backgroundColor={themeV2.background.action("focused")}
backgroundColor={themeV2.raise(themeV2.background())}
justifyContent={narrow() ? "flex-start" : "space-between"}
alignItems={narrow() ? "flex-start" : "center"}
>
Expand Down
4 changes: 3 additions & 1 deletion packages/tui/src/theme/v2/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { RGBA } from "@opentui/core"
import type { Accessor } from "solid-js"
import type {
ActionVariant,
Mode,
ResolvedActionState,
ResolvedThemeView,
} from "./index"
import { ActionState, HueStep } from "./schema"

type StateFlags = Partial<Record<ActionState, boolean>>

export function createComponentTheme(current: Accessor<ResolvedThemeView>) {
export function createComponentTheme(current: Accessor<ResolvedThemeView>, mode: Accessor<Mode>) {
const textAction = actions((variant, state) => current().text.action[variant][state])
const backgroundAction = actions((variant, state) => current().background.action[variant][state])
const textFormfield = formfield((state) => current().text.formfield[state])
Expand Down Expand Up @@ -78,6 +79,7 @@ export function createComponentTheme(current: Accessor<ResolvedThemeView>) {
hue,
increase: (color: RGBA, amount = 1) => current().increase(color, amount),
decrease: (color: RGBA, amount = 1) => current().decrease(color, amount),
raise: (color: RGBA) => (mode() === "light" ? current().increase(color) : current().decrease(color)),
text,
background,
border: () => current().border.default,
Expand Down
10 changes: 6 additions & 4 deletions packages/tui/src/theme/v2/solid.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { createComponent, createContext, useContext, type Accessor, type ParentProps } from "solid-js"
import { createComponentTheme, type ComponentTheme } from "./component"
import type { ContextKey, ResolvedTheme } from "./index"
import type { ContextKey, Mode, ResolvedTheme } from "./index"

type ThemeRuntime = {
readonly resolved: Accessor<ResolvedTheme>
readonly mode: Accessor<Mode>
readonly component: ComponentTheme
}

const ThemeContext = createContext<ThemeRuntime>()

export function ThemeProvider(props: ParentProps<{ theme: ResolvedTheme }>) {
export function ThemeProvider(props: ParentProps<{ theme: ResolvedTheme; mode?: Mode }>) {
const resolved = () => props.theme
const mode = () => props.mode ?? "light"
return createComponent(ThemeContext.Provider, {
value: { resolved, component: createComponentTheme(resolved) },
value: { resolved, mode, component: createComponentTheme(resolved, mode) },
get children() {
return props.children
},
Expand All @@ -28,7 +30,7 @@ export function ContextProvider(props: ParentProps<{ context: ContextKey }>) {
}
context()
return createComponent(ThemeContext.Provider, {
value: { resolved: parent.resolved, component: createComponentTheme(context) },
value: { resolved: parent.resolved, mode: parent.mode, component: createComponentTheme(context, parent.mode) },
get children() {
return props.children
},
Expand Down
6 changes: 5 additions & 1 deletion packages/tui/test/theme/v2/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ import type { ContextKey } from "../../../src/theme/v2"

test("provides reactive property, variant, state, and context accessors", () => {
const [resolved, setResolved] = createSignal(resolveTheme(selectTheme(DEFAULT_THEME, "light")))
const [mode, setMode] = createSignal<"light" | "dark">("light")
const [context, setContext] = createSignal<ContextKey>()
const theme = createComponentTheme(() => {
const key = context()
return key ? resolved().contexts[key] ?? resolved() : resolved()
})
}, mode)

expect(theme.text()).toBe(resolved().text.default)
expect(theme.hue.accent(500)).toBe(resolved().hue.accent[500])
expect(theme.hue.interactive(500)).toBe(resolved().hue.interactive[500])
expect(theme.hue.gray(200)).toBe(resolved().hue.gray[200])
expect(theme.increase(theme.background.surface.offset(), 1)).toBe(resolved().hue.neutral[300])
expect(theme.raise(theme.background.surface.offset())).toBe(resolved().hue.neutral[300])
expect(theme.decrease(theme.hue.red(300), 2)).toBe(resolved().hue.red[100])
expect(theme.increase(theme.hue.red(900), 3)).toBe(resolved().hue.red[900])
expect(theme.decrease(theme.hue.red(100), 3)).toBe(resolved().hue.red[100])
Expand Down Expand Up @@ -81,6 +83,8 @@ test("provides reactive property, variant, state, and context accessors", () =>
)

setResolved(resolveTheme(selectTheme(DEFAULT_THEME, "dark")))
setMode("dark")
expect(theme.text()).toBe(resolved().contexts["@context:elevated"]!.text.default)
expect(theme.decrease(theme.background.surface.offset(), 1)).toBe(resolved().hue.neutral[700])
expect(theme.raise(theme.background.surface.offset())).toBe(resolved().hue.neutral[700])
})
Loading