mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
Key pending follow-output by terminal leaf
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -92,7 +92,7 @@ describe('handleFocusTerminalPaneDetail', () => {
|
||||
)
|
||||
|
||||
expect(manager.setActivePane).toHaveBeenCalledWith(7, { focus: true })
|
||||
expect(scrollToBottomIfOutputSinceLastView).toHaveBeenCalledWith(7)
|
||||
expect(scrollToBottomIfOutputSinceLastView).toHaveBeenCalledWith(LEAF_ID)
|
||||
})
|
||||
|
||||
it('does not focus, flash, or ack when the numeric pane no longer owns the leaf', () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ type FocusTerminalPaneEventDeps = {
|
||||
manager: FocusTerminalPaneManager | null
|
||||
acknowledgeAgents: (paneKeys: string[]) => void
|
||||
surfaceStaleAgentRow: (tabId: string, leafId: string) => void
|
||||
scrollToBottomIfOutputSinceLastView?: (paneId: number) => void
|
||||
scrollToBottomIfOutputSinceLastView?: (leafId: ManagedPane['leafId']) => void
|
||||
}
|
||||
|
||||
export function handleFocusTerminalPaneDetail(
|
||||
@@ -48,7 +48,7 @@ export function handleFocusTerminalPaneDetail(
|
||||
}
|
||||
manager.setActivePane(resolution.numericPaneId, { focus: true })
|
||||
if (detail.scrollToBottomIfOutputSinceLastView) {
|
||||
scrollToBottomIfOutputSinceLastView?.(resolution.numericPaneId)
|
||||
scrollToBottomIfOutputSinceLastView?.(resolution.leafId)
|
||||
}
|
||||
if (detail.flashFocusedPane) {
|
||||
const pane = manager.getPanes().find((candidate) => candidate.id === resolution.numericPaneId)
|
||||
|
||||
+82
-2
@@ -115,7 +115,7 @@ describe('useTerminalScrollVisibilityMemory', () => {
|
||||
paneCount: 1
|
||||
})
|
||||
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded(1)
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded('leaf-1' as never)
|
||||
animationFrames.shift()?.(16)
|
||||
animationFrames.shift()?.(32)
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('useTerminalScrollVisibilityMemory', () => {
|
||||
paneCount: 1
|
||||
})
|
||||
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded(1)
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded('leaf-1' as never)
|
||||
runEffectCleanups()
|
||||
|
||||
expect(cancelAnimationFrame).toHaveBeenCalledWith(7)
|
||||
@@ -243,4 +243,84 @@ describe('useTerminalScrollVisibilityMemory', () => {
|
||||
)
|
||||
expect(mocks.captureScrollState).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('does not apply pending follow-output to a reused pane id with a different leaf', () => {
|
||||
const oldTerminal = {
|
||||
onScroll: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
scrollToBottom: vi.fn()
|
||||
}
|
||||
const newTerminal = {
|
||||
onScroll: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
scrollToBottom: vi.fn()
|
||||
}
|
||||
let panes = [{ id: 1, leafId: 'old-leaf', terminal: oldTerminal }]
|
||||
const manager = {
|
||||
getPanes: vi.fn(() => panes)
|
||||
}
|
||||
const animationFrames: FrameRequestCallback[] = []
|
||||
globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||
animationFrames.push(callback)
|
||||
return animationFrames.length
|
||||
})
|
||||
|
||||
beginHookRender()
|
||||
const visibilityMemory = useTerminalScrollVisibilityMemory({
|
||||
managerRef: { current: manager as never },
|
||||
isVisibleRef: { current: true },
|
||||
visibleResumeCompleteRef: { current: true },
|
||||
paneCount: 1
|
||||
})
|
||||
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded('old-leaf' as never)
|
||||
panes = [{ id: 1, leafId: 'new-leaf', terminal: newTerminal }]
|
||||
animationFrames.shift()?.(16)
|
||||
animationFrames.shift()?.(32)
|
||||
|
||||
expect(oldTerminal.scrollToBottom).not.toHaveBeenCalled()
|
||||
expect(newTerminal.scrollToBottom).not.toHaveBeenCalled()
|
||||
expect(mocks.flushTerminalOutput).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not follow output when the remembered visible position was not at bottom', () => {
|
||||
const onScrollListeners: (() => void)[] = []
|
||||
const terminal = {
|
||||
onScroll: vi.fn((listener: () => void) => {
|
||||
onScrollListeners.push(listener)
|
||||
return { dispose: vi.fn() }
|
||||
}),
|
||||
scrollToBottom: vi.fn()
|
||||
}
|
||||
const manager = {
|
||||
getPanes: vi.fn(() => [{ id: 1, leafId: 'leaf-1', terminal }])
|
||||
}
|
||||
const animationFrames: FrameRequestCallback[] = []
|
||||
globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||
animationFrames.push(callback)
|
||||
return animationFrames.length
|
||||
})
|
||||
mocks.captureScrollState.mockReturnValue({
|
||||
bufferType: 'normal',
|
||||
wasAtBottom: false,
|
||||
viewportY: 42,
|
||||
baseY: 100
|
||||
})
|
||||
|
||||
beginHookRender()
|
||||
const visibilityMemory = useTerminalScrollVisibilityMemory({
|
||||
managerRef: { current: manager as never },
|
||||
isVisibleRef: { current: true },
|
||||
visibleResumeCompleteRef: { current: true },
|
||||
paneCount: 1
|
||||
})
|
||||
onScrollListeners[0]?.()
|
||||
|
||||
visibilityMemory.scheduleFollowOutputIfNeeded('leaf-1' as never)
|
||||
animationFrames.shift()?.(16)
|
||||
animationFrames.shift()?.(32)
|
||||
|
||||
expect(mocks.flushTerminalOutput).toHaveBeenCalledWith(terminal, {
|
||||
maxChars: 256 * 1024
|
||||
})
|
||||
expect(terminal.scrollToBottom).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ type TerminalScrollVisibilityMemory = {
|
||||
) => Map<ManagedPane['leafId'], ScrollState>
|
||||
withSuppressedScrollTracking: (callback: () => void) => void
|
||||
applyPendingFollowOutputRequests: () => boolean
|
||||
scheduleFollowOutputIfNeeded: (paneId: number) => void
|
||||
scheduleFollowOutputIfNeeded: (leafId: ManagedPane['leafId']) => void
|
||||
}
|
||||
|
||||
const FOLLOW_OUTPUT_FLUSH_CHARS = 256 * 1024
|
||||
@@ -44,7 +44,7 @@ export function useTerminalScrollVisibilityMemory({
|
||||
)
|
||||
const scrollDisposablesRef = useRef<Map<ManagedPane['leafId'], IDisposable>>(new Map())
|
||||
const suppressScrollTrackingRef = useRef(false)
|
||||
const pendingFollowOutputPaneIdsRef = useRef<Set<number>>(new Set())
|
||||
const pendingFollowOutputLeafIdsRef = useRef<Set<ManagedPane['leafId']>>(new Set())
|
||||
const followOutputFrameIdsRef = useRef<number[]>([])
|
||||
|
||||
const captureVisibleScrollSnapshot = useCallback(
|
||||
@@ -98,7 +98,7 @@ export function useTerminalScrollVisibilityMemory({
|
||||
}, [])
|
||||
|
||||
const applyPendingFollowOutputRequests = useCallback((): boolean => {
|
||||
const pending = pendingFollowOutputPaneIdsRef.current
|
||||
const pending = pendingFollowOutputLeafIdsRef.current
|
||||
if (pending.size === 0) {
|
||||
return false
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export function useTerminalScrollVisibilityMemory({
|
||||
}
|
||||
let didScroll = false
|
||||
for (const pane of manager.getPanes()) {
|
||||
if (!pending.has(pane.id)) {
|
||||
if (!pending.has(pane.leafId)) {
|
||||
continue
|
||||
}
|
||||
const previous = visibleScrollSnapshotsRef.current.get(pane.leafId)
|
||||
@@ -121,13 +121,13 @@ export function useTerminalScrollVisibilityMemory({
|
||||
flushTerminalOutput(pane.terminal, { maxChars: FOLLOW_OUTPUT_FLUSH_CHARS })
|
||||
const currentEpoch = getTerminalOutputEpoch(pane.terminal)
|
||||
const hasNewOutput = previous ? currentEpoch > previous.outputEpoch : currentEpoch > 0
|
||||
if (hasNewOutput) {
|
||||
if (hasNewOutput && (previous?.scrollState.wasAtBottom ?? true)) {
|
||||
cancelDeferredScrollRestore(pane.terminal)
|
||||
pane.terminal.scrollToBottom()
|
||||
rememberVisibleScrollSnapshot(pane.leafId, pane.terminal)
|
||||
didScroll = true
|
||||
}
|
||||
pending.delete(pane.id)
|
||||
pending.delete(pane.leafId)
|
||||
}
|
||||
return didScroll
|
||||
}, [isVisibleRef, managerRef, rememberVisibleScrollSnapshot, visibleResumeCompleteRef])
|
||||
@@ -140,8 +140,8 @@ export function useTerminalScrollVisibilityMemory({
|
||||
}, [])
|
||||
|
||||
const scheduleFollowOutputIfNeeded = useCallback(
|
||||
(paneId: number): void => {
|
||||
pendingFollowOutputPaneIdsRef.current.add(paneId)
|
||||
(leafId: ManagedPane['leafId']): void => {
|
||||
pendingFollowOutputLeafIdsRef.current.add(leafId)
|
||||
if (followOutputFrameIdsRef.current.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user