@@ -3,9 +3,10 @@ import { defaultTheme, extendTheme, Spinner, TextInput, ThemeProvider } from "@i
33import { Box , Text , useApp , useInput , useStdout } from "ink" ;
44import { ScrollView , type ScrollViewRef } from "ink-scroll-view" ;
55import { type ReactNode , useCallback , useEffect , useRef , useState } from "react" ;
6- import type { Agent } from "#/agent" ;
6+ import type { Agent , ModelInfo } from "#/agent" ;
77import { isLightScheme } from "./color-scheme.ts" ;
88import { type Command , parseCommand } from "./commands.ts" ;
9+ import { ApiKeyPrompt } from "./components/ApiKeyPrompt.tsx" ;
910import { AssistantMessage } from "./components/AssistantMessage.tsx" ;
1011import { Banner } from "./components/Banner.tsx" ;
1112import { ErrorMessage } from "./components/ErrorMessage.tsx" ;
@@ -37,9 +38,10 @@ type OverlayState = {
3738
3839type AppProps = {
3940 agent : Agent ;
41+ attachApiKey : ( apiKey : string ) => ModelInfo | null ;
4042} ;
4143
42- export function App ( { agent } : AppProps ) {
44+ export function App ( { agent, attachApiKey } : AppProps ) {
4345 const { exit } = useApp ( ) ;
4446 const { stdout } = useStdout ( ) ;
4547 const [ rows , setRows ] = useState ( stdout ?. rows ?? 24 ) ;
@@ -51,6 +53,11 @@ export function App({ agent }: AppProps) {
5153 const [ elapsedMs , setElapsedMs ] = useState < number | null > ( null ) ;
5254 const [ working , setWorking ] = useState ( false ) ;
5355
56+ const [ modelInfo , setModelInfo ] = useState ( agent . modelInfo ( ) ) ;
57+ const [ apiKeyError , setApiKeyError ] = useState < string > ( ) ;
58+ const [ apiKeyAttempt , setApiKeyAttempt ] = useState ( 0 ) ;
59+ const needsApiKey = modelInfo === null ;
60+
5461 const scrollRef = useRef < ScrollViewRef > ( null ) ;
5562 const [ scrollOffset , setScrollOffset ] = useState ( 0 ) ;
5663 const [ contentHeight , setContentHeight ] = useState ( 0 ) ;
@@ -126,24 +133,41 @@ export function App({ agent }: AppProps) {
126133 case "tools_list" :
127134 setOverlay ( toolsOverlay ( ) ) ;
128135 return ;
136+
129137 case "tools_remove" :
130138 await agent . removeTool ( command . name ) ;
131139 appendSystemMessage ( `Removed tool '${ command . name } '` ) ;
132140 setOverlay ( ( prev ) => ( prev ?. kind === "tools" ? toolsOverlay ( ) : prev ) ) ;
133141 return ;
142+
134143 case "clear" :
135144 await agent . clear ( ) ;
136145 setTranscript ( [ ] ) ;
137146 setShowIntro ( true ) ;
138147 setOverlay ( null ) ;
139148 setElapsedMs ( null ) ;
140149 return ;
150+
141151 case "exit" :
142152 exit ( ) ;
143153 return ;
144154 }
145155 } ;
146156
157+ const handleApiKeySubmit = ( value : string ) => {
158+ const apiKey = value . trim ( ) ;
159+ if ( apiKey === "" ) return ;
160+
161+ const modelInfo = attachApiKey ( apiKey ) ;
162+ if ( ! modelInfo ) {
163+ setApiKeyError ( "Could not initialise an LLM client from that key." ) ;
164+ setApiKeyAttempt ( ( attempt ) => attempt + 1 ) ;
165+ return ;
166+ }
167+
168+ setModelInfo ( modelInfo ) ;
169+ } ;
170+
147171 const handleSubmit = async ( input : string ) => {
148172 if ( input . trim ( ) === "" ) return ;
149173 setTextInputKey ( ( k ) => k + 1 ) ;
@@ -208,7 +232,15 @@ export function App({ agent }: AppProps) {
208232 >
209233 < Box key = "header" flexDirection = "column" paddingTop = { 1 } gap = { 1 } >
210234 < Banner />
211- { showIntro && < IntroMessage /> }
235+ { needsApiKey ? (
236+ < ApiKeyPrompt
237+ key = { apiKeyAttempt }
238+ error = { apiKeyError }
239+ onSubmit = { handleApiKeySubmit }
240+ />
241+ ) : (
242+ showIntro && < IntroMessage />
243+ ) }
212244 </ Box >
213245 { transcript . map ( ( item ) => (
214246 < Box key = { `${ item . kind } -${ item . id } ` } marginTop = { 1 } >
@@ -248,25 +280,27 @@ export function App({ agent }: AppProps) {
248280 ) }
249281 </ Box >
250282 { overlay && < Overlay title = { overlay . title } > { overlay . content } </ Overlay > }
251- < Box
252- paddingX = { 1 }
253- borderStyle = "round"
254- borderColor = { theme . accent }
255- borderDimColor = { ! isLightScheme ( ) }
256- >
257- < Text color = { theme . accent } bold >
258- ❯{ " " }
259- </ Text >
260- < Box flexGrow = { 1 } >
261- < TextInput
262- key = { textInputKey }
263- isDisabled = { working }
264- placeholder = "Type a message (Ctrl+C or /exit to quit)"
265- onSubmit = { handleSubmit }
266- />
283+ { ! needsApiKey && (
284+ < Box
285+ paddingX = { 1 }
286+ borderStyle = "round"
287+ borderColor = { theme . accent }
288+ borderDimColor = { ! isLightScheme ( ) }
289+ >
290+ < Text color = { theme . accent } bold >
291+ ❯{ " " }
292+ </ Text >
293+ < Box flexGrow = { 1 } >
294+ < TextInput
295+ key = { textInputKey }
296+ isDisabled = { working }
297+ placeholder = "Type a message (Ctrl+C or /exit to quit)"
298+ onSubmit = { handleSubmit }
299+ />
300+ </ Box >
267301 </ Box >
268- </ Box >
269- < StatusBar { ...agent . modelInfo ( ) } />
302+ ) }
303+ { modelInfo && < StatusBar { ...modelInfo } /> }
270304 </ Box >
271305 </ Box >
272306 </ ThemeProvider >
0 commit comments