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

@ -77,7 +77,7 @@ function TreeRow({
return (
<Box>
<NoSelect flexShrink={0} fromLeftEdge width={lead.length}>
<Text color={stemColor ?? t.color.dim} dim={stemDim}>
<Text color={stemColor ?? t.color.muted} dim={stemDim}>
{lead}
</Text>
</NoSelect>
@ -246,12 +246,12 @@ function Chevron({
title: string
tone?: 'dim' | 'error' | 'warn'
}) {
const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.dim
const color = tone === 'error' ? t.color.error : tone === 'warn' ? t.color.warn : t.color.muted
return (
<Box onClick={(e: any) => onClick(!!e?.shiftKey || !!e?.ctrlKey)}>
<Text color={color} dim={tone === 'dim'}>
<Text color={t.color.amber}>{open ? '▾ ' : '▸ '}</Text>
<Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
{title}
{typeof count === 'number' ? ` (${count})` : ''}
{suffix ? (
@ -266,7 +266,7 @@ function Chevron({
}
function heatColor(node: SubagentNode, peak: number, theme: Theme): string | undefined {
const palette = [theme.color.bronze, theme.color.amber, theme.color.gold, theme.color.warn, theme.color.error]
const palette = [theme.color.border, theme.color.accent, theme.color.primary, theme.color.warn, theme.color.error]
const idx = hotnessBucket(node.aggregate.hotness, peak, palette.length)
// Below the median bucket we keep the default dim stem so cool branches
@ -394,7 +394,7 @@ function SubagentAccordion({
const hasTools = item.tools.length > 0
const noteRows = [...(summary ? [summary] : []), ...item.notes]
const hasNotes = noteRows.length > 0
const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.dim
const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.muted
const sections: {
header: ReactNode
@ -460,10 +460,10 @@ function SubagentAccordion({
{item.tools.map((line, index) => (
<TreeTextRow
branch={index === item.tools.length - 1 ? 'last' : 'mid'}
color={t.color.cornsilk}
color={t.color.text}
content={
<>
<Text color={t.color.amber}> </Text>
<Text color={t.color.accent}> </Text>
{line}
</>
}
@ -649,22 +649,22 @@ export const Thinking = memo(function Thinking({
{preview ? (
mode === 'full' ? (
lines.map((line, index) => (
<Text color={t.color.dim} key={index} wrap="wrap-trim">
<Text color={t.color.muted} key={index} wrap="wrap-trim">
{line || ' '}
{index === lines.length - 1 ? (
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
) : null}
</Text>
))
) : (
<Text color={t.color.dim} wrap="truncate-end">
<Text color={t.color.muted} wrap="truncate-end">
{preview}
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
</Text>
)
) : (
<Text color={t.color.dim}>
<StreamCursor color={t.color.dim} streaming={streaming} visible={active} />
<Text color={t.color.muted}>
<StreamCursor color={t.color.muted} streaming={streaming} visible={active} />
</Text>
)}
</Box>
@ -792,7 +792,7 @@ export const ToolTrail = memo(function ToolTrail({
if (parsed) {
groups.push({
color: parsed.mark === '✗' ? t.color.error : t.color.cornsilk,
color: parsed.mark === '✗' ? t.color.error : t.color.text,
content: parsed.call,
details: [],
key: `tr-${i}`,
@ -801,7 +801,7 @@ export const ToolTrail = memo(function ToolTrail({
if (parsed.detail) {
pushDetail({
color: parsed.mark === '✗' ? t.color.error : t.color.dim,
color: parsed.mark === '✗' ? t.color.error : t.color.muted,
content: parsed.detail,
dimColor: parsed.mark !== '✗',
key: `tr-${i}-d`
@ -815,9 +815,9 @@ export const ToolTrail = memo(function ToolTrail({
const label = toolTrailLabel(line.slice(9).replace(/…$/, '').trim())
groups.push({
color: t.color.cornsilk,
color: t.color.text,
content: label,
details: [{ color: t.color.dim, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }],
details: [{ color: t.color.muted, content: 'drafting...', dimColor: true, key: `tr-${i}-d` }],
key: `tr-${i}`,
label
})
@ -827,12 +827,12 @@ export const ToolTrail = memo(function ToolTrail({
if (line === 'analyzing tool output…') {
pushDetail({
color: t.color.dim,
color: t.color.muted,
dimColor: true,
key: `tr-${i}`,
content: groups.length ? (
<>
<Spinner color={t.color.amber} variant="think" /> {line}
<Spinner color={t.color.accent} variant="think" /> {line}
</>
) : (
line
@ -842,20 +842,20 @@ export const ToolTrail = memo(function ToolTrail({
continue
}
meta.push({ color: t.color.dim, content: line, dimColor: true, key: `tr-${i}` })
meta.push({ color: t.color.muted, content: line, dimColor: true, key: `tr-${i}` })
}
for (const tool of tools) {
const label = formatToolCall(tool.name, tool.context || '')
groups.push({
color: t.color.cornsilk,
color: t.color.text,
key: tool.id,
label,
details: [],
content: (
<>
<Spinner color={t.color.amber} variant="tool" /> {label}
<Spinner color={t.color.accent} variant="tool" /> {label}
{tool.startedAt ? ` (${fmtElapsed(now - tool.startedAt)})` : ''}
</>
)
@ -864,7 +864,7 @@ export const ToolTrail = memo(function ToolTrail({
for (const item of activity.slice(-4)) {
const glyph = item.tone === 'error' ? '✗' : item.tone === 'warn' ? '!' : '·'
const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.dim
const color = item.tone === 'error' ? t.color.error : item.tone === 'warn' ? t.color.warn : t.color.muted
meta.push({ color, content: `${glyph} ${item.text}`, dimColor: item.tone === 'info', key: `a-${item.id}` })
}
@ -998,14 +998,14 @@ export const ToolTrail = memo(function ToolTrail({
}
}}
>
<Text color={t.color.dim} dim={!thinkingLive}>
<Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
<Text color={t.color.muted} dim={!thinkingLive}>
<Text color={t.color.accent}>{openThinking ? '▾ ' : '▸ '}</Text>
{thinkingLive ? (
<Text bold color={t.color.cornsilk}>
<Text bold color={t.color.text}>
Thinking
</Text>
) : (
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
Thinking
</Text>
)}
@ -1068,7 +1068,7 @@ export const ToolTrail = memo(function ToolTrail({
color={group.color}
content={
<>
<Text color={t.color.amber}> </Text>
<Text color={t.color.accent}> </Text>
{toolLabel(group)}
</>
}
@ -1182,7 +1182,7 @@ export const ToolTrail = memo(function ToolTrail({
color={t.color.statusFg}
content={
<>
<Text color={t.color.amber}>Σ </Text>
<Text color={t.color.accent}>Σ </Text>
{totalTokensLabel}
</>
}
@ -1192,7 +1192,7 @@ export const ToolTrail = memo(function ToolTrail({
) : null}
{outcome ? (
<Box marginTop={1}>
<Text color={t.color.dim} dim>
<Text color={t.color.muted} dim>
· {outcome}
</Text>
</Box>