@@ -12,7 +12,7 @@ import { sendNotification } from '../services/notifier.js';
1212import { OAuthService } from '../services/oauth/index.js' ;
1313import { getOauthAccountInfo , validateForceLoginOrg } from '../utils/auth.js' ;
1414import { logError } from '../utils/log.js' ;
15- import { getSettings_DEPRECATED } from '../utils/settings/settings.js' ;
15+ import { getSettings_DEPRECATED , updateSettingsForSource } from '../utils/settings/settings.js' ;
1616import { Select } from './CustomSelect/select.js' ;
1717import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' ;
1818import { Spinner } from './Spinner.js' ;
@@ -29,6 +29,15 @@ type OAuthStatus = {
2929| {
3030 state : 'platform_setup' ;
3131} // Show platform setup info (Bedrock/Vertex/Foundry)
32+ | {
33+ state : 'custom_platform' ;
34+ baseUrl : string ;
35+ apiKey : string ;
36+ haikuModel : string ;
37+ sonnetModel : string ;
38+ opusModel : string ;
39+ activeField : 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
40+ } // Custom platform: configure API endpoint and model names
3241| {
3342 state : 'ready_to_start' ;
3443} // Flow started, waiting for browser to open
@@ -325,7 +334,7 @@ export function ConsoleOAuthFlow({
325334 </ Box >
326335 </ Box > }
327336 < Box paddingLeft = { 1 } flexDirection = "column" gap = { 1 } >
328- < OAuthStatusMessage oauthStatus = { oauthStatus } mode = { mode } startingMessage = { startingMessage } forcedMethodMessage = { forcedMethodMessage } showPastePrompt = { showPastePrompt } pastedCode = { pastedCode } setPastedCode = { setPastedCode } cursorOffset = { cursorOffset } setCursorOffset = { setCursorOffset } textInputColumns = { textInputColumns } handleSubmitCode = { handleSubmitCode } setOAuthStatus = { setOAuthStatus } setLoginWithClaudeAi = { setLoginWithClaudeAi } />
337+ < OAuthStatusMessage oauthStatus = { oauthStatus } mode = { mode } startingMessage = { startingMessage } forcedMethodMessage = { forcedMethodMessage } showPastePrompt = { showPastePrompt } pastedCode = { pastedCode } setPastedCode = { setPastedCode } cursorOffset = { cursorOffset } setCursorOffset = { setCursorOffset } textInputColumns = { textInputColumns } handleSubmitCode = { handleSubmitCode } setOAuthStatus = { setOAuthStatus } setLoginWithClaudeAi = { setLoginWithClaudeAi } onDone = { onDone } />
329338 </ Box >
330339 </ Box > ;
331340}
@@ -343,6 +352,7 @@ type OAuthStatusMessageProps = {
343352 handleSubmitCode : ( value : string , url : string ) => void ;
344353 setOAuthStatus : ( status : OAuthStatus ) => void ;
345354 setLoginWithClaudeAi : ( value : boolean ) => void ;
355+ onDone : ( ) => void ;
346356} ;
347357function OAuthStatusMessage ( t0 ) {
348358 const $ = _c ( 51 ) ;
@@ -359,7 +369,8 @@ function OAuthStatusMessage(t0) {
359369 textInputColumns,
360370 handleSubmitCode,
361371 setOAuthStatus,
362- setLoginWithClaudeAi
372+ setLoginWithClaudeAi,
373+ onDone
363374 } = t0 ;
364375 switch ( oauthStatus . state ) {
365376 case "idle" :
@@ -402,7 +413,10 @@ function OAuthStatusMessage(t0) {
402413 }
403414 let t6 ;
404415 if ( $ [ 5 ] === Symbol . for ( "react.memo_cache_sentinel" ) ) {
405- t6 = [ t4 , t5 , {
416+ t6 = [ {
417+ label : < Text > Custom Platform ·{ " " } < Text dimColor = { true } > Configure your own API endpoint</ Text > { "\n" } </ Text > ,
418+ value : "custom_platform"
419+ } , t4 , t5 , {
406420 label : < Text > 3rd-party platform ·{ " " } < Text dimColor = { true } > Amazon Bedrock, Microsoft Foundry, or Vertex AI</ Text > { "\n" } </ Text > ,
407421 value : "platform"
408422 } ] ;
@@ -413,7 +427,18 @@ function OAuthStatusMessage(t0) {
413427 let t7 ;
414428 if ( $ [ 6 ] !== setLoginWithClaudeAi || $ [ 7 ] !== setOAuthStatus ) {
415429 t7 = < Box > < Select options = { t6 } onChange = { value_0 => {
416- if ( value_0 === "platform" ) {
430+ if ( value_0 === "custom_platform" ) {
431+ logEvent ( "tengu_custom_platform_selected" , { } ) ;
432+ setOAuthStatus ( {
433+ state : "custom_platform" ,
434+ baseUrl : process . env . ANTHROPIC_BASE_URL ?? "" ,
435+ apiKey : process . env . ANTHROPIC_AUTH_TOKEN ?? "" ,
436+ haikuModel : process . env . ANTHROPIC_DEFAULT_HAIKU_MODEL ?? "" ,
437+ sonnetModel : process . env . ANTHROPIC_DEFAULT_SONNET_MODEL ?? "" ,
438+ opusModel : process . env . ANTHROPIC_DEFAULT_OPUS_MODEL ?? "" ,
439+ activeField : "base_url"
440+ } ) ;
441+ } else if ( value_0 === "platform" ) {
417442 logEvent ( "tengu_oauth_platform_selected" , { } ) ;
418443 setOAuthStatus ( {
419444 state : "platform_setup"
@@ -505,6 +530,115 @@ function OAuthStatusMessage(t0) {
505530 }
506531 return t8 ;
507532 }
533+ case "custom_platform" :
534+ {
535+ type Field = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model' ;
536+ const FIELDS : Field [ ] = [ 'base_url' , 'api_key' , 'haiku_model' , 'sonnet_model' , 'opus_model' ] ;
537+ const cp = oauthStatus as { state : 'custom_platform' ; activeField : Field ; baseUrl : string ; apiKey : string ; haikuModel : string ; sonnetModel : string ; opusModel : string } ;
538+ const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = cp ;
539+ const displayValues : Record < Field , string > = { base_url : baseUrl , api_key : apiKey , haiku_model : haikuModel , sonnet_model : sonnetModel , opus_model : opusModel } ;
540+
541+ const [ inputValue , setInputValue ] = useState ( ( ) => displayValues [ activeField ] ) ;
542+ const [ inputCursorOffset , setInputCursorOffset ] = useState ( ( ) => displayValues [ activeField ] . length ) ;
543+
544+ // Build updated state with a given field changed
545+ const buildState = useCallback ( ( field : Field , value : string , newActive ?: Field ) => {
546+ const s = { state : 'custom_platform' as const , activeField : newActive ?? activeField , baseUrl, apiKey, haikuModel, sonnetModel, opusModel } ;
547+ switch ( field ) {
548+ case 'base_url' : return { ...s , baseUrl : value } ;
549+ case 'api_key' : return { ...s , apiKey : value } ;
550+ case 'haiku_model' : return { ...s , haikuModel : value } ;
551+ case 'sonnet_model' : return { ...s , sonnetModel : value } ;
552+ case 'opus_model' : return { ...s , opusModel : value } ;
553+ }
554+ } , [ activeField , baseUrl , apiKey , haikuModel , sonnetModel , opusModel ] ) ;
555+
556+ // Tab switching: save current → update state → load target
557+ const switchTo = useCallback ( ( target : Field ) => {
558+ setOAuthStatus ( buildState ( activeField , inputValue , target ) ) ;
559+ setInputValue ( displayValues [ target ] ?? '' ) ;
560+ setInputCursorOffset ( ( displayValues [ target ] ?? '' ) . length ) ;
561+ } , [ activeField , inputValue , displayValues , buildState , setOAuthStatus ] ) ;
562+
563+ const doSave = useCallback ( ( ) => {
564+ const finalVals = { ...displayValues , [ activeField ] : inputValue } ;
565+ const env : Record < string , string > = { } ;
566+ if ( finalVals . base_url ) env . ANTHROPIC_BASE_URL = finalVals . base_url ;
567+ if ( finalVals . api_key ) env . ANTHROPIC_AUTH_TOKEN = finalVals . api_key ;
568+ if ( finalVals . haiku_model ) env . ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals . haiku_model ;
569+ if ( finalVals . sonnet_model ) env . ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals . sonnet_model ;
570+ if ( finalVals . opus_model ) env . ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals . opus_model ;
571+ const { error } = updateSettingsForSource ( 'userSettings' , { env } as any ) ;
572+ if ( error ) {
573+ setOAuthStatus ( { state : 'error' , message : `Failed to save: ${ error . message } ` , toRetry : { state : 'custom_platform' , baseUrl : '' , apiKey : '' , haikuModel : '' , sonnetModel : '' , opusModel : '' , activeField : 'base_url' } } ) ;
574+ } else {
575+ for ( const [ k , v ] of Object . entries ( env ) ) process . env [ k ] = v ;
576+ setOAuthStatus ( { state : 'success' } ) ;
577+ void onDone ( ) ;
578+ }
579+ } , [ activeField , inputValue , displayValues , setOAuthStatus , onDone ] ) ;
580+
581+ const handleEnter = useCallback ( ( ) => {
582+ const idx = FIELDS . indexOf ( activeField ) ;
583+ // Update current field value in state
584+ setOAuthStatus ( buildState ( activeField , inputValue ) ) ;
585+ if ( idx === FIELDS . length - 1 ) {
586+ doSave ( ) ;
587+ } else {
588+ const next = FIELDS [ idx + 1 ] ! ;
589+ setInputValue ( displayValues [ next ] ?? '' ) ;
590+ setInputCursorOffset ( ( displayValues [ next ] ?? '' ) . length ) ;
591+ }
592+ } , [ activeField , inputValue , buildState , doSave , displayValues , setOAuthStatus ] ) ;
593+
594+ useKeybinding ( 'tabs:next' , ( ) => {
595+ const idx = FIELDS . indexOf ( activeField ) ;
596+ if ( idx < FIELDS . length - 1 ) {
597+ setOAuthStatus ( buildState ( activeField , inputValue , FIELDS [ idx + 1 ] ) ) ;
598+ setInputValue ( displayValues [ FIELDS [ idx + 1 ] ! ] ?? '' ) ;
599+ setInputCursorOffset ( ( displayValues [ FIELDS [ idx + 1 ] ! ] ?? '' ) . length ) ;
600+ }
601+ } , { context : 'Tabs' } ) ;
602+ useKeybinding ( 'tabs:previous' , ( ) => {
603+ const idx = FIELDS . indexOf ( activeField ) ;
604+ if ( idx > 0 ) {
605+ setOAuthStatus ( buildState ( activeField , inputValue , FIELDS [ idx - 1 ] ) ) ;
606+ setInputValue ( displayValues [ FIELDS [ idx - 1 ] ! ] ?? '' ) ;
607+ setInputCursorOffset ( ( displayValues [ FIELDS [ idx - 1 ] ! ] ?? '' ) . length ) ;
608+ }
609+ } , { context : 'Tabs' } ) ;
610+ useKeybinding ( 'confirm:no' , ( ) => {
611+ setOAuthStatus ( { state : 'idle' } ) ;
612+ } , { context : 'Confirmation' } ) ;
613+
614+ const columns = useTerminalSize ( ) . columns - 20 ;
615+
616+ const renderRow = ( field : Field , label : string , opts ?: { mask ?: boolean ; placeholder ?: string } ) => {
617+ const active = activeField === field ;
618+ const val = displayValues [ field ] ;
619+ return < Box >
620+ < Text backgroundColor = { active ? 'suggestion' : undefined } color = { active ? 'inverseText' : undefined } > { ` ${ label } ` } </ Text >
621+ < Text > </ Text >
622+ { active
623+ ? < TextInput value = { inputValue } onChange = { setInputValue } onSubmit = { handleEnter } cursorOffset = { inputCursorOffset } onChangeCursorOffset = { setInputCursorOffset } columns = { columns } mask = { opts ?. mask ? "*" : undefined } focus = { true } />
624+ : ( val
625+ ? < Text color = "success" > { opts ?. mask ? val . slice ( 0 , 8 ) + '·' . repeat ( Math . max ( 0 , val . length - 8 ) ) : val } </ Text >
626+ : null ) }
627+ </ Box > ;
628+ } ;
629+
630+ return < Box flexDirection = "column" gap = { 1 } >
631+ < Text bold = { true } > Custom Platform Setup</ Text >
632+ < Box flexDirection = "column" gap = { 1 } >
633+ { renderRow ( 'base_url' , 'Base URL ' ) }
634+ { renderRow ( 'api_key' , 'API Key ' , { mask : true } ) }
635+ { renderRow ( 'haiku_model' , 'Haiku ' ) }
636+ { renderRow ( 'sonnet_model' , 'Sonnet ' ) }
637+ { renderRow ( 'opus_model' , 'Opus ' ) }
638+ </ Box >
639+ < Text dimColor > Tab to switch · Enter on last field to save · Esc to go back</ Text >
640+ </ Box > ;
641+ }
508642 case "waiting_for_login" :
509643 {
510644 let t1 ;
0 commit comments