diff --git a/README.ja.md b/README.ja.md
index 55c8851..5a42a1b 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -133,12 +133,21 @@ bun reset-project # クリーン状態にリセット
### APIキーの設定
+Jules APIキーは2つの方法で設定できます:
+
+**方法1:環境変数(開発時に推奨)**
+ルートディレクトリに`.env`ファイルを作成し、キーを追加します:
+```bash
+JULES_API_KEY=あなたの_api_key
+```
+
+**方法2:アプリ内設定**
1. アプリを開く
2. **Settings**タブに移動
3. Jules APIキーを入力
4. キーはデバイスにセキュアに保存されます
-> APIキーは[Google Cloud Console](https://console.cloud.google.com/)またはJulesの設定ページから取得できます。
+> 💡 APIキーは[Google Cloud Console](https://console.cloud.google.com/)またはJulesの設定ページから取得できます。
## プロジェクト構成
diff --git a/README.md b/README.md
index b48714a..474f67d 100644
--- a/README.md
+++ b/README.md
@@ -154,8 +154,17 @@ bun reset-project # Reset to clean state
### API Key Setup
+You can configure your Jules API key in two ways:
+
+**Method 1: Environment Variable (Recommended for development)**
+Create a `.env` file in the root directory and add your key:
+```bash
+JULES_API_KEY=your_api_key_here
+```
+
+**Method 2: In-App Settings**
1. Open the app
-2. Navigate to **Settings** tab
+2. Navigate to the **Settings** tab
3. Enter your Jules API Key
4. The key is securely stored on your device
diff --git a/app/create-session.tsx b/app/create-session.tsx
index 02f460a..87cf974 100644
--- a/app/create-session.tsx
+++ b/app/create-session.tsx
@@ -1,23 +1,16 @@
-import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
import {
View,
Text,
- TextInput,
TouchableOpacity,
- StyleSheet,
ScrollView,
Alert,
- ActivityIndicator,
KeyboardAvoidingView,
Platform,
- Animated,
} from 'react-native';
import { router, Stack } from 'expo-router';
-import { LinearGradient } from 'expo-linear-gradient';
-import * as Haptics from 'expo-haptics';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useHeaderHeight } from '@react-navigation/elements';
-import { IconSymbol } from '@/components/ui/icon-symbol';
import { useJulesApi } from '@/hooks/use-jules-api';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useI18n } from '@/constants/i18n-context';
@@ -26,443 +19,13 @@ 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 { Skeleton } from '@/components/ui/skeleton';
-/**
- * フォームスケルトン
- */
-function FormSkeleton({ paddingBottom }: { paddingBottom: number }) {
- const colorScheme = useColorScheme();
- const isDark = colorScheme === 'dark';
-
- return (
-
- {/* ラベル1 */}
-
-
- {/* セレクトボックス */}
-
-
-
-
-
-
- {/* ラベル2 */}
-
-
- {/* テキストエリア */}
-
-
-
-
-
-
-
- {/* ボタン */}
-
-
- );
-}
-
-const skeletonStyles = StyleSheet.create({
- content: {
- padding: 16,
- },
- section: {
- gap: 8,
- },
- selectBox: {
- backgroundColor: '#ffffff',
- borderWidth: 1,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- padding: 14,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- selectBoxDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- },
- textArea: {
- backgroundColor: '#ffffff',
- borderWidth: 1,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- padding: 14,
- height: 120,
- },
- textAreaDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- },
-});
-
-function SourceSelector({
- isDark,
- t,
- isLoading,
- selectedSource,
- sourcesMap,
- getSourceDisplayName,
- toggleSources,
- isDropdownOpen,
- sourcesLoaded,
- sources,
- sourceQuery,
- setSourceQuery,
- handleSourcesScroll,
- filteredRecentRepos,
- filteredAllSources,
- setSelectedSource,
- setIsDropdownOpen,
- isLoadingMoreSources,
- hasMoreSources,
-}: any) {
- return (
-
-
- {t('selectRepo')}
-
-
-
- {selectedSource
- ? (() => {
- const source = sourcesMap.get(selectedSource);
- return source ? getSourceDisplayName(source) : selectedSource;
- })()
- : t('selectPlaceholder')}
-
-
-
-
- {/* Source list with lazy loading */}
- {isDropdownOpen && sourcesLoaded && sources.length > 0 && (
-
-
-
-
-
-
-
- {/* Recent Repositories Section */}
- {filteredRecentRepos.length > 0 && (
- <>
-
-
-
- {t('recentRepos')}
-
-
- {filteredRecentRepos.map((source: any) => {
- return (
- {
- setSelectedSource(source.name);
- setIsDropdownOpen(false);
- setSourceQuery('');
- }}
- >
-
-
- {getSourceDisplayName(source)}
-
-
- );
- })}
- >
- )}
-
- {/* All Repositories Section */}
- {filteredAllSources.length > 0 && (
- <>
- {filteredRecentRepos.length > 0 && (
-
-
-
- {t('allRepos')}
-
-
- )}
- {filteredAllSources.map((source: any) => {
- return (
- {
- setSelectedSource(source.name);
- setIsDropdownOpen(false);
- setSourceQuery('');
- }}
- >
-
-
- {getSourceDisplayName(source)}
-
-
- );
- })}
- >
- )}
- {/* Loading indicator for more sources */}
- {isLoadingMoreSources && (
-
-
-
- {t('loadingMore')}
-
-
- )}
- {/* End of list indicator */}
- {!hasMoreSources && sources.length > 20 && (
-
-
- {sources.length} repos
-
-
- )}
-
-
- )}
-
- {sourcesLoaded && isDropdownOpen && (sources.length === 0 || (filteredRecentRepos.length === 0 && filteredAllSources.length === 0 && !isLoadingMoreSources)) && (
-
- {t('noSourcesFound')}
-
- )}
-
- {/* Helper hint */}
- {!isDropdownOpen && sourcesLoaded && sources.length > 0 && (
-
- {t('repoHint')}
-
- )}
-
- );
-}
-
-function PromptInput({
- isDark,
- t,
- prompt,
- setPrompt,
-}: any) {
- return (
-
-
- {t('promptLabel')}
-
- {prompt.length} chars
-
-
-
-
- );
-}
-
-function ExecutionModeSelector({
- isDark,
- t,
- requirePlanApproval,
- setRequirePlanApproval,
-}: any) {
- return (
-
- {t('executionMode')}
-
- {/* Start Mode */}
- setRequirePlanApproval(false)}
- activeOpacity={0.7}
- >
-
-
-
- {t('modeStart')}
-
-
-
- {t('modeStartDesc')}
-
-
-
- {/* Review Mode */}
- setRequirePlanApproval(true)}
- activeOpacity={0.7}
- >
-
-
-
- {t('modeReview')}
-
-
-
- {t('modeReviewDesc')}
-
-
-
-
- );
-}
-
-function SubmitButton({
- selectedSource,
- prompt,
- isLoading,
- handleCreate,
- t,
- colors,
-}: any) {
- const buttonLabel = isLoading ? 'Creating...' : 'Start Task';
-
- return (
- {
- void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
- void handleCreate();
- }}
- disabled={!selectedSource || !prompt.trim() || isLoading}
- activeOpacity={0.9}
- >
- {(!selectedSource || !prompt.trim()) ? (
-
-
-
- {buttonLabel}
-
-
- ) : (
-
- {isLoading ? (
-
- ) : (
-
- )}
- {buttonLabel}
-
- )}
-
- );
-}
+import { FormSkeleton } from '@/components/jules/create-session/form-skeleton';
+import { SourceSelector } from '@/components/jules/create-session/source-selector';
+import { PromptInput } from '@/components/jules/create-session/prompt-input';
+import { ExecutionModeSelector } from '@/components/jules/create-session/execution-mode-selector';
+import { SubmitButton } from '@/components/jules/create-session/submit-button';
+import { styles } from '@/components/jules/create-session/styles';
export default function CreateSessionScreen() {
const colorScheme = useColorScheme();
@@ -582,7 +145,6 @@ export default function CreateSessionScreen() {
: source.displayName || source.name;
}, []);
-
// Toggle dropdown (sources already loaded)
const toggleSources = useCallback(() => {
if (isDropdownOpen) {
@@ -722,307 +284,3 @@ export default function CreateSessionScreen() {
>
);
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#f8fafc',
- },
- containerDark: {
- backgroundColor: '#020617',
- },
- content: {
- padding: 16,
- },
- errorBanner: {
- marginBottom: 16,
- padding: 12,
- backgroundColor: 'rgba(239, 68, 68, 0.1)',
- borderRadius: 8,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- errorBannerDark: {
- backgroundColor: 'rgba(239, 68, 68, 0.2)',
- },
- errorText: {
- color: '#dc2626',
- fontSize: 13,
- flex: 1,
- },
- errorClose: {
- color: '#dc2626',
- fontSize: 18,
- fontWeight: '700',
- paddingLeft: 12,
- },
- section: {
- gap: 8,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- color: '#334155',
- },
- labelDark: {
- color: '#cbd5e1',
- },
- labelRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 8,
- },
- charCounter: {
- fontSize: 12,
- color: '#94a3b8',
- },
- charCounterDark: {
- color: '#64748b',
- },
- selectButton: {
- backgroundColor: '#ffffff',
- borderWidth: 1,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- padding: 14,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- selectButtonDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- },
- selectButtonText: {
- fontSize: 15,
- color: '#0f172a',
- flex: 1,
- },
- selectButtonTextDark: {
- color: '#f8fafc',
- },
- placeholderText: {
- color: '#94a3b8',
- },
- sourceList: {
- backgroundColor: '#ffffff',
- borderWidth: 1,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- marginTop: 8,
- overflow: 'hidden',
- maxHeight: 300,
- },
- repoSearchContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#e2e8f0',
- backgroundColor: '#f8fafc',
- },
- repoSearchContainerDark: {
- borderBottomColor: '#334155',
- backgroundColor: '#0f172a',
- },
- repoSearchInput: {
- flex: 1,
- fontSize: 13,
- color: '#334155',
- paddingVertical: 0,
- },
- repoSearchInputDark: {
- color: '#cbd5e1',
- },
- sourceListDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- },
- sourceItem: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- padding: 12,
- borderBottomWidth: 1,
- borderBottomColor: '#f1f5f9',
- },
- sourceItemDark: {
- borderBottomColor: '#334155',
- },
- sourceItemSelected: {
- backgroundColor: 'rgba(37, 99, 235, 0.1)',
- },
- sourceItemText: {
- fontSize: 14,
- color: '#334155',
- flex: 1,
- },
- sourceItemTextDark: {
- color: '#e2e8f0',
- },
- sourceItemTextSelected: {
- color: '#2563eb',
- fontWeight: '600',
- },
- hint: {
- fontSize: 12,
- color: '#94a3b8',
- marginTop: 4,
- },
- hintDark: {
- color: '#64748b',
- },
- textArea: {
- backgroundColor: '#ffffff',
- borderWidth: 1,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- padding: 14,
- fontSize: 15,
- color: '#0f172a',
- height: 120,
- },
- textAreaDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- color: '#f8fafc',
- },
- createButton: {
- borderRadius: 14,
- marginTop: 24,
- shadowColor: '#6366f1',
- shadowOffset: { width: 0, height: 6 },
- shadowOpacity: 0.3,
- shadowRadius: 12,
- elevation: 6,
- overflow: 'hidden',
- },
- createButtonContent: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 10,
- paddingVertical: 16,
- backgroundColor: '#e2e8f0',
- },
- createButtonGradient: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 10,
- paddingVertical: 16,
- },
- createButtonDisabled: {
- shadowOpacity: 0,
- },
- createButtonText: {
- color: '#ffffff',
- fontSize: 17,
- fontWeight: '700',
- letterSpacing: 0.3,
- },
- createButtonTextDisabled: {
- color: '#94a3b8',
- },
- loadingMore: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- padding: 12,
- gap: 8,
- },
- loadingMoreText: {
- fontSize: 12,
- color: '#64748b',
- },
- loadingMoreTextDark: {
- color: '#94a3b8',
- },
- endOfList: {
- alignItems: 'center',
- padding: 8,
- },
- endOfListText: {
- fontSize: 11,
- color: '#94a3b8',
- },
- endOfListTextDark: {
- color: '#64748b',
- },
- sectionHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- paddingHorizontal: 12,
- paddingVertical: 8,
- backgroundColor: '#f8fafc',
- borderBottomWidth: 1,
- borderBottomColor: '#e2e8f0',
- },
- sectionHeaderDark: {
- backgroundColor: '#0f172a',
- borderBottomColor: '#334155',
- },
- sectionHeaderText: {
- fontSize: 12,
- fontWeight: '600',
- color: '#64748b',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- sectionHeaderTextDark: {
- color: '#94a3b8',
- },
- modeContainer: {
- flexDirection: 'row',
- gap: 12,
- },
- modeButton: {
- flex: 1,
- backgroundColor: '#ffffff',
- borderWidth: 2,
- borderColor: '#e2e8f0',
- borderRadius: 12,
- padding: 14,
- gap: 8,
- },
- modeButtonDark: {
- backgroundColor: '#1e293b',
- borderColor: '#334155',
- },
- modeButtonSelected: {
- borderColor: '#2563eb',
- backgroundColor: 'rgba(37, 99, 235, 0.05)',
- },
- modeHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- modeTitle: {
- fontSize: 15,
- fontWeight: '600',
- color: '#334155',
- },
- modeTitleDark: {
- color: '#cbd5e1',
- },
- modeTitleSelected: {
- color: '#2563eb',
- },
- modeDesc: {
- fontSize: 12,
- color: '#64748b',
- lineHeight: 16,
- },
- modeDescDark: {
- color: '#94a3b8',
- },
- modeDescSelected: {
- color: '#3b82f6',
- },
-});
diff --git a/components/jules/create-session/execution-mode-selector.tsx b/components/jules/create-session/execution-mode-selector.tsx
new file mode 100644
index 0000000..b54863f
--- /dev/null
+++ b/components/jules/create-session/execution-mode-selector.tsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { View, Text, TouchableOpacity } from 'react-native';
+import { IconSymbol } from '@/components/ui/icon-symbol';
+import { styles } from './styles';
+
+interface ExecutionModeSelectorProps {
+ isDark: boolean;
+ t: (key: any) => string;
+ requirePlanApproval: boolean;
+ setRequirePlanApproval: (requirePlanApproval: boolean) => void;
+}
+
+export function ExecutionModeSelector({
+ isDark,
+ t,
+ requirePlanApproval,
+ setRequirePlanApproval,
+}: ExecutionModeSelectorProps) {
+ return (
+
+ {t('executionMode')}
+
+ {/* Start Mode */}
+ setRequirePlanApproval(false)}
+ activeOpacity={0.7}
+ >
+
+
+
+ {t('modeStart')}
+
+
+
+ {t('modeStartDesc')}
+
+
+
+ {/* Review Mode */}
+ setRequirePlanApproval(true)}
+ activeOpacity={0.7}
+ >
+
+
+
+ {t('modeReview')}
+
+
+
+ {t('modeReviewDesc')}
+
+
+
+
+ );
+}
diff --git a/components/jules/create-session/form-skeleton.tsx b/components/jules/create-session/form-skeleton.tsx
new file mode 100644
index 0000000..c2acd41
--- /dev/null
+++ b/components/jules/create-session/form-skeleton.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { View, ScrollView, StyleSheet } from 'react-native';
+import { Skeleton } from '@/components/ui/skeleton';
+import { useColorScheme } from '@/hooks/use-color-scheme';
+
+/**
+ * フォームスケルトン
+ */
+export function FormSkeleton({ paddingBottom }: { paddingBottom: number }) {
+ const colorScheme = useColorScheme();
+ const isDark = colorScheme === 'dark';
+
+ return (
+
+ {/* ラベル1 */}
+
+
+ {/* セレクトボックス */}
+
+
+
+
+
+
+ {/* ラベル2 */}
+
+
+ {/* テキストエリア */}
+
+
+
+
+
+
+
+ {/* ボタン */}
+
+
+ );
+}
+
+const skeletonStyles = StyleSheet.create({
+ content: {
+ padding: 16,
+ },
+ section: {
+ gap: 8,
+ },
+ selectBox: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ padding: 14,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ selectBoxDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+ textArea: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ padding: 14,
+ height: 120,
+ },
+ textAreaDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+});
diff --git a/components/jules/create-session/prompt-input.tsx b/components/jules/create-session/prompt-input.tsx
new file mode 100644
index 0000000..b320dd9
--- /dev/null
+++ b/components/jules/create-session/prompt-input.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { View, Text, TextInput } from 'react-native';
+import { styles } from './styles';
+
+interface PromptInputProps {
+ isDark: boolean;
+ t: (key: any) => string;
+ prompt: string;
+ setPrompt: (prompt: string) => void;
+}
+
+export function PromptInput({
+ isDark,
+ t,
+ prompt,
+ setPrompt,
+}: PromptInputProps) {
+ return (
+
+
+ {t('promptLabel')}
+
+ {prompt.length} chars
+
+
+
+
+ );
+}
diff --git a/components/jules/create-session/source-selector.tsx b/components/jules/create-session/source-selector.tsx
new file mode 100644
index 0000000..68b2bb7
--- /dev/null
+++ b/components/jules/create-session/source-selector.tsx
@@ -0,0 +1,233 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ ScrollView,
+ ActivityIndicator,
+} from 'react-native';
+import { IconSymbol } from '@/components/ui/icon-symbol';
+import { styles } from './styles';
+import type { Source } from '@/constants/types';
+
+interface SourceSelectorProps {
+ isDark: boolean;
+ t: (key: any) => string;
+ isLoading: boolean;
+ selectedSource: string;
+ sourcesMap: Map;
+ getSourceDisplayName: (source: Source) => string;
+ toggleSources: () => void;
+ isDropdownOpen: boolean;
+ sourcesLoaded: boolean;
+ sources: Source[];
+ sourceQuery: string;
+ setSourceQuery: (query: string) => void;
+ handleSourcesScroll: (event: any) => void;
+ filteredRecentRepos: Source[];
+ filteredAllSources: Source[];
+ setSelectedSource: (source: string) => void;
+ setIsDropdownOpen: (isOpen: boolean) => void;
+ isLoadingMoreSources: boolean;
+ hasMoreSources: boolean;
+}
+
+export function SourceSelector({
+ isDark,
+ t,
+ isLoading,
+ selectedSource,
+ sourcesMap,
+ getSourceDisplayName,
+ toggleSources,
+ isDropdownOpen,
+ sourcesLoaded,
+ sources,
+ sourceQuery,
+ setSourceQuery,
+ handleSourcesScroll,
+ filteredRecentRepos,
+ filteredAllSources,
+ setSelectedSource,
+ setIsDropdownOpen,
+ isLoadingMoreSources,
+ hasMoreSources,
+}: SourceSelectorProps) {
+ return (
+
+
+ {t('selectRepo')}
+
+
+
+ {selectedSource
+ ? (() => {
+ const source = sourcesMap.get(selectedSource);
+ return source ? getSourceDisplayName(source) : selectedSource;
+ })()
+ : t('selectPlaceholder')}
+
+
+
+
+ {/* Source list with lazy loading */}
+ {isDropdownOpen && sourcesLoaded && sources.length > 0 && (
+
+
+
+
+
+
+
+ {/* Recent Repositories Section */}
+ {filteredRecentRepos.length > 0 && (
+ <>
+
+
+
+ {t('recentRepos')}
+
+
+ {filteredRecentRepos.map((source: Source) => {
+ return (
+ {
+ setSelectedSource(source.name);
+ setIsDropdownOpen(false);
+ setSourceQuery('');
+ }}
+ >
+
+
+ {getSourceDisplayName(source)}
+
+
+ );
+ })}
+ >
+ )}
+
+ {/* All Repositories Section */}
+ {filteredAllSources.length > 0 && (
+ <>
+ {filteredRecentRepos.length > 0 && (
+
+
+
+ {t('allRepos')}
+
+
+ )}
+ {filteredAllSources.map((source: Source) => {
+ return (
+ {
+ setSelectedSource(source.name);
+ setIsDropdownOpen(false);
+ setSourceQuery('');
+ }}
+ >
+
+
+ {getSourceDisplayName(source)}
+
+
+ );
+ })}
+ >
+ )}
+ {/* Loading indicator for more sources */}
+ {isLoadingMoreSources && (
+
+
+
+ {t('loadingMore')}
+
+
+ )}
+ {/* End of list indicator */}
+ {!hasMoreSources && sources.length > 20 && (
+
+
+ {sources.length} repos
+
+
+ )}
+
+
+ )}
+
+ {sourcesLoaded && isDropdownOpen && (sources.length === 0 || (filteredRecentRepos.length === 0 && filteredAllSources.length === 0 && !isLoadingMoreSources)) && (
+
+ {t('noSourcesFound')}
+
+ )}
+
+ {/* Helper hint */}
+ {!isDropdownOpen && sourcesLoaded && sources.length > 0 && (
+
+ {t('repoHint')}
+
+ )}
+
+ );
+}
diff --git a/components/jules/create-session/styles.ts b/components/jules/create-session/styles.ts
new file mode 100644
index 0000000..e1b2755
--- /dev/null
+++ b/components/jules/create-session/styles.ts
@@ -0,0 +1,305 @@
+import { StyleSheet } from 'react-native';
+
+export const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f8fafc',
+ },
+ containerDark: {
+ backgroundColor: '#020617',
+ },
+ content: {
+ padding: 16,
+ },
+ errorBanner: {
+ marginBottom: 16,
+ padding: 12,
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
+ borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ errorBannerDark: {
+ backgroundColor: 'rgba(239, 68, 68, 0.2)',
+ },
+ errorText: {
+ color: '#dc2626',
+ fontSize: 13,
+ flex: 1,
+ },
+ errorClose: {
+ color: '#dc2626',
+ fontSize: 18,
+ fontWeight: '700',
+ paddingLeft: 12,
+ },
+ section: {
+ gap: 8,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#334155',
+ },
+ labelDark: {
+ color: '#cbd5e1',
+ },
+ labelRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ charCounter: {
+ fontSize: 12,
+ color: '#94a3b8',
+ },
+ charCounterDark: {
+ color: '#64748b',
+ },
+ selectButton: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ padding: 14,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ selectButtonDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+ selectButtonText: {
+ fontSize: 15,
+ color: '#0f172a',
+ flex: 1,
+ },
+ selectButtonTextDark: {
+ color: '#f8fafc',
+ },
+ placeholderText: {
+ color: '#94a3b8',
+ },
+ sourceList: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ marginTop: 8,
+ overflow: 'hidden',
+ maxHeight: 300,
+ },
+ repoSearchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: '#e2e8f0',
+ backgroundColor: '#f8fafc',
+ },
+ repoSearchContainerDark: {
+ borderBottomColor: '#334155',
+ backgroundColor: '#0f172a',
+ },
+ repoSearchInput: {
+ flex: 1,
+ fontSize: 13,
+ color: '#334155',
+ paddingVertical: 0,
+ },
+ repoSearchInputDark: {
+ color: '#cbd5e1',
+ },
+ sourceListDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+ sourceItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ padding: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: '#f1f5f9',
+ },
+ sourceItemDark: {
+ borderBottomColor: '#334155',
+ },
+ sourceItemSelected: {
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
+ },
+ sourceItemText: {
+ fontSize: 14,
+ color: '#334155',
+ flex: 1,
+ },
+ sourceItemTextDark: {
+ color: '#e2e8f0',
+ },
+ sourceItemTextSelected: {
+ color: '#2563eb',
+ fontWeight: '600',
+ },
+ hint: {
+ fontSize: 12,
+ color: '#94a3b8',
+ marginTop: 4,
+ },
+ hintDark: {
+ color: '#64748b',
+ },
+ textArea: {
+ backgroundColor: '#ffffff',
+ borderWidth: 1,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ padding: 14,
+ fontSize: 15,
+ color: '#0f172a',
+ height: 120,
+ },
+ textAreaDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ color: '#f8fafc',
+ },
+ createButton: {
+ borderRadius: 14,
+ marginTop: 24,
+ shadowColor: '#6366f1',
+ shadowOffset: { width: 0, height: 6 },
+ shadowOpacity: 0.3,
+ shadowRadius: 12,
+ elevation: 6,
+ overflow: 'hidden',
+ },
+ createButtonContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 10,
+ paddingVertical: 16,
+ backgroundColor: '#e2e8f0',
+ },
+ createButtonGradient: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 10,
+ paddingVertical: 16,
+ },
+ createButtonDisabled: {
+ shadowOpacity: 0,
+ },
+ createButtonText: {
+ color: '#ffffff',
+ fontSize: 17,
+ fontWeight: '700',
+ letterSpacing: 0.3,
+ },
+ createButtonTextDisabled: {
+ color: '#94a3b8',
+ },
+ loadingMore: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 12,
+ gap: 8,
+ },
+ loadingMoreText: {
+ fontSize: 12,
+ color: '#64748b',
+ },
+ loadingMoreTextDark: {
+ color: '#94a3b8',
+ },
+ endOfList: {
+ alignItems: 'center',
+ padding: 8,
+ },
+ endOfListText: {
+ fontSize: 11,
+ color: '#94a3b8',
+ },
+ endOfListTextDark: {
+ color: '#64748b',
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ backgroundColor: '#f8fafc',
+ borderBottomWidth: 1,
+ borderBottomColor: '#e2e8f0',
+ },
+ sectionHeaderDark: {
+ backgroundColor: '#0f172a',
+ borderBottomColor: '#334155',
+ },
+ sectionHeaderText: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: '#64748b',
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ sectionHeaderTextDark: {
+ color: '#94a3b8',
+ },
+ modeContainer: {
+ flexDirection: 'row',
+ gap: 12,
+ },
+ modeButton: {
+ flex: 1,
+ backgroundColor: '#ffffff',
+ borderWidth: 2,
+ borderColor: '#e2e8f0',
+ borderRadius: 12,
+ padding: 14,
+ gap: 8,
+ },
+ modeButtonDark: {
+ backgroundColor: '#1e293b',
+ borderColor: '#334155',
+ },
+ modeButtonSelected: {
+ borderColor: '#2563eb',
+ backgroundColor: 'rgba(37, 99, 235, 0.05)',
+ },
+ modeHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ modeTitle: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: '#334155',
+ },
+ modeTitleDark: {
+ color: '#cbd5e1',
+ },
+ modeTitleSelected: {
+ color: '#2563eb',
+ },
+ modeDesc: {
+ fontSize: 12,
+ color: '#64748b',
+ lineHeight: 16,
+ },
+ modeDescDark: {
+ color: '#94a3b8',
+ },
+ modeDescSelected: {
+ color: '#3b82f6',
+ },
+});
diff --git a/components/jules/create-session/submit-button.tsx b/components/jules/create-session/submit-button.tsx
new file mode 100644
index 0000000..86ce8ee
--- /dev/null
+++ b/components/jules/create-session/submit-button.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import * as Haptics from 'expo-haptics';
+import { IconSymbol } from '@/components/ui/icon-symbol';
+import { styles } from './styles';
+
+interface SubmitButtonProps {
+ selectedSource: string;
+ prompt: string;
+ isLoading: boolean;
+ handleCreate: () => void;
+ t: (key: any) => string;
+ colors: any;
+}
+
+export function SubmitButton({
+ selectedSource,
+ prompt,
+ isLoading,
+ handleCreate,
+ t,
+ colors,
+}: SubmitButtonProps) {
+ const buttonLabel = isLoading ? 'Creating...' : 'Start Task';
+
+ return (
+ {
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ void handleCreate();
+ }}
+ disabled={!selectedSource || !prompt.trim() || isLoading}
+ activeOpacity={0.9}
+ >
+ {(!selectedSource || !prompt.trim()) ? (
+
+
+
+ {buttonLabel}
+
+
+ ) : (
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ {buttonLabel}
+
+ )}
+
+ );
+}
diff --git a/constants/api-key-context.tsx b/constants/api-key-context.tsx
index b178929..6cad5b1 100644
--- a/constants/api-key-context.tsx
+++ b/constants/api-key-context.tsx
@@ -16,7 +16,7 @@ interface ApiKeyProviderProps {
}
export function ApiKeyProvider({ children }: ApiKeyProviderProps) {
- const [apiKey, setApiKeyState] = useState('');
+ const [apiKey, setApiKeyState] = useState(process.env.JULES_API_KEY || process.env.EXPO_PUBLIC_JULES_API_KEY || '');
const [isLoaded, setIsLoaded] = useState(false);
// Load API key on mount
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 8ffc13e..00fcabc 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -39,6 +39,15 @@ bun web # Web browser
### 3. Configure API Key
+You can configure your API key using an environment variable or via the app's settings UI.
+
+**Method 1: Environment Variable (Recommended)**
+Create a `.env` file in the root directory:
+```bash
+JULES_API_KEY=your_api_key_here
+```
+
+**Method 2: In-App Settings**
1. Open the app on your device/simulator
2. Go to Settings tab
3. Enter your Jules API Key
diff --git a/output.log b/output.log
deleted file mode 100644
index b7183b6..0000000
--- a/output.log
+++ /dev/null
@@ -1,97 +0,0 @@
-Starting project at /app
-React Compiler enabled
-Starting Metro Bundler
-The following packages should be updated for best compatibility with the installed expo version:
- expo@54.0.30 - expected version: ~54.0.33
- expo-constants@18.0.12 - expected version: ~18.0.13
- expo-file-system@18.0.12 - expected version: ~19.0.21
- expo-font@14.0.10 - expected version: ~14.0.11
- expo-router@6.0.21 - expected version: ~6.0.23
-Your project may not work correctly until you install the expected versions of the packages.
-Waiting on http://localhost:8081
-Logs for your project will appear below.
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-Web node_modules/expo-router/entry.js ▓▓▓▓░░░░░░░░░░░░ 28.0% (16/94)
-λ node_modules/expo-router/node/render.js ▓▓░░░░░░░░░░░░░░ 17.4% (121/290)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 47.1% (173/252)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓░░░░░░░░░░ 40.4% (314/494)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 47.1% (270/399)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓░░░░░░░ 61.6% (543/718)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓░░░░░░░░░ 49.5% (375/538)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓░░░░░ 70.0% (643/774)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓░░░░░░░ 59.2% (544/714)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 80.2% (839/937)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 76.5% (740/846)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 89.8% ( 942/1000)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 90.0% ( 962/1014)
-λ node_modules/expo-router/node/render.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 93.4% (1149/1189)
-λ Bundled 26965ms node_modules/expo-router/node/render.js (1238 modules)
-"shadow*" style props are deprecated. Use "boxShadow".
-(node:4029) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
-(Use `node --trace-deprecation ...` to show where the warning was created)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ 99.5% (1284/1287)
-Web Bundled 30057ms node_modules/expo-router/entry.js (1287 modules)
-Error: Premature close
- at onclose (node:internal/streams/end-of-stream:171:30)
- at processTicksAndRejections (node:internal/process/task_queues:84:11)
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 48ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 360ms node_modules/expo-router/entry.js (1 module)
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 42ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 692ms node_modules/expo-router/entry.js (1 module)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓░░░░░░░░░░ 37.7% (242/428)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓░░░░░░ 63.0% (551/694)
-Web node_modules/expo-router/entry.js ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░ 85.3% ( 945/1023)
-Web Bundled 11818ms node_modules/expo-router/entry.js (1288 modules)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 75ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 486ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 106ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 35ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 358ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 64ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 36ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 354ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 64ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 35ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 349ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 69ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 59ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 434ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 89ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 40ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 350ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 77ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 63ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 440ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 83ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console
-Web node_modules/expo-router/entry.js ░░░░░░░░░░░░░░░░ 0.0% (0/1)
-λ Bundled 34ms node_modules/expo-router/node/render.js (1 module)
-"shadow*" style props are deprecated. Use "boxShadow".
-Web Bundled 353ms node_modules/expo-router/entry.js (1 module)
-Web Bundled 53ms node_modules/expo-router/entry.js (1 module)
- LOG [web] Logs will appear in the browser console