mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
chore(tui): clean remaining Ink perf scaffolding
Trim narration comments and collapse small one-off helpers in the remaining ui-tui perf support files while preserving behaviour.
This commit is contained in:
parent
7da2f07641
commit
2e4b65b9f5
8 changed files with 43 additions and 78 deletions
|
|
@ -1,12 +1,3 @@
|
||||||
// React Compiler runs as a post-pass over tsc's `dist/` output.
|
|
||||||
//
|
|
||||||
// tsc emits JSX as _jsx() calls (jsx: "react-jsx"). babel-plugin-react-compiler
|
|
||||||
// accepts that shape and auto-memoizes every component it recognizes via the
|
|
||||||
// default `infer` compilation mode (PascalCase components + use-prefixed
|
|
||||||
// hooks). The `sources` filter keeps it from walking node_modules files that
|
|
||||||
// end up in source maps.
|
|
||||||
//
|
|
||||||
// target=19 matches our react ^19.2.4 dependency.
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
assumptions: {
|
assumptions: {
|
||||||
setPublicClassFields: true
|
setPublicClassFields: true
|
||||||
|
|
@ -16,17 +7,9 @@ module.exports = {
|
||||||
'babel-plugin-react-compiler',
|
'babel-plugin-react-compiler',
|
||||||
{
|
{
|
||||||
target: '19',
|
target: '19',
|
||||||
sources: (filename) => {
|
sources: filename => Boolean(filename && !filename.includes('node_modules'))
|
||||||
if (!filename) return false
|
|
||||||
if (filename.includes('node_modules')) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
// We feed already-compiled JS into babel; don't re-parse as TS/JSX.
|
|
||||||
// @babel/preset-env etc. would over-transform — the compiler is our only
|
|
||||||
// transform here. babelrc:false stops @babel/cli from walking up the
|
|
||||||
// filesystem looking for other configs (the parent repo might add one).
|
|
||||||
babelrc: false
|
babelrc: false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,6 @@ export default [
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'no-undef': 'off',
|
'no-undef': 'off',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
// React Compiler: warn (not error) so the gate doesn't block merges
|
|
||||||
// while we migrate. Flags patterns that would break the compiler at
|
|
||||||
// runtime (mutating refs during render, non-PascalCase components,
|
|
||||||
// etc.). See audit §5 — we run the compiler in `npm run build` as a
|
|
||||||
// post-pass over tsc's `dist/` output.
|
|
||||||
'react-compiler/react-compiler': 'warn',
|
'react-compiler/react-compiler': 'warn',
|
||||||
'padding-line-between-statements': [
|
'padding-line-between-statements': [
|
||||||
1,
|
1,
|
||||||
|
|
@ -97,8 +92,6 @@ export default [
|
||||||
'no-constant-condition': 'off',
|
'no-constant-condition': 'off',
|
||||||
'no-empty': 'off',
|
'no-empty': 'off',
|
||||||
'no-redeclare': 'off',
|
'no-redeclare': 'off',
|
||||||
// Ink internals: reconciler, style pool, DOM node impl — full of
|
|
||||||
// intentional side effects the compiler rules reject.
|
|
||||||
'react-compiler/react-compiler': 'off',
|
'react-compiler/react-compiler': 'off',
|
||||||
'react-hooks/exhaustive-deps': 'off'
|
'react-hooks/exhaustive-deps': 'off'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,11 +205,6 @@ export default class App extends PureComponent<Props, State> {
|
||||||
</TerminalSizeContext.Provider>
|
</TerminalSizeContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override componentDidMount() {
|
|
||||||
// Keep the native terminal cursor visible. Ink parks it at the declared
|
|
||||||
// input caret after each frame, so the terminal emulator provides the
|
|
||||||
// normal blinking block/bar without React-driven blink re-renders.
|
|
||||||
}
|
|
||||||
override componentWillUnmount() {
|
override componentWillUnmount() {
|
||||||
if (this.props.stdout.isTTY) {
|
if (this.props.stdout.isTTY) {
|
||||||
this.props.stdout.write(SHOW_CURSOR)
|
this.props.stdout.write(SHOW_CURSOR)
|
||||||
|
|
@ -574,9 +569,7 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
||||||
const row = m.row - 1
|
const row = m.row - 1
|
||||||
const baseButton = m.button & 0x03
|
const baseButton = m.button & 0x03
|
||||||
|
|
||||||
// Allow disabling app click/selection handling while keeping wheel scroll
|
// Disable app click handling without blocking wheel/right-click dispatch.
|
||||||
// and DOM mouse dispatch alive. Put this after coordinate/button decoding
|
|
||||||
// and exempt non-left buttons so scrollbar/right-click handlers still work.
|
|
||||||
if (isMouseClicksDisabled() && baseButton === 0) {
|
if (isMouseClicksDisabled() && baseButton === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,20 +124,6 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollByNow = (dy: number) => {
|
|
||||||
const el = domRef.current
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
el.stickyScroll = false
|
|
||||||
manualScrollAtRef.current = Date.now()
|
|
||||||
el.scrollAnchor = undefined
|
|
||||||
el.pendingScrollDelta = (el.pendingScrollDelta ?? 0) + Math.floor(dy)
|
|
||||||
scrollMutated(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
(): ScrollBoxHandle => ({
|
(): ScrollBoxHandle => ({
|
||||||
|
|
@ -173,7 +159,19 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
|
||||||
}
|
}
|
||||||
scrollMutated(box)
|
scrollMutated(box)
|
||||||
},
|
},
|
||||||
scrollBy: scrollByNow,
|
scrollBy(dy: number) {
|
||||||
|
const el = domRef.current
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
el.stickyScroll = false
|
||||||
|
manualScrollAtRef.current = Date.now()
|
||||||
|
el.scrollAnchor = undefined
|
||||||
|
el.pendingScrollDelta = (el.pendingScrollDelta ?? 0) + Math.floor(dy)
|
||||||
|
scrollMutated(el)
|
||||||
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
const el = domRef.current
|
const el = domRef.current
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { nonAlphanumericKeys, type ParsedKey } from '../parse-keypress.js'
|
||||||
|
|
||||||
import { Event } from './event.js'
|
import { Event } from './event.js'
|
||||||
|
|
||||||
|
const inputForSpecialSequence = (name: string): string =>
|
||||||
|
name === 'space' ? ' ' : name === 'return' || name === 'escape' ? '' : name
|
||||||
|
|
||||||
export type Key = {
|
export type Key = {
|
||||||
upArrow: boolean
|
upArrow: boolean
|
||||||
downArrow: boolean
|
downArrow: boolean
|
||||||
|
|
@ -116,11 +119,7 @@ function parseKey(keypress: ParsedKey): [Key, string] {
|
||||||
// so the raw "[57358u" doesn't leak into the prompt. See #38781.
|
// so the raw "[57358u" doesn't leak into the prompt. See #38781.
|
||||||
input = ''
|
input = ''
|
||||||
} else {
|
} else {
|
||||||
// 'space' → ' '; functional keys like Enter/Escape carry their state
|
input = inputForSpecialSequence(keypress.name)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processedAsSpecialSequence = true
|
processedAsSpecialSequence = true
|
||||||
|
|
@ -138,8 +137,7 @@ function parseKey(keypress: ParsedKey): [Key, string] {
|
||||||
// guards against future terminal behavior.
|
// guards against future terminal behavior.
|
||||||
input = ''
|
input = ''
|
||||||
} else {
|
} else {
|
||||||
input =
|
input = inputForSpecialSequence(keypress.name)
|
||||||
keypress.name === 'space' ? ' ' : keypress.name === 'return' || keypress.name === 'escape' ? '' : keypress.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processedAsSpecialSequence = true
|
processedAsSpecialSequence = true
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,13 @@ export type FrameEvent = {
|
||||||
write: number
|
write: number
|
||||||
/** Pre-optimize patch count (proxy for how much changed this frame) */
|
/** Pre-optimize patch count (proxy for how much changed this frame) */
|
||||||
patches: number
|
patches: number
|
||||||
/** Post-optimize patch count — what was actually written to stdout. */
|
/** Post-optimize patch count. */
|
||||||
optimizedPatches: number
|
optimizedPatches: number
|
||||||
/** Bytes written to stdout this frame (escape sequences + payload). */
|
/** Bytes written to stdout this frame. */
|
||||||
writeBytes: number
|
writeBytes: number
|
||||||
/** Whether stdout.write returned false (backpressure = outer terminal slow). */
|
/** Whether stdout.write returned false. */
|
||||||
backpressure: boolean
|
backpressure: boolean
|
||||||
/** ms from this frame's stdout.write until the write-callback fired.
|
/** Previous stdout.write callback latency; 0 if drained before next frame. */
|
||||||
* Populated on the NEXT frame (async), so this field reflects the
|
|
||||||
* PREVIOUS frame's terminal-drain time. 0 = callback already fired
|
|
||||||
* before next frame started (drained in sub-ms). */
|
|
||||||
prevFrameDrainMs: number
|
prevFrameDrainMs: number
|
||||||
/** yoga calculateLayout() time (runs in resetAfterCommit, before onRender) */
|
/** yoga calculateLayout() time (runs in resetAfterCommit, before onRender) */
|
||||||
yoga: number
|
yoga: number
|
||||||
|
|
|
||||||
|
|
@ -203,13 +203,7 @@ export async function setClipboard(text: string): Promise<ClipboardResult> {
|
||||||
|
|
||||||
// Inner OSC uses BEL directly (not osc()) — ST's ESC would need doubling
|
// Inner OSC uses BEL directly (not osc()) — ST's ESC would need doubling
|
||||||
// too, and BEL works everywhere for OSC 52.
|
// too, and BEL works everywhere for OSC 52.
|
||||||
const sequence = tmuxBufferLoaded
|
const sequence = emitSequence ? (tmuxBufferLoaded ? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`) : raw) : ''
|
||||||
? emitSequence
|
|
||||||
? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`)
|
|
||||||
: ''
|
|
||||||
: emitSequence
|
|
||||||
? raw
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// Success if any path was taken. Native and tmux are fire-and-forget,
|
// 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
|
// so we can't truly confirm the clipboard was written — but if native
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
/* global Buffer, console, process, setImmediate */
|
||||||
import inspector from 'node:inspector'
|
import inspector from 'node:inspector'
|
||||||
import { performance } from 'node:perf_hooks'
|
import { performance } from 'node:perf_hooks'
|
||||||
|
|
||||||
|
|
@ -15,6 +16,9 @@ const post = (method, params = {}) => new Promise((resolve, reject) => {
|
||||||
session.post(method, params, (err, result) => err ? reject(err) : resolve(result))
|
session.post(method, params, (err, result) => err ? reject(err) : resolve(result))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const historySize = Number(process.env.HISTORY || 500)
|
||||||
|
const mountedRows = Number(process.env.MOUNTED || 120)
|
||||||
|
|
||||||
class Sink {
|
class Sink {
|
||||||
columns = Number(process.env.COLS || 120)
|
columns = Number(process.env.COLS || 120)
|
||||||
rows = Number(process.env.ROWS || 42)
|
rows = Number(process.env.ROWS || 42)
|
||||||
|
|
@ -23,8 +27,7 @@ class Sink {
|
||||||
writes = 0
|
writes = 0
|
||||||
listeners = new Map()
|
listeners = new Map()
|
||||||
write(chunk) {
|
write(chunk) {
|
||||||
const s = String(chunk ?? '')
|
this.bytes += Buffer.byteLength(String(chunk ?? ''))
|
||||||
this.bytes += Buffer.byteLength(s)
|
|
||||||
this.writes++
|
this.writes++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -45,13 +48,17 @@ const theme = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
const makeMsg = i => ({ role: i % 5 === 0 ? 'user' : 'assistant', text: `message ${i}\n${'lorem ipsum '.repeat(80)}` })
|
const historyItems = [
|
||||||
const historyItems = [{ kind: 'intro', role: 'system', text: '', info: { model: 'test', tools: {}, skills: {}, version: 'test' } }, ...Array.from({ length: Number(process.env.HISTORY || 500) }, (_, i) => makeMsg(i))]
|
{ kind: 'intro', role: 'system', text: '', info: { model: 'test', tools: {}, skills: {}, version: 'test' } },
|
||||||
const mkRows = items => items.map((msg, index) => ({ index, key: `m${index}`, msg }))
|
...Array.from({ length: historySize }, (_, i) => ({
|
||||||
|
role: i % 5 === 0 ? 'user' : 'assistant',
|
||||||
|
text: `message ${i}\n${'lorem ipsum '.repeat(80)}`
|
||||||
|
}))
|
||||||
|
]
|
||||||
const scrollRef = { current: {
|
const scrollRef = { current: {
|
||||||
getScrollTop: () => 0,
|
getScrollTop: () => 0,
|
||||||
getPendingDelta: () => 0,
|
getPendingDelta: () => 0,
|
||||||
getScrollHeight: () => Number(process.env.HISTORY || 500) * 4,
|
getScrollHeight: () => historySize * 4,
|
||||||
getViewportHeight: () => 30,
|
getViewportHeight: () => 30,
|
||||||
getViewportTop: () => 0,
|
getViewportTop: () => 0,
|
||||||
isSticky: () => true,
|
isSticky: () => true,
|
||||||
|
|
@ -76,13 +83,15 @@ const baseProps = streamingText => ({
|
||||||
transcript: {
|
transcript: {
|
||||||
historyItems,
|
historyItems,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
virtualHistory: { bottomSpacer: 0, end: historyItems.length, measureRef: () => noop, offsets: historyItems.map((_, i) => i * 4), start: Math.max(0, historyItems.length - Number(process.env.MOUNTED || 120)), topSpacer: 0 },
|
virtualHistory: { bottomSpacer: 0, end: historyItems.length, measureRef: () => noop, offsets: historyItems.map((_, i) => i * 4), start: Math.max(0, historyItems.length - mountedRows), topSpacer: 0 },
|
||||||
virtualRows: mkRows(historyItems)
|
virtualRows: historyItems.map((msg, index) => ({ index, key: `m${index}`, msg }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
resetUiState(); resetTurnState(); resetOverlayState()
|
resetUiState()
|
||||||
|
resetTurnState()
|
||||||
|
resetOverlayState()
|
||||||
const stdout = new Sink()
|
const stdout = new Sink()
|
||||||
const stdin = { isTTY: true, setRawMode: noop, on: noop, off: noop, resume: noop, pause: noop }
|
const stdin = { isTTY: true, setRawMode: noop, on: noop, off: noop, resume: noop, pause: noop }
|
||||||
const text = Array.from({ length: Number(process.env.LINES || 1200) }, (_, i) => `stream line ${i} ${'x'.repeat(90)}`).join('\n')
|
const text = Array.from({ length: Number(process.env.LINES || 1200) }, (_, i) => `stream line ${i} ${'x'.repeat(90)}`).join('\n')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue