mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
feat(renderer): search worktrees by review metadata
This commit is contained in:
@@ -104,6 +104,8 @@ import {
|
||||
getCmdJQuickActions,
|
||||
CREATE_WORKSPACE_QUICK_ACTION_ID
|
||||
} from '@/components/cmd-j/quick-actions'
|
||||
import { buildWorktreeChecksReviewIndex } from '@/components/cmd-j/worktree-checks-review-index'
|
||||
import { selectWorktreePaletteCacheInputs } from '@/components/cmd-j/worktree-palette-cache-inputs'
|
||||
import {
|
||||
getComposerEligibleRepos,
|
||||
resolveComposerGitRepoId
|
||||
@@ -382,8 +384,9 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
const agentStatusEpoch = useAppStore((s) =>
|
||||
visible || statusInputsLingering ? s.agentStatusEpoch : 0
|
||||
)
|
||||
const prCache = useAppStore((s) => s.prCache)
|
||||
const issueCache = useAppStore((s) => s.issueCache)
|
||||
const { prCache, issueCache, hostedReviewCache } = useAppStore(
|
||||
useShallow((s) => selectWorktreePaletteCacheInputs(s, visible || statusInputsLingering))
|
||||
)
|
||||
const migrationUnsupportedByPtyId = useAppStore((s) => s.migrationUnsupportedByPtyId)
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeTabType = useAppStore((s) => s.activeTabType)
|
||||
@@ -622,6 +625,18 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
[browserSortedWorktrees]
|
||||
)
|
||||
|
||||
const checksReviewByWorktreeId = useMemo(
|
||||
() =>
|
||||
buildWorktreeChecksReviewIndex({
|
||||
worktrees: allWorktrees,
|
||||
repoMap,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
settings
|
||||
}),
|
||||
[allWorktrees, hostedReviewCache, prCache, repoMap, settings]
|
||||
)
|
||||
|
||||
const worktreeMatches = useMemo(
|
||||
() =>
|
||||
searchWorktrees(
|
||||
@@ -630,9 +645,18 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
repoMap,
|
||||
prCache,
|
||||
issueCache,
|
||||
getWorkspacePortsByWorktreeId(workspacePortScan)
|
||||
getWorkspacePortsByWorktreeId(workspacePortScan),
|
||||
checksReviewByWorktreeId
|
||||
),
|
||||
[sortedWorktrees, deferredQuery, repoMap, prCache, issueCache, workspacePortScan]
|
||||
[
|
||||
sortedWorktrees,
|
||||
deferredQuery,
|
||||
repoMap,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortScan,
|
||||
checksReviewByWorktreeId
|
||||
]
|
||||
)
|
||||
|
||||
const browserPageEntries = useMemo<SearchableBrowserPage[]>(() => {
|
||||
@@ -2364,5 +2388,7 @@ function getPaletteSupportingTextLabel(
|
||||
return translate('worktreeJumpPalette.matchLabel.port', 'Port')
|
||||
case 'pr':
|
||||
return translate('worktreeJumpPalette.matchLabel.pr', 'PR')
|
||||
case 'mr':
|
||||
return translate('worktreeJumpPalette.matchLabel.mr', 'MR')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
|
||||
import type { PRInfo, Repo, Worktree } from '../../../../shared/types'
|
||||
import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key'
|
||||
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity'
|
||||
import { buildWorktreeChecksReviewIndex } from './worktree-checks-review-index'
|
||||
|
||||
const repo: Repo = {
|
||||
id: 'repo-1',
|
||||
path: '/remote/orca',
|
||||
displayName: 'orca',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 0,
|
||||
executionHostId: 'ssh:staging'
|
||||
}
|
||||
|
||||
const worktree: Worktree = {
|
||||
id: 'worktree-1',
|
||||
repoId: repo.id,
|
||||
path: '/remote/orca-worktrees/search',
|
||||
head: 'abc123',
|
||||
branch: 'refs/heads/feature/search',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
displayName: 'Search reviews',
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: 42,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
}
|
||||
|
||||
function makePR(): PRInfo {
|
||||
return {
|
||||
number: 42,
|
||||
title: 'Search worktrees by their pull requests',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/orca/pull/42',
|
||||
checksStatus: 'success',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
}
|
||||
}
|
||||
|
||||
function makeGitLabReview(): HostedReviewInfo {
|
||||
return {
|
||||
provider: 'gitlab',
|
||||
number: 17,
|
||||
title: 'Search worktrees by merge request',
|
||||
state: 'open',
|
||||
url: 'https://gitlab.com/acme/orca/-/merge_requests/17',
|
||||
status: 'pending',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildWorktreeChecksReviewIndex', () => {
|
||||
it('reads the same host-scoped GitHub PR cache entry as Checks', () => {
|
||||
const key = getGitHubPRCacheKey(
|
||||
repo.path,
|
||||
repo.id,
|
||||
'feature/search',
|
||||
null,
|
||||
repo.connectionId,
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
|
||||
const reviews = buildWorktreeChecksReviewIndex({
|
||||
worktrees: [worktree],
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
prCache: { [key]: { data: makePR(), fetchedAt: 1 } },
|
||||
hostedReviewCache: {},
|
||||
settings: null
|
||||
})
|
||||
|
||||
expect(reviews.get(worktree.id)).toMatchObject({
|
||||
provider: 'github',
|
||||
number: 42,
|
||||
title: 'Search worktrees by their pull requests'
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the GitLab review selected by Checks instead of stale GitHub metadata', () => {
|
||||
const gitLabWorktree = { ...worktree, linkedGitLabMR: 17 }
|
||||
const prKey = getGitHubPRCacheKey(
|
||||
repo.path,
|
||||
repo.id,
|
||||
'feature/search',
|
||||
null,
|
||||
repo.connectionId,
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const reviewKey = getHostedReviewCacheKey(
|
||||
repo.path,
|
||||
'feature/search',
|
||||
null,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const gitLabReview = makeGitLabReview()
|
||||
|
||||
const reviews = buildWorktreeChecksReviewIndex({
|
||||
worktrees: [gitLabWorktree],
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
prCache: { [prKey]: { data: makePR(), fetchedAt: 1 } },
|
||||
hostedReviewCache: { [reviewKey]: { data: gitLabReview, fetchedAt: 1 } },
|
||||
settings: null
|
||||
})
|
||||
|
||||
expect(reviews.get(worktree.id)).toBe(gitLabReview)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { branchName } from '@/lib/git-utils'
|
||||
import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key'
|
||||
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
|
||||
import type { Repo, Worktree } from '../../../../shared/types'
|
||||
import { selectChecksPanelReview } from '../right-sidebar/checks-panel-review'
|
||||
|
||||
type WorktreeChecksReviewIndexArgs = {
|
||||
worktrees: readonly Worktree[]
|
||||
repoMap: ReadonlyMap<string, Repo>
|
||||
prCache: AppState['prCache'] | null
|
||||
hostedReviewCache: AppState['hostedReviewCache'] | null
|
||||
settings: AppState['settings']
|
||||
}
|
||||
|
||||
export function buildWorktreeChecksReviewIndex({
|
||||
worktrees,
|
||||
repoMap,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
settings
|
||||
}: WorktreeChecksReviewIndexArgs): Map<string, HostedReviewInfo> {
|
||||
const reviews = new Map<string, HostedReviewInfo>()
|
||||
if (!prCache || !hostedReviewCache) {
|
||||
return reviews
|
||||
}
|
||||
|
||||
for (const worktree of worktrees) {
|
||||
const repo = repoMap.get(worktree.repoId)
|
||||
if (!repo) {
|
||||
continue
|
||||
}
|
||||
const branch = branchName(worktree.branch)
|
||||
const prKey = getGitHubPRCacheKey(
|
||||
repo.path,
|
||||
repo.id,
|
||||
branch,
|
||||
settings,
|
||||
repo.connectionId,
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
const hostedReviewKey = getHostedReviewCacheKey(
|
||||
repo.path,
|
||||
branch,
|
||||
settings,
|
||||
repo.id,
|
||||
repo.connectionId,
|
||||
repo.executionHostId,
|
||||
true
|
||||
)
|
||||
// Why: Cmd+J should expose exactly the review metadata Checks has already
|
||||
// resolved, without starting another provider lookup from the search path.
|
||||
const review = selectChecksPanelReview({
|
||||
hostedReview: hostedReviewCache[hostedReviewKey]?.data,
|
||||
pr: prCache[prKey]?.data,
|
||||
linkedGitLabMR: worktree.linkedGitLabMR ?? null,
|
||||
linkedBitbucketPR: worktree.linkedBitbucketPR ?? null,
|
||||
linkedAzureDevOpsPR: worktree.linkedAzureDevOpsPR ?? null,
|
||||
linkedGiteaPR: worktree.linkedGiteaPR ?? null
|
||||
})
|
||||
if (review) {
|
||||
reviews.set(worktree.id, review)
|
||||
}
|
||||
}
|
||||
|
||||
return reviews
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { AppState } from '@/store/types'
|
||||
import {
|
||||
EMPTY_WORKTREE_PALETTE_CACHE_INPUTS,
|
||||
selectWorktreePaletteCacheInputs
|
||||
} from './worktree-palette-cache-inputs'
|
||||
|
||||
describe('selectWorktreePaletteCacheInputs', () => {
|
||||
it('stays referentially stable across review cache writes while the palette is closed', () => {
|
||||
const emptyState: Pick<AppState, 'prCache' | 'issueCache' | 'hostedReviewCache'> = {
|
||||
prCache: {},
|
||||
issueCache: {},
|
||||
hostedReviewCache: {}
|
||||
}
|
||||
const filledState: Pick<AppState, 'prCache' | 'issueCache' | 'hostedReviewCache'> = {
|
||||
prCache: { pr: { data: null, fetchedAt: 1 } },
|
||||
issueCache: { issue: { data: null, fetchedAt: 1 } },
|
||||
hostedReviewCache: { review: { data: null, fetchedAt: 1 } }
|
||||
}
|
||||
|
||||
const before = selectWorktreePaletteCacheInputs(emptyState, false)
|
||||
const after = selectWorktreePaletteCacheInputs(filledState, false)
|
||||
|
||||
expect(before).toBe(EMPTY_WORKTREE_PALETTE_CACHE_INPUTS)
|
||||
expect(after).toBe(before)
|
||||
})
|
||||
|
||||
it('returns live cache identities while the palette is visible', () => {
|
||||
const state = {
|
||||
prCache: {},
|
||||
issueCache: {},
|
||||
hostedReviewCache: {}
|
||||
} as Pick<AppState, 'prCache' | 'issueCache' | 'hostedReviewCache'>
|
||||
|
||||
const selected = selectWorktreePaletteCacheInputs(state, true)
|
||||
|
||||
expect(selected).toEqual(state)
|
||||
expect(selected.prCache).toBe(state.prCache)
|
||||
expect(selected.issueCache).toBe(state.issueCache)
|
||||
expect(selected.hostedReviewCache).toBe(state.hostedReviewCache)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
|
||||
export type WorktreePaletteCacheInputs = {
|
||||
prCache: AppState['prCache'] | null
|
||||
issueCache: AppState['issueCache'] | null
|
||||
hostedReviewCache: AppState['hostedReviewCache'] | null
|
||||
}
|
||||
|
||||
export const EMPTY_WORKTREE_PALETTE_CACHE_INPUTS: WorktreePaletteCacheInputs = Object.freeze({
|
||||
prCache: null,
|
||||
issueCache: null,
|
||||
hostedReviewCache: null
|
||||
})
|
||||
|
||||
export function selectWorktreePaletteCacheInputs(
|
||||
state: Pick<AppState, 'prCache' | 'issueCache' | 'hostedReviewCache'>,
|
||||
active: boolean
|
||||
): WorktreePaletteCacheInputs {
|
||||
// Why: the palette stays mounted while closed; cache replacement from Checks
|
||||
// must not rerender it when no search results are visible.
|
||||
if (!active) {
|
||||
return EMPTY_WORKTREE_PALETTE_CACHE_INPUTS
|
||||
}
|
||||
return {
|
||||
prCache: state.prCache,
|
||||
issueCache: state.issueCache,
|
||||
hostedReviewCache: state.hostedReviewCache
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
"matchLabel": {
|
||||
"comment": "Comment",
|
||||
"issue": "Issue",
|
||||
"mr": "MR",
|
||||
"port": "Port",
|
||||
"pr": "PR"
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"matchLabel": {
|
||||
"comment": "Comentario",
|
||||
"issue": "Issue",
|
||||
"mr": "MR",
|
||||
"port": "Puerto",
|
||||
"pr": "PR"
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"matchLabel": {
|
||||
"comment": "コメント",
|
||||
"issue": "Issue",
|
||||
"mr": "MR",
|
||||
"port": "ポート",
|
||||
"pr": "PR"
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"matchLabel": {
|
||||
"comment": "댓글",
|
||||
"issue": "이슈",
|
||||
"mr": "MR",
|
||||
"port": "포트",
|
||||
"pr": "PR"
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"matchLabel": {
|
||||
"comment": "评论",
|
||||
"issue": "议题",
|
||||
"mr": "MR",
|
||||
"port": "端口",
|
||||
"pr": "PR"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { HostedReviewInfo } from '../../../shared/hosted-review'
|
||||
|
||||
type SearchableReview = Pick<HostedReviewInfo, 'number' | 'title' | 'provider'>
|
||||
type WorktreePaletteReviewMatch = {
|
||||
labelKind: 'pr' | 'mr'
|
||||
text: string
|
||||
matchRange: { start: number; end: number }
|
||||
}
|
||||
|
||||
export function matchWorktreePaletteReview(
|
||||
review: SearchableReview,
|
||||
query: string,
|
||||
numericQuery: string
|
||||
): WorktreePaletteReviewMatch | null {
|
||||
const isMergeRequest = review.provider === 'gitlab'
|
||||
const numberPrefix = isMergeRequest ? 'MR !' : 'PR #'
|
||||
const hasPullRequestSigil = query.startsWith('#')
|
||||
const hasMergeRequestSigil = query.startsWith('!')
|
||||
const sigilMatchesProvider =
|
||||
(!hasPullRequestSigil && !hasMergeRequestSigil) ||
|
||||
(hasPullRequestSigil && !isMergeRequest) ||
|
||||
(hasMergeRequestSigil && isMergeRequest)
|
||||
const reviewNumericQuery = hasMergeRequestSigil ? query.slice(1) : numericQuery
|
||||
const reviewNumberIndex = sigilMatchesProvider
|
||||
? String(review.number).indexOf(reviewNumericQuery)
|
||||
: -1
|
||||
if (reviewNumericQuery && reviewNumberIndex !== -1) {
|
||||
return {
|
||||
labelKind: isMergeRequest ? 'mr' : 'pr',
|
||||
text: `${numberPrefix}${review.number}`,
|
||||
matchRange: {
|
||||
start: numberPrefix.length + reviewNumberIndex,
|
||||
end: numberPrefix.length + reviewNumberIndex + reviewNumericQuery.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const titleIndex = review.title.toLowerCase().indexOf(query)
|
||||
if (titleIndex === -1) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
labelKind: isMergeRequest ? 'mr' : 'pr',
|
||||
text: review.title,
|
||||
matchRange: { start: titleIndex, end: titleIndex + query.length }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isWorktreePaletteQueryTooLarge
|
||||
} from './worktree-palette-query-bounds'
|
||||
import type { Repo, Worktree } from '../../../shared/types'
|
||||
import type { HostedReviewInfo } from '../../../shared/hosted-review'
|
||||
|
||||
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
return {
|
||||
@@ -172,6 +173,147 @@ describe('worktree-palette-search', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('matches the GitLab review title and number already selected by Checks', () => {
|
||||
const review: HostedReviewInfo = {
|
||||
provider: 'gitlab',
|
||||
number: 17,
|
||||
title: 'Reuse checks tab review metadata',
|
||||
state: 'open',
|
||||
url: 'https://gitlab.com/acme/orca/-/merge_requests/17',
|
||||
status: 'success',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
}
|
||||
const reviews = new Map([['wt-1', review]])
|
||||
|
||||
const titleResults = searchWorktrees(
|
||||
[makeWorktree()],
|
||||
'checks tab',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
reviews
|
||||
)
|
||||
const numberResults = searchWorktrees(
|
||||
[makeWorktree()],
|
||||
'!17',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
reviews
|
||||
)
|
||||
|
||||
expect(titleResults[0].supportingText).toEqual({
|
||||
labelKind: 'mr',
|
||||
text: review.title,
|
||||
matchRange: { start: 6, end: 16 }
|
||||
})
|
||||
expect(numberResults[0].supportingText).toEqual({
|
||||
labelKind: 'mr',
|
||||
text: 'MR !17',
|
||||
matchRange: { start: 4, end: 6 }
|
||||
})
|
||||
})
|
||||
|
||||
it('does not search stale GitHub cache metadata when Checks selected another review', () => {
|
||||
const review: HostedReviewInfo = {
|
||||
provider: 'gitlab',
|
||||
number: 17,
|
||||
title: 'Current merge request',
|
||||
state: 'open',
|
||||
url: 'https://gitlab.com/acme/orca/-/merge_requests/17',
|
||||
status: 'success',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
}
|
||||
|
||||
expect(
|
||||
searchWorktrees(
|
||||
[makeWorktree({ branch: 'refs/heads/feature/palette-refresh' })],
|
||||
'stale github title',
|
||||
repoMap,
|
||||
{
|
||||
'/repo/orca::feature/palette-refresh': {
|
||||
data: { number: 99, title: 'Stale GitHub title' }
|
||||
}
|
||||
},
|
||||
null,
|
||||
undefined,
|
||||
new Map([['wt-1', review]])
|
||||
)
|
||||
).toEqual([])
|
||||
|
||||
expect(
|
||||
searchWorktrees(
|
||||
[makeWorktree({ linkedPR: 99 })],
|
||||
'#99',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
new Map([['wt-1', review]])
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('scopes PR and MR number sigils to their providers', () => {
|
||||
const gitHubReview: HostedReviewInfo = {
|
||||
provider: 'github',
|
||||
number: 42,
|
||||
title: 'GitHub pull request',
|
||||
state: 'open',
|
||||
url: 'https://github.com/acme/orca/pull/42',
|
||||
status: 'success',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
}
|
||||
const gitLabReview: HostedReviewInfo = {
|
||||
provider: 'gitlab',
|
||||
number: 17,
|
||||
title: 'GitLab merge request',
|
||||
state: 'open',
|
||||
url: 'https://gitlab.com/acme/orca/-/merge_requests/17',
|
||||
status: 'success',
|
||||
updatedAt: '2026-07-12T00:00:00Z',
|
||||
mergeable: 'MERGEABLE'
|
||||
}
|
||||
|
||||
expect(
|
||||
searchWorktrees(
|
||||
[makeWorktree()],
|
||||
'!42',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
new Map([['wt-1', gitHubReview]])
|
||||
)
|
||||
).toEqual([])
|
||||
expect(
|
||||
searchWorktrees(
|
||||
[makeWorktree()],
|
||||
'#17',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
new Map([['wt-1', gitLabReview]])
|
||||
)
|
||||
).toEqual([])
|
||||
expect(
|
||||
searchWorktrees(
|
||||
[makeWorktree({ linkedIssue: 42 })],
|
||||
'!42',
|
||||
repoMap,
|
||||
null,
|
||||
null,
|
||||
new Map([['wt-1', [{ port: 42 }]]])
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('preserves input order when query matches a repo name', () => {
|
||||
const worktrees = [
|
||||
makeWorktree({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { branchName } from '@/lib/git-utils'
|
||||
import { issueCacheKey as getIssueCacheKey } from '@/store/slices/github'
|
||||
import type { HostedReviewInfo } from '../../../shared/hosted-review'
|
||||
import type { Repo, Worktree } from '../../../shared/types'
|
||||
import { extractWorktreePaletteCommentSnippet } from './worktree-palette-comment-snippet'
|
||||
import { isWorktreePaletteQueryTooLarge } from './worktree-palette-query-bounds'
|
||||
import { matchWorktreePaletteReview } from './worktree-palette-review-match'
|
||||
|
||||
export type MatchRange = { start: number; end: number }
|
||||
|
||||
@@ -16,7 +18,7 @@ export type PaletteMatchedField =
|
||||
| 'port'
|
||||
|
||||
export type PaletteSupportingText = {
|
||||
labelKind: 'comment' | 'pr' | 'issue' | 'port'
|
||||
labelKind: 'comment' | 'pr' | 'mr' | 'issue' | 'port'
|
||||
text: string
|
||||
matchRange: MatchRange | null
|
||||
}
|
||||
@@ -69,7 +71,8 @@ export function searchWorktrees(
|
||||
repoMap: Map<string, Repo>,
|
||||
prCache: Record<string, PRCacheEntry> | null,
|
||||
issueCache: Record<string, IssueCacheEntry> | null,
|
||||
workspacePortsByWorktreeId?: Map<string, { port: number; processName?: string }[]>
|
||||
workspacePortsByWorktreeId?: Map<string, { port: number; processName?: string }[]>,
|
||||
checksReviewByWorktreeId?: ReadonlyMap<string, HostedReviewInfo>
|
||||
): PaletteSearchResult[] {
|
||||
if (isWorktreePaletteQueryTooLarge(query)) {
|
||||
return []
|
||||
@@ -199,42 +202,29 @@ export function searchWorktrees(
|
||||
}
|
||||
|
||||
const repo = repoMap.get(worktree.repoId)
|
||||
const checksReview = checksReviewByWorktreeId?.get(worktree.id)
|
||||
if (checksReview) {
|
||||
const supportingText = matchWorktreePaletteReview(checksReview, q, numericQuery)
|
||||
if (supportingText) {
|
||||
results.push(makeResult(worktree.id, 'pr', { supportingText }))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const prKey = repo ? `${repo.path}::${branch}` : ''
|
||||
const pr = prKey && prCache ? prCache[prKey]?.data : undefined
|
||||
const pr = !checksReview && prKey && prCache ? prCache[prKey]?.data : undefined
|
||||
|
||||
if (pr) {
|
||||
const prText = `PR #${pr.number}`
|
||||
const prNumberIndex = String(pr.number).indexOf(numericQuery)
|
||||
if (prNumberIndex !== -1) {
|
||||
results.push(
|
||||
makeResult(worktree.id, 'pr', {
|
||||
supportingText: {
|
||||
labelKind: 'pr',
|
||||
text: prText,
|
||||
matchRange: {
|
||||
start: 'PR #'.length + prNumberIndex,
|
||||
end: 'PR #'.length + prNumberIndex + numericQuery.length
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
const supportingText = matchWorktreePaletteReview(
|
||||
{ ...pr, provider: 'github' },
|
||||
q,
|
||||
numericQuery
|
||||
)
|
||||
if (supportingText) {
|
||||
results.push(makeResult(worktree.id, 'pr', { supportingText }))
|
||||
continue
|
||||
}
|
||||
|
||||
const prTitleIndex = pr.title.toLowerCase().indexOf(q)
|
||||
if (prTitleIndex !== -1) {
|
||||
results.push(
|
||||
makeResult(worktree.id, 'pr', {
|
||||
supportingText: {
|
||||
labelKind: 'pr',
|
||||
text: pr.title,
|
||||
matchRange: { start: prTitleIndex, end: prTitleIndex + q.length }
|
||||
}
|
||||
})
|
||||
)
|
||||
continue
|
||||
}
|
||||
} else if (worktree.linkedPR != null) {
|
||||
} else if (!checksReview && worktree.linkedPR != null) {
|
||||
const prText = `PR #${worktree.linkedPR}`
|
||||
const prNumberIndex = String(worktree.linkedPR).indexOf(numericQuery)
|
||||
if (prNumberIndex !== -1) {
|
||||
|
||||
Reference in New Issue
Block a user