hermes-agent/ui-opentui/scripts/demo.tsx
alt-glitch 8afb7bc570 opentui(v6): double-click word / triple-click line selection with held drag-extend
Editor-grade mouse selection parity with the Ink TUI (hermes-ink selection.ts):
a second click in the 500ms/1-cell chain selects the same-class character run
under the cursor (iTerm2 word set, wide-glyph aware), a third selects the line,
and dragging with the button held extends word-by-word / line-by-line while the
clicked span stays selected — anchor flips across the span on direction change.

Core knows only press-drag char selection, so this is a boundary shim
(multiClickSelect.ts) wrapping the renderer's startSelection/updateSelection
seam; word bounds read the presented frame's char grid. Native quirks probed
and pinned: per-renderable selection anchors are fixed at set time (anchor
flips restart the selection) and forward selections exclude the focus cell
(inclusive spans seed focus at hi+1). Pure scanning logic in logic/multiClick.ts;
20 new tests (pure + real-mouse-path frames); demo.tsx installs the seam for
tmux smokes.
2026-06-11 15:58:33 +05:30

52 lines
2 KiB
TypeScript

/**
* DEV DEMO — NOT a test, NOT production. Renders the bench fixture (lorem-ipsum +
* fat tool-turns from ./fixture.ts) in a REAL CliRenderer so you can attach over
* tmux, scroll, and eyeball the transcript + the rolling-cap truncation notice.
* No gateway is spawned (purely the fixture seeded into the store via the resume
* path), so typing won't reach a backend — it's for viewing/scrolling.
*
* Run (Node 26 — needs the esbuild/Solid transform, then --experimental-ffi):
* node scripts/build.mjs scripts/demo.tsx .demo
* node --experimental-ffi --no-warnings .demo/demo.js # inside tmux (needs a TTY)
* DEMO_TOTAL=200 fixture messages to seed (default 200)
* HERMES_TUI_MAX_MESSAGES=80 cap → the "⤒ N earlier messages" notice fires
* Quit: Ctrl+C.
*/
import { createCliRenderer } from '@opentui/core'
import { render } from '@opentui/solid'
import { installMultiClickSelection } from '../src/boundary/multiClickSelect.ts'
import { createSessionStore } from '../src/logic/store.ts'
import { App } from '../src/view/App.tsx'
import { ThemeProvider } from '../src/view/theme.tsx'
import { materialize } from './fixture.ts'
const TOTAL = Number.parseInt(process.env.DEMO_TOTAL ?? '', 10) || 200
const store = createSessionStore()
store.apply({ type: 'gateway.ready' })
store.setSessionId('demo-fixture-20260609')
// Seed via the resume path so the cap slices + the `dropped` counter is set
// (drives the truncation notice) exactly as a real `session.resume` would.
store.beginBuffer()
store.commitSnapshot(materialize(TOTAL))
const renderer = await createCliRenderer({
externalOutputMode: 'passthrough',
targetFps: 60,
exitOnCtrlC: true,
useKittyKeyboard: {},
useMouse: true
})
// Same seam the live entry installs (boundary/renderer.ts) so the demo smokes
// double-click word / triple-click line / drag-extend too.
installMultiClickSelection(renderer)
void render(
() => (
<ThemeProvider theme={() => store.state.theme}>
<App store={store} />
</ThemeProvider>
),
renderer
)