mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 07:10:55 +00:00
merge: resolve conflict in client.ts hook condition
Keep both changes: SendMessageType.Cron skip from our branch and hasHooksForEvent check from main.
This commit is contained in:
commit
9a8829c5a5
38 changed files with 1009 additions and 209 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -140,6 +140,71 @@ describe('clearCommand', () => {
|
|||
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should clear UI before resetChat for immediate responsiveness', async () => {
|
||||
if (!clearCommand.action) {
|
||||
throw new Error('clearCommand must have an action.');
|
||||
}
|
||||
|
||||
const callOrder: string[] = [];
|
||||
(mockContext.ui.clear as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
() => {
|
||||
callOrder.push('ui.clear');
|
||||
},
|
||||
);
|
||||
mockResetChat.mockImplementation(async () => {
|
||||
callOrder.push('resetChat');
|
||||
});
|
||||
|
||||
await clearCommand.action(mockContext, '');
|
||||
|
||||
// ui.clear should be called before resetChat for immediate UI feedback
|
||||
const clearIndex = callOrder.indexOf('ui.clear');
|
||||
const resetIndex = callOrder.indexOf('resetChat');
|
||||
expect(clearIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(resetIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(clearIndex).toBeLessThan(resetIndex);
|
||||
});
|
||||
|
||||
it('should not await hook events (fire-and-forget)', async () => {
|
||||
if (!clearCommand.action) {
|
||||
throw new Error('clearCommand must have an action.');
|
||||
}
|
||||
|
||||
// Make hooks take a long time - they should not block
|
||||
let sessionEndResolved = false;
|
||||
let sessionStartResolved = false;
|
||||
mockFireSessionEndEvent.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
sessionEndResolved = true;
|
||||
resolve(undefined);
|
||||
}, 5000);
|
||||
}),
|
||||
);
|
||||
mockFireSessionStartEvent.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
sessionStartResolved = true;
|
||||
resolve(undefined);
|
||||
}, 5000);
|
||||
}),
|
||||
);
|
||||
|
||||
await clearCommand.action(mockContext, '');
|
||||
|
||||
// The action should complete immediately without waiting for hooks
|
||||
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetChat).toHaveBeenCalledTimes(1);
|
||||
// Hooks should have been called but not necessarily resolved
|
||||
expect(mockFireSessionEndEvent).toHaveBeenCalled();
|
||||
expect(mockFireSessionStartEvent).toHaveBeenCalled();
|
||||
// Hooks should NOT have resolved yet since they have 5s timeouts
|
||||
expect(sessionEndResolved).toBe(false);
|
||||
expect(sessionStartResolved).toBe(false);
|
||||
});
|
||||
|
||||
it('should not attempt to reset chat if config service is not available', async () => {
|
||||
if (!clearCommand.action) {
|
||||
throw new Error('clearCommand must have an action.');
|
||||
|
|
|
|||
|
|
@ -27,14 +27,13 @@ export const clearCommand: SlashCommand = {
|
|||
const { config } = context.services;
|
||||
|
||||
if (config) {
|
||||
// Fire SessionEnd event before clearing (current session ends)
|
||||
try {
|
||||
await config
|
||||
.getHookSystem()
|
||||
?.fireSessionEndEvent(SessionEndReason.Clear);
|
||||
} catch (err) {
|
||||
config.getDebugLogger().warn(`SessionEnd hook failed: ${err}`);
|
||||
}
|
||||
// Fire SessionEnd event (non-blocking to avoid UI lag)
|
||||
config
|
||||
.getHookSystem()
|
||||
?.fireSessionEndEvent(SessionEndReason.Clear)
|
||||
.catch((err) => {
|
||||
config.getDebugLogger().warn(`SessionEnd hook failed: ${err}`);
|
||||
});
|
||||
|
||||
const newSessionId = config.startNewSession();
|
||||
|
||||
|
|
@ -54,6 +53,9 @@ export const clearCommand: SlashCommand = {
|
|||
context.session.startNewSession(newSessionId);
|
||||
}
|
||||
|
||||
// Clear UI first for immediate responsiveness
|
||||
context.ui.clear();
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
if (geminiClient) {
|
||||
context.ui.setDebugMessage(
|
||||
|
|
@ -66,22 +68,20 @@ export const clearCommand: SlashCommand = {
|
|||
context.ui.setDebugMessage(t('Starting a new session and clearing.'));
|
||||
}
|
||||
|
||||
// Fire SessionStart event after clearing (new session starts)
|
||||
try {
|
||||
await config
|
||||
.getHookSystem()
|
||||
?.fireSessionStartEvent(
|
||||
SessionStartSource.Clear,
|
||||
config.getModel() ?? '',
|
||||
String(config.getApprovalMode()) as PermissionMode,
|
||||
);
|
||||
} catch (err) {
|
||||
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
||||
}
|
||||
// Fire SessionStart event (non-blocking to avoid UI lag)
|
||||
config
|
||||
.getHookSystem()
|
||||
?.fireSessionStartEvent(
|
||||
SessionStartSource.Clear,
|
||||
config.getModel() ?? '',
|
||||
String(config.getApprovalMode()) as PermissionMode,
|
||||
)
|
||||
.catch((err) => {
|
||||
config.getDebugLogger().warn(`SessionStart hook failed: ${err}`);
|
||||
});
|
||||
} else {
|
||||
context.ui.setDebugMessage(t('Starting a new session and clearing.'));
|
||||
context.ui.clear();
|
||||
}
|
||||
|
||||
context.ui.clear();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue