mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Revert "fix(terminal): clear the render model on the plain-refocus repaint path"
This reverts commit 0f7ec4458d.
This commit is contained in:
@@ -2,7 +2,6 @@ import type { ManagedPaneInternal } from './pane-manager-types'
|
|||||||
import { reattachWebglIfNeeded } from './pane-webgl-reattach'
|
import { reattachWebglIfNeeded } from './pane-webgl-reattach'
|
||||||
import { resetWebglTextureAtlas } from './pane-webgl-renderer'
|
import { resetWebglTextureAtlas } from './pane-webgl-renderer'
|
||||||
import { releaseAbandonedSynchronizedOutput } from './terminal-synchronized-output-release'
|
import { releaseAbandonedSynchronizedOutput } from './terminal-synchronized-output-release'
|
||||||
import { clearTerminalRenderModel } from './terminal-render-model-clear'
|
|
||||||
|
|
||||||
function scheduleSettledFrame(callback: () => void): void {
|
function scheduleSettledFrame(callback: () => void): void {
|
||||||
if (typeof globalThis.requestAnimationFrame !== 'function') {
|
if (typeof globalThis.requestAnimationFrame !== 'function') {
|
||||||
@@ -50,21 +49,14 @@ export function schedulePaneRevealRepaint(getPanes: () => Iterable<ManagedPaneIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repaints already-visible panes without clearing the shared glyph atlas.
|
* Presents already-visible panes without clearing the shared glyph atlas.
|
||||||
*
|
*
|
||||||
* Why the model clear: a `refresh` alone is diff-based, so it skips every cell
|
* Why: a plain window refocus never hid its panes, so their WebGL model is
|
||||||
* whose cached model entry still matches the buffer. An occluded window (the
|
* already current — a `refresh` re-presents the live buffer (covering a
|
||||||
* "switch away to the desktop" case) can lose its canvas contents while that
|
* compositor that dropped frames while occluded). Using the atlas-clearing
|
||||||
* model stays populated, and the refresh then skips exactly the cells that went
|
* reveal repaint here would wipe the atlas shared by every same-config pane and
|
||||||
* stale — the pane keeps presenting pre-hide pixels until a resize rebuilds
|
* re-arm the mid-stream page-merge garble race (xterm.js issue 4480); this path
|
||||||
* everything. Clearing the model first makes the refresh a guaranteed full
|
* must stay texture-atlas-preserving.
|
||||||
* repaint.
|
|
||||||
*
|
|
||||||
* Why not the atlas-clearing reveal repaint: that wipes the atlas shared by
|
|
||||||
* every same-config pane and re-arms the mid-stream page-merge garble race
|
|
||||||
* (xterm.js issue 4480). Clearing only the model repaints this pane in full
|
|
||||||
* while leaving sibling panes' glyphs intact, so this path stays
|
|
||||||
* texture-atlas-preserving.
|
|
||||||
*/
|
*/
|
||||||
export function schedulePaneRevealPresent(getPanes: () => Iterable<ManagedPaneInternal>): void {
|
export function schedulePaneRevealPresent(getPanes: () => Iterable<ManagedPaneInternal>): void {
|
||||||
forEachPaneOnSettledFrame(getPanes, (pane) => {
|
forEachPaneOnSettledFrame(getPanes, (pane) => {
|
||||||
@@ -73,7 +65,6 @@ export function schedulePaneRevealPresent(getPanes: () => Iterable<ManagedPaneIn
|
|||||||
// latched, RenderService buffers refreshes instead of rendering them, so
|
// latched, RenderService buffers refreshes instead of rendering them, so
|
||||||
// this present would paint nothing. See terminal-synchronized-output-release.
|
// this present would paint nothing. See terminal-synchronized-output-release.
|
||||||
releaseAbandonedSynchronizedOutput(pane.terminal)
|
releaseAbandonedSynchronizedOutput(pane.terminal)
|
||||||
clearTerminalRenderModel(pane.terminal)
|
|
||||||
if (pane.terminal.rows > 0) {
|
if (pane.terminal.rows > 0) {
|
||||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from './terminal-webgl-auto-policy'
|
} from './terminal-webgl-auto-policy'
|
||||||
import { safeFitAndThen } from './pane-fit'
|
import { safeFitAndThen } from './pane-fit'
|
||||||
import { releaseAbandonedSynchronizedOutput } from './terminal-synchronized-output-release'
|
import { releaseAbandonedSynchronizedOutput } from './terminal-synchronized-output-release'
|
||||||
import { clearTerminalRenderModel } from './terminal-render-model-clear'
|
|
||||||
|
|
||||||
export const ENABLE_WEBGL_RENDERER = true
|
export const ENABLE_WEBGL_RENDERER = true
|
||||||
let suggestedRendererType: 'dom' | undefined
|
let suggestedRendererType: 'dom' | undefined
|
||||||
@@ -138,16 +137,7 @@ export function resetWebglTextureAtlas(pane: ManagedPaneInternal): void {
|
|||||||
// Why: rapid TUI redraws can corrupt xterm's WebGL glyph atlas without a
|
// Why: rapid TUI redraws can corrupt xterm's WebGL glyph atlas without a
|
||||||
// context-loss event. Clearing the atlas preserves GPU rendering and forces
|
// context-loss event. Clearing the atlas preserves GPU rendering and forces
|
||||||
// a fresh paint when the pane becomes visible/focused again.
|
// a fresh paint when the pane becomes visible/focused again.
|
||||||
if (pane.webglAddon) {
|
pane.webglAddon?.clearTextureAtlas()
|
||||||
pane.webglAddon.clearTextureAtlas()
|
|
||||||
} else {
|
|
||||||
// Why: clearTextureAtlas is what clears the render model on the WebGL
|
|
||||||
// path. Without an addon nothing invalidates it, so the refresh below
|
|
||||||
// would diff against a model the occluded canvas no longer matches and
|
|
||||||
// skip the stale cells. Clear it explicitly so DOM-rendered panes get the
|
|
||||||
// same guaranteed full repaint.
|
|
||||||
clearTerminalRenderModel(pane.terminal)
|
|
||||||
}
|
|
||||||
// Why: on reveal xterm's IntersectionObserver can still report the pane as
|
// Why: on reveal xterm's IntersectionObserver can still report the pane as
|
||||||
// not intersecting, so a plain refresh() is swallowed by RenderService's
|
// not intersecting, so a plain refresh() is swallowed by RenderService's
|
||||||
// paused-render gate and the cleared model never repaints (stale bottom rows
|
// paused-render gate and the cleared model never repaints (stale bottom rows
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
|
||||||
import { clearTerminalRenderModel } from './terminal-render-model-clear'
|
|
||||||
import { schedulePaneRevealPresent } from './pane-reveal-repaint'
|
|
||||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
|
||||||
|
|
||||||
describe('clearTerminalRenderModel', () => {
|
|
||||||
it('clears the render model through the render service', () => {
|
|
||||||
const clear = vi.fn()
|
|
||||||
expect(clearTerminalRenderModel({ _core: { _renderService: { clear } } })).toBe(true)
|
|
||||||
expect(clear).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('degrades to a no-op when the render service is unavailable', () => {
|
|
||||||
expect(clearTerminalRenderModel({})).toBe(false)
|
|
||||||
expect(clearTerminalRenderModel({ _core: {} })).toBe(false)
|
|
||||||
expect(clearTerminalRenderModel({ _core: { _renderService: {} } })).toBe(false)
|
|
||||||
expect(clearTerminalRenderModel(null)).toBe(false)
|
|
||||||
expect(clearTerminalRenderModel(undefined)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('reports failure instead of throwing when the pane was disposed mid-frame', () => {
|
|
||||||
const clear = vi.fn(() => {
|
|
||||||
throw new Error('disposed')
|
|
||||||
})
|
|
||||||
expect(clearTerminalRenderModel({ _core: { _renderService: { clear } } })).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('schedulePaneRevealPresent repaints a stale canvas', () => {
|
|
||||||
it('clears the model before refreshing, and leaves the shared atlas alone', () => {
|
|
||||||
// Why this ordering matters: refresh() is diff-based, so a model still
|
|
||||||
// holding the pre-hide cells makes it skip exactly the cells an occluded
|
|
||||||
// canvas lost. Clearing first turns the refresh into a full repaint. The
|
|
||||||
// atlas must NOT be cleared here — it is shared with every same-config
|
|
||||||
// pane and wiping it re-arms xterm's page-merge garble race (#4480).
|
|
||||||
const calls: string[] = []
|
|
||||||
const clearTextureAtlas = vi.fn(() => calls.push('atlas'))
|
|
||||||
const pane = {
|
|
||||||
id: 1,
|
|
||||||
gpuRenderingEnabled: true,
|
|
||||||
webglDisabledAfterContextLoss: false,
|
|
||||||
webglAttachmentDeferred: false,
|
|
||||||
webglAddon: { clearTextureAtlas },
|
|
||||||
terminal: {
|
|
||||||
rows: 24,
|
|
||||||
refresh: () => calls.push('refresh'),
|
|
||||||
_core: { _renderService: { clear: () => calls.push('clear-model') } }
|
|
||||||
}
|
|
||||||
} as unknown as ManagedPaneInternal
|
|
||||||
|
|
||||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
||||||
callback(0)
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
schedulePaneRevealPresent(() => [pane])
|
|
||||||
} finally {
|
|
||||||
vi.unstubAllGlobals()
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(calls).toEqual(['clear-model', 'refresh'])
|
|
||||||
expect(
|
|
||||||
clearTextureAtlas,
|
|
||||||
'the shared glyph atlas must survive a refocus'
|
|
||||||
).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* Clears a terminal's per-cell render model WITHOUT touching the shared glyph
|
|
||||||
* atlas, so the next refresh rebuilds every visible cell from the buffer.
|
|
||||||
*
|
|
||||||
* Why this is needed: xterm's renderers are diff-based. `terminal.refresh()`
|
|
||||||
* walks the buffer and skips any cell whose code/fg/bg/ext match the render
|
|
||||||
* model's cached copy (WebglRenderer._updateModel's "Nothing has changed, no
|
|
||||||
* updates needed" early-continue). While a pane is occluded — the window hidden
|
|
||||||
* behind another app, the classic "switch away to the desktop" — the compositor
|
|
||||||
* can drop the canvas contents while that model stays populated. On return the
|
|
||||||
* diff then reports those cells unchanged, the refresh skips them, and the
|
|
||||||
* canvas keeps presenting pre-hide pixels until something forces a full
|
|
||||||
* rebuild. Resizing the window is what users find, because a resize reallocates
|
|
||||||
* the model and repaints everything.
|
|
||||||
*
|
|
||||||
* `RenderService.clear()` delegates to the active renderer's `clear()`, which
|
|
||||||
* calls `_clearModel(true)` — it drops the cached cells and the glyph renderer's
|
|
||||||
* vertices but does NOT clear the texture atlas. That distinction matters: the
|
|
||||||
* atlas is shared by every same-config terminal, and wiping it mid-stream
|
|
||||||
* re-arms xterm's page-merge garble race (xterm.js #4480), which is why the
|
|
||||||
* plain-refocus path is deliberately atlas-preserving. Clearing only the model
|
|
||||||
* gives a guaranteed full repaint at no cost to sibling panes.
|
|
||||||
*
|
|
||||||
* All access is behind typeof guards: an xterm upgrade that renames these
|
|
||||||
* internals degrades to a no-op (the caller's plain refresh still runs) rather
|
|
||||||
* than throwing into a render frame.
|
|
||||||
*/
|
|
||||||
|
|
||||||
type MaybeClearableRenderService = {
|
|
||||||
clear?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type TerminalWithRenderService = {
|
|
||||||
_core?: {
|
|
||||||
_renderService?: MaybeClearableRenderService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drops the cached render model so the caller's next refresh repaints every
|
|
||||||
* visible cell. Returns whether the model was actually cleared.
|
|
||||||
*/
|
|
||||||
export function clearTerminalRenderModel(terminal: unknown): boolean {
|
|
||||||
const service = (terminal as TerminalWithRenderService | null)?._core?._renderService
|
|
||||||
if (typeof service?.clear !== 'function') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
service.clear()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
// A pane disposed mid-frame must not break the caller's repaint.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* STA-2694 repro attempt: switching away from a workspace running an alt-screen
|
* STA-2694 repro: switching away from a workspace running an alt-screen AI TUI
|
||||||
* AI TUI (OpenCode/OpenTUI, also Claude Code and grok) and returning later shows
|
* (OpenCode/OpenTUI, also Claude Code and grok) and returning later shows a
|
||||||
* a garbled/distorted terminal that only a manual window resize repairs.
|
* garbled/distorted terminal that only a manual window resize repairs.
|
||||||
*
|
*
|
||||||
* Why this spec exists next to terminal-inline-tui-reveal-convergence.spec.ts:
|
* Why a new spec next to terminal-inline-tui-reveal-convergence.spec.ts: that
|
||||||
* that one drives the INLINE shape (normal buffer, live block glued to the
|
* one drives the INLINE shape (normal buffer, live block glued to the bottom,
|
||||||
* bottom, history scrolling into scrollback). OpenCode runs FULL-SCREEN on the
|
* history scrolling into scrollback). OpenCode runs FULL-SCREEN on the
|
||||||
* alternate buffer and repaints absolutely-positioned rows — nothing scrolls, so
|
* alternate buffer and repaints absolutely-positioned rows — nothing scrolls,
|
||||||
* no row ever self-heals through the scroll path. These tests cover that shape
|
* so no row ever self-heals through the scroll path, and a row that paints
|
||||||
* across the hide/reveal boundaries (worktree switch, cold park, idle agent,
|
* wrong stays wrong until a resize rebuilds the render model. The inline spec's
|
||||||
* desktop hide) and guard the convergence properties we CAN observe: the buffer
|
* assertions (viewport anchored to baseY, input-box row present, frame
|
||||||
* converges to the live frame, the pane stays on the alt screen, and xterm's
|
* advancing) all pass while the canvas is visibly garbled, because they read
|
||||||
* grid, the fit proposal, and the PTY-applied size all agree without a resize.
|
* xterm's BUFFER. This spec asserts the PIXELS.
|
||||||
*
|
*
|
||||||
* ⚠ KNOWN LIMITATION — these tests do NOT detect a stale/garbled CANVAS.
|
* The decisive assertion is resize-referenced: capture the revealed pane, then
|
||||||
*
|
* force the repair the user performs by hand (a window resize), and require the
|
||||||
* The field defect is "xterm's buffer is correct but the compositor keeps
|
* revealed pixels to already match the repaired pixels. "A resize fixes it" is
|
||||||
* showing pre-hide pixels". Two oracles were built for it and both were proven
|
* the literal user report, so "identical to post-resize" is the literal fix.
|
||||||
* blind by injecting that exact defect (freeze RenderService.refreshRows, then
|
|
||||||
* write new content, so the buffer advances while the canvas cannot):
|
|
||||||
*
|
|
||||||
* 1. Canvas-vs-buffer ink sampling (the render-desync sentinel's method).
|
|
||||||
* `drawImage` on a non-preserveDrawingBuffer WebGL canvas hands back a
|
|
||||||
* re-rendered copy, so it reported 0 missing cells against 5263 cells of
|
|
||||||
* text the canvas had never drawn.
|
|
||||||
* 2. Screenshot comparison against a forced repaint. Playwright's screenshot
|
|
||||||
* drives a fresh compositor frame, which HEALS the stale paint before it
|
|
||||||
* is captured; and the "repair" calls the same repaint code the reveal
|
|
||||||
* already ran, so a defect shared by both shots cancels out.
|
|
||||||
*
|
|
||||||
* So a green run here means "no buffer/geometry regression", NOT "the canvas
|
|
||||||
* painted correctly". Confirming a canvas-level fix needs the in-app
|
|
||||||
* render-desync sentinel on real hardware:
|
|
||||||
* localStorage.setItem('orca:render-desync-sentinel', '1') // then reload
|
|
||||||
* which samples the presented canvas before any forced redraw can heal it, and
|
|
||||||
* writes the corrupt pixels + buffer to app data on a confirmed trip.
|
|
||||||
*/
|
*/
|
||||||
import { readFileSync, writeFileSync } from 'node:fs'
|
import { readFileSync, writeFileSync } from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
@@ -616,119 +598,6 @@ test.describe('OpenCode alt-screen reveal artifacts (STA-2694)', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// The field condition the other tests miss: the agent is IDLE when you come
|
|
||||||
// back. Every test above keeps the TUI streaming across the reveal, so live
|
|
||||||
// frames repaint whatever the reveal got wrong — the defect heals itself
|
|
||||||
// before any assertion runs. A real OpenCode session sits waiting for input,
|
|
||||||
// so nothing arrives to heal it, and whatever the reveal painted is what the
|
|
||||||
// user stares at until they resize the window.
|
|
||||||
test('parked reveal of an IDLE alt-screen agent paints without a manual repair', async ({
|
|
||||||
orcaPage
|
|
||||||
}, testInfo) => {
|
|
||||||
test.setTimeout(240_000)
|
|
||||||
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
|
|
||||||
try {
|
|
||||||
test.skip(
|
|
||||||
!(await hasWebglPane(orcaPage, setup.tabId)),
|
|
||||||
'WebGL renderer unavailable in this environment'
|
|
||||||
)
|
|
||||||
// Go idle BEFORE hiding, cleanly between brackets — a settled agent
|
|
||||||
// holding its last full-screen frame.
|
|
||||||
await sendToTerminal(orcaPage, setup.ptyId, 'ORCA_FREEZE_NOW').catch(() => {})
|
|
||||||
await orcaPage.waitForTimeout(2_000)
|
|
||||||
const idleFrame = latestMatch(
|
|
||||||
(await probeRevealedPane(orcaPage, setup.tabId))?.screenRows.join('\n') ?? '',
|
|
||||||
FRAME_RE
|
|
||||||
)
|
|
||||||
expect(idleFrame, 'fixture never painted a frame before going idle').toBeGreaterThan(0)
|
|
||||||
|
|
||||||
// Cold-park the idle tab: renderer torn down, so the reveal must restore
|
|
||||||
// and repaint entirely from the snapshot with no live output to help.
|
|
||||||
await createActiveTerminalTab(orcaPage, setup.worktreeId)
|
|
||||||
await createActiveTerminalTab(orcaPage, setup.worktreeId)
|
|
||||||
await waitForTabParked(orcaPage, setup.tabId, { parkDelayMs: PARKING_DELAY_MS })
|
|
||||||
await orcaPage.waitForTimeout(3_000)
|
|
||||||
|
|
||||||
await withCpuThrottle(orcaPage, 6, async () => {
|
|
||||||
await activateTerminalTab(orcaPage, setup.tabId)
|
|
||||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
|
||||||
await orcaPage.waitForTimeout(3_000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// The same frame must still be on screen — the agent produced nothing new.
|
|
||||||
const revealedProbe = await probeRevealedPane(orcaPage, setup.tabId)
|
|
||||||
expect(
|
|
||||||
revealedProbe?.bufferType,
|
|
||||||
`idle-parked-reveal: pane is not on the alt screen: ${describeProbe(revealedProbe)}`
|
|
||||||
).toBe('alternate')
|
|
||||||
expect(
|
|
||||||
latestMatch(revealedProbe?.screenRows.join('\n') ?? '', FRAME_RE),
|
|
||||||
`idle-parked-reveal: idle frame lost across the park: ${describeProbe(revealedProbe)}`
|
|
||||||
).toBeGreaterThan(0)
|
|
||||||
|
|
||||||
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'idle-parked-reveal')
|
|
||||||
} finally {
|
|
||||||
await setup.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// The LITERAL user action: "switch away to the desktop". That is an OS-level
|
|
||||||
// window hide/occlusion, not in-app navigation — it flips
|
|
||||||
// document.visibilityState, releases the WebGL context, and comes back
|
|
||||||
// through the window-wake recovery path rather than the worktree-reveal path.
|
|
||||||
// The pane never unmounts and its worktree never changes, so none of the
|
|
||||||
// reveal-repaint machinery the other tests exercise even runs.
|
|
||||||
test('@headful desktop switch away and back paints an idle agent without a manual repair', async ({
|
|
||||||
orcaPage,
|
|
||||||
electronApp
|
|
||||||
}, testInfo) => {
|
|
||||||
test.setTimeout(240_000)
|
|
||||||
const setup = await startStreamingAltScreenTui(orcaPage, testInfo)
|
|
||||||
try {
|
|
||||||
test.skip(
|
|
||||||
!(await hasWebglPane(orcaPage, setup.tabId)),
|
|
||||||
'WebGL renderer unavailable in this environment'
|
|
||||||
)
|
|
||||||
// Idle agent holding a full-screen frame — the field state on return.
|
|
||||||
await sendToTerminal(orcaPage, setup.ptyId, 'ORCA_FREEZE_NOW').catch(() => {})
|
|
||||||
await orcaPage.waitForTimeout(2_000)
|
|
||||||
|
|
||||||
// Switch away to the desktop: hide the window entirely.
|
|
||||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
|
||||||
const window = BrowserWindow.getAllWindows()[0]
|
|
||||||
if (!window) {
|
|
||||||
throw new Error('No Electron window')
|
|
||||||
}
|
|
||||||
window.hide()
|
|
||||||
})
|
|
||||||
await orcaPage.waitForTimeout(4_000)
|
|
||||||
|
|
||||||
// ...and come back to it.
|
|
||||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
|
||||||
const window = BrowserWindow.getAllWindows()[0]
|
|
||||||
if (!window) {
|
|
||||||
throw new Error('No Electron window')
|
|
||||||
}
|
|
||||||
window.show()
|
|
||||||
window.focus()
|
|
||||||
})
|
|
||||||
await orcaPage.waitForTimeout(3_000)
|
|
||||||
|
|
||||||
const revealedProbe = await probeRevealedPane(orcaPage, setup.tabId)
|
|
||||||
expect(
|
|
||||||
revealedProbe?.bufferType,
|
|
||||||
`desktop-return: pane is not on the alt screen: ${describeProbe(revealedProbe)}`
|
|
||||||
).toBe('alternate')
|
|
||||||
|
|
||||||
await assertRevealPixelsNeedNoRepair(orcaPage, testInfo, setup, 'desktop-return')
|
|
||||||
} finally {
|
|
||||||
await electronApp
|
|
||||||
.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.show())
|
|
||||||
.catch(() => {})
|
|
||||||
await setup.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// The mechanism behind STA-2694: OpenTUI-style TUIs bracket every repaint in
|
// The mechanism behind STA-2694: OpenTUI-style TUIs bracket every repaint in
|
||||||
// `?2026h … ?2026l`, so a hide can land inside an open bracket. xterm then
|
// `?2026h … ?2026l`, so a hide can land inside an open bracket. xterm then
|
||||||
// keeps synchronizedOutput latched, and RenderService checks that latch
|
// keeps synchronizedOutput latched, and RenderService checks that latch
|
||||||
|
|||||||
Reference in New Issue
Block a user