@kitlangton/ghui 0.1.22 → 0.2.1
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 +5 -4
- package/src/App.tsx +381 -357
- package/src/appCommands.ts +16 -4
- package/src/domain.ts +11 -0
- package/src/index.tsx +1 -7
- package/src/keyboard/opentuiAdapter.ts +43 -0
- package/src/keymap/all.ts +103 -0
- package/src/keymap/closeModal.ts +13 -0
- package/src/keymap/commandPalette.ts +16 -0
- package/src/keymap/commentModal.ts +73 -0
- package/src/keymap/commentThreadModal.ts +24 -0
- package/src/keymap/detailView.ts +30 -0
- package/src/keymap/diffView.ts +91 -0
- package/src/keymap/filterMode.ts +13 -0
- package/src/keymap/helpers.ts +24 -0
- package/src/keymap/labelModal.ts +16 -0
- package/src/keymap/listNav.ts +119 -0
- package/src/keymap/mergeModal.ts +23 -0
- package/src/keymap/openRepositoryModal.ts +13 -0
- package/src/keymap/themeModal.ts +49 -0
- package/src/mergeActions.ts +4 -1
- package/src/services/GitHubService.ts +41 -6
- package/src/services/MockGitHubService.ts +37 -2
- package/src/themeStore.ts +28 -11
- package/src/ui/CommandPalette.tsx +115 -65
- package/src/ui/DetailsPane.tsx +152 -32
- package/src/ui/LoadingLogo.tsx +75 -0
- package/src/ui/PullRequestDiffPane.tsx +19 -17
- package/src/ui/comments.tsx +143 -0
- package/src/ui/diff.ts +194 -8
- package/src/ui/modals.tsx +4 -39
- package/src/ui/primitives.tsx +5 -1
- package/src/ui/singleLineInput.ts +2 -1
- package/src/ui/spinner.ts +1 -0
- package/src/keyboard/createKeymap.ts +0 -25
- package/src/keyboard/useAppCommandRegistry.ts +0 -30
- package/src/keyboard/useScopedBindings.ts +0 -66
package/src/App.tsx
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useKeymap } from "@ghui/keymap/react"
|
|
4
|
+
import { appKeymap, type AppCtx } from "./keymap/all.js"
|
|
5
|
+
import { useOpenTuiSubscribe } from "./keyboard/opentuiAdapter.js"
|
|
5
6
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
6
7
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
7
8
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
8
9
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
9
10
|
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
10
|
-
import { useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
11
|
+
import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
11
12
|
import { buildAppCommands } from "./appCommands.js"
|
|
12
13
|
import type { AppCommand } from "./commands.js"
|
|
13
14
|
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
14
15
|
import { config } from "./config.js"
|
|
15
|
-
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
16
|
+
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestConversationItem, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
16
17
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
17
18
|
import { errorMessage } from "./errors.js"
|
|
18
19
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
@@ -23,12 +24,13 @@ import { BrowserOpener } from "./services/BrowserOpener.js"
|
|
|
23
24
|
import { Clipboard } from "./services/Clipboard.js"
|
|
24
25
|
import { CommandRunner } from "./services/CommandRunner.js"
|
|
25
26
|
import { GitHubService } from "./services/GitHubService.js"
|
|
26
|
-
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
27
|
+
import { loadStoredDiffWhitespaceMode, loadStoredThemeId, saveStoredDiffWhitespaceMode, saveStoredThemeId } from "./themeStore.js"
|
|
27
28
|
import { colors, filterThemeDefinitions, mixHex, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
28
29
|
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"
|
|
29
|
-
import { buildStackedDiffFiles, diffCommentAnchorLabel, diffCommentLineLabel, diffCommentLocationKey, diffCommentSideLabel, getStackedDiffCommentAnchors, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles,
|
|
30
|
-
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getScrollableDetailBodyHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
30
|
+
import { buildStackedDiffFiles, diffAnchorOnSide, diffCommentAnchorLabel, diffCommentLineLabel, diffCommentLocationKey, diffCommentSideLabel, getStackedDiffCommentAnchors, minimizeWhitespaceDiffFiles, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileIndexAtLine, type DiffCommentAnchor, type DiffCommentKind, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type StackedDiffCommentAnchor, verticalDiffAnchor } from "./ui/diff.js"
|
|
31
|
+
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getScrollableDetailBodyHeight, LoadingPane, type DetailConversationStatus, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
31
32
|
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
33
|
+
import { LoadingLogoPane } from "./ui/LoadingLogo.js"
|
|
32
34
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
33
35
|
import { CommandPalette } from "./ui/CommandPalette.js"
|
|
34
36
|
import { CloseModal, CommentModal, CommentThreadModal, filterLabels, initialCloseModalState, initialCommandPaletteState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialOpenRepositoryModalState, initialThemeModalState, LabelModal, MergeModal, Modal, OpenRepositoryModal, ThemeModal, type CloseModalState, type CommandPaletteState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type OpenRepositoryModalState, type ThemeModalState } from "./ui/modals.js"
|
|
@@ -36,6 +38,7 @@ import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
|
36
38
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
37
39
|
import { buildPullRequestListRows, pullRequestListRowIndex, PullRequestList } from "./ui/PullRequestList.js"
|
|
38
40
|
import { editSingleLineInput, isSingleLineInputKey, printableKeyText, singleLineText } from "./ui/singleLineInput.js"
|
|
41
|
+
import { SPINNER_FRAMES } from "./ui/spinner.js"
|
|
39
42
|
|
|
40
43
|
const parseOptionalPositiveInt = (value: string | undefined, fallback: number | null) => {
|
|
41
44
|
if (value === undefined) return fallback
|
|
@@ -55,7 +58,10 @@ const githubRuntime = Atom.runtime(
|
|
|
55
58
|
Layer.provideMerge(Observability.layer),
|
|
56
59
|
),
|
|
57
60
|
)
|
|
58
|
-
const initialThemeId = await
|
|
61
|
+
const [initialThemeId, initialDiffWhitespaceMode] = await Promise.all([
|
|
62
|
+
Effect.runPromise(loadStoredThemeId),
|
|
63
|
+
Effect.runPromise(loadStoredDiffWhitespaceMode),
|
|
64
|
+
])
|
|
59
65
|
|
|
60
66
|
interface PullRequestLoad {
|
|
61
67
|
readonly view: PullRequestView
|
|
@@ -112,7 +118,6 @@ const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
|
112
118
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
113
119
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
114
120
|
const DIFF_STICKY_HEADER_LINES = 2
|
|
115
|
-
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
116
121
|
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
117
122
|
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
118
123
|
const LOAD_MORE_SCROLL_THRESHOLD = 3
|
|
@@ -120,24 +125,6 @@ const DETAIL_PREFETCH_BEHIND = 1
|
|
|
120
125
|
const DETAIL_PREFETCH_AHEAD = 3
|
|
121
126
|
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
122
127
|
const DETAIL_PREFETCH_DELAY_MS = 120
|
|
123
|
-
const MAX_KEYMAP_COUNT_PREFIX = 99
|
|
124
|
-
|
|
125
|
-
const countSequence = (count: number, key: string) => `${String(count).split("").join(" ")} ${key}`
|
|
126
|
-
|
|
127
|
-
const countedVerticalBindings = (
|
|
128
|
-
moveUp: (count: number) => void,
|
|
129
|
-
moveDown: (count: number) => void,
|
|
130
|
-
): Record<string, ScopedBindingAction> => {
|
|
131
|
-
const bindings: Record<string, ScopedBindingAction> = {}
|
|
132
|
-
for (let count = 1; count <= MAX_KEYMAP_COUNT_PREFIX; count++) {
|
|
133
|
-
bindings[countSequence(count, "k")] = () => moveUp(count)
|
|
134
|
-
bindings[countSequence(count, "up")] = () => moveUp(count)
|
|
135
|
-
bindings[countSequence(count, "j")] = () => moveDown(count)
|
|
136
|
-
bindings[countSequence(count, "down")] = () => moveDown(count)
|
|
137
|
-
}
|
|
138
|
-
return bindings
|
|
139
|
-
}
|
|
140
|
-
|
|
141
128
|
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
|
|
142
129
|
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
|
|
143
130
|
const mergedIncoming = mergeCachedDetails(incoming, existing)
|
|
@@ -209,10 +196,14 @@ const diffFileIndexAtom = Atom.make(0)
|
|
|
209
196
|
const diffScrollTopAtom = Atom.make(0)
|
|
210
197
|
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
211
198
|
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
199
|
+
const diffWhitespaceModeAtom = Atom.make<DiffWhitespaceMode>(initialDiffWhitespaceMode)
|
|
212
200
|
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
201
|
+
const diffPreferredSideAtom = Atom.make<DiffCommentSide | null>(null)
|
|
213
202
|
const diffCommentRangeStartIndexAtom = Atom.make<number | null>(null)
|
|
214
203
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
215
204
|
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
205
|
+
const pullRequestConversationAtom = Atom.make<Record<string, readonly PullRequestConversationItem[]>>({}).pipe(Atom.keepAlive)
|
|
206
|
+
const pullRequestConversationLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
216
207
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
217
208
|
|
|
218
209
|
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
@@ -346,6 +337,9 @@ const pullRequestDiffAtom = Atom.family((key: string) => {
|
|
|
346
337
|
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
347
338
|
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
348
339
|
)
|
|
340
|
+
const listPullRequestConversationAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
341
|
+
GitHubService.use((github) => github.listPullRequestConversation(input.repository, input.number))
|
|
342
|
+
)
|
|
349
343
|
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
350
344
|
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
351
345
|
)
|
|
@@ -548,10 +542,14 @@ export const App = () => {
|
|
|
548
542
|
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
549
543
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
550
544
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
545
|
+
const [diffWhitespaceMode, setDiffWhitespaceMode] = useAtom(diffWhitespaceModeAtom)
|
|
551
546
|
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
547
|
+
const [diffPreferredSide, setDiffPreferredSide] = useAtom(diffPreferredSideAtom)
|
|
552
548
|
const [diffCommentRangeStartIndex, setDiffCommentRangeStartIndex] = useAtom(diffCommentRangeStartIndexAtom)
|
|
553
549
|
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
554
550
|
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
551
|
+
const setPullRequestConversation = useAtomSet(pullRequestConversationAtom)
|
|
552
|
+
const setPullRequestConversationLoaded = useAtomSet(pullRequestConversationLoadedAtom)
|
|
555
553
|
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
556
554
|
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
557
555
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
@@ -603,7 +601,9 @@ export const App = () => {
|
|
|
603
601
|
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
604
602
|
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
605
603
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
604
|
+
const [startupLoadComplete, setStartupLoadComplete] = useState(false)
|
|
606
605
|
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
606
|
+
const [detailPreviewScrollTop, setDetailPreviewScrollTop] = useState(0)
|
|
607
607
|
const usernameResult = useAtomValue(usernameAtom)
|
|
608
608
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
609
609
|
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
@@ -611,6 +611,7 @@ export const App = () => {
|
|
|
611
611
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
612
612
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
613
613
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
614
|
+
const listPullRequestConversation = useAtomSet(listPullRequestConversationAtom, { mode: "promise" })
|
|
614
615
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
615
616
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
616
617
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
@@ -640,10 +641,11 @@ export const App = () => {
|
|
|
640
641
|
const terminalFocusedRef = useRef(true)
|
|
641
642
|
const terminalWasBlurredRef = useRef(false)
|
|
642
643
|
const pullRequestStatusRef = useRef<LoadStatus>("loading")
|
|
643
|
-
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
644
|
+
const refreshPullRequestsRef = useRef<(message?: string, options?: { readonly resetTransientState?: boolean }) => void>(() => {})
|
|
644
645
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
645
646
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
646
647
|
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
648
|
+
const detailPreviewScrollTopRef = useRef(0)
|
|
647
649
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
648
650
|
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
649
651
|
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
@@ -682,7 +684,7 @@ export const App = () => {
|
|
|
682
684
|
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
683
685
|
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
684
686
|
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
685
|
-
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
687
|
+
const isInitialLoading = !startupLoadComplete && pullRequestStatus === "loading" && pullRequests.length === 0
|
|
686
688
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
687
689
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
688
690
|
pullRequestStatusRef.current = pullRequestStatus
|
|
@@ -692,6 +694,8 @@ export const App = () => {
|
|
|
692
694
|
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
693
695
|
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
694
696
|
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
697
|
+
const pullRequestConversation = useAtomValue(pullRequestConversationAtom)
|
|
698
|
+
const pullRequestConversationLoaded = useAtomValue(pullRequestConversationLoadedAtom)
|
|
695
699
|
const selectedRepository = viewRepository(activeView)
|
|
696
700
|
const activeViews = activePullRequestViews(activeView)
|
|
697
701
|
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
@@ -710,9 +714,22 @@ export const App = () => {
|
|
|
710
714
|
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
711
715
|
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
712
716
|
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
717
|
+
const selectedConversationItems = selectedDiffKey ? pullRequestConversation[selectedDiffKey] ?? [] : []
|
|
718
|
+
const selectedConversationStatus: DetailConversationStatus = selectedDiffKey ? pullRequestConversationLoaded[selectedDiffKey] ?? "idle" : "idle"
|
|
713
719
|
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
714
720
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
715
|
-
const readyDiffFiles =
|
|
721
|
+
const readyDiffFiles = useMemo(
|
|
722
|
+
() => selectedDiffState?._tag === "Ready"
|
|
723
|
+
? diffWhitespaceMode === "ignore" ? minimizeWhitespaceDiffFiles(selectedDiffState.files) : selectedDiffState.files
|
|
724
|
+
: [],
|
|
725
|
+
[selectedDiffState, diffWhitespaceMode],
|
|
726
|
+
)
|
|
727
|
+
const displayedDiffState = useMemo(
|
|
728
|
+
() => selectedDiffState?._tag === "Ready"
|
|
729
|
+
? PullRequestDiffState.Ready({ patch: readyDiffFiles.map((file) => file.patch).join("\n"), files: readyDiffFiles })
|
|
730
|
+
: selectedDiffState,
|
|
731
|
+
[selectedDiffState, readyDiffFiles],
|
|
732
|
+
)
|
|
716
733
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
717
734
|
const diffCommentAnchors = useMemo(
|
|
718
735
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
@@ -783,12 +800,36 @@ export const App = () => {
|
|
|
783
800
|
if (!pullRequest) return
|
|
784
801
|
setPullRequestOverrides((current) => ({ ...current, [url]: transform(pullRequest) }))
|
|
785
802
|
}
|
|
786
|
-
const
|
|
803
|
+
const markPullRequestCompleted = (pullRequest: PullRequestItem, state: "closed" | "merged") => {
|
|
804
|
+
setRecentlyCompletedPullRequests((current) => ({
|
|
805
|
+
...current,
|
|
806
|
+
[pullRequest.url]: {
|
|
807
|
+
...pullRequest,
|
|
808
|
+
state,
|
|
809
|
+
autoMergeEnabled: false,
|
|
810
|
+
},
|
|
811
|
+
}))
|
|
812
|
+
}
|
|
813
|
+
const restoreOptimisticPullRequest = (pullRequest: PullRequestItem) => {
|
|
814
|
+
setRecentlyCompletedPullRequests((current) => {
|
|
815
|
+
if (!(pullRequest.url in current)) return current
|
|
816
|
+
const next = { ...current }
|
|
817
|
+
delete next[pullRequest.url]
|
|
818
|
+
return next
|
|
819
|
+
})
|
|
820
|
+
updatePullRequest(pullRequest.url, () => pullRequest)
|
|
821
|
+
}
|
|
822
|
+
const refreshPullRequests = (message?: string, options: { readonly resetTransientState?: boolean } = {}) => {
|
|
787
823
|
refreshGenerationRef.current += 1
|
|
788
824
|
detailHydrationRef.current.clear()
|
|
789
825
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
790
826
|
setLoadingMoreKey(null)
|
|
791
827
|
setPullRequestOverrides({})
|
|
828
|
+
if (options.resetTransientState) {
|
|
829
|
+
setRecentlyCompletedPullRequests({})
|
|
830
|
+
setPullRequestConversation({})
|
|
831
|
+
setPullRequestConversationLoaded({})
|
|
832
|
+
}
|
|
792
833
|
if (message) {
|
|
793
834
|
setNotice(null)
|
|
794
835
|
setRefreshCompletionMessage(message)
|
|
@@ -891,6 +932,29 @@ export const App = () => {
|
|
|
891
932
|
})
|
|
892
933
|
return true
|
|
893
934
|
}
|
|
935
|
+
const loadPullRequestConversation = (pullRequest: PullRequestItem, force = false) => {
|
|
936
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
937
|
+
const previousLoadState = registry.get(pullRequestConversationLoadedAtom)[key]
|
|
938
|
+
if (!force && previousLoadState) return
|
|
939
|
+
const generation = refreshGenerationRef.current
|
|
940
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
941
|
+
void listPullRequestConversation({ repository: pullRequest.repository, number: pullRequest.number })
|
|
942
|
+
.then((items) => {
|
|
943
|
+
if (generation !== refreshGenerationRef.current) return
|
|
944
|
+
setPullRequestConversation((current) => ({ ...current, [key]: items }))
|
|
945
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
946
|
+
})
|
|
947
|
+
.catch((error) => {
|
|
948
|
+
if (generation !== refreshGenerationRef.current) return
|
|
949
|
+
setPullRequestConversationLoaded((current) => {
|
|
950
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
951
|
+
const next = { ...current }
|
|
952
|
+
delete next[key]
|
|
953
|
+
return next
|
|
954
|
+
})
|
|
955
|
+
flashNotice(errorMessage(error))
|
|
956
|
+
})
|
|
957
|
+
}
|
|
894
958
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
895
959
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
896
960
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -1006,10 +1070,19 @@ export const App = () => {
|
|
|
1006
1070
|
setDiffFileIndex(0)
|
|
1007
1071
|
setDiffScrollTop(0)
|
|
1008
1072
|
setDiffCommentAnchorIndex(0)
|
|
1073
|
+
setDiffPreferredSide(null)
|
|
1009
1074
|
setDiffCommentRangeStartIndex(null)
|
|
1010
1075
|
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1076
|
+
if (detailPreviewScrollTopRef.current !== 0) {
|
|
1077
|
+
detailPreviewScrollTopRef.current = 0
|
|
1078
|
+
setDetailPreviewScrollTop(0)
|
|
1079
|
+
}
|
|
1011
1080
|
}, [selectedIndex])
|
|
1012
1081
|
|
|
1082
|
+
useEffect(() => {
|
|
1083
|
+
setDiffFileIndex((current) => safeDiffFileIndex(readyDiffFiles, current))
|
|
1084
|
+
}, [readyDiffFiles.length])
|
|
1085
|
+
|
|
1013
1086
|
useEffect(() => {
|
|
1014
1087
|
setDiffCommentAnchorIndex((current) => {
|
|
1015
1088
|
if (diffCommentAnchors.length === 0) return 0
|
|
@@ -1069,24 +1142,48 @@ export const App = () => {
|
|
|
1069
1142
|
}
|
|
1070
1143
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
1071
1144
|
}, [selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, selectedDiffCommentRangeAnchors, diffLineColorContextKey, effectiveDiffRenderView, diffCommentThreadAnchors])
|
|
1145
|
+
|
|
1146
|
+
// Scroll the selected line into view when the diff view is opened. Previously
|
|
1147
|
+
// opentui's `focused` scrollbox did this auto-scroll on mount; with the keymap
|
|
1148
|
+
// migration the scrollbox is `focusable={false}` so we have to scroll explicitly.
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
if (!diffFullView) return
|
|
1151
|
+
if (!selectedDiffCommentAnchor) return
|
|
1152
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
1153
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1154
|
+
}, [diffFullView])
|
|
1072
1155
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
1073
1156
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
1074
1157
|
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || isLoadingMorePullRequests || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
1075
|
-
const loadingIndicator =
|
|
1158
|
+
const loadingIndicator = SPINNER_FRAMES[loadingFrame % SPINNER_FRAMES.length]!
|
|
1076
1159
|
|
|
1077
1160
|
useEffect(() => {
|
|
1078
1161
|
if (!hasActiveLoadingIndicator) return
|
|
1079
1162
|
const interval = globalThis.setInterval(() => {
|
|
1080
|
-
setLoadingFrame((current) =>
|
|
1163
|
+
setLoadingFrame((current) => current + 1)
|
|
1081
1164
|
}, 120)
|
|
1082
1165
|
return () => globalThis.clearInterval(interval)
|
|
1083
1166
|
}, [hasActiveLoadingIndicator])
|
|
1084
1167
|
|
|
1168
|
+
useEffect(() => {
|
|
1169
|
+
if (isInitialLoading) setLoadingFrame(0)
|
|
1170
|
+
}, [isInitialLoading])
|
|
1171
|
+
|
|
1172
|
+
useEffect(() => {
|
|
1173
|
+
if (startupLoadComplete || pullRequestStatus === "loading") return
|
|
1174
|
+
setStartupLoadComplete(true)
|
|
1175
|
+
}, [startupLoadComplete, pullRequestStatus])
|
|
1176
|
+
|
|
1085
1177
|
useEffect(() => {
|
|
1086
1178
|
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1087
1179
|
hydratePullRequestDetails(selectedPullRequest, true)
|
|
1088
1180
|
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1089
1181
|
|
|
1182
|
+
useEffect(() => {
|
|
1183
|
+
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1184
|
+
loadPullRequestConversation(selectedPullRequest)
|
|
1185
|
+
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1186
|
+
|
|
1090
1187
|
useEffect(() => {
|
|
1091
1188
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1092
1189
|
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
@@ -1120,8 +1217,6 @@ export const App = () => {
|
|
|
1120
1217
|
title: `${loadingIndicator} Loading pull request details`,
|
|
1121
1218
|
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1122
1219
|
} : detailPlaceholderContent
|
|
1123
|
-
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true)
|
|
1124
|
-
|
|
1125
1220
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
1126
1221
|
|
|
1127
1222
|
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
@@ -1217,6 +1312,7 @@ export const App = () => {
|
|
|
1217
1312
|
setDiffFileIndex(0)
|
|
1218
1313
|
setDiffScrollTop(0)
|
|
1219
1314
|
setDiffCommentAnchorIndex(0)
|
|
1315
|
+
setDiffPreferredSide(null)
|
|
1220
1316
|
setDiffCommentRangeStartIndex(null)
|
|
1221
1317
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
1222
1318
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
@@ -1238,12 +1334,27 @@ export const App = () => {
|
|
|
1238
1334
|
const scrollTop = diffScrollRef.current?.scrollTop
|
|
1239
1335
|
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
1240
1336
|
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
1241
|
-
const nextIndex =
|
|
1337
|
+
const nextIndex = Math.max(0, stackedDiffFileIndexAtLine(stackedDiffFiles, scrollTop))
|
|
1242
1338
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
1243
1339
|
}
|
|
1244
1340
|
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1341
|
+
const syncDetailPreviewScrollState = useCallback(() => {
|
|
1342
|
+
const scrollTop = detailPreviewScrollRef.current?.scrollTop
|
|
1343
|
+
if (scrollTop === undefined) return
|
|
1344
|
+
const nextTop = Math.max(0, Math.floor(scrollTop))
|
|
1345
|
+
if (detailPreviewScrollTopRef.current === nextTop) return
|
|
1346
|
+
detailPreviewScrollTopRef.current = nextTop
|
|
1347
|
+
setDetailPreviewScrollTop(nextTop)
|
|
1348
|
+
}, [])
|
|
1349
|
+
|
|
1350
|
+
const scrollDetailPreviewBy = (y: number) => {
|
|
1351
|
+
detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1352
|
+
syncDetailPreviewScrollState()
|
|
1353
|
+
}
|
|
1354
|
+
const scrollDetailPreviewTo = (y: number) => {
|
|
1355
|
+
detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
1356
|
+
syncDetailPreviewScrollState()
|
|
1357
|
+
}
|
|
1247
1358
|
|
|
1248
1359
|
const ensureDiffLineVisible = (line: number) => {
|
|
1249
1360
|
const scroll = diffScrollRef.current
|
|
@@ -1262,12 +1373,23 @@ export const App = () => {
|
|
|
1262
1373
|
return () => globalThis.clearInterval(interval)
|
|
1263
1374
|
}, [diffFullView, stackedDiffFiles])
|
|
1264
1375
|
|
|
1376
|
+
useLayoutEffect(() => {
|
|
1377
|
+
if (!isWideLayout || detailFullView || diffFullView) return
|
|
1378
|
+
const scroll = detailPreviewScrollRef.current
|
|
1379
|
+
if (!scroll) return
|
|
1380
|
+
const sync = () => { syncDetailPreviewScrollState() }
|
|
1381
|
+
scroll.verticalScrollBar.on("change", sync)
|
|
1382
|
+
syncDetailPreviewScrollState()
|
|
1383
|
+
return () => { scroll.verticalScrollBar.off("change", sync) }
|
|
1384
|
+
}, [isWideLayout, detailFullView, diffFullView, syncDetailPreviewScrollState])
|
|
1385
|
+
|
|
1265
1386
|
const jumpDiffFile = (delta: 1 | -1) => {
|
|
1266
1387
|
if (readyDiffFiles.length === 0) return
|
|
1267
1388
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
1268
1389
|
setDiffFileIndex(nextIndex)
|
|
1269
1390
|
setDiffCommentRangeStartIndex(null)
|
|
1270
|
-
const
|
|
1391
|
+
const targetSide = diffPreferredSide ?? selectedDiffCommentAnchor?.side
|
|
1392
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === targetSide)
|
|
1271
1393
|
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
1272
1394
|
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1273
1395
|
scrollToDiffFile(nextIndex)
|
|
@@ -1280,13 +1402,8 @@ export const App = () => {
|
|
|
1280
1402
|
const moveDiffCommentAnchor = (delta: number, options: { readonly preserveViewportRow?: boolean } = {}) => {
|
|
1281
1403
|
const anchors = navigableDiffCommentAnchors()
|
|
1282
1404
|
if (anchors.length === 0) return
|
|
1283
|
-
const rows = [...new Set(anchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right)
|
|
1284
1405
|
const currentAnchor = selectedDiffCommentAnchor && anchors.includes(selectedDiffCommentAnchor) ? selectedDiffCommentAnchor : anchors[0]
|
|
1285
|
-
const
|
|
1286
|
-
const nextRow = rows[Math.max(0, Math.min(rows.length - 1, currentRowIndex + delta))]
|
|
1287
|
-
if (nextRow === undefined) return
|
|
1288
|
-
const nextAnchor = anchors.find((anchor) => anchor.renderLine === nextRow && anchor.side === currentAnchor?.side)
|
|
1289
|
-
?? anchors.find((anchor) => anchor.renderLine === nextRow)
|
|
1406
|
+
const nextAnchor = verticalDiffAnchor(anchors, currentAnchor ?? null, delta, diffPreferredSide)
|
|
1290
1407
|
if (!nextAnchor) return
|
|
1291
1408
|
if (options.preserveViewportRow) {
|
|
1292
1409
|
const scroll = diffScrollRef.current
|
|
@@ -1328,8 +1445,9 @@ export const App = () => {
|
|
|
1328
1445
|
}
|
|
1329
1446
|
|
|
1330
1447
|
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1448
|
+
setDiffPreferredSide(side)
|
|
1331
1449
|
if (!selectedDiffCommentAnchor) return
|
|
1332
|
-
const nextAnchor = diffCommentAnchors
|
|
1450
|
+
const nextAnchor = diffAnchorOnSide(diffCommentAnchors, selectedDiffCommentAnchor, side)
|
|
1333
1451
|
if (!nextAnchor) return
|
|
1334
1452
|
setDiffCommentRangeStartIndex(null)
|
|
1335
1453
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
@@ -1340,6 +1458,7 @@ export const App = () => {
|
|
|
1340
1458
|
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1341
1459
|
if (!nextAnchor) return
|
|
1342
1460
|
suppressNextDiffCommentScrollRef.current = true
|
|
1461
|
+
setDiffPreferredSide(side ?? nextAnchor.side)
|
|
1343
1462
|
if (diffCommentRangeStartAnchor && !sameDiffCommentTarget(diffCommentRangeStartAnchor, nextAnchor)) {
|
|
1344
1463
|
setDiffCommentRangeStartIndex(null)
|
|
1345
1464
|
}
|
|
@@ -1516,16 +1635,7 @@ export const App = () => {
|
|
|
1516
1635
|
setCloseModal((current) => ({ ...current, running: true, error: null }))
|
|
1517
1636
|
void closePullRequest({ repository, number })
|
|
1518
1637
|
.then(() => {
|
|
1519
|
-
if (previousPullRequest)
|
|
1520
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
1521
|
-
...current,
|
|
1522
|
-
[previousPullRequest.url]: {
|
|
1523
|
-
...previousPullRequest,
|
|
1524
|
-
state: "closed",
|
|
1525
|
-
autoMergeEnabled: false,
|
|
1526
|
-
},
|
|
1527
|
-
}))
|
|
1528
|
-
}
|
|
1638
|
+
if (previousPullRequest) markPullRequestCompleted(previousPullRequest, "closed")
|
|
1529
1639
|
closeActiveModal()
|
|
1530
1640
|
refreshPullRequests(`Closed #${number}`)
|
|
1531
1641
|
})
|
|
@@ -1560,6 +1670,12 @@ export const App = () => {
|
|
|
1560
1670
|
setThemeId(id)
|
|
1561
1671
|
}
|
|
1562
1672
|
|
|
1673
|
+
const toggleDiffWhitespaceMode = () => {
|
|
1674
|
+
const next = diffWhitespaceMode === "ignore" ? "show" : "ignore"
|
|
1675
|
+
setDiffWhitespaceMode(next)
|
|
1676
|
+
void Effect.runPromise(saveStoredDiffWhitespaceMode(next)).catch((error) => flashNotice(errorMessage(error)))
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1563
1679
|
const moveThemeSelection = (delta: number) => {
|
|
1564
1680
|
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
1565
1681
|
if (filteredThemes.length === 0) return
|
|
@@ -1655,30 +1771,15 @@ export const App = () => {
|
|
|
1655
1771
|
const { repository, number } = mergeModal.info
|
|
1656
1772
|
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.repository === repository && pullRequest.number === number)
|
|
1657
1773
|
const previousPullRequest = targetPullRequest ?? null
|
|
1658
|
-
const previousMergeInfo = mergeModal.info
|
|
1659
1774
|
|
|
1660
1775
|
if (targetPullRequest && option.optimisticAutoMergeEnabled !== undefined) {
|
|
1661
1776
|
updatePullRequest(targetPullRequest.url, (pullRequest) => ({ ...pullRequest, autoMergeEnabled: option.optimisticAutoMergeEnabled! }))
|
|
1662
|
-
setMergeModal((current) => ({
|
|
1663
|
-
...current,
|
|
1664
|
-
info: current.info ? { ...current.info, autoMergeEnabled: option.optimisticAutoMergeEnabled! } : current.info,
|
|
1665
|
-
}))
|
|
1666
1777
|
}
|
|
1778
|
+
if (targetPullRequest && option.optimisticState === "merged") markPullRequestCompleted(targetPullRequest, "merged")
|
|
1667
1779
|
|
|
1668
|
-
|
|
1780
|
+
closeActiveModal()
|
|
1669
1781
|
void mergePullRequest({ repository, number, action: option.action })
|
|
1670
1782
|
.then(() => {
|
|
1671
|
-
if (option.refreshOnSuccess && previousPullRequest) {
|
|
1672
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
1673
|
-
...current,
|
|
1674
|
-
[previousPullRequest.url]: {
|
|
1675
|
-
...previousPullRequest,
|
|
1676
|
-
state: "merged",
|
|
1677
|
-
autoMergeEnabled: false,
|
|
1678
|
-
},
|
|
1679
|
-
}))
|
|
1680
|
-
}
|
|
1681
|
-
closeActiveModal()
|
|
1682
1783
|
if (option.refreshOnSuccess) {
|
|
1683
1784
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
1684
1785
|
} else {
|
|
@@ -1686,8 +1787,7 @@ export const App = () => {
|
|
|
1686
1787
|
}
|
|
1687
1788
|
})
|
|
1688
1789
|
.catch((error) => {
|
|
1689
|
-
if (previousPullRequest)
|
|
1690
|
-
setMergeModal((current) => ({ ...current, running: false, info: previousMergeInfo, error: errorMessage(error) }))
|
|
1790
|
+
if (previousPullRequest) restoreOptimisticPullRequest(previousPullRequest)
|
|
1691
1791
|
flashNotice(errorMessage(error))
|
|
1692
1792
|
})
|
|
1693
1793
|
}
|
|
@@ -1801,6 +1901,7 @@ export const App = () => {
|
|
|
1801
1901
|
diffReady: selectedDiffState?._tag === "Ready",
|
|
1802
1902
|
effectiveDiffRenderView,
|
|
1803
1903
|
diffWrapMode,
|
|
1904
|
+
diffWhitespaceMode,
|
|
1804
1905
|
readyDiffFileCount: readyDiffFiles.length,
|
|
1805
1906
|
diffFileIndex,
|
|
1806
1907
|
diffRangeActive: diffCommentRangeActive,
|
|
@@ -1843,6 +1944,7 @@ export const App = () => {
|
|
|
1843
1944
|
},
|
|
1844
1945
|
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1845
1946
|
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1947
|
+
toggleDiffWhitespaceMode,
|
|
1846
1948
|
jumpDiffFile,
|
|
1847
1949
|
openSelectedDiffComment,
|
|
1848
1950
|
toggleDiffCommentRange,
|
|
@@ -1874,7 +1976,6 @@ export const App = () => {
|
|
|
1874
1976
|
}
|
|
1875
1977
|
const runCommandByIdRef = useRef(runCommandById)
|
|
1876
1978
|
runCommandByIdRef.current = runCommandById
|
|
1877
|
-
useAppCommandRegistry(appCommands, runCommandByIdRef)
|
|
1878
1979
|
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1879
1980
|
if (!commandPaletteActive) return []
|
|
1880
1981
|
const repository = parseRepositoryInput(commandPalette.query)
|
|
@@ -1901,217 +2002,30 @@ export const App = () => {
|
|
|
1901
2002
|
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1902
2003
|
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1903
2004
|
|
|
1904
|
-
|
|
1905
|
-
const globalLayerActive = noModalActive
|
|
1906
|
-
&& !diffFullView
|
|
1907
|
-
&& !detailFullView
|
|
1908
|
-
&& !filterMode
|
|
1909
|
-
useScopedBindings({
|
|
1910
|
-
when: true,
|
|
1911
|
-
bindings: {
|
|
1912
|
-
"ctrl+p": "command.open",
|
|
1913
|
-
"meta+k": "command.open",
|
|
1914
|
-
},
|
|
1915
|
-
})
|
|
1916
|
-
|
|
1917
|
-
useScopedBindings({
|
|
1918
|
-
when: closeModalActive,
|
|
1919
|
-
bindings: {
|
|
1920
|
-
escape: closeActiveModal,
|
|
1921
|
-
return: confirmClosePullRequest,
|
|
1922
|
-
},
|
|
1923
|
-
})
|
|
1924
|
-
|
|
2005
|
+
// === Helpers used by the keymap layers ===
|
|
1925
2006
|
const moveMergeSelection = (delta: -1 | 1) => setMergeModal((current) => {
|
|
1926
2007
|
const max = Math.max(0, availableMergeActions(mergeModal.info).length - 1)
|
|
1927
2008
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1928
2009
|
})
|
|
1929
|
-
useScopedBindings({
|
|
1930
|
-
when: mergeModalActive,
|
|
1931
|
-
bindings: {
|
|
1932
|
-
escape: closeActiveModal,
|
|
1933
|
-
return: () => {
|
|
1934
|
-
if (availableMergeActions(mergeModal.info).length > 0) confirmMergeAction()
|
|
1935
|
-
},
|
|
1936
|
-
up: () => moveMergeSelection(-1),
|
|
1937
|
-
k: () => moveMergeSelection(-1),
|
|
1938
|
-
down: () => moveMergeSelection(1),
|
|
1939
|
-
j: () => moveMergeSelection(1),
|
|
1940
|
-
},
|
|
1941
|
-
})
|
|
1942
|
-
|
|
1943
2010
|
const scrollCommentThread = (delta: number) => setCommentThreadModal((current) => ({
|
|
1944
2011
|
...current,
|
|
1945
2012
|
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
1946
2013
|
}))
|
|
1947
|
-
useScopedBindings({
|
|
1948
|
-
when: commentThreadModalActive,
|
|
1949
|
-
bindings: {
|
|
1950
|
-
escape: closeActiveModal,
|
|
1951
|
-
return: openDiffCommentModal,
|
|
1952
|
-
a: openDiffCommentModal,
|
|
1953
|
-
c: openDiffCommentModal,
|
|
1954
|
-
up: () => scrollCommentThread(-1),
|
|
1955
|
-
k: () => scrollCommentThread(-1),
|
|
1956
|
-
down: () => scrollCommentThread(1),
|
|
1957
|
-
j: () => scrollCommentThread(1),
|
|
1958
|
-
pageup: () => scrollCommentThread(-halfPage),
|
|
1959
|
-
"ctrl+u": () => scrollCommentThread(-halfPage),
|
|
1960
|
-
pagedown: () => scrollCommentThread(halfPage),
|
|
1961
|
-
"ctrl+d": () => scrollCommentThread(halfPage),
|
|
1962
|
-
"ctrl+v": () => scrollCommentThread(halfPage),
|
|
1963
|
-
},
|
|
1964
|
-
})
|
|
1965
|
-
|
|
1966
2014
|
const moveLabelSelection = (delta: -1 | 1) => setLabelModal((current) => {
|
|
1967
2015
|
const max = Math.max(0, filterLabels(labelModal.availableLabels, labelModal.query).length - 1)
|
|
1968
2016
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1969
2017
|
})
|
|
1970
|
-
useScopedBindings({
|
|
1971
|
-
when: labelModalActive,
|
|
1972
|
-
bindings: {
|
|
1973
|
-
escape: closeActiveModal,
|
|
1974
|
-
return: toggleLabelAtIndex,
|
|
1975
|
-
up: () => moveLabelSelection(-1),
|
|
1976
|
-
k: () => moveLabelSelection(-1),
|
|
1977
|
-
down: () => moveLabelSelection(1),
|
|
1978
|
-
j: () => moveLabelSelection(1),
|
|
1979
|
-
},
|
|
1980
|
-
})
|
|
1981
|
-
|
|
1982
|
-
useScopedBindings({
|
|
1983
|
-
when: themeModalActive,
|
|
1984
|
-
bindings: {
|
|
1985
|
-
escape: () => {
|
|
1986
|
-
if (themeModal.filterMode) updateThemeQuery("", { filterMode: false })
|
|
1987
|
-
else closeThemeModal(false)
|
|
1988
|
-
},
|
|
1989
|
-
"/": () => updateThemeQuery("", { filterMode: true }),
|
|
1990
|
-
return: () => {
|
|
1991
|
-
if (themeModal.filterMode && filterThemeDefinitions(themeModal.query).length === 0) return
|
|
1992
|
-
closeThemeModal(true)
|
|
1993
|
-
},
|
|
1994
|
-
up: () => moveThemeSelection(-1),
|
|
1995
|
-
down: () => moveThemeSelection(1),
|
|
1996
|
-
k: () => { if (!themeModal.filterMode) moveThemeSelection(-1) },
|
|
1997
|
-
j: () => { if (!themeModal.filterMode) moveThemeSelection(1) },
|
|
1998
|
-
},
|
|
1999
|
-
})
|
|
2000
|
-
|
|
2001
|
-
useScopedBindings({
|
|
2002
|
-
when: openRepositoryModalActive,
|
|
2003
|
-
bindings: {
|
|
2004
|
-
escape: closeActiveModal,
|
|
2005
|
-
return: openRepositoryFromInput,
|
|
2006
|
-
},
|
|
2007
|
-
})
|
|
2008
|
-
|
|
2009
|
-
useScopedBindings({
|
|
2010
|
-
when: commentModalActive,
|
|
2011
|
-
bindings: {
|
|
2012
|
-
escape: closeActiveModal,
|
|
2013
|
-
"ctrl+s": submitDiffComment,
|
|
2014
|
-
"ctrl+a": () => editComment(moveLineStart),
|
|
2015
|
-
"ctrl+e": () => editComment(moveLineEnd),
|
|
2016
|
-
"ctrl+b": () => editComment(editorMoveLeft),
|
|
2017
|
-
"ctrl+f": () => editComment(editorMoveRight),
|
|
2018
|
-
"ctrl+w": () => editComment(deleteWordBackward),
|
|
2019
|
-
"ctrl+u": () => editComment(deleteToLineStart),
|
|
2020
|
-
"ctrl+k": () => editComment(deleteToLineEnd),
|
|
2021
|
-
"ctrl+d": () => editComment(editorDeleteForward),
|
|
2022
|
-
"meta+b": () => editComment(moveWordBackward),
|
|
2023
|
-
"meta+left": () => editComment(moveWordBackward),
|
|
2024
|
-
"meta+f": () => editComment(moveWordForward),
|
|
2025
|
-
"meta+right": () => editComment(moveWordForward),
|
|
2026
|
-
"meta+backspace": () => editComment(deleteWordBackward),
|
|
2027
|
-
"meta+delete": () => editComment(deleteWordForward),
|
|
2028
|
-
backspace: () => editComment(editorBackspace),
|
|
2029
|
-
delete: () => editComment(editorDeleteForward),
|
|
2030
|
-
left: () => editComment(editorMoveLeft),
|
|
2031
|
-
right: () => editComment(editorMoveRight),
|
|
2032
|
-
up: () => editComment((state) => moveVertically(state, -1)),
|
|
2033
|
-
down: () => editComment((state) => moveVertically(state, 1)),
|
|
2034
|
-
home: () => editComment(moveLineStart),
|
|
2035
|
-
end: () => editComment(moveLineEnd),
|
|
2036
|
-
"shift+return": () => editComment((state) => insertText(state, "\n")),
|
|
2037
|
-
return: submitDiffComment,
|
|
2038
|
-
},
|
|
2039
|
-
})
|
|
2040
|
-
|
|
2041
2018
|
const moveCommandPaletteSelection = (delta: -1 | 1) => setCommandPalette((current) => {
|
|
2042
2019
|
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCommands)
|
|
2043
2020
|
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2044
2021
|
})
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
escape: closeActiveModal,
|
|
2049
|
-
"ctrl+c": closeActiveModal,
|
|
2050
|
-
return: () => { if (selectedCommand) runCommand(selectedCommand, { notifyDisabled: true, closePalette: true }) },
|
|
2051
|
-
up: () => moveCommandPaletteSelection(-1),
|
|
2052
|
-
down: () => moveCommandPaletteSelection(1),
|
|
2053
|
-
},
|
|
2054
|
-
})
|
|
2055
|
-
|
|
2056
|
-
useScopedBindings({
|
|
2057
|
-
when: filterMode,
|
|
2058
|
-
bindings: {
|
|
2059
|
-
escape: () => { setFilterDraft(filterQuery); setFilterMode(false) },
|
|
2060
|
-
return: () => { setFilterQuery(filterDraft); setFilterMode(false) },
|
|
2061
|
-
},
|
|
2062
|
-
})
|
|
2063
|
-
|
|
2064
|
-
useScopedBindings({
|
|
2065
|
-
when: diffFullView && noModalActive,
|
|
2066
|
-
bindings: {
|
|
2067
|
-
escape: () => {
|
|
2068
|
-
if (diffCommentRangeActive) setDiffCommentRangeStartIndex(null)
|
|
2069
|
-
else runCommandById("diff.close")
|
|
2070
|
-
},
|
|
2071
|
-
return: openSelectedDiffComment,
|
|
2072
|
-
a: "diff.add-comment",
|
|
2073
|
-
v: "diff.toggle-range",
|
|
2074
|
-
"shift+v": "diff.toggle-view",
|
|
2075
|
-
w: "diff.toggle-wrap",
|
|
2076
|
-
r: "diff.reload",
|
|
2077
|
-
n: "diff.next-thread",
|
|
2078
|
-
p: "diff.previous-thread",
|
|
2079
|
-
pageup: () => moveDiffCommentAnchor(-halfPage, { preserveViewportRow: true }),
|
|
2080
|
-
"ctrl+u": () => moveDiffCommentAnchor(-halfPage, { preserveViewportRow: true }),
|
|
2081
|
-
pagedown: () => moveDiffCommentAnchor(halfPage, { preserveViewportRow: true }),
|
|
2082
|
-
"ctrl+d": () => moveDiffCommentAnchor(halfPage, { preserveViewportRow: true }),
|
|
2083
|
-
"ctrl+v": () => moveDiffCommentAnchor(halfPage, { preserveViewportRow: true }),
|
|
2084
|
-
"shift+up": () => moveDiffCommentAnchor(-8),
|
|
2085
|
-
"shift+k": () => moveDiffCommentAnchor(-8),
|
|
2086
|
-
"meta+up": () => moveDiffCommentAnchor(-8),
|
|
2087
|
-
"meta+k": () => moveDiffCommentAnchor(-8),
|
|
2088
|
-
"shift+down": () => moveDiffCommentAnchor(8),
|
|
2089
|
-
"shift+j": () => moveDiffCommentAnchor(8),
|
|
2090
|
-
"meta+down": () => moveDiffCommentAnchor(8),
|
|
2091
|
-
"meta+j": () => moveDiffCommentAnchor(8),
|
|
2092
|
-
...countedVerticalBindings(
|
|
2093
|
-
(count) => moveDiffCommentAnchor(-count),
|
|
2094
|
-
(count) => moveDiffCommentAnchor(count),
|
|
2095
|
-
),
|
|
2096
|
-
up: () => moveDiffCommentAnchor(-1),
|
|
2097
|
-
k: () => moveDiffCommentAnchor(-1),
|
|
2098
|
-
down: () => moveDiffCommentAnchor(1),
|
|
2099
|
-
j: () => moveDiffCommentAnchor(1),
|
|
2100
|
-
left: () => selectDiffCommentSide("LEFT"),
|
|
2101
|
-
h: () => selectDiffCommentSide("LEFT"),
|
|
2102
|
-
right: () => selectDiffCommentSide("RIGHT"),
|
|
2103
|
-
l: () => selectDiffCommentSide("RIGHT"),
|
|
2104
|
-
"]": "diff.next-file",
|
|
2105
|
-
"[": "diff.previous-file",
|
|
2106
|
-
"g g": () => moveDiffCommentToBoundary("first"),
|
|
2107
|
-
"shift+g": () => moveDiffCommentToBoundary("last"),
|
|
2108
|
-
"z z": () => alignSelectedDiffCommentAnchor("center"),
|
|
2109
|
-
"z t": () => alignSelectedDiffCommentAnchor("top"),
|
|
2110
|
-
"z b": () => alignSelectedDiffCommentAnchor("bottom"),
|
|
2111
|
-
o: "pull.open-browser",
|
|
2112
|
-
},
|
|
2022
|
+
const selectCommandPaletteIndex = (index: number) => setCommandPalette((current) => {
|
|
2023
|
+
const selectedIndex = clampCommandIndex(index, commandPaletteCommands)
|
|
2024
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2113
2025
|
})
|
|
2114
|
-
|
|
2026
|
+
const runCommandPaletteCommand = (command: AppCommand) => {
|
|
2027
|
+
runCommand(command, { notifyDisabled: true, closePalette: true })
|
|
2028
|
+
}
|
|
2115
2029
|
const scrollDetailFullViewBy = (delta: number) => {
|
|
2116
2030
|
detailScrollRef.current?.scrollBy({ x: 0, y: delta })
|
|
2117
2031
|
setDetailScrollOffset((current) => Math.max(0, current + delta))
|
|
@@ -2120,26 +2034,6 @@ export const App = () => {
|
|
|
2120
2034
|
detailScrollRef.current?.scrollTo({ x: 0, y })
|
|
2121
2035
|
setDetailScrollOffset(y)
|
|
2122
2036
|
}
|
|
2123
|
-
useScopedBindings({
|
|
2124
|
-
when: detailFullView && noModalActive,
|
|
2125
|
-
bindings: {
|
|
2126
|
-
...scrollBindings(scrollDetailFullViewBy, halfPage, scrollDetailFullViewTo),
|
|
2127
|
-
escape: "detail.close",
|
|
2128
|
-
return: "detail.close",
|
|
2129
|
-
t: "theme.open",
|
|
2130
|
-
d: "diff.open",
|
|
2131
|
-
x: "pull.close",
|
|
2132
|
-
l: "pull.labels",
|
|
2133
|
-
m: "pull.merge",
|
|
2134
|
-
"shift+m": "pull.merge",
|
|
2135
|
-
s: "pull.toggle-draft",
|
|
2136
|
-
"shift+s": "pull.toggle-draft",
|
|
2137
|
-
r: "pull.refresh",
|
|
2138
|
-
o: "pull.open-browser",
|
|
2139
|
-
y: "pull.copy-metadata",
|
|
2140
|
-
},
|
|
2141
|
-
})
|
|
2142
|
-
|
|
2143
2037
|
const moveSelectedToPreviousGroup = () => setSelectedIndex((current) => {
|
|
2144
2038
|
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2145
2039
|
const currentGroup = getCurrentGroupIndex(current)
|
|
@@ -2178,48 +2072,168 @@ export const App = () => {
|
|
|
2178
2072
|
if (visiblePullRequests.length === 0) return 0
|
|
2179
2073
|
return current <= 0 ? visiblePullRequests.length - 1 : current - 1
|
|
2180
2074
|
})
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
up: stepSelectedUpWrap,
|
|
2216
|
-
k: stepSelectedUpWrap,
|
|
2217
|
-
down: stepSelectedDownWithLoadMore,
|
|
2218
|
-
j: stepSelectedDownWithLoadMore,
|
|
2219
|
-
"g g": () => setSelectedIndex(0),
|
|
2220
|
-
"shift+g": () => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2075
|
+
const handleQuitOrClose = () => {
|
|
2076
|
+
if (themeModalActive) {
|
|
2077
|
+
closeThemeModal(false)
|
|
2078
|
+
return
|
|
2079
|
+
}
|
|
2080
|
+
if (activeModal._tag !== "None") {
|
|
2081
|
+
closeActiveModal()
|
|
2082
|
+
return
|
|
2083
|
+
}
|
|
2084
|
+
runCommandById("app.quit")
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// === Build the keymap context ===
|
|
2088
|
+
const appCtx: AppCtx = {
|
|
2089
|
+
closeModalActive,
|
|
2090
|
+
mergeModalActive,
|
|
2091
|
+
commentThreadModalActive,
|
|
2092
|
+
labelModalActive,
|
|
2093
|
+
themeModalActive,
|
|
2094
|
+
openRepositoryModalActive,
|
|
2095
|
+
commentModalActive,
|
|
2096
|
+
commandPaletteActive,
|
|
2097
|
+
filterMode,
|
|
2098
|
+
diffFullView,
|
|
2099
|
+
detailFullView,
|
|
2100
|
+
textInputActive: commentModalActive
|
|
2101
|
+
|| commandPaletteActive
|
|
2102
|
+
|| openRepositoryModalActive
|
|
2103
|
+
|| labelModalActive
|
|
2104
|
+
|| filterMode
|
|
2105
|
+
|| (themeModalActive && themeModal.filterMode),
|
|
2106
|
+
closeModal: {
|
|
2107
|
+
closeModal: closeActiveModal,
|
|
2108
|
+
confirmClose: confirmClosePullRequest,
|
|
2221
2109
|
},
|
|
2222
|
-
|
|
2110
|
+
mergeModal: {
|
|
2111
|
+
availableActionCount: availableMergeActions(mergeModal.info).length,
|
|
2112
|
+
closeModal: closeActiveModal,
|
|
2113
|
+
confirmMerge: confirmMergeAction,
|
|
2114
|
+
moveSelection: moveMergeSelection,
|
|
2115
|
+
},
|
|
2116
|
+
commentThreadModal: {
|
|
2117
|
+
halfPage,
|
|
2118
|
+
closeModal: closeActiveModal,
|
|
2119
|
+
openInlineComment: openDiffCommentModal,
|
|
2120
|
+
scrollBy: scrollCommentThread,
|
|
2121
|
+
},
|
|
2122
|
+
labelModal: {
|
|
2123
|
+
closeModal: closeActiveModal,
|
|
2124
|
+
toggleSelected: toggleLabelAtIndex,
|
|
2125
|
+
moveSelection: moveLabelSelection,
|
|
2126
|
+
},
|
|
2127
|
+
themeModal: {
|
|
2128
|
+
filterMode: themeModal.filterMode,
|
|
2129
|
+
hasFilteredResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
2130
|
+
closeWithoutSaving: () => closeThemeModal(false),
|
|
2131
|
+
clearFilter: () => updateThemeQuery("", { filterMode: false }),
|
|
2132
|
+
enterFilterMode: () => updateThemeQuery("", { filterMode: true }),
|
|
2133
|
+
confirmSelection: () => closeThemeModal(true),
|
|
2134
|
+
moveSelection: moveThemeSelection,
|
|
2135
|
+
},
|
|
2136
|
+
openRepositoryModal: {
|
|
2137
|
+
closeModal: closeActiveModal,
|
|
2138
|
+
openFromInput: openRepositoryFromInput,
|
|
2139
|
+
},
|
|
2140
|
+
commentModal: {
|
|
2141
|
+
closeModal: closeActiveModal,
|
|
2142
|
+
submit: submitDiffComment,
|
|
2143
|
+
insertNewline: () => editComment((state) => insertText(state, "\n")),
|
|
2144
|
+
moveLeft: () => editComment(editorMoveLeft),
|
|
2145
|
+
moveRight: () => editComment(editorMoveRight),
|
|
2146
|
+
moveUp: () => editComment((state) => moveVertically(state, -1)),
|
|
2147
|
+
moveDown: () => editComment((state) => moveVertically(state, 1)),
|
|
2148
|
+
moveLineStart: () => editComment(moveLineStart),
|
|
2149
|
+
moveLineEnd: () => editComment(moveLineEnd),
|
|
2150
|
+
moveWordBackward: () => editComment(moveWordBackward),
|
|
2151
|
+
moveWordForward: () => editComment(moveWordForward),
|
|
2152
|
+
backspace: () => editComment(editorBackspace),
|
|
2153
|
+
deleteForward: () => editComment(editorDeleteForward),
|
|
2154
|
+
deleteWordBackward: () => editComment(deleteWordBackward),
|
|
2155
|
+
deleteWordForward: () => editComment(deleteWordForward),
|
|
2156
|
+
deleteToLineStart: () => editComment(deleteToLineStart),
|
|
2157
|
+
deleteToLineEnd: () => editComment(deleteToLineEnd),
|
|
2158
|
+
},
|
|
2159
|
+
commandPalette: {
|
|
2160
|
+
closeModal: closeActiveModal,
|
|
2161
|
+
runSelected: () => {
|
|
2162
|
+
if (selectedCommand) runCommandPaletteCommand(selectedCommand)
|
|
2163
|
+
},
|
|
2164
|
+
moveSelection: moveCommandPaletteSelection,
|
|
2165
|
+
},
|
|
2166
|
+
filterModeCtx: {
|
|
2167
|
+
cancel: () => {
|
|
2168
|
+
setFilterDraft(filterQuery)
|
|
2169
|
+
setFilterMode(false)
|
|
2170
|
+
},
|
|
2171
|
+
commit: () => {
|
|
2172
|
+
setFilterQuery(filterDraft)
|
|
2173
|
+
setFilterMode(false)
|
|
2174
|
+
},
|
|
2175
|
+
},
|
|
2176
|
+
diff: {
|
|
2177
|
+
halfPage,
|
|
2178
|
+
handleEscape: () => {
|
|
2179
|
+
if (diffCommentRangeActive) setDiffCommentRangeStartIndex(null)
|
|
2180
|
+
else runCommandById("diff.close")
|
|
2181
|
+
},
|
|
2182
|
+
openSelectedComment: openSelectedDiffComment,
|
|
2183
|
+
addComment: () => runCommandById("diff.add-comment"),
|
|
2184
|
+
toggleRange: () => runCommandById("diff.toggle-range"),
|
|
2185
|
+
toggleView: () => runCommandById("diff.toggle-view"),
|
|
2186
|
+
toggleWrap: () => runCommandById("diff.toggle-wrap"),
|
|
2187
|
+
reload: () => runCommandById("diff.reload"),
|
|
2188
|
+
nextThread: () => runCommandById("diff.next-thread"),
|
|
2189
|
+
previousThread: () => runCommandById("diff.previous-thread"),
|
|
2190
|
+
moveAnchor: moveDiffCommentAnchor,
|
|
2191
|
+
moveAnchorToBoundary: moveDiffCommentToBoundary,
|
|
2192
|
+
alignAnchor: alignSelectedDiffCommentAnchor,
|
|
2193
|
+
selectSide: selectDiffCommentSide,
|
|
2194
|
+
nextFile: () => runCommandById("diff.next-file"),
|
|
2195
|
+
previousFile: () => runCommandById("diff.previous-file"),
|
|
2196
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2197
|
+
},
|
|
2198
|
+
detail: {
|
|
2199
|
+
halfPage,
|
|
2200
|
+
scrollBy: scrollDetailFullViewBy,
|
|
2201
|
+
scrollTo: scrollDetailFullViewTo,
|
|
2202
|
+
closeDetail: () => runCommandById("detail.close"),
|
|
2203
|
+
openTheme: () => runCommandById("theme.open"),
|
|
2204
|
+
openDiff: () => runCommandById("diff.open"),
|
|
2205
|
+
closePullRequest: () => runCommandById("pull.close"),
|
|
2206
|
+
openLabels: () => runCommandById("pull.labels"),
|
|
2207
|
+
openMerge: () => runCommandById("pull.merge"),
|
|
2208
|
+
toggleDraft: () => runCommandById("pull.toggle-draft"),
|
|
2209
|
+
refresh: () => runCommandById("pull.refresh"),
|
|
2210
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2211
|
+
copyMetadata: () => runCommandById("pull.copy-metadata"),
|
|
2212
|
+
},
|
|
2213
|
+
listNav: {
|
|
2214
|
+
halfPage,
|
|
2215
|
+
visibleCount: visiblePullRequests.length,
|
|
2216
|
+
hasFilter: filterQuery.length > 0,
|
|
2217
|
+
canScrollDetailPreview: isWideLayout && selectedPullRequest !== null,
|
|
2218
|
+
runCommandById: (id) => { runCommandById(id) },
|
|
2219
|
+
switchQueueMode,
|
|
2220
|
+
scrollDetailPreviewBy,
|
|
2221
|
+
scrollDetailPreviewTo,
|
|
2222
|
+
clearFilter: () => { runCommandById("filter.clear") },
|
|
2223
|
+
stepSelected,
|
|
2224
|
+
stepSelectedUp,
|
|
2225
|
+
stepSelectedDown,
|
|
2226
|
+
stepSelectedUpWrap,
|
|
2227
|
+
stepSelectedDownWithLoadMore,
|
|
2228
|
+
moveSelectedToPreviousGroup,
|
|
2229
|
+
moveSelectedToNextGroup,
|
|
2230
|
+
setSelected: (index) => setSelectedIndex(index),
|
|
2231
|
+
},
|
|
2232
|
+
openCommandPalette: () => { runCommandById("command.open") },
|
|
2233
|
+
handleQuitOrClose,
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
useKeymap(appKeymap, appCtx, useOpenTuiSubscribe())
|
|
2223
2237
|
|
|
2224
2238
|
useKeyboard((key) => {
|
|
2225
2239
|
if (commandPaletteActive) {
|
|
@@ -2243,18 +2257,9 @@ export const App = () => {
|
|
|
2243
2257
|
return
|
|
2244
2258
|
}
|
|
2245
2259
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
return
|
|
2250
|
-
}
|
|
2251
|
-
if (activeModal._tag !== "None") {
|
|
2252
|
-
closeActiveModal()
|
|
2253
|
-
return
|
|
2254
|
-
}
|
|
2255
|
-
runCommandById("app.quit")
|
|
2256
|
-
return
|
|
2257
|
-
}
|
|
2260
|
+
// q / ctrl+c quit/close-modal logic now lives in the keymap layer
|
|
2261
|
+
// (handleQuitOrClose). This useKeyboard callback only handles raw text
|
|
2262
|
+
// input for modals that need character-by-character accumulation.
|
|
2258
2263
|
|
|
2259
2264
|
if (themeModalActive) {
|
|
2260
2265
|
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
@@ -2290,16 +2295,34 @@ export const App = () => {
|
|
|
2290
2295
|
}
|
|
2291
2296
|
})
|
|
2292
2297
|
|
|
2298
|
+
if (isInitialLoading) {
|
|
2299
|
+
return (
|
|
2300
|
+
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
2301
|
+
<LoadingLogoPane content={detailPlaceholderContent} width={contentWidth} height={terminalHeight} frame={loadingFrame} />
|
|
2302
|
+
</box>
|
|
2303
|
+
)
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2293
2306
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
2294
2307
|
const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
|
|
2295
2308
|
const fullscreenDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout)
|
|
2296
2309
|
const fullscreenDetailBodyViewportHeight = Math.max(1, wideBodyHeight - fullscreenDetailHeaderHeight)
|
|
2297
|
-
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth)
|
|
2310
|
+
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2298
2311
|
const fullscreenDetailBodyScrollable = fullscreenDetailBodyHeight > fullscreenDetailBodyViewportHeight
|
|
2299
2312
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2300
2313
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2301
|
-
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2314
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2302
2315
|
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2316
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows({
|
|
2317
|
+
pullRequest: selectedPullRequest,
|
|
2318
|
+
paneWidth: rightPaneWidth,
|
|
2319
|
+
showChecks: true,
|
|
2320
|
+
contentWidth: rightContentWidth,
|
|
2321
|
+
conversationItems: selectedConversationItems,
|
|
2322
|
+
conversationStatus: selectedConversationStatus,
|
|
2323
|
+
bodyScrollTop: detailPreviewScrollTop,
|
|
2324
|
+
bodyViewportHeight: wideDetailBodyViewportHeight,
|
|
2325
|
+
})
|
|
2303
2326
|
|
|
2304
2327
|
const prListProps = {
|
|
2305
2328
|
groups: visibleGroups,
|
|
@@ -2370,20 +2393,19 @@ export const App = () => {
|
|
|
2370
2393
|
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
2371
2394
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
2372
2395
|
</box>
|
|
2373
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2396
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2374
2397
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┬" />
|
|
2375
2398
|
) : (
|
|
2376
2399
|
<Divider width={contentWidth} />
|
|
2377
2400
|
)}
|
|
2378
|
-
{
|
|
2379
|
-
<LoadingPane content={detailPlaceholderContent} width={contentWidth} height={wideBodyHeight} />
|
|
2380
|
-
) : diffFullView ? (
|
|
2401
|
+
{diffFullView ? (
|
|
2381
2402
|
<PullRequestDiffPane
|
|
2382
2403
|
pullRequest={selectedPullRequest}
|
|
2383
|
-
diffState={
|
|
2404
|
+
diffState={displayedDiffState}
|
|
2384
2405
|
stackedFiles={stackedDiffFiles}
|
|
2385
2406
|
scrollTop={diffScrollTop}
|
|
2386
2407
|
view={effectiveDiffRenderView}
|
|
2408
|
+
whitespaceMode={diffWhitespaceMode}
|
|
2387
2409
|
wrapMode={diffWrapMode}
|
|
2388
2410
|
paneWidth={contentWidth}
|
|
2389
2411
|
height={wideBodyHeight}
|
|
@@ -2406,8 +2428,8 @@ export const App = () => {
|
|
|
2406
2428
|
{selectedPullRequest ? (
|
|
2407
2429
|
<>
|
|
2408
2430
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks />
|
|
2409
|
-
<scrollbox ref={detailScrollRef}
|
|
2410
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2431
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2432
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2411
2433
|
</scrollbox>
|
|
2412
2434
|
</>
|
|
2413
2435
|
) : (
|
|
@@ -2434,7 +2456,7 @@ export const App = () => {
|
|
|
2434
2456
|
<>
|
|
2435
2457
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2436
2458
|
<scrollbox ref={detailPreviewScrollRef} flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2437
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2459
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2438
2460
|
</scrollbox>
|
|
2439
2461
|
</>
|
|
2440
2462
|
) : (
|
|
@@ -2447,8 +2469,8 @@ export const App = () => {
|
|
|
2447
2469
|
{selectedPullRequest ? (
|
|
2448
2470
|
<>
|
|
2449
2471
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} />
|
|
2450
|
-
<scrollbox ref={detailScrollRef}
|
|
2451
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2472
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2473
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2452
2474
|
</scrollbox>
|
|
2453
2475
|
</>
|
|
2454
2476
|
) : (
|
|
@@ -2457,7 +2479,7 @@ export const App = () => {
|
|
|
2457
2479
|
</box>
|
|
2458
2480
|
) : (
|
|
2459
2481
|
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2460
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2482
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2461
2483
|
<Divider width={contentWidth} />
|
|
2462
2484
|
<box flexGrow={1} flexDirection="column">
|
|
2463
2485
|
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
@@ -2469,7 +2491,7 @@ export const App = () => {
|
|
|
2469
2491
|
</box>
|
|
2470
2492
|
)}
|
|
2471
2493
|
|
|
2472
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2494
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2473
2495
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┴" />
|
|
2474
2496
|
) : (
|
|
2475
2497
|
<Divider width={contentWidth} />
|
|
@@ -2573,6 +2595,8 @@ export const App = () => {
|
|
|
2573
2595
|
modalHeight={commandPaletteHeight}
|
|
2574
2596
|
offsetLeft={commandPaletteLeft}
|
|
2575
2597
|
offsetTop={commandPaletteTop}
|
|
2598
|
+
onSelectCommandIndex={selectCommandPaletteIndex}
|
|
2599
|
+
onRunCommand={runCommandPaletteCommand}
|
|
2576
2600
|
/>
|
|
2577
2601
|
) : null}
|
|
2578
2602
|
</box>
|