diff --git a/packages/app/assets/icons/claude.svg b/packages/app/assets/icons/claude.svg
new file mode 100644
index 000000000..e29f32825
--- /dev/null
+++ b/packages/app/assets/icons/claude.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app/assets/icons/codex.svg b/packages/app/assets/icons/codex.svg
new file mode 100644
index 000000000..50d94d6c1
--- /dev/null
+++ b/packages/app/assets/icons/codex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx
index 5ea5261ae..e301752f8 100644
--- a/packages/app/src/app/agent/[serverId]/[agentId].tsx
+++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx
@@ -673,7 +673,7 @@ function AgentScreenContent({
rightContent={
-
+
}
diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx
index 2314e189a..ee8baa417 100644
--- a/packages/app/src/components/agent-input-area.tsx
+++ b/packages/app/src/components/agent-input-area.tsx
@@ -22,8 +22,13 @@ import { AgentStatusBar } from "./agent-status-bar";
import { RealtimeControls } from "./realtime-controls";
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
import { useSessionStore } from "@/stores/session-store";
-import { MessageInput, type MessagePayload, type ImageAttachment } from "./message-input";
+import {
+ MessageInput,
+ type MessagePayload,
+ type ImageAttachment,
+} from "./message-input";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
+import { Theme } from "@/styles/theme";
type QueuedMessage = {
id: string;
@@ -48,8 +53,12 @@ const MAX_INPUT_WIDTH = 960;
// Android currently crashes inside ViewGroup.dispatchDraw when running Reanimated
// entering/exiting animations (see react-native-reanimated#8422), so guard them.
const SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS = Platform.OS === "android";
-const REALTIME_FADE_IN = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeIn.duration(250);
-const REALTIME_FADE_OUT = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeOut.duration(250);
+const REALTIME_FADE_IN = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS
+ ? undefined
+ : FadeIn.duration(250);
+const REALTIME_FADE_OUT = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS
+ ? undefined
+ : FadeOut.duration(250);
export function AgentInputArea({
agentId,
@@ -219,7 +228,11 @@ export function AgentInputArea({
}
function handleSubmit(payload: MessagePayload) {
- void sendMessageWithContent(payload.text, payload.images, payload.forceSend);
+ void sendMessageWithContent(
+ payload.text,
+ payload.images,
+ payload.forceSend
+ );
}
async function handlePickImage() {
@@ -262,7 +275,8 @@ export function AgentInputArea({
useEffect(() => {
const existing = getDraftInput(agentId);
const isSameText = existing?.text === userInput;
- const existingImages: ImageAttachment[] = (existing?.images ?? []) as ImageAttachment[];
+ const existingImages: ImageAttachment[] = (existing?.images ??
+ []) as ImageAttachment[];
const isSameImages =
existingImages.length === selectedImages.length &&
existingImages.every((img, idx) => {
@@ -387,17 +401,16 @@ export function AgentInputArea({
>
);
- const leftContent = (
-
- );
+ const leftContent = ;
return (
- {/* Border separator */}
-
-
{/* Realtime controls - only when active */}
{isRealtimeMode && (
({
+const BUTTON_SIZE = 40;
+
+const styles = StyleSheet.create(((theme: Theme) => ({
container: {
flexDirection: "column",
position: "relative",
- backgroundColor: theme.colors.background,
},
borderSeparator: {
height: theme.borderWidth[1],
@@ -485,21 +499,20 @@ const styles = StyleSheet.create(((theme: any) => ({
inputAreaContainer: {
position: "relative",
minHeight: FOOTER_HEIGHT,
+ marginHorizontal: "auto",
alignItems: "center",
width: "100%",
overflow: "hidden",
- backgroundColor: theme.colors.background,
+ padding: theme.spacing[4],
},
inputAreaContent: {
width: "100%",
maxWidth: MAX_INPUT_WIDTH,
- paddingHorizontal: theme.spacing[4],
- paddingVertical: theme.spacing[3],
gap: theme.spacing[3],
},
realtimeButton: {
- width: 40,
- height: 40,
+ width: 34,
+ height: 34,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.accentForeground,
alignItems: "center",
@@ -509,8 +522,8 @@ const styles = StyleSheet.create(((theme: any) => ({
backgroundColor: theme.colors.palette.blue[600],
},
cancelButton: {
- width: 40,
- height: 40,
+ width: BUTTON_SIZE,
+ height: BUTTON_SIZE,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.palette.red[500],
alignItems: "center",
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 106d94b60..2154b86e0 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time";
-import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
@@ -89,81 +88,47 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
const renderAgentItem = useCallback>(
({ item: agent }) => {
const timeAgo = formatTimeAgo(agent.lastActivityAt);
- const providerLabel = getAgentProviderDefinition(agent.provider).label;
const isRunning = agent.status === "running";
const isSelected = selectedAgentId === agent.id;
+ const statusColor = isRunning ? "#3b82f6" : agent.requiresAttention ? "#22c55e" : null;
return (
[
+ style={({ pressed, hovered }) => [
styles.agentItem,
isSelected && styles.agentItemSelected,
+ hovered && styles.agentItemHovered,
pressed && styles.agentItemPressed,
]}
onPress={() => handleAgentPress(agent.serverId, agent.id)}
onLongPress={() => handleAgentLongPress(agent)}
>
+ {({ hovered }) => (
-
+
+ {statusColor && (
+
+ )}
{agent.title || "New Agent"}
-
-
- {agent.serverLabel}
-
-
-
- {agent.cwd}
+
+ {agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} ยท {timeAgo}
-
-
-
-
-
- {providerLabel}
-
-
-
- {isRunning ? (
-
-
-
- Running
-
-
- ) : agent.requiresAttention ? (
-
-
-
- Finished
-
-
- ) : null}
-
-
-
- {timeAgo}
-
-
+ )}
);
},
- [handleAgentLongPress, handleAgentPress, selectedAgentId, theme.colors.muted, theme.colors.mutedForeground, theme.colors.primary, theme.colors.primaryForeground]
+ [handleAgentLongPress, handleAgentPress, selectedAgentId]
);
const keyExtractor = useCallback(
@@ -254,89 +219,52 @@ const styles = StyleSheet.create((theme) => ({
paddingBottom: theme.spacing[4],
},
agentItem: {
- paddingVertical: theme.spacing[4],
- paddingHorizontal: theme.spacing[4],
+ paddingVertical: theme.spacing[3],
+ paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[2],
- backgroundColor: theme.colors.muted,
- borderWidth: 1,
- borderColor: "transparent",
+ backgroundColor: theme.colors.palette.zinc[950],
},
agentItemSelected: {
- backgroundColor: theme.colors.accent,
- borderColor: theme.colors.primary,
+ backgroundColor: theme.colors.palette.zinc[800],
+ },
+ agentItemHovered: {
+ backgroundColor: theme.colors.palette.zinc[850],
},
agentItemPressed: {
- opacity: 0.7,
- backgroundColor: theme.colors.accent,
+ backgroundColor: theme.colors.palette.zinc[800],
},
agentContent: {
flex: 1,
+ gap: theme.spacing[1],
},
- titleRow: {
+ row: {
flexDirection: "row",
alignItems: "center",
- justifyContent: "space-between",
gap: theme.spacing[2],
- marginBottom: theme.spacing[1],
},
agentTitle: {
flex: 1,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: {
+ xs: "400",
+ md: "300",
+ },
+ color: theme.colors.palette.zinc[200],
+ },
+ agentTitleHighlighted: {
color: theme.colors.foreground,
},
- hostBadge: {
- borderRadius: theme.borderRadius.full,
- paddingHorizontal: theme.spacing[2],
- paddingVertical: 2,
- },
- hostText: {
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.normal,
- },
- agentDirectory: {
+ secondaryRow: {
fontSize: theme.fontSize.sm,
+ fontWeight: "300",
color: theme.colors.mutedForeground,
- marginBottom: theme.spacing[1],
- },
- statusRow: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- gap: theme.spacing[2],
- },
- statusGroup: {
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[2],
- },
- providerBadge: {
- borderRadius: theme.borderRadius.full,
- },
- providerText: {
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
- textTransform: "uppercase",
- },
- statusBadge: {
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[1],
},
statusDot: {
- width: 6,
- height: 6,
+ width: 8,
+ height: 8,
borderRadius: theme.borderRadius.full,
},
- statusText: {
- fontSize: theme.fontSize.xs,
- fontWeight: theme.fontWeight.semibold,
- },
- timeAgo: {
- fontSize: theme.fontSize.xs,
- color: theme.colors.mutedForeground,
- },
sheetOverlay: {
flex: 1,
justifyContent: "flex-end",
diff --git a/packages/app/src/components/agent-sidebar.tsx b/packages/app/src/components/agent-sidebar.tsx
deleted file mode 100644
index a6622d960..000000000
--- a/packages/app/src/components/agent-sidebar.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import { View, Text, Pressable, ScrollView } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { Plus, X } from "lucide-react-native";
-import { useEffect, useState } from "react";
-import { GestureDetector, Gesture } from "react-native-gesture-handler";
-import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- cancelAnimation,
- runOnJS,
- Easing,
- type SharedValue,
-} from "react-native-reanimated";
-import type { Agent } from "@/contexts/session-context";
-import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status";
-import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
-
-interface AgentSidebarProps {
- isOpen: boolean;
- agents: Agent[];
- activeAgentId: string | null;
- onClose: () => void;
- onSelectAgent: (serverId: string, agentId: string) => void;
- onNewAgent: () => void;
- edgeSwipeTranslateX?: SharedValue | null;
-}
-
-export function AgentSidebar({
- isOpen,
- agents,
- activeAgentId,
- onClose,
- onSelectAgent,
- onNewAgent,
- edgeSwipeTranslateX,
-}: AgentSidebarProps) {
- const { theme } = useUnistyles();
- const insets = useSafeAreaInsets();
- const savedTranslateX = useSharedValue(0);
- const [isVisible, setIsVisible] = useState(isOpen);
- const fallbackTranslateX = useSharedValue(-300);
- const translateX = edgeSwipeTranslateX ?? fallbackTranslateX;
-
- useEffect(() => {
- if (isOpen) {
- setIsVisible(true);
- // Only animate if starting from fully closed position (not mid-animation from edge swipe)
- if (translateX.value === -300) {
- translateX.value = withTiming(0, {
- duration: 150,
- easing: Easing.out(Easing.ease),
- });
- }
- } else {
- // Only animate if starting from fully open position
- if (translateX.value === 0) {
- translateX.value = withTiming(-300, {
- duration: 150,
- easing: Easing.out(Easing.ease),
- }, (finished) => {
- if (finished) {
- runOnJS(setIsVisible)(false);
- }
- });
- } else if (translateX.value === -300) {
- // Already closed, just hide
- setIsVisible(false);
- }
- }
- }, [isOpen]);
-
- function handleAgentSelect(agentId: string, serverId: string) {
- onSelectAgent(serverId, agentId);
- onClose();
- }
-
- // Pan gesture for closing sidebar (swipe left on the sidebar)
- const closeGesture = Gesture.Pan()
- .enabled(isOpen) // Only active when sidebar is open
- .activeOffsetX(-10) // Require leftward swipe
- .onBegin(() => {
- cancelAnimation(translateX);
- savedTranslateX.value = translateX.value;
- })
- .onChange((event) => {
- // Directly update translateX - clamp between -300 and 0
- const newTranslateX = Math.max(-300, Math.min(0, savedTranslateX.value + event.translationX));
- translateX.value = newTranslateX;
- })
- .onEnd((event) => {
- const shouldClose = translateX.value < -150 || event.velocityX < -500;
-
- if (shouldClose) {
- translateX.value = withTiming(-300, {
- duration: 150,
- easing: Easing.out(Easing.ease),
- }, (finished) => {
- if (finished) {
- runOnJS(setIsVisible)(false);
- runOnJS(onClose)();
- }
- });
- } else {
- translateX.value = withTiming(0, {
- duration: 150,
- easing: Easing.out(Easing.ease),
- });
- }
- });
-
- const animatedSidebarStyle = useAnimatedStyle(() => ({
- transform: [{ translateX: translateX.value }],
- }));
-
- const animatedBackdropStyle = useAnimatedStyle(() => {
- // Map translateX (-300 to 0) to opacity (0 to 1)
- const opacity = (translateX.value + 300) / 300;
- return { opacity };
- });
-
- // Don't render if not visible
- if (!isVisible) {
- return null;
- }
-
- return (
- <>
- {/* Backdrop - fade in/out with sidebar */}
-
-
-
-
- {/* Sidebar with close gesture */}
-
-
- {/* Header */}
-
-
- Agents
-
-
-
-
-
-
- {/* New Agent Button */}
- {
- onNewAgent();
- onClose();
- }}
- >
-
-
- New Agent
-
-
-
- {/* Agent List */}
-
- {agents.map((agent) => {
- const isActive = agent.id === activeAgentId;
- const statusColor = getAgentStatusColor(agent.status);
- const statusLabel = getAgentStatusLabel(agent.status);
- const providerLabel = getAgentProviderDefinition(agent.provider).label;
-
- return (
- handleAgentSelect(agent.id, agent.serverId)}
- >
-
-
- {agent.title || "New Agent"}
-
-
-
-
- {agent.cwd}
-
-
-
-
-
- {providerLabel}
-
-
-
-
-
- {statusLabel}
-
-
-
-
-
- );
- })}
-
- {agents.length === 0 && (
-
-
- No agents yet
-
-
- )}
-
-
-
- >
- );
-}
-
-const styles = StyleSheet.create({
- backdrop: {
- flex: 1,
- backgroundColor: "rgba(0, 0, 0, 0.5)",
- zIndex: 998,
- },
- sidebar: {
- position: "absolute",
- left: 0,
- top: 0,
- bottom: 0,
- width: 300,
- zIndex: 999,
- paddingHorizontal: 16,
- },
- header: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: 16,
- },
- headerTitle: {
- fontSize: 24,
- fontWeight: "700",
- },
- closeButton: {
- padding: 4,
- },
- newAgentButton: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderRadius: 8,
- marginBottom: 16,
- gap: 8,
- },
- newAgentText: {
- fontSize: 16,
- fontWeight: "600",
- },
- agentList: {
- flex: 1,
- },
- agentItem: {
- paddingVertical: 12,
- paddingHorizontal: 12,
- borderRadius: 8,
- marginBottom: 4,
- },
- agentContent: {
- flex: 1,
- },
- agentTitle: {
- fontSize: 15,
- fontWeight: "500",
- marginBottom: 2,
- },
- directoryRow: {
- flexDirection: "row",
- alignItems: "center",
- gap: 8,
- },
- agentDirectory: {
- flex: 1,
- fontSize: 12,
- },
- statusBadge: {
- flexDirection: "row",
- alignItems: "center",
- gap: 4,
- },
- providerBadge: {
- paddingHorizontal: 8,
- paddingVertical: 2,
- borderRadius: 999,
- },
- providerText: {
- fontSize: 11,
- fontWeight: "600",
- },
- statusDot: {
- width: 6,
- height: 6,
- borderRadius: 3,
- },
- statusText: {
- fontSize: 11,
- fontWeight: "500",
- },
- emptyState: {
- paddingVertical: 32,
- alignItems: "center",
- },
- emptyText: {
- fontSize: 14,
- },
-});
diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx
index 682505509..c48ea2c7a 100644
--- a/packages/app/src/components/agent-status-bar.tsx
+++ b/packages/app/src/components/agent-status-bar.tsx
@@ -19,8 +19,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
);
// Get the setAgentMode action (actions are stable, won't cause rerenders)
- const setAgentMode = useSessionStore((state) =>
- state.sessions[serverId]?.methods?.setAgentMode
+ const setAgentMode = useSessionStore(
+ (state) => state.sessions[serverId]?.methods?.setAgentMode
);
const [showModeSelector, setShowModeSelector] = useState(false);
@@ -47,7 +47,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
]}
>
- {agent.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
+ {agent.availableModes?.find((m) => m.id === agent.currentModeId)
+ ?.label ||
agent.currentModeId ||
"default"}
@@ -75,14 +76,14 @@ const styles = StyleSheet.create((theme) => ({
modeBadge: {
flexDirection: "row",
alignItems: "center",
- gap: theme.spacing[2],
+ backgroundColor: theme.colors.popover,
+ gap: theme.spacing[1],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
- backgroundColor: theme.colors.muted,
- borderRadius: theme.borderRadius.full,
+ borderRadius: theme.borderRadius["2xl"],
},
modeBadgePressed: {
- backgroundColor: theme.colors.accent,
+ backgroundColor: theme.colors.background,
},
modeBadgeText: {
color: theme.colors.mutedForeground,
diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx
index 5d34a566d..c40773ebf 100644
--- a/packages/app/src/components/dictation-controls.tsx
+++ b/packages/app/src/components/dictation-controls.tsx
@@ -24,7 +24,9 @@ interface DictationControlsProps {
function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
- return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
+ return `${mins.toString().padStart(2, "0")}:${secs
+ .toString()
+ .padStart(2, "0")}`;
}
export function DictationControls({
@@ -122,7 +124,9 @@ export function DictationControls({
)}
{retryStatusText && (isRetrying || isFailed) && (
-
+
{retryStatusText}
)}
@@ -158,7 +162,12 @@ export function DictationOverlay({
}
return (
-
+
-
+
{formatDuration(duration)}
@@ -186,28 +200,52 @@ export function DictationOverlay({
{actionsDisabled ? (
-
+
) : isFailed ? (
-
+
) : (
<>
-
+
-
+
>
)}
@@ -290,6 +328,8 @@ const overlayStyles = StyleSheet.create((theme) => ({
container: {
flexDirection: "row",
alignItems: "center",
+ width: "100%",
+ borderRadius: theme.borderRadius["2xl"],
justifyContent: "space-between",
paddingHorizontal: theme.spacing[4],
paddingVertical: OVERLAY_VERTICAL_PADDING,
diff --git a/packages/app/src/components/headers/back-header.tsx b/packages/app/src/components/headers/back-header.tsx
index a43d22f82..a70c723da 100644
--- a/packages/app/src/components/headers/back-header.tsx
+++ b/packages/app/src/components/headers/back-header.tsx
@@ -22,7 +22,7 @@ export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
onPress={onBack ?? (() => router.back())}
style={styles.backButton}
>
-
+
{title && (
@@ -39,16 +39,22 @@ export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
const styles = StyleSheet.create((theme) => ({
left: {
- gap: theme.spacing[3],
+ gap: theme.spacing[2],
},
backButton: {
- padding: theme.spacing[3],
+ padding: {
+ xs: theme.spacing[3],
+ md: theme.spacing[2],
+ },
borderRadius: theme.borderRadius.lg,
},
title: {
flex: 1,
fontSize: theme.fontSize.lg,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: {
+ xs: theme.fontWeight.semibold,
+ md: "400",
+ },
color: theme.colors.foreground,
},
}));
diff --git a/packages/app/src/components/headers/home-header.tsx b/packages/app/src/components/headers/home-header.tsx
index 069fbfdde..4af7603db 100644
--- a/packages/app/src/components/headers/home-header.tsx
+++ b/packages/app/src/components/headers/home-header.tsx
@@ -51,7 +51,10 @@ export function HomeHeader({ onCreateAgent, onImportAgent }: HomeHeaderProps) {
const styles = StyleSheet.create((theme) => ({
iconButton: {
- padding: theme.spacing[3],
+ padding: {
+ xs: theme.spacing[3],
+ md: theme.spacing[2],
+ },
borderRadius: theme.borderRadius.lg,
},
}));
diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx
index 907eb263e..33a78d308 100644
--- a/packages/app/src/components/headers/screen-header.tsx
+++ b/packages/app/src/components/headers/screen-header.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { View, type StyleProp, type ViewStyle } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
interface ScreenHeaderProps {
left?: ReactNode;
@@ -17,10 +17,11 @@ interface ScreenHeaderProps {
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
const insets = useSafeAreaInsets();
const { theme } = useUnistyles();
+ const topPadding = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm" ? 8 : 4;
return (
-
+
{left}
{right}
@@ -39,8 +40,14 @@ const styles = StyleSheet.create((theme) => ({
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
- paddingHorizontal: theme.spacing[3],
- paddingBottom: theme.spacing[2],
+ paddingHorizontal: {
+ xs: theme.spacing[2],
+ md: theme.spacing[1],
+ },
+ paddingBottom: {
+ xs: theme.spacing[2],
+ md: theme.spacing[1],
+ },
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
},
diff --git a/packages/app/src/components/icons/claude-icon.tsx b/packages/app/src/components/icons/claude-icon.tsx
new file mode 100644
index 000000000..17881ba8b
--- /dev/null
+++ b/packages/app/src/components/icons/claude-icon.tsx
@@ -0,0 +1,14 @@
+import Svg, { Path } from "react-native-svg";
+
+interface ClaudeIconProps {
+ size?: number;
+ color?: string;
+}
+
+export function ClaudeIcon({ size = 16, color = "currentColor" }: ClaudeIconProps) {
+ return (
+
+ );
+}
diff --git a/packages/app/src/components/icons/codex-icon.tsx b/packages/app/src/components/icons/codex-icon.tsx
new file mode 100644
index 000000000..c12b5f3f0
--- /dev/null
+++ b/packages/app/src/components/icons/codex-icon.tsx
@@ -0,0 +1,14 @@
+import Svg, { Path } from "react-native-svg";
+
+interface CodexIconProps {
+ size?: number;
+ color?: string;
+}
+
+export function CodexIcon({ size = 16, color = "currentColor" }: CodexIconProps) {
+ return (
+
+ );
+}
diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx
index 0e7911c37..3007015eb 100644
--- a/packages/app/src/components/message-input.tsx
+++ b/packages/app/src/components/message-input.tsx
@@ -7,6 +7,7 @@ import {
TextInputKeyPressEventData,
Image,
Platform,
+ BackHandler,
} from "react-native";
import { useState, useRef, useCallback, useEffect } from "react";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -57,7 +58,7 @@ export interface MessageInputProps {
onQueue?: (payload: MessagePayload) => void;
}
-const MIN_INPUT_HEIGHT = 50;
+const MIN_INPUT_HEIGHT = 30;
const MAX_INPUT_HEIGHT = 160;
const IS_WEB = Platform.OS === "web";
@@ -98,7 +99,9 @@ export function MessageInput({
}: MessageInputProps) {
const { theme } = useUnistyles();
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
- const textInputRef = useRef unknown }) | null>(null);
+ const textInputRef = useRef<
+ TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
+ >(null);
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
const baselineInputHeightRef = useRef(null);
const overlayTransition = useSharedValue(0);
@@ -188,8 +191,14 @@ export function MessageInput({
// Animate overlay
useEffect(() => {
- const showOverlay = isDictating || isDictationProcessing || dictationStatus === "retrying" || dictationStatus === "failed";
- overlayTransition.value = withTiming(showOverlay ? 1 : 0, { duration: 200 });
+ const showOverlay =
+ isDictating ||
+ isDictationProcessing ||
+ dictationStatus === "retrying" ||
+ dictationStatus === "failed";
+ overlayTransition.value = withTiming(showOverlay ? 1 : 0, {
+ duration: 200,
+ });
}, [isDictating, isDictationProcessing, dictationStatus, overlayTransition]);
const overlayAnimatedStyle = useAnimatedStyle(() => ({
@@ -234,7 +243,10 @@ export function MessageInput({
const handleSendMessage = useCallback(() => {
const trimmed = value.trim();
if (!trimmed && images.length === 0) return;
- const payload = { text: trimmed, images: images.length > 0 ? images : undefined };
+ const payload = {
+ text: trimmed,
+ images: images.length > 0 ? images : undefined,
+ };
if (isAgentRunning && onQueue) {
onQueue(payload);
} else {
@@ -286,7 +298,10 @@ export function MessageInput({
const baseline = baselineInputHeightRef.current ?? MIN_INPUT_HEIGHT;
const rawTarget = scrollHeight > 0 ? scrollHeight : baseline;
- const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, rawTarget));
+ const bounded = Math.max(
+ MIN_INPUT_HEIGHT,
+ Math.min(MAX_INPUT_HEIGHT, rawTarget)
+ );
if (Math.abs(inputHeightRef.current - bounded) >= 1) {
inputHeightRef.current = bounded;
@@ -297,7 +312,10 @@ export function MessageInput({
}
function setBoundedInputHeight(nextHeight: number) {
- const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight));
+ const bounded = Math.max(
+ MIN_INPUT_HEIGHT,
+ Math.min(MAX_INPUT_HEIGHT, nextHeight)
+ );
if (Math.abs(inputHeightRef.current - bounded) < 1) return;
inputHeightRef.current = bounded;
setInputHeight(bounded);
@@ -364,9 +382,15 @@ export function MessageInput({
{images.map((image, index) => (
-
+
{onRemoveImage && (
- onRemoveImage(index)} style={styles.removeImageButton}>
+ onRemoveImage(index)}
+ style={styles.removeImageButton}
+ >
)}
@@ -384,14 +408,20 @@ export function MessageInput({
placeholderTextColor={theme.colors.mutedForeground}
style={[
styles.textInput,
- { height: inputHeight, minHeight: MIN_INPUT_HEIGHT, maxHeight: MAX_INPUT_HEIGHT },
+ {
+ height: inputHeight,
+ minHeight: MIN_INPUT_HEIGHT,
+ maxHeight: MAX_INPUT_HEIGHT,
+ },
]}
multiline
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
onContentSizeChange={handleContentSizeChange}
editable={!isDictating && isConnected && !disabled}
- onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined}
- autoFocus={autoFocus}
+ onKeyPress={
+ shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined
+ }
+ autoFocus={IS_WEB && autoFocus}
/>
{/* Button row */}
@@ -418,10 +448,19 @@ export function MessageInput({
{shouldShowSendButton && (
@@ -458,8 +497,16 @@ export function MessageInput({
onCancel={handleCancelRecording}
onAccept={handleAcceptRecording}
onAcceptAndSend={handleAcceptAndSendRecording}
- onRetry={dictationStatus === "failed" ? handleRetryFailedRecording : undefined}
- onDiscard={dictationStatus === "failed" ? handleDiscardFailedRecording : undefined}
+ onRetry={
+ dictationStatus === "failed"
+ ? handleRetryFailedRecording
+ : undefined
+ }
+ onDiscard={
+ dictationStatus === "failed"
+ ? handleDiscardFailedRecording
+ : undefined
+ }
/>
@@ -473,6 +520,18 @@ const styles = StyleSheet.create(((theme: any) => ({
inputWrapper: {
flexDirection: "column",
gap: theme.spacing[3],
+ backgroundColor: theme.colors.muted,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.accentBorder,
+ borderRadius: theme.borderRadius["2xl"],
+ paddingVertical: {
+ xs: theme.spacing[2],
+ md: theme.spacing[4],
+ },
+ paddingHorizontal: {
+ xs: theme.spacing[3],
+ md: theme.spacing[4],
+ },
},
imagePreviewContainer: {
flexDirection: "row",
@@ -500,12 +559,9 @@ const styles = StyleSheet.create(((theme: any) => ({
},
textInput: {
width: "100%",
- paddingHorizontal: theme.spacing[4],
- paddingVertical: theme.spacing[3],
- backgroundColor: theme.colors.muted,
- borderRadius: theme.borderRadius.lg,
color: theme.colors.foreground,
fontSize: theme.fontSize.lg,
+ fontWeight: theme.fontWeight.normal,
lineHeight: theme.fontSize.lg * 1.4,
...(IS_WEB
? {
@@ -531,16 +587,15 @@ const styles = StyleSheet.create(((theme: any) => ({
gap: theme.spacing[2],
},
attachButton: {
- width: 40,
- height: 40,
+ width: 20,
+ height: 20,
alignItems: "center",
justifyContent: "center",
},
voiceButton: {
- width: 40,
- height: 40,
+ width: 34,
+ height: 34,
borderRadius: theme.borderRadius.full,
- backgroundColor: "transparent",
alignItems: "center",
justifyContent: "center",
},
@@ -548,8 +603,8 @@ const styles = StyleSheet.create(((theme: any) => ({
backgroundColor: theme.colors.destructive,
},
sendButton: {
- width: 40,
- height: 40,
+ width: 34,
+ height: 34,
borderRadius: theme.borderRadius.full,
backgroundColor: theme.colors.palette.blue[600],
alignItems: "center",
@@ -560,8 +615,13 @@ const styles = StyleSheet.create(((theme: any) => ({
},
overlayContainer: {
position: "absolute",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
top: 0,
left: 0,
+ width: "100%",
+ height: "100%",
right: 0,
bottom: 0,
},
diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts
index efa6514da..602f1a5d7 100644
--- a/packages/app/src/styles/theme.ts
+++ b/packages/app/src/styles/theme.ts
@@ -14,7 +14,9 @@ export const baseColors = {
600: "#52525b",
700: "#3f3f46",
800: "#27272a",
+ 850: "#1a1a1d",
900: "#18181b",
+ 950: "#121214",
},
// Gray scale
@@ -120,6 +122,7 @@ const lightSemanticColors = {
destructive: "#ef4444",
destructiveForeground: "#fafafa",
border: "#e4e4e7",
+ accentBorder: "#3f3f46",
input: "#e4e4e7",
ring: "#18181b",
} as const;
@@ -141,7 +144,8 @@ const darkSemanticColors = {
accentForeground: "#fafafa",
destructive: "#7f1d1d",
destructiveForeground: "#fafafa",
- border: "#27272a",
+ border: "#27272e",
+ accentBorder: "#414146",
input: "#27272a",
ring: "#d4d4d8",
} as const;