@kitlangton/ghui 0.2.0 → 0.3.0
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.
- package/README.md +1 -4
- package/package.json +1 -1
- package/src/App.tsx +233 -19
- package/src/appCommands.ts +28 -3
- package/src/config.ts +0 -8
- package/src/domain.ts +13 -3
- package/src/keymap/all.ts +23 -10
- package/src/keymap/changedFilesModal.ts +23 -0
- package/src/keymap/commandPalette.ts +2 -2
- package/src/keymap/commentThreadModal.ts +1 -1
- package/src/keymap/diffView.ts +4 -2
- package/src/keymap/labelModal.ts +2 -2
- package/src/keymap/submitReviewModal.ts +48 -0
- package/src/keymap/themeModal.ts +2 -2
- package/src/pullRequestViews.ts +1 -3
- package/src/services/GitHubService.ts +20 -5
- package/src/services/MockGitHubService.ts +1 -0
- package/src/ui/CommandPalette.tsx +11 -51
- package/src/ui/DetailsPane.tsx +25 -17
- package/src/ui/PullRequestDiffPane.tsx +3 -2
- package/src/ui/PullRequestList.tsx +1 -17
- package/src/ui/colors.ts +7 -0
- package/src/ui/modals.tsx +366 -16
- package/src/ui/primitives.tsx +146 -1
package/src/domain.ts
CHANGED
|
@@ -16,10 +16,10 @@ export const pullRequestQueueLabels = {
|
|
|
16
16
|
mentioned: "mentioned",
|
|
17
17
|
} as const satisfies Record<PullRequestQueueMode, string>
|
|
18
18
|
|
|
19
|
-
export const pullRequestQueueSearchQualifier = (mode: PullRequestQueueMode,
|
|
19
|
+
export const pullRequestQueueSearchQualifier = (mode: PullRequestQueueMode, repository: string | null) => {
|
|
20
20
|
const qualifiers = {
|
|
21
|
-
repository: repository ? `repo:${repository}` :
|
|
22
|
-
authored:
|
|
21
|
+
repository: repository ? `repo:${repository}` : "author:@me",
|
|
22
|
+
authored: "author:@me",
|
|
23
23
|
review: "review-requested:@me",
|
|
24
24
|
assigned: "assignee:@me",
|
|
25
25
|
mentioned: "mentions:@me",
|
|
@@ -44,6 +44,9 @@ export type DiffCommentSide = Schema.Schema.Type<typeof DiffCommentSide>
|
|
|
44
44
|
|
|
45
45
|
export type PullRequestMergeAction = "squash" | "auto" | "admin" | "disable-auto"
|
|
46
46
|
|
|
47
|
+
export const pullRequestReviewEvents = ["COMMENT", "APPROVE", "REQUEST_CHANGES"] as const
|
|
48
|
+
export type PullRequestReviewEvent = (typeof pullRequestReviewEvents)[number]
|
|
49
|
+
|
|
47
50
|
export interface CheckItem {
|
|
48
51
|
readonly name: string
|
|
49
52
|
readonly status: CheckRunStatus
|
|
@@ -67,6 +70,13 @@ export interface CreatePullRequestCommentInput {
|
|
|
67
70
|
readonly body: string
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
export interface SubmitPullRequestReviewInput {
|
|
74
|
+
readonly repository: string
|
|
75
|
+
readonly number: number
|
|
76
|
+
readonly event: PullRequestReviewEvent
|
|
77
|
+
readonly body: string
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
export interface PullRequestReviewComment {
|
|
71
81
|
readonly id: string
|
|
72
82
|
readonly path: string
|
package/src/keymap/all.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { context } from "@ghui/keymap"
|
|
2
|
+
import { changedFilesModalKeymap, type ChangedFilesModalCtx } from "./changedFilesModal.ts"
|
|
2
3
|
import { closeModalKeymap, type CloseModalCtx } from "./closeModal.ts"
|
|
3
4
|
import { commandPaletteKeymap, type CommandPaletteCtx } from "./commandPalette.ts"
|
|
4
5
|
import { commentModalKeymap, type CommentModalCtx } from "./commentModal.ts"
|
|
@@ -10,6 +11,7 @@ import { labelModalKeymap, type LabelModalCtx } from "./labelModal.ts"
|
|
|
10
11
|
import { listNavKeymap, type ListNavCtx } from "./listNav.ts"
|
|
11
12
|
import { mergeModalKeymap, type MergeModalCtx } from "./mergeModal.ts"
|
|
12
13
|
import { openRepositoryModalKeymap, type OpenRepositoryModalCtx } from "./openRepositoryModal.ts"
|
|
14
|
+
import { submitReviewModalKeymap, type SubmitReviewModalCtx } from "./submitReviewModal.ts"
|
|
13
15
|
import { themeModalKeymap, type ThemeModalCtx } from "./themeModal.ts"
|
|
14
16
|
|
|
15
17
|
export interface AppCtx {
|
|
@@ -17,6 +19,8 @@ export interface AppCtx {
|
|
|
17
19
|
readonly closeModalActive: boolean
|
|
18
20
|
readonly mergeModalActive: boolean
|
|
19
21
|
readonly commentThreadModalActive: boolean
|
|
22
|
+
readonly changedFilesModalActive: boolean
|
|
23
|
+
readonly submitReviewModalActive: boolean
|
|
20
24
|
readonly labelModalActive: boolean
|
|
21
25
|
readonly themeModalActive: boolean
|
|
22
26
|
readonly openRepositoryModalActive: boolean
|
|
@@ -34,6 +38,8 @@ export interface AppCtx {
|
|
|
34
38
|
readonly closeModal: CloseModalCtx
|
|
35
39
|
readonly mergeModal: MergeModalCtx
|
|
36
40
|
readonly commentThreadModal: CommentThreadModalCtx
|
|
41
|
+
readonly changedFilesModal: ChangedFilesModalCtx
|
|
42
|
+
readonly submitReviewModal: SubmitReviewModalCtx
|
|
37
43
|
readonly labelModal: LabelModalCtx
|
|
38
44
|
readonly themeModal: ThemeModalCtx
|
|
39
45
|
readonly openRepositoryModal: OpenRepositoryModalCtx
|
|
@@ -51,15 +57,20 @@ export interface AppCtx {
|
|
|
51
57
|
|
|
52
58
|
const App = context<AppCtx>()
|
|
53
59
|
|
|
60
|
+
const modalActive = (a: AppCtx): boolean =>
|
|
61
|
+
a.closeModalActive
|
|
62
|
+
|| a.mergeModalActive
|
|
63
|
+
|| a.commentThreadModalActive
|
|
64
|
+
|| a.changedFilesModalActive
|
|
65
|
+
|| a.submitReviewModalActive
|
|
66
|
+
|| a.labelModalActive
|
|
67
|
+
|| a.themeModalActive
|
|
68
|
+
|| a.openRepositoryModalActive
|
|
69
|
+
|| a.commentModalActive
|
|
70
|
+
|| a.commandPaletteActive
|
|
71
|
+
|
|
54
72
|
const inListMode = (a: AppCtx): boolean =>
|
|
55
|
-
!a
|
|
56
|
-
&& !a.mergeModalActive
|
|
57
|
-
&& !a.commentThreadModalActive
|
|
58
|
-
&& !a.labelModalActive
|
|
59
|
-
&& !a.themeModalActive
|
|
60
|
-
&& !a.openRepositoryModalActive
|
|
61
|
-
&& !a.commentModalActive
|
|
62
|
-
&& !a.commandPaletteActive
|
|
73
|
+
!modalActive(a)
|
|
63
74
|
&& !a.filterMode
|
|
64
75
|
&& !a.diffFullView
|
|
65
76
|
&& !a.detailFullView
|
|
@@ -87,6 +98,8 @@ export const appKeymap = App(
|
|
|
87
98
|
closeModalKeymap.scope((a) => a.closeModalActive && a.closeModal),
|
|
88
99
|
mergeModalKeymap.scope((a) => a.mergeModalActive && a.mergeModal),
|
|
89
100
|
commentThreadModalKeymap.scope((a) => a.commentThreadModalActive && a.commentThreadModal),
|
|
101
|
+
changedFilesModalKeymap.scope((a) => a.changedFilesModalActive && a.changedFilesModal),
|
|
102
|
+
submitReviewModalKeymap.scope((a) => a.submitReviewModalActive && a.submitReviewModal),
|
|
90
103
|
labelModalKeymap.scope((a) => a.labelModalActive && a.labelModal),
|
|
91
104
|
themeModalKeymap.scope((a) => a.themeModalActive && a.themeModal),
|
|
92
105
|
openRepositoryModalKeymap.scope((a) => a.openRepositoryModalActive && a.openRepositoryModal),
|
|
@@ -95,8 +108,8 @@ export const appKeymap = App(
|
|
|
95
108
|
filterModeKeymap.scope((a) => a.filterMode && a.filterModeCtx),
|
|
96
109
|
|
|
97
110
|
// Full-view layers (only when no modal is on top)
|
|
98
|
-
diffViewKeymap.scope((a) => a.diffFullView && !a
|
|
99
|
-
detailViewKeymap.scope((a) => a.detailFullView && !a
|
|
111
|
+
diffViewKeymap.scope((a) => a.diffFullView && !modalActive(a) && a.diff),
|
|
112
|
+
detailViewKeymap.scope((a) => a.detailFullView && !modalActive(a) && a.detail),
|
|
100
113
|
|
|
101
114
|
// PR list nav
|
|
102
115
|
listNavKeymap.scope((a) => inListMode(a) && a.listNav),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { context } from "@ghui/keymap"
|
|
2
|
+
|
|
3
|
+
export interface ChangedFilesModalCtx {
|
|
4
|
+
readonly hasResults: boolean
|
|
5
|
+
readonly closeModal: () => void
|
|
6
|
+
readonly selectFile: () => void
|
|
7
|
+
readonly moveSelection: (delta: -1 | 1) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ChangedFiles = context<ChangedFilesModalCtx>()
|
|
11
|
+
|
|
12
|
+
export const changedFilesModalKeymap = ChangedFiles(
|
|
13
|
+
{ id: "changed-files.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
|
|
14
|
+
{
|
|
15
|
+
id: "changed-files.select",
|
|
16
|
+
title: "Jump to file",
|
|
17
|
+
keys: ["return"],
|
|
18
|
+
enabled: (s) => s.hasResults ? true : "No matching files.",
|
|
19
|
+
run: (s) => s.selectFile(),
|
|
20
|
+
},
|
|
21
|
+
{ id: "changed-files.up", title: "Up", keys: ["k", "up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
|
|
22
|
+
{ id: "changed-files.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
|
|
23
|
+
)
|
|
@@ -11,6 +11,6 @@ const Palette = context<CommandPaletteCtx>()
|
|
|
11
11
|
export const commandPaletteKeymap = Palette(
|
|
12
12
|
{ id: "palette.close", title: "Close palette", keys: ["escape", "ctrl+c"], run: (s) => s.closeModal() },
|
|
13
13
|
{ id: "palette.run", title: "Run command", keys: ["return"], run: (s) => s.runSelected() },
|
|
14
|
-
{ id: "palette.up", title: "Up", keys: ["up"], run: (s) => s.moveSelection(-1) },
|
|
15
|
-
{ id: "palette.down", title: "Down", keys: ["down"], run: (s) => s.moveSelection(1) },
|
|
14
|
+
{ id: "palette.up", title: "Up", keys: ["up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
|
|
15
|
+
{ id: "palette.down", title: "Down", keys: ["down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
|
|
16
16
|
)
|
|
@@ -11,7 +11,7 @@ const Thread = context<CommentThreadModalCtx>()
|
|
|
11
11
|
|
|
12
12
|
export const commentThreadModalKeymap = Thread(
|
|
13
13
|
{ id: "comment-thread.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
|
|
14
|
-
{ id: "comment-thread.reply", title: "Reply", keys: ["return"
|
|
14
|
+
{ id: "comment-thread.reply", title: "Reply", keys: ["return"], run: (s) => s.openInlineComment() },
|
|
15
15
|
{ id: "comment-thread.up", title: "Up", keys: ["k", "up"], run: (s) => s.scrollBy(-1) },
|
|
16
16
|
{ id: "comment-thread.down", title: "Down", keys: ["j", "down"], run: (s) => s.scrollBy(1) },
|
|
17
17
|
{ id: "comment-thread.half-up", title: "Half page up", keys: ["pageup", "ctrl+u"], run: (s) => s.scrollBy(-s.halfPage) },
|
package/src/keymap/diffView.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface DiffViewCtx {
|
|
|
8
8
|
readonly halfPage: number
|
|
9
9
|
readonly handleEscape: () => void // closes diff, or clears comment range if active
|
|
10
10
|
readonly openSelectedComment: () => void
|
|
11
|
-
readonly addComment: () => void
|
|
12
11
|
readonly toggleRange: () => void
|
|
13
12
|
readonly toggleView: () => void
|
|
14
13
|
readonly toggleWrap: () => void
|
|
@@ -19,6 +18,8 @@ export interface DiffViewCtx {
|
|
|
19
18
|
readonly moveAnchorToBoundary: (boundary: "first" | "last") => void
|
|
20
19
|
readonly alignAnchor: (align: DiffAlign) => void
|
|
21
20
|
readonly selectSide: (side: DiffSide) => void
|
|
21
|
+
readonly openChangedFiles: () => void
|
|
22
|
+
readonly openSubmitReview: () => void
|
|
22
23
|
readonly nextFile: () => void
|
|
23
24
|
readonly previousFile: () => void
|
|
24
25
|
readonly openInBrowser: () => void
|
|
@@ -29,7 +30,6 @@ const Diff = context<DiffViewCtx>()
|
|
|
29
30
|
export const diffViewKeymap = Diff(
|
|
30
31
|
{ id: "diff.escape", title: "Close diff / clear range", keys: ["escape"], run: (s) => s.handleEscape() },
|
|
31
32
|
{ id: "diff.open-comment", title: "Open / add comment", keys: ["return"], run: (s) => s.openSelectedComment() },
|
|
32
|
-
{ id: "diff.add-comment", title: "Add comment on line", keys: ["a"], run: (s) => s.addComment() },
|
|
33
33
|
{ id: "diff.toggle-range", title: "Toggle comment range", keys: ["v"], run: (s) => s.toggleRange() },
|
|
34
34
|
{ id: "diff.toggle-view", title: "Toggle split/unified", keys: ["shift+v"], run: (s) => s.toggleView() },
|
|
35
35
|
{ id: "diff.toggle-wrap", title: "Toggle wrap", keys: ["w"], run: (s) => s.toggleWrap() },
|
|
@@ -77,8 +77,10 @@ export const diffViewKeymap = Diff(
|
|
|
77
77
|
{ id: "diff.side-right", title: "New side", keys: ["right", "l"], run: (s) => s.selectSide("RIGHT") },
|
|
78
78
|
|
|
79
79
|
// File nav
|
|
80
|
+
{ id: "diff.changed-files", title: "Changed files", keys: ["f"], run: (s) => s.openChangedFiles() },
|
|
80
81
|
{ id: "diff.next-file", title: "Next file", keys: ["]"], run: (s) => s.nextFile() },
|
|
81
82
|
{ id: "diff.previous-file", title: "Previous file", keys: ["["], run: (s) => s.previousFile() },
|
|
83
|
+
{ id: "diff.submit-review", title: "Submit review", keys: ["shift+r"], run: (s) => s.openSubmitReview() },
|
|
82
84
|
|
|
83
85
|
// Boundary jumps + align
|
|
84
86
|
{ id: "diff.first", title: "First comment", keys: ["g g"], run: (s) => s.moveAnchorToBoundary("first") },
|
package/src/keymap/labelModal.ts
CHANGED
|
@@ -11,6 +11,6 @@ const Label = context<LabelModalCtx>()
|
|
|
11
11
|
export const labelModalKeymap = Label(
|
|
12
12
|
{ id: "label-modal.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
|
|
13
13
|
{ id: "label-modal.toggle", title: "Toggle label", keys: ["return"], run: (s) => s.toggleSelected() },
|
|
14
|
-
{ id: "label-modal.up", title: "Up", keys: ["k", "up"], run: (s) => s.moveSelection(-1) },
|
|
15
|
-
{ id: "label-modal.down", title: "Down", keys: ["j", "down"], run: (s) => s.moveSelection(1) },
|
|
14
|
+
{ id: "label-modal.up", title: "Up", keys: ["k", "up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
|
|
15
|
+
{ id: "label-modal.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
|
|
16
16
|
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { context } from "@ghui/keymap"
|
|
2
|
+
|
|
3
|
+
export interface SubmitReviewModalCtx {
|
|
4
|
+
readonly closeModal: () => void
|
|
5
|
+
readonly submit: () => void
|
|
6
|
+
readonly insertNewline: () => void
|
|
7
|
+
readonly moveActionSelection: (delta: -1 | 1) => void
|
|
8
|
+
readonly moveLeft: () => void
|
|
9
|
+
readonly moveRight: () => void
|
|
10
|
+
readonly moveUp: () => void
|
|
11
|
+
readonly moveDown: () => void
|
|
12
|
+
readonly moveLineStart: () => void
|
|
13
|
+
readonly moveLineEnd: () => void
|
|
14
|
+
readonly moveWordBackward: () => void
|
|
15
|
+
readonly moveWordForward: () => void
|
|
16
|
+
readonly backspace: () => void
|
|
17
|
+
readonly deleteForward: () => void
|
|
18
|
+
readonly deleteWordBackward: () => void
|
|
19
|
+
readonly deleteWordForward: () => void
|
|
20
|
+
readonly deleteToLineStart: () => void
|
|
21
|
+
readonly deleteToLineEnd: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SubmitReview = context<SubmitReviewModalCtx>()
|
|
25
|
+
|
|
26
|
+
export const submitReviewModalKeymap = SubmitReview(
|
|
27
|
+
{ id: "submit-review.escape", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
|
|
28
|
+
{ id: "submit-review.submit", title: "Submit", keys: ["ctrl+s", "return"], run: (s) => s.submit() },
|
|
29
|
+
{ id: "submit-review.newline", title: "Insert newline", keys: ["shift+return"], run: (s) => s.insertNewline() },
|
|
30
|
+
{ id: "submit-review.next-action", title: "Next action", keys: ["tab"], run: (s) => s.moveActionSelection(1) },
|
|
31
|
+
{ id: "submit-review.previous-action", title: "Previous action", keys: ["shift+tab"], run: (s) => s.moveActionSelection(-1) },
|
|
32
|
+
|
|
33
|
+
{ id: "submit-review.move-left", title: "Cursor left", keys: ["left", "ctrl+b"], run: (s) => s.moveLeft() },
|
|
34
|
+
{ id: "submit-review.move-right", title: "Cursor right", keys: ["right", "ctrl+f"], run: (s) => s.moveRight() },
|
|
35
|
+
{ id: "submit-review.move-up", title: "Cursor up", keys: ["up"], run: (s) => s.moveUp() },
|
|
36
|
+
{ id: "submit-review.move-down", title: "Cursor down", keys: ["down"], run: (s) => s.moveDown() },
|
|
37
|
+
{ id: "submit-review.line-start", title: "Line start", keys: ["home", "ctrl+a"], run: (s) => s.moveLineStart() },
|
|
38
|
+
{ id: "submit-review.line-end", title: "Line end", keys: ["end", "ctrl+e"], run: (s) => s.moveLineEnd() },
|
|
39
|
+
{ id: "submit-review.word-back", title: "Word backward", keys: ["meta+b", "meta+left"], run: (s) => s.moveWordBackward() },
|
|
40
|
+
{ id: "submit-review.word-forward", title: "Word forward", keys: ["meta+f", "meta+right"], run: (s) => s.moveWordForward() },
|
|
41
|
+
|
|
42
|
+
{ id: "submit-review.backspace", title: "Backspace", keys: ["backspace"], run: (s) => s.backspace() },
|
|
43
|
+
{ id: "submit-review.delete", title: "Delete", keys: ["delete", "ctrl+d"], run: (s) => s.deleteForward() },
|
|
44
|
+
{ id: "submit-review.delete-word-back", title: "Delete word backward", keys: ["ctrl+w", "meta+backspace"], run: (s) => s.deleteWordBackward() },
|
|
45
|
+
{ id: "submit-review.delete-word-forward", title: "Delete word forward", keys: ["meta+delete"], run: (s) => s.deleteWordForward() },
|
|
46
|
+
{ id: "submit-review.delete-to-line-start", title: "Delete to line start", keys: ["ctrl+u"], run: (s) => s.deleteToLineStart() },
|
|
47
|
+
{ id: "submit-review.delete-to-line-end", title: "Delete to line end", keys: ["ctrl+k"], run: (s) => s.deleteToLineEnd() },
|
|
48
|
+
)
|
package/src/keymap/themeModal.ts
CHANGED
|
@@ -30,8 +30,8 @@ export const themeModalKeymap = Theme(
|
|
|
30
30
|
enabled: (s) => s.filterMode && !s.hasFilteredResults ? "No matching themes." : true,
|
|
31
31
|
run: (s) => s.confirmSelection(),
|
|
32
32
|
},
|
|
33
|
-
{ id: "theme-modal.up-arrow", title: "Up", keys: ["up"], run: (s) => s.moveSelection(-1) },
|
|
34
|
-
{ id: "theme-modal.down-arrow", title: "Down", keys: ["down"], run: (s) => s.moveSelection(1) },
|
|
33
|
+
{ id: "theme-modal.up-arrow", title: "Up", keys: ["up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
|
|
34
|
+
{ id: "theme-modal.down-arrow", title: "Down", keys: ["down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
|
|
35
35
|
{
|
|
36
36
|
id: "theme-modal.up-letter",
|
|
37
37
|
title: "Up",
|
package/src/pullRequestViews.ts
CHANGED
|
@@ -4,9 +4,7 @@ export type PullRequestView =
|
|
|
4
4
|
| { readonly _tag: "Repository"; readonly repository: string }
|
|
5
5
|
| { readonly _tag: "Queue"; readonly mode: PullRequestUserQueueMode; readonly repository: string | null }
|
|
6
6
|
|
|
7
|
-
export const initialPullRequestView = (
|
|
8
|
-
? { _tag: "Repository", repository }
|
|
9
|
-
: { _tag: "Queue", mode: "authored", repository: null }
|
|
7
|
+
export const initialPullRequestView = (): PullRequestView => ({ _tag: "Queue", mode: "authored", repository: null })
|
|
10
8
|
|
|
11
9
|
export const viewMode = (view: PullRequestView): PullRequestQueueMode => view._tag === "Repository" ? "repository" : view.mode
|
|
12
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
2
2
|
import { config } from "../config.js"
|
|
3
|
-
import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type ListPullRequestPageInput, type Mergeable, type PullRequestConversationItem, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestPage, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus } from "../domain.js"
|
|
3
|
+
import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type ListPullRequestPageInput, type Mergeable, type PullRequestConversationItem, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestPage, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus, type SubmitPullRequestReviewInput } from "../domain.js"
|
|
4
4
|
import { getMergeActionDefinition } from "../mergeActions.js"
|
|
5
5
|
import { CommandError, CommandRunner, type JsonParseError } from "./CommandRunner.js"
|
|
6
6
|
|
|
@@ -413,9 +413,9 @@ const parsePullRequest = (item: RawPullRequestNode): PullRequestItem => {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
const searchQuery = (mode: PullRequestQueueMode,
|
|
416
|
+
const searchQuery = (mode: PullRequestQueueMode, repository: string | null) => {
|
|
417
417
|
const sort = mode === "repository" ? "sort:updated-desc" : "sort:created-desc"
|
|
418
|
-
return `${pullRequestQueueSearchQualifier(mode,
|
|
418
|
+
return `${pullRequestQueueSearchQualifier(mode, repository)} is:pr is:open ${sort}`
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
const pullRequestPage = <Item>(connection: PullRequestConnection<Item>, parse: (node: Item) => PullRequestItem): PullRequestPage => ({
|
|
@@ -506,7 +506,7 @@ const fallbackCreatedComment = (input: CreatePullRequestCommentInput): PullReque
|
|
|
506
506
|
path: input.path,
|
|
507
507
|
line: input.line,
|
|
508
508
|
side: input.side,
|
|
509
|
-
author:
|
|
509
|
+
author: "you",
|
|
510
510
|
body: input.body,
|
|
511
511
|
createdAt: new Date(),
|
|
512
512
|
url: null,
|
|
@@ -522,6 +522,12 @@ const MERGEABLE_BY_RAW: Record<string, Mergeable> = {
|
|
|
522
522
|
const normalizeMergeable = (value: string): Mergeable =>
|
|
523
523
|
MERGEABLE_BY_RAW[value] ?? "unknown"
|
|
524
524
|
|
|
525
|
+
const REVIEW_EVENT_CLI_FLAG = {
|
|
526
|
+
COMMENT: "--comment",
|
|
527
|
+
APPROVE: "--approve",
|
|
528
|
+
REQUEST_CHANGES: "--request-changes",
|
|
529
|
+
} as const satisfies Record<SubmitPullRequestReviewInput["event"], string>
|
|
530
|
+
|
|
525
531
|
export class GitHubService extends Context.Service<GitHubService, {
|
|
526
532
|
readonly listOpenPullRequests: (mode: PullRequestQueueMode, repository: string | null) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
527
533
|
readonly listOpenPullRequestPage: (input: ListPullRequestPageInput) => Effect.Effect<PullRequestPage, GitHubError>
|
|
@@ -535,6 +541,7 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
535
541
|
readonly mergePullRequest: (repository: string, number: number, action: PullRequestMergeAction) => Effect.Effect<void, CommandError>
|
|
536
542
|
readonly closePullRequest: (repository: string, number: number) => Effect.Effect<void, CommandError>
|
|
537
543
|
readonly createPullRequestComment: (input: CreatePullRequestCommentInput) => Effect.Effect<PullRequestReviewComment, GitHubError>
|
|
544
|
+
readonly submitPullRequestReview: (input: SubmitPullRequestReviewInput) => Effect.Effect<void, CommandError>
|
|
538
545
|
readonly toggleDraftStatus: (repository: string, number: number, isDraft: boolean) => Effect.Effect<void, CommandError>
|
|
539
546
|
readonly listRepoLabels: (repository: string) => Effect.Effect<readonly { readonly name: string; readonly color: string | null }[], GitHubError>
|
|
540
547
|
readonly addPullRequestLabel: (repository: string, number: number, label: string) => Effect.Effect<void, CommandError>
|
|
@@ -557,7 +564,7 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
557
564
|
const response: SearchResponse<Item["Type"]> = yield* command.runSchema(responseSchema, "gh", [
|
|
558
565
|
"api", "graphql",
|
|
559
566
|
"-f", `query=${query}`,
|
|
560
|
-
"-F", `searchQuery=${searchQuery(input.mode,
|
|
567
|
+
"-F", `searchQuery=${searchQuery(input.mode, input.repository)}`,
|
|
561
568
|
"-F", `first=${input.pageSize}`,
|
|
562
569
|
...(input.cursor ? ["-F", `after=${input.cursor}`] : []),
|
|
563
570
|
])
|
|
@@ -702,6 +709,13 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
702
709
|
return parsePullRequestComment(response) ?? fallbackCreatedComment(input)
|
|
703
710
|
})
|
|
704
711
|
|
|
712
|
+
const submitPullRequestReview = (input: SubmitPullRequestReviewInput) =>
|
|
713
|
+
ghVoid("submitPullRequestReview", [
|
|
714
|
+
"pr", "review", String(input.number), "--repo", input.repository,
|
|
715
|
+
REVIEW_EVENT_CLI_FLAG[input.event],
|
|
716
|
+
"--body", input.body,
|
|
717
|
+
])
|
|
718
|
+
|
|
705
719
|
const toggleDraftStatus = (repository: string, number: number, isDraft: boolean) =>
|
|
706
720
|
ghVoid("toggleDraftStatus", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
|
|
707
721
|
|
|
@@ -729,6 +743,7 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
729
743
|
mergePullRequest,
|
|
730
744
|
closePullRequest,
|
|
731
745
|
createPullRequestComment,
|
|
746
|
+
submitPullRequestReview,
|
|
732
747
|
toggleDraftStatus,
|
|
733
748
|
listRepoLabels,
|
|
734
749
|
addPullRequestLabel,
|
|
@@ -171,6 +171,7 @@ export const MockGitHubService = {
|
|
|
171
171
|
createdAt: new Date(),
|
|
172
172
|
url: null,
|
|
173
173
|
} satisfies PullRequestReviewComment),
|
|
174
|
+
submitPullRequestReview: () => Effect.void,
|
|
174
175
|
toggleDraftStatus: () => Effect.void,
|
|
175
176
|
listRepoLabels: () => Effect.succeed([]),
|
|
176
177
|
addPullRequestLabel: () => Effect.void,
|
|
@@ -4,7 +4,7 @@ import type { AppCommand } from "../commands.js"
|
|
|
4
4
|
import { clampCommandIndex } from "../commands.js"
|
|
5
5
|
import { colors } from "./colors.js"
|
|
6
6
|
import { scrollTopForVisibleLine } from "./diff.js"
|
|
7
|
-
import { centerCell,
|
|
7
|
+
import { centerCell, Filler, fitCell, HintRow, PlainLine, searchModalDims, SearchModalFrame, TextLine, trimCell } from "./primitives.js"
|
|
8
8
|
|
|
9
9
|
const scopeLabels = {
|
|
10
10
|
Global: "App",
|
|
@@ -79,8 +79,7 @@ export const CommandPalette = ({
|
|
|
79
79
|
onSelectCommandIndex: (index: number) => void
|
|
80
80
|
onRunCommand: (command: AppCommand) => void
|
|
81
81
|
}) => {
|
|
82
|
-
const {
|
|
83
|
-
const listHeight = Math.max(1, modalHeight - 6)
|
|
82
|
+
const { bodyHeight: listHeight, rowWidth } = searchModalDims(modalWidth, modalHeight)
|
|
84
83
|
const clampedIndex = clampCommandIndex(selectedIndex, commands)
|
|
85
84
|
const [scrollTop, setScrollTop] = useState(0)
|
|
86
85
|
const rows = useMemo(() => buildCommandPaletteRows(commands), [commands])
|
|
@@ -88,18 +87,6 @@ export const CommandPalette = ({
|
|
|
88
87
|
const visibleRows = rows.slice(scrollTop, scrollTop + listHeight)
|
|
89
88
|
const bottomPaddingRows = Math.max(0, listHeight - visibleRows.length)
|
|
90
89
|
const countText = commands.length === 1 ? "1 command" : `${commands.length} commands`
|
|
91
|
-
const placeholder = "Search"
|
|
92
|
-
const titleText = "Commands"
|
|
93
|
-
const headerGap = 1
|
|
94
|
-
const headerDivider = "│"
|
|
95
|
-
const searchGap = 1
|
|
96
|
-
const dividerColumn = 1 + titleText.length + headerGap
|
|
97
|
-
const searchStart = titleText.length + headerGap + headerDivider.length + searchGap
|
|
98
|
-
const countGap = countText.length > 0 ? 2 : 0
|
|
99
|
-
const searchWidth = Math.max(1, contentWidth - searchStart - countGap - countText.length)
|
|
100
|
-
const queryText = trimCell(query, Math.max(0, searchWidth - 1))
|
|
101
|
-
const queryPadding = Math.max(0, searchWidth - queryText.length - 1)
|
|
102
|
-
const caretFg = colors.background === "transparent" ? colors.text : colors.background
|
|
103
90
|
const emptyTopRows = Math.max(0, Math.floor((listHeight - 1) / 2))
|
|
104
91
|
const emptyBottomRows = Math.max(0, listHeight - emptyTopRows - 1)
|
|
105
92
|
const runCommandOnMouseDown = (command: AppCommand) => (event: MouseEvent) => {
|
|
@@ -171,46 +158,19 @@ export const CommandPalette = ({
|
|
|
171
158
|
setScrollTop((current) => commandPaletteScrollTop({ current, rowsLength: rows.length, listHeight, selectedRowIndex }))
|
|
172
159
|
}, [listHeight, rows.length, selectedRowIndex])
|
|
173
160
|
return (
|
|
174
|
-
<
|
|
161
|
+
<SearchModalFrame
|
|
175
162
|
left={offsetLeft}
|
|
176
163
|
top={offsetTop}
|
|
177
164
|
width={modalWidth}
|
|
178
165
|
height={modalHeight}
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
title="Commands"
|
|
167
|
+
query={query}
|
|
168
|
+
placeholder="Search"
|
|
169
|
+
countText={countText}
|
|
170
|
+
onBodyMouseScroll={handleMouseScroll}
|
|
171
|
+
footer={<HintRow items={[{ key: "↑↓", label: "select" }, { key: "enter", label: "run" }, { key: "esc", label: "close" }]} />}
|
|
181
172
|
>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{titleText}</span>
|
|
185
|
-
<span>{" ".repeat(headerGap)}</span>
|
|
186
|
-
<span fg={colors.separator}>{headerDivider}</span>
|
|
187
|
-
<span>{" ".repeat(searchGap)}</span>
|
|
188
|
-
{query.length > 0 ? (
|
|
189
|
-
<>
|
|
190
|
-
<span fg={colors.text}>{queryText}</span>
|
|
191
|
-
<span bg={colors.muted} fg={caretFg}> </span>
|
|
192
|
-
{queryPadding > 0 ? <span>{" ".repeat(queryPadding)}</span> : null}
|
|
193
|
-
</>
|
|
194
|
-
) : (
|
|
195
|
-
<>
|
|
196
|
-
<span bg={colors.muted} fg={caretFg}>{placeholder[0]}</span>
|
|
197
|
-
<span fg={colors.muted}>{fitCell(placeholder.slice(1), Math.max(0, searchWidth - 1))}</span>
|
|
198
|
-
</>
|
|
199
|
-
)}
|
|
200
|
-
{countText.length > 0 && searchWidth > placeholder.length ? (
|
|
201
|
-
<>
|
|
202
|
-
<span>{" ".repeat(countGap)}</span>
|
|
203
|
-
<span fg={colors.muted}>{countText}</span>
|
|
204
|
-
</>
|
|
205
|
-
) : null}
|
|
206
|
-
</TextLine>
|
|
207
|
-
</PaddedRow>
|
|
208
|
-
<Divider width={innerWidth} junctionAt={dividerColumn} junctionChar="┴" />
|
|
209
|
-
<box height={listHeight} flexDirection="column" onMouseScroll={handleMouseScroll}>{content}</box>
|
|
210
|
-
<Divider width={innerWidth} />
|
|
211
|
-
<PaddedRow>
|
|
212
|
-
<HintRow items={[{ key: "↑↓", label: "select" }, { key: "enter", label: "run" }, { key: "ctrl-u", label: "clear" }, { key: "ctrl-w", label: "word" }, { key: "esc", label: "close" }]} />
|
|
213
|
-
</PaddedRow>
|
|
214
|
-
</ModalFrame>
|
|
173
|
+
{content}
|
|
174
|
+
</SearchModalFrame>
|
|
215
175
|
)
|
|
216
176
|
}
|
package/src/ui/DetailsPane.tsx
CHANGED
|
@@ -345,35 +345,43 @@ const ChecksSection = ({ checks, contentWidth }: { checks: readonly CheckItem[];
|
|
|
345
345
|
|
|
346
346
|
const conversationDividerBodyRow = (pullRequest: PullRequestItem, contentWidth: number, conversationItems: readonly PullRequestConversationItem[], conversationStatus: DetailConversationStatus) => {
|
|
347
347
|
if (!pullRequest.detailLoaded) return null
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
status: conversationStatus,
|
|
352
|
-
width: contentWidth,
|
|
353
|
-
limit: Math.max(0, DETAIL_BODY_SCROLL_LIMIT - summaryRows.length - 1),
|
|
354
|
-
})
|
|
355
|
-
return conversationRows.length > 0 ? summaryRows.length : null
|
|
348
|
+
const dividerIndex = detailBodyPreview({ pullRequest, contentWidth, limit: DETAIL_BODY_SCROLL_LIMIT, conversationItems, conversationStatus })
|
|
349
|
+
.findIndex((line) => line.divider === true)
|
|
350
|
+
return dividerIndex >= 0 ? dividerIndex : null
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
export const getDetailJunctionRows = (
|
|
359
|
-
pullRequest
|
|
360
|
-
paneWidth
|
|
353
|
+
export const getDetailJunctionRows = ({
|
|
354
|
+
pullRequest,
|
|
355
|
+
paneWidth,
|
|
361
356
|
showChecks = false,
|
|
362
|
-
contentWidth
|
|
363
|
-
conversationItems
|
|
364
|
-
conversationStatus
|
|
365
|
-
|
|
357
|
+
contentWidth,
|
|
358
|
+
conversationItems = [],
|
|
359
|
+
conversationStatus = "idle",
|
|
360
|
+
bodyScrollTop = 0,
|
|
361
|
+
bodyViewportHeight = Number.POSITIVE_INFINITY,
|
|
362
|
+
}: {
|
|
363
|
+
readonly pullRequest: PullRequestItem | null
|
|
364
|
+
readonly paneWidth: number
|
|
365
|
+
readonly showChecks?: boolean
|
|
366
|
+
readonly contentWidth?: number
|
|
367
|
+
readonly conversationItems?: readonly PullRequestConversationItem[]
|
|
368
|
+
readonly conversationStatus?: DetailConversationStatus
|
|
369
|
+
readonly bodyScrollTop?: number
|
|
370
|
+
readonly bodyViewportHeight?: number
|
|
371
|
+
}): readonly number[] => {
|
|
366
372
|
if (!pullRequest) return [DETAIL_PLACEHOLDER_ROWS]
|
|
373
|
+
const resolvedContentWidth = contentWidth ?? Math.max(1, paneWidth - 2)
|
|
367
374
|
const titleLines = wrapText(pullRequest.title, Math.max(1, paneWidth - 2)).length
|
|
368
375
|
const detailDividerRow = 1 + titleLines + 1
|
|
369
376
|
const checks = deduplicateChecks(pullRequest.checks)
|
|
370
377
|
const checksDividerRow = checks.length > 0 ? detailDividerRow + 1 + checksRowCount(checks) + 1 : -1
|
|
371
378
|
const headerHeight = getDetailHeaderHeight(pullRequest, paneWidth, showChecks)
|
|
372
|
-
const conversationDivider = conversationDividerBodyRow(pullRequest,
|
|
379
|
+
const conversationDivider = conversationDividerBodyRow(pullRequest, resolvedContentWidth, conversationItems, conversationStatus)
|
|
380
|
+
const visibleConversationDivider = conversationDivider === null ? null : conversationDivider - Math.max(0, Math.floor(bodyScrollTop))
|
|
373
381
|
return [
|
|
374
382
|
detailDividerRow,
|
|
375
383
|
showChecks && checks.length > 0 ? checksDividerRow : -1,
|
|
376
|
-
|
|
384
|
+
visibleConversationDivider === null || visibleConversationDivider < 0 || visibleConversationDivider >= bodyViewportHeight ? -1 : headerHeight + visibleConversationDivider,
|
|
377
385
|
].filter((row) => row >= 0)
|
|
378
386
|
}
|
|
379
387
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DiffRenderable, MouseEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useMemo, type Ref } from "react"
|
|
3
3
|
import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
4
|
-
import { colors, type ThemeId } from "./colors.js"
|
|
4
|
+
import { colors, lineNumberTextColor, type ThemeId } from "./colors.js"
|
|
5
5
|
import { CommentBodyLine, commentCountText, commentMetaSegments, CommentSegmentsLine } from "./comments.js"
|
|
6
6
|
import { createDiffSyntaxStyle, diffCommentAnchorLabel, diffCommentLineLabel, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileIndexAtLine, type DiffFileStats, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
|
|
7
7
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
@@ -162,6 +162,7 @@ export const PullRequestDiffPane = ({
|
|
|
162
162
|
return ` ${selectedCommentLabel ?? diffCommentAnchorLabel(selectedCommentAnchor)}`
|
|
163
163
|
}
|
|
164
164
|
const stickyCommentColor = selectedCommentAnchor?.side === "LEFT" ? colors.status.failing : colors.status.passing
|
|
165
|
+
const diffLineNumberFg = lineNumberTextColor(colors.diff.lineNumberBg, colors.text)
|
|
165
166
|
const handleDiffMouseDown = function (this: ScrollBoxRenderable, event: MouseEvent) {
|
|
166
167
|
if (event.button !== 0) return
|
|
167
168
|
const localY = event.y - this.viewport.y
|
|
@@ -201,7 +202,7 @@ export const PullRequestDiffPane = ({
|
|
|
201
202
|
contextBg={colors.diff.contextBg}
|
|
202
203
|
addedSignColor={colors.status.passing}
|
|
203
204
|
removedSignColor={colors.status.failing}
|
|
204
|
-
lineNumberFg={
|
|
205
|
+
lineNumberFg={diffLineNumberFg}
|
|
205
206
|
lineNumberBg={colors.diff.lineNumberBg}
|
|
206
207
|
addedLineNumberBg={colors.diff.addedLineNumberBg}
|
|
207
208
|
removedLineNumberBg={colors.diff.removedLineNumberBg}
|
|
@@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core"
|
|
|
2
2
|
import type { LoadStatus, PullRequestItem } from "../domain.js"
|
|
3
3
|
import { daysOpen } from "../date.js"
|
|
4
4
|
import { colors } from "./colors.js"
|
|
5
|
-
import { fitCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
|
|
5
|
+
import { fitCell, MatchedCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
|
|
6
6
|
import { pullRequestRowDisplay, repoColor, reviewIcon } from "./pullRequests.js"
|
|
7
7
|
|
|
8
8
|
export type PullRequestGroups = Array<[string, PullRequestItem[]]>
|
|
@@ -37,22 +37,6 @@ const groupAgeWidth = (pullRequests: readonly PullRequestItem[]) => {
|
|
|
37
37
|
return Math.max(4, maxLen + 1)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const MatchedCell = ({ text, width, query, align = "left" }: { text: string; width: number; query: string; align?: "left" | "right" }) => {
|
|
41
|
-
const fitted = fitCell(text, width, align)
|
|
42
|
-
const needle = query.trim().toLowerCase()
|
|
43
|
-
const index = needle.length > 0 ? fitted.toLowerCase().indexOf(needle) : -1
|
|
44
|
-
if (index < 0) return <span>{fitted}</span>
|
|
45
|
-
|
|
46
|
-
const end = Math.min(fitted.length, index + needle.length)
|
|
47
|
-
return (
|
|
48
|
-
<>
|
|
49
|
-
{index > 0 ? <span>{fitted.slice(0, index)}</span> : null}
|
|
50
|
-
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{fitted.slice(index, end)}</span>
|
|
51
|
-
{end < fitted.length ? <span>{fitted.slice(end)}</span> : null}
|
|
52
|
-
</>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
40
|
const GroupTitle = ({ label, color, filterText }: { label: string; color: string; filterText: string }) => (
|
|
57
41
|
<TextLine>
|
|
58
42
|
<span fg={color}>{GROUP_ICON} </span>
|
package/src/ui/colors.ts
CHANGED
|
@@ -154,6 +154,13 @@ const mutedTextColor = (background: string) => {
|
|
|
154
154
|
|
|
155
155
|
const contrastText = (background: string) => luminance(background) > 128 ? "#000000" : "#ffffff"
|
|
156
156
|
|
|
157
|
+
export const lineNumberTextColor = (background: string, foreground: string) => {
|
|
158
|
+
const bg = readableHex(background, foreground)
|
|
159
|
+
const fg = readableHex(foreground, contrastText(bg))
|
|
160
|
+
const contrast = Math.abs(luminance(bg) - luminance(fg))
|
|
161
|
+
return mixHex(bg, fg, contrast < 90 ? 0.62 : 0.5)
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
const ghuiColors: ColorPalette = {
|
|
158
165
|
background: "#111018",
|
|
159
166
|
modalBackground: "#1a1a2e",
|