@kitlangton/ghui 0.1.22 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/App.tsx +338 -353
- 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 +144 -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,7 +1,8 @@
|
|
|
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"
|
|
@@ -12,7 +13,7 @@ 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,6 +601,7 @@ 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)
|
|
607
606
|
const usernameResult = useAtomValue(usernameAtom)
|
|
608
607
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
@@ -611,6 +610,7 @@ export const App = () => {
|
|
|
611
610
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
612
611
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
613
612
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
613
|
+
const listPullRequestConversation = useAtomSet(listPullRequestConversationAtom, { mode: "promise" })
|
|
614
614
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
615
615
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
616
616
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
@@ -640,7 +640,7 @@ export const App = () => {
|
|
|
640
640
|
const terminalFocusedRef = useRef(true)
|
|
641
641
|
const terminalWasBlurredRef = useRef(false)
|
|
642
642
|
const pullRequestStatusRef = useRef<LoadStatus>("loading")
|
|
643
|
-
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
643
|
+
const refreshPullRequestsRef = useRef<(message?: string, options?: { readonly resetTransientState?: boolean }) => void>(() => {})
|
|
644
644
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
645
645
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
646
646
|
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
@@ -682,7 +682,7 @@ export const App = () => {
|
|
|
682
682
|
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
683
683
|
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
684
684
|
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
685
|
-
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
685
|
+
const isInitialLoading = !startupLoadComplete && pullRequestStatus === "loading" && pullRequests.length === 0
|
|
686
686
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
687
687
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
688
688
|
pullRequestStatusRef.current = pullRequestStatus
|
|
@@ -692,6 +692,8 @@ export const App = () => {
|
|
|
692
692
|
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
693
693
|
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
694
694
|
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
695
|
+
const pullRequestConversation = useAtomValue(pullRequestConversationAtom)
|
|
696
|
+
const pullRequestConversationLoaded = useAtomValue(pullRequestConversationLoadedAtom)
|
|
695
697
|
const selectedRepository = viewRepository(activeView)
|
|
696
698
|
const activeViews = activePullRequestViews(activeView)
|
|
697
699
|
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
@@ -710,9 +712,22 @@ export const App = () => {
|
|
|
710
712
|
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
711
713
|
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
712
714
|
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
715
|
+
const selectedConversationItems = selectedDiffKey ? pullRequestConversation[selectedDiffKey] ?? [] : []
|
|
716
|
+
const selectedConversationStatus: DetailConversationStatus = selectedDiffKey ? pullRequestConversationLoaded[selectedDiffKey] ?? "idle" : "idle"
|
|
713
717
|
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
714
718
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
715
|
-
const readyDiffFiles =
|
|
719
|
+
const readyDiffFiles = useMemo(
|
|
720
|
+
() => selectedDiffState?._tag === "Ready"
|
|
721
|
+
? diffWhitespaceMode === "ignore" ? minimizeWhitespaceDiffFiles(selectedDiffState.files) : selectedDiffState.files
|
|
722
|
+
: [],
|
|
723
|
+
[selectedDiffState, diffWhitespaceMode],
|
|
724
|
+
)
|
|
725
|
+
const displayedDiffState = useMemo(
|
|
726
|
+
() => selectedDiffState?._tag === "Ready"
|
|
727
|
+
? PullRequestDiffState.Ready({ patch: readyDiffFiles.map((file) => file.patch).join("\n"), files: readyDiffFiles })
|
|
728
|
+
: selectedDiffState,
|
|
729
|
+
[selectedDiffState, readyDiffFiles],
|
|
730
|
+
)
|
|
716
731
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
717
732
|
const diffCommentAnchors = useMemo(
|
|
718
733
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
@@ -783,12 +798,36 @@ export const App = () => {
|
|
|
783
798
|
if (!pullRequest) return
|
|
784
799
|
setPullRequestOverrides((current) => ({ ...current, [url]: transform(pullRequest) }))
|
|
785
800
|
}
|
|
786
|
-
const
|
|
801
|
+
const markPullRequestCompleted = (pullRequest: PullRequestItem, state: "closed" | "merged") => {
|
|
802
|
+
setRecentlyCompletedPullRequests((current) => ({
|
|
803
|
+
...current,
|
|
804
|
+
[pullRequest.url]: {
|
|
805
|
+
...pullRequest,
|
|
806
|
+
state,
|
|
807
|
+
autoMergeEnabled: false,
|
|
808
|
+
},
|
|
809
|
+
}))
|
|
810
|
+
}
|
|
811
|
+
const restoreOptimisticPullRequest = (pullRequest: PullRequestItem) => {
|
|
812
|
+
setRecentlyCompletedPullRequests((current) => {
|
|
813
|
+
if (!(pullRequest.url in current)) return current
|
|
814
|
+
const next = { ...current }
|
|
815
|
+
delete next[pullRequest.url]
|
|
816
|
+
return next
|
|
817
|
+
})
|
|
818
|
+
updatePullRequest(pullRequest.url, () => pullRequest)
|
|
819
|
+
}
|
|
820
|
+
const refreshPullRequests = (message?: string, options: { readonly resetTransientState?: boolean } = {}) => {
|
|
787
821
|
refreshGenerationRef.current += 1
|
|
788
822
|
detailHydrationRef.current.clear()
|
|
789
823
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
790
824
|
setLoadingMoreKey(null)
|
|
791
825
|
setPullRequestOverrides({})
|
|
826
|
+
if (options.resetTransientState) {
|
|
827
|
+
setRecentlyCompletedPullRequests({})
|
|
828
|
+
setPullRequestConversation({})
|
|
829
|
+
setPullRequestConversationLoaded({})
|
|
830
|
+
}
|
|
792
831
|
if (message) {
|
|
793
832
|
setNotice(null)
|
|
794
833
|
setRefreshCompletionMessage(message)
|
|
@@ -891,6 +930,29 @@ export const App = () => {
|
|
|
891
930
|
})
|
|
892
931
|
return true
|
|
893
932
|
}
|
|
933
|
+
const loadPullRequestConversation = (pullRequest: PullRequestItem, force = false) => {
|
|
934
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
935
|
+
const previousLoadState = registry.get(pullRequestConversationLoadedAtom)[key]
|
|
936
|
+
if (!force && previousLoadState) return
|
|
937
|
+
const generation = refreshGenerationRef.current
|
|
938
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
939
|
+
void listPullRequestConversation({ repository: pullRequest.repository, number: pullRequest.number })
|
|
940
|
+
.then((items) => {
|
|
941
|
+
if (generation !== refreshGenerationRef.current) return
|
|
942
|
+
setPullRequestConversation((current) => ({ ...current, [key]: items }))
|
|
943
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
944
|
+
})
|
|
945
|
+
.catch((error) => {
|
|
946
|
+
if (generation !== refreshGenerationRef.current) return
|
|
947
|
+
setPullRequestConversationLoaded((current) => {
|
|
948
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
949
|
+
const next = { ...current }
|
|
950
|
+
delete next[key]
|
|
951
|
+
return next
|
|
952
|
+
})
|
|
953
|
+
flashNotice(errorMessage(error))
|
|
954
|
+
})
|
|
955
|
+
}
|
|
894
956
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
895
957
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
896
958
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -1006,10 +1068,15 @@ export const App = () => {
|
|
|
1006
1068
|
setDiffFileIndex(0)
|
|
1007
1069
|
setDiffScrollTop(0)
|
|
1008
1070
|
setDiffCommentAnchorIndex(0)
|
|
1071
|
+
setDiffPreferredSide(null)
|
|
1009
1072
|
setDiffCommentRangeStartIndex(null)
|
|
1010
1073
|
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1011
1074
|
}, [selectedIndex])
|
|
1012
1075
|
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
setDiffFileIndex((current) => safeDiffFileIndex(readyDiffFiles, current))
|
|
1078
|
+
}, [readyDiffFiles.length])
|
|
1079
|
+
|
|
1013
1080
|
useEffect(() => {
|
|
1014
1081
|
setDiffCommentAnchorIndex((current) => {
|
|
1015
1082
|
if (diffCommentAnchors.length === 0) return 0
|
|
@@ -1069,24 +1136,48 @@ export const App = () => {
|
|
|
1069
1136
|
}
|
|
1070
1137
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
1071
1138
|
}, [selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, selectedDiffCommentRangeAnchors, diffLineColorContextKey, effectiveDiffRenderView, diffCommentThreadAnchors])
|
|
1139
|
+
|
|
1140
|
+
// Scroll the selected line into view when the diff view is opened. Previously
|
|
1141
|
+
// opentui's `focused` scrollbox did this auto-scroll on mount; with the keymap
|
|
1142
|
+
// migration the scrollbox is `focusable={false}` so we have to scroll explicitly.
|
|
1143
|
+
useEffect(() => {
|
|
1144
|
+
if (!diffFullView) return
|
|
1145
|
+
if (!selectedDiffCommentAnchor) return
|
|
1146
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
1147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1148
|
+
}, [diffFullView])
|
|
1072
1149
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
1073
1150
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
1074
1151
|
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || isLoadingMorePullRequests || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
1075
|
-
const loadingIndicator =
|
|
1152
|
+
const loadingIndicator = SPINNER_FRAMES[loadingFrame % SPINNER_FRAMES.length]!
|
|
1076
1153
|
|
|
1077
1154
|
useEffect(() => {
|
|
1078
1155
|
if (!hasActiveLoadingIndicator) return
|
|
1079
1156
|
const interval = globalThis.setInterval(() => {
|
|
1080
|
-
setLoadingFrame((current) =>
|
|
1157
|
+
setLoadingFrame((current) => current + 1)
|
|
1081
1158
|
}, 120)
|
|
1082
1159
|
return () => globalThis.clearInterval(interval)
|
|
1083
1160
|
}, [hasActiveLoadingIndicator])
|
|
1084
1161
|
|
|
1162
|
+
useEffect(() => {
|
|
1163
|
+
if (isInitialLoading) setLoadingFrame(0)
|
|
1164
|
+
}, [isInitialLoading])
|
|
1165
|
+
|
|
1166
|
+
useEffect(() => {
|
|
1167
|
+
if (startupLoadComplete || pullRequestStatus === "loading") return
|
|
1168
|
+
setStartupLoadComplete(true)
|
|
1169
|
+
}, [startupLoadComplete, pullRequestStatus])
|
|
1170
|
+
|
|
1085
1171
|
useEffect(() => {
|
|
1086
1172
|
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1087
1173
|
hydratePullRequestDetails(selectedPullRequest, true)
|
|
1088
1174
|
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1089
1175
|
|
|
1176
|
+
useEffect(() => {
|
|
1177
|
+
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1178
|
+
loadPullRequestConversation(selectedPullRequest)
|
|
1179
|
+
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1180
|
+
|
|
1090
1181
|
useEffect(() => {
|
|
1091
1182
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1092
1183
|
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
@@ -1120,7 +1211,7 @@ export const App = () => {
|
|
|
1120
1211
|
title: `${loadingIndicator} Loading pull request details`,
|
|
1121
1212
|
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1122
1213
|
} : detailPlaceholderContent
|
|
1123
|
-
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true)
|
|
1214
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
1124
1215
|
|
|
1125
1216
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
1126
1217
|
|
|
@@ -1217,6 +1308,7 @@ export const App = () => {
|
|
|
1217
1308
|
setDiffFileIndex(0)
|
|
1218
1309
|
setDiffScrollTop(0)
|
|
1219
1310
|
setDiffCommentAnchorIndex(0)
|
|
1311
|
+
setDiffPreferredSide(null)
|
|
1220
1312
|
setDiffCommentRangeStartIndex(null)
|
|
1221
1313
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
1222
1314
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
@@ -1238,7 +1330,7 @@ export const App = () => {
|
|
|
1238
1330
|
const scrollTop = diffScrollRef.current?.scrollTop
|
|
1239
1331
|
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
1240
1332
|
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
1241
|
-
const nextIndex =
|
|
1333
|
+
const nextIndex = Math.max(0, stackedDiffFileIndexAtLine(stackedDiffFiles, scrollTop))
|
|
1242
1334
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
1243
1335
|
}
|
|
1244
1336
|
|
|
@@ -1267,7 +1359,8 @@ export const App = () => {
|
|
|
1267
1359
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
1268
1360
|
setDiffFileIndex(nextIndex)
|
|
1269
1361
|
setDiffCommentRangeStartIndex(null)
|
|
1270
|
-
const
|
|
1362
|
+
const targetSide = diffPreferredSide ?? selectedDiffCommentAnchor?.side
|
|
1363
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === targetSide)
|
|
1271
1364
|
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
1272
1365
|
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1273
1366
|
scrollToDiffFile(nextIndex)
|
|
@@ -1280,13 +1373,8 @@ export const App = () => {
|
|
|
1280
1373
|
const moveDiffCommentAnchor = (delta: number, options: { readonly preserveViewportRow?: boolean } = {}) => {
|
|
1281
1374
|
const anchors = navigableDiffCommentAnchors()
|
|
1282
1375
|
if (anchors.length === 0) return
|
|
1283
|
-
const rows = [...new Set(anchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right)
|
|
1284
1376
|
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)
|
|
1377
|
+
const nextAnchor = verticalDiffAnchor(anchors, currentAnchor ?? null, delta, diffPreferredSide)
|
|
1290
1378
|
if (!nextAnchor) return
|
|
1291
1379
|
if (options.preserveViewportRow) {
|
|
1292
1380
|
const scroll = diffScrollRef.current
|
|
@@ -1328,8 +1416,9 @@ export const App = () => {
|
|
|
1328
1416
|
}
|
|
1329
1417
|
|
|
1330
1418
|
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1419
|
+
setDiffPreferredSide(side)
|
|
1331
1420
|
if (!selectedDiffCommentAnchor) return
|
|
1332
|
-
const nextAnchor = diffCommentAnchors
|
|
1421
|
+
const nextAnchor = diffAnchorOnSide(diffCommentAnchors, selectedDiffCommentAnchor, side)
|
|
1333
1422
|
if (!nextAnchor) return
|
|
1334
1423
|
setDiffCommentRangeStartIndex(null)
|
|
1335
1424
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
@@ -1340,6 +1429,7 @@ export const App = () => {
|
|
|
1340
1429
|
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1341
1430
|
if (!nextAnchor) return
|
|
1342
1431
|
suppressNextDiffCommentScrollRef.current = true
|
|
1432
|
+
setDiffPreferredSide(side ?? nextAnchor.side)
|
|
1343
1433
|
if (diffCommentRangeStartAnchor && !sameDiffCommentTarget(diffCommentRangeStartAnchor, nextAnchor)) {
|
|
1344
1434
|
setDiffCommentRangeStartIndex(null)
|
|
1345
1435
|
}
|
|
@@ -1516,16 +1606,7 @@ export const App = () => {
|
|
|
1516
1606
|
setCloseModal((current) => ({ ...current, running: true, error: null }))
|
|
1517
1607
|
void closePullRequest({ repository, number })
|
|
1518
1608
|
.then(() => {
|
|
1519
|
-
if (previousPullRequest)
|
|
1520
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
1521
|
-
...current,
|
|
1522
|
-
[previousPullRequest.url]: {
|
|
1523
|
-
...previousPullRequest,
|
|
1524
|
-
state: "closed",
|
|
1525
|
-
autoMergeEnabled: false,
|
|
1526
|
-
},
|
|
1527
|
-
}))
|
|
1528
|
-
}
|
|
1609
|
+
if (previousPullRequest) markPullRequestCompleted(previousPullRequest, "closed")
|
|
1529
1610
|
closeActiveModal()
|
|
1530
1611
|
refreshPullRequests(`Closed #${number}`)
|
|
1531
1612
|
})
|
|
@@ -1560,6 +1641,12 @@ export const App = () => {
|
|
|
1560
1641
|
setThemeId(id)
|
|
1561
1642
|
}
|
|
1562
1643
|
|
|
1644
|
+
const toggleDiffWhitespaceMode = () => {
|
|
1645
|
+
const next = diffWhitespaceMode === "ignore" ? "show" : "ignore"
|
|
1646
|
+
setDiffWhitespaceMode(next)
|
|
1647
|
+
void Effect.runPromise(saveStoredDiffWhitespaceMode(next)).catch((error) => flashNotice(errorMessage(error)))
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1563
1650
|
const moveThemeSelection = (delta: number) => {
|
|
1564
1651
|
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
1565
1652
|
if (filteredThemes.length === 0) return
|
|
@@ -1655,30 +1742,15 @@ export const App = () => {
|
|
|
1655
1742
|
const { repository, number } = mergeModal.info
|
|
1656
1743
|
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.repository === repository && pullRequest.number === number)
|
|
1657
1744
|
const previousPullRequest = targetPullRequest ?? null
|
|
1658
|
-
const previousMergeInfo = mergeModal.info
|
|
1659
1745
|
|
|
1660
1746
|
if (targetPullRequest && option.optimisticAutoMergeEnabled !== undefined) {
|
|
1661
1747
|
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
1748
|
}
|
|
1749
|
+
if (targetPullRequest && option.optimisticState === "merged") markPullRequestCompleted(targetPullRequest, "merged")
|
|
1667
1750
|
|
|
1668
|
-
|
|
1751
|
+
closeActiveModal()
|
|
1669
1752
|
void mergePullRequest({ repository, number, action: option.action })
|
|
1670
1753
|
.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
1754
|
if (option.refreshOnSuccess) {
|
|
1683
1755
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
1684
1756
|
} else {
|
|
@@ -1686,8 +1758,7 @@ export const App = () => {
|
|
|
1686
1758
|
}
|
|
1687
1759
|
})
|
|
1688
1760
|
.catch((error) => {
|
|
1689
|
-
if (previousPullRequest)
|
|
1690
|
-
setMergeModal((current) => ({ ...current, running: false, info: previousMergeInfo, error: errorMessage(error) }))
|
|
1761
|
+
if (previousPullRequest) restoreOptimisticPullRequest(previousPullRequest)
|
|
1691
1762
|
flashNotice(errorMessage(error))
|
|
1692
1763
|
})
|
|
1693
1764
|
}
|
|
@@ -1801,6 +1872,7 @@ export const App = () => {
|
|
|
1801
1872
|
diffReady: selectedDiffState?._tag === "Ready",
|
|
1802
1873
|
effectiveDiffRenderView,
|
|
1803
1874
|
diffWrapMode,
|
|
1875
|
+
diffWhitespaceMode,
|
|
1804
1876
|
readyDiffFileCount: readyDiffFiles.length,
|
|
1805
1877
|
diffFileIndex,
|
|
1806
1878
|
diffRangeActive: diffCommentRangeActive,
|
|
@@ -1843,6 +1915,7 @@ export const App = () => {
|
|
|
1843
1915
|
},
|
|
1844
1916
|
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1845
1917
|
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1918
|
+
toggleDiffWhitespaceMode,
|
|
1846
1919
|
jumpDiffFile,
|
|
1847
1920
|
openSelectedDiffComment,
|
|
1848
1921
|
toggleDiffCommentRange,
|
|
@@ -1874,7 +1947,6 @@ export const App = () => {
|
|
|
1874
1947
|
}
|
|
1875
1948
|
const runCommandByIdRef = useRef(runCommandById)
|
|
1876
1949
|
runCommandByIdRef.current = runCommandById
|
|
1877
|
-
useAppCommandRegistry(appCommands, runCommandByIdRef)
|
|
1878
1950
|
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1879
1951
|
if (!commandPaletteActive) return []
|
|
1880
1952
|
const repository = parseRepositoryInput(commandPalette.query)
|
|
@@ -1901,217 +1973,30 @@ export const App = () => {
|
|
|
1901
1973
|
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1902
1974
|
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1903
1975
|
|
|
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
|
-
|
|
1976
|
+
// === Helpers used by the keymap layers ===
|
|
1925
1977
|
const moveMergeSelection = (delta: -1 | 1) => setMergeModal((current) => {
|
|
1926
1978
|
const max = Math.max(0, availableMergeActions(mergeModal.info).length - 1)
|
|
1927
1979
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1928
1980
|
})
|
|
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
1981
|
const scrollCommentThread = (delta: number) => setCommentThreadModal((current) => ({
|
|
1944
1982
|
...current,
|
|
1945
1983
|
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
1946
1984
|
}))
|
|
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
1985
|
const moveLabelSelection = (delta: -1 | 1) => setLabelModal((current) => {
|
|
1967
1986
|
const max = Math.max(0, filterLabels(labelModal.availableLabels, labelModal.query).length - 1)
|
|
1968
1987
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1969
1988
|
})
|
|
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
1989
|
const moveCommandPaletteSelection = (delta: -1 | 1) => setCommandPalette((current) => {
|
|
2042
1990
|
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCommands)
|
|
2043
1991
|
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2044
1992
|
})
|
|
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
|
-
},
|
|
1993
|
+
const selectCommandPaletteIndex = (index: number) => setCommandPalette((current) => {
|
|
1994
|
+
const selectedIndex = clampCommandIndex(index, commandPaletteCommands)
|
|
1995
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2113
1996
|
})
|
|
2114
|
-
|
|
1997
|
+
const runCommandPaletteCommand = (command: AppCommand) => {
|
|
1998
|
+
runCommand(command, { notifyDisabled: true, closePalette: true })
|
|
1999
|
+
}
|
|
2115
2000
|
const scrollDetailFullViewBy = (delta: number) => {
|
|
2116
2001
|
detailScrollRef.current?.scrollBy({ x: 0, y: delta })
|
|
2117
2002
|
setDetailScrollOffset((current) => Math.max(0, current + delta))
|
|
@@ -2120,26 +2005,6 @@ export const App = () => {
|
|
|
2120
2005
|
detailScrollRef.current?.scrollTo({ x: 0, y })
|
|
2121
2006
|
setDetailScrollOffset(y)
|
|
2122
2007
|
}
|
|
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
2008
|
const moveSelectedToPreviousGroup = () => setSelectedIndex((current) => {
|
|
2144
2009
|
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2145
2010
|
const currentGroup = getCurrentGroupIndex(current)
|
|
@@ -2178,48 +2043,168 @@ export const App = () => {
|
|
|
2178
2043
|
if (visiblePullRequests.length === 0) return 0
|
|
2179
2044
|
return current <= 0 ? visiblePullRequests.length - 1 : current - 1
|
|
2180
2045
|
})
|
|
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),
|
|
2046
|
+
const handleQuitOrClose = () => {
|
|
2047
|
+
if (themeModalActive) {
|
|
2048
|
+
closeThemeModal(false)
|
|
2049
|
+
return
|
|
2050
|
+
}
|
|
2051
|
+
if (activeModal._tag !== "None") {
|
|
2052
|
+
closeActiveModal()
|
|
2053
|
+
return
|
|
2054
|
+
}
|
|
2055
|
+
runCommandById("app.quit")
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// === Build the keymap context ===
|
|
2059
|
+
const appCtx: AppCtx = {
|
|
2060
|
+
closeModalActive,
|
|
2061
|
+
mergeModalActive,
|
|
2062
|
+
commentThreadModalActive,
|
|
2063
|
+
labelModalActive,
|
|
2064
|
+
themeModalActive,
|
|
2065
|
+
openRepositoryModalActive,
|
|
2066
|
+
commentModalActive,
|
|
2067
|
+
commandPaletteActive,
|
|
2068
|
+
filterMode,
|
|
2069
|
+
diffFullView,
|
|
2070
|
+
detailFullView,
|
|
2071
|
+
textInputActive: commentModalActive
|
|
2072
|
+
|| commandPaletteActive
|
|
2073
|
+
|| openRepositoryModalActive
|
|
2074
|
+
|| labelModalActive
|
|
2075
|
+
|| filterMode
|
|
2076
|
+
|| (themeModalActive && themeModal.filterMode),
|
|
2077
|
+
closeModal: {
|
|
2078
|
+
closeModal: closeActiveModal,
|
|
2079
|
+
confirmClose: confirmClosePullRequest,
|
|
2221
2080
|
},
|
|
2222
|
-
|
|
2081
|
+
mergeModal: {
|
|
2082
|
+
availableActionCount: availableMergeActions(mergeModal.info).length,
|
|
2083
|
+
closeModal: closeActiveModal,
|
|
2084
|
+
confirmMerge: confirmMergeAction,
|
|
2085
|
+
moveSelection: moveMergeSelection,
|
|
2086
|
+
},
|
|
2087
|
+
commentThreadModal: {
|
|
2088
|
+
halfPage,
|
|
2089
|
+
closeModal: closeActiveModal,
|
|
2090
|
+
openInlineComment: openDiffCommentModal,
|
|
2091
|
+
scrollBy: scrollCommentThread,
|
|
2092
|
+
},
|
|
2093
|
+
labelModal: {
|
|
2094
|
+
closeModal: closeActiveModal,
|
|
2095
|
+
toggleSelected: toggleLabelAtIndex,
|
|
2096
|
+
moveSelection: moveLabelSelection,
|
|
2097
|
+
},
|
|
2098
|
+
themeModal: {
|
|
2099
|
+
filterMode: themeModal.filterMode,
|
|
2100
|
+
hasFilteredResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
2101
|
+
closeWithoutSaving: () => closeThemeModal(false),
|
|
2102
|
+
clearFilter: () => updateThemeQuery("", { filterMode: false }),
|
|
2103
|
+
enterFilterMode: () => updateThemeQuery("", { filterMode: true }),
|
|
2104
|
+
confirmSelection: () => closeThemeModal(true),
|
|
2105
|
+
moveSelection: moveThemeSelection,
|
|
2106
|
+
},
|
|
2107
|
+
openRepositoryModal: {
|
|
2108
|
+
closeModal: closeActiveModal,
|
|
2109
|
+
openFromInput: openRepositoryFromInput,
|
|
2110
|
+
},
|
|
2111
|
+
commentModal: {
|
|
2112
|
+
closeModal: closeActiveModal,
|
|
2113
|
+
submit: submitDiffComment,
|
|
2114
|
+
insertNewline: () => editComment((state) => insertText(state, "\n")),
|
|
2115
|
+
moveLeft: () => editComment(editorMoveLeft),
|
|
2116
|
+
moveRight: () => editComment(editorMoveRight),
|
|
2117
|
+
moveUp: () => editComment((state) => moveVertically(state, -1)),
|
|
2118
|
+
moveDown: () => editComment((state) => moveVertically(state, 1)),
|
|
2119
|
+
moveLineStart: () => editComment(moveLineStart),
|
|
2120
|
+
moveLineEnd: () => editComment(moveLineEnd),
|
|
2121
|
+
moveWordBackward: () => editComment(moveWordBackward),
|
|
2122
|
+
moveWordForward: () => editComment(moveWordForward),
|
|
2123
|
+
backspace: () => editComment(editorBackspace),
|
|
2124
|
+
deleteForward: () => editComment(editorDeleteForward),
|
|
2125
|
+
deleteWordBackward: () => editComment(deleteWordBackward),
|
|
2126
|
+
deleteWordForward: () => editComment(deleteWordForward),
|
|
2127
|
+
deleteToLineStart: () => editComment(deleteToLineStart),
|
|
2128
|
+
deleteToLineEnd: () => editComment(deleteToLineEnd),
|
|
2129
|
+
},
|
|
2130
|
+
commandPalette: {
|
|
2131
|
+
closeModal: closeActiveModal,
|
|
2132
|
+
runSelected: () => {
|
|
2133
|
+
if (selectedCommand) runCommandPaletteCommand(selectedCommand)
|
|
2134
|
+
},
|
|
2135
|
+
moveSelection: moveCommandPaletteSelection,
|
|
2136
|
+
},
|
|
2137
|
+
filterModeCtx: {
|
|
2138
|
+
cancel: () => {
|
|
2139
|
+
setFilterDraft(filterQuery)
|
|
2140
|
+
setFilterMode(false)
|
|
2141
|
+
},
|
|
2142
|
+
commit: () => {
|
|
2143
|
+
setFilterQuery(filterDraft)
|
|
2144
|
+
setFilterMode(false)
|
|
2145
|
+
},
|
|
2146
|
+
},
|
|
2147
|
+
diff: {
|
|
2148
|
+
halfPage,
|
|
2149
|
+
handleEscape: () => {
|
|
2150
|
+
if (diffCommentRangeActive) setDiffCommentRangeStartIndex(null)
|
|
2151
|
+
else runCommandById("diff.close")
|
|
2152
|
+
},
|
|
2153
|
+
openSelectedComment: openSelectedDiffComment,
|
|
2154
|
+
addComment: () => runCommandById("diff.add-comment"),
|
|
2155
|
+
toggleRange: () => runCommandById("diff.toggle-range"),
|
|
2156
|
+
toggleView: () => runCommandById("diff.toggle-view"),
|
|
2157
|
+
toggleWrap: () => runCommandById("diff.toggle-wrap"),
|
|
2158
|
+
reload: () => runCommandById("diff.reload"),
|
|
2159
|
+
nextThread: () => runCommandById("diff.next-thread"),
|
|
2160
|
+
previousThread: () => runCommandById("diff.previous-thread"),
|
|
2161
|
+
moveAnchor: moveDiffCommentAnchor,
|
|
2162
|
+
moveAnchorToBoundary: moveDiffCommentToBoundary,
|
|
2163
|
+
alignAnchor: alignSelectedDiffCommentAnchor,
|
|
2164
|
+
selectSide: selectDiffCommentSide,
|
|
2165
|
+
nextFile: () => runCommandById("diff.next-file"),
|
|
2166
|
+
previousFile: () => runCommandById("diff.previous-file"),
|
|
2167
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2168
|
+
},
|
|
2169
|
+
detail: {
|
|
2170
|
+
halfPage,
|
|
2171
|
+
scrollBy: scrollDetailFullViewBy,
|
|
2172
|
+
scrollTo: scrollDetailFullViewTo,
|
|
2173
|
+
closeDetail: () => runCommandById("detail.close"),
|
|
2174
|
+
openTheme: () => runCommandById("theme.open"),
|
|
2175
|
+
openDiff: () => runCommandById("diff.open"),
|
|
2176
|
+
closePullRequest: () => runCommandById("pull.close"),
|
|
2177
|
+
openLabels: () => runCommandById("pull.labels"),
|
|
2178
|
+
openMerge: () => runCommandById("pull.merge"),
|
|
2179
|
+
toggleDraft: () => runCommandById("pull.toggle-draft"),
|
|
2180
|
+
refresh: () => runCommandById("pull.refresh"),
|
|
2181
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2182
|
+
copyMetadata: () => runCommandById("pull.copy-metadata"),
|
|
2183
|
+
},
|
|
2184
|
+
listNav: {
|
|
2185
|
+
halfPage,
|
|
2186
|
+
visibleCount: visiblePullRequests.length,
|
|
2187
|
+
hasFilter: filterQuery.length > 0,
|
|
2188
|
+
canScrollDetailPreview: isWideLayout && selectedPullRequest !== null,
|
|
2189
|
+
runCommandById: (id) => { runCommandById(id) },
|
|
2190
|
+
switchQueueMode,
|
|
2191
|
+
scrollDetailPreviewBy,
|
|
2192
|
+
scrollDetailPreviewTo,
|
|
2193
|
+
clearFilter: () => { runCommandById("filter.clear") },
|
|
2194
|
+
stepSelected,
|
|
2195
|
+
stepSelectedUp,
|
|
2196
|
+
stepSelectedDown,
|
|
2197
|
+
stepSelectedUpWrap,
|
|
2198
|
+
stepSelectedDownWithLoadMore,
|
|
2199
|
+
moveSelectedToPreviousGroup,
|
|
2200
|
+
moveSelectedToNextGroup,
|
|
2201
|
+
setSelected: (index) => setSelectedIndex(index),
|
|
2202
|
+
},
|
|
2203
|
+
openCommandPalette: () => { runCommandById("command.open") },
|
|
2204
|
+
handleQuitOrClose,
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
useKeymap(appKeymap, appCtx, useOpenTuiSubscribe())
|
|
2223
2208
|
|
|
2224
2209
|
useKeyboard((key) => {
|
|
2225
2210
|
if (commandPaletteActive) {
|
|
@@ -2243,18 +2228,9 @@ export const App = () => {
|
|
|
2243
2228
|
return
|
|
2244
2229
|
}
|
|
2245
2230
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
return
|
|
2250
|
-
}
|
|
2251
|
-
if (activeModal._tag !== "None") {
|
|
2252
|
-
closeActiveModal()
|
|
2253
|
-
return
|
|
2254
|
-
}
|
|
2255
|
-
runCommandById("app.quit")
|
|
2256
|
-
return
|
|
2257
|
-
}
|
|
2231
|
+
// q / ctrl+c quit/close-modal logic now lives in the keymap layer
|
|
2232
|
+
// (handleQuitOrClose). This useKeyboard callback only handles raw text
|
|
2233
|
+
// input for modals that need character-by-character accumulation.
|
|
2258
2234
|
|
|
2259
2235
|
if (themeModalActive) {
|
|
2260
2236
|
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
@@ -2290,15 +2266,23 @@ export const App = () => {
|
|
|
2290
2266
|
}
|
|
2291
2267
|
})
|
|
2292
2268
|
|
|
2269
|
+
if (isInitialLoading) {
|
|
2270
|
+
return (
|
|
2271
|
+
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
2272
|
+
<LoadingLogoPane content={detailPlaceholderContent} width={contentWidth} height={terminalHeight} frame={loadingFrame} />
|
|
2273
|
+
</box>
|
|
2274
|
+
)
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2293
2277
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
2294
2278
|
const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
|
|
2295
2279
|
const fullscreenDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout)
|
|
2296
2280
|
const fullscreenDetailBodyViewportHeight = Math.max(1, wideBodyHeight - fullscreenDetailHeaderHeight)
|
|
2297
|
-
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth)
|
|
2281
|
+
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2298
2282
|
const fullscreenDetailBodyScrollable = fullscreenDetailBodyHeight > fullscreenDetailBodyViewportHeight
|
|
2299
2283
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2300
2284
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2301
|
-
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2285
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2302
2286
|
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2303
2287
|
|
|
2304
2288
|
const prListProps = {
|
|
@@ -2370,20 +2354,19 @@ export const App = () => {
|
|
|
2370
2354
|
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
2371
2355
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
2372
2356
|
</box>
|
|
2373
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2357
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2374
2358
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┬" />
|
|
2375
2359
|
) : (
|
|
2376
2360
|
<Divider width={contentWidth} />
|
|
2377
2361
|
)}
|
|
2378
|
-
{
|
|
2379
|
-
<LoadingPane content={detailPlaceholderContent} width={contentWidth} height={wideBodyHeight} />
|
|
2380
|
-
) : diffFullView ? (
|
|
2362
|
+
{diffFullView ? (
|
|
2381
2363
|
<PullRequestDiffPane
|
|
2382
2364
|
pullRequest={selectedPullRequest}
|
|
2383
|
-
diffState={
|
|
2365
|
+
diffState={displayedDiffState}
|
|
2384
2366
|
stackedFiles={stackedDiffFiles}
|
|
2385
2367
|
scrollTop={diffScrollTop}
|
|
2386
2368
|
view={effectiveDiffRenderView}
|
|
2369
|
+
whitespaceMode={diffWhitespaceMode}
|
|
2387
2370
|
wrapMode={diffWrapMode}
|
|
2388
2371
|
paneWidth={contentWidth}
|
|
2389
2372
|
height={wideBodyHeight}
|
|
@@ -2406,8 +2389,8 @@ export const App = () => {
|
|
|
2406
2389
|
{selectedPullRequest ? (
|
|
2407
2390
|
<>
|
|
2408
2391
|
<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} />
|
|
2392
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2393
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2411
2394
|
</scrollbox>
|
|
2412
2395
|
</>
|
|
2413
2396
|
) : (
|
|
@@ -2434,7 +2417,7 @@ export const App = () => {
|
|
|
2434
2417
|
<>
|
|
2435
2418
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2436
2419
|
<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} />
|
|
2420
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2438
2421
|
</scrollbox>
|
|
2439
2422
|
</>
|
|
2440
2423
|
) : (
|
|
@@ -2447,8 +2430,8 @@ export const App = () => {
|
|
|
2447
2430
|
{selectedPullRequest ? (
|
|
2448
2431
|
<>
|
|
2449
2432
|
<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} />
|
|
2433
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2434
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2452
2435
|
</scrollbox>
|
|
2453
2436
|
</>
|
|
2454
2437
|
) : (
|
|
@@ -2457,7 +2440,7 @@ export const App = () => {
|
|
|
2457
2440
|
</box>
|
|
2458
2441
|
) : (
|
|
2459
2442
|
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2460
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2443
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2461
2444
|
<Divider width={contentWidth} />
|
|
2462
2445
|
<box flexGrow={1} flexDirection="column">
|
|
2463
2446
|
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
@@ -2469,7 +2452,7 @@ export const App = () => {
|
|
|
2469
2452
|
</box>
|
|
2470
2453
|
)}
|
|
2471
2454
|
|
|
2472
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2455
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2473
2456
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┴" />
|
|
2474
2457
|
) : (
|
|
2475
2458
|
<Divider width={contentWidth} />
|
|
@@ -2573,6 +2556,8 @@ export const App = () => {
|
|
|
2573
2556
|
modalHeight={commandPaletteHeight}
|
|
2574
2557
|
offsetLeft={commandPaletteLeft}
|
|
2575
2558
|
offsetTop={commandPaletteTop}
|
|
2559
|
+
onSelectCommandIndex={selectCommandPaletteIndex}
|
|
2560
|
+
onRunCommand={runCommandPaletteCommand}
|
|
2576
2561
|
/>
|
|
2577
2562
|
) : null}
|
|
2578
2563
|
</box>
|