diff --git a/ui-tui-opentui-v2/src/view/App.tsx b/ui-tui-opentui-v2/src/view/App.tsx
index 48cf8891b05..0780ab4318b 100644
--- a/ui-tui-opentui-v2/src/view/App.tsx
+++ b/ui-tui-opentui-v2/src/view/App.tsx
@@ -18,6 +18,7 @@ import { Match, Switch } from 'solid-js'
import type { PromptHistory } from '../logic/history.ts'
import type { SessionStore } from '../logic/store.ts'
import { Composer } from './composer.tsx'
+import { DimensionsProvider } from './dimensions.tsx'
import { Header } from './header.tsx'
import { AgentsDashboard } from './overlays/agentsDashboard.tsx'
import { Pager } from './overlays/pager.tsx'
@@ -64,6 +65,7 @@ export function App(props: AppProps) {
}
return (
+
{/* a bottom rule under the header bookends the transcript with the status
bar's top rule — frames the chrome as intentional (item 8). */}
@@ -132,5 +134,6 @@ export function App(props: AppProps) {
+
)
}
diff --git a/ui-tui-opentui-v2/src/view/dimensions.tsx b/ui-tui-opentui-v2/src/view/dimensions.tsx
new file mode 100644
index 00000000000..b8db9ec9d45
--- /dev/null
+++ b/ui-tui-opentui-v2/src/view/dimensions.tsx
@@ -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>()
+const COALESCE_MS = 40
+
+export function DimensionsProvider(props: { children: JSX.Element }) {
+ const raw = useTerminalDimensions()
+ const [dims, setDims] = createSignal({ height: raw().height, width: raw().width })
+ let timer: ReturnType | 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 {props.children}
+}
+
+/** Coalesced dimensions; falls back to the raw hook outside a provider (e.g. headless tests). */
+export function useDimensions(): Accessor {
+ return useContext(DimsContext) ?? useTerminalDimensions()
+}
diff --git a/ui-tui-opentui-v2/src/view/homeHint.tsx b/ui-tui-opentui-v2/src/view/homeHint.tsx
index dd99914dc81..755b4b1c5dd 100644
--- a/ui-tui-opentui-v2/src/view/homeHint.tsx
+++ b/ui-tui-opentui-v2/src/view/homeHint.tsx
@@ -5,7 +5,7 @@
* panel (item 9), the common commands, and the key tips. Fully themed; decorative,
* so `selectable={false}` (item 4).
*/
-import { useTerminalDimensions } from '@opentui/solid'
+import { useDimensions } from './dimensions.tsx'
import { createSignal, For, Show } from 'solid-js'
import type { Catalog } from '../logic/store.ts'
@@ -33,7 +33,7 @@ const BANNER_W = 102
export function HomeHint(props: { catalog: Catalog | undefined }) {
const theme = useTheme()
- const dims = useTerminalDimensions()
+ const dims = useDimensions()
const [open, setOpen] = createSignal(false)
const wide = () => dims().width >= BANNER_W
const cat = () => props.catalog
diff --git a/ui-tui-opentui-v2/src/view/mdTable.tsx b/ui-tui-opentui-v2/src/view/mdTable.tsx
index ceb8f9931dc..22cd11c51c0 100644
--- a/ui-tui-opentui-v2/src/view/mdTable.tsx
+++ b/ui-tui-opentui-v2/src/view/mdTable.tsx
@@ -2,9 +2,9 @@
* 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
* 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 { padAligned, tableColumnWidths, type TableSegment } from '../logic/mdTable.ts'
@@ -15,7 +15,7 @@ const GUTTER = 2
export function MdTable(props: { seg: TableSegment }) {
const theme = useTheme()
- const dims = useTerminalDimensions()
+ const dims = useDimensions()
const widths = createMemo(() => tableColumnWidths(props.seg, Math.max(24, dims().width - GUTTER - 2)))
const cellText = (raw: string, i: number) =>
diff --git a/ui-tui-opentui-v2/src/view/statusBar.tsx b/ui-tui-opentui-v2/src/view/statusBar.tsx
index 0892709def3..1cc13c6c255 100644
--- a/ui-tui-opentui-v2/src/view/statusBar.tsx
+++ b/ui-tui-opentui-v2/src/view/statusBar.tsx
@@ -10,7 +10,7 @@
* terminals and the cwd is left-truncated (`…/tail`) so the row NEVER wraps or
* 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 { useTheme } from './theme.tsx'
@@ -54,7 +54,7 @@ function ctxBar(pct: number, width: number): string {
export function StatusBar(props: { store: SessionStore }) {
const theme = useTheme()
- const dims = useTerminalDimensions()
+ const dims = useDimensions()
const info = () => props.store.state.info
// Context-bar colour escalates with pressure (Ink ctxBarColor good→warn→bad→critical).
diff --git a/ui-tui-opentui-v2/src/view/toolPart.tsx b/ui-tui-opentui-v2/src/view/toolPart.tsx
index 31d27c0b2e4..cc0df3422b8 100644
--- a/ui-tui-opentui-v2/src/view/toolPart.tsx
+++ b/ui-tui-opentui-v2/src/view/toolPart.tsx
@@ -14,7 +14,7 @@
* Fully themed (no hardcoded styles); decorative glyphs are selectable={false}.
*/
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 { collapseToolOutput, truncate } from '../logic/toolOutput.ts'
@@ -36,7 +36,7 @@ function fmtDuration(s: number): string {
export function ToolPart(props: { part: ToolPartState }) {
const theme = useTheme()
- const dims = useTerminalDimensions()
+ const dims = useDimensions()
const [expanded, setExpanded] = createSignal(false)
const bodyWidth = () => Math.max(20, dims().width - GUTTER - 4)