feat: Add Projects tab with session filtering#4
feat: Add Projects tab with session filtering#4neilpoulin wants to merge 3 commits intoR44VC0RP:mainfrom
Conversation
- Add Projects tab to browse all available projects - Select a project to filter sessions by that project's directory - Show selected project indicator in Sessions header with clear button - Sessions are fetched from API with directory parameter for filtering - Display project paths relative to home directory (~/...) - Add 'x' icon for clearing project filter
📝 WalkthroughWalkthroughAdds a "projects" tab and screens; OpenCodeProvider gains selectedProject state and directory-aware session fetching; ProjectsScreen becomes prop-driven using new project utils; SessionsScreen shows a project badge with clear action; Icon set adds an Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant UI as App (Tabs)
participant OpenCode as OpenCodeProvider
participant Projects as ProjectsScreen
participant Sessions as SessionsScreen
rect rgb(240,248,255)
User->>UI: Open "Projects" tab
UI->>OpenCode: useOpenCode() → projects, loading
OpenCode-->>UI: return projects & handlers
UI->>Projects: render with projects, loading, onSelectProject
end
rect rgb(245,255,240)
User->>Projects: Select project
Projects->>OpenCode: setSelectedProject(project)
OpenCode->>OpenCode: derive directory (worktree||path), clear cache, fetch sessions(dir)
UI->>UI: navigate to Sessions tab
Sessions->>OpenCode: useOpenCode() → selectedProject
OpenCode-->>Sessions: return selectedProject
Sessions->>Sessions: render project badge in header
User->>Sessions: Tap clear
Sessions->>OpenCode: setSelectedProject(null)
OpenCode->>OpenCode: fetch sessions(without directory)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
src/providers/OpenCodeProvider.tsx (2)
1-1: Remove unuseduseMemoimport.The
useMemohook is imported but never used in this file.🔎 Proposed fix
-import React, { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react';
322-338: Consider extracting duplicate directory derivation logic.The logic
selectedProject?.worktree || selectedProject?.pathis duplicated inrefreshSessions(line 324) and theuseEffect(line 335). Consider extracting this to a small helper or deriving it once viauseMemo(which is already imported) to keep the logic DRY and easier to maintain.🔎 Proposed refactor
+ // Derive the directory for session filtering + const selectedProjectDirectory = useMemo(() => { + return selectedProject?.worktree || selectedProject?.path; + }, [selectedProject]); + // Refresh sessions (uses selected project's directory if set) const refreshSessions = useCallback(() => { - const directory = selectedProject?.worktree || selectedProject?.path; - fetchSessions(true, directory); - }, [fetchSessions, selectedProject]); + fetchSessions(true, selectedProjectDirectory); + }, [fetchSessions, selectedProjectDirectory]); // Auto-fetch sessions when connected or project changes useEffect(() => { if (connected) { // Clear sessions cache when project changes to avoid showing stale data cacheRef.current.sessions = null; setSessions([]); - const directory = selectedProject?.worktree || selectedProject?.path; - fetchSessions(false, directory); + fetchSessions(false, selectedProjectDirectory); } - }, [connected, fetchSessions, selectedProject]); + }, [connected, fetchSessions, selectedProjectDirectory]);src/screens/SessionsScreen.tsx (2)
194-203: DuplicategetProjectNamelogic exists inProjectsScreen.tsx.This helper function duplicates the logic in
ProjectsScreen.tsx(lines 54-65). Consider extracting it to a shared utility (e.g.,src/utils/project.ts) to keep the codebase DRY and ensure consistent project name derivation across the app.🔎 Example shared utility
Create a new file
src/utils/project.ts:import type { Project } from '../providers/OpenCodeProvider'; export function getProjectName(project: Project): string { if (project.name) return project.name; const path = project.worktree || project.path; if (path) { const parts = path.split('/').filter(Boolean); return parts[parts.length - 1] || path; } return project.id; }Then import and use it in both
SessionsScreen.tsxandProjectsScreen.tsx.
346-350: Consider adding gap between filter elements for consistent spacing.The project filter row uses
marginLeft: 4inline (line 217) for spacing between the icon and text. For consistency with the rest of the styles that use thespacingtokens, consider adding agapproperty to theprojectFilterstyle.🔎 Proposed fix
projectFilter: { flexDirection: 'row', alignItems: 'center', marginTop: spacing.xs, + gap: spacing.xs, },Then remove the inline
marginLeft: 4from line 217:- <Text style={[theme.small, { color: c.accent, marginLeft: 4 }]} numberOfLines={1}> + <Text style={[theme.small, { color: c.accent }]} numberOfLines={1}>src/screens/ProjectsScreen.tsx (1)
54-65:getProjectNameis duplicated withSessionsScreen.tsx.As noted for
SessionsScreen.tsx, this helper is duplicated. Extract to a shared utility for maintainability.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
app/(tabs)/_layout.tsxapp/(tabs)/projects/_layout.tsxapp/(tabs)/projects/index.tsxapp/(tabs)/sessions/index.tsxsrc/components/Icon.tsxsrc/providers/OpenCodeProvider.tsxsrc/screens/ProjectsScreen.tsxsrc/screens/SessionsScreen.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
app/(tabs)/projects/index.tsx (2)
src/providers/OpenCodeProvider.tsx (2)
useOpenCode(623-629)Project(19-24)src/screens/ProjectsScreen.tsx (1)
ProjectsScreen(24-152)
app/(tabs)/_layout.tsx (1)
src/components/Icon.tsx (1)
Icon(138-153)
src/screens/SessionsScreen.tsx (3)
src/providers/OpenCodeProvider.tsx (3)
SessionWithPreview(15-17)Session(6-13)Project(19-24)src/components/Icon.tsx (1)
Icon(138-153)src/theme/index.ts (1)
spacing(85-93)
app/(tabs)/sessions/index.tsx (2)
src/providers/OpenCodeProvider.tsx (2)
useOpenCode(623-629)Session(6-13)src/screens/SessionsScreen.tsx (1)
SessionsScreen(31-261)
src/screens/ProjectsScreen.tsx (2)
src/providers/OpenCodeProvider.tsx (1)
Project(19-24)src/hooks/useOpenCode.ts (1)
Project(6-6)
🔇 Additional comments (10)
app/(tabs)/projects/_layout.tsx (1)
1-9: LGTM!The layout follows Expo Router conventions correctly, setting up a Stack navigator with a hidden header for the Projects tab flow.
src/components/Icon.tsx (1)
42-42: LGTM!The 'x' icon addition follows the existing pattern correctly—import, type union extension, and icon map entry are all consistent with the component's structure.
Also applies to: 85-86, 135-135
app/(tabs)/_layout.tsx (1)
22-25: LGTM!The new Projects tab trigger follows the existing pattern and uses appropriate SF Symbols for the folder icon.
app/(tabs)/projects/index.tsx (1)
6-25: LGTM!The component correctly wires the Projects screen to the OpenCode context and handles project selection with navigation to the sessions tab. The state update happens synchronously before navigation, ensuring the selected project is available when the Sessions screen renders.
app/(tabs)/sessions/index.tsx (1)
13-14: LGTM!The project filter integration is well-implemented. The clear action correctly resets the selected project to null, and the props are properly wired to SessionsScreen for displaying and clearing the project filter.
Also applies to: 22-24, 33-34
src/providers/OpenCodeProvider.tsx (1)
328-338: Good cache invalidation on project change.Clearing the sessions cache and state when
selectedProjectchanges prevents displaying stale data from a previous project filter. This is a solid approach for the stale-while-revalidate pattern.src/screens/ProjectsScreen.tsx (3)
33-47: Path formatting handles cross-platform paths well.The
formatPathfunction correctly handles macOS, Linux, and Windows user home directory patterns. The Windows regex appropriately uses the case-insensitive flag (/i).One edge case: Windows paths with backslashes will be converted to have a forward slash prefix (
~/) but retain backslashes in the rest of the path (e.g.,C:\Users\john\projects\foo→~/projects\foo). If full normalization is desired, consider replacing backslashes with forward slashes:🔎 Optional: Normalize Windows backslashes
const formatPath = (path: string) => { const homePatterns = [ /^\/Users\/[^/]+\//, // macOS: /Users/username/ /^\/home\/[^/]+\//, // Linux: /home/username/ /^C:\\Users\\[^\\]+\\/i, // Windows: C:\Users\username\ ]; for (const pattern of homePatterns) { if (pattern.test(path)) { - return '~/' + path.replace(pattern, ''); + return '~/' + path.replace(pattern, '').replace(/\\/g, '/'); } } return path; };
121-128: Good conditional rendering ofRefreshControl.Conditionally rendering the
RefreshControlonly whenonRefreshis provided is a clean approach that avoids unnecessary component instantiation and follows the prop-driven design well.
24-30: Props interface and defaults are well-structured.The component correctly uses optional props with sensible defaults (
loading = false,refreshing = false). This makes the component flexible for different use cases while maintaining a clean API.src/screens/SessionsScreen.tsx (1)
211-221: The concern aboutonClearProjectbeing optional is valid, but the codebase handles it correctly.The prop is indeed optional (
onClearProject?: () => void), and the badge is only rendered whenselectedProjectis truthy. However, checking the actual usage inapp/(tabs)/sessions/index.tsx,onClearProjectis consistently passed wheneverselectedProjectis set, so the theoretical risk doesn't manifest in practice.If you want to be more defensive, you could guard the
onPresshandler withonPress={onClearProject || undefined}or conditionally render the badge only when both props are provided—but this isn't necessary given the current calling pattern.
- Extract duplicate getProjectName and path utilities to src/utils/project.ts - Use useMemo for selectedProjectDirectory to avoid duplicate derivation - Use gap style instead of inline marginLeft for consistent spacing - Normalize Windows backslashes in path formatting
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @src/utils/project.ts:
- Around line 14-24: The getProjectName function fails on Windows paths because
it only splits on '/', so normalize backslashes first: in getProjectName (and
where you call getProjectPathRaw), take the returned path, replace backslashes
with forward slashes (e.g., path = path.replace(/\\+/g, '/')) before calling
split('/'), then continue with parts[parts.length - 1] || path to extract the
folder name; this ensures Windows paths like "C:\Users\name\project" yield
"project".
🧹 Nitpick comments (1)
src/screens/SessionsScreen.tsx (1)
199-217: LGTM!The project filter badge provides clear visual feedback with a clean interaction pattern. The conditional rendering correctly guards against null values.
Optional: Consider extracting the inline style on line 199 to the StyleSheet for consistency:
🔎 Optional refactor
<View style={[theme.header, { paddingTop: topPadding }]}> - <View style={{ flex: 1 }}> + <View style={styles.headerContent}> <Text style={theme.title}>Sessions</Text>Add to StyleSheet:
+ headerContent: { + flex: 1, + },
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/providers/OpenCodeProvider.tsxsrc/screens/ProjectsScreen.tsxsrc/screens/SessionsScreen.tsxsrc/utils/project.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/utils/project.ts (1)
src/providers/OpenCodeProvider.tsx (1)
Project(19-24)
src/screens/SessionsScreen.tsx (4)
src/providers/OpenCodeProvider.tsx (3)
SessionWithPreview(15-17)Session(6-13)Project(19-24)src/components/Icon.tsx (1)
Icon(138-153)src/utils/project.ts (1)
getProjectName(14-24)src/theme/index.ts (1)
spacing(85-93)
src/screens/ProjectsScreen.tsx (2)
src/providers/OpenCodeProvider.tsx (1)
Project(19-24)src/utils/project.ts (2)
getProjectName(14-24)getProjectPath(49-55)
🔇 Additional comments (13)
src/utils/project.ts (3)
6-8: LGTM!The fallback chain correctly prioritizes
worktreeoverpath, matching the SDK's behavior as documented in the interface.
30-43: LGTM!The function correctly handles home directory patterns for macOS, Linux, and Windows, and properly normalizes backslashes to forward slashes for consistent display.
49-55: LGTM!Clean composition that correctly handles the null case when no path is available.
src/screens/ProjectsScreen.tsx (3)
17-31: LGTM!The refactor to a prop-driven component improves testability and separation of concerns. The props interface is well-structured with appropriate defaults.
34-62: LGTM!Correct usage of the new utility functions with appropriate null handling for the optional path display.
80-108: LGTM!The conditional rendering of
RefreshControland the adaptive empty state messaging provide a good user experience and handle edge cases gracefully.src/screens/SessionsScreen.tsx (1)
14-15: LGTM!The new props are properly typed with appropriate optional markers, enabling the project filtering feature without breaking existing usage.
Also applies to: 17-25, 32-40
src/providers/OpenCodeProvider.tsx (6)
19-24: LGTM!The
worktreefield addition properly reflects the SDK's behavior and enables the utility functions to access the correct project path.
112-115: LGTM!The context interface properly exposes the project selection state with appropriate null handling.
236-256: LGTM!The server-side filtering implementation correctly passes the directory parameter to the SDK only when provided, enabling efficient project-scoped session queries.
322-330: LGTM!The use of
useMemoto derive the directory avoids duplication and ensures the value is stable across renders. TherefreshSessionscallback correctly includesselectedProjectDirectoryin its dependencies.
333-341: LGTM!The cache invalidation when the selected project changes is the correct approach. While this briefly shows an empty state, it prevents displaying stale sessions from the previously selected project, which would be a worse user experience. The immediate fetch on line 339 minimizes the empty state duration.
590-617: LGTM!The context value correctly exports the new project selection state, completing the integration and making it available to consuming components throughout the app.
Ensures Windows paths like 'C:\Users\name\project' correctly yield 'project' by normalizing backslashes to forward slashes before splitting.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/utils/project.ts (1)
32-45: Consider supporting non-C: drives for Windows home directories.The Windows pattern on line 36 is hardcoded to the
C:drive. While most users have their home onC:, some systems useD:,E:, or other drives. The function will still work (returning the full path), but users on non-C: drives won't see the~/formatting.🔎 Optional enhancement to support any drive letter
const homePatterns = [ /^\/Users\/[^/]+\//, // macOS: /Users/username/ /^\/home\/[^/]+\//, // Linux: /home/username/ - /^C:\\Users\\[^\\]+\\/i, // Windows: C:\Users\username\ + /^[A-Z]:\\Users\\[^\\]+\\/i, // Windows: <Drive>:\Users\username\ ];
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/utils/project.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/utils/project.ts (1)
src/providers/OpenCodeProvider.tsx (1)
Project(19-24)
🔇 Additional comments (3)
src/utils/project.ts (3)
6-8: LGTM!The function correctly prioritizes
worktree(SDK field) overpath, with appropriate undefined handling.
14-26: Windows path handling looks good!The backslash normalization on line 20 correctly addresses the previous review concern. Windows paths are now normalized to forward slashes before splitting, ensuring the folder name is extracted correctly from paths like
C:\Users\name\project.
51-57: LGTM!Clean wrapper that composes the raw path accessor with formatting. Null handling is appropriate.
Summary
This PR adds a Projects tab that allows users to browse and switch between different OpenCode projects.
Features
~/repos/project)directoryquery parameter for server-side filteringChanges
app/(tabs)/projects/- New route for the Projects tabapp/(tabs)/_layout.tsx- Added Projects trigger to NativeTabssrc/providers/OpenCodeProvider.tsx- AddedselectedProjectstate and session filtering by directorysrc/screens/ProjectsScreen.tsx- Updated to accept projects data as props with path formattingsrc/screens/SessionsScreen.tsx- Added project filter indicator in headersrc/components/Icon.tsx- Added 'x' icon for clearing filterTesting
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.