fix: terminal state restoration and abort handling regression

Fixes regression introduced in 1a23bfa430.

- Add restoreTerminalState helper to disable kitty keyboard protocol and
  CSI u key release reporting when switching modes or cleaning up
- Improve claudeLocal abort handling with SIGINT → SIGTERM → SIGKILL
  escalation timeouts for graceful process termination
- Fix useSwitchControls to properly handle CSI u space sequences (0x20)
  and key.name/key.sequence detection for space key presses
- Fix typo: exutFuture → exitFuture in claudeLocalLauncher
This commit is contained in:
weishu
2025-12-26 18:07:45 +08:00
parent 3adaa0f77c
commit b3499bb5ed
6 changed files with 158 additions and 8 deletions
+49
View File
@@ -6,6 +6,8 @@ import { useSwitchControls, type ConfirmationMode, type ActionInProgress } from
type Key = {
ctrl?: boolean;
name?: string;
sequence?: string;
};
type SwitchState = {
@@ -194,6 +196,53 @@ describe('useSwitchControls', () => {
expect(latestState?.actionInProgress).toBe(null);
});
it('accepts CSI u space sequences', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
await triggerInput('\u001b[32u', {});
expect(latestState?.confirmationMode).toBe('switch');
});
it('accepts CSI u space sequences with modifiers', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
await triggerInput('\u001b[32;2u', {});
expect(latestState?.confirmationMode).toBe('switch');
});
it('ignores CSI u key-release space sequences', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
await triggerInput(' ', {});
expect(latestState?.confirmationMode).toBe('switch');
await triggerInput('\u001b[32;2:3u', {});
expect(latestState?.confirmationMode).toBe('switch');
expect(onSwitch).not.toHaveBeenCalled();
});
it('accepts space via key name when input is empty', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
await triggerInput('', { name: 'space' });
expect(latestState?.confirmationMode).toBe('switch');
});
it('ignores key-release sequences from key.sequence', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
await triggerInput(' ', {});
expect(latestState?.confirmationMode).toBe('switch');
await triggerInput('', { sequence: '\u001b[1:3u' });
expect(latestState?.confirmationMode).toBe('switch');
});
it('does not switch on key-release space sequences', async () => {
const onSwitch = vi.fn();
await mount({ onSwitch });
+8 -2
View File
@@ -72,8 +72,14 @@ export function useSwitchControls(opts: {
return;
}
const isKeyRelease = /^\u001b\[[0-9;]*:3u$/.test(input);
const isSpace = Boolean(onSwitch) && !isKeyRelease && input === ' ';
const sequence = typeof key.sequence === 'string' ? key.sequence : input;
const isKeyRelease = typeof sequence === 'string' && /^\u001b\[[0-9;]*:3u$/.test(sequence);
const csiUMatch = typeof sequence === 'string'
? sequence.match(/^\u001b\[(\d+)(?:;(\d+))?u$/)
: null;
const csiUCodepoint = csiUMatch ? Number(csiUMatch[1]) : null;
const isCsiUSpace = csiUCodepoint === 32;
const isSpace = Boolean(onSwitch) && !isKeyRelease && (input === ' ' || key.name === 'space' || isCsiUSpace);
const hasPrintableInput = typeof input === 'string' && input.length > 0;
if (isSpace) {