@kitlangton/ghui 0.2.1 → 0.3.2
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 +1 -4
- package/bin/ghui.js +6 -1
- package/dist/index.js +9419 -0
- package/package.json +7 -4
- package/src/App.tsx +0 -2604
- package/src/appCommands.ts +0 -384
- package/src/commands.ts +0 -73
- package/src/config.ts +0 -28
- package/src/date.ts +0 -20
- package/src/domain.ts +0 -139
- package/src/errors.ts +0 -10
- package/src/index.tsx +0 -50
- package/src/keyboard/opentuiAdapter.ts +0 -43
- package/src/keymap/all.ts +0 -103
- package/src/keymap/closeModal.ts +0 -13
- package/src/keymap/commandPalette.ts +0 -16
- package/src/keymap/commentModal.ts +0 -73
- package/src/keymap/commentThreadModal.ts +0 -24
- package/src/keymap/detailView.ts +0 -30
- package/src/keymap/diffView.ts +0 -91
- package/src/keymap/filterMode.ts +0 -13
- package/src/keymap/helpers.ts +0 -24
- package/src/keymap/labelModal.ts +0 -16
- package/src/keymap/listNav.ts +0 -119
- package/src/keymap/mergeModal.ts +0 -23
- package/src/keymap/openRepositoryModal.ts +0 -13
- package/src/keymap/themeModal.ts +0 -49
- package/src/mergeActions.ts +0 -88
- package/src/observability.ts +0 -46
- package/src/pullRequestCache.ts +0 -19
- package/src/pullRequestViews.ts +0 -45
- package/src/services/BrowserOpener.ts +0 -22
- package/src/services/Clipboard.ts +0 -46
- package/src/services/CommandRunner.ts +0 -91
- package/src/services/GitHubService.ts +0 -741
- package/src/services/MockGitHubService.ts +0 -181
- package/src/themeStore.ts +0 -60
- package/src/ui/CommandPalette.tsx +0 -216
- package/src/ui/DetailsPane.tsx +0 -656
- package/src/ui/FooterHints.tsx +0 -90
- package/src/ui/LoadingLogo.tsx +0 -75
- package/src/ui/PullRequestDiffPane.tsx +0 -248
- package/src/ui/PullRequestList.tsx +0 -210
- package/src/ui/colors.ts +0 -814
- package/src/ui/commentEditor.ts +0 -126
- package/src/ui/comments.tsx +0 -143
- package/src/ui/diff.ts +0 -650
- package/src/ui/diffStats.tsx +0 -25
- package/src/ui/modals.tsx +0 -614
- package/src/ui/primitives.tsx +0 -205
- package/src/ui/pullRequests.ts +0 -106
- package/src/ui/singleLineInput.ts +0 -26
- package/src/ui/spinner.ts +0 -1
package/src/App.tsx
DELETED
|
@@ -1,2604 +0,0 @@
|
|
|
1
|
-
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
-
import { useKeymap } from "@ghui/keymap/react"
|
|
4
|
-
import { appKeymap, type AppCtx } from "./keymap/all.js"
|
|
5
|
-
import { useOpenTuiSubscribe } from "./keyboard/opentuiAdapter.js"
|
|
6
|
-
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
7
|
-
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
8
|
-
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
9
|
-
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
10
|
-
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
11
|
-
import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
12
|
-
import { buildAppCommands } from "./appCommands.js"
|
|
13
|
-
import type { AppCommand } from "./commands.js"
|
|
14
|
-
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
15
|
-
import { config } from "./config.js"
|
|
16
|
-
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestConversationItem, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
17
|
-
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
18
|
-
import { errorMessage } from "./errors.js"
|
|
19
|
-
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
20
|
-
import { Observability } from "./observability.js"
|
|
21
|
-
import { mergeCachedDetails } from "./pullRequestCache.js"
|
|
22
|
-
import { activePullRequestViews, initialPullRequestView, nextView, parseRepositoryInput, type PullRequestView, viewCacheKey, viewEquals, viewLabel, viewMode, viewRepository } from "./pullRequestViews.js"
|
|
23
|
-
import { BrowserOpener } from "./services/BrowserOpener.js"
|
|
24
|
-
import { Clipboard } from "./services/Clipboard.js"
|
|
25
|
-
import { CommandRunner } from "./services/CommandRunner.js"
|
|
26
|
-
import { GitHubService } from "./services/GitHubService.js"
|
|
27
|
-
import { loadStoredDiffWhitespaceMode, loadStoredThemeId, saveStoredDiffWhitespaceMode, saveStoredThemeId } from "./themeStore.js"
|
|
28
|
-
import { colors, filterThemeDefinitions, mixHex, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
29
|
-
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
30
|
-
import { buildStackedDiffFiles, diffAnchorOnSide, diffCommentAnchorLabel, diffCommentLineLabel, diffCommentLocationKey, diffCommentSideLabel, getStackedDiffCommentAnchors, minimizeWhitespaceDiffFiles, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileIndexAtLine, type DiffCommentAnchor, type DiffCommentKind, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type StackedDiffCommentAnchor, verticalDiffAnchor } from "./ui/diff.js"
|
|
31
|
-
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getScrollableDetailBodyHeight, LoadingPane, type DetailConversationStatus, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
32
|
-
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
33
|
-
import { LoadingLogoPane } from "./ui/LoadingLogo.js"
|
|
34
|
-
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
35
|
-
import { CommandPalette } from "./ui/CommandPalette.js"
|
|
36
|
-
import { CloseModal, CommentModal, CommentThreadModal, filterLabels, initialCloseModalState, initialCommandPaletteState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialOpenRepositoryModalState, initialThemeModalState, LabelModal, MergeModal, Modal, OpenRepositoryModal, ThemeModal, type CloseModalState, type CommandPaletteState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type OpenRepositoryModalState, type ThemeModalState } from "./ui/modals.js"
|
|
37
|
-
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
38
|
-
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
39
|
-
import { buildPullRequestListRows, pullRequestListRowIndex, PullRequestList } from "./ui/PullRequestList.js"
|
|
40
|
-
import { editSingleLineInput, isSingleLineInputKey, printableKeyText, singleLineText } from "./ui/singleLineInput.js"
|
|
41
|
-
import { SPINNER_FRAMES } from "./ui/spinner.js"
|
|
42
|
-
|
|
43
|
-
const parseOptionalPositiveInt = (value: string | undefined, fallback: number | null) => {
|
|
44
|
-
if (value === undefined) return fallback
|
|
45
|
-
const parsed = Number.parseInt(value, 10)
|
|
46
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const mockPrCount = parseOptionalPositiveInt(process.env.GHUI_MOCK_PR_COUNT, null)
|
|
50
|
-
const pullRequestPageSize = Math.min(100, parseOptionalPositiveInt(process.env.GHUI_PR_PAGE_SIZE, config.prPageSize) ?? config.prPageSize)
|
|
51
|
-
const githubServiceLayer = mockPrCount !== null
|
|
52
|
-
? (await import("./services/MockGitHubService.js")).MockGitHubService.layer({ prCount: mockPrCount, repoCount: parseOptionalPositiveInt(process.env.GHUI_MOCK_REPO_COUNT, 4) ?? 4 })
|
|
53
|
-
: GitHubService.layerNoDeps
|
|
54
|
-
|
|
55
|
-
const githubRuntime = Atom.runtime(
|
|
56
|
-
Layer.mergeAll(githubServiceLayer, Clipboard.layerNoDeps, BrowserOpener.layerNoDeps).pipe(
|
|
57
|
-
Layer.provide(CommandRunner.layer),
|
|
58
|
-
Layer.provideMerge(Observability.layer),
|
|
59
|
-
),
|
|
60
|
-
)
|
|
61
|
-
const [initialThemeId, initialDiffWhitespaceMode] = await Promise.all([
|
|
62
|
-
Effect.runPromise(loadStoredThemeId),
|
|
63
|
-
Effect.runPromise(loadStoredDiffWhitespaceMode),
|
|
64
|
-
])
|
|
65
|
-
|
|
66
|
-
interface PullRequestLoad {
|
|
67
|
-
readonly view: PullRequestView
|
|
68
|
-
readonly data: readonly PullRequestItem[]
|
|
69
|
-
readonly fetchedAt: Date | null
|
|
70
|
-
readonly endCursor: string | null
|
|
71
|
-
readonly hasNextPage: boolean
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface DetailPlaceholderInput {
|
|
75
|
-
readonly status: LoadStatus
|
|
76
|
-
readonly retryProgress: RetryProgress
|
|
77
|
-
readonly loadingIndicator: string
|
|
78
|
-
readonly visibleCount: number
|
|
79
|
-
readonly filterText: string
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type DiffLineColorConfig = {
|
|
83
|
-
readonly gutter: string
|
|
84
|
-
readonly content: string
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type DiffSideRenderable = {
|
|
88
|
-
readonly setLineColor: (line: number, color: DiffLineColorConfig) => void
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type DiffRenderableRuntimeSides = {
|
|
92
|
-
readonly leftSide?: DiffSideRenderable
|
|
93
|
-
readonly rightSide?: DiffSideRenderable
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface AppliedDiffLineColor {
|
|
97
|
-
readonly anchor: StackedDiffCommentAnchor
|
|
98
|
-
readonly view: DiffView
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface AppliedDiffLineColorState {
|
|
102
|
-
readonly contextKey: string | null
|
|
103
|
-
readonly entries: readonly AppliedDiffLineColor[]
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface DiffCommentRangeSelection {
|
|
107
|
-
readonly start: StackedDiffCommentAnchor
|
|
108
|
-
readonly end: StackedDiffCommentAnchor
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface DetailHydration {
|
|
112
|
-
readonly token: symbol
|
|
113
|
-
notifyError: boolean
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const PR_FETCH_RETRIES = 6
|
|
117
|
-
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
118
|
-
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
119
|
-
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
120
|
-
const DIFF_STICKY_HEADER_LINES = 2
|
|
121
|
-
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
122
|
-
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
123
|
-
const LOAD_MORE_SCROLL_THRESHOLD = 3
|
|
124
|
-
const DETAIL_PREFETCH_BEHIND = 1
|
|
125
|
-
const DETAIL_PREFETCH_AHEAD = 3
|
|
126
|
-
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
127
|
-
const DETAIL_PREFETCH_DELAY_MS = 120
|
|
128
|
-
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
|
|
129
|
-
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
|
|
130
|
-
const mergedIncoming = mergeCachedDetails(incoming, existing)
|
|
131
|
-
return [...existing, ...mergedIncoming.filter((pullRequest) => !seen.has(pullRequest.url))]
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
135
|
-
const activeViewAtom = Atom.make<PullRequestView>(initialPullRequestView(config.repository)).pipe(Atom.keepAlive)
|
|
136
|
-
const queueLoadCacheAtom = Atom.make<Partial<Record<string, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
137
|
-
const queueSelectionAtom = Atom.make<Partial<Record<string, number>>>({}).pipe(Atom.keepAlive)
|
|
138
|
-
const trimQueueLoadCache = (cache: Partial<Record<string, PullRequestLoad>>) => {
|
|
139
|
-
const repositoryKeys = Object.keys(cache).filter((key) => key.startsWith("repository:"))
|
|
140
|
-
if (repositoryKeys.length <= MAX_REPOSITORY_CACHE_ENTRIES) return cache
|
|
141
|
-
const remove = new Set(repositoryKeys.slice(0, repositoryKeys.length - MAX_REPOSITORY_CACHE_ENTRIES))
|
|
142
|
-
return Object.fromEntries(Object.entries(cache).filter(([key]) => !remove.has(key))) as Partial<Record<string, PullRequestLoad>>
|
|
143
|
-
}
|
|
144
|
-
const pullRequestsAtom = githubRuntime.atom(
|
|
145
|
-
GitHubService.use((github) =>
|
|
146
|
-
Effect.gen(function*() {
|
|
147
|
-
const view = yield* Atom.get(activeViewAtom)
|
|
148
|
-
const queueMode = viewMode(view)
|
|
149
|
-
const repository = viewRepository(view)
|
|
150
|
-
const cacheKey = viewCacheKey(view)
|
|
151
|
-
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
152
|
-
const page = yield* github.listOpenPullRequestPage({
|
|
153
|
-
mode: queueMode,
|
|
154
|
-
repository,
|
|
155
|
-
cursor: null,
|
|
156
|
-
pageSize: Math.min(pullRequestPageSize, config.prFetchLimit),
|
|
157
|
-
}).pipe(
|
|
158
|
-
Effect.tapError(() =>
|
|
159
|
-
Atom.update(retryProgressAtom, (current) => RetryProgress.Retrying({
|
|
160
|
-
attempt: Math.min(RetryProgress.$match(current, { Idle: () => 0, Retrying: ({ attempt }) => attempt }) + 1, PR_FETCH_RETRIES),
|
|
161
|
-
max: PR_FETCH_RETRIES,
|
|
162
|
-
}))
|
|
163
|
-
),
|
|
164
|
-
Effect.retry({ times: PR_FETCH_RETRIES, schedule: Schedule.exponential("300 millis", 2) }),
|
|
165
|
-
Effect.tapError(() => Atom.set(retryProgressAtom, initialRetryProgress)),
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
169
|
-
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
170
|
-
const existingLoad = cache[cacheKey]
|
|
171
|
-
const data = mergeCachedDetails(page.items, existingLoad?.data)
|
|
172
|
-
const load = {
|
|
173
|
-
view,
|
|
174
|
-
data,
|
|
175
|
-
fetchedAt: new Date(),
|
|
176
|
-
endCursor: page.endCursor,
|
|
177
|
-
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
178
|
-
} satisfies PullRequestLoad
|
|
179
|
-
const nextCache = { ...cache }
|
|
180
|
-
delete nextCache[cacheKey]
|
|
181
|
-
nextCache[cacheKey] = load
|
|
182
|
-
yield* Atom.set(queueLoadCacheAtom, trimQueueLoadCache(nextCache))
|
|
183
|
-
return load
|
|
184
|
-
})
|
|
185
|
-
),
|
|
186
|
-
).pipe(Atom.keepAlive)
|
|
187
|
-
const selectedIndexAtom = Atom.make(0)
|
|
188
|
-
const noticeAtom = Atom.make<string | null>(null)
|
|
189
|
-
const filterQueryAtom = Atom.make("")
|
|
190
|
-
const filterDraftAtom = Atom.make("")
|
|
191
|
-
const filterModeAtom = Atom.make(false)
|
|
192
|
-
const detailFullViewAtom = Atom.make(false)
|
|
193
|
-
const detailScrollOffsetAtom = Atom.make(0)
|
|
194
|
-
const diffFullViewAtom = Atom.make(false)
|
|
195
|
-
const diffFileIndexAtom = Atom.make(0)
|
|
196
|
-
const diffScrollTopAtom = Atom.make(0)
|
|
197
|
-
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
198
|
-
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
199
|
-
const diffWhitespaceModeAtom = Atom.make<DiffWhitespaceMode>(initialDiffWhitespaceMode)
|
|
200
|
-
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
201
|
-
const diffPreferredSideAtom = Atom.make<DiffCommentSide | null>(null)
|
|
202
|
-
const diffCommentRangeStartIndexAtom = Atom.make<number | null>(null)
|
|
203
|
-
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
204
|
-
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
205
|
-
const pullRequestConversationAtom = Atom.make<Record<string, readonly PullRequestConversationItem[]>>({}).pipe(Atom.keepAlive)
|
|
206
|
-
const pullRequestConversationLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
207
|
-
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
208
|
-
|
|
209
|
-
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
210
|
-
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
211
|
-
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
212
|
-
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
213
|
-
const recentlyCompletedPullRequestsAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
214
|
-
const usernameAtom = githubRuntime.atom(
|
|
215
|
-
config.author === "@me"
|
|
216
|
-
? GitHubService.use((github) => github.getAuthenticatedUser())
|
|
217
|
-
: Effect.succeed(config.author.replace(/^@/, "")),
|
|
218
|
-
).pipe(Atom.keepAlive)
|
|
219
|
-
|
|
220
|
-
const pullRequestLoadAtom = Atom.make((get) => {
|
|
221
|
-
const view = get(activeViewAtom)
|
|
222
|
-
const cacheKey = viewCacheKey(view)
|
|
223
|
-
const cache = get(queueLoadCacheAtom)
|
|
224
|
-
const result = get(pullRequestsAtom)
|
|
225
|
-
const resolved = AsyncResult.getOrElse(result, () => null)
|
|
226
|
-
return cache[cacheKey] ?? (resolved && viewCacheKey(resolved.view) === cacheKey ? resolved : null)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const isLoadingQueueModeAtom = Atom.make((get) => {
|
|
230
|
-
const cacheKey = viewCacheKey(get(activeViewAtom))
|
|
231
|
-
const resolved = AsyncResult.getOrElse(get(pullRequestsAtom), () => null)
|
|
232
|
-
return resolved !== null && viewCacheKey(resolved.view) !== cacheKey
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
const pullRequestStatusAtom = Atom.make((get): LoadStatus => {
|
|
236
|
-
const result = get(pullRequestsAtom)
|
|
237
|
-
const load = get(pullRequestLoadAtom)
|
|
238
|
-
const isLoadingQueue = get(isLoadingQueueModeAtom)
|
|
239
|
-
if ((result.waiting || isLoadingQueue) && load === null) return "loading"
|
|
240
|
-
return AsyncResult.isFailure(result) ? "error" : "ready"
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
const displayedPullRequestsAtom = Atom.make((get) => {
|
|
244
|
-
const load = get(pullRequestLoadAtom)
|
|
245
|
-
const overrides = get(pullRequestOverridesAtom)
|
|
246
|
-
const recentlyCompleted = get(recentlyCompletedPullRequestsAtom)
|
|
247
|
-
const source = load?.data ?? []
|
|
248
|
-
const seenUrls = new Set<string>()
|
|
249
|
-
const open = source.map((pullRequest) => {
|
|
250
|
-
seenUrls.add(pullRequest.url)
|
|
251
|
-
return recentlyCompleted[pullRequest.url] ?? overrides[pullRequest.url] ?? pullRequest
|
|
252
|
-
})
|
|
253
|
-
return [
|
|
254
|
-
...open,
|
|
255
|
-
...Object.values(recentlyCompleted).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
256
|
-
]
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
const effectiveFilterQueryAtom = Atom.make((get) =>
|
|
260
|
-
(get(filterModeAtom) ? get(filterDraftAtom) : get(filterQueryAtom)).trim().toLowerCase(),
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
const filteredPullRequestsAtom = Atom.make((get) => {
|
|
264
|
-
const pullRequests = get(displayedPullRequestsAtom)
|
|
265
|
-
const query = get(effectiveFilterQueryAtom)
|
|
266
|
-
if (query.length === 0) return pullRequests
|
|
267
|
-
return pullRequests.flatMap((pullRequest) => {
|
|
268
|
-
const score = pullRequestFilterScore(pullRequest, query)
|
|
269
|
-
return score === null ? [] : [{ pullRequest, score }]
|
|
270
|
-
}).sort((left, right) =>
|
|
271
|
-
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
272
|
-
).map(({ pullRequest }) => pullRequest)
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
const visibleRepoOrderAtom = Atom.make((get) => {
|
|
276
|
-
const query = get(effectiveFilterQueryAtom)
|
|
277
|
-
if (query.length === 0) return [] as readonly string[]
|
|
278
|
-
return [...new Set(get(filteredPullRequestsAtom).map((pullRequest) => pullRequest.repository))]
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
const visibleGroupsAtom = Atom.make((get) =>
|
|
282
|
-
groupBy(get(filteredPullRequestsAtom), (pullRequest) => pullRequest.repository, get(visibleRepoOrderAtom)),
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
const visiblePullRequestsAtom = Atom.make((get) => get(visibleGroupsAtom).flatMap(([, pullRequests]) => pullRequests))
|
|
286
|
-
|
|
287
|
-
const groupStartsAtom = Atom.make((get) => {
|
|
288
|
-
const groups = get(visibleGroupsAtom)
|
|
289
|
-
const starts: number[] = []
|
|
290
|
-
for (let index = 0; index < groups.length; index++) {
|
|
291
|
-
if (index === 0) starts.push(0)
|
|
292
|
-
else starts.push(starts[index - 1]! + groups[index - 1]![1].length)
|
|
293
|
-
}
|
|
294
|
-
return starts
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
const selectedPullRequestAtom = Atom.make((get) => {
|
|
298
|
-
const pullRequests = get(visiblePullRequestsAtom)
|
|
299
|
-
const index = get(selectedIndexAtom)
|
|
300
|
-
return pullRequests[index] ?? null
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
const selectedDiffKeyAtom = Atom.make((get) => {
|
|
304
|
-
const pullRequest = get(selectedPullRequestAtom)
|
|
305
|
-
return pullRequest ? pullRequestDiffKey(pullRequest) : null
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
const selectedDiffStateAtom = Atom.make((get) => {
|
|
309
|
-
const key = get(selectedDiffKeyAtom)
|
|
310
|
-
if (!key) return undefined
|
|
311
|
-
return get(pullRequestDiffCacheAtom)[key]
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
315
|
-
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
316
|
-
)
|
|
317
|
-
const listOpenPullRequestPageAtom = githubRuntime.fn<ListPullRequestPageInput>()((input) =>
|
|
318
|
-
GitHubService.use((github) => github.listOpenPullRequestPage(input))
|
|
319
|
-
)
|
|
320
|
-
const pullRequestDetailsAtom = Atom.family((key: string) => {
|
|
321
|
-
const { repository, number } = parsePullRequestDetailAtomKey(key)
|
|
322
|
-
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDetails(repository, number)))
|
|
323
|
-
})
|
|
324
|
-
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
325
|
-
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
326
|
-
)
|
|
327
|
-
const removePullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
328
|
-
GitHubService.use((github) => github.removePullRequestLabel(input.repository, input.number, input.label))
|
|
329
|
-
)
|
|
330
|
-
const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly isDraft: boolean }>()((input) =>
|
|
331
|
-
GitHubService.use((github) => github.toggleDraftStatus(input.repository, input.number, input.isDraft))
|
|
332
|
-
)
|
|
333
|
-
const pullRequestDiffAtom = Atom.family((key: string) => {
|
|
334
|
-
const { repository, number } = parsePullRequestDiffAtomKey(key)
|
|
335
|
-
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDiff(repository, number)))
|
|
336
|
-
})
|
|
337
|
-
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
338
|
-
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
339
|
-
)
|
|
340
|
-
const listPullRequestConversationAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
341
|
-
GitHubService.use((github) => github.listPullRequestConversation(input.repository, input.number))
|
|
342
|
-
)
|
|
343
|
-
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
344
|
-
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
345
|
-
)
|
|
346
|
-
const mergePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly action: PullRequestMergeAction }>()((input) =>
|
|
347
|
-
GitHubService.use((github) => github.mergePullRequest(input.repository, input.number, input.action))
|
|
348
|
-
)
|
|
349
|
-
const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
350
|
-
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
351
|
-
)
|
|
352
|
-
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
353
|
-
const copyToClipboardAtom = githubRuntime.fn<string>()((text) => Clipboard.use((clipboard) => clipboard.copy(text)))
|
|
354
|
-
const openInBrowserAtom = githubRuntime.fn<PullRequestItem>()((pullRequest) => BrowserOpener.use((browser) => browser.openPullRequest(pullRequest)))
|
|
355
|
-
|
|
356
|
-
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const pasteText = (event: PasteEvent) => new TextDecoder().decode(event.bytes)
|
|
360
|
-
|
|
361
|
-
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
362
|
-
const normalized = query.trim().toLowerCase()
|
|
363
|
-
if (normalized.length === 0) return 0
|
|
364
|
-
const fields = [
|
|
365
|
-
pullRequest.title.toLowerCase(),
|
|
366
|
-
pullRequest.repository.toLowerCase(),
|
|
367
|
-
String(pullRequest.number),
|
|
368
|
-
]
|
|
369
|
-
const scores = fields.flatMap((field, index) => {
|
|
370
|
-
const matchIndex = field.indexOf(normalized)
|
|
371
|
-
return matchIndex >= 0 ? [index * 1000 + matchIndex] : []
|
|
372
|
-
})
|
|
373
|
-
return scores.length > 0 ? Math.min(...scores) : null
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const pullRequestMetadataText = (pullRequest: PullRequestItem) => {
|
|
377
|
-
const lines = [
|
|
378
|
-
pullRequest.title,
|
|
379
|
-
`${pullRequest.repository} #${pullRequest.number}`,
|
|
380
|
-
pullRequest.url,
|
|
381
|
-
]
|
|
382
|
-
const review = reviewLabel(pullRequest)
|
|
383
|
-
if (review) lines.push(`review: ${review}`)
|
|
384
|
-
if (pullRequest.checkSummary) lines.push(pullRequest.checkSummary)
|
|
385
|
-
return lines.join("\n")
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const pullRequestDetailKey = (pullRequest: PullRequestItem) => `${pullRequest.url}:${pullRequest.headRefOid}`
|
|
389
|
-
const pullRequestRevisionAtomKey = (pullRequest: PullRequestItem) => `${pullRequest.repository}\u0000${pullRequest.number}\u0000${pullRequest.headRefOid}`
|
|
390
|
-
const parsePullRequestRevisionAtomKey = (key: string, label: string) => {
|
|
391
|
-
const [repository, number] = key.split("\u0000")
|
|
392
|
-
if (!repository || !number) throw new Error(`Invalid pull request ${label} key: ${key}`)
|
|
393
|
-
return { repository, number: Number.parseInt(number, 10) }
|
|
394
|
-
}
|
|
395
|
-
const pullRequestDetailAtomKey = pullRequestRevisionAtomKey
|
|
396
|
-
const pullRequestDiffAtomKey = pullRequestRevisionAtomKey
|
|
397
|
-
const parsePullRequestDetailAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "detail")
|
|
398
|
-
const parsePullRequestDiffAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "diff")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const diffCommentThreadMapKey = (diffKey: string, location: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
403
|
-
`${diffKey}:${diffCommentLocationKey(location)}`
|
|
404
|
-
|
|
405
|
-
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
406
|
-
diffCommentThreadMapKey(pullRequestDiffKey(pullRequest), comment)
|
|
407
|
-
|
|
408
|
-
const groupDiffCommentThreads = (pullRequest: PullRequestItem, comments: readonly PullRequestReviewComment[]) => {
|
|
409
|
-
const threads: Record<string, PullRequestReviewComment[]> = {}
|
|
410
|
-
for (const comment of comments) {
|
|
411
|
-
const key = diffCommentThreadKey(pullRequest, comment)
|
|
412
|
-
const thread = threads[key]
|
|
413
|
-
if (thread) thread.push(comment)
|
|
414
|
-
else threads[key] = [comment]
|
|
415
|
-
}
|
|
416
|
-
return threads
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const isLocalDiffComment = (comment: PullRequestReviewComment) => comment.id.startsWith("local:")
|
|
420
|
-
|
|
421
|
-
const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig => {
|
|
422
|
-
if (anchor.kind === "addition") {
|
|
423
|
-
return { gutter: colors.diff.addedLineNumberBg, content: colors.diff.addedBg }
|
|
424
|
-
}
|
|
425
|
-
if (anchor.kind === "deletion") {
|
|
426
|
-
return { gutter: colors.diff.removedLineNumberBg, content: colors.diff.removedBg }
|
|
427
|
-
}
|
|
428
|
-
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const selectedDiffCommentAccentByKind = {
|
|
432
|
-
addition: () => colors.status.passing,
|
|
433
|
-
deletion: () => colors.status.failing,
|
|
434
|
-
context: () => colors.muted,
|
|
435
|
-
} satisfies Record<DiffCommentKind, () => string>
|
|
436
|
-
|
|
437
|
-
const selectedDiffCommentAccent = (kind: DiffCommentKind) => selectedDiffCommentAccentByKind[kind]()
|
|
438
|
-
|
|
439
|
-
const mixDiffLineContentColor = (base: string, accent: string, amount: number) =>
|
|
440
|
-
mixHex(base === "transparent" ? colors.background : base, accent, amount)
|
|
441
|
-
|
|
442
|
-
const diffCommentLineColor = (anchor: DiffCommentAnchor, kind: "selected" | "range" | "thread"): DiffLineColorConfig => {
|
|
443
|
-
const original = originalDiffLineColor(anchor)
|
|
444
|
-
const accent = kind === "thread" ? colors.status.pending : selectedDiffCommentAccent(anchor.kind)
|
|
445
|
-
if (kind === "thread") return { ...original, gutter: mixHex(original.gutter, accent, 0.45) }
|
|
446
|
-
return {
|
|
447
|
-
gutter: mixHex(original.gutter, accent, kind === "selected" ? 0.68 : 0.42),
|
|
448
|
-
content: mixDiffLineContentColor(original.content, accent, kind === "selected" ? 0.2 : 0.1),
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const sameDiffCommentTarget = (left: DiffCommentAnchor, right: DiffCommentAnchor) =>
|
|
453
|
-
left.path === right.path && left.side === right.side
|
|
454
|
-
|
|
455
|
-
const diffCommentRangeSelection = (start: StackedDiffCommentAnchor | null, end: StackedDiffCommentAnchor | null): DiffCommentRangeSelection | null => {
|
|
456
|
-
if (!start || !end || !sameDiffCommentTarget(start, end)) return null
|
|
457
|
-
return start.line <= end.line ? { start, end } : { start: end, end: start }
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const diffCommentRangeContains = (range: DiffCommentRangeSelection, anchor: StackedDiffCommentAnchor) =>
|
|
461
|
-
sameDiffCommentTarget(range.start, anchor) && anchor.line >= range.start.line && anchor.line <= range.end.line
|
|
462
|
-
|
|
463
|
-
const diffCommentRangeLabel = (range: DiffCommentRangeSelection) =>
|
|
464
|
-
range.start.line === range.end.line
|
|
465
|
-
? diffCommentAnchorLabel(range.end)
|
|
466
|
-
: `${diffCommentSideLabel(range.end)} ${diffCommentLineLabel(range.start)}-${diffCommentLineLabel(range.end)}`
|
|
467
|
-
|
|
468
|
-
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
469
|
-
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
470
|
-
if (view === "split") {
|
|
471
|
-
const target = anchor.side === "LEFT" ? withSides.leftSide : withSides.rightSide
|
|
472
|
-
return target ? [target] : []
|
|
473
|
-
}
|
|
474
|
-
return withSides.leftSide ? [withSides.leftSide] : []
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const setDiffCommentLineColor = (diff: DiffRenderable, entry: AppliedDiffLineColor, color: DiffLineColorConfig) => {
|
|
478
|
-
for (const target of diffSideTargets(diff, entry.anchor, entry.view)) {
|
|
479
|
-
target.setLineColor(entry.anchor.localRenderLine, color)
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const getDetailPlaceholderContent = ({
|
|
484
|
-
status,
|
|
485
|
-
retryProgress,
|
|
486
|
-
loadingIndicator,
|
|
487
|
-
visibleCount,
|
|
488
|
-
filterText,
|
|
489
|
-
}: DetailPlaceholderInput): DetailPlaceholderContent => {
|
|
490
|
-
if (status === "loading") {
|
|
491
|
-
return {
|
|
492
|
-
title: `${loadingIndicator} Loading pull requests`,
|
|
493
|
-
hint: retryProgress._tag === "Retrying" ? `Retry ${retryProgress.attempt}/${retryProgress.max}` : "Fetching latest open PRs",
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (status === "error") {
|
|
498
|
-
return {
|
|
499
|
-
title: "Could not load pull requests",
|
|
500
|
-
hint: "Press r to retry",
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (visibleCount === 0 && filterText.length > 0) {
|
|
505
|
-
return {
|
|
506
|
-
title: "No matching pull requests",
|
|
507
|
-
hint: "Press esc to clear the filter",
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (visibleCount === 0) {
|
|
512
|
-
return {
|
|
513
|
-
title: "No open pull requests",
|
|
514
|
-
hint: "Press r to refresh",
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
title: "Select a pull request",
|
|
520
|
-
hint: "Use up/down to move",
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export const App = () => {
|
|
525
|
-
const renderer = useRenderer()
|
|
526
|
-
const { width, height } = useTerminalDimensions()
|
|
527
|
-
const registry = useContext(RegistryContext)
|
|
528
|
-
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
529
|
-
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
530
|
-
const [activeView, setActiveView] = useAtom(activeViewAtom)
|
|
531
|
-
const setQueueLoadCache = useAtomSet(queueLoadCacheAtom)
|
|
532
|
-
const setQueueSelection = useAtomSet(queueSelectionAtom)
|
|
533
|
-
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
534
|
-
const [notice, setNotice] = useAtom(noticeAtom)
|
|
535
|
-
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
536
|
-
const [filterDraft, setFilterDraft] = useAtom(filterDraftAtom)
|
|
537
|
-
const [filterMode, setFilterMode] = useAtom(filterModeAtom)
|
|
538
|
-
const [detailFullView, setDetailFullView] = useAtom(detailFullViewAtom)
|
|
539
|
-
const setDetailScrollOffset = useAtomSet(detailScrollOffsetAtom)
|
|
540
|
-
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
541
|
-
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
542
|
-
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
543
|
-
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
544
|
-
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
545
|
-
const [diffWhitespaceMode, setDiffWhitespaceMode] = useAtom(diffWhitespaceModeAtom)
|
|
546
|
-
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
547
|
-
const [diffPreferredSide, setDiffPreferredSide] = useAtom(diffPreferredSideAtom)
|
|
548
|
-
const [diffCommentRangeStartIndex, setDiffCommentRangeStartIndex] = useAtom(diffCommentRangeStartIndexAtom)
|
|
549
|
-
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
550
|
-
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
551
|
-
const setPullRequestConversation = useAtomSet(pullRequestConversationAtom)
|
|
552
|
-
const setPullRequestConversationLoaded = useAtomSet(pullRequestConversationLoadedAtom)
|
|
553
|
-
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
554
|
-
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
555
|
-
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
556
|
-
const closeActiveModal = () => setActiveModal(initialModal)
|
|
557
|
-
const labelModalActive = Modal.$is("Label")(activeModal)
|
|
558
|
-
const closeModalActive = Modal.$is("Close")(activeModal)
|
|
559
|
-
const mergeModalActive = Modal.$is("Merge")(activeModal)
|
|
560
|
-
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
561
|
-
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
562
|
-
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
563
|
-
const commandPaletteActive = Modal.$is("CommandPalette")(activeModal)
|
|
564
|
-
const openRepositoryModalActive = Modal.$is("OpenRepository")(activeModal)
|
|
565
|
-
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
566
|
-
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
567
|
-
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
568
|
-
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
569
|
-
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
570
|
-
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
571
|
-
const commandPalette: CommandPaletteState = commandPaletteActive ? activeModal : initialCommandPaletteState
|
|
572
|
-
const openRepositoryModal: OpenRepositoryModalState = openRepositoryModalActive ? activeModal : initialOpenRepositoryModalState
|
|
573
|
-
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
574
|
-
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
575
|
-
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
576
|
-
if (typeof next === "function") {
|
|
577
|
-
const updater = next as (prev: ModalState<Tag>) => ModalState<Tag>
|
|
578
|
-
if (current._tag !== tag) return current
|
|
579
|
-
return ctor(updater(current as unknown as ModalState<Tag>))
|
|
580
|
-
}
|
|
581
|
-
return ctor(next)
|
|
582
|
-
})
|
|
583
|
-
const setLabelModal = makeModalSetter("Label")
|
|
584
|
-
const setCloseModal = makeModalSetter("Close")
|
|
585
|
-
const setMergeModal = makeModalSetter("Merge")
|
|
586
|
-
const setCommentModal = makeModalSetter("Comment")
|
|
587
|
-
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
588
|
-
const setThemeModal = makeModalSetter("Theme")
|
|
589
|
-
const setCommandPalette = makeModalSetter("CommandPalette")
|
|
590
|
-
const setOpenRepositoryModal = makeModalSetter("OpenRepository")
|
|
591
|
-
setActiveTheme(themeId)
|
|
592
|
-
const themeIdRef = useRef(themeId)
|
|
593
|
-
const themeModalRef = useRef(themeModal)
|
|
594
|
-
themeIdRef.current = themeId
|
|
595
|
-
themeModalRef.current = themeModal
|
|
596
|
-
const setLabelCache = useAtomSet(labelCacheAtom)
|
|
597
|
-
const setPullRequestOverrides = useAtomSet(pullRequestOverridesAtom)
|
|
598
|
-
const setRecentlyCompletedPullRequests = useAtomSet(recentlyCompletedPullRequestsAtom)
|
|
599
|
-
const retryProgress = useAtomValue(retryProgressAtom)
|
|
600
|
-
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
601
|
-
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
602
|
-
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
603
|
-
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
604
|
-
const [startupLoadComplete, setStartupLoadComplete] = useState(false)
|
|
605
|
-
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
606
|
-
const [detailPreviewScrollTop, setDetailPreviewScrollTop] = useState(0)
|
|
607
|
-
const usernameResult = useAtomValue(usernameAtom)
|
|
608
|
-
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
609
|
-
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
610
|
-
const addPullRequestLabel = useAtomSet(addPullRequestLabelAtom, { mode: "promise" })
|
|
611
|
-
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
612
|
-
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
613
|
-
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
614
|
-
const listPullRequestConversation = useAtomSet(listPullRequestConversationAtom, { mode: "promise" })
|
|
615
|
-
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
616
|
-
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
617
|
-
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
618
|
-
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
619
|
-
const copyToClipboard = useAtomSet(copyToClipboardAtom, { mode: "promise" })
|
|
620
|
-
const openInBrowser = useAtomSet(openInBrowserAtom, { mode: "promise" })
|
|
621
|
-
const terminalWidth = width ?? 100
|
|
622
|
-
const terminalHeight = height ?? 24
|
|
623
|
-
const contentWidth = Math.max(1, terminalWidth)
|
|
624
|
-
const isWideLayout = terminalWidth >= 100
|
|
625
|
-
const splitGap = 1
|
|
626
|
-
const sectionPadding = 1
|
|
627
|
-
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
|
|
628
|
-
const rightPaneWidth = isWideLayout ? Math.max(28, contentWidth - leftPaneWidth - splitGap) : contentWidth
|
|
629
|
-
const dividerJunctionAt = Math.max(1, leftPaneWidth)
|
|
630
|
-
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
631
|
-
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
632
|
-
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
633
|
-
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
634
|
-
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
635
|
-
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
636
|
-
const detailPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
637
|
-
const detailHydrationRef = useRef(new Map<string, DetailHydration>())
|
|
638
|
-
const refreshGenerationRef = useRef(0)
|
|
639
|
-
const didMountQueueModeRef = useRef(false)
|
|
640
|
-
const lastPullRequestRefreshAtRef = useRef(0)
|
|
641
|
-
const terminalFocusedRef = useRef(true)
|
|
642
|
-
const terminalWasBlurredRef = useRef(false)
|
|
643
|
-
const pullRequestStatusRef = useRef<LoadStatus>("loading")
|
|
644
|
-
const refreshPullRequestsRef = useRef<(message?: string, options?: { readonly resetTransientState?: boolean }) => void>(() => {})
|
|
645
|
-
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
646
|
-
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
647
|
-
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
648
|
-
const detailPreviewScrollTopRef = useRef(0)
|
|
649
|
-
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
650
|
-
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
651
|
-
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
652
|
-
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
653
|
-
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
654
|
-
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
655
|
-
|
|
656
|
-
const flashNotice = (message: string) => {
|
|
657
|
-
if (noticeTimeoutRef.current !== null) {
|
|
658
|
-
clearTimeout(noticeTimeoutRef.current)
|
|
659
|
-
}
|
|
660
|
-
setNotice(message)
|
|
661
|
-
noticeTimeoutRef.current = globalThis.setTimeout(() => {
|
|
662
|
-
setNotice((current) => (current === message ? null : current))
|
|
663
|
-
}, 2500)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
useEffect(() => {
|
|
667
|
-
renderer.setBackgroundColor(colors.background)
|
|
668
|
-
}, [renderer, themeId])
|
|
669
|
-
|
|
670
|
-
useEffect(() => () => {
|
|
671
|
-
refreshGenerationRef.current += 1
|
|
672
|
-
detailHydrationRef.current.clear()
|
|
673
|
-
if (noticeTimeoutRef.current !== null) {
|
|
674
|
-
clearTimeout(noticeTimeoutRef.current)
|
|
675
|
-
}
|
|
676
|
-
if (diffPrefetchTimeoutRef.current !== null) {
|
|
677
|
-
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
678
|
-
}
|
|
679
|
-
if (detailPrefetchTimeoutRef.current !== null) {
|
|
680
|
-
clearTimeout(detailPrefetchTimeoutRef.current)
|
|
681
|
-
}
|
|
682
|
-
}, [])
|
|
683
|
-
|
|
684
|
-
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
685
|
-
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
686
|
-
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
687
|
-
const isInitialLoading = !startupLoadComplete && pullRequestStatus === "loading" && pullRequests.length === 0
|
|
688
|
-
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
689
|
-
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
690
|
-
pullRequestStatusRef.current = pullRequestStatus
|
|
691
|
-
|
|
692
|
-
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
693
|
-
|
|
694
|
-
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
695
|
-
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
696
|
-
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
697
|
-
const pullRequestConversation = useAtomValue(pullRequestConversationAtom)
|
|
698
|
-
const pullRequestConversationLoaded = useAtomValue(pullRequestConversationLoadedAtom)
|
|
699
|
-
const selectedRepository = viewRepository(activeView)
|
|
700
|
-
const activeViews = activePullRequestViews(activeView)
|
|
701
|
-
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
702
|
-
const loadedPullRequestCount = pullRequestLoad?.data.length ?? 0
|
|
703
|
-
const hasMorePullRequests = Boolean(pullRequestLoad?.hasNextPage && loadedPullRequestCount < config.prFetchLimit)
|
|
704
|
-
const isLoadingMorePullRequests = loadingMoreKey === currentQueueCacheKey
|
|
705
|
-
const pullRequestListRows = useMemo(() => buildPullRequestListRows({
|
|
706
|
-
groups: visibleGroups,
|
|
707
|
-
status: pullRequestStatus,
|
|
708
|
-
error: pullRequestError,
|
|
709
|
-
filterText: visibleFilterText,
|
|
710
|
-
showFilterBar: filterMode || filterQuery.length > 0,
|
|
711
|
-
loadedCount: loadedPullRequestCount,
|
|
712
|
-
hasMore: hasMorePullRequests,
|
|
713
|
-
isLoadingMore: isLoadingMorePullRequests,
|
|
714
|
-
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
715
|
-
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
716
|
-
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
717
|
-
const selectedConversationItems = selectedDiffKey ? pullRequestConversation[selectedDiffKey] ?? [] : []
|
|
718
|
-
const selectedConversationStatus: DetailConversationStatus = selectedDiffKey ? pullRequestConversationLoaded[selectedDiffKey] ?? "idle" : "idle"
|
|
719
|
-
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
720
|
-
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
721
|
-
const readyDiffFiles = useMemo(
|
|
722
|
-
() => selectedDiffState?._tag === "Ready"
|
|
723
|
-
? diffWhitespaceMode === "ignore" ? minimizeWhitespaceDiffFiles(selectedDiffState.files) : selectedDiffState.files
|
|
724
|
-
: [],
|
|
725
|
-
[selectedDiffState, diffWhitespaceMode],
|
|
726
|
-
)
|
|
727
|
-
const displayedDiffState = useMemo(
|
|
728
|
-
() => selectedDiffState?._tag === "Ready"
|
|
729
|
-
? PullRequestDiffState.Ready({ patch: readyDiffFiles.map((file) => file.patch).join("\n"), files: readyDiffFiles })
|
|
730
|
-
: selectedDiffState,
|
|
731
|
-
[selectedDiffState, readyDiffFiles],
|
|
732
|
-
)
|
|
733
|
-
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
734
|
-
const diffCommentAnchors = useMemo(
|
|
735
|
-
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
736
|
-
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
737
|
-
)
|
|
738
|
-
const selectedDiffCommentAnchorIndex = Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))
|
|
739
|
-
const selectedDiffCommentAnchor = diffCommentAnchors[selectedDiffCommentAnchorIndex] ?? null
|
|
740
|
-
const diffCommentRangeStartAnchor = diffCommentRangeStartIndex === null
|
|
741
|
-
? null
|
|
742
|
-
: diffCommentAnchors[Math.max(0, Math.min(diffCommentRangeStartIndex, diffCommentAnchors.length - 1))] ?? null
|
|
743
|
-
const selectedDiffCommentRange = useMemo(
|
|
744
|
-
() => diffCommentRangeSelection(diffCommentRangeStartAnchor, selectedDiffCommentAnchor),
|
|
745
|
-
[diffCommentRangeStartAnchor, selectedDiffCommentAnchor],
|
|
746
|
-
)
|
|
747
|
-
const selectedDiffCommentRangeAnchors = useMemo(
|
|
748
|
-
() => selectedDiffCommentRange
|
|
749
|
-
? diffCommentAnchors.filter((anchor) => diffCommentRangeContains(selectedDiffCommentRange, anchor))
|
|
750
|
-
: [],
|
|
751
|
-
[diffCommentAnchors, selectedDiffCommentRange],
|
|
752
|
-
)
|
|
753
|
-
const diffCommentRangeActive = selectedDiffCommentRange !== null
|
|
754
|
-
const selectedDiffCommentLabel = selectedDiffCommentRange
|
|
755
|
-
? diffCommentRangeLabel(selectedDiffCommentRange)
|
|
756
|
-
: selectedDiffCommentAnchor ? diffCommentAnchorLabel(selectedDiffCommentAnchor) : null
|
|
757
|
-
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? diffCommentThreadMapKey(selectedDiffKey, selectedDiffCommentAnchor) : null
|
|
758
|
-
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
759
|
-
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
760
|
-
const diffCommentThreadAnchors = useMemo(() => {
|
|
761
|
-
if (!selectedDiffKey) return [] as readonly StackedDiffCommentAnchor[]
|
|
762
|
-
const seen = new Set<string>()
|
|
763
|
-
return diffCommentAnchors.filter((anchor) => {
|
|
764
|
-
const key = diffCommentLocationKey(anchor)
|
|
765
|
-
if (seen.has(key)) return false
|
|
766
|
-
if ((diffCommentThreads[diffCommentThreadMapKey(selectedDiffKey, anchor)]?.length ?? 0) === 0) return false
|
|
767
|
-
seen.add(key)
|
|
768
|
-
return true
|
|
769
|
-
})
|
|
770
|
-
}, [diffCommentAnchors, diffCommentThreads, selectedDiffKey])
|
|
771
|
-
const groupStarts = useAtomValue(groupStartsAtom)
|
|
772
|
-
const getCurrentGroupIndex = (current: number) => {
|
|
773
|
-
if (groupStarts.length === 0) return 0
|
|
774
|
-
let low = 0
|
|
775
|
-
let high = groupStarts.length - 1
|
|
776
|
-
while (low < high) {
|
|
777
|
-
const mid = (low + high + 1) >>> 1
|
|
778
|
-
if (groupStarts[mid]! <= current) low = mid
|
|
779
|
-
else high = mid - 1
|
|
780
|
-
}
|
|
781
|
-
return low
|
|
782
|
-
}
|
|
783
|
-
const summaryRight = pullRequestLoad?.fetchedAt
|
|
784
|
-
? `updated ${formatShortDate(pullRequestLoad.fetchedAt)} ${formatTimestamp(pullRequestLoad.fetchedAt)}`
|
|
785
|
-
: pullRequestStatus === "loading"
|
|
786
|
-
? "loading pull requests..."
|
|
787
|
-
: ""
|
|
788
|
-
const headerLeft = username ? `GHUI ${username} ${viewLabel(activeView)}` : `GHUI ${viewLabel(activeView)}`
|
|
789
|
-
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
790
|
-
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
791
|
-
const selectPullRequestByUrl = (url: string) => {
|
|
792
|
-
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
793
|
-
if (index >= 0) {
|
|
794
|
-
setSelectedIndex(index)
|
|
795
|
-
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: index }))
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
799
|
-
const pullRequest = pullRequests.find((item) => item.url === url)
|
|
800
|
-
if (!pullRequest) return
|
|
801
|
-
setPullRequestOverrides((current) => ({ ...current, [url]: transform(pullRequest) }))
|
|
802
|
-
}
|
|
803
|
-
const markPullRequestCompleted = (pullRequest: PullRequestItem, state: "closed" | "merged") => {
|
|
804
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
805
|
-
...current,
|
|
806
|
-
[pullRequest.url]: {
|
|
807
|
-
...pullRequest,
|
|
808
|
-
state,
|
|
809
|
-
autoMergeEnabled: false,
|
|
810
|
-
},
|
|
811
|
-
}))
|
|
812
|
-
}
|
|
813
|
-
const restoreOptimisticPullRequest = (pullRequest: PullRequestItem) => {
|
|
814
|
-
setRecentlyCompletedPullRequests((current) => {
|
|
815
|
-
if (!(pullRequest.url in current)) return current
|
|
816
|
-
const next = { ...current }
|
|
817
|
-
delete next[pullRequest.url]
|
|
818
|
-
return next
|
|
819
|
-
})
|
|
820
|
-
updatePullRequest(pullRequest.url, () => pullRequest)
|
|
821
|
-
}
|
|
822
|
-
const refreshPullRequests = (message?: string, options: { readonly resetTransientState?: boolean } = {}) => {
|
|
823
|
-
refreshGenerationRef.current += 1
|
|
824
|
-
detailHydrationRef.current.clear()
|
|
825
|
-
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
826
|
-
setLoadingMoreKey(null)
|
|
827
|
-
setPullRequestOverrides({})
|
|
828
|
-
if (options.resetTransientState) {
|
|
829
|
-
setRecentlyCompletedPullRequests({})
|
|
830
|
-
setPullRequestConversation({})
|
|
831
|
-
setPullRequestConversationLoaded({})
|
|
832
|
-
}
|
|
833
|
-
if (message) {
|
|
834
|
-
setNotice(null)
|
|
835
|
-
setRefreshCompletionMessage(message)
|
|
836
|
-
setRefreshStartedAt(lastPullRequestRefreshAtRef.current)
|
|
837
|
-
}
|
|
838
|
-
refreshPullRequestsAtom()
|
|
839
|
-
}
|
|
840
|
-
refreshPullRequestsRef.current = refreshPullRequests
|
|
841
|
-
const switchViewTo = (view: PullRequestView) => {
|
|
842
|
-
if (viewEquals(view, activeView)) return
|
|
843
|
-
refreshGenerationRef.current += 1
|
|
844
|
-
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: selectedIndex }))
|
|
845
|
-
setActiveView(view)
|
|
846
|
-
setSelectedIndex(registry.get(queueSelectionAtom)[viewCacheKey(view)] ?? 0)
|
|
847
|
-
setRecentlyCompletedPullRequests({})
|
|
848
|
-
detailHydrationRef.current.clear()
|
|
849
|
-
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
850
|
-
setLoadingMoreKey(null)
|
|
851
|
-
setDetailFullView(false)
|
|
852
|
-
setDiffFullView(false)
|
|
853
|
-
setDiffCommentRangeStartIndex(null)
|
|
854
|
-
setFilterDraft(filterQuery)
|
|
855
|
-
setNotice(null)
|
|
856
|
-
setRefreshCompletionMessage(null)
|
|
857
|
-
setRefreshStartedAt(null)
|
|
858
|
-
}
|
|
859
|
-
const switchQueueMode = (delta: 1 | -1) => {
|
|
860
|
-
switchViewTo(nextView(activeView, activeViews, delta))
|
|
861
|
-
}
|
|
862
|
-
const loadMorePullRequests = () => {
|
|
863
|
-
if (!pullRequestLoad || !hasMorePullRequests || isLoadingMorePullRequests || !pullRequestLoad.endCursor) return false
|
|
864
|
-
const remaining = config.prFetchLimit - pullRequestLoad.data.length
|
|
865
|
-
if (remaining <= 0) return false
|
|
866
|
-
const cacheKey = currentQueueCacheKey
|
|
867
|
-
const generation = refreshGenerationRef.current
|
|
868
|
-
setLoadingMoreKey(cacheKey)
|
|
869
|
-
void loadPullRequestPage({
|
|
870
|
-
mode: viewMode(activeView),
|
|
871
|
-
repository: selectedRepository,
|
|
872
|
-
cursor: pullRequestLoad.endCursor,
|
|
873
|
-
pageSize: Math.min(pullRequestPageSize, remaining),
|
|
874
|
-
}).then((page) => {
|
|
875
|
-
if (generation !== refreshGenerationRef.current) return
|
|
876
|
-
setQueueLoadCache((current) => {
|
|
877
|
-
const load = current[cacheKey]
|
|
878
|
-
if (!load) return current
|
|
879
|
-
const data = appendPullRequestPage(load.data, page.items)
|
|
880
|
-
return {
|
|
881
|
-
...current,
|
|
882
|
-
[cacheKey]: {
|
|
883
|
-
...load,
|
|
884
|
-
data,
|
|
885
|
-
endCursor: page.endCursor,
|
|
886
|
-
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
887
|
-
},
|
|
888
|
-
}
|
|
889
|
-
})
|
|
890
|
-
}).catch((error) => {
|
|
891
|
-
flashNotice(errorMessage(error))
|
|
892
|
-
}).finally(() => {
|
|
893
|
-
setLoadingMoreKey((current) => current === cacheKey ? null : current)
|
|
894
|
-
})
|
|
895
|
-
return true
|
|
896
|
-
}
|
|
897
|
-
const applyPullRequestDetail = (detail: PullRequestItem) => {
|
|
898
|
-
setQueueLoadCache((current) => {
|
|
899
|
-
const next = { ...current }
|
|
900
|
-
let changed = false
|
|
901
|
-
for (const [cacheKey, load] of Object.entries(current)) {
|
|
902
|
-
if (!load) continue
|
|
903
|
-
const index = load.data.findIndex((pullRequest) => pullRequest.url === detail.url)
|
|
904
|
-
if (index < 0) continue
|
|
905
|
-
const data = [...load.data]
|
|
906
|
-
data[index] = detail
|
|
907
|
-
changed = true
|
|
908
|
-
next[cacheKey] = { ...load, data }
|
|
909
|
-
}
|
|
910
|
-
return changed ? next : current
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
const hydratePullRequestDetails = (pullRequest: PullRequestItem, notifyError: boolean) => {
|
|
914
|
-
if (pullRequest.state !== "open" || pullRequest.detailLoaded) return false
|
|
915
|
-
const detailKey = pullRequestDetailKey(pullRequest)
|
|
916
|
-
const existing = detailHydrationRef.current.get(detailKey)
|
|
917
|
-
if (existing) {
|
|
918
|
-
if (notifyError) existing.notifyError = true
|
|
919
|
-
return false
|
|
920
|
-
}
|
|
921
|
-
if (!notifyError && detailHydrationRef.current.size >= DETAIL_PREFETCH_CONCURRENCY) return false
|
|
922
|
-
const entry: DetailHydration = { token: Symbol(detailKey), notifyError }
|
|
923
|
-
detailHydrationRef.current.set(detailKey, entry)
|
|
924
|
-
const generation = refreshGenerationRef.current
|
|
925
|
-
const atom = pullRequestDetailsAtom(pullRequestDetailAtomKey(pullRequest))
|
|
926
|
-
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })).then((detail) => {
|
|
927
|
-
if (generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) applyPullRequestDetail(detail)
|
|
928
|
-
}).catch((error) => {
|
|
929
|
-
if (entry.notifyError && generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) flashNotice(errorMessage(error))
|
|
930
|
-
}).finally(() => {
|
|
931
|
-
if (detailHydrationRef.current.get(detailKey) === entry) detailHydrationRef.current.delete(detailKey)
|
|
932
|
-
})
|
|
933
|
-
return true
|
|
934
|
-
}
|
|
935
|
-
const loadPullRequestConversation = (pullRequest: PullRequestItem, force = false) => {
|
|
936
|
-
const key = pullRequestDiffKey(pullRequest)
|
|
937
|
-
const previousLoadState = registry.get(pullRequestConversationLoadedAtom)[key]
|
|
938
|
-
if (!force && previousLoadState) return
|
|
939
|
-
const generation = refreshGenerationRef.current
|
|
940
|
-
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
941
|
-
void listPullRequestConversation({ repository: pullRequest.repository, number: pullRequest.number })
|
|
942
|
-
.then((items) => {
|
|
943
|
-
if (generation !== refreshGenerationRef.current) return
|
|
944
|
-
setPullRequestConversation((current) => ({ ...current, [key]: items }))
|
|
945
|
-
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
946
|
-
})
|
|
947
|
-
.catch((error) => {
|
|
948
|
-
if (generation !== refreshGenerationRef.current) return
|
|
949
|
-
setPullRequestConversationLoaded((current) => {
|
|
950
|
-
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
951
|
-
const next = { ...current }
|
|
952
|
-
delete next[key]
|
|
953
|
-
return next
|
|
954
|
-
})
|
|
955
|
-
flashNotice(errorMessage(error))
|
|
956
|
-
})
|
|
957
|
-
}
|
|
958
|
-
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
959
|
-
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
960
|
-
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
961
|
-
if (lastRefreshAt > 0 && Date.now() - lastRefreshAt < minimumAgeMs) return
|
|
962
|
-
refreshPullRequestsRef.current()
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
useEffect(() => {
|
|
966
|
-
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
967
|
-
if (fetchedAt !== undefined) {
|
|
968
|
-
lastPullRequestRefreshAtRef.current = fetchedAt
|
|
969
|
-
}
|
|
970
|
-
}, [pullRequestLoad?.fetchedAt])
|
|
971
|
-
|
|
972
|
-
useEffect(() => {
|
|
973
|
-
if (!didMountQueueModeRef.current) {
|
|
974
|
-
didMountQueueModeRef.current = true
|
|
975
|
-
return
|
|
976
|
-
}
|
|
977
|
-
if (registry.get(queueLoadCacheAtom)[currentQueueCacheKey]) return
|
|
978
|
-
refreshPullRequestsAtom()
|
|
979
|
-
}, [currentQueueCacheKey, refreshPullRequestsAtom, registry])
|
|
980
|
-
|
|
981
|
-
useEffect(() => {
|
|
982
|
-
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
983
|
-
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
984
|
-
const isHydratingDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
985
|
-
if (pullRequestStatus === "ready" && fetchedAt !== undefined && fetchedAt !== refreshStartedAt && !isHydratingDetails) {
|
|
986
|
-
flashNotice(`✓ ${refreshCompletionMessage}`)
|
|
987
|
-
setRefreshCompletionMessage(null)
|
|
988
|
-
setRefreshStartedAt(null)
|
|
989
|
-
} else if (pullRequestStatus === "error") {
|
|
990
|
-
flashNotice("Refresh failed")
|
|
991
|
-
setRefreshCompletionMessage(null)
|
|
992
|
-
setRefreshStartedAt(null)
|
|
993
|
-
}
|
|
994
|
-
}, [refreshCompletionMessage, refreshStartedAt, pullRequestStatus, pullRequestLoad?.fetchedAt, pullRequests])
|
|
995
|
-
|
|
996
|
-
useEffect(() => {
|
|
997
|
-
const handleFocus = () => {
|
|
998
|
-
terminalFocusedRef.current = true
|
|
999
|
-
setTerminalFocused(true)
|
|
1000
|
-
if (terminalWasBlurredRef.current) {
|
|
1001
|
-
maybeRefreshPullRequestsRef.current(FOCUS_RETURN_REFRESH_MIN_MS)
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
const handleBlur = () => {
|
|
1005
|
-
terminalWasBlurredRef.current = true
|
|
1006
|
-
terminalFocusedRef.current = false
|
|
1007
|
-
setTerminalFocused(false)
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
renderer.on("focus", handleFocus)
|
|
1011
|
-
renderer.on("blur", handleBlur)
|
|
1012
|
-
return () => {
|
|
1013
|
-
renderer.off("focus", handleFocus)
|
|
1014
|
-
renderer.off("blur", handleBlur)
|
|
1015
|
-
}
|
|
1016
|
-
}, [renderer])
|
|
1017
|
-
|
|
1018
|
-
useEffect(() => {
|
|
1019
|
-
if (!terminalFocused) return
|
|
1020
|
-
const lastRefreshAt = lastPullRequestRefreshAtRef.current || Date.now()
|
|
1021
|
-
const ageMs = Date.now() - lastRefreshAt
|
|
1022
|
-
const delayMs = Math.max(0, FOCUSED_IDLE_REFRESH_MS - ageMs) + Math.floor(Math.random() * AUTO_REFRESH_JITTER_MS)
|
|
1023
|
-
const timeout = globalThis.setTimeout(() => {
|
|
1024
|
-
maybeRefreshPullRequestsRef.current(FOCUSED_IDLE_REFRESH_MS)
|
|
1025
|
-
}, delayMs)
|
|
1026
|
-
return () => globalThis.clearTimeout(timeout)
|
|
1027
|
-
}, [terminalFocused, pullRequestLoad?.fetchedAt])
|
|
1028
|
-
|
|
1029
|
-
useEffect(() => {
|
|
1030
|
-
setSelectedIndex((current) => {
|
|
1031
|
-
if (visiblePullRequests.length === 0) return 0
|
|
1032
|
-
return Math.max(0, Math.min(current, visiblePullRequests.length - 1))
|
|
1033
|
-
})
|
|
1034
|
-
}, [visiblePullRequests.length])
|
|
1035
|
-
|
|
1036
|
-
useEffect(() => {
|
|
1037
|
-
setQueueSelection((current) => current[currentQueueCacheKey] === selectedIndex ? current : { ...current, [currentQueueCacheKey]: selectedIndex })
|
|
1038
|
-
}, [currentQueueCacheKey, selectedIndex])
|
|
1039
|
-
|
|
1040
|
-
useEffect(() => {
|
|
1041
|
-
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0) return
|
|
1042
|
-
const thresholdIndex = Math.max(0, visiblePullRequests.length - LOAD_MORE_SELECTION_THRESHOLD)
|
|
1043
|
-
if (selectedIndex >= thresholdIndex) loadMorePullRequests()
|
|
1044
|
-
}, [selectedIndex, visiblePullRequests.length, filterMode, filterQuery, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
1045
|
-
|
|
1046
|
-
useEffect(() => {
|
|
1047
|
-
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0 || detailFullView || diffFullView) return
|
|
1048
|
-
if (!hasMorePullRequests || isLoadingMorePullRequests) return
|
|
1049
|
-
const checkScroll = () => {
|
|
1050
|
-
const scroll = prListScrollRef.current
|
|
1051
|
-
if (!scroll || scroll.viewport.height <= 0) return
|
|
1052
|
-
const bottom = scroll.scrollTop + scroll.viewport.height
|
|
1053
|
-
if (bottom >= scroll.scrollHeight - LOAD_MORE_SCROLL_THRESHOLD) loadMorePullRequests()
|
|
1054
|
-
}
|
|
1055
|
-
checkScroll()
|
|
1056
|
-
const interval = globalThis.setInterval(checkScroll, 120)
|
|
1057
|
-
return () => globalThis.clearInterval(interval)
|
|
1058
|
-
}, [visiblePullRequests.length, filterMode, filterQuery, detailFullView, diffFullView, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
1059
|
-
|
|
1060
|
-
useEffect(() => {
|
|
1061
|
-
const scroll = prListScrollRef.current
|
|
1062
|
-
if (!scroll || selectedPullRequestRowIndex === null) return
|
|
1063
|
-
const viewportHeight = scroll.viewport.height
|
|
1064
|
-
if (viewportHeight <= 0) return
|
|
1065
|
-
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, selectedPullRequestRowIndex, 2)
|
|
1066
|
-
if (nextTop !== scroll.scrollTop) scroll.scrollTo({ x: 0, y: nextTop })
|
|
1067
|
-
}, [selectedPullRequestRowIndex])
|
|
1068
|
-
|
|
1069
|
-
useEffect(() => {
|
|
1070
|
-
setDiffFileIndex(0)
|
|
1071
|
-
setDiffScrollTop(0)
|
|
1072
|
-
setDiffCommentAnchorIndex(0)
|
|
1073
|
-
setDiffPreferredSide(null)
|
|
1074
|
-
setDiffCommentRangeStartIndex(null)
|
|
1075
|
-
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1076
|
-
if (detailPreviewScrollTopRef.current !== 0) {
|
|
1077
|
-
detailPreviewScrollTopRef.current = 0
|
|
1078
|
-
setDetailPreviewScrollTop(0)
|
|
1079
|
-
}
|
|
1080
|
-
}, [selectedIndex])
|
|
1081
|
-
|
|
1082
|
-
useEffect(() => {
|
|
1083
|
-
setDiffFileIndex((current) => safeDiffFileIndex(readyDiffFiles, current))
|
|
1084
|
-
}, [readyDiffFiles.length])
|
|
1085
|
-
|
|
1086
|
-
useEffect(() => {
|
|
1087
|
-
setDiffCommentAnchorIndex((current) => {
|
|
1088
|
-
if (diffCommentAnchors.length === 0) return 0
|
|
1089
|
-
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
1090
|
-
})
|
|
1091
|
-
setDiffCommentRangeStartIndex((current) => {
|
|
1092
|
-
if (current === null || diffCommentAnchors.length === 0) return null
|
|
1093
|
-
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
1094
|
-
})
|
|
1095
|
-
}, [diffCommentAnchors.length])
|
|
1096
|
-
|
|
1097
|
-
useEffect(() => {
|
|
1098
|
-
if (!diffFullView || !selectedDiffCommentAnchor) return
|
|
1099
|
-
setDiffFileIndex((current) => current === selectedDiffCommentAnchor.fileIndex ? current : selectedDiffCommentAnchor.fileIndex)
|
|
1100
|
-
}, [diffFullView, selectedDiffCommentAnchor?.fileIndex])
|
|
1101
|
-
|
|
1102
|
-
useEffect(() => {
|
|
1103
|
-
const previous = diffCommentLineColorsRef.current
|
|
1104
|
-
if (previous.contextKey === diffLineColorContextKey) {
|
|
1105
|
-
for (const entry of previous.entries) {
|
|
1106
|
-
const diff = diffRenderableRefs.current.get(entry.anchor.fileIndex)
|
|
1107
|
-
if (diff) setDiffCommentLineColor(diff, entry, originalDiffLineColor(entry.anchor))
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
const nextEntries: AppliedDiffLineColor[] = []
|
|
1112
|
-
const appliedKeys = new Set<string>()
|
|
1113
|
-
const applyLineColor = (anchor: StackedDiffCommentAnchor, color: DiffLineColorConfig, override = false) => {
|
|
1114
|
-
const key = `${effectiveDiffRenderView}:${anchor.side}:${anchor.renderLine}`
|
|
1115
|
-
if (appliedKeys.has(key) && !override) return
|
|
1116
|
-
appliedKeys.add(key)
|
|
1117
|
-
const entry = { anchor, view: effectiveDiffRenderView } satisfies AppliedDiffLineColor
|
|
1118
|
-
const diff = diffRenderableRefs.current.get(anchor.fileIndex)
|
|
1119
|
-
if (diff) setDiffCommentLineColor(diff, entry, color)
|
|
1120
|
-
if (!nextEntries.some((existing) => existing.view === entry.view && existing.anchor.side === anchor.side && existing.anchor.renderLine === anchor.renderLine)) {
|
|
1121
|
-
nextEntries.push(entry)
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
for (const anchor of diffCommentThreadAnchors) {
|
|
1126
|
-
applyLineColor(anchor, diffCommentLineColor(anchor, "thread"))
|
|
1127
|
-
}
|
|
1128
|
-
if (selectedDiffCommentRangeAnchors.length > 0) {
|
|
1129
|
-
for (const anchor of selectedDiffCommentRangeAnchors) {
|
|
1130
|
-
applyLineColor(anchor, diffCommentLineColor(anchor, "range"), true)
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
if (selectedDiffCommentAnchor) {
|
|
1134
|
-
applyLineColor(selectedDiffCommentAnchor, diffCommentLineColor(selectedDiffCommentAnchor, "selected"), true)
|
|
1135
|
-
if (suppressNextDiffCommentScrollRef.current) {
|
|
1136
|
-
suppressNextDiffCommentScrollRef.current = false
|
|
1137
|
-
} else {
|
|
1138
|
-
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
1139
|
-
}
|
|
1140
|
-
} else {
|
|
1141
|
-
suppressNextDiffCommentScrollRef.current = false
|
|
1142
|
-
}
|
|
1143
|
-
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
1144
|
-
}, [selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, selectedDiffCommentRangeAnchors, diffLineColorContextKey, effectiveDiffRenderView, diffCommentThreadAnchors])
|
|
1145
|
-
|
|
1146
|
-
// Scroll the selected line into view when the diff view is opened. Previously
|
|
1147
|
-
// opentui's `focused` scrollbox did this auto-scroll on mount; with the keymap
|
|
1148
|
-
// migration the scrollbox is `focusable={false}` so we have to scroll explicitly.
|
|
1149
|
-
useEffect(() => {
|
|
1150
|
-
if (!diffFullView) return
|
|
1151
|
-
if (!selectedDiffCommentAnchor) return
|
|
1152
|
-
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
1153
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1154
|
-
}, [diffFullView])
|
|
1155
|
-
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
1156
|
-
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
1157
|
-
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || isLoadingMorePullRequests || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
1158
|
-
const loadingIndicator = SPINNER_FRAMES[loadingFrame % SPINNER_FRAMES.length]!
|
|
1159
|
-
|
|
1160
|
-
useEffect(() => {
|
|
1161
|
-
if (!hasActiveLoadingIndicator) return
|
|
1162
|
-
const interval = globalThis.setInterval(() => {
|
|
1163
|
-
setLoadingFrame((current) => current + 1)
|
|
1164
|
-
}, 120)
|
|
1165
|
-
return () => globalThis.clearInterval(interval)
|
|
1166
|
-
}, [hasActiveLoadingIndicator])
|
|
1167
|
-
|
|
1168
|
-
useEffect(() => {
|
|
1169
|
-
if (isInitialLoading) setLoadingFrame(0)
|
|
1170
|
-
}, [isInitialLoading])
|
|
1171
|
-
|
|
1172
|
-
useEffect(() => {
|
|
1173
|
-
if (startupLoadComplete || pullRequestStatus === "loading") return
|
|
1174
|
-
setStartupLoadComplete(true)
|
|
1175
|
-
}, [startupLoadComplete, pullRequestStatus])
|
|
1176
|
-
|
|
1177
|
-
useEffect(() => {
|
|
1178
|
-
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1179
|
-
hydratePullRequestDetails(selectedPullRequest, true)
|
|
1180
|
-
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1181
|
-
|
|
1182
|
-
useEffect(() => {
|
|
1183
|
-
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1184
|
-
loadPullRequestConversation(selectedPullRequest)
|
|
1185
|
-
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1186
|
-
|
|
1187
|
-
useEffect(() => {
|
|
1188
|
-
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1189
|
-
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
1190
|
-
detailPrefetchTimeoutRef.current = globalThis.setTimeout(() => {
|
|
1191
|
-
detailPrefetchTimeoutRef.current = null
|
|
1192
|
-
let started = 0
|
|
1193
|
-
for (let distance = 1; distance <= Math.max(DETAIL_PREFETCH_AHEAD, DETAIL_PREFETCH_BEHIND); distance++) {
|
|
1194
|
-
const offsets = [distance <= DETAIL_PREFETCH_AHEAD ? distance : null, distance <= DETAIL_PREFETCH_BEHIND ? -distance : null]
|
|
1195
|
-
for (const offset of offsets) {
|
|
1196
|
-
if (offset === null) continue
|
|
1197
|
-
if (started >= DETAIL_PREFETCH_CONCURRENCY) return
|
|
1198
|
-
const pullRequest = visiblePullRequests[selectedIndex + offset]
|
|
1199
|
-
if (pullRequest && hydratePullRequestDetails(pullRequest, false)) started += 1
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}, DETAIL_PREFETCH_DELAY_MS)
|
|
1203
|
-
return () => {
|
|
1204
|
-
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1205
|
-
}
|
|
1206
|
-
}, [pullRequestStatus, currentQueueCacheKey, selectedIndex, visiblePullRequests])
|
|
1207
|
-
|
|
1208
|
-
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
1209
|
-
status: pullRequestStatus,
|
|
1210
|
-
retryProgress,
|
|
1211
|
-
loadingIndicator,
|
|
1212
|
-
visibleCount: visiblePullRequests.length,
|
|
1213
|
-
filterText: visibleFilterText,
|
|
1214
|
-
})
|
|
1215
|
-
const isSelectedPullRequestDetailLoading = selectedPullRequest !== null && !selectedPullRequest.detailLoaded
|
|
1216
|
-
const detailLoadingContent: DetailPlaceholderContent = selectedPullRequest ? {
|
|
1217
|
-
title: `${loadingIndicator} Loading pull request details`,
|
|
1218
|
-
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1219
|
-
} : detailPlaceholderContent
|
|
1220
|
-
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
1221
|
-
|
|
1222
|
-
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
1223
|
-
const key = pullRequestDiffKey(pullRequest)
|
|
1224
|
-
const previousLoadState = registry.get(diffCommentsLoadedAtom)[key]
|
|
1225
|
-
if (!force && previousLoadState) return
|
|
1226
|
-
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
1227
|
-
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
1228
|
-
.then((comments) => {
|
|
1229
|
-
setDiffCommentsLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
1230
|
-
setDiffCommentThreads((current) => {
|
|
1231
|
-
const prefix = `${key}:`
|
|
1232
|
-
const threads = groupDiffCommentThreads(pullRequest, comments)
|
|
1233
|
-
const next: Record<string, readonly PullRequestReviewComment[]> = Object.fromEntries(
|
|
1234
|
-
Object.entries(current).filter(([threadKey]) => !threadKey.startsWith(prefix)),
|
|
1235
|
-
)
|
|
1236
|
-
|
|
1237
|
-
for (const [threadKey, threadComments] of Object.entries(current)) {
|
|
1238
|
-
if (!threadKey.startsWith(prefix)) continue
|
|
1239
|
-
const localComments = threadComments.filter(isLocalDiffComment)
|
|
1240
|
-
if (localComments.length > 0) {
|
|
1241
|
-
next[threadKey] = [...(threads[threadKey] ?? []), ...localComments]
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
for (const [threadKey, threadComments] of Object.entries(threads)) {
|
|
1246
|
-
if (!next[threadKey]) next[threadKey] = threadComments
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
return next
|
|
1250
|
-
})
|
|
1251
|
-
})
|
|
1252
|
-
.catch((error) => {
|
|
1253
|
-
setDiffCommentsLoaded((current) => {
|
|
1254
|
-
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
1255
|
-
const next = { ...current }
|
|
1256
|
-
delete next[key]
|
|
1257
|
-
return next
|
|
1258
|
-
})
|
|
1259
|
-
flashNotice(errorMessage(error))
|
|
1260
|
-
})
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const loadPullRequestDiff = (pullRequest: PullRequestItem, options: { readonly force?: boolean; readonly includeComments?: boolean } = {}) => {
|
|
1264
|
-
const force = options.force ?? false
|
|
1265
|
-
const includeComments = options.includeComments ?? false
|
|
1266
|
-
const key = pullRequestDiffKey(pullRequest)
|
|
1267
|
-
const existing = registry.get(pullRequestDiffCacheAtom)[key]
|
|
1268
|
-
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
1269
|
-
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
1270
|
-
|
|
1271
|
-
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
1272
|
-
const atom = pullRequestDiffAtom(pullRequestDiffAtomKey(pullRequest))
|
|
1273
|
-
if (force) registry.refresh(atom)
|
|
1274
|
-
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true }))
|
|
1275
|
-
.then((patch) => {
|
|
1276
|
-
setPullRequestDiffCache((current) => ({
|
|
1277
|
-
...current,
|
|
1278
|
-
[key]: PullRequestDiffState.Ready({ patch, files: splitPatchFiles(patch) }),
|
|
1279
|
-
}))
|
|
1280
|
-
})
|
|
1281
|
-
.catch((error) => {
|
|
1282
|
-
setPullRequestDiffCache((current) => ({
|
|
1283
|
-
...current,
|
|
1284
|
-
[key]: PullRequestDiffState.Error({ error: errorMessage(error) }),
|
|
1285
|
-
}))
|
|
1286
|
-
flashNotice(errorMessage(error))
|
|
1287
|
-
})
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
useEffect(() => {
|
|
1291
|
-
if (!selectedPullRequest || diffFullView) return
|
|
1292
|
-
if (diffPrefetchTimeoutRef.current !== null) {
|
|
1293
|
-
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
1294
|
-
}
|
|
1295
|
-
diffPrefetchTimeoutRef.current = setTimeout(() => {
|
|
1296
|
-
loadPullRequestDiff(selectedPullRequest)
|
|
1297
|
-
}, 250)
|
|
1298
|
-
return () => {
|
|
1299
|
-
if (diffPrefetchTimeoutRef.current !== null) {
|
|
1300
|
-
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
1301
|
-
diffPrefetchTimeoutRef.current = null
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
}, [selectedIndex, selectedPullRequest?.url, diffFullView])
|
|
1305
|
-
|
|
1306
|
-
const openDiffView = () => {
|
|
1307
|
-
if (!selectedPullRequest) return
|
|
1308
|
-
diffRenderableRefs.current.clear()
|
|
1309
|
-
diffCommentLineColorsRef.current = { contextKey: null, entries: [] }
|
|
1310
|
-
setDiffFullView(true)
|
|
1311
|
-
setDetailFullView(false)
|
|
1312
|
-
setDiffFileIndex(0)
|
|
1313
|
-
setDiffScrollTop(0)
|
|
1314
|
-
setDiffCommentAnchorIndex(0)
|
|
1315
|
-
setDiffPreferredSide(null)
|
|
1316
|
-
setDiffCommentRangeStartIndex(null)
|
|
1317
|
-
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
1318
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1319
|
-
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
const setDiffRenderableRef = (index: number, diff: DiffRenderable | null) => {
|
|
1323
|
-
if (diff) diffRenderableRefs.current.set(index, diff)
|
|
1324
|
-
else diffRenderableRefs.current.delete(index)
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const scrollToDiffFile = (index: number) => {
|
|
1328
|
-
const stackedFile = stackedDiffFiles[index]
|
|
1329
|
-
diffScrollRef.current?.scrollTo({ x: 0, y: stackedFile?.headerLine ?? 0 })
|
|
1330
|
-
syncDiffScrollState()
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
const syncDiffScrollState = () => {
|
|
1334
|
-
const scrollTop = diffScrollRef.current?.scrollTop
|
|
1335
|
-
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
1336
|
-
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
1337
|
-
const nextIndex = Math.max(0, stackedDiffFileIndexAtLine(stackedDiffFiles, scrollTop))
|
|
1338
|
-
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
const syncDetailPreviewScrollState = useCallback(() => {
|
|
1342
|
-
const scrollTop = detailPreviewScrollRef.current?.scrollTop
|
|
1343
|
-
if (scrollTop === undefined) return
|
|
1344
|
-
const nextTop = Math.max(0, Math.floor(scrollTop))
|
|
1345
|
-
if (detailPreviewScrollTopRef.current === nextTop) return
|
|
1346
|
-
detailPreviewScrollTopRef.current = nextTop
|
|
1347
|
-
setDetailPreviewScrollTop(nextTop)
|
|
1348
|
-
}, [])
|
|
1349
|
-
|
|
1350
|
-
const scrollDetailPreviewBy = (y: number) => {
|
|
1351
|
-
detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1352
|
-
syncDetailPreviewScrollState()
|
|
1353
|
-
}
|
|
1354
|
-
const scrollDetailPreviewTo = (y: number) => {
|
|
1355
|
-
detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
1356
|
-
syncDetailPreviewScrollState()
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const ensureDiffLineVisible = (line: number) => {
|
|
1360
|
-
const scroll = diffScrollRef.current
|
|
1361
|
-
if (!scroll) return
|
|
1362
|
-
const viewportHeight = Math.max(1, wideBodyHeight - (selectedDiffCommentThread.length > 0 ? 6 : 3))
|
|
1363
|
-
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, line, DIFF_STICKY_HEADER_LINES)
|
|
1364
|
-
if (nextTop !== scroll.scrollTop) {
|
|
1365
|
-
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1366
|
-
syncDiffScrollState()
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
useEffect(() => {
|
|
1371
|
-
if (!diffFullView) return
|
|
1372
|
-
const interval = globalThis.setInterval(syncDiffScrollState, 80)
|
|
1373
|
-
return () => globalThis.clearInterval(interval)
|
|
1374
|
-
}, [diffFullView, stackedDiffFiles])
|
|
1375
|
-
|
|
1376
|
-
useLayoutEffect(() => {
|
|
1377
|
-
if (!isWideLayout || detailFullView || diffFullView) return
|
|
1378
|
-
const scroll = detailPreviewScrollRef.current
|
|
1379
|
-
if (!scroll) return
|
|
1380
|
-
const sync = () => { syncDetailPreviewScrollState() }
|
|
1381
|
-
scroll.verticalScrollBar.on("change", sync)
|
|
1382
|
-
syncDetailPreviewScrollState()
|
|
1383
|
-
return () => { scroll.verticalScrollBar.off("change", sync) }
|
|
1384
|
-
}, [isWideLayout, detailFullView, diffFullView, syncDetailPreviewScrollState])
|
|
1385
|
-
|
|
1386
|
-
const jumpDiffFile = (delta: 1 | -1) => {
|
|
1387
|
-
if (readyDiffFiles.length === 0) return
|
|
1388
|
-
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
1389
|
-
setDiffFileIndex(nextIndex)
|
|
1390
|
-
setDiffCommentRangeStartIndex(null)
|
|
1391
|
-
const targetSide = diffPreferredSide ?? selectedDiffCommentAnchor?.side
|
|
1392
|
-
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === targetSide)
|
|
1393
|
-
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
1394
|
-
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1395
|
-
scrollToDiffFile(nextIndex)
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const navigableDiffCommentAnchors = () => diffCommentRangeStartAnchor
|
|
1399
|
-
? diffCommentAnchors.filter((anchor) => sameDiffCommentTarget(anchor, diffCommentRangeStartAnchor))
|
|
1400
|
-
: diffCommentAnchors
|
|
1401
|
-
|
|
1402
|
-
const moveDiffCommentAnchor = (delta: number, options: { readonly preserveViewportRow?: boolean } = {}) => {
|
|
1403
|
-
const anchors = navigableDiffCommentAnchors()
|
|
1404
|
-
if (anchors.length === 0) return
|
|
1405
|
-
const currentAnchor = selectedDiffCommentAnchor && anchors.includes(selectedDiffCommentAnchor) ? selectedDiffCommentAnchor : anchors[0]
|
|
1406
|
-
const nextAnchor = verticalDiffAnchor(anchors, currentAnchor ?? null, delta, diffPreferredSide)
|
|
1407
|
-
if (!nextAnchor) return
|
|
1408
|
-
if (options.preserveViewportRow) {
|
|
1409
|
-
const scroll = diffScrollRef.current
|
|
1410
|
-
if (scroll && currentAnchor) {
|
|
1411
|
-
const maxScreenOffset = Math.max(DIFF_STICKY_HEADER_LINES, scroll.viewport.height - 2)
|
|
1412
|
-
const screenOffset = Math.max(DIFF_STICKY_HEADER_LINES, Math.min(maxScreenOffset, currentAnchor.renderLine - scroll.scrollTop))
|
|
1413
|
-
const maxScrollTop = Math.max(0, scroll.scrollHeight - scroll.viewport.height)
|
|
1414
|
-
const nextTop = Math.max(0, Math.min(maxScrollTop, nextAnchor.renderLine - screenOffset))
|
|
1415
|
-
suppressNextDiffCommentScrollRef.current = true
|
|
1416
|
-
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1417
|
-
syncDiffScrollState()
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const moveDiffCommentToBoundary = (boundary: "first" | "last") => {
|
|
1424
|
-
const anchors = navigableDiffCommentAnchors()
|
|
1425
|
-
const nextAnchor = boundary === "first" ? anchors[0] : anchors[anchors.length - 1]
|
|
1426
|
-
if (!nextAnchor) return
|
|
1427
|
-
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1428
|
-
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
const alignSelectedDiffCommentAnchor = (position: "top" | "center" | "bottom") => {
|
|
1432
|
-
if (!selectedDiffCommentAnchor) return
|
|
1433
|
-
const scroll = diffScrollRef.current
|
|
1434
|
-
if (!scroll) return
|
|
1435
|
-
const viewportHeight = Math.max(1, scroll.viewport.height)
|
|
1436
|
-
const offset = position === "top"
|
|
1437
|
-
? DIFF_STICKY_HEADER_LINES
|
|
1438
|
-
: position === "center"
|
|
1439
|
-
? Math.max(DIFF_STICKY_HEADER_LINES, Math.floor(viewportHeight / 2))
|
|
1440
|
-
: Math.max(DIFF_STICKY_HEADER_LINES, viewportHeight - 2)
|
|
1441
|
-
const maxScrollTop = Math.max(0, scroll.scrollHeight - viewportHeight)
|
|
1442
|
-
const nextTop = Math.max(0, Math.min(maxScrollTop, selectedDiffCommentAnchor.renderLine - offset))
|
|
1443
|
-
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1444
|
-
syncDiffScrollState()
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1448
|
-
setDiffPreferredSide(side)
|
|
1449
|
-
if (!selectedDiffCommentAnchor) return
|
|
1450
|
-
const nextAnchor = diffAnchorOnSide(diffCommentAnchors, selectedDiffCommentAnchor, side)
|
|
1451
|
-
if (!nextAnchor) return
|
|
1452
|
-
setDiffCommentRangeStartIndex(null)
|
|
1453
|
-
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
const selectDiffCommentLine = (renderLine: number, side: DiffCommentSide | null) => {
|
|
1457
|
-
const lineAnchors = diffCommentAnchors.filter((anchor) => anchor.renderLine === renderLine)
|
|
1458
|
-
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1459
|
-
if (!nextAnchor) return
|
|
1460
|
-
suppressNextDiffCommentScrollRef.current = true
|
|
1461
|
-
setDiffPreferredSide(side ?? nextAnchor.side)
|
|
1462
|
-
if (diffCommentRangeStartAnchor && !sameDiffCommentTarget(diffCommentRangeStartAnchor, nextAnchor)) {
|
|
1463
|
-
setDiffCommentRangeStartIndex(null)
|
|
1464
|
-
}
|
|
1465
|
-
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1466
|
-
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
1470
|
-
setCommentModal((current) => {
|
|
1471
|
-
const next = transform({ body: current.body, cursor: current.cursor })
|
|
1472
|
-
if (next.body === current.body && next.cursor === current.cursor && current.error === null) return current
|
|
1473
|
-
return { ...current, body: next.body, cursor: next.cursor, error: null }
|
|
1474
|
-
})
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
const openDiffCommentModal = () => {
|
|
1478
|
-
if (!selectedDiffCommentAnchor || !selectedPullRequest) return
|
|
1479
|
-
setCommentModal(initialCommentModalState)
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
const openDiffCommentThreadModal = () => {
|
|
1483
|
-
if (!selectedDiffCommentAnchor || selectedDiffCommentThread.length === 0) return
|
|
1484
|
-
setCommentThreadModal({ scrollOffset: 0 })
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const openSelectedDiffComment = () => {
|
|
1488
|
-
if (diffCommentRangeActive) {
|
|
1489
|
-
openDiffCommentModal()
|
|
1490
|
-
return
|
|
1491
|
-
}
|
|
1492
|
-
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
1493
|
-
else openDiffCommentModal()
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
const toggleDiffCommentRange = () => {
|
|
1497
|
-
if (!selectedDiffCommentAnchor) return
|
|
1498
|
-
setDiffCommentRangeStartIndex((current) => current === null ? selectedDiffCommentAnchorIndex : null)
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
const moveDiffCommentThread = (delta: 1 | -1) => {
|
|
1502
|
-
if (diffCommentThreadAnchors.length === 0) {
|
|
1503
|
-
flashNotice("No diff comments")
|
|
1504
|
-
return
|
|
1505
|
-
}
|
|
1506
|
-
const currentIndex = selectedDiffCommentAnchor
|
|
1507
|
-
? diffCommentThreadAnchors.findIndex((anchor) => diffCommentLocationKey(anchor) === diffCommentLocationKey(selectedDiffCommentAnchor))
|
|
1508
|
-
: -1
|
|
1509
|
-
const nextAnchor = currentIndex >= 0
|
|
1510
|
-
? diffCommentThreadAnchors[(currentIndex + delta + diffCommentThreadAnchors.length) % diffCommentThreadAnchors.length]
|
|
1511
|
-
: delta > 0
|
|
1512
|
-
? diffCommentThreadAnchors.find((anchor) => !selectedDiffCommentAnchor || anchor.renderLine > selectedDiffCommentAnchor.renderLine) ?? diffCommentThreadAnchors[0]
|
|
1513
|
-
: [...diffCommentThreadAnchors].reverse().find((anchor) => !selectedDiffCommentAnchor || anchor.renderLine < selectedDiffCommentAnchor.renderLine) ?? diffCommentThreadAnchors[diffCommentThreadAnchors.length - 1]
|
|
1514
|
-
if (!nextAnchor) return
|
|
1515
|
-
setDiffCommentRangeStartIndex(null)
|
|
1516
|
-
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1517
|
-
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
const submitDiffComment = () => {
|
|
1521
|
-
if (!selectedPullRequest || !selectedDiffCommentAnchor) return
|
|
1522
|
-
const body = commentModal.body.trim()
|
|
1523
|
-
if (body.length === 0) {
|
|
1524
|
-
setCommentModal((current) => ({ ...current, error: "Write a comment before saving." }))
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
const targetRange = selectedDiffCommentRange
|
|
1529
|
-
const target = targetRange?.end ?? selectedDiffCommentAnchor
|
|
1530
|
-
const threadKey = selectedDiffKey ? diffCommentThreadMapKey(selectedDiffKey, target) : null
|
|
1531
|
-
const optimisticComment = {
|
|
1532
|
-
id: `local:${Date.now()}`,
|
|
1533
|
-
path: target.path,
|
|
1534
|
-
line: target.line,
|
|
1535
|
-
side: target.side,
|
|
1536
|
-
author: username ?? "you",
|
|
1537
|
-
body,
|
|
1538
|
-
createdAt: new Date(),
|
|
1539
|
-
url: null,
|
|
1540
|
-
} satisfies PullRequestReviewComment
|
|
1541
|
-
const rangeInput = targetRange && targetRange.start.line !== targetRange.end.line
|
|
1542
|
-
? { startLine: targetRange.start.line, startSide: targetRange.start.side }
|
|
1543
|
-
: {}
|
|
1544
|
-
const input = {
|
|
1545
|
-
repository: selectedPullRequest.repository,
|
|
1546
|
-
number: selectedPullRequest.number,
|
|
1547
|
-
commitId: selectedPullRequest.headRefOid,
|
|
1548
|
-
path: target.path,
|
|
1549
|
-
line: target.line,
|
|
1550
|
-
side: target.side,
|
|
1551
|
-
body,
|
|
1552
|
-
...rangeInput,
|
|
1553
|
-
} satisfies CreatePullRequestCommentInput
|
|
1554
|
-
|
|
1555
|
-
if (threadKey) {
|
|
1556
|
-
setDiffCommentThreads((current) => ({
|
|
1557
|
-
...current,
|
|
1558
|
-
[threadKey]: [...(current[threadKey] ?? []), optimisticComment],
|
|
1559
|
-
}))
|
|
1560
|
-
}
|
|
1561
|
-
closeActiveModal()
|
|
1562
|
-
setDiffCommentRangeStartIndex(null)
|
|
1563
|
-
flashNotice(`Commenting on ${target.path}:${target.line}`)
|
|
1564
|
-
void createPullRequestComment(input).then((comment) => {
|
|
1565
|
-
if (threadKey) {
|
|
1566
|
-
setDiffCommentThreads((current) => ({
|
|
1567
|
-
...current,
|
|
1568
|
-
[threadKey]: (current[threadKey] ?? []).map((existing) => existing.id === optimisticComment.id ? comment : existing),
|
|
1569
|
-
}))
|
|
1570
|
-
}
|
|
1571
|
-
flashNotice(`Commented on ${target.path}:${target.line}`)
|
|
1572
|
-
}).catch((error) => {
|
|
1573
|
-
if (threadKey) {
|
|
1574
|
-
setDiffCommentThreads((current) => {
|
|
1575
|
-
const next = { ...current }
|
|
1576
|
-
const comments = (next[threadKey] ?? []).filter((comment) => comment.id !== optimisticComment.id)
|
|
1577
|
-
if (comments.length > 0) next[threadKey] = comments
|
|
1578
|
-
else delete next[threadKey]
|
|
1579
|
-
return next
|
|
1580
|
-
})
|
|
1581
|
-
}
|
|
1582
|
-
flashNotice(errorMessage(error))
|
|
1583
|
-
})
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
1587
|
-
void openInBrowser(pullRequest)
|
|
1588
|
-
.then(() => flashNotice(`Opened #${pullRequest.number} in browser`))
|
|
1589
|
-
.catch((error) => flashNotice(errorMessage(error)))
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
const copySelectedPullRequestMetadata = () => {
|
|
1593
|
-
if (!selectedPullRequest) return
|
|
1594
|
-
void copyToClipboard(pullRequestMetadataText(selectedPullRequest))
|
|
1595
|
-
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1596
|
-
.catch((error) => flashNotice(errorMessage(error)))
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
const toggleSelectedPullRequestDraftStatus = () => {
|
|
1600
|
-
if (!selectedPullRequest) return
|
|
1601
|
-
const previousPullRequest = selectedPullRequest
|
|
1602
|
-
const nextReviewStatus = selectedPullRequest.reviewStatus === "draft" ? "review" : "draft"
|
|
1603
|
-
updatePullRequest(selectedPullRequest.url, (pullRequest) => ({
|
|
1604
|
-
...pullRequest,
|
|
1605
|
-
reviewStatus: nextReviewStatus,
|
|
1606
|
-
}))
|
|
1607
|
-
void toggleDraftStatus({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, isDraft: selectedPullRequest.reviewStatus === "draft" })
|
|
1608
|
-
.then(() => {
|
|
1609
|
-
flashNotice(selectedPullRequest.reviewStatus === "draft" ? `Marked #${selectedPullRequest.number} ready` : `Marked #${selectedPullRequest.number} draft`)
|
|
1610
|
-
})
|
|
1611
|
-
.catch((error) => {
|
|
1612
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1613
|
-
flashNotice(errorMessage(error))
|
|
1614
|
-
})
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
const openCloseModal = () => {
|
|
1618
|
-
if (!selectedPullRequest || selectedPullRequest.state !== "open") return
|
|
1619
|
-
setCloseModal({
|
|
1620
|
-
repository: selectedPullRequest.repository,
|
|
1621
|
-
number: selectedPullRequest.number,
|
|
1622
|
-
title: selectedPullRequest.title,
|
|
1623
|
-
url: selectedPullRequest.url,
|
|
1624
|
-
running: false,
|
|
1625
|
-
error: null,
|
|
1626
|
-
})
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
const confirmClosePullRequest = () => {
|
|
1630
|
-
if (!closeModal.repository || closeModal.number === null || !closeModal.url || closeModal.running) return
|
|
1631
|
-
const { repository, number, url } = closeModal
|
|
1632
|
-
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.url === url)
|
|
1633
|
-
const previousPullRequest = targetPullRequest ?? null
|
|
1634
|
-
|
|
1635
|
-
setCloseModal((current) => ({ ...current, running: true, error: null }))
|
|
1636
|
-
void closePullRequest({ repository, number })
|
|
1637
|
-
.then(() => {
|
|
1638
|
-
if (previousPullRequest) markPullRequestCompleted(previousPullRequest, "closed")
|
|
1639
|
-
closeActiveModal()
|
|
1640
|
-
refreshPullRequests(`Closed #${number}`)
|
|
1641
|
-
})
|
|
1642
|
-
.catch((error) => {
|
|
1643
|
-
setCloseModal((current) => ({ ...current, running: false, error: errorMessage(error) }))
|
|
1644
|
-
flashNotice(errorMessage(error))
|
|
1645
|
-
})
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
const openThemeModal = () => {
|
|
1649
|
-
setThemeModal({
|
|
1650
|
-
query: "",
|
|
1651
|
-
filterMode: false,
|
|
1652
|
-
initialThemeId: themeId,
|
|
1653
|
-
})
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
const closeThemeModal = (confirm: boolean) => {
|
|
1657
|
-
const selectedTheme = themeDefinitions.find((theme) => theme.id === themeIdRef.current)
|
|
1658
|
-
if (!confirm) {
|
|
1659
|
-
setThemeId(themeModal.initialThemeId)
|
|
1660
|
-
} else if (selectedTheme) {
|
|
1661
|
-
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
1662
|
-
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
1663
|
-
}
|
|
1664
|
-
closeActiveModal()
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
const previewTheme = (id: ThemeId) => {
|
|
1668
|
-
if (id === themeIdRef.current) return
|
|
1669
|
-
themeIdRef.current = id
|
|
1670
|
-
setThemeId(id)
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
const toggleDiffWhitespaceMode = () => {
|
|
1674
|
-
const next = diffWhitespaceMode === "ignore" ? "show" : "ignore"
|
|
1675
|
-
setDiffWhitespaceMode(next)
|
|
1676
|
-
void Effect.runPromise(saveStoredDiffWhitespaceMode(next)).catch((error) => flashNotice(errorMessage(error)))
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
const moveThemeSelection = (delta: number) => {
|
|
1680
|
-
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
1681
|
-
if (filteredThemes.length === 0) return
|
|
1682
|
-
const currentIndex = Math.max(0, filteredThemes.findIndex((theme) => theme.id === themeIdRef.current))
|
|
1683
|
-
const selectedIndex = Math.max(0, Math.min(filteredThemes.length - 1, currentIndex + delta))
|
|
1684
|
-
if (selectedIndex === currentIndex) return
|
|
1685
|
-
const theme = filteredThemes[selectedIndex]
|
|
1686
|
-
if (theme) previewTheme(theme.id)
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
const updateThemeQuery = (query: string, options: { readonly previewFirst?: boolean; readonly filterMode?: boolean } = {}) => {
|
|
1690
|
-
const current = themeModalRef.current
|
|
1691
|
-
const next = {
|
|
1692
|
-
...current,
|
|
1693
|
-
query,
|
|
1694
|
-
filterMode: options.filterMode ?? current.filterMode,
|
|
1695
|
-
}
|
|
1696
|
-
if (next.query === current.query && next.filterMode === current.filterMode) return
|
|
1697
|
-
|
|
1698
|
-
themeModalRef.current = next
|
|
1699
|
-
setThemeModal(next)
|
|
1700
|
-
|
|
1701
|
-
if (options.previewFirst && query.trim().length > 0) {
|
|
1702
|
-
const firstTheme = filterThemeDefinitions(query)[0]
|
|
1703
|
-
if (firstTheme) previewTheme(firstTheme.id)
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
const editThemeQuery = (transform: (query: string) => string) => {
|
|
1708
|
-
updateThemeQuery(transform(themeModalRef.current.query), { previewFirst: true })
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
const openLabelModal = () => {
|
|
1712
|
-
if (!selectedPullRequest) return
|
|
1713
|
-
const repository = selectedPullRequest.repository
|
|
1714
|
-
const cachedLabels = registry.get(labelCacheAtom)[repository]
|
|
1715
|
-
if (cachedLabels) {
|
|
1716
|
-
setLabelModal({
|
|
1717
|
-
repository,
|
|
1718
|
-
query: "",
|
|
1719
|
-
selectedIndex: 0,
|
|
1720
|
-
availableLabels: cachedLabels,
|
|
1721
|
-
loading: false,
|
|
1722
|
-
})
|
|
1723
|
-
return
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
setLabelModal({ repository, query: "", selectedIndex: 0, availableLabels: [], loading: true })
|
|
1727
|
-
void loadRepoLabels(repository)
|
|
1728
|
-
.then((labels) => {
|
|
1729
|
-
setLabelCache((current) => ({ ...current, [repository]: labels }))
|
|
1730
|
-
setLabelModal((current) => current.repository === repository ? { ...current, availableLabels: labels, loading: false } : current)
|
|
1731
|
-
})
|
|
1732
|
-
.catch((error) => {
|
|
1733
|
-
setLabelModal((current) => current.repository === repository ? { ...current, loading: false } : current)
|
|
1734
|
-
flashNotice(errorMessage(error))
|
|
1735
|
-
})
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
const openMergeModal = () => {
|
|
1739
|
-
if (!selectedPullRequest) return
|
|
1740
|
-
const repository = selectedPullRequest.repository
|
|
1741
|
-
const number = selectedPullRequest.number
|
|
1742
|
-
const seededInfo = mergeInfoFromPullRequest(selectedPullRequest)
|
|
1743
|
-
setMergeModal({
|
|
1744
|
-
repository,
|
|
1745
|
-
number,
|
|
1746
|
-
selectedIndex: 0,
|
|
1747
|
-
loading: true,
|
|
1748
|
-
running: false,
|
|
1749
|
-
info: seededInfo,
|
|
1750
|
-
error: null,
|
|
1751
|
-
})
|
|
1752
|
-
void getPullRequestMergeInfo({ repository, number })
|
|
1753
|
-
.then((info) => {
|
|
1754
|
-
setMergeModal((current) => current.repository === repository && current.number === number
|
|
1755
|
-
? { ...current, loading: false, info, selectedIndex: 0 }
|
|
1756
|
-
: current)
|
|
1757
|
-
})
|
|
1758
|
-
.catch((error) => {
|
|
1759
|
-
setMergeModal((current) => current.repository === repository && current.number === number
|
|
1760
|
-
? { ...current, loading: false, error: errorMessage(error) }
|
|
1761
|
-
: current)
|
|
1762
|
-
})
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
const confirmMergeAction = () => {
|
|
1766
|
-
if (!mergeModal.info || mergeModal.loading || mergeModal.running) return
|
|
1767
|
-
const options = availableMergeActions(mergeModal.info)
|
|
1768
|
-
const option = options[mergeModal.selectedIndex]
|
|
1769
|
-
if (!option) return
|
|
1770
|
-
|
|
1771
|
-
const { repository, number } = mergeModal.info
|
|
1772
|
-
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.repository === repository && pullRequest.number === number)
|
|
1773
|
-
const previousPullRequest = targetPullRequest ?? null
|
|
1774
|
-
|
|
1775
|
-
if (targetPullRequest && option.optimisticAutoMergeEnabled !== undefined) {
|
|
1776
|
-
updatePullRequest(targetPullRequest.url, (pullRequest) => ({ ...pullRequest, autoMergeEnabled: option.optimisticAutoMergeEnabled! }))
|
|
1777
|
-
}
|
|
1778
|
-
if (targetPullRequest && option.optimisticState === "merged") markPullRequestCompleted(targetPullRequest, "merged")
|
|
1779
|
-
|
|
1780
|
-
closeActiveModal()
|
|
1781
|
-
void mergePullRequest({ repository, number, action: option.action })
|
|
1782
|
-
.then(() => {
|
|
1783
|
-
if (option.refreshOnSuccess) {
|
|
1784
|
-
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
1785
|
-
} else {
|
|
1786
|
-
flashNotice(`${option.pastTense} #${number}`)
|
|
1787
|
-
}
|
|
1788
|
-
})
|
|
1789
|
-
.catch((error) => {
|
|
1790
|
-
if (previousPullRequest) restoreOptimisticPullRequest(previousPullRequest)
|
|
1791
|
-
flashNotice(errorMessage(error))
|
|
1792
|
-
})
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
const toggleLabelAtIndex = () => {
|
|
1796
|
-
if (!selectedPullRequest) return
|
|
1797
|
-
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1798
|
-
const label = filtered[labelModal.selectedIndex]
|
|
1799
|
-
if (!label) return
|
|
1800
|
-
|
|
1801
|
-
const isActive = selectedPullRequest.labels.some((l) => l.name.toLowerCase() === label.name.toLowerCase())
|
|
1802
|
-
const previousPullRequest = selectedPullRequest
|
|
1803
|
-
|
|
1804
|
-
if (isActive) {
|
|
1805
|
-
updatePullRequest(selectedPullRequest.url, (pr) => ({
|
|
1806
|
-
...pr,
|
|
1807
|
-
labels: pr.labels.filter((l) => l.name.toLowerCase() !== label.name.toLowerCase()),
|
|
1808
|
-
}))
|
|
1809
|
-
void removePullRequestLabel({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, label: label.name })
|
|
1810
|
-
.then(() => flashNotice(`Removed ${label.name} from #${selectedPullRequest.number}`))
|
|
1811
|
-
.catch((error) => {
|
|
1812
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1813
|
-
flashNotice(errorMessage(error))
|
|
1814
|
-
})
|
|
1815
|
-
} else {
|
|
1816
|
-
updatePullRequest(selectedPullRequest.url, (pr) => ({
|
|
1817
|
-
...pr,
|
|
1818
|
-
labels: [...pr.labels, { name: label.name, color: label.color }],
|
|
1819
|
-
}))
|
|
1820
|
-
void addPullRequestLabel({ repository: selectedPullRequest.repository, number: selectedPullRequest.number, label: label.name })
|
|
1821
|
-
.then(() => flashNotice(`Added ${label.name} to #${selectedPullRequest.number}`))
|
|
1822
|
-
.catch((error) => {
|
|
1823
|
-
updatePullRequest(selectedPullRequest.url, () => previousPullRequest)
|
|
1824
|
-
flashNotice(errorMessage(error))
|
|
1825
|
-
})
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
const openCommandPalette = () => {
|
|
1830
|
-
setCommandPalette(initialCommandPaletteState)
|
|
1831
|
-
}
|
|
1832
|
-
const openRepositoryPicker = () => {
|
|
1833
|
-
setOpenRepositoryModal({ query: selectedRepository ?? "", error: null })
|
|
1834
|
-
}
|
|
1835
|
-
const openRepositoryFromInput = () => {
|
|
1836
|
-
const repository = parseRepositoryInput(openRepositoryModal.query)
|
|
1837
|
-
if (!repository) {
|
|
1838
|
-
setOpenRepositoryModal((current) => ({ ...current, error: "Enter a repository as owner/name or a GitHub URL." }))
|
|
1839
|
-
return
|
|
1840
|
-
}
|
|
1841
|
-
closeActiveModal()
|
|
1842
|
-
switchViewTo({ _tag: "Repository", repository })
|
|
1843
|
-
flashNotice(`Opened ${repository}`)
|
|
1844
|
-
}
|
|
1845
|
-
const insertPastedText = (text: string) => {
|
|
1846
|
-
if (text.length === 0) return false
|
|
1847
|
-
if (commandPaletteActive) {
|
|
1848
|
-
setCommandPalette((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1849
|
-
return true
|
|
1850
|
-
}
|
|
1851
|
-
if (openRepositoryModalActive) {
|
|
1852
|
-
setOpenRepositoryModal((current) => ({ ...current, query: current.query + singleLineText(text), error: null }))
|
|
1853
|
-
return true
|
|
1854
|
-
}
|
|
1855
|
-
if (themeModalActive && themeModal.filterMode) {
|
|
1856
|
-
editThemeQuery((query) => query + singleLineText(text))
|
|
1857
|
-
return true
|
|
1858
|
-
}
|
|
1859
|
-
if (commentModalActive) {
|
|
1860
|
-
editComment((state) => insertText(state, text.replace(/\r\n?/g, "\n")))
|
|
1861
|
-
return true
|
|
1862
|
-
}
|
|
1863
|
-
if (labelModalActive) {
|
|
1864
|
-
setLabelModal((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1865
|
-
return true
|
|
1866
|
-
}
|
|
1867
|
-
if (filterMode) {
|
|
1868
|
-
setFilterDraft((current) => current + singleLineText(text))
|
|
1869
|
-
return true
|
|
1870
|
-
}
|
|
1871
|
-
return false
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
useEffect(() => {
|
|
1875
|
-
const handlePaste = (event: PasteEvent) => {
|
|
1876
|
-
if (insertPastedText(pasteText(event))) event.preventDefault()
|
|
1877
|
-
}
|
|
1878
|
-
const keyInput = renderer.keyInput as unknown as {
|
|
1879
|
-
on: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1880
|
-
off: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1881
|
-
}
|
|
1882
|
-
keyInput.on("paste", handlePaste)
|
|
1883
|
-
return () => {
|
|
1884
|
-
keyInput.off("paste", handlePaste)
|
|
1885
|
-
}
|
|
1886
|
-
}, [renderer, commandPaletteActive, openRepositoryModalActive, themeModalActive, themeModal.filterMode, commentModalActive, labelModalActive, filterMode])
|
|
1887
|
-
|
|
1888
|
-
const appCommands: readonly AppCommand[] = buildAppCommands({
|
|
1889
|
-
pullRequestStatus,
|
|
1890
|
-
filterQuery,
|
|
1891
|
-
filterMode,
|
|
1892
|
-
selectedRepository,
|
|
1893
|
-
activeViews,
|
|
1894
|
-
activeView,
|
|
1895
|
-
loadedPullRequestCount,
|
|
1896
|
-
hasMorePullRequests,
|
|
1897
|
-
isLoadingMorePullRequests,
|
|
1898
|
-
selectedPullRequest,
|
|
1899
|
-
detailFullView,
|
|
1900
|
-
diffFullView,
|
|
1901
|
-
diffReady: selectedDiffState?._tag === "Ready",
|
|
1902
|
-
effectiveDiffRenderView,
|
|
1903
|
-
diffWrapMode,
|
|
1904
|
-
diffWhitespaceMode,
|
|
1905
|
-
readyDiffFileCount: readyDiffFiles.length,
|
|
1906
|
-
diffFileIndex,
|
|
1907
|
-
diffRangeActive: diffCommentRangeActive,
|
|
1908
|
-
selectedDiffCommentAnchorLabel: selectedDiffCommentLabel,
|
|
1909
|
-
selectedDiffCommentThreadCount: selectedDiffCommentThread.length,
|
|
1910
|
-
hasDiffCommentThreads: diffCommentThreadAnchors.length > 0,
|
|
1911
|
-
actions: {
|
|
1912
|
-
openCommandPalette,
|
|
1913
|
-
refreshPullRequests,
|
|
1914
|
-
openFilter: () => {
|
|
1915
|
-
setFilterDraft(filterQuery)
|
|
1916
|
-
setFilterMode(true)
|
|
1917
|
-
},
|
|
1918
|
-
clearFilter: () => {
|
|
1919
|
-
setFilterQuery("")
|
|
1920
|
-
setFilterDraft("")
|
|
1921
|
-
setFilterMode(false)
|
|
1922
|
-
},
|
|
1923
|
-
openThemeModal,
|
|
1924
|
-
openRepositoryPicker,
|
|
1925
|
-
loadMorePullRequests,
|
|
1926
|
-
switchViewTo,
|
|
1927
|
-
openDetails: () => {
|
|
1928
|
-
setDetailFullView(true)
|
|
1929
|
-
setDetailScrollOffset(0)
|
|
1930
|
-
},
|
|
1931
|
-
closeDetails: () => {
|
|
1932
|
-
setDetailFullView(false)
|
|
1933
|
-
setDetailScrollOffset(0)
|
|
1934
|
-
},
|
|
1935
|
-
openDiffView,
|
|
1936
|
-
closeDiffView: () => {
|
|
1937
|
-
setDiffFullView(false)
|
|
1938
|
-
setDiffCommentRangeStartIndex(null)
|
|
1939
|
-
},
|
|
1940
|
-
reloadDiff: () => {
|
|
1941
|
-
if (!selectedPullRequest) return
|
|
1942
|
-
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
1943
|
-
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1944
|
-
},
|
|
1945
|
-
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1946
|
-
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1947
|
-
toggleDiffWhitespaceMode,
|
|
1948
|
-
jumpDiffFile,
|
|
1949
|
-
openSelectedDiffComment,
|
|
1950
|
-
toggleDiffCommentRange,
|
|
1951
|
-
moveDiffCommentThread,
|
|
1952
|
-
openDiffCommentModal,
|
|
1953
|
-
togglePullRequestDraftStatus: toggleSelectedPullRequestDraftStatus,
|
|
1954
|
-
openLabelModal,
|
|
1955
|
-
openMergeModal,
|
|
1956
|
-
openCloseModal,
|
|
1957
|
-
openPullRequestInBrowser: () => {
|
|
1958
|
-
if (selectedPullRequest) openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1959
|
-
},
|
|
1960
|
-
copyPullRequestMetadata: copySelectedPullRequestMetadata,
|
|
1961
|
-
quit: () => renderer.destroy(),
|
|
1962
|
-
},
|
|
1963
|
-
})
|
|
1964
|
-
const runCommand = (command: AppCommand, options: { readonly notifyDisabled?: boolean; readonly closePalette?: boolean } = {}) => {
|
|
1965
|
-
if (!commandEnabled(command)) {
|
|
1966
|
-
if (options.notifyDisabled && command.disabledReason) flashNotice(command.disabledReason)
|
|
1967
|
-
return false
|
|
1968
|
-
}
|
|
1969
|
-
if (options.closePalette) closeActiveModal()
|
|
1970
|
-
command.run()
|
|
1971
|
-
return true
|
|
1972
|
-
}
|
|
1973
|
-
const runCommandById = (id: string, options: { readonly notifyDisabled?: boolean } = {}) => {
|
|
1974
|
-
const command = appCommands.find((entry) => entry.id === id)
|
|
1975
|
-
return command ? runCommand(command, options) : false
|
|
1976
|
-
}
|
|
1977
|
-
const runCommandByIdRef = useRef(runCommandById)
|
|
1978
|
-
runCommandByIdRef.current = runCommandById
|
|
1979
|
-
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1980
|
-
if (!commandPaletteActive) return []
|
|
1981
|
-
const repository = parseRepositoryInput(commandPalette.query)
|
|
1982
|
-
if (!repository || repository === selectedRepository) return []
|
|
1983
|
-
return [defineCommand({
|
|
1984
|
-
id: `view.repository.dynamic:${repository}`,
|
|
1985
|
-
title: `Open ${repository}`,
|
|
1986
|
-
scope: "View",
|
|
1987
|
-
subtitle: "Switch to this repository",
|
|
1988
|
-
run: () => switchViewTo({ _tag: "Repository", repository }),
|
|
1989
|
-
})]
|
|
1990
|
-
})()
|
|
1991
|
-
// Dynamic commands always pin to the top of the palette; they came directly from the
|
|
1992
|
-
// user's typed input so they shouldn't be filtered by fuzzy score against themselves.
|
|
1993
|
-
const staticPaletteCommands = commandPaletteActive
|
|
1994
|
-
? filterCommands(appCommands.filter((command) => command.id !== "command.open" && commandEnabled(command)), commandPalette.query)
|
|
1995
|
-
: []
|
|
1996
|
-
const commandPaletteCommands = commandPaletteActive
|
|
1997
|
-
? [
|
|
1998
|
-
...dynamicPaletteCommands,
|
|
1999
|
-
...(commandPalette.query.trim().length > 0 ? staticPaletteCommands : sortCommandsByScope(staticPaletteCommands)),
|
|
2000
|
-
]
|
|
2001
|
-
: []
|
|
2002
|
-
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
2003
|
-
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
2004
|
-
|
|
2005
|
-
// === Helpers used by the keymap layers ===
|
|
2006
|
-
const moveMergeSelection = (delta: -1 | 1) => setMergeModal((current) => {
|
|
2007
|
-
const max = Math.max(0, availableMergeActions(mergeModal.info).length - 1)
|
|
2008
|
-
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
2009
|
-
})
|
|
2010
|
-
const scrollCommentThread = (delta: number) => setCommentThreadModal((current) => ({
|
|
2011
|
-
...current,
|
|
2012
|
-
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
2013
|
-
}))
|
|
2014
|
-
const moveLabelSelection = (delta: -1 | 1) => setLabelModal((current) => {
|
|
2015
|
-
const max = Math.max(0, filterLabels(labelModal.availableLabels, labelModal.query).length - 1)
|
|
2016
|
-
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
2017
|
-
})
|
|
2018
|
-
const moveCommandPaletteSelection = (delta: -1 | 1) => setCommandPalette((current) => {
|
|
2019
|
-
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCommands)
|
|
2020
|
-
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2021
|
-
})
|
|
2022
|
-
const selectCommandPaletteIndex = (index: number) => setCommandPalette((current) => {
|
|
2023
|
-
const selectedIndex = clampCommandIndex(index, commandPaletteCommands)
|
|
2024
|
-
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2025
|
-
})
|
|
2026
|
-
const runCommandPaletteCommand = (command: AppCommand) => {
|
|
2027
|
-
runCommand(command, { notifyDisabled: true, closePalette: true })
|
|
2028
|
-
}
|
|
2029
|
-
const scrollDetailFullViewBy = (delta: number) => {
|
|
2030
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: delta })
|
|
2031
|
-
setDetailScrollOffset((current) => Math.max(0, current + delta))
|
|
2032
|
-
}
|
|
2033
|
-
const scrollDetailFullViewTo = (y: number) => {
|
|
2034
|
-
detailScrollRef.current?.scrollTo({ x: 0, y })
|
|
2035
|
-
setDetailScrollOffset(y)
|
|
2036
|
-
}
|
|
2037
|
-
const moveSelectedToPreviousGroup = () => setSelectedIndex((current) => {
|
|
2038
|
-
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2039
|
-
const currentGroup = getCurrentGroupIndex(current)
|
|
2040
|
-
if (currentGroup <= 0) return groupStarts[groupStarts.length - 1]!
|
|
2041
|
-
return groupStarts[currentGroup - 1]!
|
|
2042
|
-
})
|
|
2043
|
-
const moveSelectedToNextGroup = () => setSelectedIndex((current) => {
|
|
2044
|
-
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2045
|
-
const currentGroup = getCurrentGroupIndex(current)
|
|
2046
|
-
if (currentGroup >= groupStarts.length - 1) return groupStarts[0]!
|
|
2047
|
-
return groupStarts[currentGroup + 1]!
|
|
2048
|
-
})
|
|
2049
|
-
const stepSelected = (delta: number) => setSelectedIndex((current) => {
|
|
2050
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2051
|
-
return Math.max(0, Math.min(visiblePullRequests.length - 1, current + delta))
|
|
2052
|
-
})
|
|
2053
|
-
const stepSelectedDown = (count = 1) => {
|
|
2054
|
-
if (visiblePullRequests.length === 0) return
|
|
2055
|
-
if (selectedIndex + count >= visiblePullRequests.length && hasMorePullRequests) {
|
|
2056
|
-
loadMorePullRequests()
|
|
2057
|
-
}
|
|
2058
|
-
stepSelected(count)
|
|
2059
|
-
}
|
|
2060
|
-
const stepSelectedUp = (count = 1) => stepSelected(-count)
|
|
2061
|
-
const stepSelectedDownWithLoadMore = () => {
|
|
2062
|
-
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2063
|
-
loadMorePullRequests()
|
|
2064
|
-
return
|
|
2065
|
-
}
|
|
2066
|
-
setSelectedIndex((current) => {
|
|
2067
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2068
|
-
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
2069
|
-
})
|
|
2070
|
-
}
|
|
2071
|
-
const stepSelectedUpWrap = () => setSelectedIndex((current) => {
|
|
2072
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2073
|
-
return current <= 0 ? visiblePullRequests.length - 1 : current - 1
|
|
2074
|
-
})
|
|
2075
|
-
const handleQuitOrClose = () => {
|
|
2076
|
-
if (themeModalActive) {
|
|
2077
|
-
closeThemeModal(false)
|
|
2078
|
-
return
|
|
2079
|
-
}
|
|
2080
|
-
if (activeModal._tag !== "None") {
|
|
2081
|
-
closeActiveModal()
|
|
2082
|
-
return
|
|
2083
|
-
}
|
|
2084
|
-
runCommandById("app.quit")
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// === Build the keymap context ===
|
|
2088
|
-
const appCtx: AppCtx = {
|
|
2089
|
-
closeModalActive,
|
|
2090
|
-
mergeModalActive,
|
|
2091
|
-
commentThreadModalActive,
|
|
2092
|
-
labelModalActive,
|
|
2093
|
-
themeModalActive,
|
|
2094
|
-
openRepositoryModalActive,
|
|
2095
|
-
commentModalActive,
|
|
2096
|
-
commandPaletteActive,
|
|
2097
|
-
filterMode,
|
|
2098
|
-
diffFullView,
|
|
2099
|
-
detailFullView,
|
|
2100
|
-
textInputActive: commentModalActive
|
|
2101
|
-
|| commandPaletteActive
|
|
2102
|
-
|| openRepositoryModalActive
|
|
2103
|
-
|| labelModalActive
|
|
2104
|
-
|| filterMode
|
|
2105
|
-
|| (themeModalActive && themeModal.filterMode),
|
|
2106
|
-
closeModal: {
|
|
2107
|
-
closeModal: closeActiveModal,
|
|
2108
|
-
confirmClose: confirmClosePullRequest,
|
|
2109
|
-
},
|
|
2110
|
-
mergeModal: {
|
|
2111
|
-
availableActionCount: availableMergeActions(mergeModal.info).length,
|
|
2112
|
-
closeModal: closeActiveModal,
|
|
2113
|
-
confirmMerge: confirmMergeAction,
|
|
2114
|
-
moveSelection: moveMergeSelection,
|
|
2115
|
-
},
|
|
2116
|
-
commentThreadModal: {
|
|
2117
|
-
halfPage,
|
|
2118
|
-
closeModal: closeActiveModal,
|
|
2119
|
-
openInlineComment: openDiffCommentModal,
|
|
2120
|
-
scrollBy: scrollCommentThread,
|
|
2121
|
-
},
|
|
2122
|
-
labelModal: {
|
|
2123
|
-
closeModal: closeActiveModal,
|
|
2124
|
-
toggleSelected: toggleLabelAtIndex,
|
|
2125
|
-
moveSelection: moveLabelSelection,
|
|
2126
|
-
},
|
|
2127
|
-
themeModal: {
|
|
2128
|
-
filterMode: themeModal.filterMode,
|
|
2129
|
-
hasFilteredResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
2130
|
-
closeWithoutSaving: () => closeThemeModal(false),
|
|
2131
|
-
clearFilter: () => updateThemeQuery("", { filterMode: false }),
|
|
2132
|
-
enterFilterMode: () => updateThemeQuery("", { filterMode: true }),
|
|
2133
|
-
confirmSelection: () => closeThemeModal(true),
|
|
2134
|
-
moveSelection: moveThemeSelection,
|
|
2135
|
-
},
|
|
2136
|
-
openRepositoryModal: {
|
|
2137
|
-
closeModal: closeActiveModal,
|
|
2138
|
-
openFromInput: openRepositoryFromInput,
|
|
2139
|
-
},
|
|
2140
|
-
commentModal: {
|
|
2141
|
-
closeModal: closeActiveModal,
|
|
2142
|
-
submit: submitDiffComment,
|
|
2143
|
-
insertNewline: () => editComment((state) => insertText(state, "\n")),
|
|
2144
|
-
moveLeft: () => editComment(editorMoveLeft),
|
|
2145
|
-
moveRight: () => editComment(editorMoveRight),
|
|
2146
|
-
moveUp: () => editComment((state) => moveVertically(state, -1)),
|
|
2147
|
-
moveDown: () => editComment((state) => moveVertically(state, 1)),
|
|
2148
|
-
moveLineStart: () => editComment(moveLineStart),
|
|
2149
|
-
moveLineEnd: () => editComment(moveLineEnd),
|
|
2150
|
-
moveWordBackward: () => editComment(moveWordBackward),
|
|
2151
|
-
moveWordForward: () => editComment(moveWordForward),
|
|
2152
|
-
backspace: () => editComment(editorBackspace),
|
|
2153
|
-
deleteForward: () => editComment(editorDeleteForward),
|
|
2154
|
-
deleteWordBackward: () => editComment(deleteWordBackward),
|
|
2155
|
-
deleteWordForward: () => editComment(deleteWordForward),
|
|
2156
|
-
deleteToLineStart: () => editComment(deleteToLineStart),
|
|
2157
|
-
deleteToLineEnd: () => editComment(deleteToLineEnd),
|
|
2158
|
-
},
|
|
2159
|
-
commandPalette: {
|
|
2160
|
-
closeModal: closeActiveModal,
|
|
2161
|
-
runSelected: () => {
|
|
2162
|
-
if (selectedCommand) runCommandPaletteCommand(selectedCommand)
|
|
2163
|
-
},
|
|
2164
|
-
moveSelection: moveCommandPaletteSelection,
|
|
2165
|
-
},
|
|
2166
|
-
filterModeCtx: {
|
|
2167
|
-
cancel: () => {
|
|
2168
|
-
setFilterDraft(filterQuery)
|
|
2169
|
-
setFilterMode(false)
|
|
2170
|
-
},
|
|
2171
|
-
commit: () => {
|
|
2172
|
-
setFilterQuery(filterDraft)
|
|
2173
|
-
setFilterMode(false)
|
|
2174
|
-
},
|
|
2175
|
-
},
|
|
2176
|
-
diff: {
|
|
2177
|
-
halfPage,
|
|
2178
|
-
handleEscape: () => {
|
|
2179
|
-
if (diffCommentRangeActive) setDiffCommentRangeStartIndex(null)
|
|
2180
|
-
else runCommandById("diff.close")
|
|
2181
|
-
},
|
|
2182
|
-
openSelectedComment: openSelectedDiffComment,
|
|
2183
|
-
addComment: () => runCommandById("diff.add-comment"),
|
|
2184
|
-
toggleRange: () => runCommandById("diff.toggle-range"),
|
|
2185
|
-
toggleView: () => runCommandById("diff.toggle-view"),
|
|
2186
|
-
toggleWrap: () => runCommandById("diff.toggle-wrap"),
|
|
2187
|
-
reload: () => runCommandById("diff.reload"),
|
|
2188
|
-
nextThread: () => runCommandById("diff.next-thread"),
|
|
2189
|
-
previousThread: () => runCommandById("diff.previous-thread"),
|
|
2190
|
-
moveAnchor: moveDiffCommentAnchor,
|
|
2191
|
-
moveAnchorToBoundary: moveDiffCommentToBoundary,
|
|
2192
|
-
alignAnchor: alignSelectedDiffCommentAnchor,
|
|
2193
|
-
selectSide: selectDiffCommentSide,
|
|
2194
|
-
nextFile: () => runCommandById("diff.next-file"),
|
|
2195
|
-
previousFile: () => runCommandById("diff.previous-file"),
|
|
2196
|
-
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2197
|
-
},
|
|
2198
|
-
detail: {
|
|
2199
|
-
halfPage,
|
|
2200
|
-
scrollBy: scrollDetailFullViewBy,
|
|
2201
|
-
scrollTo: scrollDetailFullViewTo,
|
|
2202
|
-
closeDetail: () => runCommandById("detail.close"),
|
|
2203
|
-
openTheme: () => runCommandById("theme.open"),
|
|
2204
|
-
openDiff: () => runCommandById("diff.open"),
|
|
2205
|
-
closePullRequest: () => runCommandById("pull.close"),
|
|
2206
|
-
openLabels: () => runCommandById("pull.labels"),
|
|
2207
|
-
openMerge: () => runCommandById("pull.merge"),
|
|
2208
|
-
toggleDraft: () => runCommandById("pull.toggle-draft"),
|
|
2209
|
-
refresh: () => runCommandById("pull.refresh"),
|
|
2210
|
-
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2211
|
-
copyMetadata: () => runCommandById("pull.copy-metadata"),
|
|
2212
|
-
},
|
|
2213
|
-
listNav: {
|
|
2214
|
-
halfPage,
|
|
2215
|
-
visibleCount: visiblePullRequests.length,
|
|
2216
|
-
hasFilter: filterQuery.length > 0,
|
|
2217
|
-
canScrollDetailPreview: isWideLayout && selectedPullRequest !== null,
|
|
2218
|
-
runCommandById: (id) => { runCommandById(id) },
|
|
2219
|
-
switchQueueMode,
|
|
2220
|
-
scrollDetailPreviewBy,
|
|
2221
|
-
scrollDetailPreviewTo,
|
|
2222
|
-
clearFilter: () => { runCommandById("filter.clear") },
|
|
2223
|
-
stepSelected,
|
|
2224
|
-
stepSelectedUp,
|
|
2225
|
-
stepSelectedDown,
|
|
2226
|
-
stepSelectedUpWrap,
|
|
2227
|
-
stepSelectedDownWithLoadMore,
|
|
2228
|
-
moveSelectedToPreviousGroup,
|
|
2229
|
-
moveSelectedToNextGroup,
|
|
2230
|
-
setSelected: (index) => setSelectedIndex(index),
|
|
2231
|
-
},
|
|
2232
|
-
openCommandPalette: () => { runCommandById("command.open") },
|
|
2233
|
-
handleQuitOrClose,
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
useKeymap(appKeymap, appCtx, useOpenTuiSubscribe())
|
|
2237
|
-
|
|
2238
|
-
useKeyboard((key) => {
|
|
2239
|
-
if (commandPaletteActive) {
|
|
2240
|
-
if (isSingleLineInputKey(key)) {
|
|
2241
|
-
setCommandPalette((current) => {
|
|
2242
|
-
const query = editSingleLineInput(current.query, key) ?? current.query
|
|
2243
|
-
return current.query === query && current.selectedIndex === 0 ? current : { ...current, query, selectedIndex: 0 }
|
|
2244
|
-
})
|
|
2245
|
-
}
|
|
2246
|
-
return
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
if (openRepositoryModalActive) {
|
|
2250
|
-
if (isSingleLineInputKey(key)) {
|
|
2251
|
-
setOpenRepositoryModal((current) => ({
|
|
2252
|
-
...current,
|
|
2253
|
-
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
2254
|
-
error: null,
|
|
2255
|
-
}))
|
|
2256
|
-
}
|
|
2257
|
-
return
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// q / ctrl+c quit/close-modal logic now lives in the keymap layer
|
|
2261
|
-
// (handleQuitOrClose). This useKeyboard callback only handles raw text
|
|
2262
|
-
// input for modals that need character-by-character accumulation.
|
|
2263
|
-
|
|
2264
|
-
if (themeModalActive) {
|
|
2265
|
-
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
2266
|
-
editThemeQuery((query) => editSingleLineInput(query, key) ?? query)
|
|
2267
|
-
}
|
|
2268
|
-
return
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
if (commentModalActive) {
|
|
2272
|
-
const text = printableKeyText(key)
|
|
2273
|
-
if (text) editComment((state) => insertText(state, text))
|
|
2274
|
-
return
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
if (labelModalActive) {
|
|
2281
|
-
if (isSingleLineInputKey(key)) {
|
|
2282
|
-
setLabelModal((current) => ({
|
|
2283
|
-
...current,
|
|
2284
|
-
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
2285
|
-
selectedIndex: 0,
|
|
2286
|
-
}))
|
|
2287
|
-
}
|
|
2288
|
-
return
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
if (filterMode) {
|
|
2292
|
-
if (isSingleLineInputKey(key)) {
|
|
2293
|
-
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
})
|
|
2297
|
-
|
|
2298
|
-
if (isInitialLoading) {
|
|
2299
|
-
return (
|
|
2300
|
-
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
2301
|
-
<LoadingLogoPane content={detailPlaceholderContent} width={contentWidth} height={terminalHeight} frame={loadingFrame} />
|
|
2302
|
-
</box>
|
|
2303
|
-
)
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
2307
|
-
const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
|
|
2308
|
-
const fullscreenDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout)
|
|
2309
|
-
const fullscreenDetailBodyViewportHeight = Math.max(1, wideBodyHeight - fullscreenDetailHeaderHeight)
|
|
2310
|
-
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2311
|
-
const fullscreenDetailBodyScrollable = fullscreenDetailBodyHeight > fullscreenDetailBodyViewportHeight
|
|
2312
|
-
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2313
|
-
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2314
|
-
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2315
|
-
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2316
|
-
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows({
|
|
2317
|
-
pullRequest: selectedPullRequest,
|
|
2318
|
-
paneWidth: rightPaneWidth,
|
|
2319
|
-
showChecks: true,
|
|
2320
|
-
contentWidth: rightContentWidth,
|
|
2321
|
-
conversationItems: selectedConversationItems,
|
|
2322
|
-
conversationStatus: selectedConversationStatus,
|
|
2323
|
-
bodyScrollTop: detailPreviewScrollTop,
|
|
2324
|
-
bodyViewportHeight: wideDetailBodyViewportHeight,
|
|
2325
|
-
})
|
|
2326
|
-
|
|
2327
|
-
const prListProps = {
|
|
2328
|
-
groups: visibleGroups,
|
|
2329
|
-
selectedUrl: selectedPullRequest?.url ?? null,
|
|
2330
|
-
status: pullRequestStatus,
|
|
2331
|
-
error: pullRequestError,
|
|
2332
|
-
filterText: visibleFilterText,
|
|
2333
|
-
showFilterBar: filterMode || filterQuery.length > 0,
|
|
2334
|
-
isFilterEditing: filterMode,
|
|
2335
|
-
loadedCount: loadedPullRequestCount,
|
|
2336
|
-
hasMore: hasMorePullRequests,
|
|
2337
|
-
isLoadingMore: isLoadingMorePullRequests,
|
|
2338
|
-
loadingIndicator,
|
|
2339
|
-
onSelectPullRequest: selectPullRequestByUrl,
|
|
2340
|
-
} as const
|
|
2341
|
-
|
|
2342
|
-
const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
|
|
2343
|
-
const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
|
|
2344
|
-
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
2345
|
-
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2346
|
-
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
2347
|
-
const sizedModal = (minW: number, maxW: number, padX: number, maxH: number) => {
|
|
2348
|
-
const w = Math.min(maxW, Math.max(minW, contentWidth - padX))
|
|
2349
|
-
const h = Math.min(maxH, terminalHeight - 4)
|
|
2350
|
-
return { width: w, height: h, left: centeredOffset(contentWidth, w), top: centeredOffset(terminalHeight, h) }
|
|
2351
|
-
}
|
|
2352
|
-
const closeLayout = sizedModal(46, 68, 12, 12)
|
|
2353
|
-
const closeModalWidth = closeLayout.width
|
|
2354
|
-
const closeModalHeight = closeLayout.height
|
|
2355
|
-
const closeModalLeft = closeLayout.left
|
|
2356
|
-
const closeModalTop = closeLayout.top
|
|
2357
|
-
const commentLayout = sizedModal(46, 76, 8, 16)
|
|
2358
|
-
const commentModalWidth = commentLayout.width
|
|
2359
|
-
const commentModalHeight = commentLayout.height
|
|
2360
|
-
const commentModalLeft = commentLayout.left
|
|
2361
|
-
const commentModalTop = commentLayout.top
|
|
2362
|
-
const commentThreadLayout = sizedModal(50, 86, 8, 22)
|
|
2363
|
-
const commentThreadModalWidth = commentThreadLayout.width
|
|
2364
|
-
const commentThreadModalHeight = commentThreadLayout.height
|
|
2365
|
-
const commentThreadModalLeft = commentThreadLayout.left
|
|
2366
|
-
const commentThreadModalTop = commentThreadLayout.top
|
|
2367
|
-
const commentAnchorLabel = selectedDiffCommentAnchor && selectedDiffCommentLabel
|
|
2368
|
-
? `${selectedDiffCommentAnchor.path} ${selectedDiffCommentLabel}`
|
|
2369
|
-
: "No diff line selected"
|
|
2370
|
-
const mergeLayout = sizedModal(46, 68, 12, 16)
|
|
2371
|
-
const mergeModalWidth = mergeLayout.width
|
|
2372
|
-
const mergeModalHeight = mergeLayout.height
|
|
2373
|
-
const mergeModalLeft = mergeLayout.left
|
|
2374
|
-
const mergeModalTop = mergeLayout.top
|
|
2375
|
-
const themeLayout = sizedModal(38, 58, 12, 16)
|
|
2376
|
-
const themeModalWidth = themeLayout.width
|
|
2377
|
-
const themeModalHeight = themeLayout.height
|
|
2378
|
-
const themeModalLeft = themeLayout.left
|
|
2379
|
-
const themeModalTop = themeLayout.top
|
|
2380
|
-
const openRepositoryLayout = sizedModal(46, 76, 8, 8)
|
|
2381
|
-
const openRepositoryModalWidth = openRepositoryLayout.width
|
|
2382
|
-
const openRepositoryModalHeight = openRepositoryLayout.height
|
|
2383
|
-
const openRepositoryModalLeft = openRepositoryLayout.left
|
|
2384
|
-
const openRepositoryModalTop = openRepositoryLayout.top
|
|
2385
|
-
const commandPaletteLayout = sizedModal(50, 88, 8, 24)
|
|
2386
|
-
const commandPaletteWidth = commandPaletteLayout.width
|
|
2387
|
-
const commandPaletteHeight = commandPaletteLayout.height
|
|
2388
|
-
const commandPaletteLeft = commandPaletteLayout.left
|
|
2389
|
-
const commandPaletteTop = commandPaletteLayout.top
|
|
2390
|
-
|
|
2391
|
-
return (
|
|
2392
|
-
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
2393
|
-
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
2394
|
-
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
2395
|
-
</box>
|
|
2396
|
-
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2397
|
-
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┬" />
|
|
2398
|
-
) : (
|
|
2399
|
-
<Divider width={contentWidth} />
|
|
2400
|
-
)}
|
|
2401
|
-
{diffFullView ? (
|
|
2402
|
-
<PullRequestDiffPane
|
|
2403
|
-
pullRequest={selectedPullRequest}
|
|
2404
|
-
diffState={displayedDiffState}
|
|
2405
|
-
stackedFiles={stackedDiffFiles}
|
|
2406
|
-
scrollTop={diffScrollTop}
|
|
2407
|
-
view={effectiveDiffRenderView}
|
|
2408
|
-
whitespaceMode={diffWhitespaceMode}
|
|
2409
|
-
wrapMode={diffWrapMode}
|
|
2410
|
-
paneWidth={contentWidth}
|
|
2411
|
-
height={wideBodyHeight}
|
|
2412
|
-
loadingIndicator={loadingIndicator}
|
|
2413
|
-
scrollRef={diffScrollRef}
|
|
2414
|
-
setDiffRef={setDiffRenderableRef}
|
|
2415
|
-
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2416
|
-
selectedCommentLabel={selectedDiffCommentLabel}
|
|
2417
|
-
selectedCommentThread={selectedDiffCommentThread}
|
|
2418
|
-
onSelectCommentLine={selectDiffCommentLine}
|
|
2419
|
-
themeId={themeId}
|
|
2420
|
-
/>
|
|
2421
|
-
) : detailFullView && isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2422
|
-
<box flexGrow={1} flexDirection="column">
|
|
2423
|
-
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks={isWideLayout} />
|
|
2424
|
-
<LoadingPane content={detailLoadingContent} width={contentWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout))} />
|
|
2425
|
-
</box>
|
|
2426
|
-
) : isWideLayout && detailFullView ? (
|
|
2427
|
-
<box flexGrow={1} flexDirection="column">
|
|
2428
|
-
{selectedPullRequest ? (
|
|
2429
|
-
<>
|
|
2430
|
-
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks />
|
|
2431
|
-
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2432
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2433
|
-
</scrollbox>
|
|
2434
|
-
</>
|
|
2435
|
-
) : (
|
|
2436
|
-
<DetailsPane pullRequest={null} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2437
|
-
)}
|
|
2438
|
-
</box>
|
|
2439
|
-
) : isWideLayout ? (
|
|
2440
|
-
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
2441
|
-
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2442
|
-
<scrollbox ref={prListScrollRef} focusable={false} height={wideBodyHeight} flexGrow={0}>
|
|
2443
|
-
<box paddingLeft={sectionPadding} paddingRight={0}>
|
|
2444
|
-
<PullRequestList key={`wide-${leftContentWidth}`} {...prListProps} contentWidth={leftContentWidth} />
|
|
2445
|
-
</box>
|
|
2446
|
-
</scrollbox>
|
|
2447
|
-
</box>
|
|
2448
|
-
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
2449
|
-
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2450
|
-
{isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2451
|
-
<>
|
|
2452
|
-
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2453
|
-
<LoadingPane content={detailLoadingContent} width={rightPaneWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true))} />
|
|
2454
|
-
</>
|
|
2455
|
-
) : selectedPullRequest ? (
|
|
2456
|
-
<>
|
|
2457
|
-
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2458
|
-
<scrollbox ref={detailPreviewScrollRef} flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2459
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2460
|
-
</scrollbox>
|
|
2461
|
-
</>
|
|
2462
|
-
) : (
|
|
2463
|
-
<DetailPlaceholder content={detailPlaceholderContent} paneWidth={rightPaneWidth} />
|
|
2464
|
-
)}
|
|
2465
|
-
</box>
|
|
2466
|
-
</box>
|
|
2467
|
-
) : detailFullView ? (
|
|
2468
|
-
<box flexGrow={1} flexDirection="column">
|
|
2469
|
-
{selectedPullRequest ? (
|
|
2470
|
-
<>
|
|
2471
|
-
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} />
|
|
2472
|
-
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2473
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2474
|
-
</scrollbox>
|
|
2475
|
-
</>
|
|
2476
|
-
) : (
|
|
2477
|
-
<DetailsPane pullRequest={null} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2478
|
-
)}
|
|
2479
|
-
</box>
|
|
2480
|
-
) : (
|
|
2481
|
-
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2482
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2483
|
-
<Divider width={contentWidth} />
|
|
2484
|
-
<box flexGrow={1} flexDirection="column">
|
|
2485
|
-
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
2486
|
-
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2487
|
-
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
2488
|
-
</box>
|
|
2489
|
-
</scrollbox>
|
|
2490
|
-
</box>
|
|
2491
|
-
</box>
|
|
2492
|
-
)}
|
|
2493
|
-
|
|
2494
|
-
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2495
|
-
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┴" />
|
|
2496
|
-
) : (
|
|
2497
|
-
<Divider width={contentWidth} />
|
|
2498
|
-
)}
|
|
2499
|
-
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
|
|
2500
|
-
{footerNotice ? (
|
|
2501
|
-
<PlainLine text={footerNotice} fg={colors.count} />
|
|
2502
|
-
) : (
|
|
2503
|
-
<FooterHints
|
|
2504
|
-
filterEditing={filterMode}
|
|
2505
|
-
showFilterClear={filterMode || filterQuery.length > 0}
|
|
2506
|
-
detailFullView={detailFullView}
|
|
2507
|
-
diffFullView={diffFullView}
|
|
2508
|
-
diffRangeActive={diffCommentRangeActive}
|
|
2509
|
-
hasSelection={selectedPullRequest !== null}
|
|
2510
|
-
canCloseSelection={selectedPullRequest?.state === "open"}
|
|
2511
|
-
hasError={pullRequestStatus === "error"}
|
|
2512
|
-
isLoading={pullRequestStatus === "loading" || isRefreshingPullRequests || isHydratingPullRequestDetails || closeModal.running || mergeModal.running}
|
|
2513
|
-
loadingIndicator={loadingIndicator}
|
|
2514
|
-
retryProgress={retryProgress}
|
|
2515
|
-
/>
|
|
2516
|
-
)}
|
|
2517
|
-
</box>
|
|
2518
|
-
{labelModalActive ? (
|
|
2519
|
-
<LabelModal
|
|
2520
|
-
state={labelModal}
|
|
2521
|
-
currentLabels={selectedPullRequest?.labels ?? []}
|
|
2522
|
-
modalWidth={labelModalWidth}
|
|
2523
|
-
modalHeight={labelModalHeight}
|
|
2524
|
-
offsetLeft={labelModalLeft}
|
|
2525
|
-
offsetTop={labelModalTop}
|
|
2526
|
-
loadingIndicator={loadingIndicator}
|
|
2527
|
-
/>
|
|
2528
|
-
) : null}
|
|
2529
|
-
{closeModalActive ? (
|
|
2530
|
-
<CloseModal
|
|
2531
|
-
state={closeModal}
|
|
2532
|
-
modalWidth={closeModalWidth}
|
|
2533
|
-
modalHeight={closeModalHeight}
|
|
2534
|
-
offsetLeft={closeModalLeft}
|
|
2535
|
-
offsetTop={closeModalTop}
|
|
2536
|
-
loadingIndicator={loadingIndicator}
|
|
2537
|
-
/>
|
|
2538
|
-
) : null}
|
|
2539
|
-
{commentModalActive ? (
|
|
2540
|
-
<CommentModal
|
|
2541
|
-
state={commentModal}
|
|
2542
|
-
anchorLabel={commentAnchorLabel}
|
|
2543
|
-
modalWidth={commentModalWidth}
|
|
2544
|
-
modalHeight={commentModalHeight}
|
|
2545
|
-
offsetLeft={commentModalLeft}
|
|
2546
|
-
offsetTop={commentModalTop}
|
|
2547
|
-
/>
|
|
2548
|
-
) : null}
|
|
2549
|
-
{commentThreadModalActive ? (
|
|
2550
|
-
<CommentThreadModal
|
|
2551
|
-
state={commentThreadModal}
|
|
2552
|
-
anchorLabel={commentAnchorLabel}
|
|
2553
|
-
comments={selectedDiffCommentThread}
|
|
2554
|
-
modalWidth={commentThreadModalWidth}
|
|
2555
|
-
modalHeight={commentThreadModalHeight}
|
|
2556
|
-
offsetLeft={commentThreadModalLeft}
|
|
2557
|
-
offsetTop={commentThreadModalTop}
|
|
2558
|
-
/>
|
|
2559
|
-
) : null}
|
|
2560
|
-
{mergeModalActive ? (
|
|
2561
|
-
<MergeModal
|
|
2562
|
-
state={mergeModal}
|
|
2563
|
-
modalWidth={mergeModalWidth}
|
|
2564
|
-
modalHeight={mergeModalHeight}
|
|
2565
|
-
offsetLeft={mergeModalLeft}
|
|
2566
|
-
offsetTop={mergeModalTop}
|
|
2567
|
-
loadingIndicator={loadingIndicator}
|
|
2568
|
-
/>
|
|
2569
|
-
) : null}
|
|
2570
|
-
{themeModalActive ? (
|
|
2571
|
-
<ThemeModal
|
|
2572
|
-
state={themeModal}
|
|
2573
|
-
activeThemeId={themeId}
|
|
2574
|
-
modalWidth={themeModalWidth}
|
|
2575
|
-
modalHeight={themeModalHeight}
|
|
2576
|
-
offsetLeft={themeModalLeft}
|
|
2577
|
-
offsetTop={themeModalTop}
|
|
2578
|
-
/>
|
|
2579
|
-
) : null}
|
|
2580
|
-
{openRepositoryModalActive ? (
|
|
2581
|
-
<OpenRepositoryModal
|
|
2582
|
-
state={openRepositoryModal}
|
|
2583
|
-
modalWidth={openRepositoryModalWidth}
|
|
2584
|
-
modalHeight={openRepositoryModalHeight}
|
|
2585
|
-
offsetLeft={openRepositoryModalLeft}
|
|
2586
|
-
offsetTop={openRepositoryModalTop}
|
|
2587
|
-
/>
|
|
2588
|
-
) : null}
|
|
2589
|
-
{commandPaletteActive ? (
|
|
2590
|
-
<CommandPalette
|
|
2591
|
-
commands={commandPaletteCommands}
|
|
2592
|
-
query={commandPalette.query}
|
|
2593
|
-
selectedIndex={selectedCommandIndex}
|
|
2594
|
-
modalWidth={commandPaletteWidth}
|
|
2595
|
-
modalHeight={commandPaletteHeight}
|
|
2596
|
-
offsetLeft={commandPaletteLeft}
|
|
2597
|
-
offsetTop={commandPaletteTop}
|
|
2598
|
-
onSelectCommandIndex={selectCommandPaletteIndex}
|
|
2599
|
-
onRunCommand={runCommandPaletteCommand}
|
|
2600
|
-
/>
|
|
2601
|
-
) : null}
|
|
2602
|
-
</box>
|
|
2603
|
-
)
|
|
2604
|
-
}
|