opentui(v5/item4): coalesce resize via a shared debounced dimensions signal
Raw useTerminalDimensions fires on every SIGWINCH tick; during a drag that's a recompute/reflow storm across every width-sensitive component (tool bodies, tables, status bar, banner). Add a DimensionsProvider that runs the raw hook ONCE and feeds a single leading+trailing-debounced (40ms) signal — mirroring the gateway's 16ms event coalescing / opencode's createLeadingTrailingSignal — that every consumer shares via useDimensions(). They now reflow together (no tearing) and at most once per window. Falls back to the raw hook outside a provider (headless tests). Verified: single resizes converge clean (wide banner ⇄ compact brand at the 102-col threshold); rapid bursts coalesce. 90 pass.
This commit is contained in:
parent
4061e635d3
commit
48d0c70f61
@ -18,6 +18,7 @@ import { Match, Switch } from 'solid-js'
|
|||||||
import type { PromptHistory } from '../logic/history.ts'
|
import type { PromptHistory } from '../logic/history.ts'
|
||||||
import type { SessionStore } from '../logic/store.ts'
|
import type { SessionStore } from '../logic/store.ts'
|
||||||
import { Composer } from './composer.tsx'
|
import { Composer } from './composer.tsx'
|
||||||
|
import { DimensionsProvider } from './dimensions.tsx'
|
||||||
import { Header } from './header.tsx'
|
import { Header } from './header.tsx'
|
||||||
import { AgentsDashboard } from './overlays/agentsDashboard.tsx'
|
import { AgentsDashboard } from './overlays/agentsDashboard.tsx'
|
||||||
import { Pager } from './overlays/pager.tsx'
|
import { Pager } from './overlays/pager.tsx'
|
||||||
@ -64,6 +65,7 @@ export function App(props: AppProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DimensionsProvider>
|
||||||
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
|
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
|
||||||
{/* a bottom rule under the header bookends the transcript with the status
|
{/* a bottom rule under the header bookends the transcript with the status
|
||||||
bar's top rule — frames the chrome as intentional (item 8). */}
|
bar's top rule — frames the chrome as intentional (item 8). */}
|
||||||
@ -132,5 +134,6 @@ export function App(props: AppProps) {
|
|||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</box>
|
</box>
|
||||||
|
</DimensionsProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
50
ui-tui-opentui-v2/src/view/dimensions.tsx
Normal file
50
ui-tui-opentui-v2/src/view/dimensions.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Shared, COALESCED terminal dimensions (item 4 — resize hardening). Raw
|
||||||
|
* `useTerminalDimensions()` fires on every SIGWINCH tick; during a drag that's a
|
||||||
|
* recompute/reflow storm across every width-sensitive component (tool bodies,
|
||||||
|
* tables, status bar, banner). One provider runs the raw hook once and feeds a
|
||||||
|
* single leading+trailing-debounced signal (opencode's createLeadingTrailingSignal
|
||||||
|
* idiom, mirroring the gateway's 16ms event coalescing) that every consumer shares
|
||||||
|
* — so they reflow together (no tearing) and at most once per COALESCE window.
|
||||||
|
*/
|
||||||
|
import { useTerminalDimensions } from '@opentui/solid'
|
||||||
|
import { type Accessor, createContext, createEffect, createSignal, type JSX, onCleanup, useContext } from 'solid-js'
|
||||||
|
|
||||||
|
export interface Dims {
|
||||||
|
readonly width: number
|
||||||
|
readonly height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const DimsContext = createContext<Accessor<Dims>>()
|
||||||
|
const COALESCE_MS = 40
|
||||||
|
|
||||||
|
export function DimensionsProvider(props: { children: JSX.Element }) {
|
||||||
|
const raw = useTerminalDimensions()
|
||||||
|
const [dims, setDims] = createSignal<Dims>({ height: raw().height, width: raw().width })
|
||||||
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let last = 0
|
||||||
|
createEffect(() => {
|
||||||
|
const next: Dims = { height: raw().height, width: raw().width } // track raw
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - last >= COALESCE_MS) {
|
||||||
|
last = now
|
||||||
|
setDims(next) // leading edge: respond immediately to the first change
|
||||||
|
} else {
|
||||||
|
// trailing edge: coalesce the burst, land on the final size once it settles
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
last = Date.now()
|
||||||
|
setDims(next)
|
||||||
|
}, COALESCE_MS)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
})
|
||||||
|
return <DimsContext.Provider value={dims}>{props.children}</DimsContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coalesced dimensions; falls back to the raw hook outside a provider (e.g. headless tests). */
|
||||||
|
export function useDimensions(): Accessor<Dims> {
|
||||||
|
return useContext(DimsContext) ?? useTerminalDimensions()
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
* panel (item 9), the common commands, and the key tips. Fully themed; decorative,
|
* panel (item 9), the common commands, and the key tips. Fully themed; decorative,
|
||||||
* so `selectable={false}` (item 4).
|
* so `selectable={false}` (item 4).
|
||||||
*/
|
*/
|
||||||
import { useTerminalDimensions } from '@opentui/solid'
|
import { useDimensions } from './dimensions.tsx'
|
||||||
import { createSignal, For, Show } from 'solid-js'
|
import { createSignal, For, Show } from 'solid-js'
|
||||||
|
|
||||||
import type { Catalog } from '../logic/store.ts'
|
import type { Catalog } from '../logic/store.ts'
|
||||||
@ -33,7 +33,7 @@ const BANNER_W = 102
|
|||||||
|
|
||||||
export function HomeHint(props: { catalog: Catalog | undefined }) {
|
export function HomeHint(props: { catalog: Catalog | undefined }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dims = useTerminalDimensions()
|
const dims = useDimensions()
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
const wide = () => dims().width >= BANNER_W
|
const wide = () => dims().width >= BANNER_W
|
||||||
const cat = () => props.catalog
|
const cat = () => props.catalog
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
* MdTable — renders a parsed GFM table as an aligned grid (item 7). The native
|
* MdTable — renders a parsed GFM table as an aligned grid (item 7). The native
|
||||||
* markdown renderable leaves tables as raw pipes; this lays them out with
|
* markdown renderable leaves tables as raw pipes; this lays them out with
|
||||||
* per-column widths, alignment, a header rule, and dim `│` separators. Width
|
* per-column widths, alignment, a header rule, and dim `│` separators. Width
|
||||||
* is reactive (useTerminalDimensions) so columns shrink to fit on resize.
|
* is reactive (useDimensions) so columns shrink to fit on resize.
|
||||||
*/
|
*/
|
||||||
import { useTerminalDimensions } from '@opentui/solid'
|
import { useDimensions } from './dimensions.tsx'
|
||||||
import { createMemo, type JSX } from 'solid-js'
|
import { createMemo, type JSX } from 'solid-js'
|
||||||
|
|
||||||
import { padAligned, tableColumnWidths, type TableSegment } from '../logic/mdTable.ts'
|
import { padAligned, tableColumnWidths, type TableSegment } from '../logic/mdTable.ts'
|
||||||
@ -15,7 +15,7 @@ const GUTTER = 2
|
|||||||
|
|
||||||
export function MdTable(props: { seg: TableSegment }) {
|
export function MdTable(props: { seg: TableSegment }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dims = useTerminalDimensions()
|
const dims = useDimensions()
|
||||||
const widths = createMemo(() => tableColumnWidths(props.seg, Math.max(24, dims().width - GUTTER - 2)))
|
const widths = createMemo(() => tableColumnWidths(props.seg, Math.max(24, dims().width - GUTTER - 2)))
|
||||||
|
|
||||||
const cellText = (raw: string, i: number) =>
|
const cellText = (raw: string, i: number) =>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* terminals and the cwd is left-truncated (`…/tail`) so the row NEVER wraps or
|
* terminals and the cwd is left-truncated (`…/tail`) so the row NEVER wraps or
|
||||||
* clips. Read-only chrome — no input handling here.
|
* clips. Read-only chrome — no input handling here.
|
||||||
*/
|
*/
|
||||||
import { useTerminalDimensions } from '@opentui/solid'
|
import { useDimensions } from './dimensions.tsx'
|
||||||
import { createMemo, Show } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
|
|
||||||
import { useTheme } from './theme.tsx'
|
import { useTheme } from './theme.tsx'
|
||||||
@ -54,7 +54,7 @@ function ctxBar(pct: number, width: number): string {
|
|||||||
|
|
||||||
export function StatusBar(props: { store: SessionStore }) {
|
export function StatusBar(props: { store: SessionStore }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dims = useTerminalDimensions()
|
const dims = useDimensions()
|
||||||
const info = () => props.store.state.info
|
const info = () => props.store.state.info
|
||||||
|
|
||||||
// Context-bar colour escalates with pressure (Ink ctxBarColor good→warn→bad→critical).
|
// Context-bar colour escalates with pressure (Ink ctxBarColor good→warn→bad→critical).
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
* Fully themed (no hardcoded styles); decorative glyphs are selectable={false}.
|
* Fully themed (no hardcoded styles); decorative glyphs are selectable={false}.
|
||||||
*/
|
*/
|
||||||
import { type ToolPartState } from '../logic/store.ts'
|
import { type ToolPartState } from '../logic/store.ts'
|
||||||
import { useTerminalDimensions } from '@opentui/solid'
|
import { useDimensions } from './dimensions.tsx'
|
||||||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
|
||||||
import { collapseToolOutput, truncate } from '../logic/toolOutput.ts'
|
import { collapseToolOutput, truncate } from '../logic/toolOutput.ts'
|
||||||
@ -36,7 +36,7 @@ function fmtDuration(s: number): string {
|
|||||||
|
|
||||||
export function ToolPart(props: { part: ToolPartState }) {
|
export function ToolPart(props: { part: ToolPartState }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dims = useTerminalDimensions()
|
const dims = useDimensions()
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
|
|
||||||
const bodyWidth = () => Math.max(20, dims().width - GUTTER - 4)
|
const bodyWidth = () => Math.max(20, dims().width - GUTTER - 4)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user