@@ -1008,6 +1008,7 @@ <h1>Gitclaw: {{AGENT_NAME}}</h1>
10081008 < button class ="view-tab " id ="tabComms " onclick ="switchView('comms') "> Communication</ button >
10091009 < button class ="view-tab " id ="tabFlows " onclick ="switchView('flows') "> SkillFlows</ button >
10101010 < button class ="view-tab " id ="tabScheduler " onclick ="switchView('scheduler') "> Scheduler</ button >
1011+ < button class ="view-tab " id ="tabSettings " onclick ="switchView('settings') "> Settings</ button >
10111012 </ div >
10121013 </ div >
10131014 < div class ="header-right ">
@@ -1350,6 +1351,47 @@ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter
13501351 < div id ="schedulesList "> </ div >
13511352 </ div >
13521353 </ div >
1354+ < div class ="settings-view hidden " id ="settingsView " style ="display:flex;flex:1;overflow-y:auto;padding:16px; ">
1355+ < div style ="max-width:560px;width:100%;margin:0 auto; ">
1356+ < h3 style ="margin:0 0 16px;color:#e6edf3;font-size:16px; "> Settings</ h3 >
1357+ < div id ="settingsStatus " style ="display:none;padding:10px 14px;border-radius:6px;margin-bottom:16px;font-size:13px; "> </ div >
1358+ < div style ="display:flex;flex-direction:column;gap:14px; ">
1359+ < div >
1360+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> Agent Model</ label >
1361+ < select id ="settingsModel " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px; ">
1362+ < option value =""> Loading...</ option >
1363+ </ select >
1364+ </ div >
1365+ < div >
1366+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> Custom Model < span style ="color:#484f58; "> (provider:model-id)</ span > </ label >
1367+ < input id ="settingsCustomModel " type ="text " placeholder ="e.g. anthropic:claude-opus-4-6 " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box; ">
1368+ </ div >
1369+ < hr style ="border:none;border-top:1px solid #21262d;margin:4px 0; ">
1370+ < div >
1371+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> OpenAI API Key < span style ="color:#484f58; "> (voice)</ span > </ label >
1372+ < input id ="settingsOpenaiKey " type ="password " placeholder ="sk-... " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box; ">
1373+ </ div >
1374+ < div >
1375+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> Anthropic API Key < span style ="color:#484f58; "> (agent brain)</ span > </ label >
1376+ < input id ="settingsAnthropicKey " type ="password " placeholder ="sk-ant-... " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box; ">
1377+ </ div >
1378+ < div >
1379+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> Gemini API Key < span style ="color:#484f58; "> (optional)</ span > </ label >
1380+ < input id ="settingsGeminiKey " type ="password " placeholder ="AI... " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box; ">
1381+ </ div >
1382+ < div >
1383+ < label style ="display:block;color:#8b949e;font-size:12px;margin-bottom:4px; "> Composio API Key < span style ="color:#484f58; "> (optional — Gmail, Calendar, Slack)</ span > </ label >
1384+ < input id ="settingsComposioKey " type ="password " placeholder ="ak_... " style ="width:100%;padding:8px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;box-sizing:border-box; ">
1385+ </ div >
1386+ < hr style ="border:none;border-top:1px solid #21262d;margin:4px 0; ">
1387+ < div style ="display:flex;gap:8px;align-items:center; ">
1388+ < button onclick ="saveSettings() " style ="padding:8px 20px;background:#238636;border:1px solid #2ea043;border-radius:6px;color:#fff;cursor:pointer;font-size:13px; "> Save</ button >
1389+ < span id ="settingsSaving " style ="display:none;color:#8b949e;font-size:12px; "> Saving...</ span >
1390+ </ div >
1391+ < p style ="color:#484f58;font-size:11px;margin:0; "> Saves to .env and agent.yaml in your agent directory. Changes take effect on the next query.</ p >
1392+ </ div >
1393+ </ div >
1394+ </ div >
13531395 < div class ="skills-view hidden " id ="skillsView " style ="display:flex;flex-direction:column;flex:1;overflow:hidden; ">
13541396 < iframe id ="skillsFrame " src ="" style ="flex:1;border:none;background:#0d1117;width:100%; "> </ iframe >
13551397 </ div >
@@ -1360,7 +1402,7 @@ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter
13601402< script >
13611403var hasComposio = { { HAS_COMPOSIO } } ;
13621404if ( ! hasComposio ) { document . getElementById ( 'tabIntegrations' ) . style . display = 'none' ; }
1363- let currentView = 'chat' , filesLoaded = false , integrationsLoaded = false , commsLoaded = false , skillsLoaded = false , flowsLoaded = false , schedulerLoaded = false ;
1405+ let currentView = 'chat' , filesLoaded = false , integrationsLoaded = false , commsLoaded = false , skillsLoaded = false , flowsLoaded = false , schedulerLoaded = false , settingsLoaded = false ;
13641406function switchView ( v ) {
13651407 currentView = v ;
13661408 document . getElementById ( 'chatView' ) . classList . toggle ( 'hidden' , v !== 'chat' ) ;
@@ -1369,17 +1411,20 @@ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter
13691411 document . getElementById ( 'skillsView' ) . classList . toggle ( 'hidden' , v !== 'skills' ) ;
13701412 document . getElementById ( 'flowsView' ) . classList . toggle ( 'hidden' , v !== 'flows' ) ;
13711413 document . getElementById ( 'schedulerView' ) . classList . toggle ( 'hidden' , v !== 'scheduler' ) ;
1414+ document . getElementById ( 'settingsView' ) . classList . toggle ( 'hidden' , v !== 'settings' ) ;
13721415 document . getElementById ( 'tabChat' ) . classList . toggle ( 'active' , v === 'chat' ) ;
13731416 document . getElementById ( 'tabIntegrations' ) . classList . toggle ( 'active' , v === 'integrations' ) ;
13741417 document . getElementById ( 'tabComms' ) . classList . toggle ( 'active' , v === 'comms' ) ;
13751418 document . getElementById ( 'tabSkills' ) . classList . toggle ( 'active' , v === 'skills' ) ;
13761419 document . getElementById ( 'tabFlows' ) . classList . toggle ( 'active' , v === 'flows' ) ;
13771420 document . getElementById ( 'tabScheduler' ) . classList . toggle ( 'active' , v === 'scheduler' ) ;
1421+ document . getElementById ( 'tabSettings' ) . classList . toggle ( 'active' , v === 'settings' ) ;
13781422 if ( v === 'integrations' && ! integrationsLoaded ) { loadToolkits ( ) ; integrationsLoaded = true ; }
13791423 if ( v === 'comms' && ! commsLoaded ) { loadTelegramStatus ( ) ; loadWhatsAppStatus ( ) ; loadPhoneWebhookUrl ( ) ; commsLoaded = true ; }
13801424 if ( v === 'skills' && ! skillsLoaded ) { document . getElementById ( 'skillsFrame' ) . src = '/api/skills-mp/proxy?path=/' ; skillsLoaded = true ; }
13811425 if ( v === 'flows' ) { loadFlowSkills ( ) ; loadSavedFlows ( ) ; flowsLoaded = true ; }
13821426 if ( v === 'scheduler' && ! schedulerLoaded ) { loadSchedules ( ) ; schedulerLoaded = true ; }
1427+ if ( v === 'settings' && ! settingsLoaded ) { loadSettings ( ) ; settingsLoaded = true ; }
13831428}
13841429// Skills MP install handler
13851430window . addEventListener ( 'message' , function ( e ) {
@@ -2653,6 +2698,82 @@ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter
26532698 }
26542699}
26552700
2701+ // ── SETTINGS ────────────────────────────────────────────────────────
2702+ var _knownModels = [
2703+ { value :'anthropic:claude-sonnet-4-6' , label :'Claude Sonnet 4.6' } ,
2704+ { value :'anthropic:claude-opus-4-6' , label :'Claude Opus 4.6' } ,
2705+ { value :'anthropic:claude-haiku-4-5-20251001' , label :'Claude Haiku 4.5' } ,
2706+ { value :'openai:gpt-4o' , label :'GPT-4o' } ,
2707+ { value :'openai:gpt-4o-mini' , label :'GPT-4o Mini' } ,
2708+ { value :'google:gemini-2.0-flash-001' , label :'Gemini 2.0 Flash' } ,
2709+ { value :'groq:llama-3.3-70b-versatile' , label :'Groq Llama 3.3 70B' } ,
2710+ { value :'deepseek:deepseek-chat' , label :'DeepSeek Chat' } ,
2711+ { value :'' , label :'Custom (enter below)' }
2712+ ] ;
2713+ function loadSettings ( ) {
2714+ fetch ( '/api/settings' ) . then ( function ( r ) { return r . json ( ) ; } ) . then ( function ( d ) {
2715+ var sel = document . getElementById ( 'settingsModel' ) ;
2716+ sel . innerHTML = '' ;
2717+ _knownModels . forEach ( function ( m ) {
2718+ var opt = document . createElement ( 'option' ) ; opt . value = m . value ; opt . textContent = m . label ;
2719+ sel . appendChild ( opt ) ;
2720+ } ) ;
2721+ // Set current model
2722+ if ( d . model ) {
2723+ var found = _knownModels . some ( function ( m ) { return m . value === d . model ; } ) ;
2724+ if ( found ) { sel . value = d . model ; }
2725+ else { sel . value = '' ; document . getElementById ( 'settingsCustomModel' ) . value = d . model ; }
2726+ }
2727+ // Mask keys — show placeholder if set
2728+ document . getElementById ( 'settingsOpenaiKey' ) . placeholder = d . keys . OPENAI_API_KEY ?'•••••••• (set)' :'sk-...' ;
2729+ document . getElementById ( 'settingsAnthropicKey' ) . placeholder = d . keys . ANTHROPIC_API_KEY ?'•••••••• (set)' :'sk-ant-...' ;
2730+ document . getElementById ( 'settingsGeminiKey' ) . placeholder = d . keys . GEMINI_API_KEY ?'•••••••• (set)' :'AI...' ;
2731+ document . getElementById ( 'settingsComposioKey' ) . placeholder = d . keys . COMPOSIO_API_KEY ?'•••••••• (set)' :'ak_...' ;
2732+ } ) . catch ( function ( e ) { showSettingsStatus ( 'Failed to load settings: ' + e . message , 'error' ) ; } ) ;
2733+ }
2734+ function showSettingsStatus ( msg , type ) {
2735+ var el = document . getElementById ( 'settingsStatus' ) ;
2736+ el . style . display = 'block' ;
2737+ el . textContent = msg ;
2738+ el . style . background = type === 'error' ?'rgba(248,81,73,0.15)' :'rgba(63,185,80,0.15)' ;
2739+ el . style . color = type === 'error' ?'#f85149' :'#3fb950' ;
2740+ el . style . border = '1px solid ' + ( type === 'error' ?'#f8514966' :'#3fb95066' ) ;
2741+ if ( type !== 'error' ) setTimeout ( function ( ) { el . style . display = 'none' ; } , 4000 ) ;
2742+ }
2743+ function saveSettings ( ) {
2744+ var sel = document . getElementById ( 'settingsModel' ) ;
2745+ var model = sel . value || document . getElementById ( 'settingsCustomModel' ) . value ;
2746+ var payload = { model :model , keys :{ } } ;
2747+ var openai = document . getElementById ( 'settingsOpenaiKey' ) . value ;
2748+ var anthropic = document . getElementById ( 'settingsAnthropicKey' ) . value ;
2749+ var gemini = document . getElementById ( 'settingsGeminiKey' ) . value ;
2750+ var composio = document . getElementById ( 'settingsComposioKey' ) . value ;
2751+ if ( openai ) payload . keys . OPENAI_API_KEY = openai ;
2752+ if ( anthropic ) payload . keys . ANTHROPIC_API_KEY = anthropic ;
2753+ if ( gemini ) payload . keys . GEMINI_API_KEY = gemini ;
2754+ if ( composio ) payload . keys . COMPOSIO_API_KEY = composio ;
2755+ document . getElementById ( 'settingsSaving' ) . style . display = 'inline' ;
2756+ fetch ( '/api/settings' , { method :'PUT' , headers :{ 'Content-Type' :'application/json' } , body :JSON . stringify ( payload ) } )
2757+ . then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok :r . ok , data :d } ; } ) ; } )
2758+ . then ( function ( res ) {
2759+ document . getElementById ( 'settingsSaving' ) . style . display = 'none' ;
2760+ if ( res . ok ) {
2761+ showSettingsStatus ( 'Settings saved. Changes take effect on next query.' , 'success' ) ;
2762+ // Clear password fields and refresh placeholders
2763+ document . getElementById ( 'settingsOpenaiKey' ) . value = '' ;
2764+ document . getElementById ( 'settingsAnthropicKey' ) . value = '' ;
2765+ document . getElementById ( 'settingsGeminiKey' ) . value = '' ;
2766+ document . getElementById ( 'settingsComposioKey' ) . value = '' ;
2767+ settingsLoaded = false ; loadSettings ( ) ; settingsLoaded = true ;
2768+ } else {
2769+ showSettingsStatus ( res . data . error || 'Failed to save' , 'error' ) ;
2770+ }
2771+ } ) . catch ( function ( e ) {
2772+ document . getElementById ( 'settingsSaving' ) . style . display = 'none' ;
2773+ showSettingsStatus ( 'Error: ' + e . message , 'error' ) ;
2774+ } ) ;
2775+ }
2776+
26562777// ── TELEGRAM / COMMUNICATION ────────────────────────────────────────
26572778async function loadTelegramStatus ( ) {
26582779 var card = document . getElementById ( 'telegramCard' ) ;
0 commit comments