Skip to content

feat: Add Projects tab with session filtering#4

Open
neilpoulin wants to merge 3 commits intoR44VC0RP:mainfrom
neilpoulin:feature/projects-tab
Open

feat: Add Projects tab with session filtering#4
neilpoulin wants to merge 3 commits intoR44VC0RP:mainfrom
neilpoulin:feature/projects-tab

Conversation

@neilpoulin
Copy link

@neilpoulin neilpoulin commented Jan 7, 2026

Summary

This PR adds a Projects tab that allows users to browse and switch between different OpenCode projects.

Features

  • Projects Tab: New tab in the tab bar to view all available projects
  • Session Filtering: Select a project to filter sessions to only show that project's sessions
  • Filter Indicator: Shows the selected project name in the Sessions header with an X button to clear the filter
  • Path Display: Project paths are displayed relative to home directory (e.g., ~/repos/project)
  • API Integration: Sessions are fetched with the directory query parameter for server-side filtering

Changes

  • app/(tabs)/projects/ - New route for the Projects tab
  • app/(tabs)/_layout.tsx - Added Projects trigger to NativeTabs
  • src/providers/OpenCodeProvider.tsx - Added selectedProject state and session filtering by directory
  • src/screens/ProjectsScreen.tsx - Updated to accept projects data as props with path formatting
  • src/screens/SessionsScreen.tsx - Added project filter indicator in header
  • src/components/Icon.tsx - Added 'x' icon for clearing filter
Projects screen Sessions Filtered by Project
Screenshot 2026-01-06 at 8 50 31 PM Screenshot 2026-01-06 at 8 51 39 PM

Testing

  1. Connect to an OpenCode server with multiple projects
  2. Navigate to the Projects tab
  3. Select a project to filter sessions
  4. Verify sessions are filtered to the selected project
  5. Tap the project indicator to clear the filter

Summary by CodeRabbit

  • New Features
    • New "Projects" tab added to main navigation (now appears before Sessions) to browse and select workspace projects.
    • Sessions can be scoped to a selected project; header shows a project badge with a clear action to remove the filter.
    • Project list shows friendly names and cleaned/shortened paths; supports pull-to-refresh and improved loading/empty states.

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Adds 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 x icon.

Changes

Cohort / File(s) Summary
Tab Navigation
app/(tabs)/_layout.tsx
Inserted a new NativeTabs.Trigger for the "projects" tab (Icon + Label), placed before the existing "sessions" trigger.
Projects Layout & Page
app/(tabs)/projects/_layout.tsx, app/(tabs)/projects/index.tsx
New Expo Router stack layout and index page that consume useOpenCode, render ProjectsScreen, handle project selection (calls setSelectedProject) and navigate to sessions.
Projects Screen Component
src/screens/ProjectsScreen.tsx
Converted to a prop-driven component: accepts projects, loading/refreshing, and onRefresh; uses getProjectName/getProjectPath; conditional RefreshControl and updated empty-state messaging.
Sessions Screen Component
src/screens/SessionsScreen.tsx
Accepts selectedProject and onClearProject; header conditionally shows a project badge (folder icon, name, clear action) or session count; uses getProjectName.
State Management / Provider
src/providers/OpenCodeProvider.tsx
Added selectedProject state and setSelectedProject; Project adds optional worktree?: string; session fetching/refetch now accept a directory derived from selected project; provider exposes selectedProject APIs.
Project Utilities
src/utils/project.ts
New helpers: getProjectPathRaw, getProjectName, formatPath, and getProjectPath for deriving display name and formatted path (handles home ~ and Windows paths).
Icon System
src/components/Icon.tsx
IconName union extended with 'x'; added import and mapping for the X icon.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped a tab where projects bloom and rest,
I set a worktree, pointed sessions to the nest.
A tiny badge, a clear — then paths unwind,
Names trimmed neat, and icons new to find.
✨ — Hoppy changes, from your rabbit friend.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a Projects tab with session filtering capability, which aligns with all the file changes and new features introduced.
✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/providers/OpenCodeProvider.tsx (2)

1-1: Remove unused useMemo import.

The useMemo hook 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?.path is duplicated in refreshSessions (line 324) and the useEffect (line 335). Consider extracting this to a small helper or deriving it once via useMemo (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: Duplicate getProjectName logic exists in ProjectsScreen.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.tsx and ProjectsScreen.tsx.


346-350: Consider adding gap between filter elements for consistent spacing.

The project filter row uses marginLeft: 4 inline (line 217) for spacing between the icon and text. For consistency with the rest of the styles that use the spacing tokens, consider adding a gap property to the projectFilter style.

🔎 Proposed fix
   projectFilter: {
     flexDirection: 'row',
     alignItems: 'center',
     marginTop: spacing.xs,
+    gap: spacing.xs,
   },

Then remove the inline marginLeft: 4 from 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: getProjectName is duplicated with SessionsScreen.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

📥 Commits

Reviewing files that changed from the base of the PR and between 49488da and a616793.

📒 Files selected for processing (8)
  • app/(tabs)/_layout.tsx
  • app/(tabs)/projects/_layout.tsx
  • app/(tabs)/projects/index.tsx
  • app/(tabs)/sessions/index.tsx
  • src/components/Icon.tsx
  • src/providers/OpenCodeProvider.tsx
  • src/screens/ProjectsScreen.tsx
  • src/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 selectedProject changes 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 formatPath function 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 of RefreshControl.

Conditionally rendering the RefreshControl only when onRefresh is 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 about onClearProject being optional is valid, but the codebase handles it correctly.

The prop is indeed optional (onClearProject?: () => void), and the badge is only rendered when selectedProject is truthy. However, checking the actual usage in app/(tabs)/sessions/index.tsx, onClearProject is consistently passed whenever selectedProject is set, so the theoretical risk doesn't manifest in practice.

If you want to be more defensive, you could guard the onPress handler with onPress={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
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a616793 and 4bd5802.

📒 Files selected for processing (4)
  • src/providers/OpenCodeProvider.tsx
  • src/screens/ProjectsScreen.tsx
  • src/screens/SessionsScreen.tsx
  • src/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 worktree over path, 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 RefreshControl and 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 worktree field 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 useMemo to derive the directory avoids duplication and ensures the value is stable across renders. The refreshSessions callback correctly includes selectedProjectDirectory in 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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 on C:, some systems use D:, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd5802 and e3c7f28.

📒 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) over path, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant