2026-04-26 15:23:43 -05:00
|
|
|
|
#!/usr/bin/env node
|
2026-04-26 21:20:54 -05:00
|
|
|
|
/* global Buffer, console, process, setImmediate */
|
2026-04-26 15:23:43 -05:00
|
|
|
|
import inspector from 'node:inspector'
|
|
|
|
|
|
import { performance } from 'node:perf_hooks'
|
|
|
|
|
|
|
|
|
|
|
|
import React from 'react'
|
|
|
|
|
|
import { render } from '@hermes/ink'
|
|
|
|
|
|
import { AppLayout } from '../src/components/appLayout.tsx'
|
|
|
|
|
|
import { resetOverlayState } from '../src/app/overlayStore.ts'
|
|
|
|
|
|
import { resetTurnState } from '../src/app/turnStore.ts'
|
|
|
|
|
|
import { resetUiState } from '../src/app/uiStore.ts'
|
|
|
|
|
|
|
|
|
|
|
|
const session = new inspector.Session()
|
|
|
|
|
|
session.connect()
|
|
|
|
|
|
const post = (method, params = {}) => new Promise((resolve, reject) => {
|
|
|
|
|
|
session.post(method, params, (err, result) => err ? reject(err) : resolve(result))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-26 21:20:54 -05:00
|
|
|
|
const historySize = Number(process.env.HISTORY || 500)
|
|
|
|
|
|
const mountedRows = Number(process.env.MOUNTED || 120)
|
|
|
|
|
|
|
2026-04-26 15:23:43 -05:00
|
|
|
|
class Sink {
|
|
|
|
|
|
columns = Number(process.env.COLS || 120)
|
|
|
|
|
|
rows = Number(process.env.ROWS || 42)
|
|
|
|
|
|
isTTY = true
|
|
|
|
|
|
bytes = 0
|
|
|
|
|
|
writes = 0
|
|
|
|
|
|
listeners = new Map()
|
|
|
|
|
|
write(chunk) {
|
2026-04-26 21:20:54 -05:00
|
|
|
|
this.bytes += Buffer.byteLength(String(chunk ?? ''))
|
2026-04-26 15:23:43 -05:00
|
|
|
|
this.writes++
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
on(event, fn) { this.listeners.set(event, fn); return this }
|
|
|
|
|
|
off(event) { this.listeners.delete(event); return this }
|
|
|
|
|
|
once(event, fn) { this.listeners.set(event, fn); return this }
|
|
|
|
|
|
removeListener(event) { this.listeners.delete(event); return this }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const theme = {
|
|
|
|
|
|
brand: { prompt: '›' },
|
|
|
|
|
|
color: {
|
|
|
|
|
|
amber: '#d19a66', bronze: '#8b6f47', dim: '#6b7280', error: '#ff5555', gold: '#ffd166', label: '#61afef',
|
|
|
|
|
|
ok: '#98c379', warn: '#e5c07b', cornsilk: '#fff8dc', prompt: '#c678dd', shellDollar: '#98c379',
|
|
|
|
|
|
statusCritical: '#ff5555', statusBad: '#e06c75', statusWarn: '#e5c07b', statusGood: '#98c379',
|
|
|
|
|
|
selectionBg: '#44475a'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const noop = () => {}
|
2026-04-26 21:20:54 -05:00
|
|
|
|
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)}`
|
|
|
|
|
|
}))
|
|
|
|
|
|
]
|
2026-04-26 15:23:43 -05:00
|
|
|
|
const scrollRef = { current: {
|
|
|
|
|
|
getScrollTop: () => 0,
|
|
|
|
|
|
getPendingDelta: () => 0,
|
2026-04-26 21:20:54 -05:00
|
|
|
|
getScrollHeight: () => historySize * 4,
|
2026-04-26 15:23:43 -05:00
|
|
|
|
getViewportHeight: () => 30,
|
|
|
|
|
|
getViewportTop: () => 0,
|
|
|
|
|
|
isSticky: () => true,
|
|
|
|
|
|
subscribe: () => () => {},
|
|
|
|
|
|
scrollBy: noop,
|
|
|
|
|
|
scrollTo: noop,
|
|
|
|
|
|
scrollToBottom: noop,
|
|
|
|
|
|
setClampBounds: noop,
|
|
|
|
|
|
getLastManualScrollAt: () => 0
|
|
|
|
|
|
} }
|
|
|
|
|
|
|
|
|
|
|
|
const baseProps = streamingText => ({
|
|
|
|
|
|
actions: { answerApproval: noop, answerClarify: noop, answerSecret: noop, answerSudo: noop, onModelSelect: noop, resumeById: noop, setStickyPrompt: noop },
|
|
|
|
|
|
composer: { cols: 120, compIdx: 0, completions: [], empty: false, handleTextPaste: () => null, input: '', inputBuf: [], pagerPageSize: 10, queueEditIdx: null, queuedDisplay: [], submit: noop, updateInput: noop },
|
|
|
|
|
|
mouseTracking: false,
|
|
|
|
|
|
progress: {
|
|
|
|
|
|
activity: [], outcome: '', reasoning: streamingText, reasoningActive: true, reasoningStreaming: true,
|
|
|
|
|
|
reasoningTokens: Math.ceil(streamingText.length / 4), showProgressArea: true, showStreamingArea: true,
|
|
|
|
|
|
streamPendingTools: [], streamSegments: [], streaming: streamingText, subagents: [], toolTokens: 0, tools: [], turnTrail: [], todos: []
|
|
|
|
|
|
},
|
|
|
|
|
|
status: { cwdLabel: '~/repo', goodVibesTick: 0, sessionStartedAt: Date.now(), showStickyPrompt: false, statusColor: theme.color.ok, stickyPrompt: '', turnStartedAt: Date.now(), voiceLabel: 'voice off' },
|
|
|
|
|
|
transcript: {
|
|
|
|
|
|
historyItems,
|
|
|
|
|
|
scrollRef,
|
2026-04-26 21:20:54 -05:00
|
|
|
|
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 }))
|
2026-04-26 15:23:43 -05:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
2026-04-26 21:20:54 -05:00
|
|
|
|
resetUiState()
|
|
|
|
|
|
resetTurnState()
|
|
|
|
|
|
resetOverlayState()
|
2026-04-26 15:23:43 -05:00
|
|
|
|
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')
|
|
|
|
|
|
const inst = render(React.createElement(AppLayout, baseProps('')), { stdout, stdin, stderr: stdout, debug: false, exitOnCtrlC: false })
|
|
|
|
|
|
|
|
|
|
|
|
await post('Profiler.enable')
|
|
|
|
|
|
await post('HeapProfiler.enable')
|
|
|
|
|
|
await post('Profiler.start')
|
|
|
|
|
|
const startMem = process.memoryUsage()
|
|
|
|
|
|
const t0 = performance.now()
|
|
|
|
|
|
const iterations = Number(process.env.ITERS || 40)
|
|
|
|
|
|
for (let i = 1; i <= iterations; i++) {
|
|
|
|
|
|
const prefix = text.slice(0, Math.floor(text.length * i / iterations))
|
|
|
|
|
|
inst.rerender(React.createElement(AppLayout, baseProps(prefix)))
|
|
|
|
|
|
await new Promise(r => setImmediate(r))
|
|
|
|
|
|
}
|
|
|
|
|
|
const elapsed = performance.now() - t0
|
|
|
|
|
|
const prof = await post('Profiler.stop')
|
|
|
|
|
|
const endMem = process.memoryUsage()
|
|
|
|
|
|
await post('HeapProfiler.collectGarbage')
|
|
|
|
|
|
const afterGc = process.memoryUsage()
|
|
|
|
|
|
inst.unmount()
|
|
|
|
|
|
session.disconnect()
|
|
|
|
|
|
console.log(JSON.stringify({ elapsedMs: Math.round(elapsed), stdoutBytes: stdout.bytes, stdoutWrites: stdout.writes, startMem, endMem, afterGc, profileNodes: prof.profile.nodes.length }, null, 2))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch(err => { console.error(err); process.exit(1) })
|