diff --git a/packages/tui/src/component/devtools-sidebar.tsx b/packages/tui/src/component/devtools-sidebar.tsx index e47edb9a36..677e6691c9 100644 --- a/packages/tui/src/component/devtools-sidebar.tsx +++ b/packages/tui/src/component/devtools-sidebar.tsx @@ -20,7 +20,7 @@ export function DevToolsSidebar() { {(group) => ( - + {group.title} diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index d578f70e54..6b076d394d 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -187,7 +187,7 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const exit = useExit() const dimensions = useTerminalDimensions() - const { themeV2, syntax } = useTheme() + const { themeV2, syntax, mode } = useTheme() const animationsEnabled = createMemo(() => config.animations ?? true) const list = createMemo(() => props.placeholders?.normal ?? []) const shell = createMemo(() => props.placeholders?.shell ?? []) @@ -1319,7 +1319,7 @@ export function Prompt(props: PromptProps) { const highlight = createMemo(() => { if (leader()) return themeV2.border() - if (store.mode === "shell") return themeV2.background.action.primary() + if (store.mode === "shell") return themeV2.background.action() const agent = local.agent.current() if (!agent) return themeV2.border() return local.agent.color(agent.id) @@ -1378,6 +1378,12 @@ 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), + ) + return ( <> (anchor = r)} visible={props.visible !== false} width="100%"> @@ -1395,7 +1401,7 @@ export function Prompt(props: PromptProps) { paddingRight={2} paddingTop={1} flexShrink={0} - backgroundColor={themeV2.background.action.secondary("focused")} + backgroundColor={promptBg()} flexGrow={1} width="100%" > @@ -1471,10 +1477,8 @@ export function Prompt(props: PromptProps) { if (props.disabled) return r.target?.focus() }} - focusedBackgroundColor={themeV2.background.action.secondary("focused")} - cursorColor={ - props.disabled ? themeV2.background.surface.offset() : themeV2.text() - } + focusedBackgroundColor="transparent" + cursorColor={props.disabled ? themeV2.background.surface.offset() : themeV2.text()} syntaxStyle={syntax()} /> @@ -1493,16 +1497,11 @@ export function Prompt(props: PromptProps) { · {local.model.parsed().model} - - {currentProviderLabel()} - + {currentProviderLabel()} · @@ -1536,15 +1535,15 @@ export function Prompt(props: PromptProps) { borderColor={borderHighlight()} customBorderChars={{ ...EmptyBorder, - vertical: themeV2.background.action.secondary("focused").a !== 0 ? "╹" : " ", + vertical: promptBg().a !== 0 ? "╹" : " ", }} > - [⋯]} - > + [⋯]}> - 0 ? themeV2.background.action.primary() : themeV2.text()} - > + 0 ? themeV2.background.action() : themeV2.text()}> esc{" "} 0 - ? themeV2.background.action.primary() - : themeV2.text.subdued(), + fg: store.interrupt > 0 ? themeV2.background.action() : themeV2.text.subdued(), }} > {store.interrupt > 0 ? "again to interrupt" : "interrupt"} @@ -1613,13 +1604,7 @@ export function Prompt(props: PromptProps) { {(file) => ( - + {file()} )} diff --git a/packages/tui/src/routes/session/composer/shell-tab.tsx b/packages/tui/src/routes/session/composer/shell-tab.tsx index 9d44480740..edffaadefa 100644 --- a/packages/tui/src/routes/session/composer/shell-tab.tsx +++ b/packages/tui/src/routes/session/composer/shell-tab.tsx @@ -105,11 +105,11 @@ export function ShellTab(props: { sessionID: string }) { flexDirection="row" paddingLeft={1} paddingRight={1} - backgroundColor={themeV2.background.action.primary({ focused: active() })} + backgroundColor={themeV2.background.action({ focused: active() })} onMouseOver={() => setStore("selected", index())} > diff --git a/packages/tui/src/routes/session/composer/subagents-tab.tsx b/packages/tui/src/routes/session/composer/subagents-tab.tsx index 27e117754d..5be6c6c767 100644 --- a/packages/tui/src/routes/session/composer/subagents-tab.tsx +++ b/packages/tui/src/routes/session/composer/subagents-tab.tsx @@ -216,7 +216,7 @@ export function SubagentsTab(props: { sessionID: string }) { flexDirection="row" paddingLeft={1} paddingRight={1} - backgroundColor={themeV2.background.action.primary({ focused: active(), selected: entry.current })} + backgroundColor={themeV2.background.action({ focused: active(), selected: entry.current })} onMouseOver={() => setStore("selected", index())} onMouseUp={() => { setStore("selected", index()) @@ -225,7 +225,7 @@ export function SubagentsTab(props: { sessionID: string }) { > @@ -236,7 +236,7 @@ export function SubagentsTab(props: { sessionID: string }) { {external().description} { if (renderer.getSelection()?.getSelectedText()) return openExternal() diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 413459dbaa..22cddd98b5 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -891,7 +891,7 @@ export function Session() { paddingLeft: 1, visible: showScrollbar(), trackOptions: { - backgroundColor: themeV2.background.action.secondary("focused"), + backgroundColor: themeV2.background.action("focused"), foregroundColor: themeV2.border(), }, }} @@ -1222,7 +1222,7 @@ function SessionReasoningGroupView(props: { {props.count} message{props.count === 1 ? "" : "s"} reverted @@ -1644,7 +1644,7 @@ function UserMessage(props: { message: SessionMessageUser }) { paddingTop={1} paddingBottom={1} paddingLeft={2} - backgroundColor={hover() ? themeV2.background.action.secondary("focused") : themeV2.background()} + backgroundColor={hover() ? themeV2.background.action("focused") : themeV2.background()} flexShrink={0} > {props.message.text} @@ -1664,7 +1664,7 @@ function UserMessage(props: { message: SessionMessageUser }) { > {` ${label} `} - + {" "} {file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "} @@ -1880,7 +1880,7 @@ function ReasoningPart(props: { @@ -1898,7 +1898,7 @@ function ReasoningPart(props: { 0}> - Input + Input ( - Output + Output @@ -2301,7 +2301,7 @@ function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.El function StatusBadge(props: { children: string }) { const { themeV2 } = useTheme() return ( - + {" "} {props.children}{" "} @@ -2335,7 +2335,7 @@ function BlockTool(props: { paddingLeft={2} gap={1} backgroundColor={ - hover() ? themeV2.background.action.secondary("focused") : themeV2.background() + hover() ? themeV2.background.action("focused") : themeV2.background() } customBorderChars={SplitBorder.customBorderChars} borderColor={themeV2.background()} diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 9fb23e15e0..a5b57daeca 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -516,7 +516,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: ( paddingLeft={2} paddingRight={3} paddingBottom={1} - backgroundColor={themeV2.background.action.secondary("focused")} + backgroundColor={themeV2.background.action("focused")} justifyContent={narrow() ? "flex-start" : "space-between"} alignItems={narrow() ? "flex-start" : "center"} gap={1} @@ -656,7 +656,7 @@ function Prompt>(props: { >(props: { paddingLeft={2} paddingRight={3} paddingBottom={1} - backgroundColor={themeV2.background.action.secondary("focused")} + backgroundColor={themeV2.background.action("focused")} justifyContent={narrow() ? "flex-start" : "space-between"} alignItems={narrow() ? "flex-start" : "center"} > @@ -703,7 +703,7 @@ function Prompt>(props: { setStore("selected", option)} @@ -713,7 +713,7 @@ function Prompt>(props: { }} > diff --git a/packages/tui/src/routes/session/subagent-footer.tsx b/packages/tui/src/routes/session/subagent-footer.tsx index f4e2da0efd..a3da913043 100644 --- a/packages/tui/src/routes/session/subagent-footer.tsx +++ b/packages/tui/src/routes/session/subagent-footer.tsx @@ -83,7 +83,7 @@ export function SubagentFooter() { onMouseOver={() => setHover("parent")} onMouseOut={() => setHover(null)} onMouseUp={() => keymap.dispatch("session.parent")} - backgroundColor={hover() === "parent" ? themeV2.background.action.secondary("focused") : themeV2.background()} + backgroundColor={hover() === "parent" ? themeV2.background.action("focused") : themeV2.background()} > Parent {shortcuts.get("session.parent")} @@ -93,7 +93,7 @@ export function SubagentFooter() { onMouseOver={() => setHover("prev")} onMouseOut={() => setHover(null)} onMouseUp={() => keymap.dispatch("session.child.previous")} - backgroundColor={hover() === "prev" ? themeV2.background.action.secondary("focused") : themeV2.background()} + backgroundColor={hover() === "prev" ? themeV2.background.action("focused") : themeV2.background()} > Prev {shortcuts.get("session.child.previous")} @@ -103,7 +103,7 @@ export function SubagentFooter() { onMouseOver={() => setHover("next")} onMouseOut={() => setHover(null)} onMouseUp={() => keymap.dispatch("session.child.next")} - backgroundColor={hover() === "next" ? themeV2.background.action.secondary("focused") : themeV2.background()} + backgroundColor={hover() === "next" ? themeV2.background.action("focused") : themeV2.background()} > Next {shortcuts.get("session.child.next")} diff --git a/packages/tui/src/theme/v2/component.ts b/packages/tui/src/theme/v2/component.ts index 6b913f285b..10aa45a7ba 100644 --- a/packages/tui/src/theme/v2/component.ts +++ b/packages/tui/src/theme/v2/component.ts @@ -6,9 +6,8 @@ import type { ResolvedActionState, ResolvedFormfieldState, ResolvedThemeView, - HueStep, } from "./index" -import { ActionState } from "./schema" +import { ActionState, HueStep } from "./schema" export type ActionStates = Partial> @@ -79,6 +78,8 @@ export function createComponentTheme(current: Accessor) { return { hue, + increase: (color: RGBA, amount = 1) => shiftHue(current(), color, amount), + decrease: (color: RGBA, amount = 1) => shiftHue(current(), color, -amount), text, background, border: () => current().border.default, @@ -122,6 +123,17 @@ export function createComponentTheme(current: Accessor) { } } +function shiftHue(theme: ResolvedThemeView, color: RGBA, amount: number) { + const colors = Object.values(theme.hue).flatMap((scale) => + HueStep.literals.map((step, index) => ({ color: scale[step], index, scale })), + ) + const match = colors.find((entry) => entry.color === color) ?? colors.find((entry) => entry.color.equals(color)) + if (!match) return color + const offset = Number.isFinite(amount) ? Math.trunc(amount) : 0 + const index = Math.max(0, Math.min(HueStep.literals.length - 1, match.index + offset)) + return match.scale[HueStep.literals[index]] +} + function actions(get: (variant: ActionVariant, state: ResolvedActionState) => RGBA) { const action = (variant: ActionVariant) => (states: ActionState | "default" | ActionStates = "default") => { if (typeof states === "string") return get(variant, states) @@ -129,8 +141,6 @@ function actions(get: (variant: ActionVariant, state: ResolvedActionState) => RG } const primary = action("primary") return Object.assign(primary, { - primary, - secondary: action("secondary"), destructive: action("destructive"), }) } diff --git a/packages/tui/src/theme/v2/defaults.ts b/packages/tui/src/theme/v2/defaults.ts index 5b1364c997..79dcac2be0 100644 --- a/packages/tui/src/theme/v2/defaults.ts +++ b/packages/tui/src/theme/v2/defaults.ts @@ -101,7 +101,6 @@ export const DEFAULT_THEME = { subdued: "$hue.neutral.600", action: { primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" }, - secondary: { default: "$hue.neutral.900", $disabled: "$hue.neutral.500" }, destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" }, }, formfield: { @@ -132,13 +131,6 @@ export const DEFAULT_THEME = { $selected: "$hue.interactive.700", $disabled: "$hue.neutral.300", }, - secondary: { - default: "$hue.neutral.200", - $focused: "$hue.neutral.300", - $pressed: "$hue.neutral.400", - $selected: "$hue.neutral.300", - $disabled: "$hue.neutral.200", - }, destructive: { default: "$hue.red.600", $focused: "$hue.red.700", @@ -318,7 +310,6 @@ export const DEFAULT_THEME = { subdued: "$hue.neutral.400", action: { primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" }, - secondary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" }, destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" }, }, formfield: { @@ -349,13 +340,6 @@ export const DEFAULT_THEME = { $selected: "$hue.interactive.600", $disabled: "$hue.neutral.800", }, - secondary: { - default: "$hue.neutral.800", - $focused: "$hue.neutral.700", - $pressed: "$hue.neutral.900", - $selected: "$hue.neutral.700", - $disabled: "$hue.neutral.900", - }, destructive: { default: "$hue.red.600", $focused: "$hue.red.700", diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 6a5c8c379c..a04653b0bc 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -9,7 +9,7 @@ export type BaseHue = Schema.Schema.Type export const HueAlias = Schema.Literals(["accent", "interactive", "neutral"]) export type HueAlias = Schema.Schema.Type -export const ActionVariant = Schema.Literals(["primary", "secondary", "destructive"]) +export const ActionVariant = Schema.Literals(["primary", "destructive"]) export type ActionVariant = Schema.Schema.Type export const ActionState = Schema.Literals(["disabled", "pressed", "focused", "selected"]) @@ -93,7 +93,6 @@ export type FormfieldColorDefinition = Schema.Schema.Type 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.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]) + const equivalent = RGBA.fromInts(...resolved().hue.green[500].toInts()) + expect(theme.increase(equivalent, 1)).toBe(resolved().hue.green[600]) + const unmatched = RGBA.fromInts(1, 2, 3) + expect(theme.increase(unmatched, 1)).toBe(unmatched) expect(theme.text.subdued()).toBe(resolved().text.subdued) expect(theme.text.action()).toBe(resolved().text.action.primary.default) - expect(theme.text.action.primary("pressed")).toBe(resolved().text.action.primary.pressed) - expect(theme.text.action.primary("selected")).toBe(resolved().text.action.primary.selected) - expect(theme.background.action.primary("selected")).toBe(resolved().background.action.primary.selected) - expect(theme.background.action.primary({ selected: true })).toBe(resolved().background.action.primary.selected) - expect(theme.background.action.primary({ focused: true, selected: true })).toBe( + expect(theme.text.action("pressed")).toBe(resolved().text.action.primary.pressed) + expect(theme.text.action("selected")).toBe(resolved().text.action.primary.selected) + expect(theme.background.action("selected")).toBe(resolved().background.action.primary.selected) + expect(theme.background.action({ selected: true })).toBe(resolved().background.action.primary.selected) + expect(theme.background.action({ focused: true, selected: true })).toBe( resolved().background.action.primary.focused, ) - expect(theme.background.action.primary({ pressed: true, focused: true, selected: true })).toBe( + expect(theme.background.action({ pressed: true, focused: true, selected: true })).toBe( resolved().background.action.primary.pressed, ) - expect(theme.background.action.primary({ disabled: true, pressed: true, focused: true, selected: true })).toBe( + expect(theme.background.action({ disabled: true, pressed: true, focused: true, selected: true })).toBe( resolved().background.action.primary.disabled, ) - expect(theme.background.action.primary({ disabled: false, selected: false })).toBe( + expect(theme.background.action({ disabled: false, selected: false })).toBe( resolved().background.action.primary.default, ) - expect(theme.background.action.secondary("disabled")).toBe( - resolved().background.action.secondary.disabled, + expect(theme.background.action.destructive("disabled")).toBe( + resolved().background.action.destructive.disabled, ) expect(theme.background.surface.offset()).toBe(resolved().background.surface.offset) expect(theme.background.surface.overlay()).toBe(resolved().background.surface.overlay) @@ -46,7 +55,7 @@ test("provides reactive property, variant, state, and context accessors", () => setContext("@context:elevated") expect(theme.text()).toBe(resolved().contexts["@context:elevated"]!.text.default) - expect(theme.background.action.primary("focused")).toBe( + expect(theme.background.action("focused")).toBe( resolved().contexts["@context:elevated"]!.background.action.primary.focused, ) expect(theme.background.formfield("selected")).toBe( diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index ccbf4d33b1..0d8b1dd30d 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -142,7 +142,6 @@ test("resolves matched action variants and states", () => { expect(theme.text.action.primary.selected).toBeInstanceOf(RGBA) expect(theme.background.action.primary.pressed).toBeInstanceOf(RGBA) expect(theme.background.action.primary.selected).toBeInstanceOf(RGBA) - expect(theme.text.action.secondary.default).toBeInstanceOf(RGBA) expect(theme.background.action.destructive.disabled).toBeInstanceOf(RGBA) }) @@ -175,7 +174,6 @@ test("context overrides rewire semantic references and apply state precedence", default: "#111111", action: { primary: { default: "$text.default", $pressed: "#222222" }, - secondary: { default: "$text.default" }, }, }, "@context:elevated": { @@ -189,7 +187,6 @@ test("context overrides rewire semantic references and apply state precedence", const overlay = theme.contexts["@context:elevated"]! expect(overlay.text.default.toInts()).toEqual([51, 51, 51, 255]) - expect(overlay.text.action.secondary.default.toInts()).toEqual([51, 51, 51, 255]) expect(overlay.text.action.primary.pressed.toInts()).toEqual([68, 68, 68, 255]) expect(overlay.text.action.primary.focused.toInts()).toEqual([85, 85, 85, 255]) }) diff --git a/packages/tui/test/theme/v2/types.test.ts b/packages/tui/test/theme/v2/types.test.ts index a90a7a4491..d403319a4e 100644 --- a/packages/tui/test/theme/v2/types.test.ts +++ b/packages/tui/test/theme/v2/types.test.ts @@ -6,7 +6,6 @@ const text = { subdued: "$hue.neutral.600", action: { primary: { default: "$hue.neutral.100", $pressed: "$hue.neutral.200" }, - secondary: { default: "$hue.neutral.900" }, destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" }, }, formfield: { default: "$hue.neutral.600", $selected: "$hue.neutral.100" }, @@ -24,7 +23,6 @@ const background = { $pressed: "$hue.interactive.800", $selected: "$hue.interactive.700", }, - secondary: { default: "$hue.neutral.200" }, destructive: { default: "$hue.red.600" }, }, formfield: { default: "$hue.neutral.100", $selected: "$hue.interactive.600" }, diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index 0c0fb8d203..aaf9782a91 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -30,7 +30,6 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { expect(migrated.light.scrollbar?.default).toBe(hex(legacy.borderActive)) expect(migrated.light.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg)) expect(migrated.light.markdown?.emphasis).toBe(hex(legacy.markdownEmph)) - expect(resolved.background.action.secondary.focused.toInts()).toEqual(legacy.backgroundElement.toInts()) expect(resolved.background.surface.offset.toInts()).toEqual(legacy.backgroundPanel.toInts()) expect(resolved.background.surface.overlay.toInts()).toEqual(legacy.backgroundMenu.toInts()) expect(resolved.background.formfield.selected.toInts()).toEqual(legacy.background.toInts()) @@ -46,9 +45,6 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual( legacy.backgroundPanel.toInts(), ) - expect(resolved.contexts["@context:elevated"]?.background.action.secondary.default.toInts()).toEqual( - legacy.backgroundPanel.toInts(), - ) expect(resolved.contexts["@context:elevated"]?.background.action.primary.default.toInts()).toEqual([0, 0, 0, 0]) expect(resolved.contexts["@context:elevated"]?.text.action.primary.default.toInts()).toEqual( legacy.text.toInts(),