@@ -214,92 +214,98 @@ if (cliArgs.version) {
214214 process . exit ( 0 ) ;
215215}
216216
217- // ── Reusable interactive model setup (shared by --model and --setup) ────────
218- // Returns true if the user completed setup (selected a model), false if they
219- // skipped (no changes made).
220- async function runInteractiveModelSetup (
221- settings : any ,
222- modelList : Array < { model : string [ ] ; base_url ?: string ; auth_token_env ?: string ; provider : string ; proxy ?: string | null ; price ?: any } > ,
223- settingsPath : string ,
217+ // ── radioSelect helper (shared by --model and --setup) ──────────────────────
218+ function radioSelect (
219+ options : string [ ] ,
220+ activeIndex : number ,
221+ title : string ,
224222 stdin : typeof process . stdin ,
225223 stdout : typeof process . stdout ,
226- writeFileSync : ( path : string , data : string ) => void ,
227- ) : Promise < boolean > {
228- // ── radioSelect helper ────────────────────────────────────────────────────
229- function radioSelect ( options : string [ ] , activeIndex : number , title : string ) : Promise < number > {
230- return new Promise ( ( resolve ) => {
231- if ( ! stdin . isTTY ) {
232- console . log ( `${ title } \n (non-TTY mode, using default)\n` ) ;
233- resolve ( activeIndex ) ;
234- return ;
235- }
224+ ) : Promise < number > {
225+ return new Promise ( ( resolve ) => {
226+ if ( ! stdin . isTTY ) {
227+ console . log ( `${ title } \n (non-TTY mode, using default)\n` ) ;
228+ resolve ( activeIndex ) ;
229+ return ;
230+ }
236231
237- let selected = activeIndex ;
238- let firstRender = true ;
239- const totalLines = options . length + 2 ;
240- const rawModeWas = stdin . isRaw ;
241- stdin . setRawMode ( true ) ;
242- stdin . resume ( ) ;
243- stdout . write ( '\x1B[?25l' ) ;
244-
245- function render ( ) {
246- if ( ! firstRender ) {
247- stdout . write ( `\x1B[${ totalLines } A\r` ) ;
248- }
249- firstRender = false ;
250-
251- stdout . write ( `\x1B[K${ title } \n` ) ;
252- options . forEach ( ( opt , i ) => {
253- const marker = i === selected ? '\x1B[1m●\x1B[0m' : '○' ;
254- stdout . write ( `\x1B[K ${ marker } ${ opt } \n` ) ;
255- } ) ;
256- stdout . write ( `\x1B[K\n\x1B[K \x1B[2m↑↓ to navigate, Enter to confirm\x1B[0m` ) ;
232+ let selected = activeIndex ;
233+ let firstRender = true ;
234+ const totalLines = options . length + 2 ;
235+ const rawModeWas = stdin . isRaw ;
236+ stdin . setRawMode ( true ) ;
237+ stdin . resume ( ) ;
238+ stdout . write ( '\x1B[?25l' ) ;
239+
240+ function render ( ) {
241+ if ( ! firstRender ) {
242+ stdout . write ( `\x1B[${ totalLines } A\r` ) ;
257243 }
244+ firstRender = false ;
245+
246+ stdout . write ( `\x1B[K${ title } \n` ) ;
247+ options . forEach ( ( opt , i ) => {
248+ const marker = i === selected ? '\x1B[1m●\x1B[0m' : '○' ;
249+ stdout . write ( `\x1B[K ${ marker } ${ opt } \n` ) ;
250+ } ) ;
251+ stdout . write ( `\x1B[K\n\x1B[K \x1B[2m↑↓ to navigate, Enter to confirm\x1B[0m` ) ;
252+ }
258253
259- render ( ) ;
254+ render ( ) ;
260255
261- function onData ( data : Buffer ) {
262- const key = data [ 0 ] ;
263- if ( key === 13 ) {
264- cleanup ( ) ;
265- resolve ( selected ) ;
266- return ;
267- }
268- if ( key === 3 ) {
269- cleanup ( ) ;
270- stdout . write ( '\n' ) ;
271- process . exit ( 0 ) ;
272- }
273- if ( key === 27 && data . length >= 3 ) {
274- if ( data [ 1 ] === 91 ) {
275- if ( data [ 2 ] === 65 ) {
276- selected = ( selected - 1 + options . length ) % options . length ;
277- render ( ) ;
278- } else if ( data [ 2 ] === 66 ) {
279- selected = ( selected + 1 ) % options . length ;
280- render ( ) ;
281- }
256+ function onData ( data : Buffer ) {
257+ const key = data [ 0 ] ;
258+ if ( key === 13 ) {
259+ cleanup ( ) ;
260+ resolve ( selected ) ;
261+ return ;
262+ }
263+ if ( key === 3 ) {
264+ cleanup ( ) ;
265+ stdout . write ( '\n' ) ;
266+ process . exit ( 0 ) ;
267+ }
268+ if ( key === 27 && data . length >= 3 ) {
269+ if ( data [ 1 ] === 91 ) {
270+ if ( data [ 2 ] === 65 ) {
271+ selected = ( selected - 1 + options . length ) % options . length ;
272+ render ( ) ;
273+ } else if ( data [ 2 ] === 66 ) {
274+ selected = ( selected + 1 ) % options . length ;
275+ render ( ) ;
282276 }
283277 }
284278 }
279+ }
285280
286- function cleanup ( ) {
287- stdin . setRawMode ( rawModeWas ) ;
288- stdin . pause ( ) ;
289- stdin . removeListener ( 'data' , onData ) ;
290- stdout . write ( '\x1B[?25h\n' ) ;
291- }
281+ function cleanup ( ) {
282+ stdin . setRawMode ( rawModeWas ) ;
283+ stdin . pause ( ) ;
284+ stdin . removeListener ( 'data' , onData ) ;
285+ stdout . write ( '\x1B[?25h\n' ) ;
286+ }
292287
293- stdin . on ( 'data' , onData ) ;
294- } ) ;
295- }
288+ stdin . on ( 'data' , onData ) ;
289+ } ) ;
290+ }
296291
297- // ── Step 1: Provider selection ────────────────────────────────────────────
292+ // ── Reusable interactive model setup (shared by --model and --setup) ────────
293+ // Returns true if the user completed setup (selected a model), false if they
294+ // skipped (no changes made).
295+ async function runInteractiveModelSetup (
296+ settings : any ,
297+ modelList : Array < { model : string [ ] ; base_url ?: string ; auth_token_env ?: string ; provider : string ; proxy ?: string | null ; price ?: any } > ,
298+ settingsPath : string ,
299+ stdin : typeof process . stdin ,
300+ stdout : typeof process . stdout ,
301+ writeFileSync : ( path : string , data : string ) => void ,
302+ ) : Promise < boolean > {
298303 const defaultModel = settings . default_model ?? '' ;
299304 const defaultProvider = defaultModel ? defaultModel . split ( '/' ) [ 0 ] : 'deepseek' ;
300305 let selectedProvider : any ;
301306 let providerDone = false ;
302307 while ( ! providerDone ) {
308+
303309 let providerActiveIdx = modelList . findIndex ( m => m . provider === defaultProvider ) ;
304310 if ( providerActiveIdx < 0 ) providerActiveIdx = 0 ;
305311
@@ -316,6 +322,8 @@ async function runInteractiveModelSetup(
316322 providerOptions ,
317323 providerActiveIdx ,
318324 'Available providers:' ,
325+ stdin ,
326+ stdout ,
319327 ) ;
320328
321329 if ( selectedProviderIdx === modelList . length ) {
@@ -342,6 +350,8 @@ async function runInteractiveModelSetup(
342350 removeProviderOptions ,
343351 0 ,
344352 'Select provider to remove:' ,
353+ stdin ,
354+ stdout ,
345355 ) ;
346356 const targetProvider = modelList [ removeIdx ] ! . provider ;
347357 const readline = await import ( 'node:readline' ) ;
@@ -458,6 +468,8 @@ async function runInteractiveModelSetup(
458468 modelOptions ,
459469 modelActiveIdx ,
460470 `Available models for ${ selectedProvider . provider } :` ,
471+ stdin ,
472+ stdout ,
461473 ) ;
462474
463475 if ( selectedModelIdx === selectedProvider . model . length ) {
@@ -474,6 +486,8 @@ async function runInteractiveModelSetup(
474486 removeModelOptions ,
475487 0 ,
476488 `Select model to remove from ${ selectedProvider . provider } :` ,
489+ stdin ,
490+ stdout ,
477491 ) ;
478492 const modelToRemove = selectedProvider . model [ removeIdx ] ! ;
479493 const readline = await import ( 'node:readline' ) ;
@@ -627,18 +641,18 @@ if (cliArgs.setup || process.argv.includes('setup')) {
627641
628642 // ── Step 1: Theme ──────────────────────────────────────────────────────────
629643 {
630- const rl = readline . createInterface ( { input : stdin , output : stdout } ) ;
631- const theme = await new Promise < string > ( resolve => {
632- rl . question ( 'Theme ( dark / light ) [ dark ] : ', answer => {
633- const trimmed = answer . trim ( ) . toLowerCase ( ) ;
634- resolve ( trimmed === 'light ' ? 'light ' : 'dark ') ;
635- } ) ;
636- } ) ;
644+ const themeIdx = await radioSelect (
645+ [ 'dark' , 'light' ] ,
646+ 0 ,
647+ 'Choose theme:' ,
648+ stdin ,
649+ stdout ,
650+ ) ;
651+ const theme = themeIdx === 1 ? 'light' : 'dark' ;
637652 settings . theme = theme ;
638653 // Apply immediately for the current TUI session
639654 process . env . CODER_TUI_THEME = theme ;
640655 console . log ( ` Theme: ${ theme } \n` ) ;
641- rl . close ( ) ;
642656 }
643657
644658 // ── Step 2: max_tokens ─────────────────────────────────────────────────────
0 commit comments