fix(kimi-code): avoid terminal focus flicker on Linux Wayland (#1094)

The focus-driven clipboard image hint spawned wl-paste/xclip on every
terminal focus event. On Wayland this perturbs seat focus and re-triggers
the focus event, creating a feedback loop that made the terminal window
repeatedly gain and lose focus and broke IME input.

Limit the probe to macOS and Windows, which use the in-process native
module and do not perturb focus. Image paste is unaffected.

Resolve #1090
This commit is contained in:
liruifengv 2026-06-25 12:59:01 +08:00 committed by GitHub
parent ea03f30e51
commit 8ee5c0ff81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 209 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix the terminal window repeatedly losing focus on Linux Wayland, which broke IME input.

View file

@ -1,43 +1,6 @@
import {
DEFAULT_LIST_TIMEOUT_MS,
isFileLikeNativeFormat,
isSupportedImageMimeType,
isWaylandSession,
isWSL,
parseTargetList,
runCommandAsync,
safeAvailableFormats,
type RunCommandAsync,
} from './clipboard-common';
import { isFileLikeNativeFormat, safeAvailableFormats } from './clipboard-common';
import { clipboard, type ClipboardModule } from './clipboard-native';
const DEFAULT_POWERSHELL_TIMEOUT_MS = 2000;
async function hasImageViaWlPaste(run: RunCommandAsync): Promise<boolean> {
const list = await run('wl-paste', ['--list-types'], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
if (!list.ok) return false;
return parseTargetList(list.stdout).some((t) => isSupportedImageMimeType(t));
}
async function hasImageViaXclip(run: RunCommandAsync): Promise<boolean> {
const targets = await run('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], {
timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
});
if (!targets.ok) return false;
return parseTargetList(targets.stdout).some((t) => isSupportedImageMimeType(t));
}
async function hasImageViaPowerShell(run: RunCommandAsync): Promise<boolean> {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); ($img -ne $null)";
const result = await run('powershell.exe', ['-NoProfile', '-Command', script], {
timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,
});
if (!result.ok) return false;
const output = result.stdout.toString('utf-8').trim().toLowerCase();
return output === 'true';
}
async function hasImageViaNative(clip: ClipboardModule | null): Promise<boolean> {
if (clip === null) return false;
@ -58,44 +21,24 @@ export async function clipboardHasImage(options?: {
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
clipboard?: ClipboardModule | null;
runCommand?: RunCommandAsync;
}): Promise<boolean> {
const env = options?.env ?? process.env;
const platform = options?.platform ?? process.platform;
const clip = options?.clipboard ?? clipboard;
const run = options?.runCommand ?? runCommandAsync;
if (env['TERMUX_VERSION'] !== undefined) return false;
if (platform === 'linux') {
const wayland = isWaylandSession(env);
const wsl = isWSL(env);
// The focus-driven clipboard-image hint does not probe on Linux. The probe
// would spawn wl-paste / xclip, which on Wayland perturbs seat focus and
// re-triggers the terminal's focus event, creating a focus feedback loop
// (window repeatedly gains/loses focus, IME candidate window cannot stay
// focused — see issue #1090). macOS and Windows are fine: both use the
// in-process native module, which neither spawns a subprocess nor perturbs
// focus.
//
// Image *paste* is unaffected on all platforms: it reads the clipboard
// through readClipboardMedia() on the explicit paste path, not here.
if (platform !== 'darwin' && platform !== 'win32') return false;
let xclipResult: Promise<boolean> | undefined;
const xclipHasImage = (): Promise<boolean> => {
xclipResult ??= hasImageViaXclip(run);
return xclipResult;
};
if (wayland || wsl) {
if (await hasImageViaWlPaste(run)) return true;
if (await xclipHasImage()) return true;
}
if (wsl && (await hasImageViaPowerShell(run))) return true;
if (!wayland) {
if (await xclipHasImage()) return true;
if (await hasImageViaNative(clip)) return true;
}
return false;
}
if (platform === 'darwin') {
return hasImageViaNative(clip);
}
if (platform === 'win32') {
return hasImageViaNative(clip);
}
return false;
return hasImageViaNative(clip);
}

View file

@ -25,10 +25,8 @@ describe('clipboardHasImage', () => {
it('returns false on macOS when native clipboard reports no image', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false }));
const result = await clipboardHasImage({ platform: 'darwin', clipboard: clip, runCommand });
const result = await clipboardHasImage({ platform: 'darwin', clipboard: clip });
expect(result).toBe(false);
expect(runCommand).not.toHaveBeenCalledWith('osascript', expect.anything(), expect.anything());
});
it('returns false on macOS when native clipboard throws', async () => {
@ -51,156 +49,29 @@ describe('clipboardHasImage', () => {
expect(clip.hasImage).not.toHaveBeenCalled();
});
it('detects image on Wayland via wl-paste list-types', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'wl-paste' && args[0] === '--list-types') {
return { stdout: Buffer.from('text/plain\nimage/png\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({ platform: 'linux', env: { WAYLAND_DISPLAY: 'wayland-1' }, runCommand });
expect(result).toBe(true);
});
it('returns false on Wayland when target list contains unsupported MIME types', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'wl-paste' && args[0] === '--list-types') {
return { stdout: Buffer.from('text/plain\nimage/bmp\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
// The focus-driven hint must not probe the clipboard on Linux: spawning
// wl-paste / xclip on Wayland perturbs seat focus and re-triggers the
// terminal focus event, creating a focus feedback loop (issue #1090).
it('returns false on Linux without reading the clipboard', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => true) });
const result = await clipboardHasImage({
platform: 'linux',
env: { WAYLAND_DISPLAY: 'wayland-1' },
runCommand,
clipboard: clip,
});
expect(result).toBe(false);
expect(clip.hasImage).not.toHaveBeenCalled();
});
it('falls back to xclip on Wayland when wl-paste reports no image', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'wl-paste' && args[0] === '--list-types') {
return { stdout: Buffer.from('text/plain\n'), ok: true };
}
if (command === 'xclip' && args.includes('TARGETS')) {
return { stdout: Buffer.from('TARGETS\nimage/png\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({
platform: 'linux',
env: { WAYLAND_DISPLAY: 'wayland-1' },
runCommand,
});
expect(result).toBe(true);
expect(runCommand).toHaveBeenCalledWith('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], expect.anything());
});
it('detects image on X11 via xclip TARGETS', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'xclip' && args.includes('TARGETS')) {
return { stdout: Buffer.from('TARGETS\nimage/jpeg\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({ platform: 'linux', env: {}, runCommand });
expect(result).toBe(true);
});
it('returns false on X11 when target list contains unsupported MIME types', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'xclip' && args.includes('TARGETS')) {
return { stdout: Buffer.from('TARGETS\nimage/tiff\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const result = await clipboardHasImage({ platform: 'linux', env: {}, runCommand, clipboard: clip });
expect(result).toBe(false);
});
it('returns false on X11 when target list is empty', async () => {
const runCommand = vi.fn(async (command: string, args: string[]) => {
if (command === 'xclip' && args.includes('TARGETS')) {
return { stdout: Buffer.from('TARGETS\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const result = await clipboardHasImage({ platform: 'linux', env: {}, runCommand, clipboard: clip });
expect(result).toBe(false);
});
it('falls back to native hasImage on Linux X11 when xclip fails', async () => {
it('returns true on Windows when native clipboard reports an image', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => true) });
const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false }));
const result = await clipboardHasImage({ platform: 'linux', env: {}, clipboard: clip, runCommand });
const result = await clipboardHasImage({ platform: 'win32', clipboard: clip });
expect(result).toBe(true);
});
it('returns false on Linux X11 when xclip and native both fail', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false }));
const result = await clipboardHasImage({ platform: 'linux', env: {}, clipboard: clip, runCommand });
expect(result).toBe(false);
});
it('detects WSL via WSL_DISTRO_NAME and checks PowerShell', async () => {
const runCommand = vi.fn(async (command: string) => {
if (command === 'powershell.exe') {
return { stdout: Buffer.from('True\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({
platform: 'linux',
env: { WSL_DISTRO_NAME: 'Ubuntu' },
runCommand,
});
expect(result).toBe(true);
});
it('detects WSL via WSLENV and checks PowerShell', async () => {
const runCommand = vi.fn(async (command: string) => {
if (command === 'powershell.exe') {
return { stdout: Buffer.from('True\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({
platform: 'linux',
env: { WSLENV: 'WT_SESSION' },
runCommand,
});
expect(result).toBe(true);
});
it('returns false on Linux when runCommand fails for all fallbacks', async () => {
const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false }));
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const result = await clipboardHasImage({ platform: 'linux', env: {}, runCommand, clipboard: clip });
expect(result).toBe(false);
});
it('detects image on Windows via native clipboard', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => true) });
const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false }));
const result = await clipboardHasImage({ platform: 'win32', clipboard: clip, runCommand });
expect(result).toBe(true);
expect(runCommand).not.toHaveBeenCalledWith('powershell.exe', expect.anything(), expect.anything());
});
it('returns false on Windows when native clipboard reports no image', async () => {
const clip = fakeClipboard({ hasImage: vi.fn(() => false) });
const runCommand = vi.fn(async (command: string) => {
if (command === 'powershell.exe') {
return { stdout: Buffer.from('True\n'), ok: true };
}
return { stdout: Buffer.alloc(0), ok: false };
});
const result = await clipboardHasImage({ platform: 'win32', clipboard: clip, runCommand });
const result = await clipboardHasImage({ platform: 'win32', clipboard: clip });
expect(result).toBe(false);
});
});