@kitlangton/ghui 0.1.17 → 0.1.18
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 +11 -4
- package/package.json +2 -1
- package/src/App.tsx +879 -141
- package/src/domain.ts +46 -0
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +280 -184
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +52 -0
- package/src/ui/PullRequestDiffPane.tsx +105 -33
- package/src/ui/PullRequestList.tsx +28 -9
- package/src/ui/colors.ts +41 -0
- package/src/ui/commentEditor.ts +126 -0
- package/src/ui/diff.ts +204 -7
- package/src/ui/modals.tsx +236 -9
package/src/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
1
|
+
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
3
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
4
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
@@ -6,18 +6,19 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
|
6
6
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
7
7
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
8
8
|
import { config } from "./config.js"
|
|
9
|
-
import type
|
|
9
|
+
import { pullRequestQueueLabels, pullRequestQueueModes, type CreatePullRequestCommentInput, type DiffCommentSide, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestQueueMode, type PullRequestReviewComment } from "./domain.js"
|
|
10
10
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
11
11
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
12
12
|
import { Observability } from "./observability.js"
|
|
13
13
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
14
|
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
15
15
|
import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
16
|
-
import {
|
|
16
|
+
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
17
|
+
import { buildStackedDiffFiles, diffCommentAnchorKey, diffCommentLocationKey, getStackedDiffCommentAnchors, nearestDiffCommentAnchorIndex, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, type DiffCommentAnchor, type PullRequestDiffState, type StackedDiffCommentAnchor } from "./ui/diff.js"
|
|
17
18
|
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
18
19
|
import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
|
|
19
20
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
20
|
-
import { CloseModal, initialCloseModalState, initialLabelModalState, initialMergeModalState, initialThemeModalState, LabelModal, MergeModal, ThemeModal } from "./ui/modals.js"
|
|
21
|
+
import { CloseModal, CommentModal, CommentThreadModal, initialCloseModalState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialThemeModalState, LabelModal, MergeModal, Modal, ThemeModal, type CloseModalState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type ThemeModalState } from "./ui/modals.js"
|
|
21
22
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
22
23
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
23
24
|
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
@@ -28,8 +29,10 @@ const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
|
28
29
|
type LoadStatus = "loading" | "ready" | "error"
|
|
29
30
|
|
|
30
31
|
interface PullRequestLoad {
|
|
32
|
+
readonly queueMode: PullRequestQueueMode
|
|
31
33
|
readonly data: readonly PullRequestItem[]
|
|
32
34
|
readonly fetchedAt: Date | null
|
|
35
|
+
readonly detailsFetchedAt: Date | null
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
interface DetailPlaceholderInput {
|
|
@@ -40,18 +43,67 @@ interface DetailPlaceholderInput {
|
|
|
40
43
|
readonly filterText: string
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
type DiffLineColorConfig = {
|
|
47
|
+
readonly gutter: string
|
|
48
|
+
readonly content: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type DiffSideRenderable = {
|
|
52
|
+
readonly setLineColor: (line: number, color: DiffLineColorConfig) => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type DiffRenderableRuntimeSides = {
|
|
56
|
+
readonly leftSide?: DiffSideRenderable
|
|
57
|
+
readonly rightSide?: DiffSideRenderable
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface AppliedDiffLineColor {
|
|
61
|
+
readonly anchor: StackedDiffCommentAnchor
|
|
62
|
+
readonly view: "unified" | "split"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AppliedDiffLineColorState {
|
|
66
|
+
readonly contextKey: string | null
|
|
67
|
+
readonly entries: readonly AppliedDiffLineColor[]
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
const PR_FETCH_RETRIES = 6
|
|
44
71
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
45
72
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
46
73
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
47
74
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
48
75
|
|
|
76
|
+
const mergeCachedDetails = (fresh: readonly PullRequestItem[], cached: readonly PullRequestItem[] | undefined) => {
|
|
77
|
+
if (!cached) return fresh
|
|
78
|
+
const cachedByUrl = new Map(cached.map((pullRequest) => [pullRequest.url, pullRequest]))
|
|
79
|
+
return fresh.map((pullRequest) => {
|
|
80
|
+
const cachedPullRequest = cachedByUrl.get(pullRequest.url)
|
|
81
|
+
if (!cachedPullRequest?.detailLoaded) return pullRequest
|
|
82
|
+
return {
|
|
83
|
+
...pullRequest,
|
|
84
|
+
body: cachedPullRequest.body,
|
|
85
|
+
labels: cachedPullRequest.labels,
|
|
86
|
+
additions: cachedPullRequest.additions,
|
|
87
|
+
deletions: cachedPullRequest.deletions,
|
|
88
|
+
changedFiles: cachedPullRequest.changedFiles,
|
|
89
|
+
checkStatus: cachedPullRequest.checkStatus,
|
|
90
|
+
checkSummary: cachedPullRequest.checkSummary,
|
|
91
|
+
checks: cachedPullRequest.checks,
|
|
92
|
+
detailLoaded: true,
|
|
93
|
+
} satisfies PullRequestItem
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
const retryProgressAtom = Atom.make<RetryProgress | null>(null).pipe(Atom.keepAlive)
|
|
98
|
+
const queueModeAtom = Atom.make<PullRequestQueueMode>("authored").pipe(Atom.keepAlive)
|
|
99
|
+
const queueLoadCacheAtom = Atom.make<Partial<Record<PullRequestQueueMode, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
100
|
+
const queueSelectionAtom = Atom.make<Partial<Record<PullRequestQueueMode, number>>>({}).pipe(Atom.keepAlive)
|
|
50
101
|
const pullRequestsAtom = githubRuntime.atom(
|
|
51
102
|
GitHubService.use((github) =>
|
|
52
103
|
Effect.gen(function*() {
|
|
104
|
+
const queueMode = yield* Atom.get(queueModeAtom)
|
|
53
105
|
yield* Atom.set(retryProgressAtom, null)
|
|
54
|
-
const data = yield* github.listOpenPullRequests().pipe(
|
|
106
|
+
const data = yield* github.listOpenPullRequests(queueMode).pipe(
|
|
55
107
|
Effect.tapError(() =>
|
|
56
108
|
Atom.update(retryProgressAtom, (current) => ({
|
|
57
109
|
attempt: Math.min((current?.attempt ?? 0) + 1, PR_FETCH_RETRIES),
|
|
@@ -63,7 +115,15 @@ const pullRequestsAtom = githubRuntime.atom(
|
|
|
63
115
|
)
|
|
64
116
|
|
|
65
117
|
yield* Atom.set(retryProgressAtom, null)
|
|
66
|
-
|
|
118
|
+
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
119
|
+
const load = {
|
|
120
|
+
queueMode,
|
|
121
|
+
data: mergeCachedDetails(data, cache[queueMode]?.data),
|
|
122
|
+
fetchedAt: new Date(),
|
|
123
|
+
detailsFetchedAt: null,
|
|
124
|
+
} satisfies PullRequestLoad
|
|
125
|
+
yield* Atom.set(queueLoadCacheAtom, { ...cache, [queueMode]: load })
|
|
126
|
+
return load
|
|
67
127
|
})
|
|
68
128
|
),
|
|
69
129
|
).pipe(Atom.keepAlive)
|
|
@@ -79,13 +139,14 @@ const diffFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
|
79
139
|
const diffFileIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
80
140
|
const diffRenderViewAtom = Atom.make<"unified" | "split">("split").pipe(Atom.keepAlive)
|
|
81
141
|
const diffWrapModeAtom = Atom.make<"none" | "word">("none").pipe(Atom.keepAlive)
|
|
142
|
+
const diffCommentModeAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
143
|
+
const diffCommentAnchorIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
144
|
+
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
145
|
+
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
82
146
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
83
147
|
|
|
84
|
-
const
|
|
85
|
-
const closeModalAtom = Atom.make(initialCloseModalState).pipe(Atom.keepAlive)
|
|
86
|
-
const mergeModalAtom = Atom.make(initialMergeModalState).pipe(Atom.keepAlive)
|
|
148
|
+
const activeModalAtom = Atom.make<Modal>(initialModal).pipe(Atom.keepAlive)
|
|
87
149
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
88
|
-
const themeModalAtom = Atom.make(initialThemeModalState).pipe(Atom.keepAlive)
|
|
89
150
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
90
151
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
91
152
|
const recentlyCompletedPullRequestsAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -98,8 +159,8 @@ const usernameAtom = githubRuntime.atom(
|
|
|
98
159
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
99
160
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
100
161
|
)
|
|
101
|
-
const listOpenPullRequestDetailsAtom = githubRuntime.fn<
|
|
102
|
-
GitHubService.use((github) => github.listOpenPullRequestDetails())
|
|
162
|
+
const listOpenPullRequestDetailsAtom = githubRuntime.fn<PullRequestQueueMode>()((queueMode) =>
|
|
163
|
+
GitHubService.use((github) => github.listOpenPullRequestDetails(queueMode))
|
|
103
164
|
)
|
|
104
165
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
105
166
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
@@ -113,6 +174,9 @@ const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly
|
|
|
113
174
|
const getPullRequestDiffAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
114
175
|
GitHubService.use((github) => github.getPullRequestDiff(input.repository, input.number))
|
|
115
176
|
)
|
|
177
|
+
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
178
|
+
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
179
|
+
)
|
|
116
180
|
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
117
181
|
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
118
182
|
)
|
|
@@ -122,11 +186,29 @@ const mergePullRequestAtom = githubRuntime.fn<{ readonly repository: string; rea
|
|
|
122
186
|
const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
123
187
|
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
124
188
|
)
|
|
189
|
+
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
190
|
+
|
|
191
|
+
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
125
192
|
|
|
126
193
|
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
127
194
|
|
|
128
195
|
const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
|
|
129
196
|
|
|
197
|
+
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
198
|
+
const normalized = query.trim().toLowerCase()
|
|
199
|
+
if (normalized.length === 0) return 0
|
|
200
|
+
const fields = [
|
|
201
|
+
pullRequest.title.toLowerCase(),
|
|
202
|
+
pullRequest.repository.toLowerCase(),
|
|
203
|
+
String(pullRequest.number),
|
|
204
|
+
]
|
|
205
|
+
const scores = fields.flatMap((field, index) => {
|
|
206
|
+
const matchIndex = field.indexOf(normalized)
|
|
207
|
+
return matchIndex >= 0 ? [index * 1000 + matchIndex] : []
|
|
208
|
+
})
|
|
209
|
+
return scores.length > 0 ? Math.min(...scores) : null
|
|
210
|
+
}
|
|
211
|
+
|
|
130
212
|
const clipboardCommands = (): readonly (readonly string[])[] => {
|
|
131
213
|
if (process.platform === "darwin") return [["pbcopy"]]
|
|
132
214
|
if (process.platform === "linux") {
|
|
@@ -207,6 +289,52 @@ const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => k
|
|
|
207
289
|
|
|
208
290
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
209
291
|
|
|
292
|
+
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
293
|
+
const index = pullRequestQueueModes.indexOf(mode)
|
|
294
|
+
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
298
|
+
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
299
|
+
|
|
300
|
+
const groupDiffCommentThreads = (pullRequest: PullRequestItem, comments: readonly PullRequestReviewComment[]) => {
|
|
301
|
+
const threads: Record<string, PullRequestReviewComment[]> = {}
|
|
302
|
+
for (const comment of comments) {
|
|
303
|
+
const key = diffCommentThreadKey(pullRequest, comment)
|
|
304
|
+
const thread = threads[key]
|
|
305
|
+
if (thread) thread.push(comment)
|
|
306
|
+
else threads[key] = [comment]
|
|
307
|
+
}
|
|
308
|
+
return threads
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const isLocalDiffComment = (comment: PullRequestReviewComment) => comment.id.startsWith("local:")
|
|
312
|
+
|
|
313
|
+
const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig => {
|
|
314
|
+
if (anchor.kind === "addition") {
|
|
315
|
+
return { gutter: colors.diff.addedLineNumberBg, content: colors.diff.addedBg }
|
|
316
|
+
}
|
|
317
|
+
if (anchor.kind === "deletion") {
|
|
318
|
+
return { gutter: colors.diff.removedLineNumberBg, content: colors.diff.removedBg }
|
|
319
|
+
}
|
|
320
|
+
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: "unified" | "split") => {
|
|
324
|
+
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
325
|
+
if (view === "split") {
|
|
326
|
+
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
327
|
+
return target ? [target] : []
|
|
328
|
+
}
|
|
329
|
+
return withSides.leftSide ? [withSides.leftSide] : []
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const setDiffCommentLineColor = (diff: DiffRenderable, entry: AppliedDiffLineColor, color: DiffLineColorConfig) => {
|
|
333
|
+
for (const target of diffSideTargets(diff, entry.anchor, entry.view)) {
|
|
334
|
+
target.setLineColor(entry.anchor.localRenderLine, color)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
210
338
|
const getDetailPlaceholderContent = ({
|
|
211
339
|
status,
|
|
212
340
|
retryProgress,
|
|
@@ -253,6 +381,9 @@ export const App = () => {
|
|
|
253
381
|
const { width, height } = useTerminalDimensions()
|
|
254
382
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
255
383
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
384
|
+
const [queueMode, setQueueMode] = useAtom(queueModeAtom)
|
|
385
|
+
const [queueLoadCache, setQueueLoadCache] = useAtom(queueLoadCacheAtom)
|
|
386
|
+
const [queueSelection, setQueueSelection] = useAtom(queueSelectionAtom)
|
|
256
387
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
257
388
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
258
389
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -265,12 +396,43 @@ export const App = () => {
|
|
|
265
396
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
266
397
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
267
398
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
399
|
+
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
400
|
+
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
401
|
+
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
402
|
+
const [diffCommentsLoaded, setDiffCommentsLoaded] = useAtom(diffCommentsLoadedAtom)
|
|
268
403
|
const [pullRequestDiffCache, setPullRequestDiffCache] = useAtom(pullRequestDiffCacheAtom)
|
|
269
|
-
const [
|
|
270
|
-
const [closeModal, setCloseModal] = useAtom(closeModalAtom)
|
|
271
|
-
const [mergeModal, setMergeModal] = useAtom(mergeModalAtom)
|
|
404
|
+
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
272
405
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
273
|
-
const
|
|
406
|
+
const closeActiveModal = () => setActiveModal(initialModal)
|
|
407
|
+
const labelModalActive = Modal.$is("Label")(activeModal)
|
|
408
|
+
const closeModalActive = Modal.$is("Close")(activeModal)
|
|
409
|
+
const mergeModalActive = Modal.$is("Merge")(activeModal)
|
|
410
|
+
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
411
|
+
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
412
|
+
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
413
|
+
const labelModal: LabelModalState = labelModalActive ? activeModal.state : initialLabelModalState
|
|
414
|
+
const closeModal: CloseModalState = closeModalActive ? activeModal.state : initialCloseModalState
|
|
415
|
+
const mergeModal: MergeModalState = mergeModalActive ? activeModal.state : initialMergeModalState
|
|
416
|
+
const commentModal: CommentModalState = commentModalActive ? activeModal.state : initialCommentModalState
|
|
417
|
+
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal.state : initialCommentThreadModalState
|
|
418
|
+
const themeModal: ThemeModalState = themeModalActive ? activeModal.state : initialThemeModalState
|
|
419
|
+
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
420
|
+
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
421
|
+
const ctor = Modal[tag] as (args: { state: ModalState<Tag> }) => Modal
|
|
422
|
+
if (typeof next === "function") {
|
|
423
|
+
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
424
|
+
if (current._tag !== tag) return current
|
|
425
|
+
const prev = (current as unknown as { readonly state: ModalState<Tag> }).state
|
|
426
|
+
return ctor({ state: updater(prev) })
|
|
427
|
+
}
|
|
428
|
+
return ctor({ state: next })
|
|
429
|
+
})
|
|
430
|
+
const setLabelModal = makeModalSetter("Label")
|
|
431
|
+
const setCloseModal = makeModalSetter("Close")
|
|
432
|
+
const setMergeModal = makeModalSetter("Merge")
|
|
433
|
+
const setCommentModal = makeModalSetter("Comment")
|
|
434
|
+
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
435
|
+
const setThemeModal = makeModalSetter("Theme")
|
|
274
436
|
setActiveTheme(themeId)
|
|
275
437
|
const themeIdRef = useRef(themeId)
|
|
276
438
|
const themeModalRef = useRef(themeModal)
|
|
@@ -291,9 +453,11 @@ export const App = () => {
|
|
|
291
453
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
292
454
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
293
455
|
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
456
|
+
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
294
457
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
295
458
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
296
459
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
460
|
+
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
297
461
|
const terminalWidth = width ?? 100
|
|
298
462
|
const terminalHeight = height ?? 24
|
|
299
463
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -312,6 +476,7 @@ export const App = () => {
|
|
|
312
476
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
313
477
|
const detailHydrationRef = useRef<number | null>(null)
|
|
314
478
|
const refreshGenerationRef = useRef(0)
|
|
479
|
+
const didMountQueueModeRef = useRef(false)
|
|
315
480
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
316
481
|
const terminalFocusedRef = useRef(true)
|
|
317
482
|
const terminalWasBlurredRef = useRef(false)
|
|
@@ -320,6 +485,9 @@ export const App = () => {
|
|
|
320
485
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
321
486
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
322
487
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
488
|
+
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
489
|
+
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
490
|
+
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
323
491
|
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
324
492
|
|
|
325
493
|
const flashNotice = (message: string) => {
|
|
@@ -348,7 +516,9 @@ export const App = () => {
|
|
|
348
516
|
}
|
|
349
517
|
}, [])
|
|
350
518
|
|
|
351
|
-
const
|
|
519
|
+
const resolvedPullRequestLoad = AsyncResult.getOrElse(pullRequestResult, () => null)
|
|
520
|
+
const pullRequestLoad = queueLoadCache[queueMode] ?? (resolvedPullRequestLoad?.queueMode === queueMode ? resolvedPullRequestLoad : null)
|
|
521
|
+
const isLoadingQueueMode = resolvedPullRequestLoad !== null && resolvedPullRequestLoad.queueMode !== queueMode
|
|
352
522
|
const pullRequests = useMemo(() => {
|
|
353
523
|
const source = pullRequestLoad?.data ?? []
|
|
354
524
|
const seenUrls = new Set<string>()
|
|
@@ -362,7 +532,7 @@ export const App = () => {
|
|
|
362
532
|
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
363
533
|
]
|
|
364
534
|
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
365
|
-
const pullRequestStatus: LoadStatus = pullRequestResult.waiting && pullRequestLoad === null
|
|
535
|
+
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
366
536
|
? "loading"
|
|
367
537
|
: AsyncResult.isFailure(pullRequestResult)
|
|
368
538
|
? "error"
|
|
@@ -375,18 +545,47 @@ export const App = () => {
|
|
|
375
545
|
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
376
546
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
377
547
|
|
|
378
|
-
const filteredPullRequests = useMemo(() =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
548
|
+
const filteredPullRequests = useMemo(() => {
|
|
549
|
+
if (effectiveFilterQuery.length === 0) return pullRequests
|
|
550
|
+
return pullRequests.flatMap((pullRequest) => {
|
|
551
|
+
const score = pullRequestFilterScore(pullRequest, effectiveFilterQuery)
|
|
552
|
+
return score === null ? [] : [{ pullRequest, score }]
|
|
553
|
+
}).sort((left, right) =>
|
|
554
|
+
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
555
|
+
).map(({ pullRequest }) => pullRequest)
|
|
556
|
+
}, [pullRequests, effectiveFilterQuery])
|
|
557
|
+
|
|
558
|
+
const visibleRepoOrder = useMemo(() => effectiveFilterQuery.length > 0
|
|
559
|
+
? [...new Set(filteredPullRequests.map((pullRequest) => pullRequest.repository))]
|
|
560
|
+
: [], [filteredPullRequests, effectiveFilterQuery.length])
|
|
385
561
|
const visibleGroups = useMemo(
|
|
386
|
-
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository),
|
|
387
|
-
[filteredPullRequests],
|
|
562
|
+
() => groupBy(filteredPullRequests, (pullRequest) => pullRequest.repository, visibleRepoOrder),
|
|
563
|
+
[filteredPullRequests, visibleRepoOrder],
|
|
388
564
|
)
|
|
389
565
|
const visiblePullRequests = useMemo(() => visibleGroups.flatMap(([, pullRequests]) => pullRequests), [visibleGroups])
|
|
566
|
+
const selectedPullRequest = visiblePullRequests[selectedIndex] ?? null
|
|
567
|
+
const selectedDiffState = selectedPullRequest ? pullRequestDiffCache[pullRequestDiffKey(selectedPullRequest)] : undefined
|
|
568
|
+
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
569
|
+
const readyDiffFiles = selectedDiffState?.status === "ready" ? selectedDiffState.files : []
|
|
570
|
+
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
571
|
+
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
572
|
+
const selectedDiffCommentCount = useMemo(() => selectedDiffKey
|
|
573
|
+
? Object.entries(diffCommentThreads)
|
|
574
|
+
.filter(([key, comments]) => key.startsWith(`${selectedDiffKey}:`) && comments.length > 0)
|
|
575
|
+
.reduce((count, [, comments]) => count + comments.length, 0)
|
|
576
|
+
: 0, [diffCommentThreads, selectedDiffKey])
|
|
577
|
+
const diffCommentAnchors = useMemo(
|
|
578
|
+
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
579
|
+
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
580
|
+
)
|
|
581
|
+
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
582
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentAnchorKey(selectedDiffCommentAnchor)}` : null
|
|
583
|
+
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
584
|
+
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
585
|
+
const diffCommentRows = useMemo(
|
|
586
|
+
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
587
|
+
[diffCommentAnchors],
|
|
588
|
+
)
|
|
390
589
|
const groupStarts = useMemo(() => visibleGroups.reduce<Array<number>>((starts, [, pullRequests], index) => {
|
|
391
590
|
if (index === 0) {
|
|
392
591
|
starts.push(0)
|
|
@@ -406,12 +605,15 @@ export const App = () => {
|
|
|
406
605
|
: pullRequestStatus === "loading"
|
|
407
606
|
? "loading pull requests..."
|
|
408
607
|
: ""
|
|
409
|
-
const headerLeft = username ? `GHUI ${username}` :
|
|
608
|
+
const headerLeft = username ? `GHUI ${username} ${pullRequestQueueLabels[queueMode]}` : `GHUI ${pullRequestQueueLabels[queueMode]}`
|
|
410
609
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
411
610
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
412
611
|
const selectPullRequestByUrl = (url: string) => {
|
|
413
612
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
414
|
-
if (index >= 0)
|
|
613
|
+
if (index >= 0) {
|
|
614
|
+
setSelectedIndex(index)
|
|
615
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: index }))
|
|
616
|
+
}
|
|
415
617
|
}
|
|
416
618
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
417
619
|
const pullRequest = pullRequests.find((item) => item.url === url)
|
|
@@ -429,6 +631,23 @@ export const App = () => {
|
|
|
429
631
|
refreshPullRequestsAtom()
|
|
430
632
|
}
|
|
431
633
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
634
|
+
const switchQueueMode = (delta: 1 | -1) => {
|
|
635
|
+
const mode = nextQueueMode(queueMode, delta)
|
|
636
|
+
if (mode === queueMode) return
|
|
637
|
+
refreshGenerationRef.current += 1
|
|
638
|
+
setQueueSelection((current) => ({ ...current, [queueMode]: selectedIndex }))
|
|
639
|
+
setQueueMode(mode)
|
|
640
|
+
setSelectedIndex(queueSelection[mode] ?? 0)
|
|
641
|
+
setRecentlyCompletedPullRequests({})
|
|
642
|
+
detailHydrationRef.current = null
|
|
643
|
+
setDetailFullView(false)
|
|
644
|
+
setDiffFullView(false)
|
|
645
|
+
setDiffCommentMode(false)
|
|
646
|
+
setFilterDraft(filterQuery)
|
|
647
|
+
setNotice(null)
|
|
648
|
+
setRefreshCompletionMessage(null)
|
|
649
|
+
setRefreshStartedAt(null)
|
|
650
|
+
}
|
|
432
651
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
433
652
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
434
653
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -443,6 +662,15 @@ export const App = () => {
|
|
|
443
662
|
}
|
|
444
663
|
}, [pullRequestLoad?.fetchedAt])
|
|
445
664
|
|
|
665
|
+
useEffect(() => {
|
|
666
|
+
if (!didMountQueueModeRef.current) {
|
|
667
|
+
didMountQueueModeRef.current = true
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
if (queueLoadCache[queueMode]) return
|
|
671
|
+
refreshPullRequestsAtom()
|
|
672
|
+
}, [queueMode, queueLoadCache, refreshPullRequestsAtom])
|
|
673
|
+
|
|
446
674
|
useEffect(() => {
|
|
447
675
|
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
448
676
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
@@ -498,13 +726,69 @@ export const App = () => {
|
|
|
498
726
|
})
|
|
499
727
|
}, [visiblePullRequests.length])
|
|
500
728
|
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
setQueueSelection((current) => current[queueMode] === selectedIndex ? current : { ...current, [queueMode]: selectedIndex })
|
|
731
|
+
}, [queueMode, selectedIndex])
|
|
732
|
+
|
|
501
733
|
useEffect(() => {
|
|
502
734
|
setDiffFileIndex(0)
|
|
735
|
+
setDiffCommentAnchorIndex(0)
|
|
503
736
|
}, [selectedIndex])
|
|
504
737
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
738
|
+
useEffect(() => {
|
|
739
|
+
setDiffCommentAnchorIndex((current) => {
|
|
740
|
+
if (diffCommentAnchors.length === 0) return 0
|
|
741
|
+
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
742
|
+
})
|
|
743
|
+
}, [diffCommentAnchors.length])
|
|
744
|
+
|
|
745
|
+
useEffect(() => {
|
|
746
|
+
if (!diffCommentMode || !selectedDiffCommentAnchor) return
|
|
747
|
+
setDiffFileIndex((current) => current === selectedDiffCommentAnchor.fileIndex ? current : selectedDiffCommentAnchor.fileIndex)
|
|
748
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.fileIndex])
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const previous = diffCommentLineColorsRef.current
|
|
752
|
+
if (previous.contextKey === diffLineColorContextKey) {
|
|
753
|
+
for (const entry of previous.entries) {
|
|
754
|
+
const diff = diffRenderableRefs.current.get(entry.anchor.fileIndex)
|
|
755
|
+
if (diff) setDiffCommentLineColor(diff, entry, originalDiffLineColor(entry.anchor))
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const nextEntries: AppliedDiffLineColor[] = []
|
|
760
|
+
const appliedKeys = new Set<string>()
|
|
761
|
+
const applyLineColor = (anchor: StackedDiffCommentAnchor, gutter: string, override = false) => {
|
|
762
|
+
const key = `${effectiveDiffRenderView}:${anchor.side}:${anchor.renderLine}`
|
|
763
|
+
if (appliedKeys.has(key) && !override) return
|
|
764
|
+
appliedKeys.add(key)
|
|
765
|
+
const entry = { anchor, view: effectiveDiffRenderView } satisfies AppliedDiffLineColor
|
|
766
|
+
const diff = diffRenderableRefs.current.get(anchor.fileIndex)
|
|
767
|
+
if (diff) setDiffCommentLineColor(diff, entry, { ...originalDiffLineColor(anchor), gutter })
|
|
768
|
+
if (!nextEntries.some((existing) => existing.view === entry.view && existing.anchor.side === anchor.side && existing.anchor.renderLine === anchor.renderLine)) {
|
|
769
|
+
nextEntries.push(entry)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (selectedDiffKey) {
|
|
774
|
+
for (const anchor of diffCommentAnchors) {
|
|
775
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentAnchorKey(anchor)}`]?.length ?? 0) > 0) {
|
|
776
|
+
applyLineColor(anchor, colors.status.pending)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (diffCommentMode && selectedDiffCommentAnchor) {
|
|
781
|
+
applyLineColor(selectedDiffCommentAnchor, selectedDiffCommentAnchor.side === "RIGHT" ? colors.status.passing : colors.status.failing, true)
|
|
782
|
+
if (suppressNextDiffCommentScrollRef.current) {
|
|
783
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
784
|
+
} else {
|
|
785
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
suppressNextDiffCommentScrollRef.current = false
|
|
789
|
+
}
|
|
790
|
+
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
791
|
+
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
508
792
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)
|
|
509
793
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
510
794
|
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?.status === "loading"
|
|
@@ -521,23 +805,29 @@ export const App = () => {
|
|
|
521
805
|
useEffect(() => {
|
|
522
806
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
523
807
|
if (pullRequestStatus !== "ready" || fetchedAt === undefined) return
|
|
524
|
-
if (detailHydrationRef.current === fetchedAt) return
|
|
808
|
+
if (detailHydrationRef.current === fetchedAt || pullRequestLoad?.detailsFetchedAt?.getTime() === fetchedAt) return
|
|
525
809
|
if (!pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)) return
|
|
526
810
|
detailHydrationRef.current = fetchedAt
|
|
527
811
|
const generation = refreshGenerationRef.current
|
|
528
|
-
void loadPullRequestDetails().then((details) => {
|
|
812
|
+
void loadPullRequestDetails(queueMode).then((details) => {
|
|
529
813
|
if (generation !== refreshGenerationRef.current) return
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
814
|
+
setQueueLoadCache((current) => {
|
|
815
|
+
const load = current[queueMode]
|
|
816
|
+
if (!load) return current
|
|
817
|
+
const detailsByUrl = new Map(details.map((detail) => [detail.url, detail]))
|
|
818
|
+
return {
|
|
819
|
+
...current,
|
|
820
|
+
[queueMode]: {
|
|
821
|
+
...load,
|
|
822
|
+
data: load.data.map((pullRequest) => detailsByUrl.get(pullRequest.url) ?? pullRequest),
|
|
823
|
+
detailsFetchedAt: load.fetchedAt,
|
|
824
|
+
},
|
|
534
825
|
}
|
|
535
|
-
return next
|
|
536
826
|
})
|
|
537
827
|
}).catch((error) => {
|
|
538
|
-
flashNotice(
|
|
828
|
+
flashNotice(errorMessage(error))
|
|
539
829
|
})
|
|
540
|
-
}, [pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
830
|
+
}, [queueMode, pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests.length])
|
|
541
831
|
|
|
542
832
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
543
833
|
status: pullRequestStatus,
|
|
@@ -550,9 +840,53 @@ export const App = () => {
|
|
|
550
840
|
|
|
551
841
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
552
842
|
|
|
553
|
-
const
|
|
843
|
+
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
844
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
845
|
+
const previousLoadState = diffCommentsLoaded[key]
|
|
846
|
+
if (!force && previousLoadState) return
|
|
847
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
848
|
+
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
849
|
+
.then((comments) => {
|
|
850
|
+
setDiffCommentsLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
851
|
+
setDiffCommentThreads((current) => {
|
|
852
|
+
const prefix = `${key}:`
|
|
853
|
+
const threads = groupDiffCommentThreads(pullRequest, comments)
|
|
854
|
+
const next: Record<string, readonly PullRequestReviewComment[]> = Object.fromEntries(
|
|
855
|
+
Object.entries(current).filter(([threadKey]) => !threadKey.startsWith(prefix)),
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
for (const [threadKey, threadComments] of Object.entries(current)) {
|
|
859
|
+
if (!threadKey.startsWith(prefix)) continue
|
|
860
|
+
const localComments = threadComments.filter(isLocalDiffComment)
|
|
861
|
+
if (localComments.length > 0) {
|
|
862
|
+
next[threadKey] = [...(threads[threadKey] ?? []), ...localComments]
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
for (const [threadKey, threadComments] of Object.entries(threads)) {
|
|
867
|
+
if (!next[threadKey]) next[threadKey] = threadComments
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return next
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
.catch((error) => {
|
|
874
|
+
setDiffCommentsLoaded((current) => {
|
|
875
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
876
|
+
const next = { ...current }
|
|
877
|
+
delete next[key]
|
|
878
|
+
return next
|
|
879
|
+
})
|
|
880
|
+
flashNotice(errorMessage(error))
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const loadPullRequestDiff = (pullRequest: PullRequestItem, options: { readonly force?: boolean; readonly includeComments?: boolean } = {}) => {
|
|
885
|
+
const force = options.force ?? false
|
|
886
|
+
const includeComments = options.includeComments ?? false
|
|
554
887
|
const key = pullRequestDiffKey(pullRequest)
|
|
555
888
|
const existing = pullRequestDiffCache[key]
|
|
889
|
+
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
556
890
|
if (!force && (existing?.status === "ready" || existing?.status === "loading")) return
|
|
557
891
|
|
|
558
892
|
setPullRequestDiffCache((current) => ({ ...current, [key]: { status: "loading" } }))
|
|
@@ -590,12 +924,168 @@ export const App = () => {
|
|
|
590
924
|
|
|
591
925
|
const openDiffView = () => {
|
|
592
926
|
if (!selectedPullRequest) return
|
|
927
|
+
diffRenderableRefs.current.clear()
|
|
928
|
+
diffCommentLineColorsRef.current = { contextKey: null, entries: [] }
|
|
593
929
|
setDiffFullView(true)
|
|
594
930
|
setDetailFullView(false)
|
|
931
|
+
setDiffCommentMode(false)
|
|
595
932
|
setDiffFileIndex(0)
|
|
596
933
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
597
934
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
598
|
-
loadPullRequestDiff(selectedPullRequest)
|
|
935
|
+
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const setDiffRenderableRef = (index: number, diff: DiffRenderable | null) => {
|
|
939
|
+
if (diff) diffRenderableRefs.current.set(index, diff)
|
|
940
|
+
else diffRenderableRefs.current.delete(index)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const scrollToDiffFile = (index: number) => {
|
|
944
|
+
const stackedFile = stackedDiffFiles[index]
|
|
945
|
+
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const syncDiffFileIndexToScroll = () => {
|
|
949
|
+
const scrollTop = diffScrollRef.current?.scrollTop
|
|
950
|
+
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
951
|
+
const nextIndex = stackedDiffFiles.reduce((current, file) => file.headerLine <= scrollTop + 1 ? file.index : current, 0)
|
|
952
|
+
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const scrollDiffBy = (y: number) => {
|
|
956
|
+
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
957
|
+
syncDiffFileIndexToScroll()
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const scrollDiffTo = (y: number) => {
|
|
961
|
+
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
962
|
+
if (y <= 0) setDiffFileIndex(0)
|
|
963
|
+
else if (y === Number.MAX_SAFE_INTEGER) setDiffFileIndex(Math.max(0, readyDiffFiles.length - 1))
|
|
964
|
+
else syncDiffFileIndexToScroll()
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const ensureDiffLineVisible = (line: number) => {
|
|
968
|
+
const scroll = diffScrollRef.current
|
|
969
|
+
if (!scroll) return
|
|
970
|
+
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
971
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line)
|
|
972
|
+
if (nextTop !== scroll.scrollTop) scroll.scrollTo({ x: 0, y: nextTop })
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const jumpDiffFile = (delta: 1 | -1) => {
|
|
976
|
+
if (readyDiffFiles.length === 0) return
|
|
977
|
+
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
978
|
+
setDiffFileIndex(nextIndex)
|
|
979
|
+
if (diffCommentMode) {
|
|
980
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === selectedDiffCommentAnchor?.side)
|
|
981
|
+
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
982
|
+
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
983
|
+
}
|
|
984
|
+
scrollToDiffFile(nextIndex)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const enterDiffCommentMode = () => {
|
|
988
|
+
const scrollTop = diffScrollRef.current?.scrollTop ?? 0
|
|
989
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
990
|
+
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop))
|
|
991
|
+
setDiffCommentMode(true)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const moveDiffCommentAnchor = (delta: number) => {
|
|
995
|
+
if (diffCommentAnchors.length === 0) return
|
|
996
|
+
const currentAnchor = selectedDiffCommentAnchor ?? diffCommentAnchors[0]
|
|
997
|
+
const currentRowIndex = Math.max(0, currentAnchor ? diffCommentRows.indexOf(currentAnchor.renderLine) : 0)
|
|
998
|
+
const nextRow = diffCommentRows[Math.max(0, Math.min(diffCommentRows.length - 1, currentRowIndex + delta))]
|
|
999
|
+
if (nextRow === undefined) return
|
|
1000
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow && anchor.side === currentAnchor?.side)
|
|
1001
|
+
?? diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow)
|
|
1002
|
+
if (!nextAnchor) return
|
|
1003
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1007
|
+
if (!selectedDiffCommentAnchor) return
|
|
1008
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === selectedDiffCommentAnchor.renderLine && anchor.side === side)
|
|
1009
|
+
if (!nextAnchor) return
|
|
1010
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1014
|
+
setCommentModal((current) => {
|
|
1015
|
+
const next = transform({ body: current.body, cursor: current.cursor })
|
|
1016
|
+
if (next.body === current.body && next.cursor === current.cursor && current.error === null) return current
|
|
1017
|
+
return { ...current, body: next.body, cursor: next.cursor, error: null }
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const openDiffCommentModal = () => {
|
|
1022
|
+
if (!selectedDiffCommentAnchor || !selectedPullRequest) return
|
|
1023
|
+
setCommentModal(initialCommentModalState)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const openDiffCommentThreadModal = () => {
|
|
1027
|
+
if (!selectedDiffCommentAnchor || selectedDiffCommentThread.length === 0) return
|
|
1028
|
+
setCommentThreadModal({ scrollOffset: 0 })
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const submitDiffComment = () => {
|
|
1032
|
+
if (!selectedPullRequest || !selectedDiffCommentAnchor) return
|
|
1033
|
+
const body = commentModal.body.trim()
|
|
1034
|
+
if (body.length === 0) {
|
|
1035
|
+
setCommentModal((current) => ({ ...current, error: "Write a comment before saving." }))
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const threadKey = selectedDiffCommentThreadKey
|
|
1040
|
+
const target = selectedDiffCommentAnchor
|
|
1041
|
+
const optimisticComment = {
|
|
1042
|
+
id: `local:${Date.now()}`,
|
|
1043
|
+
path: target.path,
|
|
1044
|
+
line: target.line,
|
|
1045
|
+
side: target.side,
|
|
1046
|
+
author: username ?? "you",
|
|
1047
|
+
body,
|
|
1048
|
+
createdAt: new Date(),
|
|
1049
|
+
url: null,
|
|
1050
|
+
} satisfies PullRequestReviewComment
|
|
1051
|
+
const input = {
|
|
1052
|
+
repository: selectedPullRequest.repository,
|
|
1053
|
+
number: selectedPullRequest.number,
|
|
1054
|
+
commitId: selectedPullRequest.headRefOid,
|
|
1055
|
+
path: target.path,
|
|
1056
|
+
line: target.line,
|
|
1057
|
+
side: target.side,
|
|
1058
|
+
body,
|
|
1059
|
+
} satisfies CreatePullRequestCommentInput
|
|
1060
|
+
|
|
1061
|
+
if (threadKey) {
|
|
1062
|
+
setDiffCommentThreads((current) => ({
|
|
1063
|
+
...current,
|
|
1064
|
+
[threadKey]: [...(current[threadKey] ?? []), optimisticComment],
|
|
1065
|
+
}))
|
|
1066
|
+
}
|
|
1067
|
+
closeActiveModal()
|
|
1068
|
+
flashNotice(`Commenting on ${target.path}:${target.line}`)
|
|
1069
|
+
void createPullRequestComment(input).then((comment) => {
|
|
1070
|
+
if (threadKey) {
|
|
1071
|
+
setDiffCommentThreads((current) => ({
|
|
1072
|
+
...current,
|
|
1073
|
+
[threadKey]: (current[threadKey] ?? []).map((existing) => existing.id === optimisticComment.id ? comment : existing),
|
|
1074
|
+
}))
|
|
1075
|
+
}
|
|
1076
|
+
flashNotice(`Commented on ${target.path}:${target.line}`)
|
|
1077
|
+
}).catch((error) => {
|
|
1078
|
+
if (threadKey) {
|
|
1079
|
+
setDiffCommentThreads((current) => {
|
|
1080
|
+
const next = { ...current }
|
|
1081
|
+
const comments = (next[threadKey] ?? []).filter((comment) => comment.id !== optimisticComment.id)
|
|
1082
|
+
if (comments.length > 0) next[threadKey] = comments
|
|
1083
|
+
else delete next[threadKey]
|
|
1084
|
+
return next
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
flashNotice(errorMessage(error))
|
|
1088
|
+
})
|
|
599
1089
|
}
|
|
600
1090
|
|
|
601
1091
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
@@ -604,13 +1094,34 @@ export const App = () => {
|
|
|
604
1094
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
605
1095
|
}
|
|
606
1096
|
|
|
1097
|
+
const copySelectedPullRequestMetadata = () => {
|
|
1098
|
+
if (!selectedPullRequest) return
|
|
1099
|
+
void copyPullRequestMetadata(selectedPullRequest)
|
|
1100
|
+
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1101
|
+
.catch((error) => flashNotice(errorMessage(error)))
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const toggleSelectedPullRequestDraftStatus = () => {
|
|
1105
|
+
if (!selectedPullRequest) return
|
|
1106
|
+
const previousPullRequest = selectedPullRequest
|
|
1107
|
+
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1108
|
+
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1109
|
+
...pullRequest,
|
|
1110
|
+
reviewStatus: nextReviewStatus,
|
|
1111
|
+
}))
|
|
1112
|
+
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1113
|
+
.then(() => {
|
|
1114
|
+
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1115
|
+
})
|
|
1116
|
+
.catch((error) => {
|
|
1117
|
+
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1118
|
+
flashNotice(errorMessage(error))
|
|
1119
|
+
})
|
|
1120
|
+
}
|
|
1121
|
+
|
|
607
1122
|
const openCloseModal = () => {
|
|
608
1123
|
if (!selectedPullRequest || selectedPullRequest.state !== "open") return
|
|
609
|
-
setLabelModal(initialLabelModalState)
|
|
610
|
-
setMergeModal(initialMergeModalState)
|
|
611
|
-
setThemeModal(initialThemeModalState)
|
|
612
1124
|
setCloseModal({
|
|
613
|
-
open: true,
|
|
614
1125
|
repository: selectedPullRequest.repository,
|
|
615
1126
|
number: selectedPullRequest.number,
|
|
616
1127
|
title: selectedPullRequest.title,
|
|
@@ -639,7 +1150,7 @@ export const App = () => {
|
|
|
639
1150
|
},
|
|
640
1151
|
}))
|
|
641
1152
|
}
|
|
642
|
-
|
|
1153
|
+
closeActiveModal()
|
|
643
1154
|
refreshPullRequests(`Closed #${number}`)
|
|
644
1155
|
})
|
|
645
1156
|
.catch((error) => {
|
|
@@ -649,11 +1160,7 @@ export const App = () => {
|
|
|
649
1160
|
}
|
|
650
1161
|
|
|
651
1162
|
const openThemeModal = () => {
|
|
652
|
-
setLabelModal(initialLabelModalState)
|
|
653
|
-
setCloseModal(initialCloseModalState)
|
|
654
|
-
setMergeModal(initialMergeModalState)
|
|
655
1163
|
setThemeModal({
|
|
656
|
-
open: true,
|
|
657
1164
|
query: "",
|
|
658
1165
|
filterMode: false,
|
|
659
1166
|
initialThemeId: themeId,
|
|
@@ -668,7 +1175,7 @@ export const App = () => {
|
|
|
668
1175
|
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
669
1176
|
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
670
1177
|
}
|
|
671
|
-
|
|
1178
|
+
closeActiveModal()
|
|
672
1179
|
}
|
|
673
1180
|
|
|
674
1181
|
const previewTheme = (id: ThemeId) => {
|
|
@@ -711,14 +1218,10 @@ export const App = () => {
|
|
|
711
1218
|
|
|
712
1219
|
const openLabelModal = () => {
|
|
713
1220
|
if (!selectedPullRequest) return
|
|
714
|
-
setCloseModal(initialCloseModalState)
|
|
715
|
-
setMergeModal(initialMergeModalState)
|
|
716
|
-
setThemeModal(initialThemeModalState)
|
|
717
1221
|
const repository = selectedPullRequest.repository
|
|
718
1222
|
const cachedLabels = labelCache[repository]
|
|
719
1223
|
if (cachedLabels) {
|
|
720
1224
|
setLabelModal({
|
|
721
|
-
open: true,
|
|
722
1225
|
repository,
|
|
723
1226
|
query: "",
|
|
724
1227
|
selectedIndex: 0,
|
|
@@ -728,7 +1231,7 @@ export const App = () => {
|
|
|
728
1231
|
return
|
|
729
1232
|
}
|
|
730
1233
|
|
|
731
|
-
setLabelModal(
|
|
1234
|
+
setLabelModal({ repository, query: "", selectedIndex: 0, availableLabels: [], loading: true })
|
|
732
1235
|
void loadRepoLabels(repository)
|
|
733
1236
|
.then((labels) => {
|
|
734
1237
|
setLabelCache((current) => ({ ...current, [repository]: labels }))
|
|
@@ -736,20 +1239,16 @@ export const App = () => {
|
|
|
736
1239
|
})
|
|
737
1240
|
.catch((error) => {
|
|
738
1241
|
setLabelModal((current) => current.repository === repository ? { ...current, loading: false } : current)
|
|
739
|
-
flashNotice(
|
|
1242
|
+
flashNotice(errorMessage(error))
|
|
740
1243
|
})
|
|
741
1244
|
}
|
|
742
1245
|
|
|
743
1246
|
const openMergeModal = () => {
|
|
744
1247
|
if (!selectedPullRequest) return
|
|
745
|
-
setCloseModal(initialCloseModalState)
|
|
746
|
-
setThemeModal(initialThemeModalState)
|
|
747
1248
|
const repository = selectedPullRequest.repository
|
|
748
1249
|
const number = selectedPullRequest.number
|
|
749
1250
|
const seededInfo = mergeInfoFromPullRequest(selectedPullRequest)
|
|
750
|
-
setLabelModal(initialLabelModalState)
|
|
751
1251
|
setMergeModal({
|
|
752
|
-
open: true,
|
|
753
1252
|
repository,
|
|
754
1253
|
number,
|
|
755
1254
|
selectedIndex: 0,
|
|
@@ -803,7 +1302,7 @@ export const App = () => {
|
|
|
803
1302
|
},
|
|
804
1303
|
}))
|
|
805
1304
|
}
|
|
806
|
-
|
|
1305
|
+
closeActiveModal()
|
|
807
1306
|
if (option.refreshOnSuccess) {
|
|
808
1307
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
809
1308
|
} else {
|
|
@@ -837,7 +1336,7 @@ export const App = () => {
|
|
|
837
1336
|
.then(() => flashNotice(`Removed ${label.name} from #${selectedPullRequest.number}`))
|
|
838
1337
|
.catch((error) => {
|
|
839
1338
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
840
|
-
flashNotice(
|
|
1339
|
+
flashNotice(errorMessage(error))
|
|
841
1340
|
})
|
|
842
1341
|
} else {
|
|
843
1342
|
updatePullRequest(selectedPullRequest.url, (pr) => ({
|
|
@@ -848,34 +1347,26 @@ export const App = () => {
|
|
|
848
1347
|
.then(() => flashNotice(`Added ${label.name} to #${selectedPullRequest.number}`))
|
|
849
1348
|
.catch((error) => {
|
|
850
1349
|
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
851
|
-
flashNotice(
|
|
1350
|
+
flashNotice(errorMessage(error))
|
|
852
1351
|
})
|
|
853
1352
|
}
|
|
854
1353
|
}
|
|
855
1354
|
|
|
856
1355
|
useKeyboard((key) => {
|
|
857
|
-
if ((key.name === "q" && !(
|
|
858
|
-
if (
|
|
1356
|
+
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1357
|
+
if (themeModalActive) {
|
|
859
1358
|
closeThemeModal(false)
|
|
860
1359
|
return
|
|
861
1360
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
return
|
|
865
|
-
}
|
|
866
|
-
if (mergeModal.open) {
|
|
867
|
-
setMergeModal(initialMergeModalState)
|
|
868
|
-
return
|
|
869
|
-
}
|
|
870
|
-
if (labelModal.open) {
|
|
871
|
-
setLabelModal(initialLabelModalState)
|
|
1361
|
+
if (activeModal._tag !== "None") {
|
|
1362
|
+
closeActiveModal()
|
|
872
1363
|
return
|
|
873
1364
|
}
|
|
874
1365
|
renderer.destroy()
|
|
875
1366
|
return
|
|
876
1367
|
}
|
|
877
1368
|
|
|
878
|
-
if (
|
|
1369
|
+
if (themeModalActive) {
|
|
879
1370
|
if (key.name === "escape") {
|
|
880
1371
|
if (themeModal.filterMode) {
|
|
881
1372
|
updateThemeQuery("", { filterMode: false })
|
|
@@ -916,9 +1407,137 @@ export const App = () => {
|
|
|
916
1407
|
return
|
|
917
1408
|
}
|
|
918
1409
|
|
|
919
|
-
if (
|
|
1410
|
+
if (commentModalActive) {
|
|
1411
|
+
if (key.name === "escape") {
|
|
1412
|
+
closeActiveModal()
|
|
1413
|
+
return
|
|
1414
|
+
}
|
|
1415
|
+
if (key.ctrl && key.name === "s") {
|
|
1416
|
+
submitDiffComment()
|
|
1417
|
+
return
|
|
1418
|
+
}
|
|
1419
|
+
if (key.ctrl && key.name === "a") {
|
|
1420
|
+
editComment(moveLineStart)
|
|
1421
|
+
return
|
|
1422
|
+
}
|
|
1423
|
+
if (key.ctrl && key.name === "e") {
|
|
1424
|
+
editComment(moveLineEnd)
|
|
1425
|
+
return
|
|
1426
|
+
}
|
|
1427
|
+
if (key.ctrl && key.name === "b") {
|
|
1428
|
+
editComment(editorMoveLeft)
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
if (key.ctrl && key.name === "f") {
|
|
1432
|
+
editComment(editorMoveRight)
|
|
1433
|
+
return
|
|
1434
|
+
}
|
|
1435
|
+
if (key.ctrl && key.name === "w") {
|
|
1436
|
+
editComment(deleteWordBackward)
|
|
1437
|
+
return
|
|
1438
|
+
}
|
|
1439
|
+
if (key.ctrl && key.name === "u") {
|
|
1440
|
+
editComment(deleteToLineStart)
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1443
|
+
if (key.ctrl && key.name === "k") {
|
|
1444
|
+
editComment(deleteToLineEnd)
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
if (key.ctrl && key.name === "d") {
|
|
1448
|
+
editComment(editorDeleteForward)
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
if ((key.meta || key.option) && (key.name === "b" || key.name === "left")) {
|
|
1452
|
+
editComment(moveWordBackward)
|
|
1453
|
+
return
|
|
1454
|
+
}
|
|
1455
|
+
if ((key.meta || key.option) && (key.name === "f" || key.name === "right")) {
|
|
1456
|
+
editComment(moveWordForward)
|
|
1457
|
+
return
|
|
1458
|
+
}
|
|
1459
|
+
if ((key.meta || key.option) && (key.name === "backspace" || key.name === "delete")) {
|
|
1460
|
+
editComment(key.name === "delete" ? deleteWordForward : deleteWordBackward)
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
if (key.name === "backspace") {
|
|
1464
|
+
editComment(editorBackspace)
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
if (key.name === "delete") {
|
|
1468
|
+
editComment(editorDeleteForward)
|
|
1469
|
+
return
|
|
1470
|
+
}
|
|
1471
|
+
if (key.name === "left") {
|
|
1472
|
+
editComment(editorMoveLeft)
|
|
1473
|
+
return
|
|
1474
|
+
}
|
|
1475
|
+
if (key.name === "right") {
|
|
1476
|
+
editComment(editorMoveRight)
|
|
1477
|
+
return
|
|
1478
|
+
}
|
|
1479
|
+
if (key.name === "up") {
|
|
1480
|
+
editComment((state) => moveVertically(state, -1))
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
if (key.name === "down") {
|
|
1484
|
+
editComment((state) => moveVertically(state, 1))
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
if (key.name === "home") {
|
|
1488
|
+
editComment(moveLineStart)
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
if (key.name === "end") {
|
|
1492
|
+
editComment(moveLineEnd)
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
if ((key.name === "return" || key.name === "enter") && key.shift) {
|
|
1496
|
+
editComment((state) => insertText(state, "\n"))
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1500
|
+
submitDiffComment()
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1504
|
+
editComment((state) => insertText(state, key.sequence))
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
return
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (commentThreadModalActive) {
|
|
1511
|
+
if (key.name === "escape") {
|
|
1512
|
+
closeActiveModal()
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1515
|
+
if (key.name === "return" || key.name === "enter" || key.name === "a" || key.name === "c") {
|
|
1516
|
+
openDiffCommentModal()
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
if (key.name === "up" || key.name === "k") {
|
|
1520
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - 1) }))
|
|
1521
|
+
return
|
|
1522
|
+
}
|
|
1523
|
+
if (key.name === "down" || key.name === "j") {
|
|
1524
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + 1 }))
|
|
1525
|
+
return
|
|
1526
|
+
}
|
|
1527
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1528
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - halfPage) }))
|
|
1529
|
+
return
|
|
1530
|
+
}
|
|
1531
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1532
|
+
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + halfPage }))
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
return
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (closeModalActive) {
|
|
920
1539
|
if (key.name === "escape") {
|
|
921
|
-
|
|
1540
|
+
closeActiveModal()
|
|
922
1541
|
return
|
|
923
1542
|
}
|
|
924
1543
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -928,10 +1547,10 @@ export const App = () => {
|
|
|
928
1547
|
return
|
|
929
1548
|
}
|
|
930
1549
|
|
|
931
|
-
if (
|
|
1550
|
+
if (mergeModalActive) {
|
|
932
1551
|
const options = availableMergeActions(mergeModal.info)
|
|
933
1552
|
if (key.name === "escape") {
|
|
934
|
-
|
|
1553
|
+
closeActiveModal()
|
|
935
1554
|
return
|
|
936
1555
|
}
|
|
937
1556
|
if ((key.name === "return" || key.name === "enter") && options.length > 0) {
|
|
@@ -956,9 +1575,9 @@ export const App = () => {
|
|
|
956
1575
|
}
|
|
957
1576
|
|
|
958
1577
|
// Label modal takes priority over everything else
|
|
959
|
-
if (
|
|
1578
|
+
if (labelModalActive) {
|
|
960
1579
|
if (key.name === "escape") {
|
|
961
|
-
|
|
1580
|
+
closeActiveModal()
|
|
962
1581
|
return
|
|
963
1582
|
}
|
|
964
1583
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -1006,28 +1625,94 @@ export const App = () => {
|
|
|
1006
1625
|
}
|
|
1007
1626
|
|
|
1008
1627
|
if (diffFullView) {
|
|
1628
|
+
if (diffCommentMode) {
|
|
1629
|
+
if (key.name === "escape") {
|
|
1630
|
+
setDiffCommentMode(false)
|
|
1631
|
+
return
|
|
1632
|
+
}
|
|
1633
|
+
if (key.name === "c") {
|
|
1634
|
+
setDiffCommentMode(false)
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1637
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1638
|
+
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
1639
|
+
else openDiffCommentModal()
|
|
1640
|
+
return
|
|
1641
|
+
}
|
|
1642
|
+
if (key.name === "a") {
|
|
1643
|
+
openDiffCommentModal()
|
|
1644
|
+
return
|
|
1645
|
+
}
|
|
1646
|
+
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1647
|
+
moveDiffCommentAnchor(-halfPage)
|
|
1648
|
+
return
|
|
1649
|
+
}
|
|
1650
|
+
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1651
|
+
moveDiffCommentAnchor(halfPage)
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1654
|
+
if ((key.shift || key.option || key.meta) && (key.name === "up" || key.name === "k") || key.name === "K") {
|
|
1655
|
+
moveDiffCommentAnchor(-8)
|
|
1656
|
+
return
|
|
1657
|
+
}
|
|
1658
|
+
if ((key.shift || key.option || key.meta) && (key.name === "down" || key.name === "j") || key.name === "J") {
|
|
1659
|
+
moveDiffCommentAnchor(8)
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1662
|
+
if (key.name === "up" || key.name === "k") {
|
|
1663
|
+
moveDiffCommentAnchor(-1)
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
if (key.name === "down" || key.name === "j") {
|
|
1667
|
+
moveDiffCommentAnchor(1)
|
|
1668
|
+
return
|
|
1669
|
+
}
|
|
1670
|
+
if (key.name === "left" || key.name === "h") {
|
|
1671
|
+
selectDiffCommentSide("LEFT")
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1674
|
+
if (key.name === "right" || key.name === "l") {
|
|
1675
|
+
selectDiffCommentSide("RIGHT")
|
|
1676
|
+
return
|
|
1677
|
+
}
|
|
1678
|
+
if (key.name === "]" && selectedDiffState?.status === "ready") {
|
|
1679
|
+
jumpDiffFile(1)
|
|
1680
|
+
return
|
|
1681
|
+
}
|
|
1682
|
+
if (key.name === "[" && selectedDiffState?.status === "ready") {
|
|
1683
|
+
jumpDiffFile(-1)
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1009
1689
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
1010
1690
|
setDiffFullView(false)
|
|
1691
|
+
setDiffCommentMode(false)
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
if (key.name === "c" && selectedDiffState?.status === "ready") {
|
|
1695
|
+
enterDiffCommentMode()
|
|
1011
1696
|
return
|
|
1012
1697
|
}
|
|
1013
1698
|
if (key.name === "home") {
|
|
1014
|
-
|
|
1699
|
+
scrollDiffTo(0)
|
|
1015
1700
|
return
|
|
1016
1701
|
}
|
|
1017
1702
|
if (key.name === "end") {
|
|
1018
|
-
|
|
1703
|
+
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
1019
1704
|
return
|
|
1020
1705
|
}
|
|
1021
1706
|
if (key.name === "pageup") {
|
|
1022
|
-
|
|
1707
|
+
scrollDiffBy(-halfPage)
|
|
1023
1708
|
return
|
|
1024
1709
|
}
|
|
1025
1710
|
if (key.name === "pagedown") {
|
|
1026
|
-
|
|
1711
|
+
scrollDiffBy(halfPage)
|
|
1027
1712
|
return
|
|
1028
1713
|
}
|
|
1029
1714
|
if (isShiftG(key)) {
|
|
1030
|
-
|
|
1715
|
+
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
1031
1716
|
setPendingG(false)
|
|
1032
1717
|
if (pendingGTimeoutRef.current !== null) {
|
|
1033
1718
|
clearTimeout(pendingGTimeoutRef.current)
|
|
@@ -1037,7 +1722,7 @@ export const App = () => {
|
|
|
1037
1722
|
}
|
|
1038
1723
|
if (key.name === "g") {
|
|
1039
1724
|
if (pendingG) {
|
|
1040
|
-
|
|
1725
|
+
scrollDiffTo(0)
|
|
1041
1726
|
setPendingG(false)
|
|
1042
1727
|
if (pendingGTimeoutRef.current !== null) {
|
|
1043
1728
|
clearTimeout(pendingGTimeoutRef.current)
|
|
@@ -1053,19 +1738,19 @@ export const App = () => {
|
|
|
1053
1738
|
return
|
|
1054
1739
|
}
|
|
1055
1740
|
if (key.name === "up" || key.name === "k") {
|
|
1056
|
-
|
|
1741
|
+
scrollDiffBy(-1)
|
|
1057
1742
|
return
|
|
1058
1743
|
}
|
|
1059
1744
|
if (key.name === "down" || key.name === "j") {
|
|
1060
|
-
|
|
1745
|
+
scrollDiffBy(1)
|
|
1061
1746
|
return
|
|
1062
1747
|
}
|
|
1063
1748
|
if (key.ctrl && key.name === "u") {
|
|
1064
|
-
|
|
1749
|
+
scrollDiffBy(-halfPage)
|
|
1065
1750
|
return
|
|
1066
1751
|
}
|
|
1067
1752
|
if (key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1068
|
-
|
|
1753
|
+
scrollDiffBy(halfPage)
|
|
1069
1754
|
return
|
|
1070
1755
|
}
|
|
1071
1756
|
if (key.name === "v") {
|
|
@@ -1077,18 +1762,16 @@ export const App = () => {
|
|
|
1077
1762
|
return
|
|
1078
1763
|
}
|
|
1079
1764
|
if (key.name === "r" && selectedPullRequest) {
|
|
1080
|
-
loadPullRequestDiff(selectedPullRequest, true)
|
|
1765
|
+
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
1081
1766
|
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1082
1767
|
return
|
|
1083
1768
|
}
|
|
1084
1769
|
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?.status === "ready") {
|
|
1085
|
-
|
|
1086
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1770
|
+
jumpDiffFile(1)
|
|
1087
1771
|
return
|
|
1088
1772
|
}
|
|
1089
1773
|
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?.status === "ready") {
|
|
1090
|
-
|
|
1091
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1774
|
+
jumpDiffFile(-1)
|
|
1092
1775
|
return
|
|
1093
1776
|
}
|
|
1094
1777
|
if (key.name === "o" && selectedPullRequest) {
|
|
@@ -1100,11 +1783,40 @@ export const App = () => {
|
|
|
1100
1783
|
|
|
1101
1784
|
// Fullscreen detail mode handles its own navigation keys.
|
|
1102
1785
|
if (detailFullView) {
|
|
1786
|
+
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
1103
1787
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1104
1788
|
setDetailFullView(false)
|
|
1105
1789
|
setDetailScrollOffset(0)
|
|
1106
1790
|
return
|
|
1107
1791
|
}
|
|
1792
|
+
if (isThemeKey(key)) {
|
|
1793
|
+
openThemeModal()
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1796
|
+
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1797
|
+
openDiffView()
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1801
|
+
openCloseModal()
|
|
1802
|
+
return
|
|
1803
|
+
}
|
|
1804
|
+
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1805
|
+
openLabelModal()
|
|
1806
|
+
return
|
|
1807
|
+
}
|
|
1808
|
+
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1809
|
+
openMergeModal()
|
|
1810
|
+
return
|
|
1811
|
+
}
|
|
1812
|
+
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1813
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1814
|
+
return
|
|
1815
|
+
}
|
|
1816
|
+
if (plainKey && key.name === "r") {
|
|
1817
|
+
refreshPullRequests("Refreshed")
|
|
1818
|
+
return
|
|
1819
|
+
}
|
|
1108
1820
|
if (key.name === "home") {
|
|
1109
1821
|
detailScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1110
1822
|
setDetailScrollOffset(0)
|
|
@@ -1168,14 +1880,12 @@ export const App = () => {
|
|
|
1168
1880
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1169
1881
|
return
|
|
1170
1882
|
}
|
|
1171
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
1883
|
+
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1172
1884
|
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1173
1885
|
return
|
|
1174
1886
|
}
|
|
1175
|
-
if (key.name === "y" && selectedPullRequest) {
|
|
1176
|
-
|
|
1177
|
-
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1178
|
-
.catch((error) => flashNotice(error instanceof Error ? error.message : String(error)))
|
|
1887
|
+
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1888
|
+
copySelectedPullRequestMetadata()
|
|
1179
1889
|
return
|
|
1180
1890
|
}
|
|
1181
1891
|
return
|
|
@@ -1210,6 +1920,11 @@ export const App = () => {
|
|
|
1210
1920
|
}
|
|
1211
1921
|
}
|
|
1212
1922
|
|
|
1923
|
+
if (key.name === "tab") {
|
|
1924
|
+
switchQueueMode(key.shift ? -1 : 1)
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1213
1928
|
if (isThemeKey(key)) {
|
|
1214
1929
|
openThemeModal()
|
|
1215
1930
|
return
|
|
@@ -1316,7 +2031,7 @@ export const App = () => {
|
|
|
1316
2031
|
setDetailScrollOffset(0)
|
|
1317
2032
|
return
|
|
1318
2033
|
}
|
|
1319
|
-
if (
|
|
2034
|
+
if (key.name === "d" && selectedPullRequest) {
|
|
1320
2035
|
openDiffView()
|
|
1321
2036
|
return
|
|
1322
2037
|
}
|
|
@@ -1337,30 +2052,12 @@ export const App = () => {
|
|
|
1337
2052
|
return
|
|
1338
2053
|
}
|
|
1339
2054
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1340
|
-
|
|
1341
|
-
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1342
|
-
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1343
|
-
...pullRequest,
|
|
1344
|
-
reviewStatus: nextReviewStatus,
|
|
1345
|
-
}))
|
|
1346
|
-
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1347
|
-
.then(() => {
|
|
1348
|
-
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1349
|
-
})
|
|
1350
|
-
.catch((error) => {
|
|
1351
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1352
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1353
|
-
})
|
|
2055
|
+
toggleSelectedPullRequestDraftStatus()
|
|
1354
2056
|
return
|
|
1355
2057
|
}
|
|
1356
2058
|
if (key.name === "y" && selectedPullRequest) {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
flashNotice(`Copied #${selectedPullRequest.number} metadata`)
|
|
1360
|
-
})
|
|
1361
|
-
.catch((error) => {
|
|
1362
|
-
flashNotice(error instanceof Error ? error.message : String(error))
|
|
1363
|
-
})
|
|
2059
|
+
copySelectedPullRequestMetadata()
|
|
2060
|
+
return
|
|
1364
2061
|
}
|
|
1365
2062
|
})
|
|
1366
2063
|
|
|
@@ -1397,20 +2094,31 @@ export const App = () => {
|
|
|
1397
2094
|
const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
|
|
1398
2095
|
const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
|
|
1399
2096
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
1400
|
-
const labelModalLeft =
|
|
1401
|
-
const labelModalTop =
|
|
2097
|
+
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2098
|
+
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
1402
2099
|
const closeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1403
2100
|
const closeModalHeight = Math.min(12, terminalHeight - 4)
|
|
1404
|
-
const closeModalLeft =
|
|
1405
|
-
const closeModalTop =
|
|
2101
|
+
const closeModalLeft = centeredOffset(contentWidth, closeModalWidth)
|
|
2102
|
+
const closeModalTop = centeredOffset(terminalHeight, closeModalHeight)
|
|
2103
|
+
const commentModalWidth = Math.min(76, Math.max(46, contentWidth - 8))
|
|
2104
|
+
const commentModalHeight = Math.min(16, terminalHeight - 4)
|
|
2105
|
+
const commentModalLeft = centeredOffset(contentWidth, commentModalWidth)
|
|
2106
|
+
const commentModalTop = centeredOffset(terminalHeight, commentModalHeight)
|
|
2107
|
+
const commentThreadModalWidth = Math.min(86, Math.max(50, contentWidth - 8))
|
|
2108
|
+
const commentThreadModalHeight = Math.min(22, terminalHeight - 4)
|
|
2109
|
+
const commentThreadModalLeft = centeredOffset(contentWidth, commentThreadModalWidth)
|
|
2110
|
+
const commentThreadModalTop = centeredOffset(terminalHeight, commentThreadModalHeight)
|
|
2111
|
+
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2112
|
+
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2113
|
+
: "No diff line selected"
|
|
1406
2114
|
const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1407
2115
|
const mergeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1408
|
-
const mergeModalLeft =
|
|
1409
|
-
const mergeModalTop =
|
|
2116
|
+
const mergeModalLeft = centeredOffset(contentWidth, mergeModalWidth)
|
|
2117
|
+
const mergeModalTop = centeredOffset(terminalHeight, mergeModalHeight)
|
|
1410
2118
|
const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
|
|
1411
2119
|
const themeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1412
|
-
const themeModalLeft =
|
|
1413
|
-
const themeModalTop =
|
|
2120
|
+
const themeModalLeft = centeredOffset(contentWidth, themeModalWidth)
|
|
2121
|
+
const themeModalTop = centeredOffset(terminalHeight, themeModalHeight)
|
|
1414
2122
|
|
|
1415
2123
|
return (
|
|
1416
2124
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -1428,6 +2136,7 @@ export const App = () => {
|
|
|
1428
2136
|
<PullRequestDiffPane
|
|
1429
2137
|
pullRequest={selectedPullRequest}
|
|
1430
2138
|
diffState={selectedDiffState}
|
|
2139
|
+
stackedFiles={stackedDiffFiles}
|
|
1431
2140
|
fileIndex={diffFileIndex}
|
|
1432
2141
|
view={effectiveDiffRenderView}
|
|
1433
2142
|
wrapMode={diffWrapMode}
|
|
@@ -1435,6 +2144,11 @@ export const App = () => {
|
|
|
1435
2144
|
height={wideBodyHeight}
|
|
1436
2145
|
loadingIndicator={loadingIndicator}
|
|
1437
2146
|
scrollRef={diffScrollRef}
|
|
2147
|
+
setDiffRef={setDiffRenderableRef}
|
|
2148
|
+
commentMode={diffCommentMode}
|
|
2149
|
+
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2150
|
+
selectedCommentThread={selectedDiffCommentThread}
|
|
2151
|
+
commentCount={selectedDiffCommentCount}
|
|
1438
2152
|
themeId={themeId}
|
|
1439
2153
|
/>
|
|
1440
2154
|
) : isWideLayout && detailFullView ? (
|
|
@@ -1442,6 +2156,7 @@ export const App = () => {
|
|
|
1442
2156
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
1443
2157
|
<DetailsPane
|
|
1444
2158
|
pullRequest={selectedPullRequest}
|
|
2159
|
+
viewerUsername={username}
|
|
1445
2160
|
contentWidth={fullscreenContentWidth}
|
|
1446
2161
|
bodyLines={fullscreenBodyLines}
|
|
1447
2162
|
paneWidth={contentWidth}
|
|
@@ -1463,7 +2178,7 @@ export const App = () => {
|
|
|
1463
2178
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
1464
2179
|
{selectedPullRequest ? (
|
|
1465
2180
|
<>
|
|
1466
|
-
<DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2181
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
1467
2182
|
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
1468
2183
|
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1469
2184
|
</scrollbox>
|
|
@@ -1478,6 +2193,7 @@ export const App = () => {
|
|
|
1478
2193
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: narrowFullscreenDetailScrollable }}>
|
|
1479
2194
|
<DetailsPane
|
|
1480
2195
|
pullRequest={selectedPullRequest}
|
|
2196
|
+
viewerUsername={username}
|
|
1481
2197
|
contentWidth={fullscreenContentWidth}
|
|
1482
2198
|
bodyLines={fullscreenBodyLines}
|
|
1483
2199
|
paneWidth={contentWidth}
|
|
@@ -1489,7 +2205,7 @@ export const App = () => {
|
|
|
1489
2205
|
</box>
|
|
1490
2206
|
) : (
|
|
1491
2207
|
<box height={wideBodyHeight} flexDirection="column">
|
|
1492
|
-
<DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2208
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1493
2209
|
<Divider width={contentWidth} />
|
|
1494
2210
|
<box flexGrow={1} flexDirection="column">
|
|
1495
2211
|
<scrollbox flexGrow={1}>
|
|
@@ -1515,6 +2231,7 @@ export const App = () => {
|
|
|
1515
2231
|
showFilterClear={filterMode || filterQuery.length > 0}
|
|
1516
2232
|
detailFullView={detailFullView}
|
|
1517
2233
|
diffFullView={diffFullView}
|
|
2234
|
+
diffCommentMode={diffCommentMode}
|
|
1518
2235
|
hasSelection={selectedPullRequest !== null}
|
|
1519
2236
|
canCloseSelection={selectedPullRequest?.state === "open"}
|
|
1520
2237
|
hasError={pullRequestStatus === "error"}
|
|
@@ -1524,7 +2241,7 @@ export const App = () => {
|
|
|
1524
2241
|
/>
|
|
1525
2242
|
)}
|
|
1526
2243
|
</box>
|
|
1527
|
-
{
|
|
2244
|
+
{labelModalActive ? (
|
|
1528
2245
|
<LabelModal
|
|
1529
2246
|
state={labelModal}
|
|
1530
2247
|
currentLabels={selectedPullRequest?.labels ?? []}
|
|
@@ -1535,7 +2252,7 @@ export const App = () => {
|
|
|
1535
2252
|
loadingIndicator={loadingIndicator}
|
|
1536
2253
|
/>
|
|
1537
2254
|
) : null}
|
|
1538
|
-
{
|
|
2255
|
+
{closeModalActive ? (
|
|
1539
2256
|
<CloseModal
|
|
1540
2257
|
state={closeModal}
|
|
1541
2258
|
modalWidth={closeModalWidth}
|
|
@@ -1545,7 +2262,28 @@ export const App = () => {
|
|
|
1545
2262
|
loadingIndicator={loadingIndicator}
|
|
1546
2263
|
/>
|
|
1547
2264
|
) : null}
|
|
1548
|
-
{
|
|
2265
|
+
{commentModalActive ? (
|
|
2266
|
+
<CommentModal
|
|
2267
|
+
state={commentModal}
|
|
2268
|
+
anchorLabel={commentAnchorLabel}
|
|
2269
|
+
modalWidth={commentModalWidth}
|
|
2270
|
+
modalHeight={commentModalHeight}
|
|
2271
|
+
offsetLeft={commentModalLeft}
|
|
2272
|
+
offsetTop={commentModalTop}
|
|
2273
|
+
/>
|
|
2274
|
+
) : null}
|
|
2275
|
+
{commentThreadModalActive ? (
|
|
2276
|
+
<CommentThreadModal
|
|
2277
|
+
state={commentThreadModal}
|
|
2278
|
+
anchorLabel={commentAnchorLabel}
|
|
2279
|
+
comments={selectedDiffCommentThread}
|
|
2280
|
+
modalWidth={commentThreadModalWidth}
|
|
2281
|
+
modalHeight={commentThreadModalHeight}
|
|
2282
|
+
offsetLeft={commentThreadModalLeft}
|
|
2283
|
+
offsetTop={commentThreadModalTop}
|
|
2284
|
+
/>
|
|
2285
|
+
) : null}
|
|
2286
|
+
{mergeModalActive ? (
|
|
1549
2287
|
<MergeModal
|
|
1550
2288
|
state={mergeModal}
|
|
1551
2289
|
modalWidth={mergeModalWidth}
|
|
@@ -1555,7 +2293,7 @@ export const App = () => {
|
|
|
1555
2293
|
loadingIndicator={loadingIndicator}
|
|
1556
2294
|
/>
|
|
1557
2295
|
) : null}
|
|
1558
|
-
{
|
|
2296
|
+
{themeModalActive ? (
|
|
1559
2297
|
<ThemeModal
|
|
1560
2298
|
state={themeModal}
|
|
1561
2299
|
activeThemeId={themeId}
|