mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
fix: little box typey thing
This commit is contained in:
parent
8efd3db1b4
commit
4b026d6761
6 changed files with 142 additions and 62 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import { Box, Text, TextInput } from '@hermes/ink'
|
||||
import { Box, Text } from '@hermes/ink'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { Theme } from '../theme.js'
|
||||
import { TextInput } from './textInput.js'
|
||||
|
||||
export function MaskedPrompt({
|
||||
cols = 80,
|
||||
icon,
|
||||
label,
|
||||
onSubmit,
|
||||
sub,
|
||||
t
|
||||
}: {
|
||||
cols?: number
|
||||
icon: string
|
||||
label: string
|
||||
onSubmit: (v: string) => void
|
||||
|
|
@ -27,7 +30,7 @@ export function MaskedPrompt({
|
|||
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'> '}</Text>
|
||||
<TextInput mask="*" onChange={setValue} onSubmit={onSubmit} value={value} />
|
||||
<TextInput columns={Math.max(20, cols - 6)} mask="*" onChange={setValue} onSubmit={onSubmit} value={value} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ export const MessageLine = memo(function MessageLine({
|
|||
msg: Msg
|
||||
t: Theme
|
||||
}) {
|
||||
if (msg.kind === 'trail' && msg.tools?.length) {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<ToolTrail t={t} trail={msg.tools} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (msg.role === 'tool') {
|
||||
const preview = compactPreview(hasAnsi(msg.text) ? stripAnsi(msg.text) : msg.text, Math.max(24, cols - 14))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Box, Text, TextInput, useInput } from '@hermes/ink'
|
||||
import { Box, Text, useInput } from '@hermes/ink'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { Theme } from '../theme.js'
|
||||
import type { ApprovalReq, ClarifyReq } from '../types.js'
|
||||
import { TextInput } from './textInput.js'
|
||||
|
||||
export function ApprovalPrompt({ onChoice, req, t }: { onChoice: (s: string) => void; req: ApprovalReq; t: Theme }) {
|
||||
const [sel, setSel] = useState(3)
|
||||
|
|
@ -59,68 +60,77 @@ export function ApprovalPrompt({ onChoice, req, t }: { onChoice: (s: string) =>
|
|||
)
|
||||
}
|
||||
|
||||
export function ClarifyPrompt({ onAnswer, req, t }: { onAnswer: (s: string) => void; req: ClarifyReq; t: Theme }) {
|
||||
export function ClarifyPrompt({
|
||||
cols = 80,
|
||||
onAnswer,
|
||||
onCancel,
|
||||
req,
|
||||
t
|
||||
}: {
|
||||
cols?: number
|
||||
onAnswer: (s: string) => void
|
||||
onCancel: () => void
|
||||
req: ClarifyReq
|
||||
t: Theme
|
||||
}) {
|
||||
const [sel, setSel] = useState(0)
|
||||
const [custom, setCustom] = useState('')
|
||||
const [typing, setTyping] = useState(false)
|
||||
const choices = req.choices ?? []
|
||||
|
||||
const heading = (
|
||||
<Text bold>
|
||||
<Text color={t.color.amber}>ask</Text>
|
||||
<Text color={t.color.cornsilk}> {req.question}</Text>
|
||||
</Text>
|
||||
)
|
||||
|
||||
useInput((ch, key) => {
|
||||
if (typing) {
|
||||
if (key.escape) {
|
||||
typing && choices.length ? setTyping(false) : onCancel()
|
||||
return
|
||||
}
|
||||
|
||||
if (key.upArrow && sel > 0) {
|
||||
setSel(s => s - 1)
|
||||
}
|
||||
if (typing) return
|
||||
|
||||
if (key.downArrow && sel < choices.length) {
|
||||
setSel(s => s + 1)
|
||||
}
|
||||
if (key.upArrow && sel > 0) setSel(s => s - 1)
|
||||
if (key.downArrow && sel < choices.length) setSel(s => s + 1)
|
||||
|
||||
if (key.return) {
|
||||
if (sel === choices.length) {
|
||||
setTyping(true)
|
||||
} else if (choices[sel]) {
|
||||
onAnswer(choices[sel]!)
|
||||
}
|
||||
sel === choices.length ? setTyping(true) : choices[sel] && onAnswer(choices[sel]!)
|
||||
}
|
||||
|
||||
const n = parseInt(ch)
|
||||
|
||||
if (n >= 1 && n <= choices.length) {
|
||||
onAnswer(choices[n - 1]!)
|
||||
}
|
||||
if (n >= 1 && n <= choices.length) onAnswer(choices[n - 1]!)
|
||||
})
|
||||
|
||||
if (typing || !choices.length) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.amber}>
|
||||
❓ {req.question}
|
||||
</Text>
|
||||
{heading}
|
||||
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'> '}</Text>
|
||||
<TextInput onChange={setCustom} onSubmit={onAnswer} value={custom} />
|
||||
<TextInput columns={Math.max(20, cols - 6)} onChange={setCustom} onSubmit={onAnswer} value={custom} />
|
||||
</Box>
|
||||
|
||||
<Text color={t.color.dim}>Enter send · Esc back · Ctrl+C cancel</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.amber}>
|
||||
❓ {req.question}
|
||||
</Text>
|
||||
{heading}
|
||||
|
||||
{[...choices, 'Other (type your answer)'].map((c, i) => (
|
||||
<Text key={i}>
|
||||
<Text color={sel === i ? t.color.label : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
|
||||
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>
|
||||
{i + 1}. {c}
|
||||
</Text>
|
||||
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>{i + 1}. {c}</Text>
|
||||
</Text>
|
||||
))}
|
||||
<Text color={t.color.dim}>↑/↓ select · Enter confirm · 1-{choices.length} quick pick</Text>
|
||||
|
||||
<Text color={t.color.dim}>↑/↓ select · Enter confirm · 1-{choices.length} quick pick · Esc/Ctrl+C cancel</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type InkExt = typeof Ink & {
|
|||
}
|
||||
|
||||
const ink = Ink as unknown as InkExt
|
||||
const { Box, Text, useInput, stringWidth, useDeclaredCursor, useTerminalFocus } = ink
|
||||
const { Box, Text, useStdin, useInput, stringWidth, useDeclaredCursor, useTerminalFocus } = ink
|
||||
|
||||
// ── ANSI escapes ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ const INV = `${ESC}[7m`
|
|||
const INV_OFF = `${ESC}[27m`
|
||||
const DIM = `${ESC}[2m`
|
||||
const DIM_OFF = `${ESC}[22m`
|
||||
const FWD_DEL_RE = new RegExp(`${ESC}\\[3(?:[~$^]|;)`)
|
||||
const PRINTABLE = /^[ -~\u00a0-\uffff]+$/
|
||||
const BRACKET_PASTE = new RegExp(`${ESC}?\\[20[01]~`, 'g')
|
||||
|
||||
|
|
@ -121,6 +122,31 @@ function renderWithCursor(value: string, cursor: number) {
|
|||
return done ? out : out + invert(' ')
|
||||
}
|
||||
|
||||
// ── Forward-delete detection hook ────────────────────────────────────
|
||||
|
||||
function useFwdDelete(active: boolean) {
|
||||
const ref = useRef(false)
|
||||
const { inputEmitter: ee } = useStdin()
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
|
||||
const h = (d: string) => {
|
||||
ref.current = FWD_DEL_RE.test(d)
|
||||
}
|
||||
|
||||
ee.prependListener('input', h)
|
||||
|
||||
return () => {
|
||||
ee.removeListener('input', h)
|
||||
}
|
||||
}, [active, ee])
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PasteEvent {
|
||||
|
|
@ -137,14 +163,25 @@ interface Props {
|
|||
onChange: (v: string) => void
|
||||
onSubmit?: (v: string) => void
|
||||
onPaste?: (e: PasteEvent) => { cursor: number; value: string } | null
|
||||
mask?: string
|
||||
placeholder?: string
|
||||
focus?: boolean
|
||||
}
|
||||
|
||||
// ── Component ────────────────────────────────────────────────────────
|
||||
|
||||
export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, placeholder = '', focus = true }: Props) {
|
||||
export function TextInput({
|
||||
columns = 80,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onSubmit,
|
||||
mask,
|
||||
placeholder = '',
|
||||
focus = true
|
||||
}: Props) {
|
||||
const [cur, setCur] = useState(value.length)
|
||||
const fwdDel = useFwdDelete(focus)
|
||||
const termFocus = useTerminalFocus()
|
||||
|
||||
const curRef = useRef(cur)
|
||||
|
|
@ -163,7 +200,8 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
|
|||
cbSubmit.current = onSubmit
|
||||
cbPaste.current = onPaste
|
||||
|
||||
const display = self.current ? vRef.current : value
|
||||
const raw = self.current ? vRef.current : value
|
||||
const display = mask ? raw.replace(/[^\n]/g, mask[0] ?? '*') : raw
|
||||
|
||||
// ── Cursor declaration ───────────────────────────────────────────
|
||||
|
||||
|
|
@ -337,7 +375,7 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
|
|||
}
|
||||
|
||||
// Deletion
|
||||
else if (k.backspace && c > 0) {
|
||||
else if ((k.backspace || k.delete) && !fwdDel.current && c > 0) {
|
||||
if (mod) {
|
||||
const t = wordLeft(v, c)
|
||||
v = v.slice(0, t) + v.slice(c)
|
||||
|
|
@ -346,7 +384,7 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
|
|||
v = v.slice(0, c - 1) + v.slice(c)
|
||||
c--
|
||||
}
|
||||
} else if (k.delete && c < v.length) {
|
||||
} else if (k.delete && fwdDel.current && c < v.length) {
|
||||
if (mod) {
|
||||
const t = wordRight(v, c)
|
||||
v = v.slice(0, c) + v.slice(t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue