fix(tui): restore macOS copy behavior and theme polish (#17131)

This PR groups the TUI fixes that restore macOS Terminal usability and clean up the theme/composer regressions:

- copy transcript selections on macOS drag-release so Terminal.app users can copy while mouse tracking is enabled
- copy composer selections on macOS drag-release; composer selection is internal to TextInput and does not use the global Ink selection bus
- keep IDE Cmd+C forwarding setup macOS-only, and make keybinding conflict checks respect simple when-clause overlap/negation
- force truecolor before chalk initializes (unless NO_COLOR / FORCE_COLOR / HERMES_TUI_TRUECOLOR opt-outs apply) so the default banner keeps its gold/amber/bronze gradient in Terminal.app
- move TUI surfaces onto semantic theme tokens and preserve skin prompt symbols as bare tokens with renderer-owned spacing
- render focused placeholders as dim hint text in TTY mode instead of inverse/selected-looking synthetic cursor text
This commit is contained in:
brooklyn! 2026-04-28 16:47:14 -07:00 committed by GitHub
parent a9efa46b69
commit 6b09df39be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 828 additions and 337 deletions

View file

@ -26,12 +26,12 @@ export function Banner({ t }: { t: Theme }) {
{cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (
<ArtLines lines={logoLines} />
) : (
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{t.brand.icon} NOUS HERMES
</Text>
)}
<Text color={t.color.dim}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
<Text color={t.color.muted}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
</Box>
)
}
@ -70,19 +70,19 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
return (
<Box flexDirection="column" marginTop={1}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
Available {title}
</Text>
{shown.map(([k, vs]) => (
<Text key={k} wrap="truncate">
<Text color={t.color.dim}>{strip(k)}: </Text>
<Text color={t.color.cornsilk}>{truncLine(strip(k) + ': ', vs)}</Text>
<Text color={t.color.muted}>{strip(k)}: </Text>
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
</Text>
))}
{overflow > 0 && (
<Text color={t.color.dim}>
<Text color={t.color.muted}>
(and {overflow} {overflowLabel})
</Text>
)}
@ -91,18 +91,18 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
}
return (
<Box borderColor={t.color.bronze} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
<Box borderColor={t.color.border} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
{wide && (
<Box flexDirection="column" marginRight={2} width={leftW}>
<ArtLines lines={heroLines} />
<Text />
<Text color={t.color.amber}>
<Text color={t.color.accent}>
{info.model.split('/').pop()}
<Text color={t.color.dim}> · Nous Research</Text>
<Text color={t.color.muted}> · Nous Research</Text>
</Text>
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{info.cwd || process.cwd()}
</Text>
@ -117,7 +117,7 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
<Box flexDirection="column" width={w}>
<Box justifyContent="center" marginBottom={1}>
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{t.brand.name}
{info.version ? ` v${info.version}` : ''}
{info.release_date ? ` (${info.release_date})` : ''}
@ -129,17 +129,17 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
{info.mcp_servers && info.mcp_servers.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
MCP Servers
</Text>
{info.mcp_servers.map(s => (
<Text key={s.name} wrap="truncate">
<Text color={t.color.dim}>{` ${s.name} `}</Text>
<Text color={t.color.dim}>{`[${s.transport}]`}</Text>
<Text color={t.color.dim}>: </Text>
<Text color={t.color.muted}>{` ${s.name} `}</Text>
<Text color={t.color.muted}>{`[${s.transport}]`}</Text>
<Text color={t.color.muted}>: </Text>
{s.connected ? (
<Text color={t.color.cornsilk}>
<Text color={t.color.text}>
{s.tools} tool{s.tools === 1 ? '' : 's'}
</Text>
) : (
@ -152,12 +152,12 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
<Text />
<Text color={t.color.cornsilk}>
<Text color={t.color.text}>
{flat(info.tools).length} tools{' · '}
{flat(info.skills).length} skills
{info.mcp_servers?.length ? ` · ${info.mcp_servers.length} MCP` : ''}
{' · '}
<Text color={t.color.dim}>/help for commands</Text>
<Text color={t.color.muted}>/help for commands</Text>
</Text>
{typeof info.update_behind === 'number' && info.update_behind > 0 && (
@ -183,9 +183,9 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
export function Panel({ sections, t, title }: PanelProps) {
return (
<Box borderColor={t.color.bronze} borderStyle="round" flexDirection="column" paddingX={2} paddingY={1}>
<Box borderColor={t.color.border} borderStyle="round" flexDirection="column" paddingX={2} paddingY={1}>
<Box justifyContent="center" marginBottom={1}>
<Text bold color={t.color.gold}>
<Text bold color={t.color.primary}>
{title}
</Text>
</Box>
@ -193,25 +193,25 @@ export function Panel({ sections, t, title }: PanelProps) {
{sections.map((sec, si) => (
<Box flexDirection="column" key={si} marginTop={si > 0 ? 1 : 0}>
{sec.title && (
<Text bold color={t.color.amber}>
<Text bold color={t.color.accent}>
{sec.title}
</Text>
)}
{sec.rows?.map(([k, v], ri) => (
<Text key={ri} wrap="truncate">
<Text color={t.color.dim}>{k.padEnd(20)}</Text>
<Text color={t.color.cornsilk}>{v}</Text>
<Text color={t.color.muted}>{k.padEnd(20)}</Text>
<Text color={t.color.text}>{v}</Text>
</Text>
))}
{sec.items?.map((item, ii) => (
<Text color={t.color.cornsilk} key={ii} wrap="truncate">
<Text color={t.color.text} key={ii} wrap="truncate">
{item}
</Text>
))}
{sec.text && <Text color={t.color.dim}>{sec.text}</Text>}
{sec.text && <Text color={t.color.muted}>{sec.text}</Text>}
</Box>
))}
</Box>