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.
This commit is contained in:
@@ -17,9 +17,11 @@ export function isUsableClipboardText(text: null | string): text is string {
|
||||
}
|
||||
|
||||
let suspicious = 0
|
||||
|
||||
for (const ch of text) {
|
||||
const code = ch.charCodeAt(0)
|
||||
const isControl = code < 0x20 && ch !== '\n' && ch !== '\r' && ch !== '\t'
|
||||
|
||||
if (isControl || ch === '\ufffd') {
|
||||
suspicious += 1
|
||||
}
|
||||
@@ -28,7 +30,10 @@ export function isUsableClipboardText(text: null | string): text is string {
|
||||
return suspicious <= Math.max(2, Math.floor(text.length * 0.02))
|
||||
}
|
||||
|
||||
function readClipboardCommands(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): Array<{ args: readonly string[]; cmd: string }> {
|
||||
function readClipboardCommands(
|
||||
platform: NodeJS.Platform,
|
||||
env: NodeJS.ProcessEnv
|
||||
): Array<{ args: readonly string[]; cmd: string }> {
|
||||
if (platform === 'darwin') {
|
||||
return [{ cmd: 'pbpaste', args: [] }]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function readOsc52Clipboard(querier: null | OscQuerier, timeoutMs =
|
||||
}
|
||||
|
||||
const timeout = new Promise<undefined>(resolve => setTimeout(resolve, timeoutMs))
|
||||
|
||||
const query = querier.send<OscResponse>({
|
||||
request: buildOsc52ClipboardQuery(),
|
||||
match: (r: unknown): r is OscResponse => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const isMac = process.platform === 'darwin'
|
||||
|
||||
/** True when the platform action-modifier is pressed (Cmd on macOS, Ctrl elsewhere). */
|
||||
export const isActionMod = (key: { ctrl: boolean; meta: boolean; super?: boolean }): boolean =>
|
||||
(isMac ? key.meta || key.super === true : key.ctrl)
|
||||
isMac ? key.meta || key.super === true : key.ctrl
|
||||
|
||||
/**
|
||||
* Some macOS terminals rewrite Cmd navigation/deletion into readline control keys.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { detectVSCodeLikeTerminal, isRemoteShellSession, shouldPromptForTerminalSetup, type FileOps } from './terminalSetup.js'
|
||||
import {
|
||||
detectVSCodeLikeTerminal,
|
||||
type FileOps,
|
||||
isRemoteShellSession,
|
||||
shouldPromptForTerminalSetup
|
||||
} from './terminalSetup.js'
|
||||
|
||||
export type MacTerminalHint = {
|
||||
key: string
|
||||
@@ -31,7 +36,10 @@ export async function terminalParityHints(
|
||||
const ctx = detectMacTerminalContext(env)
|
||||
const hints: MacTerminalHint[] = []
|
||||
|
||||
if (ctx.vscodeLike && (await shouldPromptForTerminalSetup({ env, fileOps: options?.fileOps, homeDir: options?.homeDir }))) {
|
||||
if (
|
||||
ctx.vscodeLike &&
|
||||
(await shouldPromptForTerminalSetup({ env, fileOps: options?.fileOps, homeDir: options?.homeDir }))
|
||||
) {
|
||||
hints.push({
|
||||
key: 'ide-setup',
|
||||
tone: 'info',
|
||||
@@ -43,7 +51,8 @@ export async function terminalParityHints(
|
||||
hints.push({
|
||||
key: 'apple-terminal',
|
||||
tone: 'warn',
|
||||
message: 'Apple Terminal detected · use /paste for image-only clipboard fallback, and try Ctrl+A / Ctrl+E / Ctrl+U if Cmd+←/→/⌫ gets rewritten'
|
||||
message:
|
||||
'Apple Terminal detected · use /paste for image-only clipboard fallback, and try Ctrl+A / Ctrl+E / Ctrl+U if Cmd+←/→/⌫ gets rewritten'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +60,8 @@ export async function terminalParityHints(
|
||||
hints.push({
|
||||
key: 'tmux',
|
||||
tone: 'warn',
|
||||
message: 'tmux detected · clipboard copy/paste uses passthrough when available; allow-passthrough improves OSC52 reliability'
|
||||
message:
|
||||
'tmux detected · clipboard copy/paste uses passthrough when available; allow-passthrough improves OSC52 reliability'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,7 +69,8 @@ export async function terminalParityHints(
|
||||
hints.push({
|
||||
key: 'remote',
|
||||
tone: 'warn',
|
||||
message: 'SSH session detected · text clipboard can bridge via OSC52, but image clipboard and local screenshot paths still depend on the machine running Hermes'
|
||||
message:
|
||||
'SSH session detected · text clipboard can bridge via OSC52, but image clipboard and local screenshot paths still depend on the machine running Hermes'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export type TerminalSetupResult = {
|
||||
|
||||
const DEFAULT_FILE_OPS: FileOps = { copyFile, mkdir, readFile, writeFile }
|
||||
const MULTILINE_SEQUENCE = '\\\r\n'
|
||||
|
||||
const TERMINAL_META: Record<SupportedTerminal, { appName: string; label: string }> = {
|
||||
vscode: { appName: 'Code', label: 'VS Code' },
|
||||
cursor: { appName: 'Cursor', label: 'Cursor' },
|
||||
@@ -99,18 +100,22 @@ export function stripJsonComments(content: string): string {
|
||||
// String literal — copy as-is, including any comment-like chars inside
|
||||
if (ch === '"') {
|
||||
let j = i + 1
|
||||
|
||||
while (j < len) {
|
||||
if (content[j] === '\\') {
|
||||
j += 2 // skip escaped char
|
||||
} else if (content[j] === '"') {
|
||||
j++
|
||||
|
||||
break
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
result += content.slice(i, j)
|
||||
i = j
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -118,6 +123,7 @@ export function stripJsonComments(content: string): string {
|
||||
if (ch === '/' && content[i + 1] === '/') {
|
||||
const eol = content.indexOf('\n', i)
|
||||
i = eol === -1 ? len : eol
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -125,6 +131,7 @@ export function stripJsonComments(content: string): string {
|
||||
if (ch === '/' && content[i + 1] === '*') {
|
||||
const end = content.indexOf('*/', i + 2)
|
||||
i = end === -1 ? len : end + 2
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -208,19 +215,23 @@ export async function configureTerminalKeybindings(
|
||||
|
||||
let keybindings: unknown[] = []
|
||||
let hasExistingFile = false
|
||||
|
||||
try {
|
||||
const content = await ops.readFile(keybindingsFile, 'utf8')
|
||||
hasExistingFile = true
|
||||
const parsed: unknown = JSON.parse(stripJsonComments(content))
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `${meta.label} keybindings.json is not a JSON array: ${keybindingsFile}`
|
||||
}
|
||||
}
|
||||
|
||||
keybindings = parsed
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException | undefined)?.code
|
||||
|
||||
if (code !== 'ENOENT') {
|
||||
return {
|
||||
success: false,
|
||||
@@ -230,7 +241,9 @@ export async function configureTerminalKeybindings(
|
||||
}
|
||||
|
||||
const conflicts = TARGET_BINDINGS.filter(target =>
|
||||
keybindings.some(existing => isKeybinding(existing) && existing.key === target.key && !sameBinding(existing, target))
|
||||
keybindings.some(
|
||||
existing => isKeybinding(existing) && existing.key === target.key && !sameBinding(existing, target)
|
||||
)
|
||||
)
|
||||
|
||||
if (conflicts.length) {
|
||||
@@ -242,8 +255,10 @@ export async function configureTerminalKeybindings(
|
||||
}
|
||||
|
||||
let added = 0
|
||||
|
||||
for (const target of TARGET_BINDINGS.slice().reverse()) {
|
||||
const exists = keybindings.some(existing => isKeybinding(existing) && sameBinding(existing, target))
|
||||
|
||||
if (!exists) {
|
||||
keybindings.unshift(target)
|
||||
added += 1
|
||||
@@ -320,11 +335,14 @@ export async function shouldPromptForTerminalSetup(options?: {
|
||||
try {
|
||||
const content = await ops.readFile(join(configDir, 'keybindings.json'), 'utf8')
|
||||
const parsed: unknown = JSON.parse(stripJsonComments(content))
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return TARGET_BINDINGS.some(target => !parsed.some(existing => isKeybinding(existing) && sameBinding(existing, target)))
|
||||
return TARGET_BINDINGS.some(
|
||||
target => !parsed.some(existing => isKeybinding(existing) && sameBinding(existing, target))
|
||||
)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user