Files
orca/src/main/runtime/runtime-github-repository-query-commands.ts
T
Kelvin Amoaba 75b658c278 feat(native-chat): hover cards for GitHub PR and issue links (#35199)
* feat(native-chat): hover cards for GitHub PR and issue links

Links to the chat's own repo (origin or upstream) open a card with the
state, title, author, update time, and +/- or comment count. Bare #123
references in agent replies now link too. Cards use a light one-call
lookup; older hosts ignore the flag and answer with the full lookup.

* test(github): expect the preview options argument in runtime owner/repo lookups

* fix(native-chat): route GitHub link cards by the chat's own host and retry failed repo lookups

The repo lookup took the first worktree with the chat's id, so a twin on
another host could answer, and the preview cache keyed by repo id alone.
A failed slug lookup also left cards off until the chat remounted.
2026-10-29 15:29:32 +00:00

181 lines
5.5 KiB
TypeScript

import type { ListWorkItemsResult } from '../../shared/github/work-item-types'
import type { Repo } from '../../shared/repo-types'
import type { LocalProjectGhExecOptions } from '../project-runtime-git-options'
import {
countWorkItems,
getWorkItem,
getWorkItemByOwnerRepo,
listAssignableUsers,
listIssues,
listLabels,
listWorkItems,
type MainWorkItem
} from '../github/client'
import {
listGhAccountBindingInventory,
validateGhAccountBinding
} from '../github/gh-account-binding-inventory'
import { getRateLimit } from '../github/rate-limit'
import { resolveGhAccountRequiredHost } from '../github/account-binding-required-host'
import { getWorkItemDetails } from '../github/work-item-details'
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
type LocalGitArgs = [] | [LocalProjectGhExecOptions]
type RuntimeGitHubRepositoryQueryCommandsDeps = {
resolveRepo: (selector: string) => Promise<Repo>
getLocalGitArgs: (repo: Repo) => LocalGitArgs
}
export class RuntimeGitHubRepositoryQueryCommands {
constructor(private readonly deps: RuntimeGitHubRepositoryQueryCommandsDeps) {}
async listRepoWorkItems(
repoSelector: string,
limit?: number,
query?: string,
page?: number,
noCache?: boolean
): Promise<ListWorkItemsResult<MainWorkItem>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listWorkItems(
repo.path,
limit,
query,
page,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
noCache,
...this.deps.getLocalGitArgs(repo)
)
}
async listRepoIssues(
repoSelector: string,
limit?: number
): Promise<Awaited<ReturnType<typeof listIssues>>['items']> {
const repo = await this.deps.resolveRepo(repoSelector)
const result = await listIssues(
repo.path,
limit,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
return result.items
}
async getRepoWorkItem(
repoSelector: string,
number: number,
type?: 'issue' | 'pr'
): Promise<Awaited<ReturnType<typeof getWorkItem>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return getWorkItem(
repo.path,
number,
type,
getRepoRoutingConnectionId(repo),
this.deps.getLocalGitArgs(repo)[0] ?? {},
repo.issueSourcePreference
)
}
async getRepoWorkItemByOwnerRepo(
repoSelector: string,
ownerRepo: { owner: string; repo: string; host?: string },
number: number,
type: 'issue' | 'pr',
options: { preview?: boolean } = {}
): Promise<Awaited<ReturnType<typeof getWorkItemByOwnerRepo>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return getWorkItemByOwnerRepo(
repo.path,
ownerRepo,
number,
type,
getRepoRoutingConnectionId(repo),
this.deps.getLocalGitArgs(repo)[0],
options
)
}
async getRepoWorkItemDetails(
repoSelector: string,
number: number,
type?: 'issue' | 'pr'
): Promise<Awaited<ReturnType<typeof getWorkItemDetails>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return getWorkItemDetails(
repo.path,
number,
type,
getRepoRoutingConnectionId(repo),
this.deps.getLocalGitArgs(repo)[0] ?? {},
repo.issueSourcePreference
)
}
async countRepoWorkItems(repoSelector: string, query?: string): Promise<number> {
const repo = await this.deps.resolveRepo(repoSelector)
return countWorkItems(
repo.path,
query,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async listRepoLabels(repoSelector: string): Promise<Awaited<ReturnType<typeof listLabels>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listLabels(
repo.path,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
async listRepoAssignableUsers(
repoSelector: string
): Promise<Awaited<ReturnType<typeof listAssignableUsers>>> {
const repo = await this.deps.resolveRepo(repoSelector)
return listAssignableUsers(
repo.path,
repo.issueSourcePreference,
getRepoRoutingConnectionId(repo),
...this.deps.getLocalGitArgs(repo)
)
}
getGitHubRateLimit(options?: {
force?: boolean
}): Promise<Awaited<ReturnType<typeof getRateLimit>>> {
return getRateLimit(options)
}
async listGitHubBindableAccounts(
repoSelector: string,
options?: { refreshCapability?: boolean }
): Promise<Awaited<ReturnType<typeof listGhAccountBindingInventory>>> {
const repo = await this.deps.resolveRepo(repoSelector)
const localGitOptions = this.deps.getLocalGitArgs(repo)[0] ?? {}
const requiredHost = await resolveGhAccountRequiredHost(repo, localGitOptions)
return listGhAccountBindingInventory(localGitOptions, {
refreshCapability: options?.refreshCapability === true,
...(requiredHost ? { requiredHost } : {})
})
}
async validateGitHubAccountBinding(
repoSelector: string,
binding: { host: string; user: string }
): Promise<Awaited<ReturnType<typeof validateGhAccountBinding>>> {
const repo = await this.deps.resolveRepo(repoSelector)
const localGitOptions = this.deps.getLocalGitArgs(repo)[0] ?? {}
const requiredHost = await resolveGhAccountRequiredHost(repo, localGitOptions)
return validateGhAccountBinding(binding, localGitOptions, requiredHost ? { requiredHost } : {})
}
}