From 11df9f0f19da1aa8d358bf16ec7dadcc4c2eac4b Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Tue, 8 Sep 2026 13:19:04 -0700 Subject: [PATCH] fix(renderer): align native chat pane layering --- src/renderer/src/assets/main.css | 5 - ...turedAgentSessionPaneOverlayLayer.test.tsx | 6 +- ...StructuredAgentSessionPaneOverlayLayer.tsx | 37 +--- .../native-chat-stop-layering.test.ts | 43 +--- .../tab-group/RetainedPaneHost.test.tsx | 107 +++++++++ .../components/tab-group/RetainedPaneHost.tsx | 208 ++++++++++++++++++ .../terminal-pane/TerminalOverlaySlot.tsx | 187 +--------------- ...TerminalPaneOverlayLayer.react185.test.tsx | 3 +- 8 files changed, 345 insertions(+), 251 deletions(-) create mode 100644 src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx create mode 100644 src/renderer/src/components/tab-group/RetainedPaneHost.tsx diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index b4051a3b988..a65dfe59e9a 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -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; diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx index 05101f14fd1..5f6d057ada7 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx @@ -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( ) @@ -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() diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx index 01b8c256277..219a2633c3b 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx @@ -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( - () => - 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 ( -
-
+ ) }) diff --git a/src/renderer/src/components/native-chat/native-chat-stop-layering.test.ts b/src/renderer/src/components/native-chat/native-chat-stop-layering.test.ts index 46bee5d6172..26b4009caaa 100644 --- a/src/renderer/src/components/native-chat/native-chat-stop-layering.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-stop-layering.test.ts @@ -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(' { - // 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', () => { diff --git a/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx b/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx new file mode 100644 index 00000000000..964214e0d9f --- /dev/null +++ b/src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx @@ -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 = + const view = render( + + {content} + + ) + 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( + + {content} + + ) + 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( + + {content} + + ) + expect(host.style.display).toBe('none') + expect(host.hasAttribute('inert')).toBe(true) + expect(host.contains(input)).toBe(true) + view.rerender( + + {content} + + ) + 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( + + + + ) + 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( + + + + ) + expect(timeout).not.toHaveBeenCalled() + timeout.mockRestore() +}) diff --git a/src/renderer/src/components/tab-group/RetainedPaneHost.tsx b/src/renderer/src/components/tab-group/RetainedPaneHost.tsx new file mode 100644 index 00000000000..6007f2b1c20 --- /dev/null +++ b/src/renderer/src/components/tab-group/RetainedPaneHost.tsx @@ -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(null) + const [measuredFallbackRect, setMeasuredFallbackRect] = useState( + null + ) + useLayoutEffect(() => { + if (!anchorName || shouldUseCssAnchorPositioning() || !groupId) { + return + } + + const findBody = (): HTMLElement | null => { + for (const candidate of document.querySelectorAll('[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 ( +
+ {children} +
+ ) +} diff --git a/src/renderer/src/components/terminal-pane/TerminalOverlaySlot.tsx b/src/renderer/src/components/terminal-pane/TerminalOverlaySlot.tsx index 7c3807ced2f..379ef8095d9 100644 --- a/src/renderer/src/components/terminal-pane/TerminalOverlaySlot.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalOverlaySlot.tsx @@ -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(null) - const [measuredFallbackRect, setMeasuredFallbackRect] = useState( - 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('[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} - {/* The chat/terminal toggle now lives in the pane header's action cluster - (TerminalPaneHeaderOverlay), beside split/close — not as a separate - floating overlay. */} - + ) }) diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx index 0a7b3a99fb3..dbdca355779 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx @@ -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') })