diff --git a/src/renderer/src/assets/terminal-container-geometry.test.ts b/src/renderer/src/assets/terminal-container-geometry.test.ts index db079c9095f..cf17cb00540 100644 --- a/src/renderer/src/assets/terminal-container-geometry.test.ts +++ b/src/renderer/src/assets/terminal-container-geometry.test.ts @@ -15,4 +15,8 @@ describe('terminal container geometry', () => { /\.pane-link-tooltip\s*{[^}]*height:\s*var\(--orca-terminal-link-tooltip-height\);/s ) }) + + it('bounds cursor-blink repaints to the terminal surface (#10481)', () => { + expect(terminalCss).toMatch(/\.xterm-container\s*{[^}]*contain:\s*paint;/s) + }) }) diff --git a/src/renderer/src/assets/terminal.css b/src/renderer/src/assets/terminal.css index 1ee09586892..0d17b1f77df 100644 --- a/src/renderer/src/assets/terminal.css +++ b/src/renderer/src/assets/terminal.css @@ -512,6 +512,10 @@ height: calc(100% - var(--pane-padding-y, 4px)); margin-top: var(--pane-padding-y, 4px); margin-left: var(--pane-padding-x, 4px); + /* Why (#10481): a blinking cursor otherwise invalidates paint all the way up + the pane ancestry. The link tooltip and drag handle are .pane siblings, so + clipping to this box costs no visible chrome. */ + contain: paint; } /* When a pane has a title, shift the terminal content down to make room. diff --git a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx index 125d4326b8f..2e1d05e0ffe 100644 --- a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx +++ b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx @@ -276,7 +276,10 @@ export function NativeChatMentionHint({ event.preventDefault() onAccept() }} - className="absolute bottom-full left-3 right-3 mb-1 flex w-auto items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-left text-xs text-muted-foreground shadow-md sm:left-4 sm:right-4" + // Why z-20: matches the slash picker. The composer shell below is a paint + // containment boundary (#10481), so it now paints at z-index 0 in tree + // order and would otherwise cover this hint's drop shadow. + className="absolute bottom-full left-3 right-3 z-20 mb-1 flex w-auto items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-left text-xs text-muted-foreground shadow-md sm:left-4 sm:right-4" > {translate('components.native-chat.composer.mentionHint', 'Referencing file:')}{' '} @{query || '…'} diff --git a/src/renderer/src/components/native-chat/NativeChatComposerField.tsx b/src/renderer/src/components/native-chat/NativeChatComposerField.tsx index f51bf4c5400..a4b104efd0f 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposerField.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposerField.tsx @@ -192,7 +192,15 @@ export function NativeChatComposerField({ // no focus/click border flash. The box is a container, not a // focus target. 'rounded-lg border border-border p-1.5 shadow-xs', - 'bg-muted/50 dark:bg-input/40' + 'bg-muted/50 dark:bg-input/40', + // Why (#10481): the native caret blink invalidates paint up to the + // nearest containment boundary; without this the whole transcript + // re-rasterizes twice a second. Pickers are siblings and every menu + // and tooltip in here is a Radix portal, so nothing floating clips. + // Tightest descendant is the attachment remove button, which + // overhangs its thumbnail by 6px and clears this box's padding by + // 4px — keep that slack if the padding below ever shrinks. + '[contain:paint]' )} > {imageAttachments.length > 0 ? ( diff --git a/src/renderer/src/components/native-chat/native-chat-composer-containment.test.ts b/src/renderer/src/components/native-chat/native-chat-composer-containment.test.ts new file mode 100644 index 00000000000..2c8a08ffa89 --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-composer-containment.test.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs' +import { describe, expect, it } from 'vitest' + +const composerField = fs.readFileSync( + new URL('./NativeChatComposerField.tsx', import.meta.url), + 'utf8' +) +const autocompleteMenus = fs.readFileSync( + new URL('./NativeChatAutocompleteMenus.tsx', import.meta.url), + 'utf8' +) + +describe('native chat composer paint containment (#10481)', () => { + it('bounds caret repaints to the composer input shell', () => { + expect(composerField).toContain('[contain:paint]') + }) + + it('keeps the outer composer uncontained so the pickers can overflow it', () => { + // The pickers are siblings that render above the shell via `bottom-full`; + // containing their parent would clip them. + const outerShell = composerField.slice(0, composerField.indexOf('[contain:paint]')) + expect(outerShell).toContain('