Revert "Add terminal jump-to-present control (#660)" (#697)

This reverts commit d256879f3e.
This commit is contained in:
Jinjing 2026-04-15 22:08:25 -07:00 committed by GitHub
parent a68bab60a6
commit 53b75f6f16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 0 additions and 161 deletions

View file

@ -3,7 +3,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
import { createPortal } from 'react-dom'
import type { CSSProperties } from 'react'
import type { IDisposable } from '@xterm/xterm'
import { ChevronDown } from 'lucide-react'
import { useAppStore } from '../../store'
import {
DEFAULT_TERMINAL_DIVIDER_DARK,
@ -63,7 +62,6 @@ export default function TerminalPane({
)
const paneTransportsRef = useRef<Map<number, PtyTransport>>(new Map())
const panePtyBindingsRef = useRef<Map<number, IDisposable>>(new Map())
const paneScrollListenerDisposablesRef = useRef<Map<number, IDisposable>>(new Map())
const pendingWritesRef = useRef<Map<number, string>>(new Map())
const isActiveRef = useRef(isActive)
isActiveRef.current = isActive
@ -77,9 +75,6 @@ export default function TerminalPane({
const searchStateRef = useRef<SearchState>({ query: '', caseSensitive: false, regex: false })
const [closeConfirmPaneId, setCloseConfirmPaneId] = useState<number | null>(null)
const [terminalError, setTerminalError] = useState<string | null>(null)
const [paneJumpToPresentVisible, setPaneJumpToPresentVisible] = useState<Record<number, boolean>>(
{}
)
// Pane title state — keyed by ephemeral paneId, persisted via titlesByLeafId
// in the layout snapshot. Ref keeps persistLayoutSnapshot closures fresh.
@ -334,7 +329,6 @@ export default function TerminalPane({
paneFontSizesRef,
paneTransportsRef,
panePtyBindingsRef,
paneScrollListenerDisposablesRef,
pendingWritesRef,
isActiveRef,
isVisibleRef,
@ -355,7 +349,6 @@ export default function TerminalPane({
setExpandedPane,
syncExpandedLayout,
persistLayoutSnapshot,
setPaneJumpToPresentVisible,
setPaneTitles,
paneTitlesRef,
setRenamingPaneId
@ -486,18 +479,6 @@ export default function TerminalPane({
toggleExpandPane
})
const handleJumpToPresent = useCallback((paneId: number): void => {
const manager = managerRef.current
const pane = manager?.getPanes().find((candidate) => candidate.id === paneId)
if (!manager || !pane) {
return
}
manager.setActivePane(paneId, { focus: false })
pane.terminal.scrollToBottom()
pane.terminal.focus()
}, [])
// Intercept paste at the keydown level (Cmd+V / Ctrl+V) AND as a fallback
// on the paste event. We must handle keydown because Chromium does not fire
// a paste event when the clipboard contains only image data (no text
@ -863,26 +844,6 @@ export default function TerminalPane({
/>,
activePane.container
)}
{managerRef.current?.getPanes().map((pane) => {
if (!paneJumpToPresentVisible[pane.id]) {
return null
}
return createPortal(
<button
type="button"
aria-label="Scroll to bottom"
title="Scroll to bottom"
className="absolute bottom-3 right-3 z-20 inline-flex h-8 items-center gap-1.5 rounded-md border border-zinc-700/80 bg-zinc-900/90 pl-2 pr-3.5 text-xs font-medium text-zinc-100 shadow-lg backdrop-blur-sm transition hover:bg-zinc-800/95"
onMouseDown={(event) => event.preventDefault()}
onClick={() => handleJumpToPresent(pane.id)}
>
<ChevronDown className="size-3.5 shrink-0" />
<span>Scroll to bottom</span>
</button>,
pane.container,
`jump-to-present-${pane.id}`
)
})}
<TerminalContextMenu
open={contextMenu.open}
onOpenChange={contextMenu.setOpen}

View file

@ -1,32 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getPaneJumpState, SCROLLED_AWAY_THRESHOLD } from './jump-to-present'
describe('getPaneJumpState', () => {
it('stays hidden when the viewport is already at the live bottom', () => {
expect(getPaneJumpState({ baseY: 24, viewportY: 24 })).toEqual({
showJumpToPresent: false,
hiddenLineCount: 0
})
})
it('stays hidden within the scroll threshold', () => {
expect(getPaneJumpState({ baseY: 24, viewportY: 24 - SCROLLED_AWAY_THRESHOLD })).toEqual({
showJumpToPresent: false,
hiddenLineCount: SCROLLED_AWAY_THRESHOLD
})
})
it('shows once the viewport is meaningfully above the bottom', () => {
expect(getPaneJumpState({ baseY: 24, viewportY: 24 - SCROLLED_AWAY_THRESHOLD - 1 })).toEqual({
showJumpToPresent: true,
hiddenLineCount: SCROLLED_AWAY_THRESHOLD + 1
})
})
it('clamps negative hidden-line counts caused by transient viewport races', () => {
expect(getPaneJumpState({ baseY: 8, viewportY: 12 })).toEqual({
showJumpToPresent: false,
hiddenLineCount: 0
})
})
})

View file

@ -1,25 +0,0 @@
// Why: raised from 1 to 5 so the button only appears after a deliberate scroll
// (a few visible lines), not on any transient viewport nudge.
export const SCROLLED_AWAY_THRESHOLD = 20
export type PaneJumpState = {
showJumpToPresent: boolean
hiddenLineCount: number
}
type TerminalBufferViewport = {
baseY: number
viewportY: number
}
// Why: the design intentionally tolerates a one-line delta before showing the
// affordance because xterm can transiently perturb viewportY during resize,
// split, and refit flows. Without this threshold, the button would flash while
// Orca is preserving the user's current bottom-of-buffer position.
export function getPaneJumpState(buffer: TerminalBufferViewport): PaneJumpState {
const hiddenLineCount = Math.max(0, buffer.baseY - buffer.viewportY)
return {
showJumpToPresent: hiddenLineCount > SCROLLED_AWAY_THRESHOLD,
hiddenLineCount
}
}

View file

@ -24,7 +24,6 @@ import { connectPanePty } from './pty-connection'
import type { PtyTransport } from './pty-transport'
import { fitAndFocusPanes, fitPanes } from './pane-helpers'
import { registerRuntimeTerminalTab, scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph'
import { getPaneJumpState } from './jump-to-present'
type UseTerminalPaneLifecycleDeps = {
tabId: string
@ -50,7 +49,6 @@ type UseTerminalPaneLifecycleDeps = {
paneFontSizesRef: React.RefObject<Map<number, number>>
paneTransportsRef: React.RefObject<Map<number, PtyTransport>>
panePtyBindingsRef: React.RefObject<Map<number, IDisposable>>
paneScrollListenerDisposablesRef: React.RefObject<Map<number, IDisposable>>
pendingWritesRef: React.RefObject<Map<number, string>>
isActiveRef: React.RefObject<boolean>
isVisibleRef: React.RefObject<boolean>
@ -74,7 +72,6 @@ type UseTerminalPaneLifecycleDeps = {
setExpandedPane: (paneId: number | null) => void
syncExpandedLayout: () => void
persistLayoutSnapshot: () => void
setPaneJumpToPresentVisible: React.Dispatch<React.SetStateAction<Record<number, boolean>>>
setPaneTitles: React.Dispatch<React.SetStateAction<Record<number, string>>>
paneTitlesRef: React.RefObject<Record<number, string>>
setRenamingPaneId: React.Dispatch<React.SetStateAction<number | null>>
@ -98,7 +95,6 @@ export function useTerminalPaneLifecycle({
paneFontSizesRef,
paneTransportsRef,
panePtyBindingsRef,
paneScrollListenerDisposablesRef,
pendingWritesRef,
isActiveRef,
isVisibleRef,
@ -119,7 +115,6 @@ export function useTerminalPaneLifecycle({
setExpandedPane,
syncExpandedLayout,
persistLayoutSnapshot,
setPaneJumpToPresentVisible,
setPaneTitles,
paneTitlesRef,
setRenamingPaneId
@ -127,7 +122,6 @@ export function useTerminalPaneLifecycle({
const systemPrefersDarkRef = useRef(systemPrefersDark)
systemPrefersDarkRef.current = systemPrefersDark
const linkProviderDisposablesRef = useRef(new Map<number, IDisposable>())
const paneJumpToPresentVisibleRef = useRef(new Map<number, boolean>())
const applyAppearance = (manager: PaneManager): void => {
const currentSettings = settingsRef.current
@ -152,7 +146,6 @@ export function useTerminalPaneLifecycle({
const expandedStyleSnapshots = expandedStyleSnapshotRef.current
const paneTransports = paneTransportsRef.current
const panePtyBindings = panePtyBindingsRef.current
const paneScrollListenerDisposables = paneScrollListenerDisposablesRef.current
const pendingWrites = pendingWritesRef.current
const linkDisposables = linkProviderDisposablesRef.current
const worktreePath =
@ -264,46 +257,6 @@ export function useTerminalPaneLifecycle({
restoredLeafId
})
panePtyBindings.set(pane.id, panePtyBinding)
const syncPaneJumpStateFor = (): void => {
const nextShowJumpToPresent = getPaneJumpState(
pane.terminal.buffer.active
).showJumpToPresent
if (paneJumpToPresentVisibleRef.current.get(pane.id) === nextShowJumpToPresent) {
return
}
paneJumpToPresentVisibleRef.current.set(pane.id, nextShowJumpToPresent)
setPaneJumpToPresentVisible((prev) => ({
...prev,
[pane.id]: nextShowJumpToPresent
}))
}
const scrollDisposable = pane.terminal.onScroll(syncPaneJumpStateFor)
// Why: when the user is paused above the live bottom, new PTY output
// advances baseY without moving viewportY. Listening only to onScroll
// leaves the affordance stale until the user scrolls again, so writes
// must also trigger a sync.
const writeParsedDisposable = pane.terminal.onWriteParsed(syncPaneJumpStateFor)
// Why: fitAddon.fit() drives terminal.resize(), which can reflow the
// wrapped buffer and change baseY/viewportY without any explicit user
// scroll. Watching xterm's resize event keeps the affordance aligned
// with split, zoom, and container-resize refits.
const resizeDisposable = pane.terminal.onResize(syncPaneJumpStateFor)
paneScrollListenerDisposables.set(pane.id, {
dispose: () => {
scrollDisposable.dispose()
writeParsedDisposable.dispose()
resizeDisposable.dispose()
}
})
// Why: openTerminal() and the layout replay both schedule work into the
// next frame. Deferring the first sync avoids reading a transient
// viewportY before xterm finishes its initial fit/reflow pass.
requestAnimationFrame(() => {
if (!paneScrollListenerDisposables.has(pane.id)) {
return
}
syncPaneJumpStateFor()
})
scheduleRuntimeGraphSync()
queueResizeAll(true)
},
@ -328,23 +281,9 @@ export function useTerminalPaneLifecycle({
transport.destroy?.()
paneTransportsRef.current.delete(paneId)
}
const scrollDisposable = paneScrollListenerDisposables.get(paneId)
if (scrollDisposable) {
scrollDisposable.dispose()
paneScrollListenerDisposables.delete(paneId)
}
paneJumpToPresentVisibleRef.current.delete(paneId)
clearRuntimePaneTitle(tabId, paneId)
paneFontSizesRef.current.delete(paneId)
pendingWritesRef.current.delete(paneId)
setPaneJumpToPresentVisible((prev) => {
if (!(paneId in prev)) {
return prev
}
const next = { ...prev }
delete next[paneId]
return next
})
// Clean up pane title state so closed panes don't leave stale entries.
setPaneTitles((prev) => {
if (!(paneId in prev)) {
@ -529,10 +468,6 @@ export function useTerminalPaneLifecycle({
disposable.dispose()
}
linkDisposables.clear()
for (const disposable of paneScrollListenerDisposables.values()) {
disposable.dispose()
}
paneScrollListenerDisposables.clear()
for (const transport of paneTransports.values()) {
if (tabStillExists && transport.getPtyId()) {
// Why: moving a terminal tab between groups currently rehomes the