In Chapter 1: Command Registration & Gating, we built the "Front Door" to our application. We learned how to register a command and guard it with security policies.
Now that the user has successfully entered, what do they see?
Terminals can be scary places. Usually, when you run a command, you stare at a blinking cursor, hoping something is happening. For a complex setup process, this "silent treatment" is bad user experience.
We want to build a Guided Wizard that:
To do this in a terminal, we use React. Yes, the same tool used for websites can render text-based UIs in your terminal!
Think of our setup process like a traffic light. It can only be in one "state" at a time.
We use a library called Ink. It allows us to use React components like <Text>, <Box>, and hooks like useState. Instead of rendering HTML <div>s, it renders text lines in the console.
We are working in remote-setup.tsx. Let's build this step-by-step.
First, we define the possible phases of our journey. We also need a place to store data (like the GitHub token) once we find it.
type Step =
| { name: 'checking' }
| { name: 'confirm'; token: RedactedGithubToken }
| { name: 'uploading' };
// Inside our Component:
const [step, setStep] = useState<Step>({ name: 'checking' });
useState to remember which "screen" we are currently showing. We start at 'checking'.
When the command starts, we immediately want to check if the user is logged in. We use useEffect for this. This acts like a generic "start up" function.
useEffect(() => {
// Start the check immediately
checkLoginState().then(async (result) => {
if (result.status === 'has_gh_token') {
// If we found a token, move to the CONFIRM screen
setStep({ name: 'confirm', token: result.token });
}
// ... handle other error cases ...
});
}, []);
checkLoginState (which we will explore in GitHub CLI Integration) runs in the background. If it succeeds, we update our step state.React allows us to decide what to show based on our state. If we are checking, we show a spinner.
if (step.name === 'checking') {
// Renders a spinning animation
return <LoadingState message="Checking login statusβ¦" />;
}
β Checking login statusβ¦
If the check succeeds, the state changes to confirm. Now we show a dialog box asking for permission.
// If we are in the 'confirm' step...
return (
<Dialog title="Connect Claude to GitHub?" onCancel={handleCancel}>
<Text>
We need to connect your account to push code on your behalf.
</Text>
<Select
options={[{ label: 'Continue', value: 'send' }]}
onChange={(val) => val === 'send' ? handleConfirm(step.token) : handleCancel()}
/>
</Dialog>
);
<Dialog> and <Select> are UI components that handle user input (arrow keys and Enter).handleConfirm.
When the user says "Continue", we move the state to uploading and send the data to the backend.
const handleConfirm = async (token: RedactedGithubToken) => {
// 1. Update UI to show we are working
setStep({ name: 'uploading' });
// 2. Perform the actual upload (Backend API)
const result = await importGithubToken(token);
// 3. Finish the command
onDone(`Connected as ${result.username}`);
};
uploading state) before the command finishes completely.How does the data flow through this interactive system?
Let's look at the Web component in remote-setup.tsx. This component orchestrates the entire experience.
We export a function call that renders our component.
export async function call(onDone: LocalJSXCommandOnDone) {
// Renders the Web component into the terminal
return <Web onDone={onDone} />;
}
What if the user isn't logged in? We handle that in our useEffect.
// Inside useEffect checkLoginState().then(...)
case 'gh_not_installed':
const url = `${getCodeWebUrl()}/onboarding`;
// Open their browser to help them
await openBrowser(url);
// Close the command with an error message
onDone(`GitHub CLI not found. Please visit ${url}`);
return;
We have successfully built a State-Driven UI.
checking, confirm, uploading).
However, the "Checking" phase relied on a function called checkLoginState to interact with the system's GitHub tools. How does that work?
Next Chapter: GitHub CLI Integration
Generated by Code IQ