diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index fa83b53a319..2d940287c2d 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -23,7 +23,9 @@ export default function RootLayout() {
headerTitle: () =>
}}
/>
-
+
+
+
>
diff --git a/mobile/app/about.tsx b/mobile/app/about.tsx
new file mode 100644
index 00000000000..46454a2c38f
--- /dev/null
+++ b/mobile/app/about.tsx
@@ -0,0 +1,146 @@
+import { View, Text, StyleSheet, Pressable, Linking } from 'react-native'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+import { useRouter } from 'expo-router'
+import { ChevronLeft, Globe } from 'lucide-react-native'
+import Svg, { Path } from 'react-native-svg'
+import { OrcaLogo } from '../src/components/OrcaLogo'
+import { colors, spacing, typography } from '../src/theme/mobile-theme'
+
+function GithubIcon({ size = 16, color = colors.textSecondary }) {
+ return (
+
+ )
+}
+
+function XIcon({ size = 16, color = colors.textSecondary }) {
+ return (
+
+ )
+}
+
+export default function AboutScreen() {
+ const router = useRouter()
+ const insets = useSafeAreaInsets()
+
+ return (
+
+
+ router.back()}>
+
+
+ About
+
+
+
+
+ Orca
+ Open-source agent IDE for 100x builders
+
+
+
+ [styles.row, pressed && styles.rowPressed]}
+ onPress={() => void Linking.openURL('https://onOrca.dev')}
+ >
+
+ Website
+ onOrca.dev
+
+
+ [styles.row, pressed && styles.rowPressed]}
+ onPress={() => void Linking.openURL('https://github.com/stablyai/orca')}
+ >
+
+ GitHub
+ stablyai/orca
+
+
+ [styles.row, pressed && styles.rowPressed]}
+ onPress={() => void Linking.openURL('https://x.com/orca_build')}
+ >
+
+ X
+ @orca_build
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.bgBase,
+ padding: spacing.lg
+ },
+ topRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: spacing.xl
+ },
+ backButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: spacing.sm
+ },
+ heading: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: colors.textPrimary
+ },
+ brand: {
+ alignItems: 'center',
+ paddingVertical: spacing.xl,
+ marginBottom: spacing.lg
+ },
+ brandName: {
+ fontSize: 22,
+ fontWeight: '800',
+ color: colors.textPrimary,
+ marginTop: spacing.sm
+ },
+ brandSub: {
+ fontSize: 13,
+ color: colors.textMuted,
+ marginTop: spacing.xs
+ },
+ section: {
+ backgroundColor: colors.bgPanel,
+ borderRadius: 12,
+ overflow: 'hidden'
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.sm + 2,
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.md + 2
+ },
+ rowPressed: {
+ backgroundColor: colors.bgRaised
+ },
+ rowLabel: {
+ flex: 1,
+ fontSize: typography.bodySize,
+ fontWeight: '500',
+ color: colors.textPrimary
+ },
+ rowValue: {
+ fontSize: typography.bodySize,
+ color: colors.accentBlue
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.borderSubtle,
+ marginHorizontal: spacing.md
+ }
+})
diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx
index eea266a3c86..120ecc0cf77 100644
--- a/mobile/app/h/[hostId]/index.tsx
+++ b/mobile/app/h/[hostId]/index.tsx
@@ -22,7 +22,9 @@ import {
ChevronRight,
ChevronLeft,
Plus,
- Moon
+ Moon,
+ Filter,
+ Check
} from 'lucide-react-native'
import { connect, type RpcClient } from '../../../src/transport/rpc-client'
import { loadHosts, updateLastConnected, removeHost } from '../../../src/transport/host-store'
@@ -33,6 +35,7 @@ import { NewWorktreeModal } from '../../../src/components/NewWorktreeModal'
import { AgentSpinner } from '../../../src/components/AgentSpinner'
import { PickerModal, type PickerOption } from '../../../src/components/PickerModal'
import { ActionSheetModal } from '../../../src/components/ActionSheetModal'
+import { BottomDrawer } from '../../../src/components/BottomDrawer'
import { colors, spacing, typography } from '../../../src/theme/mobile-theme'
import {
loadPinnedIds,
@@ -57,9 +60,14 @@ type Worktree = {
}
type SortMode = 'smart' | 'name' | 'recent'
-type FilterMode = 'all' | 'active'
+type _FilterMode = 'all' | 'active'
type GroupMode = 'none' | 'repo' | 'prStatus'
+type FilterState = {
+ activeOnly: boolean
+ selectedRepos: Set
+}
+
const STATUS_LABELS: Record = {
connecting: 'Connecting…',
handshaking: 'Securing…',
@@ -114,11 +122,14 @@ function sortWorktrees(worktrees: Worktree[], mode: SortMode): Worktree[] {
})
}
-function filterWorktrees(worktrees: Worktree[], filter: FilterMode, search: string): Worktree[] {
+function filterWorktrees(worktrees: Worktree[], filters: FilterState, search: string): Worktree[] {
let result = worktrees
- if (filter === 'active') {
+ if (filters.activeOnly) {
result = result.filter(isWorktreeActive)
}
+ if (filters.selectedRepos.size > 0) {
+ result = result.filter((w) => filters.selectedRepos.has(w.repo))
+ }
if (search.trim()) {
const q = search.toLowerCase()
result = result.filter(
@@ -162,12 +173,12 @@ function isWorktreePinned(w: Worktree, localPins: Set): boolean {
function buildSections(
worktrees: Worktree[],
sortMode: SortMode,
- filterMode: FilterMode,
+ filters: FilterState,
search: string,
groupMode: GroupMode,
pinnedIds: Set
): Section[] {
- const filtered = filterWorktrees(worktrees, filterMode, search)
+ const filtered = filterWorktrees(worktrees, filters, search)
const sorted = sortWorktrees(filtered, sortMode)
const pinned = sorted.filter((w) => isWorktreePinned(w, pinnedIds))
@@ -232,12 +243,16 @@ export default function HostScreen() {
const [search, setSearch] = useState('')
const [showSearch, setShowSearch] = useState(false)
const [sortMode, setSortMode] = useState('smart')
- const [filterMode, setFilterMode] = useState('all')
+ const [filters, setFilters] = useState({
+ activeOnly: false,
+ selectedRepos: new Set()
+ })
const [groupMode, setGroupMode] = useState('none')
// Modals
const [showSortPicker, setShowSortPicker] = useState(false)
const [showGroupPicker, setShowGroupPicker] = useState(false)
+ const [showFilterModal, setShowFilterModal] = useState(false)
const [actionTarget, setActionTarget] = useState(null)
const [confirmDelete, setConfirmDelete] = useState(null)
const [confirmRemoveHost, setConfirmRemoveHost] = useState(false)
@@ -255,7 +270,10 @@ export default function HostScreen() {
const [pins, prefs] = await Promise.all([loadPinnedIds(hostId), loadPreferences(hostId)])
setPinnedIds(pins)
setSortMode(prefs.sortMode as SortMode)
- setFilterMode(prefs.filterMode as FilterMode)
+ setFilters({
+ activeOnly: prefs.filterMode === 'active',
+ selectedRepos: new Set(prefs.selectedRepos ?? [])
+ })
setGroupMode(prefs.groupMode as GroupMode)
setCollapsedGroups(new Set(prefs.collapsedGroups))
setPrefsLoaded(true)
@@ -391,14 +409,43 @@ export default function HostScreen() {
[hostId]
)
- const handleFilterToggle = useCallback(() => {
- setFilterMode((prev) => {
- const next: FilterMode = prev === 'all' ? 'active' : 'all'
- if (hostId) void savePreferences(hostId, { filterMode: next })
+ const toggleActiveFilter = useCallback(() => {
+ setFilters((prev) => {
+ const next = { ...prev, activeOnly: !prev.activeOnly }
+ if (hostId)
+ void savePreferences(hostId, {
+ filterMode: next.activeOnly ? 'active' : 'all'
+ })
return next
})
}, [hostId])
+ const toggleRepoFilter = useCallback(
+ (repo: string) => {
+ setFilters((prev) => {
+ const next = new Set(prev.selectedRepos)
+ if (next.has(repo)) next.delete(repo)
+ else next.add(repo)
+ const updated = { ...prev, selectedRepos: next }
+ if (hostId) void savePreferences(hostId, { selectedRepos: [...next] })
+ return updated
+ })
+ },
+ [hostId]
+ )
+
+ const clearFilters = useCallback(() => {
+ setFilters({ activeOnly: false, selectedRepos: new Set() })
+ if (hostId) void savePreferences(hostId, { filterMode: 'all', selectedRepos: [] })
+ }, [hostId])
+
+ const activeFilterCount = useMemo(() => {
+ let count = 0
+ if (filters.activeOnly) count++
+ count += filters.selectedRepos.size
+ return count
+ }, [filters])
+
const handleGroupChange = useCallback(
(value: GroupMode) => {
setGroupMode(value)
@@ -412,6 +459,14 @@ export default function HostScreen() {
? lastKnownWorktrees
: worktrees
+ const uniqueRepos = useMemo(() => {
+ const repos = new Map()
+ for (const w of displayWorktrees) {
+ if (!repos.has(w.repo)) repos.set(w.repo, repoColor(w.repo))
+ }
+ return [...repos.entries()].map(([name, color]) => ({ name, color }))
+ }, [displayWorktrees])
+
const toggleCollapsed = useCallback(
(title: string) => {
setCollapsedGroups((prev) => {
@@ -426,8 +481,8 @@ export default function HostScreen() {
)
const rawSections = useMemo(
- () => buildSections(displayWorktrees, sortMode, filterMode, search, groupMode, pinnedIds),
- [displayWorktrees, sortMode, filterMode, search, groupMode, pinnedIds]
+ () => buildSections(displayWorktrees, sortMode, filters, search, groupMode, pinnedIds),
+ [displayWorktrees, sortMode, filters, search, groupMode, pinnedIds]
)
const sections = useMemo(
@@ -470,16 +525,17 @@ export default function HostScreen() {
{/* Filter/sort/group toolbar */}
0 && styles.filterChipActive]}
+ onPress={() => setShowFilterModal(true)}
>
+ 0 ? colors.textPrimary : colors.textSecondary}
+ />
0 && styles.filterChipTextActive]}
>
- Active
+ Filter{activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}
@@ -574,8 +630,8 @@ export default function HostScreen() {
{search
? 'No matching worktrees'
- : filterMode === 'active'
- ? 'No active worktrees'
+ : activeFilterCount > 0
+ ? 'No worktrees match filters'
: 'No worktrees'}
@@ -697,6 +753,48 @@ export default function HostScreen() {
onClose={() => setShowGroupPicker(false)}
/>
+ {/* Filter modal — matches desktop's Status + Repositories dropdown */}
+ setShowFilterModal(false)}>
+
+ Filter
+ {activeFilterCount > 0 && (
+
+ Clear filters
+
+ )}
+
+
+ Status
+
+
+ Active only
+ {filters.activeOnly && }
+
+
+
+ {uniqueRepos.length > 1 && (
+ <>
+ Repositories
+
+ {uniqueRepos.map((repo, i) => (
+
+ {i > 0 && }
+ toggleRepoFilter(repo.name)}>
+
+
+ {repo.name}
+
+ {filters.selectedRepos.has(repo.name) && (
+
+ )}
+
+
+ ))}
+
+ >
+ )}
+
+
{/* Worktree long-press action sheet */}
>(new Set())
const activeHandleRef = useRef(null)
const subscribeSeqRef = useRef