@kitlangton/ghui 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/App.tsx +78 -4
- package/src/index.tsx +9 -1
- package/src/ui/DetailsPane.tsx +31 -10
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { Observability } from "./observability.js"
|
|
|
13
13
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
14
|
import { colors } from "./ui/colors.js"
|
|
15
15
|
import { pullRequestDiffKey, splitPatchFiles, type PullRequestDiffState } from "./ui/diff.js"
|
|
16
|
-
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailJunctionRows, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
16
|
+
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
17
17
|
import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
|
|
18
18
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
19
19
|
import { initialLabelModalState, initialMergeModalState, LabelModal, MergeModal } from "./ui/modals.js"
|
|
@@ -39,6 +39,9 @@ interface DetailPlaceholderInput {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const PR_FETCH_RETRIES = 6
|
|
42
|
+
const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
43
|
+
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
44
|
+
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
42
45
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
43
46
|
|
|
44
47
|
const retryProgressAtom = Atom.make<RetryProgress | null>(null).pipe(Atom.keepAlive)
|
|
@@ -218,6 +221,7 @@ export const App = () => {
|
|
|
218
221
|
const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
|
|
219
222
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
220
223
|
const [loadingFrame, setLoadingFrame] = useState(0)
|
|
224
|
+
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
221
225
|
const usernameResult = useAtomValue(usernameAtom)
|
|
222
226
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
223
227
|
const loadPullRequestDetails = useAtomSet(listOpenPullRequestDetailsAtom, { mode: "promise" })
|
|
@@ -242,6 +246,12 @@ export const App = () => {
|
|
|
242
246
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
243
247
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
244
248
|
const detailHydrationRef = useRef<number | null>(null)
|
|
249
|
+
const lastPullRequestRefreshAtRef = useRef(0)
|
|
250
|
+
const terminalFocusedRef = useRef(true)
|
|
251
|
+
const terminalWasBlurredRef = useRef(false)
|
|
252
|
+
const pullRequestStatusRef = useRef<LoadStatus>("loading")
|
|
253
|
+
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
254
|
+
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
245
255
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
246
256
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
247
257
|
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
@@ -281,6 +291,7 @@ export const App = () => {
|
|
|
281
291
|
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
282
292
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
283
293
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
294
|
+
pullRequestStatusRef.current = pullRequestStatus
|
|
284
295
|
|
|
285
296
|
const effectiveFilterQuery = (filterMode ? filterDraft : filterQuery).trim().toLowerCase()
|
|
286
297
|
const visibleFilterText = filterMode ? filterDraft : filterQuery
|
|
@@ -332,6 +343,53 @@ export const App = () => {
|
|
|
332
343
|
refreshPullRequestsAtom()
|
|
333
344
|
if (message) flashNotice(message)
|
|
334
345
|
}
|
|
346
|
+
refreshPullRequestsRef.current = refreshPullRequests
|
|
347
|
+
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
348
|
+
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
349
|
+
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
350
|
+
if (lastRefreshAt > 0 && Date.now() - lastRefreshAt < minimumAgeMs) return
|
|
351
|
+
refreshPullRequestsRef.current()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
const fetchedAt = pullRequestLoad?.fetchedAt?.getTime()
|
|
356
|
+
if (fetchedAt !== undefined) {
|
|
357
|
+
lastPullRequestRefreshAtRef.current = fetchedAt
|
|
358
|
+
}
|
|
359
|
+
}, [pullRequestLoad?.fetchedAt])
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
const handleFocus = () => {
|
|
363
|
+
terminalFocusedRef.current = true
|
|
364
|
+
setTerminalFocused(true)
|
|
365
|
+
if (terminalWasBlurredRef.current) {
|
|
366
|
+
maybeRefreshPullRequestsRef.current(FOCUS_RETURN_REFRESH_MIN_MS)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const handleBlur = () => {
|
|
370
|
+
terminalWasBlurredRef.current = true
|
|
371
|
+
terminalFocusedRef.current = false
|
|
372
|
+
setTerminalFocused(false)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
renderer.on("focus", handleFocus)
|
|
376
|
+
renderer.on("blur", handleBlur)
|
|
377
|
+
return () => {
|
|
378
|
+
renderer.off("focus", handleFocus)
|
|
379
|
+
renderer.off("blur", handleBlur)
|
|
380
|
+
}
|
|
381
|
+
}, [renderer])
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (!terminalFocused) return
|
|
385
|
+
const lastRefreshAt = lastPullRequestRefreshAtRef.current || Date.now()
|
|
386
|
+
const ageMs = Date.now() - lastRefreshAt
|
|
387
|
+
const delayMs = Math.max(0, FOCUSED_IDLE_REFRESH_MS - ageMs) + Math.floor(Math.random() * AUTO_REFRESH_JITTER_MS)
|
|
388
|
+
const timeout = globalThis.setTimeout(() => {
|
|
389
|
+
maybeRefreshPullRequestsRef.current(FOCUSED_IDLE_REFRESH_MS)
|
|
390
|
+
}, delayMs)
|
|
391
|
+
return () => globalThis.clearTimeout(timeout)
|
|
392
|
+
}, [terminalFocused, pullRequestLoad?.fetchedAt])
|
|
335
393
|
|
|
336
394
|
useEffect(() => {
|
|
337
395
|
setSelectedIndex((current) => {
|
|
@@ -1013,6 +1071,22 @@ export const App = () => {
|
|
|
1013
1071
|
|
|
1014
1072
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
1015
1073
|
const fullscreenBodyLines = Math.max(8, (height ?? 24) - 8)
|
|
1074
|
+
const wideFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
1075
|
+
pullRequest: selectedPullRequest,
|
|
1076
|
+
contentWidth: fullscreenContentWidth,
|
|
1077
|
+
bodyLines: fullscreenBodyLines,
|
|
1078
|
+
paneWidth: contentWidth,
|
|
1079
|
+
showChecks: true,
|
|
1080
|
+
}) > wideBodyHeight
|
|
1081
|
+
const narrowFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
1082
|
+
pullRequest: selectedPullRequest,
|
|
1083
|
+
contentWidth: fullscreenContentWidth,
|
|
1084
|
+
bodyLines: fullscreenBodyLines,
|
|
1085
|
+
paneWidth: contentWidth,
|
|
1086
|
+
}) > wideBodyHeight
|
|
1087
|
+
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
1088
|
+
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
1089
|
+
const wideDetailBodyScrollable = getDetailBodyHeight(selectedPullRequest, rightContentWidth, wideDetailLines) > wideDetailBodyViewportHeight
|
|
1016
1090
|
|
|
1017
1091
|
const prListProps = {
|
|
1018
1092
|
groups: visibleGroups,
|
|
@@ -1061,7 +1135,7 @@ export const App = () => {
|
|
|
1061
1135
|
/>
|
|
1062
1136
|
) : isWideLayout && detailFullView ? (
|
|
1063
1137
|
<box flexGrow={1} flexDirection="column">
|
|
1064
|
-
<scrollbox ref={detailScrollRef} focused flexGrow={1}>
|
|
1138
|
+
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: wideFullscreenDetailScrollable }}>
|
|
1065
1139
|
<DetailsPane
|
|
1066
1140
|
pullRequest={selectedPullRequest}
|
|
1067
1141
|
contentWidth={fullscreenContentWidth}
|
|
@@ -1085,7 +1159,7 @@ export const App = () => {
|
|
|
1085
1159
|
{selectedPullRequest ? (
|
|
1086
1160
|
<>
|
|
1087
1161
|
<DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
1088
|
-
<scrollbox flexGrow={1}>
|
|
1162
|
+
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
1089
1163
|
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} />
|
|
1090
1164
|
</scrollbox>
|
|
1091
1165
|
</>
|
|
@@ -1096,7 +1170,7 @@ export const App = () => {
|
|
|
1096
1170
|
</box>
|
|
1097
1171
|
) : detailFullView ? (
|
|
1098
1172
|
<box flexGrow={1} flexDirection="column">
|
|
1099
|
-
<scrollbox ref={detailScrollRef} focused flexGrow={1}>
|
|
1173
|
+
<scrollbox ref={detailScrollRef} focused flexGrow={1} verticalScrollbarOptions={{ visible: narrowFullscreenDetailScrollable }}>
|
|
1100
1174
|
<DetailsPane
|
|
1101
1175
|
pullRequest={selectedPullRequest}
|
|
1102
1176
|
contentWidth={fullscreenContentWidth}
|
package/src/index.tsx
CHANGED
|
@@ -7,13 +7,21 @@ import { App } from "./App.js"
|
|
|
7
7
|
|
|
8
8
|
process.env.OTUI_USE_ALTERNATE_SCREEN = "true"
|
|
9
9
|
|
|
10
|
+
const FOCUS_REPORTING_ENABLE = "\x1b[?1004h"
|
|
11
|
+
const FOCUS_REPORTING_DISABLE = "\x1b[?1004l"
|
|
12
|
+
|
|
10
13
|
const renderer = await createCliRenderer({
|
|
11
14
|
exitOnCtrlC: false,
|
|
12
15
|
screenMode: "alternate-screen",
|
|
13
16
|
externalOutputMode: "passthrough",
|
|
14
|
-
onDestroy: () =>
|
|
17
|
+
onDestroy: () => {
|
|
18
|
+
process.stdout.write(FOCUS_REPORTING_DISABLE)
|
|
19
|
+
process.exit(0)
|
|
20
|
+
},
|
|
15
21
|
})
|
|
16
22
|
|
|
23
|
+
process.stdout.write(FOCUS_REPORTING_ENABLE)
|
|
24
|
+
|
|
17
25
|
createRoot(renderer).render(
|
|
18
26
|
<RegistryProvider>
|
|
19
27
|
<App />
|
package/src/ui/DetailsPane.tsx
CHANGED
|
@@ -266,6 +266,36 @@ export const getDetailJunctionRows = (pullRequest: PullRequestItem | null, paneW
|
|
|
266
266
|
return showChecks && checks.length > 0 ? [detailDividerRow, checksDividerRow] : [detailDividerRow]
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
export const getDetailHeaderHeight = (pullRequest: PullRequestItem | null, paneWidth: number, showChecks = false) => {
|
|
270
|
+
if (!pullRequest) return DETAIL_PLACEHOLDER_ROWS + 1
|
|
271
|
+
const titleLines = wrapText(pullRequest.title, Math.max(1, paneWidth - 2)).length
|
|
272
|
+
const checks = deduplicateChecks(pullRequest.checks)
|
|
273
|
+
const checksHeight = showChecks && checks.length > 0 ? checksRowCount(checks) + 2 : 0
|
|
274
|
+
return titleLines + 3 + checksHeight
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const getDetailBodyHeight = (pullRequest: PullRequestItem | null, contentWidth: number, bodyLines = DETAIL_BODY_LINES) => {
|
|
278
|
+
if (!pullRequest) return bodyLines
|
|
279
|
+
if (!pullRequest.detailLoaded) return bodyLines
|
|
280
|
+
return bodyPreview(pullRequest.body, contentWidth, bodyLines).length
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const getDetailsPaneHeight = ({
|
|
284
|
+
pullRequest,
|
|
285
|
+
contentWidth,
|
|
286
|
+
bodyLines = DETAIL_BODY_LINES,
|
|
287
|
+
paneWidth = contentWidth + 2,
|
|
288
|
+
showChecks = false,
|
|
289
|
+
}: {
|
|
290
|
+
pullRequest: PullRequestItem | null
|
|
291
|
+
contentWidth: number
|
|
292
|
+
bodyLines?: number
|
|
293
|
+
paneWidth?: number
|
|
294
|
+
showChecks?: boolean
|
|
295
|
+
}) => pullRequest
|
|
296
|
+
? getDetailHeaderHeight(pullRequest, paneWidth, showChecks) + getDetailBodyHeight(pullRequest, contentWidth, bodyLines)
|
|
297
|
+
: bodyLines + DETAIL_PLACEHOLDER_ROWS + 1
|
|
298
|
+
|
|
269
299
|
export const DetailHeader = ({
|
|
270
300
|
pullRequest,
|
|
271
301
|
contentWidth,
|
|
@@ -465,16 +495,7 @@ export const DetailsPane = ({
|
|
|
465
495
|
placeholderContent: DetailPlaceholderContent
|
|
466
496
|
loadingIndicator: string
|
|
467
497
|
}) => {
|
|
468
|
-
const
|
|
469
|
-
const uniqueChecks = pullRequest ? deduplicateChecks(pullRequest.checks) : []
|
|
470
|
-
const checkRows = checksRowCount(uniqueChecks)
|
|
471
|
-
const checksHeight = showChecks && uniqueChecks.length > 0 ? 1 + checkRows + 1 : 0
|
|
472
|
-
const previewLines = useMemo(
|
|
473
|
-
() => (pullRequest ? bodyPreview(pullRequest.body, contentWidth, bodyLines) : []),
|
|
474
|
-
[pullRequest?.body, contentWidth, bodyLines],
|
|
475
|
-
)
|
|
476
|
-
const bodyHeight = pullRequest && !pullRequest.detailLoaded ? bodyLines : previewLines.length
|
|
477
|
-
const contentHeight = pullRequest ? titleLines + 2 + 1 + checksHeight + bodyHeight : bodyLines + DETAIL_PLACEHOLDER_ROWS + 1
|
|
498
|
+
const contentHeight = getDetailsPaneHeight({ pullRequest, contentWidth, bodyLines, paneWidth, showChecks })
|
|
478
499
|
|
|
479
500
|
return (
|
|
480
501
|
<box flexDirection="column" height={contentHeight}>
|