From 7f0dde2ece3f9a004e934d69258dfd47c954043c Mon Sep 17 00:00:00 2001 From: liruifengv Date: Fri, 12 Jun 2026 16:49:00 +0800 Subject: [PATCH] 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. --- .changeset/iterm2-progress-notifications.md | 5 ++++ apps/kimi-code/src/tui/kimi-tui.ts | 1 + .../src/tui/utils/terminal-notification.ts | 19 +++++++++++++ .../kimi-code/src/tui/utils/terminal-state.ts | 8 +++++- apps/kimi-code/test/tui/activity-pane.test.ts | 19 +++++++++++++ .../test/tui/terminal-notification.test.ts | 27 +++++++++++++++++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .changeset/iterm2-progress-notifications.md diff --git a/.changeset/iterm2-progress-notifications.md b/.changeset/iterm2-progress-notifications.md new file mode 100644 index 000000000..473a415db --- /dev/null +++ b/.changeset/iterm2-progress-notifications.md @@ -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. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 569e68070..0551b2a64 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -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; diff --git a/apps/kimi-code/src/tui/utils/terminal-notification.ts b/apps/kimi-code/src/tui/utils/terminal-notification.ts index 2a0247cc7..ab6f1bff7 100644 --- a/apps/kimi-code/src/tui/utils/terminal-notification.ts +++ b/apps/kimi-code/src/tui/utils/terminal-notification.ts @@ -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;` 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; diff --git a/apps/kimi-code/src/tui/utils/terminal-state.ts b/apps/kimi-code/src/tui/utils/terminal-state.ts index 86d2bab9f..29127ea02 100644 --- a/apps/kimi-code/src/tui/utils/terminal-state.ts +++ b/apps/kimi-code/src/tui/utils/terminal-state.ts @@ -1,9 +1,14 @@ -import { isInsideTmux, supportsOsc9Notification } from './terminal-notification'; +import { + isInsideTmux, + supportsOsc9Notification, + supportsTerminalProgress, +} from './terminal-notification'; export interface TerminalState { notificationKeys: Set; focused: boolean; supportsOsc9: boolean; + supportsProgress: boolean; insideTmux: boolean; progressActive: boolean; } @@ -13,6 +18,7 @@ export function createTerminalState(): TerminalState { notificationKeys: new Set(), focused: true, supportsOsc9: supportsOsc9Notification(), + supportsProgress: supportsTerminalProgress(), insideTmux: isInsideTmux(), progressActive: false, }; diff --git a/apps/kimi-code/test/tui/activity-pane.test.ts b/apps/kimi-code/test/tui/activity-pane.test.ts index 2b12a76ee..b719da163 100644 --- a/apps/kimi-code/test/tui/activity-pane.test.ts +++ b/apps/kimi-code/test/tui/activity-pane.test.ts @@ -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; diff --git a/apps/kimi-code/test/tui/terminal-notification.test.ts b/apps/kimi-code/test/tui/terminal-notification.test.ts index 2d5d904ee..cef7b8086 100644 --- a/apps/kimi-code/test/tui/terminal-notification.test.ts +++ b/apps/kimi-code/test/tui/terminal-notification.test.ts @@ -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);