Files
hermes-agent/apps/desktop/src/components/pane-shell/geometry.ts
T
Brooklyn Nicholson 2c867b05ce perf(desktop): 60fps sash drag on real sessions — height-gate the RO pins
Driving HER real instance (real profile, real transcripts, streams live)
via CDP instead of synthetic tiles finally exposed the remaining stall.
The timeline on a real 60-frame sash drag:

  style recalc 2736ms | script 1027ms | layout 89ms
  top callsite: pin @ fallback.tsx — 927ms

Two pin-to-bottom ResizeObservers (the bounded tool window's and the
reasoning preview's) pinned on EVERY resize delivery. A sash drag changes
every message's WIDTH once per frame, so each frame ran scrollTop write ->
scrollHeight read across every tool group: a forced write-read reflow
cascade that the render counters could never see (zero React involvement).

Both pins are now height-gated off the RO entry (reflow-free): only
content GROWTH pins. Width-only deliveries return immediately.

Measured on the live app, same drag, before -> after:
  fps      11.5 -> 59-60
  p95      101ms -> 18ms
  slow>33  60/60 -> 1/60

Also in this batch (each was verified live before the next was attempted):
- thread/list: split messageSignature into STRUCTURAL (ids/roles — keys
  boundaries + row identity) and WEIGHT (part counts — budget only), and
  memoize groups + row JSX. A streamed part-append re-rendered every
  turn's boundary via its resetKey prop; explain() measured 540-865
  wasted Block renders per drag/stream sample, now {}.
- message-render-boundary: document the structural-only resetKey contract.
- tool/fallback: memoize ToolFallback's part object + ToolEntry/ToolTitle/
  ToolGlyph (151 renders each, 100% wasted, on real transcripts).
- use-message-stream: ADAPTIVE flush floor — next flush waits 3x the
  measured cost of the last one (33ms floor, 250ms cap), so multi-stream
  load degrades text update rate instead of input latency.
- tree-split: preview sash drags with inline flex on the two seam
  wrappers, committing the store ONCE on release (fixed-zone sides get
  flexBasis only, so a hidden sidebar can't leave a phantom gap).
- debug/: perf-live LoAF long-frame attribution, explain() cascade walker
  with changed-hook indices, diag-real-loop/key-latency/switch-trace
  probes that drive the real app over CDP.

Typing during 2 live streams: keystroke->paint p50 3.3ms, p95 18.4ms,
zero frames over 33ms. Session switch p50 ~35ms settled; the remaining
~1.3s outlier tail is streaming-session switches (React work-loop, not
style/layout) — next target.
2026-07-27 01:07:48 -05:00

309 lines
9.7 KiB
TypeScript

/**
* Pane geometry — AABB INTERSECTION → WINDOW-CONTROL AWARENESS. The native
* window controls (macOS traffic lights top-left / Windows-WSLg overlay
* top-right) are a rectangle in viewport pixels. Any region whose rect
* intersects it must reserve that space and expose a drag strip. One
* `intersect()` call replaces per-layout inset special cases.
*
* Also publishes the WORKSPACE zone's edges as CSS vars (see
* publishWorkspaceGeometry) — chrome that aligns to the main pane (titlebar
* title et al) consumes `var(--workspace-left/right)` in plain CSS instead of
* threading rects through React.
*/
import { type RefObject, useLayoutEffect, useState, useSyncExternalStore } from 'react'
import { $layoutTree } from '@/components/pane-shell/tree/store'
import { $connection } from '@/store/session'
// ---------------------------------------------------------------------------
// Rects
// ---------------------------------------------------------------------------
export interface Rect {
x: number
y: number
width: number
height: number
}
/** AABB intersection. Returns null when the rects don't overlap. */
export function intersect(a: Rect, b: Rect): Rect | null {
const x = Math.max(a.x, b.x)
const y = Math.max(a.y, b.y)
const right = Math.min(a.x + a.width, b.x + b.width)
const bottom = Math.min(a.y + a.height, b.y + b.height)
if (right <= x || bottom <= y) {
return null
}
return { x, y, width: right - x, height: bottom - y }
}
// ---------------------------------------------------------------------------
// Native window controls rect
// ---------------------------------------------------------------------------
/** Height of the band the native controls live in. */
const CONTROLS_BAND_HEIGHT = 34
/** Width of the macOS traffic-light cluster measured from the buttons' x. */
const MACOS_LIGHTS_WIDTH = 58
const MACOS_FALLBACK_BUTTON_X = 24
interface ConnectionLike {
windowButtonPosition?: { x: number; y: number } | null
nativeOverlayWidth?: number | null
isFullscreen?: boolean | null
}
/**
* The native window-control rectangle in viewport pixels, or null when there
* is nothing to dodge (fullscreen, plain browser, secondary windows with
* hidden controls).
*/
export function windowControlsRect(connection: ConnectionLike | null, viewportWidth: number): Rect | null {
const inElectron = typeof window !== 'undefined' && 'hermesDesktop' in window
if (!inElectron) {
return null
}
if (connection?.isFullscreen) {
return null
}
// Windows / WSLg: native overlay on the top-right.
const overlayWidth = connection?.nativeOverlayWidth ?? 0
if (overlayWidth > 0) {
return { x: viewportWidth - overlayWidth, y: 0, width: overlayWidth, height: CONTROLS_BAND_HEIGHT }
}
// macOS: traffic lights on the top-left. windowButtonPosition === null means
// the platform has no left-side controls at all (Windows/Linux w/o overlay).
const pos = connection?.windowButtonPosition
if (pos === null) {
return null
}
const isMac = typeof navigator !== 'undefined' && /mac/i.test(navigator.platform)
if (!pos && !isMac) {
return null
}
const x = pos?.x ?? MACOS_FALLBACK_BUTTON_X
return { x: 0, y: 0, width: x + MACOS_LIGHTS_WIDTH, height: CONTROLS_BAND_HEIGHT }
}
// ---------------------------------------------------------------------------
// Live hook
// ---------------------------------------------------------------------------
let cachedRect: Rect | null = null
let cachedKey = ''
function rectKey(r: Rect | null) {
return r ? `${r.x},${r.y},${r.width},${r.height}` : ''
}
function readControlsRect(): Rect | null {
const next = windowControlsRect($connection.get(), typeof window === 'undefined' ? 0 : window.innerWidth)
const key = rectKey(next)
// Referentially stable snapshot for useSyncExternalStore.
if (key !== cachedKey) {
cachedKey = key
cachedRect = next
}
return cachedRect
}
function subscribeControlsRect(cb: () => void) {
const unsubConnection = $connection.subscribe(() => cb())
window.addEventListener('resize', cb)
return () => {
unsubConnection()
window.removeEventListener('resize', cb)
}
}
/** Reactive native window-controls rect (connection + viewport aware). */
export function useWindowControlsRect(): Rect | null {
return useSyncExternalStore(subscribeControlsRect, readControlsRect, () => null)
}
// ---------------------------------------------------------------------------
// Per-element overlap
// ---------------------------------------------------------------------------
function sameRect(a: Rect | null, b: Rect | null) {
if (a === b) {
return true
}
if (!a || !b) {
return false
}
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
}
// ---------------------------------------------------------------------------
// Workspace-edge CSS vars
// ---------------------------------------------------------------------------
// --- Sash-drag deferral ------------------------------------------------------
// The tree sash sets this for the duration of a resize gesture (pointerdown to
// pointerup). While set, `publishWorkspaceGeometry` skips its :root custom
// property writes: each one invalidates computed style for the whole document,
// and the sash's ResizeObserver fires per frame. Measured live (LoAF, real
// session): drag frames of ~68ms with style+layout=67ms and no script ≥5ms;
// suppressing the writes recovered 14fps → 51fps. The vars only align titlebar
// chrome — republishing once on release is visually identical.
let sashDragDepth = 0
let onSashDragEnd: null | (() => void) = null
export function beginSashDrag() {
sashDragDepth += 1
}
export function endSashDrag() {
sashDragDepth = Math.max(0, sashDragDepth - 1)
if (sashDragDepth === 0) {
onSashDragEnd?.()
}
}
const sashDragging = () => sashDragDepth > 0
/**
* Publish the workspace zone's viewport edges as root CSS vars:
* --workspace-left : px from the viewport's left to the main zone
* --workspace-right : px from the main zone's right to the viewport's right
*
* Measured once per relevant change (zone resize via ResizeObserver, layout
* mutations via $layoutTree, window resize) and consumed in plain CSS — the
* measured-var idiom (--composer-measured-height), not per-component JS
* geometry. Call once from the tree root; returns the disposer.
*/
export function publishWorkspaceGeometry(): () => void {
const root = document.documentElement
let el: HTMLElement | null = null
let lastLeft = NaN
let lastRight = NaN
const ro = new ResizeObserver(() => measure())
const measure = () => {
// DEFER during a sash drag (see beginSashDrag above) — republished once on
// release via the onSashDragEnd hook registered below.
if (sashDragging()) {
return
}
const next = document.querySelector<HTMLElement>('[data-session-anchor="workspace"]')
if (next !== el) {
if (el) {
ro.unobserve(el)
}
el = next
if (el) {
ro.observe(el)
}
}
if (!el) {
return
}
const r = el.getBoundingClientRect()
const left = Math.round(r.left)
const right = Math.round(window.innerWidth - r.right)
// Skip unchanged writes: a sash drag fires the RO every frame, and each
// :root custom-property set dirties style for everything that reads them.
if (left !== lastLeft) {
lastLeft = left
root.style.setProperty('--workspace-left', `${left}px`)
}
if (right !== lastRight) {
lastRight = right
root.style.setProperty('--workspace-right', `${right}px`)
}
}
// Tree mutations move zones without resizing them (⌘\ flip) — re-measure a
// frame later, after the DOM committed. RO covers width changes (sash drags,
// side collapses); window resize covers the rest.
const unsubTree = $layoutTree.listen(() => requestAnimationFrame(measure))
// Drag released → publish the final geometry the deferral above skipped.
onSashDragEnd = () => requestAnimationFrame(measure)
window.addEventListener('resize', measure)
measure()
return () => {
unsubTree()
onSashDragEnd = null
window.removeEventListener('resize', measure)
ro.disconnect()
root.style.removeProperty('--workspace-left')
root.style.removeProperty('--workspace-right')
}
}
/**
* Intersects an element's live viewport rect with the native window-controls
* rect. Returns the overlap in ELEMENT-LOCAL coordinates (null when clear), so
* the consumer can reserve the space and paint a drag strip without knowing
* anything about platform, fullscreen state, or layout position.
*/
export function useWindowControlsOverlap(ref: RefObject<HTMLElement | null>, enabled = true): Rect | null {
const controls = useWindowControlsRect()
const [overlap, setOverlap] = useState<Rect | null>(null)
useLayoutEffect(() => {
const el = ref.current
if (!enabled || !controls || !el) {
setOverlap(null)
return
}
const update = () => {
const r = el.getBoundingClientRect()
const hit = intersect(controls, { x: r.x, y: r.y, width: r.width, height: r.height })
const local = hit ? { x: hit.x - r.x, y: hit.y - r.y, width: hit.width, height: hit.height } : null
setOverlap(prev => (sameRect(prev, local) ? prev : local))
}
update()
// Size changes fire the observer; cross-window moves fire `resize`. A pane
// shifted only by a sibling's resize re-measures on its own grid reflow
// (its track width changes), so this covers the shell's real cases.
const ro = new ResizeObserver(update)
ro.observe(el)
window.addEventListener('resize', update)
return () => {
ro.disconnect()
window.removeEventListener('resize', update)
}
}, [controls, enabled, ref])
return overlap
}