refactor(ui-tui): every selectable list rides the shared selection chip — zero inverse left

/agents, /journey, model picker, skills hub, plugins hub, pet picker, and
the approval/confirm prompts all used SGR inverse for the active row —
terminal-interpreted against unknowable defaults (black slab on transparent
profiles) and visually divergent from completions/session-switcher. New
spreadable chipRowProps(t, active) in overlayPrimitives (chip bg + lifted
ink + bold; spread after `color` so chip ink wins) converts each site to
the one selection treatment. rg confirms zero inverse={} remaining in
components/.
This commit is contained in:
Brooklyn Nicholson 2026-07-20 18:35:56 -05:00
parent ce8c2c97aa
commit 5bfffcd445
8 changed files with 39 additions and 32 deletions

View file

@ -32,6 +32,7 @@ import { compactPreview } from '../lib/text.js'
import type { Theme } from '../theme.js'
import type { SubagentNode, SubagentProgress } from '../types.js'
import { listRowStyle } from './overlayPrimitives.js'
import { OverlayScrollbar } from './overlayScrollbar.js'
// ── Types + lookup tables ────────────────────────────────────────────
@ -464,14 +465,17 @@ function ListRow({
const paren = line ? line.indexOf('(') : -1
const toolShort = line ? (paren > 0 ? line.slice(0, paren) : line).trim() : ''
const trailing = toolShort ? ` · ${compactPreview(toolShort, 14)}` : ''
const fg = active ? t.color.accent : t.color.text
// Selection chip, not `inverse` — inverse swaps against the terminal's
// unknowable defaults (black slab on transparent profiles).
const row = listRowStyle(t, active)
const fg = active ? (row.color ?? t.color.accent) : t.color.text
return (
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
<Text backgroundColor={row.backgroundColor} bold={active} color={fg} wrap="truncate-end">
{' '}
<Text color={active ? fg : t.color.muted}>{formatRowId(index)} </Text>
{indentFor(node.item.depth)}
{heatMarker ? <Text color={heatMarker}></Text> : null}
{heatMarker ? <Text color={active ? fg : heatMarker}></Text> : null}
<Text color={active ? fg : color}>{glyph}</Text> {goal}
<Text color={active ? fg : t.color.muted}>
{toolsCount}

View file

@ -7,6 +7,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
import { deriveStarmapPalette, fadeHex, fadeInk, type StarmapPalette } from '../lib/starmapPalette.js'
import type { Theme } from '../theme.js'
import { listRowStyle } from './overlayPrimitives.js'
import { OverlayScrollbar } from './overlayScrollbar.js'
interface MutationResult {
@ -115,12 +116,14 @@ function ChartRow({ palette, row }: { palette: StarmapPalette; row: Run[] }) {
}
// Full-width selectable row, matching the /agents list treatment: the active
// row inverts and collapses every segment onto the accent foreground.
// row carries the shared selection chip (never `inverse`, which swaps against
// the terminal's unknowable defaults) and collapses onto the chip ink.
function ListRow({ active, cells, t }: { active: boolean; cells: Cell[]; t: Theme }) {
const fg = active ? t.color.accent : t.color.text
const row = listRowStyle(t, active)
const fg = active ? (row.color ?? t.color.accent) : t.color.text
return (
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
<Text backgroundColor={row.backgroundColor} bold={active} color={fg} wrap="truncate-end">
{cells.map((c, i) => (
<Text color={active ? fg : (c.color ?? t.color.text)} key={i}>
{c.text}

View file

@ -10,6 +10,7 @@ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
import type { Theme } from '../theme.js'
import { OverlayHint, useOverlayKeys, windowItems } from './overlayControls.js'
import { chipRowProps } from './overlayPrimitives.js'
const VISIBLE = 12
const MIN_WIDTH = 40
@ -596,9 +597,8 @@ export function ModelPicker({
return row ? (
<Text
bold={providerIdx === idx}
color={providerIdx === idx ? t.color.accent : dimmed ? t.color.label : t.color.muted}
inverse={providerIdx === idx}
color={dimmed ? t.color.label : t.color.muted}
{...chipRowProps(t, providerIdx === idx)}
key={p?.slug ?? `row-${idx}`}
wrap="truncate-end"
>
@ -669,9 +669,8 @@ export function ModelPicker({
return (
<Text
bold={modelIdx === idx}
color={modelIdx === idx ? t.color.accent : t.color.muted}
inverse={modelIdx === idx}
color={t.color.muted}
{...chipRowProps(t, modelIdx === idx)}
key={`${provider?.slug ?? 'prov'}:${idx}:${row}`}
wrap="truncate-end"
>

View file

@ -86,6 +86,16 @@ export function listRowStyle(t: Theme, active: boolean): { backgroundColor?: str
return { backgroundColor, color: liftForContrast(t.color.text, backgroundColor, 4.5) }
}
/** Spreadable props for a selectable row: chip bg + ink + bold when active.
* Spread AFTER `color` so the chip ink wins on the active row. Replaces
* `inverse`, which swaps against the terminal's unknowable default colors
* (a black slab on transparent profiles). */
export function chipRowProps(t: Theme, active: boolean): { backgroundColor?: string; bold: boolean; color?: string } {
const row = listRowStyle(t, active)
return { backgroundColor: row.backgroundColor, bold: active, ...(row.color ? { color: row.color } : {}) }
}
/** A numbered menu row with the cursor (mirrors ClarifyPrompt). Active rows
* carry the shared list-row selection chip same treatment as completions
* and the session switcher instead of `inverse`, whose contrast depends on

View file

@ -6,6 +6,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
import type { Theme } from '../theme.js'
import { OverlayHint, windowItems } from './overlayControls.js'
import { chipRowProps } from './overlayPrimitives.js'
const VISIBLE = 10
const MIN_WIDTH = 40
@ -155,7 +156,7 @@ export function PetPicker({ gw, maxWidth, onClose, t }: PetPickerProps) {
const tag = pet.installed ? '' : pet.curated ? ' · official' : ''
return (
<Text bold={at} color={at ? t.color.accent : t.color.muted} inverse={at} key={pet.slug} wrap="truncate-end">
<Text color={t.color.muted} {...chipRowProps(t, at)} key={pet.slug} wrap="truncate-end">
{at ? '▸ ' : ' '}
{mark} {pet.displayName}
<Text color={at ? t.color.accent : t.color.muted}>

View file

@ -6,6 +6,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
import type { Theme } from '../theme.js'
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.js'
import { chipRowProps } from './overlayPrimitives.js'
const VISIBLE = 12
const MIN_WIDTH = 44
@ -209,9 +210,8 @@ export function PluginsHub({ gw, maxWidth, onClose, t }: PluginsHubProps) {
return (
<Text
bold={active}
color={active ? t.color.accent : t.color.muted}
inverse={active}
color={t.color.muted}
{...chipRowProps(t, active)}
key={effectiveRows[lineIdx]?.name ?? row}
wrap="truncate-end"
>

View file

@ -5,6 +5,7 @@ import { isMac } from '../lib/platform.js'
import type { Theme } from '../theme.js'
import type { ApprovalReq, ClarifyReq, ConfirmReq } from '../types.js'
import { chipRowProps } from './overlayPrimitives.js'
import { TextInput } from './textInput.js'
const APPROVAL_OPTS = ['once', 'session', 'always', 'deny'] as const
@ -129,7 +130,7 @@ export function ApprovalPrompt({ cols = 80, onChoice, req, t }: ApprovalPromptPr
{opts.map((o, i) => (
<Text key={o}>
<Text bold={sel === i} color={sel === i ? t.color.warn : t.color.muted} inverse={sel === i}>
<Text color={t.color.muted} {...chipRowProps(t, sel === i)}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {LABELS[o]}
</Text>
@ -208,7 +209,7 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
{[...choices, 'Other (type your answer)'].map((c, i) => (
<Text key={i}>
<Text bold={sel === i} color={sel === i ? t.color.label : t.color.muted} inverse={sel === i}>
<Text color={t.color.muted} {...chipRowProps(t, sel === i)}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {c}
</Text>

View file

@ -6,6 +6,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
import type { Theme } from '../theme.js'
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.js'
import { chipRowProps } from './overlayPrimitives.js'
const VISIBLE = 12
const MIN_WIDTH = 40
@ -220,13 +221,7 @@ export function SkillsHub({ gw, maxWidth, onClose, t }: SkillsHubProps) {
const idx = offset + i
return (
<Text
bold={catIdx === idx}
color={catIdx === idx ? t.color.accent : t.color.muted}
inverse={catIdx === idx}
key={row}
wrap="truncate-end"
>
<Text color={t.color.muted} {...chipRowProps(t, catIdx === idx)} key={row} wrap="truncate-end">
{catIdx === idx ? '▸ ' : ' '}
{i + 1}. {row}
</Text>
@ -256,13 +251,7 @@ export function SkillsHub({ gw, maxWidth, onClose, t }: SkillsHubProps) {
const idx = offset + i
return (
<Text
bold={skillIdx === idx}
color={skillIdx === idx ? t.color.accent : t.color.muted}
inverse={skillIdx === idx}
key={row}
wrap="truncate-end"
>
<Text color={t.color.muted} {...chipRowProps(t, skillIdx === idx)} key={row} wrap="truncate-end">
{skillIdx === idx ? '▸ ' : ' '}
{i + 1}. {row}
</Text>