mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(ui): contain idle caret paint so agent panes stop burning CPU (#10554)
An idle agent pane kept ~40% of a core busy just by being frontmost. The xterm cursor and the native chat caret blink with no paint-containment boundary, so Chromium treated each blink as damage to the whole pane ancestry and re-rasterized it twice a second. - `.xterm-container` and the native composer's input shell get `contain: paint`, bounding blink damage to the surface that blinks. - The mention hint gains `z-20` to match the slash picker: a contained element becomes a stacking context and paints at z-index 0 in tree order, which would otherwise cover the hint's drop shadow. Also records that DECSCUSR pins `decPrivateModes.cursorBlink`, which wins over the option in `_updateCursorBlink` — so parking `cursorBlink` does not reliably stop a hidden pane blinking. Pre-existing, documented only. Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com>
This commit is contained in:
co-authored by
Wooseong Kim
parent
389d672dab
commit
46eb5959fa
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:')}{' '}
|
||||
<span className="font-medium text-foreground">@{query || '…'}</span>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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('<div className="shrink-0 bg-background">')
|
||||
expect(outerShell).not.toContain('contain:paint')
|
||||
})
|
||||
|
||||
it('lifts both pickers above the contained shell', () => {
|
||||
// The shell is a stacking context now, so it paints at z-index 0 in tree
|
||||
// order — an unlayered picker would lose its drop shadow to it.
|
||||
for (const picker of ['bottom-full left-0 right-0 z-20', 'bottom-full left-3 right-3 z-20']) {
|
||||
expect(autocompleteMenus).toContain(picker)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -12,10 +12,16 @@ import type { Terminal } from '@xterm/xterm'
|
||||
* and the pane blinks — redrawing its whole cursor row through
|
||||
* `WebglRenderer._updateModel` — until the 5-minute idle timeout.
|
||||
*
|
||||
* `cursorBlink` is the public option that tears the timer down deterministically
|
||||
* `cursorBlink` is the public option that tears the timer down
|
||||
* (`RenderService.handleOptionsChanged` -> `WebglRenderer._updateCursorBlink`), so
|
||||
* "a hidden pane does not blink" stops depending on which CSS hid it.
|
||||
*
|
||||
* Not unconditional, though: `_updateCursorBlink` resolves
|
||||
* `decPrivateModes.cursorBlink ?? options.cursorBlink`, and DECSCUSR with a
|
||||
* blinking style (`CSI 5 SP q`) pins that DEC mode. On a pane whose shell or agent
|
||||
* has emitted one, parking the option here has no effect and the hidden pane keeps
|
||||
* blinking. Making this deterministic means clearing the DEC mode too.
|
||||
*
|
||||
* Resume restores the parked value rather than the settings value, so a pane that
|
||||
* was not blinking before the hide never comes back blinking.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user