Compare commits
1 Commits
v0.2.17-wi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ebae39026f |
@@ -36,6 +36,9 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
const isAtBottomRef = useRef(true);
|
const isAtBottomRef = useRef(true);
|
||||||
|
// Tracks user intent to follow output — only set to false by explicit user
|
||||||
|
// actions (mouse wheel up), not by xterm scroll events during writes.
|
||||||
|
const autoFollowRef = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
@@ -132,6 +135,15 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
sendInput(sessionId, data);
|
sendInput(sessionId, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Detect user-initiated scroll-up (mouse wheel) to pause auto-follow.
|
||||||
|
// Captured during capture phase so it fires before xterm's own handler.
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
autoFollowRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
containerRef.current.addEventListener("wheel", handleWheel, { capture: true, passive: true });
|
||||||
|
|
||||||
// Track scroll position to show "Jump to Current" button.
|
// Track scroll position to show "Jump to Current" button.
|
||||||
// Debounce state updates via rAF to avoid excessive re-renders during rapid output.
|
// Debounce state updates via rAF to avoid excessive re-renders during rapid output.
|
||||||
let scrollStateRafId: number | null = null;
|
let scrollStateRafId: number | null = null;
|
||||||
@@ -139,6 +151,10 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const buf = term.buffer.active;
|
const buf = term.buffer.active;
|
||||||
const atBottom = buf.viewportY >= buf.baseY;
|
const atBottom = buf.viewportY >= buf.baseY;
|
||||||
isAtBottomRef.current = atBottom;
|
isAtBottomRef.current = atBottom;
|
||||||
|
// Re-enable auto-follow when viewport reaches the bottom
|
||||||
|
if (atBottom) {
|
||||||
|
autoFollowRef.current = true;
|
||||||
|
}
|
||||||
if (scrollStateRafId === null) {
|
if (scrollStateRafId === null) {
|
||||||
scrollStateRafId = requestAnimationFrame(() => {
|
scrollStateRafId = requestAnimationFrame(() => {
|
||||||
scrollStateRafId = null;
|
scrollStateRafId = null;
|
||||||
@@ -199,10 +215,13 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
if (aborted) return;
|
if (aborted) return;
|
||||||
term.write(data, () => {
|
term.write(data, () => {
|
||||||
// Keep viewport pinned to bottom when user hasn't scrolled up.
|
// Keep viewport pinned to bottom when user hasn't scrolled up.
|
||||||
// Check ref at callback time (not capture time) so that a user
|
// Uses autoFollowRef (user intent) rather than isAtBottomRef (viewport
|
||||||
// scroll-up between the write() call and callback is respected.
|
// position) so that xterm desync during writes doesn't kill auto-follow,
|
||||||
if (isAtBottomRef.current) {
|
// but an explicit user scroll-up does pause it.
|
||||||
|
if (autoFollowRef.current) {
|
||||||
term.scrollToBottom();
|
term.scrollToBottom();
|
||||||
|
isAtBottomRef.current = true;
|
||||||
|
setIsAtBottom(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
detector.feed(data);
|
detector.feed(data);
|
||||||
@@ -268,6 +287,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
scrollDisposable.dispose();
|
scrollDisposable.dispose();
|
||||||
selectionDisposable.dispose();
|
selectionDisposable.dispose();
|
||||||
setTerminalHasSelection(false);
|
setTerminalHasSelection(false);
|
||||||
|
containerRef.current?.removeEventListener("wheel", handleWheel, { capture: true });
|
||||||
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
containerRef.current?.removeEventListener("paste", handlePaste, { capture: true });
|
||||||
outputPromise.then((fn) => fn?.());
|
outputPromise.then((fn) => fn?.());
|
||||||
exitPromise.then((fn) => fn?.());
|
exitPromise.then((fn) => fn?.());
|
||||||
@@ -339,6 +359,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const handleScrollToBottom = useCallback(() => {
|
const handleScrollToBottom = useCallback(() => {
|
||||||
const term = termRef.current;
|
const term = termRef.current;
|
||||||
if (term) {
|
if (term) {
|
||||||
|
autoFollowRef.current = true;
|
||||||
// Re-fit first to fix viewport desync (same thing a resize does)
|
// Re-fit first to fix viewport desync (same thing a resize does)
|
||||||
fitRef.current?.fit();
|
fitRef.current?.fit();
|
||||||
term.scrollToBottom();
|
term.scrollToBottom();
|
||||||
|
|||||||
Reference in New Issue
Block a user