Skip to content

Commit 35ea874

Browse files
committed
feat: enhance update prompt UI
1 parent 4d2453e commit 35ea874

3 files changed

Lines changed: 115 additions & 26 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ui/UpdatePrompt.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { useState } from "react";
2+
import { Box, Text, useApp, useInput } from "ink";
3+
4+
export type UpdatePromptChoice = "install" | "ignore-once" | "ignore-version";
5+
6+
type UpdatePromptOption = {
7+
value: UpdatePromptChoice;
8+
label: string;
9+
};
10+
11+
type Props = {
12+
currentVersion: string;
13+
latestVersion: string;
14+
installCommand: string;
15+
onSelect: (choice: UpdatePromptChoice) => void;
16+
};
17+
18+
export function UpdatePrompt({
19+
currentVersion,
20+
latestVersion,
21+
installCommand,
22+
onSelect
23+
}: Props): React.ReactElement {
24+
const { exit } = useApp();
25+
const [selectedIndex, setSelectedIndex] = useState(0);
26+
const options: UpdatePromptOption[] = [
27+
{
28+
value: "install",
29+
label: `Install the latest version with \`${installCommand}\``
30+
},
31+
{
32+
value: "ignore-once",
33+
label: "Ignore once"
34+
},
35+
{
36+
value: "ignore-version",
37+
label: `Ignore this version (${latestVersion})`
38+
}
39+
];
40+
41+
useInput((input, key) => {
42+
if (key.upArrow) {
43+
setSelectedIndex((index) => (index - 1 + options.length) % options.length);
44+
return;
45+
}
46+
if (key.downArrow || key.tab) {
47+
setSelectedIndex((index) => (index + 1) % options.length);
48+
return;
49+
}
50+
if (key.return) {
51+
onSelect(options[selectedIndex]?.value ?? "ignore-once");
52+
exit();
53+
return;
54+
}
55+
if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
56+
onSelect("ignore-once");
57+
exit();
58+
return;
59+
}
60+
if (/^[1-3]$/.test(input)) {
61+
onSelect(options[Number(input) - 1]?.value ?? "ignore-once");
62+
exit();
63+
}
64+
});
65+
66+
return (
67+
<Box flexDirection="column" marginY={1}>
68+
<Text bold>
69+
Deep Code latest version has been released: {currentVersion} -&gt; {latestVersion}
70+
</Text>
71+
<Box flexDirection="column" marginTop={1}>
72+
{options.map((option, index) => {
73+
const selected = index === selectedIndex;
74+
return (
75+
<Text key={option.value} color={selected ? "green" : undefined}>
76+
{selected ? "> " : " "}
77+
{index + 1}. {option.label}
78+
</Text>
79+
);
80+
})}
81+
</Box>
82+
<Box marginTop={1}>
83+
<Text dimColor>Use Up/Down to choose, Enter to confirm, Esc to ignore once.</Text>
84+
</Box>
85+
</Box>
86+
);
87+
}

src/updateCheck.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { spawn } from "child_process";
2+
import React from "react";
23
import * as fs from "fs";
34
import * as os from "os";
45
import * as path from "path";
5-
import * as readline from "readline/promises";
6+
import { render, type Instance } from "ink";
7+
import chalk from "chalk";
8+
import { UpdatePrompt, type UpdatePromptChoice } from "./ui/UpdatePrompt";
69

710
export type PackageInfo = {
811
name: string;
@@ -52,7 +55,7 @@ export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise<
5255
const ok = await runNpmInstallGlobal(installSpec);
5356
if (ok) {
5457
writeUpdateState({ ...state, pending: null });
55-
process.stdout.write("Deep Code has been updated. Please restart the CLI to use the new version.\n");
58+
process.stdout.write(`\n${chalk.red("Deep Code has been updated. Please restart the CLI to use the new version.")}\n\n`);
5659
}
5760
return { installed: ok };
5861
}
@@ -129,29 +132,28 @@ async function promptUpdateChoice({
129132
latestVersion: string;
130133
installCommand: string;
131134
}): Promise<"install" | "ignore-once" | "ignore-version"> {
132-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133-
try {
134-
process.stdout.write(`Deep Code latest version has been released: ${currentVersion} -> ${latestVersion}\n`);
135-
process.stdout.write(`1. Install the latest version with \`${installCommand}\`\n`);
136-
process.stdout.write("2. Ignore once\n");
137-
process.stdout.write(`3. Ignore this version (${latestVersion})\n`);
138-
139-
while (true) {
140-
const answer = (await rl.question("Choose 1, 2, or 3: ")).trim();
141-
if (answer === "1") {
142-
return "install";
143-
}
144-
if (answer === "2" || answer === "") {
145-
return "ignore-once";
146-
}
147-
if (answer === "3") {
148-
return "ignore-version";
135+
return new Promise<UpdatePromptChoice>((resolve) => {
136+
let selected = false;
137+
let instance: Instance | null = null;
138+
const handleSelect = (choice: UpdatePromptChoice): void => {
139+
if (selected) {
140+
return;
149141
}
150-
process.stdout.write("Please enter 1, 2, or 3.\n");
151-
}
152-
} finally {
153-
rl.close();
154-
}
142+
selected = true;
143+
resolve(choice);
144+
instance?.unmount();
145+
};
146+
147+
instance = render(
148+
React.createElement(UpdatePrompt, {
149+
currentVersion,
150+
latestVersion,
151+
installCommand,
152+
onSelect: handleSelect
153+
}),
154+
{ exitOnCtrlC: false }
155+
);
156+
});
155157
}
156158

157159
async function runNpmInstallGlobal(installSpec: string): Promise<boolean> {

0 commit comments

Comments
 (0)