handleVideoModeChange('off')}
+ callStatus={
+ videoChatMode === 'call'
+ ? tts.state !== 'idle'
+ ? 'speaking'
+ : activeIsProcessing
+ ? 'thinking'
+ : 'listening'
+ : undefined
+ }
+ interimText={videoChatMode === 'call' ? voice.interimText : undefined}
+ />
)}
{/* Talking head hovers over the active view while avatar voice mode is
on (hidden during the tour, which shows its own mascot) */}
diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
index 25d1be5e..7dd73aed 100644
--- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
+++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
@@ -239,9 +239,12 @@ interface ChatInputInnerProps {
onTtsModeChange?: (mode: 'summary' | 'full') => void
ttsAvatarEnabled?: boolean
onToggleTtsAvatar?: () => void
- /** Video chat mode: live webcam frames are attached to each message. */
- videoEnabled?: boolean
- onToggleVideo?: () => void
+ /** Video chat mode: 'chat' attaches webcam frames to messages; 'call' is
+ * fully hands-free — continuous listening, spoken responses. */
+ videoChatMode?: 'off' | 'chat' | 'call'
+ onVideoModeChange?: (mode: 'off' | 'chat' | 'call') => void
+ /** Hands-free call needs both voice input (STT) and voice output (TTS). */
+ videoCallAvailable?: boolean
/** Fired when the user picks a different model in the dropdown (only when no run exists yet). */
onSelectedModelChange?: (model: SelectedModel | null) => void
/** Work directory for this chat (per-chat). Null when none is set. */
@@ -282,8 +285,9 @@ function ChatInputInner({
onTtsModeChange,
ttsAvatarEnabled,
onToggleTtsAvatar,
- videoEnabled,
- onToggleVideo,
+ videoChatMode = 'off',
+ onVideoModeChange,
+ videoCallAvailable,
onSelectedModelChange,
workDir = null,
onWorkDirChange,
@@ -1356,32 +1360,61 @@ function ChatInputInner({
)}
- {onToggleVideo && (
-
-
-
-
-
- {videoEnabled ? 'Video chat on — frames are shared with the assistant' : 'Video chat off'}
-
-
+ {onVideoModeChange && (
+
+
+
+
+
+
+ {videoChatMode === 'off'
+ ? 'Video chat off'
+ : videoChatMode === 'chat'
+ ? 'Video chat on — camera frames are shared with the assistant'
+ : 'Video call on — speak freely, the assistant answers out loud'}
+
+
+ {videoChatMode !== 'off' && (
+
+
+
+
+
+ onVideoModeChange(v as 'chat' | 'call')}
+ >
+ Video + chat
+
+ Video call (hands-free)
+
+
+
+
+ )}
+
)}
{voiceAvailable && onStartRecording && (