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
package/dist/cli.js ADDED
@@ -0,0 +1,3830 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+
6
+ // src/app.tsx
7
+ import React16, { useState as useState16, useCallback as useCallback6 } from "react";
8
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
+ import { Box as Box26, useApp, useInput as useInput13, useStdout as useStdout8 } from "ink";
10
+
11
+ // src/theme/index.ts
12
+ import React, { createContext, useContext } from "react";
13
+
14
+ // src/theme/themes.ts
15
+ var tokyoNight = {
16
+ name: "tokyo-night",
17
+ colors: {
18
+ bg: "#1a1b26",
19
+ text: "#c0caf5",
20
+ accent: "#7aa2f7",
21
+ muted: "#565f89",
22
+ border: "#3b4261",
23
+ primary: "#7aa2f7",
24
+ secondary: "#bb9af7",
25
+ success: "#9ece6a",
26
+ error: "#f7768e",
27
+ warning: "#e0af68",
28
+ info: "#7dcfff",
29
+ diffAdd: "#9ece6a",
30
+ diffDel: "#f7768e",
31
+ selection: "#283457",
32
+ listSelectedFg: "#c0caf5",
33
+ listSelectedBg: "#283457"
34
+ }
35
+ };
36
+ var dracula = {
37
+ name: "dracula",
38
+ colors: {
39
+ bg: "#282a36",
40
+ text: "#f8f8f2",
41
+ accent: "#bd93f9",
42
+ muted: "#6272a4",
43
+ border: "#44475a",
44
+ primary: "#bd93f9",
45
+ secondary: "#ff79c6",
46
+ success: "#50fa7b",
47
+ error: "#ff5555",
48
+ warning: "#f1fa8c",
49
+ info: "#8be9fd",
50
+ diffAdd: "#50fa7b",
51
+ diffDel: "#ff5555",
52
+ selection: "#44475a",
53
+ listSelectedFg: "#f8f8f2",
54
+ listSelectedBg: "#44475a"
55
+ }
56
+ };
57
+ var catppuccinMocha = {
58
+ name: "catppuccin-mocha",
59
+ colors: {
60
+ bg: "#1e1e2e",
61
+ text: "#cdd6f4",
62
+ accent: "#89b4fa",
63
+ muted: "#6c7086",
64
+ border: "#313244",
65
+ primary: "#89b4fa",
66
+ secondary: "#cba6f7",
67
+ success: "#a6e3a1",
68
+ error: "#f38ba8",
69
+ warning: "#f9e2af",
70
+ info: "#89dceb",
71
+ diffAdd: "#a6e3a1",
72
+ diffDel: "#f38ba8",
73
+ selection: "#313244",
74
+ listSelectedFg: "#cdd6f4",
75
+ listSelectedBg: "#313244"
76
+ }
77
+ };
78
+ var themes = {
79
+ "tokyo-night": tokyoNight,
80
+ dracula,
81
+ "catppuccin-mocha": catppuccinMocha
82
+ };
83
+ var defaultTheme = tokyoNight;
84
+
85
+ // src/theme/index.ts
86
+ var ThemeContext = createContext(defaultTheme);
87
+ function ThemeProvider({
88
+ theme = defaultTheme,
89
+ children
90
+ }) {
91
+ return React.createElement(ThemeContext.Provider, { value: theme }, children);
92
+ }
93
+ function useTheme() {
94
+ return useContext(ThemeContext);
95
+ }
96
+ function getThemeByName(name) {
97
+ return themes[name] ?? defaultTheme;
98
+ }
99
+
100
+ // src/components/layout/TopBar.tsx
101
+ import { Box, Text } from "ink";
102
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
103
+ function TopBar({
104
+ username,
105
+ provider,
106
+ repoPath
107
+ }) {
108
+ const theme = useTheme();
109
+ return /* @__PURE__ */ jsxs(
110
+ Box,
111
+ {
112
+ height: 1,
113
+ width: "100%",
114
+ justifyContent: "space-between",
115
+ paddingX: 1,
116
+ marginTop: 0.6,
117
+ children: [
118
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
119
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.accent, bold: true, children: "LazyReview" }),
120
+ repoPath && /* @__PURE__ */ jsxs(Fragment, { children: [
121
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "\u2502" }),
122
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.text, children: repoPath })
123
+ ] })
124
+ ] }),
125
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
126
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: provider }),
127
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "\u2502" }),
128
+ /* @__PURE__ */ jsx(Text, { color: theme.colors.secondary, children: username })
129
+ ] })
130
+ ]
131
+ }
132
+ );
133
+ }
134
+
135
+ // src/components/layout/Sidebar.tsx
136
+ import { Box as Box2, Text as Text2 } from "ink";
137
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
138
+ var SIDEBAR_ITEMS = [
139
+ "Involved",
140
+ "My PRs",
141
+ "For Review",
142
+ "This Repo",
143
+ "Settings"
144
+ ];
145
+ var sidebarIcons = {
146
+ Involved: "\u25C6",
147
+ "My PRs": "\u25CF",
148
+ "For Review": "\u25CE",
149
+ "This Repo": "\u25C8",
150
+ Settings: "\u2699"
151
+ };
152
+ function Sidebar({
153
+ selectedIndex,
154
+ visible,
155
+ isActive
156
+ }) {
157
+ const theme = useTheme();
158
+ if (!visible) return null;
159
+ return /* @__PURE__ */ jsxs2(
160
+ Box2,
161
+ {
162
+ flexDirection: "column",
163
+ width: 24,
164
+ borderStyle: "single",
165
+ borderColor: isActive ? theme.colors.accent : theme.colors.border,
166
+ children: [
167
+ /* @__PURE__ */ jsx2(Box2, { paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx2(Text2, { color: theme.colors.accent, bold: isActive, dimColor: !isActive, children: "Navigation" }) }),
168
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingTop: 1, children: SIDEBAR_ITEMS.map((label, index) => {
169
+ const isSelected = index === selectedIndex;
170
+ const icon = sidebarIcons[label];
171
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(
172
+ Text2,
173
+ {
174
+ color: isSelected ? theme.colors.accent : theme.colors.text,
175
+ backgroundColor: isSelected ? theme.colors.selection : void 0,
176
+ bold: isSelected,
177
+ dimColor: !isActive && !isSelected,
178
+ children: [
179
+ isSelected ? "\u25B8 " : " ",
180
+ icon,
181
+ " ",
182
+ label
183
+ ]
184
+ }
185
+ ) }, label);
186
+ }) })
187
+ ]
188
+ }
189
+ );
190
+ }
191
+
192
+ // src/components/layout/MainPanel.tsx
193
+ import { Box as Box3 } from "ink";
194
+ import { jsx as jsx3 } from "react/jsx-runtime";
195
+ function MainPanel({
196
+ children,
197
+ isActive = false
198
+ }) {
199
+ const theme = useTheme();
200
+ return /* @__PURE__ */ jsx3(
201
+ Box3,
202
+ {
203
+ flexDirection: "column",
204
+ flexGrow: 1,
205
+ borderStyle: "single",
206
+ borderColor: isActive ? theme.colors.accent : theme.colors.border,
207
+ children
208
+ }
209
+ );
210
+ }
211
+
212
+ // src/components/layout/StatusBar.tsx
213
+ import { Box as Box4, Text as Text4 } from "ink";
214
+
215
+ // src/components/common/Spinner.tsx
216
+ import { useState, useEffect } from "react";
217
+ import { Text as Text3 } from "ink";
218
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
219
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
220
+ function Spinner({ label }) {
221
+ const theme = useTheme();
222
+ const [frame, setFrame] = useState(0);
223
+ useEffect(() => {
224
+ const timer = setInterval(() => {
225
+ setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
226
+ }, 80);
227
+ return () => clearInterval(timer);
228
+ }, []);
229
+ return /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.accent, children: [
230
+ SPINNER_FRAMES[frame],
231
+ " ",
232
+ label
233
+ ] });
234
+ }
235
+
236
+ // src/hooks/useLoading.ts
237
+ import { useSyncExternalStore } from "react";
238
+ var loadingService = null;
239
+ var emptyState = { isLoading: false, message: null };
240
+ function useLoading() {
241
+ return useSyncExternalStore(
242
+ (callback) => {
243
+ if (!loadingService) return () => {
244
+ };
245
+ return loadingService.subscribe(callback);
246
+ },
247
+ () => loadingService?.getState() ?? emptyState,
248
+ () => emptyState
249
+ );
250
+ }
251
+
252
+ // src/components/layout/StatusBar.tsx
253
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
254
+ var PANEL_HINTS = {
255
+ sidebar: "j/k:nav gg/G:top/bottom Enter:select Tab:list b:toggle ?:help q:quit",
256
+ list: "j/k:nav gg/G:top/bottom Enter:detail Esc:sidebar Tab:next ?:help q:quit",
257
+ detail: "j/k:scroll Tab:tabs Esc:list ?:help q:quit"
258
+ };
259
+ function StatusBar({ activePanel = "sidebar" }) {
260
+ const theme = useTheme();
261
+ const loadingState = useLoading();
262
+ const hints = PANEL_HINTS[activePanel];
263
+ return /* @__PURE__ */ jsxs4(
264
+ Box4,
265
+ {
266
+ height: 1,
267
+ width: "100%",
268
+ justifyContent: "space-between",
269
+ paddingX: 1,
270
+ children: [
271
+ /* @__PURE__ */ jsx4(Box4, { children: loadingState.isLoading ? /* @__PURE__ */ jsx4(Spinner, { label: loadingState.message ?? "Loading..." }) : /* @__PURE__ */ jsx4(Text4, { color: theme.colors.success, children: "Ready" }) }),
272
+ /* @__PURE__ */ jsx4(Box4, { gap: 1, children: /* @__PURE__ */ jsx4(Text4, { color: theme.colors.muted, children: hints }) })
273
+ ]
274
+ }
275
+ );
276
+ }
277
+
278
+ // src/components/layout/HelpModal.tsx
279
+ import { Box as Box7, Text as Text6 } from "ink";
280
+
281
+ // src/components/common/Divider.tsx
282
+ import { Box as Box5, Text as Text5, useStdout } from "ink";
283
+ import { jsx as jsx5 } from "react/jsx-runtime";
284
+ var DEFAULT_LINE_LENGTH = 36;
285
+ function Divider({ title }) {
286
+ const theme = useTheme();
287
+ const { stdout } = useStdout();
288
+ const cols = stdout?.columns ?? 80;
289
+ const len = Math.min(DEFAULT_LINE_LENGTH, Math.max(10, cols - 8));
290
+ const line = "\u2500".repeat(len);
291
+ if (title) {
292
+ const pad = Math.max(0, len - title.length - 2);
293
+ const left = Math.floor(pad / 2);
294
+ const right = pad - left;
295
+ const text = "\u2500".repeat(left) + ` ${title} ` + "\u2500".repeat(right);
296
+ return /* @__PURE__ */ jsx5(Box5, { paddingY: 0, children: /* @__PURE__ */ jsx5(Text5, { color: theme.colors.border, children: text }) });
297
+ }
298
+ return /* @__PURE__ */ jsx5(Box5, { paddingY: 0, children: /* @__PURE__ */ jsx5(Text5, { color: theme.colors.border, children: line }) });
299
+ }
300
+
301
+ // src/components/common/Modal.tsx
302
+ import { Box as Box6, useStdout as useStdout2 } from "ink";
303
+ import { jsx as jsx6 } from "react/jsx-runtime";
304
+ function Modal({ children }) {
305
+ const { stdout } = useStdout2();
306
+ const width = stdout?.columns ?? 80;
307
+ const height = stdout?.rows ?? 24;
308
+ return /* @__PURE__ */ jsx6(
309
+ Box6,
310
+ {
311
+ position: "absolute",
312
+ width,
313
+ height,
314
+ justifyContent: "center",
315
+ alignItems: "center",
316
+ children
317
+ }
318
+ );
319
+ }
320
+
321
+ // src/components/layout/HelpModal.tsx
322
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
323
+ var shortcuts = [
324
+ { key: "j / k", description: "Move down / up" },
325
+ { key: "Enter", description: "Select / Open" },
326
+ { key: "Tab", description: "Switch focus panel" },
327
+ { key: "b", description: "Toggle sidebar" },
328
+ { key: "/", description: "Search / Filter PRs" },
329
+ { key: "s", description: "Sort PRs" },
330
+ { key: "n / p", description: "Next / Previous page" },
331
+ { key: "1 / 2 / 3", description: "Switch PR detail tabs" },
332
+ { key: "q", description: "Back / Quit" },
333
+ { key: "?", description: "Toggle this help" },
334
+ { key: "Ctrl+c", description: "Force quit" }
335
+ ];
336
+ function HelpModal({ onClose }) {
337
+ const theme = useTheme();
338
+ return /* @__PURE__ */ jsx7(Modal, { children: /* @__PURE__ */ jsxs5(
339
+ Box7,
340
+ {
341
+ flexDirection: "column",
342
+ borderStyle: "round",
343
+ borderColor: theme.colors.accent,
344
+ backgroundColor: theme.colors.bg,
345
+ paddingX: 2,
346
+ paddingY: 1,
347
+ gap: 1,
348
+ children: [
349
+ /* @__PURE__ */ jsx7(Text6, { color: theme.colors.accent, bold: true, children: "Keyboard Shortcuts" }),
350
+ /* @__PURE__ */ jsx7(Divider, {}),
351
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: shortcuts.map((s) => /* @__PURE__ */ jsxs5(Box7, { gap: 2, children: [
352
+ /* @__PURE__ */ jsx7(Box7, { width: 16, children: /* @__PURE__ */ jsx7(Text6, { color: theme.colors.warning, children: s.key }) }),
353
+ /* @__PURE__ */ jsx7(Text6, { color: theme.colors.text, children: s.description })
354
+ ] }, s.key)) }),
355
+ /* @__PURE__ */ jsx7(Divider, {}),
356
+ /* @__PURE__ */ jsx7(Text6, { color: theme.colors.muted, dimColor: true, children: "Press ? to close" })
357
+ ]
358
+ }
359
+ ) });
360
+ }
361
+
362
+ // src/components/layout/TokenInputModal.tsx
363
+ import { useState as useState3, useEffect as useEffect2 } from "react";
364
+ import { Box as Box8, Text as Text7, useInput } from "ink";
365
+ import { TextInput } from "@inkjs/ui";
366
+
367
+ // src/hooks/useInputFocus.tsx
368
+ import React3, { createContext as createContext2, useContext as useContext2, useState as useState2, useCallback } from "react";
369
+ var InputFocusContext = createContext2({
370
+ isInputActive: false,
371
+ setInputActive: () => {
372
+ }
373
+ });
374
+ function InputFocusProvider({
375
+ children
376
+ }) {
377
+ const [isInputActive, setIsInputActive] = useState2(false);
378
+ const setInputActive = useCallback((active) => {
379
+ setIsInputActive(active);
380
+ }, []);
381
+ return React3.createElement(
382
+ InputFocusContext.Provider,
383
+ { value: { isInputActive, setInputActive } },
384
+ children
385
+ );
386
+ }
387
+ function useInputFocus() {
388
+ return useContext2(InputFocusContext);
389
+ }
390
+
391
+ // src/components/layout/TokenInputModal.tsx
392
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
393
+ function TokenInputModal({
394
+ onSubmit,
395
+ error
396
+ }) {
397
+ const theme = useTheme();
398
+ const { setInputActive } = useInputFocus();
399
+ const [value, setValue] = useState3("");
400
+ useEffect2(() => {
401
+ setInputActive(true);
402
+ return () => setInputActive(false);
403
+ }, [setInputActive]);
404
+ const handleSubmit = () => {
405
+ const trimmed = value.trim();
406
+ if (trimmed) {
407
+ onSubmit(trimmed);
408
+ }
409
+ };
410
+ useInput((_input, key) => {
411
+ if (key.return) {
412
+ handleSubmit();
413
+ }
414
+ });
415
+ return /* @__PURE__ */ jsx8(Modal, { children: /* @__PURE__ */ jsxs6(
416
+ Box8,
417
+ {
418
+ flexDirection: "column",
419
+ borderStyle: "round",
420
+ borderColor: theme.colors.accent,
421
+ backgroundColor: theme.colors.bg,
422
+ paddingX: 2,
423
+ paddingY: 1,
424
+ gap: 1,
425
+ children: [
426
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.accent, bold: true, children: "GitHub Token Required" }),
427
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
428
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.text, children: "No GitHub token found in your environment." }),
429
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.muted, children: "Please enter your GitHub Personal Access Token:" })
430
+ ] }),
431
+ error && /* @__PURE__ */ jsxs6(Text7, { color: theme.colors.error, children: [
432
+ "Error: ",
433
+ error
434
+ ] }),
435
+ /* @__PURE__ */ jsx8(
436
+ Box8,
437
+ {
438
+ borderStyle: "single",
439
+ borderColor: theme.colors.border,
440
+ paddingX: 1,
441
+ width: 50,
442
+ children: /* @__PURE__ */ jsx8(TextInput, { defaultValue: value, onChange: setValue })
443
+ }
444
+ ),
445
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
446
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.muted, dimColor: true, children: "The token will be saved to ~/.config/lazyreview/.token" }),
447
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.muted, dimColor: true, children: "Press Enter to submit, Ctrl+C to quit." })
448
+ ] }),
449
+ /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
450
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.info, children: "Get a token at: github.com/settings/tokens" }),
451
+ /* @__PURE__ */ jsx8(Text7, { color: theme.colors.muted, children: "Required scopes: repo, read:user" })
452
+ ] })
453
+ ]
454
+ }
455
+ ) });
456
+ }
457
+
458
+ // src/screens/PRDetailScreen.tsx
459
+ import { useState as useState6 } from "react";
460
+ import { Box as Box16, useInput as useInput4, useStdout as useStdout7 } from "ink";
461
+ import { Match as Match2 } from "effect";
462
+
463
+ // src/hooks/useGitHub.ts
464
+ import { useQuery } from "@tanstack/react-query";
465
+ import { Effect as Effect4 } from "effect";
466
+
467
+ // src/services/GitHubApi.ts
468
+ import { Context as Context2, Effect as Effect2, Layer as Layer2, Schema as S8 } from "effect";
469
+
470
+ // src/models/errors.ts
471
+ import { Data } from "effect";
472
+ var GitHubError = class extends Data.TaggedError("GitHubError") {
473
+ };
474
+ var AuthError = class extends Data.TaggedError("AuthError") {
475
+ };
476
+ var ConfigError = class extends Data.TaggedError("ConfigError") {
477
+ };
478
+ var NetworkError = class extends Data.TaggedError("NetworkError") {
479
+ };
480
+
481
+ // src/models/pull-request.ts
482
+ import { Schema as S2 } from "effect";
483
+
484
+ // src/models/user.ts
485
+ import { Schema as S } from "effect";
486
+ var User = class extends S.Class("User")({
487
+ login: S.String,
488
+ id: S.Number,
489
+ avatar_url: S.String,
490
+ html_url: S.String,
491
+ type: S.optionalWith(S.String, { default: () => "User" })
492
+ }) {
493
+ };
494
+
495
+ // src/models/pull-request.ts
496
+ var Label = class extends S2.Class("Label")({
497
+ id: S2.Number,
498
+ name: S2.String,
499
+ color: S2.String,
500
+ description: S2.optionalWith(S2.NullOr(S2.String), { default: () => null })
501
+ }) {
502
+ };
503
+ var BranchRef = class extends S2.Class("BranchRef")({
504
+ ref: S2.optionalWith(S2.String, { default: () => "" }),
505
+ sha: S2.optionalWith(S2.String, { default: () => "" }),
506
+ label: S2.optional(S2.String)
507
+ }) {
508
+ };
509
+ var PullRequest = class extends S2.Class("PullRequest")({
510
+ id: S2.Number,
511
+ number: S2.Number,
512
+ title: S2.String,
513
+ body: S2.optionalWith(S2.NullOr(S2.String), { default: () => null }),
514
+ state: S2.Literal("open", "closed"),
515
+ draft: S2.optionalWith(S2.Boolean, { default: () => false }),
516
+ merged: S2.optionalWith(S2.Boolean, { default: () => false }),
517
+ user: User,
518
+ labels: S2.optionalWith(S2.Array(Label), { default: () => [] }),
519
+ created_at: S2.String,
520
+ updated_at: S2.String,
521
+ merged_at: S2.optionalWith(S2.NullOr(S2.String), { default: () => null }),
522
+ closed_at: S2.optionalWith(S2.NullOr(S2.String), { default: () => null }),
523
+ html_url: S2.String,
524
+ head: S2.optionalWith(BranchRef, { default: () => new BranchRef({ ref: "", sha: "" }) }),
525
+ base: S2.optionalWith(BranchRef, { default: () => new BranchRef({ ref: "", sha: "" }) }),
526
+ additions: S2.optionalWith(S2.Number, { default: () => 0 }),
527
+ deletions: S2.optionalWith(S2.Number, { default: () => 0 }),
528
+ changed_files: S2.optionalWith(S2.Number, { default: () => 0 }),
529
+ comments: S2.optionalWith(S2.Number, { default: () => 0 }),
530
+ review_comments: S2.optionalWith(S2.Number, { default: () => 0 }),
531
+ requested_reviewers: S2.optionalWith(S2.Array(User), { default: () => [] })
532
+ }) {
533
+ };
534
+
535
+ // src/models/comment.ts
536
+ import { Schema as S3 } from "effect";
537
+ var Comment = class extends S3.Class("Comment")({
538
+ id: S3.Number,
539
+ body: S3.String,
540
+ user: User,
541
+ created_at: S3.String,
542
+ updated_at: S3.String,
543
+ html_url: S3.String,
544
+ path: S3.optional(S3.String),
545
+ line: S3.optional(S3.NullOr(S3.Number)),
546
+ side: S3.optional(S3.Literal("LEFT", "RIGHT")),
547
+ in_reply_to_id: S3.optional(S3.Number)
548
+ }) {
549
+ };
550
+
551
+ // src/models/review.ts
552
+ import { Schema as S4 } from "effect";
553
+ var Review = class extends S4.Class("Review")({
554
+ id: S4.Number,
555
+ user: User,
556
+ body: S4.optionalWith(S4.NullOr(S4.String), { default: () => null }),
557
+ state: S4.Literal(
558
+ "APPROVED",
559
+ "CHANGES_REQUESTED",
560
+ "COMMENTED",
561
+ "DISMISSED",
562
+ "PENDING"
563
+ ),
564
+ submitted_at: S4.optionalWith(S4.NullOr(S4.String), { default: () => null }),
565
+ html_url: S4.String
566
+ }) {
567
+ };
568
+
569
+ // src/models/file-change.ts
570
+ import { Schema as S5 } from "effect";
571
+ var FileChange = class extends S5.Class("FileChange")({
572
+ sha: S5.String,
573
+ filename: S5.String,
574
+ status: S5.Literal(
575
+ "added",
576
+ "removed",
577
+ "modified",
578
+ "renamed",
579
+ "copied",
580
+ "changed",
581
+ "unchanged"
582
+ ),
583
+ additions: S5.Number,
584
+ deletions: S5.Number,
585
+ changes: S5.Number,
586
+ patch: S5.optional(S5.String),
587
+ previous_filename: S5.optional(S5.String),
588
+ blob_url: S5.optional(S5.String),
589
+ raw_url: S5.optional(S5.String)
590
+ }) {
591
+ };
592
+
593
+ // src/models/commit.ts
594
+ import { Schema as S6 } from "effect";
595
+ var CommitAuthor = class extends S6.Class("CommitAuthor")({
596
+ name: S6.String,
597
+ email: S6.String,
598
+ date: S6.String
599
+ }) {
600
+ };
601
+ var CommitDetails = class extends S6.Class("CommitDetails")({
602
+ message: S6.String,
603
+ author: CommitAuthor
604
+ }) {
605
+ };
606
+ var Commit = class extends S6.Class("Commit")({
607
+ sha: S6.String,
608
+ commit: CommitDetails,
609
+ author: S6.optionalWith(S6.NullOr(User), { default: () => null }),
610
+ html_url: S6.String
611
+ }) {
612
+ };
613
+
614
+ // src/services/Auth.ts
615
+ import { Context, Effect, Layer, Schema as S7 } from "effect";
616
+ import { execFile } from "child_process";
617
+ import { promisify } from "util";
618
+ import { writeFile, readFile, mkdir, unlink } from "fs/promises";
619
+ import { homedir } from "os";
620
+ import { join } from "path";
621
+ var execFileAsync = promisify(execFile);
622
+ var CONFIG_DIR = join(homedir(), ".config", "lazyreview");
623
+ var TOKEN_FILE = join(CONFIG_DIR, ".token");
624
+ var sessionToken = null;
625
+ var preferredSource = null;
626
+ async function loadSavedToken() {
627
+ try {
628
+ const token = await readFile(TOKEN_FILE, "utf-8");
629
+ return token.trim() || null;
630
+ } catch {
631
+ return null;
632
+ }
633
+ }
634
+ async function saveTokenToFile(token) {
635
+ await mkdir(CONFIG_DIR, { recursive: true });
636
+ await writeFile(TOKEN_FILE, token, { mode: 384 });
637
+ }
638
+ async function deleteSavedToken() {
639
+ try {
640
+ await unlink(TOKEN_FILE);
641
+ } catch {
642
+ }
643
+ }
644
+ var savedToken = null;
645
+ loadSavedToken().then((token) => {
646
+ savedToken = token;
647
+ });
648
+ function maskToken(token) {
649
+ if (token.length <= 8) return "****";
650
+ return `${token.slice(0, 4)}...${token.slice(-4)}`;
651
+ }
652
+ async function tryGetGhToken() {
653
+ try {
654
+ const { stdout } = await execFileAsync("gh", ["auth", "token"]);
655
+ return stdout.trim() || null;
656
+ } catch {
657
+ return null;
658
+ }
659
+ }
660
+ var Auth = class extends Context.Tag("Auth")() {
661
+ };
662
+ function resolveToken() {
663
+ return Effect.gen(function* () {
664
+ if (preferredSource === "manual") {
665
+ const manualToken2 = sessionToken ?? savedToken;
666
+ if (manualToken2) return manualToken2;
667
+ return yield* Effect.fail(
668
+ new AuthError({ message: "No manual token found", reason: "no_token" })
669
+ );
670
+ }
671
+ if (preferredSource === "env") {
672
+ const envToken2 = process.env["LAZYREVIEW_GITHUB_TOKEN"];
673
+ if (envToken2) return envToken2;
674
+ return yield* Effect.fail(
675
+ new AuthError({ message: "LAZYREVIEW_GITHUB_TOKEN not set", reason: "no_token" })
676
+ );
677
+ }
678
+ if (preferredSource === "gh_cli") {
679
+ const ghToken = yield* Effect.tryPromise({
680
+ try: tryGetGhToken,
681
+ catch: () => new AuthError({ message: "gh CLI failed", reason: "no_token" })
682
+ });
683
+ if (ghToken) return ghToken;
684
+ return yield* Effect.fail(
685
+ new AuthError({ message: "gh CLI token not available", reason: "no_token" })
686
+ );
687
+ }
688
+ const envToken = process.env["LAZYREVIEW_GITHUB_TOKEN"];
689
+ if (envToken) return envToken;
690
+ const manualToken = sessionToken ?? savedToken;
691
+ if (manualToken) return manualToken;
692
+ const ghResult = yield* Effect.tryPromise({
693
+ try: tryGetGhToken,
694
+ catch: () => new AuthError({
695
+ message: "No GitHub token found. Set LAZYREVIEW_GITHUB_TOKEN or configure in Settings.",
696
+ reason: "no_token"
697
+ })
698
+ });
699
+ if (ghResult) return ghResult;
700
+ return yield* Effect.fail(
701
+ new AuthError({
702
+ message: "No GitHub token found. Set LAZYREVIEW_GITHUB_TOKEN or configure in Settings.",
703
+ reason: "no_token"
704
+ })
705
+ );
706
+ });
707
+ }
708
+ function resolveTokenInfo() {
709
+ return Effect.gen(function* () {
710
+ if (preferredSource === "manual") {
711
+ const manualToken2 = sessionToken ?? savedToken;
712
+ if (manualToken2) {
713
+ return {
714
+ source: "manual",
715
+ token: manualToken2,
716
+ maskedToken: maskToken(manualToken2)
717
+ };
718
+ }
719
+ }
720
+ if (preferredSource === "env") {
721
+ const envToken2 = process.env["LAZYREVIEW_GITHUB_TOKEN"];
722
+ if (envToken2) {
723
+ return {
724
+ source: "env",
725
+ token: envToken2,
726
+ maskedToken: maskToken(envToken2)
727
+ };
728
+ }
729
+ }
730
+ if (preferredSource === "gh_cli") {
731
+ const ghToken2 = yield* Effect.promise(tryGetGhToken);
732
+ if (ghToken2) {
733
+ return {
734
+ source: "gh_cli",
735
+ token: ghToken2,
736
+ maskedToken: maskToken(ghToken2)
737
+ };
738
+ }
739
+ }
740
+ const envToken = process.env["LAZYREVIEW_GITHUB_TOKEN"];
741
+ if (envToken) {
742
+ return {
743
+ source: "env",
744
+ token: envToken,
745
+ maskedToken: maskToken(envToken)
746
+ };
747
+ }
748
+ const manualToken = sessionToken ?? savedToken;
749
+ if (manualToken) {
750
+ return {
751
+ source: "manual",
752
+ token: manualToken,
753
+ maskedToken: maskToken(manualToken)
754
+ };
755
+ }
756
+ const ghToken = yield* Effect.promise(tryGetGhToken);
757
+ if (ghToken) {
758
+ return {
759
+ source: "gh_cli",
760
+ token: ghToken,
761
+ maskedToken: maskToken(ghToken)
762
+ };
763
+ }
764
+ return {
765
+ source: "none",
766
+ token: null,
767
+ maskedToken: null
768
+ };
769
+ });
770
+ }
771
+ var AuthLive = Layer.succeed(
772
+ Auth,
773
+ Auth.of({
774
+ getToken: resolveToken,
775
+ getUser: () => Effect.gen(function* () {
776
+ const token = yield* resolveToken();
777
+ const user = yield* Effect.tryPromise({
778
+ try: async () => {
779
+ const response = await fetch("https://api.github.com/user", {
780
+ headers: {
781
+ Authorization: `Bearer ${token}`,
782
+ Accept: "application/vnd.github+json"
783
+ }
784
+ });
785
+ if (!response.ok) {
786
+ throw new Error(`GitHub API returned ${response.status}`);
787
+ }
788
+ const data = await response.json();
789
+ return S7.decodeUnknownSync(User)(data);
790
+ },
791
+ catch: (error) => new AuthError({
792
+ message: `Failed to get user: ${String(error)}`,
793
+ reason: "invalid_token"
794
+ })
795
+ });
796
+ return user;
797
+ }),
798
+ isAuthenticated: () => Effect.gen(function* () {
799
+ const result = yield* Effect.either(resolveToken());
800
+ return result._tag === "Right";
801
+ }),
802
+ setToken: (token) => Effect.gen(function* () {
803
+ sessionToken = token;
804
+ savedToken = token;
805
+ preferredSource = "manual";
806
+ yield* Effect.promise(() => saveTokenToFile(token));
807
+ }),
808
+ getTokenInfo: resolveTokenInfo,
809
+ setPreferredSource: (source) => Effect.sync(() => {
810
+ preferredSource = source === "none" ? null : source;
811
+ }),
812
+ getAvailableSources: () => Effect.gen(function* () {
813
+ const sources = [];
814
+ const envToken = process.env["LAZYREVIEW_GITHUB_TOKEN"];
815
+ if (envToken) {
816
+ sources.push("env");
817
+ }
818
+ if (sessionToken || savedToken) {
819
+ sources.push("manual");
820
+ }
821
+ const ghToken = yield* Effect.promise(tryGetGhToken);
822
+ if (ghToken) {
823
+ sources.push("gh_cli");
824
+ }
825
+ return sources;
826
+ }),
827
+ clearManualToken: () => Effect.gen(function* () {
828
+ sessionToken = null;
829
+ savedToken = null;
830
+ if (preferredSource === "manual") {
831
+ preferredSource = null;
832
+ }
833
+ yield* Effect.promise(deleteSavedToken);
834
+ })
835
+ })
836
+ );
837
+
838
+ // src/services/GitHubApi.ts
839
+ var BASE_URL = "https://api.github.com";
840
+ var GitHubApi = class extends Context2.Tag("GitHubApi")() {
841
+ };
842
+ function fetchGitHub(path, token, schema) {
843
+ const url = `${BASE_URL}${path}`;
844
+ const decode = S8.decodeUnknownSync(schema);
845
+ return Effect2.tryPromise({
846
+ try: async () => {
847
+ const response = await fetch(url, {
848
+ headers: {
849
+ Authorization: `Bearer ${token}`,
850
+ Accept: "application/vnd.github+json",
851
+ "X-GitHub-Api-Version": "2022-11-28"
852
+ }
853
+ });
854
+ if (!response.ok) {
855
+ const body = await response.text().catch(() => "");
856
+ throw new GitHubError({
857
+ message: `GitHub API error: ${response.status} ${response.statusText} - ${body}`,
858
+ status: response.status,
859
+ url
860
+ });
861
+ }
862
+ const data = await response.json();
863
+ return decode(data);
864
+ },
865
+ catch: (error) => {
866
+ if (error instanceof GitHubError) return error;
867
+ return new NetworkError({
868
+ message: `Network request failed: ${String(error)}`,
869
+ cause: error
870
+ });
871
+ }
872
+ });
873
+ }
874
+ var SearchResultSchema = S8.Struct({
875
+ total_count: S8.Number,
876
+ incomplete_results: S8.Boolean,
877
+ items: S8.Array(PullRequest)
878
+ });
879
+ function fetchGitHubSearch(query, token) {
880
+ const url = `${BASE_URL}/search/issues?q=${encodeURIComponent(query)}&per_page=100`;
881
+ const decode = S8.decodeUnknownSync(SearchResultSchema);
882
+ return Effect2.tryPromise({
883
+ try: async () => {
884
+ const response = await fetch(url, {
885
+ headers: {
886
+ Authorization: `Bearer ${token}`,
887
+ Accept: "application/vnd.github+json",
888
+ "X-GitHub-Api-Version": "2022-11-28"
889
+ }
890
+ });
891
+ if (!response.ok) {
892
+ const body = await response.text().catch(() => "");
893
+ throw new GitHubError({
894
+ message: `GitHub API error: ${response.status} ${response.statusText} - ${body}`,
895
+ status: response.status,
896
+ url
897
+ });
898
+ }
899
+ const data = await response.json();
900
+ const result = decode(data);
901
+ return result.items;
902
+ },
903
+ catch: (error) => {
904
+ if (error instanceof GitHubError) return error;
905
+ return new NetworkError({
906
+ message: `Network request failed: ${String(error)}`,
907
+ cause: error
908
+ });
909
+ }
910
+ });
911
+ }
912
+ function buildQueryString(options) {
913
+ const params = new URLSearchParams();
914
+ if (options.state) params.set("state", options.state);
915
+ if (options.sort) params.set("sort", options.sort);
916
+ if (options.direction) params.set("direction", options.direction);
917
+ if (options.perPage) params.set("per_page", String(options.perPage));
918
+ if (options.page) params.set("page", String(options.page));
919
+ const qs = params.toString();
920
+ return qs ? `?${qs}` : "";
921
+ }
922
+ var GitHubApiLive = Layer2.effect(
923
+ GitHubApi,
924
+ Effect2.gen(function* () {
925
+ const auth = yield* Auth;
926
+ return GitHubApi.of({
927
+ listPullRequests: (owner, repo, options = {}) => Effect2.gen(function* () {
928
+ const token = yield* auth.getToken();
929
+ const mergedOptions = { ...options, perPage: options.perPage ?? 100 };
930
+ const qs = buildQueryString(mergedOptions);
931
+ return yield* fetchGitHub(
932
+ `/repos/${owner}/${repo}/pulls${qs}`,
933
+ token,
934
+ S8.Array(PullRequest)
935
+ );
936
+ }),
937
+ getPullRequest: (owner, repo, number) => Effect2.gen(function* () {
938
+ const token = yield* auth.getToken();
939
+ return yield* fetchGitHub(
940
+ `/repos/${owner}/${repo}/pulls/${number}`,
941
+ token,
942
+ PullRequest
943
+ );
944
+ }),
945
+ getPullRequestFiles: (owner, repo, number) => Effect2.gen(function* () {
946
+ const token = yield* auth.getToken();
947
+ return yield* fetchGitHub(
948
+ `/repos/${owner}/${repo}/pulls/${number}/files`,
949
+ token,
950
+ S8.Array(FileChange)
951
+ );
952
+ }),
953
+ getPullRequestComments: (owner, repo, number) => Effect2.gen(function* () {
954
+ const token = yield* auth.getToken();
955
+ return yield* fetchGitHub(
956
+ `/repos/${owner}/${repo}/pulls/${number}/comments`,
957
+ token,
958
+ S8.Array(Comment)
959
+ );
960
+ }),
961
+ getPullRequestReviews: (owner, repo, number) => Effect2.gen(function* () {
962
+ const token = yield* auth.getToken();
963
+ return yield* fetchGitHub(
964
+ `/repos/${owner}/${repo}/pulls/${number}/reviews`,
965
+ token,
966
+ S8.Array(Review)
967
+ );
968
+ }),
969
+ getPullRequestCommits: (owner, repo, number) => Effect2.gen(function* () {
970
+ const token = yield* auth.getToken();
971
+ return yield* fetchGitHub(
972
+ `/repos/${owner}/${repo}/pulls/${number}/commits`,
973
+ token,
974
+ S8.Array(Commit)
975
+ );
976
+ }),
977
+ getMyPRs: () => Effect2.gen(function* () {
978
+ const token = yield* auth.getToken();
979
+ return yield* fetchGitHubSearch("is:pr is:open author:@me", token);
980
+ }),
981
+ getReviewRequests: () => Effect2.gen(function* () {
982
+ const token = yield* auth.getToken();
983
+ return yield* fetchGitHubSearch(
984
+ "is:pr is:open review-requested:@me",
985
+ token
986
+ );
987
+ }),
988
+ getInvolvedPRs: () => Effect2.gen(function* () {
989
+ const token = yield* auth.getToken();
990
+ return yield* fetchGitHubSearch("is:pr is:open involves:@me", token);
991
+ })
992
+ });
993
+ })
994
+ );
995
+
996
+ // src/services/index.ts
997
+ import { Layer as Layer5 } from "effect";
998
+
999
+ // src/services/Config.ts
1000
+ import { Context as Context3, Effect as Effect3, Layer as Layer3, Schema as S9 } from "effect";
1001
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1002
+ import { homedir as homedir2 } from "os";
1003
+ import { join as join2, dirname } from "path";
1004
+ import { parse, stringify } from "yaml";
1005
+ var KeybindingsSchema = S9.Struct({
1006
+ toggleSidebar: S9.optionalWith(S9.String, { default: () => "b" }),
1007
+ help: S9.optionalWith(S9.String, { default: () => "?" }),
1008
+ quit: S9.optionalWith(S9.String, { default: () => "q" })
1009
+ });
1010
+ var AppConfig = class extends S9.Class("AppConfig")({
1011
+ provider: S9.optionalWith(S9.Literal("github"), {
1012
+ default: () => "github"
1013
+ }),
1014
+ theme: S9.optionalWith(S9.String, { default: () => "tokyo-night" }),
1015
+ defaultOwner: S9.optional(S9.String),
1016
+ defaultRepo: S9.optional(S9.String),
1017
+ pageSize: S9.optionalWith(S9.Number.pipe(S9.int(), S9.between(1, 100)), {
1018
+ default: () => 30
1019
+ }),
1020
+ keybindings: S9.optionalWith(KeybindingsSchema, {
1021
+ default: () => ({ toggleSidebar: "b", help: "?", quit: "q" })
1022
+ })
1023
+ }) {
1024
+ };
1025
+ var defaultConfig = S9.decodeUnknownSync(AppConfig)({});
1026
+ var Config = class extends Context3.Tag("Config")() {
1027
+ };
1028
+ function getConfigPath() {
1029
+ return join2(homedir2(), ".config", "lazyreview", "config.yaml");
1030
+ }
1031
+ var ConfigLive = Layer3.succeed(
1032
+ Config,
1033
+ Config.of({
1034
+ getPath: getConfigPath,
1035
+ load: () => Effect3.tryPromise({
1036
+ try: async () => {
1037
+ const configPath = getConfigPath();
1038
+ try {
1039
+ const content = await readFile2(configPath, "utf-8");
1040
+ const parsed = parse(content);
1041
+ return S9.decodeUnknownSync(AppConfig)(parsed);
1042
+ } catch {
1043
+ return defaultConfig;
1044
+ }
1045
+ },
1046
+ catch: (error) => new ConfigError({
1047
+ message: `Failed to load config: ${String(error)}`,
1048
+ path: getConfigPath()
1049
+ })
1050
+ }),
1051
+ save: (config) => Effect3.tryPromise({
1052
+ try: async () => {
1053
+ const configPath = getConfigPath();
1054
+ await mkdir2(dirname(configPath), { recursive: true });
1055
+ await writeFile2(configPath, stringify(config), "utf-8");
1056
+ },
1057
+ catch: (error) => new ConfigError({
1058
+ message: `Failed to save config: ${String(error)}`,
1059
+ path: getConfigPath()
1060
+ })
1061
+ })
1062
+ })
1063
+ );
1064
+
1065
+ // src/services/Loading.ts
1066
+ import { Context as Context4, Layer as Layer4 } from "effect";
1067
+ var Loading = class extends Context4.Tag("Loading")() {
1068
+ };
1069
+ function createLoadingService() {
1070
+ let state = { isLoading: false, message: null };
1071
+ const listeners = /* @__PURE__ */ new Set();
1072
+ function notify() {
1073
+ for (const listener of listeners) {
1074
+ listener();
1075
+ }
1076
+ }
1077
+ return {
1078
+ start: (message) => {
1079
+ state = { isLoading: true, message };
1080
+ notify();
1081
+ },
1082
+ stop: () => {
1083
+ state = { isLoading: false, message: null };
1084
+ notify();
1085
+ },
1086
+ getState: () => state,
1087
+ subscribe: (listener) => {
1088
+ listeners.add(listener);
1089
+ return () => {
1090
+ listeners.delete(listener);
1091
+ };
1092
+ }
1093
+ };
1094
+ }
1095
+ var LoadingLive = Layer4.sync(Loading, createLoadingService);
1096
+
1097
+ // src/services/index.ts
1098
+ var GitHubApiFullLive = GitHubApiLive.pipe(Layer5.provide(AuthLive));
1099
+ var AppLayer = Layer5.mergeAll(
1100
+ ConfigLive,
1101
+ AuthLive,
1102
+ LoadingLive,
1103
+ GitHubApiFullLive
1104
+ );
1105
+
1106
+ // src/hooks/useGitHub.ts
1107
+ function runEffect(effect) {
1108
+ return Effect4.runPromise(
1109
+ effect.pipe(Effect4.provide(AppLayer))
1110
+ );
1111
+ }
1112
+ function usePullRequests(owner, repo, options) {
1113
+ return useQuery({
1114
+ queryKey: ["prs", owner, repo, options],
1115
+ queryFn: () => runEffect(
1116
+ Effect4.gen(function* () {
1117
+ const api = yield* GitHubApi;
1118
+ return yield* api.listPullRequests(owner, repo, options);
1119
+ })
1120
+ ),
1121
+ enabled: !!owner && !!repo
1122
+ });
1123
+ }
1124
+ function usePRFiles(owner, repo, number) {
1125
+ return useQuery({
1126
+ queryKey: ["pr-files", owner, repo, number],
1127
+ queryFn: () => runEffect(
1128
+ Effect4.gen(function* () {
1129
+ const api = yield* GitHubApi;
1130
+ return yield* api.getPullRequestFiles(owner, repo, number);
1131
+ })
1132
+ ),
1133
+ enabled: !!owner && !!repo && !!number
1134
+ });
1135
+ }
1136
+ function usePRComments(owner, repo, number) {
1137
+ return useQuery({
1138
+ queryKey: ["pr-comments", owner, repo, number],
1139
+ queryFn: () => runEffect(
1140
+ Effect4.gen(function* () {
1141
+ const api = yield* GitHubApi;
1142
+ return yield* api.getPullRequestComments(owner, repo, number);
1143
+ })
1144
+ ),
1145
+ enabled: !!owner && !!repo && !!number
1146
+ });
1147
+ }
1148
+ function usePRReviews(owner, repo, number) {
1149
+ return useQuery({
1150
+ queryKey: ["pr-reviews", owner, repo, number],
1151
+ queryFn: () => runEffect(
1152
+ Effect4.gen(function* () {
1153
+ const api = yield* GitHubApi;
1154
+ return yield* api.getPullRequestReviews(owner, repo, number);
1155
+ })
1156
+ ),
1157
+ enabled: !!owner && !!repo && !!number
1158
+ });
1159
+ }
1160
+ function usePRCommits(owner, repo, number) {
1161
+ return useQuery({
1162
+ queryKey: ["pr-commits", owner, repo, number],
1163
+ queryFn: () => runEffect(
1164
+ Effect4.gen(function* () {
1165
+ const api = yield* GitHubApi;
1166
+ return yield* api.getPullRequestCommits(owner, repo, number);
1167
+ })
1168
+ ),
1169
+ enabled: !!owner && !!repo && !!number
1170
+ });
1171
+ }
1172
+ function useMyPRs() {
1173
+ return useQuery({
1174
+ queryKey: ["my-prs"],
1175
+ queryFn: () => runEffect(
1176
+ Effect4.gen(function* () {
1177
+ const api = yield* GitHubApi;
1178
+ return yield* api.getMyPRs();
1179
+ })
1180
+ )
1181
+ });
1182
+ }
1183
+ function useReviewRequests() {
1184
+ return useQuery({
1185
+ queryKey: ["review-requests"],
1186
+ queryFn: () => runEffect(
1187
+ Effect4.gen(function* () {
1188
+ const api = yield* GitHubApi;
1189
+ return yield* api.getReviewRequests();
1190
+ })
1191
+ )
1192
+ });
1193
+ }
1194
+ function useInvolvedPRs() {
1195
+ return useQuery({
1196
+ queryKey: ["involved-prs"],
1197
+ queryFn: () => runEffect(
1198
+ Effect4.gen(function* () {
1199
+ const api = yield* GitHubApi;
1200
+ return yield* api.getInvolvedPRs();
1201
+ })
1202
+ )
1203
+ });
1204
+ }
1205
+
1206
+ // src/components/pr/PRHeader.tsx
1207
+ import { Box as Box9, Text as Text8 } from "ink";
1208
+
1209
+ // src/utils/date.ts
1210
+ import { formatDistanceToNow, parseISO, format } from "date-fns";
1211
+ function timeAgo(dateString) {
1212
+ try {
1213
+ const date = parseISO(dateString);
1214
+ return formatDistanceToNow(date, { addSuffix: true });
1215
+ } catch {
1216
+ return dateString;
1217
+ }
1218
+ }
1219
+
1220
+ // src/components/pr/PRHeader.tsx
1221
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1222
+ function PRHeader({ pr }) {
1223
+ const theme = useTheme();
1224
+ const stateColor = pr.draft ? theme.colors.muted : pr.state === "open" ? theme.colors.success : theme.colors.error;
1225
+ const stateLabel = pr.draft ? "Draft" : pr.merged ? "Merged" : pr.state === "open" ? "Open" : "Closed";
1226
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
1227
+ /* @__PURE__ */ jsxs7(Box9, { gap: 1, children: [
1228
+ /* @__PURE__ */ jsxs7(Text8, { color: stateColor, bold: true, children: [
1229
+ "[",
1230
+ stateLabel,
1231
+ "]"
1232
+ ] }),
1233
+ /* @__PURE__ */ jsxs7(Text8, { color: theme.colors.accent, bold: true, children: [
1234
+ "#",
1235
+ pr.number
1236
+ ] }),
1237
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.text, bold: true, children: pr.title })
1238
+ ] }),
1239
+ /* @__PURE__ */ jsxs7(Box9, { gap: 1, paddingLeft: 2, children: [
1240
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.secondary, children: pr.user.login }),
1241
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.muted, children: "wants to merge" }),
1242
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.info, children: pr.head.ref }),
1243
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.muted, children: "into" }),
1244
+ /* @__PURE__ */ jsx9(Text8, { color: theme.colors.info, children: pr.base.ref })
1245
+ ] }),
1246
+ /* @__PURE__ */ jsxs7(Box9, { gap: 2, paddingLeft: 2, children: [
1247
+ /* @__PURE__ */ jsxs7(Text8, { color: theme.colors.muted, children: [
1248
+ "opened ",
1249
+ timeAgo(pr.created_at)
1250
+ ] }),
1251
+ /* @__PURE__ */ jsxs7(Text8, { color: theme.colors.diffAdd, children: [
1252
+ "+",
1253
+ pr.additions
1254
+ ] }),
1255
+ /* @__PURE__ */ jsxs7(Text8, { color: theme.colors.diffDel, children: [
1256
+ "-",
1257
+ pr.deletions
1258
+ ] }),
1259
+ /* @__PURE__ */ jsxs7(Text8, { color: theme.colors.muted, children: [
1260
+ pr.changed_files,
1261
+ " files changed"
1262
+ ] })
1263
+ ] }),
1264
+ pr.labels.length > 0 && /* @__PURE__ */ jsx9(Box9, { gap: 1, paddingLeft: 2, children: pr.labels.map((label) => /* @__PURE__ */ jsxs7(Text8, { color: `#${label.color}`, children: [
1265
+ "[",
1266
+ label.name,
1267
+ "]"
1268
+ ] }, label.id)) })
1269
+ ] });
1270
+ }
1271
+
1272
+ // src/components/pr/PRTabs.tsx
1273
+ import { Box as Box10 } from "ink";
1274
+ import { Tab, Tabs } from "ink-tab";
1275
+ import { jsx as jsx10 } from "react/jsx-runtime";
1276
+ var PR_TAB_NAMES = ["Conversations", "Commits", "Files"];
1277
+ function PRTabs({ activeIndex, onChange }) {
1278
+ const theme = useTheme();
1279
+ return /* @__PURE__ */ jsx10(
1280
+ Box10,
1281
+ {
1282
+ flexDirection: "row",
1283
+ paddingX: 1,
1284
+ paddingY: 1,
1285
+ borderStyle: "single",
1286
+ borderColor: theme.colors.border,
1287
+ children: /* @__PURE__ */ jsx10(
1288
+ Tabs,
1289
+ {
1290
+ defaultValue: PR_TAB_NAMES[activeIndex],
1291
+ onChange: (name) => {
1292
+ const index = PR_TAB_NAMES.indexOf(name);
1293
+ if (index >= 0) onChange(index);
1294
+ },
1295
+ showIndex: true,
1296
+ isFocused: true,
1297
+ colors: {
1298
+ activeTab: {
1299
+ color: theme.colors.accent,
1300
+ backgroundColor: theme.colors.bg
1301
+ }
1302
+ },
1303
+ keyMap: { useNumbers: true, useTab: true },
1304
+ children: PR_TAB_NAMES.map((name) => /* @__PURE__ */ jsx10(Tab, { name, children: name }, name))
1305
+ },
1306
+ activeIndex
1307
+ )
1308
+ }
1309
+ );
1310
+ }
1311
+
1312
+ // src/components/pr/FilesTab.tsx
1313
+ import React5, { useEffect as useEffect4, useMemo, useRef as useRef2, useState as useState5 } from "react";
1314
+ import { Box as Box12, Text as Text10, useInput as useInput3, useStdout as useStdout3 } from "ink";
1315
+ import { UnorderedList } from "@inkjs/ui";
1316
+ import { ScrollList } from "ink-scroll-list";
1317
+ import SyntaxHighlight from "ink-syntax-highlight";
1318
+
1319
+ // src/hooks/useListNavigation.ts
1320
+ import { useInput as useInput2 } from "ink";
1321
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState4 } from "react";
1322
+ function useListNavigation({
1323
+ itemCount,
1324
+ viewportHeight,
1325
+ isActive = true
1326
+ }) {
1327
+ const [selectedIndex, setSelectedIndex] = useState4(0);
1328
+ const gPressedAt = useRef(null);
1329
+ const prevItemCount = useRef(itemCount);
1330
+ useEffect3(() => {
1331
+ const wasAtEnd = selectedIndex === prevItemCount.current - 1;
1332
+ prevItemCount.current = itemCount;
1333
+ if (wasAtEnd && itemCount > 0) {
1334
+ setSelectedIndex(itemCount - 1);
1335
+ }
1336
+ }, [itemCount, selectedIndex]);
1337
+ useEffect3(() => {
1338
+ if (itemCount === 0) {
1339
+ setSelectedIndex(0);
1340
+ } else if (selectedIndex >= itemCount) {
1341
+ setSelectedIndex(itemCount - 1);
1342
+ }
1343
+ }, [itemCount, selectedIndex]);
1344
+ const clamp = useCallback2(
1345
+ (index) => Math.max(0, Math.min(index, itemCount - 1)),
1346
+ [itemCount]
1347
+ );
1348
+ useInput2(
1349
+ (input, key) => {
1350
+ if (!isActive || itemCount === 0) return;
1351
+ if (input === "j" || key.downArrow) {
1352
+ setSelectedIndex((i) => clamp(i + 1));
1353
+ return;
1354
+ }
1355
+ if (input === "k" || key.upArrow) {
1356
+ setSelectedIndex((i) => clamp(i - 1));
1357
+ return;
1358
+ }
1359
+ if (input === "G") {
1360
+ setSelectedIndex(itemCount - 1);
1361
+ return;
1362
+ }
1363
+ if (input === "g") {
1364
+ const now = Date.now();
1365
+ if (gPressedAt.current !== null && now - gPressedAt.current < 500) {
1366
+ setSelectedIndex(0);
1367
+ gPressedAt.current = null;
1368
+ } else {
1369
+ gPressedAt.current = now;
1370
+ }
1371
+ return;
1372
+ }
1373
+ if (key.ctrl && input === "d") {
1374
+ setSelectedIndex((i) => clamp(i + Math.floor(viewportHeight / 2)));
1375
+ return;
1376
+ }
1377
+ if (key.ctrl && input === "u") {
1378
+ setSelectedIndex((i) => clamp(i - Math.floor(viewportHeight / 2)));
1379
+ return;
1380
+ }
1381
+ gPressedAt.current = null;
1382
+ },
1383
+ { isActive }
1384
+ );
1385
+ const scrollOffset = deriveScrollOffset(
1386
+ selectedIndex,
1387
+ viewportHeight,
1388
+ itemCount
1389
+ );
1390
+ return { selectedIndex, scrollOffset, setSelectedIndex };
1391
+ }
1392
+ function deriveScrollOffset(selectedIndex, viewportHeight, itemCount) {
1393
+ if (itemCount <= viewportHeight) return 0;
1394
+ let offset = selectedIndex - Math.floor(viewportHeight / 2);
1395
+ offset = Math.max(0, offset);
1396
+ offset = Math.min(offset, itemCount - viewportHeight);
1397
+ return offset;
1398
+ }
1399
+
1400
+ // src/models/diff.ts
1401
+ function parseDiffPatch(patch) {
1402
+ const hunks = [];
1403
+ const lines = patch.split("\n");
1404
+ let currentHunk = null;
1405
+ let currentLines = [];
1406
+ let oldLine = 0;
1407
+ let newLine = 0;
1408
+ for (const line of lines) {
1409
+ const hunkMatch = line.match(
1410
+ /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/
1411
+ );
1412
+ if (hunkMatch) {
1413
+ if (currentHunk) {
1414
+ hunks.push({ ...currentHunk, lines: currentLines });
1415
+ }
1416
+ oldLine = parseInt(hunkMatch[1], 10);
1417
+ newLine = parseInt(hunkMatch[3], 10);
1418
+ currentLines = [{ type: "header", content: line }];
1419
+ currentHunk = {
1420
+ header: line,
1421
+ oldStart: oldLine,
1422
+ oldCount: parseInt(hunkMatch[2] ?? "1", 10),
1423
+ newStart: newLine,
1424
+ newCount: parseInt(hunkMatch[4] ?? "1", 10),
1425
+ lines: []
1426
+ };
1427
+ continue;
1428
+ }
1429
+ if (!currentHunk) continue;
1430
+ if (line.startsWith("+")) {
1431
+ currentLines.push({
1432
+ type: "add",
1433
+ content: line.slice(1),
1434
+ newLineNumber: newLine++
1435
+ });
1436
+ } else if (line.startsWith("-")) {
1437
+ currentLines.push({
1438
+ type: "del",
1439
+ content: line.slice(1),
1440
+ oldLineNumber: oldLine++
1441
+ });
1442
+ } else if (line.startsWith(" ") || line === "") {
1443
+ currentLines.push({
1444
+ type: "context",
1445
+ content: line.slice(1),
1446
+ oldLineNumber: oldLine++,
1447
+ newLineNumber: newLine++
1448
+ });
1449
+ }
1450
+ }
1451
+ if (currentHunk) {
1452
+ hunks.push({ ...currentHunk, lines: currentLines });
1453
+ }
1454
+ return hunks;
1455
+ }
1456
+
1457
+ // src/components/common/EmptyState.tsx
1458
+ import { Box as Box11, Text as Text9 } from "ink";
1459
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1460
+ function EmptyState({
1461
+ icon = "~",
1462
+ message,
1463
+ hint
1464
+ }) {
1465
+ const theme = useTheme();
1466
+ return /* @__PURE__ */ jsxs8(
1467
+ Box11,
1468
+ {
1469
+ flexDirection: "column",
1470
+ alignItems: "center",
1471
+ justifyContent: "center",
1472
+ flexGrow: 1,
1473
+ paddingY: 2,
1474
+ children: [
1475
+ /* @__PURE__ */ jsx11(Text9, { color: theme.colors.muted, children: icon }),
1476
+ /* @__PURE__ */ jsx11(Text9, { color: theme.colors.muted, children: message }),
1477
+ hint && /* @__PURE__ */ jsx11(Text9, { color: theme.colors.muted, dimColor: true, children: hint })
1478
+ ]
1479
+ }
1480
+ );
1481
+ }
1482
+
1483
+ // src/components/pr/FilesTab.tsx
1484
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1485
+ function buildFileTree(files) {
1486
+ const root = { dirs: {}, files: [] };
1487
+ for (const file of files) {
1488
+ const parts = file.filename.split("/");
1489
+ let current = root;
1490
+ for (let i = 0; i < parts.length - 1; i++) {
1491
+ const segment = parts[i];
1492
+ if (!current.dirs[segment]) {
1493
+ current.dirs[segment] = { dirs: {}, files: [] };
1494
+ }
1495
+ current = current.dirs[segment];
1496
+ }
1497
+ const leafName = parts[parts.length - 1] ?? file.filename;
1498
+ current.files.push(file);
1499
+ }
1500
+ function toTree(node) {
1501
+ const result = [];
1502
+ const dirNames = Object.keys(node.dirs).sort((a, b) => a.localeCompare(b));
1503
+ const files2 = [...node.files].sort(
1504
+ (a, b) => a.filename.localeCompare(b.filename)
1505
+ );
1506
+ for (const name of dirNames) {
1507
+ result.push({
1508
+ type: "dir",
1509
+ name,
1510
+ children: toTree(node.dirs[name])
1511
+ });
1512
+ }
1513
+ for (const file of files2) {
1514
+ result.push({ type: "file", file });
1515
+ }
1516
+ return result;
1517
+ }
1518
+ return toTree(root);
1519
+ }
1520
+ function flattenTreeToFiles(nodes) {
1521
+ const out = [];
1522
+ function walk(n) {
1523
+ for (const node of n) {
1524
+ if (node.type === "file") out.push(node.file);
1525
+ else walk(node.children);
1526
+ }
1527
+ }
1528
+ walk(nodes);
1529
+ return out;
1530
+ }
1531
+ function buildDisplayRows(nodes, indent = 0, fileIndexRef) {
1532
+ const rows = [];
1533
+ for (const node of nodes) {
1534
+ if (node.type === "file") {
1535
+ const parts = node.file.filename.split("/");
1536
+ const name = parts[parts.length - 1] ?? node.file.filename;
1537
+ rows.push({
1538
+ indent,
1539
+ type: "file",
1540
+ name,
1541
+ file: node.file,
1542
+ fileIndex: fileIndexRef.current
1543
+ });
1544
+ fileIndexRef.current += 1;
1545
+ } else {
1546
+ rows.push({ indent, type: "dir", name: node.name });
1547
+ rows.push(...buildDisplayRows(node.children, indent + 1, fileIndexRef));
1548
+ }
1549
+ }
1550
+ return rows;
1551
+ }
1552
+ function getLanguageFromFilename(filename) {
1553
+ const ext = filename.split(".").pop()?.toLowerCase();
1554
+ const map = {
1555
+ ts: "typescript",
1556
+ tsx: "typescript",
1557
+ js: "javascript",
1558
+ jsx: "javascript",
1559
+ json: "json",
1560
+ md: "markdown",
1561
+ py: "python",
1562
+ go: "go",
1563
+ rs: "rust",
1564
+ css: "css",
1565
+ scss: "scss",
1566
+ html: "html",
1567
+ yaml: "yaml",
1568
+ yml: "yaml"
1569
+ };
1570
+ return ext ? map[ext] : void 0;
1571
+ }
1572
+ function FileItem({
1573
+ item,
1574
+ isFocus,
1575
+ isSelected
1576
+ }) {
1577
+ const theme = useTheme();
1578
+ const statusColor = item.status === "added" ? theme.colors.diffAdd : item.status === "removed" ? theme.colors.diffDel : theme.colors.warning;
1579
+ const statusIcon = item.status === "added" ? "A" : item.status === "removed" ? "D" : item.status === "renamed" ? "R" : "M";
1580
+ const parts = item.filename.split("/");
1581
+ const filename = parts[parts.length - 1] ?? item.filename;
1582
+ return /* @__PURE__ */ jsxs9(Box12, { paddingX: 0, gap: 1, width: "100%", children: [
1583
+ /* @__PURE__ */ jsx12(Text10, { color: statusColor, bold: true, children: statusIcon }),
1584
+ /* @__PURE__ */ jsx12(
1585
+ Text10,
1586
+ {
1587
+ color: isFocus ? theme.colors.listSelectedFg : isSelected ? theme.colors.accent : theme.colors.text,
1588
+ bold: isFocus || isSelected,
1589
+ inverse: isFocus,
1590
+ children: filename
1591
+ }
1592
+ )
1593
+ ] });
1594
+ }
1595
+ function DiffLineView({
1596
+ line,
1597
+ lineNumber,
1598
+ isFocus,
1599
+ language
1600
+ }) {
1601
+ const theme = useTheme();
1602
+ const bgColor = isFocus ? theme.colors.selection : void 0;
1603
+ const textColor = line.type === "add" ? theme.colors.diffAdd : line.type === "del" ? theme.colors.diffDel : line.type === "header" ? theme.colors.info : theme.colors.text;
1604
+ const prefix = line.type === "add" ? "+" : line.type === "del" ? "-" : line.type === "header" ? "" : " ";
1605
+ const useSyntaxHighlight = line.type === "context" && language && line.content.trim().length > 0;
1606
+ return (
1607
+ // @ts-ignore
1608
+ /* @__PURE__ */ jsxs9(Box12, { backgroundColor: bgColor, children: [
1609
+ /* @__PURE__ */ jsx12(Box12, { width: 5, children: /* @__PURE__ */ jsx12(Text10, { color: theme.colors.muted, children: line.type === "header" ? "" : String(lineNumber).padStart(4, " ") }) }),
1610
+ useSyntaxHighlight ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "row", children: [
1611
+ /* @__PURE__ */ jsx12(Text10, { color: theme.colors.text, children: prefix }),
1612
+ /* @__PURE__ */ jsx12(SyntaxHighlight, { code: line.content, language })
1613
+ ] }) : /* @__PURE__ */ jsxs9(Text10, { color: textColor, bold: isFocus, inverse: isFocus, children: [
1614
+ prefix,
1615
+ line.content
1616
+ ] })
1617
+ ] })
1618
+ );
1619
+ }
1620
+ function DiffView({
1621
+ hunks,
1622
+ selectedLine,
1623
+ scrollOffset,
1624
+ viewportHeight,
1625
+ isActive,
1626
+ filename
1627
+ }) {
1628
+ const language = filename ? getLanguageFromFilename(filename) : void 0;
1629
+ const theme = useTheme();
1630
+ if (hunks.length === 0) {
1631
+ return /* @__PURE__ */ jsx12(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx12(Text10, { color: theme.colors.muted, children: "No diff available" }) });
1632
+ }
1633
+ const allLines = [];
1634
+ let lineNumber = 1;
1635
+ for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
1636
+ const hunk = hunks[hunkIndex];
1637
+ for (const line of hunk.lines) {
1638
+ allLines.push({ line, lineNumber, hunkIndex });
1639
+ if (line.type !== "header") {
1640
+ lineNumber++;
1641
+ }
1642
+ }
1643
+ }
1644
+ const visibleLines = allLines.slice(
1645
+ scrollOffset,
1646
+ scrollOffset + viewportHeight
1647
+ );
1648
+ return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: visibleLines.map((item, index) => /* @__PURE__ */ jsx12(
1649
+ DiffLineView,
1650
+ {
1651
+ line: item.line,
1652
+ lineNumber: item.lineNumber,
1653
+ isFocus: isActive && scrollOffset + index === selectedLine,
1654
+ language
1655
+ },
1656
+ `${item.hunkIndex}-${scrollOffset + index}`
1657
+ )) });
1658
+ }
1659
+ function FilesTab({
1660
+ files,
1661
+ isActive
1662
+ }) {
1663
+ const { stdout } = useStdout3();
1664
+ const theme = useTheme();
1665
+ const viewportHeight = Math.max(1, (stdout?.rows ?? 24) - 10);
1666
+ const [focusPanel, setFocusPanel] = useState5("tree");
1667
+ const [selectedFileIndex, setSelectedFileIndex] = useState5(0);
1668
+ const tree = useMemo(() => buildFileTree(files), [files]);
1669
+ const fileOrder = useMemo(() => flattenTreeToFiles(tree), [tree]);
1670
+ const displayRows = useMemo(
1671
+ () => buildDisplayRows(tree, 0, { current: 0 }),
1672
+ [tree]
1673
+ );
1674
+ const { selectedIndex: treeSelectedIndex } = useListNavigation({
1675
+ itemCount: fileOrder.length,
1676
+ viewportHeight,
1677
+ isActive: isActive && focusPanel === "tree"
1678
+ });
1679
+ const treeViewportHeight = viewportHeight - 2;
1680
+ const fileTreeListRef = useRef2(null);
1681
+ const selectedRowIndex = displayRows.findIndex(
1682
+ (r) => r.type === "file" && r.fileIndex === treeSelectedIndex
1683
+ );
1684
+ const effectiveRowIndex = selectedRowIndex >= 0 ? selectedRowIndex : 0;
1685
+ useEffect4(() => {
1686
+ const handleResize = () => fileTreeListRef.current?.remeasure();
1687
+ stdout?.on("resize", handleResize);
1688
+ return () => {
1689
+ stdout?.off("resize", handleResize);
1690
+ };
1691
+ }, [stdout]);
1692
+ React5.useEffect(() => {
1693
+ if (focusPanel === "tree") {
1694
+ setSelectedFileIndex(treeSelectedIndex);
1695
+ }
1696
+ }, [treeSelectedIndex, focusPanel]);
1697
+ const selectedFile = fileOrder[selectedFileIndex] ?? fileOrder[0] ?? null;
1698
+ const hunks = selectedFile?.patch ? parseDiffPatch(selectedFile.patch) : [];
1699
+ const totalDiffLines = hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
1700
+ const { selectedIndex: diffSelectedLine, scrollOffset: diffScrollOffset } = useListNavigation({
1701
+ itemCount: totalDiffLines,
1702
+ viewportHeight,
1703
+ isActive: isActive && focusPanel === "diff"
1704
+ });
1705
+ useInput3(
1706
+ (input, key) => {
1707
+ if (key.tab) {
1708
+ setFocusPanel((prev) => prev === "tree" ? "diff" : "tree");
1709
+ } else if (input === "h" || key.leftArrow) {
1710
+ setFocusPanel("tree");
1711
+ } else if (input === "l" || key.rightArrow) {
1712
+ setFocusPanel("diff");
1713
+ }
1714
+ },
1715
+ { isActive }
1716
+ );
1717
+ if (files.length === 0) {
1718
+ return /* @__PURE__ */ jsx12(EmptyState, { message: "No files changed" });
1719
+ }
1720
+ const isPanelFocused = focusPanel === "tree" && isActive;
1721
+ return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "row", flexGrow: 1, children: [
1722
+ /* @__PURE__ */ jsxs9(
1723
+ Box12,
1724
+ {
1725
+ flexDirection: "column",
1726
+ width: "30%",
1727
+ borderStyle: "single",
1728
+ borderColor: focusPanel === "tree" && isActive ? theme.colors.accent : theme.colors.border,
1729
+ children: [
1730
+ /* @__PURE__ */ jsx12(Box12, { paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsxs9(Text10, { color: theme.colors.accent, bold: true, children: [
1731
+ "Files (",
1732
+ files.length,
1733
+ ")"
1734
+ ] }) }),
1735
+ /* @__PURE__ */ jsx12(
1736
+ Box12,
1737
+ {
1738
+ flexDirection: "column",
1739
+ paddingX: 1,
1740
+ overflow: "hidden",
1741
+ height: treeViewportHeight,
1742
+ minHeight: treeViewportHeight,
1743
+ flexShrink: 0,
1744
+ children: /* @__PURE__ */ jsx12(
1745
+ ScrollList,
1746
+ {
1747
+ ref: fileTreeListRef,
1748
+ selectedIndex: effectiveRowIndex,
1749
+ scrollAlignment: "auto",
1750
+ children: displayRows.map(
1751
+ (row, rowIndex) => row.type === "dir" ? /* @__PURE__ */ jsx12(Box12, { paddingLeft: row.indent * 2, children: /* @__PURE__ */ jsxs9(Text10, { color: theme.colors.muted, children: [
1752
+ row.name,
1753
+ "/"
1754
+ ] }) }, `row-${rowIndex}`) : /* @__PURE__ */ jsx12(Box12, { paddingLeft: row.indent * 2, children: /* @__PURE__ */ jsx12(
1755
+ FileItem,
1756
+ {
1757
+ item: row.file,
1758
+ isFocus: isPanelFocused && row.fileIndex === treeSelectedIndex,
1759
+ isSelected: row.fileIndex === selectedFileIndex
1760
+ }
1761
+ ) }, `row-${rowIndex}`)
1762
+ )
1763
+ }
1764
+ )
1765
+ }
1766
+ )
1767
+ ]
1768
+ }
1769
+ ),
1770
+ /* @__PURE__ */ jsxs9(
1771
+ Box12,
1772
+ {
1773
+ flexDirection: "column",
1774
+ flexGrow: 1,
1775
+ borderStyle: "single",
1776
+ borderColor: focusPanel === "diff" && isActive ? theme.colors.accent : theme.colors.border,
1777
+ children: [
1778
+ /* @__PURE__ */ jsxs9(Box12, { paddingX: 1, paddingY: 0, gap: 2, children: [
1779
+ /* @__PURE__ */ jsx12(Text10, { color: theme.colors.accent, bold: true, children: selectedFile?.filename ?? "No file selected" }),
1780
+ selectedFile && /* @__PURE__ */ jsxs9(Box12, { gap: 1, children: [
1781
+ /* @__PURE__ */ jsxs9(Text10, { color: theme.colors.diffAdd, children: [
1782
+ "+",
1783
+ selectedFile.additions
1784
+ ] }),
1785
+ /* @__PURE__ */ jsxs9(Text10, { color: theme.colors.diffDel, children: [
1786
+ "-",
1787
+ selectedFile.deletions
1788
+ ] })
1789
+ ] })
1790
+ ] }),
1791
+ /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: /* @__PURE__ */ jsx12(
1792
+ DiffView,
1793
+ {
1794
+ hunks,
1795
+ selectedLine: diffSelectedLine,
1796
+ scrollOffset: diffScrollOffset,
1797
+ viewportHeight: viewportHeight - 2,
1798
+ isActive: isActive && focusPanel === "diff",
1799
+ filename: selectedFile?.filename
1800
+ }
1801
+ ) })
1802
+ ]
1803
+ }
1804
+ )
1805
+ ] });
1806
+ }
1807
+
1808
+ // src/components/pr/ConversationsTab.tsx
1809
+ import { useEffect as useEffect5, useRef as useRef3 } from "react";
1810
+ import { Box as Box13, Text as Text11, useStdout as useStdout4 } from "ink";
1811
+ import { Match } from "effect";
1812
+ import { ScrollList as ScrollList2 } from "ink-scroll-list";
1813
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1814
+ function buildTimeline(pr, comments, reviews) {
1815
+ const items = [];
1816
+ items.push({
1817
+ id: "description",
1818
+ type: "description",
1819
+ user: pr.user.login,
1820
+ body: pr.body,
1821
+ date: pr.created_at
1822
+ });
1823
+ for (const review of reviews) {
1824
+ if (review.state !== "PENDING") {
1825
+ items.push({
1826
+ id: `review-${review.id}`,
1827
+ type: "review",
1828
+ user: review.user.login,
1829
+ body: review.body,
1830
+ date: review.submitted_at ?? pr.created_at,
1831
+ state: review.state
1832
+ });
1833
+ }
1834
+ }
1835
+ for (const comment of comments) {
1836
+ items.push({
1837
+ id: `comment-${comment.id}`,
1838
+ type: "comment",
1839
+ user: comment.user.login,
1840
+ body: comment.body,
1841
+ date: comment.created_at,
1842
+ path: comment.path,
1843
+ line: comment.line
1844
+ });
1845
+ }
1846
+ items.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
1847
+ return items;
1848
+ }
1849
+ function TimelineItemView({
1850
+ item,
1851
+ isFocus
1852
+ }) {
1853
+ const theme = useTheme();
1854
+ const getStateIcon = (state) => Match.value(state).pipe(
1855
+ Match.when("APPROVED", () => ({
1856
+ icon: "\u2713",
1857
+ color: theme.colors.success
1858
+ })),
1859
+ Match.when("CHANGES_REQUESTED", () => ({
1860
+ icon: "\u2717",
1861
+ color: theme.colors.error
1862
+ })),
1863
+ Match.when("COMMENTED", () => ({ icon: "\u{1F4AC}", color: theme.colors.info })),
1864
+ Match.when("DISMISSED", () => ({ icon: "\u2014", color: theme.colors.muted })),
1865
+ Match.orElse(() => ({ icon: "\u2022", color: theme.colors.muted }))
1866
+ );
1867
+ const { icon, color } = item.type === "review" ? getStateIcon(item.state) : item.type === "description" ? { icon: "\u{1F4DD}", color: theme.colors.accent } : { icon: "\u{1F4AC}", color: theme.colors.info };
1868
+ const stateLabel = item.type === "review" && item.state ? item.state.toLowerCase().replace("_", " ") : "";
1869
+ const location = item.type === "comment" && item.path ? ` on ${item.path}${item.line != null ? `:${item.line}` : ""}` : "";
1870
+ return /* @__PURE__ */ jsxs10(
1871
+ Box13,
1872
+ {
1873
+ flexDirection: "column",
1874
+ paddingX: 1,
1875
+ paddingY: 1,
1876
+ marginBottom: 2,
1877
+ gap: 1,
1878
+ children: [
1879
+ /* @__PURE__ */ jsxs10(Box13, { flexDirection: "row", children: [
1880
+ isFocus && /* @__PURE__ */ jsx13(Text11, { color: theme.colors.accent, children: "\u25B8 " }),
1881
+ /* @__PURE__ */ jsx13(Text11, { color, children: icon }),
1882
+ /* @__PURE__ */ jsx13(Text11, { children: " " }),
1883
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.secondary, bold: true, children: item.user }),
1884
+ stateLabel ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
1885
+ /* @__PURE__ */ jsx13(Text11, { children: " " }),
1886
+ /* @__PURE__ */ jsx13(Text11, { color, children: stateLabel })
1887
+ ] }) : null,
1888
+ location ? /* @__PURE__ */ jsx13(Text11, { color: theme.colors.muted, children: location }) : null,
1889
+ /* @__PURE__ */ jsxs10(Text11, { color: theme.colors.muted, children: [
1890
+ " \xB7 ",
1891
+ timeAgo(item.date)
1892
+ ] })
1893
+ ] }),
1894
+ item.body ? /* @__PURE__ */ jsx13(Box13, { paddingLeft: isFocus ? 3 : 2, marginTop: 0, width: "80%", children: /* @__PURE__ */ jsx13(Text11, { color: theme.colors.text, wrap: "wrap", children: item.body }) }) : null
1895
+ ]
1896
+ }
1897
+ );
1898
+ }
1899
+ function PRInfoSection({
1900
+ pr
1901
+ }) {
1902
+ const theme = useTheme();
1903
+ return /* @__PURE__ */ jsxs10(
1904
+ Box13,
1905
+ {
1906
+ flexDirection: "column",
1907
+ paddingX: 1,
1908
+ paddingY: 1,
1909
+ borderStyle: "single",
1910
+ borderColor: theme.colors.border,
1911
+ children: [
1912
+ /* @__PURE__ */ jsxs10(Box13, { flexDirection: "row", children: [
1913
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.muted, children: "Author: " }),
1914
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.secondary, bold: true, children: pr.user.login })
1915
+ ] }),
1916
+ pr.requested_reviewers.length > 0 ? /* @__PURE__ */ jsxs10(Box13, { flexDirection: "row", marginTop: 0, children: [
1917
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.muted, children: "Reviewers: " }),
1918
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.text, children: pr.requested_reviewers.map((r) => r.login).join(", ") })
1919
+ ] }) : null,
1920
+ pr.labels.length > 0 ? /* @__PURE__ */ jsxs10(Box13, { flexDirection: "row", marginTop: 0, children: [
1921
+ /* @__PURE__ */ jsx13(Text11, { color: theme.colors.muted, children: "Labels: " }),
1922
+ pr.labels.map((label) => /* @__PURE__ */ jsxs10(Text11, { color: `#${label.color}`, children: [
1923
+ "[",
1924
+ label.name,
1925
+ "]",
1926
+ " "
1927
+ ] }, label.id))
1928
+ ] }) : null,
1929
+ /* @__PURE__ */ jsx13(Box13, { paddingY: 0, children: /* @__PURE__ */ jsx13(Divider, {}) }),
1930
+ /* @__PURE__ */ jsxs10(Box13, { flexDirection: "row", marginTop: 0, children: [
1931
+ /* @__PURE__ */ jsxs10(Text11, { color: theme.colors.diffAdd, children: [
1932
+ "+",
1933
+ pr.additions
1934
+ ] }),
1935
+ /* @__PURE__ */ jsx13(Text11, { children: " " }),
1936
+ /* @__PURE__ */ jsxs10(Text11, { color: theme.colors.diffDel, children: [
1937
+ "-",
1938
+ pr.deletions
1939
+ ] }),
1940
+ /* @__PURE__ */ jsxs10(Text11, { color: theme.colors.muted, children: [
1941
+ " ",
1942
+ pr.changed_files,
1943
+ " files changed"
1944
+ ] })
1945
+ ] })
1946
+ ]
1947
+ }
1948
+ );
1949
+ }
1950
+ var CONVERSATIONS_RESERVED_LINES = 18;
1951
+ function ConversationsTab({
1952
+ pr,
1953
+ comments,
1954
+ reviews,
1955
+ isActive
1956
+ }) {
1957
+ const theme = useTheme();
1958
+ const { stdout } = useStdout4();
1959
+ const listRef = useRef3(null);
1960
+ const timeline = buildTimeline(pr, comments, reviews);
1961
+ const viewportHeight = Math.max(1, (stdout?.rows ?? 24) - CONVERSATIONS_RESERVED_LINES);
1962
+ const { selectedIndex } = useListNavigation({
1963
+ itemCount: timeline.length,
1964
+ viewportHeight,
1965
+ isActive
1966
+ });
1967
+ useEffect5(() => {
1968
+ const handleResize = () => {
1969
+ listRef.current?.remeasure();
1970
+ };
1971
+ stdout?.on("resize", handleResize);
1972
+ return () => {
1973
+ stdout?.off("resize", handleResize);
1974
+ };
1975
+ }, [stdout]);
1976
+ return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", flexGrow: 1, children: [
1977
+ /* @__PURE__ */ jsx13(PRInfoSection, { pr }),
1978
+ /* @__PURE__ */ jsx13(Box13, { flexDirection: "row", paddingX: 1, paddingY: 0, marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text11, { color: theme.colors.accent, bold: true, children: [
1979
+ "Timeline (",
1980
+ timeline.length,
1981
+ " items)"
1982
+ ] }) }),
1983
+ /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexGrow: 1, overflow: "hidden", height: viewportHeight, children: timeline.length === 0 ? /* @__PURE__ */ jsx13(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { color: theme.colors.muted, children: "No conversations yet" }) }) : /* @__PURE__ */ jsx13(
1984
+ ScrollList2,
1985
+ {
1986
+ ref: listRef,
1987
+ selectedIndex,
1988
+ scrollAlignment: "auto",
1989
+ children: timeline.map((item, index) => /* @__PURE__ */ jsx13(
1990
+ TimelineItemView,
1991
+ {
1992
+ item,
1993
+ isFocus: index === selectedIndex
1994
+ },
1995
+ item.id
1996
+ ))
1997
+ }
1998
+ ) })
1999
+ ] });
2000
+ }
2001
+
2002
+ // src/components/pr/CommitsTab.tsx
2003
+ import { useEffect as useEffect6, useRef as useRef4 } from "react";
2004
+ import { Box as Box14, Text as Text12, useStdout as useStdout5 } from "ink";
2005
+ import { ScrollList as ScrollList3 } from "ink-scroll-list";
2006
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
2007
+ function CommitItem({
2008
+ commit,
2009
+ isFocus
2010
+ }) {
2011
+ const theme = useTheme();
2012
+ const shortSha = commit.sha.slice(0, 7);
2013
+ const message = commit.commit.message.split("\n")[0] ?? "";
2014
+ const author = commit.author?.login ?? commit.commit.author.name;
2015
+ const date = commit.commit.author.date;
2016
+ return /* @__PURE__ */ jsxs11(
2017
+ Box14,
2018
+ {
2019
+ paddingX: 1,
2020
+ paddingY: 0,
2021
+ gap: 1,
2022
+ backgroundColor: isFocus ? theme.colors.selection : void 0,
2023
+ children: [
2024
+ /* @__PURE__ */ jsx14(Box14, { width: 10, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.warning, bold: isFocus, children: shortSha }) }),
2025
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx14(
2026
+ Text12,
2027
+ {
2028
+ color: isFocus ? theme.colors.listSelectedFg : theme.colors.text,
2029
+ bold: isFocus,
2030
+ wrap: "truncate",
2031
+ children: message
2032
+ }
2033
+ ) }),
2034
+ /* @__PURE__ */ jsx14(Box14, { width: 16, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.secondary, children: author }) }),
2035
+ /* @__PURE__ */ jsx14(Box14, { width: 14, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.muted, children: timeAgo(date) }) })
2036
+ ]
2037
+ }
2038
+ );
2039
+ }
2040
+ function CommitsTab({
2041
+ commits,
2042
+ isActive
2043
+ }) {
2044
+ const { stdout } = useStdout5();
2045
+ const theme = useTheme();
2046
+ const viewportHeight = Math.max(1, (stdout?.rows ?? 24) - 10);
2047
+ const listRef = useRef4(null);
2048
+ const { selectedIndex } = useListNavigation({
2049
+ itemCount: commits.length,
2050
+ viewportHeight,
2051
+ isActive
2052
+ });
2053
+ useEffect6(() => {
2054
+ const handleResize = () => listRef.current?.remeasure();
2055
+ stdout?.on("resize", handleResize);
2056
+ return () => {
2057
+ stdout?.off("resize", handleResize);
2058
+ };
2059
+ }, [stdout]);
2060
+ if (commits.length === 0) {
2061
+ return /* @__PURE__ */ jsx14(EmptyState, { message: "No commits found" });
2062
+ }
2063
+ return /* @__PURE__ */ jsxs11(Box14, { flexDirection: "column", flexGrow: 1, children: [
2064
+ /* @__PURE__ */ jsxs11(Box14, { paddingX: 1, paddingY: 1, gap: 1, children: [
2065
+ /* @__PURE__ */ jsx14(Text12, { color: theme.colors.accent, bold: true, children: "Commits" }),
2066
+ /* @__PURE__ */ jsxs11(Text12, { color: theme.colors.muted, children: [
2067
+ "(",
2068
+ commits.length,
2069
+ ")"
2070
+ ] })
2071
+ ] }),
2072
+ /* @__PURE__ */ jsxs11(
2073
+ Box14,
2074
+ {
2075
+ paddingX: 1,
2076
+ paddingBottom: 1,
2077
+ gap: 1,
2078
+ borderStyle: "single",
2079
+ borderColor: theme.colors.border,
2080
+ borderTop: false,
2081
+ borderLeft: false,
2082
+ borderRight: false,
2083
+ children: [
2084
+ /* @__PURE__ */ jsx14(Box14, { width: 10, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.muted, bold: true, children: "SHA" }) }),
2085
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.muted, bold: true, children: "Message" }) }),
2086
+ /* @__PURE__ */ jsx14(Box14, { width: 16, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.muted, bold: true, children: "Author" }) }),
2087
+ /* @__PURE__ */ jsx14(Box14, { width: 14, children: /* @__PURE__ */ jsx14(Text12, { color: theme.colors.muted, bold: true, children: "Date" }) })
2088
+ ]
2089
+ }
2090
+ ),
2091
+ /* @__PURE__ */ jsx14(Box14, { flexDirection: "column", overflow: "hidden", height: viewportHeight, children: /* @__PURE__ */ jsx14(ScrollList3, { ref: listRef, selectedIndex, scrollAlignment: "auto", children: commits.map((commit, index) => /* @__PURE__ */ jsx14(
2092
+ CommitItem,
2093
+ {
2094
+ commit,
2095
+ isFocus: index === selectedIndex
2096
+ },
2097
+ commit.sha
2098
+ )) }) })
2099
+ ] });
2100
+ }
2101
+
2102
+ // src/components/common/LoadingIndicator.tsx
2103
+ import { Box as Box15, Text as Text13, useStdout as useStdout6 } from "ink";
2104
+ import { Spinner as Spinner2 } from "@inkjs/ui";
2105
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
2106
+ function LoadingIndicator({
2107
+ message = "Loading..."
2108
+ }) {
2109
+ const theme = useTheme();
2110
+ const { stdout } = useStdout6();
2111
+ const height = stdout?.rows ?? 24;
2112
+ return /* @__PURE__ */ jsx15(
2113
+ Box15,
2114
+ {
2115
+ flexDirection: "column",
2116
+ justifyContent: "center",
2117
+ alignItems: "center",
2118
+ height: height - 4,
2119
+ flexGrow: 1,
2120
+ children: /* @__PURE__ */ jsxs12(Box15, { gap: 1, children: [
2121
+ /* @__PURE__ */ jsx15(Spinner2, {}),
2122
+ /* @__PURE__ */ jsx15(Text13, { color: theme.colors.accent, children: message })
2123
+ ] })
2124
+ }
2125
+ );
2126
+ }
2127
+
2128
+ // src/screens/PRDetailScreen.tsx
2129
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
2130
+ var PR_DETAIL_RESERVED_LINES = 12;
2131
+ function PRDetailScreen({
2132
+ pr,
2133
+ owner,
2134
+ repo,
2135
+ onBack
2136
+ }) {
2137
+ const { stdout } = useStdout7();
2138
+ const [currentTab, setCurrentTab] = useState6(0);
2139
+ const contentHeight = Math.max(1, (stdout?.rows ?? 24) - PR_DETAIL_RESERVED_LINES);
2140
+ const { data: files = [], isLoading: filesLoading } = usePRFiles(owner, repo, pr.number);
2141
+ const { data: comments = [], isLoading: commentsLoading } = usePRComments(owner, repo, pr.number);
2142
+ const { data: reviews = [], isLoading: reviewsLoading } = usePRReviews(owner, repo, pr.number);
2143
+ const { data: commits = [], isLoading: commitsLoading } = usePRCommits(owner, repo, pr.number);
2144
+ const isLoading = filesLoading || commentsLoading || reviewsLoading || commitsLoading;
2145
+ useInput4((input, key) => {
2146
+ if (input === "1") {
2147
+ setCurrentTab(0);
2148
+ } else if (input === "2") {
2149
+ setCurrentTab(1);
2150
+ } else if (input === "3") {
2151
+ setCurrentTab(2);
2152
+ } else if (input === "q" || key.escape) {
2153
+ onBack();
2154
+ }
2155
+ });
2156
+ const renderTabContent = () => {
2157
+ if (isLoading) {
2158
+ return /* @__PURE__ */ jsx16(LoadingIndicator, { message: "Loading PR details..." });
2159
+ }
2160
+ return Match2.value(currentTab).pipe(
2161
+ Match2.when(0, () => /* @__PURE__ */ jsx16(
2162
+ ConversationsTab,
2163
+ {
2164
+ pr,
2165
+ comments,
2166
+ reviews,
2167
+ isActive: true
2168
+ }
2169
+ )),
2170
+ Match2.when(1, () => /* @__PURE__ */ jsx16(CommitsTab, { commits, isActive: true })),
2171
+ Match2.when(2, () => /* @__PURE__ */ jsx16(FilesTab, { files, isActive: true })),
2172
+ Match2.orElse(() => /* @__PURE__ */ jsx16(
2173
+ ConversationsTab,
2174
+ {
2175
+ pr,
2176
+ comments,
2177
+ reviews,
2178
+ isActive: true
2179
+ }
2180
+ ))
2181
+ );
2182
+ };
2183
+ return /* @__PURE__ */ jsxs13(Box16, { flexDirection: "column", flexGrow: 1, children: [
2184
+ /* @__PURE__ */ jsx16(PRHeader, { pr }),
2185
+ /* @__PURE__ */ jsx16(PRTabs, { activeIndex: currentTab, onChange: setCurrentTab }),
2186
+ /* @__PURE__ */ jsx16(Box16, { height: contentHeight, overflow: "hidden", flexDirection: "column", children: renderTabContent() })
2187
+ ] });
2188
+ }
2189
+
2190
+ // src/screens/MyPRsScreen.tsx
2191
+ import { useState as useState10 } from "react";
2192
+ import { Box as Box21, Text as Text18, useInput as useInput7 } from "ink";
2193
+
2194
+ // src/hooks/usePagination.ts
2195
+ import { useState as useState7, useMemo as useMemo2, useCallback as useCallback3, useEffect as useEffect7, useRef as useRef5 } from "react";
2196
+ function usePagination(items, options = {}) {
2197
+ const pageSize = options.pageSize ?? 18;
2198
+ const [currentPage, setCurrentPage] = useState7(1);
2199
+ const prevItemsLengthRef = useRef5(items.length);
2200
+ const totalPages = useMemo2(
2201
+ () => Math.max(1, Math.ceil(items.length / pageSize)),
2202
+ [items.length, pageSize]
2203
+ );
2204
+ useEffect7(() => {
2205
+ if (items.length !== prevItemsLengthRef.current) {
2206
+ setCurrentPage(1);
2207
+ prevItemsLengthRef.current = items.length;
2208
+ }
2209
+ }, [items.length]);
2210
+ const safePage = Math.min(currentPage, totalPages);
2211
+ const startIndex = (safePage - 1) * pageSize;
2212
+ const endIndex = Math.min(startIndex + pageSize, items.length);
2213
+ const pageItems = useMemo2(
2214
+ () => items.slice(startIndex, endIndex),
2215
+ [items, startIndex, endIndex]
2216
+ );
2217
+ const hasNextPage = safePage < totalPages;
2218
+ const hasPrevPage = safePage > 1;
2219
+ const nextPage = useCallback3(() => {
2220
+ if (hasNextPage) {
2221
+ setCurrentPage((p) => p + 1);
2222
+ }
2223
+ }, [hasNextPage]);
2224
+ const prevPage = useCallback3(() => {
2225
+ if (hasPrevPage) {
2226
+ setCurrentPage((p) => p - 1);
2227
+ }
2228
+ }, [hasPrevPage]);
2229
+ const goToPage = useCallback3(
2230
+ (page) => {
2231
+ const clampedPage = Math.max(1, Math.min(page, totalPages));
2232
+ setCurrentPage(clampedPage);
2233
+ },
2234
+ [totalPages]
2235
+ );
2236
+ return {
2237
+ currentPage: safePage,
2238
+ totalPages,
2239
+ pageItems,
2240
+ hasNextPage,
2241
+ hasPrevPage,
2242
+ nextPage,
2243
+ prevPage,
2244
+ goToPage,
2245
+ startIndex,
2246
+ endIndex
2247
+ };
2248
+ }
2249
+
2250
+ // src/hooks/useFilter.ts
2251
+ import { useState as useState8, useMemo as useMemo3, useCallback as useCallback4 } from "react";
2252
+ var defaultFilter = {
2253
+ search: "",
2254
+ repo: null,
2255
+ author: null,
2256
+ label: null,
2257
+ sortBy: "updated",
2258
+ sortDirection: "desc"
2259
+ };
2260
+ function extractRepoFromUrl(url) {
2261
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)\/pull/);
2262
+ return match?.[1] ?? null;
2263
+ }
2264
+ function matchesSearch(pr, search) {
2265
+ if (!search) return true;
2266
+ const lowerSearch = search.toLowerCase();
2267
+ return pr.title.toLowerCase().includes(lowerSearch) || pr.user.login.toLowerCase().includes(lowerSearch) || String(pr.number).includes(lowerSearch);
2268
+ }
2269
+ function matchesRepo(pr, repo) {
2270
+ if (!repo) return true;
2271
+ const prRepo = extractRepoFromUrl(pr.html_url);
2272
+ return prRepo?.toLowerCase().includes(repo.toLowerCase()) ?? false;
2273
+ }
2274
+ function matchesAuthor(pr, author) {
2275
+ if (!author) return true;
2276
+ return pr.user.login.toLowerCase().includes(author.toLowerCase());
2277
+ }
2278
+ function matchesLabel(pr, label) {
2279
+ if (!label) return true;
2280
+ const lowerLabel = label.toLowerCase();
2281
+ return pr.labels.some((l) => l.name.toLowerCase().includes(lowerLabel));
2282
+ }
2283
+ function comparePRs(a, b, sortBy, sortDirection) {
2284
+ let comparison = 0;
2285
+ switch (sortBy) {
2286
+ case "updated":
2287
+ comparison = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
2288
+ break;
2289
+ case "created":
2290
+ comparison = new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
2291
+ break;
2292
+ case "repo": {
2293
+ const repoA = extractRepoFromUrl(a.html_url) ?? "";
2294
+ const repoB = extractRepoFromUrl(b.html_url) ?? "";
2295
+ comparison = repoA.localeCompare(repoB);
2296
+ break;
2297
+ }
2298
+ case "author":
2299
+ comparison = a.user.login.localeCompare(b.user.login);
2300
+ break;
2301
+ case "title":
2302
+ comparison = a.title.localeCompare(b.title);
2303
+ break;
2304
+ }
2305
+ return sortDirection === "asc" ? -comparison : comparison;
2306
+ }
2307
+ function useFilter(items) {
2308
+ const [filter, setFilter] = useState8(defaultFilter);
2309
+ const availableRepos = useMemo3(() => {
2310
+ const repos = /* @__PURE__ */ new Set();
2311
+ items.forEach((pr) => {
2312
+ const repo = extractRepoFromUrl(pr.html_url);
2313
+ if (repo) repos.add(repo);
2314
+ });
2315
+ return Array.from(repos).sort();
2316
+ }, [items]);
2317
+ const availableAuthors = useMemo3(() => {
2318
+ const authors = /* @__PURE__ */ new Set();
2319
+ items.forEach((pr) => authors.add(pr.user.login));
2320
+ return Array.from(authors).sort();
2321
+ }, [items]);
2322
+ const availableLabels = useMemo3(() => {
2323
+ const labels = /* @__PURE__ */ new Set();
2324
+ items.forEach((pr) => pr.labels.forEach((l) => labels.add(l.name)));
2325
+ return Array.from(labels).sort();
2326
+ }, [items]);
2327
+ const filteredItems = useMemo3(() => {
2328
+ return items.filter((pr) => matchesSearch(pr, filter.search)).filter((pr) => matchesRepo(pr, filter.repo)).filter((pr) => matchesAuthor(pr, filter.author)).filter((pr) => matchesLabel(pr, filter.label)).sort((a, b) => comparePRs(a, b, filter.sortBy, filter.sortDirection));
2329
+ }, [items, filter]);
2330
+ const setSearch = useCallback4((search) => {
2331
+ setFilter((prev) => ({ ...prev, search }));
2332
+ }, []);
2333
+ const setRepo = useCallback4((repo) => {
2334
+ setFilter((prev) => ({ ...prev, repo }));
2335
+ }, []);
2336
+ const setAuthor = useCallback4((author) => {
2337
+ setFilter((prev) => ({ ...prev, author }));
2338
+ }, []);
2339
+ const setLabel = useCallback4((label) => {
2340
+ setFilter((prev) => ({ ...prev, label }));
2341
+ }, []);
2342
+ const setSortBy = useCallback4((sortBy) => {
2343
+ setFilter((prev) => ({ ...prev, sortBy }));
2344
+ }, []);
2345
+ const toggleSortDirection = useCallback4(() => {
2346
+ setFilter((prev) => ({
2347
+ ...prev,
2348
+ sortDirection: prev.sortDirection === "asc" ? "desc" : "asc"
2349
+ }));
2350
+ }, []);
2351
+ const clearFilters = useCallback4(() => {
2352
+ setFilter(defaultFilter);
2353
+ }, []);
2354
+ const hasActiveFilters = filter.search !== "" || filter.repo !== null || filter.author !== null || filter.label !== null;
2355
+ return {
2356
+ filter,
2357
+ filteredItems,
2358
+ setSearch,
2359
+ setRepo,
2360
+ setAuthor,
2361
+ setLabel,
2362
+ setSortBy,
2363
+ toggleSortDirection,
2364
+ clearFilters,
2365
+ hasActiveFilters,
2366
+ availableRepos,
2367
+ availableAuthors,
2368
+ availableLabels
2369
+ };
2370
+ }
2371
+
2372
+ // src/components/pr/PRListItem.tsx
2373
+ import { Box as Box17, Text as Text14 } from "ink";
2374
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
2375
+ function extractRepoFromUrl2(url) {
2376
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)\/pull/);
2377
+ return match?.[1] ?? null;
2378
+ }
2379
+ function PRListItem({
2380
+ item,
2381
+ isFocus
2382
+ }) {
2383
+ const theme = useTheme();
2384
+ const stateColor = item.draft ? theme.colors.muted : item.state === "open" ? theme.colors.success : theme.colors.error;
2385
+ const stateIcon = item.draft ? "D" : item.state === "open" ? "O" : "C";
2386
+ const repoName = extractRepoFromUrl2(item.html_url);
2387
+ return /* @__PURE__ */ jsxs14(Box17, { flexDirection: "column", paddingX: 1, children: [
2388
+ /* @__PURE__ */ jsxs14(Box17, { gap: 1, children: [
2389
+ /* @__PURE__ */ jsx17(Text14, { color: stateColor, bold: true, children: stateIcon }),
2390
+ /* @__PURE__ */ jsxs14(
2391
+ Text14,
2392
+ {
2393
+ color: isFocus ? theme.colors.listSelectedFg : theme.colors.text,
2394
+ bold: isFocus,
2395
+ inverse: isFocus,
2396
+ children: [
2397
+ "#",
2398
+ item.number
2399
+ ]
2400
+ }
2401
+ ),
2402
+ /* @__PURE__ */ jsx17(
2403
+ Text14,
2404
+ {
2405
+ color: isFocus ? theme.colors.listSelectedFg : theme.colors.text,
2406
+ bold: isFocus,
2407
+ inverse: isFocus,
2408
+ children: item.title
2409
+ }
2410
+ )
2411
+ ] }),
2412
+ /* @__PURE__ */ jsxs14(Box17, { gap: 1, paddingLeft: 3, children: [
2413
+ repoName && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2414
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.secondary, children: repoName }),
2415
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: "|" })
2416
+ ] }),
2417
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: item.user.login }),
2418
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: "|" }),
2419
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: timeAgo(item.created_at) }),
2420
+ item.requested_reviewers.length > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2421
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: "|" }),
2422
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.info, children: item.requested_reviewers.map((r) => r.login).join(", ") })
2423
+ ] }),
2424
+ item.comments > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2425
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: "|" }),
2426
+ /* @__PURE__ */ jsxs14(Text14, { color: theme.colors.muted, children: [
2427
+ item.comments,
2428
+ " comments"
2429
+ ] })
2430
+ ] }),
2431
+ item.labels.length > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
2432
+ /* @__PURE__ */ jsx17(Text14, { color: theme.colors.muted, children: "|" }),
2433
+ item.labels.map(
2434
+ (label) => /* @__PURE__ */ jsxs14(Text14, { color: `#${label.color}`, children: [
2435
+ "[",
2436
+ label.name,
2437
+ "]"
2438
+ ] }, label.id)
2439
+ )
2440
+ ] })
2441
+ ] })
2442
+ ] });
2443
+ }
2444
+
2445
+ // src/components/common/PaginationBar.tsx
2446
+ import { Box as Box18, Text as Text15 } from "ink";
2447
+ import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
2448
+ function PaginationBar({
2449
+ currentPage,
2450
+ totalPages,
2451
+ totalItems,
2452
+ startIndex,
2453
+ endIndex,
2454
+ hasNextPage,
2455
+ hasPrevPage
2456
+ }) {
2457
+ const theme = useTheme();
2458
+ if (totalPages <= 1) {
2459
+ return /* @__PURE__ */ jsx18(Box18, { paddingX: 1, children: /* @__PURE__ */ jsxs15(Text15, { color: theme.colors.muted, children: [
2460
+ totalItems,
2461
+ " item",
2462
+ totalItems !== 1 ? "s" : ""
2463
+ ] }) });
2464
+ }
2465
+ return /* @__PURE__ */ jsxs15(Box18, { paddingX: 1, gap: 2, children: [
2466
+ /* @__PURE__ */ jsxs15(Text15, { color: theme.colors.muted, children: [
2467
+ startIndex + 1,
2468
+ "-",
2469
+ endIndex,
2470
+ " of ",
2471
+ totalItems
2472
+ ] }),
2473
+ /* @__PURE__ */ jsxs15(Box18, { gap: 1, children: [
2474
+ /* @__PURE__ */ jsx18(Text15, { color: hasPrevPage ? theme.colors.accent : theme.colors.muted, children: hasPrevPage ? "\u2190 [p]rev" : "\u2190 prev" }),
2475
+ /* @__PURE__ */ jsx18(Text15, { color: theme.colors.muted, children: "\u2502" }),
2476
+ /* @__PURE__ */ jsxs15(Text15, { color: theme.colors.text, children: [
2477
+ "Page ",
2478
+ currentPage,
2479
+ "/",
2480
+ totalPages
2481
+ ] }),
2482
+ /* @__PURE__ */ jsx18(Text15, { color: theme.colors.muted, children: "\u2502" }),
2483
+ /* @__PURE__ */ jsx18(Text15, { color: hasNextPage ? theme.colors.accent : theme.colors.muted, children: hasNextPage ? "[n]ext \u2192" : "next \u2192" })
2484
+ ] })
2485
+ ] });
2486
+ }
2487
+
2488
+ // src/components/common/FilterModal.tsx
2489
+ import { useState as useState9, useEffect as useEffect8 } from "react";
2490
+ import { Box as Box19, Text as Text16, useInput as useInput5 } from "ink";
2491
+ import { TextInput as TextInput2 } from "@inkjs/ui";
2492
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
2493
+ function FilterModal({
2494
+ filter,
2495
+ onSearchChange,
2496
+ onClear,
2497
+ onClose
2498
+ }) {
2499
+ const theme = useTheme();
2500
+ const { setInputActive } = useInputFocus();
2501
+ const [searchValue, setSearchValue] = useState9(filter.search);
2502
+ const [showClearConfirm, setShowClearConfirm] = useState9(false);
2503
+ useEffect8(() => {
2504
+ setInputActive(true);
2505
+ return () => setInputActive(false);
2506
+ }, [setInputActive]);
2507
+ useInput5((input, key) => {
2508
+ if (showClearConfirm) {
2509
+ if (input === "y" || input === "Y") {
2510
+ onClear();
2511
+ onClose();
2512
+ } else if (input === "n" || input === "N" || key.escape) {
2513
+ setShowClearConfirm(false);
2514
+ }
2515
+ return;
2516
+ }
2517
+ if (key.escape) {
2518
+ onClose();
2519
+ } else if (key.return) {
2520
+ onSearchChange(searchValue);
2521
+ onClose();
2522
+ } else if (input === "c" && filter.search) {
2523
+ setShowClearConfirm(true);
2524
+ }
2525
+ });
2526
+ if (showClearConfirm) {
2527
+ return /* @__PURE__ */ jsx19(Modal, { children: /* @__PURE__ */ jsxs16(
2528
+ Box19,
2529
+ {
2530
+ flexDirection: "column",
2531
+ borderStyle: "round",
2532
+ borderColor: theme.colors.accent,
2533
+ backgroundColor: theme.colors.bg,
2534
+ paddingX: 2,
2535
+ paddingY: 1,
2536
+ gap: 1,
2537
+ children: [
2538
+ /* @__PURE__ */ jsx19(Text16, { color: theme.colors.accent, bold: true, children: "Clear all filters?" }),
2539
+ /* @__PURE__ */ jsx19(Text16, { color: theme.colors.text, children: "y: Yes, n: No" })
2540
+ ]
2541
+ }
2542
+ ) });
2543
+ }
2544
+ return /* @__PURE__ */ jsx19(Modal, { children: /* @__PURE__ */ jsxs16(
2545
+ Box19,
2546
+ {
2547
+ flexDirection: "column",
2548
+ borderStyle: "round",
2549
+ borderColor: theme.colors.accent,
2550
+ backgroundColor: theme.colors.bg,
2551
+ paddingX: 2,
2552
+ paddingY: 1,
2553
+ gap: 1,
2554
+ children: [
2555
+ /* @__PURE__ */ jsx19(Text16, { color: theme.colors.accent, bold: true, children: "Search PRs" }),
2556
+ /* @__PURE__ */ jsx19(Text16, { color: theme.colors.muted, children: "Filter by title, number, or author" }),
2557
+ /* @__PURE__ */ jsx19(Box19, { children: /* @__PURE__ */ jsx19(
2558
+ TextInput2,
2559
+ {
2560
+ defaultValue: searchValue,
2561
+ onChange: setSearchValue,
2562
+ placeholder: "Type to search..."
2563
+ }
2564
+ ) }),
2565
+ /* @__PURE__ */ jsxs16(Text16, { color: theme.colors.muted, dimColor: true, children: [
2566
+ "Enter: apply | Esc: cancel",
2567
+ filter.search ? " | c: clear" : ""
2568
+ ] })
2569
+ ]
2570
+ }
2571
+ ) });
2572
+ }
2573
+
2574
+ // src/components/common/SortModal.tsx
2575
+ import { useMemo as useMemo4 } from "react";
2576
+ import { Box as Box20, Text as Text17, useInput as useInput6 } from "ink";
2577
+ import SelectInput from "ink-select-input";
2578
+ import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
2579
+ var SORT_OPTIONS = [
2580
+ { key: "updated", label: "Last Updated" },
2581
+ { key: "created", label: "Created Date" },
2582
+ { key: "repo", label: "Repository" },
2583
+ { key: "author", label: "Author" },
2584
+ { key: "title", label: "Title" }
2585
+ ];
2586
+ function SortModal({
2587
+ currentSort,
2588
+ sortDirection,
2589
+ onSortChange,
2590
+ onSortDirectionToggle,
2591
+ onClose
2592
+ }) {
2593
+ const theme = useTheme();
2594
+ useInput6((input, key) => {
2595
+ if (key.escape || input === "s") {
2596
+ onClose();
2597
+ }
2598
+ });
2599
+ const items = useMemo4(
2600
+ () => SORT_OPTIONS.map((option) => ({
2601
+ label: `${option.label}${option.key === currentSort ? sortDirection === "desc" ? " \u2193" : " \u2191" : ""}`,
2602
+ value: option.key
2603
+ })),
2604
+ [currentSort, sortDirection]
2605
+ );
2606
+ const initialIndex = Math.max(
2607
+ 0,
2608
+ SORT_OPTIONS.findIndex((o) => o.key === currentSort)
2609
+ );
2610
+ const handleSelect = (item) => {
2611
+ if (item.value === currentSort) {
2612
+ onSortDirectionToggle();
2613
+ } else {
2614
+ onSortChange(item.value);
2615
+ }
2616
+ onClose();
2617
+ };
2618
+ return /* @__PURE__ */ jsx20(Modal, { children: /* @__PURE__ */ jsxs17(
2619
+ Box20,
2620
+ {
2621
+ flexDirection: "column",
2622
+ borderStyle: "round",
2623
+ borderColor: theme.colors.accent,
2624
+ backgroundColor: theme.colors.bg,
2625
+ paddingX: 2,
2626
+ paddingY: 1,
2627
+ gap: 1,
2628
+ children: [
2629
+ /* @__PURE__ */ jsx20(Text17, { color: theme.colors.accent, bold: true, children: "Sort by" }),
2630
+ /* @__PURE__ */ jsx20(
2631
+ SelectInput,
2632
+ {
2633
+ items,
2634
+ initialIndex,
2635
+ onSelect: handleSelect,
2636
+ isFocused: true
2637
+ }
2638
+ ),
2639
+ /* @__PURE__ */ jsx20(Text17, { color: theme.colors.muted, dimColor: true, children: "j/k: move | Enter: select | Esc: close" })
2640
+ ]
2641
+ }
2642
+ ) });
2643
+ }
2644
+
2645
+ // src/screens/MyPRsScreen.tsx
2646
+ import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
2647
+ function MyPRsScreen({
2648
+ onSelect
2649
+ }) {
2650
+ const theme = useTheme();
2651
+ const { data: prs = [], isLoading, error } = useMyPRs();
2652
+ const [showFilter, setShowFilter] = useState10(false);
2653
+ const [showSort, setShowSort] = useState10(false);
2654
+ const {
2655
+ filter,
2656
+ filteredItems,
2657
+ setSearch,
2658
+ setRepo,
2659
+ setAuthor,
2660
+ setLabel,
2661
+ setSortBy,
2662
+ toggleSortDirection,
2663
+ clearFilters,
2664
+ hasActiveFilters,
2665
+ availableRepos,
2666
+ availableAuthors,
2667
+ availableLabels
2668
+ } = useFilter(prs);
2669
+ const {
2670
+ currentPage,
2671
+ totalPages,
2672
+ pageItems,
2673
+ hasNextPage,
2674
+ hasPrevPage,
2675
+ nextPage,
2676
+ prevPage,
2677
+ startIndex,
2678
+ endIndex
2679
+ } = usePagination(filteredItems, { pageSize: 18 });
2680
+ const { selectedIndex } = useListNavigation({
2681
+ itemCount: pageItems.length,
2682
+ viewportHeight: pageItems.length,
2683
+ isActive: !showFilter && !showSort
2684
+ });
2685
+ useInput7(
2686
+ (input, key) => {
2687
+ if (key.return && pageItems[selectedIndex]) {
2688
+ onSelect(pageItems[selectedIndex]);
2689
+ } else if (input === "n" && hasNextPage) {
2690
+ nextPage();
2691
+ } else if (input === "p" && hasPrevPage) {
2692
+ prevPage();
2693
+ } else if (input === "/") {
2694
+ setShowFilter(true);
2695
+ } else if (input === "s") {
2696
+ setShowSort(true);
2697
+ }
2698
+ },
2699
+ { isActive: !showFilter && !showSort }
2700
+ );
2701
+ if (isLoading && prs.length === 0) {
2702
+ return /* @__PURE__ */ jsx21(LoadingIndicator, { message: "Loading your PRs..." });
2703
+ }
2704
+ if (error) {
2705
+ return /* @__PURE__ */ jsx21(Box21, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs18(Text18, { color: theme.colors.error, children: [
2706
+ "Error: ",
2707
+ String(error)
2708
+ ] }) });
2709
+ }
2710
+ if (prs.length === 0) {
2711
+ return /* @__PURE__ */ jsx21(EmptyState, { message: "You have no open pull requests" });
2712
+ }
2713
+ return /* @__PURE__ */ jsxs18(Box21, { flexDirection: "column", flexGrow: 1, children: [
2714
+ /* @__PURE__ */ jsxs18(Box21, { paddingX: 1, justifyContent: "space-between", children: [
2715
+ /* @__PURE__ */ jsxs18(Box21, { gap: 2, children: [
2716
+ /* @__PURE__ */ jsx21(Text18, { color: theme.colors.accent, bold: true, children: "My Pull Requests" }),
2717
+ hasActiveFilters && /* @__PURE__ */ jsx21(Text18, { color: theme.colors.warning, children: "(filtered)" }),
2718
+ /* @__PURE__ */ jsx21(Text18, { color: theme.colors.muted, children: "/ filter s sort" })
2719
+ ] }),
2720
+ /* @__PURE__ */ jsx21(
2721
+ PaginationBar,
2722
+ {
2723
+ currentPage,
2724
+ totalPages,
2725
+ totalItems: filteredItems.length,
2726
+ startIndex,
2727
+ endIndex,
2728
+ hasNextPage,
2729
+ hasPrevPage
2730
+ }
2731
+ )
2732
+ ] }),
2733
+ /* @__PURE__ */ jsx21(Box21, { flexDirection: "column", children: pageItems.length === 0 ? /* @__PURE__ */ jsx21(Box21, { padding: 1, children: /* @__PURE__ */ jsx21(Text18, { color: theme.colors.muted, children: "No PRs match the current filters" }) }) : pageItems.map((pr, index) => /* @__PURE__ */ jsx21(
2734
+ PRListItem,
2735
+ {
2736
+ item: pr,
2737
+ isFocus: index === selectedIndex
2738
+ },
2739
+ pr.id
2740
+ )) }),
2741
+ showFilter && /* @__PURE__ */ jsx21(
2742
+ FilterModal,
2743
+ {
2744
+ filter,
2745
+ availableRepos,
2746
+ availableAuthors,
2747
+ availableLabels,
2748
+ onSearchChange: setSearch,
2749
+ onRepoChange: setRepo,
2750
+ onAuthorChange: setAuthor,
2751
+ onLabelChange: setLabel,
2752
+ onSortChange: setSortBy,
2753
+ onSortDirectionToggle: toggleSortDirection,
2754
+ onClear: clearFilters,
2755
+ onClose: () => setShowFilter(false)
2756
+ }
2757
+ ),
2758
+ showSort && /* @__PURE__ */ jsx21(
2759
+ SortModal,
2760
+ {
2761
+ currentSort: filter.sortBy,
2762
+ sortDirection: filter.sortDirection,
2763
+ onSortChange: setSortBy,
2764
+ onSortDirectionToggle: toggleSortDirection,
2765
+ onClose: () => setShowSort(false)
2766
+ }
2767
+ )
2768
+ ] });
2769
+ }
2770
+
2771
+ // src/screens/ReviewRequestsScreen.tsx
2772
+ import { useState as useState11 } from "react";
2773
+ import { Box as Box22, Text as Text19, useInput as useInput8 } from "ink";
2774
+ import { jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
2775
+ function ReviewRequestsScreen({
2776
+ onSelect
2777
+ }) {
2778
+ const theme = useTheme();
2779
+ const { data: prs = [], isLoading, error } = useReviewRequests();
2780
+ const [showFilter, setShowFilter] = useState11(false);
2781
+ const [showSort, setShowSort] = useState11(false);
2782
+ const {
2783
+ filter,
2784
+ filteredItems,
2785
+ setSearch,
2786
+ setRepo,
2787
+ setAuthor,
2788
+ setLabel,
2789
+ setSortBy,
2790
+ toggleSortDirection,
2791
+ clearFilters,
2792
+ hasActiveFilters,
2793
+ availableRepos,
2794
+ availableAuthors,
2795
+ availableLabels
2796
+ } = useFilter(prs);
2797
+ const {
2798
+ currentPage,
2799
+ totalPages,
2800
+ pageItems,
2801
+ hasNextPage,
2802
+ hasPrevPage,
2803
+ nextPage,
2804
+ prevPage,
2805
+ startIndex,
2806
+ endIndex
2807
+ } = usePagination(filteredItems, { pageSize: 18 });
2808
+ const { selectedIndex } = useListNavigation({
2809
+ itemCount: pageItems.length,
2810
+ viewportHeight: pageItems.length,
2811
+ isActive: !showFilter && !showSort
2812
+ });
2813
+ useInput8(
2814
+ (input, key) => {
2815
+ if (key.return && pageItems[selectedIndex]) {
2816
+ onSelect(pageItems[selectedIndex]);
2817
+ } else if (input === "n" && hasNextPage) {
2818
+ nextPage();
2819
+ } else if (input === "p" && hasPrevPage) {
2820
+ prevPage();
2821
+ } else if (input === "/") {
2822
+ setShowFilter(true);
2823
+ } else if (input === "s") {
2824
+ setShowSort(true);
2825
+ }
2826
+ },
2827
+ { isActive: !showFilter && !showSort }
2828
+ );
2829
+ if (isLoading && prs.length === 0) {
2830
+ return /* @__PURE__ */ jsx22(LoadingIndicator, { message: "Loading review requests..." });
2831
+ }
2832
+ if (error) {
2833
+ return /* @__PURE__ */ jsx22(Box22, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs19(Text19, { color: theme.colors.error, children: [
2834
+ "Error: ",
2835
+ String(error)
2836
+ ] }) });
2837
+ }
2838
+ if (prs.length === 0) {
2839
+ return /* @__PURE__ */ jsx22(EmptyState, { message: "No review requests" });
2840
+ }
2841
+ return /* @__PURE__ */ jsxs19(Box22, { flexDirection: "column", flexGrow: 1, children: [
2842
+ /* @__PURE__ */ jsxs19(Box22, { paddingX: 1, justifyContent: "space-between", children: [
2843
+ /* @__PURE__ */ jsxs19(Box22, { gap: 2, children: [
2844
+ /* @__PURE__ */ jsx22(Text19, { color: theme.colors.accent, bold: true, children: "For Review" }),
2845
+ hasActiveFilters && /* @__PURE__ */ jsx22(Text19, { color: theme.colors.warning, children: "(filtered)" }),
2846
+ /* @__PURE__ */ jsx22(Text19, { color: theme.colors.muted, children: "/ filter s sort" })
2847
+ ] }),
2848
+ /* @__PURE__ */ jsx22(
2849
+ PaginationBar,
2850
+ {
2851
+ currentPage,
2852
+ totalPages,
2853
+ totalItems: filteredItems.length,
2854
+ startIndex,
2855
+ endIndex,
2856
+ hasNextPage,
2857
+ hasPrevPage
2858
+ }
2859
+ )
2860
+ ] }),
2861
+ /* @__PURE__ */ jsx22(Box22, { flexDirection: "column", children: pageItems.length === 0 ? /* @__PURE__ */ jsx22(Box22, { padding: 1, children: /* @__PURE__ */ jsx22(Text19, { color: theme.colors.muted, children: "No PRs match the current filters" }) }) : pageItems.map((pr, index) => /* @__PURE__ */ jsx22(
2862
+ PRListItem,
2863
+ {
2864
+ item: pr,
2865
+ isFocus: index === selectedIndex
2866
+ },
2867
+ pr.id
2868
+ )) }),
2869
+ showFilter && /* @__PURE__ */ jsx22(
2870
+ FilterModal,
2871
+ {
2872
+ filter,
2873
+ availableRepos,
2874
+ availableAuthors,
2875
+ availableLabels,
2876
+ onSearchChange: setSearch,
2877
+ onRepoChange: setRepo,
2878
+ onAuthorChange: setAuthor,
2879
+ onLabelChange: setLabel,
2880
+ onSortChange: setSortBy,
2881
+ onSortDirectionToggle: toggleSortDirection,
2882
+ onClear: clearFilters,
2883
+ onClose: () => setShowFilter(false)
2884
+ }
2885
+ ),
2886
+ showSort && /* @__PURE__ */ jsx22(
2887
+ SortModal,
2888
+ {
2889
+ currentSort: filter.sortBy,
2890
+ sortDirection: filter.sortDirection,
2891
+ onSortChange: setSortBy,
2892
+ onSortDirectionToggle: toggleSortDirection,
2893
+ onClose: () => setShowSort(false)
2894
+ }
2895
+ )
2896
+ ] });
2897
+ }
2898
+
2899
+ // src/screens/SettingsScreen.tsx
2900
+ import { useState as useState12 } from "react";
2901
+ import { Box as Box23, Text as Text20, useInput as useInput9 } from "ink";
2902
+ import { TextInput as TextInput3 } from "@inkjs/ui";
2903
+
2904
+ // src/hooks/useConfig.ts
2905
+ import { useQuery as useQuery2, useMutation, useQueryClient } from "@tanstack/react-query";
2906
+ import { Effect as Effect5 } from "effect";
2907
+ function useConfig() {
2908
+ const queryClient2 = useQueryClient();
2909
+ const { data, error, isLoading } = useQuery2({
2910
+ queryKey: ["config"],
2911
+ queryFn: () => Effect5.runPromise(
2912
+ Effect5.gen(function* () {
2913
+ const configService = yield* Config;
2914
+ return yield* configService.load();
2915
+ }).pipe(Effect5.provide(ConfigLive))
2916
+ )
2917
+ });
2918
+ const mutation = useMutation({
2919
+ mutationFn: (newConfig) => Effect5.runPromise(
2920
+ Effect5.gen(function* () {
2921
+ const configService = yield* Config;
2922
+ yield* configService.save(newConfig);
2923
+ }).pipe(Effect5.provide(ConfigLive))
2924
+ ),
2925
+ onSuccess: () => {
2926
+ queryClient2.invalidateQueries({ queryKey: ["config"] });
2927
+ }
2928
+ });
2929
+ const updateConfig = (updates) => {
2930
+ if (!data) return;
2931
+ const newConfig = { ...data, ...updates };
2932
+ queryClient2.setQueryData(["config"], newConfig);
2933
+ mutation.mutate(newConfig);
2934
+ };
2935
+ return {
2936
+ config: data ?? null,
2937
+ error: error ? String(error) : null,
2938
+ loading: isLoading,
2939
+ updateConfig
2940
+ };
2941
+ }
2942
+
2943
+ // src/hooks/useAuth.ts
2944
+ import { useQuery as useQuery3, useMutation as useMutation2, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
2945
+ import { Effect as Effect6 } from "effect";
2946
+ function useAuth() {
2947
+ const queryClient2 = useQueryClient2();
2948
+ const { data, error, isLoading, refetch } = useQuery3({
2949
+ queryKey: ["auth"],
2950
+ queryFn: () => Effect6.runPromise(
2951
+ Effect6.gen(function* () {
2952
+ const auth = yield* Auth;
2953
+ const authenticated = yield* auth.isAuthenticated();
2954
+ if (!authenticated) {
2955
+ return { user: null, isAuthenticated: false };
2956
+ }
2957
+ const currentUser = yield* auth.getUser();
2958
+ return { user: currentUser, isAuthenticated: true };
2959
+ }).pipe(Effect6.provide(AuthLive))
2960
+ ),
2961
+ staleTime: Infinity
2962
+ });
2963
+ const { data: tokenInfoData } = useQuery3({
2964
+ queryKey: ["tokenInfo"],
2965
+ queryFn: () => Effect6.runPromise(
2966
+ Effect6.gen(function* () {
2967
+ const auth = yield* Auth;
2968
+ return yield* auth.getTokenInfo();
2969
+ }).pipe(Effect6.provide(AuthLive))
2970
+ ),
2971
+ staleTime: 0
2972
+ // Always refetch
2973
+ });
2974
+ const { data: availableSourcesData } = useQuery3({
2975
+ queryKey: ["availableSources"],
2976
+ queryFn: () => Effect6.runPromise(
2977
+ Effect6.gen(function* () {
2978
+ const auth = yield* Auth;
2979
+ return yield* auth.getAvailableSources();
2980
+ }).pipe(Effect6.provide(AuthLive))
2981
+ ),
2982
+ staleTime: 0
2983
+ });
2984
+ const saveTokenMutation = useMutation2({
2985
+ mutationFn: async (token) => {
2986
+ await Effect6.runPromise(
2987
+ Effect6.gen(function* () {
2988
+ const auth = yield* Auth;
2989
+ yield* auth.setToken(token);
2990
+ }).pipe(Effect6.provide(AuthLive))
2991
+ );
2992
+ },
2993
+ onSuccess: () => {
2994
+ queryClient2.invalidateQueries({ queryKey: ["auth"] });
2995
+ queryClient2.invalidateQueries({ queryKey: ["tokenInfo"] });
2996
+ queryClient2.invalidateQueries({ queryKey: ["availableSources"] });
2997
+ }
2998
+ });
2999
+ const setPreferredSourceMutation = useMutation2({
3000
+ mutationFn: async (source) => {
3001
+ await Effect6.runPromise(
3002
+ Effect6.gen(function* () {
3003
+ const auth = yield* Auth;
3004
+ yield* auth.setPreferredSource(source);
3005
+ }).pipe(Effect6.provide(AuthLive))
3006
+ );
3007
+ },
3008
+ onSuccess: () => {
3009
+ queryClient2.invalidateQueries({ queryKey: ["auth"] });
3010
+ queryClient2.invalidateQueries({ queryKey: ["tokenInfo"] });
3011
+ }
3012
+ });
3013
+ const clearManualTokenMutation = useMutation2({
3014
+ mutationFn: async () => {
3015
+ await Effect6.runPromise(
3016
+ Effect6.gen(function* () {
3017
+ const auth = yield* Auth;
3018
+ yield* auth.clearManualToken();
3019
+ }).pipe(Effect6.provide(AuthLive))
3020
+ );
3021
+ },
3022
+ onSuccess: () => {
3023
+ queryClient2.invalidateQueries({ queryKey: ["auth"] });
3024
+ queryClient2.invalidateQueries({ queryKey: ["tokenInfo"] });
3025
+ queryClient2.invalidateQueries({ queryKey: ["availableSources"] });
3026
+ }
3027
+ });
3028
+ return {
3029
+ user: data?.user ?? null,
3030
+ isAuthenticated: data?.isAuthenticated ?? false,
3031
+ error: error ? String(error) : null,
3032
+ loading: isLoading,
3033
+ saveToken: saveTokenMutation.mutateAsync,
3034
+ isSavingToken: saveTokenMutation.isPending,
3035
+ tokenInfo: tokenInfoData ?? null,
3036
+ availableSources: availableSourcesData ?? [],
3037
+ setPreferredSource: setPreferredSourceMutation.mutateAsync,
3038
+ clearManualToken: clearManualTokenMutation.mutateAsync,
3039
+ refetch: () => {
3040
+ refetch();
3041
+ queryClient2.invalidateQueries({ queryKey: ["tokenInfo"] });
3042
+ queryClient2.invalidateQueries({ queryKey: ["availableSources"] });
3043
+ }
3044
+ };
3045
+ }
3046
+
3047
+ // src/screens/SettingsScreen.tsx
3048
+ import { jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
3049
+ function SettingRow({
3050
+ label,
3051
+ value,
3052
+ isSelected,
3053
+ isEditing
3054
+ }) {
3055
+ const theme = useTheme();
3056
+ return /* @__PURE__ */ jsxs20(Box23, { gap: 2, paddingX: 2, children: [
3057
+ /* @__PURE__ */ jsx23(Box23, { width: 20, children: /* @__PURE__ */ jsxs20(
3058
+ Text20,
3059
+ {
3060
+ color: isSelected ? theme.colors.accent : theme.colors.muted,
3061
+ bold: isSelected,
3062
+ children: [
3063
+ isSelected ? "> " : " ",
3064
+ label
3065
+ ]
3066
+ }
3067
+ ) }),
3068
+ /* @__PURE__ */ jsx23(
3069
+ Text20,
3070
+ {
3071
+ color: isEditing ? theme.colors.accent : theme.colors.text,
3072
+ inverse: isEditing,
3073
+ children: value
3074
+ }
3075
+ )
3076
+ ] });
3077
+ }
3078
+ function TokenSourceLabel({ source }) {
3079
+ const theme = useTheme();
3080
+ const labels = {
3081
+ manual: { text: "Manual Token", color: theme.colors.warning },
3082
+ env: { text: "Environment Variable", color: theme.colors.success },
3083
+ gh_cli: { text: "GitHub CLI (gh)", color: theme.colors.info },
3084
+ none: { text: "Not configured", color: theme.colors.error }
3085
+ };
3086
+ const { text, color } = labels[source];
3087
+ return /* @__PURE__ */ jsx23(Text20, { color, children: text });
3088
+ }
3089
+ function SettingsScreen() {
3090
+ const theme = useTheme();
3091
+ const { config, loading: configLoading, error: configError } = useConfig();
3092
+ const {
3093
+ tokenInfo,
3094
+ availableSources,
3095
+ setPreferredSource,
3096
+ saveToken,
3097
+ loading: authLoading
3098
+ } = useAuth();
3099
+ const [selectedItem, setSelectedItem] = useState12("token_source");
3100
+ const [isEditingToken, setIsEditingToken] = useState12(false);
3101
+ const [newTokenValue, setNewTokenValue] = useState12("");
3102
+ const [tokenMessage, setTokenMessage] = useState12(null);
3103
+ const settingsItems = ["token_source", "new_token", "theme", "page_size"];
3104
+ useInput9(
3105
+ (input, key) => {
3106
+ if (isEditingToken) {
3107
+ if (key.escape) {
3108
+ setIsEditingToken(false);
3109
+ setNewTokenValue("");
3110
+ } else if (key.return && newTokenValue.trim()) {
3111
+ saveToken(newTokenValue.trim()).then(() => {
3112
+ setTokenMessage("Token saved successfully!");
3113
+ setIsEditingToken(false);
3114
+ setNewTokenValue("");
3115
+ setTimeout(() => setTokenMessage(null), 3e3);
3116
+ }).catch((err) => {
3117
+ setTokenMessage(`Error: ${String(err)}`);
3118
+ });
3119
+ }
3120
+ return;
3121
+ }
3122
+ if (input === "j" || key.downArrow) {
3123
+ const currentIndex = settingsItems.indexOf(selectedItem);
3124
+ const nextIndex = Math.min(currentIndex + 1, settingsItems.length - 1);
3125
+ setSelectedItem(settingsItems[nextIndex]);
3126
+ } else if (input === "k" || key.upArrow) {
3127
+ const currentIndex = settingsItems.indexOf(selectedItem);
3128
+ const prevIndex = Math.max(currentIndex - 1, 0);
3129
+ setSelectedItem(settingsItems[prevIndex]);
3130
+ } else if (key.return) {
3131
+ if (selectedItem === "token_source") {
3132
+ const currentSource = tokenInfo?.source ?? "none";
3133
+ const sourceOrder = ["gh_cli", "env", "manual"];
3134
+ const availableInOrder = sourceOrder.filter((s) => availableSources.includes(s));
3135
+ if (availableInOrder.length > 0) {
3136
+ const currentIndex = availableInOrder.indexOf(currentSource);
3137
+ const nextIndex = (currentIndex + 1) % availableInOrder.length;
3138
+ setPreferredSource(availableInOrder[nextIndex]);
3139
+ }
3140
+ } else if (selectedItem === "new_token") {
3141
+ setIsEditingToken(true);
3142
+ }
3143
+ }
3144
+ },
3145
+ { isActive: true }
3146
+ );
3147
+ if (configLoading || authLoading) {
3148
+ return /* @__PURE__ */ jsx23(LoadingIndicator, { message: "Loading settings..." });
3149
+ }
3150
+ if (configError) {
3151
+ return /* @__PURE__ */ jsx23(Box23, { padding: 1, children: /* @__PURE__ */ jsxs20(Text20, { color: theme.colors.error, children: [
3152
+ "Error: ",
3153
+ configError
3154
+ ] }) });
3155
+ }
3156
+ return /* @__PURE__ */ jsxs20(Box23, { flexDirection: "column", flexGrow: 1, children: [
3157
+ /* @__PURE__ */ jsx23(Box23, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.accent, bold: true, children: "Settings" }) }),
3158
+ /* @__PURE__ */ jsx23(Box23, { paddingX: 1, children: /* @__PURE__ */ jsx23(Divider, {}) }),
3159
+ /* @__PURE__ */ jsx23(Box23, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.secondary, bold: true, children: "Authentication" }) }),
3160
+ /* @__PURE__ */ jsxs20(Box23, { flexDirection: "column", gap: 0, children: [
3161
+ /* @__PURE__ */ jsxs20(Box23, { gap: 2, paddingX: 2, children: [
3162
+ /* @__PURE__ */ jsx23(Box23, { width: 20, children: /* @__PURE__ */ jsxs20(
3163
+ Text20,
3164
+ {
3165
+ color: selectedItem === "token_source" ? theme.colors.accent : theme.colors.muted,
3166
+ bold: selectedItem === "token_source",
3167
+ children: [
3168
+ selectedItem === "token_source" ? "> " : " ",
3169
+ "Token Source"
3170
+ ]
3171
+ }
3172
+ ) }),
3173
+ /* @__PURE__ */ jsx23(TokenSourceLabel, { source: tokenInfo?.source ?? "none" }),
3174
+ availableSources.length > 1 && selectedItem === "token_source" && /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, dimColor: true, children: "(Enter to switch)" })
3175
+ ] }),
3176
+ /* @__PURE__ */ jsxs20(Box23, { gap: 2, paddingX: 2, children: [
3177
+ /* @__PURE__ */ jsx23(Box23, { width: 20, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, children: " Token" }) }),
3178
+ /* @__PURE__ */ jsx23(Text20, { color: theme.colors.text, children: tokenInfo?.maskedToken ?? "(none)" })
3179
+ ] }),
3180
+ /* @__PURE__ */ jsxs20(Box23, { gap: 2, paddingX: 2, children: [
3181
+ /* @__PURE__ */ jsx23(Box23, { width: 20, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, children: " Available" }) }),
3182
+ /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, children: availableSources.length > 0 ? availableSources.join(", ") : "none" })
3183
+ ] }),
3184
+ /* @__PURE__ */ jsxs20(Box23, { gap: 2, paddingX: 2, marginTop: 1, children: [
3185
+ /* @__PURE__ */ jsx23(Box23, { width: 20, children: /* @__PURE__ */ jsxs20(
3186
+ Text20,
3187
+ {
3188
+ color: selectedItem === "new_token" ? theme.colors.accent : theme.colors.muted,
3189
+ bold: selectedItem === "new_token",
3190
+ children: [
3191
+ selectedItem === "new_token" ? "> " : " ",
3192
+ "Set New Token"
3193
+ ]
3194
+ }
3195
+ ) }),
3196
+ isEditingToken ? /* @__PURE__ */ jsx23(Box23, { borderStyle: "single", borderColor: theme.colors.accent, paddingX: 1, width: 40, children: /* @__PURE__ */ jsx23(
3197
+ TextInput3,
3198
+ {
3199
+ defaultValue: newTokenValue,
3200
+ onChange: setNewTokenValue,
3201
+ placeholder: "ghp_xxxx..."
3202
+ }
3203
+ ) }) : /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, dimColor: true, children: "(Enter to add)" })
3204
+ ] }),
3205
+ tokenMessage && /* @__PURE__ */ jsx23(Box23, { paddingX: 4, marginTop: 1, children: /* @__PURE__ */ jsx23(
3206
+ Text20,
3207
+ {
3208
+ color: tokenMessage.startsWith("Error") ? theme.colors.error : theme.colors.success,
3209
+ children: tokenMessage
3210
+ }
3211
+ ) })
3212
+ ] }),
3213
+ /* @__PURE__ */ jsx23(Box23, { paddingX: 1, marginTop: 1, children: /* @__PURE__ */ jsx23(Divider, { title: "Configuration" }) }),
3214
+ /* @__PURE__ */ jsx23(Box23, { flexDirection: "column", paddingX: 1, marginTop: 0, marginBottom: 1, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.secondary, bold: true, children: "Configuration" }) }),
3215
+ /* @__PURE__ */ jsxs20(Box23, { flexDirection: "column", gap: 0, children: [
3216
+ /* @__PURE__ */ jsx23(
3217
+ SettingRow,
3218
+ {
3219
+ label: "Theme",
3220
+ value: config?.theme ?? "tokyo-night",
3221
+ isSelected: selectedItem === "theme"
3222
+ }
3223
+ ),
3224
+ /* @__PURE__ */ jsx23(
3225
+ SettingRow,
3226
+ {
3227
+ label: "Page Size",
3228
+ value: String(config?.pageSize ?? 30),
3229
+ isSelected: selectedItem === "page_size"
3230
+ }
3231
+ ),
3232
+ /* @__PURE__ */ jsx23(
3233
+ SettingRow,
3234
+ {
3235
+ label: "Provider",
3236
+ value: config?.provider ?? "github"
3237
+ }
3238
+ ),
3239
+ /* @__PURE__ */ jsx23(
3240
+ SettingRow,
3241
+ {
3242
+ label: "Default Owner",
3243
+ value: config?.defaultOwner ?? "(not set)"
3244
+ }
3245
+ ),
3246
+ /* @__PURE__ */ jsx23(
3247
+ SettingRow,
3248
+ {
3249
+ label: "Default Repo",
3250
+ value: config?.defaultRepo ?? "(not set)"
3251
+ }
3252
+ )
3253
+ ] }),
3254
+ /* @__PURE__ */ jsxs20(Box23, { paddingX: 1, paddingTop: 2, flexDirection: "column", children: [
3255
+ /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, dimColor: true, children: "Config: ~/.config/lazyreview/config.yaml" }),
3256
+ /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, dimColor: true, children: "Token: ~/.config/lazyreview/.token" })
3257
+ ] }),
3258
+ /* @__PURE__ */ jsx23(Box23, { paddingX: 1, paddingTop: 1, children: /* @__PURE__ */ jsx23(Text20, { color: theme.colors.muted, dimColor: true, children: "j/k: navigate | Enter: select/toggle | Esc: cancel" }) })
3259
+ ] });
3260
+ }
3261
+
3262
+ // src/screens/InvolvedScreen.tsx
3263
+ import { useState as useState13 } from "react";
3264
+ import { Box as Box24, Text as Text21, useInput as useInput10 } from "ink";
3265
+ import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
3266
+ function InvolvedScreen({
3267
+ onSelect
3268
+ }) {
3269
+ const theme = useTheme();
3270
+ const { data: prs = [], isLoading, error } = useInvolvedPRs();
3271
+ const [showFilter, setShowFilter] = useState13(false);
3272
+ const [showSort, setShowSort] = useState13(false);
3273
+ const {
3274
+ filter,
3275
+ filteredItems,
3276
+ setSearch,
3277
+ setRepo,
3278
+ setAuthor,
3279
+ setLabel,
3280
+ setSortBy,
3281
+ toggleSortDirection,
3282
+ clearFilters,
3283
+ hasActiveFilters,
3284
+ availableRepos,
3285
+ availableAuthors,
3286
+ availableLabels
3287
+ } = useFilter(prs);
3288
+ const {
3289
+ currentPage,
3290
+ totalPages,
3291
+ pageItems,
3292
+ hasNextPage,
3293
+ hasPrevPage,
3294
+ nextPage,
3295
+ prevPage,
3296
+ startIndex,
3297
+ endIndex
3298
+ } = usePagination(filteredItems, { pageSize: 18 });
3299
+ const { selectedIndex } = useListNavigation({
3300
+ itemCount: pageItems.length,
3301
+ viewportHeight: pageItems.length,
3302
+ isActive: !showFilter && !showSort
3303
+ });
3304
+ useInput10(
3305
+ (input, key) => {
3306
+ if (key.return && pageItems[selectedIndex]) {
3307
+ onSelect(pageItems[selectedIndex]);
3308
+ } else if (input === "n" && hasNextPage) {
3309
+ nextPage();
3310
+ } else if (input === "p" && hasPrevPage) {
3311
+ prevPage();
3312
+ } else if (input === "/") {
3313
+ setShowFilter(true);
3314
+ } else if (input === "s") {
3315
+ setShowSort(true);
3316
+ }
3317
+ },
3318
+ { isActive: !showFilter && !showSort }
3319
+ );
3320
+ if (isLoading && prs.length === 0) {
3321
+ return /* @__PURE__ */ jsx24(LoadingIndicator, { message: "Loading involved PRs..." });
3322
+ }
3323
+ if (error) {
3324
+ return /* @__PURE__ */ jsx24(Box24, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs21(Text21, { color: theme.colors.error, children: [
3325
+ "Error: ",
3326
+ String(error)
3327
+ ] }) });
3328
+ }
3329
+ if (prs.length === 0) {
3330
+ return /* @__PURE__ */ jsx24(EmptyState, { message: "No pull requests you're involved in" });
3331
+ }
3332
+ return /* @__PURE__ */ jsxs21(Box24, { flexDirection: "column", flexGrow: 1, children: [
3333
+ /* @__PURE__ */ jsxs21(Box24, { paddingX: 1, justifyContent: "space-between", children: [
3334
+ /* @__PURE__ */ jsxs21(Box24, { gap: 2, children: [
3335
+ /* @__PURE__ */ jsx24(Text21, { color: theme.colors.accent, bold: true, children: "Involved Pull Requests" }),
3336
+ hasActiveFilters && /* @__PURE__ */ jsx24(Text21, { color: theme.colors.warning, children: "(filtered)" }),
3337
+ /* @__PURE__ */ jsx24(Text21, { color: theme.colors.muted, children: "/ filter s sort" })
3338
+ ] }),
3339
+ /* @__PURE__ */ jsx24(
3340
+ PaginationBar,
3341
+ {
3342
+ currentPage,
3343
+ totalPages,
3344
+ totalItems: filteredItems.length,
3345
+ startIndex,
3346
+ endIndex,
3347
+ hasNextPage,
3348
+ hasPrevPage
3349
+ }
3350
+ )
3351
+ ] }),
3352
+ /* @__PURE__ */ jsx24(Box24, { flexDirection: "column", children: pageItems.length === 0 ? /* @__PURE__ */ jsx24(Box24, { padding: 1, children: /* @__PURE__ */ jsx24(Text21, { color: theme.colors.muted, children: "No PRs match the current filters" }) }) : pageItems.map((pr, index) => /* @__PURE__ */ jsx24(
3353
+ PRListItem,
3354
+ {
3355
+ item: pr,
3356
+ isFocus: index === selectedIndex
3357
+ },
3358
+ pr.id
3359
+ )) }),
3360
+ showFilter && /* @__PURE__ */ jsx24(
3361
+ FilterModal,
3362
+ {
3363
+ filter,
3364
+ availableRepos,
3365
+ availableAuthors,
3366
+ availableLabels,
3367
+ onSearchChange: setSearch,
3368
+ onRepoChange: setRepo,
3369
+ onAuthorChange: setAuthor,
3370
+ onLabelChange: setLabel,
3371
+ onSortChange: setSortBy,
3372
+ onSortDirectionToggle: toggleSortDirection,
3373
+ onClear: clearFilters,
3374
+ onClose: () => setShowFilter(false)
3375
+ }
3376
+ ),
3377
+ showSort && /* @__PURE__ */ jsx24(
3378
+ SortModal,
3379
+ {
3380
+ currentSort: filter.sortBy,
3381
+ sortDirection: filter.sortDirection,
3382
+ onSortChange: setSortBy,
3383
+ onSortDirectionToggle: toggleSortDirection,
3384
+ onClose: () => setShowSort(false)
3385
+ }
3386
+ )
3387
+ ] });
3388
+ }
3389
+
3390
+ // src/screens/ThisRepoScreen.tsx
3391
+ import { useState as useState14 } from "react";
3392
+ import { Box as Box25, Text as Text22, useInput as useInput11 } from "ink";
3393
+ import { jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
3394
+ function ThisRepoScreen({
3395
+ owner,
3396
+ repo,
3397
+ onSelect
3398
+ }) {
3399
+ const theme = useTheme();
3400
+ const {
3401
+ data: prs = [],
3402
+ isLoading,
3403
+ error
3404
+ } = usePullRequests(owner ?? "", repo ?? "", { state: "open" });
3405
+ const [showFilter, setShowFilter] = useState14(false);
3406
+ const [showSort, setShowSort] = useState14(false);
3407
+ const {
3408
+ filter,
3409
+ filteredItems,
3410
+ setSearch,
3411
+ setRepo: setRepoFilter,
3412
+ setAuthor,
3413
+ setLabel,
3414
+ setSortBy,
3415
+ toggleSortDirection,
3416
+ clearFilters,
3417
+ hasActiveFilters,
3418
+ availableRepos,
3419
+ availableAuthors,
3420
+ availableLabels
3421
+ } = useFilter(prs);
3422
+ const {
3423
+ currentPage,
3424
+ totalPages,
3425
+ pageItems,
3426
+ hasNextPage,
3427
+ hasPrevPage,
3428
+ nextPage,
3429
+ prevPage,
3430
+ startIndex,
3431
+ endIndex
3432
+ } = usePagination(filteredItems, { pageSize: 18 });
3433
+ const { selectedIndex } = useListNavigation({
3434
+ itemCount: pageItems.length,
3435
+ viewportHeight: pageItems.length,
3436
+ isActive: !showFilter && !showSort
3437
+ });
3438
+ useInput11(
3439
+ (input, key) => {
3440
+ if (key.return && pageItems[selectedIndex]) {
3441
+ onSelect(pageItems[selectedIndex]);
3442
+ } else if (input === "n" && hasNextPage) {
3443
+ nextPage();
3444
+ } else if (input === "p" && hasPrevPage) {
3445
+ prevPage();
3446
+ } else if (input === "/") {
3447
+ setShowFilter(true);
3448
+ } else if (input === "s") {
3449
+ setShowSort(true);
3450
+ }
3451
+ },
3452
+ { isActive: !showFilter && !showSort }
3453
+ );
3454
+ if (!owner || !repo) {
3455
+ return /* @__PURE__ */ jsx25(EmptyState, { message: "Not in a git repository or remote not detected" });
3456
+ }
3457
+ if (isLoading && prs.length === 0) {
3458
+ return /* @__PURE__ */ jsx25(LoadingIndicator, { message: `Loading PRs for ${owner}/${repo}...` });
3459
+ }
3460
+ if (error) {
3461
+ return /* @__PURE__ */ jsx25(Box25, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs22(Text22, { color: theme.colors.error, children: [
3462
+ "Error: ",
3463
+ String(error)
3464
+ ] }) });
3465
+ }
3466
+ if (prs.length === 0) {
3467
+ return /* @__PURE__ */ jsx25(EmptyState, { message: `No open PRs in ${owner}/${repo}` });
3468
+ }
3469
+ return /* @__PURE__ */ jsxs22(Box25, { flexDirection: "column", flexGrow: 1, children: [
3470
+ /* @__PURE__ */ jsxs22(Box25, { paddingX: 1, justifyContent: "space-between", children: [
3471
+ /* @__PURE__ */ jsxs22(Box25, { gap: 2, children: [
3472
+ /* @__PURE__ */ jsxs22(Text22, { color: theme.colors.accent, bold: true, children: [
3473
+ owner,
3474
+ "/",
3475
+ repo
3476
+ ] }),
3477
+ hasActiveFilters && /* @__PURE__ */ jsx25(Text22, { color: theme.colors.warning, children: "(filtered)" }),
3478
+ /* @__PURE__ */ jsx25(Text22, { color: theme.colors.muted, children: "/ filter s sort" })
3479
+ ] }),
3480
+ /* @__PURE__ */ jsx25(
3481
+ PaginationBar,
3482
+ {
3483
+ currentPage,
3484
+ totalPages,
3485
+ totalItems: filteredItems.length,
3486
+ startIndex,
3487
+ endIndex,
3488
+ hasNextPage,
3489
+ hasPrevPage
3490
+ }
3491
+ )
3492
+ ] }),
3493
+ /* @__PURE__ */ jsx25(Box25, { flexDirection: "column", children: pageItems.length === 0 ? /* @__PURE__ */ jsx25(Box25, { padding: 1, children: /* @__PURE__ */ jsx25(Text22, { color: theme.colors.muted, children: "No PRs match the current filters" }) }) : pageItems.map((pr, index) => /* @__PURE__ */ jsx25(
3494
+ PRListItem,
3495
+ {
3496
+ item: pr,
3497
+ isFocus: index === selectedIndex
3498
+ },
3499
+ pr.id
3500
+ )) }),
3501
+ showFilter && /* @__PURE__ */ jsx25(
3502
+ FilterModal,
3503
+ {
3504
+ filter,
3505
+ availableRepos,
3506
+ availableAuthors,
3507
+ availableLabels,
3508
+ onSearchChange: setSearch,
3509
+ onRepoChange: setRepoFilter,
3510
+ onAuthorChange: setAuthor,
3511
+ onLabelChange: setLabel,
3512
+ onSortChange: setSortBy,
3513
+ onSortDirectionToggle: toggleSortDirection,
3514
+ onClear: clearFilters,
3515
+ onClose: () => setShowFilter(false)
3516
+ }
3517
+ ),
3518
+ showSort && /* @__PURE__ */ jsx25(
3519
+ SortModal,
3520
+ {
3521
+ currentSort: filter.sortBy,
3522
+ sortDirection: filter.sortDirection,
3523
+ onSortChange: setSortBy,
3524
+ onSortDirectionToggle: toggleSortDirection,
3525
+ onClose: () => setShowSort(false)
3526
+ }
3527
+ )
3528
+ ] });
3529
+ }
3530
+
3531
+ // src/app.tsx
3532
+ import { Match as Match3 } from "effect";
3533
+
3534
+ // src/hooks/useActivePanel.ts
3535
+ import { useInput as useInput12 } from "ink";
3536
+ import { useCallback as useCallback5, useEffect as useEffect9, useState as useState15 } from "react";
3537
+ function useActivePanel({
3538
+ hasSelection
3539
+ }) {
3540
+ const [activePanel, setActivePanel] = useState15("sidebar");
3541
+ useEffect9(() => {
3542
+ if (!hasSelection && activePanel === "detail") {
3543
+ setActivePanel("list");
3544
+ }
3545
+ }, [hasSelection, activePanel]);
3546
+ const handleEscape = useCallback5(() => {
3547
+ if (activePanel === "detail") {
3548
+ setActivePanel("list");
3549
+ } else if (activePanel === "list") {
3550
+ setActivePanel("sidebar");
3551
+ }
3552
+ }, [activePanel]);
3553
+ const handleTab = useCallback5(() => {
3554
+ if (activePanel === "sidebar") {
3555
+ setActivePanel("list");
3556
+ } else if (activePanel === "list" && hasSelection) {
3557
+ setActivePanel("detail");
3558
+ } else if (activePanel === "detail") {
3559
+ setActivePanel("sidebar");
3560
+ }
3561
+ }, [activePanel, hasSelection]);
3562
+ useInput12((_input, key) => {
3563
+ if (key.escape) {
3564
+ handleEscape();
3565
+ } else if (key.tab) {
3566
+ handleTab();
3567
+ }
3568
+ });
3569
+ return { activePanel, setActivePanel };
3570
+ }
3571
+
3572
+ // src/app.tsx
3573
+ import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
3574
+ function AppContent({
3575
+ repoOwner,
3576
+ repoName
3577
+ }) {
3578
+ const { exit } = useApp();
3579
+ const { stdout } = useStdout8();
3580
+ const { user, isAuthenticated, loading, saveToken, error } = useAuth();
3581
+ const [sidebarVisible, setSidebarVisible] = useState16(true);
3582
+ const [currentScreen, setCurrentScreen] = useState16({
3583
+ type: "list"
3584
+ });
3585
+ const [tokenError, setTokenError] = useState16(null);
3586
+ const [showHelp, setShowHelp] = useState16(false);
3587
+ const [showTokenInput, setShowTokenInput] = useState16(false);
3588
+ const { activePanel, setActivePanel } = useActivePanel({
3589
+ hasSelection: currentScreen.type === "detail"
3590
+ });
3591
+ const { isInputActive } = useInputFocus();
3592
+ const { selectedIndex: sidebarIndex } = useListNavigation({
3593
+ itemCount: SIDEBAR_ITEMS.length,
3594
+ viewportHeight: SIDEBAR_ITEMS.length,
3595
+ isActive: activePanel === "sidebar" && !showHelp && !showTokenInput
3596
+ });
3597
+ React16.useEffect(() => {
3598
+ if (!loading && !isAuthenticated && !showTokenInput) {
3599
+ setShowTokenInput(true);
3600
+ } else if (isAuthenticated && showTokenInput) {
3601
+ setShowTokenInput(false);
3602
+ }
3603
+ }, [loading, isAuthenticated, showTokenInput]);
3604
+ const handleTokenSubmit = useCallback6(
3605
+ async (token) => {
3606
+ try {
3607
+ setTokenError(null);
3608
+ await saveToken(token);
3609
+ } catch (err) {
3610
+ setTokenError(String(err));
3611
+ }
3612
+ },
3613
+ [saveToken]
3614
+ );
3615
+ useInput13(
3616
+ (input, key) => {
3617
+ if (showHelp || showTokenInput) {
3618
+ if (key.escape || showHelp && input === "?") {
3619
+ setShowHelp(false);
3620
+ }
3621
+ return;
3622
+ }
3623
+ if (input === "b") {
3624
+ setSidebarVisible((prev) => !prev);
3625
+ } else if (input === "?") {
3626
+ setShowHelp(true);
3627
+ } else if (input === "q") {
3628
+ if (currentScreen.type === "detail") {
3629
+ setCurrentScreen({ type: "list" });
3630
+ } else {
3631
+ exit();
3632
+ }
3633
+ } else if (key.return && activePanel === "sidebar") {
3634
+ setActivePanel("list");
3635
+ }
3636
+ },
3637
+ { isActive: !showTokenInput && !isInputActive }
3638
+ );
3639
+ const handleSelectPR = useCallback6((pr) => {
3640
+ setCurrentScreen({ type: "detail", pr });
3641
+ }, []);
3642
+ const handleBackToList = useCallback6(() => {
3643
+ setCurrentScreen({ type: "list" });
3644
+ }, []);
3645
+ function renderScreen() {
3646
+ if (currentScreen.type === "detail") {
3647
+ const prUrl = currentScreen.pr.html_url;
3648
+ const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull/);
3649
+ const prOwner = match?.[1] ?? repoOwner ?? "";
3650
+ const prRepo = match?.[2] ?? repoName ?? "";
3651
+ return /* @__PURE__ */ jsx26(
3652
+ PRDetailScreen,
3653
+ {
3654
+ pr: currentScreen.pr,
3655
+ owner: prOwner,
3656
+ repo: prRepo,
3657
+ onBack: handleBackToList
3658
+ }
3659
+ );
3660
+ }
3661
+ return Match3.value(sidebarIndex).pipe(
3662
+ Match3.when(0, () => /* @__PURE__ */ jsx26(InvolvedScreen, { onSelect: handleSelectPR })),
3663
+ Match3.when(1, () => /* @__PURE__ */ jsx26(MyPRsScreen, { onSelect: handleSelectPR })),
3664
+ Match3.when(2, () => /* @__PURE__ */ jsx26(ReviewRequestsScreen, { onSelect: handleSelectPR })),
3665
+ Match3.when(3, () => /* @__PURE__ */ jsx26(
3666
+ ThisRepoScreen,
3667
+ {
3668
+ owner: repoOwner,
3669
+ repo: repoName,
3670
+ onSelect: handleSelectPR
3671
+ }
3672
+ )),
3673
+ Match3.when(4, () => /* @__PURE__ */ jsx26(SettingsScreen, {})),
3674
+ Match3.orElse(() => /* @__PURE__ */ jsx26(InvolvedScreen, { onSelect: handleSelectPR }))
3675
+ );
3676
+ }
3677
+ const terminalHeight = stdout?.rows ?? 24;
3678
+ const repoPath = repoOwner && repoName ? `${repoOwner}/${repoName}` : void 0;
3679
+ return /* @__PURE__ */ jsxs23(Box26, { flexDirection: "column", height: terminalHeight, children: [
3680
+ /* @__PURE__ */ jsx26(
3681
+ TopBar,
3682
+ {
3683
+ username: user?.login ?? "anonymous",
3684
+ provider: "github",
3685
+ repoPath
3686
+ }
3687
+ ),
3688
+ /* @__PURE__ */ jsxs23(Box26, { flexDirection: "row", flexGrow: 1, children: [
3689
+ /* @__PURE__ */ jsx26(
3690
+ Sidebar,
3691
+ {
3692
+ selectedIndex: sidebarIndex,
3693
+ visible: sidebarVisible,
3694
+ isActive: activePanel === "sidebar"
3695
+ }
3696
+ ),
3697
+ /* @__PURE__ */ jsx26(MainPanel, { isActive: activePanel === "list", children: renderScreen() })
3698
+ ] }),
3699
+ /* @__PURE__ */ jsx26(StatusBar, { activePanel }),
3700
+ showHelp && /* @__PURE__ */ jsx26(HelpModal, { onClose: () => setShowHelp(false) }),
3701
+ showTokenInput && /* @__PURE__ */ jsx26(
3702
+ TokenInputModal,
3703
+ {
3704
+ onSubmit: handleTokenSubmit,
3705
+ onClose: () => setShowTokenInput(false),
3706
+ error: tokenError ?? error
3707
+ }
3708
+ )
3709
+ ] });
3710
+ }
3711
+ var queryClient = new QueryClient({
3712
+ defaultOptions: {
3713
+ queries: {
3714
+ staleTime: 1e3 * 60,
3715
+ // 1 minute
3716
+ retry: 1
3717
+ }
3718
+ }
3719
+ });
3720
+ function AppWithTheme({
3721
+ repoOwner,
3722
+ repoName
3723
+ }) {
3724
+ const { config } = useConfig();
3725
+ const themeName = config?.theme ?? "tokyo-night";
3726
+ const theme = getThemeByName(themeName);
3727
+ return /* @__PURE__ */ jsx26(ThemeProvider, { theme, children: /* @__PURE__ */ jsx26(AppContent, { repoOwner, repoName }) });
3728
+ }
3729
+ function App({ repoOwner, repoName }) {
3730
+ return /* @__PURE__ */ jsx26(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx26(InputFocusProvider, { children: /* @__PURE__ */ jsx26(AppWithTheme, { repoOwner, repoName }) }) });
3731
+ }
3732
+
3733
+ // src/utils/git.ts
3734
+ import { execFile as execFile2 } from "child_process";
3735
+ import { promisify as promisify2 } from "util";
3736
+ var execFileAsync2 = promisify2(execFile2);
3737
+ async function detectGitRepo() {
3738
+ try {
3739
+ await execFileAsync2("git", ["rev-parse", "--git-dir"]);
3740
+ const { stdout } = await execFileAsync2("git", [
3741
+ "remote",
3742
+ "get-url",
3743
+ "origin"
3744
+ ]);
3745
+ const remoteUrl = stdout.trim();
3746
+ const parsed = parseGitHubUrl(remoteUrl);
3747
+ return {
3748
+ isGitRepo: true,
3749
+ owner: parsed?.owner ?? null,
3750
+ repo: parsed?.repo ?? null,
3751
+ remoteUrl
3752
+ };
3753
+ } catch {
3754
+ return {
3755
+ isGitRepo: false,
3756
+ owner: null,
3757
+ repo: null,
3758
+ remoteUrl: null
3759
+ };
3760
+ }
3761
+ }
3762
+ function parseGitHubUrl(url) {
3763
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?/);
3764
+ if (sshMatch) {
3765
+ return { owner: sshMatch[1], repo: sshMatch[2] };
3766
+ }
3767
+ const httpsMatch = url.match(
3768
+ /https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?/
3769
+ );
3770
+ if (httpsMatch) {
3771
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
3772
+ }
3773
+ return null;
3774
+ }
3775
+
3776
+ // src/cli.tsx
3777
+ import { jsx as jsx27 } from "react/jsx-runtime";
3778
+ var ENTER_ALT_SCREEN = "\x1B[?1049h";
3779
+ var EXIT_ALT_SCREEN = "\x1B[?1049l";
3780
+ var CLEAR_SCREEN = "\x1B[2J";
3781
+ var CURSOR_HOME = "\x1B[H";
3782
+ var HIDE_CURSOR = "\x1B[?25l";
3783
+ var SHOW_CURSOR = "\x1B[?25h";
3784
+ function parseArgs(argv) {
3785
+ const repoArg = argv[2];
3786
+ if (repoArg && repoArg.includes("/")) {
3787
+ const [owner, repo] = repoArg.split("/");
3788
+ if (owner && repo) {
3789
+ return { owner, repo };
3790
+ }
3791
+ }
3792
+ return null;
3793
+ }
3794
+ function cleanup() {
3795
+ process.stdout.write(SHOW_CURSOR + EXIT_ALT_SCREEN);
3796
+ }
3797
+ async function main() {
3798
+ process.stdout.write(
3799
+ ENTER_ALT_SCREEN + CLEAR_SCREEN + CURSOR_HOME + HIDE_CURSOR
3800
+ );
3801
+ process.on("exit", cleanup);
3802
+ process.on("SIGINT", () => {
3803
+ cleanup();
3804
+ process.exit(0);
3805
+ });
3806
+ process.on("SIGTERM", () => {
3807
+ cleanup();
3808
+ process.exit(0);
3809
+ });
3810
+ const argsRepo = parseArgs(process.argv);
3811
+ let repoOwner = null;
3812
+ let repoName = null;
3813
+ if (argsRepo) {
3814
+ repoOwner = argsRepo.owner;
3815
+ repoName = argsRepo.repo;
3816
+ } else {
3817
+ const gitInfo = await detectGitRepo();
3818
+ if (gitInfo.isGitRepo && gitInfo.owner && gitInfo.repo) {
3819
+ repoOwner = gitInfo.owner;
3820
+ repoName = gitInfo.repo;
3821
+ }
3822
+ }
3823
+ render(/* @__PURE__ */ jsx27(App, { repoOwner, repoName }));
3824
+ }
3825
+ main().catch((error) => {
3826
+ cleanup();
3827
+ console.error("Failed to start:", error);
3828
+ process.exit(1);
3829
+ });
3830
+ //# sourceMappingURL=cli.js.map