mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(tui): stabilize live progress rendering
This commit is contained in:
parent
d4dde6b5f2
commit
a7831b63db
28 changed files with 619 additions and 154 deletions
|
|
@ -38,6 +38,7 @@ export type ScrollBoxHandle = {
|
|||
* padding). Used for drag-to-scroll edge detection.
|
||||
*/
|
||||
getViewportTop: () => number
|
||||
getLastManualScrollAt: () => number
|
||||
/**
|
||||
* True when scroll is pinned to the bottom. Set by scrollToBottom, the
|
||||
* initial stickyScroll attribute, and by the renderer when positional
|
||||
|
|
@ -94,6 +95,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
|||
// forces a React render: sticky is attribute-observed, no DOM-only path.
|
||||
const [, forceRender] = useState(0)
|
||||
const listenersRef = useRef(new Set<() => void>())
|
||||
const manualScrollAtRef = useRef(0)
|
||||
const renderQueuedRef = useRef(false)
|
||||
|
||||
const notify = () => {
|
||||
|
|
@ -130,6 +132,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
|||
}
|
||||
|
||||
el.stickyScroll = false
|
||||
manualScrollAtRef.current = Date.now()
|
||||
el.scrollAnchor = undefined
|
||||
el.pendingScrollDelta = (el.pendingScrollDelta ?? 0) + Math.floor(dy)
|
||||
scrollMutated(el)
|
||||
|
|
@ -148,6 +151,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
|||
// Explicit false overrides the DOM attribute so manual scroll
|
||||
// breaks stickiness. Render code checks ?? precedence.
|
||||
el.stickyScroll = false
|
||||
manualScrollAtRef.current = Date.now()
|
||||
el.pendingScrollDelta = undefined
|
||||
el.scrollAnchor = undefined
|
||||
el.scrollTop = Math.max(0, Math.floor(y))
|
||||
|
|
@ -161,6 +165,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
|||
}
|
||||
|
||||
box.stickyScroll = false
|
||||
manualScrollAtRef.current = Date.now()
|
||||
box.pendingScrollDelta = undefined
|
||||
box.scrollAnchor = {
|
||||
el,
|
||||
|
|
@ -205,6 +210,9 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
|||
getViewportTop() {
|
||||
return domRef.current?.scrollViewportTop ?? 0
|
||||
},
|
||||
getLastManualScrollAt() {
|
||||
return manualScrollAtRef.current
|
||||
},
|
||||
isSticky() {
|
||||
const el = domRef.current
|
||||
|
||||
|
|
|
|||
|
|
@ -120,11 +120,7 @@ function parseKey(keypress: ParsedKey): [Key, string] {
|
|||
// through key.return/key.escape, and processedAsSpecialSequence bypasses
|
||||
// the nonAlphanumericKeys clear below, so clear them explicitly here.
|
||||
input =
|
||||
keypress.name === 'space'
|
||||
? ' '
|
||||
: keypress.name === 'return' || keypress.name === 'escape'
|
||||
? ''
|
||||
: keypress.name
|
||||
keypress.name === 'space' ? ' ' : keypress.name === 'return' || keypress.name === 'escape' ? '' : keypress.name
|
||||
}
|
||||
|
||||
processedAsSpecialSequence = true
|
||||
|
|
@ -143,11 +139,7 @@ function parseKey(keypress: ParsedKey): [Key, string] {
|
|||
input = ''
|
||||
} else {
|
||||
input =
|
||||
keypress.name === 'space'
|
||||
? ' '
|
||||
: keypress.name === 'return' || keypress.name === 'escape'
|
||||
? ''
|
||||
: keypress.name
|
||||
keypress.name === 'space' ? ' ' : keypress.name === 'return' || keypress.name === 'escape' ? '' : keypress.name
|
||||
}
|
||||
|
||||
processedAsSpecialSequence = true
|
||||
|
|
|
|||
|
|
@ -1328,7 +1328,9 @@ export default class Ink {
|
|||
}
|
||||
|
||||
if (process.env.HERMES_TUI_DEBUG_CLIPBOARD) {
|
||||
console.error('[clipboard] no path reached the clipboard (headless + no tmux?) — set HERMES_TUI_FORCE_OSC52=1 to force the escape sequence')
|
||||
console.error(
|
||||
'[clipboard] no path reached the clipboard (headless + no tmux?) — set HERMES_TUI_FORCE_OSC52=1 to force the escape sequence'
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
if (process.env.HERMES_TUI_DEBUG_CLIPBOARD) {
|
||||
|
|
@ -1799,6 +1801,7 @@ export default class Ink {
|
|||
|
||||
if (this.selectionDragCell?.col === col && this.selectionDragCell.row === row) {
|
||||
this.updateSelectionAutoScroll(row)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1822,6 +1825,7 @@ export default class Ink {
|
|||
private updateSelectionAutoScroll(row: number): void {
|
||||
if (!this.selection.isDragging || !this.altScreenActive) {
|
||||
this.stopSelectionAutoScroll()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1829,6 +1833,7 @@ export default class Ink {
|
|||
|
||||
if (dir === 0) {
|
||||
this.stopSelectionAutoScroll()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1844,6 +1849,7 @@ export default class Ink {
|
|||
private stepSelectionAutoScroll(): void {
|
||||
if (!this.selection.isDragging || !this.altScreenActive || this.selectionAutoScrollDir === 0) {
|
||||
this.stopSelectionAutoScroll()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1851,6 +1857,7 @@ export default class Ink {
|
|||
|
||||
if (!box) {
|
||||
this.stopSelectionAutoScroll()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1889,7 +1896,10 @@ export default class Ink {
|
|||
}
|
||||
}
|
||||
|
||||
this.applySelectionDrag(this.selectionDragCell?.col ?? 0, this.selectionDragCell?.row ?? (this.selectionAutoScrollDir > 0 ? bottom : top))
|
||||
this.applySelectionDrag(
|
||||
this.selectionDragCell?.col ?? 0,
|
||||
this.selectionDragCell?.row ?? (this.selectionAutoScrollDir > 0 ? bottom : top)
|
||||
)
|
||||
}
|
||||
|
||||
private stopSelectionAutoScroll(): void {
|
||||
|
|
@ -1908,7 +1918,11 @@ export default class Ink {
|
|||
while (stack.length) {
|
||||
const node = stack.shift()!
|
||||
|
||||
if (node.style.overflowY === 'scroll' && node.scrollHeight !== undefined && node.scrollViewportHeight !== undefined) {
|
||||
if (
|
||||
node.style.overflowY === 'scroll' &&
|
||||
node.scrollHeight !== undefined &&
|
||||
node.scrollViewportHeight !== undefined
|
||||
) {
|
||||
return node
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ export function shouldEmitClipboardSequence(env: NodeJS.ProcessEnv = process.env
|
|||
const override = (
|
||||
env.HERMES_TUI_FORCE_OSC52 ??
|
||||
env.HERMES_TUI_CLIPBOARD_OSC52 ??
|
||||
env.HERMES_TUI_COPY_OSC52 ?? ''
|
||||
env.HERMES_TUI_COPY_OSC52 ??
|
||||
''
|
||||
).trim()
|
||||
|
||||
if (ENV_ON_RE.test(override)) {
|
||||
|
|
@ -196,16 +197,19 @@ export async function setClipboard(text: string): Promise<ClipboardResult> {
|
|||
// forever but SSH_CONNECTION is in tmux's default update-environment and
|
||||
// clears on local attach. Fire-and-forget, but `copyNativeAttempted`
|
||||
// tells us whether ANY native path will be tried on this platform.
|
||||
const nativeAttempted =
|
||||
!process.env['SSH_CONNECTION'] && copyNative(text)
|
||||
const nativeAttempted = !process.env['SSH_CONNECTION'] && copyNative(text)
|
||||
|
||||
const tmuxBufferLoaded = await tmuxLoadBuffer(text)
|
||||
|
||||
// Inner OSC uses BEL directly (not osc()) — ST's ESC would need doubling
|
||||
// too, and BEL works everywhere for OSC 52.
|
||||
const sequence = tmuxBufferLoaded
|
||||
? (emitSequence ? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`) : '')
|
||||
: (emitSequence ? raw : '')
|
||||
? emitSequence
|
||||
? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`)
|
||||
: ''
|
||||
: emitSequence
|
||||
? raw
|
||||
: ''
|
||||
|
||||
// Success if any path was taken. Native and tmux are fire-and-forget,
|
||||
// so we can't truly confirm the clipboard was written — but if native
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue