lazyreview 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.github/workflows/publish-npm.yml +51 -0
  2. package/.prettierrc +7 -0
  3. package/CLAUDE.md +50 -0
  4. package/README.md +119 -0
  5. package/dist/cli.js +3830 -0
  6. package/dist/cli.js.map +1 -0
  7. package/package.json +64 -0
  8. package/pnpm-workspace.yaml +5 -0
  9. package/src/app.tsx +235 -0
  10. package/src/cli.tsx +78 -0
  11. package/src/components/common/BorderedBox.tsx +41 -0
  12. package/src/components/common/Divider.tsx +31 -0
  13. package/src/components/common/EmptyState.tsx +35 -0
  14. package/src/components/common/FilterModal.tsx +117 -0
  15. package/src/components/common/LoadingIndicator.tsx +31 -0
  16. package/src/components/common/Modal.tsx +24 -0
  17. package/src/components/common/PaginationBar.tsx +56 -0
  18. package/src/components/common/SortModal.tsx +91 -0
  19. package/src/components/common/Spinner.tsx +28 -0
  20. package/src/components/common/index.ts +9 -0
  21. package/src/components/layout/HelpModal.tsx +61 -0
  22. package/src/components/layout/MainPanel.tsx +26 -0
  23. package/src/components/layout/Sidebar.tsx +71 -0
  24. package/src/components/layout/StatusBar.tsx +42 -0
  25. package/src/components/layout/TokenInputModal.tsx +92 -0
  26. package/src/components/layout/TopBar.tsx +44 -0
  27. package/src/components/layout/index.ts +6 -0
  28. package/src/components/pr/CommentsTab.tsx +92 -0
  29. package/src/components/pr/CommitsTab.tsx +142 -0
  30. package/src/components/pr/ConversationsTab.tsx +273 -0
  31. package/src/components/pr/FilesTab.tsx +532 -0
  32. package/src/components/pr/PRHeader.tsx +69 -0
  33. package/src/components/pr/PRListItem.tsx +92 -0
  34. package/src/components/pr/PRTabs.tsx +50 -0
  35. package/src/components/pr/index.ts +8 -0
  36. package/src/hooks/index.ts +12 -0
  37. package/src/hooks/useActivePanel.ts +54 -0
  38. package/src/hooks/useAppKeymap.ts +22 -0
  39. package/src/hooks/useAuth.ts +131 -0
  40. package/src/hooks/useConfig.ts +52 -0
  41. package/src/hooks/useFilter.ts +192 -0
  42. package/src/hooks/useGitHub.ts +260 -0
  43. package/src/hooks/useInputFocus.tsx +35 -0
  44. package/src/hooks/useListNavigation.ts +121 -0
  45. package/src/hooks/useLoading.ts +25 -0
  46. package/src/hooks/usePagination.ts +87 -0
  47. package/src/models/comment.ts +15 -0
  48. package/src/models/commit.ts +20 -0
  49. package/src/models/diff.ts +93 -0
  50. package/src/models/errors.ts +24 -0
  51. package/src/models/file-change.ts +22 -0
  52. package/src/models/index.ts +12 -0
  53. package/src/models/pull-request.ts +40 -0
  54. package/src/models/review.ts +17 -0
  55. package/src/models/user.ts +9 -0
  56. package/src/screens/InvolvedScreen.tsx +161 -0
  57. package/src/screens/MyPRsScreen.tsx +161 -0
  58. package/src/screens/PRDetailScreen.tsx +88 -0
  59. package/src/screens/PRListScreen.tsx +96 -0
  60. package/src/screens/ReviewRequestsScreen.tsx +161 -0
  61. package/src/screens/SettingsScreen.tsx +284 -0
  62. package/src/screens/ThisRepoScreen.tsx +175 -0
  63. package/src/screens/index.ts +7 -0
  64. package/src/services/Auth.ts +314 -0
  65. package/src/services/Config.ts +81 -0
  66. package/src/services/GitHubApi.ts +262 -0
  67. package/src/services/Loading.ts +54 -0
  68. package/src/services/index.ts +27 -0
  69. package/src/theme/index.ts +28 -0
  70. package/src/theme/themes.ts +84 -0
  71. package/src/theme/types.ts +28 -0
  72. package/src/utils/date.ts +28 -0
  73. package/src/utils/git.ts +67 -0
  74. package/src/utils/index.ts +2 -0
  75. package/src/utils/terminal.ts +25 -0
  76. package/tsconfig.json +24 -0
  77. package/tsup.config.ts +13 -0
  78. package/vitest.config.ts +26 -0
@@ -0,0 +1,161 @@
1
+ import React, { useState } from 'react'
2
+ import { Box, Text, useInput } from 'ink'
3
+ import { useTheme } from '../theme/index'
4
+ import { useReviewRequests } from '../hooks/useGitHub'
5
+ import { useListNavigation } from '../hooks/useListNavigation'
6
+ import { usePagination } from '../hooks/usePagination'
7
+ import { useFilter } from '../hooks/useFilter'
8
+ import { PRListItem } from '../components/pr/PRListItem'
9
+ import { EmptyState } from '../components/common/EmptyState'
10
+ import { LoadingIndicator } from '../components/common/LoadingIndicator'
11
+ import { PaginationBar } from '../components/common/PaginationBar'
12
+ import { FilterModal } from '../components/common/FilterModal'
13
+ import { SortModal } from '../components/common/SortModal'
14
+ import type { PullRequest } from '../models/pull-request'
15
+
16
+ interface ReviewRequestsScreenProps {
17
+ readonly onSelect: (pr: PullRequest) => void
18
+ }
19
+
20
+ export function ReviewRequestsScreen({
21
+ onSelect,
22
+ }: ReviewRequestsScreenProps): React.ReactElement {
23
+ const theme = useTheme()
24
+ const { data: prs = [], isLoading, error } = useReviewRequests()
25
+ const [showFilter, setShowFilter] = useState(false)
26
+ const [showSort, setShowSort] = useState(false)
27
+
28
+ const {
29
+ filter,
30
+ filteredItems,
31
+ setSearch,
32
+ setRepo,
33
+ setAuthor,
34
+ setLabel,
35
+ setSortBy,
36
+ toggleSortDirection,
37
+ clearFilters,
38
+ hasActiveFilters,
39
+ availableRepos,
40
+ availableAuthors,
41
+ availableLabels,
42
+ } = useFilter(prs)
43
+
44
+ const {
45
+ currentPage,
46
+ totalPages,
47
+ pageItems,
48
+ hasNextPage,
49
+ hasPrevPage,
50
+ nextPage,
51
+ prevPage,
52
+ startIndex,
53
+ endIndex,
54
+ } = usePagination(filteredItems, { pageSize: 18 })
55
+
56
+ const { selectedIndex } = useListNavigation({
57
+ itemCount: pageItems.length,
58
+ viewportHeight: pageItems.length,
59
+ isActive: !showFilter && !showSort,
60
+ })
61
+
62
+ useInput(
63
+ (input, key) => {
64
+ if (key.return && pageItems[selectedIndex]) {
65
+ onSelect(pageItems[selectedIndex])
66
+ } else if (input === 'n' && hasNextPage) {
67
+ nextPage()
68
+ } else if (input === 'p' && hasPrevPage) {
69
+ prevPage()
70
+ } else if (input === '/') {
71
+ setShowFilter(true)
72
+ } else if (input === 's') {
73
+ setShowSort(true)
74
+ }
75
+ },
76
+ { isActive: !showFilter && !showSort },
77
+ )
78
+
79
+ if (isLoading && prs.length === 0) {
80
+ return <LoadingIndicator message="Loading review requests..." />
81
+ }
82
+
83
+ if (error) {
84
+ return (
85
+ <Box flexDirection="column" padding={1}>
86
+ <Text color={theme.colors.error}>Error: {String(error)}</Text>
87
+ </Box>
88
+ )
89
+ }
90
+
91
+ if (prs.length === 0) {
92
+ return <EmptyState message="No review requests" />
93
+ }
94
+
95
+ return (
96
+ <Box flexDirection="column" flexGrow={1}>
97
+ <Box paddingX={1} justifyContent="space-between">
98
+ <Box gap={2}>
99
+ <Text color={theme.colors.accent} bold>
100
+ For Review
101
+ </Text>
102
+ {hasActiveFilters && (
103
+ <Text color={theme.colors.warning}>(filtered)</Text>
104
+ )}
105
+ <Text color={theme.colors.muted}>/ filter s sort</Text>
106
+ </Box>
107
+ <PaginationBar
108
+ currentPage={currentPage}
109
+ totalPages={totalPages}
110
+ totalItems={filteredItems.length}
111
+ startIndex={startIndex}
112
+ endIndex={endIndex}
113
+ hasNextPage={hasNextPage}
114
+ hasPrevPage={hasPrevPage}
115
+ />
116
+ </Box>
117
+ <Box flexDirection="column">
118
+ {pageItems.length === 0 ? (
119
+ <Box padding={1}>
120
+ <Text color={theme.colors.muted}>
121
+ No PRs match the current filters
122
+ </Text>
123
+ </Box>
124
+ ) : (
125
+ pageItems.map((pr, index) => (
126
+ <PRListItem
127
+ key={pr.id}
128
+ item={pr}
129
+ isFocus={index === selectedIndex}
130
+ />
131
+ ))
132
+ )}
133
+ </Box>
134
+ {showFilter && (
135
+ <FilterModal
136
+ filter={filter}
137
+ availableRepos={availableRepos}
138
+ availableAuthors={availableAuthors}
139
+ availableLabels={availableLabels}
140
+ onSearchChange={setSearch}
141
+ onRepoChange={setRepo}
142
+ onAuthorChange={setAuthor}
143
+ onLabelChange={setLabel}
144
+ onSortChange={setSortBy}
145
+ onSortDirectionToggle={toggleSortDirection}
146
+ onClear={clearFilters}
147
+ onClose={() => setShowFilter(false)}
148
+ />
149
+ )}
150
+ {showSort && (
151
+ <SortModal
152
+ currentSort={filter.sortBy}
153
+ sortDirection={filter.sortDirection}
154
+ onSortChange={setSortBy}
155
+ onSortDirectionToggle={toggleSortDirection}
156
+ onClose={() => setShowSort(false)}
157
+ />
158
+ )}
159
+ </Box>
160
+ )
161
+ }
@@ -0,0 +1,284 @@
1
+ import React, { useState } from 'react'
2
+ import { Box, Text, useInput } from 'ink'
3
+ import { TextInput } from '@inkjs/ui'
4
+ import { useTheme } from '../theme/index'
5
+ import { useConfig } from '../hooks/useConfig'
6
+ import { useAuth } from '../hooks/useAuth'
7
+ import { Divider } from '../components/common/Divider'
8
+ import { LoadingIndicator } from '../components/common/LoadingIndicator'
9
+ import type { TokenSource } from '../services/Auth'
10
+
11
+ function SettingRow({
12
+ label,
13
+ value,
14
+ isSelected,
15
+ isEditing,
16
+ }: {
17
+ readonly label: string
18
+ readonly value: string
19
+ readonly isSelected?: boolean
20
+ readonly isEditing?: boolean
21
+ }): React.ReactElement {
22
+ const theme = useTheme()
23
+
24
+ return (
25
+ <Box gap={2} paddingX={2}>
26
+ <Box width={20}>
27
+ <Text
28
+ color={isSelected ? theme.colors.accent : theme.colors.muted}
29
+ bold={isSelected}
30
+ >
31
+ {isSelected ? '> ' : ' '}
32
+ {label}
33
+ </Text>
34
+ </Box>
35
+ <Text
36
+ color={isEditing ? theme.colors.accent : theme.colors.text}
37
+ inverse={isEditing}
38
+ >
39
+ {value}
40
+ </Text>
41
+ </Box>
42
+ )
43
+ }
44
+
45
+ function TokenSourceLabel({ source }: { readonly source: TokenSource }): React.ReactElement {
46
+ const theme = useTheme()
47
+
48
+ const labels: Record<TokenSource, { text: string; color: string }> = {
49
+ manual: { text: 'Manual Token', color: theme.colors.warning },
50
+ env: { text: 'Environment Variable', color: theme.colors.success },
51
+ gh_cli: { text: 'GitHub CLI (gh)', color: theme.colors.info },
52
+ none: { text: 'Not configured', color: theme.colors.error },
53
+ }
54
+
55
+ const { text, color } = labels[source]
56
+ return <Text color={color}>{text}</Text>
57
+ }
58
+
59
+ type SettingsItem = 'token_source' | 'new_token' | 'theme' | 'page_size'
60
+
61
+ export function SettingsScreen(): React.ReactElement {
62
+ const theme = useTheme()
63
+ const { config, loading: configLoading, error: configError } = useConfig()
64
+ const {
65
+ tokenInfo,
66
+ availableSources,
67
+ setPreferredSource,
68
+ saveToken,
69
+ loading: authLoading,
70
+ } = useAuth()
71
+
72
+ const [selectedItem, setSelectedItem] = useState<SettingsItem>('token_source')
73
+ const [isEditingToken, setIsEditingToken] = useState(false)
74
+ const [newTokenValue, setNewTokenValue] = useState('')
75
+ const [tokenMessage, setTokenMessage] = useState<string | null>(null)
76
+
77
+ const settingsItems: SettingsItem[] = ['token_source', 'new_token', 'theme', 'page_size']
78
+
79
+ useInput(
80
+ (input, key) => {
81
+ if (isEditingToken) {
82
+ if (key.escape) {
83
+ setIsEditingToken(false)
84
+ setNewTokenValue('')
85
+ } else if (key.return && newTokenValue.trim()) {
86
+ saveToken(newTokenValue.trim())
87
+ .then(() => {
88
+ setTokenMessage('Token saved successfully!')
89
+ setIsEditingToken(false)
90
+ setNewTokenValue('')
91
+ setTimeout(() => setTokenMessage(null), 3000)
92
+ })
93
+ .catch((err) => {
94
+ setTokenMessage(`Error: ${String(err)}`)
95
+ })
96
+ }
97
+ return
98
+ }
99
+
100
+ if (input === 'j' || key.downArrow) {
101
+ const currentIndex = settingsItems.indexOf(selectedItem)
102
+ const nextIndex = Math.min(currentIndex + 1, settingsItems.length - 1)
103
+ setSelectedItem(settingsItems[nextIndex]!)
104
+ } else if (input === 'k' || key.upArrow) {
105
+ const currentIndex = settingsItems.indexOf(selectedItem)
106
+ const prevIndex = Math.max(currentIndex - 1, 0)
107
+ setSelectedItem(settingsItems[prevIndex]!)
108
+ } else if (key.return) {
109
+ if (selectedItem === 'token_source') {
110
+ // Cycle through available sources
111
+ const currentSource = tokenInfo?.source ?? 'none'
112
+ const sourceOrder: TokenSource[] = ['gh_cli', 'env', 'manual']
113
+ const availableInOrder = sourceOrder.filter((s) => availableSources.includes(s))
114
+
115
+ if (availableInOrder.length > 0) {
116
+ const currentIndex = availableInOrder.indexOf(currentSource)
117
+ const nextIndex = (currentIndex + 1) % availableInOrder.length
118
+ setPreferredSource(availableInOrder[nextIndex]!)
119
+ }
120
+ } else if (selectedItem === 'new_token') {
121
+ setIsEditingToken(true)
122
+ }
123
+ }
124
+ },
125
+ { isActive: true },
126
+ )
127
+
128
+ if (configLoading || authLoading) {
129
+ return <LoadingIndicator message="Loading settings..." />
130
+ }
131
+
132
+ if (configError) {
133
+ return (
134
+ <Box padding={1}>
135
+ <Text color={theme.colors.error}>Error: {configError}</Text>
136
+ </Box>
137
+ )
138
+ }
139
+
140
+ return (
141
+ <Box flexDirection="column" flexGrow={1}>
142
+ <Box paddingX={1} paddingY={1}>
143
+ <Text color={theme.colors.accent} bold>
144
+ Settings
145
+ </Text>
146
+ </Box>
147
+ <Box paddingX={1}>
148
+ <Divider />
149
+ </Box>
150
+
151
+ {/* Token Section */}
152
+ <Box flexDirection="column" paddingX={1} marginBottom={1}>
153
+ <Text color={theme.colors.secondary} bold>
154
+ Authentication
155
+ </Text>
156
+ </Box>
157
+
158
+ <Box flexDirection="column" gap={0}>
159
+ <Box gap={2} paddingX={2}>
160
+ <Box width={20}>
161
+ <Text
162
+ color={selectedItem === 'token_source' ? theme.colors.accent : theme.colors.muted}
163
+ bold={selectedItem === 'token_source'}
164
+ >
165
+ {selectedItem === 'token_source' ? '> ' : ' '}
166
+ Token Source
167
+ </Text>
168
+ </Box>
169
+ <TokenSourceLabel source={tokenInfo?.source ?? 'none'} />
170
+ {availableSources.length > 1 && selectedItem === 'token_source' && (
171
+ <Text color={theme.colors.muted} dimColor>
172
+ (Enter to switch)
173
+ </Text>
174
+ )}
175
+ </Box>
176
+
177
+ <Box gap={2} paddingX={2}>
178
+ <Box width={20}>
179
+ <Text color={theme.colors.muted}> Token</Text>
180
+ </Box>
181
+ <Text color={theme.colors.text}>
182
+ {tokenInfo?.maskedToken ?? '(none)'}
183
+ </Text>
184
+ </Box>
185
+
186
+ <Box gap={2} paddingX={2}>
187
+ <Box width={20}>
188
+ <Text color={theme.colors.muted}> Available</Text>
189
+ </Box>
190
+ <Text color={theme.colors.muted}>
191
+ {availableSources.length > 0
192
+ ? availableSources.join(', ')
193
+ : 'none'}
194
+ </Text>
195
+ </Box>
196
+
197
+ <Box gap={2} paddingX={2} marginTop={1}>
198
+ <Box width={20}>
199
+ <Text
200
+ color={selectedItem === 'new_token' ? theme.colors.accent : theme.colors.muted}
201
+ bold={selectedItem === 'new_token'}
202
+ >
203
+ {selectedItem === 'new_token' ? '> ' : ' '}
204
+ Set New Token
205
+ </Text>
206
+ </Box>
207
+ {isEditingToken ? (
208
+ <Box borderStyle="single" borderColor={theme.colors.accent} paddingX={1} width={40}>
209
+ <TextInput
210
+ defaultValue={newTokenValue}
211
+ onChange={setNewTokenValue}
212
+ placeholder="ghp_xxxx..."
213
+ />
214
+ </Box>
215
+ ) : (
216
+ <Text color={theme.colors.muted} dimColor>
217
+ (Enter to add)
218
+ </Text>
219
+ )}
220
+ </Box>
221
+
222
+ {tokenMessage && (
223
+ <Box paddingX={4} marginTop={1}>
224
+ <Text
225
+ color={tokenMessage.startsWith('Error') ? theme.colors.error : theme.colors.success}
226
+ >
227
+ {tokenMessage}
228
+ </Text>
229
+ </Box>
230
+ )}
231
+ </Box>
232
+
233
+ <Box paddingX={1} marginTop={1}>
234
+ <Divider title="Configuration" />
235
+ </Box>
236
+ {/* Config Section */}
237
+ <Box flexDirection="column" paddingX={1} marginTop={0} marginBottom={1}>
238
+ <Text color={theme.colors.secondary} bold>
239
+ Configuration
240
+ </Text>
241
+ </Box>
242
+
243
+ <Box flexDirection="column" gap={0}>
244
+ <SettingRow
245
+ label="Theme"
246
+ value={config?.theme ?? 'tokyo-night'}
247
+ isSelected={selectedItem === 'theme'}
248
+ />
249
+ <SettingRow
250
+ label="Page Size"
251
+ value={String(config?.pageSize ?? 30)}
252
+ isSelected={selectedItem === 'page_size'}
253
+ />
254
+ <SettingRow
255
+ label="Provider"
256
+ value={config?.provider ?? 'github'}
257
+ />
258
+ <SettingRow
259
+ label="Default Owner"
260
+ value={config?.defaultOwner ?? '(not set)'}
261
+ />
262
+ <SettingRow
263
+ label="Default Repo"
264
+ value={config?.defaultRepo ?? '(not set)'}
265
+ />
266
+ </Box>
267
+
268
+ <Box paddingX={1} paddingTop={2} flexDirection="column">
269
+ <Text color={theme.colors.muted} dimColor>
270
+ Config: ~/.config/lazyreview/config.yaml
271
+ </Text>
272
+ <Text color={theme.colors.muted} dimColor>
273
+ Token: ~/.config/lazyreview/.token
274
+ </Text>
275
+ </Box>
276
+
277
+ <Box paddingX={1} paddingTop={1}>
278
+ <Text color={theme.colors.muted} dimColor>
279
+ j/k: navigate | Enter: select/toggle | Esc: cancel
280
+ </Text>
281
+ </Box>
282
+ </Box>
283
+ )
284
+ }
@@ -0,0 +1,175 @@
1
+ import React, { useState } from 'react'
2
+ import { Box, Text, useInput } from 'ink'
3
+ import { useTheme } from '../theme/index'
4
+ import { usePullRequests } from '../hooks/useGitHub'
5
+ import { useListNavigation } from '../hooks/useListNavigation'
6
+ import { usePagination } from '../hooks/usePagination'
7
+ import { useFilter } from '../hooks/useFilter'
8
+ import { PRListItem } from '../components/pr/PRListItem'
9
+ import { EmptyState } from '../components/common/EmptyState'
10
+ import { LoadingIndicator } from '../components/common/LoadingIndicator'
11
+ import { PaginationBar } from '../components/common/PaginationBar'
12
+ import { FilterModal } from '../components/common/FilterModal'
13
+ import { SortModal } from '../components/common/SortModal'
14
+ import type { PullRequest } from '../models/pull-request'
15
+
16
+ interface ThisRepoScreenProps {
17
+ readonly owner: string | null
18
+ readonly repo: string | null
19
+ readonly onSelect: (pr: PullRequest) => void
20
+ }
21
+
22
+ export function ThisRepoScreen({
23
+ owner,
24
+ repo,
25
+ onSelect,
26
+ }: ThisRepoScreenProps): React.ReactElement {
27
+ const theme = useTheme()
28
+ const {
29
+ data: prs = [],
30
+ isLoading,
31
+ error,
32
+ } = usePullRequests(owner ?? '', repo ?? '', { state: 'open' })
33
+ const [showFilter, setShowFilter] = useState(false)
34
+ const [showSort, setShowSort] = useState(false)
35
+
36
+ const {
37
+ filter,
38
+ filteredItems,
39
+ setSearch,
40
+ setRepo: setRepoFilter,
41
+ setAuthor,
42
+ setLabel,
43
+ setSortBy,
44
+ toggleSortDirection,
45
+ clearFilters,
46
+ hasActiveFilters,
47
+ availableRepos,
48
+ availableAuthors,
49
+ availableLabels,
50
+ } = useFilter(prs)
51
+
52
+ const {
53
+ currentPage,
54
+ totalPages,
55
+ pageItems,
56
+ hasNextPage,
57
+ hasPrevPage,
58
+ nextPage,
59
+ prevPage,
60
+ startIndex,
61
+ endIndex,
62
+ } = usePagination(filteredItems, { pageSize: 18 })
63
+
64
+ const { selectedIndex } = useListNavigation({
65
+ itemCount: pageItems.length,
66
+ viewportHeight: pageItems.length,
67
+ isActive: !showFilter && !showSort,
68
+ })
69
+
70
+ useInput(
71
+ (input, key) => {
72
+ if (key.return && pageItems[selectedIndex]) {
73
+ onSelect(pageItems[selectedIndex])
74
+ } else if (input === 'n' && hasNextPage) {
75
+ nextPage()
76
+ } else if (input === 'p' && hasPrevPage) {
77
+ prevPage()
78
+ } else if (input === '/') {
79
+ setShowFilter(true)
80
+ } else if (input === 's') {
81
+ setShowSort(true)
82
+ }
83
+ },
84
+ { isActive: !showFilter && !showSort },
85
+ )
86
+
87
+ if (!owner || !repo) {
88
+ return (
89
+ <EmptyState message="Not in a git repository or remote not detected" />
90
+ )
91
+ }
92
+
93
+ if (isLoading && prs.length === 0) {
94
+ return <LoadingIndicator message={`Loading PRs for ${owner}/${repo}...`} />
95
+ }
96
+
97
+ if (error) {
98
+ return (
99
+ <Box flexDirection="column" padding={1}>
100
+ <Text color={theme.colors.error}>Error: {String(error)}</Text>
101
+ </Box>
102
+ )
103
+ }
104
+
105
+ if (prs.length === 0) {
106
+ return <EmptyState message={`No open PRs in ${owner}/${repo}`} />
107
+ }
108
+
109
+ return (
110
+ <Box flexDirection="column" flexGrow={1}>
111
+ <Box paddingX={1} justifyContent="space-between">
112
+ <Box gap={2}>
113
+ <Text color={theme.colors.accent} bold>
114
+ {owner}/{repo}
115
+ </Text>
116
+ {hasActiveFilters && (
117
+ <Text color={theme.colors.warning}>(filtered)</Text>
118
+ )}
119
+ <Text color={theme.colors.muted}>/ filter s sort</Text>
120
+ </Box>
121
+ <PaginationBar
122
+ currentPage={currentPage}
123
+ totalPages={totalPages}
124
+ totalItems={filteredItems.length}
125
+ startIndex={startIndex}
126
+ endIndex={endIndex}
127
+ hasNextPage={hasNextPage}
128
+ hasPrevPage={hasPrevPage}
129
+ />
130
+ </Box>
131
+ <Box flexDirection="column">
132
+ {pageItems.length === 0 ? (
133
+ <Box padding={1}>
134
+ <Text color={theme.colors.muted}>
135
+ No PRs match the current filters
136
+ </Text>
137
+ </Box>
138
+ ) : (
139
+ pageItems.map((pr, index) => (
140
+ <PRListItem
141
+ key={pr.id}
142
+ item={pr}
143
+ isFocus={index === selectedIndex}
144
+ />
145
+ ))
146
+ )}
147
+ </Box>
148
+ {showFilter && (
149
+ <FilterModal
150
+ filter={filter}
151
+ availableRepos={availableRepos}
152
+ availableAuthors={availableAuthors}
153
+ availableLabels={availableLabels}
154
+ onSearchChange={setSearch}
155
+ onRepoChange={setRepoFilter}
156
+ onAuthorChange={setAuthor}
157
+ onLabelChange={setLabel}
158
+ onSortChange={setSortBy}
159
+ onSortDirectionToggle={toggleSortDirection}
160
+ onClear={clearFilters}
161
+ onClose={() => setShowFilter(false)}
162
+ />
163
+ )}
164
+ {showSort && (
165
+ <SortModal
166
+ currentSort={filter.sortBy}
167
+ sortDirection={filter.sortDirection}
168
+ onSortChange={setSortBy}
169
+ onSortDirectionToggle={toggleSortDirection}
170
+ onClose={() => setShowSort(false)}
171
+ />
172
+ )}
173
+ </Box>
174
+ )
175
+ }
@@ -0,0 +1,7 @@
1
+ export { PRListScreen } from './PRListScreen'
2
+ export { PRDetailScreen } from './PRDetailScreen'
3
+ export { MyPRsScreen } from './MyPRsScreen'
4
+ export { ReviewRequestsScreen } from './ReviewRequestsScreen'
5
+ export { SettingsScreen } from './SettingsScreen'
6
+ export { InvolvedScreen } from './InvolvedScreen'
7
+ export { ThisRepoScreen } from './ThisRepoScreen'