diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx
index 6e61e2c04..f1b062432 100644
--- a/packages/app/src/components/agent-input-area.tsx
+++ b/packages/app/src/components/agent-input-area.tsx
@@ -7,11 +7,10 @@ import {
} from "react-native";
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { ArrowUp, AudioLines, MicOff, Square, Pencil } from "lucide-react-native";
+import { ArrowUp, Square, Pencil } from "lucide-react-native";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useVoice } from "@/contexts/voice-context";
import { useIsFocused } from "@react-navigation/native";
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
import { generateMessageId } from "@/types/stream";
@@ -94,8 +93,6 @@ export function AgentInputArea({
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
- const { isVoiceMode, isMuted: isVoiceMuted, toggleMute: toggleVoiceMute } = useVoice();
-
const [internalInput, setInternalInput] = useState("");
const userInput = value ?? internalInput;
const setUserInput = onChangeText ?? setInternalInput;
@@ -573,35 +570,6 @@ export function AgentInputArea({
/>
)}
- {/* Voice quick mute indicator */}
- {isVoiceMode && (
-
-
- {isVoiceMuted ? (
-
- ) : (
-
- )}
-
- {isVoiceMuted ? "Voice muted" : "Voice on"}
-
-
-
- )}
-
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
({
buttonDisabled: {
opacity: 0.5,
},
- voiceIndicatorRow: {
- flexDirection: "row",
- justifyContent: "flex-end",
- },
- voiceIndicatorPill: {
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- paddingHorizontal: theme.spacing[3],
- height: 32,
- borderRadius: theme.borderRadius.full,
- backgroundColor: theme.colors.surface2,
- borderWidth: theme.borderWidth[1],
- borderColor: theme.colors.border,
- },
- voiceIndicatorPillMuted: {
- backgroundColor: theme.colors.palette.red[600],
- borderColor: theme.colors.palette.red[800],
- },
- voiceIndicatorText: {
- color: theme.colors.foreground,
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.medium,
- },
- voiceIndicatorTextMuted: {
- color: theme.colors.surface0,
- },
queueContainer: {
flexDirection: "column",
gap: theme.spacing[2],
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 3418ddfdb..8b4077db0 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -52,6 +52,7 @@ import { ToolCallSheetProvider } from "./tool-call-sheet";
import { createMarkdownStyles } from "@/styles/markdown-styles";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
+import { VoiceCompactIndicator } from "./voice-compact-indicator";
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
const isToolSequenceItem = (item?: StreamItem) =>
@@ -541,6 +542,7 @@ export function AgentStreamView({
{showWorkingIndicator ? (
+
) : null}
@@ -1179,8 +1181,11 @@ const stylesheet = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
},
workingIndicatorWrapper: {
- alignItems: "flex-start",
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
paddingLeft: 3,
+ paddingRight: 3,
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[2],
},
diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx
index 5ef241307..bd2c96967 100644
--- a/packages/app/src/components/dictation-controls.tsx
+++ b/packages/app/src/components/dictation-controls.tsx
@@ -179,6 +179,7 @@ export function DictationOverlay({
isDetecting
isSpeaking={false}
orientation="horizontal"
+ color={theme.colors.palette.white}
/>
-
-
-
-
- Choose a host
- {voiceEligibleHosts.map((entry) => (
- handleSelectVoiceHost(entry.daemon.id)}
- >
- {entry.daemon.label}
-
- ))}
-
- Cancel
+
+ {voiceEligibleHosts.map((entry) => (
+ handleSelectVoiceHost(entry.daemon.id)}
+ >
+ {entry.daemon.label}
-
+ ))}
+
+ Cancel
+
-
+
);
}
@@ -460,6 +461,32 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
+
+
+
+ {voiceEligibleHosts.map((entry) => (
+ handleSelectVoiceHost(entry.daemon.id)}
+ >
+ {entry.daemon.label}
+
+ ))}
+
+ Cancel
+
+
+
);
}
@@ -547,33 +574,10 @@ const styles = StyleSheet.create((theme) => ({
footerButtonTextHovered: {
color: theme.colors.foreground,
},
- hostPickerOverlay: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: "rgba(0, 0, 0, 0.5)",
- padding: theme.spacing[4],
- },
- hostPickerBackdrop: {
- ...StyleSheet.absoluteFillObject,
- },
- hostPickerContainer: {
- width: "100%",
- maxWidth: 360,
- backgroundColor: theme.colors.surface0,
- borderRadius: theme.borderRadius["2xl"],
- padding: theme.spacing[4],
- borderWidth: theme.borderWidth[1],
- borderColor: theme.colors.border,
+ hostPickerList: {
gap: theme.spacing[2],
},
- hostPickerTitle: {
- fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
- color: theme.colors.foreground,
- marginBottom: theme.spacing[2],
- },
- hostPickerButton: {
+ hostPickerOption: {
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
@@ -581,12 +585,11 @@ const styles = StyleSheet.create((theme) => ({
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
- hostPickerButtonText: {
+ hostPickerOptionText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
hostPickerCancel: {
- marginTop: theme.spacing[2],
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
diff --git a/packages/app/src/components/voice-compact-indicator.tsx b/packages/app/src/components/voice-compact-indicator.tsx
new file mode 100644
index 000000000..e1215d85c
--- /dev/null
+++ b/packages/app/src/components/voice-compact-indicator.tsx
@@ -0,0 +1,76 @@
+import { Pressable, View } from "react-native";
+import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { AudioLines, MicOff } from "lucide-react-native";
+import { VolumeMeter } from "@/components/volume-meter";
+import { useVoice } from "@/contexts/voice-context";
+
+export function VoiceCompactIndicator() {
+ const { theme } = useUnistyles();
+ const { isVoiceMode, volume, isMuted, isDetecting, isSpeaking, toggleMute } =
+ useVoice();
+
+ if (!isVoiceMode) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ {isMuted ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ container: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ paddingLeft: theme.spacing[2],
+ paddingRight: theme.spacing[1],
+ height: 32,
+ borderRadius: theme.borderRadius.full,
+ backgroundColor: theme.colors.surface2,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.border,
+ },
+ meterContainer: {
+ justifyContent: "center",
+ },
+ muteButton: {
+ width: 28,
+ height: 28,
+ borderRadius: theme.borderRadius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: theme.colors.surface0,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.border,
+ },
+ muteButtonMuted: {
+ backgroundColor: theme.colors.palette.red[600],
+ borderColor: theme.colors.palette.red[800],
+ },
+}));
+
diff --git a/packages/app/src/components/voice-panel.tsx b/packages/app/src/components/voice-panel.tsx
index 0043eaad3..c5875f972 100644
--- a/packages/app/src/components/voice-panel.tsx
+++ b/packages/app/src/components/voice-panel.tsx
@@ -35,40 +35,42 @@ export function VoicePanel() {
-
-
-
-
-
-
-
+
+
-
+
- void stopVoice()}
- accessibilityRole="button"
- accessibilityLabel="Stop voice mode"
- style={[styles.iconButton, styles.iconButtonStop]}
- >
-
-
+
+
+
+
+
+ void stopVoice()}
+ accessibilityRole="button"
+ accessibilityLabel="Stop voice mode"
+ style={[styles.iconButton, styles.iconButtonStop]}
+ >
+
+
+
);
@@ -109,7 +111,14 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
- meterRow: {
+ contentRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: theme.spacing[3],
+ },
+ meterContainer: {
+ flex: 1,
justifyContent: "center",
},
actionsRow: {
diff --git a/packages/app/src/components/volume-meter.tsx b/packages/app/src/components/volume-meter.tsx
index 6dab61d88..dd55dd98b 100644
--- a/packages/app/src/components/volume-meter.tsx
+++ b/packages/app/src/components/volume-meter.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useRef } from "react";
+import { useEffect } from "react";
import { View } from "react-native";
import ReanimatedAnimated, {
useSharedValue,
@@ -17,16 +17,29 @@ interface VolumeMeterProps {
isDetecting?: boolean;
isSpeaking?: boolean;
orientation?: "vertical" | "horizontal";
+ variant?: "default" | "compact";
+ color?: string;
}
-export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSpeaking = false, orientation = "vertical" }: VolumeMeterProps) {
+export function VolumeMeter({
+ volume,
+ isMuted = false,
+ isDetecting = false,
+ isSpeaking = false,
+ orientation = "vertical",
+ variant = "default",
+ color,
+}: VolumeMeterProps) {
const { theme } = useUnistyles();
+ const isCompact = variant === "compact";
// Base dimensions
- const LINE_SPACING = 8;
- const LINE_WIDTH = 8;
- const MAX_HEIGHT = orientation === "horizontal" ? 30 : 50;
- const MIN_HEIGHT = orientation === "horizontal" ? 12 : 20;
+ const LINE_SPACING = isCompact ? 6 : 8;
+ const LINE_WIDTH = isCompact ? 6 : 8;
+ const MAX_HEIGHT =
+ orientation === "horizontal" ? (isCompact ? 18 : 30) : isCompact ? 32 : 50;
+ const MIN_HEIGHT =
+ orientation === "horizontal" ? (isCompact ? 8 : 12) : isCompact ? 14 : 20;
// Create shared values for 3 dots unconditionally
const line1Height = useSharedValue(MIN_HEIGHT);
@@ -127,8 +140,9 @@ export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSp
}
}, [volume, isMuted]);
- const lineColor = "#FFFFFF";
- const containerHeight = orientation === "horizontal" ? 60 : 100;
+ const lineColor = color ?? theme.colors.foreground;
+ const containerHeight =
+ orientation === "horizontal" ? (isCompact ? 32 : 60) : isCompact ? 64 : 100;
// Create animated styles unconditionally at top level
const line1Style = useAnimatedStyle(() => {