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

@ -41,7 +41,9 @@ export interface SelectionApi {
captureScrolledRows: (firstRow: number, lastRow: number, side: 'above' | 'below') => void
clearSelection: () => void
copySelection: () => Promise<string>
copySelectionNoClear: () => Promise<string>
getState: () => unknown
version: () => number
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
}

View file

@ -17,6 +17,7 @@ import type {
import { useGitBranch } from '../hooks/useGitBranch.js'
import { useVirtualHistory } from '../hooks/useVirtualHistory.js'
import { appendTranscriptMessage } from '../lib/messages.js'
import { isMac } from '../lib/platform.js'
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
import { terminalParityHints } from '../lib/terminalParity.js'
import { buildToolTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js'
@ -52,7 +53,7 @@ const capHistory = (items: Msg[]): Msg[] => {
return items[0]?.kind === 'intro' ? [items[0]!, ...items.slice(-(MAX_HISTORY - 1))] : items.slice(-MAX_HISTORY)
}
const statusColorOf = (status: string, t: { dim: string; error: string; ok: string; warn: string }) => {
const statusColorOf = (status: string, t: { error: string; muted: string; ok: string; warn: string }) => {
if (status === 'ready') {
return t.ok
}
@ -65,7 +66,7 @@ const statusColorOf = (status: string, t: { dim: string; error: string; ok: stri
return t.warn
}
return t.dim
return t.muted
}
export function useMainApp(gw: GatewayClient) {
@ -143,11 +144,47 @@ export function useMainApp(gw: GatewayClient) {
const hasSelection = useHasSelection()
const selection = useSelection()
const lastCopiedVersionRef = useRef(-1)
useEffect(() => {
selection.setSelectionBgColor(ui.theme.color.selectionBg)
}, [selection, ui.theme.color.selectionBg])
// macOS Terminal.app does not forward Cmd+C to fullscreen TUIs that enable
// mouse tracking, so the only reliable native-feeling path is iTerm-style
// copy-on-select: once a drag creates a stable TUI selection, write it to
// the system clipboard while keeping the highlight visible.
//
// Subscribe directly via the ink selection bus (not useSyncExternalStore)
// so React doesn't re-render MainApp on every drag-move tick. The version
// ref de-dupes against re-entrant notifications.
useEffect(() => {
if (!isMac) {
return
}
return selection.subscribe(() => {
if (!selection.hasSelection()) {
return
}
const state = selection.getState() as { isDragging?: boolean } | null
if (state?.isDragging) {
return
}
const version = selection.version()
if (version === lastCopiedVersionRef.current) {
return
}
lastCopiedVersionRef.current = version
void selection.copySelectionNoClear()
})
}, [selection])
const clearSelection = useCallback(() => {
selection.clearSelection()
getInputSelection()?.collapseToEnd()