Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 10 additions & 27 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import {
TextInput,
ActivityIndicator,
ScrollView,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { SessionCard, SwipeableSessionCard, SessionCardSkeleton } from '@/components/jules/session-card';
Expand All @@ -23,6 +21,7 @@ import { useColorScheme } from '@/hooks/use-color-scheme';
import type { Session } from '@/constants/types';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
import { useAlert } from '@/contexts/alert-context';
import {
useSecureStorage,
type SessionFilterPreset,
Expand Down Expand Up @@ -63,6 +62,7 @@ export default function SessionsScreen() {
const colorScheme = useColorScheme();
const isDark = colorScheme === 'dark';
const { t } = useI18n();
const { showAlert } = useAlert();
const colors = isDark ? Colors.dark : Colors.light;

const { apiKey } = useApiKey();
Expand Down Expand Up @@ -238,11 +238,11 @@ export default function SessionsScreen() {
}, [approvePlan, fetchSessions]);

const handleDeleteSession = useCallback((sessionName: string) => {
Alert.alert(
showAlert(
t('deleteSession'),
t('deleteSessionConfirm'),
[
{ text: t('cancel'), style: 'cancel' },
{ text: t('cancel'), style: 'cancel', onPress: () => {} },
{
text: t('delete'),
style: 'destructive',
Expand All @@ -253,7 +253,7 @@ export default function SessionsScreen() {
},
]
);
}, [deleteSession, t]);
}, [deleteSession, t, showAlert]);

const renderSessionItem = useCallback(({ item }: { item: Session }) => (
<TouchableOpacity onLongPress={() => handleDeleteSession(item.name)} delayLongPress={500}>
Expand Down Expand Up @@ -313,25 +313,13 @@ export default function SessionsScreen() {

return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
{/* Modern Header with Gradient */}
{/* Modern Header */}
<View style={[styles.header, isDark && styles.headerDark]}>
<LinearGradient
colors={isDark
? [colors.surface, colors.surfaceSecondary]
: [colors.surface, colors.surfaceSecondary]
}
style={StyleSheet.absoluteFill}
/>
<View style={styles.headerContent}>
<View style={styles.headerLeft}>
<LinearGradient
colors={[colors.primary, colors.primaryLight]}
style={styles.logoContainer}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={[styles.logoContainer, { backgroundColor: colors.primary }]}>
<IconSymbol name="terminal" size={20} color={colors.surface} />
</LinearGradient>
</View>
<View>
<Text style={[styles.headerTitle, { color: colors.text }]}>Jules Client</Text>
<Text style={[styles.headerSubtitle, { color: colors.icon }]}>
Expand Down Expand Up @@ -550,14 +538,9 @@ export default function SessionsScreen() {
accessibilityRole="button"
accessibilityHint="Create a new coding task session"
>
<LinearGradient
colors={[colors.primary, colors.primaryLight]}
style={styles.fabGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={[styles.fabGradient, { backgroundColor: colors.primary }]}>
<IconSymbol name="plus" size={28} color={colors.surface} />
</LinearGradient>
</View>
</TouchableOpacity>
</Animated.View>
)}
Expand Down
32 changes: 10 additions & 22 deletions app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,20 @@ import {
TouchableOpacity,
StyleSheet,
ScrollView,
Alert,
Switch,
Appearance,
Linking,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import Constants from 'expo-constants';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { useSecureStorage } from '@/hooks/use-secure-storage';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
import { useAlert } from '@/contexts/alert-context';
import { Colors } from '@/constants/theme';
import { isValidExternalLink } from '@/utils/url';

Expand Down Expand Up @@ -93,6 +92,7 @@ export default function SettingsScreen() {
const colors = isDark ? Colors.dark : Colors.light;

const { apiKey, setApiKey: saveApiKeyToContext } = useApiKey();
const { showAlert } = useAlert();
const [localApiKey, setLocalApiKey] = useState(apiKey);
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
const [manualDarkMode, setManualDarkMode] = useState(isDark);
Expand Down Expand Up @@ -120,10 +120,10 @@ export default function SettingsScreen() {
try {
await saveApiKeyToContext(localApiKey);
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Alert.alert(t('savedSuccess'));
showAlert(t('savedSuccess'), undefined, undefined, 'success');
} catch {
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert(t('error'), t('savedError'));
showAlert(t('error'), t('savedError'), undefined, 'error');
}
};

Expand All @@ -144,31 +144,24 @@ export default function SettingsScreen() {
const openURL = async (url: string) => {
try {
if (!isValidExternalLink(url)) {
Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.');
showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error');
return;
}
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.');
showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please check your device settings.', undefined, 'error');
}
} catch (error) {
Alert.alert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.');
showAlert(t('error'), t('unableToOpenLink') || 'Unable to open this link. Please try again later.', undefined, 'error');
}
};

return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
{/* Modern Header with Gradient */}
{/* Modern Header */}
<View style={[styles.header, isDark && styles.headerDark]}>
<LinearGradient
colors={isDark
? [colors.surface, colors.surfaceSecondary]
: [colors.surface, colors.surfaceSecondary]
}
style={StyleSheet.absoluteFill}
/>
<View style={styles.headerContent}>
<Text style={[styles.headerTitle, { color: colors.text }]}>{t('settings')}</Text>
</View>
Expand Down Expand Up @@ -212,15 +205,10 @@ export default function SettingsScreen() {
onPress={handleSave}
activeOpacity={0.9}
>
<LinearGradient
colors={[colors.primary, colors.primaryLight]}
style={styles.saveButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<View style={[styles.saveButtonGradient, { backgroundColor: colors.primary }]}>
<IconSymbol name="checkmark.circle.fill" size={20} color="#ffffff" />
<Text style={styles.saveButtonText}>{t('save')}</Text>
</LinearGradient>
</View>
</TouchableOpacity>

{/* テーマ切り替え */}
Expand Down
21 changes: 12 additions & 9 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'react-native-reanimated';

import { ApiKeyProvider } from '@/constants/api-key-context';
import { I18nProvider } from '@/constants/i18n-context';
import { AlertProvider } from '@/contexts/alert-context';
import { useColorScheme } from '@/hooks/use-color-scheme';

export default function RootLayout() {
Expand All @@ -13,15 +14,17 @@ export default function RootLayout() {
return (
<ApiKeyProvider>
<I18nProvider>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
<Stack.Screen name="create-session" options={{ presentation: 'modal', title: 'New Task' }} />
<Stack.Screen name="session/[id]" options={{ title: 'Session' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
<AlertProvider>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
<Stack.Screen name="create-session" options={{ presentation: 'modal', title: 'New Task' }} />
<Stack.Screen name="session/[id]" options={{ title: 'Session' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</AlertProvider>
</I18nProvider>
</ApiKeyProvider>
);
Expand Down
13 changes: 7 additions & 6 deletions app/create-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Text,
TouchableOpacity,
ScrollView,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
Expand All @@ -19,6 +18,7 @@ import { useSecureStorage } from '@/hooks/use-secure-storage';
import { useSourcesCache } from '@/hooks/use-sources-cache';
import type { Source } from '@/constants/types';
import { Colors } from '@/constants/theme';
import { useAlert } from '@/contexts/alert-context';

import { FormSkeleton } from '@/components/jules/create-session/form-skeleton';
import { SourceSelector } from '@/components/jules/create-session/source-selector';
Expand All @@ -38,6 +38,7 @@ export default function CreateSessionScreen() {
const { apiKey } = useApiKey();
const { saveRecentRepo, getRecentRepos } = useSecureStorage();
const { getCachedSources, saveCachedSources } = useSourcesCache();
const { showAlert } = useAlert();
const [selectedSource, setSelectedSource] = useState('');
const [prompt, setPrompt] = useState('');
const [requirePlanApproval, setRequirePlanApproval] = useState(false); // false = Start/Run, true = Review
Expand Down Expand Up @@ -174,12 +175,12 @@ export default function CreateSessionScreen() {
// Create session and save to recent repos
const handleCreate = useCallback(async () => {
if (!selectedSource || !prompt.trim()) {
Alert.alert(t('error'), t('inputError'));
showAlert(t('error'), t('inputError'), undefined, 'error');
return;
}

if (prompt.length > 50000) {
Alert.alert(t('error'), t('promptTooLong'));
showAlert(t('error'), t('promptTooLong'), undefined, 'error');
return;
}

Expand All @@ -191,14 +192,14 @@ export default function CreateSessionScreen() {
if (session && source) {
// Save to recent repos
await saveRecentRepo(source);
Alert.alert(t('createSuccess'), '', [
showAlert(t('createSuccess'), undefined, [
{
text: 'OK',
onPress: () => router.back(),
},
]);
], 'success');
}
}, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t]);
}, [selectedSource, prompt, requirePlanApproval, sourcesMap, createSession, saveRecentRepo, t, showAlert]);

return (
<>
Expand Down
25 changes: 10 additions & 15 deletions app/session/id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import {
Platform,
Animated,
Linking,
Alert,
ActionSheetIOS,
} from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { useHeaderHeight } from '@react-navigation/elements';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { ActivityItem, ActivityItemSkeleton } from '@/components/jules';
import { useJulesApi } from '@/hooks/use-jules-api';
Expand All @@ -27,6 +25,7 @@ import { shareSession } from '@/hooks/use-export-session';
import type { Activity, Session } from '@/constants/types';
import { useI18n } from '@/constants/i18n-context';
import { useApiKey } from '@/constants/api-key-context';
import { useAlert } from '@/contexts/alert-context';
import { SessionHeaderRight } from '@/components/jules/session-header-right';
import { ErrorBanner } from '@/components/jules/error-banner';
import { ApprovalBanner } from '@/components/jules/approval-banner';
Expand Down Expand Up @@ -239,7 +238,7 @@ export default function SessionDetailScreen() {
// Export session handler
const handleExportSession = useCallback(async (format: 'markdown' | 'json') => {
if (!currentSession || activities.length === 0) {
Alert.alert(t('error'), t('noSessionDataToExport'));
showAlert(t('error'), t('noSessionDataToExport'), undefined, 'error');
return;
}

Expand All @@ -250,12 +249,12 @@ export default function SessionDetailScreen() {
} catch (err) {
const errorMessage = err instanceof Error ? err.message : t('exportFailed');
if (errorMessage.includes('not available')) {
Alert.alert(t('error'), t('sharingNotAvailable'));
showAlert(t('error'), t('sharingNotAvailable'), undefined, 'error');
} else {
Alert.alert(t('error'), errorMessage);
showAlert(t('error'), errorMessage, undefined, 'error');
}
}
}, [currentSession, activities, t]);
}, [currentSession, activities, t, showAlert]);

// Show export menu
const showExportMenu = useCallback(() => {
Expand All @@ -277,17 +276,17 @@ export default function SessionDetailScreen() {
);
} else {
// Android - show simple alert
Alert.alert(
showAlert(
t('exportSession'),
t('chooseExportFormat'),
[
{ text: t('cancel'), style: 'cancel' },
{ text: t('cancel'), style: 'cancel', onPress: () => {} },
{ text: t('exportAsMarkdown'), onPress: () => void handleExportSession('markdown') },
{ text: t('exportAsJSON'), onPress: () => void handleExportSession('json') },
]
);
}
}, [t, handleExportSession]);
}, [t, handleExportSession, showAlert]);


return (
Expand Down Expand Up @@ -370,11 +369,7 @@ export default function SessionDetailScreen() {
if (url) void Linking.openURL(url);
}}
>
<LinearGradient
colors={['#059669', '#10b981']}
style={styles.prBannerGradient}
start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}
>
<View style={[styles.prBannerGradient, { backgroundColor: '#10b981' }]}>
<IconSymbol name="arrow.triangle.pull" size={18} color="#ffffff" />
<View style={{ flex: 1, marginLeft: 8 }}>
<Text style={styles.prBannerTitle}>Pull Request Created!</Text>
Expand All @@ -385,7 +380,7 @@ export default function SessionDetailScreen() {
)}
</View>
<IconSymbol name="chevron.right" size={14} color="rgba(255,255,255,0.7)" />
</LinearGradient>
</View>
</TouchableOpacity>
) : null
}
Expand Down
Loading
Loading