Skip to content

Commit a2ef1a1

Browse files
shreyas-lyzrclaude
andcommitted
feat: add Settings tab to voice UI — edit model and API keys from browser
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5c330d9 commit a2ef1a1

3 files changed

Lines changed: 180 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitclaw",
3-
"version": "1.1.6",
3+
"version": "1.1.7",
44
"description": "A universal git-native multimodal always learning AI Agent (TinyHuman)",
55
"author": "shreyaskapale",
66
"license": "MIT",

src/voice/server.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,63 @@ ${runningContext}`;
16461646
} else if (url.pathname === "/api/vitals") {
16471647
jsonReply(res, 200, getVitalsSnapshot());
16481648

1649+
} else if (url.pathname === "/api/settings" && req.method === "GET") {
1650+
// Read current model from agent.yaml and key presence from .env
1651+
let model = "";
1652+
try {
1653+
const yamlRaw = readFileSync(join(agentRoot, "agent.yaml"), "utf-8");
1654+
const m = yamlRaw.match(/preferred:\s*["']?([^"'\n]+)["']?/);
1655+
if (m) model = m[1].trim();
1656+
} catch { /* no agent.yaml */ }
1657+
const keys: Record<string, boolean> = {};
1658+
for (const k of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "COMPOSIO_API_KEY"]) {
1659+
keys[k] = !!process.env[k];
1660+
}
1661+
jsonReply(res, 200, { model, keys });
1662+
1663+
} else if (url.pathname === "/api/settings" && req.method === "PUT") {
1664+
try {
1665+
const body = JSON.parse(await readBody(req));
1666+
1667+
// Update .env with new keys
1668+
const envPath = join(agentRoot, ".env");
1669+
let envContent = "";
1670+
try { envContent = readFileSync(envPath, "utf-8"); } catch { /* new file */ }
1671+
1672+
const envKeys = body.keys || {};
1673+
for (const [key, val] of Object.entries(envKeys)) {
1674+
if (typeof val !== "string" || !val) continue;
1675+
process.env[key] = val;
1676+
const regex = new RegExp(`^${key}=.*$`, "m");
1677+
if (regex.test(envContent)) {
1678+
envContent = envContent.replace(regex, `${key}=${val}`);
1679+
} else {
1680+
envContent += (envContent.endsWith("\n") || !envContent ? "" : "\n") + `${key}=${val}\n`;
1681+
}
1682+
}
1683+
writeFileSync(envPath, envContent, "utf-8");
1684+
1685+
// Update model in agent.yaml
1686+
if (body.model) {
1687+
const yamlPath = join(agentRoot, "agent.yaml");
1688+
try {
1689+
let yamlContent = readFileSync(yamlPath, "utf-8");
1690+
if (/preferred:\s*["']?[^"'\n]*["']?/.test(yamlContent)) {
1691+
yamlContent = yamlContent.replace(
1692+
/preferred:\s*["']?[^"'\n]*["']?/,
1693+
`preferred: "${body.model}"`,
1694+
);
1695+
}
1696+
writeFileSync(yamlPath, yamlContent, "utf-8");
1697+
} catch { /* no agent.yaml to update */ }
1698+
}
1699+
1700+
console.log("[settings] Configuration updated — keys in process.env, model in agent.yaml");
1701+
jsonReply(res, 200, { ok: true });
1702+
} catch (err: any) {
1703+
jsonReply(res, 400, { error: err.message || "Invalid request" });
1704+
}
1705+
16491706
} else if (url.pathname === "/" || url.pathname === "/test") {
16501707
res.writeHead(200, { "Content-Type": "text/html" });
16511708
res.end(uiHtml);

src/voice/ui.html

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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>
13611403
var hasComposio={{HAS_COMPOSIO}};
13621404
if(!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;
13641406
function 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
13851430
window.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 ────────────────────────────────────────
26572778
async function loadTelegramStatus(){
26582779
var card=document.getElementById('telegramCard');

0 commit comments

Comments
 (0)