@kitlangton/ghui 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +18 -7
- package/src/App.tsx +1075 -653
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +73 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +29 -20
- package/src/errors.ts +10 -0
- package/src/index.tsx +30 -3
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +290 -126
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +156 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +84 -179
- package/src/ui/PullRequestDiffPane.tsx +29 -50
- package/src/ui/PullRequestList.tsx +99 -45
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +34 -44
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +263 -295
- package/src/ui/primitives.tsx +90 -0
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
package/src/App.tsx
CHANGED
|
@@ -1,37 +1,67 @@
|
|
|
1
|
-
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import { useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
1
|
+
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
+
import { useBindings } from "@opentui/keymap/react"
|
|
3
4
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
5
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
5
6
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
6
7
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
7
|
-
import
|
|
8
|
+
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
9
|
+
import { useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
10
|
+
import { buildAppCommands } from "./appCommands.js"
|
|
11
|
+
import type { AppCommand } from "./commands.js"
|
|
12
|
+
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
8
13
|
import { config } from "./config.js"
|
|
9
|
-
import {
|
|
14
|
+
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
10
15
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
16
|
+
import { errorMessage } from "./errors.js"
|
|
11
17
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
12
18
|
import { Observability } from "./observability.js"
|
|
19
|
+
import { mergeCachedDetails } from "./pullRequestCache.js"
|
|
20
|
+
import { activePullRequestViews, initialPullRequestView, nextView, parseRepositoryInput, type PullRequestView, viewCacheKey, viewEquals, viewLabel, viewMode, viewRepository } from "./pullRequestViews.js"
|
|
21
|
+
import { BrowserOpener } from "./services/BrowserOpener.js"
|
|
22
|
+
import { Clipboard } from "./services/Clipboard.js"
|
|
23
|
+
import { CommandRunner } from "./services/CommandRunner.js"
|
|
13
24
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
25
|
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
15
|
-
import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
26
|
+
import { colors, filterThemeDefinitions, mixHex, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
16
27
|
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
17
|
-
import { buildStackedDiffFiles,
|
|
18
|
-
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane,
|
|
28
|
+
import { buildStackedDiffFiles, diffCommentLocationKey, getStackedDiffCommentAnchors, nearestDiffCommentAnchorIndex, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileAtLine, type DiffCommentAnchor, type DiffView, type DiffWrapMode, type StackedDiffCommentAnchor } from "./ui/diff.js"
|
|
29
|
+
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, getScrollableDetailBodyHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
19
30
|
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
20
31
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
21
|
-
import {
|
|
32
|
+
import { CommandPalette } from "./ui/CommandPalette.js"
|
|
33
|
+
import { CloseModal, CommentModal, CommentThreadModal, filterLabels, initialCloseModalState, initialCommandPaletteState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialOpenRepositoryModalState, initialThemeModalState, LabelModal, MergeModal, Modal, OpenRepositoryModal, ThemeModal, type CloseModalState, type CommandPaletteState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type OpenRepositoryModalState, type ThemeModalState } from "./ui/modals.js"
|
|
22
34
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
23
35
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
24
|
-
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
36
|
+
import { buildPullRequestListRows, pullRequestListRowIndex, PullRequestList } from "./ui/PullRequestList.js"
|
|
37
|
+
import { editSingleLineInput, isSingleLineInputKey, printableKeyText, singleLineText } from "./ui/singleLineInput.js"
|
|
25
38
|
|
|
26
|
-
const
|
|
27
|
-
|
|
39
|
+
const parseOptionalPositiveInt = (value: string | undefined, fallback: number | null) => {
|
|
40
|
+
if (value === undefined) return fallback
|
|
41
|
+
const parsed = Number.parseInt(value, 10)
|
|
42
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
43
|
+
}
|
|
28
44
|
|
|
45
|
+
const mockPrCount = parseOptionalPositiveInt(process.env.GHUI_MOCK_PR_COUNT, null)
|
|
46
|
+
const pullRequestPageSize = Math.min(100, parseOptionalPositiveInt(process.env.GHUI_PR_PAGE_SIZE, config.prPageSize) ?? config.prPageSize)
|
|
47
|
+
const githubServiceLayer = mockPrCount !== null
|
|
48
|
+
? (await import("./services/MockGitHubService.js")).MockGitHubService.layer({ prCount: mockPrCount, repoCount: parseOptionalPositiveInt(process.env.GHUI_MOCK_REPO_COUNT, 4) ?? 4 })
|
|
49
|
+
: GitHubService.layerNoDeps
|
|
50
|
+
|
|
51
|
+
const githubRuntime = Atom.runtime(
|
|
52
|
+
Layer.mergeAll(githubServiceLayer, Clipboard.layerNoDeps, BrowserOpener.layerNoDeps).pipe(
|
|
53
|
+
Layer.provide(CommandRunner.layer),
|
|
54
|
+
Layer.provideMerge(Observability.layer),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
29
58
|
|
|
30
59
|
interface PullRequestLoad {
|
|
31
|
-
readonly
|
|
60
|
+
readonly view: PullRequestView
|
|
32
61
|
readonly data: readonly PullRequestItem[]
|
|
33
62
|
readonly fetchedAt: Date | null
|
|
34
|
-
readonly
|
|
63
|
+
readonly endCursor: string | null
|
|
64
|
+
readonly hasNextPage: boolean
|
|
35
65
|
}
|
|
36
66
|
|
|
37
67
|
interface DetailPlaceholderInput {
|
|
@@ -66,87 +96,104 @@ interface AppliedDiffLineColorState {
|
|
|
66
96
|
readonly entries: readonly AppliedDiffLineColor[]
|
|
67
97
|
}
|
|
68
98
|
|
|
99
|
+
interface DetailHydration {
|
|
100
|
+
readonly token: symbol
|
|
101
|
+
notifyError: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
const PR_FETCH_RETRIES = 6
|
|
70
105
|
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
71
106
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
72
107
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
73
108
|
const DIFF_STICKY_HEADER_LINES = 2
|
|
74
109
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
deletions: cachedPullRequest.deletions,
|
|
88
|
-
changedFiles: cachedPullRequest.changedFiles,
|
|
89
|
-
checkStatus: cachedPullRequest.checkStatus,
|
|
90
|
-
checkSummary: cachedPullRequest.checkSummary,
|
|
91
|
-
checks: cachedPullRequest.checks,
|
|
92
|
-
detailLoaded: true,
|
|
93
|
-
} satisfies PullRequestItem
|
|
94
|
-
})
|
|
110
|
+
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
111
|
+
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
112
|
+
const LOAD_MORE_SCROLL_THRESHOLD = 3
|
|
113
|
+
const DETAIL_PREFETCH_BEHIND = 1
|
|
114
|
+
const DETAIL_PREFETCH_AHEAD = 3
|
|
115
|
+
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
116
|
+
const DETAIL_PREFETCH_DELAY_MS = 120
|
|
117
|
+
|
|
118
|
+
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
|
|
119
|
+
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
|
|
120
|
+
const mergedIncoming = mergeCachedDetails(incoming, existing)
|
|
121
|
+
return [...existing, ...mergedIncoming.filter((pullRequest) => !seen.has(pullRequest.url))]
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
const retryProgressAtom = Atom.make<RetryProgress>(initialRetryProgress).pipe(Atom.keepAlive)
|
|
98
|
-
const
|
|
99
|
-
const queueLoadCacheAtom = Atom.make<Partial<Record<
|
|
100
|
-
const queueSelectionAtom = Atom.make<Partial<Record<
|
|
125
|
+
const activeViewAtom = Atom.make<PullRequestView>(initialPullRequestView(config.repository)).pipe(Atom.keepAlive)
|
|
126
|
+
const queueLoadCacheAtom = Atom.make<Partial<Record<string, PullRequestLoad>>>({}).pipe(Atom.keepAlive)
|
|
127
|
+
const queueSelectionAtom = Atom.make<Partial<Record<string, number>>>({}).pipe(Atom.keepAlive)
|
|
128
|
+
const trimQueueLoadCache = (cache: Partial<Record<string, PullRequestLoad>>) => {
|
|
129
|
+
const repositoryKeys = Object.keys(cache).filter((key) => key.startsWith("repository:"))
|
|
130
|
+
if (repositoryKeys.length <= MAX_REPOSITORY_CACHE_ENTRIES) return cache
|
|
131
|
+
const remove = new Set(repositoryKeys.slice(0, repositoryKeys.length - MAX_REPOSITORY_CACHE_ENTRIES))
|
|
132
|
+
return Object.fromEntries(Object.entries(cache).filter(([key]) => !remove.has(key))) as Partial<Record<string, PullRequestLoad>>
|
|
133
|
+
}
|
|
101
134
|
const pullRequestsAtom = githubRuntime.atom(
|
|
102
135
|
GitHubService.use((github) =>
|
|
103
136
|
Effect.gen(function*() {
|
|
104
|
-
const
|
|
137
|
+
const view = yield* Atom.get(activeViewAtom)
|
|
138
|
+
const queueMode = viewMode(view)
|
|
139
|
+
const repository = viewRepository(view)
|
|
140
|
+
const cacheKey = viewCacheKey(view)
|
|
105
141
|
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
const page = yield* github.listOpenPullRequestPage({
|
|
143
|
+
mode: queueMode,
|
|
144
|
+
repository,
|
|
145
|
+
cursor: null,
|
|
146
|
+
pageSize: Math.min(pullRequestPageSize, config.prFetchLimit),
|
|
147
|
+
}).pipe(
|
|
148
|
+
Effect.tapError(() =>
|
|
149
|
+
Atom.update(retryProgressAtom, (current) => RetryProgress.Retrying({
|
|
150
|
+
attempt: Math.min(RetryProgress.$match(current, { Idle: () => 0, Retrying: ({ attempt }) => attempt }) + 1, PR_FETCH_RETRIES),
|
|
151
|
+
max: PR_FETCH_RETRIES,
|
|
152
|
+
}))
|
|
153
|
+
),
|
|
154
|
+
Effect.retry({ times: PR_FETCH_RETRIES, schedule: Schedule.exponential("300 millis", 2) }),
|
|
155
|
+
Effect.tapError(() => Atom.set(retryProgressAtom, initialRetryProgress)),
|
|
156
|
+
)
|
|
116
157
|
|
|
117
158
|
yield* Atom.set(retryProgressAtom, initialRetryProgress)
|
|
118
159
|
const cache = yield* Atom.get(queueLoadCacheAtom)
|
|
160
|
+
const existingLoad = cache[cacheKey]
|
|
161
|
+
const data = mergeCachedDetails(page.items, existingLoad?.data)
|
|
119
162
|
const load = {
|
|
120
|
-
|
|
121
|
-
data
|
|
163
|
+
view,
|
|
164
|
+
data,
|
|
122
165
|
fetchedAt: new Date(),
|
|
123
|
-
|
|
166
|
+
endCursor: page.endCursor,
|
|
167
|
+
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
124
168
|
} satisfies PullRequestLoad
|
|
125
|
-
|
|
169
|
+
const nextCache = { ...cache }
|
|
170
|
+
delete nextCache[cacheKey]
|
|
171
|
+
nextCache[cacheKey] = load
|
|
172
|
+
yield* Atom.set(queueLoadCacheAtom, trimQueueLoadCache(nextCache))
|
|
126
173
|
return load
|
|
127
174
|
})
|
|
128
175
|
),
|
|
129
176
|
).pipe(Atom.keepAlive)
|
|
130
|
-
const selectedIndexAtom = Atom.make(0)
|
|
131
|
-
const noticeAtom = Atom.make<string | null>(null)
|
|
132
|
-
const filterQueryAtom = Atom.make("")
|
|
133
|
-
const filterDraftAtom = Atom.make("")
|
|
134
|
-
const filterModeAtom = Atom.make(false)
|
|
135
|
-
const pendingGAtom = Atom.make(false)
|
|
136
|
-
const detailFullViewAtom = Atom.make(false)
|
|
137
|
-
const detailScrollOffsetAtom = Atom.make(0)
|
|
138
|
-
const diffFullViewAtom = Atom.make(false)
|
|
139
|
-
const diffFileIndexAtom = Atom.make(0)
|
|
140
|
-
const diffScrollTopAtom = Atom.make(0)
|
|
141
|
-
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
142
|
-
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
143
|
-
const diffCommentModeAtom = Atom.make(false)
|
|
144
|
-
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
177
|
+
const selectedIndexAtom = Atom.make(0)
|
|
178
|
+
const noticeAtom = Atom.make<string | null>(null)
|
|
179
|
+
const filterQueryAtom = Atom.make("")
|
|
180
|
+
const filterDraftAtom = Atom.make("")
|
|
181
|
+
const filterModeAtom = Atom.make(false)
|
|
182
|
+
const pendingGAtom = Atom.make(false)
|
|
183
|
+
const detailFullViewAtom = Atom.make(false)
|
|
184
|
+
const detailScrollOffsetAtom = Atom.make(0)
|
|
185
|
+
const diffFullViewAtom = Atom.make(false)
|
|
186
|
+
const diffFileIndexAtom = Atom.make(0)
|
|
187
|
+
const diffScrollTopAtom = Atom.make(0)
|
|
188
|
+
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
189
|
+
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
190
|
+
const diffCommentModeAtom = Atom.make(false)
|
|
191
|
+
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
145
192
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
146
193
|
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
147
194
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
148
195
|
|
|
149
|
-
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
196
|
+
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
150
197
|
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
151
198
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
152
199
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -157,12 +204,110 @@ const usernameAtom = githubRuntime.atom(
|
|
|
157
204
|
: Effect.succeed(config.author.replace(/^@/, "")),
|
|
158
205
|
).pipe(Atom.keepAlive)
|
|
159
206
|
|
|
207
|
+
const pullRequestLoadAtom = Atom.make((get) => {
|
|
208
|
+
const view = get(activeViewAtom)
|
|
209
|
+
const cacheKey = viewCacheKey(view)
|
|
210
|
+
const cache = get(queueLoadCacheAtom)
|
|
211
|
+
const result = get(pullRequestsAtom)
|
|
212
|
+
const resolved = AsyncResult.getOrElse(result, () => null)
|
|
213
|
+
return cache[cacheKey] ?? (resolved && viewCacheKey(resolved.view) === cacheKey ? resolved : null)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const isLoadingQueueModeAtom = Atom.make((get) => {
|
|
217
|
+
const cacheKey = viewCacheKey(get(activeViewAtom))
|
|
218
|
+
const resolved = AsyncResult.getOrElse(get(pullRequestsAtom), () => null)
|
|
219
|
+
return resolved !== null && viewCacheKey(resolved.view) !== cacheKey
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const pullRequestStatusAtom = Atom.make((get): LoadStatus => {
|
|
223
|
+
const result = get(pullRequestsAtom)
|
|
224
|
+
const load = get(pullRequestLoadAtom)
|
|
225
|
+
const isLoadingQueue = get(isLoadingQueueModeAtom)
|
|
226
|
+
if ((result.waiting || isLoadingQueue) && load === null) return "loading"
|
|
227
|
+
return AsyncResult.isFailure(result) ? "error" : "ready"
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const displayedPullRequestsAtom = Atom.make((get) => {
|
|
231
|
+
const load = get(pullRequestLoadAtom)
|
|
232
|
+
const overrides = get(pullRequestOverridesAtom)
|
|
233
|
+
const recentlyCompleted = get(recentlyCompletedPullRequestsAtom)
|
|
234
|
+
const source = load?.data ?? []
|
|
235
|
+
const seenUrls = new Set<string>()
|
|
236
|
+
const open = source.map((pullRequest) => {
|
|
237
|
+
seenUrls.add(pullRequest.url)
|
|
238
|
+
return recentlyCompleted[pullRequest.url] ?? overrides[pullRequest.url] ?? pullRequest
|
|
239
|
+
})
|
|
240
|
+
return [
|
|
241
|
+
...open,
|
|
242
|
+
...Object.values(recentlyCompleted).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
243
|
+
]
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const effectiveFilterQueryAtom = Atom.make((get) =>
|
|
247
|
+
(get(filterModeAtom) ? get(filterDraftAtom) : get(filterQueryAtom)).trim().toLowerCase(),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const filteredPullRequestsAtom = Atom.make((get) => {
|
|
251
|
+
const pullRequests = get(displayedPullRequestsAtom)
|
|
252
|
+
const query = get(effectiveFilterQueryAtom)
|
|
253
|
+
if (query.length === 0) return pullRequests
|
|
254
|
+
return pullRequests.flatMap((pullRequest) => {
|
|
255
|
+
const score = pullRequestFilterScore(pullRequest, query)
|
|
256
|
+
return score === null ? [] : [{ pullRequest, score }]
|
|
257
|
+
}).sort((left, right) =>
|
|
258
|
+
left.score - right.score || right.pullRequest.createdAt.getTime() - left.pullRequest.createdAt.getTime()
|
|
259
|
+
).map(({ pullRequest }) => pullRequest)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const visibleRepoOrderAtom = Atom.make((get) => {
|
|
263
|
+
const query = get(effectiveFilterQueryAtom)
|
|
264
|
+
if (query.length === 0) return [] as readonly string[]
|
|
265
|
+
return [...new Set(get(filteredPullRequestsAtom).map((pullRequest) => pullRequest.repository))]
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const visibleGroupsAtom = Atom.make((get) =>
|
|
269
|
+
groupBy(get(filteredPullRequestsAtom), (pullRequest) => pullRequest.repository, get(visibleRepoOrderAtom)),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const visiblePullRequestsAtom = Atom.make((get) => get(visibleGroupsAtom).flatMap(([, pullRequests]) => pullRequests))
|
|
273
|
+
|
|
274
|
+
const groupStartsAtom = Atom.make((get) => {
|
|
275
|
+
const groups = get(visibleGroupsAtom)
|
|
276
|
+
const starts: number[] = []
|
|
277
|
+
for (let index = 0; index < groups.length; index++) {
|
|
278
|
+
if (index === 0) starts.push(0)
|
|
279
|
+
else starts.push(starts[index - 1]! + groups[index - 1]![1].length)
|
|
280
|
+
}
|
|
281
|
+
return starts
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const selectedPullRequestAtom = Atom.make((get) => {
|
|
285
|
+
const pullRequests = get(visiblePullRequestsAtom)
|
|
286
|
+
const index = get(selectedIndexAtom)
|
|
287
|
+
return pullRequests[index] ?? null
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const selectedDiffKeyAtom = Atom.make((get) => {
|
|
291
|
+
const pullRequest = get(selectedPullRequestAtom)
|
|
292
|
+
return pullRequest ? pullRequestDiffKey(pullRequest) : null
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const selectedDiffStateAtom = Atom.make((get) => {
|
|
296
|
+
const key = get(selectedDiffKeyAtom)
|
|
297
|
+
if (!key) return undefined
|
|
298
|
+
return get(pullRequestDiffCacheAtom)[key]
|
|
299
|
+
})
|
|
300
|
+
|
|
160
301
|
const listRepoLabelsAtom = githubRuntime.fn<string>()((repository) =>
|
|
161
302
|
GitHubService.use((github) => github.listRepoLabels(repository))
|
|
162
303
|
)
|
|
163
|
-
const
|
|
164
|
-
GitHubService.use((github) => github.
|
|
304
|
+
const listOpenPullRequestPageAtom = githubRuntime.fn<ListPullRequestPageInput>()((input) =>
|
|
305
|
+
GitHubService.use((github) => github.listOpenPullRequestPage(input))
|
|
165
306
|
)
|
|
307
|
+
const pullRequestDetailsAtom = Atom.family((key: string) => {
|
|
308
|
+
const { repository, number } = parsePullRequestDetailAtomKey(key)
|
|
309
|
+
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDetails(repository, number)))
|
|
310
|
+
})
|
|
166
311
|
const addPullRequestLabelAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly label: string }>()((input) =>
|
|
167
312
|
GitHubService.use((github) => github.addPullRequestLabel(input.repository, input.number, input.label))
|
|
168
313
|
)
|
|
@@ -172,9 +317,10 @@ const removePullRequestLabelAtom = githubRuntime.fn<{ readonly repository: strin
|
|
|
172
317
|
const toggleDraftAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number; readonly isDraft: boolean }>()((input) =>
|
|
173
318
|
GitHubService.use((github) => github.toggleDraftStatus(input.repository, input.number, input.isDraft))
|
|
174
319
|
)
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
)
|
|
320
|
+
const pullRequestDiffAtom = Atom.family((key: string) => {
|
|
321
|
+
const { repository, number } = parsePullRequestDiffAtomKey(key)
|
|
322
|
+
return githubRuntime.atom(GitHubService.use((github) => github.getPullRequestDiff(repository, number)))
|
|
323
|
+
})
|
|
178
324
|
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
179
325
|
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
180
326
|
)
|
|
@@ -188,12 +334,13 @@ const closePullRequestAtom = githubRuntime.fn<{ readonly repository: string; rea
|
|
|
188
334
|
GitHubService.use((github) => github.closePullRequest(input.repository, input.number))
|
|
189
335
|
)
|
|
190
336
|
const createPullRequestCommentAtom = githubRuntime.fn<CreatePullRequestCommentInput>()((input) => GitHubService.use((github) => github.createPullRequestComment(input)))
|
|
337
|
+
const copyToClipboardAtom = githubRuntime.fn<string>()((text) => Clipboard.use((clipboard) => clipboard.copy(text)))
|
|
338
|
+
const openInBrowserAtom = githubRuntime.fn<PullRequestItem>()((pullRequest) => BrowserOpener.use((browser) => browser.openPullRequest(pullRequest)))
|
|
191
339
|
|
|
192
340
|
const centeredOffset = (outer: number, inner: number) => Math.floor((outer - inner) / 2)
|
|
193
341
|
|
|
194
|
-
const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
195
342
|
|
|
196
|
-
const
|
|
343
|
+
const pasteText = (event: PasteEvent) => new TextDecoder().decode(event.bytes)
|
|
197
344
|
|
|
198
345
|
const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) => {
|
|
199
346
|
const normalized = query.trim().toLowerCase()
|
|
@@ -210,91 +357,34 @@ const pullRequestFilterScore = (pullRequest: PullRequestItem, query: string) =>
|
|
|
210
357
|
return scores.length > 0 ? Math.min(...scores) : null
|
|
211
358
|
}
|
|
212
359
|
|
|
213
|
-
const
|
|
214
|
-
if (process.platform === "darwin") return [["pbcopy"]]
|
|
215
|
-
if (process.platform === "linux") {
|
|
216
|
-
return [
|
|
217
|
-
...(process.env.WAYLAND_DISPLAY ? [["wl-copy"]] : []),
|
|
218
|
-
["xclip", "-selection", "clipboard"],
|
|
219
|
-
["xsel", "--clipboard", "--input"],
|
|
220
|
-
]
|
|
221
|
-
}
|
|
222
|
-
return []
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const copyToClipboard = async (text: string) => {
|
|
226
|
-
const commands = clipboardCommands()
|
|
227
|
-
let lastError = ""
|
|
228
|
-
|
|
229
|
-
for (const command of commands) {
|
|
230
|
-
let proc: Bun.Subprocess<"pipe", "ignore", "pipe">
|
|
231
|
-
try {
|
|
232
|
-
proc = Bun.spawn({
|
|
233
|
-
cmd: [...command],
|
|
234
|
-
stdin: "pipe",
|
|
235
|
-
stdout: "ignore",
|
|
236
|
-
stderr: "pipe",
|
|
237
|
-
})
|
|
238
|
-
} catch (error) {
|
|
239
|
-
lastError = errorMessage(error)
|
|
240
|
-
continue
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
proc.stdin.write(text)
|
|
244
|
-
proc.stdin.end()
|
|
245
|
-
|
|
246
|
-
const exitCode = await proc.exited
|
|
247
|
-
if (exitCode === 0) return
|
|
248
|
-
|
|
249
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
250
|
-
lastError = stderr.trim()
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const installHint = process.platform === "linux" ? " Install wl-clipboard, xclip, or xsel." : ""
|
|
254
|
-
throw new Error(lastError || `Clipboard is not available.${installHint}`)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const openPullRequestInBrowser = async (pullRequest: PullRequestItem) => {
|
|
258
|
-
const proc = Bun.spawn({
|
|
259
|
-
cmd: ["gh", "pr", "view", String(pullRequest.number), "--repo", pullRequest.repository, "--web"],
|
|
260
|
-
stdout: "ignore",
|
|
261
|
-
stderr: "pipe",
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
const exitCode = await proc.exited
|
|
265
|
-
if (exitCode === 0) return
|
|
266
|
-
|
|
267
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
268
|
-
throw new Error(stderr.trim() || "Could not open PR in browser")
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
|
|
360
|
+
const pullRequestMetadataText = (pullRequest: PullRequestItem) => {
|
|
272
361
|
const lines = [
|
|
273
362
|
pullRequest.title,
|
|
274
363
|
`${pullRequest.repository} #${pullRequest.number}`,
|
|
275
364
|
pullRequest.url,
|
|
276
365
|
]
|
|
277
|
-
|
|
278
366
|
const review = reviewLabel(pullRequest)
|
|
279
|
-
if (review) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
lines.push(pullRequest.checkSummary)
|
|
284
|
-
}
|
|
367
|
+
if (review) lines.push(`review: ${review}`)
|
|
368
|
+
if (pullRequest.checkSummary) lines.push(pullRequest.checkSummary)
|
|
369
|
+
return lines.join("\n")
|
|
370
|
+
}
|
|
285
371
|
|
|
286
|
-
|
|
372
|
+
const pullRequestDetailKey = (pullRequest: PullRequestItem) => `${pullRequest.url}:${pullRequest.headRefOid}`
|
|
373
|
+
const pullRequestRevisionAtomKey = (pullRequest: PullRequestItem) => `${pullRequest.repository}\u0000${pullRequest.number}\u0000${pullRequest.headRefOid}`
|
|
374
|
+
const parsePullRequestRevisionAtomKey = (key: string, label: string) => {
|
|
375
|
+
const [repository, number] = key.split("\u0000")
|
|
376
|
+
if (!repository || !number) throw new Error(`Invalid pull request ${label} key: ${key}`)
|
|
377
|
+
return { repository, number: Number.parseInt(number, 10) }
|
|
287
378
|
}
|
|
379
|
+
const pullRequestDetailAtomKey = pullRequestRevisionAtomKey
|
|
380
|
+
const pullRequestDiffAtomKey = pullRequestRevisionAtomKey
|
|
381
|
+
const parsePullRequestDetailAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "detail")
|
|
382
|
+
const parsePullRequestDiffAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "diff")
|
|
288
383
|
|
|
289
384
|
const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
|
|
290
385
|
|
|
291
386
|
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
292
387
|
|
|
293
|
-
const nextQueueMode = (mode: PullRequestQueueMode, delta: 1 | -1) => {
|
|
294
|
-
const index = pullRequestQueueModes.indexOf(mode)
|
|
295
|
-
return pullRequestQueueModes[(index + delta + pullRequestQueueModes.length) % pullRequestQueueModes.length]!
|
|
296
|
-
}
|
|
297
|
-
|
|
298
388
|
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
299
389
|
`${pullRequestDiffKey(pullRequest)}:${diffCommentLocationKey(comment)}`
|
|
300
390
|
|
|
@@ -321,28 +411,11 @@ const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig =
|
|
|
321
411
|
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
322
412
|
}
|
|
323
413
|
|
|
324
|
-
const mixHexColor = (color: string, base: string, amount: number) => {
|
|
325
|
-
const parse = (hex: string) => {
|
|
326
|
-
const normalized = hex.replace(/^#/, "")
|
|
327
|
-
if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null
|
|
328
|
-
return {
|
|
329
|
-
r: Number.parseInt(normalized.slice(0, 2), 16),
|
|
330
|
-
g: Number.parseInt(normalized.slice(2, 4), 16),
|
|
331
|
-
b: Number.parseInt(normalized.slice(4, 6), 16),
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const left = parse(color)
|
|
335
|
-
const right = parse(base)
|
|
336
|
-
if (!left || !right) return color
|
|
337
|
-
const channel = (key: "r" | "g" | "b") => Math.round(left[key] * amount + right[key] * (1 - amount)).toString(16).padStart(2, "0")
|
|
338
|
-
return `#${channel("r")}${channel("g")}${channel("b")}`
|
|
339
|
-
}
|
|
340
|
-
|
|
341
414
|
const diffCommentGutterColor = (anchor: DiffCommentAnchor, kind: "selected" | "thread") => {
|
|
342
415
|
const accent = kind === "thread"
|
|
343
416
|
? colors.status.pending
|
|
344
417
|
: anchor.side === "RIGHT" ? colors.status.passing : colors.status.failing
|
|
345
|
-
return
|
|
418
|
+
return mixHex(originalDiffLineColor(anchor).gutter, accent, 0.45)
|
|
346
419
|
}
|
|
347
420
|
|
|
348
421
|
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
@@ -404,11 +477,12 @@ const getDetailPlaceholderContent = ({
|
|
|
404
477
|
export const App = () => {
|
|
405
478
|
const renderer = useRenderer()
|
|
406
479
|
const { width, height } = useTerminalDimensions()
|
|
480
|
+
const registry = useContext(RegistryContext)
|
|
407
481
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
408
482
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
409
|
-
const [
|
|
410
|
-
const
|
|
411
|
-
const
|
|
483
|
+
const [activeView, setActiveView] = useAtom(activeViewAtom)
|
|
484
|
+
const setQueueLoadCache = useAtomSet(queueLoadCacheAtom)
|
|
485
|
+
const setQueueSelection = useAtomSet(queueSelectionAtom)
|
|
412
486
|
const [selectedIndex, setSelectedIndex] = useAtom(selectedIndexAtom)
|
|
413
487
|
const [notice, setNotice] = useAtom(noticeAtom)
|
|
414
488
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
@@ -416,7 +490,7 @@ export const App = () => {
|
|
|
416
490
|
const [filterMode, setFilterMode] = useAtom(filterModeAtom)
|
|
417
491
|
const [pendingG, setPendingG] = useAtom(pendingGAtom)
|
|
418
492
|
const [detailFullView, setDetailFullView] = useAtom(detailFullViewAtom)
|
|
419
|
-
const
|
|
493
|
+
const setDetailScrollOffset = useAtomSet(detailScrollOffsetAtom)
|
|
420
494
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
421
495
|
const [diffFileIndex, setDiffFileIndex] = useAtom(diffFileIndexAtom)
|
|
422
496
|
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
@@ -425,8 +499,8 @@ export const App = () => {
|
|
|
425
499
|
const [diffCommentMode, setDiffCommentMode] = useAtom(diffCommentModeAtom)
|
|
426
500
|
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
427
501
|
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
428
|
-
const
|
|
429
|
-
const
|
|
502
|
+
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
503
|
+
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
430
504
|
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
431
505
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
432
506
|
const closeActiveModal = () => setActiveModal(initialModal)
|
|
@@ -436,12 +510,16 @@ export const App = () => {
|
|
|
436
510
|
const commentModalActive = Modal.$is("Comment")(activeModal)
|
|
437
511
|
const commentThreadModalActive = Modal.$is("CommentThread")(activeModal)
|
|
438
512
|
const themeModalActive = Modal.$is("Theme")(activeModal)
|
|
513
|
+
const commandPaletteActive = Modal.$is("CommandPalette")(activeModal)
|
|
514
|
+
const openRepositoryModalActive = Modal.$is("OpenRepository")(activeModal)
|
|
439
515
|
const labelModal: LabelModalState = labelModalActive ? activeModal : initialLabelModalState
|
|
440
516
|
const closeModal: CloseModalState = closeModalActive ? activeModal : initialCloseModalState
|
|
441
517
|
const mergeModal: MergeModalState = mergeModalActive ? activeModal : initialMergeModalState
|
|
442
518
|
const commentModal: CommentModalState = commentModalActive ? activeModal : initialCommentModalState
|
|
443
519
|
const commentThreadModal: CommentThreadModalState = commentThreadModalActive ? activeModal : initialCommentThreadModalState
|
|
444
520
|
const themeModal: ThemeModalState = themeModalActive ? activeModal : initialThemeModalState
|
|
521
|
+
const commandPalette: CommandPaletteState = commandPaletteActive ? activeModal : initialCommandPaletteState
|
|
522
|
+
const openRepositoryModal: OpenRepositoryModalState = openRepositoryModalActive ? activeModal : initialOpenRepositoryModalState
|
|
445
523
|
const makeModalSetter = <Tag extends Exclude<ModalTag, "None">>(tag: Tag) =>
|
|
446
524
|
(next: ModalState<Tag> | ((prev: ModalState<Tag>) => ModalState<Tag>)) => setActiveModal((current) => {
|
|
447
525
|
const ctor = Modal[tag] as unknown as (args: ModalState<Tag>) => Modal
|
|
@@ -458,31 +536,35 @@ export const App = () => {
|
|
|
458
536
|
const setCommentModal = makeModalSetter("Comment")
|
|
459
537
|
const setCommentThreadModal = makeModalSetter("CommentThread")
|
|
460
538
|
const setThemeModal = makeModalSetter("Theme")
|
|
539
|
+
const setCommandPalette = makeModalSetter("CommandPalette")
|
|
540
|
+
const setOpenRepositoryModal = makeModalSetter("OpenRepository")
|
|
461
541
|
setActiveTheme(themeId)
|
|
462
542
|
const themeIdRef = useRef(themeId)
|
|
463
543
|
const themeModalRef = useRef(themeModal)
|
|
464
544
|
themeIdRef.current = themeId
|
|
465
545
|
themeModalRef.current = themeModal
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
const
|
|
546
|
+
const setLabelCache = useAtomSet(labelCacheAtom)
|
|
547
|
+
const setPullRequestOverrides = useAtomSet(pullRequestOverridesAtom)
|
|
548
|
+
const setRecentlyCompletedPullRequests = useAtomSet(recentlyCompletedPullRequestsAtom)
|
|
469
549
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
470
550
|
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
471
551
|
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
472
552
|
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
473
553
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
554
|
+
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
474
555
|
const usernameResult = useAtomValue(usernameAtom)
|
|
475
556
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
476
|
-
const
|
|
557
|
+
const loadPullRequestPage = useAtomSet(listOpenPullRequestPageAtom, { mode: "promise" })
|
|
477
558
|
const addPullRequestLabel = useAtomSet(addPullRequestLabelAtom, { mode: "promise" })
|
|
478
559
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
479
560
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
480
|
-
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
481
561
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
482
562
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
483
563
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
484
564
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
485
565
|
const createPullRequestComment = useAtomSet(createPullRequestCommentAtom, { mode: "promise" })
|
|
566
|
+
const copyToClipboard = useAtomSet(copyToClipboardAtom, { mode: "promise" })
|
|
567
|
+
const openInBrowser = useAtomSet(openInBrowserAtom, { mode: "promise" })
|
|
486
568
|
const terminalWidth = width ?? 100
|
|
487
569
|
const terminalHeight = height ?? 24
|
|
488
570
|
const contentWidth = Math.max(1, terminalWidth)
|
|
@@ -492,14 +574,15 @@ export const App = () => {
|
|
|
492
574
|
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
|
|
493
575
|
const rightPaneWidth = isWideLayout ? Math.max(28, contentWidth - leftPaneWidth - splitGap) : contentWidth
|
|
494
576
|
const dividerJunctionAt = Math.max(1, leftPaneWidth)
|
|
495
|
-
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth -
|
|
577
|
+
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
496
578
|
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
497
|
-
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
579
|
+
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
498
580
|
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
499
581
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
500
582
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
501
583
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
502
|
-
const
|
|
584
|
+
const detailPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
585
|
+
const detailHydrationRef = useRef(new Map<string, DetailHydration>())
|
|
503
586
|
const refreshGenerationRef = useRef(0)
|
|
504
587
|
const didMountQueueModeRef = useRef(false)
|
|
505
588
|
const lastPullRequestRefreshAtRef = useRef(0)
|
|
@@ -509,7 +592,9 @@ export const App = () => {
|
|
|
509
592
|
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
510
593
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
511
594
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
595
|
+
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
512
596
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
597
|
+
const prListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
513
598
|
const diffRenderableRefs = useRef(new Map<number, DiffRenderable>())
|
|
514
599
|
const diffCommentLineColorsRef = useRef<AppliedDiffLineColorState>({ contextKey: null, entries: [] })
|
|
515
600
|
const suppressNextDiffCommentScrollRef = useRef(false)
|
|
@@ -530,6 +615,8 @@ export const App = () => {
|
|
|
530
615
|
}, [renderer, themeId])
|
|
531
616
|
|
|
532
617
|
useEffect(() => () => {
|
|
618
|
+
refreshGenerationRef.current += 1
|
|
619
|
+
detailHydrationRef.current.clear()
|
|
533
620
|
if (noticeTimeoutRef.current !== null) {
|
|
534
621
|
clearTimeout(noticeTimeoutRef.current)
|
|
535
622
|
}
|
|
@@ -539,100 +626,83 @@ export const App = () => {
|
|
|
539
626
|
if (diffPrefetchTimeoutRef.current !== null) {
|
|
540
627
|
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
541
628
|
}
|
|
629
|
+
if (detailPrefetchTimeoutRef.current !== null) {
|
|
630
|
+
clearTimeout(detailPrefetchTimeoutRef.current)
|
|
631
|
+
}
|
|
542
632
|
}, [])
|
|
543
633
|
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const pullRequests = useMemo(() => {
|
|
548
|
-
const source = pullRequestLoad?.data ?? []
|
|
549
|
-
const seenUrls = new Set<string>()
|
|
550
|
-
const openPullRequests = source.map((pullRequest) => {
|
|
551
|
-
seenUrls.add(pullRequest.url)
|
|
552
|
-
return recentlyCompletedPullRequests[pullRequest.url] ?? pullRequestOverrides[pullRequest.url] ?? pullRequest
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
return [
|
|
556
|
-
...openPullRequests,
|
|
557
|
-
...Object.values(recentlyCompletedPullRequests).filter((pullRequest) => !seenUrls.has(pullRequest.url)),
|
|
558
|
-
]
|
|
559
|
-
}, [pullRequestLoad?.data, pullRequestOverrides, recentlyCompletedPullRequests])
|
|
560
|
-
const pullRequestStatus: LoadStatus = (pullRequestResult.waiting || isLoadingQueueMode) && pullRequestLoad === null
|
|
561
|
-
? "loading"
|
|
562
|
-
: AsyncResult.isFailure(pullRequestResult)
|
|
563
|
-
? "error"
|
|
564
|
-
: "ready"
|
|
634
|
+
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
635
|
+
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
636
|
+
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
565
637
|
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
566
638
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
567
639
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
568
640
|
pullRequestStatusRef.current = pullRequestStatus
|
|
569
641
|
|
|
570
|
-
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
571
642
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
572
643
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
644
|
+
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
645
|
+
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
646
|
+
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
647
|
+
const selectedRepository = viewRepository(activeView)
|
|
648
|
+
const activeViews = activePullRequestViews(activeView)
|
|
649
|
+
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
650
|
+
const loadedPullRequestCount = pullRequestLoad?.data.length ?? 0
|
|
651
|
+
const hasMorePullRequests = Boolean(pullRequestLoad?.hasNextPage && loadedPullRequestCount < config.prFetchLimit)
|
|
652
|
+
const isLoadingMorePullRequests = loadingMoreKey === currentQueueCacheKey
|
|
653
|
+
const pullRequestListRows = useMemo(() => buildPullRequestListRows({
|
|
654
|
+
groups: visibleGroups,
|
|
655
|
+
status: pullRequestStatus,
|
|
656
|
+
error: pullRequestError,
|
|
657
|
+
filterText: visibleFilterText,
|
|
658
|
+
showFilterBar: filterMode || filterQuery.length > 0,
|
|
659
|
+
loadedCount: loadedPullRequestCount,
|
|
660
|
+
hasMore: hasMorePullRequests,
|
|
661
|
+
isLoadingMore: isLoadingMorePullRequests,
|
|
662
|
+
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
663
|
+
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
664
|
+
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
665
|
+
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
593
666
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
594
667
|
const readyDiffFiles = selectedDiffState?._tag === "Ready" ? selectedDiffState.files : []
|
|
595
668
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
596
|
-
const selectedDiffKey = selectedPullRequest ? pullRequestDiffKey(selectedPullRequest) : null
|
|
597
669
|
const diffCommentAnchors = useMemo(
|
|
598
670
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
599
671
|
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
600
672
|
)
|
|
601
673
|
const selectedDiffCommentAnchor = diffCommentAnchors[Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))] ?? null
|
|
602
|
-
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${
|
|
674
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? `${selectedDiffKey}:${diffCommentLocationKey(selectedDiffCommentAnchor)}` : null
|
|
603
675
|
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
604
676
|
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
605
677
|
const diffCommentRows = useMemo(
|
|
606
678
|
() => [...new Set(diffCommentAnchors.map((anchor) => anchor.renderLine))].sort((left, right) => left - right),
|
|
607
679
|
[diffCommentAnchors],
|
|
608
680
|
)
|
|
609
|
-
const groupStarts =
|
|
610
|
-
if (index === 0) {
|
|
611
|
-
starts.push(0)
|
|
612
|
-
return starts
|
|
613
|
-
}
|
|
614
|
-
starts.push(starts[index - 1]! + visibleGroups[index - 1]![1].length)
|
|
615
|
-
return starts
|
|
616
|
-
}, []), [visibleGroups])
|
|
681
|
+
const groupStarts = useAtomValue(groupStartsAtom)
|
|
617
682
|
const getCurrentGroupIndex = (current: number) => {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
683
|
+
if (groupStarts.length === 0) return 0
|
|
684
|
+
let low = 0
|
|
685
|
+
let high = groupStarts.length - 1
|
|
686
|
+
while (low < high) {
|
|
687
|
+
const mid = (low + high + 1) >>> 1
|
|
688
|
+
if (groupStarts[mid]! <= current) low = mid
|
|
689
|
+
else high = mid - 1
|
|
690
|
+
}
|
|
691
|
+
return low
|
|
622
692
|
}
|
|
623
693
|
const summaryRight = pullRequestLoad?.fetchedAt
|
|
624
694
|
? `updated ${formatShortDate(pullRequestLoad.fetchedAt)} ${formatTimestamp(pullRequestLoad.fetchedAt)}`
|
|
625
695
|
: pullRequestStatus === "loading"
|
|
626
696
|
? "loading pull requests..."
|
|
627
697
|
: ""
|
|
628
|
-
const headerLeft = username ? `GHUI ${username} ${
|
|
698
|
+
const headerLeft = username ? `GHUI ${username} ${viewLabel(activeView)}` : `GHUI ${viewLabel(activeView)}`
|
|
629
699
|
const headerLine = `${fitCell(headerLeft, Math.max(0, headerFooterWidth - summaryRight.length))}${summaryRight}`
|
|
630
700
|
const footerNotice = notice ? fitCell(notice, headerFooterWidth) : null
|
|
631
701
|
const selectPullRequestByUrl = (url: string) => {
|
|
632
702
|
const index = visiblePullRequests.findIndex((pullRequest) => pullRequest.url === url)
|
|
633
703
|
if (index >= 0) {
|
|
634
704
|
setSelectedIndex(index)
|
|
635
|
-
setQueueSelection((current) => ({ ...current, [
|
|
705
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: index }))
|
|
636
706
|
}
|
|
637
707
|
}
|
|
638
708
|
const updatePullRequest = (url: string, transform: (pullRequest: PullRequestItem) => PullRequestItem) => {
|
|
@@ -642,6 +712,9 @@ export const App = () => {
|
|
|
642
712
|
}
|
|
643
713
|
const refreshPullRequests = (message?: string) => {
|
|
644
714
|
refreshGenerationRef.current += 1
|
|
715
|
+
detailHydrationRef.current.clear()
|
|
716
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
717
|
+
setLoadingMoreKey(null)
|
|
645
718
|
setPullRequestOverrides({})
|
|
646
719
|
if (message) {
|
|
647
720
|
setNotice(null)
|
|
@@ -651,15 +724,16 @@ export const App = () => {
|
|
|
651
724
|
refreshPullRequestsAtom()
|
|
652
725
|
}
|
|
653
726
|
refreshPullRequestsRef.current = refreshPullRequests
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
if (mode === queueMode) return
|
|
727
|
+
const switchViewTo = (view: PullRequestView) => {
|
|
728
|
+
if (viewEquals(view, activeView)) return
|
|
657
729
|
refreshGenerationRef.current += 1
|
|
658
|
-
setQueueSelection((current) => ({ ...current, [
|
|
659
|
-
|
|
660
|
-
setSelectedIndex(
|
|
730
|
+
setQueueSelection((current) => ({ ...current, [currentQueueCacheKey]: selectedIndex }))
|
|
731
|
+
setActiveView(view)
|
|
732
|
+
setSelectedIndex(registry.get(queueSelectionAtom)[viewCacheKey(view)] ?? 0)
|
|
661
733
|
setRecentlyCompletedPullRequests({})
|
|
662
|
-
detailHydrationRef.current
|
|
734
|
+
detailHydrationRef.current.clear()
|
|
735
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
736
|
+
setLoadingMoreKey(null)
|
|
663
737
|
setDetailFullView(false)
|
|
664
738
|
setDiffFullView(false)
|
|
665
739
|
setDiffCommentMode(false)
|
|
@@ -668,6 +742,82 @@ export const App = () => {
|
|
|
668
742
|
setRefreshCompletionMessage(null)
|
|
669
743
|
setRefreshStartedAt(null)
|
|
670
744
|
}
|
|
745
|
+
const switchQueueMode = (delta: 1 | -1) => {
|
|
746
|
+
switchViewTo(nextView(activeView, activeViews, delta))
|
|
747
|
+
}
|
|
748
|
+
const loadMorePullRequests = () => {
|
|
749
|
+
if (!pullRequestLoad || !hasMorePullRequests || isLoadingMorePullRequests || !pullRequestLoad.endCursor) return false
|
|
750
|
+
const remaining = config.prFetchLimit - pullRequestLoad.data.length
|
|
751
|
+
if (remaining <= 0) return false
|
|
752
|
+
const cacheKey = currentQueueCacheKey
|
|
753
|
+
const generation = refreshGenerationRef.current
|
|
754
|
+
setLoadingMoreKey(cacheKey)
|
|
755
|
+
void loadPullRequestPage({
|
|
756
|
+
mode: viewMode(activeView),
|
|
757
|
+
repository: selectedRepository,
|
|
758
|
+
cursor: pullRequestLoad.endCursor,
|
|
759
|
+
pageSize: Math.min(pullRequestPageSize, remaining),
|
|
760
|
+
}).then((page) => {
|
|
761
|
+
if (generation !== refreshGenerationRef.current) return
|
|
762
|
+
setQueueLoadCache((current) => {
|
|
763
|
+
const load = current[cacheKey]
|
|
764
|
+
if (!load) return current
|
|
765
|
+
const data = appendPullRequestPage(load.data, page.items)
|
|
766
|
+
return {
|
|
767
|
+
...current,
|
|
768
|
+
[cacheKey]: {
|
|
769
|
+
...load,
|
|
770
|
+
data,
|
|
771
|
+
endCursor: page.endCursor,
|
|
772
|
+
hasNextPage: page.hasNextPage && data.length < config.prFetchLimit,
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
}).catch((error) => {
|
|
777
|
+
flashNotice(errorMessage(error))
|
|
778
|
+
}).finally(() => {
|
|
779
|
+
setLoadingMoreKey((current) => current === cacheKey ? null : current)
|
|
780
|
+
})
|
|
781
|
+
return true
|
|
782
|
+
}
|
|
783
|
+
const applyPullRequestDetail = (detail: PullRequestItem) => {
|
|
784
|
+
setQueueLoadCache((current) => {
|
|
785
|
+
const next = { ...current }
|
|
786
|
+
let changed = false
|
|
787
|
+
for (const [cacheKey, load] of Object.entries(current)) {
|
|
788
|
+
if (!load) continue
|
|
789
|
+
const index = load.data.findIndex((pullRequest) => pullRequest.url === detail.url)
|
|
790
|
+
if (index < 0) continue
|
|
791
|
+
const data = [...load.data]
|
|
792
|
+
data[index] = detail
|
|
793
|
+
changed = true
|
|
794
|
+
next[cacheKey] = { ...load, data }
|
|
795
|
+
}
|
|
796
|
+
return changed ? next : current
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
const hydratePullRequestDetails = (pullRequest: PullRequestItem, notifyError: boolean) => {
|
|
800
|
+
if (pullRequest.state !== "open" || pullRequest.detailLoaded) return false
|
|
801
|
+
const detailKey = pullRequestDetailKey(pullRequest)
|
|
802
|
+
const existing = detailHydrationRef.current.get(detailKey)
|
|
803
|
+
if (existing) {
|
|
804
|
+
if (notifyError) existing.notifyError = true
|
|
805
|
+
return false
|
|
806
|
+
}
|
|
807
|
+
if (!notifyError && detailHydrationRef.current.size >= DETAIL_PREFETCH_CONCURRENCY) return false
|
|
808
|
+
const entry: DetailHydration = { token: Symbol(detailKey), notifyError }
|
|
809
|
+
detailHydrationRef.current.set(detailKey, entry)
|
|
810
|
+
const generation = refreshGenerationRef.current
|
|
811
|
+
const atom = pullRequestDetailsAtom(pullRequestDetailAtomKey(pullRequest))
|
|
812
|
+
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })).then((detail) => {
|
|
813
|
+
if (generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) applyPullRequestDetail(detail)
|
|
814
|
+
}).catch((error) => {
|
|
815
|
+
if (entry.notifyError && generation === refreshGenerationRef.current && detailHydrationRef.current.get(detailKey) === entry) flashNotice(errorMessage(error))
|
|
816
|
+
}).finally(() => {
|
|
817
|
+
if (detailHydrationRef.current.get(detailKey) === entry) detailHydrationRef.current.delete(detailKey)
|
|
818
|
+
})
|
|
819
|
+
return true
|
|
820
|
+
}
|
|
671
821
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
672
822
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
673
823
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -687,14 +837,14 @@ export const App = () => {
|
|
|
687
837
|
didMountQueueModeRef.current = true
|
|
688
838
|
return
|
|
689
839
|
}
|
|
690
|
-
if (
|
|
840
|
+
if (registry.get(queueLoadCacheAtom)[currentQueueCacheKey]) return
|
|
691
841
|
refreshPullRequestsAtom()
|
|
692
|
-
}, [
|
|
842
|
+
}, [currentQueueCacheKey, refreshPullRequestsAtom, registry])
|
|
693
843
|
|
|
694
844
|
useEffect(() => {
|
|
695
845
|
if (!refreshCompletionMessage || refreshStartedAt === null) return
|
|
696
846
|
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
697
|
-
const isHydratingDetails = pullRequestStatus === "ready" &&
|
|
847
|
+
const isHydratingDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
698
848
|
if (pullRequestStatus === "ready" && fetchedAt !== undefined && fetchedAt !== refreshStartedAt && !isHydratingDetails) {
|
|
699
849
|
flashNotice(`✓ ${refreshCompletionMessage}`)
|
|
700
850
|
setRefreshCompletionMessage(null)
|
|
@@ -747,13 +897,43 @@ export const App = () => {
|
|
|
747
897
|
}, [visiblePullRequests.length])
|
|
748
898
|
|
|
749
899
|
useEffect(() => {
|
|
750
|
-
setQueueSelection((current) => current[
|
|
751
|
-
}, [
|
|
900
|
+
setQueueSelection((current) => current[currentQueueCacheKey] === selectedIndex ? current : { ...current, [currentQueueCacheKey]: selectedIndex })
|
|
901
|
+
}, [currentQueueCacheKey, selectedIndex])
|
|
902
|
+
|
|
903
|
+
useEffect(() => {
|
|
904
|
+
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0) return
|
|
905
|
+
const thresholdIndex = Math.max(0, visiblePullRequests.length - LOAD_MORE_SELECTION_THRESHOLD)
|
|
906
|
+
if (selectedIndex >= thresholdIndex) loadMorePullRequests()
|
|
907
|
+
}, [selectedIndex, visiblePullRequests.length, filterMode, filterQuery, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
908
|
+
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0 || detailFullView || diffFullView) return
|
|
911
|
+
if (!hasMorePullRequests || isLoadingMorePullRequests) return
|
|
912
|
+
const checkScroll = () => {
|
|
913
|
+
const scroll = prListScrollRef.current
|
|
914
|
+
if (!scroll || scroll.viewport.height <= 0) return
|
|
915
|
+
const bottom = scroll.scrollTop + scroll.viewport.height
|
|
916
|
+
if (bottom >= scroll.scrollHeight - LOAD_MORE_SCROLL_THRESHOLD) loadMorePullRequests()
|
|
917
|
+
}
|
|
918
|
+
checkScroll()
|
|
919
|
+
const interval = globalThis.setInterval(checkScroll, 120)
|
|
920
|
+
return () => globalThis.clearInterval(interval)
|
|
921
|
+
}, [visiblePullRequests.length, filterMode, filterQuery, detailFullView, diffFullView, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
922
|
+
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
const scroll = prListScrollRef.current
|
|
925
|
+
if (!scroll || selectedPullRequestRowIndex === null) return
|
|
926
|
+
const viewportHeight = scroll.viewport.height
|
|
927
|
+
if (viewportHeight <= 0) return
|
|
928
|
+
const nextTop = scrollTopForVisibleLine(scroll.scrollTop, viewportHeight, selectedPullRequestRowIndex, 2)
|
|
929
|
+
if (nextTop !== scroll.scrollTop) scroll.scrollTo({ x: 0, y: nextTop })
|
|
930
|
+
}, [selectedPullRequestRowIndex])
|
|
752
931
|
|
|
753
932
|
useEffect(() => {
|
|
754
933
|
setDiffFileIndex(0)
|
|
755
934
|
setDiffScrollTop(0)
|
|
756
935
|
setDiffCommentAnchorIndex(0)
|
|
936
|
+
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
757
937
|
}, [selectedIndex])
|
|
758
938
|
|
|
759
939
|
useEffect(() => {
|
|
@@ -793,7 +973,7 @@ export const App = () => {
|
|
|
793
973
|
|
|
794
974
|
if (selectedDiffKey) {
|
|
795
975
|
for (const anchor of diffCommentAnchors) {
|
|
796
|
-
if ((diffCommentThreads[`${selectedDiffKey}:${
|
|
976
|
+
if ((diffCommentThreads[`${selectedDiffKey}:${diffCommentLocationKey(anchor)}`]?.length ?? 0) > 0) {
|
|
797
977
|
applyLineColor(anchor, diffCommentGutterColor(anchor, "thread"))
|
|
798
978
|
}
|
|
799
979
|
}
|
|
@@ -810,7 +990,7 @@ export const App = () => {
|
|
|
810
990
|
}
|
|
811
991
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
812
992
|
}, [diffCommentMode, selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, diffLineColorContextKey, effectiveDiffRenderView, diffCommentAnchors, diffCommentThreads])
|
|
813
|
-
const isHydratingPullRequestDetails = pullRequestStatus === "ready" &&
|
|
993
|
+
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
814
994
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
815
995
|
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
816
996
|
const loadingIndicator = LOADING_FRAMES[loadingFrame % LOADING_FRAMES.length]!
|
|
@@ -824,31 +1004,30 @@ export const App = () => {
|
|
|
824
1004
|
}, [hasActiveLoadingIndicator])
|
|
825
1005
|
|
|
826
1006
|
useEffect(() => {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
detailsFetchedAt: load.fetchedAt,
|
|
845
|
-
},
|
|
1007
|
+
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1008
|
+
hydratePullRequestDetails(selectedPullRequest, true)
|
|
1009
|
+
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1010
|
+
|
|
1011
|
+
useEffect(() => {
|
|
1012
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1013
|
+
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
1014
|
+
detailPrefetchTimeoutRef.current = globalThis.setTimeout(() => {
|
|
1015
|
+
detailPrefetchTimeoutRef.current = null
|
|
1016
|
+
let started = 0
|
|
1017
|
+
for (let distance = 1; distance <= Math.max(DETAIL_PREFETCH_AHEAD, DETAIL_PREFETCH_BEHIND); distance++) {
|
|
1018
|
+
const offsets = [distance <= DETAIL_PREFETCH_AHEAD ? distance : null, distance <= DETAIL_PREFETCH_BEHIND ? -distance : null]
|
|
1019
|
+
for (const offset of offsets) {
|
|
1020
|
+
if (offset === null) continue
|
|
1021
|
+
if (started >= DETAIL_PREFETCH_CONCURRENCY) return
|
|
1022
|
+
const pullRequest = visiblePullRequests[selectedIndex + offset]
|
|
1023
|
+
if (pullRequest && hydratePullRequestDetails(pullRequest, false)) started += 1
|
|
846
1024
|
}
|
|
847
|
-
}
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1025
|
+
}
|
|
1026
|
+
}, DETAIL_PREFETCH_DELAY_MS)
|
|
1027
|
+
return () => {
|
|
1028
|
+
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1029
|
+
}
|
|
1030
|
+
}, [pullRequestStatus, currentQueueCacheKey, selectedIndex, visiblePullRequests])
|
|
852
1031
|
|
|
853
1032
|
const detailPlaceholderContent = getDetailPlaceholderContent({
|
|
854
1033
|
status: pullRequestStatus,
|
|
@@ -857,13 +1036,18 @@ export const App = () => {
|
|
|
857
1036
|
visibleCount: visiblePullRequests.length,
|
|
858
1037
|
filterText: visibleFilterText,
|
|
859
1038
|
})
|
|
860
|
-
const
|
|
1039
|
+
const isSelectedPullRequestDetailLoading = selectedPullRequest !== null && !selectedPullRequest.detailLoaded
|
|
1040
|
+
const detailLoadingContent: DetailPlaceholderContent = selectedPullRequest ? {
|
|
1041
|
+
title: `${loadingIndicator} Loading pull request details`,
|
|
1042
|
+
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1043
|
+
} : detailPlaceholderContent
|
|
1044
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true)
|
|
861
1045
|
|
|
862
1046
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
863
1047
|
|
|
864
1048
|
const loadPullRequestComments = (pullRequest: PullRequestItem, force = false) => {
|
|
865
1049
|
const key = pullRequestDiffKey(pullRequest)
|
|
866
|
-
const previousLoadState =
|
|
1050
|
+
const previousLoadState = registry.get(diffCommentsLoadedAtom)[key]
|
|
867
1051
|
if (!force && previousLoadState) return
|
|
868
1052
|
setDiffCommentsLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
869
1053
|
void listPullRequestComments({ repository: pullRequest.repository, number: pullRequest.number })
|
|
@@ -906,12 +1090,14 @@ export const App = () => {
|
|
|
906
1090
|
const force = options.force ?? false
|
|
907
1091
|
const includeComments = options.includeComments ?? false
|
|
908
1092
|
const key = pullRequestDiffKey(pullRequest)
|
|
909
|
-
const existing =
|
|
1093
|
+
const existing = registry.get(pullRequestDiffCacheAtom)[key]
|
|
910
1094
|
if (includeComments) loadPullRequestComments(pullRequest, force)
|
|
911
1095
|
if (!force && existing && (existing._tag === "Ready" || existing._tag === "Loading")) return
|
|
912
1096
|
|
|
913
1097
|
setPullRequestDiffCache((current) => ({ ...current, [key]: PullRequestDiffState.Loading() }))
|
|
914
|
-
|
|
1098
|
+
const atom = pullRequestDiffAtom(pullRequestDiffAtomKey(pullRequest))
|
|
1099
|
+
if (force) registry.refresh(atom)
|
|
1100
|
+
void Effect.runPromise(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true }))
|
|
915
1101
|
.then((patch) => {
|
|
916
1102
|
setPullRequestDiffCache((current) => ({
|
|
917
1103
|
...current,
|
|
@@ -985,6 +1171,8 @@ export const App = () => {
|
|
|
985
1171
|
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
986
1172
|
syncDiffScrollState()
|
|
987
1173
|
}
|
|
1174
|
+
const scrollDetailPreviewBy = (y: number) => detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1175
|
+
const scrollDetailPreviewTo = (y: number) => detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
988
1176
|
|
|
989
1177
|
const clearPendingGTimeout = () => {
|
|
990
1178
|
if (pendingGTimeoutRef.current !== null) {
|
|
@@ -1161,14 +1349,14 @@ export const App = () => {
|
|
|
1161
1349
|
}
|
|
1162
1350
|
|
|
1163
1351
|
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
1164
|
-
void
|
|
1352
|
+
void openInBrowser(pullRequest)
|
|
1165
1353
|
.then(() => flashNotice(`Opened #${pullRequest.number} in browser`))
|
|
1166
1354
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1167
1355
|
}
|
|
1168
1356
|
|
|
1169
1357
|
const copySelectedPullRequestMetadata = () => {
|
|
1170
1358
|
if (!selectedPullRequest) return
|
|
1171
|
-
void
|
|
1359
|
+
void copyToClipboard(pullRequestMetadataText(selectedPullRequest))
|
|
1172
1360
|
.then(() => flashNotice(`Copied #${selectedPullRequest.number} metadata`))
|
|
1173
1361
|
.catch((error) => flashNotice(errorMessage(error)))
|
|
1174
1362
|
}
|
|
@@ -1291,7 +1479,7 @@ export const App = () => {
|
|
|
1291
1479
|
const openLabelModal = () => {
|
|
1292
1480
|
if (!selectedPullRequest) return
|
|
1293
1481
|
const repository = selectedPullRequest.repository
|
|
1294
|
-
const cachedLabels =
|
|
1482
|
+
const cachedLabels = registry.get(labelCacheAtom)[repository]
|
|
1295
1483
|
if (cachedLabels) {
|
|
1296
1484
|
setLabelModal({
|
|
1297
1485
|
repository,
|
|
@@ -1390,9 +1578,7 @@ export const App = () => {
|
|
|
1390
1578
|
|
|
1391
1579
|
const toggleLabelAtIndex = () => {
|
|
1392
1580
|
if (!selectedPullRequest) return
|
|
1393
|
-
const filtered = labelModal.availableLabels.
|
|
1394
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1395
|
-
)
|
|
1581
|
+
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
1396
1582
|
const label = filtered[labelModal.selectedIndex]
|
|
1397
1583
|
if (!label) return
|
|
1398
1584
|
|
|
@@ -1424,274 +1610,505 @@ export const App = () => {
|
|
|
1424
1610
|
}
|
|
1425
1611
|
}
|
|
1426
1612
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
renderer.destroy()
|
|
1613
|
+
const openCommandPalette = () => {
|
|
1614
|
+
setCommandPalette(initialCommandPaletteState)
|
|
1615
|
+
}
|
|
1616
|
+
const openRepositoryPicker = () => {
|
|
1617
|
+
setOpenRepositoryModal({ query: selectedRepository ?? "", error: null })
|
|
1618
|
+
}
|
|
1619
|
+
const openRepositoryFromInput = () => {
|
|
1620
|
+
const repository = parseRepositoryInput(openRepositoryModal.query)
|
|
1621
|
+
if (!repository) {
|
|
1622
|
+
setOpenRepositoryModal((current) => ({ ...current, error: "Enter a repository as owner/name or a GitHub URL." }))
|
|
1438
1623
|
return
|
|
1439
1624
|
}
|
|
1625
|
+
closeActiveModal()
|
|
1626
|
+
switchViewTo({ _tag: "Repository", repository })
|
|
1627
|
+
flashNotice(`Opened ${repository}`)
|
|
1628
|
+
}
|
|
1629
|
+
const insertPastedText = (text: string) => {
|
|
1630
|
+
if (text.length === 0) return false
|
|
1631
|
+
if (commandPaletteActive) {
|
|
1632
|
+
setCommandPalette((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1633
|
+
return true
|
|
1634
|
+
}
|
|
1635
|
+
if (openRepositoryModalActive) {
|
|
1636
|
+
setOpenRepositoryModal((current) => ({ ...current, query: current.query + singleLineText(text), error: null }))
|
|
1637
|
+
return true
|
|
1638
|
+
}
|
|
1639
|
+
if (themeModalActive && themeModal.filterMode) {
|
|
1640
|
+
editThemeQuery((query) => query + singleLineText(text))
|
|
1641
|
+
return true
|
|
1642
|
+
}
|
|
1643
|
+
if (commentModalActive) {
|
|
1644
|
+
editComment((state) => insertText(state, text.replace(/\r\n?/g, "\n")))
|
|
1645
|
+
return true
|
|
1646
|
+
}
|
|
1647
|
+
if (labelModalActive) {
|
|
1648
|
+
setLabelModal((current) => ({ ...current, query: current.query + singleLineText(text), selectedIndex: 0 }))
|
|
1649
|
+
return true
|
|
1650
|
+
}
|
|
1651
|
+
if (filterMode) {
|
|
1652
|
+
setFilterDraft((current) => current + singleLineText(text))
|
|
1653
|
+
return true
|
|
1654
|
+
}
|
|
1655
|
+
return false
|
|
1656
|
+
}
|
|
1440
1657
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1658
|
+
useEffect(() => {
|
|
1659
|
+
const handlePaste = (event: PasteEvent) => {
|
|
1660
|
+
if (insertPastedText(pasteText(event))) event.preventDefault()
|
|
1661
|
+
}
|
|
1662
|
+
const keyInput = renderer.keyInput as unknown as {
|
|
1663
|
+
on: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1664
|
+
off: (event: "paste", handler: (event: PasteEvent) => void) => void
|
|
1665
|
+
}
|
|
1666
|
+
keyInput.on("paste", handlePaste)
|
|
1667
|
+
return () => {
|
|
1668
|
+
keyInput.off("paste", handlePaste)
|
|
1669
|
+
}
|
|
1670
|
+
}, [renderer, commandPaletteActive, openRepositoryModalActive, themeModalActive, themeModal.filterMode, commentModalActive, labelModalActive, filterMode])
|
|
1671
|
+
|
|
1672
|
+
const appCommands: readonly AppCommand[] = buildAppCommands({
|
|
1673
|
+
pullRequestStatus,
|
|
1674
|
+
filterQuery,
|
|
1675
|
+
filterMode,
|
|
1676
|
+
selectedRepository,
|
|
1677
|
+
activeViews,
|
|
1678
|
+
activeView,
|
|
1679
|
+
loadedPullRequestCount,
|
|
1680
|
+
hasMorePullRequests,
|
|
1681
|
+
isLoadingMorePullRequests,
|
|
1682
|
+
selectedPullRequest,
|
|
1683
|
+
detailFullView,
|
|
1684
|
+
diffFullView,
|
|
1685
|
+
diffReady: selectedDiffState?._tag === "Ready",
|
|
1686
|
+
effectiveDiffRenderView,
|
|
1687
|
+
diffWrapMode,
|
|
1688
|
+
readyDiffFileCount: readyDiffFiles.length,
|
|
1689
|
+
diffFileIndex,
|
|
1690
|
+
diffCommentMode,
|
|
1691
|
+
selectedDiffCommentAnchorLabel: selectedDiffCommentAnchor ? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line}` : null,
|
|
1692
|
+
actions: {
|
|
1693
|
+
openCommandPalette,
|
|
1694
|
+
refreshPullRequests,
|
|
1695
|
+
openFilter: () => {
|
|
1696
|
+
setFilterDraft(filterQuery)
|
|
1697
|
+
setFilterMode(true)
|
|
1698
|
+
},
|
|
1699
|
+
clearFilter: () => {
|
|
1700
|
+
setFilterQuery("")
|
|
1701
|
+
setFilterDraft("")
|
|
1702
|
+
setFilterMode(false)
|
|
1703
|
+
},
|
|
1704
|
+
openThemeModal,
|
|
1705
|
+
openRepositoryPicker,
|
|
1706
|
+
loadMorePullRequests,
|
|
1707
|
+
switchViewTo,
|
|
1708
|
+
openDetails: () => {
|
|
1709
|
+
setDetailFullView(true)
|
|
1710
|
+
setDetailScrollOffset(0)
|
|
1711
|
+
},
|
|
1712
|
+
closeDetails: () => {
|
|
1713
|
+
setDetailFullView(false)
|
|
1714
|
+
setDetailScrollOffset(0)
|
|
1715
|
+
},
|
|
1716
|
+
openDiffView,
|
|
1717
|
+
closeDiffView: () => {
|
|
1718
|
+
setDiffFullView(false)
|
|
1719
|
+
setDiffCommentMode(false)
|
|
1720
|
+
},
|
|
1721
|
+
reloadDiff: () => {
|
|
1722
|
+
if (!selectedPullRequest) return
|
|
1723
|
+
loadPullRequestDiff(selectedPullRequest, { force: true, includeComments: true })
|
|
1724
|
+
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
1725
|
+
},
|
|
1726
|
+
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1727
|
+
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1728
|
+
jumpDiffFile,
|
|
1729
|
+
toggleDiffCommentMode: () => {
|
|
1730
|
+
if (diffCommentMode) setDiffCommentMode(false)
|
|
1731
|
+
else enterDiffCommentMode()
|
|
1732
|
+
},
|
|
1733
|
+
openDiffCommentModal,
|
|
1734
|
+
togglePullRequestDraftStatus: toggleSelectedPullRequestDraftStatus,
|
|
1735
|
+
openLabelModal,
|
|
1736
|
+
openMergeModal,
|
|
1737
|
+
openCloseModal,
|
|
1738
|
+
openPullRequestInBrowser: () => {
|
|
1739
|
+
if (selectedPullRequest) openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1740
|
+
},
|
|
1741
|
+
copyPullRequestMetadata: copySelectedPullRequestMetadata,
|
|
1742
|
+
quit: () => renderer.destroy(),
|
|
1743
|
+
},
|
|
1744
|
+
})
|
|
1745
|
+
const runCommand = (command: AppCommand, options: { readonly notifyDisabled?: boolean; readonly closePalette?: boolean } = {}) => {
|
|
1746
|
+
if (!commandEnabled(command)) {
|
|
1747
|
+
if (options.notifyDisabled && command.disabledReason) flashNotice(command.disabledReason)
|
|
1748
|
+
return false
|
|
1749
|
+
}
|
|
1750
|
+
if (options.closePalette) closeActiveModal()
|
|
1751
|
+
command.run()
|
|
1752
|
+
return true
|
|
1753
|
+
}
|
|
1754
|
+
const runCommandById = (id: string, options: { readonly notifyDisabled?: boolean } = {}) => {
|
|
1755
|
+
const command = appCommands.find((entry) => entry.id === id)
|
|
1756
|
+
return command ? runCommand(command, options) : false
|
|
1757
|
+
}
|
|
1758
|
+
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1759
|
+
if (!commandPaletteActive) return []
|
|
1760
|
+
const repository = parseRepositoryInput(commandPalette.query)
|
|
1761
|
+
if (!repository || repository === selectedRepository) return []
|
|
1762
|
+
return [defineCommand({
|
|
1763
|
+
id: `view.repository.dynamic:${repository}`,
|
|
1764
|
+
title: `Open ${repository}`,
|
|
1765
|
+
scope: "View",
|
|
1766
|
+
subtitle: "Switch to this repository",
|
|
1767
|
+
run: () => switchViewTo({ _tag: "Repository", repository }),
|
|
1768
|
+
})]
|
|
1769
|
+
})()
|
|
1770
|
+
// Dynamic commands always pin to the top of the palette; they came directly from the
|
|
1771
|
+
// user's typed input so they shouldn't be filtered by fuzzy score against themselves.
|
|
1772
|
+
const commandPaletteCommands = commandPaletteActive
|
|
1773
|
+
? [
|
|
1774
|
+
...dynamicPaletteCommands,
|
|
1775
|
+
...sortCommandsByScope(filterCommands(appCommands.filter((command) => command.id !== "command.open" && commandEnabled(command)), commandPalette.query)),
|
|
1776
|
+
]
|
|
1777
|
+
: []
|
|
1778
|
+
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1779
|
+
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1780
|
+
|
|
1781
|
+
// Keymap migration phase 2: simple cmd-id bindings move out of useKeyboard.
|
|
1782
|
+
// Gated to "global mode" — no modal active, no full-view, not in filter editing —
|
|
1783
|
+
// so these don't dispatch on top of modal-specific handlers below.
|
|
1784
|
+
const globalKeymapActiveRef = useRef(false)
|
|
1785
|
+
globalKeymapActiveRef.current = !commandPaletteActive
|
|
1786
|
+
&& !openRepositoryModalActive
|
|
1787
|
+
&& !labelModalActive
|
|
1788
|
+
&& !commentModalActive
|
|
1789
|
+
&& !commentThreadModalActive
|
|
1790
|
+
&& !closeModalActive
|
|
1791
|
+
&& !mergeModalActive
|
|
1792
|
+
&& !themeModalActive
|
|
1793
|
+
&& !diffFullView
|
|
1794
|
+
&& !detailFullView
|
|
1795
|
+
&& !filterMode
|
|
1796
|
+
const runCommandByIdRef = useRef(runCommandById)
|
|
1797
|
+
runCommandByIdRef.current = runCommandById
|
|
1798
|
+
useBindings(() => ({
|
|
1799
|
+
enabled: () => globalKeymapActiveRef.current,
|
|
1800
|
+
bindings: [
|
|
1801
|
+
{ key: "/", cmd: () => runCommandByIdRef.current("filter.open") },
|
|
1802
|
+
{ key: "r", cmd: () => runCommandByIdRef.current("pull.refresh") },
|
|
1803
|
+
{ key: "t", cmd: () => runCommandByIdRef.current("theme.open") },
|
|
1804
|
+
{ key: "d", cmd: () => runCommandByIdRef.current("diff.open") },
|
|
1805
|
+
{ key: "l", cmd: () => runCommandByIdRef.current("pull.labels") },
|
|
1806
|
+
{ key: "m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1807
|
+
{ key: "shift+m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1808
|
+
{ key: "x", cmd: () => runCommandByIdRef.current("pull.close") },
|
|
1809
|
+
{ key: "o", cmd: () => runCommandByIdRef.current("pull.open-browser") },
|
|
1810
|
+
{ key: "s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1811
|
+
{ key: "shift+s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1812
|
+
{ key: "y", cmd: () => runCommandByIdRef.current("pull.copy-metadata") },
|
|
1813
|
+
{ key: "return", cmd: () => runCommandByIdRef.current("detail.open") },
|
|
1814
|
+
],
|
|
1815
|
+
}), [])
|
|
1816
|
+
// Always-on bindings — work even while modals are open.
|
|
1817
|
+
useBindings(() => ({
|
|
1818
|
+
bindings: [
|
|
1819
|
+
{ key: "ctrl+p", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1820
|
+
{ key: "meta+k", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1821
|
+
],
|
|
1822
|
+
}), [])
|
|
1823
|
+
|
|
1824
|
+
// CloseModal: escape closes, enter confirms.
|
|
1825
|
+
const closeModalActiveRef = useRef(false)
|
|
1826
|
+
closeModalActiveRef.current = closeModalActive
|
|
1827
|
+
const closeActiveModalRef = useRef(closeActiveModal)
|
|
1828
|
+
closeActiveModalRef.current = closeActiveModal
|
|
1829
|
+
const confirmClosePullRequestRef = useRef(confirmClosePullRequest)
|
|
1830
|
+
confirmClosePullRequestRef.current = confirmClosePullRequest
|
|
1831
|
+
useBindings(() => ({
|
|
1832
|
+
enabled: () => closeModalActiveRef.current,
|
|
1833
|
+
bindings: [
|
|
1834
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1835
|
+
{ key: "return", cmd: () => confirmClosePullRequestRef.current() },
|
|
1836
|
+
],
|
|
1837
|
+
}), [])
|
|
1838
|
+
|
|
1839
|
+
// MergeModal: escape, enter (when options>0), up/down/j/k navigation.
|
|
1840
|
+
const mergeModalActiveRef = useRef(false)
|
|
1841
|
+
mergeModalActiveRef.current = mergeModalActive
|
|
1842
|
+
const mergeModalContextRef = useRef({ availableCount: 0, confirm: confirmMergeAction, setMergeModal })
|
|
1843
|
+
mergeModalContextRef.current = {
|
|
1844
|
+
availableCount: availableMergeActions(mergeModal.info).length,
|
|
1845
|
+
confirm: confirmMergeAction,
|
|
1846
|
+
setMergeModal,
|
|
1847
|
+
}
|
|
1848
|
+
const moveMergeSelection = (delta: -1 | 1) => mergeModalContextRef.current.setMergeModal((current) => {
|
|
1849
|
+
const max = Math.max(0, mergeModalContextRef.current.availableCount - 1)
|
|
1850
|
+
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1851
|
+
})
|
|
1852
|
+
useBindings(() => ({
|
|
1853
|
+
enabled: () => mergeModalActiveRef.current,
|
|
1854
|
+
bindings: [
|
|
1855
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1856
|
+
{ key: "return", cmd: () => {
|
|
1857
|
+
if (mergeModalContextRef.current.availableCount > 0) mergeModalContextRef.current.confirm()
|
|
1858
|
+
} },
|
|
1859
|
+
{ key: "up", cmd: () => moveMergeSelection(-1) },
|
|
1860
|
+
{ key: "k", cmd: () => moveMergeSelection(-1) },
|
|
1861
|
+
{ key: "down", cmd: () => moveMergeSelection(1) },
|
|
1862
|
+
{ key: "j", cmd: () => moveMergeSelection(1) },
|
|
1863
|
+
],
|
|
1864
|
+
}), [])
|
|
1865
|
+
|
|
1866
|
+
// CommentThreadModal: scroll the thread, shortcut to compose a reply.
|
|
1867
|
+
const commentThreadModalActiveRef = useRef(false)
|
|
1868
|
+
commentThreadModalActiveRef.current = commentThreadModalActive
|
|
1869
|
+
const commentThreadCtxRef = useRef({ openDiffCommentModal, setCommentThreadModal, halfPage })
|
|
1870
|
+
commentThreadCtxRef.current = { openDiffCommentModal, setCommentThreadModal, halfPage }
|
|
1871
|
+
const scrollCommentThread = (delta: number) => commentThreadCtxRef.current.setCommentThreadModal((current) => ({
|
|
1872
|
+
...current,
|
|
1873
|
+
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
1874
|
+
}))
|
|
1875
|
+
useBindings(() => ({
|
|
1876
|
+
enabled: () => commentThreadModalActiveRef.current,
|
|
1877
|
+
bindings: [
|
|
1878
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1879
|
+
{ key: "return", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1880
|
+
{ key: "a", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1881
|
+
{ key: "c", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1882
|
+
{ key: "up", cmd: () => scrollCommentThread(-1) },
|
|
1883
|
+
{ key: "k", cmd: () => scrollCommentThread(-1) },
|
|
1884
|
+
{ key: "down", cmd: () => scrollCommentThread(1) },
|
|
1885
|
+
{ key: "j", cmd: () => scrollCommentThread(1) },
|
|
1886
|
+
{ key: "pageup", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1887
|
+
{ key: "ctrl+u", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1888
|
+
{ key: "pagedown", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1889
|
+
{ key: "ctrl+d", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1890
|
+
{ key: "ctrl+v", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1891
|
+
],
|
|
1892
|
+
}), [])
|
|
1893
|
+
|
|
1894
|
+
// LabelModal: nav keys via keymap; text input stays in useKeyboard fallback.
|
|
1895
|
+
const labelModalActiveRef = useRef(false)
|
|
1896
|
+
labelModalActiveRef.current = labelModalActive
|
|
1897
|
+
const labelModalCtxRef = useRef({ toggleLabelAtIndex, setLabelModal, filteredCount: 0 })
|
|
1898
|
+
labelModalCtxRef.current = {
|
|
1899
|
+
toggleLabelAtIndex,
|
|
1900
|
+
setLabelModal,
|
|
1901
|
+
filteredCount: filterLabels(labelModal.availableLabels, labelModal.query).length,
|
|
1902
|
+
}
|
|
1903
|
+
const moveLabelSelection = (delta: -1 | 1) => labelModalCtxRef.current.setLabelModal((current) => {
|
|
1904
|
+
const max = Math.max(0, labelModalCtxRef.current.filteredCount - 1)
|
|
1905
|
+
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1906
|
+
})
|
|
1907
|
+
useBindings(() => ({
|
|
1908
|
+
enabled: () => labelModalActiveRef.current,
|
|
1909
|
+
bindings: [
|
|
1910
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1911
|
+
{ key: "return", cmd: () => labelModalCtxRef.current.toggleLabelAtIndex() },
|
|
1912
|
+
{ key: "up", cmd: () => moveLabelSelection(-1) },
|
|
1913
|
+
{ key: "k", cmd: () => moveLabelSelection(-1) },
|
|
1914
|
+
{ key: "down", cmd: () => moveLabelSelection(1) },
|
|
1915
|
+
{ key: "j", cmd: () => moveLabelSelection(1) },
|
|
1916
|
+
],
|
|
1917
|
+
}), [])
|
|
1918
|
+
|
|
1919
|
+
// ThemeModal: nav + filter-mode toggle. j/k only navigate when not in filter mode
|
|
1920
|
+
// (so users can type those letters into the query).
|
|
1921
|
+
const themeModalActiveRef = useRef(false)
|
|
1922
|
+
themeModalActiveRef.current = themeModalActive
|
|
1923
|
+
const themeModalCtxRef = useRef({
|
|
1924
|
+
filterMode: false,
|
|
1925
|
+
hasResults: true,
|
|
1926
|
+
closeThemeModal,
|
|
1927
|
+
updateThemeQuery,
|
|
1928
|
+
moveThemeSelection,
|
|
1929
|
+
})
|
|
1930
|
+
themeModalCtxRef.current = {
|
|
1931
|
+
filterMode: themeModal.filterMode,
|
|
1932
|
+
hasResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
1933
|
+
closeThemeModal,
|
|
1934
|
+
updateThemeQuery,
|
|
1935
|
+
moveThemeSelection,
|
|
1936
|
+
}
|
|
1937
|
+
useBindings(() => ({
|
|
1938
|
+
enabled: () => themeModalActiveRef.current,
|
|
1939
|
+
bindings: [
|
|
1940
|
+
{ key: "escape", cmd: () => {
|
|
1941
|
+
if (themeModalCtxRef.current.filterMode) themeModalCtxRef.current.updateThemeQuery("", { filterMode: false })
|
|
1942
|
+
else themeModalCtxRef.current.closeThemeModal(false)
|
|
1943
|
+
} },
|
|
1944
|
+
{ key: "/", cmd: () => themeModalCtxRef.current.updateThemeQuery("", { filterMode: true }) },
|
|
1945
|
+
{ key: "return", cmd: () => {
|
|
1946
|
+
if (themeModalCtxRef.current.filterMode && !themeModalCtxRef.current.hasResults) return
|
|
1947
|
+
themeModalCtxRef.current.closeThemeModal(true)
|
|
1948
|
+
} },
|
|
1949
|
+
{ key: "up", cmd: () => themeModalCtxRef.current.moveThemeSelection(-1) },
|
|
1950
|
+
{ key: "down", cmd: () => themeModalCtxRef.current.moveThemeSelection(1) },
|
|
1951
|
+
{ key: "k", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(-1) } },
|
|
1952
|
+
{ key: "j", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(1) } },
|
|
1953
|
+
],
|
|
1954
|
+
}), [])
|
|
1955
|
+
|
|
1956
|
+
// OpenRepositoryModal: escape closes, return submits.
|
|
1957
|
+
const openRepositoryModalActiveRef = useRef(false)
|
|
1958
|
+
openRepositoryModalActiveRef.current = openRepositoryModalActive
|
|
1959
|
+
const openRepositoryFromInputRef = useRef(openRepositoryFromInput)
|
|
1960
|
+
openRepositoryFromInputRef.current = openRepositoryFromInput
|
|
1961
|
+
useBindings(() => ({
|
|
1962
|
+
enabled: () => openRepositoryModalActiveRef.current,
|
|
1963
|
+
bindings: [
|
|
1964
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1965
|
+
{ key: "return", cmd: () => openRepositoryFromInputRef.current() },
|
|
1966
|
+
],
|
|
1967
|
+
}), [])
|
|
1968
|
+
|
|
1969
|
+
// CommentModal: full text editor — escape, submit, all the cursor/edit bindings.
|
|
1970
|
+
const commentModalActiveRef = useRef(false)
|
|
1971
|
+
commentModalActiveRef.current = commentModalActive
|
|
1972
|
+
const commentModalCtxRef = useRef({ submitDiffComment, editComment })
|
|
1973
|
+
commentModalCtxRef.current = { submitDiffComment, editComment }
|
|
1974
|
+
const editComm = (transform: Parameters<typeof editComment>[0]) => commentModalCtxRef.current.editComment(transform)
|
|
1975
|
+
useBindings(() => ({
|
|
1976
|
+
enabled: () => commentModalActiveRef.current,
|
|
1977
|
+
bindings: [
|
|
1978
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1979
|
+
{ key: "ctrl+s", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
1980
|
+
{ key: "ctrl+a", cmd: () => editComm(moveLineStart) },
|
|
1981
|
+
{ key: "ctrl+e", cmd: () => editComm(moveLineEnd) },
|
|
1982
|
+
{ key: "ctrl+b", cmd: () => editComm(editorMoveLeft) },
|
|
1983
|
+
{ key: "ctrl+f", cmd: () => editComm(editorMoveRight) },
|
|
1984
|
+
{ key: "ctrl+w", cmd: () => editComm(deleteWordBackward) },
|
|
1985
|
+
{ key: "ctrl+u", cmd: () => editComm(deleteToLineStart) },
|
|
1986
|
+
{ key: "ctrl+k", cmd: () => editComm(deleteToLineEnd) },
|
|
1987
|
+
{ key: "ctrl+d", cmd: () => editComm(editorDeleteForward) },
|
|
1988
|
+
{ key: "meta+b", cmd: () => editComm(moveWordBackward) },
|
|
1989
|
+
{ key: "meta+left", cmd: () => editComm(moveWordBackward) },
|
|
1990
|
+
{ key: "meta+f", cmd: () => editComm(moveWordForward) },
|
|
1991
|
+
{ key: "meta+right", cmd: () => editComm(moveWordForward) },
|
|
1992
|
+
{ key: "meta+backspace", cmd: () => editComm(deleteWordBackward) },
|
|
1993
|
+
{ key: "meta+delete", cmd: () => editComm(deleteWordForward) },
|
|
1994
|
+
{ key: "backspace", cmd: () => editComm(editorBackspace) },
|
|
1995
|
+
{ key: "delete", cmd: () => editComm(editorDeleteForward) },
|
|
1996
|
+
{ key: "left", cmd: () => editComm(editorMoveLeft) },
|
|
1997
|
+
{ key: "right", cmd: () => editComm(editorMoveRight) },
|
|
1998
|
+
{ key: "up", cmd: () => editComm((state) => moveVertically(state, -1)) },
|
|
1999
|
+
{ key: "down", cmd: () => editComm((state) => moveVertically(state, 1)) },
|
|
2000
|
+
{ key: "home", cmd: () => editComm(moveLineStart) },
|
|
2001
|
+
{ key: "end", cmd: () => editComm(moveLineEnd) },
|
|
2002
|
+
{ key: "shift+return", cmd: () => editComm((state) => insertText(state, "\n")) },
|
|
2003
|
+
{ key: "return", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
2004
|
+
],
|
|
2005
|
+
}), [])
|
|
2006
|
+
|
|
2007
|
+
// CommandPalette: escape closes, return runs, up/k & down/j navigate.
|
|
2008
|
+
const commandPaletteActiveRef = useRef(false)
|
|
2009
|
+
commandPaletteActiveRef.current = commandPaletteActive
|
|
2010
|
+
const commandPaletteCtxRef = useRef({
|
|
2011
|
+
runSelected: () => {},
|
|
2012
|
+
setCommandPalette,
|
|
2013
|
+
paletteCommands: commandPaletteCommands,
|
|
2014
|
+
})
|
|
2015
|
+
commandPaletteCtxRef.current = {
|
|
2016
|
+
runSelected: () => { if (selectedCommand) runCommand(selectedCommand, { notifyDisabled: true, closePalette: true }) },
|
|
2017
|
+
setCommandPalette,
|
|
2018
|
+
paletteCommands: commandPaletteCommands,
|
|
2019
|
+
}
|
|
2020
|
+
const moveCommandPaletteSelection = (delta: -1 | 1) => commandPaletteCtxRef.current.setCommandPalette((current) => {
|
|
2021
|
+
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCtxRef.current.paletteCommands)
|
|
2022
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2023
|
+
})
|
|
2024
|
+
useBindings(() => ({
|
|
2025
|
+
enabled: () => commandPaletteActiveRef.current,
|
|
2026
|
+
bindings: [
|
|
2027
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
2028
|
+
{ key: "ctrl+c", cmd: () => closeActiveModalRef.current() },
|
|
2029
|
+
{ key: "return", cmd: () => commandPaletteCtxRef.current.runSelected() },
|
|
2030
|
+
{ key: "up", cmd: () => moveCommandPaletteSelection(-1) },
|
|
2031
|
+
{ key: "down", cmd: () => moveCommandPaletteSelection(1) },
|
|
2032
|
+
],
|
|
2033
|
+
}), [])
|
|
2034
|
+
|
|
2035
|
+
// FilterMode: escape cancels, return commits.
|
|
2036
|
+
const filterModeRef = useRef(false)
|
|
2037
|
+
filterModeRef.current = filterMode
|
|
2038
|
+
const filterCtxRef = useRef({ filterQuery, filterDraft, setFilterQuery, setFilterDraft, setFilterMode })
|
|
2039
|
+
filterCtxRef.current = { filterQuery, filterDraft, setFilterQuery, setFilterDraft, setFilterMode }
|
|
2040
|
+
useBindings(() => ({
|
|
2041
|
+
enabled: () => filterModeRef.current,
|
|
2042
|
+
bindings: [
|
|
2043
|
+
{ key: "escape", cmd: () => {
|
|
2044
|
+
filterCtxRef.current.setFilterDraft(filterCtxRef.current.filterQuery)
|
|
2045
|
+
filterCtxRef.current.setFilterMode(false)
|
|
2046
|
+
} },
|
|
2047
|
+
{ key: "return", cmd: () => {
|
|
2048
|
+
filterCtxRef.current.setFilterQuery(filterCtxRef.current.filterDraft)
|
|
2049
|
+
filterCtxRef.current.setFilterMode(false)
|
|
2050
|
+
} },
|
|
2051
|
+
],
|
|
2052
|
+
}), [])
|
|
2053
|
+
|
|
2054
|
+
useKeyboard((key) => {
|
|
2055
|
+
if (commandPaletteActive) {
|
|
2056
|
+
if (isSingleLineInputKey(key)) {
|
|
2057
|
+
setCommandPalette((current) => {
|
|
2058
|
+
const query = editSingleLineInput(current.query, key) ?? current.query
|
|
2059
|
+
return current.query === query && current.selectedIndex === 0 ? current : { ...current, query, selectedIndex: 0 }
|
|
2060
|
+
})
|
|
1478
2061
|
}
|
|
1479
2062
|
return
|
|
1480
2063
|
}
|
|
1481
2064
|
|
|
1482
|
-
if (
|
|
1483
|
-
if (key
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
return
|
|
1490
|
-
}
|
|
1491
|
-
if (key.ctrl && key.name === "a") {
|
|
1492
|
-
editComment(moveLineStart)
|
|
1493
|
-
return
|
|
1494
|
-
}
|
|
1495
|
-
if (key.ctrl && key.name === "e") {
|
|
1496
|
-
editComment(moveLineEnd)
|
|
1497
|
-
return
|
|
1498
|
-
}
|
|
1499
|
-
if (key.ctrl && key.name === "b") {
|
|
1500
|
-
editComment(editorMoveLeft)
|
|
1501
|
-
return
|
|
1502
|
-
}
|
|
1503
|
-
if (key.ctrl && key.name === "f") {
|
|
1504
|
-
editComment(editorMoveRight)
|
|
1505
|
-
return
|
|
1506
|
-
}
|
|
1507
|
-
if (key.ctrl && key.name === "w") {
|
|
1508
|
-
editComment(deleteWordBackward)
|
|
1509
|
-
return
|
|
1510
|
-
}
|
|
1511
|
-
if (key.ctrl && key.name === "u") {
|
|
1512
|
-
editComment(deleteToLineStart)
|
|
1513
|
-
return
|
|
1514
|
-
}
|
|
1515
|
-
if (key.ctrl && key.name === "k") {
|
|
1516
|
-
editComment(deleteToLineEnd)
|
|
1517
|
-
return
|
|
1518
|
-
}
|
|
1519
|
-
if (key.ctrl && key.name === "d") {
|
|
1520
|
-
editComment(editorDeleteForward)
|
|
1521
|
-
return
|
|
1522
|
-
}
|
|
1523
|
-
if ((key.meta || key.option) && (key.name === "b" || key.name === "left")) {
|
|
1524
|
-
editComment(moveWordBackward)
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
if ((key.meta || key.option) && (key.name === "f" || key.name === "right")) {
|
|
1528
|
-
editComment(moveWordForward)
|
|
1529
|
-
return
|
|
1530
|
-
}
|
|
1531
|
-
if ((key.meta || key.option) && (key.name === "backspace" || key.name === "delete")) {
|
|
1532
|
-
editComment(key.name === "delete" ? deleteWordForward : deleteWordBackward)
|
|
1533
|
-
return
|
|
1534
|
-
}
|
|
1535
|
-
if (key.name === "backspace") {
|
|
1536
|
-
editComment(editorBackspace)
|
|
1537
|
-
return
|
|
1538
|
-
}
|
|
1539
|
-
if (key.name === "delete") {
|
|
1540
|
-
editComment(editorDeleteForward)
|
|
1541
|
-
return
|
|
1542
|
-
}
|
|
1543
|
-
if (key.name === "left") {
|
|
1544
|
-
editComment(editorMoveLeft)
|
|
1545
|
-
return
|
|
1546
|
-
}
|
|
1547
|
-
if (key.name === "right") {
|
|
1548
|
-
editComment(editorMoveRight)
|
|
1549
|
-
return
|
|
1550
|
-
}
|
|
1551
|
-
if (key.name === "up") {
|
|
1552
|
-
editComment((state) => moveVertically(state, -1))
|
|
1553
|
-
return
|
|
1554
|
-
}
|
|
1555
|
-
if (key.name === "down") {
|
|
1556
|
-
editComment((state) => moveVertically(state, 1))
|
|
1557
|
-
return
|
|
1558
|
-
}
|
|
1559
|
-
if (key.name === "home") {
|
|
1560
|
-
editComment(moveLineStart)
|
|
1561
|
-
return
|
|
1562
|
-
}
|
|
1563
|
-
if (key.name === "end") {
|
|
1564
|
-
editComment(moveLineEnd)
|
|
1565
|
-
return
|
|
1566
|
-
}
|
|
1567
|
-
if ((key.name === "return" || key.name === "enter") && key.shift) {
|
|
1568
|
-
editComment((state) => insertText(state, "\n"))
|
|
1569
|
-
return
|
|
1570
|
-
}
|
|
1571
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1572
|
-
submitDiffComment()
|
|
1573
|
-
return
|
|
1574
|
-
}
|
|
1575
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1576
|
-
editComment((state) => insertText(state, key.sequence))
|
|
1577
|
-
return
|
|
2065
|
+
if (openRepositoryModalActive) {
|
|
2066
|
+
if (isSingleLineInputKey(key)) {
|
|
2067
|
+
setOpenRepositoryModal((current) => ({
|
|
2068
|
+
...current,
|
|
2069
|
+
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
2070
|
+
error: null,
|
|
2071
|
+
}))
|
|
1578
2072
|
}
|
|
1579
2073
|
return
|
|
1580
2074
|
}
|
|
1581
2075
|
|
|
1582
|
-
if (
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
return
|
|
1586
|
-
}
|
|
1587
|
-
if (key.name === "return" || key.name === "enter" || key.name === "a" || key.name === "c") {
|
|
1588
|
-
openDiffCommentModal()
|
|
1589
|
-
return
|
|
1590
|
-
}
|
|
1591
|
-
if (key.name === "up" || key.name === "k") {
|
|
1592
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - 1) }))
|
|
1593
|
-
return
|
|
1594
|
-
}
|
|
1595
|
-
if (key.name === "down" || key.name === "j") {
|
|
1596
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + 1 }))
|
|
1597
|
-
return
|
|
1598
|
-
}
|
|
1599
|
-
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1600
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - halfPage) }))
|
|
2076
|
+
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
2077
|
+
if (themeModalActive) {
|
|
2078
|
+
closeThemeModal(false)
|
|
1601
2079
|
return
|
|
1602
2080
|
}
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
2081
|
+
if (activeModal._tag !== "None") {
|
|
2082
|
+
closeActiveModal()
|
|
1605
2083
|
return
|
|
1606
2084
|
}
|
|
2085
|
+
runCommandById("app.quit")
|
|
1607
2086
|
return
|
|
1608
2087
|
}
|
|
1609
2088
|
|
|
1610
|
-
if (
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
1613
|
-
return
|
|
1614
|
-
}
|
|
1615
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1616
|
-
confirmClosePullRequest()
|
|
1617
|
-
return
|
|
2089
|
+
if (themeModalActive) {
|
|
2090
|
+
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
2091
|
+
editThemeQuery((query) => editSingleLineInput(query, key) ?? query)
|
|
1618
2092
|
}
|
|
1619
2093
|
return
|
|
1620
2094
|
}
|
|
1621
2095
|
|
|
1622
|
-
if (
|
|
1623
|
-
const
|
|
1624
|
-
if (
|
|
1625
|
-
closeActiveModal()
|
|
1626
|
-
return
|
|
1627
|
-
}
|
|
1628
|
-
if ((key.name === "return" || key.name === "enter") && options.length > 0) {
|
|
1629
|
-
confirmMergeAction()
|
|
1630
|
-
return
|
|
1631
|
-
}
|
|
1632
|
-
if (key.name === "up" || key.name === "k") {
|
|
1633
|
-
setMergeModal((current) => ({
|
|
1634
|
-
...current,
|
|
1635
|
-
selectedIndex: Math.max(0, current.selectedIndex - 1),
|
|
1636
|
-
}))
|
|
1637
|
-
return
|
|
1638
|
-
}
|
|
1639
|
-
if (key.name === "down" || key.name === "j") {
|
|
1640
|
-
setMergeModal((current) => ({
|
|
1641
|
-
...current,
|
|
1642
|
-
selectedIndex: Math.min(Math.max(0, options.length - 1), current.selectedIndex + 1),
|
|
1643
|
-
}))
|
|
1644
|
-
return
|
|
1645
|
-
}
|
|
2096
|
+
if (commentModalActive) {
|
|
2097
|
+
const text = printableKeyText(key)
|
|
2098
|
+
if (text) editComment((state) => insertText(state, text))
|
|
1646
2099
|
return
|
|
1647
2100
|
}
|
|
1648
2101
|
|
|
1649
|
-
|
|
2102
|
+
|
|
2103
|
+
|
|
2104
|
+
|
|
1650
2105
|
if (labelModalActive) {
|
|
1651
|
-
if (key
|
|
1652
|
-
closeActiveModal()
|
|
1653
|
-
return
|
|
1654
|
-
}
|
|
1655
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1656
|
-
toggleLabelAtIndex()
|
|
1657
|
-
return
|
|
1658
|
-
}
|
|
1659
|
-
if (key.name === "up" || key.name === "k") {
|
|
2106
|
+
if (isSingleLineInputKey(key)) {
|
|
1660
2107
|
setLabelModal((current) => ({
|
|
1661
2108
|
...current,
|
|
1662
|
-
|
|
1663
|
-
}))
|
|
1664
|
-
return
|
|
1665
|
-
}
|
|
1666
|
-
if (key.name === "down" || key.name === "j") {
|
|
1667
|
-
const filtered = labelModal.availableLabels.filter((label) =>
|
|
1668
|
-
labelModal.query.length === 0 || label.name.toLowerCase().includes(labelModal.query.toLowerCase()),
|
|
1669
|
-
)
|
|
1670
|
-
setLabelModal((current) => ({
|
|
1671
|
-
...current,
|
|
1672
|
-
selectedIndex: Math.min(Math.max(0, filtered.length - 1), current.selectedIndex + 1),
|
|
1673
|
-
}))
|
|
1674
|
-
return
|
|
1675
|
-
}
|
|
1676
|
-
if (key.name === "backspace") {
|
|
1677
|
-
setLabelModal((current) => ({
|
|
1678
|
-
...current,
|
|
1679
|
-
query: current.query.slice(0, -1),
|
|
2109
|
+
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
1680
2110
|
selectedIndex: 0,
|
|
1681
2111
|
}))
|
|
1682
|
-
return
|
|
1683
|
-
}
|
|
1684
|
-
if (key.ctrl && key.name === "u") {
|
|
1685
|
-
setLabelModal((current) => ({ ...current, query: "", selectedIndex: 0 }))
|
|
1686
|
-
return
|
|
1687
|
-
}
|
|
1688
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
1689
|
-
setLabelModal((current) => ({
|
|
1690
|
-
...current,
|
|
1691
|
-
query: current.query + key.sequence,
|
|
1692
|
-
selectedIndex: 0,
|
|
1693
|
-
}))
|
|
1694
|
-
return
|
|
1695
2112
|
}
|
|
1696
2113
|
return
|
|
1697
2114
|
}
|
|
@@ -1703,7 +2120,7 @@ export const App = () => {
|
|
|
1703
2120
|
return
|
|
1704
2121
|
}
|
|
1705
2122
|
if (key.name === "c") {
|
|
1706
|
-
|
|
2123
|
+
runCommandById("diff.comment-mode")
|
|
1707
2124
|
return
|
|
1708
2125
|
}
|
|
1709
2126
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -1712,7 +2129,7 @@ export const App = () => {
|
|
|
1712
2129
|
return
|
|
1713
2130
|
}
|
|
1714
2131
|
if (key.name === "a") {
|
|
1715
|
-
|
|
2132
|
+
runCommandById("diff.add-comment")
|
|
1716
2133
|
return
|
|
1717
2134
|
}
|
|
1718
2135
|
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
@@ -1748,23 +2165,22 @@ export const App = () => {
|
|
|
1748
2165
|
return
|
|
1749
2166
|
}
|
|
1750
2167
|
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
1751
|
-
|
|
2168
|
+
runCommandById("diff.next-file")
|
|
1752
2169
|
return
|
|
1753
2170
|
}
|
|
1754
2171
|
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
1755
|
-
|
|
2172
|
+
runCommandById("diff.previous-file")
|
|
1756
2173
|
return
|
|
1757
2174
|
}
|
|
1758
2175
|
return
|
|
1759
2176
|
}
|
|
1760
2177
|
|
|
1761
2178
|
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
1762
|
-
|
|
1763
|
-
setDiffCommentMode(false)
|
|
2179
|
+
runCommandById("diff.close")
|
|
1764
2180
|
return
|
|
1765
2181
|
}
|
|
1766
2182
|
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
1767
|
-
|
|
2183
|
+
runCommandById("diff.comment-mode")
|
|
1768
2184
|
return
|
|
1769
2185
|
}
|
|
1770
2186
|
if (key.name === "home") {
|
|
@@ -1801,67 +2217,64 @@ export const App = () => {
|
|
|
1801
2217
|
return
|
|
1802
2218
|
}
|
|
1803
2219
|
if (key.name === "v") {
|
|
1804
|
-
|
|
2220
|
+
runCommandById("diff.toggle-view")
|
|
1805
2221
|
return
|
|
1806
2222
|
}
|
|
1807
2223
|
if (key.name === "w") {
|
|
1808
|
-
|
|
2224
|
+
runCommandById("diff.toggle-wrap")
|
|
1809
2225
|
return
|
|
1810
2226
|
}
|
|
1811
2227
|
if (key.name === "r" && selectedPullRequest) {
|
|
1812
|
-
|
|
1813
|
-
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
|
|
2228
|
+
runCommandById("diff.reload")
|
|
1814
2229
|
return
|
|
1815
2230
|
}
|
|
1816
2231
|
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
1817
|
-
|
|
2232
|
+
runCommandById("diff.next-file")
|
|
1818
2233
|
return
|
|
1819
2234
|
}
|
|
1820
2235
|
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
1821
|
-
|
|
2236
|
+
runCommandById("diff.previous-file")
|
|
1822
2237
|
return
|
|
1823
2238
|
}
|
|
1824
2239
|
if (key.name === "o" && selectedPullRequest) {
|
|
1825
|
-
|
|
2240
|
+
runCommandById("pull.open-browser")
|
|
1826
2241
|
return
|
|
1827
2242
|
}
|
|
1828
2243
|
return
|
|
1829
2244
|
}
|
|
1830
2245
|
|
|
1831
|
-
// Fullscreen detail mode handles its own navigation keys.
|
|
1832
2246
|
if (detailFullView) {
|
|
1833
2247
|
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
1834
2248
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1835
|
-
|
|
1836
|
-
setDetailScrollOffset(0)
|
|
2249
|
+
runCommandById("detail.close")
|
|
1837
2250
|
return
|
|
1838
2251
|
}
|
|
1839
2252
|
if (isThemeKey(key)) {
|
|
1840
|
-
|
|
2253
|
+
runCommandById("theme.open")
|
|
1841
2254
|
return
|
|
1842
2255
|
}
|
|
1843
2256
|
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
1844
|
-
|
|
2257
|
+
runCommandById("diff.open")
|
|
1845
2258
|
return
|
|
1846
2259
|
}
|
|
1847
2260
|
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
1848
|
-
|
|
2261
|
+
runCommandById("pull.close")
|
|
1849
2262
|
return
|
|
1850
2263
|
}
|
|
1851
2264
|
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
1852
|
-
|
|
2265
|
+
runCommandById("pull.labels")
|
|
1853
2266
|
return
|
|
1854
2267
|
}
|
|
1855
2268
|
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
1856
|
-
|
|
2269
|
+
runCommandById("pull.merge")
|
|
1857
2270
|
return
|
|
1858
2271
|
}
|
|
1859
2272
|
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
1860
|
-
|
|
2273
|
+
runCommandById("pull.toggle-draft")
|
|
1861
2274
|
return
|
|
1862
2275
|
}
|
|
1863
2276
|
if (plainKey && key.name === "r") {
|
|
1864
|
-
|
|
2277
|
+
runCommandById("pull.refresh")
|
|
1865
2278
|
return
|
|
1866
2279
|
}
|
|
1867
2280
|
if (key.name === "home") {
|
|
@@ -1909,43 +2322,21 @@ export const App = () => {
|
|
|
1909
2322
|
return
|
|
1910
2323
|
}
|
|
1911
2324
|
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
1912
|
-
|
|
2325
|
+
runCommandById("pull.open-browser")
|
|
1913
2326
|
return
|
|
1914
2327
|
}
|
|
1915
2328
|
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
1916
|
-
|
|
2329
|
+
runCommandById("pull.copy-metadata")
|
|
1917
2330
|
return
|
|
1918
2331
|
}
|
|
1919
2332
|
return
|
|
1920
2333
|
}
|
|
1921
2334
|
|
|
1922
2335
|
if (filterMode) {
|
|
1923
|
-
if (key
|
|
1924
|
-
setFilterDraft(
|
|
1925
|
-
setFilterMode(false)
|
|
1926
|
-
return
|
|
1927
|
-
}
|
|
1928
|
-
if (key.name === "enter") {
|
|
1929
|
-
setFilterQuery(filterDraft)
|
|
1930
|
-
setFilterMode(false)
|
|
1931
|
-
return
|
|
1932
|
-
}
|
|
1933
|
-
if (key.ctrl && key.name === "u") {
|
|
1934
|
-
setFilterDraft("")
|
|
1935
|
-
return
|
|
1936
|
-
}
|
|
1937
|
-
if (key.ctrl && key.name === "w") {
|
|
1938
|
-
setFilterDraft((current) => deleteLastWord(current))
|
|
1939
|
-
return
|
|
1940
|
-
}
|
|
1941
|
-
if (key.name === "backspace") {
|
|
1942
|
-
setFilterDraft((current) => current.slice(0, -1))
|
|
1943
|
-
return
|
|
1944
|
-
}
|
|
1945
|
-
if (!key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
1946
|
-
setFilterDraft((current) => current + key.sequence)
|
|
1947
|
-
return
|
|
2336
|
+
if (isSingleLineInputKey(key)) {
|
|
2337
|
+
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
1948
2338
|
}
|
|
2339
|
+
return
|
|
1949
2340
|
}
|
|
1950
2341
|
|
|
1951
2342
|
if (key.name === "tab") {
|
|
@@ -1953,25 +2344,27 @@ export const App = () => {
|
|
|
1953
2344
|
return
|
|
1954
2345
|
}
|
|
1955
2346
|
|
|
1956
|
-
if (isThemeKey(key)) {
|
|
1957
|
-
openThemeModal()
|
|
1958
|
-
return
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
if (key.name === "/") {
|
|
1962
|
-
setFilterDraft(filterQuery)
|
|
1963
|
-
setFilterMode(true)
|
|
1964
|
-
return
|
|
1965
|
-
}
|
|
1966
2347
|
if (key.name === "escape" && filterQuery.length > 0) {
|
|
1967
|
-
|
|
1968
|
-
setFilterDraft("")
|
|
1969
|
-
setFilterMode(false)
|
|
2348
|
+
runCommandById("filter.clear")
|
|
1970
2349
|
return
|
|
1971
2350
|
}
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
1974
|
-
|
|
2351
|
+
if (isWideLayout && selectedPullRequest && !detailFullView && !diffFullView) {
|
|
2352
|
+
if (key.name === "home") {
|
|
2353
|
+
scrollDetailPreviewTo(0)
|
|
2354
|
+
return
|
|
2355
|
+
}
|
|
2356
|
+
if (key.name === "end") {
|
|
2357
|
+
scrollDetailPreviewTo(Number.MAX_SAFE_INTEGER)
|
|
2358
|
+
return
|
|
2359
|
+
}
|
|
2360
|
+
if (key.name === "pageup") {
|
|
2361
|
+
scrollDetailPreviewBy(-halfPage)
|
|
2362
|
+
return
|
|
2363
|
+
}
|
|
2364
|
+
if (key.name === "pagedown") {
|
|
2365
|
+
scrollDetailPreviewBy(halfPage)
|
|
2366
|
+
return
|
|
2367
|
+
}
|
|
1975
2368
|
}
|
|
1976
2369
|
if (
|
|
1977
2370
|
key.name === "[" ||
|
|
@@ -2023,6 +2416,10 @@ export const App = () => {
|
|
|
2023
2416
|
return
|
|
2024
2417
|
}
|
|
2025
2418
|
if (key.name === "down" || key.name === "j") {
|
|
2419
|
+
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2420
|
+
loadMorePullRequests()
|
|
2421
|
+
return
|
|
2422
|
+
}
|
|
2026
2423
|
setSelectedIndex((current) => {
|
|
2027
2424
|
if (visiblePullRequests.length === 0) return 0
|
|
2028
2425
|
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
@@ -2033,39 +2430,6 @@ export const App = () => {
|
|
|
2033
2430
|
() => setSelectedIndex(0),
|
|
2034
2431
|
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2035
2432
|
)) return
|
|
2036
|
-
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
2037
|
-
setDetailFullView(true)
|
|
2038
|
-
setDetailScrollOffset(0)
|
|
2039
|
-
return
|
|
2040
|
-
}
|
|
2041
|
-
if (key.name === "d" && selectedPullRequest) {
|
|
2042
|
-
openDiffView()
|
|
2043
|
-
return
|
|
2044
|
-
}
|
|
2045
|
-
if (key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2046
|
-
openCloseModal()
|
|
2047
|
-
return
|
|
2048
|
-
}
|
|
2049
|
-
if (key.name === "l" && selectedPullRequest) {
|
|
2050
|
-
openLabelModal()
|
|
2051
|
-
return
|
|
2052
|
-
}
|
|
2053
|
-
if (key.name === "m" || key.name === "M") {
|
|
2054
|
-
if (selectedPullRequest) openMergeModal()
|
|
2055
|
-
return
|
|
2056
|
-
}
|
|
2057
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
2058
|
-
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
2059
|
-
return
|
|
2060
|
-
}
|
|
2061
|
-
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
2062
|
-
toggleSelectedPullRequestDraftStatus()
|
|
2063
|
-
return
|
|
2064
|
-
}
|
|
2065
|
-
if (key.name === "y" && selectedPullRequest) {
|
|
2066
|
-
copySelectedPullRequestMetadata()
|
|
2067
|
-
return
|
|
2068
|
-
}
|
|
2069
2433
|
})
|
|
2070
2434
|
|
|
2071
2435
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
@@ -2073,19 +2437,20 @@ export const App = () => {
|
|
|
2073
2437
|
const wideFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2074
2438
|
pullRequest: selectedPullRequest,
|
|
2075
2439
|
contentWidth: fullscreenContentWidth,
|
|
2076
|
-
bodyLines:
|
|
2440
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2077
2441
|
paneWidth: contentWidth,
|
|
2078
2442
|
showChecks: true,
|
|
2079
2443
|
}) > wideBodyHeight
|
|
2080
2444
|
const narrowFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2081
2445
|
pullRequest: selectedPullRequest,
|
|
2082
2446
|
contentWidth: fullscreenContentWidth,
|
|
2083
|
-
bodyLines:
|
|
2447
|
+
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2084
2448
|
paneWidth: contentWidth,
|
|
2085
2449
|
}) > wideBodyHeight
|
|
2086
2450
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2087
2451
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2088
|
-
const
|
|
2452
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2453
|
+
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2089
2454
|
|
|
2090
2455
|
const prListProps = {
|
|
2091
2456
|
groups: visibleGroups,
|
|
@@ -2095,6 +2460,9 @@ export const App = () => {
|
|
|
2095
2460
|
filterText: visibleFilterText,
|
|
2096
2461
|
showFilterBar: filterMode || filterQuery.length > 0,
|
|
2097
2462
|
isFilterEditing: filterMode,
|
|
2463
|
+
loadedCount: loadedPullRequestCount,
|
|
2464
|
+
hasMore: hasMorePullRequests,
|
|
2465
|
+
isLoadingMore: isLoadingMorePullRequests,
|
|
2098
2466
|
onSelectPullRequest: selectPullRequestByUrl,
|
|
2099
2467
|
} as const
|
|
2100
2468
|
|
|
@@ -2103,29 +2471,49 @@ export const App = () => {
|
|
|
2103
2471
|
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
2104
2472
|
const labelModalLeft = centeredOffset(contentWidth, labelModalWidth)
|
|
2105
2473
|
const labelModalTop = centeredOffset(terminalHeight, labelModalHeight)
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
const
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2116
|
-
const
|
|
2117
|
-
const
|
|
2474
|
+
const sizedModal = (minW: number, maxW: number, padX: number, maxH: number) => {
|
|
2475
|
+
const w = Math.min(maxW, Math.max(minW, contentWidth - padX))
|
|
2476
|
+
const h = Math.min(maxH, terminalHeight - 4)
|
|
2477
|
+
return { width: w, height: h, left: centeredOffset(contentWidth, w), top: centeredOffset(terminalHeight, h) }
|
|
2478
|
+
}
|
|
2479
|
+
const closeLayout = sizedModal(46, 68, 12, 12)
|
|
2480
|
+
const closeModalWidth = closeLayout.width
|
|
2481
|
+
const closeModalHeight = closeLayout.height
|
|
2482
|
+
const closeModalLeft = closeLayout.left
|
|
2483
|
+
const closeModalTop = closeLayout.top
|
|
2484
|
+
const commentLayout = sizedModal(46, 76, 8, 16)
|
|
2485
|
+
const commentModalWidth = commentLayout.width
|
|
2486
|
+
const commentModalHeight = commentLayout.height
|
|
2487
|
+
const commentModalLeft = commentLayout.left
|
|
2488
|
+
const commentModalTop = commentLayout.top
|
|
2489
|
+
const commentThreadLayout = sizedModal(50, 86, 8, 22)
|
|
2490
|
+
const commentThreadModalWidth = commentThreadLayout.width
|
|
2491
|
+
const commentThreadModalHeight = commentThreadLayout.height
|
|
2492
|
+
const commentThreadModalLeft = commentThreadLayout.left
|
|
2493
|
+
const commentThreadModalTop = commentThreadLayout.top
|
|
2118
2494
|
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2119
2495
|
? `${selectedDiffCommentAnchor.path}:${selectedDiffCommentAnchor.line} ${selectedDiffCommentAnchor.side === "RIGHT" ? "right" : "left"}`
|
|
2120
2496
|
: "No diff line selected"
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
const
|
|
2126
|
-
const
|
|
2127
|
-
const
|
|
2128
|
-
const
|
|
2497
|
+
const mergeLayout = sizedModal(46, 68, 12, 16)
|
|
2498
|
+
const mergeModalWidth = mergeLayout.width
|
|
2499
|
+
const mergeModalHeight = mergeLayout.height
|
|
2500
|
+
const mergeModalLeft = mergeLayout.left
|
|
2501
|
+
const mergeModalTop = mergeLayout.top
|
|
2502
|
+
const themeLayout = sizedModal(38, 58, 12, 16)
|
|
2503
|
+
const themeModalWidth = themeLayout.width
|
|
2504
|
+
const themeModalHeight = themeLayout.height
|
|
2505
|
+
const themeModalLeft = themeLayout.left
|
|
2506
|
+
const themeModalTop = themeLayout.top
|
|
2507
|
+
const openRepositoryLayout = sizedModal(46, 76, 8, 8)
|
|
2508
|
+
const openRepositoryModalWidth = openRepositoryLayout.width
|
|
2509
|
+
const openRepositoryModalHeight = openRepositoryLayout.height
|
|
2510
|
+
const openRepositoryModalLeft = openRepositoryLayout.left
|
|
2511
|
+
const openRepositoryModalTop = openRepositoryLayout.top
|
|
2512
|
+
const commandPaletteLayout = sizedModal(50, 88, 8, 24)
|
|
2513
|
+
const commandPaletteWidth = commandPaletteLayout.width
|
|
2514
|
+
const commandPaletteHeight = commandPaletteLayout.height
|
|
2515
|
+
const commandPaletteLeft = commandPaletteLayout.left
|
|
2516
|
+
const commandPaletteTop = commandPaletteLayout.top
|
|
2129
2517
|
|
|
2130
2518
|
return (
|
|
2131
2519
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
@@ -2158,6 +2546,11 @@ export const App = () => {
|
|
|
2158
2546
|
onSelectCommentLine={selectDiffCommentLine}
|
|
2159
2547
|
themeId={themeId}
|
|
2160
2548
|
/>
|
|
2549
|
+
) : detailFullView && isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2550
|
+
<box flexGrow={1} flexDirection="column">
|
|
2551
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks={isWideLayout} />
|
|
2552
|
+
<LoadingPane content={detailLoadingContent} width={contentWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout))} />
|
|
2553
|
+
</box>
|
|
2161
2554
|
) : isWideLayout && detailFullView ? (
|
|
2162
2555
|
<box flexGrow={1} flexDirection="column">
|
|
2163
2556
|
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
@@ -2166,6 +2559,7 @@ export const App = () => {
|
|
|
2166
2559
|
viewerUsername={username}
|
|
2167
2560
|
contentWidth={fullscreenContentWidth}
|
|
2168
2561
|
bodyLines={fullscreenBodyLines}
|
|
2562
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2169
2563
|
paneWidth={contentWidth}
|
|
2170
2564
|
showChecks
|
|
2171
2565
|
placeholderContent={detailPlaceholderContent}
|
|
@@ -2175,19 +2569,26 @@ export const App = () => {
|
|
|
2175
2569
|
</scrollbox>
|
|
2176
2570
|
</box>
|
|
2177
2571
|
) : isWideLayout ? (
|
|
2178
|
-
|
|
2179
|
-
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column"
|
|
2180
|
-
<scrollbox height={wideBodyHeight} flexGrow={0}>
|
|
2181
|
-
<
|
|
2572
|
+
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
2573
|
+
<box width={leftPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2574
|
+
<scrollbox ref={prListScrollRef} focusable={false} height={wideBodyHeight} flexGrow={0}>
|
|
2575
|
+
<box paddingLeft={sectionPadding} paddingRight={0}>
|
|
2576
|
+
<PullRequestList key={`wide-${leftContentWidth}`} {...prListProps} contentWidth={leftContentWidth} />
|
|
2577
|
+
</box>
|
|
2182
2578
|
</scrollbox>
|
|
2183
2579
|
</box>
|
|
2184
2580
|
<SeparatorColumn height={wideBodyHeight} junctionRows={detailJunctions} />
|
|
2185
2581
|
<box width={rightPaneWidth} height={wideBodyHeight} flexDirection="column">
|
|
2186
|
-
{selectedPullRequest ? (
|
|
2582
|
+
{isSelectedPullRequestDetailLoading && selectedPullRequest ? (
|
|
2583
|
+
<>
|
|
2584
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2585
|
+
<LoadingPane content={detailLoadingContent} width={rightPaneWidth} height={Math.max(1, wideBodyHeight - getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true))} />
|
|
2586
|
+
</>
|
|
2587
|
+
) : selectedPullRequest ? (
|
|
2187
2588
|
<>
|
|
2188
2589
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2189
|
-
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2190
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2590
|
+
<scrollbox ref={detailPreviewScrollRef} flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2591
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2191
2592
|
</scrollbox>
|
|
2192
2593
|
</>
|
|
2193
2594
|
) : (
|
|
@@ -2203,6 +2604,7 @@ export const App = () => {
|
|
|
2203
2604
|
viewerUsername={username}
|
|
2204
2605
|
contentWidth={fullscreenContentWidth}
|
|
2205
2606
|
bodyLines={fullscreenBodyLines}
|
|
2607
|
+
bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT}
|
|
2206
2608
|
paneWidth={contentWidth}
|
|
2207
2609
|
placeholderContent={detailPlaceholderContent}
|
|
2208
2610
|
loadingIndicator={loadingIndicator}
|
|
@@ -2215,7 +2617,7 @@ export const App = () => {
|
|
|
2215
2617
|
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2216
2618
|
<Divider width={contentWidth} />
|
|
2217
2619
|
<box flexGrow={1} flexDirection="column">
|
|
2218
|
-
<scrollbox flexGrow={1}>
|
|
2620
|
+
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
2219
2621
|
<box paddingLeft={sectionPadding} paddingRight={sectionPadding}>
|
|
2220
2622
|
<PullRequestList key={`narrow-${fullscreenContentWidth}`} {...prListProps} contentWidth={fullscreenContentWidth} />
|
|
2221
2623
|
</box>
|
|
@@ -2310,6 +2712,26 @@ export const App = () => {
|
|
|
2310
2712
|
offsetTop={themeModalTop}
|
|
2311
2713
|
/>
|
|
2312
2714
|
) : null}
|
|
2715
|
+
{openRepositoryModalActive ? (
|
|
2716
|
+
<OpenRepositoryModal
|
|
2717
|
+
state={openRepositoryModal}
|
|
2718
|
+
modalWidth={openRepositoryModalWidth}
|
|
2719
|
+
modalHeight={openRepositoryModalHeight}
|
|
2720
|
+
offsetLeft={openRepositoryModalLeft}
|
|
2721
|
+
offsetTop={openRepositoryModalTop}
|
|
2722
|
+
/>
|
|
2723
|
+
) : null}
|
|
2724
|
+
{commandPaletteActive ? (
|
|
2725
|
+
<CommandPalette
|
|
2726
|
+
commands={commandPaletteCommands}
|
|
2727
|
+
query={commandPalette.query}
|
|
2728
|
+
selectedIndex={selectedCommandIndex}
|
|
2729
|
+
modalWidth={commandPaletteWidth}
|
|
2730
|
+
modalHeight={commandPaletteHeight}
|
|
2731
|
+
offsetLeft={commandPaletteLeft}
|
|
2732
|
+
offsetTop={commandPaletteTop}
|
|
2733
|
+
/>
|
|
2734
|
+
) : null}
|
|
2313
2735
|
</box>
|
|
2314
2736
|
)
|
|
2315
2737
|
}
|