Merge pull request #2718 from QwenLM/fix/terminal-response-leak-ssh

fix(cli): prevent terminal response leakage on high-latency SSH
This commit is contained in:
DennisYu07 2026-03-30 16:08:12 +08:00 committed by GitHub
commit 067430eef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 23 additions and 122 deletions

View file

@ -558,121 +558,4 @@ describe('AuthDialog', () => {
expect(handleAuthSelect).toHaveBeenCalledWith(undefined);
unmount();
});
it('shows API Key subtype menu and opens custom info', async () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
originalSettings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
originalSettings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { stdin, lastFrame, unmount } = renderAuthDialog(settings);
await wait();
// Move from Qwen OAuth -> Coding Plan -> API Key, then enter
stdin.write('\u001B[B');
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain('Select API Key Type');
expect(lastFrame()).toContain('Alibaba Cloud ModelStudio Standard API Key');
expect(lastFrame()).toContain('Custom API Key');
// Move to Custom API Key and enter
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain('Custom Configuration');
unmount();
});
it('shows Alibaba Cloud ModelStudio Standard API Key region endpoint', async () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
originalSettings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
originalSettings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { stdin, lastFrame, unmount } = renderAuthDialog(settings, {}, {});
await wait();
// Main -> API Key
stdin.write('\u001B[B');
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
// API Key type -> Alibaba Cloud ModelStudio Standard API Key (default)
stdin.write('\r');
await wait();
// Region -> Singapore
stdin.write('\u001B[B');
stdin.write('\r');
await wait();
expect(lastFrame()).toContain(
'Enter Alibaba Cloud ModelStudio Standard API Key',
);
expect(lastFrame()).toContain(
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
);
unmount();
});
});

View file

@ -540,7 +540,16 @@ export function KeypressProvider({
}
};
// Matches terminal query responses (DA1, DA2, Kitty protocol query)
// that may arrive late from startup detection in kittyProtocolDetector.
// These are never valid user input.
// eslint-disable-next-line no-control-regex
const TERMINAL_RESPONSE_RE = /^\x1b\[[?>][\d;]*[uc]$/;
const handleKeypress = async (_: unknown, key: Key) => {
if (TERMINAL_RESPONSE_RE.test(key.sequence)) {
return;
}
if (key.sequence === FOCUS_IN || key.sequence === FOCUS_OUT) {
return;
}

View file

@ -37,11 +37,20 @@ export async function detectAndEnableKittyProtocol(): Promise<boolean> {
const onTimeout = () => {
timeoutId = undefined;
process.stdin.removeListener('data', handleData);
if (!originalRawMode) {
process.stdin.setRawMode(false);
}
detectionComplete = true;
resolve(false);
// Keep a drain handler briefly to consume any late-arriving terminal
// responses that would otherwise leak into the application input.
const drainHandler = () => {};
process.stdin.on('data', drainHandler);
setTimeout(() => {
process.stdin.removeListener('data', drainHandler);
if (!originalRawMode) {
process.stdin.setRawMode(false);
}
detectionComplete = true;
resolve(false);
}, 100);
};
const handleData = (data: Buffer) => {