Skip to content

Commit c0d1404

Browse files
jimgqyuDeepSeek
andcommitted
feat: use radioSelect (●/○) for theme picker in coder setup
Replace the text-input theme prompt with the arrow-key radio button selector, matching the --model interactive flow. Also extract radioSelect as a standalone function shared by both --model and --setup handlers. Co-Authored-By: DeepSeek <noreply@deepseek.com>
1 parent 20eecfa commit c0d1404

1 file changed

Lines changed: 91 additions & 77 deletions

File tree

packages/cli/src/entry.tsx

Lines changed: 91 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)