```
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:
@@ -24,9 +24,7 @@
|
|||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
"foregroundImage": "./assets/images/android-icon-foreground.png"
|
||||||
"backgroundImage": "./assets/images/android-icon-background.png",
|
|
||||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false,
|
"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 |
@@ -13,6 +13,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|||||||
import { X } from "lucide-react-native";
|
import { X } from "lucide-react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { theme as defaultTheme } from "@/styles/theme";
|
import { theme as defaultTheme } from "@/styles/theme";
|
||||||
|
import { useRecentPaths } from "@/hooks/use-recent-paths";
|
||||||
|
|
||||||
interface CreateAgentModalProps {
|
interface CreateAgentModalProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
@@ -40,6 +41,7 @@ export function CreateAgentModal({
|
|||||||
}: CreateAgentModalProps) {
|
}: CreateAgentModalProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||||
|
const { recentPaths, addRecentPath } = useRecentPaths();
|
||||||
|
|
||||||
const [workingDir, setWorkingDir] = useState("");
|
const [workingDir, setWorkingDir] = useState("");
|
||||||
const [selectedMode, setSelectedMode] = useState("plan");
|
const [selectedMode, setSelectedMode] = useState("plan");
|
||||||
@@ -56,13 +58,23 @@ export function CreateAgentModal({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleCreate() {
|
async function handleCreate() {
|
||||||
if (!workingDir.trim()) {
|
if (!workingDir.trim()) {
|
||||||
setErrorMessage("Working directory is required");
|
setErrorMessage("Working directory is required");
|
||||||
return;
|
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();
|
handleClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +135,28 @@ export function CreateAgentModal({
|
|||||||
Absolute path to the project directory
|
Absolute path to the project directory
|
||||||
</Text>
|
</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>
|
</View>
|
||||||
|
|
||||||
{/* Mode Selector */}
|
{/* Mode Selector */}
|
||||||
@@ -339,4 +373,23 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
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,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ export function EmptyState({ onCreateAgent }: EmptyStateProps) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>Hammock</Text>
|
<Text style={styles.title}>Hammock</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>What would you like to work on?</Text>
|
||||||
What would you like to work on?
|
|
||||||
</Text>
|
|
||||||
<Pressable onPress={onCreateAgent} style={styles.button}>
|
<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>
|
<Text style={styles.buttonText}>New agent</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ export function HomeHeader({ onCreateAgent }: HomeHeaderProps) {
|
|||||||
>
|
>
|
||||||
<MessageSquare size={20} color={theme.colors.foreground} />
|
<MessageSquare size={20} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable onPress={onCreateAgent} style={styles.iconButton}>
|
||||||
onPress={onCreateAgent}
|
|
||||||
style={styles.iconButton}
|
|
||||||
>
|
|
||||||
<Plus size={20} color={theme.colors.foreground} />
|
<Plus size={20} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@@ -74,6 +71,5 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
iconButton: {
|
iconButton: {
|
||||||
padding: theme.spacing[3],
|
padding: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
backgroundColor: theme.colors.muted,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type WriteTextFileResponse,
|
type WriteTextFileResponse,
|
||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { expandTilde } from "../terminal-mcp/tmux.js";
|
||||||
import type {
|
import type {
|
||||||
AgentStatus,
|
AgentStatus,
|
||||||
AgentInfo,
|
AgentInfo,
|
||||||
@@ -113,7 +114,7 @@ export class AgentManager {
|
|||||||
*/
|
*/
|
||||||
async createAgent(options: CreateAgentOptions): Promise<string> {
|
async createAgent(options: CreateAgentOptions): Promise<string> {
|
||||||
const agentId = uuidv4();
|
const agentId = uuidv4();
|
||||||
const cwd = options.cwd;
|
const cwd = expandTilde(options.cwd);
|
||||||
|
|
||||||
// Validate that the working directory exists
|
// Validate that the working directory exists
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ export async function createSession(name: string): Promise<TmuxSession | null> {
|
|||||||
/**
|
/**
|
||||||
* Expand tilde in path to home directory
|
* Expand tilde in path to home directory
|
||||||
*/
|
*/
|
||||||
function expandTilde(path: string): string {
|
export function expandTilde(path: string): string {
|
||||||
if (path.startsWith("~/")) {
|
if (path.startsWith("~/")) {
|
||||||
const homeDir = process.env.HOME || os.homedir();
|
const homeDir = process.env.HOME || os.homedir();
|
||||||
return path.replace("~", homeDir);
|
return path.replace("~", homeDir);
|
||||||
|
|||||||
Reference in New Issue
Block a user