Skip to content

Commit ac8d648

Browse files
shreyas-lyzrclaude
andcommitted
feat: text-only fallback when voice key missing, mobile responsive UI, install.sh text mode
- Graceful fallback when OpenAI API key missing — shows warning, routes text to agent directly - install.sh: new "Text Only" setup option (no OpenAI key needed) - Mobile responsive: scrollable tabs, 44px touch targets, stacked layouts under 700px - Skill install now refreshes SkillFlows immediately Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29e2ae0 commit ac8d648

5 files changed

Lines changed: 144 additions & 40 deletions

File tree

install.sh

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,10 @@ if [ -d "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/agent.yaml" ]; then
183183
# Determine adapter from available keys
184184
if [ -n "${GEMINI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
185185
ADAPTER_LABEL="Gemini Live"
186-
else
186+
elif [ -n "${OPENAI_API_KEY:-}" ]; then
187187
ADAPTER_LABEL="OpenAI Realtime"
188+
else
189+
ADAPTER_LABEL="Text Only"
188190
fi
189191

190192
PORT="${PORT:-3333}"
@@ -195,10 +197,11 @@ if [ -d "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/agent.yaml" ]; then
195197
else
196198

197199
# ── Setup Mode Selection ─────────────────────────────────────────
198-
echo -e " ${BOLD}How would you like to set up?${NC}"
200+
echo -e " ${BOLD}How would you like to run?${NC}"
199201
echo ""
200-
echo -e " ${RED}${BOLD}1)${NC} ${BOLD}Quick Setup${NC} ${DIM}— OpenAI voice + Claude agent, get started in 30 seconds${NC}"
201-
echo -e " ${RED}${BOLD}2)${NC} ${BOLD}Advanced Setup${NC} ${DIM}— choose voice adapter, model, project dir, integrations${NC}"
202+
echo -e " ${RED}${BOLD}1)${NC} ${BOLD}Voice + Text${NC} ${DIM}— real-time voice chat + text (requires OpenAI key)${NC}"
203+
echo -e " ${RED}${BOLD}2)${NC} ${BOLD}Text Only${NC} ${DIM}— text chat only, no voice (just Anthropic key)${NC}"
204+
echo -e " ${RED}${BOLD}3)${NC} ${BOLD}Advanced Setup${NC} ${DIM}— choose voice adapter, model, project dir, integrations${NC}"
202205
echo ""
203206
read -rp " Choice [1]: " SETUP_MODE
204207
SETUP_MODE="${SETUP_MODE:-1}"
@@ -207,23 +210,35 @@ echo ""
207210
# ═══════════════════════════════════════════════════════════════════
208211
# QUICK SETUP
209212
# ═══════════════════════════════════════════════════════════════════
210-
if [ "$SETUP_MODE" = "1" ]; then
213+
if [ "$SETUP_MODE" = "1" ] || [ "$SETUP_MODE" = "2" ]; then
214+
215+
VOICE_ENABLED=true
216+
if [ "$SETUP_MODE" = "2" ]; then
217+
VOICE_ENABLED=false
218+
fi
211219

212220
echo -e " ${DIM}────────────────────────────────────────────────────${NC}"
213-
echo -e " ${RED}${BOLD}Quick Setup${NC}"
214-
echo -e " ${DIM}Voice: OpenAI Realtime • Agent: Claude Sonnet 4.6${NC}"
221+
if [ "$VOICE_ENABLED" = true ]; then
222+
echo -e " ${RED}${BOLD}Voice + Text Setup${NC}"
223+
echo -e " ${DIM}Voice: OpenAI Realtime • Agent: Claude Sonnet 4.6${NC}"
224+
else
225+
echo -e " ${RED}${BOLD}Text Only Setup${NC}"
226+
echo -e " ${DIM}Agent: Claude Sonnet 4.6 • No voice, text chat via browser${NC}"
227+
fi
215228
echo ""
216229

217-
# OpenAI key
218-
echo -e " ${BOLD}OpenAI API Key${NC} ${DIM}(for voice — get one at platform.openai.com)${NC}"
219-
read -rsp " Key: " OPENAI_KEY
220-
echo ""
221-
if [ -z "$OPENAI_KEY" ]; then
222-
echo -e " ${RED}✗ OpenAI key is required for voice mode${NC}"
223-
exit 1
230+
# OpenAI key (required for voice, optional for text-only)
231+
if [ "$VOICE_ENABLED" = true ]; then
232+
echo -e " ${BOLD}OpenAI API Key${NC} ${DIM}(for voice — get one at platform.openai.com)${NC}"
233+
read -rsp " Key: " OPENAI_KEY
234+
echo ""
235+
if [ -z "$OPENAI_KEY" ]; then
236+
echo -e " ${RED}✗ OpenAI key is required for voice mode${NC}"
237+
exit 1
238+
fi
239+
export OPENAI_API_KEY="$OPENAI_KEY"
240+
echo -e " ${GREEN}${NC} OPENAI_API_KEY saved"
224241
fi
225-
export OPENAI_API_KEY="$OPENAI_KEY"
226-
echo -e " ${GREEN}${NC} OPENAI_API_KEY saved"
227242

228243
# Anthropic key
229244
echo ""
@@ -250,7 +265,11 @@ if [ "$SETUP_MODE" = "1" ]; then
250265
fi
251266

252267
ADAPTER="openai"
253-
ADAPTER_LABEL="OpenAI Realtime"
268+
if [ "$VOICE_ENABLED" = true ]; then
269+
ADAPTER_LABEL="OpenAI Realtime"
270+
else
271+
ADAPTER_LABEL="Text Only"
272+
fi
254273
MODEL="anthropic:claude-sonnet-4-6"
255274
PROJECT_DIR="${HOME}/assistant"
256275

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.8",
3+
"version": "1.1.9",
44
"description": "A universal git-native multimodal always learning AI Agent (TinyHuman)",
55
"author": "shreyaskapale",
66
"license": "MIT",

src/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,15 @@ async function main(): Promise<void> {
382382

383383
if (voice === "gemini") {
384384
adapterBackend = "gemini-live";
385-
apiKey = process.env.GEMINI_API_KEY;
385+
apiKey = process.env.GEMINI_API_KEY || "";
386386
if (!apiKey) {
387-
console.error(red("Error: GEMINI_API_KEY is required for --voice gemini"));
388-
process.exit(1);
387+
console.log(dim("[voice] No GEMINI_API_KEY — voice disabled, text-only mode"));
389388
}
390389
} else {
391390
adapterBackend = "openai-realtime";
392-
apiKey = process.env.OPENAI_API_KEY;
391+
apiKey = process.env.OPENAI_API_KEY || "";
393392
if (!apiKey) {
394-
console.error(red("Error: OPENAI_API_KEY is required for --voice mode"));
395-
process.exit(1);
393+
console.log(dim("[voice] No OPENAI_API_KEY — voice disabled, text-only mode"));
396394
}
397395
}
398396

src/voice/server.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,7 +2592,7 @@ a{color:#58a6ff;}</style></head>
25922592
instructions,
25932593
},
25942594
};
2595-
const adapter = createAdapter(adapterOpts);
2595+
let adapter: MultimodalAdapter | null = opts.adapterConfig.apiKey ? createAdapter(adapterOpts) : null;
25962596
const sendToBrowser = (msg: ServerMessage) => {
25972597
safeSend(browserWs, JSON.stringify(msg));
25982598
appendMessage(opts.agentDir, activeBranch, msg);
@@ -2628,17 +2628,24 @@ a{color:#58a6ff;}</style></head>
26282628
}
26292629
};
26302630

2631-
try {
2632-
await adapter.connect({
2633-
toolHandler: createToolHandler(sendToBrowser),
2634-
onMessage: sendToBrowser,
2635-
});
2636-
console.log(dim(`[voice] Adapter ready (${opts.adapter})`));
2637-
} catch (err: any) {
2638-
console.error(dim(`[voice] Adapter connection failed: ${err.message}`));
2639-
safeSend(browserWs, JSON.stringify({ type: "error", message: `Adapter failed: ${err.message}` }));
2640-
browserWs.close();
2641-
return;
2631+
if (adapter) {
2632+
try {
2633+
await adapter.connect({
2634+
toolHandler: createToolHandler(sendToBrowser),
2635+
onMessage: sendToBrowser,
2636+
});
2637+
console.log(dim(`[voice] Adapter ready (${opts.adapter})`));
2638+
} catch (err: any) {
2639+
console.error(dim(`[voice] Adapter connection failed: ${err.message}`));
2640+
safeSend(browserWs, JSON.stringify({ type: "error", message: `Voice connection failed: ${err.message}` }));
2641+
adapter = null; // Fall back to text-only
2642+
}
2643+
}
2644+
if (!adapter) {
2645+
safeSend(browserWs, JSON.stringify({
2646+
type: "transcript", role: "assistant",
2647+
text: "Voice mode unavailable — no API key set. You can still chat via text.",
2648+
}));
26422649
}
26432650

26442651
// Parse browser messages into ClientMessage and forward to adapter
@@ -2700,6 +2707,19 @@ a{color:#58a6ff;}</style></head>
27002707
safeSend(browserWs, JSON.stringify({ type: "files_changed" }));
27012708
});
27022709
}
2710+
2711+
// Text-only mode — call agent directly when no voice adapter
2712+
if (!adapter) {
2713+
const handler = createToolHandler(sendToBrowser);
2714+
handler(msg.text).then((result) => {
2715+
safeSend(browserWs, JSON.stringify({ type: "agent_done", result }));
2716+
appendMessage(opts.agentDir, activeBranch, { type: "transcript", role: "assistant", text: result });
2717+
safeSend(browserWs, JSON.stringify({ type: "files_changed" }));
2718+
}).catch((err: any) => {
2719+
safeSend(browserWs, JSON.stringify({ type: "error", message: err.message }));
2720+
});
2721+
return;
2722+
}
27032723
} else if (msg.type === "file") {
27042724
// Save uploaded file to disk so the text agent can use it
27052725
const uploadsDir = join(agentRoot, "workspace");
@@ -2719,15 +2739,15 @@ a{color:#58a6ff;}</style></head>
27192739
text: `${userText} [Attached: ${safeName}${relPath}]`.trim(),
27202740
});
27212741
}
2722-
adapter.send(msg);
2742+
adapter?.send(msg);
27232743
} catch {
27242744
// Ignore unparseable messages
27252745
}
27262746
});
27272747

27282748
browserWs.on("close", () => {
27292749
console.log(dim("[voice] Browser disconnected"));
2730-
adapter.disconnect().catch(() => {});
2750+
adapter?.disconnect().catch(() => {});
27312751
// Summarize chat history, save mood, and write journal — track promises for graceful shutdown
27322752
const p = Promise.allSettled([
27332753
summarizeHistory(opts.agentDir, activeBranch).catch((err) => {

src/voice/ui.html

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,12 +985,66 @@
985985

986986
@keyframes fade-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
987987
@media (max-width: 700px) {
988+
/* ── Header ── */
989+
.header { padding: 0 8px; height: 36px; min-height: 36px; }
990+
.header-left { gap: 6px; flex: 1; min-width: 0; }
991+
.header-left h1 { display: none; }
992+
.header-right { gap: 8px; }
993+
.audit-toggle span:last-child { display: none; }
994+
.audit-toggle { padding: 4px 6px; }
995+
996+
/* ── Tabs — scrollable strip ── */
997+
.view-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; flex-shrink: 1; min-width: 0; scrollbar-width: none; }
998+
.view-tabs::-webkit-scrollbar { display: none; }
999+
.view-tab { padding: 3px 10px; font-size: 10px; white-space: nowrap; flex-shrink: 0; }
1000+
1001+
/* ── Chat layout — stack vertically ── */
9881002
.chat-view { flex-direction: column; }
989-
.panel-cam { width: 100%; min-width: 0; border-right: none; border-bottom: 1px solid #21262d; }
1003+
.panel-cam { width: 100%; min-width: 0; border-right: none; border-bottom: 1px solid #21262d; padding: 10px; gap: 10px; }
9901004
.camera-container { aspect-ratio: 16/9; }
1005+
1006+
/* ── Controls — larger touch targets ── */
1007+
.controls { gap: 6px; }
1008+
.ctrl-btn { min-height: 44px; padding: 10px 12px; font-size: 12px; }
1009+
.speaker-toggle { min-height: 44px; font-size: 12px; }
1010+
1011+
/* ── Vitals — compact ── */
1012+
.agent-vitals { font-size: 10px; }
1013+
.vital-value { font-size: 14px; }
1014+
.vital-label { font-size: 7px; }
1015+
.vitals-wave { height: 28px; }
1016+
1017+
/* ── Conversation ── */
1018+
.conversation { padding: 10px 12px; font-size: 12px; }
1019+
1020+
/* ── Input bar — full width, tappable ── */
1021+
.input-bar { padding: 8px 10px; gap: 6px; }
1022+
.input-bar input { min-height: 44px; font-size: 14px; padding: 10px 12px; }
1023+
.input-bar button { min-height: 44px; padding: 10px 14px; }
1024+
1025+
/* ── Chat sidebar — overlay when expanded ── */
1026+
.chat-sidebar { position: absolute; left: 0; top: 0; bottom: 0; z-index: 60; width: 260px; min-width: 260px; }
1027+
.chat-sidebar.collapsed { width: 0; min-width: 0; }
1028+
1029+
/* ── File sidebar / Files panel ── */
9911030
.activity-bar { display: none; }
992-
.file-sidebar { width: 100%; min-width: 0; max-height: 40vh; border-right: none; border-bottom: 1px solid #21262d; }
1031+
.file-sidebar { display: none; }
9931032
.files-panel { position: absolute; right: 0; top: 0; bottom: 0; z-index: 50; width: 280px; min-width: 280px; }
1033+
1034+
/* ── SkillFlows — stack panels ── */
1035+
.flows-view { flex-direction: column; }
1036+
.flows-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; border-bottom: 1px solid #21262d; }
1037+
1038+
/* ── Scheduler — stack panels ── */
1039+
.scheduler-view { flex-direction: column; }
1040+
.scheduler-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; }
1041+
1042+
/* ── Comms — stack panels ── */
1043+
.comms-view { flex-direction: column; overflow-y: auto; }
1044+
.comms-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; }
1045+
1046+
/* ── Settings — already responsive via max-width ── */
1047+
.settings-view { padding: 12px; }
9941048
}
9951049
</style>
9961050
</head>
@@ -1041,6 +1095,9 @@ <h1>Gitclaw: {{AGENT_NAME}}</h1>
10411095
</div>
10421096
</div>
10431097
<div class="chat-view" id="chatView">
1098+
<div id="voiceWarning" style="display:none;padding:8px 14px;margin:8px 8px 0;background:rgba(210,153,34,0.12);border:1px solid rgba(210,153,34,0.3);border-radius:6px;color:#d29922;font-size:12px;">
1099+
Voice mode unavailable — no API key set. Use the <a href="#" onclick="switchView('settings');return false;" style="color:#58a6ff;text-decoration:underline;">Settings</a> tab to add your key. Text chat works normally.
1100+
</div>
10441101
<div class="panel-cam">
10451102
<div class="camera-container">
10461103
<div class="camera-off" id="cameraOff">Camera off</div>
@@ -1441,6 +1498,8 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
14411498
// Notify the iframe that install succeeded
14421499
var frame=document.getElementById('skillsFrame');
14431500
if(frame&&frame.contentWindow)frame.contentWindow.postMessage({type:'install_success',source:source},'*');
1501+
// Refresh SkillFlows so newly installed skill appears immediately
1502+
loadFlowSkills();
14441503
} else {
14451504
alert('Install failed: '+(d.error||'Unknown error'));
14461505
}
@@ -1939,6 +1998,14 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
19391998
case'audio_delta':playAudioDelta(m.audio);break;
19401999
case'interrupt':flushAudio();break;
19412000
case'transcript':
2001+
if(m.role==='assistant'&&m.text&&m.text.indexOf('Voice mode unavailable')===0){
2002+
document.getElementById('voiceWarning').style.display='block';
2003+
var _mic=document.getElementById('micBtn');if(_mic)_mic.style.display='none';
2004+
var _cam=document.getElementById('cameraBtn');if(_cam)_cam.style.display='none';
2005+
var _scr=document.getElementById('screenBtn');if(_scr)_scr.style.display='none';
2006+
var _spk=document.querySelector('.speaker-toggle');if(_spk)_spk.style.display='none';
2007+
break;
2008+
}
19422009
if(m.role==='user'&&!m.partial){
19432010
var isTg=m.text&&m.text.startsWith('[Telegram]');
19442011
var isWa=m.text&&m.text.startsWith('[WhatsApp]');

0 commit comments

Comments
 (0)