@kitlangton/ghui 0.1.16 → 0.1.18
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 +29 -27
- package/package.json +2 -1
- package/src/App.tsx +1006 -132
- package/src/domain.ts +47 -1
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +286 -184
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +60 -0
- package/src/ui/PullRequestDiffPane.tsx +105 -33
- package/src/ui/PullRequestList.tsx +38 -13
- package/src/ui/colors.ts +41 -0
- package/src/ui/commentEditor.ts +126 -0
- package/src/ui/diff.ts +204 -7
- package/src/ui/modals.tsx +322 -8
- package/src/ui/pullRequests.ts +2 -0
package/src/ui/DetailsPane.tsx
CHANGED
|
@@ -298,11 +298,13 @@ export const getDetailsPaneHeight = ({
|
|
|
298
298
|
|
|
299
299
|
export const DetailHeader = ({
|
|
300
300
|
pullRequest,
|
|
301
|
+
viewerUsername,
|
|
301
302
|
contentWidth,
|
|
302
303
|
paneWidth,
|
|
303
304
|
showChecks = false,
|
|
304
305
|
}: {
|
|
305
306
|
pullRequest: PullRequestItem
|
|
307
|
+
viewerUsername: string | null
|
|
306
308
|
contentWidth: number
|
|
307
309
|
paneWidth: number
|
|
308
310
|
showChecks?: boolean
|
|
@@ -319,34 +321,31 @@ export const DetailHeader = ({
|
|
|
319
321
|
: "no labels".length
|
|
320
322
|
const showStats = contentWidth - labelsWidth - statsText.length >= 2
|
|
321
323
|
const statsGap = Math.max(2, contentWidth - labelsWidth - statsText.length)
|
|
324
|
+
const opened = formatRelativeDate(pullRequest.createdAt)
|
|
325
|
+
const repo = shortRepoName(pullRequest.repository)
|
|
326
|
+
const author = viewerUsername && pullRequest.author !== viewerUsername ? ` by ${pullRequest.author}` : ""
|
|
327
|
+
const number = String(pullRequest.number)
|
|
328
|
+
const review = reviewLabel(pullRequest)
|
|
329
|
+
const checks = pullRequest.checkSummary?.replace(/^checks\s+/, "")
|
|
330
|
+
const statusParts = [review, checks].filter((part): part is string => Boolean(part))
|
|
331
|
+
const rightSide = statusParts.length > 0 ? `${statusParts.join(" ")} ${opened}` : opened
|
|
332
|
+
const leftWidth = 1 + number.length + 1 + repo.length + author.length
|
|
333
|
+
const gap = Math.max(2, contentWidth - leftWidth - rightSide.length)
|
|
322
334
|
|
|
323
335
|
return (
|
|
324
336
|
<>
|
|
325
337
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<TextLine>
|
|
339
|
-
<span fg={colors.count}>#{number}</span>
|
|
340
|
-
<span fg={colors.muted}> {repo}</span>
|
|
341
|
-
<span fg={colors.muted}>{" ".repeat(gap)}</span>
|
|
342
|
-
{review ? <span fg={statusColor(pullRequest.reviewStatus)}>{review}</span> : null}
|
|
343
|
-
{review && checks ? <span fg={colors.muted}> </span> : null}
|
|
344
|
-
{checks ? <span fg={statusColor(pullRequest.checkStatus)}>{checks}</span> : null}
|
|
345
|
-
{statusParts.length > 0 ? <span fg={colors.muted}> </span> : null}
|
|
346
|
-
<span fg={colors.muted}>{opened}</span>
|
|
347
|
-
</TextLine>
|
|
348
|
-
)
|
|
349
|
-
})()}
|
|
338
|
+
<TextLine>
|
|
339
|
+
<span fg={colors.count}>#{number}</span>
|
|
340
|
+
<span fg={colors.muted}> {repo}</span>
|
|
341
|
+
{author ? <span fg={colors.muted}>{author}</span> : null}
|
|
342
|
+
<span fg={colors.muted}>{" ".repeat(gap)}</span>
|
|
343
|
+
{review ? <span fg={statusColor(pullRequest.reviewStatus)}>{review}</span> : null}
|
|
344
|
+
{review && checks ? <span fg={colors.muted}> </span> : null}
|
|
345
|
+
{checks ? <span fg={statusColor(pullRequest.checkStatus)}>{checks}</span> : null}
|
|
346
|
+
{statusParts.length > 0 ? <span fg={colors.muted}> </span> : null}
|
|
347
|
+
<span fg={colors.muted}>{opened}</span>
|
|
348
|
+
</TextLine>
|
|
350
349
|
</box>
|
|
351
350
|
<box height={wrappedTitle.length} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
352
351
|
{wrappedTitle.map((line, index) => (
|
|
@@ -482,6 +481,7 @@ export const LoadingPane = ({ content, width, height }: { content: DetailPlaceho
|
|
|
482
481
|
|
|
483
482
|
export const DetailsPane = ({
|
|
484
483
|
pullRequest,
|
|
484
|
+
viewerUsername,
|
|
485
485
|
contentWidth,
|
|
486
486
|
bodyLines = DETAIL_BODY_LINES,
|
|
487
487
|
paneWidth = contentWidth + 2,
|
|
@@ -491,6 +491,7 @@ export const DetailsPane = ({
|
|
|
491
491
|
themeId,
|
|
492
492
|
}: {
|
|
493
493
|
pullRequest: PullRequestItem | null
|
|
494
|
+
viewerUsername: string | null
|
|
494
495
|
contentWidth: number
|
|
495
496
|
bodyLines?: number
|
|
496
497
|
paneWidth?: number
|
|
@@ -505,7 +506,7 @@ export const DetailsPane = ({
|
|
|
505
506
|
<box flexDirection="column" height={contentHeight}>
|
|
506
507
|
{pullRequest ? (
|
|
507
508
|
<>
|
|
508
|
-
<DetailHeader pullRequest={pullRequest} contentWidth={contentWidth} paneWidth={paneWidth} showChecks={showChecks} />
|
|
509
|
+
<DetailHeader pullRequest={pullRequest} viewerUsername={viewerUsername} contentWidth={contentWidth} paneWidth={paneWidth} showChecks={showChecks} />
|
|
509
510
|
<DetailBody pullRequest={pullRequest} contentWidth={contentWidth} bodyLines={bodyLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
510
511
|
</>
|
|
511
512
|
) : (
|
package/src/ui/FooterHints.tsx
CHANGED
|
@@ -11,7 +11,9 @@ export const FooterHints = ({
|
|
|
11
11
|
showFilterClear,
|
|
12
12
|
detailFullView,
|
|
13
13
|
diffFullView,
|
|
14
|
+
diffCommentMode,
|
|
14
15
|
hasSelection,
|
|
16
|
+
canCloseSelection,
|
|
15
17
|
hasError,
|
|
16
18
|
isLoading,
|
|
17
19
|
loadingIndicator,
|
|
@@ -21,7 +23,9 @@ export const FooterHints = ({
|
|
|
21
23
|
showFilterClear: boolean
|
|
22
24
|
detailFullView: boolean
|
|
23
25
|
diffFullView: boolean
|
|
26
|
+
diffCommentMode: boolean
|
|
24
27
|
hasSelection: boolean
|
|
28
|
+
canCloseSelection: boolean
|
|
25
29
|
hasError: boolean
|
|
26
30
|
isLoading: boolean
|
|
27
31
|
loadingIndicator: string
|
|
@@ -47,6 +51,28 @@ export const FooterHints = ({
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
if (diffFullView) {
|
|
54
|
+
if (diffCommentMode) {
|
|
55
|
+
return (
|
|
56
|
+
<TextLine>
|
|
57
|
+
<span fg={colors.count}>↑↓</span>
|
|
58
|
+
<span fg={colors.muted}> line </span>
|
|
59
|
+
<span fg={colors.count}>pgup/pgdn</span>
|
|
60
|
+
<span fg={colors.muted}> jump </span>
|
|
61
|
+
<span fg={colors.count}>←→</span>
|
|
62
|
+
<span fg={colors.muted}> side </span>
|
|
63
|
+
<span fg={colors.count}>enter</span>
|
|
64
|
+
<span fg={colors.muted}> open </span>
|
|
65
|
+
<span fg={colors.count}>a</span>
|
|
66
|
+
<span fg={colors.muted}> comment </span>
|
|
67
|
+
<span fg={colors.count}>c</span>
|
|
68
|
+
<span fg={colors.muted}> done </span>
|
|
69
|
+
<span fg={colors.count}>[]</span>
|
|
70
|
+
<span fg={colors.muted}> files </span>
|
|
71
|
+
<span fg={colors.count}>esc</span>
|
|
72
|
+
<span fg={colors.muted}> back</span>
|
|
73
|
+
</TextLine>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
50
76
|
return (
|
|
51
77
|
<TextLine>
|
|
52
78
|
<span fg={colors.count}>esc</span>
|
|
@@ -55,6 +81,8 @@ export const FooterHints = ({
|
|
|
55
81
|
<span fg={colors.muted}> view </span>
|
|
56
82
|
<span fg={colors.count}>w</span>
|
|
57
83
|
<span fg={colors.muted}> wrap </span>
|
|
84
|
+
<span fg={colors.count}>c</span>
|
|
85
|
+
<span fg={colors.muted}> comment </span>
|
|
58
86
|
<span fg={colors.count}>[]</span>
|
|
59
87
|
<span fg={colors.muted}> files </span>
|
|
60
88
|
<span fg={colors.count}>r</span>
|
|
@@ -72,6 +100,30 @@ export const FooterHints = ({
|
|
|
72
100
|
<TextLine>
|
|
73
101
|
<span fg={colors.count}>esc</span>
|
|
74
102
|
<span fg={colors.muted}> back </span>
|
|
103
|
+
<span fg={colors.count}>↑↓</span>
|
|
104
|
+
<span fg={colors.muted}> scroll </span>
|
|
105
|
+
<span fg={colors.count}>r</span>
|
|
106
|
+
<span fg={colors.muted}>{hasError ? " retry " : " refresh "}</span>
|
|
107
|
+
<span fg={colors.count}>t</span>
|
|
108
|
+
<span fg={colors.muted}> theme </span>
|
|
109
|
+
{hasSelection ? (
|
|
110
|
+
<>
|
|
111
|
+
<span fg={colors.count}>s</span>
|
|
112
|
+
<span fg={colors.muted}> state </span>
|
|
113
|
+
<span fg={colors.count}>d</span>
|
|
114
|
+
<span fg={colors.muted}> diff </span>
|
|
115
|
+
<span fg={colors.count}>l</span>
|
|
116
|
+
<span fg={colors.muted}> labels </span>
|
|
117
|
+
<span fg={colors.count}>m</span>
|
|
118
|
+
<span fg={colors.muted}> merge </span>
|
|
119
|
+
{canCloseSelection ? (
|
|
120
|
+
<>
|
|
121
|
+
<span fg={colors.count}>x</span>
|
|
122
|
+
<span fg={colors.muted}> close </span>
|
|
123
|
+
</>
|
|
124
|
+
) : null}
|
|
125
|
+
</>
|
|
126
|
+
) : null}
|
|
75
127
|
<span fg={colors.count}>o</span>
|
|
76
128
|
<span fg={colors.muted}> open </span>
|
|
77
129
|
<span fg={colors.count}>y</span>
|
|
@@ -84,6 +136,8 @@ export const FooterHints = ({
|
|
|
84
136
|
|
|
85
137
|
return (
|
|
86
138
|
<TextLine>
|
|
139
|
+
<span fg={colors.count}>tab</span>
|
|
140
|
+
<span fg={colors.muted}> queue </span>
|
|
87
141
|
<span fg={colors.count}>/</span>
|
|
88
142
|
<span fg={colors.muted}> filter </span>
|
|
89
143
|
<span fg={colors.count}>t</span>
|
|
@@ -117,6 +171,12 @@ export const FooterHints = ({
|
|
|
117
171
|
<span fg={colors.muted}> labels </span>
|
|
118
172
|
<span fg={colors.count}>m</span>
|
|
119
173
|
<span fg={colors.muted}> merge </span>
|
|
174
|
+
{canCloseSelection ? (
|
|
175
|
+
<>
|
|
176
|
+
<span fg={colors.count}>x</span>
|
|
177
|
+
<span fg={colors.muted}> close </span>
|
|
178
|
+
</>
|
|
179
|
+
) : null}
|
|
120
180
|
<span fg={colors.count}>o</span>
|
|
121
181
|
<span fg={colors.muted}> open </span>
|
|
122
182
|
<span fg={colors.count}>y</span>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
1
|
+
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useMemo, type Ref } from "react"
|
|
3
|
-
import type { PullRequestItem } from "../domain.js"
|
|
3
|
+
import type { PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
4
4
|
import { colors, type ThemeId } from "./colors.js"
|
|
5
|
-
import { createDiffSyntaxStyle, diffStatText,
|
|
5
|
+
import { createDiffSyntaxStyle, diffFileStats, diffFileStatText, diffStatText, safeDiffFileIndex, type DiffCommentAnchor, type PullRequestDiffState, type StackedDiffFilePatch } from "./diff.js"
|
|
6
6
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
7
7
|
import { Divider, fitCell, PlainLine, TextLine } from "./primitives.js"
|
|
8
8
|
import { shortRepoName } from "./pullRequests.js"
|
|
@@ -27,9 +27,35 @@ const DiffStats = ({ pullRequest }: { pullRequest: PullRequestItem }) => {
|
|
|
27
27
|
)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const FileStats = ({ file }: { file: StackedDiffFilePatch["file"] }) => {
|
|
31
|
+
const stats = diffFileStats(file)
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{stats.additions > 0 ? <span fg={colors.status.passing}>{`+${stats.additions}`}</span> : null}
|
|
35
|
+
{stats.additions > 0 && stats.deletions > 0 ? <span fg={colors.muted}> </span> : null}
|
|
36
|
+
{stats.deletions > 0 ? <span fg={colors.status.failing}>{`-${stats.deletions}`}</span> : null}
|
|
37
|
+
</>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FileHeader = ({ file, index, count, width }: { file: StackedDiffFilePatch["file"]; index: number; count: number; width: number }) => {
|
|
42
|
+
const counter = `${index + 1}/${count}`
|
|
43
|
+
const statsText = diffFileStatText(file)
|
|
44
|
+
const nameWidth = Math.max(1, width - counter.length - statsText.length - 5)
|
|
45
|
+
return (
|
|
46
|
+
<TextLine>
|
|
47
|
+
<span fg={colors.muted}>{counter} </span>
|
|
48
|
+
<span fg={colors.text}>{fitCell(file.name, nameWidth)}</span>
|
|
49
|
+
{statsText ? <span fg={colors.muted}> </span> : null}
|
|
50
|
+
<FileStats file={file} />
|
|
51
|
+
</TextLine>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
export const PullRequestDiffPane = ({
|
|
31
56
|
pullRequest,
|
|
32
57
|
diffState,
|
|
58
|
+
stackedFiles,
|
|
33
59
|
fileIndex,
|
|
34
60
|
view,
|
|
35
61
|
wrapMode,
|
|
@@ -37,10 +63,16 @@ export const PullRequestDiffPane = ({
|
|
|
37
63
|
height,
|
|
38
64
|
loadingIndicator,
|
|
39
65
|
scrollRef,
|
|
66
|
+
setDiffRef,
|
|
67
|
+
commentMode,
|
|
68
|
+
selectedCommentAnchor,
|
|
69
|
+
selectedCommentThread,
|
|
70
|
+
commentCount,
|
|
40
71
|
themeId,
|
|
41
72
|
}: {
|
|
42
73
|
pullRequest: PullRequestItem | null
|
|
43
74
|
diffState: PullRequestDiffState | undefined
|
|
75
|
+
stackedFiles: readonly StackedDiffFilePatch[]
|
|
44
76
|
fileIndex: number
|
|
45
77
|
view: "unified" | "split"
|
|
46
78
|
wrapMode: "none" | "word"
|
|
@@ -48,15 +80,16 @@ export const PullRequestDiffPane = ({
|
|
|
48
80
|
height: number
|
|
49
81
|
loadingIndicator: string
|
|
50
82
|
scrollRef: Ref<ScrollBoxRenderable>
|
|
83
|
+
setDiffRef: (index: number, diff: DiffRenderable | null) => void
|
|
84
|
+
commentMode: boolean
|
|
85
|
+
selectedCommentAnchor: DiffCommentAnchor | null
|
|
86
|
+
selectedCommentThread: readonly PullRequestReviewComment[]
|
|
87
|
+
commentCount: number
|
|
51
88
|
themeId: ThemeId
|
|
52
89
|
}) => {
|
|
53
90
|
const readyFiles = diffState?.status === "ready" ? diffState.files : []
|
|
54
|
-
const safeIndex = readyFiles
|
|
91
|
+
const safeIndex = safeDiffFileIndex(readyFiles, fileIndex)
|
|
55
92
|
const file = readyFiles[safeIndex] ?? null
|
|
56
|
-
const diffHeight = useMemo(
|
|
57
|
-
() => file ? patchRenderableLineCount(file.patch, view, wrapMode, paneWidth) : 1,
|
|
58
|
-
[file?.patch, view, wrapMode, paneWidth],
|
|
59
|
-
)
|
|
60
93
|
const syntaxStyle = useMemo(() => createDiffSyntaxStyle(), [themeId])
|
|
61
94
|
|
|
62
95
|
if (!pullRequest) {
|
|
@@ -102,7 +135,23 @@ export const PullRequestDiffPane = ({
|
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
const fileCounter = `${safeIndex + 1}/${readyFiles.length}`
|
|
105
|
-
const
|
|
138
|
+
const currentFileStatsText = file ? diffFileStatText(file) : ""
|
|
139
|
+
const selectedSideLabel = selectedCommentAnchor?.side === "RIGHT" ? "right" : selectedCommentAnchor?.side === "LEFT" ? "left" : null
|
|
140
|
+
const commentLabel = commentMode && selectedCommentAnchor
|
|
141
|
+
? ` ${selectedSideLabel} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line}`
|
|
142
|
+
: commentMode ? " c no lines" : commentCount > 0 ? ` ● ${commentCount}` : ""
|
|
143
|
+
const commentLabelColor = commentMode && selectedCommentAnchor?.side === "LEFT"
|
|
144
|
+
? colors.status.failing
|
|
145
|
+
: commentMode ? colors.status.passing : colors.status.passing
|
|
146
|
+
const fileNameWidth = Math.max(8, headerWidth - fileCounter.length - currentFileStatsText.length - commentLabel.length - 4)
|
|
147
|
+
const commentPeek = commentMode && selectedCommentAnchor && selectedCommentThread.length > 0
|
|
148
|
+
? selectedCommentThread[selectedCommentThread.length - 1]!
|
|
149
|
+
: null
|
|
150
|
+
const commentPeekCount = selectedCommentThread.length === 1 ? "1 comment" : `${selectedCommentThread.length} comments`
|
|
151
|
+
const commentPeekBody = commentPeek?.body.split("\n")[0]?.trim() || "(empty comment)"
|
|
152
|
+
const commentPeekMeta = commentPeek && selectedCommentAnchor
|
|
153
|
+
? `${selectedSideLabel ?? "line"} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line} ${commentPeek.author} ${commentPeekCount} enter thread a comment`
|
|
154
|
+
: ""
|
|
106
155
|
|
|
107
156
|
return (
|
|
108
157
|
<box height={height} flexDirection="column">
|
|
@@ -118,34 +167,57 @@ export const PullRequestDiffPane = ({
|
|
|
118
167
|
<TextLine>
|
|
119
168
|
<span fg={colors.text}>{fitCell(file.name, fileNameWidth)}</span>
|
|
120
169
|
<span fg={colors.muted}> {fileCounter}</span>
|
|
170
|
+
{currentFileStatsText ? <span fg={colors.muted}> </span> : null}
|
|
171
|
+
<FileStats file={file} />
|
|
172
|
+
{commentLabel ? <span fg={commentLabelColor}>{commentLabel}</span> : null}
|
|
121
173
|
</TextLine>
|
|
122
174
|
</box>
|
|
123
175
|
<Divider width={paneWidth} />
|
|
124
|
-
<scrollbox ref={scrollRef} focused flexGrow={1} scrollY scrollX={false}>
|
|
125
|
-
|
|
126
|
-
key={`${pullRequest.url}-${
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false}>
|
|
177
|
+
{stackedFiles.map((stackedFile) => (
|
|
178
|
+
<box key={`${pullRequest.url}-${stackedFile.index}-${view}-${wrapMode}`} flexDirection="column" flexShrink={0}>
|
|
179
|
+
{stackedFile.index > 0 ? <Divider width={paneWidth} /> : null}
|
|
180
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
181
|
+
<FileHeader file={stackedFile.file} index={stackedFile.index} count={readyFiles.length} width={paneWidth} />
|
|
182
|
+
</box>
|
|
183
|
+
<Divider width={paneWidth} />
|
|
184
|
+
<diff
|
|
185
|
+
ref={(diff: DiffRenderable | null) => setDiffRef(stackedFile.index, diff)}
|
|
186
|
+
diff={stackedFile.file.patch}
|
|
187
|
+
view={view}
|
|
188
|
+
syncScroll
|
|
189
|
+
filetype={stackedFile.file.filetype ?? "text"}
|
|
190
|
+
syntaxStyle={syntaxStyle}
|
|
191
|
+
showLineNumbers
|
|
192
|
+
wrapMode={wrapMode}
|
|
193
|
+
addedBg={colors.diff.addedBg}
|
|
194
|
+
removedBg={colors.diff.removedBg}
|
|
195
|
+
contextBg={colors.diff.contextBg}
|
|
196
|
+
addedSignColor={colors.status.passing}
|
|
197
|
+
removedSignColor={colors.status.failing}
|
|
198
|
+
lineNumberFg={colors.muted}
|
|
199
|
+
lineNumberBg={colors.diff.lineNumberBg}
|
|
200
|
+
addedLineNumberBg={colors.diff.addedLineNumberBg}
|
|
201
|
+
removedLineNumberBg={colors.diff.removedLineNumberBg}
|
|
202
|
+
selectionBg={colors.selectedBg}
|
|
203
|
+
selectionFg={colors.selectedText}
|
|
204
|
+
height={stackedFile.diffHeight}
|
|
205
|
+
style={{ flexShrink: 0 }}
|
|
206
|
+
/>
|
|
207
|
+
</box>
|
|
208
|
+
))}
|
|
148
209
|
</scrollbox>
|
|
210
|
+
{commentPeek ? (
|
|
211
|
+
<>
|
|
212
|
+
<Divider width={paneWidth} />
|
|
213
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
214
|
+
<PlainLine text={fitCell(commentPeekMeta, Math.max(1, paneWidth - 2))} fg={colors.count} />
|
|
215
|
+
</box>
|
|
216
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
217
|
+
<PlainLine text={fitCell(commentPeekBody, Math.max(1, paneWidth - 2))} fg={colors.text} />
|
|
218
|
+
</box>
|
|
219
|
+
</>
|
|
220
|
+
) : null}
|
|
149
221
|
</box>
|
|
150
222
|
)
|
|
151
223
|
}
|
|
@@ -25,10 +25,26 @@ const groupNumberWidth = (pullRequests: readonly PullRequestItem[]) => {
|
|
|
25
25
|
return maxLen + 1
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const MatchedCell = ({ text, width, query, align = "left" }: { text: string; width: number; query: string; align?: "left" | "right" }) => {
|
|
29
|
+
const fitted = fitCell(text, width, align)
|
|
30
|
+
const needle = query.trim().toLowerCase()
|
|
31
|
+
const index = needle.length > 0 ? fitted.toLowerCase().indexOf(needle) : -1
|
|
32
|
+
if (index < 0) return <span>{fitted}</span>
|
|
33
|
+
|
|
34
|
+
const end = Math.min(fitted.length, index + needle.length)
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
{index > 0 ? <span>{fitted.slice(0, index)}</span> : null}
|
|
38
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{fitted.slice(index, end)}</span>
|
|
39
|
+
{end < fitted.length ? <span>{fitted.slice(end)}</span> : null}
|
|
40
|
+
</>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const GroupTitle = ({ label, color, filterText }: { label: string; color: string; filterText: string }) => (
|
|
29
45
|
<TextLine>
|
|
30
46
|
<span fg={color}>{GROUP_ICON} </span>
|
|
31
|
-
<span fg={color} attributes={TextAttributes.BOLD}
|
|
47
|
+
<span fg={color} attributes={TextAttributes.BOLD}><MatchedCell text={label} width={label.length} query={filterText} /></span>
|
|
32
48
|
</TextLine>
|
|
33
49
|
)
|
|
34
50
|
|
|
@@ -37,30 +53,38 @@ const PullRequestRow = ({
|
|
|
37
53
|
selected,
|
|
38
54
|
contentWidth,
|
|
39
55
|
numWidth,
|
|
56
|
+
filterText,
|
|
40
57
|
onSelect,
|
|
41
58
|
}: {
|
|
42
59
|
pullRequest: PullRequestItem
|
|
43
60
|
selected: boolean
|
|
44
61
|
contentWidth: number
|
|
45
62
|
numWidth: number
|
|
63
|
+
filterText: string
|
|
46
64
|
onSelect: () => void
|
|
47
65
|
}) => {
|
|
48
|
-
const
|
|
66
|
+
const isClosed = pullRequest.state === "closed"
|
|
67
|
+
const isMerged = pullRequest.state === "merged"
|
|
68
|
+
const isFinal = isClosed || isMerged
|
|
69
|
+
const checkText = isMerged ? "merged" : isClosed ? "closed" : checkLabel(pullRequest)?.replace(/^checks\s+/, "") ?? ""
|
|
49
70
|
const ageText = `${daysOpen(pullRequest.createdAt)}d`
|
|
50
71
|
const { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth } = getRowLayout(contentWidth, numWidth)
|
|
51
72
|
const rowWidth = reviewWidth + 1 + numberWidth + 1 + titleWidth + checkWidth + ageWidth
|
|
52
73
|
const fillerWidth = Math.max(0, contentWidth - rowWidth)
|
|
53
|
-
const indicatorColor = pullRequest.autoMergeEnabled ? colors.accent : statusColor(pullRequest.reviewStatus)
|
|
74
|
+
const indicatorColor = isMerged ? colors.status.passing : isClosed ? colors.muted : pullRequest.autoMergeEnabled ? colors.accent : statusColor(pullRequest.reviewStatus)
|
|
75
|
+
const rowTextColor = selected ? colors.selectedText : isFinal ? colors.muted : colors.text
|
|
76
|
+
const numberColor = selected ? colors.accent : isFinal ? colors.muted : colors.count
|
|
77
|
+
const checkColor = isMerged ? colors.status.passing : isClosed ? colors.muted : statusColor(pullRequest.checkStatus)
|
|
54
78
|
|
|
55
79
|
return (
|
|
56
80
|
<box height={1} onMouseDown={onSelect}>
|
|
57
|
-
<TextLine fg={
|
|
81
|
+
<TextLine fg={rowTextColor} bg={selected ? colors.selectedBg : undefined}>
|
|
58
82
|
<span fg={indicatorColor}>{fitCell(reviewIcon(pullRequest), reviewWidth)}</span>
|
|
59
83
|
<span> </span>
|
|
60
|
-
<span fg={
|
|
84
|
+
<span fg={numberColor}><MatchedCell text={`#${pullRequest.number}`} width={numberWidth} query={filterText} align="right" /></span>
|
|
61
85
|
<span> </span>
|
|
62
|
-
<span
|
|
63
|
-
<span fg={
|
|
86
|
+
<span><MatchedCell text={pullRequest.title} width={titleWidth} query={filterText} /></span>
|
|
87
|
+
<span fg={checkColor}>{fitCell(checkText, checkWidth, "right")}</span>
|
|
64
88
|
<span fg={colors.muted}>{fitCell(ageText, ageWidth, "right")}</span>
|
|
65
89
|
{fillerWidth > 0 ? <span>{" ".repeat(fillerWidth)}</span> : null}
|
|
66
90
|
</TextLine>
|
|
@@ -109,15 +133,16 @@ export const PullRequestList = ({
|
|
|
109
133
|
const numWidth = groupNumberWidth(pullRequests)
|
|
110
134
|
return (
|
|
111
135
|
<box key={repo} flexDirection="column">
|
|
112
|
-
<GroupTitle label={repo} color={repoColor(repo)} />
|
|
136
|
+
<GroupTitle label={repo} color={repoColor(repo)} filterText={filterText} />
|
|
113
137
|
{pullRequests.map((pullRequest) => (
|
|
114
138
|
<PullRequestRow
|
|
115
139
|
key={pullRequest.url}
|
|
116
140
|
pullRequest={pullRequest}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
selected={pullRequest.url === selectedUrl}
|
|
142
|
+
contentWidth={contentWidth}
|
|
143
|
+
numWidth={numWidth}
|
|
144
|
+
filterText={filterText}
|
|
145
|
+
onSelect={() => onSelectPullRequest(pullRequest.url)}
|
|
121
146
|
/>
|
|
122
147
|
))}
|
|
123
148
|
</box>
|
package/src/ui/colors.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type ThemeId =
|
|
|
11
11
|
| "monokai"
|
|
12
12
|
| "solarized-dark"
|
|
13
13
|
| "everforest"
|
|
14
|
+
| "vesper"
|
|
14
15
|
| "opencode"
|
|
15
16
|
|
|
16
17
|
export interface ColorPalette {
|
|
@@ -566,6 +567,45 @@ const everforestColors: ColorPalette = {
|
|
|
566
567
|
},
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
const vesperColors: ColorPalette = {
|
|
571
|
+
background: "#101010",
|
|
572
|
+
modalBackground: "#1A1A1A",
|
|
573
|
+
text: "#FFFFFF",
|
|
574
|
+
muted: "#A0A0A0",
|
|
575
|
+
separator: "#282828",
|
|
576
|
+
accent: "#FFC799",
|
|
577
|
+
inlineCode: "#99FFE4",
|
|
578
|
+
error: "#FF8080",
|
|
579
|
+
selectedBg: "#232323",
|
|
580
|
+
selectedText: "#FFFFFF",
|
|
581
|
+
count: "#FFC799",
|
|
582
|
+
status: {
|
|
583
|
+
draft: "#FFC799",
|
|
584
|
+
approved: "#99FFE4",
|
|
585
|
+
changes: "#FF8080",
|
|
586
|
+
review: "#B0B0B0",
|
|
587
|
+
none: "#7E7E7E",
|
|
588
|
+
passing: "#99FFE4",
|
|
589
|
+
pending: "#FFC799",
|
|
590
|
+
failing: "#FF8080",
|
|
591
|
+
},
|
|
592
|
+
repos: {
|
|
593
|
+
opencode: "#FFC799",
|
|
594
|
+
"effect-smol": "#99FFE4",
|
|
595
|
+
"opencode-console": "#FFD1A8",
|
|
596
|
+
opencontrol: "#FFC799",
|
|
597
|
+
default: "#B0B0B0",
|
|
598
|
+
},
|
|
599
|
+
diff: {
|
|
600
|
+
addedBg: "#17312d",
|
|
601
|
+
removedBg: "#351f1f",
|
|
602
|
+
contextBg: "transparent",
|
|
603
|
+
lineNumberBg: "#101010",
|
|
604
|
+
addedLineNumberBg: "#142b28",
|
|
605
|
+
removedLineNumberBg: "#2f1c1c",
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
|
|
569
609
|
export const themeDefinitions: readonly ThemeDefinition[] = [
|
|
570
610
|
{ id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
|
|
571
611
|
{ id: "tokyo-night", name: "Tokyo Night", description: "Cool indigo surfaces with neon editor accents", colors: tokyoNightColors },
|
|
@@ -579,6 +619,7 @@ export const themeDefinitions: readonly ThemeDefinition[] = [
|
|
|
579
619
|
{ id: "monokai", name: "Monokai", description: "Classic dark olive with electric syntax colors", colors: monokaiColors },
|
|
580
620
|
{ id: "solarized-dark", name: "Solarized Dark", description: "Low-contrast blue-green base with calibrated accents", colors: solarizedDarkColors },
|
|
581
621
|
{ id: "everforest", name: "Everforest", description: "Soft green-gray forest tones with warm highlights", colors: everforestColors },
|
|
622
|
+
{ id: "vesper", name: "Vesper", description: "Minimal black surfaces with peach and aqua accents", colors: vesperColors },
|
|
582
623
|
{ id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
|
|
583
624
|
] as const
|
|
584
625
|
|