@kitlangton/ghui 0.1.18 → 0.1.19
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/package.json +1 -1
- package/src/App.tsx +142 -135
- package/src/domain.ts +35 -14
- package/src/services/GitHubService.ts +39 -37
- package/src/ui/FooterHints.tsx +10 -6
- package/src/ui/PullRequestDiffPane.tsx +80 -41
- package/src/ui/PullRequestList.tsx +9 -10
- package/src/ui/diff.ts +35 -19
- package/src/ui/modals.tsx +7 -7
- package/src/ui/primitives.tsx +2 -2
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -6,7 +6,7 @@ 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 { pullRequestQueueLabels, pullRequestQueueModes, type CreatePullRequestCommentInput, type DiffCommentSide, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestQueueMode, type PullRequestReviewComment } from "./domain.js"
|
|
9
|
+
import { pullRequestQueueLabels, pullRequestQueueModes, type CreatePullRequestCommentInput, type DiffCommentSide, type LoadStatus, 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"
|
|
@@ -14,9 +14,9 @@ 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
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
|
|
17
|
+
import { buildStackedDiffFiles, diffCommentAnchorKey, diffCommentLocationKey, getStackedDiffCommentAnchors, nearestDiffCommentAnchorIndex, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileAtLine, type DiffCommentAnchor, type DiffView, type DiffWrapMode, type StackedDiffCommentAnchor } from "./ui/diff.js"
|
|
18
18
|
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
19
|
-
import { FooterHints,
|
|
19
|
+
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
20
20
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
21
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"
|
|
22
22
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
@@ -26,7 +26,6 @@ import { PullRequestList } from "./ui/PullRequestList.js"
|
|
|
26
26
|
const githubRuntime = Atom.runtime(GitHubService.layer.pipe(Layer.provideMerge(Observability.layer)))
|
|
27
27
|
const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
28
28
|
|
|
29
|
-
type LoadStatus = "loading" | "ready" | "error"
|
|
30
29
|
|
|
31
30
|
interface PullRequestLoad {
|
|
32
31
|
readonly queueMode: PullRequestQueueMode
|
|
@@ -37,7 +36,7 @@ interface PullRequestLoad {
|
|
|
37
36
|
|
|
38
37
|
interface DetailPlaceholderInput {
|
|
39
38
|
readonly status: LoadStatus
|
|
40
|
-
readonly retryProgress: RetryProgress
|
|
39
|
+
readonly retryProgress: RetryProgress
|
|
41
40
|
readonly loadingIndicator: string
|
|
42
41
|
readonly visibleCount: number
|
|
43
42
|
readonly filterText: string
|
|
@@ -59,7 +58,7 @@ type DiffRenderableRuntimeSides = {
|
|
|
59
58
|
|
|
60
59
|
interface AppliedDiffLineColor {
|
|
61
60
|
readonly anchor: StackedDiffCommentAnchor
|
|
62
|
-
readonly view:
|
|
61
|
+
readonly view: DiffView
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
interface AppliedDiffLineColorState {
|
|
@@ -71,6 +70,7 @@ const PR_FETCH_RETRIES = 6
|
|
|
71
70
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
72
71
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
73
72
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
73
|
+
const DIFF_STICKY_HEADER_LINES = 2
|
|
74
74
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
75
75
|
|
|
76
76
|
const mergeCachedDetails = (fresh: readonly PullRequestItem[], cached: readonly PullRequestItem[] | undefined) => {
|
|
@@ -94,7 +94,7 @@ const mergeCachedDetails = (fresh: readonly PullRequestItem[], cached: readonly
|
|
|
94
94
|
})
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const retryProgressAtom = Atom.make<RetryProgress
|
|
97
|
+
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
98
98
|
const queueModeAtom = Atom.make<PullRequestQueueMode>("authored").pipe(Atom.keepAlive)
|
|
99
99
|
const queueLoadCacheAtom = Atom.make<Partial<Record<PullRequestQueueMode, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
100
100
|
const queueSelectionAtom = Atom.make<Partial<Record<PullRequestQueueMode, number>>>({}).pipe(Atom.keepAlive)
|
|
@@ -102,19 +102,19 @@ const pullRequestsAtom = githubRuntime.atom(
|
|
|
102
102
|
GitHubService.use((github) =>
|
|
103
103
|
Effect.gen(function*() {
|
|
104
104
|
const queueMode = yield* Atom.get(queueModeAtom)
|
|
105
|
-
yield* Atom.set(retryProgressAtom,
|
|
105
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
106
106
|
const data = yield* github.listOpenPullRequests(queueMode).pipe(
|
|
107
107
|
Effect.tapError(() =>
|
|
108
|
-
Atom.update(retryProgressAtom, (current) => ({
|
|
109
|
-
attempt: Math.min((current
|
|
108
|
+
Atom.update(retryProgressAtom, (current) => RetryProgress.Retrying({
|
|
109
|
+
attempt: Math.min(RetryProgress.$match(current, { Idle: () => 0, Retrying: ({ attempt }) => attempt }) + 1, PR_FETCH_RETRIES),
|
|
110
110
|
max: PR_FETCH_RETRIES,
|
|
111
111
|
}))
|
|
112
112
|
),
|
|
113
113
|
Effect.retry({ times: PR_FETCH_RETRIES, schedule: Schedule.exponential("300 millis", 2) }),
|
|
114
|
-
Effect.tapError(() => Atom.set(retryProgressAtom,
|
|
114
|
+
Effect.tapError(() => Atom.set(retryProgressAtom, initialRetryProgress)),
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
-
yield* Atom.set(retryProgressAtom,
|
|
117
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
118
118
|
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
119
119
|
const load = {
|
|
120
120
|
queueMode,
|
|
@@ -137,8 +137,9 @@ const detailFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
|
137
137
|
const detailScrollOffsetAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
138
138
|
const diffFullViewAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
139
139
|
const diffFileIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
140
|
-
const
|
|
141
|
-
const
|
|
140
|
+
const diffScrollTopAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
141
|
+
const diffRenderViewAtom = Atom.make<DiffView>("split").pipe(Atom.keepAlive)
|
|
142
|
+
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none").pipe(Atom.keepAlive)
|
|
142
143
|
const diffCommentModeAtom = Atom.make(false).pipe(Atom.keepAlive)
|
|
143
144
|
const diffCommentAnchorIndexAtom = Atom.make(0).pipe(Atom.keepAlive)
|
|
144
145
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
@@ -320,7 +321,31 @@ const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig =
|
|
|
320
321
|
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
const
|
|
324
|
+
const mixHexColor = (color: string, base: string, amount: number) => {
|
|
325
|
+
const parse = (hex: string) => {
|
|
326
|
+
const normalized = hex.replace(/^#/, "")
|
|
327
|
+
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null
|
|
328
|
+
return {
|
|
329
|
+
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
330
|
+
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
331
|
+
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const left = parse(color)
|
|
335
|
+
const right = parse(base)
|
|
336
|
+
if (!left || !right) return color
|
|
337
|
+
const channel = (key: "r" | "g" | "b") => Math.round(left[key] * amount + right[key] * (1 - amount)).toString(16).padStart(2, "0")
|
|
338
|
+
return `#${channel("r")}${channel("g")}${channel("b")}`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const diffCommentGutterColor = (anchor: DiffCommentAnchor, kind: "selected" | "thread") => {
|
|
342
|
+
const accent = kind === "thread"
|
|
343
|
+
? colors.status.pending
|
|
344
|
+
: anchor.side === "RIGHT" ? colors.status.passing : colors.status.failing
|
|
345
|
+
return mixHexColor(accent, originalDiffLineColor(anchor).gutter, 0.45)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
324
349
|
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
325
350
|
if (view === "split") {
|
|
326
351
|
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
@@ -345,7 +370,7 @@ const getDetailPlaceholderContent = ({
|
|
|
345
370
|
if (status === "loading") {
|
|
346
371
|
return {
|
|
347
372
|
title: `${loadingIndicator} Loading pull requests`,
|
|
348
|
-
hint: retryProgress ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
373
|
+
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
349
374
|
}
|
|
350
375
|
}
|
|
351
376
|
|
|
@@ -394,6 +419,7 @@ export const App = () => {
|
|
|
394
419
|
const [_detailScrollOffset, setDetailScrollOffset] = useAtom(detailScrollOffsetAtom)
|
|
395
420
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
396
421
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
422
|
+
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
397
423
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
398
424
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
399
425
|
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
@@ -410,22 +436,21 @@ export const App = () => {
|
|
|
410
436
|
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
411
437
|
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
412
438
|
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
413
|
-
const labelModal: LabelModalState = labelModalActive ? activeModal
|
|
414
|
-
const closeModal: CloseModalState = closeModalActive ? activeModal
|
|
415
|
-
const mergeModal: MergeModalState = mergeModalActive ? activeModal
|
|
416
|
-
const commentModal: CommentModalState = commentModalActive ? activeModal
|
|
417
|
-
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal
|
|
418
|
-
const themeModal: ThemeModalState = themeModalActive ? activeModal
|
|
439
|
+
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
440
|
+
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
441
|
+
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
442
|
+
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
443
|
+
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
444
|
+
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
419
445
|
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
420
446
|
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
421
|
-
const ctor = Modal[tag] as (args:
|
|
447
|
+
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
422
448
|
if (typeof next === "function") {
|
|
423
449
|
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
424
450
|
if (current._tag !== tag) return current
|
|
425
|
-
|
|
426
|
-
return ctor({ state: updater(prev) })
|
|
451
|
+
return ctor(updater(current as unknown as ModalState<Tag>))
|
|
427
452
|
}
|
|
428
|
-
return ctor(
|
|
453
|
+
return ctor(next)
|
|
429
454
|
})
|
|
430
455
|
const setLabelModal = makeModalSetter("Label")
|
|
431
456
|
const setCloseModal = makeModalSetter("Close")
|
|
@@ -566,14 +591,9 @@ export const App = () => {
|
|
|
566
591
|
const selectedPullRequest = visiblePullRequests[selectedIndex] ?? null
|
|
567
592
|
const selectedDiffState = selectedPullRequest ? pullRequestDiffCache[pullRequestDiffKey(selectedPullRequest)] : undefined
|
|
568
593
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
569
|
-
const readyDiffFiles = selectedDiffState?.
|
|
594
|
+
const readyDiffFiles = selectedDiffState?._tag === "Ready" ? selectedDiffState.files : []
|
|
570
595
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
571
596
|
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
597
|
const diffCommentAnchors = useMemo(
|
|
578
598
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
579
599
|
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
@@ -732,6 +752,7 @@ export const App = () => {
|
|
|
732
752
|
|
|
733
753
|
useEffect(() => {
|
|
734
754
|
setDiffFileIndex(0)
|
|
755
|
+
setDiffScrollTop(0)
|
|
735
756
|
setDiffCommentAnchorIndex(0)
|
|
736
757
|
}, [selectedIndex])
|
|
737
758
|
|
|
@@ -773,12 +794,12 @@ export const App = () => {
|
|
|
773
794
|
if (selectedDiffKey) {
|
|
774
795
|
for (const anchor of diffCommentAnchors) {
|
|
775
796
|
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentAnchorKey(anchor)}`]?.length ?? 0) > 0) {
|
|
776
|
-
applyLineColor(anchor,
|
|
797
|
+
applyLineColor(anchor, diffCommentGutterColor(anchor, "thread"))
|
|
777
798
|
}
|
|
778
799
|
}
|
|
779
800
|
}
|
|
780
801
|
if (diffCommentMode && selectedDiffCommentAnchor) {
|
|
781
|
-
applyLineColor(selectedDiffCommentAnchor, selectedDiffCommentAnchor
|
|
802
|
+
applyLineColor(selectedDiffCommentAnchor, diffCommentGutterColor(selectedDiffCommentAnchor, "selected"), true)
|
|
782
803
|
if (suppressNextDiffCommentScrollRef.current) {
|
|
783
804
|
suppressNextDiffCommentScrollRef.current = false
|
|
784
805
|
} else {
|
|
@@ -791,7 +812,7 @@ export const App = () => {
|
|
|
791
812
|
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
792
813
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && pullRequests.some((pullRequest) => pullRequest.state === "open" && !pullRequest.detailLoaded)
|
|
793
814
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
794
|
-
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?.
|
|
815
|
+
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
795
816
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
796
817
|
|
|
797
818
|
useEffect(() => {
|
|
@@ -887,20 +908,20 @@ export const App = () => {
|
|
|
887
908
|
const key = pullRequestDiffKey(pullRequest)
|
|
888
909
|
const existing = pullRequestDiffCache[key]
|
|
889
910
|
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
890
|
-
if (!force && (existing
|
|
911
|
+
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
891
912
|
|
|
892
|
-
setPullRequestDiffCache((current) => ({ ...current, [key]:
|
|
913
|
+
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
893
914
|
void getPullRequestDiff({ repository: pullRequest.repository, number: pullRequest.number })
|
|
894
915
|
.then((patch) => {
|
|
895
916
|
setPullRequestDiffCache((current) => ({
|
|
896
917
|
...current,
|
|
897
|
-
[key]: {
|
|
918
|
+
[key]: PullRequestDiffState.Ready({ patch, files: splitPatchFiles(patch) }),
|
|
898
919
|
}))
|
|
899
920
|
})
|
|
900
921
|
.catch((error) => {
|
|
901
922
|
setPullRequestDiffCache((current) => ({
|
|
902
923
|
...current,
|
|
903
|
-
[key]: {
|
|
924
|
+
[key]: PullRequestDiffState.Error({ error: errorMessage(error) }),
|
|
904
925
|
}))
|
|
905
926
|
flashNotice(errorMessage(error))
|
|
906
927
|
})
|
|
@@ -930,6 +951,7 @@ export const App = () => {
|
|
|
930
951
|
setDetailFullView(false)
|
|
931
952
|
setDiffCommentMode(false)
|
|
932
953
|
setDiffFileIndex(0)
|
|
954
|
+
setDiffScrollTop(0)
|
|
933
955
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
934
956
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
935
957
|
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
@@ -943,35 +965,75 @@ export const App = () => {
|
|
|
943
965
|
const scrollToDiffFile = (index: number) => {
|
|
944
966
|
const stackedFile = stackedDiffFiles[index]
|
|
945
967
|
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
968
|
+
syncDiffScrollState()
|
|
946
969
|
}
|
|
947
970
|
|
|
948
|
-
const
|
|
971
|
+
const syncDiffScrollState = () => {
|
|
949
972
|
const scrollTop = diffScrollRef.current?.scrollTop
|
|
950
973
|
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
951
|
-
|
|
974
|
+
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
975
|
+
const nextIndex = stackedDiffFileAtLine(stackedDiffFiles, scrollTop)?.index ?? 0
|
|
952
976
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
953
977
|
}
|
|
954
978
|
|
|
955
979
|
const scrollDiffBy = (y: number) => {
|
|
956
980
|
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
957
|
-
|
|
981
|
+
syncDiffScrollState()
|
|
958
982
|
}
|
|
959
983
|
|
|
960
984
|
const scrollDiffTo = (y: number) => {
|
|
961
985
|
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
986
|
+
syncDiffScrollState()
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const clearPendingGTimeout = () => {
|
|
990
|
+
if (pendingGTimeoutRef.current !== null) {
|
|
991
|
+
clearTimeout(pendingGTimeoutRef.current)
|
|
992
|
+
pendingGTimeoutRef.current = null
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const handleVimGoto = (key: { readonly name: string; readonly shift?: boolean }, gotoStart: () => void, gotoEnd: () => void): boolean => {
|
|
997
|
+
if (isShiftG(key)) {
|
|
998
|
+
gotoEnd()
|
|
999
|
+
setPendingG(false)
|
|
1000
|
+
clearPendingGTimeout()
|
|
1001
|
+
return true
|
|
1002
|
+
}
|
|
1003
|
+
if (key.name === "g") {
|
|
1004
|
+
if (pendingG) {
|
|
1005
|
+
gotoStart()
|
|
1006
|
+
setPendingG(false)
|
|
1007
|
+
clearPendingGTimeout()
|
|
1008
|
+
} else {
|
|
1009
|
+
setPendingG(true)
|
|
1010
|
+
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1011
|
+
setPendingG(false)
|
|
1012
|
+
pendingGTimeoutRef.current = null
|
|
1013
|
+
}, 500)
|
|
1014
|
+
}
|
|
1015
|
+
return true
|
|
1016
|
+
}
|
|
1017
|
+
return false
|
|
965
1018
|
}
|
|
966
1019
|
|
|
967
1020
|
const ensureDiffLineVisible = (line: number) => {
|
|
968
1021
|
const scroll = diffScrollRef.current
|
|
969
1022
|
if (!scroll) return
|
|
970
1023
|
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
971
|
-
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line)
|
|
972
|
-
if (nextTop !== scroll.scrollTop)
|
|
1024
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line, DIFF_STICKY_HEADER_LINES)
|
|
1025
|
+
if (nextTop !== scroll.scrollTop) {
|
|
1026
|
+
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1027
|
+
syncDiffScrollState()
|
|
1028
|
+
}
|
|
973
1029
|
}
|
|
974
1030
|
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (!diffFullView) return
|
|
1033
|
+
const interval = globalThis.setInterval(syncDiffScrollState, 80)
|
|
1034
|
+
return () => globalThis.clearInterval(interval)
|
|
1035
|
+
}, [diffFullView, stackedDiffFiles])
|
|
1036
|
+
|
|
975
1037
|
const jumpDiffFile = (delta: 1 | -1) => {
|
|
976
1038
|
if (readyDiffFiles.length === 0) return
|
|
977
1039
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
@@ -987,7 +1049,7 @@ export const App = () => {
|
|
|
987
1049
|
const enterDiffCommentMode = () => {
|
|
988
1050
|
const scrollTop = diffScrollRef.current?.scrollTop ?? 0
|
|
989
1051
|
suppressNextDiffCommentScrollRef.current = true
|
|
990
|
-
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop))
|
|
1052
|
+
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop + DIFF_STICKY_HEADER_LINES))
|
|
991
1053
|
setDiffCommentMode(true)
|
|
992
1054
|
}
|
|
993
1055
|
|
|
@@ -1010,6 +1072,16 @@ export const App = () => {
|
|
|
1010
1072
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1011
1073
|
}
|
|
1012
1074
|
|
|
1075
|
+
const selectDiffCommentLine = (renderLine: number, side: DiffCommentSide | null) => {
|
|
1076
|
+
const lineAnchors = diffCommentAnchors.filter((anchor) => anchor.renderLine === renderLine)
|
|
1077
|
+
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1078
|
+
if (!nextAnchor) return
|
|
1079
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
1080
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1081
|
+
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1082
|
+
setDiffCommentMode(true)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1013
1085
|
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1014
1086
|
setCommentModal((current) => {
|
|
1015
1087
|
const next = transform({ body: current.body, cursor: current.cursor })
|
|
@@ -1675,11 +1747,11 @@ export const App = () => {
|
|
|
1675
1747
|
selectDiffCommentSide("RIGHT")
|
|
1676
1748
|
return
|
|
1677
1749
|
}
|
|
1678
|
-
if (key.name === "]" && selectedDiffState?.
|
|
1750
|
+
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
1679
1751
|
jumpDiffFile(1)
|
|
1680
1752
|
return
|
|
1681
1753
|
}
|
|
1682
|
-
if (key.name === "[" && selectedDiffState?.
|
|
1754
|
+
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
1683
1755
|
jumpDiffFile(-1)
|
|
1684
1756
|
return
|
|
1685
1757
|
}
|
|
@@ -1691,7 +1763,7 @@ export const App = () => {
|
|
|
1691
1763
|
setDiffCommentMode(false)
|
|
1692
1764
|
return
|
|
1693
1765
|
}
|
|
1694
|
-
if (key.name === "c" && selectedDiffState?.
|
|
1766
|
+
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
1695
1767
|
enterDiffCommentMode()
|
|
1696
1768
|
return
|
|
1697
1769
|
}
|
|
@@ -1711,32 +1783,7 @@ export const App = () => {
|
|
|
1711
1783
|
scrollDiffBy(halfPage)
|
|
1712
1784
|
return
|
|
1713
1785
|
}
|
|
1714
|
-
if (
|
|
1715
|
-
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
1716
|
-
setPendingG(false)
|
|
1717
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1718
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1719
|
-
pendingGTimeoutRef.current = null
|
|
1720
|
-
}
|
|
1721
|
-
return
|
|
1722
|
-
}
|
|
1723
|
-
if (key.name === "g") {
|
|
1724
|
-
if (pendingG) {
|
|
1725
|
-
scrollDiffTo(0)
|
|
1726
|
-
setPendingG(false)
|
|
1727
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1728
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1729
|
-
pendingGTimeoutRef.current = null
|
|
1730
|
-
}
|
|
1731
|
-
} else {
|
|
1732
|
-
setPendingG(true)
|
|
1733
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1734
|
-
setPendingG(false)
|
|
1735
|
-
pendingGTimeoutRef.current = null
|
|
1736
|
-
}, 500)
|
|
1737
|
-
}
|
|
1738
|
-
return
|
|
1739
|
-
}
|
|
1786
|
+
if (handleVimGoto(key, () => scrollDiffTo(0), () => scrollDiffTo(Number.MAX_SAFE_INTEGER))) return
|
|
1740
1787
|
if (key.name === "up" || key.name === "k") {
|
|
1741
1788
|
scrollDiffBy(-1)
|
|
1742
1789
|
return
|
|
@@ -1766,11 +1813,11 @@ export const App = () => {
|
|
|
1766
1813
|
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1767
1814
|
return
|
|
1768
1815
|
}
|
|
1769
|
-
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?.
|
|
1816
|
+
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
1770
1817
|
jumpDiffFile(1)
|
|
1771
1818
|
return
|
|
1772
1819
|
}
|
|
1773
|
-
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?.
|
|
1820
|
+
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
1774
1821
|
jumpDiffFile(-1)
|
|
1775
1822
|
return
|
|
1776
1823
|
}
|
|
@@ -1822,14 +1869,9 @@ export const App = () => {
|
|
|
1822
1869
|
setDetailScrollOffset(0)
|
|
1823
1870
|
return
|
|
1824
1871
|
}
|
|
1825
|
-
if (key.name === "end"
|
|
1872
|
+
if (key.name === "end") {
|
|
1826
1873
|
detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER })
|
|
1827
1874
|
setDetailScrollOffset(Number.MAX_SAFE_INTEGER)
|
|
1828
|
-
setPendingG(false)
|
|
1829
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1830
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1831
|
-
pendingGTimeoutRef.current = null
|
|
1832
|
-
}
|
|
1833
1875
|
return
|
|
1834
1876
|
}
|
|
1835
1877
|
if (key.name === "pageup") {
|
|
@@ -1842,24 +1884,10 @@ export const App = () => {
|
|
|
1842
1884
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1843
1885
|
return
|
|
1844
1886
|
}
|
|
1845
|
-
if (key
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
setPendingG(false)
|
|
1850
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1851
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1852
|
-
pendingGTimeoutRef.current = null
|
|
1853
|
-
}
|
|
1854
|
-
} else {
|
|
1855
|
-
setPendingG(true)
|
|
1856
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1857
|
-
setPendingG(false)
|
|
1858
|
-
pendingGTimeoutRef.current = null
|
|
1859
|
-
}, 500)
|
|
1860
|
-
}
|
|
1861
|
-
return
|
|
1862
|
-
}
|
|
1887
|
+
if (handleVimGoto(key,
|
|
1888
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: 0 }); setDetailScrollOffset(0) },
|
|
1889
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER }); setDetailScrollOffset(Number.MAX_SAFE_INTEGER) },
|
|
1890
|
+
)) return
|
|
1863
1891
|
if (key.name === "up" || key.name === "k") {
|
|
1864
1892
|
detailScrollRef.current?.scrollBy({ x: 0, y: -1 })
|
|
1865
1893
|
setDetailScrollOffset((current) => Math.max(0, current - 1))
|
|
@@ -2001,31 +2029,10 @@ export const App = () => {
|
|
|
2001
2029
|
})
|
|
2002
2030
|
return
|
|
2003
2031
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
return visiblePullRequests.length - 1
|
|
2009
|
-
})
|
|
2010
|
-
return
|
|
2011
|
-
}
|
|
2012
|
-
if (key.name === "g") {
|
|
2013
|
-
if (pendingG) {
|
|
2014
|
-
setSelectedIndex(0)
|
|
2015
|
-
setPendingG(false)
|
|
2016
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
2017
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
2018
|
-
pendingGTimeoutRef.current = null
|
|
2019
|
-
}
|
|
2020
|
-
} else {
|
|
2021
|
-
setPendingG(true)
|
|
2022
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
2023
|
-
setPendingG(false)
|
|
2024
|
-
pendingGTimeoutRef.current = null
|
|
2025
|
-
}, 500)
|
|
2026
|
-
}
|
|
2027
|
-
return
|
|
2028
|
-
}
|
|
2032
|
+
if (handleVimGoto(key,
|
|
2033
|
+
() => setSelectedIndex(0),
|
|
2034
|
+
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2035
|
+
)) return
|
|
2029
2036
|
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
2030
2037
|
setDetailFullView(true)
|
|
2031
2038
|
setDetailScrollOffset(0)
|
|
@@ -2137,7 +2144,7 @@ export const App = () => {
|
|
|
2137
2144
|
pullRequest={selectedPullRequest}
|
|
2138
2145
|
diffState={selectedDiffState}
|
|
2139
2146
|
stackedFiles={stackedDiffFiles}
|
|
2140
|
-
|
|
2147
|
+
scrollTop={diffScrollTop}
|
|
2141
2148
|
view={effectiveDiffRenderView}
|
|
2142
2149
|
wrapMode={diffWrapMode}
|
|
2143
2150
|
paneWidth={contentWidth}
|
|
@@ -2148,7 +2155,7 @@ export const App = () => {
|
|
|
2148
2155
|
commentMode={diffCommentMode}
|
|
2149
2156
|
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2150
2157
|
selectedCommentThread={selectedDiffCommentThread}
|
|
2151
|
-
|
|
2158
|
+
onSelectCommentLine={selectDiffCommentLine}
|
|
2152
2159
|
themeId={themeId}
|
|
2153
2160
|
/>
|
|
2154
2161
|
) : isWideLayout && detailFullView ? (
|
|
@@ -2168,10 +2175,10 @@ export const App = () => {
|
|
|
2168
2175
|
</scrollbox>
|
|
2169
2176
|
</box>
|
|
2170
2177
|
) : isWideLayout ? (
|
|
2171
|
-
<box flexGrow={1} flexDirection="row">
|
|
2178
|
+
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
2172
2179
|
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column" paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2173
2180
|
<scrollbox height={wideBodyHeight} flexGrow={0}>
|
|
2174
|
-
<PullRequestList {...prListProps} contentWidth={leftContentWidth} />
|
|
2181
|
+
<PullRequestList key={`wide-${leftContentWidth}`} {...prListProps} contentWidth={leftContentWidth} />
|
|
2175
2182
|
</scrollbox>
|
|
2176
2183
|
</box>
|
|
2177
2184
|
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
@@ -2204,13 +2211,13 @@ export const App = () => {
|
|
|
2204
2211
|
</scrollbox>
|
|
2205
2212
|
</box>
|
|
2206
2213
|
) : (
|
|
2207
|
-
<box height={wideBodyHeight} flexDirection="column">
|
|
2208
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={
|
|
2214
|
+
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2215
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2209
2216
|
<Divider width={contentWidth} />
|
|
2210
2217
|
<box flexGrow={1} flexDirection="column">
|
|
2211
2218
|
<scrollbox flexGrow={1}>
|
|
2212
2219
|
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2213
|
-
<PullRequestList {...prListProps} contentWidth={
|
|
2220
|
+
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
2214
2221
|
</box>
|
|
2215
2222
|
</scrollbox>
|
|
2216
2223
|
</box>
|
package/src/domain.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { Schema } from "effect"
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const LoadStatus = Schema.Literals(["loading", "ready", "error"])
|
|
4
|
+
export type LoadStatus = Schema.Schema.Type<typeof LoadStatus>
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export const PullRequestState = Schema.Literals(["open", "closed", "merged"])
|
|
7
|
+
export type PullRequestState = Schema.Schema.Type<typeof PullRequestState>
|
|
8
|
+
|
|
9
|
+
export const PullRequestQueueMode = Schema.Literals(["authored", "review", "assigned", "mentioned"])
|
|
10
|
+
export type PullRequestQueueMode = Schema.Schema.Type<typeof PullRequestQueueMode>
|
|
11
|
+
export const pullRequestQueueModes = PullRequestQueueMode.literals
|
|
6
12
|
|
|
7
13
|
export const pullRequestQueueLabels = {
|
|
8
14
|
authored: "authored",
|
|
@@ -21,11 +27,30 @@ export const pullRequestQueueSearchQualifier = (mode: PullRequestQueueMode, auth
|
|
|
21
27
|
return qualifiers[mode]
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export
|
|
30
|
+
export const CheckConclusion = Schema.Literals(["success", "failure", "neutral", "skipped", "cancelled", "timed_out"])
|
|
31
|
+
export type CheckConclusion = Schema.Schema.Type<typeof CheckConclusion>
|
|
32
|
+
|
|
33
|
+
export const CheckRunStatus = Schema.Literals(["completed", "in_progress", "queued", "pending"])
|
|
34
|
+
export type CheckRunStatus = Schema.Schema.Type<typeof CheckRunStatus>
|
|
35
|
+
|
|
36
|
+
export const CheckRollupStatus = Schema.Literals(["passing", "pending", "failing", "none"])
|
|
37
|
+
export type CheckRollupStatus = Schema.Schema.Type<typeof CheckRollupStatus>
|
|
38
|
+
|
|
39
|
+
export const ReviewStatus = Schema.Literals(["draft", "approved", "changes", "review", "none"])
|
|
40
|
+
export type ReviewStatus = Schema.Schema.Type<typeof ReviewStatus>
|
|
41
|
+
|
|
42
|
+
export const Mergeable = Schema.Literals(["mergeable", "conflicting", "unknown"])
|
|
43
|
+
export type Mergeable = Schema.Schema.Type<typeof Mergeable>
|
|
44
|
+
|
|
45
|
+
export const DiffCommentSide = Schema.Literals(["LEFT", "RIGHT"])
|
|
46
|
+
export type DiffCommentSide = Schema.Schema.Type<typeof DiffCommentSide>
|
|
47
|
+
|
|
48
|
+
export const PullRequestMergeAction = Schema.Literals(["squash", "auto", "admin", "disable-auto"])
|
|
49
|
+
export type PullRequestMergeAction = Schema.Schema.Type<typeof PullRequestMergeAction>
|
|
25
50
|
|
|
26
51
|
export interface CheckItem {
|
|
27
52
|
readonly name: string
|
|
28
|
-
readonly status:
|
|
53
|
+
readonly status: CheckRunStatus
|
|
29
54
|
readonly conclusion: CheckConclusion | null
|
|
30
55
|
}
|
|
31
56
|
|
|
@@ -34,8 +59,6 @@ export interface PullRequestLabel {
|
|
|
34
59
|
readonly color: string | null
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
export type DiffCommentSide = "LEFT" | "RIGHT"
|
|
38
|
-
|
|
39
62
|
export interface CreatePullRequestCommentInput {
|
|
40
63
|
readonly repository: string
|
|
41
64
|
readonly number: number
|
|
@@ -69,8 +92,8 @@ export interface PullRequestItem {
|
|
|
69
92
|
readonly deletions: number
|
|
70
93
|
readonly changedFiles: number
|
|
71
94
|
readonly state: PullRequestState
|
|
72
|
-
readonly reviewStatus:
|
|
73
|
-
readonly checkStatus:
|
|
95
|
+
readonly reviewStatus: ReviewStatus
|
|
96
|
+
readonly checkStatus: CheckRollupStatus
|
|
74
97
|
readonly checkSummary: string | null
|
|
75
98
|
readonly checks: readonly CheckItem[]
|
|
76
99
|
readonly autoMergeEnabled: boolean
|
|
@@ -86,11 +109,9 @@ export interface PullRequestMergeInfo {
|
|
|
86
109
|
readonly title: string
|
|
87
110
|
readonly state: PullRequestState
|
|
88
111
|
readonly isDraft: boolean
|
|
89
|
-
readonly mergeable:
|
|
90
|
-
readonly reviewStatus:
|
|
91
|
-
readonly checkStatus:
|
|
112
|
+
readonly mergeable: Mergeable
|
|
113
|
+
readonly reviewStatus: ReviewStatus
|
|
114
|
+
readonly checkStatus: CheckRollupStatus
|
|
92
115
|
readonly checkSummary: string | null
|
|
93
116
|
readonly autoMergeEnabled: boolean
|
|
94
117
|
}
|
|
95
|
-
|
|
96
|
-
export type PullRequestMergeAction = "squash" | "auto" | "admin" | "disable-auto"
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
2
2
|
import { config } from "../config.js"
|
|
3
|
-
import { pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestQueueMode, type PullRequestReviewComment } from "../domain.js"
|
|
3
|
+
import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type Mergeable, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus } from "../domain.js"
|
|
4
4
|
import { getMergeActionDefinition } from "../mergeActions.js"
|
|
5
5
|
import { CommandRunner, type CommandError, type JsonParseError } from "./CommandRunner.js"
|
|
6
6
|
|
|
7
7
|
const NullableString = Schema.NullOr(Schema.String)
|
|
8
|
-
const OptionalNullableString = Schema.
|
|
9
|
-
const OptionalNullableNumber = Schema.
|
|
10
|
-
|
|
11
|
-
const RawCheckRunSchema = Schema.Struct({
|
|
12
|
-
__typename: Schema.Literal("CheckRun"),
|
|
13
|
-
name: OptionalNullableString,
|
|
14
|
-
status: OptionalNullableString,
|
|
15
|
-
conclusion: OptionalNullableString,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const RawStatusContextSchema = Schema.Struct({
|
|
19
|
-
__typename: Schema.Literal("StatusContext"),
|
|
20
|
-
context: OptionalNullableString,
|
|
21
|
-
state: OptionalNullableString,
|
|
22
|
-
})
|
|
8
|
+
const OptionalNullableString = Schema.optionalKey(NullableString)
|
|
9
|
+
const OptionalNullableNumber = Schema.optionalKey(Schema.NullOr(Schema.Number))
|
|
23
10
|
|
|
24
|
-
const RawCheckContextSchema = Schema.Union([
|
|
11
|
+
const RawCheckContextSchema = Schema.Union([
|
|
12
|
+
Schema.Struct({
|
|
13
|
+
__typename: Schema.tag("CheckRun"),
|
|
14
|
+
name: OptionalNullableString,
|
|
15
|
+
status: OptionalNullableString,
|
|
16
|
+
conclusion: OptionalNullableString,
|
|
17
|
+
}),
|
|
18
|
+
Schema.Struct({
|
|
19
|
+
__typename: Schema.tag("StatusContext"),
|
|
20
|
+
context: OptionalNullableString,
|
|
21
|
+
state: OptionalNullableString,
|
|
22
|
+
}),
|
|
23
|
+
]).pipe(Schema.toTaggedUnion("__typename"))
|
|
25
24
|
|
|
26
25
|
const RawAuthorSchema = Schema.Struct({ login: Schema.String })
|
|
27
26
|
const RawRepositorySchema = Schema.Struct({ nameWithOwner: Schema.String })
|
|
@@ -55,7 +54,7 @@ const RawPullRequestNodeSchema = Schema.Struct({
|
|
|
55
54
|
additions: Schema.Number,
|
|
56
55
|
deletions: Schema.Number,
|
|
57
56
|
changedFiles: Schema.Number,
|
|
58
|
-
statusCheckRollup: Schema.
|
|
57
|
+
statusCheckRollup: Schema.optionalKey(Schema.NullOr(Schema.Struct({
|
|
59
58
|
contexts: Schema.Struct({ nodes: Schema.Array(RawCheckContextSchema) }),
|
|
60
59
|
}))),
|
|
61
60
|
})
|
|
@@ -88,22 +87,20 @@ const MergeInfoResponseSchema = Schema.Struct({
|
|
|
88
87
|
statusCheckRollup: Schema.Array(RawCheckContextSchema),
|
|
89
88
|
})
|
|
90
89
|
|
|
91
|
-
const DiffCommentSideSchema = Schema.Union([Schema.Literal("LEFT"), Schema.Literal("RIGHT")])
|
|
92
|
-
|
|
93
90
|
const PullRequestCommentSchema = Schema.Struct({
|
|
94
|
-
id: Schema.
|
|
91
|
+
id: Schema.optionalKey(Schema.NullOr(Schema.Union([Schema.Number, Schema.String]))),
|
|
95
92
|
node_id: OptionalNullableString,
|
|
96
93
|
body: OptionalNullableString,
|
|
97
94
|
html_url: OptionalNullableString,
|
|
98
95
|
url: OptionalNullableString,
|
|
99
96
|
created_at: OptionalNullableString,
|
|
100
|
-
user: Schema.
|
|
97
|
+
user: Schema.optionalKey(Schema.NullOr(Schema.Struct({
|
|
101
98
|
login: OptionalNullableString,
|
|
102
99
|
}))),
|
|
103
100
|
path: OptionalNullableString,
|
|
104
101
|
line: OptionalNullableNumber,
|
|
105
102
|
original_line: OptionalNullableNumber,
|
|
106
|
-
side: Schema.
|
|
103
|
+
side: Schema.optionalKey(Schema.NullOr(DiffCommentSide)),
|
|
107
104
|
})
|
|
108
105
|
|
|
109
106
|
const CommentsResponseSchema = Schema.Union([
|
|
@@ -205,13 +202,13 @@ const normalizeDate = (value: string | null | undefined) => {
|
|
|
205
202
|
const getPullRequestState = (item: { readonly state: string; readonly merged: boolean }): PullRequestItem["state"] =>
|
|
206
203
|
item.merged ? "merged" : item.state.toLowerCase() === "open" ? "open" : "closed"
|
|
207
204
|
|
|
208
|
-
const REVIEW_STATUS_BY_DECISION: Record<string,
|
|
205
|
+
const REVIEW_STATUS_BY_DECISION: Record<string, ReviewStatus> = {
|
|
209
206
|
APPROVED: "approved",
|
|
210
207
|
CHANGES_REQUESTED: "changes",
|
|
211
208
|
REVIEW_REQUIRED: "review",
|
|
212
209
|
}
|
|
213
210
|
|
|
214
|
-
const getReviewStatus = (item: { readonly isDraft: boolean; readonly reviewDecision: string | null }):
|
|
211
|
+
const getReviewStatus = (item: { readonly isDraft: boolean; readonly reviewDecision: string | null }): ReviewStatus => {
|
|
215
212
|
if (item.isDraft) return "draft"
|
|
216
213
|
if (item.reviewDecision) return REVIEW_STATUS_BY_DECISION[item.reviewDecision] ?? "none"
|
|
217
214
|
return "none"
|
|
@@ -239,19 +236,24 @@ const normalizeCheckStatus = (raw: string | null | undefined): CheckItem["status
|
|
|
239
236
|
const normalizeCheckConclusion = (raw: string | null | undefined): CheckItem["conclusion"] =>
|
|
240
237
|
raw ? CHECK_CONCLUSION_BY_RAW[raw] ?? null : null
|
|
241
238
|
|
|
242
|
-
const getContextStatus = (context: RawCheckContext): CheckItem["status"] =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
239
|
+
const getContextStatus = (context: RawCheckContext): CheckItem["status"] =>
|
|
240
|
+
RawCheckContextSchema.match(context, {
|
|
241
|
+
CheckRun: (run) => normalizeCheckStatus(run.status),
|
|
242
|
+
StatusContext: (status) => status.state === "PENDING" ? "in_progress" : "completed",
|
|
243
|
+
})
|
|
247
244
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return null
|
|
245
|
+
const STATUS_CONTEXT_CONCLUSION: Record<string, NonNullable<CheckItem["conclusion"]>> = {
|
|
246
|
+
SUCCESS: "success",
|
|
247
|
+
FAILURE: "failure",
|
|
248
|
+
ERROR: "failure",
|
|
253
249
|
}
|
|
254
250
|
|
|
251
|
+
const getContextConclusion = (context: RawCheckContext): CheckItem["conclusion"] =>
|
|
252
|
+
RawCheckContextSchema.match(context, {
|
|
253
|
+
CheckRun: (run) => normalizeCheckConclusion(run.conclusion),
|
|
254
|
+
StatusContext: (status) => (status.state ? STATUS_CONTEXT_CONCLUSION[status.state] : null) ?? null,
|
|
255
|
+
})
|
|
256
|
+
|
|
255
257
|
const getCheckInfoFromContexts = (contexts: readonly RawCheckContext[]): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> => {
|
|
256
258
|
if (contexts.length === 0) {
|
|
257
259
|
return { checkStatus: "none", checkSummary: null, checks: [] }
|
|
@@ -379,12 +381,12 @@ const fallbackCreatedComment = (input: CreatePullRequestCommentInput): PullReque
|
|
|
379
381
|
|
|
380
382
|
export type GitHubError = CommandError | JsonParseError | Schema.SchemaError
|
|
381
383
|
|
|
382
|
-
const MERGEABLE_BY_RAW: Record<string,
|
|
384
|
+
const MERGEABLE_BY_RAW: Record<string, Mergeable> = {
|
|
383
385
|
MERGEABLE: "mergeable",
|
|
384
386
|
CONFLICTING: "conflicting",
|
|
385
387
|
}
|
|
386
388
|
|
|
387
|
-
const normalizeMergeable = (value: string):
|
|
389
|
+
const normalizeMergeable = (value: string): Mergeable =>
|
|
388
390
|
MERGEABLE_BY_RAW[value] ?? "unknown"
|
|
389
391
|
|
|
390
392
|
export class GitHubService extends Context.Service<GitHubService, {
|
package/src/ui/FooterHints.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { Data } from "effect"
|
|
1
2
|
import { colors } from "./colors.js"
|
|
2
3
|
import { TextLine } from "./primitives.js"
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
readonly max: number
|
|
7
|
-
}
|
|
5
|
+
export type RetryProgress = Data.TaggedEnum<{
|
|
6
|
+
Idle: {}
|
|
7
|
+
Retrying: { readonly attempt: number; readonly max: number }
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
export const RetryProgress = Data.taggedEnum<RetryProgress>()
|
|
11
|
+
export const initialRetryProgress: RetryProgress = RetryProgress.Idle()
|
|
8
12
|
|
|
9
13
|
export const FooterHints = ({
|
|
10
14
|
filterEditing,
|
|
@@ -29,7 +33,7 @@ export const FooterHints = ({
|
|
|
29
33
|
hasError: boolean
|
|
30
34
|
isLoading: boolean
|
|
31
35
|
loadingIndicator: string
|
|
32
|
-
retryProgress: RetryProgress
|
|
36
|
+
retryProgress: RetryProgress
|
|
33
37
|
}) => {
|
|
34
38
|
if (filterEditing) {
|
|
35
39
|
return (
|
|
@@ -148,7 +152,7 @@ export const FooterHints = ({
|
|
|
148
152
|
<span fg={colors.muted}> clear </span>
|
|
149
153
|
</>
|
|
150
154
|
) : null}
|
|
151
|
-
{retryProgress ? (
|
|
155
|
+
{retryProgress._tag === "Retrying" ? (
|
|
152
156
|
<>
|
|
153
157
|
<span fg={colors.status.pending}>retry</span>
|
|
154
158
|
<span fg={colors.muted}> {retryProgress.attempt}/{retryProgress.max} </span>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
1
|
+
import type { DiffRenderable, MouseEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useMemo, type Ref } from "react"
|
|
3
|
-
import type { PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
3
|
+
import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
4
4
|
import { colors, type ThemeId } from "./colors.js"
|
|
5
|
-
import { createDiffSyntaxStyle, diffFileStats,
|
|
5
|
+
import { createDiffSyntaxStyle, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileAtLine, type DiffFileStats, type DiffView, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
|
|
6
6
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
7
7
|
import { Divider, fitCell, PlainLine, TextLine } from "./primitives.js"
|
|
8
8
|
import { shortRepoName } from "./pullRequests.js"
|
|
@@ -27,8 +27,7 @@ const DiffStats = ({ pullRequest }: { pullRequest: PullRequestItem }) => {
|
|
|
27
27
|
)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const FileStats = ({
|
|
31
|
-
const stats = diffFileStats(file)
|
|
30
|
+
const FileStats = ({ stats }: { stats: DiffFileStats }) => {
|
|
32
31
|
return (
|
|
33
32
|
<>
|
|
34
33
|
{stats.additions > 0 ? <span fg={colors.status.passing}>{`+${stats.additions}`}</span> : null}
|
|
@@ -38,16 +37,32 @@ const FileStats = ({ file }: { file: StackedDiffFilePatch["file"] }) => {
|
|
|
38
37
|
)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
const FileHeader = ({
|
|
40
|
+
const FileHeader = ({
|
|
41
|
+
file,
|
|
42
|
+
index,
|
|
43
|
+
count,
|
|
44
|
+
width,
|
|
45
|
+
suffix = "",
|
|
46
|
+
suffixColor = colors.muted,
|
|
47
|
+
}: {
|
|
48
|
+
file: StackedDiffFilePatch["file"]
|
|
49
|
+
index: number
|
|
50
|
+
count: number
|
|
51
|
+
width: number
|
|
52
|
+
suffix?: string
|
|
53
|
+
suffixColor?: string
|
|
54
|
+
}) => {
|
|
42
55
|
const counter = `${index + 1}/${count}`
|
|
43
|
-
const
|
|
44
|
-
const
|
|
56
|
+
const stats = diffFileStats(file)
|
|
57
|
+
const statsText = diffFileStatsText(stats)
|
|
58
|
+
const nameWidth = Math.max(1, width - counter.length - statsText.length - suffix.length - 5)
|
|
45
59
|
return (
|
|
46
60
|
<TextLine>
|
|
47
61
|
<span fg={colors.muted}>{counter} </span>
|
|
48
62
|
<span fg={colors.text}>{fitCell(file.name, nameWidth)}</span>
|
|
49
63
|
{statsText ? <span fg={colors.muted}> </span> : null}
|
|
50
|
-
<FileStats
|
|
64
|
+
<FileStats stats={stats} />
|
|
65
|
+
{suffix ? <span fg={suffixColor}>{suffix}</span> : null}
|
|
51
66
|
</TextLine>
|
|
52
67
|
)
|
|
53
68
|
}
|
|
@@ -56,7 +71,7 @@ export const PullRequestDiffPane = ({
|
|
|
56
71
|
pullRequest,
|
|
57
72
|
diffState,
|
|
58
73
|
stackedFiles,
|
|
59
|
-
|
|
74
|
+
scrollTop,
|
|
60
75
|
view,
|
|
61
76
|
wrapMode,
|
|
62
77
|
paneWidth,
|
|
@@ -67,29 +82,27 @@ export const PullRequestDiffPane = ({
|
|
|
67
82
|
commentMode,
|
|
68
83
|
selectedCommentAnchor,
|
|
69
84
|
selectedCommentThread,
|
|
70
|
-
|
|
85
|
+
onSelectCommentLine,
|
|
71
86
|
themeId,
|
|
72
87
|
}: {
|
|
73
88
|
pullRequest: PullRequestItem | null
|
|
74
89
|
diffState: PullRequestDiffState | undefined
|
|
75
90
|
stackedFiles: readonly StackedDiffFilePatch[]
|
|
76
|
-
|
|
77
|
-
view:
|
|
78
|
-
wrapMode:
|
|
91
|
+
scrollTop: number
|
|
92
|
+
view: DiffView
|
|
93
|
+
wrapMode: DiffWrapMode
|
|
79
94
|
paneWidth: number
|
|
80
95
|
height: number
|
|
81
96
|
loadingIndicator: string
|
|
82
97
|
scrollRef: Ref<ScrollBoxRenderable>
|
|
83
98
|
setDiffRef: (index: number, diff: DiffRenderable | null) => void
|
|
84
99
|
commentMode: boolean
|
|
85
|
-
selectedCommentAnchor:
|
|
100
|
+
selectedCommentAnchor: StackedDiffCommentAnchor | null
|
|
86
101
|
selectedCommentThread: readonly PullRequestReviewComment[]
|
|
87
|
-
|
|
102
|
+
onSelectCommentLine: (renderLine: number, side: DiffCommentSide | null) => void
|
|
88
103
|
themeId: ThemeId
|
|
89
104
|
}) => {
|
|
90
|
-
const readyFiles = diffState?.
|
|
91
|
-
const safeIndex = safeDiffFileIndex(readyFiles, fileIndex)
|
|
92
|
-
const file = readyFiles[safeIndex] ?? null
|
|
105
|
+
const readyFiles = diffState?._tag === "Ready" ? diffState.files : []
|
|
93
106
|
const syntaxStyle = useMemo(() => createDiffSyntaxStyle(), [themeId])
|
|
94
107
|
|
|
95
108
|
if (!pullRequest) {
|
|
@@ -101,7 +114,7 @@ export const PullRequestDiffPane = ({
|
|
|
101
114
|
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
102
115
|
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
103
116
|
|
|
104
|
-
if (!diffState || diffState.
|
|
117
|
+
if (!diffState || diffState._tag === "Loading") {
|
|
105
118
|
return (
|
|
106
119
|
<box height={height} flexDirection="column">
|
|
107
120
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
@@ -118,7 +131,7 @@ export const PullRequestDiffPane = ({
|
|
|
118
131
|
)
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
if (diffState.
|
|
134
|
+
if (diffState._tag === "Error") {
|
|
122
135
|
return (
|
|
123
136
|
<box height={height} flexDirection="column">
|
|
124
137
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
@@ -130,20 +143,11 @@ export const PullRequestDiffPane = ({
|
|
|
130
143
|
)
|
|
131
144
|
}
|
|
132
145
|
|
|
133
|
-
if (readyFiles.length === 0 ||
|
|
146
|
+
if (readyFiles.length === 0 || stackedFiles.length === 0) {
|
|
134
147
|
return <LoadingPane content={{ title: "No diff", hint: "This PR has no patch contents" }} width={paneWidth} height={height} />
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
const fileCounter = `${safeIndex + 1}/${readyFiles.length}`
|
|
138
|
-
const currentFileStatsText = file ? diffFileStatText(file) : ""
|
|
139
150
|
const selectedSideLabel = selectedCommentAnchor?.side === "RIGHT" ? "right" : selectedCommentAnchor?.side === "LEFT" ? "left" : null
|
|
140
|
-
const commentLabel = commentMode && selectedCommentAnchor
|
|
141
|
-
? ` ${selectedSideLabel} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line}`
|
|
142
|
-
: commentMode ? " c no lines" : commentCount > 0 ? ` ● ${commentCount}` : ""
|
|
143
|
-
const commentLabelColor = commentMode && selectedCommentAnchor?.side === "LEFT"
|
|
144
|
-
? colors.status.failing
|
|
145
|
-
: commentMode ? colors.status.passing : colors.status.passing
|
|
146
|
-
const fileNameWidth = Math.max(8, headerWidth - fileCounter.length - currentFileStatsText.length - commentLabel.length - 4)
|
|
147
151
|
const commentPeek = commentMode && selectedCommentAnchor && selectedCommentThread.length > 0
|
|
148
152
|
? selectedCommentThread[selectedCommentThread.length - 1]!
|
|
149
153
|
: null
|
|
@@ -152,6 +156,31 @@ export const PullRequestDiffPane = ({
|
|
|
152
156
|
const commentPeekMeta = commentPeek && selectedCommentAnchor
|
|
153
157
|
? `${selectedSideLabel ?? "line"} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line} ${commentPeek.author} ${commentPeekCount} enter thread a comment`
|
|
154
158
|
: ""
|
|
159
|
+
const stickyScrollTop = Math.max(0, Math.floor(scrollTop))
|
|
160
|
+
const stickyFile = stackedDiffFileAtLine(stackedFiles, stickyScrollTop) ?? stackedFiles[0]
|
|
161
|
+
const stickyArrayIndex = stickyFile ? stackedFiles.indexOf(stickyFile) : -1
|
|
162
|
+
const incomingStickyFile = stickyArrayIndex >= 0 ? stackedFiles[stickyArrayIndex + 1] : undefined
|
|
163
|
+
const incomingHeaderDistance = incomingStickyFile ? incomingStickyFile.headerLine - stickyScrollTop : Number.POSITIVE_INFINITY
|
|
164
|
+
const incomingFile = incomingHeaderDistance === 1 ? incomingStickyFile : undefined
|
|
165
|
+
const stickyCommentLabelFor = (stackedFile: StackedDiffFilePatch | undefined) => {
|
|
166
|
+
if (!commentMode) return ""
|
|
167
|
+
if (!selectedCommentAnchor) return " c no lines"
|
|
168
|
+
if (selectedCommentAnchor.fileIndex !== stackedFile?.index) return ""
|
|
169
|
+
return ` ${selectedCommentAnchor.side === "RIGHT" ? "right" : "left"} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line}`
|
|
170
|
+
}
|
|
171
|
+
const stickyCommentColor = selectedCommentAnchor?.side === "LEFT" ? colors.status.failing : colors.status.passing
|
|
172
|
+
const handleDiffMouseDown = function (this: ScrollBoxRenderable, event: MouseEvent) {
|
|
173
|
+
if (event.button !== 0) return
|
|
174
|
+
const localY = event.y - this.viewport.y
|
|
175
|
+
if (localY < 0 || localY >= this.viewport.height) return
|
|
176
|
+
const localX = event.x - this.viewport.x
|
|
177
|
+
const side = view === "split"
|
|
178
|
+
? localX < Math.floor(paneWidth / 2) ? "LEFT" : "RIGHT"
|
|
179
|
+
: null
|
|
180
|
+
onSelectCommentLine(Math.max(0, Math.floor(this.scrollTop + localY)), side)
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
event.stopPropagation()
|
|
183
|
+
}
|
|
155
184
|
|
|
156
185
|
return (
|
|
157
186
|
<box height={height} flexDirection="column">
|
|
@@ -163,17 +192,8 @@ export const PullRequestDiffPane = ({
|
|
|
163
192
|
<DiffStats pullRequest={pullRequest} />
|
|
164
193
|
</TextLine>
|
|
165
194
|
</box>
|
|
166
|
-
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
167
|
-
<TextLine>
|
|
168
|
-
<span fg={colors.text}>{fitCell(file.name, fileNameWidth)}</span>
|
|
169
|
-
<span fg={colors.muted}> {fileCounter}</span>
|
|
170
|
-
{currentFileStatsText ? <span fg={colors.muted}> </span> : null}
|
|
171
|
-
<FileStats file={file} />
|
|
172
|
-
{commentLabel ? <span fg={commentLabelColor}>{commentLabel}</span> : null}
|
|
173
|
-
</TextLine>
|
|
174
|
-
</box>
|
|
175
195
|
<Divider width={paneWidth} />
|
|
176
|
-
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false}>
|
|
196
|
+
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false} onMouseDown={handleDiffMouseDown}>
|
|
177
197
|
{stackedFiles.map((stackedFile) => (
|
|
178
198
|
<box key={`${pullRequest.url}-${stackedFile.index}-${view}-${wrapMode}`} flexDirection="column" flexShrink={0}>
|
|
179
199
|
{stackedFile.index > 0 ? <Divider width={paneWidth} /> : null}
|
|
@@ -207,6 +227,25 @@ export const PullRequestDiffPane = ({
|
|
|
207
227
|
</box>
|
|
208
228
|
))}
|
|
209
229
|
</scrollbox>
|
|
230
|
+
{stickyFile ? (
|
|
231
|
+
<box position="absolute" top={2} left={0} width={paneWidth} height={2} zIndex={10} flexDirection="column" backgroundColor={colors.background}>
|
|
232
|
+
{incomingFile ? (
|
|
233
|
+
<>
|
|
234
|
+
<Divider width={paneWidth} />
|
|
235
|
+
<box height={1} paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
|
|
236
|
+
<FileHeader file={incomingFile.file} index={incomingFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(incomingFile)} suffixColor={stickyCommentColor} />
|
|
237
|
+
</box>
|
|
238
|
+
</>
|
|
239
|
+
) : (
|
|
240
|
+
<>
|
|
241
|
+
<box height={1} paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
|
|
242
|
+
<FileHeader file={stickyFile.file} index={stickyFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(stickyFile)} suffixColor={stickyCommentColor} />
|
|
243
|
+
</box>
|
|
244
|
+
<Divider width={paneWidth} />
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
</box>
|
|
248
|
+
) : null}
|
|
210
249
|
{commentPeek ? (
|
|
211
250
|
<>
|
|
212
251
|
<Divider width={paneWidth} />
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core"
|
|
2
|
-
import type { PullRequestItem } from "../domain.js"
|
|
2
|
+
import type { LoadStatus, PullRequestItem } from "../domain.js"
|
|
3
3
|
import { daysOpen } from "../date.js"
|
|
4
4
|
import { colors } from "./colors.js"
|
|
5
5
|
import { fitCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
|
|
6
6
|
import { checkLabel, repoColor, reviewIcon, statusColor } from "./pullRequests.js"
|
|
7
7
|
|
|
8
|
-
export type LoadStatus = "loading" | "ready" | "error"
|
|
9
8
|
export type PullRequestGroups = Array<[string, PullRequestItem[]]>
|
|
10
9
|
|
|
11
10
|
const GROUP_ICON = "◆"
|
|
@@ -77,8 +76,8 @@ const PullRequestRow = ({
|
|
|
77
76
|
const checkColor = isMerged ? colors.status.passing : isClosed ? colors.muted : statusColor(pullRequest.checkStatus)
|
|
78
77
|
|
|
79
78
|
return (
|
|
80
|
-
<box height={1} onMouseDown={onSelect}>
|
|
81
|
-
<TextLine fg={rowTextColor} bg={selected ? colors.selectedBg : undefined}>
|
|
79
|
+
<box width={contentWidth} height={1} onMouseDown={onSelect}>
|
|
80
|
+
<TextLine width={contentWidth} fg={rowTextColor} bg={selected ? colors.selectedBg : undefined}>
|
|
82
81
|
<span fg={indicatorColor}>{fitCell(reviewIcon(pullRequest), reviewWidth)}</span>
|
|
83
82
|
<span> </span>
|
|
84
83
|
<span fg={numberColor}><MatchedCell text={`#${pullRequest.number}`} width={numberWidth} query={filterText} align="right" /></span>
|
|
@@ -117,7 +116,7 @@ export const PullRequestList = ({
|
|
|
117
116
|
const emptyText = filterText.length > 0 ? "- No matching pull requests." : "- No open pull requests."
|
|
118
117
|
|
|
119
118
|
return (
|
|
120
|
-
<box flexDirection="column">
|
|
119
|
+
<box width={contentWidth} flexDirection="column">
|
|
121
120
|
<SectionTitle title="PULL REQUESTS" />
|
|
122
121
|
{showFilterBar ? (
|
|
123
122
|
<TextLine>
|
|
@@ -138,11 +137,11 @@ export const PullRequestList = ({
|
|
|
138
137
|
<PullRequestRow
|
|
139
138
|
key={pullRequest.url}
|
|
140
139
|
pullRequest={pullRequest}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
selected={pullRequest.url === selectedUrl}
|
|
141
|
+
contentWidth={contentWidth}
|
|
142
|
+
numWidth={numWidth}
|
|
143
|
+
filterText={filterText}
|
|
144
|
+
onSelect={() => onSelectPullRequest(pullRequest.url)}
|
|
146
145
|
/>
|
|
147
146
|
))}
|
|
148
147
|
</box>
|
package/src/ui/diff.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { parseColor, SyntaxStyle } from "@opentui/core"
|
|
2
|
+
import { Data, Schema } from "effect"
|
|
2
3
|
import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
3
4
|
import { colors } from "./colors.js"
|
|
4
5
|
|
|
6
|
+
export const DiffView = Schema.Literals(["unified", "split"])
|
|
7
|
+
export type DiffView = Schema.Schema.Type<typeof DiffView>
|
|
8
|
+
|
|
9
|
+
export const DiffWrapMode = Schema.Literals(["none", "word"])
|
|
10
|
+
export type DiffWrapMode = Schema.Schema.Type<typeof DiffWrapMode>
|
|
11
|
+
|
|
12
|
+
export const DiffCommentKind = Schema.Literals(["addition", "deletion", "context"])
|
|
13
|
+
export type DiffCommentKind = Schema.Schema.Type<typeof DiffCommentKind>
|
|
14
|
+
|
|
5
15
|
export interface DiffFilePatch {
|
|
6
16
|
readonly name: string
|
|
7
17
|
readonly filetype: string | undefined
|
|
@@ -25,7 +35,7 @@ export interface DiffCommentAnchor {
|
|
|
25
35
|
readonly path: string
|
|
26
36
|
readonly line: number
|
|
27
37
|
readonly side: DiffCommentSide
|
|
28
|
-
readonly kind:
|
|
38
|
+
readonly kind: DiffCommentKind
|
|
29
39
|
readonly renderLine: number
|
|
30
40
|
readonly text: string
|
|
31
41
|
}
|
|
@@ -35,10 +45,13 @@ export type StackedDiffCommentAnchor = DiffCommentAnchor & {
|
|
|
35
45
|
readonly localRenderLine: number
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
export type PullRequestDiffState =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
export type PullRequestDiffState = Data.TaggedEnum<{
|
|
49
|
+
Loading: {}
|
|
50
|
+
Ready: { readonly patch: string; readonly files: readonly DiffFilePatch[] }
|
|
51
|
+
Error: { readonly error: string }
|
|
52
|
+
}>
|
|
53
|
+
|
|
54
|
+
export const PullRequestDiffState = Data.taggedEnum<PullRequestDiffState>()
|
|
42
55
|
|
|
43
56
|
export const createDiffSyntaxStyle = () => SyntaxStyle.fromStyles({
|
|
44
57
|
keyword: { fg: parseColor(colors.accent), bold: true },
|
|
@@ -176,8 +189,8 @@ export const safeDiffFileIndex = (files: readonly DiffFilePatch[], index: number
|
|
|
176
189
|
|
|
177
190
|
export const buildStackedDiffFiles = (
|
|
178
191
|
files: readonly DiffFilePatch[],
|
|
179
|
-
view:
|
|
180
|
-
wrapMode:
|
|
192
|
+
view: DiffView,
|
|
193
|
+
wrapMode: DiffWrapMode,
|
|
181
194
|
width: number,
|
|
182
195
|
): readonly StackedDiffFilePatch[] => {
|
|
183
196
|
let offset = 0
|
|
@@ -197,14 +210,14 @@ export const buildStackedDiffFiles = (
|
|
|
197
210
|
})
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
export const stackedDiffFileAtLine = (stackedFiles: readonly StackedDiffFilePatch[], line: number) =>
|
|
214
|
+
stackedFiles.reduce<StackedDiffFilePatch | undefined>((current, file) => file.headerLine <= line ? file : current, undefined)
|
|
215
|
+
|
|
200
216
|
export const diffStatText = (pullRequest: PullRequestItem) => {
|
|
201
217
|
if (!pullRequest.detailLoaded) return "loading details"
|
|
202
218
|
const files = pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pullRequest.deletions > 0 ? `-${pullRequest.deletions}` : null,
|
|
206
|
-
files,
|
|
207
|
-
].filter((part): part is string => part !== null).join(" ")
|
|
219
|
+
const stats = diffFileStatsText(pullRequest)
|
|
220
|
+
return stats ? `${stats} ${files}` : files
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
export const diffCommentLocationKey = (location: Pick<PullRequestReviewComment, "path" | "side" | "line">) => `${location.path}:${location.side}:${location.line}`
|
|
@@ -213,7 +226,7 @@ export const diffCommentAnchorKey = diffCommentLocationKey
|
|
|
213
226
|
|
|
214
227
|
type PendingDiffCommentAnchor = Omit<DiffCommentAnchor, "renderLine">
|
|
215
228
|
|
|
216
|
-
const diffContentWidth = (lines: readonly string[], view:
|
|
229
|
+
const diffContentWidth = (lines: readonly string[], view: DiffView, width: number) => {
|
|
217
230
|
const lineNumberGutterWidth = patchLineNumberGutterWidth(lines)
|
|
218
231
|
return view === "split"
|
|
219
232
|
? Math.max(1, Math.floor(width / 2) - lineNumberGutterWidth)
|
|
@@ -242,14 +255,17 @@ export const diffFileStats = (file: DiffFilePatch): DiffFileStats => {
|
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
export const diffFileStatText = (file: DiffFilePatch) => {
|
|
245
|
-
|
|
258
|
+
return diffFileStatsText(diffFileStats(file))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const diffFileStatsText = (stats: DiffFileStats) => {
|
|
246
262
|
return [
|
|
247
263
|
stats.additions > 0 ? `+${stats.additions}` : null,
|
|
248
264
|
stats.deletions > 0 ? `-${stats.deletions}` : null,
|
|
249
265
|
].filter((part): part is string => part !== null).join(" ")
|
|
250
266
|
}
|
|
251
267
|
|
|
252
|
-
export const getDiffCommentAnchors = (file: DiffFilePatch, view:
|
|
268
|
+
export const getDiffCommentAnchors = (file: DiffFilePatch, view: DiffView = "unified", wrapMode: DiffWrapMode = "none", width = 120): readonly DiffCommentAnchor[] => {
|
|
253
269
|
const anchors: DiffCommentAnchor[] = []
|
|
254
270
|
const lines = file.patch.split("\n")
|
|
255
271
|
const contentWidth = diffContentWidth(lines, view, width)
|
|
@@ -330,8 +346,8 @@ export const getDiffCommentAnchors = (file: DiffFilePatch, view: "unified" | "sp
|
|
|
330
346
|
|
|
331
347
|
export const getStackedDiffCommentAnchors = (
|
|
332
348
|
stackedFiles: readonly StackedDiffFilePatch[],
|
|
333
|
-
view:
|
|
334
|
-
wrapMode:
|
|
349
|
+
view: DiffView = "unified",
|
|
350
|
+
wrapMode: DiffWrapMode = "none",
|
|
335
351
|
width = 120,
|
|
336
352
|
): readonly StackedDiffCommentAnchor[] =>
|
|
337
353
|
stackedFiles.flatMap((stackedFile) => getDiffCommentAnchors(stackedFile.file, view, wrapMode, width).map((anchor) => ({
|
|
@@ -354,7 +370,7 @@ export const scrollTopForVisibleLine = (currentTop: number, viewportHeight: numb
|
|
|
354
370
|
return currentTop
|
|
355
371
|
}
|
|
356
372
|
|
|
357
|
-
const estimatedWrappedLineCount = (text: string, width: number, wrapMode:
|
|
373
|
+
const estimatedWrappedLineCount = (text: string, width: number, wrapMode: DiffWrapMode) => {
|
|
358
374
|
if (wrapMode === "none") return 1
|
|
359
375
|
return Math.max(1, Math.ceil(Bun.stringWidth(text) / Math.max(1, width)))
|
|
360
376
|
}
|
|
@@ -394,7 +410,7 @@ const patchLineNumberGutterWidth = (lines: readonly string[]) => {
|
|
|
394
410
|
return Math.max(3, digits + 2) + (hasSigns ? 2 : 0)
|
|
395
411
|
}
|
|
396
412
|
|
|
397
|
-
export const patchRenderableLineCount = (patch: string, view:
|
|
413
|
+
export const patchRenderableLineCount = (patch: string, view: DiffView, wrapMode: DiffWrapMode, width: number) => {
|
|
398
414
|
const lines = patch.split("\n")
|
|
399
415
|
const contentWidth = diffContentWidth(lines, view, width)
|
|
400
416
|
let count = 0
|
package/src/ui/modals.tsx
CHANGED
|
@@ -96,19 +96,19 @@ export const initialThemeModalState: ThemeModalState = {
|
|
|
96
96
|
|
|
97
97
|
export type Modal = Data.TaggedEnum<{
|
|
98
98
|
None: {}
|
|
99
|
-
Label:
|
|
100
|
-
Close:
|
|
101
|
-
Merge:
|
|
102
|
-
Comment:
|
|
103
|
-
CommentThread:
|
|
104
|
-
Theme:
|
|
99
|
+
Label: LabelModalState
|
|
100
|
+
Close: CloseModalState
|
|
101
|
+
Merge: MergeModalState
|
|
102
|
+
Comment: CommentModalState
|
|
103
|
+
CommentThread: CommentThreadModalState
|
|
104
|
+
Theme: ThemeModalState
|
|
105
105
|
}>
|
|
106
106
|
|
|
107
107
|
export const Modal = Data.taggedEnum<Modal>()
|
|
108
108
|
export const initialModal: Modal = Modal.None()
|
|
109
109
|
|
|
110
110
|
export type ModalTag = Modal["_tag"]
|
|
111
|
-
export type ModalState<Tag extends Exclude<ModalTag, "None">> = Extract<Modal, { _tag: Tag }
|
|
111
|
+
export type ModalState<Tag extends Exclude<ModalTag, "None">> = Omit<Extract<Modal, { _tag: Tag }>, "_tag">
|
|
112
112
|
|
|
113
113
|
export const modalInitialStates = {
|
|
114
114
|
Label: initialLabelModalState,
|
package/src/ui/primitives.tsx
CHANGED
|
@@ -29,8 +29,8 @@ export const PlainLine = ({ text, fg = colors.text, bold = false }: { text: stri
|
|
|
29
29
|
</box>
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
export const TextLine = ({ children, fg = colors.text, bg }: { children: React.ReactNode; fg?: string; bg?: string | undefined }) => (
|
|
33
|
-
<box height={1}>
|
|
32
|
+
export const TextLine = ({ children, fg = colors.text, bg, width }: { children: React.ReactNode; fg?: string; bg?: string | undefined; width?: number }) => (
|
|
33
|
+
<box height={1} {...(width === undefined ? {} : { width })}>
|
|
34
34
|
{bg ? (
|
|
35
35
|
<text wrapMode="none" truncate fg={fg} bg={bg}>
|
|
36
36
|
{children}
|