mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
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:
parent
8d251f8ab4
commit
7f0dde2ece
6 changed files with 78 additions and 1 deletions
5
.changeset/iterm2-progress-notifications.md
Normal file
5
.changeset/iterm2-progress-notifications.md
Normal 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.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue