hermes-agent/ui-tui/src/lib/osc52.ts
Brooklyn Nicholson fc6a27098e fix(tui): raise picker selection contrast with inverse + bold
Selected rows in the model/session/skills pickers and approval/clarify
prompts only changed from dim gray to cornsilk, which reads as low
contrast on lighter themes and LCDs (reported during TUI v2 blitz).

Switch the selected row to `inverse bold` with the brand accent color
across modelPicker, sessionPicker, skillsHub, and prompts so the
highlight is terminal-portable and unambiguous. Unselected rows stay
dim. Also extends the sessionPicker middle meta column (which was
always dim) to inherit the row's selection state.
2026-04-21 14:31:21 -05:00

73 lines
1.9 KiB
TypeScript

const ESC = '\x1b'
const BEL = '\x07'
const ST = `${ESC}\\`
export const OSC52_CLIPBOARD_QUERY = `${ESC}]52;c;?${BEL}`
type OscResponse = { code: number; data: string; type: 'osc' }
type OscQuerier = {
flush: () => Promise<void>
send: <T>(query: { match: (r: unknown) => r is T; request: string }) => Promise<T | undefined>
}
function wrapForMultiplexer(sequence: string): string {
if (process.env['TMUX']) {
return `${ESC}Ptmux;${sequence.split(ESC).join(ESC + ESC)}${ST}`
}
if (process.env['STY']) {
return `${ESC}P${sequence}${ST}`
}
return sequence
}
export function buildOsc52ClipboardQuery(): string {
return wrapForMultiplexer(OSC52_CLIPBOARD_QUERY)
}
export function parseOsc52ClipboardData(data: string): null | string {
const firstSep = data.indexOf(';')
if (firstSep === -1) {
return null
}
const selection = data.slice(0, firstSep)
const payload = data.slice(firstSep + 1)
if ((selection !== 'c' && selection !== 'p') || !payload || payload === '?') {
return null
}
try {
return Buffer.from(payload, 'base64').toString('utf8')
} catch {
return null
}
}
export async function readOsc52Clipboard(querier: null | OscQuerier, timeoutMs = 500): Promise<null | string> {
if (!querier) {
return null
}
const timeout = new Promise<undefined>(resolve => setTimeout(resolve, timeoutMs))
const query = querier.send<OscResponse>({
request: buildOsc52ClipboardQuery(),
match: (r: unknown): r is OscResponse => {
return !!r && typeof r === 'object' && (r as OscResponse).type === 'osc' && (r as OscResponse).code === 52
}
})
const response = await Promise.race([query, timeout])
await querier.flush()
return response ? parseOsc52ClipboardData(response.data) : null
}
export const writeOsc52Clipboard = (s: string) =>
process.stdout.write(`\x1b]52;c;${Buffer.from(s, 'utf8').toString('base64')}\x07`)