Skip to content

Commit 7f0dde2

Browse files
authored
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.
1 parent 8d251f8 commit 7f0dde2

6 files changed

Lines changed: 78 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Fix endless desktop notifications in iTerm2 by only sending terminal progress sequences to terminals that support them.

apps/kimi-code/src/tui/kimi-tui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,7 @@ export class KimiTUI {
17221722
}
17231723

17241724
private syncTerminalProgress(active: boolean): void {
1725+
if (!this.state.terminalState.supportsProgress) return;
17251726
if (this.state.terminalState.progressActive === active) return;
17261727
this.state.terminal.setProgress(active);
17271728
this.state.terminalState.progressActive = active;

apps/kimi-code/src/tui/utils/terminal-notification.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ export function supportsOsc9Notification(env: NodeJS.ProcessEnv = process.env):
110110
return false;
111111
}
112112

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

apps/kimi-code/src/tui/utils/terminal-state.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { isInsideTmux, supportsOsc9Notification } from './terminal-notification';
1+
import {
2+
isInsideTmux,
3+
supportsOsc9Notification,
4+
supportsTerminalProgress,
5+
} from './terminal-notification';
26

37
export interface TerminalState {
48
notificationKeys: Set<string>;
59
focused: boolean;
610
supportsOsc9: boolean;
11+
supportsProgress: boolean;
712
insideTmux: boolean;
813
progressActive: boolean;
914
}
@@ -13,6 +18,7 @@ export function createTerminalState(): TerminalState {
1318
notificationKeys: new Set<string>(),
1419
focused: true,
1520
supportsOsc9: supportsOsc9Notification(),
21+
supportsProgress: supportsTerminalProgress(),
1622
insideTmux: isInsideTmux(),
1723
progressActive: false,
1824
};

apps/kimi-code/test/tui/activity-pane.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function makeDriverWithTerminalProgress(): {
4747
const driver = new KimiTUI({} as never, makeStartupInput()) as unknown as ActivityDriver;
4848
vi.spyOn(driver.state.ui, 'requestRender').mockImplementation(() => {});
4949
driver.state.terminal = { columns: 80, setProgress } as unknown as TUIState['terminal'];
50+
driver.state.terminalState.supportsProgress = true;
5051
return { driver, state: driver.state, setProgress };
5152
}
5253

@@ -100,6 +101,24 @@ describe('updateActivityPane terminal progress', () => {
100101
}
101102
});
102103

104+
it('never emits terminal progress when the terminal does not support OSC 9;4', () => {
105+
vi.useFakeTimers();
106+
try {
107+
const { driver, state, setProgress } = makeDriverWithTerminalProgress();
108+
state.terminalState.supportsProgress = false;
109+
110+
state.livePane = { ...state.livePane, mode: 'waiting' };
111+
driver.updateActivityPane();
112+
state.livePane = { ...state.livePane, mode: 'idle' };
113+
driver.updateActivityPane();
114+
115+
expect(setProgress).not.toHaveBeenCalled();
116+
expect(state.terminalState.progressActive).toBe(false);
117+
} finally {
118+
vi.useRealTimers();
119+
}
120+
});
121+
103122
it('keeps compaction visible as terminal progress even though the pane is hidden', () => {
104123
const { driver, state, setProgress } = makeDriverWithTerminalProgress();
105124
state.appState.isCompacting = true;

apps/kimi-code/test/tui/terminal-notification.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isInsideTmux,
99
notifyTerminalOnce,
1010
supportsOsc9Notification,
11+
supportsTerminalProgress,
1112
} from '#/tui/utils/terminal-notification';
1213

1314
function makeNotificationState(args: {
@@ -215,6 +216,32 @@ describe('supportsOsc9Notification', () => {
215216
});
216217
});
217218

219+
describe('supportsTerminalProgress', () => {
220+
it('detects Windows Terminal / ConEmu via env flags', () => {
221+
expect(supportsTerminalProgress({ WT_SESSION: 'abc-123' })).toBe(true);
222+
expect(supportsTerminalProgress({ ConEmuANSI: 'ON' })).toBe(true);
223+
});
224+
225+
it('detects Ghostty / WezTerm via TERM_PROGRAM and TERM', () => {
226+
expect(supportsTerminalProgress({ TERM_PROGRAM: 'ghostty' })).toBe(true);
227+
expect(supportsTerminalProgress({ TERM: 'xterm-ghostty' })).toBe(true);
228+
expect(supportsTerminalProgress({ TERM_PROGRAM: 'WezTerm' })).toBe(true);
229+
});
230+
231+
it('rejects terminals that show every OSC 9 payload as a notification', () => {
232+
// iTerm2 treats any OSC 9 payload as a desktop notification, so the
233+
// ConEmu-style 9;4 progress sequence must never be sent there.
234+
expect(supportsTerminalProgress({ TERM_PROGRAM: 'iTerm.app' })).toBe(false);
235+
expect(supportsTerminalProgress({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false);
236+
expect(supportsTerminalProgress({ TERM_PROGRAM: 'WarpTerminal' })).toBe(false);
237+
expect(supportsTerminalProgress({ TERM: 'xterm-kitty' })).toBe(false);
238+
expect(supportsTerminalProgress({ TERM: 'xterm-256color' })).toBe(false);
239+
expect(supportsTerminalProgress({ ConEmuANSI: 'OFF' })).toBe(false);
240+
expect(supportsTerminalProgress({ WT_SESSION: '' })).toBe(false);
241+
expect(supportsTerminalProgress({})).toBe(false);
242+
});
243+
});
244+
218245
describe('isInsideTmux', () => {
219246
it('detects tmux via the TMUX env var', () => {
220247
expect(isInsideTmux({ TMUX: '/private/tmp/tmux-501/default,1234,0' })).toBe(true);

0 commit comments

Comments
 (0)