mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
Revert "feat(tui): auto copy-on-select for transcript text"
This reverts commit 6701288fe0.
This commit is contained in:
@@ -3134,27 +3134,6 @@ def _(rid, params: dict) -> dict:
|
||||
_write_config_key("display.tui_mouse", nv)
|
||||
return _ok(rid, {"key": key, "value": "on" if nv else "off"})
|
||||
|
||||
if key == "copy_on_select":
|
||||
raw = str(value or "").strip().lower()
|
||||
display = (
|
||||
_load_cfg().get("display")
|
||||
if isinstance(_load_cfg().get("display"), dict)
|
||||
else {}
|
||||
)
|
||||
current = bool(display.get("tui_copy_on_select", True))
|
||||
|
||||
if raw in ("", "toggle"):
|
||||
nv = not current
|
||||
elif raw == "on":
|
||||
nv = True
|
||||
elif raw == "off":
|
||||
nv = False
|
||||
else:
|
||||
return _err(rid, 4002, f"unknown copy_on_select value: {value}")
|
||||
|
||||
_write_config_key("display.tui_copy_on_select", nv)
|
||||
return _ok(rid, {"key": key, "value": "on" if nv else "off"})
|
||||
|
||||
if key in ("prompt", "personality", "skin"):
|
||||
try:
|
||||
cfg = _load_cfg()
|
||||
@@ -3302,14 +3281,6 @@ def _(rid, params: dict) -> dict:
|
||||
display = _load_cfg().get("display")
|
||||
on = display.get("tui_mouse", True) if isinstance(display, dict) else True
|
||||
return _ok(rid, {"value": "on" if on else "off"})
|
||||
if key == "copy_on_select":
|
||||
display = _load_cfg().get("display")
|
||||
on = (
|
||||
display.get("tui_copy_on_select", True)
|
||||
if isinstance(display, dict)
|
||||
else True
|
||||
)
|
||||
return _ok(rid, {"value": "on" if on else "off"})
|
||||
if key == "mtime":
|
||||
cfg_path = _hermes_home / "config.yaml"
|
||||
try:
|
||||
|
||||
@@ -86,7 +86,6 @@ export interface UiState {
|
||||
bgTasks: Set<string>
|
||||
busy: boolean
|
||||
compact: boolean
|
||||
copyOnSelect: boolean
|
||||
detailsMode: DetailsMode
|
||||
detailsModeCommandOverride: boolean
|
||||
info: null | SessionInfo
|
||||
|
||||
@@ -107,27 +107,6 @@ export const coreCommands: SlashCommand[] = [
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
aliases: ['cos'],
|
||||
help: 'toggle auto copy-on-drag-release [on|off|toggle]',
|
||||
name: 'copyselect',
|
||||
run: (arg, ctx) => {
|
||||
const current = ctx.ui.copyOnSelect
|
||||
const next = flagFromArg(arg, current)
|
||||
|
||||
if (next === null) {
|
||||
return ctx.transcript.sys('usage: /copyselect [on|off|toggle]')
|
||||
}
|
||||
|
||||
patchUiState({ copyOnSelect: next })
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'copy_on_select', value: next ? 'on' : 'off' })
|
||||
.catch(() => {})
|
||||
|
||||
queueMicrotask(() => ctx.transcript.sys(`copy-on-select ${next ? 'on' : 'off'}`))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
aliases: ['new'],
|
||||
help: 'start a new session',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
import { COPY_ON_SELECT, MOUSE_TRACKING } from '../config/env.js'
|
||||
import { MOUSE_TRACKING } from '../config/env.js'
|
||||
import { ZERO } from '../domain/usage.js'
|
||||
import { DEFAULT_THEME } from '../theme.js'
|
||||
|
||||
@@ -10,7 +10,6 @@ const buildUiState = (): UiState => ({
|
||||
bgTasks: new Set(),
|
||||
busy: false,
|
||||
compact: false,
|
||||
copyOnSelect: COPY_ON_SELECT,
|
||||
detailsMode: 'collapsed',
|
||||
detailsModeCommandOverride: false,
|
||||
info: null,
|
||||
|
||||
@@ -44,7 +44,6 @@ export const applyDisplay = (cfg: ConfigFullResponse | null, setBell: (v: boolea
|
||||
setBell(!!d.bell_on_complete)
|
||||
patchUiState({
|
||||
compact: !!d.tui_compact,
|
||||
copyOnSelect: d.tui_copy_on_select !== false,
|
||||
detailsMode: resolveDetailsMode(d),
|
||||
detailsModeCommandOverride: false,
|
||||
inlineDiffs: d.inline_diffs !== false,
|
||||
|
||||
@@ -142,73 +142,11 @@ export function useMainApp(gw: GatewayClient) {
|
||||
|
||||
const hasSelection = useHasSelection()
|
||||
const selection = useSelection()
|
||||
const copyOnSelect = ui.copyOnSelect
|
||||
|
||||
useEffect(() => {
|
||||
selection.setSelectionBgColor(ui.theme.color.selectionBg)
|
||||
}, [selection, ui.theme.color.selectionBg])
|
||||
|
||||
// Auto copy-on-select: when a drag completes with a real selection
|
||||
// (anchor + focus, not a bare click), push the text to the clipboard
|
||||
// without clearing the highlight. Matches iTerm2's "Copy to pasteboard
|
||||
// on selection" — drag → release → already on clipboard, paste anywhere.
|
||||
// Subscribes once and tracks the previous isDragging on a ref so the
|
||||
// effect doesn't re-attach on every selection tick.
|
||||
const wasDraggingRef = useRef(false)
|
||||
const lastCopiedRef = useRef<{ anchor: string; focus: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!copyOnSelect) {
|
||||
wasDraggingRef.current = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const fingerprint = (s: { anchor: { col: number; row: number } | null; focus: { col: number; row: number } | null }) => ({
|
||||
anchor: s.anchor ? `${s.anchor.row}:${s.anchor.col}` : '',
|
||||
focus: s.focus ? `${s.focus.row}:${s.focus.col}` : ''
|
||||
})
|
||||
|
||||
return selection.subscribe(() => {
|
||||
const state = selection.getState()
|
||||
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
|
||||
const dragging = state.isDragging
|
||||
const prev = wasDraggingRef.current
|
||||
wasDraggingRef.current = dragging
|
||||
|
||||
if (!state.anchor) {
|
||||
lastCopiedRef.current = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!prev || dragging || !state.focus) {
|
||||
return
|
||||
}
|
||||
|
||||
const fp = fingerprint(state)
|
||||
const last = lastCopiedRef.current
|
||||
|
||||
if (last && last.anchor === fp.anchor && last.focus === fp.focus) {
|
||||
return
|
||||
}
|
||||
|
||||
lastCopiedRef.current = fp
|
||||
|
||||
void selection.copySelectionNoClear().then(text => {
|
||||
if (text) {
|
||||
const len = text.length
|
||||
|
||||
turnController.pushActivity(`copied ${len} char${len === 1 ? '' : 's'} · Esc to clear`, 'info')
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [copyOnSelect, selection])
|
||||
|
||||
const composer = useComposerState({
|
||||
gw,
|
||||
onClipboardPaste: quiet => clipboardPasteRef.current(quiet),
|
||||
|
||||
@@ -2,9 +2,6 @@ const truthy = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim())
|
||||
|
||||
export const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
||||
export const MOUSE_TRACKING = !truthy(process.env.HERMES_TUI_DISABLE_MOUSE)
|
||||
// Drag-release auto-copies the selected text. Off by default for terminals
|
||||
// that do clipboard their own way; opt out with the env var or /copyselect.
|
||||
export const COPY_ON_SELECT = !truthy(process.env.HERMES_TUI_DISABLE_COPY_ON_SELECT)
|
||||
export const NO_CONFIRM_DESTRUCTIVE = truthy(process.env.HERMES_TUI_NO_CONFIRM)
|
||||
|
||||
// Skip AlternateScreen — TUI renders into the primary buffer so the host
|
||||
|
||||
@@ -16,11 +16,7 @@ const copyHotkeys: [string, string][] = isMac
|
||||
: [['Ctrl+C', 'copy selection / interrupt / clear draft / exit']]
|
||||
|
||||
export const HOTKEYS: [string, string][] = [
|
||||
['drag', 'select text in transcript (auto-copies on release)'],
|
||||
['double / triple click', 'select word / line'],
|
||||
['click input', 'position cursor inside the prompt'],
|
||||
...copyHotkeys,
|
||||
['Esc', 'clear selection'],
|
||||
[action + '+D', 'exit'],
|
||||
[action + '+G / Alt+G', 'open $EDITOR (Alt+G fallback for VSCode/Cursor)'],
|
||||
[action + '+L', 'redraw / repaint'],
|
||||
|
||||
@@ -61,7 +61,6 @@ export interface ConfigDisplayConfig {
|
||||
streaming?: boolean
|
||||
thinking_mode?: string
|
||||
tui_compact?: boolean
|
||||
tui_copy_on_select?: boolean
|
||||
tui_mouse?: boolean
|
||||
tui_statusbar?: 'bottom' | 'off' | 'on' | 'top' | boolean
|
||||
}
|
||||
|
||||
7
ui-tui/src/types/hermes-ink.d.ts
vendored
7
ui-tui/src/types/hermes-ink.d.ts
vendored
@@ -139,17 +139,12 @@ declare module '@hermes/ink' {
|
||||
export function useExternalProcess(): (run: RunExternalProcess) => Promise<void>
|
||||
export function withInkSuspended(run: RunExternalProcess): Promise<void>
|
||||
export function useInput(handler: InputHandler, options?: { readonly isActive?: boolean }): void
|
||||
export type InkSelectionState = {
|
||||
readonly anchor: { readonly col: number; readonly row: number } | null
|
||||
readonly focus: { readonly col: number; readonly row: number } | null
|
||||
readonly isDragging: boolean
|
||||
}
|
||||
export function useSelection(): {
|
||||
readonly copySelection: () => Promise<string>
|
||||
readonly copySelectionNoClear: () => Promise<string>
|
||||
readonly clearSelection: () => void
|
||||
readonly hasSelection: () => boolean
|
||||
readonly getState: () => InkSelectionState | null
|
||||
readonly getState: () => unknown
|
||||
readonly subscribe: (cb: () => void) => () => void
|
||||
readonly shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
|
||||
readonly shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
|
||||
|
||||
Reference in New Issue
Block a user