mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(tui): use command shortcuts on macOS
Make the Ink TUI match macOS keyboard expectations: Command handles copy and common editor/session shortcuts, while Control remains reserved for interrupt/cancel flows. Update the visible hotkey help to show platform-appropriate labels.
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
VoiceRecordResponse
|
||||
} from '../gatewayTypes.js'
|
||||
import { writeOsc52Clipboard } from '../lib/osc52.js'
|
||||
import { isAction, isMac } from '../lib/platform.js'
|
||||
|
||||
import { getInputSelection } from './inputSelectionStore.js'
|
||||
import type { InputHandlerContext, InputHandlerResult } from './interfaces.js'
|
||||
@@ -224,10 +225,6 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return terminal.scrollWithSelection(key.pageUp ? -step : step)
|
||||
}
|
||||
|
||||
if (key.ctrl && key.shift && ch.toLowerCase() === 'c') {
|
||||
return copySelection()
|
||||
}
|
||||
|
||||
if (key.escape && terminal.hasSelection) {
|
||||
return clearSelection()
|
||||
}
|
||||
@@ -244,7 +241,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'c')) {
|
||||
if (isAction(key, ch, 'c') || (key.ctrl && key.shift && ch.toLowerCase() === 'c')) {
|
||||
if (terminal.hasSelection) {
|
||||
return copySelection()
|
||||
}
|
||||
@@ -254,6 +251,21 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
if (inputSel && inputSel.end > inputSel.start) {
|
||||
writeOsc52Clipboard(inputSel.value.slice(inputSel.start, inputSel.end))
|
||||
inputSel.clear()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'c')) {
|
||||
if (!isMac && terminal.hasSelection) {
|
||||
return copySelection()
|
||||
}
|
||||
|
||||
const inputSel = getInputSelection()
|
||||
|
||||
if (!isMac && inputSel && inputSel.end > inputSel.start) {
|
||||
writeOsc52Clipboard(inputSel.value.slice(inputSel.start, inputSel.end))
|
||||
inputSel.clear()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -274,11 +286,11 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return actions.die()
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'd')) {
|
||||
if (isAction(key, ch, 'd') || isCtrl(key, ch, 'd')) {
|
||||
return actions.die()
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'l')) {
|
||||
if (isAction(key, ch, 'l') || isCtrl(key, ch, 'l')) {
|
||||
if (actions.guardBusySessionSwitch()) {
|
||||
return
|
||||
}
|
||||
@@ -288,11 +300,11 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return actions.newSession()
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'b')) {
|
||||
if (isAction(key, ch, 'b') || isCtrl(key, ch, 'b')) {
|
||||
return voice.recording ? voiceStop() : voiceStart()
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'g')) {
|
||||
if (isAction(key, ch, 'g') || isCtrl(key, ch, 'g')) {
|
||||
return cActions.openEditor()
|
||||
}
|
||||
|
||||
@@ -311,7 +323,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'k') && cRefs.queueRef.current.length && live.sid) {
|
||||
if ((isAction(key, ch, 'k') || isCtrl(key, ch, 'k')) && cRefs.queueRef.current.length && live.sid) {
|
||||
const next = cActions.dequeue()
|
||||
|
||||
if (next) {
|
||||
|
||||
@@ -519,22 +519,22 @@ export function TextInput({
|
||||
const range = selRange()
|
||||
const delFwd = k.delete || fwdDel.current
|
||||
|
||||
if (k.ctrl && inp === 'z') {
|
||||
if ((k.ctrl || k.meta) && inp === 'z') {
|
||||
return swap(undo, redo)
|
||||
}
|
||||
|
||||
if ((k.ctrl && inp === 'y') || (k.meta && k.shift && inp === 'z')) {
|
||||
if (((k.ctrl || k.meta) && inp === 'y') || ((k.ctrl || k.meta) && k.shift && inp === 'z')) {
|
||||
return swap(redo, undo)
|
||||
}
|
||||
|
||||
if (k.ctrl && inp === 'a') {
|
||||
if ((k.ctrl || k.meta) && inp === 'a') {
|
||||
return selectAll()
|
||||
}
|
||||
|
||||
if (k.home) {
|
||||
clearSel()
|
||||
c = 0
|
||||
} else if (k.end || (k.ctrl && inp === 'e')) {
|
||||
} else if (k.end || (k.ctrl && inp === 'e') || (k.meta && inp === 'e')) {
|
||||
clearSel()
|
||||
c = v.length
|
||||
} else if (k.leftArrow) {
|
||||
@@ -553,10 +553,10 @@ export function TextInput({
|
||||
clearSel()
|
||||
c = mod ? wordRight(v, c) : nextPos(v, c)
|
||||
}
|
||||
} else if (k.meta && inp === 'b') {
|
||||
} else if ((k.ctrl || k.meta) && inp === 'b') {
|
||||
clearSel()
|
||||
c = wordLeft(v, c)
|
||||
} else if (k.meta && inp === 'f') {
|
||||
} else if ((k.ctrl || k.meta) && inp === 'f') {
|
||||
clearSel()
|
||||
c = wordRight(v, c)
|
||||
} else if (range && (k.backspace || delFwd)) {
|
||||
@@ -579,7 +579,7 @@ export function TextInput({
|
||||
} else {
|
||||
v = v.slice(0, c) + v.slice(nextPos(v, c))
|
||||
}
|
||||
} else if (k.ctrl && inp === 'w') {
|
||||
} else if ((k.ctrl || k.meta) && inp === 'w') {
|
||||
if (range) {
|
||||
v = v.slice(0, range.start) + v.slice(range.end)
|
||||
c = range.start
|
||||
@@ -591,7 +591,7 @@ export function TextInput({
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else if (k.ctrl && inp === 'u') {
|
||||
} else if ((k.ctrl || k.meta) && inp === 'u') {
|
||||
if (range) {
|
||||
v = v.slice(0, range.start) + v.slice(range.end)
|
||||
c = range.start
|
||||
@@ -599,7 +599,7 @@ export function TextInput({
|
||||
v = v.slice(c)
|
||||
c = 0
|
||||
}
|
||||
} else if (k.ctrl && inp === 'k') {
|
||||
} else if ((k.ctrl || k.meta) && inp === 'k') {
|
||||
if (range) {
|
||||
v = v.slice(0, range.start) + v.slice(range.end)
|
||||
c = range.start
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { isMac } from '../lib/platform.js'
|
||||
|
||||
const mod = isMac ? 'Cmd' : 'Ctrl'
|
||||
const pasteMod = isMac ? 'Cmd' : 'Alt'
|
||||
|
||||
export const HOTKEYS: [string, string][] = [
|
||||
['Ctrl+C', 'interrupt / clear draft / exit'],
|
||||
['Ctrl+D', 'exit'],
|
||||
['Ctrl+G', 'open $EDITOR for prompt'],
|
||||
['Ctrl+L', 'new session (clear)'],
|
||||
['Alt+V / /paste', 'paste clipboard image'],
|
||||
[mod + '+C / ' + mod + '+Shift+C', 'copy selection'],
|
||||
[mod + '+D', 'exit'],
|
||||
[mod + '+G', 'open $EDITOR for prompt'],
|
||||
[mod + '+L', 'new session (clear)'],
|
||||
[pasteMod + '+V / /paste', 'paste clipboard image'],
|
||||
['Tab', 'apply completion'],
|
||||
['↑/↓', 'completions / queue edit / history'],
|
||||
['Ctrl+A/E', 'home / end of line'],
|
||||
['Ctrl+Z / Ctrl+Y', 'undo / redo input edits'],
|
||||
['Ctrl+W', 'delete word'],
|
||||
['Ctrl+U/K', 'delete to start / end'],
|
||||
['Ctrl+←/→', 'jump word'],
|
||||
[mod + '+A/E', 'home / end of line'],
|
||||
[mod + '+Z / ' + mod + '+Y', 'undo / redo input edits'],
|
||||
[mod + '+W', 'delete word'],
|
||||
[mod + '+U/K', 'delete to start / end'],
|
||||
[mod + '+←/→', 'jump word'],
|
||||
['Home/End', 'start / end of line'],
|
||||
['Shift+Enter / Alt+Enter', 'insert newline'],
|
||||
['\\+Enter', 'multi-line continuation (fallback)'],
|
||||
|
||||
19
ui-tui/src/lib/platform.ts
Normal file
19
ui-tui/src/lib/platform.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/** Platform-aware keybinding helpers.
|
||||
*
|
||||
* On macOS the "action" modifier is Cmd (key.meta in Ink), on other platforms
|
||||
* it is Ctrl. Ctrl+C is ALWAYS the interrupt key regardless of platform —
|
||||
* it must never be remapped to copy.
|
||||
*/
|
||||
|
||||
export const isMac = process.platform === 'darwin'
|
||||
|
||||
/** The display label for the action modifier key. */
|
||||
export const modLabel = isMac ? '⌘' : 'Ctrl'
|
||||
|
||||
/** True when the platform action-modifier is pressed (Cmd on macOS, Ctrl elsewhere). */
|
||||
export const isActionMod = (key: { ctrl: boolean; meta: boolean }): boolean =>
|
||||
isMac ? key.meta : key.ctrl
|
||||
|
||||
/** Match action-modifier + a single character (case-insensitive). */
|
||||
export const isAction = (key: { ctrl: boolean; meta: boolean }, ch: string, target: string): boolean =>
|
||||
isActionMod(key) && ch.toLowerCase() === target
|
||||
Reference in New Issue
Block a user