55 */
66
77import React , { useState } from 'react' ;
8- import { Box , Text , useInput } from 'ink' ;
8+ import { Box , Text } from 'ink' ;
99import { Colors } from '../colors.js' ;
1010import { ClaudeModelSelector , CLAUDE_MODELS , ClaudeModel } from './ClaudeModelSelector.js' ;
11+ import { useKeypress , Key } from '../hooks/useKeypress.js' ;
12+ import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js' ;
1113
1214interface 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 - z A - Z ] / g, '' ) // eslint-disable-line no-control-regex
61- // Filter paste start marker [200~
62- . replace ( / \[ 2 0 0 ~ / g, '' )
63- // Filter paste end marker [201~
64- . replace ( / \[ 2 0 1 ~ / 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