@kitlangton/ghui 0.1.19 → 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 +818 -403
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +68 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +29 -20
- 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 +290 -126
- 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 +84 -179
- package/src/ui/PullRequestDiffPane.tsx +29 -50
- package/src/ui/PullRequestList.tsx +99 -45
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +34 -44
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +263 -295
- package/src/ui/primitives.tsx +90 -0
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
package/src/App.tsx
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
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,
|
|
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"
|
|
19
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
|
+
}
|
|
28
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
|
|
49
|
+
|
|
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)
|
|
29
57
|
|
|
30
58
|
interface PullRequestLoad {
|
|
31
|
-
readonly
|
|
59
|
+
readonly view: PullRequestView
|
|
32
60
|
readonly data: readonly PullRequestItem[]
|
|
33
61
|
readonly fetchedAt: Date | null
|
|
34
|
-
readonly
|
|
62
|
+
readonly endCursor: string | null
|
|
63
|
+
readonly hasNextPage: boolean
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
interface DetailPlaceholderInput {
|
|
@@ -66,87 +95,103 @@ interface AppliedDiffLineColorState {
|
|
|
66
95
|
readonly entries: readonly AppliedDiffLineColor[]
|
|
67
96
|
}
|
|
68
97
|
|
|
98
|
+
interface DetailHydration {
|
|
99
|
+
readonly token: symbol
|
|
100
|
+
notifyError: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
const PR_FETCH_RETRIES = 6
|
|
70
104
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
71
105
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
72
106
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
73
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
122
|
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
98
|
-
const
|
|
99
|
-
const queueLoadCacheAtom = Atom.make<Partial<Record<
|
|
100
|
-
const queueSelectionAtom = Atom.make<Partial<Record<
|
|
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
|
|
135
|
+
const view = yield* Atom.get(activeViewAtom)
|
|
136
|
+
const queueMode = viewMode(view)
|
|
137
|
+
const repository = viewRepository(view)
|
|
138
|
+
const cacheKey = viewCacheKey(view)
|
|
105
139
|
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
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 diffScrollTopAtom = Atom.make(0)
|
|
141
|
-
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
142
|
-
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
143
|
-
const diffCommentModeAtom = Atom.make(false)
|
|
144
|
-
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
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)
|
|
145
190
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
146
191
|
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
147
192
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
148
193
|
|
|
149
|
-
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
194
|
+
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
150
195
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
151
196
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
152
197
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -157,12 +202,110 @@ const usernameAtom = githubRuntime.atom(
|
|
|
157
202
|
: Effect.succeed(config.author.replace(/^@/, "")),
|
|
158
203
|
).pipe(Atom.keepAlive)
|
|
159
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
|
+
|
|
160
299
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
161
300
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
162
301
|
)
|
|
163
|
-
const
|
|
164
|
-
GitHubService.use((github) => github.
|
|
302
|
+
const listOpenPullRequestPageAtom = githubRuntime.fn<ListPullRequestPageInput>()((input) =>
|
|
303
|
+
GitHubService.use((github) => github.listOpenPullRequestPage(input))
|
|
165
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
|
+
})
|
|
166
309
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
167
310
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
168
311
|
)
|
|
@@ -172,9 +315,10 @@ const removePullRequestLabelAtom = githubRuntime.fn<{ readonly repository: strin
|
|
|
172
315
|
const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly isDraft: boolean }>()((input) =>
|
|
173
316
|
GitHubService.use((github) => github.toggleDraftStatus(input.repository, input.number, input.isDraft))
|
|
174
317
|
)
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
)
|
|
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
|
+
})
|
|
178
322
|
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
179
323
|
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
180
324
|
)
|
|
@@ -188,12 +332,13 @@ const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; rea
|
|
|
188
332
|
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
189
333
|
)
|
|
190
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)))
|
|
191
337
|
|
|
192
338
|
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
193
339
|
|
|
194
|
-
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
195
340
|
|
|
196
|
-
const
|
|
341
|
+
const pasteText = (event: PasteEvent) => new TextDecoder().decode(event.bytes)
|
|
197
342
|
|
|
198
343
|
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
199
344
|
const normalized = query.trim().toLowerCase()
|
|
@@ -210,91 +355,34 @@ const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) =>
|
|
|
210
355
|
return scores.length > 0 ? Math.min(...scores) : null
|
|
211
356
|
}
|
|
212
357
|
|
|
213
|
-
const
|
|
214
|
-
if (process.platform === "darwin") return [["pbcopy"]]
|
|
215
|
-
if (process.platform === "linux") {
|
|
216
|
-
return [
|
|
217
|
-
...(process.env.WAYLAND_DISPLAY ? [["wl-copy"]] : []),
|
|
218
|
-
["xclip", "-selection", "clipboard"],
|
|
219
|
-
["xsel", "--clipboard", "--input"],
|
|
220
|
-
]
|
|
221
|
-
}
|
|
222
|
-
return []
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const copyToClipboard = async (text: string) => {
|
|
226
|
-
const commands = clipboardCommands()
|
|
227
|
-
let lastError = ""
|
|
228
|
-
|
|
229
|
-
for (const command of commands) {
|
|
230
|
-
let proc: Bun.Subprocess<"pipe", "ignore", "pipe">
|
|
231
|
-
try {
|
|
232
|
-
proc = Bun.spawn({
|
|
233
|
-
cmd: [...command],
|
|
234
|
-
stdin: "pipe",
|
|
235
|
-
stdout: "ignore",
|
|
236
|
-
stderr: "pipe",
|
|
237
|
-
})
|
|
238
|
-
} catch (error) {
|
|
239
|
-
lastError = errorMessage(error)
|
|
240
|
-
continue
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
proc.stdin.write(text)
|
|
244
|
-
proc.stdin.end()
|
|
245
|
-
|
|
246
|
-
const exitCode = await proc.exited
|
|
247
|
-
if (exitCode === 0) return
|
|
248
|
-
|
|
249
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
250
|
-
lastError = stderr.trim()
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const installHint = process.platform === "linux" ? " Install wl-clipboard, xclip, or xsel." : ""
|
|
254
|
-
throw new Error(lastError || `Clipboard is not available.${installHint}`)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const openPullRequestInBrowser = async (pullRequest: PullRequestItem) => {
|
|
258
|
-
const proc = Bun.spawn({
|
|
259
|
-
cmd: ["gh", "pr", "view", String(pullRequest.number), "--repo", pullRequest.repository, "--web"],
|
|
260
|
-
stdout: "ignore",
|
|
261
|
-
stderr: "pipe",
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
const exitCode = await proc.exited
|
|
265
|
-
if (exitCode === 0) return
|
|
266
|
-
|
|
267
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
268
|
-
throw new Error(stderr.trim() || "Could not open PR in browser")
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
|
|
358
|
+
const pullRequestMetadataText = (pullRequest: PullRequestItem) => {
|
|
272
359
|
const lines = [
|
|
273
360
|
pullRequest.title,
|
|
274
361
|
`${pullRequest.repository} #${pullRequest.number}`,
|
|
275
362
|
pullRequest.url,
|
|
276
363
|
]
|
|
277
|
-
|
|
278
364
|
const review = reviewLabel(pullRequest)
|
|
279
|
-
if (review) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
lines.push(pullRequest.checkSummary)
|
|
284
|
-
}
|
|
365
|
+
if (review) lines.push(`review: ${review}`)
|
|
366
|
+
if (pullRequest.checkSummary) lines.push(pullRequest.checkSummary)
|
|
367
|
+
return lines.join("\n")
|
|
368
|
+
}
|
|
285
369
|
|
|
286
|
-
|
|
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) }
|
|
287
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")
|
|
288
381
|
|
|
289
382
|
const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
|
|
290
383
|
|
|
291
384
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
292
385
|
|
|
293
|
-
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
294
|
-
const index = pullRequestQueueModes.indexOf(mode)
|
|
295
|
-
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
296
|
-
}
|
|
297
|
-
|
|
298
386
|
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
299
387
|
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
300
388
|
|
|
@@ -321,28 +409,11 @@ const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig =
|
|
|
321
409
|
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
322
410
|
}
|
|
323
411
|
|
|
324
|
-
const mixHexColor = (color: string, base: string, amount: number) => {
|
|
325
|
-
const parse = (hex: string) => {
|
|
326
|
-
const normalized = hex.replace(/^#/, "")
|
|
327
|
-
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null
|
|
328
|
-
return {
|
|
329
|
-
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
330
|
-
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
331
|
-
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const left = parse(color)
|
|
335
|
-
const right = parse(base)
|
|
336
|
-
if (!left || !right) return color
|
|
337
|
-
const channel = (key: "r" | "g" | "b") => Math.round(left[key] * amount + right[key] * (1 - amount)).toString(16).padStart(2, "0")
|
|
338
|
-
return `#${channel("r")}${channel("g")}${channel("b")}`
|
|
339
|
-
}
|
|
340
|
-
|
|
341
412
|
const diffCommentGutterColor = (anchor: DiffCommentAnchor, kind: "selected" | "thread") => {
|
|
342
413
|
const accent = kind === "thread"
|
|
343
414
|
? colors.status.pending
|
|
344
415
|
: anchor.side === "RIGHT" ? colors.status.passing : colors.status.failing
|
|
345
|
-
return
|
|
416
|
+
return mixHex(originalDiffLineColor(anchor).gutter, accent, 0.45)
|
|
346
417
|
}
|
|
347
418
|
|
|
348
419
|
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
@@ -404,11 +475,12 @@ const getDetailPlaceholderContent = ({
|
|
|
404
475
|
export const App = () => {
|
|
405
476
|
const renderer = useRenderer()
|
|
406
477
|
const { width, height } = useTerminalDimensions()
|
|
478
|
+
const registry = useContext(RegistryContext)
|
|
407
479
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
408
480
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
409
|
-
const [
|
|
410
|
-
const
|
|
411
|
-
const
|
|
481
|
+
const [activeView, setActiveView] = useAtom(activeViewAtom)
|
|
482
|
+
const setQueueLoadCache = useAtomSet(queueLoadCacheAtom)
|
|
483
|
+
const setQueueSelection = useAtomSet(queueSelectionAtom)
|
|
412
484
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
413
485
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
414
486
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -416,7 +488,7 @@ export const App = () => {
|
|
|
416
488
|
const [filterMode, setFilterMode] = useAtom(filterModeAtom)
|
|
417
489
|
const [pendingG, setPendingG] = useAtom(pendingGAtom)
|
|
418
490
|
const [detailFullView, setDetailFullView] = useAtom(detailFullViewAtom)
|
|
419
|
-
const
|
|
491
|
+
const setDetailScrollOffset = useAtomSet(detailScrollOffsetAtom)
|
|
420
492
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
421
493
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
422
494
|
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
@@ -425,8 +497,8 @@ export const App = () => {
|
|
|
425
497
|
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
426
498
|
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
427
499
|
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
428
|
-
const
|
|
429
|
-
const
|
|
500
|
+
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
501
|
+
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
430
502
|
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
431
503
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
432
504
|
const closeActiveModal = () => setActiveModal(initialModal)
|
|
@@ -436,12 +508,16 @@ export const App = () => {
|
|
|
436
508
|
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
437
509
|
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
438
510
|
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
511
|
+
const commandPaletteActive = Modal.$is("CommandPalette")(activeModal)
|
|
512
|
+
const openRepositoryModalActive = Modal.$is("OpenRepository")(activeModal)
|
|
439
513
|
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
440
514
|
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
441
515
|
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
442
516
|
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
443
517
|
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
444
518
|
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
519
|
+
const commandPalette: CommandPaletteState = commandPaletteActive ? activeModal : initialCommandPaletteState
|
|
520
|
+
const openRepositoryModal: OpenRepositoryModalState = openRepositoryModalActive ? activeModal : initialOpenRepositoryModalState
|
|
445
521
|
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
446
522
|
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
447
523
|
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
@@ -458,31 +534,35 @@ export const App = () => {
|
|
|
458
534
|
const setCommentModal = makeModalSetter("Comment")
|
|
459
535
|
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
460
536
|
const setThemeModal = makeModalSetter("Theme")
|
|
537
|
+
const setCommandPalette = makeModalSetter("CommandPalette")
|
|
538
|
+
const setOpenRepositoryModal = makeModalSetter("OpenRepository")
|
|
461
539
|
setActiveTheme(themeId)
|
|
462
540
|
const themeIdRef = useRef(themeId)
|
|
463
541
|
const themeModalRef = useRef(themeModal)
|
|
464
542
|
themeIdRef.current = themeId
|
|
465
543
|
themeModalRef.current = themeModal
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
const
|
|
544
|
+
const setLabelCache = useAtomSet(labelCacheAtom)
|
|
545
|
+
const setPullRequestOverrides = useAtomSet(pullRequestOverridesAtom)
|
|
546
|
+
const setRecentlyCompletedPullRequests = useAtomSet(recentlyCompletedPullRequestsAtom)
|
|
469
547
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
470
548
|
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
471
549
|
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
472
550
|
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
473
551
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
552
|
+
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
474
553
|
const usernameResult = useAtomValue(usernameAtom)
|
|
475
554
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
476
|
-
const
|
|
555
|
+
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
477
556
|
const addPullRequestLabel = useAtomSet(addPullRequestLabelAtom, { mode: "promise" })
|
|
478
557
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
479
558
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
480
|
-
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
481
559
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
482
560
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
483
561
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
484
562
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
485
563
|
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
564
|
+
const copyToClipboard = useAtomSet(copyToClipboardAtom, { mode: "promise" })
|
|
565
|
+
const openInBrowser = useAtomSet(openInBrowserAtom, { mode: "promise" })
|
|
486
566
|
const terminalWidth = width ?? 100
|
|
487
567
|
const terminalHeight = height ?? 24
|
|
488
568
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -492,14 +572,15 @@ export const App = () => {
|
|
|
492
572
|
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
|
|
493
573
|
const rightPaneWidth = isWideLayout ? Math.max(28, contentWidth - leftPaneWidth - splitGap) : contentWidth
|
|
494
574
|
const dividerJunctionAt = Math.max(1, leftPaneWidth)
|
|
495
|
-
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth -
|
|
575
|
+
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
496
576
|
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
497
|
-
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
577
|
+
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
498
578
|
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
499
579
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
500
580
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
501
581
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
502
|
-
const
|
|
582
|
+
const detailPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
583
|
+
const detailHydrationRef = useRef(new Map<string, DetailHydration>())
|
|
503
584
|
const refreshGenerationRef = useRef(0)
|
|
504
585
|
const didMountQueueModeRef = useRef(false)
|
|
505
586
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
@@ -509,7 +590,9 @@ export const App = () => {
|
|
|
509
590
|
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
510
591
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
511
592
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
593
|
+
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
512
594
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
595
|
+
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
513
596
|
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
514
597
|
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
515
598
|
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
@@ -530,6 +613,8 @@ export const App = () => {
|
|
|
530
613
|
}, [renderer, themeId])
|
|
531
614
|
|
|
532
615
|
useEffect(() => () => {
|
|
616
|
+
refreshGenerationRef.current += 1
|
|
617
|
+
detailHydrationRef.current.clear()
|
|
533
618
|
if (noticeTimeoutRef.current !== null) {
|
|
534
619
|
clearTimeout(noticeTimeoutRef.current)
|
|
535
620
|
}
|
|
@@ -539,100 +624,83 @@ export const App = () => {
|
|
|
539
624
|
if (diffPrefetchTimeoutRef.current !== null) {
|
|
540
625
|
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
541
626
|
}
|
|
627
|
+
if (detailPrefetchTimeoutRef.current !== null) {
|
|
628
|
+
clearTimeout(detailPrefetchTimeoutRef.current)
|
|
629
|
+
}
|
|
542
630
|
}, [])
|
|
543
631
|
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const pullRequests = useMemo(() => {
|
|
548
|
-
const source = pullRequestLoad?.data ?? []
|
|
549
|
-
const seenUrls = new Set<string>()
|
|
550
|
-
const openPullRequests = source.map((pullRequest) => {
|
|
551
|
-
seenUrls.add(pullRequest.url)
|
|
552
|
-
return recentlyCompletedPullRequests[pullRequest.url] ?? pullRequestOverrides[pullRequest.url] ?? pullRequest
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
return [
|
|
556
|
-
...openPullRequests,
|
|
557
|
-
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
558
|
-
]
|
|
559
|
-
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
560
|
-
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
561
|
-
? "loading"
|
|
562
|
-
: AsyncResult.isFailure(pullRequestResult)
|
|
563
|
-
? "error"
|
|
564
|
-
: "ready"
|
|
632
|
+
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
633
|
+
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
634
|
+
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
565
635
|
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
566
636
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
567
637
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
568
638
|
pullRequestStatusRef.current = pullRequestStatus
|
|
569
639
|
|
|
570
|
-
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
571
640
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
572
641
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
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)
|
|
593
664
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
594
665
|
const readyDiffFiles = selectedDiffState?._tag === "Ready" ? selectedDiffState.files : []
|
|
595
666
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
596
|
-
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
597
667
|
const diffCommentAnchors = useMemo(
|
|
598
668
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
599
669
|
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
600
670
|
)
|
|
601
671
|
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
602
|
-
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${
|
|
672
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentLocationKey(selectedDiffCommentAnchor)}` : null
|
|
603
673
|
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
604
674
|
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
605
675
|
const diffCommentRows = useMemo(
|
|
606
676
|
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
607
677
|
[diffCommentAnchors],
|
|
608
678
|
)
|
|
609
|
-
const groupStarts =
|
|
610
|
-
if (index === 0) {
|
|
611
|
-
starts.push(0)
|
|
612
|
-
return starts
|
|
613
|
-
}
|
|
614
|
-
starts.push(starts[index - 1]! + visibleGroups[index - 1]![1].length)
|
|
615
|
-
return starts
|
|
616
|
-
}, []), [visibleGroups])
|
|
679
|
+
const groupStarts = useAtomValue(groupStartsAtom)
|
|
617
680
|
const getCurrentGroupIndex = (current: number) => {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
622
690
|
}
|
|
623
691
|
const summaryRight = pullRequestLoad?.fetchedAt
|
|
624
692
|
? `updated ${formatShortDate(pullRequestLoad.fetchedAt)} ${formatTimestamp(pullRequestLoad.fetchedAt)}`
|
|
625
693
|
: pullRequestStatus === "loading"
|
|
626
694
|
? "loading pull requests..."
|
|
627
695
|
: ""
|
|
628
|
-
const headerLeft = username ? `GHUI ${username} ${
|
|
696
|
+
const headerLeft = username ? `GHUI ${username} ${viewLabel(activeView)}` : `GHUI ${viewLabel(activeView)}`
|
|
629
697
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
630
698
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
631
699
|
const selectPullRequestByUrl = (url: string) => {
|
|
632
700
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
633
701
|
if (index >= 0) {
|
|
634
702
|
setSelectedIndex(index)
|
|
635
|
-
setQueueSelection((current) => ({ ...current, [
|
|
703
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: index }))
|
|
636
704
|
}
|
|
637
705
|
}
|
|
638
706
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
@@ -642,6 +710,9 @@ export const App = () => {
|
|
|
642
710
|
}
|
|
643
711
|
const refreshPullRequests = (message?: string) => {
|
|
644
712
|
refreshGenerationRef.current += 1
|
|
713
|
+
detailHydrationRef.current.clear()
|
|
714
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
715
|
+
setLoadingMoreKey(null)
|
|
645
716
|
setPullRequestOverrides({})
|
|
646
717
|
if (message) {
|
|
647
718
|
setNotice(null)
|
|
@@ -651,15 +722,16 @@ export const App = () => {
|
|
|
651
722
|
refreshPullRequestsAtom()
|
|
652
723
|
}
|
|
653
724
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
if (mode === queueMode) return
|
|
725
|
+
const switchViewTo = (view: PullRequestView) => {
|
|
726
|
+
if (viewEquals(view, activeView)) return
|
|
657
727
|
refreshGenerationRef.current += 1
|
|
658
|
-
setQueueSelection((current) => ({ ...current, [
|
|
659
|
-
|
|
660
|
-
setSelectedIndex(
|
|
728
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: selectedIndex }))
|
|
729
|
+
setActiveView(view)
|
|
730
|
+
setSelectedIndex(registry.get(queueSelectionAtom)[viewCacheKey(view)] ?? 0)
|
|
661
731
|
setRecentlyCompletedPullRequests({})
|
|
662
|
-
detailHydrationRef.current
|
|
732
|
+
detailHydrationRef.current.clear()
|
|
733
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
734
|
+
setLoadingMoreKey(null)
|
|
663
735
|
setDetailFullView(false)
|
|
664
736
|
setDiffFullView(false)
|
|
665
737
|
setDiffCommentMode(false)
|
|
@@ -668,6 +740,82 @@ export const App = () => {
|
|
|
668
740
|
setRefreshCompletionMessage(null)
|
|
669
741
|
setRefreshStartedAt(null)
|
|
670
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
|
+
}
|
|
671
819
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
672
820
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
673
821
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -687,14 +835,14 @@ export const App = () => {
|
|
|
687
835
|
didMountQueueModeRef.current = true
|
|
688
836
|
return
|
|
689
837
|
}
|
|
690
|
-
if (
|
|
838
|
+
if (registry.get(queueLoadCacheAtom)[currentQueueCacheKey]) return
|
|
691
839
|
refreshPullRequestsAtom()
|
|
692
|
-
}, [
|
|
840
|
+
}, [currentQueueCacheKey, refreshPullRequestsAtom, registry])
|
|
693
841
|
|
|
694
842
|
useEffect(() => {
|
|
695
843
|
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
696
844
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
697
|
-
const isHydratingDetails = pullRequestStatus === "ready" &&
|
|
845
|
+
const isHydratingDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
698
846
|
if (pullRequestStatus === "ready" && fetchedAt !== undefined && fetchedAt !== refreshStartedAt && !isHydratingDetails) {
|
|
699
847
|
flashNotice(`✓ ${refreshCompletionMessage}`)
|
|
700
848
|
setRefreshCompletionMessage(null)
|
|
@@ -747,13 +895,29 @@ export const App = () => {
|
|
|
747
895
|
}, [visiblePullRequests.length])
|
|
748
896
|
|
|
749
897
|
useEffect(() => {
|
|
750
|
-
setQueueSelection((current) => current[
|
|
751
|
-
}, [
|
|
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])
|
|
752
915
|
|
|
753
916
|
useEffect(() => {
|
|
754
917
|
setDiffFileIndex(0)
|
|
755
918
|
setDiffScrollTop(0)
|
|
756
919
|
setDiffCommentAnchorIndex(0)
|
|
920
|
+
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
757
921
|
}, [selectedIndex])
|
|
758
922
|
|
|
759
923
|
useEffect(() => {
|
|
@@ -793,7 +957,7 @@ export const App = () => {
|
|
|
793
957
|
|
|
794
958
|
if (selectedDiffKey) {
|
|
795
959
|
for (const anchor of diffCommentAnchors) {
|
|
796
|
-
if ((diffCommentThreads[`${selectedDiffKey}:${
|
|
960
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentLocationKey(anchor)}`]?.length ?? 0) > 0) {
|
|
797
961
|
applyLineColor(anchor, diffCommentGutterColor(anchor, "thread"))
|
|
798
962
|
}
|
|
799
963
|
}
|
|
@@ -810,7 +974,7 @@ export const App = () => {
|
|
|
810
974
|
}
|
|
811
975
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
812
976
|
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
813
|
-
const isHydratingPullRequestDetails = pullRequestStatus === "ready" &&
|
|
977
|
+
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
814
978
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
815
979
|
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
816
980
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
@@ -824,31 +988,30 @@ export const App = () => {
|
|
|
824
988
|
}, [hasActiveLoadingIndicator])
|
|
825
989
|
|
|
826
990
|
useEffect(() => {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
detailsFetchedAt: load.fetchedAt,
|
|
845
|
-
},
|
|
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
|
|
846
1008
|
}
|
|
847
|
-
}
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1009
|
+
}
|
|
1010
|
+
}, DETAIL_PREFETCH_DELAY_MS)
|
|
1011
|
+
return () => {
|
|
1012
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1013
|
+
}
|
|
1014
|
+
}, [pullRequestStatus, currentQueueCacheKey, selectedIndex, visiblePullRequests])
|
|
852
1015
|
|
|
853
1016
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
854
1017
|
status: pullRequestStatus,
|
|
@@ -857,13 +1020,18 @@ export const App = () => {
|
|
|
857
1020
|
visibleCount: visiblePullRequests.length,
|
|
858
1021
|
filterText: visibleFilterText,
|
|
859
1022
|
})
|
|
860
|
-
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)
|
|
861
1029
|
|
|
862
1030
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
863
1031
|
|
|
864
1032
|
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
865
1033
|
const key = pullRequestDiffKey(pullRequest)
|
|
866
|
-
const previousLoadState =
|
|
1034
|
+
const previousLoadState = registry.get(diffCommentsLoadedAtom)[key]
|
|
867
1035
|
if (!force && previousLoadState) return
|
|
868
1036
|
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
869
1037
|
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
@@ -906,12 +1074,14 @@ export const App = () => {
|
|
|
906
1074
|
const force = options.force ?? false
|
|
907
1075
|
const includeComments = options.includeComments ?? false
|
|
908
1076
|
const key = pullRequestDiffKey(pullRequest)
|
|
909
|
-
const existing =
|
|
1077
|
+
const existing = registry.get(pullRequestDiffCacheAtom)[key]
|
|
910
1078
|
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
911
1079
|
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
912
1080
|
|
|
913
1081
|
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
914
|
-
|
|
1082
|
+
const atom = pullRequestDiffAtom(pullRequestDiffAtomKey(pullRequest))
|
|
1083
|
+
if (force) registry.refresh(atom)
|
|
1084
|
+
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true }))
|
|
915
1085
|
.then((patch) => {
|
|
916
1086
|
setPullRequestDiffCache((current) => ({
|
|
917
1087
|
...current,
|
|
@@ -985,6 +1155,8 @@ export const App = () => {
|
|
|
985
1155
|
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
986
1156
|
syncDiffScrollState()
|
|
987
1157
|
}
|
|
1158
|
+
const scrollDetailPreviewBy = (y: number) => detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1159
|
+
const scrollDetailPreviewTo = (y: number) => detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
988
1160
|
|
|
989
1161
|
const clearPendingGTimeout = () => {
|
|
990
1162
|
if (pendingGTimeoutRef.current !== null) {
|
|
@@ -1161,14 +1333,14 @@ export const App = () => {
|
|
|
1161
1333
|
}
|
|
1162
1334
|
|
|
1163
1335
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
1164
|
-
void
|
|
1336
|
+
void openInBrowser(pullRequest)
|
|
1165
1337
|
.then(() => flashNotice(`Opened #${pullRequest.number} in browser`))
|
|
1166
1338
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1167
1339
|
}
|
|
1168
1340
|
|
|
1169
1341
|
const copySelectedPullRequestMetadata = () => {
|
|
1170
1342
|
if (!selectedPullRequest) return
|
|
1171
|
-
void
|
|
1343
|
+
void copyToClipboard(pullRequestMetadataText(selectedPullRequest))
|
|
1172
1344
|
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1173
1345
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1174
1346
|
}
|
|
@@ -1291,7 +1463,7 @@ export const App = () => {
|
|
|
1291
1463
|
const openLabelModal = () => {
|
|
1292
1464
|
if (!selectedPullRequest) return
|
|
1293
1465
|
const repository = selectedPullRequest.repository
|
|
1294
|
-
const cachedLabels =
|
|
1466
|
+
const cachedLabels = registry.get(labelCacheAtom)[repository]
|
|
1295
1467
|
if (cachedLabels) {
|
|
1296
1468
|
setLabelModal({
|
|
1297
1469
|
repository,
|
|
@@ -1390,9 +1562,7 @@ export const App = () => {
|
|
|
1390
1562
|
|
|
1391
1563
|
const toggleLabelAtIndex = () => {
|
|
1392
1564
|
if (!selectedPullRequest) return
|
|
1393
|
-
const filtered = labelModal.availableLabels.
|
|
1394
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1395
|
-
)
|
|
1565
|
+
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1396
1566
|
const label = filtered[labelModal.selectedIndex]
|
|
1397
1567
|
if (!label) return
|
|
1398
1568
|
|
|
@@ -1424,7 +1594,214 @@ export const App = () => {
|
|
|
1424
1594
|
}
|
|
1425
1595
|
}
|
|
1426
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
|
+
|
|
1427
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
|
+
|
|
1428
1805
|
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1429
1806
|
if (themeModalActive) {
|
|
1430
1807
|
closeThemeModal(false)
|
|
@@ -1434,7 +1811,7 @@ export const App = () => {
|
|
|
1434
1811
|
closeActiveModal()
|
|
1435
1812
|
return
|
|
1436
1813
|
}
|
|
1437
|
-
|
|
1814
|
+
runCommandById("app.quit")
|
|
1438
1815
|
return
|
|
1439
1816
|
}
|
|
1440
1817
|
|
|
@@ -1464,16 +1841,8 @@ export const App = () => {
|
|
|
1464
1841
|
moveThemeSelection(1)
|
|
1465
1842
|
return
|
|
1466
1843
|
}
|
|
1467
|
-
if (themeModal.filterMode && key
|
|
1468
|
-
editThemeQuery((query) => query
|
|
1469
|
-
return
|
|
1470
|
-
}
|
|
1471
|
-
if (themeModal.filterMode && key.ctrl && key.name === "u") {
|
|
1472
|
-
updateThemeQuery("")
|
|
1473
|
-
return
|
|
1474
|
-
}
|
|
1475
|
-
if (themeModal.filterMode && !key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
1476
|
-
editThemeQuery((query) => query + key.sequence)
|
|
1844
|
+
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
1845
|
+
editThemeQuery((query) => editSingleLineInput(query, key) ?? query)
|
|
1477
1846
|
return
|
|
1478
1847
|
}
|
|
1479
1848
|
return
|
|
@@ -1572,8 +1941,9 @@ export const App = () => {
|
|
|
1572
1941
|
submitDiffComment()
|
|
1573
1942
|
return
|
|
1574
1943
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1944
|
+
const text = printableKeyText(key)
|
|
1945
|
+
if (text) {
|
|
1946
|
+
editComment((state) => insertText(state, text))
|
|
1577
1947
|
return
|
|
1578
1948
|
}
|
|
1579
1949
|
return
|
|
@@ -1646,7 +2016,6 @@ export const App = () => {
|
|
|
1646
2016
|
return
|
|
1647
2017
|
}
|
|
1648
2018
|
|
|
1649
|
-
// Label modal takes priority over everything else
|
|
1650
2019
|
if (labelModalActive) {
|
|
1651
2020
|
if (key.name === "escape") {
|
|
1652
2021
|
closeActiveModal()
|
|
@@ -1664,31 +2033,17 @@ export const App = () => {
|
|
|
1664
2033
|
return
|
|
1665
2034
|
}
|
|
1666
2035
|
if (key.name === "down" || key.name === "j") {
|
|
1667
|
-
const filtered = labelModal.availableLabels.
|
|
1668
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1669
|
-
)
|
|
2036
|
+
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1670
2037
|
setLabelModal((current) => ({
|
|
1671
2038
|
...current,
|
|
1672
2039
|
selectedIndex: Math.min(Math.max(0, filtered.length - 1), current.selectedIndex + 1),
|
|
1673
2040
|
}))
|
|
1674
2041
|
return
|
|
1675
2042
|
}
|
|
1676
|
-
if (key
|
|
2043
|
+
if (isSingleLineInputKey(key)) {
|
|
1677
2044
|
setLabelModal((current) => ({
|
|
1678
2045
|
...current,
|
|
1679
|
-
query: current.query
|
|
1680
|
-
selectedIndex: 0,
|
|
1681
|
-
}))
|
|
1682
|
-
return
|
|
1683
|
-
}
|
|
1684
|
-
if (key.ctrl && key.name === "u") {
|
|
1685
|
-
setLabelModal((current) => ({ ...current, query: "", selectedIndex: 0 }))
|
|
1686
|
-
return
|
|
1687
|
-
}
|
|
1688
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1689
|
-
setLabelModal((current) => ({
|
|
1690
|
-
...current,
|
|
1691
|
-
query: current.query + key.sequence,
|
|
2046
|
+
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
1692
2047
|
selectedIndex: 0,
|
|
1693
2048
|
}))
|
|
1694
2049
|
return
|
|
@@ -1703,7 +2058,7 @@ export const App = () => {
|
|
|
1703
2058
|
return
|
|
1704
2059
|
}
|
|
1705
2060
|
if (key.name === "c") {
|
|
1706
|
-
|
|
2061
|
+
runCommandById("diff.comment-mode")
|
|
1707
2062
|
return
|
|
1708
2063
|
}
|
|
1709
2064
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -1712,7 +2067,7 @@ export const App = () => {
|
|
|
1712
2067
|
return
|
|
1713
2068
|
}
|
|
1714
2069
|
if (key.name === "a") {
|
|
1715
|
-
|
|
2070
|
+
runCommandById("diff.add-comment")
|
|
1716
2071
|
return
|
|
1717
2072
|
}
|
|
1718
2073
|
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
@@ -1748,23 +2103,22 @@ export const App = () => {
|
|
|
1748
2103
|
return
|
|
1749
2104
|
}
|
|
1750
2105
|
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
1751
|
-
|
|
2106
|
+
runCommandById("diff.next-file")
|
|
1752
2107
|
return
|
|
1753
2108
|
}
|
|
1754
2109
|
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
1755
|
-
|
|
2110
|
+
runCommandById("diff.previous-file")
|
|
1756
2111
|
return
|
|
1757
2112
|
}
|
|
1758
2113
|
return
|
|
1759
2114
|
}
|
|
1760
2115
|
|
|
1761
2116
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
1762
|
-
|
|
1763
|
-
setDiffCommentMode(false)
|
|
2117
|
+
runCommandById("diff.close")
|
|
1764
2118
|
return
|
|
1765
2119
|
}
|
|
1766
2120
|
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
1767
|
-
|
|
2121
|
+
runCommandById("diff.comment-mode")
|
|
1768
2122
|
return
|
|
1769
2123
|
}
|
|
1770
2124
|
if (key.name === "home") {
|
|
@@ -1801,67 +2155,64 @@ export const App = () => {
|
|
|
1801
2155
|
return
|
|
1802
2156
|
}
|
|
1803
2157
|
if (key.name === "v") {
|
|
1804
|
-
|
|
2158
|
+
runCommandById("diff.toggle-view")
|
|
1805
2159
|
return
|
|
1806
2160
|
}
|
|
1807
2161
|
if (key.name === "w") {
|
|
1808
|
-
|
|
2162
|
+
runCommandById("diff.toggle-wrap")
|
|
1809
2163
|
return
|
|
1810
2164
|
}
|
|
1811
2165
|
if (key.name === "r" && selectedPullRequest) {
|
|
1812
|
-
|
|
1813
|
-
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
2166
|
+
runCommandById("diff.reload")
|
|
1814
2167
|
return
|
|
1815
2168
|
}
|
|
1816
2169
|
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
1817
|
-
|
|
2170
|
+
runCommandById("diff.next-file")
|
|
1818
2171
|
return
|
|
1819
2172
|
}
|
|
1820
2173
|
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
1821
|
-
|
|
2174
|
+
runCommandById("diff.previous-file")
|
|
1822
2175
|
return
|
|
1823
2176
|
}
|
|
1824
2177
|
if (key.name === "o" && selectedPullRequest) {
|
|
1825
|
-
|
|
2178
|
+
runCommandById("pull.open-browser")
|
|
1826
2179
|
return
|
|
1827
2180
|
}
|
|
1828
2181
|
return
|
|
1829
2182
|
}
|
|
1830
2183
|
|
|
1831
|
-
// Fullscreen detail mode handles its own navigation keys.
|
|
1832
2184
|
if (detailFullView) {
|
|
1833
2185
|
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
1834
2186
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1835
|
-
|
|
1836
|
-
setDetailScrollOffset(0)
|
|
2187
|
+
runCommandById("detail.close")
|
|
1837
2188
|
return
|
|
1838
2189
|
}
|
|
1839
2190
|
if (isThemeKey(key)) {
|
|
1840
|
-
|
|
2191
|
+
runCommandById("theme.open")
|
|
1841
2192
|
return
|
|
1842
2193
|
}
|
|
1843
2194
|
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1844
|
-
|
|
2195
|
+
runCommandById("diff.open")
|
|
1845
2196
|
return
|
|
1846
2197
|
}
|
|
1847
2198
|
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1848
|
-
|
|
2199
|
+
runCommandById("pull.close")
|
|
1849
2200
|
return
|
|
1850
2201
|
}
|
|
1851
2202
|
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1852
|
-
|
|
2203
|
+
runCommandById("pull.labels")
|
|
1853
2204
|
return
|
|
1854
2205
|
}
|
|
1855
2206
|
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1856
|
-
|
|
2207
|
+
runCommandById("pull.merge")
|
|
1857
2208
|
return
|
|
1858
2209
|
}
|
|
1859
2210
|
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1860
|
-
|
|
2211
|
+
runCommandById("pull.toggle-draft")
|
|
1861
2212
|
return
|
|
1862
2213
|
}
|
|
1863
2214
|
if (plainKey && key.name === "r") {
|
|
1864
|
-
|
|
2215
|
+
runCommandById("pull.refresh")
|
|
1865
2216
|
return
|
|
1866
2217
|
}
|
|
1867
2218
|
if (key.name === "home") {
|
|
@@ -1909,11 +2260,11 @@ export const App = () => {
|
|
|
1909
2260
|
return
|
|
1910
2261
|
}
|
|
1911
2262
|
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1912
|
-
|
|
2263
|
+
runCommandById("pull.open-browser")
|
|
1913
2264
|
return
|
|
1914
2265
|
}
|
|
1915
2266
|
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1916
|
-
|
|
2267
|
+
runCommandById("pull.copy-metadata")
|
|
1917
2268
|
return
|
|
1918
2269
|
}
|
|
1919
2270
|
return
|
|
@@ -1925,25 +2276,13 @@ export const App = () => {
|
|
|
1925
2276
|
setFilterMode(false)
|
|
1926
2277
|
return
|
|
1927
2278
|
}
|
|
1928
|
-
if (key.name === "enter") {
|
|
2279
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1929
2280
|
setFilterQuery(filterDraft)
|
|
1930
2281
|
setFilterMode(false)
|
|
1931
2282
|
return
|
|
1932
2283
|
}
|
|
1933
|
-
if (key
|
|
1934
|
-
setFilterDraft(
|
|
1935
|
-
return
|
|
1936
|
-
}
|
|
1937
|
-
if (key.ctrl && key.name === "w") {
|
|
1938
|
-
setFilterDraft((current) => deleteLastWord(current))
|
|
1939
|
-
return
|
|
1940
|
-
}
|
|
1941
|
-
if (key.name === "backspace") {
|
|
1942
|
-
setFilterDraft((current) => current.slice(0, -1))
|
|
1943
|
-
return
|
|
1944
|
-
}
|
|
1945
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
1946
|
-
setFilterDraft((current) => current + key.sequence)
|
|
2284
|
+
if (isSingleLineInputKey(key)) {
|
|
2285
|
+
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
1947
2286
|
return
|
|
1948
2287
|
}
|
|
1949
2288
|
}
|
|
@@ -1954,25 +2293,40 @@ export const App = () => {
|
|
|
1954
2293
|
}
|
|
1955
2294
|
|
|
1956
2295
|
if (isThemeKey(key)) {
|
|
1957
|
-
|
|
2296
|
+
runCommandById("theme.open")
|
|
1958
2297
|
return
|
|
1959
2298
|
}
|
|
1960
2299
|
|
|
1961
2300
|
if (key.name === "/") {
|
|
1962
|
-
|
|
1963
|
-
setFilterMode(true)
|
|
2301
|
+
runCommandById("filter.open")
|
|
1964
2302
|
return
|
|
1965
2303
|
}
|
|
1966
2304
|
if (key.name === "escape" && filterQuery.length > 0) {
|
|
1967
|
-
|
|
1968
|
-
setFilterDraft("")
|
|
1969
|
-
setFilterMode(false)
|
|
2305
|
+
runCommandById("filter.clear")
|
|
1970
2306
|
return
|
|
1971
2307
|
}
|
|
1972
2308
|
if (key.name === "r") {
|
|
1973
|
-
|
|
2309
|
+
runCommandById("pull.refresh")
|
|
1974
2310
|
return
|
|
1975
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
|
+
}
|
|
1976
2330
|
if (
|
|
1977
2331
|
key.name === "[" ||
|
|
1978
2332
|
((key.option || key.meta) && (key.name === "up" || key.name === "k")) ||
|
|
@@ -2023,6 +2377,10 @@ export const App = () => {
|
|
|
2023
2377
|
return
|
|
2024
2378
|
}
|
|
2025
2379
|
if (key.name === "down" || key.name === "j") {
|
|
2380
|
+
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2381
|
+
loadMorePullRequests()
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
2026
2384
|
setSelectedIndex((current) => {
|
|
2027
2385
|
if (visiblePullRequests.length === 0) return 0
|
|
2028
2386
|
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
@@ -2034,36 +2392,35 @@ export const App = () => {
|
|
|
2034
2392
|
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2035
2393
|
)) return
|
|
2036
2394
|
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
2037
|
-
|
|
2038
|
-
setDetailScrollOffset(0)
|
|
2395
|
+
runCommandById("detail.open")
|
|
2039
2396
|
return
|
|
2040
2397
|
}
|
|
2041
2398
|
if (key.name === "d" && selectedPullRequest) {
|
|
2042
|
-
|
|
2399
|
+
runCommandById("diff.open")
|
|
2043
2400
|
return
|
|
2044
2401
|
}
|
|
2045
2402
|
if (key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2046
|
-
|
|
2403
|
+
runCommandById("pull.close")
|
|
2047
2404
|
return
|
|
2048
2405
|
}
|
|
2049
2406
|
if (key.name === "l" && selectedPullRequest) {
|
|
2050
|
-
|
|
2407
|
+
runCommandById("pull.labels")
|
|
2051
2408
|
return
|
|
2052
2409
|
}
|
|
2053
2410
|
if (key.name === "m" || key.name === "M") {
|
|
2054
|
-
if (selectedPullRequest)
|
|
2411
|
+
if (selectedPullRequest) runCommandById("pull.merge")
|
|
2055
2412
|
return
|
|
2056
2413
|
}
|
|
2057
2414
|
if (key.name === "o" && selectedPullRequest) {
|
|
2058
|
-
|
|
2415
|
+
runCommandById("pull.open-browser")
|
|
2059
2416
|
return
|
|
2060
2417
|
}
|
|
2061
2418
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
2062
|
-
|
|
2419
|
+
runCommandById("pull.toggle-draft")
|
|
2063
2420
|
return
|
|
2064
2421
|
}
|
|
2065
2422
|
if (key.name === "y" && selectedPullRequest) {
|
|
2066
|
-
|
|
2423
|
+
runCommandById("pull.copy-metadata")
|
|
2067
2424
|
return
|
|
2068
2425
|
}
|
|
2069
2426
|
})
|
|
@@ -2073,19 +2430,20 @@ export const App = () => {
|
|
|
2073
2430
|
const wideFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2074
2431
|
pullRequest: selectedPullRequest,
|
|
2075
2432
|
contentWidth: fullscreenContentWidth,
|
|
2076
|
-
bodyLines:
|
|
2433
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2077
2434
|
paneWidth: contentWidth,
|
|
2078
2435
|
showChecks: true,
|
|
2079
2436
|
}) > wideBodyHeight
|
|
2080
2437
|
const narrowFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2081
2438
|
pullRequest: selectedPullRequest,
|
|
2082
2439
|
contentWidth: fullscreenContentWidth,
|
|
2083
|
-
bodyLines:
|
|
2440
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2084
2441
|
paneWidth: contentWidth,
|
|
2085
2442
|
}) > wideBodyHeight
|
|
2086
2443
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2087
2444
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2088
|
-
const
|
|
2445
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2446
|
+
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2089
2447
|
|
|
2090
2448
|
const prListProps = {
|
|
2091
2449
|
groups: visibleGroups,
|
|
@@ -2095,6 +2453,9 @@ export const App = () => {
|
|
|
2095
2453
|
filterText: visibleFilterText,
|
|
2096
2454
|
showFilterBar: filterMode || filterQuery.length > 0,
|
|
2097
2455
|
isFilterEditing: filterMode,
|
|
2456
|
+
loadedCount: loadedPullRequestCount,
|
|
2457
|
+
hasMore: hasMorePullRequests,
|
|
2458
|
+
isLoadingMore: isLoadingMorePullRequests,
|
|
2098
2459
|
onSelectPullRequest: selectPullRequestByUrl,
|
|
2099
2460
|
} as const
|
|
2100
2461
|
|
|
@@ -2103,29 +2464,49 @@ export const App = () => {
|
|
|
2103
2464
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
2104
2465
|
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2105
2466
|
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
const
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2117
|
-
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
|
|
2118
2487
|
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2119
2488
|
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2120
2489
|
: "No diff line selected"
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
const
|
|
2126
|
-
const
|
|
2127
|
-
const
|
|
2128
|
-
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
|
|
2129
2510
|
|
|
2130
2511
|
return (
|
|
2131
2512
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -2158,6 +2539,11 @@ export const App = () => {
|
|
|
2158
2539
|
onSelectCommentLine={selectDiffCommentLine}
|
|
2159
2540
|
themeId={themeId}
|
|
2160
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>
|
|
2161
2547
|
) : isWideLayout && detailFullView ? (
|
|
2162
2548
|
<box flexGrow={1} flexDirection="column">
|
|
2163
2549
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
@@ -2166,6 +2552,7 @@ export const App = () => {
|
|
|
2166
2552
|
viewerUsername={username}
|
|
2167
2553
|
contentWidth={fullscreenContentWidth}
|
|
2168
2554
|
bodyLines={fullscreenBodyLines}
|
|
2555
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2169
2556
|
paneWidth={contentWidth}
|
|
2170
2557
|
showChecks
|
|
2171
2558
|
placeholderContent={detailPlaceholderContent}
|
|
@@ -2175,19 +2562,26 @@ export const App = () => {
|
|
|
2175
2562
|
</scrollbox>
|
|
2176
2563
|
</box>
|
|
2177
2564
|
) : isWideLayout ? (
|
|
2178
|
-
|
|
2179
|
-
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column"
|
|
2180
|
-
<scrollbox height={wideBodyHeight} flexGrow={0}>
|
|
2181
|
-
<
|
|
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>
|
|
2182
2571
|
</scrollbox>
|
|
2183
2572
|
</box>
|
|
2184
2573
|
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
2185
2574
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2186
|
-
{selectedPullRequest ? (
|
|
2575
|
+
{isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2187
2576
|
<>
|
|
2188
2577
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2189
|
-
<
|
|
2190
|
-
|
|
2578
|
+
<LoadingPane content={detailLoadingContent} width={rightPaneWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true))} />
|
|
2579
|
+
</>
|
|
2580
|
+
) : selectedPullRequest ? (
|
|
2581
|
+
<>
|
|
2582
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
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} />
|
|
2191
2585
|
</scrollbox>
|
|
2192
2586
|
</>
|
|
2193
2587
|
) : (
|
|
@@ -2203,6 +2597,7 @@ export const App = () => {
|
|
|
2203
2597
|
viewerUsername={username}
|
|
2204
2598
|
contentWidth={fullscreenContentWidth}
|
|
2205
2599
|
bodyLines={fullscreenBodyLines}
|
|
2600
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2206
2601
|
paneWidth={contentWidth}
|
|
2207
2602
|
placeholderContent={detailPlaceholderContent}
|
|
2208
2603
|
loadingIndicator={loadingIndicator}
|
|
@@ -2215,7 +2610,7 @@ export const App = () => {
|
|
|
2215
2610
|
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2216
2611
|
<Divider width={contentWidth} />
|
|
2217
2612
|
<box flexGrow={1} flexDirection="column">
|
|
2218
|
-
<scrollbox flexGrow={1}>
|
|
2613
|
+
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
2219
2614
|
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2220
2615
|
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
2221
2616
|
</box>
|
|
@@ -2310,6 +2705,26 @@ export const App = () => {
|
|
|
2310
2705
|
offsetTop={themeModalTop}
|
|
2311
2706
|
/>
|
|
2312
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}
|
|
2313
2728
|
</box>
|
|
2314
2729
|
)
|
|
2315
2730
|
}
|