From ac1b802820fc931f890ae764bf13768fa299a32c Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 16 Jul 2026 16:20:16 -0400 Subject: [PATCH] fix(tui): restore selected action styling, fix subagent list (#37371) --- .../src/routes/session/composer/shell-tab.tsx | 11 +- .../routes/session/composer/subagents-tab.tsx | 27 +- packages/tui/src/theme/v2/defaults.ts | 278 ++++++++++-------- packages/tui/src/theme/v2/schema.ts | 3 +- packages/tui/src/theme/v2/v1-migrate.ts | 9 +- packages/tui/test/theme/v2/component.test.ts | 2 + packages/tui/test/theme/v2/resolve.test.ts | 2 + packages/tui/test/theme/v2/types.test.ts | 7 +- packages/tui/test/theme/v2/v1-migrate.test.ts | 3 + 9 files changed, 187 insertions(+), 155 deletions(-) diff --git a/packages/tui/src/routes/session/composer/shell-tab.tsx b/packages/tui/src/routes/session/composer/shell-tab.tsx index ee43b53161..00dd83d380 100644 --- a/packages/tui/src/routes/session/composer/shell-tab.tsx +++ b/packages/tui/src/routes/session/composer/shell-tab.tsx @@ -13,7 +13,6 @@ export function ShellTab(props: { sessionID: string }) { const location = useLocation() const client = useClient() const { themeV2 } = useTheme() - const fg = themeV2.text.action.primary("focused") const composer = useComposerTab() const shortcuts = Keymap.useShortcuts() @@ -96,11 +95,7 @@ export function ShellTab(props: { sessionID: string }) { return ( - (scroll = r)} - > + (scroll = r)}> 0} fallback={ No shell commands}> {(shell, index) => { @@ -110,11 +105,11 @@ export function ShellTab(props: { sessionID: string }) { flexDirection="row" paddingLeft={1} paddingRight={1} - backgroundColor={active() ? themeV2.background.action.primary() : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? themeV2.background.action.primary("selected") : RGBA.fromInts(0, 0, 0, 0)} 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 7e92aa975e..f96cb2898c 100644 --- a/packages/tui/src/routes/session/composer/subagents-tab.tsx +++ b/packages/tui/src/routes/session/composer/subagents-tab.tsx @@ -22,7 +22,6 @@ export function SubagentsTab(props: { sessionID: string }) { const data = useData() const client = useClient() const { themeV2 } = useTheme() - const fg = themeV2.text.action.primary("focused") const navigate = useRoute().navigate const composer = useComposerTab() const shortcuts = Keymap.useShortcuts() @@ -39,7 +38,11 @@ export function SubagentsTab(props: { sessionID: string }) { const siblings = data.session.list().filter((s) => s.parentID === current.parentID) for (const sibling of siblings) { const agentMatch = sibling.title.match(/@(\w+) subagent/) - const agent = sibling.agent ? Locale.titlecase(sibling.agent) : agentMatch ? Locale.titlecase(agentMatch[1]) : "Subagent" + const agent = sibling.agent + ? Locale.titlecase(sibling.agent) + : agentMatch + ? Locale.titlecase(agentMatch[1]) + : "Subagent" const name = agentMatch ? sibling.title.replace(agentMatch[0], "").trim() || sibling.title : sibling.title result.push({ sessionID: sibling.id, @@ -53,7 +56,11 @@ export function SubagentsTab(props: { sessionID: string }) { const children = data.session.list().filter((s) => s.parentID === props.sessionID) for (const child of children) { const agentMatch = child.title.match(/@(\w+) subagent/) - const agent = child.agent ? Locale.titlecase(child.agent) : agentMatch ? Locale.titlecase(agentMatch[1]) : "Subagent" + const agent = child.agent + ? Locale.titlecase(child.agent) + : agentMatch + ? Locale.titlecase(agentMatch[1]) + : "Subagent" const name = agentMatch ? child.title.replace(agentMatch[0], "").trim() || child.title : child.title result.push({ sessionID: child.id, @@ -195,11 +202,7 @@ export function SubagentsTab(props: { sessionID: string }) { return ( - (scroll = r)} - > + (scroll = r)}> 0} fallback={ No subagents}> {(entry, index) => { @@ -213,7 +216,7 @@ export function SubagentsTab(props: { sessionID: string }) { flexDirection="row" paddingLeft={1} paddingRight={1} - backgroundColor={active() ? themeV2.background.action.primary() : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={themeV2.background.action.primary(active() ? "focused" : "default")} onMouseOver={() => setStore("selected", index())} onMouseUp={() => { setStore("selected", index()) @@ -222,7 +225,9 @@ export function SubagentsTab(props: { sessionID: string }) { > @@ -230,7 +235,7 @@ export function SubagentsTab(props: { sessionID: string }) { - + {status()} diff --git a/packages/tui/src/theme/v2/defaults.ts b/packages/tui/src/theme/v2/defaults.ts index c1dcdec3a9..5b1364c997 100644 --- a/packages/tui/src/theme/v2/defaults.ts +++ b/packages/tui/src/theme/v2/defaults.ts @@ -97,76 +97,87 @@ export const DEFAULT_THEME = { neutral: "$hue.gray", }, text: { + default: "$hue.neutral.900", + 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: { default: "$hue.neutral.900", - 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: { - default: "$hue.neutral.900", - $focused: "$text.action.primary.default", - $pressed: "$hue.neutral.100", - $disabled: "$hue.neutral.500", - $selected: "$hue.interactive.600", - }, - feedback: { - error: { default: "$hue.red.700", subdued: "$hue.red.600" }, - warning: { default: "$hue.yellow.800", subdued: "$hue.yellow.700" }, - success: { default: "$hue.green.700", subdued: "$hue.green.600" }, - info: { default: "$hue.cyan.700", subdued: "$hue.cyan.600" }, - }, + $focused: "$text.action.primary.default", + $pressed: "$hue.neutral.100", + $disabled: "$hue.neutral.500", + $selected: "$hue.interactive.600", }, - background: { - default: "$hue.neutral.100", - surface: { - offset: "$hue.neutral.200", - overlay: "$hue.neutral.300", - }, - action: { - primary: { - default: "$hue.interactive.600", $focused: "$hue.interactive.700", $pressed: "$hue.interactive.800", - $disabled: "$hue.neutral.300", - }, - secondary: { - default: "$hue.neutral.200", $focused: "$hue.neutral.300", $pressed: "$hue.neutral.400", - $disabled: "$hue.neutral.200", - }, - destructive: { - default: "$hue.red.600", $focused: "$hue.red.700", $pressed: "$hue.red.800", - $disabled: "$hue.neutral.300", - }, - }, - formfield: { - default: "$background.default", - $focused: "$background.action.primary.default", + feedback: { + error: { default: "$hue.red.700", subdued: "$hue.red.600" }, + warning: { default: "$hue.yellow.800", subdued: "$hue.yellow.700" }, + success: { default: "$hue.green.700", subdued: "$hue.green.600" }, + info: { default: "$hue.cyan.700", subdued: "$hue.cyan.600" }, + }, + }, + background: { + default: "$hue.neutral.100", + surface: { + offset: "$hue.neutral.200", + overlay: "$hue.neutral.300", + }, + action: { + primary: { + default: "$hue.interactive.600", + $focused: "$hue.interactive.700", $pressed: "$hue.interactive.800", - $disabled: "$background.default", - $selected: "$background.formfield.default", + $selected: "$hue.interactive.700", + $disabled: "$hue.neutral.300", }, - feedback: { - error: { default: "$background.default" }, - warning: { default: "$background.default" }, - success: { default: "$background.default" }, - info: { default: "$background.default" }, + 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", + $pressed: "$hue.red.800", + $selected: "$hue.red.700", + $disabled: "$hue.neutral.300", }, }, - border: { default: "$hue.neutral.300" }, - scrollbar: { default: "$hue.neutral.400" }, - diff: { - text: { - added: "$hue.green.700", removed: "$hue.red.700", context: "$hue.neutral.900", - hunkHeader: "$hue.purple.600", - }, - background: { added: "$hue.green.100", removed: "$hue.red.100", context: "$hue.neutral.100" }, - highlight: { added: "$hue.green.600", removed: "$hue.red.600" }, - lineNumber: { - text: "$hue.neutral.600", - background: { added: "$hue.green.200", removed: "$hue.red.200" }, - }, + formfield: { + default: "$background.default", + $focused: "$background.action.primary.default", + $pressed: "$hue.interactive.800", + $disabled: "$background.default", + $selected: "$background.formfield.default", }, - syntax: { + feedback: { + error: { default: "$background.default" }, + warning: { default: "$background.default" }, + success: { default: "$background.default" }, + info: { default: "$background.default" }, + }, + }, + border: { default: "$hue.neutral.300" }, + scrollbar: { default: "$hue.neutral.400" }, + diff: { + text: { + added: "$hue.green.700", + removed: "$hue.red.700", + context: "$hue.neutral.900", + hunkHeader: "$hue.purple.600", + }, + background: { added: "$hue.green.100", removed: "$hue.red.100", context: "$hue.neutral.100" }, + highlight: { added: "$hue.green.600", removed: "$hue.red.600" }, + lineNumber: { + text: "$hue.neutral.600", + background: { added: "$hue.green.200", removed: "$hue.red.200" }, + }, + }, + syntax: { comment: "$hue.neutral.600", keyword: "$hue.purple.600", function: "$hue.accent.600", @@ -177,7 +188,7 @@ export const DEFAULT_THEME = { operator: "$hue.cyan.600", punctuation: "$hue.neutral.900", }, - markdown: { + markdown: { text: "$hue.neutral.900", heading: "$hue.purple.600", link: "$hue.accent.600", @@ -303,76 +314,87 @@ export const DEFAULT_THEME = { neutral: "$hue.gray", }, text: { + default: "$hue.neutral.100", + 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: { default: "$hue.neutral.100", - 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: { - default: "$hue.neutral.100", - $focused: "$text.action.primary.default", - $pressed: "$hue.neutral.100", - $disabled: "$hue.neutral.500", - $selected: "$hue.interactive.500", - }, - feedback: { - error: { default: "$hue.red.300", subdued: "$hue.red.400" }, - warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" }, - success: { default: "$hue.green.300", subdued: "$hue.green.400" }, - info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" }, - }, + $focused: "$text.action.primary.default", + $pressed: "$hue.neutral.100", + $disabled: "$hue.neutral.500", + $selected: "$hue.interactive.500", }, - background: { - default: "$hue.neutral.900", - surface: { - offset: "$hue.neutral.800", - overlay: "$hue.neutral.700", - }, - action: { - primary: { - default: "$hue.interactive.500", $focused: "$hue.interactive.600", $pressed: "$hue.interactive.800", - $disabled: "$hue.neutral.800", - }, - secondary: { - default: "$hue.neutral.800", $focused: "$hue.neutral.700", $pressed: "$hue.neutral.900", - $disabled: "$hue.neutral.900", - }, - destructive: { - default: "$hue.red.600", $focused: "$hue.red.700", $pressed: "$hue.red.800", - $disabled: "$hue.neutral.800", - }, - }, - formfield: { - default: "$background.default", - $focused: "$background.action.primary.default", + feedback: { + error: { default: "$hue.red.300", subdued: "$hue.red.400" }, + warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" }, + success: { default: "$hue.green.300", subdued: "$hue.green.400" }, + info: { default: "$hue.cyan.300", subdued: "$hue.cyan.400" }, + }, + }, + background: { + default: "$hue.neutral.900", + surface: { + offset: "$hue.neutral.800", + overlay: "$hue.neutral.700", + }, + action: { + primary: { + default: "$hue.interactive.500", + $focused: "$hue.interactive.600", $pressed: "$hue.interactive.800", - $disabled: "$background.default", - $selected: "$background.formfield.default", + $selected: "$hue.interactive.600", + $disabled: "$hue.neutral.800", }, - feedback: { - error: { default: "$background.default" }, - warning: { default: "$background.default" }, - success: { default: "$background.default" }, - info: { default: "$background.default" }, + 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", + $pressed: "$hue.red.800", + $selected: "$hue.red.700", + $disabled: "$hue.neutral.800", }, }, - border: { default: "$hue.neutral.700" }, - scrollbar: { default: "$hue.neutral.600" }, - diff: { - text: { - added: "$hue.green.300", removed: "$hue.red.300", context: "$hue.neutral.100", - hunkHeader: "$hue.purple.400", - }, - background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" }, - highlight: { added: "$hue.green.400", removed: "$hue.red.400" }, - lineNumber: { - text: "$hue.neutral.400", - background: { added: "$hue.green.800", removed: "$hue.red.800" }, - }, + formfield: { + default: "$background.default", + $focused: "$background.action.primary.default", + $pressed: "$hue.interactive.800", + $disabled: "$background.default", + $selected: "$background.formfield.default", }, - syntax: { + feedback: { + error: { default: "$background.default" }, + warning: { default: "$background.default" }, + success: { default: "$background.default" }, + info: { default: "$background.default" }, + }, + }, + border: { default: "$hue.neutral.700" }, + scrollbar: { default: "$hue.neutral.600" }, + diff: { + text: { + added: "$hue.green.300", + removed: "$hue.red.300", + context: "$hue.neutral.100", + hunkHeader: "$hue.purple.400", + }, + background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" }, + highlight: { added: "$hue.green.400", removed: "$hue.red.400" }, + lineNumber: { + text: "$hue.neutral.400", + background: { added: "$hue.green.800", removed: "$hue.red.800" }, + }, + }, + syntax: { comment: "$hue.neutral.400", keyword: "$hue.purple.400", function: "$hue.accent.400", @@ -383,7 +405,7 @@ export const DEFAULT_THEME = { operator: "$hue.cyan.400", punctuation: "$hue.neutral.100", }, - markdown: { + markdown: { text: "$hue.neutral.100", heading: "$hue.purple.400", link: "$hue.accent.400", diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 26f032f2c9..f863077c4e 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -12,7 +12,7 @@ export type HueAlias = Schema.Schema.Type export const ActionVariant = Schema.Literals(["primary", "secondary", "destructive"]) export type ActionVariant = Schema.Schema.Type -export const ActionState = Schema.Literals(["focused", "pressed", "disabled"]) +export const ActionState = Schema.Literals(["focused", "pressed", "selected", "disabled"]) export type ActionState = Schema.Schema.Type export type ActionStateKey = `$${ActionState}` @@ -77,6 +77,7 @@ const StatefulColorDefinition = Schema.Struct({ default: Schema.optional(ColorValue), $focused: Schema.optional(ColorValue), $pressed: Schema.optional(ColorValue), + $selected: Schema.optional(ColorValue), $disabled: Schema.optional(ColorValue), }) export type StatefulColorDefinition = Schema.Schema.Type diff --git a/packages/tui/src/theme/v2/v1-migrate.ts b/packages/tui/src/theme/v2/v1-migrate.ts index 70129419d4..d6a2e46596 100644 --- a/packages/tui/src/theme/v2/v1-migrate.ts +++ b/packages/tui/src/theme/v2/v1-migrate.ts @@ -54,6 +54,7 @@ function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] { default: "$text.default", $disabled: textMuted, $focused: selected, + $selected: primary, }, secondary: { default: "$text.default", @@ -82,7 +83,7 @@ function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] { overlay: backgroundMenu, }, action: { - primary: { default: "transparent", $focused: primary }, + primary: { default: "transparent", $focused: primary, $selected: primary }, secondary: { default: "$background.default", $focused: color("backgroundElement"), @@ -150,11 +151,7 @@ function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] { imageText: color("markdownImageText"), codeBlock: color("markdownCodeBlock"), }, - "@context:elevated": { - background: { - default: "$background.surface.offset", - }, - }, + "@context:elevated": { background: { default: "$background.surface.offset" } }, "@context:overlay": { background: { default: "$background.surface.overlay" } }, } } diff --git a/packages/tui/test/theme/v2/component.test.ts b/packages/tui/test/theme/v2/component.test.ts index cb0b46c800..a65a2e5577 100644 --- a/packages/tui/test/theme/v2/component.test.ts +++ b/packages/tui/test/theme/v2/component.test.ts @@ -21,6 +21,8 @@ test("provides reactive property, variant, state, and context accessors", () => 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.secondary("disabled")).toBe( resolved().background.action.secondary.disabled, ) diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index e029aa23b1..ccbf4d33b1 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -139,7 +139,9 @@ test("resolves matched action variants and states", () => { const theme = resolveTheme(light) expect(theme.text.action.primary.pressed).toBeInstanceOf(RGBA) + 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) }) diff --git a/packages/tui/test/theme/v2/types.test.ts b/packages/tui/test/theme/v2/types.test.ts index eec8f7d9d3..a90a7a4491 100644 --- a/packages/tui/test/theme/v2/types.test.ts +++ b/packages/tui/test/theme/v2/types.test.ts @@ -19,7 +19,11 @@ const background = { default: "$hue.neutral.100", surface: { offset: "$hue.neutral.200", overlay: "$hue.neutral.300" }, action: { - primary: { default: "$hue.interactive.600", $pressed: "$hue.interactive.800" }, + primary: { + default: "$hue.interactive.600", + $pressed: "$hue.interactive.800", + $selected: "$hue.interactive.700", + }, secondary: { default: "$hue.neutral.200" }, destructive: { default: "$hue.red.600" }, }, @@ -45,6 +49,7 @@ test("supports property-first definitions, variants, states, and contexts", () = expect(text.action.primary.$pressed).toBe("$hue.neutral.200") expect(text.formfield.$selected).toBe("$hue.neutral.100") expect(background.action.destructive.default).toBe("$hue.red.600") + expect(background.action.primary.$selected).toBe("$hue.interactive.700") expect(background.surface.offset).toBe("$hue.neutral.200") expect(definition["@context:elevated"].text?.default).toBe("$hue.neutral.800") expect(definition["@context:overlay"].background?.default).toBe("$hue.neutral.300") diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index d9c00c0973..0c0fb8d203 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -26,6 +26,7 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { expect(migrated.dark.background?.surface?.offset).toBe("$hue.neutral.800") expect(migrated.dark.background?.surface?.overlay).toBe("$hue.neutral.700") expect(migrated.light.text?.action?.primary?.default).toBe("$text.default") + expect(migrated.light.background?.action?.primary?.$selected).toBe("$hue.interactive.900") 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)) @@ -39,6 +40,8 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { expect(resolved.text.formfield.focused.toInts()).toEqual(legacy.primary.toInts()) expect(resolved.hue.accent[900].toInts()).toEqual(legacy.accent.toInts()) expect(resolved.hue.interactive[900].toInts()).toEqual(legacy.primary.toInts()) + expect(resolved.background.action.primary.selected.toInts()).toEqual(legacy.primary.toInts()) + expect(resolved.text.action.primary.selected.toInts()).toEqual(legacy.primary.toInts()) expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts()) expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual( legacy.backgroundPanel.toInts(),