fix(renderer): align native chat pane layering

This commit is contained in:
Merge Sim
2026-09-08 13:19:04 -07:00
parent d346d6f447
commit 11df9f0f19
8 changed files with 345 additions and 251 deletions
-5
View File
@@ -400,11 +400,6 @@
z-index: 40 !important;
}
/* Above the z-40 updater/onboarding chrome, below the floating workspace panel's z-45. */
.native-chat-pane-shell:has([data-native-chat-working='true']) {
z-index: 44;
}
[data-sonner-toaster] [data-sonner-toast][data-styled='true'] {
align-items: flex-start;
flex-wrap: wrap;
@@ -148,7 +148,7 @@ describe('StructuredAgentSessionPaneOverlayLayer', () => {
expect(mocks.focusGroup).toHaveBeenCalledWith(WORKTREE_ID, GROUP_ID)
})
it('keeps the base z-layer overridable by the working-chat stylesheet rule', () => {
it('keeps a working session at the base pane layer', () => {
const view = render(
<StructuredAgentSessionPaneOverlayLayer worktreeId={WORKTREE_ID} isWorktreeActive />
)
@@ -157,7 +157,9 @@ describe('StructuredAgentSessionPaneOverlayLayer', () => {
)
expect(slot).not.toBeNull()
expect(slot?.classList.contains('native-chat-pane-shell')).toBe(true)
expect(slot?.hasAttribute('data-retained-pane-host')).toBe(true)
expect(slot?.classList.contains('isolate')).toBe(true)
expect(slot?.classList.contains('overflow-hidden')).toBe(true)
expect(slot?.classList.contains('z-10')).toBe(true)
expect(slot?.style.zIndex).toBe('')
expect(slot?.querySelector('[data-native-chat-working="true"]')).not.toBeNull()
@@ -5,7 +5,7 @@ import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-p
import { useAppStore } from '@/store'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { getActiveRuntimeTarget, type RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
import { tabGroupBodyAnchorName } from '../tab-group/tab-group-body-anchor'
import { RetainedPaneHost } from '../tab-group/RetainedPaneHost'
import NativeChatView from './NativeChatView'
type StructuredAgentSessionTab = Tab & {
@@ -29,37 +29,12 @@ const StructuredAgentSessionOverlaySlot = memo(function StructuredAgentSessionOv
target: RuntimeClientTarget
onFocusOwningGroup: ((groupId: string) => void) | undefined
}): React.JSX.Element {
const anchorName = groupId !== undefined ? tabGroupBodyAnchorName(groupId) : undefined
const style = useMemo<React.CSSProperties>(
() =>
anchorName
? {
position: 'absolute',
positionAnchor: anchorName,
top: `anchor(${anchorName} top)`,
left: `anchor(${anchorName} left)`,
width: `anchor-size(${anchorName} width)`,
height: `anchor-size(${anchorName} height)`,
display: isActive ? 'flex' : 'none',
pointerEvents: isActive ? 'auto' : 'none'
}
: { display: 'none' },
[anchorName, isActive]
)
const focusOwningGroup = useCallback(() => {
if (groupId !== undefined && onFocusOwningGroup) {
onFocusOwningGroup(groupId)
}
}, [groupId, onFocusOwningGroup])
return (
<div
style={style}
className="native-chat-pane-shell z-10 min-h-0 min-w-0"
<RetainedPaneHost
groupId={groupId}
isVisible={isActive}
data-structured-agent-session-overlay-tab-id={tab.id}
aria-hidden={!isActive}
onPointerDown={focusOwningGroup}
onFocusCapture={focusOwningGroup}
onFocusOwningGroup={onFocusOwningGroup}
>
<NativeChatView
mode="structured"
@@ -70,7 +45,7 @@ const StructuredAgentSessionOverlaySlot = memo(function StructuredAgentSessionOv
isVisible={isActive}
target={target}
/>
</div>
</RetainedPaneHost>
)
})
@@ -6,42 +6,17 @@ function source(path: string): string {
return readFileSync(join(process.cwd(), path), 'utf8')
}
function workingChatZIndex(css: string): number {
const match =
/\.native-chat-pane-shell:has\(\[data-native-chat-working='true'\]\)[^{]*\{[^}]*z-index:\s*(\d+);/s.exec(
css
)
expect(match, 'working native-chat z-index rule not found in main.css').not.toBeNull()
return Number(match?.[1])
}
describe('native chat Stop layering', () => {
it('keeps a working chat pane above bottom-right product chrome', () => {
describe('native chat layering', () => {
it('keeps working chat at the pane layer below app notifications and floating surfaces', () => {
const css = source('src/renderer/src/assets/main.css')
const terminalPane = source(
'src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.tsx'
)
expect(terminalPane).toContain('native-chat-pane-shell absolute inset-0 z-10')
for (const path of [
'src/renderer/src/components/terminal-pane/TerminalOverlaySlot.tsx',
'src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx'
]) {
expect(source(path)).toContain('<RetainedPaneHost')
}
expect(css).not.toMatch(/\.native-chat-pane-shell:has\(\[data-native-chat-working/)
expect(css).toMatch(/\[data-sonner-toaster\][^{]*\{[^}]*z-index:\s*40\s*!important;/s)
expect(workingChatZIndex(css)).toBeGreaterThan(40)
})
// Why both bounds: raising the working pane over the panel hides a summoned
// floating workspace behind the chat column while an agent streams.
it('stays under the floating workspace panel while working', () => {
// Comments stripped first: the surrounding layering comment cites bare z-40/z-50
// tiers, and a reworded one could otherwise be read as the panel's own class.
const panel = source(
'src/renderer/src/components/floating-terminal/FloatingTerminalPanelSurface.tsx'
).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '')
const panelZIndex = Number(
/data-floating-terminal-panel[\s\S]*?className=[\s\S]*?z-\[(\d+)\]/.exec(panel)?.[1]
)
// FloatingTerminalPanel.bounds.test.tsx pins this same 45 through a real render.
expect(panelZIndex).toBe(45)
expect(workingChatZIndex(source('src/renderer/src/assets/main.css'))).toBeLessThan(panelZIndex)
})
it('publishes working state from both structured and bridge chat roots', () => {
@@ -0,0 +1,107 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { RetainedPaneHost } from './RetainedPaneHost'
const disconnect = vi.fn()
let notifyResize: () => void
let anchors: HTMLDivElement[]
beforeEach(() => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
vi.stubGlobal(
'ResizeObserver',
class {
constructor(callback: () => void) {
notifyResize = callback
}
observe(): void {}
disconnect = disconnect
}
)
disconnect.mockClear()
anchors = ['left', 'right'].map((id, index) => {
const anchor = document.createElement('div')
anchor.dataset.tabGroupBodyId = id
anchor.getBoundingClientRect = () => new DOMRect(index * 400, 32, 400, 568)
document.body.append(anchor)
return anchor
})
})
afterEach(() => {
cleanup()
anchors.forEach((anchor) => anchor.remove())
vi.unstubAllGlobals()
})
it('retains pane content across group moves and visibility changes using measured browser bounds', () => {
const focus = vi.fn()
const content = <input defaultValue="draft" />
const view = render(
<RetainedPaneHost groupId="left" isVisible onFocusOwningGroup={focus}>
{content}
</RetainedPaneHost>
)
const host = view.container.firstElementChild as HTMLDivElement
const input = view.getByRole('textbox')
expect(host.style.top).toBe('32px')
expect(host.style.width).toBe('400px')
fireEvent.change(input, { target: { value: 'unsent draft' } })
view.rerender(
<RetainedPaneHost groupId="right" isVisible onFocusOwningGroup={focus}>
{content}
</RetainedPaneHost>
)
expect(host.style.left).toBe('400px')
expect(view.getByRole('textbox')).toBe(input)
expect((input as HTMLInputElement).value).toBe('unsent draft')
fireEvent.pointerDown(input)
expect(focus).toHaveBeenLastCalledWith('right')
anchors[1].getBoundingClientRect = () => new DOMRect(450, 32, 350, 500)
act(() => notifyResize())
expect(host.style.left).toBe('450px')
expect(host.style.width).toBe('350px')
view.rerender(
<RetainedPaneHost groupId="right" isVisible={false}>
{content}
</RetainedPaneHost>
)
expect(host.style.display).toBe('none')
expect(host.hasAttribute('inert')).toBe(true)
expect(host.contains(input)).toBe(true)
view.rerender(
<RetainedPaneHost groupId="right" isVisible>
{content}
</RetainedPaneHost>
)
expect(host.style.display).toBe('flex')
expect(host.hasAttribute('inert')).toBe(false)
expect(view.getByRole('textbox')).toBe(input)
view.unmount()
expect(disconnect).toHaveBeenCalled()
})
it('allows hidden terminal startup measurement without exposing input or starting fit timers for chat', () => {
const timeout = vi.spyOn(window, 'setTimeout')
const view = render(
<RetainedPaneHost groupId="left" isVisible={false} measureWhileHidden>
<input />
</RetainedPaneHost>
)
const host = view.container.firstElementChild as HTMLDivElement
expect(host.style.display).toBe('flex')
expect(host.style.opacity).toBe('0')
expect(host.style.pointerEvents).toBe('none')
expect(host.hasAttribute('inert')).toBe(true)
view.rerender(
<RetainedPaneHost groupId="left" isVisible>
<input />
</RetainedPaneHost>
)
expect(timeout).not.toHaveBeenCalled()
timeout.mockRestore()
})
@@ -0,0 +1,208 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { SYNC_FIT_PANES_EVENT } from '@/constants/terminal'
import { tabGroupBodyAnchorName } from './tab-group-body-anchor'
const HAS_CSS_ANCHOR_POSITIONING =
typeof CSS !== 'undefined' &&
CSS.supports('position-anchor', '--orca-terminal-overlay-probe') &&
CSS.supports('top', 'anchor(--orca-terminal-overlay-probe top)') &&
CSS.supports('width', 'anchor-size(--orca-terminal-overlay-probe width)')
const MIN_OVERLAY_FIT_WIDTH_PX = 48
const MIN_OVERLAY_FIT_HEIGHT_PX = 24
const FALLBACK_RECT_MIN_CHANGE_PX = 1
function shouldUseCssAnchorPositioning(): boolean {
return (
HAS_CSS_ANCHOR_POSITIONING &&
(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ !== true
)
}
type MeasuredFallbackRect = {
top: number
left: number
width: number
height: number
}
type RetainedPaneHostProps = {
groupId: string | undefined
isVisible: boolean
measureWhileHidden?: boolean
fitTerminal?: boolean
onFocusOwningGroup?: (groupId: string) => void
children: React.ReactNode
'data-terminal-overlay-tab-id'?: string
'data-structured-agent-session-overlay-tab-id'?: string
}
export function RetainedPaneHost({
groupId,
isVisible,
measureWhileHidden = false,
fitTerminal = false,
onFocusOwningGroup,
children,
...identity
}: RetainedPaneHostProps): React.JSX.Element {
const anchorName = groupId !== undefined ? tabGroupBodyAnchorName(groupId) : undefined
const overlayRef = useRef<HTMLDivElement | null>(null)
const [measuredFallbackRect, setMeasuredFallbackRect] = useState<MeasuredFallbackRect | null>(
null
)
useLayoutEffect(() => {
if (!anchorName || shouldUseCssAnchorPositioning() || !groupId) {
return
}
const findBody = (): HTMLElement | null => {
for (const candidate of document.querySelectorAll<HTMLElement>('[data-tab-group-body-id]')) {
if (candidate.dataset.tabGroupBodyId === groupId) {
return candidate
}
}
return null
}
const updateRect = (): void => {
const overlay = overlayRef.current
const parent = overlay?.parentElement
const body = findBody()
if (!parent || !body) {
setMeasuredFallbackRect(null)
return
}
const parentRect = parent.getBoundingClientRect()
const bodyRect = body.getBoundingClientRect()
const next: MeasuredFallbackRect = {
top: bodyRect.top - parentRect.top,
left: bodyRect.left - parentRect.left,
width: bodyRect.width,
height: bodyRect.height
}
// Why: ResizeObserver and xterm fit can otherwise amplify sub-pixel jitter forever.
setMeasuredFallbackRect((prev) =>
prev &&
Math.abs(prev.top - next.top) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.left - next.left) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.width - next.width) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.height - next.height) < FALLBACK_RECT_MIN_CHANGE_PX
? prev
: next
)
}
updateRect()
const body = findBody()
const parent = overlayRef.current?.parentElement
const resizeObserver = new ResizeObserver(updateRect)
if (body) {
resizeObserver.observe(body)
}
if (parent) {
resizeObserver.observe(parent)
}
window.addEventListener('resize', updateRect)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', updateRect)
}
}, [anchorName, groupId, isVisible])
useLayoutEffect(() => {
if (!fitTerminal || !isVisible || !anchorName) {
return
}
const dispatchFitIfMeasurable = (): void => {
const rect = overlayRef.current?.getBoundingClientRect()
if (
!rect ||
rect.width < MIN_OVERLAY_FIT_WIDTH_PX ||
rect.height < MIN_OVERLAY_FIT_HEIGHT_PX
) {
return
}
window.dispatchEvent(new Event(SYNC_FIT_PANES_EVENT))
}
// Why: tab switches can resume visibility before anchor/fallback geometry
// settles. Re-fit only after the overlay has real dimensions so the PTY
// never stays pinned at a stale ~2-col width.
const frameId = requestAnimationFrame(() => {
dispatchFitIfMeasurable()
})
const retryId = window.setTimeout(() => {
dispatchFitIfMeasurable()
}, 50)
const settledRetryId = window.setTimeout(() => {
dispatchFitIfMeasurable()
}, 150)
return () => {
cancelAnimationFrame(frameId)
window.clearTimeout(retryId)
window.clearTimeout(settledRetryId)
}
}, [anchorName, fitTerminal, isVisible, measuredFallbackRect])
const style: React.CSSProperties = useMemo(
() =>
anchorName && shouldUseCssAnchorPositioning()
? {
position: 'absolute',
positionAnchor: anchorName,
top: `anchor(${anchorName} top)`,
left: `anchor(${anchorName} left)`,
width: `anchor-size(${anchorName} width)`,
height: `anchor-size(${anchorName} height)`,
display: isVisible || measureWhileHidden ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: anchorName
? {
// Why: Chrome builds without CSS anchor positioning otherwise
// mount the terminal into a 0x0 overlay. Measure the tab-group
// body so the fallback does not cover the tab strip.
position: 'absolute',
top: measuredFallbackRect?.top ?? 32,
left: measuredFallbackRect?.left ?? 0,
width: measuredFallbackRect?.width ?? '100%',
height: measuredFallbackRect?.height ?? 'calc(100% - 32px)',
display: isVisible || measureWhileHidden ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: {
position: 'absolute',
top: 0,
left: 0,
width: 0,
height: 0,
display: 'none',
pointerEvents: 'none'
},
[anchorName, isVisible, measuredFallbackRect, measureWhileHidden]
)
const focusGroup = useCallback(() => {
if (groupId !== undefined && onFocusOwningGroup) {
onFocusOwningGroup(groupId)
}
}, [groupId, onFocusOwningGroup])
return (
<div
ref={overlayRef}
style={style}
// Pane-local layers cannot compete with app notifications or escape their split rectangle.
className="isolate z-10 min-h-0 min-w-0 overflow-hidden"
data-retained-pane-host=""
{...identity}
inert={!isVisible}
aria-hidden={!isVisible}
onPointerDown={focusGroup}
onFocusCapture={focusGroup}
>
{children}
</div>
)
}
@@ -1,37 +1,13 @@
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { memo, useLayoutEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useAppStore } from '../../store'
import { isProvenProcessExit } from '../../../../shared/terminal-exit-cause'
import { SYNC_FIT_PANES_EVENT } from '@/constants/terminal'
import { tabGroupBodyAnchorName } from '../tab-group/tab-group-body-anchor'
import { RetainedPaneHost } from '../tab-group/RetainedPaneHost'
import type { ActivityTerminalPortalTarget } from '../activity/activity-terminal-portal'
import TerminalPane from './TerminalPane'
import { closeTerminalTab } from '../terminal/terminal-tab-actions'
import { shouldDeferParkedPtyExitTabClose } from './terminal-parked-tab-watchers'
const HAS_CSS_ANCHOR_POSITIONING =
typeof CSS !== 'undefined' &&
CSS.supports('position-anchor', '--orca-terminal-overlay-probe') &&
CSS.supports('top', 'anchor(--orca-terminal-overlay-probe top)') &&
CSS.supports('width', 'anchor-size(--orca-terminal-overlay-probe width)')
const MIN_OVERLAY_FIT_WIDTH_PX = 48
const MIN_OVERLAY_FIT_HEIGHT_PX = 24
const FALLBACK_RECT_MIN_CHANGE_PX = 1
function shouldUseCssAnchorPositioning(): boolean {
return (
HAS_CSS_ANCHOR_POSITIONING &&
(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ !== true
)
}
type MeasuredFallbackRect = {
top: number
left: number
width: number
height: number
}
type TerminalOverlaySlotProps = {
terminalTabId: string
terminalGeneration: number | undefined
@@ -63,11 +39,6 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
consumeSuppressedPtyExit,
leaveWorktreeIfEmpty
}: TerminalOverlaySlotProps): React.JSX.Element {
const anchorName = groupId !== undefined ? tabGroupBodyAnchorName(groupId) : undefined
const overlayRef = useRef<HTMLDivElement | null>(null)
const [measuredFallbackRect, setMeasuredFallbackRect] = useState<MeasuredFallbackRect | null>(
null
)
const [shouldMeasureHiddenStartup, setShouldMeasureHiddenStartup] = useState(
() => useAppStore.getState().pendingStartupByTabId[terminalTabId] !== undefined
)
@@ -76,144 +47,6 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
setShouldMeasureHiddenStartup(false)
}
}, [isVisible, shouldMeasureHiddenStartup])
useLayoutEffect(() => {
if (!anchorName || shouldUseCssAnchorPositioning() || !groupId) {
return
}
const findBody = (): HTMLElement | null => {
for (const candidate of document.querySelectorAll<HTMLElement>('[data-tab-group-body-id]')) {
if (candidate.dataset.tabGroupBodyId === groupId) {
return candidate
}
}
return null
}
const updateRect = (): void => {
const overlay = overlayRef.current
const parent = overlay?.parentElement
const body = findBody()
if (!parent || !body) {
setMeasuredFallbackRect(null)
return
}
const parentRect = parent.getBoundingClientRect()
const bodyRect = body.getBoundingClientRect()
const next: MeasuredFallbackRect = {
top: bodyRect.top - parentRect.top,
left: bodyRect.left - parentRect.left,
width: bodyRect.width,
height: bodyRect.height
}
// Why: ResizeObserver and xterm fit can otherwise amplify sub-pixel jitter forever.
setMeasuredFallbackRect((prev) =>
prev &&
Math.abs(prev.top - next.top) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.left - next.left) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.width - next.width) < FALLBACK_RECT_MIN_CHANGE_PX &&
Math.abs(prev.height - next.height) < FALLBACK_RECT_MIN_CHANGE_PX
? prev
: next
)
}
updateRect()
const body = findBody()
const parent = overlayRef.current?.parentElement
const resizeObserver = new ResizeObserver(updateRect)
if (body) {
resizeObserver.observe(body)
}
if (parent) {
resizeObserver.observe(parent)
}
window.addEventListener('resize', updateRect)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', updateRect)
}
}, [anchorName, groupId, isVisible])
useLayoutEffect(() => {
if (!isVisible || !anchorName) {
return
}
const dispatchFitIfMeasurable = (): void => {
const rect = overlayRef.current?.getBoundingClientRect()
if (
!rect ||
rect.width < MIN_OVERLAY_FIT_WIDTH_PX ||
rect.height < MIN_OVERLAY_FIT_HEIGHT_PX
) {
return
}
window.dispatchEvent(new Event(SYNC_FIT_PANES_EVENT))
}
// Why: tab switches can resume visibility before anchor/fallback geometry
// settles. Re-fit only after the overlay has real dimensions so the PTY
// never stays pinned at a stale ~2-col width.
const frameId = requestAnimationFrame(() => {
dispatchFitIfMeasurable()
})
const retryId = window.setTimeout(() => {
dispatchFitIfMeasurable()
}, 50)
const settledRetryId = window.setTimeout(() => {
dispatchFitIfMeasurable()
}, 150)
return () => {
cancelAnimationFrame(frameId)
window.clearTimeout(retryId)
window.clearTimeout(settledRetryId)
}
}, [anchorName, isVisible, measuredFallbackRect])
const style: React.CSSProperties = useMemo(
() =>
anchorName && shouldUseCssAnchorPositioning()
? {
position: 'absolute',
positionAnchor: anchorName,
top: `anchor(${anchorName} top)`,
left: `anchor(${anchorName} left)`,
width: `anchor-size(${anchorName} width)`,
height: `anchor-size(${anchorName} height)`,
display: isVisible || shouldMeasureHiddenStartup ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: anchorName
? {
// Why: Chrome builds without CSS anchor positioning otherwise
// mount the terminal into a 0x0 overlay. Measure the tab-group
// body so the fallback does not cover the tab strip.
position: 'absolute',
top: measuredFallbackRect?.top ?? 32,
left: measuredFallbackRect?.left ?? 0,
width: measuredFallbackRect?.width ?? '100%',
height: measuredFallbackRect?.height ?? 'calc(100% - 32px)',
display: isVisible || shouldMeasureHiddenStartup ? 'flex' : 'none',
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none'
}
: {
position: 'absolute',
top: 0,
left: 0,
width: 0,
height: 0,
display: 'none',
pointerEvents: 'none'
},
[anchorName, isVisible, measuredFallbackRect, shouldMeasureHiddenStartup]
)
const focusGroup = useCallback(() => {
if (groupId !== undefined && onFocusOwningGroup) {
onFocusOwningGroup(groupId)
}
}, [groupId, onFocusOwningGroup])
const terminalPane = (
<TerminalPane
@@ -267,17 +100,15 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
}
return (
<div
ref={overlayRef}
style={style}
<RetainedPaneHost
groupId={groupId}
isVisible={isVisible}
measureWhileHidden={shouldMeasureHiddenStartup}
fitTerminal
data-terminal-overlay-tab-id={terminalTabId}
onPointerDown={focusGroup}
onFocusCapture={focusGroup}
onFocusOwningGroup={onFocusOwningGroup}
>
{terminalPane}
{/* The chat/terminal toggle now lives in the pane header's action cluster
(TerminalPaneHeaderOverlay), beside split/close — not as a separate
floating overlay. */}
</div>
</RetainedPaneHost>
)
})
@@ -174,7 +174,8 @@ describe('TerminalPaneOverlayLayer fallback measure<->fit loop (React #185)', ()
capturedResizeCallback?.()
})
expect(terminalPaneRenderCount - rendersAfterMount).toBe(1)
// Geometry updates belong to the host and do not rerender terminal content.
expect(terminalPaneRenderCount - rendersAfterMount).toBe(0)
expect(overlay?.style.top).toBe('34px')
expect(overlay?.style.width).toBe('760px')
})