fix: debounce pty resize and skip no-op resizes (kills duplicated TUI frames on maximize/restore)

This commit is contained in:
Bill committed 2026-09-14 01:12:15 +08:00
1 parent ab5f8c2f77
commit acdf955918
2 files changed
+29 -3

No files matched your search

+5
View File
@@ -41,3 +41,8 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生 - 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生
- 标签强调色:`settings.tabAccentColor`(默认 `#3fb950`),经 `--tab-accent` CSS 变量生效 - 标签强调色:`settings.tabAccentColor`(默认 `#3fb950`),经 `--tab-accent` CSS 变量生效
## 终端尺寸同步
- `TerminalView.scheduleFit`:fit 后**去抖 100ms** 再把 cols/rows 发给 PTY,并跳过与上次相同的尺寸。每次 ResizeObserver 都戳 PTY 会让全屏 TUI(Claude Code 等)在最大化/还原的中间尺寸上反复重绘,留下重复帧
- 拖动窗口期间 xterm 网格立即更新,PTY 尺寸在停止后 100ms 生效
+24 -3
View File
@@ -226,6 +226,13 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
const searchRef = useRef<SearchAddon | null>(null) const searchRef = useRef<SearchAddon | null>(null)
const observerRef = useRef<ResizeObserver | null>(null) const observerRef = useRef<ResizeObserver | null>(null)
const timerRef = useRef<number>(0) const timerRef = useRef<number>(0)
// PTY resize bookkeeping: `ptyTimerRef` debounces the resize while a window
// drag/maximize settles, `ptySizeRef` drops the no-op resizes in between.
// Poking the pty on every intermediate layout tick makes full-screen TUIs
// (Claude Code et al.) redraw repeatedly at half-settled sizes, which is what
// leaves duplicated frames behind in the scrollback.
const ptyTimerRef = useRef<number>(0)
const ptySizeRef = useRef<{ cols: number; rows: number } | null>(null)
const deadRef = useRef(false) const deadRef = useRef(false)
const searchOpenRef = useRef(false) const searchOpenRef = useRef(false)
const searchTermRef = useRef('') const searchTermRef = useRef('')
@@ -337,9 +344,21 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
return return
} }
const term = termRef.current const term = termRef.current
if (term && !deadRef.current) { if (!term || deadRef.current) return
window.api.resizePty(sessionId, term.cols, term.rows) // The xterm grid is already correct; only tell the pty once it settles.
} const last = ptySizeRef.current
if (last && last.cols === term.cols && last.rows === term.rows) return
window.clearTimeout(ptyTimerRef.current)
ptyTimerRef.current = window.setTimeout(() => {
const live = termRef.current
if (!live || deadRef.current) return
if (live.cols === ptySizeRef.current?.cols && live.rows === ptySizeRef.current?.rows) return
ptySizeRef.current = { cols: live.cols, rows: live.rows }
window.api.resizePty(sessionId, live.cols, live.rows)
// 100ms mirrors what other Electron terminals use: long enough to swallow
// a maximize/restore animation tick storm, short enough that a plain
// window drag does not visibly wrap against a stale width.
}, 100)
}) })
}, [sessionId]) }, [sessionId])
@@ -616,6 +635,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
// M5: reset per-session state on (re)bind — fresh line buffer, no leftovers // M5: reset per-session state on (re)bind — fresh line buffer, no leftovers
// from a template apply or panel reuse; recording resumes idle. // from a template apply or panel reuse; recording resumes idle.
lineBufRef.current = '' lineBufRef.current = ''
ptySizeRef.current = null
setSuggestions([]) setSuggestions([])
setSuggestionIndex(0) setSuggestionIndex(0)
if (recordingRef.current) { if (recordingRef.current) {
@@ -746,6 +766,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
return () => { return () => {
if (carryTimer !== undefined) window.clearTimeout(carryTimer) if (carryTimer !== undefined) window.clearTimeout(carryTimer)
if (ptyTimerRef.current) window.clearTimeout(ptyTimerRef.current)
for (const unsubscribe of unsubscribes) unsubscribe() for (const unsubscribe of unsubscribes) unsubscribe()
for (const disposable of disposables) disposable.dispose() for (const disposable of disposables) disposable.dispose()
observer?.disconnect() observer?.disconnect()