feat: add recent paths suggestions and improve UX polish

- Add recent paths hook to persist and suggest working directories
- Update Android icon assets and simplify adaptive icon configuration
- Remove background styling from header icon buttons for cleaner look
- Export expandTilde utility for path normalization in agent creation
- Add horizontal scrollable chips for recent path suggestions
```
This commit is contained in:
Mohamed Boudra
2025-10-23 11:26:36 +02:00
parent 8177aa0c2a
commit 85a0e46ba9
10 changed files with 130 additions and 16 deletions

View File

@@ -24,9 +24,7 @@
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
"foregroundImage": "./assets/images/android-icon-foreground.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -13,6 +13,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { X } from "lucide-react-native";
import { StyleSheet } from "react-native-unistyles";
import { theme as defaultTheme } from "@/styles/theme";
import { useRecentPaths } from "@/hooks/use-recent-paths";
interface CreateAgentModalProps {
isVisible: boolean;
@@ -40,6 +41,7 @@ export function CreateAgentModal({
}: CreateAgentModalProps) {
const insets = useSafeAreaInsets();
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
const { recentPaths, addRecentPath } = useRecentPaths();
const [workingDir, setWorkingDir] = useState("");
const [selectedMode, setSelectedMode] = useState("plan");
@@ -56,13 +58,23 @@ export function CreateAgentModal({
};
});
function handleCreate() {
async function handleCreate() {
if (!workingDir.trim()) {
setErrorMessage("Working directory is required");
return;
}
onCreateAgent(workingDir.trim(), selectedMode);
const path = workingDir.trim();
// Save to recent paths
try {
await addRecentPath(path);
} catch (error) {
console.error("[CreateAgentModal] Failed to save recent path:", error);
// Continue anyway - don't block agent creation
}
onCreateAgent(path, selectedMode);
handleClose();
}
@@ -123,6 +135,28 @@ export function CreateAgentModal({
Absolute path to the project directory
</Text>
)}
{/* Recent Paths Chips */}
{recentPaths.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.recentPathsContainer}
keyboardShouldPersistTaps="handled"
>
{recentPaths.map((path) => (
<Pressable
key={path}
style={styles.recentPathChip}
onPress={() => setWorkingDir(path)}
>
<Text style={styles.recentPathChipText} numberOfLines={1}>
{path}
</Text>
</Pressable>
))}
</ScrollView>
)}
</View>
{/* Mode Selector */}
@@ -339,4 +373,23 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
},
recentPathsContainer: {
flexDirection: "row",
gap: theme.spacing[2],
paddingVertical: theme.spacing[3],
},
recentPathChip: {
backgroundColor: theme.colors.muted,
borderRadius: theme.borderRadius.full,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
maxWidth: 200,
},
recentPathChipText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
},
}));

View File

@@ -12,11 +12,9 @@ export function EmptyState({ onCreateAgent }: EmptyStateProps) {
return (
<View style={styles.container}>
<Text style={styles.title}>Hammock</Text>
<Text style={styles.subtitle}>
What would you like to work on?
</Text>
<Text style={styles.subtitle}>What would you like to work on?</Text>
<Pressable onPress={onCreateAgent} style={styles.button}>
<Plus size={20} color={theme.colors.foreground} />
<Plus size={20} color={styles.buttonText.color} />
<Text style={styles.buttonText}>New agent</Text>
</Pressable>
</View>

View File

@@ -34,10 +34,7 @@ export function HomeHeader({ onCreateAgent }: HomeHeaderProps) {
>
<MessageSquare size={20} color={theme.colors.foreground} />
</Pressable>
<Pressable
onPress={onCreateAgent}
style={styles.iconButton}
>
<Pressable onPress={onCreateAgent} style={styles.iconButton}>
<Plus size={20} color={theme.colors.foreground} />
</Pressable>
</View>
@@ -74,6 +71,5 @@ const styles = StyleSheet.create((theme) => ({
iconButton: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.muted,
},
}));

View File

@@ -0,0 +1,68 @@
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = '@voice-dev:recent-paths';
const MAX_RECENT_PATHS = 3;
export interface UseRecentPathsReturn {
recentPaths: string[];
isLoading: boolean;
addRecentPath: (path: string) => Promise<void>;
clearRecentPaths: () => Promise<void>;
}
export function useRecentPaths(): UseRecentPathsReturn {
const [recentPaths, setRecentPaths] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Load recent paths from AsyncStorage on mount
useEffect(() => {
loadRecentPaths();
}, []);
async function loadRecentPaths() {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored) as string[];
setRecentPaths(parsed);
}
} catch (error) {
console.error('[RecentPaths] Failed to load recent paths:', error);
// Continue with empty array
} finally {
setIsLoading(false);
}
}
const addRecentPath = useCallback(async (path: string) => {
try {
// Remove duplicates and add to front
const filtered = recentPaths.filter((p) => p !== path);
const updated = [path, ...filtered].slice(0, MAX_RECENT_PATHS);
setRecentPaths(updated);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
} catch (error) {
console.error('[RecentPaths] Failed to save recent path:', error);
throw error;
}
}, [recentPaths]);
const clearRecentPaths = useCallback(async () => {
try {
setRecentPaths([]);
await AsyncStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('[RecentPaths] Failed to clear recent paths:', error);
throw error;
}
}, []);
return {
recentPaths,
isLoading,
addRecentPath,
clearRecentPaths,
};
}