mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
Merge pull request #14145 from NousResearch/bb/tui-polish
fix(tui): input wrap, shift-tab yolo, statusline, clean boot
This commit is contained in:
commit
a1d57292af
25 changed files with 715 additions and 229 deletions
|
|
@ -156,7 +156,11 @@ export function GoodVibesHeart({ tick, t }: { tick: number; t: Theme }) {
|
|||
return () => clearTimeout(id)
|
||||
}, [t.color.amber, tick])
|
||||
|
||||
return <Text color={color}>{active ? '♥' : ' '}</Text>
|
||||
if (!active) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Text color={color}>♥</Text>
|
||||
}
|
||||
|
||||
export function StatusRule({
|
||||
|
|
@ -187,7 +191,7 @@ export function StatusRule({
|
|||
const leftWidth = Math.max(12, cols - cwdLabel.length - 3)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box height={1}>
|
||||
<Box flexShrink={1} width={leftWidth}>
|
||||
<Text color={t.color.bronze} wrap="truncate-end">
|
||||
{'─ '}
|
||||
|
|
|
|||
|
|
@ -183,37 +183,19 @@ const ComposerPane = memo(function ComposerPane({
|
|||
<Text> </Text>
|
||||
)}
|
||||
|
||||
<Box flexDirection="column" position="relative">
|
||||
{ui.statusBar && (
|
||||
<StatusRule
|
||||
bgCount={ui.bgTasks.size}
|
||||
busy={ui.busy}
|
||||
cols={composer.cols}
|
||||
cwdLabel={status.cwdLabel}
|
||||
model={ui.info?.model?.split('/').pop() ?? ''}
|
||||
sessionStartedAt={status.sessionStartedAt}
|
||||
showCost={ui.showCost}
|
||||
status={ui.status}
|
||||
statusColor={status.statusColor}
|
||||
t={ui.theme}
|
||||
turnStartedAt={status.turnStartedAt}
|
||||
usage={ui.usage}
|
||||
voiceLabel={status.voiceLabel}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FloatingOverlays
|
||||
cols={composer.cols}
|
||||
compIdx={composer.compIdx}
|
||||
completions={composer.completions}
|
||||
onModelSelect={actions.onModelSelect}
|
||||
onPickerSelect={actions.resumeById}
|
||||
pagerPageSize={composer.pagerPageSize}
|
||||
/>
|
||||
</Box>
|
||||
<StatusRulePane at="top" composer={composer} status={status} />
|
||||
|
||||
{!isBlocked && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="column" marginTop={ui.statusBar === 'top' ? 0 : 1} position="relative">
|
||||
<FloatingOverlays
|
||||
cols={composer.cols}
|
||||
compIdx={composer.compIdx}
|
||||
completions={composer.completions}
|
||||
onModelSelect={actions.onModelSelect}
|
||||
onPickerSelect={actions.resumeById}
|
||||
pagerPageSize={composer.pagerPageSize}
|
||||
/>
|
||||
|
||||
{composer.inputBuf.map((line, i) => (
|
||||
<Box key={i}>
|
||||
<Box width={3}>
|
||||
|
|
@ -236,8 +218,9 @@ const ComposerPane = memo(function ComposerPane({
|
|||
</Box>
|
||||
|
||||
<Box flexGrow={1} position="relative">
|
||||
{/* subtract NoSelect paddingX={1} (2 cols) + pw so wrap-ansi and cursorLayout agree */}
|
||||
<TextInput
|
||||
columns={Math.max(20, composer.cols - pw)}
|
||||
columns={Math.max(20, composer.cols - pw - 2)}
|
||||
onChange={composer.updateInput}
|
||||
onPaste={composer.handleTextPaste}
|
||||
onSubmit={composer.submit}
|
||||
|
|
@ -273,6 +256,38 @@ const AgentsOverlayPane = memo(function AgentsOverlayPane() {
|
|||
)
|
||||
})
|
||||
|
||||
const StatusRulePane = memo(function StatusRulePane({
|
||||
at,
|
||||
composer,
|
||||
status
|
||||
}: Pick<AppLayoutProps, 'composer' | 'status'> & { at: 'bottom' | 'top' }) {
|
||||
const ui = useStore($uiState)
|
||||
|
||||
if (ui.statusBar !== at) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box marginTop={at === 'top' ? 1 : 0}>
|
||||
<StatusRule
|
||||
bgCount={ui.bgTasks.size}
|
||||
busy={ui.busy}
|
||||
cols={composer.cols}
|
||||
cwdLabel={status.cwdLabel}
|
||||
model={ui.info?.model?.split('/').pop() ?? ''}
|
||||
sessionStartedAt={status.sessionStartedAt}
|
||||
showCost={ui.showCost}
|
||||
status={ui.status}
|
||||
statusColor={status.statusColor}
|
||||
t={ui.theme}
|
||||
turnStartedAt={status.turnStartedAt}
|
||||
usage={ui.usage}
|
||||
voiceLabel={status.voiceLabel}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
export const AppLayout = memo(function AppLayout({
|
||||
actions,
|
||||
composer,
|
||||
|
|
@ -295,16 +310,20 @@ export const AppLayout = memo(function AppLayout({
|
|||
</Box>
|
||||
|
||||
{!overlay.agents && (
|
||||
<PromptZone
|
||||
cols={composer.cols}
|
||||
onApprovalChoice={actions.answerApproval}
|
||||
onClarifyAnswer={actions.answerClarify}
|
||||
onSecretSubmit={actions.answerSecret}
|
||||
onSudoSubmit={actions.answerSudo}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<PromptZone
|
||||
cols={composer.cols}
|
||||
onApprovalChoice={actions.answerApproval}
|
||||
onClarifyAnswer={actions.answerClarify}
|
||||
onSecretSubmit={actions.answerSecret}
|
||||
onSudoSubmit={actions.answerSudo}
|
||||
/>
|
||||
|
||||
{!overlay.agents && <ComposerPane actions={actions} composer={composer} status={status} />}
|
||||
<ComposerPane actions={actions} composer={composer} status={status} />
|
||||
|
||||
<StatusRulePane at="bottom" composer={composer} status={status} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</AlternateScreen>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -167,9 +167,11 @@ export function lineNav(s: string, p: number, dir: -1 | 1): null | number {
|
|||
return snapPos(s, Math.min(nextBreak + 1 + col, lineEnd))
|
||||
}
|
||||
|
||||
function cursorLayout(value: string, cursor: number, cols: number) {
|
||||
// mirrors wrap-ansi(..., { wordWrap: false, hard: true }) so the declared
|
||||
// cursor lines up with what <Text wrap="wrap-char"> actually renders
|
||||
export function cursorLayout(value: string, cursor: number, cols: number) {
|
||||
const pos = Math.max(0, Math.min(cursor, value.length))
|
||||
const w = Math.max(1, cols - 1)
|
||||
const w = Math.max(1, cols)
|
||||
|
||||
let col = 0,
|
||||
line = 0
|
||||
|
|
@ -200,17 +202,23 @@ function cursorLayout(value: string, cursor: number, cols: number) {
|
|||
col += sw
|
||||
}
|
||||
|
||||
// trailing cursor-cell overflows to the next row at the wrap column
|
||||
if (col >= w) {
|
||||
line++
|
||||
col = 0
|
||||
}
|
||||
|
||||
return { column: col, line }
|
||||
}
|
||||
|
||||
function offsetFromPosition(value: string, row: number, col: number, cols: number) {
|
||||
export function offsetFromPosition(value: string, row: number, col: number, cols: number) {
|
||||
if (!value.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const targetRow = Math.max(0, Math.floor(row))
|
||||
const targetCol = Math.max(0, Math.floor(col))
|
||||
const w = Math.max(1, cols - 1)
|
||||
const w = Math.max(1, cols)
|
||||
|
||||
let line = 0
|
||||
let column = 0
|
||||
|
|
@ -802,7 +810,7 @@ export function TextInput({
|
|||
}}
|
||||
ref={boxRef}
|
||||
>
|
||||
<Text wrap="wrap">{rendered}</Text>
|
||||
<Text wrap="wrap-char">{rendered}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue