Skip to content

Commit 655766e

Browse files
tjdoomerclaude
andcommitted
Fix Kitty protocol handling in auth dialog and credential input
Replace ink's useInput with our custom useKeypress in OpenAIKeyPrompt so Kitty protocol sequences are properly decoded to characters instead of being stripped by CSI regex filtering. Also deactivate AuthDialog's useKeypress when the credential prompt is shown to prevent two readline interfaces from conflicting on stdin. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c455adc commit 655766e

2 files changed

Lines changed: 98 additions & 90 deletions

File tree

packages/cli/src/ui/components/AuthDialog.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { LoadedSettings, SettingScope } from '../../config/settings.js';
2121
import { Colors } from '../colors.js';
2222
import { useKeypress } from '../hooks/useKeypress.js';
23+
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
2324
import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js';
2425
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
2526

@@ -41,6 +42,7 @@ export function AuthDialog({
4142
const [providerMode, setProviderMode] = useState<
4243
'openai' | 'google' | 'azure' | 'bedrock' | 'claude'
4344
>('openai');
45+
const kittyProtocolStatus = useKittyKeyboardProtocol();
4446

4547
// Two options: OpenAI-compatible or Anthropic via OpenAI-compatible provider
4648
type ProviderChoice = {
@@ -127,10 +129,6 @@ export function AuthDialog({
127129

128130
useKeypress(
129131
(key) => {
130-
if (showOpenAIKeyPrompt) {
131-
return;
132-
}
133-
134132
if (key.name === 'escape') {
135133
// Prevent exit if there is an error message.
136134
// This means they user is not authenticated yet.
@@ -147,7 +145,10 @@ export function AuthDialog({
147145
onSelect(undefined, SettingScope.User);
148146
}
149147
},
150-
{ isActive: true },
148+
{
149+
isActive: !showOpenAIKeyPrompt,
150+
kittyProtocolEnabled: kittyProtocolStatus.enabled,
151+
},
151152
);
152153

153154
if (showOpenAIKeyPrompt) {

packages/cli/src/ui/components/OpenAIKeyPrompt.tsx

Lines changed: 92 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
*/
66

77
import React, { useState } from 'react';
8-
import { Box, Text, useInput } from 'ink';
8+
import { Box, Text } from 'ink';
99
import { Colors } from '../colors.js';
1010
import { ClaudeModelSelector, CLAUDE_MODELS, ClaudeModel } from './ClaudeModelSelector.js';
11+
import { useKeypress, Key } from '../hooks/useKeypress.js';
12+
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
1113

1214
interface OpenAIKeyPromptProps {
1315
onSubmit: (apiKey: string, baseUrl: string, model: string) => void;
@@ -36,6 +38,7 @@ export function OpenAIKeyPrompt({
3638
'apiKey' | 'baseUrl' | 'model'
3739
>('apiKey');
3840
const [showModelSelector, setShowModelSelector] = useState(false);
41+
const kittyProtocolStatus = useKittyKeyboardProtocol();
3942

4043
const handleModelSelect = (selectedModel: ClaudeModel) => {
4144
setModel(selectedModel.id);
@@ -53,109 +56,113 @@ export function OpenAIKeyPrompt({
5356
// Stay on model field
5457
};
5558

56-
useInput((input, key) => {
57-
// Filter paste-related control sequences
58-
let cleanInput = (input || '')
59-
// Filter ESC-led control sequences (e.g., \u001b[200~, \u001b[201~)
60-
.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex
61-
// Filter paste start marker [200~
62-
.replace(/\[200~/g, '')
63-
// Filter paste end marker [201~
64-
.replace(/\[201~/g, '')
65-
// Filter stray '[' and '~' characters (leftover paste markers)
66-
.replace(/^\[|~$/g, '');
59+
const insertText = (text: string) => {
60+
if (currentField === 'apiKey') {
61+
setApiKey((prev) => prev + text);
62+
} else if (currentField === 'baseUrl') {
63+
setBaseUrl((prev) => prev + text);
64+
} else if (currentField === 'model') {
65+
setModel((prev) => prev + text);
66+
}
67+
};
6768

68-
// Then filter all non-printable ASCII (< 32), except carriage return/newline
69-
cleanInput = cleanInput
70-
.split('')
71-
.filter((ch) => ch.charCodeAt(0) >= 32)
72-
.join('');
69+
useKeypress(
70+
(key: Key) => {
71+
if (showModelSelector) return;
7372

74-
if (cleanInput.length > 0) {
75-
if (currentField === 'apiKey') {
76-
setApiKey((prev) => prev + cleanInput);
77-
} else if (currentField === 'baseUrl') {
78-
setBaseUrl((prev) => prev + cleanInput);
79-
} else if (currentField === 'model') {
80-
setModel((prev) => prev + cleanInput);
73+
// Handle paste
74+
if (key.paste) {
75+
const text = key.sequence
76+
.split('')
77+
.filter((ch) => ch.charCodeAt(0) >= 32)
78+
.join('');
79+
if (text) insertText(text);
80+
return;
8181
}
82-
return;
83-
}
8482

85-
// Check for Enter (by detecting newline characters)
86-
if (input.includes('\n') || input.includes('\r')) {
87-
if (currentField === 'apiKey') {
88-
// Allow empty API key to advance; user can return later to edit
89-
setCurrentField('baseUrl');
90-
return;
91-
} else if (currentField === 'baseUrl') {
92-
setCurrentField('model');
93-
return;
94-
} else if (currentField === 'model') {
95-
if (mode === 'claude') {
96-
// Show model selector for Claude
97-
setShowModelSelector(true);
98-
} else {
99-
// Validate API key only on final submit
100-
if (apiKey.trim()) {
83+
// Handle Enter
84+
if (key.name === 'return') {
85+
if (currentField === 'apiKey') {
86+
setCurrentField('baseUrl');
87+
} else if (currentField === 'baseUrl') {
88+
setCurrentField('model');
89+
} else if (currentField === 'model') {
90+
if (mode === 'claude') {
91+
setShowModelSelector(true);
92+
} else if (apiKey.trim()) {
10193
onSubmit(apiKey.trim(), baseUrl.trim(), model.trim());
10294
} else {
103-
// If API key is empty, return focus to the API key field
10495
setCurrentField('apiKey');
10596
}
10697
}
98+
return;
10799
}
108-
return;
109-
}
110100

111-
if (key.escape) {
112-
onCancel();
113-
return;
114-
}
101+
if (key.name === 'escape') {
102+
onCancel();
103+
return;
104+
}
115105

116-
// Handle Tab key for field navigation
117-
if (key.tab) {
118-
if (currentField === 'apiKey') {
119-
setCurrentField('baseUrl');
120-
} else if (currentField === 'baseUrl') {
121-
setCurrentField('model');
122-
} else if (currentField === 'model') {
123-
setCurrentField('apiKey');
106+
// Handle Tab key for field navigation
107+
if (key.name === 'tab') {
108+
if (currentField === 'apiKey') {
109+
setCurrentField('baseUrl');
110+
} else if (currentField === 'baseUrl') {
111+
setCurrentField('model');
112+
} else if (currentField === 'model') {
113+
setCurrentField('apiKey');
114+
}
115+
return;
124116
}
125-
return;
126-
}
127117

128-
// Handle arrow keys for field navigation
129-
if (key.upArrow) {
130-
if (currentField === 'baseUrl') {
131-
setCurrentField('apiKey');
132-
} else if (currentField === 'model') {
133-
setCurrentField('baseUrl');
118+
// Handle arrow keys for field navigation
119+
if (key.name === 'up') {
120+
if (currentField === 'baseUrl') {
121+
setCurrentField('apiKey');
122+
} else if (currentField === 'model') {
123+
setCurrentField('baseUrl');
124+
}
125+
return;
134126
}
135-
return;
136-
}
137127

138-
if (key.downArrow) {
139-
if (currentField === 'apiKey') {
140-
setCurrentField('baseUrl');
141-
} else if (currentField === 'baseUrl') {
142-
setCurrentField('model');
128+
if (key.name === 'down') {
129+
if (currentField === 'apiKey') {
130+
setCurrentField('baseUrl');
131+
} else if (currentField === 'baseUrl') {
132+
setCurrentField('model');
133+
}
134+
return;
143135
}
144-
return;
145-
}
146136

147-
// Handle backspace - check both key.backspace and delete key
148-
if (key.backspace || key.delete) {
149-
if (currentField === 'apiKey') {
150-
setApiKey((prev) => prev.slice(0, -1));
151-
} else if (currentField === 'baseUrl') {
152-
setBaseUrl((prev) => prev.slice(0, -1));
153-
} else if (currentField === 'model') {
154-
setModel((prev) => prev.slice(0, -1));
137+
// Handle backspace and delete
138+
if (key.name === 'backspace' || key.name === 'delete') {
139+
if (currentField === 'apiKey') {
140+
setApiKey((prev) => prev.slice(0, -1));
141+
} else if (currentField === 'baseUrl') {
142+
setBaseUrl((prev) => prev.slice(0, -1));
143+
} else if (currentField === 'model') {
144+
setModel((prev) => prev.slice(0, -1));
145+
}
146+
return;
147+
}
148+
149+
// Skip control/meta combos
150+
if (key.ctrl || key.meta) return;
151+
152+
// Insert printable characters
153+
if (key.sequence) {
154+
const printable = key.sequence
155+
.split('')
156+
.filter((ch) => ch.charCodeAt(0) >= 32)
157+
.join('');
158+
if (printable) insertText(printable);
155159
}
156-
return;
157-
}
158-
});
160+
},
161+
{
162+
isActive: !showModelSelector,
163+
kittyProtocolEnabled: kittyProtocolStatus.enabled,
164+
},
165+
);
159166

160167
// Show model selector for Claude mode
161168
if (showModelSelector && mode === 'claude') {

0 commit comments

Comments
 (0)