@kitlangton/ghui 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +6 -2
- package/src/App.tsx +932 -510
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +68 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +43 -13
- package/src/errors.ts +10 -0
- package/src/index.tsx +23 -2
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +327 -161
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +143 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +91 -182
- package/src/ui/PullRequestDiffPane.tsx +105 -87
- package/src/ui/PullRequestList.tsx +102 -49
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +69 -63
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +270 -302
- package/src/ui/primitives.tsx +92 -2
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
package/src/App.tsx
CHANGED
|
@@ -1,43 +1,71 @@
|
|
|
1
|
-
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import { useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
1
|
+
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
3
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
4
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
5
5
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
6
6
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
7
|
-
import
|
|
7
|
+
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
8
|
+
import { useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
9
|
+
import { buildAppCommands } from "./appCommands.js"
|
|
10
|
+
import type { AppCommand } from "./commands.js"
|
|
11
|
+
import { clampCommandIndex, commandEnabled, filterCommands } from "./commands.js"
|
|
8
12
|
import { config } from "./config.js"
|
|
9
|
-
import {
|
|
13
|
+
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
10
14
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
15
|
+
import { errorMessage } from "./errors.js"
|
|
11
16
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
12
17
|
import { Observability } from "./observability.js"
|
|
18
|
+
import { mergeCachedDetails } from "./pullRequestCache.js"
|
|
19
|
+
import { activePullRequestViews, initialPullRequestView, nextView, parseRepositoryInput, type PullRequestView, viewCacheKey, viewEquals, viewLabel, viewMode, viewRepository } from "./pullRequestViews.js"
|
|
20
|
+
import { BrowserOpener } from "./services/BrowserOpener.js"
|
|
21
|
+
import { Clipboard } from "./services/Clipboard.js"
|
|
22
|
+
import { CommandRunner } from "./services/CommandRunner.js"
|
|
13
23
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
24
|
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
15
|
-
import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
25
|
+
import { colors, filterThemeDefinitions, mixHex, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
16
26
|
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
17
|
-
import { buildStackedDiffFiles,
|
|
18
|
-
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane,
|
|
19
|
-
import { FooterHints,
|
|
27
|
+
import { buildStackedDiffFiles, diffCommentLocationKey, getStackedDiffCommentAnchors, nearestDiffCommentAnchorIndex, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileAtLine, type DiffCommentAnchor, type DiffView, type DiffWrapMode, type StackedDiffCommentAnchor } from "./ui/diff.js"
|
|
28
|
+
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, getScrollableDetailBodyHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
29
|
+
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
20
30
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
21
|
-
import {
|
|
31
|
+
import { CommandPalette } from "./ui/CommandPalette.js"
|
|
32
|
+
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"
|
|
22
33
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
23
34
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
24
|
-
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
35
|
+
import { buildPullRequestListRows, pullRequestListRowIndex, PullRequestList } from "./ui/PullRequestList.js"
|
|
36
|
+
import { editSingleLineInput, isSingleLineInputKey, printableKeyText, singleLineText } from "./ui/singleLineInput.js"
|
|
25
37
|
|
|
26
|
-
const
|
|
27
|
-
|
|
38
|
+
const parseOptionalPositiveInt = (value: string | undefined, fallback: number | null) => {
|
|
39
|
+
if (value === undefined) return fallback
|
|
40
|
+
const parsed = Number.parseInt(value, 10)
|
|
41
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mockPrCount = parseOptionalPositiveInt(process.env.GHUI_MOCK_PR_COUNT, null)
|
|
45
|
+
const pullRequestPageSize = Math.min(100, parseOptionalPositiveInt(process.env.GHUI_PR_PAGE_SIZE, config.prPageSize) ?? config.prPageSize)
|
|
46
|
+
const githubServiceLayer = mockPrCount !== null
|
|
47
|
+
? (await import("./services/MockGitHubService.js")).MockGitHubService.layer({ prCount: mockPrCount, repoCount: parseOptionalPositiveInt(process.env.GHUI_MOCK_REPO_COUNT, 4) ?? 4 })
|
|
48
|
+
: GitHubService.layerNoDeps
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
const githubRuntime = Atom.runtime(
|
|
51
|
+
Layer.mergeAll(githubServiceLayer, Clipboard.layerNoDeps, BrowserOpener.layerNoDeps).pipe(
|
|
52
|
+
Layer.provide(CommandRunner.layer),
|
|
53
|
+
Layer.provideMerge(Observability.layer),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
30
57
|
|
|
31
58
|
interface PullRequestLoad {
|
|
32
|
-
readonly
|
|
59
|
+
readonly view: PullRequestView
|
|
33
60
|
readonly data: readonly PullRequestItem[]
|
|
34
61
|
readonly fetchedAt: Date | null
|
|
35
|
-
readonly
|
|
62
|
+
readonly endCursor: string | null
|
|
63
|
+
readonly hasNextPage: boolean
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
interface DetailPlaceholderInput {
|
|
39
67
|
readonly status: LoadStatus
|
|
40
|
-
readonly retryProgress: RetryProgress
|
|
68
|
+
readonly retryProgress: RetryProgress
|
|
41
69
|
readonly loadingIndicator: string
|
|
42
70
|
readonly visibleCount: number
|
|
43
71
|
readonly filterText: string
|
|
@@ -59,7 +87,7 @@ type DiffRenderableRuntimeSides = {
|
|
|
59
87
|
|
|
60
88
|
interface AppliedDiffLineColor {
|
|
61
89
|
readonly anchor: StackedDiffCommentAnchor
|
|
62
|
-
readonly view:
|
|
90
|
+
readonly view: DiffView
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
interface AppliedDiffLineColorState {
|
|
@@ -67,85 +95,103 @@ interface AppliedDiffLineColorState {
|
|
|
67
95
|
readonly entries: readonly AppliedDiffLineColor[]
|
|
68
96
|
}
|
|
69
97
|
|
|
98
|
+
interface DetailHydration {
|
|
99
|
+
readonly token: symbol
|
|
100
|
+
notifyError: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
70
103
|
const PR_FETCH_RETRIES = 6
|
|
71
104
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
72
105
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
73
106
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
107
|
+
const DIFF_STICKY_HEADER_LINES = 2
|
|
74
108
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
additions: cachedPullRequest.additions,
|
|
87
|
-
deletions: cachedPullRequest.deletions,
|
|
88
|
-
changedFiles: cachedPullRequest.changedFiles,
|
|
89
|
-
checkStatus: cachedPullRequest.checkStatus,
|
|
90
|
-
checkSummary: cachedPullRequest.checkSummary,
|
|
91
|
-
checks: cachedPullRequest.checks,
|
|
92
|
-
detailLoaded: true,
|
|
93
|
-
} satisfies PullRequestItem
|
|
94
|
-
})
|
|
109
|
+
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
110
|
+
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
111
|
+
const DETAIL_PREFETCH_BEHIND = 1
|
|
112
|
+
const DETAIL_PREFETCH_AHEAD = 3
|
|
113
|
+
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
114
|
+
const DETAIL_PREFETCH_DELAY_MS = 120
|
|
115
|
+
|
|
116
|
+
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
|
|
117
|
+
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
|
|
118
|
+
const mergedIncoming = mergeCachedDetails(incoming, existing)
|
|
119
|
+
return [...existing, ...mergedIncoming.filter((pullRequest) => !seen.has(pullRequest.url))]
|
|
95
120
|
}
|
|
96
121
|
|
|
97
|
-
const retryProgressAtom = Atom.make<RetryProgress
|
|
98
|
-
const
|
|
99
|
-
const queueLoadCacheAtom = Atom.make<Partial<Record<
|
|
100
|
-
const queueSelectionAtom = Atom.make<Partial<Record<
|
|
122
|
+
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
123
|
+
const activeViewAtom = Atom.make<PullRequestView>(initialPullRequestView(config.repository)).pipe(Atom.keepAlive)
|
|
124
|
+
const queueLoadCacheAtom = Atom.make<Partial<Record<string, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
125
|
+
const queueSelectionAtom = Atom.make<Partial<Record<string, number>>>({}).pipe(Atom.keepAlive)
|
|
126
|
+
const trimQueueLoadCache = (cache: Partial<Record<string, PullRequestLoad>>) => {
|
|
127
|
+
const repositoryKeys = Object.keys(cache).filter((key) => key.startsWith("repository:"))
|
|
128
|
+
if (repositoryKeys.length <= MAX_REPOSITORY_CACHE_ENTRIES) return cache
|
|
129
|
+
const remove = new Set(repositoryKeys.slice(0, repositoryKeys.length - MAX_REPOSITORY_CACHE_ENTRIES))
|
|
130
|
+
return Object.fromEntries(Object.entries(cache).filter(([key]) => !remove.has(key))) as Partial<Record<string, PullRequestLoad>>
|
|
131
|
+
}
|
|
101
132
|
const pullRequestsAtom = githubRuntime.atom(
|
|
102
133
|
GitHubService.use((github) =>
|
|
103
134
|
Effect.gen(function*() {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
const view = yield* Atom.get(activeViewAtom)
|
|
136
|
+
const queueMode = viewMode(view)
|
|
137
|
+
const repository = viewRepository(view)
|
|
138
|
+
const cacheKey = viewCacheKey(view)
|
|
139
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
140
|
+
const page = yield* github.listOpenPullRequestPage({
|
|
141
|
+
mode: queueMode,
|
|
142
|
+
repository,
|
|
143
|
+
cursor: null,
|
|
144
|
+
pageSize: Math.min(pullRequestPageSize, config.prFetchLimit),
|
|
145
|
+
}).pipe(
|
|
146
|
+
Effect.tapError(() =>
|
|
147
|
+
Atom.update(retryProgressAtom, (current) => RetryProgress.Retrying({
|
|
148
|
+
attempt: Math.min(RetryProgress.$match(current, { Idle: () => 0, Retrying: ({ attempt }) => attempt }) + 1, PR_FETCH_RETRIES),
|
|
149
|
+
max: PR_FETCH_RETRIES,
|
|
150
|
+
}))
|
|
151
|
+
),
|
|
152
|
+
Effect.retry({ times: PR_FETCH_RETRIES, schedule: Schedule.exponential("300 millis", 2) }),
|
|
153
|
+
Effect.tapError(() => Atom.set(retryProgressAtom, initialRetryProgress)),
|
|
154
|
+
)
|
|
116
155
|
|
|
117
|
-
yield* Atom.set(retryProgressAtom,
|
|
156
|
+
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
118
157
|
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
158
|
+
const existingLoad = cache[cacheKey]
|
|
159
|
+
const data = mergeCachedDetails(page.items, existingLoad?.data)
|
|
119
160
|
const load = {
|
|
120
|
-
|
|
121
|
-
data
|
|
161
|
+
view,
|
|
162
|
+
data,
|
|
122
163
|
fetchedAt: new Date(),
|
|
123
|
-
|
|
164
|
+
endCursor: page.endCursor,
|
|
165
|
+
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
124
166
|
} satisfies PullRequestLoad
|
|
125
|
-
|
|
167
|
+
const nextCache = { ...cache }
|
|
168
|
+
delete nextCache[cacheKey]
|
|
169
|
+
nextCache[cacheKey] = load
|
|
170
|
+
yield* Atom.set(queueLoadCacheAtom, trimQueueLoadCache(nextCache))
|
|
126
171
|
return load
|
|
127
172
|
})
|
|
128
173
|
),
|
|
129
174
|
).pipe(Atom.keepAlive)
|
|
130
|
-
const selectedIndexAtom = Atom.make(0)
|
|
131
|
-
const noticeAtom = Atom.make<string | null>(null)
|
|
132
|
-
const filterQueryAtom = Atom.make("")
|
|
133
|
-
const filterDraftAtom = Atom.make("")
|
|
134
|
-
const filterModeAtom = Atom.make(false)
|
|
135
|
-
const pendingGAtom = Atom.make(false)
|
|
136
|
-
const detailFullViewAtom = Atom.make(false)
|
|
137
|
-
const detailScrollOffsetAtom = Atom.make(0)
|
|
138
|
-
const diffFullViewAtom = Atom.make(false)
|
|
139
|
-
const diffFileIndexAtom = Atom.make(0)
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
175
|
+
const selectedIndexAtom = Atom.make(0)
|
|
176
|
+
const noticeAtom = Atom.make<string | null>(null)
|
|
177
|
+
const filterQueryAtom = Atom.make("")
|
|
178
|
+
const filterDraftAtom = Atom.make("")
|
|
179
|
+
const filterModeAtom = Atom.make(false)
|
|
180
|
+
const pendingGAtom = Atom.make(false)
|
|
181
|
+
const detailFullViewAtom = Atom.make(false)
|
|
182
|
+
const detailScrollOffsetAtom = Atom.make(0)
|
|
183
|
+
const diffFullViewAtom = Atom.make(false)
|
|
184
|
+
const diffFileIndexAtom = Atom.make(0)
|
|
185
|
+
const diffScrollTopAtom = Atom.make(0)
|
|
186
|
+
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
187
|
+
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
188
|
+
const diffCommentModeAtom = Atom.make(false)
|
|
189
|
+
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
144
190
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
145
191
|
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
146
192
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
147
193
|
|
|
148
|
-
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
194
|
+
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
149
195
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
150
196
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
151
197
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -156,12 +202,110 @@ const usernameAtom = githubRuntime.atom(
|
|
|
156
202
|
: Effect.succeed(config.author.replace(/^@/, "")),
|
|
157
203
|
).pipe(Atom.keepAlive)
|
|
158
204
|
|
|
205
|
+
const pullRequestLoadAtom = Atom.make((get) => {
|
|
206
|
+
const view = get(activeViewAtom)
|
|
207
|
+
const cacheKey = viewCacheKey(view)
|
|
208
|
+
const cache = get(queueLoadCacheAtom)
|
|
209
|
+
const result = get(pullRequestsAtom)
|
|
210
|
+
const resolved = AsyncResult.getOrElse(result, () => null)
|
|
211
|
+
return cache[cacheKey] ?? (resolved && viewCacheKey(resolved.view) === cacheKey ? resolved : null)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const isLoadingQueueModeAtom = Atom.make((get) => {
|
|
215
|
+
const cacheKey = viewCacheKey(get(activeViewAtom))
|
|
216
|
+
const resolved = AsyncResult.getOrElse(get(pullRequestsAtom), () => null)
|
|
217
|
+
return resolved !== null && viewCacheKey(resolved.view) !== cacheKey
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const pullRequestStatusAtom = Atom.make((get): LoadStatus => {
|
|
221
|
+
const result = get(pullRequestsAtom)
|
|
222
|
+
const load = get(pullRequestLoadAtom)
|
|
223
|
+
const isLoadingQueue = get(isLoadingQueueModeAtom)
|
|
224
|
+
if ((result.waiting || isLoadingQueue) && load === null) return "loading"
|
|
225
|
+
return AsyncResult.isFailure(result) ? "error" : "ready"
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const displayedPullRequestsAtom = Atom.make((get) => {
|
|
229
|
+
const load = get(pullRequestLoadAtom)
|
|
230
|
+
const overrides = get(pullRequestOverridesAtom)
|
|
231
|
+
const recentlyCompleted = get(recentlyCompletedPullRequestsAtom)
|
|
232
|
+
const source = load?.data ?? []
|
|
233
|
+
const seenUrls = new Set<string>()
|
|
234
|
+
const open = source.map((pullRequest) => {
|
|
235
|
+
seenUrls.add(pullRequest.url)
|
|
236
|
+
return recentlyCompleted[pullRequest.url] ?? overrides[pullRequest.url] ?? pullRequest
|
|
237
|
+
})
|
|
238
|
+
return [
|
|
239
|
+
...open,
|
|
240
|
+
...Object.values(recentlyCompleted).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
241
|
+
]
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const effectiveFilterQueryAtom = Atom.make((get) =>
|
|
245
|
+
(get(filterModeAtom) ? get(filterDraftAtom) : get(filterQueryAtom)).trim().toLowerCase(),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const filteredPullRequestsAtom = Atom.make((get) => {
|
|
249
|
+
const pullRequests = get(displayedPullRequestsAtom)
|
|
250
|
+
const query = get(effectiveFilterQueryAtom)
|
|
251
|
+
if (query.length === 0) return pullRequests
|
|
252
|
+
return pullRequests.flatMap((pullRequest) => {
|
|
253
|
+
const score = pullRequestFilterScore(pullRequest, query)
|
|
254
|
+
return score === null ? [] : [{ pullRequest, score }]
|
|
255
|
+
}).sort((left, right) =>
|
|
256
|
+
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
257
|
+
).map(({ pullRequest }) => pullRequest)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const visibleRepoOrderAtom = Atom.make((get) => {
|
|
261
|
+
const query = get(effectiveFilterQueryAtom)
|
|
262
|
+
if (query.length === 0) return [] as readonly string[]
|
|
263
|
+
return [...new Set(get(filteredPullRequestsAtom).map((pullRequest) => pullRequest.repository))]
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const visibleGroupsAtom = Atom.make((get) =>
|
|
267
|
+
groupBy(get(filteredPullRequestsAtom), (pullRequest) => pullRequest.repository, get(visibleRepoOrderAtom)),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const visiblePullRequestsAtom = Atom.make((get) => get(visibleGroupsAtom).flatMap(([, pullRequests]) => pullRequests))
|
|
271
|
+
|
|
272
|
+
const groupStartsAtom = Atom.make((get) => {
|
|
273
|
+
const groups = get(visibleGroupsAtom)
|
|
274
|
+
const starts: number[] = []
|
|
275
|
+
for (let index = 0; index < groups.length; index++) {
|
|
276
|
+
if (index === 0) starts.push(0)
|
|
277
|
+
else starts.push(starts[index - 1]! + groups[index - 1]![1].length)
|
|
278
|
+
}
|
|
279
|
+
return starts
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const selectedPullRequestAtom = Atom.make((get) => {
|
|
283
|
+
const pullRequests = get(visiblePullRequestsAtom)
|
|
284
|
+
const index = get(selectedIndexAtom)
|
|
285
|
+
return pullRequests[index] ?? null
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const selectedDiffKeyAtom = Atom.make((get) => {
|
|
289
|
+
const pullRequest = get(selectedPullRequestAtom)
|
|
290
|
+
return pullRequest ? pullRequestDiffKey(pullRequest) : null
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const selectedDiffStateAtom = Atom.make((get) => {
|
|
294
|
+
const key = get(selectedDiffKeyAtom)
|
|
295
|
+
if (!key) return undefined
|
|
296
|
+
return get(pullRequestDiffCacheAtom)[key]
|
|
297
|
+
})
|
|
298
|
+
|
|
159
299
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
160
300
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
161
301
|
)
|
|
162
|
-
const
|
|
163
|
-
GitHubService.use((github) => github.
|
|
302
|
+
const listOpenPullRequestPageAtom = githubRuntime.fn<ListPullRequestPageInput>()((input) =>
|
|
303
|
+
GitHubService.use((github) => github.listOpenPullRequestPage(input))
|
|
164
304
|
)
|
|
305
|
+
const pullRequestDetailsAtom = Atom.family((key: string) => {
|
|
306
|
+
const { repository, number } = parsePullRequestDetailAtomKey(key)
|
|
307
|
+
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDetails(repository, number)))
|
|
308
|
+
})
|
|
165
309
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
166
310
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
167
311
|
)
|
|
@@ -171,9 +315,10 @@ const removePullRequestLabelAtom = githubRuntime.fn<{ readonly repository: strin
|
|
|
171
315
|
const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly isDraft: boolean }>()((input) =>
|
|
172
316
|
GitHubService.use((github) => github.toggleDraftStatus(input.repository, input.number, input.isDraft))
|
|
173
317
|
)
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
)
|
|
318
|
+
const pullRequestDiffAtom = Atom.family((key: string) => {
|
|
319
|
+
const { repository, number } = parsePullRequestDiffAtomKey(key)
|
|
320
|
+
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDiff(repository, number)))
|
|
321
|
+
})
|
|
177
322
|
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
178
323
|
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
179
324
|
)
|
|
@@ -187,12 +332,13 @@ const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; rea
|
|
|
187
332
|
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
188
333
|
)
|
|
189
334
|
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
335
|
+
const copyToClipboardAtom = githubRuntime.fn<string>()((text) => Clipboard.use((clipboard) => clipboard.copy(text)))
|
|
336
|
+
const openInBrowserAtom = githubRuntime.fn<PullRequestItem>()((pullRequest) => BrowserOpener.use((browser) => browser.openPullRequest(pullRequest)))
|
|
190
337
|
|
|
191
338
|
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
192
339
|
|
|
193
|
-
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
194
340
|
|
|
195
|
-
const
|
|
341
|
+
const pasteText = (event: PasteEvent) => new TextDecoder().decode(event.bytes)
|
|
196
342
|
|
|
197
343
|
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
198
344
|
const normalized = query.trim().toLowerCase()
|
|
@@ -209,91 +355,34 @@ const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) =>
|
|
|
209
355
|
return scores.length > 0 ? Math.min(...scores) : null
|
|
210
356
|
}
|
|
211
357
|
|
|
212
|
-
const
|
|
213
|
-
if (process.platform === "darwin") return [["pbcopy"]]
|
|
214
|
-
if (process.platform === "linux") {
|
|
215
|
-
return [
|
|
216
|
-
...(process.env.WAYLAND_DISPLAY ? [["wl-copy"]] : []),
|
|
217
|
-
["xclip", "-selection", "clipboard"],
|
|
218
|
-
["xsel", "--clipboard", "--input"],
|
|
219
|
-
]
|
|
220
|
-
}
|
|
221
|
-
return []
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const copyToClipboard = async (text: string) => {
|
|
225
|
-
const commands = clipboardCommands()
|
|
226
|
-
let lastError = ""
|
|
227
|
-
|
|
228
|
-
for (const command of commands) {
|
|
229
|
-
let proc: Bun.Subprocess<"pipe", "ignore", "pipe">
|
|
230
|
-
try {
|
|
231
|
-
proc = Bun.spawn({
|
|
232
|
-
cmd: [...command],
|
|
233
|
-
stdin: "pipe",
|
|
234
|
-
stdout: "ignore",
|
|
235
|
-
stderr: "pipe",
|
|
236
|
-
})
|
|
237
|
-
} catch (error) {
|
|
238
|
-
lastError = errorMessage(error)
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
proc.stdin.write(text)
|
|
243
|
-
proc.stdin.end()
|
|
244
|
-
|
|
245
|
-
const exitCode = await proc.exited
|
|
246
|
-
if (exitCode === 0) return
|
|
247
|
-
|
|
248
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
249
|
-
lastError = stderr.trim()
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const installHint = process.platform === "linux" ? " Install wl-clipboard, xclip, or xsel." : ""
|
|
253
|
-
throw new Error(lastError || `Clipboard is not available.${installHint}`)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const openPullRequestInBrowser = async (pullRequest: PullRequestItem) => {
|
|
257
|
-
const proc = Bun.spawn({
|
|
258
|
-
cmd: ["gh", "pr", "view", String(pullRequest.number), "--repo", pullRequest.repository, "--web"],
|
|
259
|
-
stdout: "ignore",
|
|
260
|
-
stderr: "pipe",
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
const exitCode = await proc.exited
|
|
264
|
-
if (exitCode === 0) return
|
|
265
|
-
|
|
266
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
267
|
-
throw new Error(stderr.trim() || "Could not open PR in browser")
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
|
|
358
|
+
const pullRequestMetadataText = (pullRequest: PullRequestItem) => {
|
|
271
359
|
const lines = [
|
|
272
360
|
pullRequest.title,
|
|
273
361
|
`${pullRequest.repository} #${pullRequest.number}`,
|
|
274
362
|
pullRequest.url,
|
|
275
363
|
]
|
|
276
|
-
|
|
277
364
|
const review = reviewLabel(pullRequest)
|
|
278
|
-
if (review) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
lines.push(pullRequest.checkSummary)
|
|
283
|
-
}
|
|
365
|
+
if (review) lines.push(`review: ${review}`)
|
|
366
|
+
if (pullRequest.checkSummary) lines.push(pullRequest.checkSummary)
|
|
367
|
+
return lines.join("\n")
|
|
368
|
+
}
|
|
284
369
|
|
|
285
|
-
|
|
370
|
+
const pullRequestDetailKey = (pullRequest: PullRequestItem) => `${pullRequest.url}:${pullRequest.headRefOid}`
|
|
371
|
+
const pullRequestRevisionAtomKey = (pullRequest: PullRequestItem) => `${pullRequest.repository}\u0000${pullRequest.number}\u0000${pullRequest.headRefOid}`
|
|
372
|
+
const parsePullRequestRevisionAtomKey = (key: string, label: string) => {
|
|
373
|
+
const [repository, number] = key.split("\u0000")
|
|
374
|
+
if (!repository || !number) throw new Error(`Invalid pull request ${label} key: ${key}`)
|
|
375
|
+
return { repository, number: Number.parseInt(number, 10) }
|
|
286
376
|
}
|
|
377
|
+
const pullRequestDetailAtomKey = pullRequestRevisionAtomKey
|
|
378
|
+
const pullRequestDiffAtomKey = pullRequestRevisionAtomKey
|
|
379
|
+
const parsePullRequestDetailAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "detail")
|
|
380
|
+
const parsePullRequestDiffAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "diff")
|
|
287
381
|
|
|
288
382
|
const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
|
|
289
383
|
|
|
290
384
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
291
385
|
|
|
292
|
-
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
293
|
-
const index = pullRequestQueueModes.indexOf(mode)
|
|
294
|
-
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
295
|
-
}
|
|
296
|
-
|
|
297
386
|
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
298
387
|
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
299
388
|
|
|
@@ -320,7 +409,14 @@ const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig =
|
|
|
320
409
|
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
321
410
|
}
|
|
322
411
|
|
|
323
|
-
const
|
|
412
|
+
const diffCommentGutterColor = (anchor: DiffCommentAnchor, kind: "selected" | "thread") => {
|
|
413
|
+
const accent = kind === "thread"
|
|
414
|
+
? colors.status.pending
|
|
415
|
+
: anchor.side === "RIGHT" ? colors.status.passing : colors.status.failing
|
|
416
|
+
return mixHex(originalDiffLineColor(anchor).gutter, accent, 0.45)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
324
420
|
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
325
421
|
if (view === "split") {
|
|
326
422
|
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
@@ -345,7 +441,7 @@ const getDetailPlaceholderContent = ({
|
|
|
345
441
|
if (status === "loading") {
|
|
346
442
|
return {
|
|
347
443
|
title: `${loadingIndicator} Loading pull requests`,
|
|
348
|
-
hint: retryProgress ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
444
|
+
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
349
445
|
}
|
|
350
446
|
}
|
|
351
447
|
|
|
@@ -379,11 +475,12 @@ const getDetailPlaceholderContent = ({
|
|
|
379
475
|
export const App = () => {
|
|
380
476
|
const renderer = useRenderer()
|
|
381
477
|
const { width, height } = useTerminalDimensions()
|
|
478
|
+
const registry = useContext(RegistryContext)
|
|
382
479
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
383
480
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
384
|
-
const [
|
|
385
|
-
const
|
|
386
|
-
const
|
|
481
|
+
const [activeView, setActiveView] = useAtom(activeViewAtom)
|
|
482
|
+
const setQueueLoadCache = useAtomSet(queueLoadCacheAtom)
|
|
483
|
+
const setQueueSelection = useAtomSet(queueSelectionAtom)
|
|
387
484
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
388
485
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
389
486
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -391,16 +488,17 @@ export const App = () => {
|
|
|
391
488
|
const [filterMode, setFilterMode] = useAtom(filterModeAtom)
|
|
392
489
|
const [pendingG, setPendingG] = useAtom(pendingGAtom)
|
|
393
490
|
const [detailFullView, setDetailFullView] = useAtom(detailFullViewAtom)
|
|
394
|
-
const
|
|
491
|
+
const setDetailScrollOffset = useAtomSet(detailScrollOffsetAtom)
|
|
395
492
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
396
493
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
494
|
+
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
397
495
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
398
496
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
399
497
|
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
400
498
|
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
401
499
|
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
402
|
-
const
|
|
403
|
-
const
|
|
500
|
+
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
501
|
+
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
404
502
|
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
405
503
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
406
504
|
const closeActiveModal = () => setActiveModal(initialModal)
|
|
@@ -410,22 +508,25 @@ export const App = () => {
|
|
|
410
508
|
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
411
509
|
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
412
510
|
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const
|
|
511
|
+
const commandPaletteActive = Modal.$is("CommandPalette")(activeModal)
|
|
512
|
+
const openRepositoryModalActive = Modal.$is("OpenRepository")(activeModal)
|
|
513
|
+
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
514
|
+
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
515
|
+
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
516
|
+
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
517
|
+
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
518
|
+
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
519
|
+
const commandPalette: CommandPaletteState = commandPaletteActive ? activeModal : initialCommandPaletteState
|
|
520
|
+
const openRepositoryModal: OpenRepositoryModalState = openRepositoryModalActive ? activeModal : initialOpenRepositoryModalState
|
|
419
521
|
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
420
522
|
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
421
|
-
const ctor = Modal[tag] as (args:
|
|
523
|
+
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
422
524
|
if (typeof next === "function") {
|
|
423
525
|
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
424
526
|
if (current._tag !== tag) return current
|
|
425
|
-
|
|
426
|
-
return ctor({ state: updater(prev) })
|
|
527
|
+
return ctor(updater(current as unknown as ModalState<Tag>))
|
|
427
528
|
}
|
|
428
|
-
return ctor(
|
|
529
|
+
return ctor(next)
|
|
429
530
|
})
|
|
430
531
|
const setLabelModal = makeModalSetter("Label")
|
|
431
532
|
const setCloseModal = makeModalSetter("Close")
|
|
@@ -433,31 +534,35 @@ export const App = () => {
|
|
|
433
534
|
const setCommentModal = makeModalSetter("Comment")
|
|
434
535
|
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
435
536
|
const setThemeModal = makeModalSetter("Theme")
|
|
537
|
+
const setCommandPalette = makeModalSetter("CommandPalette")
|
|
538
|
+
const setOpenRepositoryModal = makeModalSetter("OpenRepository")
|
|
436
539
|
setActiveTheme(themeId)
|
|
437
540
|
const themeIdRef = useRef(themeId)
|
|
438
541
|
const themeModalRef = useRef(themeModal)
|
|
439
542
|
themeIdRef.current = themeId
|
|
440
543
|
themeModalRef.current = themeModal
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
const
|
|
544
|
+
const setLabelCache = useAtomSet(labelCacheAtom)
|
|
545
|
+
const setPullRequestOverrides = useAtomSet(pullRequestOverridesAtom)
|
|
546
|
+
const setRecentlyCompletedPullRequests = useAtomSet(recentlyCompletedPullRequestsAtom)
|
|
444
547
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
445
548
|
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
446
549
|
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
447
550
|
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
448
551
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
552
|
+
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
449
553
|
const usernameResult = useAtomValue(usernameAtom)
|
|
450
554
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
451
|
-
const
|
|
555
|
+
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
452
556
|
const addPullRequestLabel = useAtomSet(addPullRequestLabelAtom, { mode: "promise" })
|
|
453
557
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
454
558
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
455
|
-
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
456
559
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
457
560
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
458
561
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
459
562
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
460
563
|
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
564
|
+
const copyToClipboard = useAtomSet(copyToClipboardAtom, { mode: "promise" })
|
|
565
|
+
const openInBrowser = useAtomSet(openInBrowserAtom, { mode: "promise" })
|
|
461
566
|
const terminalWidth = width ?? 100
|
|
462
567
|
const terminalHeight = height ?? 24
|
|
463
568
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -467,14 +572,15 @@ export const App = () => {
|
|
|
467
572
|
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
|
|
468
573
|
const rightPaneWidth = isWideLayout ? Math.max(28, contentWidth - leftPaneWidth - splitGap) : contentWidth
|
|
469
574
|
const dividerJunctionAt = Math.max(1, leftPaneWidth)
|
|
470
|
-
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth -
|
|
575
|
+
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
471
576
|
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
472
|
-
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
577
|
+
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
473
578
|
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
474
579
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
475
580
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
476
581
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
477
|
-
const
|
|
582
|
+
const detailPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
583
|
+
const detailHydrationRef = useRef(new Map<string, DetailHydration>())
|
|
478
584
|
const refreshGenerationRef = useRef(0)
|
|
479
585
|
const didMountQueueModeRef = useRef(false)
|
|
480
586
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
@@ -484,7 +590,9 @@ export const App = () => {
|
|
|
484
590
|
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
485
591
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
486
592
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
593
|
+
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
487
594
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
595
|
+
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
488
596
|
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
489
597
|
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
490
598
|
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
@@ -505,6 +613,8 @@ export const App = () => {
|
|
|
505
613
|
}, [renderer, themeId])
|
|
506
614
|
|
|
507
615
|
useEffect(() => () => {
|
|
616
|
+
refreshGenerationRef.current += 1
|
|
617
|
+
detailHydrationRef.current.clear()
|
|
508
618
|
if (noticeTimeoutRef.current !== null) {
|
|
509
619
|
clearTimeout(noticeTimeoutRef.current)
|
|
510
620
|
}
|
|
@@ -514,105 +624,83 @@ export const App = () => {
|
|
|
514
624
|
if (diffPrefetchTimeoutRef.current !== null) {
|
|
515
625
|
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
516
626
|
}
|
|
627
|
+
if (detailPrefetchTimeoutRef.current !== null) {
|
|
628
|
+
clearTimeout(detailPrefetchTimeoutRef.current)
|
|
629
|
+
}
|
|
517
630
|
}, [])
|
|
518
631
|
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
const pullRequests = useMemo(() => {
|
|
523
|
-
const source = pullRequestLoad?.data ?? []
|
|
524
|
-
const seenUrls = new Set<string>()
|
|
525
|
-
const openPullRequests = source.map((pullRequest) => {
|
|
526
|
-
seenUrls.add(pullRequest.url)
|
|
527
|
-
return recentlyCompletedPullRequests[pullRequest.url] ?? pullRequestOverrides[pullRequest.url] ?? pullRequest
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
return [
|
|
531
|
-
...openPullRequests,
|
|
532
|
-
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
533
|
-
]
|
|
534
|
-
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
535
|
-
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
536
|
-
? "loading"
|
|
537
|
-
: AsyncResult.isFailure(pullRequestResult)
|
|
538
|
-
? "error"
|
|
539
|
-
: "ready"
|
|
632
|
+
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
633
|
+
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
634
|
+
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
540
635
|
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
541
636
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
542
637
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
543
638
|
pullRequestStatusRef.current = pullRequestStatus
|
|
544
639
|
|
|
545
|
-
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
546
640
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
547
641
|
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
642
|
+
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
643
|
+
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
644
|
+
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
645
|
+
const selectedRepository = viewRepository(activeView)
|
|
646
|
+
const activeViews = activePullRequestViews(activeView)
|
|
647
|
+
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
648
|
+
const loadedPullRequestCount = pullRequestLoad?.data.length ?? 0
|
|
649
|
+
const hasMorePullRequests = Boolean(pullRequestLoad?.hasNextPage && loadedPullRequestCount < config.prFetchLimit)
|
|
650
|
+
const isLoadingMorePullRequests = loadingMoreKey === currentQueueCacheKey
|
|
651
|
+
const pullRequestListRows = useMemo(() => buildPullRequestListRows({
|
|
652
|
+
groups: visibleGroups,
|
|
653
|
+
status: pullRequestStatus,
|
|
654
|
+
error: pullRequestError,
|
|
655
|
+
filterText: visibleFilterText,
|
|
656
|
+
showFilterBar: filterMode || filterQuery.length > 0,
|
|
657
|
+
loadedCount: loadedPullRequestCount,
|
|
658
|
+
hasMore: hasMorePullRequests,
|
|
659
|
+
isLoadingMore: isLoadingMorePullRequests,
|
|
660
|
+
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
661
|
+
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
662
|
+
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
663
|
+
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
568
664
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
569
|
-
const readyDiffFiles = selectedDiffState?.
|
|
665
|
+
const readyDiffFiles = selectedDiffState?._tag === "Ready" ? selectedDiffState.files : []
|
|
570
666
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
571
|
-
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
572
|
-
const selectedDiffCommentCount = useMemo(() => selectedDiffKey
|
|
573
|
-
? Object.entries(diffCommentThreads)
|
|
574
|
-
.filter(([key, comments]) => key.startsWith(`${selectedDiffKey}:`) && comments.length > 0)
|
|
575
|
-
.reduce((count, [, comments]) => count + comments.length, 0)
|
|
576
|
-
: 0, [diffCommentThreads, selectedDiffKey])
|
|
577
667
|
const diffCommentAnchors = useMemo(
|
|
578
668
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
579
669
|
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
580
670
|
)
|
|
581
671
|
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
582
|
-
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${
|
|
672
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentLocationKey(selectedDiffCommentAnchor)}` : null
|
|
583
673
|
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
584
674
|
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
585
675
|
const diffCommentRows = useMemo(
|
|
586
676
|
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
587
677
|
[diffCommentAnchors],
|
|
588
678
|
)
|
|
589
|
-
const groupStarts =
|
|
590
|
-
if (index === 0) {
|
|
591
|
-
starts.push(0)
|
|
592
|
-
return starts
|
|
593
|
-
}
|
|
594
|
-
starts.push(starts[index - 1]! + visibleGroups[index - 1]![1].length)
|
|
595
|
-
return starts
|
|
596
|
-
}, []), [visibleGroups])
|
|
679
|
+
const groupStarts = useAtomValue(groupStartsAtom)
|
|
597
680
|
const getCurrentGroupIndex = (current: number) => {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
681
|
+
if (groupStarts.length === 0) return 0
|
|
682
|
+
let low = 0
|
|
683
|
+
let high = groupStarts.length - 1
|
|
684
|
+
while (low < high) {
|
|
685
|
+
const mid = (low + high + 1) >>> 1
|
|
686
|
+
if (groupStarts[mid]! <= current) low = mid
|
|
687
|
+
else high = mid - 1
|
|
688
|
+
}
|
|
689
|
+
return low
|
|
602
690
|
}
|
|
603
691
|
const summaryRight = pullRequestLoad?.fetchedAt
|
|
604
692
|
? `updated ${formatShortDate(pullRequestLoad.fetchedAt)} ${formatTimestamp(pullRequestLoad.fetchedAt)}`
|
|
605
693
|
: pullRequestStatus === "loading"
|
|
606
694
|
? "loading pull requests..."
|
|
607
695
|
: ""
|
|
608
|
-
const headerLeft = username ? `GHUI ${username} ${
|
|
696
|
+
const headerLeft = username ? `GHUI ${username} ${viewLabel(activeView)}` : `GHUI ${viewLabel(activeView)}`
|
|
609
697
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
610
698
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
611
699
|
const selectPullRequestByUrl = (url: string) => {
|
|
612
700
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
613
701
|
if (index >= 0) {
|
|
614
702
|
setSelectedIndex(index)
|
|
615
|
-
setQueueSelection((current) => ({ ...current, [
|
|
703
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: index }))
|
|
616
704
|
}
|
|
617
705
|
}
|
|
618
706
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
@@ -622,6 +710,9 @@ export const App = () => {
|
|
|
622
710
|
}
|
|
623
711
|
const refreshPullRequests = (message?: string) => {
|
|
624
712
|
refreshGenerationRef.current += 1
|
|
713
|
+
detailHydrationRef.current.clear()
|
|
714
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
715
|
+
setLoadingMoreKey(null)
|
|
625
716
|
setPullRequestOverrides({})
|
|
626
717
|
if (message) {
|
|
627
718
|
setNotice(null)
|
|
@@ -631,15 +722,16 @@ export const App = () => {
|
|
|
631
722
|
refreshPullRequestsAtom()
|
|
632
723
|
}
|
|
633
724
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
if (mode === queueMode) return
|
|
725
|
+
const switchViewTo = (view: PullRequestView) => {
|
|
726
|
+
if (viewEquals(view, activeView)) return
|
|
637
727
|
refreshGenerationRef.current += 1
|
|
638
|
-
setQueueSelection((current) => ({ ...current, [
|
|
639
|
-
|
|
640
|
-
setSelectedIndex(
|
|
728
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: selectedIndex }))
|
|
729
|
+
setActiveView(view)
|
|
730
|
+
setSelectedIndex(registry.get(queueSelectionAtom)[viewCacheKey(view)] ?? 0)
|
|
641
731
|
setRecentlyCompletedPullRequests({})
|
|
642
|
-
detailHydrationRef.current
|
|
732
|
+
detailHydrationRef.current.clear()
|
|
733
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
734
|
+
setLoadingMoreKey(null)
|
|
643
735
|
setDetailFullView(false)
|
|
644
736
|
setDiffFullView(false)
|
|
645
737
|
setDiffCommentMode(false)
|
|
@@ -648,6 +740,82 @@ export const App = () => {
|
|
|
648
740
|
setRefreshCompletionMessage(null)
|
|
649
741
|
setRefreshStartedAt(null)
|
|
650
742
|
}
|
|
743
|
+
const switchQueueMode = (delta: 1 | -1) => {
|
|
744
|
+
switchViewTo(nextView(activeView, activeViews, delta))
|
|
745
|
+
}
|
|
746
|
+
const loadMorePullRequests = () => {
|
|
747
|
+
if (!pullRequestLoad || !hasMorePullRequests || isLoadingMorePullRequests || !pullRequestLoad.endCursor) return false
|
|
748
|
+
const remaining = config.prFetchLimit - pullRequestLoad.data.length
|
|
749
|
+
if (remaining <= 0) return false
|
|
750
|
+
const cacheKey = currentQueueCacheKey
|
|
751
|
+
const generation = refreshGenerationRef.current
|
|
752
|
+
setLoadingMoreKey(cacheKey)
|
|
753
|
+
void loadPullRequestPage({
|
|
754
|
+
mode: viewMode(activeView),
|
|
755
|
+
repository: selectedRepository,
|
|
756
|
+
cursor: pullRequestLoad.endCursor,
|
|
757
|
+
pageSize: Math.min(pullRequestPageSize, remaining),
|
|
758
|
+
}).then((page) => {
|
|
759
|
+
if (generation !== refreshGenerationRef.current) return
|
|
760
|
+
setQueueLoadCache((current) => {
|
|
761
|
+
const load = current[cacheKey]
|
|
762
|
+
if (!load) return current
|
|
763
|
+
const data = appendPullRequestPage(load.data, page.items)
|
|
764
|
+
return {
|
|
765
|
+
...current,
|
|
766
|
+
[cacheKey]: {
|
|
767
|
+
...load,
|
|
768
|
+
data,
|
|
769
|
+
endCursor: page.endCursor,
|
|
770
|
+
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
771
|
+
},
|
|
772
|
+
}
|
|
773
|
+
})
|
|
774
|
+
}).catch((error) => {
|
|
775
|
+
flashNotice(errorMessage(error))
|
|
776
|
+
}).finally(() => {
|
|
777
|
+
setLoadingMoreKey((current) => current === cacheKey ? null : current)
|
|
778
|
+
})
|
|
779
|
+
return true
|
|
780
|
+
}
|
|
781
|
+
const applyPullRequestDetail = (detail: PullRequestItem) => {
|
|
782
|
+
setQueueLoadCache((current) => {
|
|
783
|
+
const next = { ...current }
|
|
784
|
+
let changed = false
|
|
785
|
+
for (const [cacheKey, load] of Object.entries(current)) {
|
|
786
|
+
if (!load) continue
|
|
787
|
+
const index = load.data.findIndex((pullRequest) => pullRequest.url === detail.url)
|
|
788
|
+
if (index < 0) continue
|
|
789
|
+
const data = [...load.data]
|
|
790
|
+
data[index] = detail
|
|
791
|
+
changed = true
|
|
792
|
+
next[cacheKey] = { ...load, data }
|
|
793
|
+
}
|
|
794
|
+
return changed ? next : current
|
|
795
|
+
})
|
|
796
|
+
}
|
|
797
|
+
const hydratePullRequestDetails = (pullRequest: PullRequestItem, notifyError: boolean) => {
|
|
798
|
+
if (pullRequest.state !== "open" || pullRequest.detailLoaded) return false
|
|
799
|
+
const detailKey = pullRequestDetailKey(pullRequest)
|
|
800
|
+
const existing = detailHydrationRef.current.get(detailKey)
|
|
801
|
+
if (existing) {
|
|
802
|
+
if (notifyError) existing.notifyError = true
|
|
803
|
+
return false
|
|
804
|
+
}
|
|
805
|
+
if (!notifyError && detailHydrationRef.current.size >= DETAIL_PREFETCH_CONCURRENCY) return false
|
|
806
|
+
const entry: DetailHydration = { token: Symbol(detailKey), notifyError }
|
|
807
|
+
detailHydrationRef.current.set(detailKey, entry)
|
|
808
|
+
const generation = refreshGenerationRef.current
|
|
809
|
+
const atom = pullRequestDetailsAtom(pullRequestDetailAtomKey(pullRequest))
|
|
810
|
+
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })).then((detail) => {
|
|
811
|
+
if (generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) applyPullRequestDetail(detail)
|
|
812
|
+
}).catch((error) => {
|
|
813
|
+
if (entry.notifyError && generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) flashNotice(errorMessage(error))
|
|
814
|
+
}).finally(() => {
|
|
815
|
+
if (detailHydrationRef.current.get(detailKey) === entry) detailHydrationRef.current.delete(detailKey)
|
|
816
|
+
})
|
|
817
|
+
return true
|
|
818
|
+
}
|
|
651
819
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
652
820
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
653
821
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -667,14 +835,14 @@ export const App = () => {
|
|
|
667
835
|
didMountQueueModeRef.current = true
|
|
668
836
|
return
|
|
669
837
|
}
|
|
670
|
-
if (
|
|
838
|
+
if (registry.get(queueLoadCacheAtom)[currentQueueCacheKey]) return
|
|
671
839
|
refreshPullRequestsAtom()
|
|
672
|
-
}, [
|
|
840
|
+
}, [currentQueueCacheKey, refreshPullRequestsAtom, registry])
|
|
673
841
|
|
|
674
842
|
useEffect(() => {
|
|
675
843
|
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
676
844
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
677
|
-
const isHydratingDetails = pullRequestStatus === "ready" &&
|
|
845
|
+
const isHydratingDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
678
846
|
if (pullRequestStatus === "ready" && fetchedAt !== undefined && fetchedAt !== refreshStartedAt && !isHydratingDetails) {
|
|
679
847
|
flashNotice(`✓ ${refreshCompletionMessage}`)
|
|
680
848
|
setRefreshCompletionMessage(null)
|
|
@@ -727,12 +895,29 @@ export const App = () => {
|
|
|
727
895
|
}, [visiblePullRequests.length])
|
|
728
896
|
|
|
729
897
|
useEffect(() => {
|
|
730
|
-
setQueueSelection((current) => current[
|
|
731
|
-
}, [
|
|
898
|
+
setQueueSelection((current) => current[currentQueueCacheKey] === selectedIndex ? current : { ...current, [currentQueueCacheKey]: selectedIndex })
|
|
899
|
+
}, [currentQueueCacheKey, selectedIndex])
|
|
900
|
+
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0) return
|
|
903
|
+
const thresholdIndex = Math.max(0, visiblePullRequests.length - LOAD_MORE_SELECTION_THRESHOLD)
|
|
904
|
+
if (selectedIndex >= thresholdIndex) loadMorePullRequests()
|
|
905
|
+
}, [selectedIndex, visiblePullRequests.length, filterMode, filterQuery, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
906
|
+
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
const scroll = prListScrollRef.current
|
|
909
|
+
if (!scroll || selectedPullRequestRowIndex === null) return
|
|
910
|
+
const viewportHeight = scroll.viewport.height
|
|
911
|
+
if (viewportHeight <= 0) return
|
|
912
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, selectedPullRequestRowIndex, 2)
|
|
913
|
+
if (nextTop !== scroll.scrollTop) scroll.scrollTo({ x: 0, y: nextTop })
|
|
914
|
+
}, [selectedPullRequestRowIndex])
|
|
732
915
|
|
|
733
916
|
useEffect(() => {
|
|
734
917
|
setDiffFileIndex(0)
|
|
918
|
+
setDiffScrollTop(0)
|
|
735
919
|
setDiffCommentAnchorIndex(0)
|
|
920
|
+
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
736
921
|
}, [selectedIndex])
|
|
737
922
|
|
|
738
923
|
useEffect(() => {
|
|
@@ -772,13 +957,13 @@ export const App = () => {
|
|
|
772
957
|
|
|
773
958
|
if (selectedDiffKey) {
|
|
774
959
|
for (const anchor of diffCommentAnchors) {
|
|
775
|
-
if ((diffCommentThreads[`${selectedDiffKey}:${
|
|
776
|
-
applyLineColor(anchor,
|
|
960
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentLocationKey(anchor)}`]?.length ?? 0) > 0) {
|
|
961
|
+
applyLineColor(anchor, diffCommentGutterColor(anchor, "thread"))
|
|
777
962
|
}
|
|
778
963
|
}
|
|
779
964
|
}
|
|
780
965
|
if (diffCommentMode && selectedDiffCommentAnchor) {
|
|
781
|
-
applyLineColor(selectedDiffCommentAnchor, selectedDiffCommentAnchor
|
|
966
|
+
applyLineColor(selectedDiffCommentAnchor, diffCommentGutterColor(selectedDiffCommentAnchor, "selected"), true)
|
|
782
967
|
if (suppressNextDiffCommentScrollRef.current) {
|
|
783
968
|
suppressNextDiffCommentScrollRef.current = false
|
|
784
969
|
} else {
|
|
@@ -789,9 +974,9 @@ export const App = () => {
|
|
|
789
974
|
}
|
|
790
975
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
791
976
|
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
792
|
-
const isHydratingPullRequestDetails = pullRequestStatus === "ready" &&
|
|
977
|
+
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
793
978
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
794
|
-
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?.
|
|
979
|
+
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
795
980
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
796
981
|
|
|
797
982
|
useEffect(() => {
|
|
@@ -803,31 +988,30 @@ export const App = () => {
|
|
|
803
988
|
}, [hasActiveLoadingIndicator])
|
|
804
989
|
|
|
805
990
|
useEffect(() => {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
detailsFetchedAt: load.fetchedAt,
|
|
824
|
-
},
|
|
991
|
+
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
992
|
+
hydratePullRequestDetails(selectedPullRequest, true)
|
|
993
|
+
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
994
|
+
|
|
995
|
+
useEffect(() => {
|
|
996
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
997
|
+
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
998
|
+
detailPrefetchTimeoutRef.current = globalThis.setTimeout(() => {
|
|
999
|
+
detailPrefetchTimeoutRef.current = null
|
|
1000
|
+
let started = 0
|
|
1001
|
+
for (let distance = 1; distance <= Math.max(DETAIL_PREFETCH_AHEAD, DETAIL_PREFETCH_BEHIND); distance++) {
|
|
1002
|
+
const offsets = [distance <= DETAIL_PREFETCH_AHEAD ? distance : null, distance <= DETAIL_PREFETCH_BEHIND ? -distance : null]
|
|
1003
|
+
for (const offset of offsets) {
|
|
1004
|
+
if (offset === null) continue
|
|
1005
|
+
if (started >= DETAIL_PREFETCH_CONCURRENCY) return
|
|
1006
|
+
const pullRequest = visiblePullRequests[selectedIndex + offset]
|
|
1007
|
+
if (pullRequest && hydratePullRequestDetails(pullRequest, false)) started += 1
|
|
825
1008
|
}
|
|
826
|
-
}
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1009
|
+
}
|
|
1010
|
+
}, DETAIL_PREFETCH_DELAY_MS)
|
|
1011
|
+
return () => {
|
|
1012
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1013
|
+
}
|
|
1014
|
+
}, [pullRequestStatus, currentQueueCacheKey, selectedIndex, visiblePullRequests])
|
|
831
1015
|
|
|
832
1016
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
833
1017
|
status: pullRequestStatus,
|
|
@@ -836,13 +1020,18 @@ export const App = () => {
|
|
|
836
1020
|
visibleCount: visiblePullRequests.length,
|
|
837
1021
|
filterText: visibleFilterText,
|
|
838
1022
|
})
|
|
839
|
-
const
|
|
1023
|
+
const isSelectedPullRequestDetailLoading = selectedPullRequest !== null && !selectedPullRequest.detailLoaded
|
|
1024
|
+
const detailLoadingContent: DetailPlaceholderContent = selectedPullRequest ? {
|
|
1025
|
+
title: `${loadingIndicator} Loading pull request details`,
|
|
1026
|
+
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1027
|
+
} : detailPlaceholderContent
|
|
1028
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true)
|
|
840
1029
|
|
|
841
1030
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
842
1031
|
|
|
843
1032
|
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
844
1033
|
const key = pullRequestDiffKey(pullRequest)
|
|
845
|
-
const previousLoadState =
|
|
1034
|
+
const previousLoadState = registry.get(diffCommentsLoadedAtom)[key]
|
|
846
1035
|
if (!force && previousLoadState) return
|
|
847
1036
|
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
848
1037
|
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
@@ -885,22 +1074,24 @@ export const App = () => {
|
|
|
885
1074
|
const force = options.force ?? false
|
|
886
1075
|
const includeComments = options.includeComments ?? false
|
|
887
1076
|
const key = pullRequestDiffKey(pullRequest)
|
|
888
|
-
const existing =
|
|
1077
|
+
const existing = registry.get(pullRequestDiffCacheAtom)[key]
|
|
889
1078
|
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
890
|
-
if (!force && (existing
|
|
1079
|
+
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
891
1080
|
|
|
892
|
-
setPullRequestDiffCache((current) => ({ ...current, [key]:
|
|
893
|
-
|
|
1081
|
+
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
1082
|
+
const atom = pullRequestDiffAtom(pullRequestDiffAtomKey(pullRequest))
|
|
1083
|
+
if (force) registry.refresh(atom)
|
|
1084
|
+
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true }))
|
|
894
1085
|
.then((patch) => {
|
|
895
1086
|
setPullRequestDiffCache((current) => ({
|
|
896
1087
|
...current,
|
|
897
|
-
[key]: {
|
|
1088
|
+
[key]: PullRequestDiffState.Ready({ patch, files: splitPatchFiles(patch) }),
|
|
898
1089
|
}))
|
|
899
1090
|
})
|
|
900
1091
|
.catch((error) => {
|
|
901
1092
|
setPullRequestDiffCache((current) => ({
|
|
902
1093
|
...current,
|
|
903
|
-
[key]: {
|
|
1094
|
+
[key]: PullRequestDiffState.Error({ error: errorMessage(error) }),
|
|
904
1095
|
}))
|
|
905
1096
|
flashNotice(errorMessage(error))
|
|
906
1097
|
})
|
|
@@ -930,6 +1121,7 @@ export const App = () => {
|
|
|
930
1121
|
setDetailFullView(false)
|
|
931
1122
|
setDiffCommentMode(false)
|
|
932
1123
|
setDiffFileIndex(0)
|
|
1124
|
+
setDiffScrollTop(0)
|
|
933
1125
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
934
1126
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
935
1127
|
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
@@ -943,35 +1135,77 @@ export const App = () => {
|
|
|
943
1135
|
const scrollToDiffFile = (index: number) => {
|
|
944
1136
|
const stackedFile = stackedDiffFiles[index]
|
|
945
1137
|
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
1138
|
+
syncDiffScrollState()
|
|
946
1139
|
}
|
|
947
1140
|
|
|
948
|
-
const
|
|
1141
|
+
const syncDiffScrollState = () => {
|
|
949
1142
|
const scrollTop = diffScrollRef.current?.scrollTop
|
|
950
1143
|
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
951
|
-
|
|
1144
|
+
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
1145
|
+
const nextIndex = stackedDiffFileAtLine(stackedDiffFiles, scrollTop)?.index ?? 0
|
|
952
1146
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
953
1147
|
}
|
|
954
1148
|
|
|
955
1149
|
const scrollDiffBy = (y: number) => {
|
|
956
1150
|
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
957
|
-
|
|
1151
|
+
syncDiffScrollState()
|
|
958
1152
|
}
|
|
959
1153
|
|
|
960
1154
|
const scrollDiffTo = (y: number) => {
|
|
961
1155
|
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1156
|
+
syncDiffScrollState()
|
|
1157
|
+
}
|
|
1158
|
+
const scrollDetailPreviewBy = (y: number) => detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1159
|
+
const scrollDetailPreviewTo = (y: number) => detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
1160
|
+
|
|
1161
|
+
const clearPendingGTimeout = () => {
|
|
1162
|
+
if (pendingGTimeoutRef.current !== null) {
|
|
1163
|
+
clearTimeout(pendingGTimeoutRef.current)
|
|
1164
|
+
pendingGTimeoutRef.current = null
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const handleVimGoto = (key: { readonly name: string; readonly shift?: boolean }, gotoStart: () => void, gotoEnd: () => void): boolean => {
|
|
1169
|
+
if (isShiftG(key)) {
|
|
1170
|
+
gotoEnd()
|
|
1171
|
+
setPendingG(false)
|
|
1172
|
+
clearPendingGTimeout()
|
|
1173
|
+
return true
|
|
1174
|
+
}
|
|
1175
|
+
if (key.name === "g") {
|
|
1176
|
+
if (pendingG) {
|
|
1177
|
+
gotoStart()
|
|
1178
|
+
setPendingG(false)
|
|
1179
|
+
clearPendingGTimeout()
|
|
1180
|
+
} else {
|
|
1181
|
+
setPendingG(true)
|
|
1182
|
+
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1183
|
+
setPendingG(false)
|
|
1184
|
+
pendingGTimeoutRef.current = null
|
|
1185
|
+
}, 500)
|
|
1186
|
+
}
|
|
1187
|
+
return true
|
|
1188
|
+
}
|
|
1189
|
+
return false
|
|
965
1190
|
}
|
|
966
1191
|
|
|
967
1192
|
const ensureDiffLineVisible = (line: number) => {
|
|
968
1193
|
const scroll = diffScrollRef.current
|
|
969
1194
|
if (!scroll) return
|
|
970
1195
|
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
971
|
-
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line)
|
|
972
|
-
if (nextTop !== scroll.scrollTop)
|
|
1196
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line, DIFF_STICKY_HEADER_LINES)
|
|
1197
|
+
if (nextTop !== scroll.scrollTop) {
|
|
1198
|
+
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1199
|
+
syncDiffScrollState()
|
|
1200
|
+
}
|
|
973
1201
|
}
|
|
974
1202
|
|
|
1203
|
+
useEffect(() => {
|
|
1204
|
+
if (!diffFullView) return
|
|
1205
|
+
const interval = globalThis.setInterval(syncDiffScrollState, 80)
|
|
1206
|
+
return () => globalThis.clearInterval(interval)
|
|
1207
|
+
}, [diffFullView, stackedDiffFiles])
|
|
1208
|
+
|
|
975
1209
|
const jumpDiffFile = (delta: 1 | -1) => {
|
|
976
1210
|
if (readyDiffFiles.length === 0) return
|
|
977
1211
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
@@ -987,7 +1221,7 @@ export const App = () => {
|
|
|
987
1221
|
const enterDiffCommentMode = () => {
|
|
988
1222
|
const scrollTop = diffScrollRef.current?.scrollTop ?? 0
|
|
989
1223
|
suppressNextDiffCommentScrollRef.current = true
|
|
990
|
-
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop))
|
|
1224
|
+
setDiffCommentAnchorIndex(nearestDiffCommentAnchorIndex(diffCommentAnchors, scrollTop + DIFF_STICKY_HEADER_LINES))
|
|
991
1225
|
setDiffCommentMode(true)
|
|
992
1226
|
}
|
|
993
1227
|
|
|
@@ -1010,6 +1244,16 @@ export const App = () => {
|
|
|
1010
1244
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1011
1245
|
}
|
|
1012
1246
|
|
|
1247
|
+
const selectDiffCommentLine = (renderLine: number, side: DiffCommentSide | null) => {
|
|
1248
|
+
const lineAnchors = diffCommentAnchors.filter((anchor) => anchor.renderLine === renderLine)
|
|
1249
|
+
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1250
|
+
if (!nextAnchor) return
|
|
1251
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
1252
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1253
|
+
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1254
|
+
setDiffCommentMode(true)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1013
1257
|
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1014
1258
|
setCommentModal((current) => {
|
|
1015
1259
|
const next = transform({ body: current.body, cursor: current.cursor })
|
|
@@ -1089,14 +1333,14 @@ export const App = () => {
|
|
|
1089
1333
|
}
|
|
1090
1334
|
|
|
1091
1335
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
1092
|
-
void
|
|
1336
|
+
void openInBrowser(pullRequest)
|
|
1093
1337
|
.then(() => flashNotice(`Opened #${pullRequest.number} in browser`))
|
|
1094
1338
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1095
1339
|
}
|
|
1096
1340
|
|
|
1097
1341
|
const copySelectedPullRequestMetadata = () => {
|
|
1098
1342
|
if (!selectedPullRequest) return
|
|
1099
|
-
void
|
|
1343
|
+
void copyToClipboard(pullRequestMetadataText(selectedPullRequest))
|
|
1100
1344
|
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1101
1345
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1102
1346
|
}
|
|
@@ -1219,7 +1463,7 @@ export const App = () => {
|
|
|
1219
1463
|
const openLabelModal = () => {
|
|
1220
1464
|
if (!selectedPullRequest) return
|
|
1221
1465
|
const repository = selectedPullRequest.repository
|
|
1222
|
-
const cachedLabels =
|
|
1466
|
+
const cachedLabels = registry.get(labelCacheAtom)[repository]
|
|
1223
1467
|
if (cachedLabels) {
|
|
1224
1468
|
setLabelModal({
|
|
1225
1469
|
repository,
|
|
@@ -1318,9 +1562,7 @@ export const App = () => {
|
|
|
1318
1562
|
|
|
1319
1563
|
const toggleLabelAtIndex = () => {
|
|
1320
1564
|
if (!selectedPullRequest) return
|
|
1321
|
-
const filtered = labelModal.availableLabels.
|
|
1322
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1323
|
-
)
|
|
1565
|
+
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1324
1566
|
const label = filtered[labelModal.selectedIndex]
|
|
1325
1567
|
if (!label) return
|
|
1326
1568
|
|
|
@@ -1352,7 +1594,214 @@ export const App = () => {
|
|
|
1352
1594
|
}
|
|
1353
1595
|
}
|
|
1354
1596
|
|
|
1597
|
+
const openCommandPalette = () => {
|
|
1598
|
+
setCommandPalette(initialCommandPaletteState)
|
|
1599
|
+
}
|
|
1600
|
+
const openRepositoryPicker = () => {
|
|
1601
|
+
setOpenRepositoryModal({ query: selectedRepository ?? "", error: null })
|
|
1602
|
+
}
|
|
1603
|
+
const openRepositoryFromInput = () => {
|
|
1604
|
+
const repository = parseRepositoryInput(openRepositoryModal.query)
|
|
1605
|
+
if (!repository) {
|
|
1606
|
+
setOpenRepositoryModal((current) => ({ ...current, error: "Enter a repository as owner/name or a GitHub URL." }))
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
closeActiveModal()
|
|
1610
|
+
switchViewTo({ _tag: "Repository", repository })
|
|
1611
|
+
flashNotice(`Opened ${repository}`)
|
|
1612
|
+
}
|
|
1613
|
+
const insertPastedText = (text: string) => {
|
|
1614
|
+
if (text.length === 0) return false
|
|
1615
|
+
if (commandPaletteActive) {
|
|
1616
|
+
setCommandPalette((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1617
|
+
return true
|
|
1618
|
+
}
|
|
1619
|
+
if (openRepositoryModalActive) {
|
|
1620
|
+
setOpenRepositoryModal((current) => ({ ...current, query: current.query + singleLineText(text), error: null }))
|
|
1621
|
+
return true
|
|
1622
|
+
}
|
|
1623
|
+
if (themeModalActive && themeModal.filterMode) {
|
|
1624
|
+
editThemeQuery((query) => query + singleLineText(text))
|
|
1625
|
+
return true
|
|
1626
|
+
}
|
|
1627
|
+
if (commentModalActive) {
|
|
1628
|
+
editComment((state) => insertText(state, text.replace(/\r\n?/g, "\n")))
|
|
1629
|
+
return true
|
|
1630
|
+
}
|
|
1631
|
+
if (labelModalActive) {
|
|
1632
|
+
setLabelModal((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1633
|
+
return true
|
|
1634
|
+
}
|
|
1635
|
+
if (filterMode) {
|
|
1636
|
+
setFilterDraft((current) => current + singleLineText(text))
|
|
1637
|
+
return true
|
|
1638
|
+
}
|
|
1639
|
+
return false
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
useEffect(() => {
|
|
1643
|
+
const handlePaste = (event: PasteEvent) => {
|
|
1644
|
+
if (insertPastedText(pasteText(event))) event.preventDefault()
|
|
1645
|
+
}
|
|
1646
|
+
const keyInput = renderer.keyInput as unknown as {
|
|
1647
|
+
on: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1648
|
+
off: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1649
|
+
}
|
|
1650
|
+
keyInput.on("paste", handlePaste)
|
|
1651
|
+
return () => {
|
|
1652
|
+
keyInput.off("paste", handlePaste)
|
|
1653
|
+
}
|
|
1654
|
+
}, [renderer, commandPaletteActive, openRepositoryModalActive, themeModalActive, themeModal.filterMode, commentModalActive, labelModalActive, filterMode])
|
|
1655
|
+
|
|
1656
|
+
const appCommands: readonly AppCommand[] = buildAppCommands({
|
|
1657
|
+
pullRequestStatus,
|
|
1658
|
+
filterQuery,
|
|
1659
|
+
filterMode,
|
|
1660
|
+
selectedRepository,
|
|
1661
|
+
activeViews,
|
|
1662
|
+
activeView,
|
|
1663
|
+
loadedPullRequestCount,
|
|
1664
|
+
hasMorePullRequests,
|
|
1665
|
+
isLoadingMorePullRequests,
|
|
1666
|
+
selectedPullRequest,
|
|
1667
|
+
detailFullView,
|
|
1668
|
+
diffFullView,
|
|
1669
|
+
diffReady: selectedDiffState?._tag === "Ready",
|
|
1670
|
+
effectiveDiffRenderView,
|
|
1671
|
+
diffWrapMode,
|
|
1672
|
+
readyDiffFileCount: readyDiffFiles.length,
|
|
1673
|
+
diffFileIndex,
|
|
1674
|
+
diffCommentMode,
|
|
1675
|
+
selectedDiffCommentAnchorLabel: selectedDiffCommentAnchor ? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line}` : null,
|
|
1676
|
+
actions: {
|
|
1677
|
+
openCommandPalette,
|
|
1678
|
+
refreshPullRequests,
|
|
1679
|
+
openFilter: () => {
|
|
1680
|
+
setFilterDraft(filterQuery)
|
|
1681
|
+
setFilterMode(true)
|
|
1682
|
+
},
|
|
1683
|
+
clearFilter: () => {
|
|
1684
|
+
setFilterQuery("")
|
|
1685
|
+
setFilterDraft("")
|
|
1686
|
+
setFilterMode(false)
|
|
1687
|
+
},
|
|
1688
|
+
openThemeModal,
|
|
1689
|
+
openRepositoryPicker,
|
|
1690
|
+
loadMorePullRequests,
|
|
1691
|
+
switchViewTo,
|
|
1692
|
+
openDetails: () => {
|
|
1693
|
+
setDetailFullView(true)
|
|
1694
|
+
setDetailScrollOffset(0)
|
|
1695
|
+
},
|
|
1696
|
+
closeDetails: () => {
|
|
1697
|
+
setDetailFullView(false)
|
|
1698
|
+
setDetailScrollOffset(0)
|
|
1699
|
+
},
|
|
1700
|
+
openDiffView,
|
|
1701
|
+
closeDiffView: () => {
|
|
1702
|
+
setDiffFullView(false)
|
|
1703
|
+
setDiffCommentMode(false)
|
|
1704
|
+
},
|
|
1705
|
+
reloadDiff: () => {
|
|
1706
|
+
if (!selectedPullRequest) return
|
|
1707
|
+
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
1708
|
+
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1709
|
+
},
|
|
1710
|
+
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1711
|
+
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1712
|
+
jumpDiffFile,
|
|
1713
|
+
toggleDiffCommentMode: () => {
|
|
1714
|
+
if (diffCommentMode) setDiffCommentMode(false)
|
|
1715
|
+
else enterDiffCommentMode()
|
|
1716
|
+
},
|
|
1717
|
+
openDiffCommentModal,
|
|
1718
|
+
togglePullRequestDraftStatus: toggleSelectedPullRequestDraftStatus,
|
|
1719
|
+
openLabelModal,
|
|
1720
|
+
openMergeModal,
|
|
1721
|
+
openCloseModal,
|
|
1722
|
+
openPullRequestInBrowser: () => {
|
|
1723
|
+
if (selectedPullRequest) openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1724
|
+
},
|
|
1725
|
+
copyPullRequestMetadata: copySelectedPullRequestMetadata,
|
|
1726
|
+
quit: () => renderer.destroy(),
|
|
1727
|
+
},
|
|
1728
|
+
})
|
|
1729
|
+
const runCommand = (command: AppCommand, options: { readonly notifyDisabled?: boolean; readonly closePalette?: boolean } = {}) => {
|
|
1730
|
+
if (!commandEnabled(command)) {
|
|
1731
|
+
if (options.notifyDisabled && command.disabledReason) flashNotice(command.disabledReason)
|
|
1732
|
+
return false
|
|
1733
|
+
}
|
|
1734
|
+
if (options.closePalette) closeActiveModal()
|
|
1735
|
+
command.run()
|
|
1736
|
+
return true
|
|
1737
|
+
}
|
|
1738
|
+
const runCommandById = (id: string, options: { readonly notifyDisabled?: boolean } = {}) => {
|
|
1739
|
+
const command = appCommands.find((entry) => entry.id === id)
|
|
1740
|
+
return command ? runCommand(command, options) : false
|
|
1741
|
+
}
|
|
1742
|
+
const commandPaletteCommands = commandPaletteActive ? filterCommands(appCommands.filter((command) => command.id !== "command.open" && commandEnabled(command)), commandPalette.query) : []
|
|
1743
|
+
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1744
|
+
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1745
|
+
|
|
1355
1746
|
useKeyboard((key) => {
|
|
1747
|
+
if (commandPaletteActive) {
|
|
1748
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
1749
|
+
closeActiveModal()
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1753
|
+
if (selectedCommand) runCommand(selectedCommand, { notifyDisabled: true, closePalette: true })
|
|
1754
|
+
return
|
|
1755
|
+
}
|
|
1756
|
+
if (key.name === "up" || key.name === "k" && !key.ctrl && !key.meta) {
|
|
1757
|
+
setCommandPalette((current) => {
|
|
1758
|
+
const selectedIndex = clampCommandIndex(current.selectedIndex - 1, commandPaletteCommands)
|
|
1759
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1760
|
+
})
|
|
1761
|
+
return
|
|
1762
|
+
}
|
|
1763
|
+
if (key.name === "down" || key.name === "j" && !key.ctrl && !key.meta) {
|
|
1764
|
+
setCommandPalette((current) => {
|
|
1765
|
+
const selectedIndex = clampCommandIndex(current.selectedIndex + 1, commandPaletteCommands)
|
|
1766
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1767
|
+
})
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
if (isSingleLineInputKey(key)) {
|
|
1771
|
+
setCommandPalette((current) => {
|
|
1772
|
+
const query = editSingleLineInput(current.query, key) ?? current.query
|
|
1773
|
+
return current.query === query && current.selectedIndex === 0 ? current : { ...current, query, selectedIndex: 0 }
|
|
1774
|
+
})
|
|
1775
|
+
return
|
|
1776
|
+
}
|
|
1777
|
+
return
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (openRepositoryModalActive) {
|
|
1781
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
1782
|
+
closeActiveModal()
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1786
|
+
openRepositoryFromInput()
|
|
1787
|
+
return
|
|
1788
|
+
}
|
|
1789
|
+
if (isSingleLineInputKey(key)) {
|
|
1790
|
+
setOpenRepositoryModal((current) => ({
|
|
1791
|
+
...current,
|
|
1792
|
+
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
1793
|
+
error: null,
|
|
1794
|
+
}))
|
|
1795
|
+
return
|
|
1796
|
+
}
|
|
1797
|
+
return
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if ((key.ctrl && key.name === "p") || (key.meta && key.name === "k")) {
|
|
1801
|
+
runCommandById("command.open")
|
|
1802
|
+
return
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1356
1805
|
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1357
1806
|
if (themeModalActive) {
|
|
1358
1807
|
closeThemeModal(false)
|
|
@@ -1362,7 +1811,7 @@ export const App = () => {
|
|
|
1362
1811
|
closeActiveModal()
|
|
1363
1812
|
return
|
|
1364
1813
|
}
|
|
1365
|
-
|
|
1814
|
+
runCommandById("app.quit")
|
|
1366
1815
|
return
|
|
1367
1816
|
}
|
|
1368
1817
|
|
|
@@ -1392,16 +1841,8 @@ export const App = () => {
|
|
|
1392
1841
|
moveThemeSelection(1)
|
|
1393
1842
|
return
|
|
1394
1843
|
}
|
|
1395
|
-
if (themeModal.filterMode && key
|
|
1396
|
-
editThemeQuery((query) => query
|
|
1397
|
-
return
|
|
1398
|
-
}
|
|
1399
|
-
if (themeModal.filterMode && key.ctrl && key.name === "u") {
|
|
1400
|
-
updateThemeQuery("")
|
|
1401
|
-
return
|
|
1402
|
-
}
|
|
1403
|
-
if (themeModal.filterMode && !key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
1404
|
-
editThemeQuery((query) => query + key.sequence)
|
|
1844
|
+
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
1845
|
+
editThemeQuery((query) => editSingleLineInput(query, key) ?? query)
|
|
1405
1846
|
return
|
|
1406
1847
|
}
|
|
1407
1848
|
return
|
|
@@ -1500,8 +1941,9 @@ export const App = () => {
|
|
|
1500
1941
|
submitDiffComment()
|
|
1501
1942
|
return
|
|
1502
1943
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1944
|
+
const text = printableKeyText(key)
|
|
1945
|
+
if (text) {
|
|
1946
|
+
editComment((state) => insertText(state, text))
|
|
1505
1947
|
return
|
|
1506
1948
|
}
|
|
1507
1949
|
return
|
|
@@ -1574,7 +2016,6 @@ export const App = () => {
|
|
|
1574
2016
|
return
|
|
1575
2017
|
}
|
|
1576
2018
|
|
|
1577
|
-
// Label modal takes priority over everything else
|
|
1578
2019
|
if (labelModalActive) {
|
|
1579
2020
|
if (key.name === "escape") {
|
|
1580
2021
|
closeActiveModal()
|
|
@@ -1592,31 +2033,17 @@ export const App = () => {
|
|
|
1592
2033
|
return
|
|
1593
2034
|
}
|
|
1594
2035
|
if (key.name === "down" || key.name === "j") {
|
|
1595
|
-
const filtered = labelModal.availableLabels.
|
|
1596
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1597
|
-
)
|
|
2036
|
+
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1598
2037
|
setLabelModal((current) => ({
|
|
1599
2038
|
...current,
|
|
1600
2039
|
selectedIndex: Math.min(Math.max(0, filtered.length - 1), current.selectedIndex + 1),
|
|
1601
2040
|
}))
|
|
1602
2041
|
return
|
|
1603
2042
|
}
|
|
1604
|
-
if (key
|
|
2043
|
+
if (isSingleLineInputKey(key)) {
|
|
1605
2044
|
setLabelModal((current) => ({
|
|
1606
2045
|
...current,
|
|
1607
|
-
query: current.query
|
|
1608
|
-
selectedIndex: 0,
|
|
1609
|
-
}))
|
|
1610
|
-
return
|
|
1611
|
-
}
|
|
1612
|
-
if (key.ctrl && key.name === "u") {
|
|
1613
|
-
setLabelModal((current) => ({ ...current, query: "", selectedIndex: 0 }))
|
|
1614
|
-
return
|
|
1615
|
-
}
|
|
1616
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1617
|
-
setLabelModal((current) => ({
|
|
1618
|
-
...current,
|
|
1619
|
-
query: current.query + key.sequence,
|
|
2046
|
+
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
1620
2047
|
selectedIndex: 0,
|
|
1621
2048
|
}))
|
|
1622
2049
|
return
|
|
@@ -1631,7 +2058,7 @@ export const App = () => {
|
|
|
1631
2058
|
return
|
|
1632
2059
|
}
|
|
1633
2060
|
if (key.name === "c") {
|
|
1634
|
-
|
|
2061
|
+
runCommandById("diff.comment-mode")
|
|
1635
2062
|
return
|
|
1636
2063
|
}
|
|
1637
2064
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -1640,7 +2067,7 @@ export const App = () => {
|
|
|
1640
2067
|
return
|
|
1641
2068
|
}
|
|
1642
2069
|
if (key.name === "a") {
|
|
1643
|
-
|
|
2070
|
+
runCommandById("diff.add-comment")
|
|
1644
2071
|
return
|
|
1645
2072
|
}
|
|
1646
2073
|
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
@@ -1675,24 +2102,23 @@ export const App = () => {
|
|
|
1675
2102
|
selectDiffCommentSide("RIGHT")
|
|
1676
2103
|
return
|
|
1677
2104
|
}
|
|
1678
|
-
if (key.name === "]" && selectedDiffState?.
|
|
1679
|
-
|
|
2105
|
+
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
2106
|
+
runCommandById("diff.next-file")
|
|
1680
2107
|
return
|
|
1681
2108
|
}
|
|
1682
|
-
if (key.name === "[" && selectedDiffState?.
|
|
1683
|
-
|
|
2109
|
+
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
2110
|
+
runCommandById("diff.previous-file")
|
|
1684
2111
|
return
|
|
1685
2112
|
}
|
|
1686
2113
|
return
|
|
1687
2114
|
}
|
|
1688
2115
|
|
|
1689
2116
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
1690
|
-
|
|
1691
|
-
setDiffCommentMode(false)
|
|
2117
|
+
runCommandById("diff.close")
|
|
1692
2118
|
return
|
|
1693
2119
|
}
|
|
1694
|
-
if (key.name === "c" && selectedDiffState?.
|
|
1695
|
-
|
|
2120
|
+
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
2121
|
+
runCommandById("diff.comment-mode")
|
|
1696
2122
|
return
|
|
1697
2123
|
}
|
|
1698
2124
|
if (key.name === "home") {
|
|
@@ -1711,32 +2137,7 @@ export const App = () => {
|
|
|
1711
2137
|
scrollDiffBy(halfPage)
|
|
1712
2138
|
return
|
|
1713
2139
|
}
|
|
1714
|
-
if (
|
|
1715
|
-
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
1716
|
-
setPendingG(false)
|
|
1717
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1718
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1719
|
-
pendingGTimeoutRef.current = null
|
|
1720
|
-
}
|
|
1721
|
-
return
|
|
1722
|
-
}
|
|
1723
|
-
if (key.name === "g") {
|
|
1724
|
-
if (pendingG) {
|
|
1725
|
-
scrollDiffTo(0)
|
|
1726
|
-
setPendingG(false)
|
|
1727
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1728
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1729
|
-
pendingGTimeoutRef.current = null
|
|
1730
|
-
}
|
|
1731
|
-
} else {
|
|
1732
|
-
setPendingG(true)
|
|
1733
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1734
|
-
setPendingG(false)
|
|
1735
|
-
pendingGTimeoutRef.current = null
|
|
1736
|
-
}, 500)
|
|
1737
|
-
}
|
|
1738
|
-
return
|
|
1739
|
-
}
|
|
2140
|
+
if (handleVimGoto(key, () => scrollDiffTo(0), () => scrollDiffTo(Number.MAX_SAFE_INTEGER))) return
|
|
1740
2141
|
if (key.name === "up" || key.name === "k") {
|
|
1741
2142
|
scrollDiffBy(-1)
|
|
1742
2143
|
return
|
|
@@ -1754,67 +2155,64 @@ export const App = () => {
|
|
|
1754
2155
|
return
|
|
1755
2156
|
}
|
|
1756
2157
|
if (key.name === "v") {
|
|
1757
|
-
|
|
2158
|
+
runCommandById("diff.toggle-view")
|
|
1758
2159
|
return
|
|
1759
2160
|
}
|
|
1760
2161
|
if (key.name === "w") {
|
|
1761
|
-
|
|
2162
|
+
runCommandById("diff.toggle-wrap")
|
|
1762
2163
|
return
|
|
1763
2164
|
}
|
|
1764
2165
|
if (key.name === "r" && selectedPullRequest) {
|
|
1765
|
-
|
|
1766
|
-
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
2166
|
+
runCommandById("diff.reload")
|
|
1767
2167
|
return
|
|
1768
2168
|
}
|
|
1769
|
-
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?.
|
|
1770
|
-
|
|
2169
|
+
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
2170
|
+
runCommandById("diff.next-file")
|
|
1771
2171
|
return
|
|
1772
2172
|
}
|
|
1773
|
-
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?.
|
|
1774
|
-
|
|
2173
|
+
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
2174
|
+
runCommandById("diff.previous-file")
|
|
1775
2175
|
return
|
|
1776
2176
|
}
|
|
1777
2177
|
if (key.name === "o" && selectedPullRequest) {
|
|
1778
|
-
|
|
2178
|
+
runCommandById("pull.open-browser")
|
|
1779
2179
|
return
|
|
1780
2180
|
}
|
|
1781
2181
|
return
|
|
1782
2182
|
}
|
|
1783
2183
|
|
|
1784
|
-
// Fullscreen detail mode handles its own navigation keys.
|
|
1785
2184
|
if (detailFullView) {
|
|
1786
2185
|
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
1787
2186
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1788
|
-
|
|
1789
|
-
setDetailScrollOffset(0)
|
|
2187
|
+
runCommandById("detail.close")
|
|
1790
2188
|
return
|
|
1791
2189
|
}
|
|
1792
2190
|
if (isThemeKey(key)) {
|
|
1793
|
-
|
|
2191
|
+
runCommandById("theme.open")
|
|
1794
2192
|
return
|
|
1795
2193
|
}
|
|
1796
2194
|
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1797
|
-
|
|
2195
|
+
runCommandById("diff.open")
|
|
1798
2196
|
return
|
|
1799
2197
|
}
|
|
1800
2198
|
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1801
|
-
|
|
2199
|
+
runCommandById("pull.close")
|
|
1802
2200
|
return
|
|
1803
2201
|
}
|
|
1804
2202
|
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1805
|
-
|
|
2203
|
+
runCommandById("pull.labels")
|
|
1806
2204
|
return
|
|
1807
2205
|
}
|
|
1808
2206
|
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1809
|
-
|
|
2207
|
+
runCommandById("pull.merge")
|
|
1810
2208
|
return
|
|
1811
2209
|
}
|
|
1812
2210
|
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1813
|
-
|
|
2211
|
+
runCommandById("pull.toggle-draft")
|
|
1814
2212
|
return
|
|
1815
2213
|
}
|
|
1816
2214
|
if (plainKey && key.name === "r") {
|
|
1817
|
-
|
|
2215
|
+
runCommandById("pull.refresh")
|
|
1818
2216
|
return
|
|
1819
2217
|
}
|
|
1820
2218
|
if (key.name === "home") {
|
|
@@ -1822,14 +2220,9 @@ export const App = () => {
|
|
|
1822
2220
|
setDetailScrollOffset(0)
|
|
1823
2221
|
return
|
|
1824
2222
|
}
|
|
1825
|
-
if (key.name === "end"
|
|
2223
|
+
if (key.name === "end") {
|
|
1826
2224
|
detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER })
|
|
1827
2225
|
setDetailScrollOffset(Number.MAX_SAFE_INTEGER)
|
|
1828
|
-
setPendingG(false)
|
|
1829
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1830
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1831
|
-
pendingGTimeoutRef.current = null
|
|
1832
|
-
}
|
|
1833
2226
|
return
|
|
1834
2227
|
}
|
|
1835
2228
|
if (key.name === "pageup") {
|
|
@@ -1842,24 +2235,10 @@ export const App = () => {
|
|
|
1842
2235
|
setDetailScrollOffset((current) => current + halfPage)
|
|
1843
2236
|
return
|
|
1844
2237
|
}
|
|
1845
|
-
if (key
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
setPendingG(false)
|
|
1850
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1851
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1852
|
-
pendingGTimeoutRef.current = null
|
|
1853
|
-
}
|
|
1854
|
-
} else {
|
|
1855
|
-
setPendingG(true)
|
|
1856
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1857
|
-
setPendingG(false)
|
|
1858
|
-
pendingGTimeoutRef.current = null
|
|
1859
|
-
}, 500)
|
|
1860
|
-
}
|
|
1861
|
-
return
|
|
1862
|
-
}
|
|
2238
|
+
if (handleVimGoto(key,
|
|
2239
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: 0 }); setDetailScrollOffset(0) },
|
|
2240
|
+
() => { detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER }); setDetailScrollOffset(Number.MAX_SAFE_INTEGER) },
|
|
2241
|
+
)) return
|
|
1863
2242
|
if (key.name === "up" || key.name === "k") {
|
|
1864
2243
|
detailScrollRef.current?.scrollBy({ x: 0, y: -1 })
|
|
1865
2244
|
setDetailScrollOffset((current) => Math.max(0, current - 1))
|
|
@@ -1881,11 +2260,11 @@ export const App = () => {
|
|
|
1881
2260
|
return
|
|
1882
2261
|
}
|
|
1883
2262
|
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1884
|
-
|
|
2263
|
+
runCommandById("pull.open-browser")
|
|
1885
2264
|
return
|
|
1886
2265
|
}
|
|
1887
2266
|
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1888
|
-
|
|
2267
|
+
runCommandById("pull.copy-metadata")
|
|
1889
2268
|
return
|
|
1890
2269
|
}
|
|
1891
2270
|
return
|
|
@@ -1897,25 +2276,13 @@ export const App = () => {
|
|
|
1897
2276
|
setFilterMode(false)
|
|
1898
2277
|
return
|
|
1899
2278
|
}
|
|
1900
|
-
if (key.name === "enter") {
|
|
2279
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1901
2280
|
setFilterQuery(filterDraft)
|
|
1902
2281
|
setFilterMode(false)
|
|
1903
2282
|
return
|
|
1904
2283
|
}
|
|
1905
|
-
if (key
|
|
1906
|
-
setFilterDraft(
|
|
1907
|
-
return
|
|
1908
|
-
}
|
|
1909
|
-
if (key.ctrl && key.name === "w") {
|
|
1910
|
-
setFilterDraft((current) => deleteLastWord(current))
|
|
1911
|
-
return
|
|
1912
|
-
}
|
|
1913
|
-
if (key.name === "backspace") {
|
|
1914
|
-
setFilterDraft((current) => current.slice(0, -1))
|
|
1915
|
-
return
|
|
1916
|
-
}
|
|
1917
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
1918
|
-
setFilterDraft((current) => current + key.sequence)
|
|
2284
|
+
if (isSingleLineInputKey(key)) {
|
|
2285
|
+
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
1919
2286
|
return
|
|
1920
2287
|
}
|
|
1921
2288
|
}
|
|
@@ -1926,25 +2293,40 @@ export const App = () => {
|
|
|
1926
2293
|
}
|
|
1927
2294
|
|
|
1928
2295
|
if (isThemeKey(key)) {
|
|
1929
|
-
|
|
2296
|
+
runCommandById("theme.open")
|
|
1930
2297
|
return
|
|
1931
2298
|
}
|
|
1932
2299
|
|
|
1933
2300
|
if (key.name === "/") {
|
|
1934
|
-
|
|
1935
|
-
setFilterMode(true)
|
|
2301
|
+
runCommandById("filter.open")
|
|
1936
2302
|
return
|
|
1937
2303
|
}
|
|
1938
2304
|
if (key.name === "escape" && filterQuery.length > 0) {
|
|
1939
|
-
|
|
1940
|
-
setFilterDraft("")
|
|
1941
|
-
setFilterMode(false)
|
|
2305
|
+
runCommandById("filter.clear")
|
|
1942
2306
|
return
|
|
1943
2307
|
}
|
|
1944
2308
|
if (key.name === "r") {
|
|
1945
|
-
|
|
2309
|
+
runCommandById("pull.refresh")
|
|
1946
2310
|
return
|
|
1947
2311
|
}
|
|
2312
|
+
if (isWideLayout && selectedPullRequest && !detailFullView && !diffFullView) {
|
|
2313
|
+
if (key.name === "home") {
|
|
2314
|
+
scrollDetailPreviewTo(0)
|
|
2315
|
+
return
|
|
2316
|
+
}
|
|
2317
|
+
if (key.name === "end") {
|
|
2318
|
+
scrollDetailPreviewTo(Number.MAX_SAFE_INTEGER)
|
|
2319
|
+
return
|
|
2320
|
+
}
|
|
2321
|
+
if (key.name === "pageup") {
|
|
2322
|
+
scrollDetailPreviewBy(-halfPage)
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
if (key.name === "pagedown") {
|
|
2326
|
+
scrollDetailPreviewBy(halfPage)
|
|
2327
|
+
return
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
1948
2330
|
if (
|
|
1949
2331
|
key.name === "[" ||
|
|
1950
2332
|
((key.option || key.meta) && (key.name === "up" || key.name === "k")) ||
|
|
@@ -1995,68 +2377,50 @@ export const App = () => {
|
|
|
1995
2377
|
return
|
|
1996
2378
|
}
|
|
1997
2379
|
if (key.name === "down" || key.name === "j") {
|
|
2380
|
+
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2381
|
+
loadMorePullRequests()
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
1998
2384
|
setSelectedIndex((current) => {
|
|
1999
2385
|
if (visiblePullRequests.length === 0) return 0
|
|
2000
2386
|
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
2001
2387
|
})
|
|
2002
2388
|
return
|
|
2003
2389
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
return visiblePullRequests.length - 1
|
|
2009
|
-
})
|
|
2010
|
-
return
|
|
2011
|
-
}
|
|
2012
|
-
if (key.name === "g") {
|
|
2013
|
-
if (pendingG) {
|
|
2014
|
-
setSelectedIndex(0)
|
|
2015
|
-
setPendingG(false)
|
|
2016
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
2017
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
2018
|
-
pendingGTimeoutRef.current = null
|
|
2019
|
-
}
|
|
2020
|
-
} else {
|
|
2021
|
-
setPendingG(true)
|
|
2022
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
2023
|
-
setPendingG(false)
|
|
2024
|
-
pendingGTimeoutRef.current = null
|
|
2025
|
-
}, 500)
|
|
2026
|
-
}
|
|
2027
|
-
return
|
|
2028
|
-
}
|
|
2390
|
+
if (handleVimGoto(key,
|
|
2391
|
+
() => setSelectedIndex(0),
|
|
2392
|
+
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2393
|
+
)) return
|
|
2029
2394
|
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
2030
|
-
|
|
2031
|
-
setDetailScrollOffset(0)
|
|
2395
|
+
runCommandById("detail.open")
|
|
2032
2396
|
return
|
|
2033
2397
|
}
|
|
2034
2398
|
if (key.name === "d" && selectedPullRequest) {
|
|
2035
|
-
|
|
2399
|
+
runCommandById("diff.open")
|
|
2036
2400
|
return
|
|
2037
2401
|
}
|
|
2038
2402
|
if (key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2039
|
-
|
|
2403
|
+
runCommandById("pull.close")
|
|
2040
2404
|
return
|
|
2041
2405
|
}
|
|
2042
2406
|
if (key.name === "l" && selectedPullRequest) {
|
|
2043
|
-
|
|
2407
|
+
runCommandById("pull.labels")
|
|
2044
2408
|
return
|
|
2045
2409
|
}
|
|
2046
2410
|
if (key.name === "m" || key.name === "M") {
|
|
2047
|
-
if (selectedPullRequest)
|
|
2411
|
+
if (selectedPullRequest) runCommandById("pull.merge")
|
|
2048
2412
|
return
|
|
2049
2413
|
}
|
|
2050
2414
|
if (key.name === "o" && selectedPullRequest) {
|
|
2051
|
-
|
|
2415
|
+
runCommandById("pull.open-browser")
|
|
2052
2416
|
return
|
|
2053
2417
|
}
|
|
2054
2418
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
2055
|
-
|
|
2419
|
+
runCommandById("pull.toggle-draft")
|
|
2056
2420
|
return
|
|
2057
2421
|
}
|
|
2058
2422
|
if (key.name === "y" && selectedPullRequest) {
|
|
2059
|
-
|
|
2423
|
+
runCommandById("pull.copy-metadata")
|
|
2060
2424
|
return
|
|
2061
2425
|
}
|
|
2062
2426
|
})
|
|
@@ -2066,19 +2430,20 @@ export const App = () => {
|
|
|
2066
2430
|
const wideFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2067
2431
|
pullRequest: selectedPullRequest,
|
|
2068
2432
|
contentWidth: fullscreenContentWidth,
|
|
2069
|
-
bodyLines:
|
|
2433
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2070
2434
|
paneWidth: contentWidth,
|
|
2071
2435
|
showChecks: true,
|
|
2072
2436
|
}) > wideBodyHeight
|
|
2073
2437
|
const narrowFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2074
2438
|
pullRequest: selectedPullRequest,
|
|
2075
2439
|
contentWidth: fullscreenContentWidth,
|
|
2076
|
-
bodyLines:
|
|
2440
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2077
2441
|
paneWidth: contentWidth,
|
|
2078
2442
|
}) > wideBodyHeight
|
|
2079
2443
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2080
2444
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2081
|
-
const
|
|
2445
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2446
|
+
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2082
2447
|
|
|
2083
2448
|
const prListProps = {
|
|
2084
2449
|
groups: visibleGroups,
|
|
@@ -2088,6 +2453,9 @@ export const App = () => {
|
|
|
2088
2453
|
filterText: visibleFilterText,
|
|
2089
2454
|
showFilterBar: filterMode || filterQuery.length > 0,
|
|
2090
2455
|
isFilterEditing: filterMode,
|
|
2456
|
+
loadedCount: loadedPullRequestCount,
|
|
2457
|
+
hasMore: hasMorePullRequests,
|
|
2458
|
+
isLoadingMore: isLoadingMorePullRequests,
|
|
2091
2459
|
onSelectPullRequest: selectPullRequestByUrl,
|
|
2092
2460
|
} as const
|
|
2093
2461
|
|
|
@@ -2096,29 +2464,49 @@ export const App = () => {
|
|
|
2096
2464
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
2097
2465
|
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2098
2466
|
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
const
|
|
2105
|
-
const
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2108
|
-
const
|
|
2109
|
-
const
|
|
2110
|
-
const
|
|
2467
|
+
const sizedModal = (minW: number, maxW: number, padX: number, maxH: number) => {
|
|
2468
|
+
const w = Math.min(maxW, Math.max(minW, contentWidth - padX))
|
|
2469
|
+
const h = Math.min(maxH, terminalHeight - 4)
|
|
2470
|
+
return { width: w, height: h, left: centeredOffset(contentWidth, w), top: centeredOffset(terminalHeight, h) }
|
|
2471
|
+
}
|
|
2472
|
+
const closeLayout = sizedModal(46, 68, 12, 12)
|
|
2473
|
+
const closeModalWidth = closeLayout.width
|
|
2474
|
+
const closeModalHeight = closeLayout.height
|
|
2475
|
+
const closeModalLeft = closeLayout.left
|
|
2476
|
+
const closeModalTop = closeLayout.top
|
|
2477
|
+
const commentLayout = sizedModal(46, 76, 8, 16)
|
|
2478
|
+
const commentModalWidth = commentLayout.width
|
|
2479
|
+
const commentModalHeight = commentLayout.height
|
|
2480
|
+
const commentModalLeft = commentLayout.left
|
|
2481
|
+
const commentModalTop = commentLayout.top
|
|
2482
|
+
const commentThreadLayout = sizedModal(50, 86, 8, 22)
|
|
2483
|
+
const commentThreadModalWidth = commentThreadLayout.width
|
|
2484
|
+
const commentThreadModalHeight = commentThreadLayout.height
|
|
2485
|
+
const commentThreadModalLeft = commentThreadLayout.left
|
|
2486
|
+
const commentThreadModalTop = commentThreadLayout.top
|
|
2111
2487
|
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2112
2488
|
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2113
2489
|
: "No diff line selected"
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2117
|
-
const
|
|
2118
|
-
const
|
|
2119
|
-
const
|
|
2120
|
-
const
|
|
2121
|
-
const
|
|
2490
|
+
const mergeLayout = sizedModal(46, 68, 12, 16)
|
|
2491
|
+
const mergeModalWidth = mergeLayout.width
|
|
2492
|
+
const mergeModalHeight = mergeLayout.height
|
|
2493
|
+
const mergeModalLeft = mergeLayout.left
|
|
2494
|
+
const mergeModalTop = mergeLayout.top
|
|
2495
|
+
const themeLayout = sizedModal(38, 58, 12, 16)
|
|
2496
|
+
const themeModalWidth = themeLayout.width
|
|
2497
|
+
const themeModalHeight = themeLayout.height
|
|
2498
|
+
const themeModalLeft = themeLayout.left
|
|
2499
|
+
const themeModalTop = themeLayout.top
|
|
2500
|
+
const openRepositoryLayout = sizedModal(46, 76, 8, 8)
|
|
2501
|
+
const openRepositoryModalWidth = openRepositoryLayout.width
|
|
2502
|
+
const openRepositoryModalHeight = openRepositoryLayout.height
|
|
2503
|
+
const openRepositoryModalLeft = openRepositoryLayout.left
|
|
2504
|
+
const openRepositoryModalTop = openRepositoryLayout.top
|
|
2505
|
+
const commandPaletteLayout = sizedModal(50, 88, 8, 24)
|
|
2506
|
+
const commandPaletteWidth = commandPaletteLayout.width
|
|
2507
|
+
const commandPaletteHeight = commandPaletteLayout.height
|
|
2508
|
+
const commandPaletteLeft = commandPaletteLayout.left
|
|
2509
|
+
const commandPaletteTop = commandPaletteLayout.top
|
|
2122
2510
|
|
|
2123
2511
|
return (
|
|
2124
2512
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -2137,7 +2525,7 @@ export const App = () => {
|
|
|
2137
2525
|
pullRequest={selectedPullRequest}
|
|
2138
2526
|
diffState={selectedDiffState}
|
|
2139
2527
|
stackedFiles={stackedDiffFiles}
|
|
2140
|
-
|
|
2528
|
+
scrollTop={diffScrollTop}
|
|
2141
2529
|
view={effectiveDiffRenderView}
|
|
2142
2530
|
wrapMode={diffWrapMode}
|
|
2143
2531
|
paneWidth={contentWidth}
|
|
@@ -2148,9 +2536,14 @@ export const App = () => {
|
|
|
2148
2536
|
commentMode={diffCommentMode}
|
|
2149
2537
|
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2150
2538
|
selectedCommentThread={selectedDiffCommentThread}
|
|
2151
|
-
|
|
2539
|
+
onSelectCommentLine={selectDiffCommentLine}
|
|
2152
2540
|
themeId={themeId}
|
|
2153
2541
|
/>
|
|
2542
|
+
) : detailFullView && isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2543
|
+
<box flexGrow={1} flexDirection="column">
|
|
2544
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks={isWideLayout} />
|
|
2545
|
+
<LoadingPane content={detailLoadingContent} width={contentWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout))} />
|
|
2546
|
+
</box>
|
|
2154
2547
|
) : isWideLayout && detailFullView ? (
|
|
2155
2548
|
<box flexGrow={1} flexDirection="column">
|
|
2156
2549
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
@@ -2159,6 +2552,7 @@ export const App = () => {
|
|
|
2159
2552
|
viewerUsername={username}
|
|
2160
2553
|
contentWidth={fullscreenContentWidth}
|
|
2161
2554
|
bodyLines={fullscreenBodyLines}
|
|
2555
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2162
2556
|
paneWidth={contentWidth}
|
|
2163
2557
|
showChecks
|
|
2164
2558
|
placeholderContent={detailPlaceholderContent}
|
|
@@ -2168,19 +2562,26 @@ export const App = () => {
|
|
|
2168
2562
|
</scrollbox>
|
|
2169
2563
|
</box>
|
|
2170
2564
|
) : isWideLayout ? (
|
|
2171
|
-
|
|
2172
|
-
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column"
|
|
2173
|
-
<scrollbox height={wideBodyHeight} flexGrow={0}>
|
|
2174
|
-
<
|
|
2565
|
+
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
2566
|
+
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2567
|
+
<scrollbox ref={prListScrollRef} focusable={false} height={wideBodyHeight} flexGrow={0}>
|
|
2568
|
+
<box paddingLeft={sectionPadding} paddingRight={0}>
|
|
2569
|
+
<PullRequestList key={`wide-${leftContentWidth}`} {...prListProps} contentWidth={leftContentWidth} />
|
|
2570
|
+
</box>
|
|
2175
2571
|
</scrollbox>
|
|
2176
2572
|
</box>
|
|
2177
2573
|
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
2178
2574
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2179
|
-
{selectedPullRequest ? (
|
|
2575
|
+
{isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2576
|
+
<>
|
|
2577
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2578
|
+
<LoadingPane content={detailLoadingContent} width={rightPaneWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true))} />
|
|
2579
|
+
</>
|
|
2580
|
+
) : selectedPullRequest ? (
|
|
2180
2581
|
<>
|
|
2181
2582
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2182
|
-
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2183
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2583
|
+
<scrollbox ref={detailPreviewScrollRef} flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2584
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2184
2585
|
</scrollbox>
|
|
2185
2586
|
</>
|
|
2186
2587
|
) : (
|
|
@@ -2196,6 +2597,7 @@ export const App = () => {
|
|
|
2196
2597
|
viewerUsername={username}
|
|
2197
2598
|
contentWidth={fullscreenContentWidth}
|
|
2198
2599
|
bodyLines={fullscreenBodyLines}
|
|
2600
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2199
2601
|
paneWidth={contentWidth}
|
|
2200
2602
|
placeholderContent={detailPlaceholderContent}
|
|
2201
2603
|
loadingIndicator={loadingIndicator}
|
|
@@ -2204,13 +2606,13 @@ export const App = () => {
|
|
|
2204
2606
|
</scrollbox>
|
|
2205
2607
|
</box>
|
|
2206
2608
|
) : (
|
|
2207
|
-
<box height={wideBodyHeight} flexDirection="column">
|
|
2208
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={
|
|
2609
|
+
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2610
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2209
2611
|
<Divider width={contentWidth} />
|
|
2210
2612
|
<box flexGrow={1} flexDirection="column">
|
|
2211
|
-
<scrollbox flexGrow={1}>
|
|
2613
|
+
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
2212
2614
|
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2213
|
-
<PullRequestList {...prListProps} contentWidth={
|
|
2615
|
+
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
2214
2616
|
</box>
|
|
2215
2617
|
</scrollbox>
|
|
2216
2618
|
</box>
|
|
@@ -2303,6 +2705,26 @@ export const App = () => {
|
|
|
2303
2705
|
offsetTop={themeModalTop}
|
|
2304
2706
|
/>
|
|
2305
2707
|
) : null}
|
|
2708
|
+
{openRepositoryModalActive ? (
|
|
2709
|
+
<OpenRepositoryModal
|
|
2710
|
+
state={openRepositoryModal}
|
|
2711
|
+
modalWidth={openRepositoryModalWidth}
|
|
2712
|
+
modalHeight={openRepositoryModalHeight}
|
|
2713
|
+
offsetLeft={openRepositoryModalLeft}
|
|
2714
|
+
offsetTop={openRepositoryModalTop}
|
|
2715
|
+
/>
|
|
2716
|
+
) : null}
|
|
2717
|
+
{commandPaletteActive ? (
|
|
2718
|
+
<CommandPalette
|
|
2719
|
+
commands={commandPaletteCommands}
|
|
2720
|
+
query={commandPalette.query}
|
|
2721
|
+
selectedIndex={selectedCommandIndex}
|
|
2722
|
+
modalWidth={commandPaletteWidth}
|
|
2723
|
+
modalHeight={commandPaletteHeight}
|
|
2724
|
+
offsetLeft={commandPaletteLeft}
|
|
2725
|
+
offsetTop={commandPaletteTop}
|
|
2726
|
+
/>
|
|
2727
|
+
) : null}
|
|
2306
2728
|
</box>
|
|
2307
2729
|
)
|
|
2308
2730
|
}
|