Fix Windows terminal clear repaint (#6382)

This commit is contained in:
Jinwoo Hong
2026-06-26 16:59:00 -04:00
committed by GitHub
parent d188e7d2f6
commit daab519c5c
9 changed files with 181 additions and 5 deletions
@@ -779,14 +779,37 @@ export default function TerminalPane({
const clearPaneScrollback = useCallback(
(pane: ManagedPane): void => {
clearedScrollbackLeafIdsRef.current.add(pane.leafId)
const transport = paneTransportsRef.current.get(pane.id)
const ptyId = transport?.getPtyId() ?? null
pane.terminal.clear()
// Why: also clear the host buffer for remote-server panes, or the next
// host snapshot replays the scrollback we just cleared locally.
const ptyId = paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null
clearWebRuntimeTerminalBuffer(ptyId)
persistLayoutSnapshot()
const isRemoteRuntimePty = typeof ptyId === 'string' && isRemoteRuntimePtyId(ptyId)
const shouldRepairWindowsClear =
isWindowsUserAgent() && getConnectionId(worktreeId) === null && !isRemoteRuntimePty
const repairWindowsClear = (): void => {
if (!shouldRepairWindowsClear) {
return
}
// Why: local Windows xterm can keep wrapped prompt/cursor paint stale
// after clear; keep this frontend-only so ConPTY does not repaint it.
safeFit(pane)
pane.terminal.refresh(0, Math.max(0, pane.terminal.rows - 1))
requestAnimationFrame(() => {
if (!managerRef.current?.getPanes().some((livePane) => livePane.id === pane.id)) {
return
}
safeFit(pane)
pane.terminal.refresh(0, Math.max(0, pane.terminal.rows - 1))
})
}
// Why: provider/session buffers can outlive the visible xterm. Clear
// them through the same contract used by remote/mobile terminal state.
void Promise.resolve(transport?.clearBuffer?.()).finally(() => {
repairWindowsClear()
persistLayoutSnapshot()
})
},
[paneTransportsRef, persistLayoutSnapshot]
[paneTransportsRef, persistLayoutSnapshot, worktreeId]
)
useEffect(() => {
@@ -363,6 +363,7 @@ export type PtyTransport = {
getPtyId: () => string | null
getConnectionId?: () => string | null | undefined
getLocalSessionMetadata?: () => LocalPtySessionMetadata | null
clearBuffer?: () => Promise<void>
serializeBuffer?: (opts?: { scrollbackRows?: number }) => Promise<PtyBufferSnapshot | null>
preserve?: () => void
/** Unregister PTY handlers without killing the process for pane remounts. */
@@ -42,6 +42,7 @@ describe('createIpcPtyTransport', () => {
writeAccepted: vi.fn().mockResolvedValue(true),
resize: vi.fn(),
kill: vi.fn(),
clearBuffer: vi.fn().mockResolvedValue(undefined),
onData: vi.fn((callback: (payload: { id: string; data: string }) => void) => {
onData = callback
return () => {}
@@ -163,6 +164,20 @@ describe('createIpcPtyTransport', () => {
expect(sshTransport.getLocalSessionMetadata?.()).toBeNull()
})
it('clears the connected PTY buffer through the preload API', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const transport = createIpcPtyTransport({})
await transport.clearBuffer?.()
expect(window.api.pty.clearBuffer).not.toHaveBeenCalled()
await transport.connect({ url: '', callbacks: {} })
await transport.clearBuffer?.()
expect(window.api.pty.clearBuffer).toHaveBeenCalledWith('pty-1')
transport.disconnect()
})
it('defers title side effects until after terminal data is delivered', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const onTitleChange = vi.fn()
@@ -967,6 +967,13 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
}
},
async clearBuffer() {
if (!connected || !ptyId) {
return
}
await window.api.pty.clearBuffer(ptyId)
},
destroy() {
destroyed = true
this.disconnect()
+1
View File
@@ -2445,6 +2445,7 @@ function createPtyApi(): NonNullable<Partial<PreloadApi>['pty']> {
onSerializeBufferRequest: () => noopUnsubscribe,
onClearBufferRequest: () => noopUnsubscribe,
sendSerializedBuffer: () => {},
clearBuffer: () => Promise.resolve(),
declarePendingPaneSerializer: () => Promise.resolve(0),
settlePaneSerializer: () => Promise.resolve(),
clearPendingPaneSerializer: () => Promise.resolve(),