fix(tui): restore selected action styling, fix subagent list (#37371)

This commit is contained in:
James Long 2026-07-16 16:20:16 -04:00 committed by GitHub
commit ac1b802820
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 187 additions and 155 deletions

View file

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

View file

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

View file

@ -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",

View file

@ -12,7 +12,7 @@ export type HueAlias = Schema.Schema.Type<typeof HueAlias>
export const ActionVariant = Schema.Literals(["primary", "secondary", "destructive"])
export type ActionVariant = Schema.Schema.Type<typeof ActionVariant>
export const ActionState = Schema.Literals(["focused", "pressed", "disabled"])
export const ActionState = Schema.Literals(["focused", "pressed", "selected", "disabled"])
export type ActionState = Schema.Schema.Type<typeof ActionState>
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<typeof StatefulColorDefinition>

View file

@ -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" } },
}
}

View file

@ -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,
)

View file

@ -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)
})

View file

@ -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")

View file

@ -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(),