diff --git a/ui-tui/babel.compiler.config.cjs b/ui-tui/babel.compiler.config.cjs index ab41a82e2b..18f2a7aaa4 100644 --- a/ui-tui/babel.compiler.config.cjs +++ b/ui-tui/babel.compiler.config.cjs @@ -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 = { assumptions: { setPublicClassFields: true @@ -16,17 +7,9 @@ module.exports = { 'babel-plugin-react-compiler', { target: '19', - sources: (filename) => { - if (!filename) return false - if (filename.includes('node_modules')) return false - return true - } + sources: filename => Boolean(filename && !filename.includes('node_modules')) } ] ], - // 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 } diff --git a/ui-tui/eslint.config.mjs b/ui-tui/eslint.config.mjs index 4452f49fa5..09af222979 100644 --- a/ui-tui/eslint.config.mjs +++ b/ui-tui/eslint.config.mjs @@ -55,11 +55,6 @@ export default [ '@typescript-eslint/no-unused-vars': 'off', 'no-undef': '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', 'padding-line-between-statements': [ 1, @@ -97,8 +92,6 @@ export default [ 'no-constant-condition': 'off', 'no-empty': '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-hooks/exhaustive-deps': 'off' } diff --git a/ui-tui/packages/hermes-ink/src/ink/components/App.tsx b/ui-tui/packages/hermes-ink/src/ink/components/App.tsx index 64c181a031..e5a13bdb68 100644 --- a/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/components/App.tsx @@ -205,11 +205,6 @@ export default class App extends PureComponent { ) } - 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() { if (this.props.stdout.isTTY) { this.props.stdout.write(SHOW_CURSOR) @@ -574,9 +569,7 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void { const row = m.row - 1 const baseButton = m.button & 0x03 - // Allow disabling app click/selection handling while keeping wheel scroll - // and DOM mouse dispatch alive. Put this after coordinate/button decoding - // and exempt non-left buttons so scrollbar/right-click handlers still work. + // Disable app click handling without blocking wheel/right-click dispatch. if (isMouseClicksDisabled() && baseButton === 0) { return } diff --git a/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx b/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx index c475773c1d..15e896cb9c 100644 --- a/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx +++ b/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx @@ -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( ref, (): ScrollBoxHandle => ({ @@ -173,7 +159,19 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren< } 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() { const el = domRef.current diff --git a/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts b/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts index 6e80070e76..19031402bc 100644 --- a/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +++ b/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts @@ -2,6 +2,9 @@ import { nonAlphanumericKeys, type ParsedKey } from '../parse-keypress.js' import { Event } from './event.js' +const inputForSpecialSequence = (name: string): string => + name === 'space' ? ' ' : name === 'return' || name === 'escape' ? '' : name + export type Key = { upArrow: 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. input = '' } else { - // 'space' → ' '; functional keys like Enter/Escape carry their state - // 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 + input = inputForSpecialSequence(keypress.name) } processedAsSpecialSequence = true @@ -138,8 +137,7 @@ function parseKey(keypress: ParsedKey): [Key, string] { // guards against future terminal behavior. input = '' } else { - input = - keypress.name === 'space' ? ' ' : keypress.name === 'return' || keypress.name === 'escape' ? '' : keypress.name + input = inputForSpecialSequence(keypress.name) } processedAsSpecialSequence = true diff --git a/ui-tui/packages/hermes-ink/src/ink/frame.ts b/ui-tui/packages/hermes-ink/src/ink/frame.ts index 760fcc52fe..1c9f55c75f 100644 --- a/ui-tui/packages/hermes-ink/src/ink/frame.ts +++ b/ui-tui/packages/hermes-ink/src/ink/frame.ts @@ -46,16 +46,13 @@ export type FrameEvent = { write: number /** Pre-optimize patch count (proxy for how much changed this frame) */ patches: number - /** Post-optimize patch count — what was actually written to stdout. */ + /** Post-optimize patch count. */ optimizedPatches: number - /** Bytes written to stdout this frame (escape sequences + payload). */ + /** Bytes written to stdout this frame. */ writeBytes: number - /** Whether stdout.write returned false (backpressure = outer terminal slow). */ + /** Whether stdout.write returned false. */ backpressure: boolean - /** ms from this frame's stdout.write until the write-callback fired. - * 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). */ + /** Previous stdout.write callback latency; 0 if drained before next frame. */ prevFrameDrainMs: number /** yoga calculateLayout() time (runs in resetAfterCommit, before onRender) */ yoga: number diff --git a/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts b/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts index fb683794ff..99dce2df34 100644 --- a/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts +++ b/ui-tui/packages/hermes-ink/src/ink/termio/osc.ts @@ -203,13 +203,7 @@ export async function setClipboard(text: string): Promise { // 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 - : '' + const sequence = emitSequence ? (tmuxBufferLoaded ? tmuxPassthrough(`${ESC}]52;c;${b64}${BEL}`) : 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 diff --git a/ui-tui/scripts/profile-tui.mjs b/ui-tui/scripts/profile-tui.mjs index 7093ef9f49..ffdfedd034 100644 --- a/ui-tui/scripts/profile-tui.mjs +++ b/ui-tui/scripts/profile-tui.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +/* global Buffer, console, process, setImmediate */ import inspector from 'node:inspector' 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)) }) +const historySize = Number(process.env.HISTORY || 500) +const mountedRows = Number(process.env.MOUNTED || 120) + class Sink { columns = Number(process.env.COLS || 120) rows = Number(process.env.ROWS || 42) @@ -23,8 +27,7 @@ class Sink { writes = 0 listeners = new Map() write(chunk) { - const s = String(chunk ?? '') - this.bytes += Buffer.byteLength(s) + this.bytes += Buffer.byteLength(String(chunk ?? '')) this.writes++ return true } @@ -45,13 +48,17 @@ const theme = { } const noop = () => {} -const makeMsg = i => ({ role: i % 5 === 0 ? 'user' : 'assistant', text: `message ${i}\n${'lorem ipsum '.repeat(80)}` }) -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))] -const mkRows = items => items.map((msg, index) => ({ index, key: `m${index}`, msg })) +const historyItems = [ + { kind: 'intro', role: 'system', text: '', info: { model: 'test', tools: {}, skills: {}, version: 'test' } }, + ...Array.from({ length: historySize }, (_, i) => ({ + role: i % 5 === 0 ? 'user' : 'assistant', + text: `message ${i}\n${'lorem ipsum '.repeat(80)}` + })) +] const scrollRef = { current: { getScrollTop: () => 0, getPendingDelta: () => 0, - getScrollHeight: () => Number(process.env.HISTORY || 500) * 4, + getScrollHeight: () => historySize * 4, getViewportHeight: () => 30, getViewportTop: () => 0, isSticky: () => true, @@ -76,13 +83,15 @@ const baseProps = streamingText => ({ transcript: { historyItems, 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 }, - virtualRows: mkRows(historyItems) + virtualHistory: { bottomSpacer: 0, end: historyItems.length, measureRef: () => noop, offsets: historyItems.map((_, i) => i * 4), start: Math.max(0, historyItems.length - mountedRows), topSpacer: 0 }, + virtualRows: historyItems.map((msg, index) => ({ index, key: `m${index}`, msg })) } }) async function main() { - resetUiState(); resetTurnState(); resetOverlayState() + resetUiState() + resetTurnState() + resetOverlayState() const stdout = new Sink() 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')