mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Use numeric scroll restore for visibility resume
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { PaneManager } from '@/lib/pane-manager/pane-manager'
|
||||
type FitPanesOptions = {
|
||||
debugSource?: string
|
||||
syncScrollbar?: boolean
|
||||
useMarkers?: boolean
|
||||
}
|
||||
|
||||
export function fitPanes(manager: PaneManager, options?: FitPanesOptions): void {
|
||||
|
||||
@@ -156,12 +156,14 @@ function resumeTerminalVisibilityHeavy(manager: PaneManager, isActive: boolean):
|
||||
if (isActive) {
|
||||
fitAndFocusPanes(manager, {
|
||||
debugSource: 'visibility-resume-fit',
|
||||
syncScrollbar: false
|
||||
syncScrollbar: false,
|
||||
useMarkers: false
|
||||
})
|
||||
} else {
|
||||
fitPanes(manager, {
|
||||
debugSource: 'visibility-resume-fit',
|
||||
syncScrollbar: false
|
||||
syncScrollbar: false,
|
||||
useMarkers: false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -182,7 +184,8 @@ function restoreTerminalViewportPositions(
|
||||
})
|
||||
restoreScrollStateAfterLayout(pane.terminal, position, {
|
||||
debugSource: 'visibility-restore',
|
||||
syncScrollbar: false
|
||||
syncScrollbar: false,
|
||||
useMarkers: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +531,8 @@ describe('useTerminalPaneGlobalEffects', () => {
|
||||
expect(manager.resumeRendering).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.fitAndFocusPanes).toHaveBeenCalledWith(manager, {
|
||||
debugSource: 'visibility-resume-fit',
|
||||
syncScrollbar: false
|
||||
syncScrollbar: false,
|
||||
useMarkers: false
|
||||
})
|
||||
expect(manager.resetWebglTextureAtlases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -623,7 +624,8 @@ describe('useTerminalPaneGlobalEffects', () => {
|
||||
expect(manager.suspendRendering).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.restoreScrollStateAfterLayout).toHaveBeenLastCalledWith(terminalA, preHideState, {
|
||||
debugSource: 'visibility-restore',
|
||||
syncScrollbar: false
|
||||
syncScrollbar: false,
|
||||
useMarkers: false
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -161,7 +161,11 @@ export class PaneManager {
|
||||
return Array.from(this.panes.values()).map(toPublicPane)
|
||||
}
|
||||
|
||||
fitAllPanes(options?: { debugSource?: string; syncScrollbar?: boolean }): void {
|
||||
fitAllPanes(options?: {
|
||||
debugSource?: string
|
||||
syncScrollbar?: boolean
|
||||
useMarkers?: boolean
|
||||
}): void {
|
||||
fitAllPanesInternal(this.panes, options)
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,23 @@ describe('scroll state', () => {
|
||||
expect(marker.dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('can disable marker restore for visibility resume', () => {
|
||||
const terminal = createTerminal({ viewportY: 10, baseY: 300 })
|
||||
const marker = createMarker(71)
|
||||
const state: ScrollState = {
|
||||
bufferType: 'normal',
|
||||
wasAtBottom: false,
|
||||
viewportY: 150,
|
||||
baseY: 154,
|
||||
firstVisibleLineMarker: marker
|
||||
}
|
||||
|
||||
restoreScrollStateAfterLayout(terminal, state, { useMarkers: false })
|
||||
|
||||
expect(terminal.scrollToLine).toHaveBeenCalledWith(150)
|
||||
expect(terminal.buffer.active.viewportY).toBe(150)
|
||||
})
|
||||
|
||||
it('reapplies a layout restore after xterm settles asynchronously', () => {
|
||||
vi.useFakeTimers()
|
||||
const rafCallbacks: FrameRequestCallback[] = []
|
||||
|
||||
@@ -22,6 +22,7 @@ const deferredScrollRestores = new WeakMap<
|
||||
type RestoreScrollStateOptions = {
|
||||
debugSource?: string
|
||||
syncScrollbar?: boolean
|
||||
useMarkers?: boolean
|
||||
}
|
||||
|
||||
export function recordTerminalOutput(terminal: Terminal): void {
|
||||
@@ -74,7 +75,7 @@ export function captureScrollState(terminal: Terminal): ScrollState {
|
||||
|
||||
export function restoreScrollState(terminal: Terminal, state: ScrollState): void {
|
||||
cancelDeferredScrollRestore(terminal)
|
||||
restoreScrollStateNow(terminal, state, { syncScrollbar: true })
|
||||
restoreScrollStateNow(terminal, state, { syncScrollbar: true, useMarkers: true })
|
||||
releaseScrollStateMarker(state)
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ export function restoreScrollStateAfterLayout(
|
||||
): void {
|
||||
cancelDeferredScrollRestore(terminal)
|
||||
const syncScrollbar = options.syncScrollbar ?? true
|
||||
const useMarkers = options.useMarkers ?? true
|
||||
const debugSource = options.debugSource
|
||||
logTerminalScrollRestore('restore-scheduled', {
|
||||
source: debugSource,
|
||||
@@ -92,7 +94,7 @@ export function restoreScrollStateAfterLayout(
|
||||
syncScrollbar,
|
||||
viewport: terminalViewportForDebug(terminal)
|
||||
})
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar })
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar, useMarkers })
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
releaseScrollStateMarker(state)
|
||||
return
|
||||
@@ -107,7 +109,7 @@ export function restoreScrollStateAfterLayout(
|
||||
}
|
||||
const restore = (): void => {
|
||||
if (!pending.cancelled) {
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar })
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar, useMarkers })
|
||||
}
|
||||
}
|
||||
const cancelPendingRafs = (): void => {
|
||||
@@ -129,7 +131,7 @@ export function restoreScrollStateAfterLayout(
|
||||
})
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!pending.cancelled) {
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar })
|
||||
restoreScrollStateNow(terminal, state, { debugSource, syncScrollbar, useMarkers })
|
||||
}
|
||||
// Why: background tabs can throttle rAF past the timeout. Once the
|
||||
// authoritative timeout restore has run, stale frame callbacks must not
|
||||
@@ -146,7 +148,7 @@ export function restoreScrollStateAfterLayout(
|
||||
function restoreScrollStateNow(
|
||||
terminal: Terminal,
|
||||
state: ScrollState,
|
||||
options: RestoreScrollStateOptions & { syncScrollbar: boolean }
|
||||
options: RestoreScrollStateOptions & { syncScrollbar: boolean; useMarkers: boolean }
|
||||
): void {
|
||||
if (!terminal.element) {
|
||||
logTerminalScrollRestore('restore-attempt', {
|
||||
@@ -197,7 +199,7 @@ function restoreScrollStateNow(
|
||||
: -1
|
||||
// Why: xterm markers can drift while TUIs rewrite/reflow the same scrollback
|
||||
// base. Prefer the exact numeric viewport unless the buffer base changed.
|
||||
const shouldUseMarkerLine = markerLine >= 0 && buf.baseY !== state.baseY
|
||||
const shouldUseMarkerLine = options.useMarkers && markerLine >= 0 && buf.baseY !== state.baseY
|
||||
const targetLine = Math.min(shouldUseMarkerLine ? markerLine : state.viewportY, buf.baseY)
|
||||
// Why: deferred rAF/timeout restores re-invoke this function after xterm
|
||||
// reflow settles; keep the original viewport and marker alive so later
|
||||
|
||||
@@ -30,6 +30,7 @@ type TreeOpsCallbacks = {
|
||||
type SafeFitOptions = {
|
||||
debugSource?: string
|
||||
syncScrollbar?: boolean
|
||||
useMarkers?: boolean
|
||||
}
|
||||
|
||||
const MIN_PANE_FIT_WIDTH_PX = 48
|
||||
@@ -110,7 +111,8 @@ export function safeFit(pane: ManagedPane, options: SafeFitOptions = {}): void {
|
||||
try {
|
||||
restoreScrollStateAfterLayout(pane.terminal, scrollState, {
|
||||
debugSource: options.debugSource ?? 'safeFit',
|
||||
syncScrollbar: options.syncScrollbar
|
||||
syncScrollbar: options.syncScrollbar,
|
||||
useMarkers: options.useMarkers
|
||||
})
|
||||
} catch {
|
||||
// Why: xterm can temporarily expose a terminal whose renderer has not
|
||||
|
||||
Reference in New Issue
Block a user