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')
})