fix(tui): gate terminal progress sequences behind OSC 9;4 support (#690)

iTerm2 interprets any OSC 9 payload as a desktop notification, so the
ConEmu-style 9;4 progress sequence (re-sent every second by the progress
keepalive) flooded users with notifications. Only emit progress on
terminals known to implement OSC 9;4: Windows Terminal, ConEmu, Ghostty,
and WezTerm.
This commit is contained in:
liruifengv 2026-06-12 16:49:00 +08:00 committed by GitHub
parent 8d251f8ab4
commit 7f0dde2ece
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 78 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix endless desktop notifications in iTerm2 by only sending terminal progress sequences to terminals that support them.

View file

@ -1722,6 +1722,7 @@ export class KimiTUI {
}
private syncTerminalProgress(active: boolean): void {
if (!this.state.terminalState.supportsProgress) return;
if (this.state.terminalState.progressActive === active) return;
this.state.terminal.setProgress(active);
this.state.terminalState.progressActive = active;

View file

@ -110,6 +110,25 @@ export function supportsOsc9Notification(env: NodeJS.ProcessEnv = process.env):
return false;
}
/**
* Best-effort detection of ConEmu-style OSC 9;4 progress support, driven
* off well-known environment variables like `supportsOsc9Notification`.
* The two allow-lists must stay separate: iTerm2 posts a desktop
* notification for ANY `OSC 9;<payload>` it receives, so sending the 9;4
* progress sequence there pops a "4;3" notification every keepalive tick.
* Terminals outside this list simply get no progress reporting, which is
* always safe.
*/
export function supportsTerminalProgress(env: NodeJS.ProcessEnv = process.env): boolean {
if ((env['WT_SESSION'] ?? '').length > 0) return true;
if (env['ConEmuANSI'] === 'ON') return true;
const termProgram = env['TERM_PROGRAM'] ?? '';
if (termProgram === 'ghostty' || termProgram === 'WezTerm') return true;
const term = env['TERM'] ?? '';
if (term === 'xterm-ghostty') return true;
return false;
}
export function isInsideTmux(env: NodeJS.ProcessEnv = process.env): boolean {
const tmux = env['TMUX'] ?? '';
return tmux.length > 0;

View file

@ -1,9 +1,14 @@
import { isInsideTmux, supportsOsc9Notification } from './terminal-notification';
import {
isInsideTmux,
supportsOsc9Notification,
supportsTerminalProgress,
} from './terminal-notification';
export interface TerminalState {
notificationKeys: Set<string>;
focused: boolean;
supportsOsc9: boolean;
supportsProgress: boolean;
insideTmux: boolean;
progressActive: boolean;
}
@ -13,6 +18,7 @@ export function createTerminalState(): TerminalState {
notificationKeys: new Set<string>(),
focused: true,
supportsOsc9: supportsOsc9Notification(),
supportsProgress: supportsTerminalProgress(),
insideTmux: isInsideTmux(),
progressActive: false,
};

View file

@ -47,6 +47,7 @@ function makeDriverWithTerminalProgress(): {
const driver = new KimiTUI({} as never, makeStartupInput()) as unknown as ActivityDriver;
vi.spyOn(driver.state.ui, 'requestRender').mockImplementation(() => {});
driver.state.terminal = { columns: 80, setProgress } as unknown as TUIState['terminal'];
driver.state.terminalState.supportsProgress = true;
return { driver, state: driver.state, setProgress };
}
@ -100,6 +101,24 @@ describe('updateActivityPane terminal progress', () => {
}
});
it('never emits terminal progress when the terminal does not support OSC 9;4', () => {
vi.useFakeTimers();
try {
const { driver, state, setProgress } = makeDriverWithTerminalProgress();
state.terminalState.supportsProgress = false;
state.livePane = { ...state.livePane, mode: 'waiting' };
driver.updateActivityPane();
state.livePane = { ...state.livePane, mode: 'idle' };
driver.updateActivityPane();
expect(setProgress).not.toHaveBeenCalled();
expect(state.terminalState.progressActive).toBe(false);
} finally {
vi.useRealTimers();
}
});
it('keeps compaction visible as terminal progress even though the pane is hidden', () => {
const { driver, state, setProgress } = makeDriverWithTerminalProgress();
state.appState.isCompacting = true;

View file

@ -8,6 +8,7 @@ import {
isInsideTmux,
notifyTerminalOnce,
supportsOsc9Notification,
supportsTerminalProgress,
} from '#/tui/utils/terminal-notification';
function makeNotificationState(args: {
@ -215,6 +216,32 @@ describe('supportsOsc9Notification', () => {
});
});
describe('supportsTerminalProgress', () => {
it('detects Windows Terminal / ConEmu via env flags', () => {
expect(supportsTerminalProgress({ WT_SESSION: 'abc-123' })).toBe(true);
expect(supportsTerminalProgress({ ConEmuANSI: 'ON' })).toBe(true);
});
it('detects Ghostty / WezTerm via TERM_PROGRAM and TERM', () => {
expect(supportsTerminalProgress({ TERM_PROGRAM: 'ghostty' })).toBe(true);
expect(supportsTerminalProgress({ TERM: 'xterm-ghostty' })).toBe(true);
expect(supportsTerminalProgress({ TERM_PROGRAM: 'WezTerm' })).toBe(true);
});
it('rejects terminals that show every OSC 9 payload as a notification', () => {
// iTerm2 treats any OSC 9 payload as a desktop notification, so the
// ConEmu-style 9;4 progress sequence must never be sent there.
expect(supportsTerminalProgress({ TERM_PROGRAM: 'iTerm.app' })).toBe(false);
expect(supportsTerminalProgress({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false);
expect(supportsTerminalProgress({ TERM_PROGRAM: 'WarpTerminal' })).toBe(false);
expect(supportsTerminalProgress({ TERM: 'xterm-kitty' })).toBe(false);
expect(supportsTerminalProgress({ TERM: 'xterm-256color' })).toBe(false);
expect(supportsTerminalProgress({ ConEmuANSI: 'OFF' })).toBe(false);
expect(supportsTerminalProgress({ WT_SESSION: '' })).toBe(false);
expect(supportsTerminalProgress({})).toBe(false);
});
});
describe('isInsideTmux', () => {
it('detects tmux via the TMUX env var', () => {
expect(isInsideTmux({ TMUX: '/private/tmp/tmux-501/default,1234,0' })).toBe(true);