From 387e5d8324bd995451ae51a480583da659345185 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:28:54 -0700 Subject: [PATCH] Support GitHub Enterprise diffs and mutations with host-scoped caches Parse GitHub host identity from work-item URLs and carry it through PR/issue mutations, labels, and assignments. Bound rate-limit and scope-probe caches (1024 and 512 entries) to prevent unbounded growth when interacting with multiple GHES instances. Normalize repository identity keys to include host so github.com and GHES slugs don't collide in cache and equality checks. --- src/main/git/gh-rate-limit-breaker.test.ts | 14 +++++ src/main/git/gh-rate-limit-breaker.ts | 22 +++++++ src/main/github/client.ts | 6 +- src/main/github/github-repository-identity.ts | 3 +- src/main/github/rate-limit.ts | 22 ++++++- .../src/components/PullRequestPage.tsx | 47 +++++++++++---- src/renderer/src/components/TaskPage.tsx | 9 ++- ...b-enterprise-slug-routing-boundary.test.ts | 54 +++++++++++++++++ ...github-item-dialog-source-boundary.test.ts | 3 +- .../github-project/GhAuthErrorHelp.test.ts | 60 +++++++++++++++++++ .../github-project/GhAuthErrorHelp.tsx | 16 +++-- .../github-project/ProjectPicker.tsx | 1 + .../github-project/ProjectViewWrapper.tsx | 4 ++ .../github/IssueSourceIndicator.tsx | 11 ++-- .../components/github/PRFilterDropdowns.tsx | 6 +- .../pull-request-page-host-boundary.test.ts | 7 ++- .../components/right-sidebar/ChecksPanel.tsx | 2 + src/renderer/src/store/slices/github.test.ts | 6 ++ src/shared/github-project-identity.test.ts | 3 + src/shared/github-project-identity.ts | 5 +- .../github-repository-identity-key.test.ts | 14 +++++ src/shared/github-repository-identity-key.ts | 5 +- 22 files changed, 276 insertions(+), 44 deletions(-) create mode 100644 src/renderer/src/components/github-enterprise-slug-routing-boundary.test.ts create mode 100644 src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts create mode 100644 src/shared/github-repository-identity-key.test.ts diff --git a/src/main/git/gh-rate-limit-breaker.test.ts b/src/main/git/gh-rate-limit-breaker.test.ts index 3e136954522..787ad3d1777 100644 --- a/src/main/git/gh-rate-limit-breaker.test.ts +++ b/src/main/git/gh-rate-limit-breaker.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { + _getGhRateLimitBlockCount, _resetGhRateLimitBreaker, classifyGhRateLimitBucket, createGhRateLimitBlockedError, @@ -117,6 +118,19 @@ describe('breaker state', () => { expect(getGhRateLimitBlockedUntilMs('core', now, 'wsl:ubuntu:github.com')).toBeNull() }) + it('bounds retained blocks across user-supplied Enterprise host scopes', () => { + const now = Date.now() + for (let i = 0; i < 1_200; i += 1) { + recordGhPrimaryRateLimit('core', now + 60_000, `native:ghe-${i}.example.test`) + } + + expect(_getGhRateLimitBlockCount()).toBe(1024) + expect(getGhRateLimitBlockedUntilMs('core', now, 'native:ghe-1199.example.test')).toBe( + now + 60_000 + ) + expect(getGhRateLimitBlockedUntilMs('core', now, 'native:ghe-0.example.test')).toBeNull() + }) + it('notifyGhPrimaryRateLimit applies a fallback block and fires the reset probe', () => { const probe = vi.fn() registerGhRateLimitResetProbe(probe) diff --git a/src/main/git/gh-rate-limit-breaker.ts b/src/main/git/gh-rate-limit-breaker.ts index 623555d8133..6da81bab879 100644 --- a/src/main/git/gh-rate-limit-breaker.ts +++ b/src/main/git/gh-rate-limit-breaker.ts @@ -57,6 +57,7 @@ export function parseGhRateLimitScopeKey(scope: string): GhRateLimitScopeParts | } const DEFAULT_SCOPE = ghRateLimitScopeKey('native', 'github.com') +const GH_RATE_LIMIT_BLOCK_MAX_ENTRIES = 1024 const blockedUntilMsByScopeAndBucket = new Map() let resetProbe: ((bucket: GhRateLimitBucket, scope: string) => void) | null = null @@ -146,7 +147,18 @@ export function recordGhPrimaryRateLimit( ): void { const key = breakerKey(bucket, scope) const existing = blockedUntilMsByScopeAndBucket.get(key) ?? 0 + // Why: host scopes can originate from pasted GHES URLs. Refresh insertion + // order and cap retained blocks so a stream of failing hosts cannot grow the + // main-process map for the lifetime of the app. + blockedUntilMsByScopeAndBucket.delete(key) blockedUntilMsByScopeAndBucket.set(key, Math.max(existing, blockedUntilMs)) + while (blockedUntilMsByScopeAndBucket.size > GH_RATE_LIMIT_BLOCK_MAX_ENTRIES) { + const oldestKey = blockedUntilMsByScopeAndBucket.keys().next().value + if (oldestKey === undefined) { + break + } + blockedUntilMsByScopeAndBucket.delete(oldestKey) + } } export function clearGhRateLimitBlock(bucket: GhRateLimitBucket, scope = DEFAULT_SCOPE): void { @@ -158,6 +170,11 @@ export function getGhRateLimitBlockedUntilMs( nowMs: number = Date.now(), scope = DEFAULT_SCOPE ): number | null { + for (const [key, blockedUntil] of blockedUntilMsByScopeAndBucket) { + if (blockedUntil <= nowMs) { + blockedUntilMsByScopeAndBucket.delete(key) + } + } const key = breakerKey(bucket, scope) const blockedUntil = blockedUntilMsByScopeAndBucket.get(key) if (blockedUntil === undefined) { @@ -170,6 +187,11 @@ export function getGhRateLimitBlockedUntilMs( return blockedUntil } +/** @internal — cache-bound tests only. */ +export function _getGhRateLimitBlockCount(): number { + return blockedUntilMsByScopeAndBucket.size +} + /** Register the (single) callback that refreshes precise reset times after a * breaker trip. Registered by the github rate-limit prober at module load. */ export function registerGhRateLimitResetProbe( diff --git a/src/main/github/client.ts b/src/main/github/client.ts index cc43aec30b9..16b19c6c5ce 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -2585,10 +2585,6 @@ function parseTrackedUpstreamBranch(upstreamRef: string): TrackedUpstreamBranch return parsed } -function prOwnerRepoKey(ownerRepo: OwnerRepo): string { - return `${ownerRepo.owner.toLowerCase()}/${ownerRepo.repo.toLowerCase()}` -} - function shouldRetryTrackedUpstreamBranch( upstreamBranch: TrackedUpstreamBranch, branchName: string, @@ -2601,7 +2597,7 @@ function shouldRetryTrackedUpstreamBranch( if (!headRepo) { return true } - return prOwnerRepoKey(upstreamHeadRepo) !== prOwnerRepoKey(headRepo) + return githubRepoIdentityKey(upstreamHeadRepo) !== githubRepoIdentityKey(headRepo) } async function getTrackedUpstreamBranch( diff --git a/src/main/github/github-repository-identity.ts b/src/main/github/github-repository-identity.ts index aa106972cdf..dce7ec6e63c 100644 --- a/src/main/github/github-repository-identity.ts +++ b/src/main/github/github-repository-identity.ts @@ -8,6 +8,7 @@ import { type GitHubRemoteIdentity } from './github-remote-identity-parsing' import { isStableMissingGitRemoteError } from './stable-missing-git-remote-error' +import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key' export type OwnerRepo = GitHubOwnerRepo @@ -229,7 +230,7 @@ export type PRRepositoryCandidates = { } function ownerRepoKey(ownerRepo: OwnerRepo): string { - return `${ownerRepo.owner.toLowerCase()}/${ownerRepo.repo.toLowerCase()}` + return githubRepoIdentityKey(ownerRepo) } export async function resolvePRRepositoryCandidates( diff --git a/src/main/github/rate-limit.ts b/src/main/github/rate-limit.ts index 524d302c5ef..2c3a12a8162 100644 --- a/src/main/github/rate-limit.ts +++ b/src/main/github/rate-limit.ts @@ -242,6 +242,24 @@ const DEFAULT_BREAKER_SCOPE = ghRateLimitScopeKey('native', 'github.com') // versa). const scopeRefinementInFlight = new Map>() const scopeProbeFailureAtMs = new Map() +const SCOPE_PROBE_FAILURE_MAX_ENTRIES = 512 + +function rememberScopeProbeFailure(scope: string, failedAt: number): void { + for (const [key, at] of scopeProbeFailureAtMs) { + if (failedAt - at >= RATE_LIMIT_CACHE_TTL_MS) { + scopeProbeFailureAtMs.delete(key) + } + } + scopeProbeFailureAtMs.delete(scope) + scopeProbeFailureAtMs.set(scope, failedAt) + while (scopeProbeFailureAtMs.size > SCOPE_PROBE_FAILURE_MAX_ENTRIES) { + const oldestKey = scopeProbeFailureAtMs.keys().next().value + if (oldestKey === undefined) { + break + } + scopeProbeFailureAtMs.delete(oldestKey) + } +} function refineBreakerForScope(scope: string): void { // Default scope keeps the existing shared-snapshot refinement path. @@ -288,7 +306,9 @@ function refineBreakerForScope(scope: string): void { release() } } catch { - scopeProbeFailureAtMs.set(scope, Date.now()) + // Why: GHES hosts can be supplied through pasted project URLs. Bound the + // negative cache so repeated failing hosts cannot accumulate forever. + rememberScopeProbeFailure(scope, Date.now()) } finally { scopeRefinementInFlight.delete(scope) } diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index 93e655c22e9..eedc4ae3264 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -225,23 +225,20 @@ import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' // Why: the GH item dialog can be opened from any work-item list surface and // doesn't have the full owner/repo context the list's cache entry carries. -// Parsing the canonical `https://github.com/{owner}/{repo}/...` URL is the +// Parsing the canonical `https://{github-host}/{owner}/{repo}/...` URL is the // simplest reliable source — the URL is already present on every work item -// and survives the main-process → IPC boundary. Non-GitHub hosts return null, -// which matches the indicator's suppression rule. +// and survives the main-process → IPC boundary. function parseOwnerRepoFromItemUrl(url: string): GitHubOwnerRepo | null { try { const parsed = new URL(url) - if (parsed.hostname !== 'github.com') { + if ((parsed.protocol !== 'https:' && parsed.protocol !== 'http:') || !parsed.host) { return null } const segments = parsed.pathname.split('/').filter(Boolean) if (segments.length < 2) { return null } - // Why: the URL proves the host. Leaving it off lets the main process stamp - // a GHES origin's host onto what is provably a github.com repo. - return { owner: segments[0], repo: segments[1], host: 'github.com' } + return { owner: segments[0], repo: segments[1], host: parsed.host } } catch { return null } @@ -298,6 +295,7 @@ function normalizeItemDialogTab( export type PullRequestPageProjectOrigin = { owner: string repo: string + host?: string number: number type: 'issue' | 'pr' projectId: string @@ -579,7 +577,8 @@ function PRAssigneesPanel({ slugOwner, slugRepo, assigneeLogins, - sourceSettings + sourceSettings, + projectOrigin?.host ?? assigneeSlug?.host ) const repoAssigneesByPath = useRepoAssignees(repoPath, item.repoId, sourceSettings) const repoAssignees = slugOwner && slugRepo ? repoAssigneesBySlug : repoAssigneesByPath @@ -858,7 +857,8 @@ function PRReviewersPanel({ open && reviewSlug ? reviewSlug.owner : null, open && reviewSlug ? reviewSlug.repo : null, reviewerSeedUsers.map((user) => user.login), - sourceSettings + sourceSettings, + reviewSlug?.host ) const reviewerMetadataByPath = useRepoAssignees( open && !reviewSlug ? repoPath : null, @@ -3298,7 +3298,10 @@ function ConversationTab({ const bodySlug = useMemo(() => parseOwnerRepoFromItemUrl(item.url), [item.url]) const markdownGitHubRepo = useMemo( - () => (projectOrigin ? { owner: projectOrigin.owner, repo: projectOrigin.repo } : bodySlug), + () => + projectOrigin + ? { owner: projectOrigin.owner, repo: projectOrigin.repo, host: projectOrigin.host } + : bodySlug, [bodySlug, projectOrigin] ) const canEditBody = @@ -5551,6 +5554,7 @@ async function runIssueUpdate(args: { const updateArgs = { owner: args.projectOrigin.owner, repo: args.projectOrigin.repo, + ...(args.projectOrigin.host ? { host: args.projectOrigin.host } : {}), number: args.number, updates: args.updates } @@ -5631,7 +5635,11 @@ async function runWorkItemBodyUpdate(args: { }): Promise { if (args.item.type === 'pr') { const targetSlug = args.projectOrigin - ? { owner: args.projectOrigin.owner, repo: args.projectOrigin.repo } + ? { + owner: args.projectOrigin.owner, + repo: args.projectOrigin.repo, + host: args.projectOrigin.host + } : args.parsedSlug if (!targetSlug) { throw new Error('No GitHub repository context available for this pull request.') @@ -5644,6 +5652,7 @@ async function runWorkItemBodyUpdate(args: { const updateArgs = { owner: targetSlug.owner, repo: targetSlug.repo, + ...(targetSlug.host ? { host: targetSlug.host } : {}), number: args.item.number, updates: { body: args.body } } @@ -5703,6 +5712,7 @@ async function runPullRequestStateUpdate(args: { const updateArgs = { owner: args.projectOrigin.owner, repo: args.projectOrigin.repo, + ...(args.projectOrigin.host ? { host: args.projectOrigin.host } : {}), number: args.number, updates: args.updates } @@ -5853,14 +5863,25 @@ function GHEditSection({ projectOrigin ? null : repoId, sourceSettings ) - const repoLabelsBySlug = useRepoLabelsBySlug(slugOwner, slugRepo, sourceSettings) + const repoLabelsBySlug = useRepoLabelsBySlug( + slugOwner, + slugRepo, + sourceSettings, + projectOrigin?.host + ) const repoLabels = projectOrigin ? repoLabelsBySlug : repoLabelsByPath const repoAssigneesByPath = useRepoAssignees( projectOrigin ? null : repoPath, projectOrigin ? null : repoId, sourceSettings ) - const repoAssigneesBySlug = useRepoAssigneesBySlug(slugOwner, slugRepo, assignees, sourceSettings) + const repoAssigneesBySlug = useRepoAssigneesBySlug( + slugOwner, + slugRepo, + assignees, + sourceSettings, + projectOrigin?.host + ) const repoAssignees = projectOrigin ? repoAssigneesBySlug : repoAssigneesByPath // Why: sync local assignees when item changes or when the detail fetch diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 2695d989cee..c002ff86408 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -1191,6 +1191,7 @@ function GHStatusCell({ { owner: parsedOwnerRepo.owner, repo: parsedOwnerRepo.repo, + ...(parsedOwnerRepo.host ? { host: parsedOwnerRepo.host } : {}), number: item.number, updates }, @@ -1199,6 +1200,7 @@ function GHStatusCell({ : window.api.gh.updateIssueBySlug({ owner: parsedOwnerRepo.owner, repo: parsedOwnerRepo.repo, + ...(parsedOwnerRepo.host ? { host: parsedOwnerRepo.host } : {}), number: item.number, updates }) @@ -1807,7 +1809,8 @@ function GHAssigneesCell({ open ? owner : null, open ? repoName : null, seedLogins, - sourceSettings + sourceSettings, + parsed?.slug.host ) const toggleAssignee = useCallback( @@ -1831,6 +1834,7 @@ function GHAssigneesCell({ const args = { owner, repo: repoName, + ...(parsed?.slug.host ? { host: parsed.slug.host } : {}), number: item.number, updates } @@ -2201,7 +2205,8 @@ function PRReviewCell({ open && reviewSlug ? reviewSlug.owner : null, open && reviewSlug ? reviewSlug.repo : null, reviewerSeedUsers.map((user) => user.login), - sourceSettings + sourceSettings, + reviewSlug?.host ) const authorLogin = item.author?.toLowerCase() ?? null diff --git a/src/renderer/src/components/github-enterprise-slug-routing-boundary.test.ts b/src/renderer/src/components/github-enterprise-slug-routing-boundary.test.ts new file mode 100644 index 00000000000..4247fd80d0d --- /dev/null +++ b/src/renderer/src/components/github-enterprise-slug-routing-boundary.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +function componentSource(relativePath: string): string { + return readFileSync(join(__dirname, relativePath), 'utf8') +} + +function sourceBetween(source: string, startPattern: string, endPattern: string): string { + const start = source.indexOf(startPattern) + expect(start).toBeGreaterThanOrEqual(0) + const end = source.indexOf(endPattern, start + startPattern.length) + expect(end).toBeGreaterThan(start) + return source.slice(start, end) +} + +describe('GitHub Enterprise slug routing boundaries', () => { + it('keeps work-item URL hosts on TaskPage metadata and issue mutations', () => { + const source = componentSource('TaskPage.tsx') + const statusSection = sourceBetween(source, 'function GHStatusCell', 'function formatPRDelta') + const assigneeSection = sourceBetween( + source, + 'function GHAssigneesCell', + 'function getChecksLabel' + ) + + expect(statusSection).toContain( + '...(parsedOwnerRepo.host ? { host: parsedOwnerRepo.host } : {})' + ) + expect(assigneeSection).toContain('parsed?.slug.host') + expect(assigneeSection).toContain('...(parsed?.slug.host ? { host: parsed.slug.host } : {})') + }) + + it('keeps PR base-repository hosts on checks-sidebar comment writes', () => { + const source = componentSource('right-sidebar/ChecksPanel.tsx') + const conversationSection = sourceBetween( + source, + 'const handleEditComment = useCallback', + 'const handleReplyToComment = useCallback' + ) + + expect(conversationSection).toContain('...(pr.prRepo.host ? { host: pr.prRepo.host } : {})') + expect(conversationSection).toContain('updateIssueCommentBySlug({') + expect(conversationSection).toContain('deleteIssueCommentBySlug({') + }) + + it('keeps the primary repository host on task filter metadata reads', () => { + const source = componentSource('github/PRFilterDropdowns.tsx') + + expect(source).toContain('useRepoLabelsBySlug(') + expect(source).toContain('useRepoAssigneesBySlug(') + expect(source.match(/primarySlug\?\.host/g)).toHaveLength(2) + }) +}) diff --git a/src/renderer/src/components/github-item-dialog-source-boundary.test.ts b/src/renderer/src/components/github-item-dialog-source-boundary.test.ts index 951e22d6651..6851fdb3a2a 100644 --- a/src/renderer/src/components/github-item-dialog-source-boundary.test.ts +++ b/src/renderer/src/components/github-item-dialog-source-boundary.test.ts @@ -57,7 +57,8 @@ describe('GitHubItemDialog source host boundaries', () => { expect(section).toContain('getTaskSourceRuntimeSettings(sourceContext)') expect(section).toContain('useRepoLabels(') - expect(section).toContain('useRepoLabelsBySlug(slugOwner, slugRepo, sourceSettings)') + expect(section).toContain('useRepoLabelsBySlug(') + expect(section).toContain('projectOrigin?.host') expect(section).toContain('useRepoAssignees(') expect(section).toContain('useRepoAssigneesBySlug(') expect(section).toContain('sourceSettings') diff --git a/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts b/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts new file mode 100644 index 00000000000..da666d54523 --- /dev/null +++ b/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from 'vitest' +import type { GhAuthDiagnostic } from '../../../../shared/github-auth-types' + +vi.mock('@/i18n/i18n', () => ({ + translate: (_key: string, fallback: string) => fallback +})) + +import { buildRemediation } from './GhAuthErrorHelp' + +function diagnostic(overrides: Partial = {}): GhAuthDiagnostic { + return { + ghAvailable: true, + activeAccount: { + host: 'ghe.acme.test:8443', + user: 'alice', + active: true, + envToken: null, + source: 'keyring', + scopes: ['project', 'read:org', 'repo'] + }, + accounts: [], + envTokenInProcess: null, + missingScopes: [], + requiredScopes: ['project', 'read:org', 'repo'], + hasKeyringFallback: false, + requiredHost: 'ghe.acme.test:8443', + requiredHostAuthenticated: true, + ...overrides + } +} + +describe('GitHub Project auth remediation host routing', () => { + it('uses the requested GHES host while the diagnostic is still loading', () => { + expect( + buildRemediation('Sign in required.', 'auth_required', null, 'ghe.acme.test:8443').commands + ).toEqual([ + { + label: 'Copy command', + command: 'gh auth login --hostname ghe.acme.test:8443' + } + ]) + expect( + buildRemediation('Missing scope.', 'scope_missing', null, 'ghe.acme.test:8443').commands + ).toEqual([ + { + label: 'Copy command', + command: 'gh auth refresh --hostname ghe.acme.test:8443 -s project -s read:org -s repo' + } + ]) + }) + + it('keeps the GHES host on best-effort refresh advice after diagnostics complete', () => { + expect(buildRemediation('Access denied.', 'scope_missing', diagnostic()).commands).toEqual([ + { + label: 'Copy refresh command', + command: 'gh auth refresh --hostname ghe.acme.test:8443 -s project -s read:org -s repo' + } + ]) + }) +}) diff --git a/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx b/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx index 84e0d16876c..30d1ea61b16 100644 --- a/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx +++ b/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx @@ -122,10 +122,11 @@ type Remediation = { docsUrl?: string } -function buildRemediation( +export function buildRemediation( errorMessage: string, kind: AuthErrorKind, - diag: GhAuthDiagnostic | null + diag: GhAuthDiagnostic | null, + requestedHost?: string ): Remediation { // Diagnostic still loading or unavailable — fall back to the canned advice // so the UI never gets worse than the pre-diagnosis behavior. @@ -138,7 +139,10 @@ function buildRemediation( 'auto.components.github.project.GhAuthErrorHelp.b436c586d1', 'Copy command' ), - command: kind === 'auth_required' ? LOGIN_CMD : REFRESH_CMD + command: + kind === 'auth_required' + ? loginCommandForHost(requestedHost) + : refreshCommandForHost(requestedHost) } ] } @@ -173,7 +177,7 @@ function buildRemediation( 'auto.components.github.project.GhAuthErrorHelp.9c2da6353b', 'Copy login command' ), - command: LOGIN_CMD + command: loginCommandForHost(diag.requiredHost ?? requestedHost) } ], docsUrl: 'https://cli.github.com/' @@ -264,7 +268,7 @@ function buildRemediation( 'auto.components.github.project.GhAuthErrorHelp.3fefeebde4', 'Copy refresh command' ), - command: REFRESH_CMD + command: refreshCommandForHost(diag.requiredHost ?? requestedHost) } ], docsUrl: @@ -299,7 +303,7 @@ export function GhAuthErrorHelp({ } }, [host]) - const remedy = buildRemediation(error.message, error.type, diag) + const remedy = buildRemediation(error.message, error.type, diag, host) const docsUrl = remedy.docsUrl if (variant === 'banner') { diff --git a/src/renderer/src/components/github-project/ProjectPicker.tsx b/src/renderer/src/components/github-project/ProjectPicker.tsx index 2bd7d516eca..a4d54b3a71e 100644 --- a/src/renderer/src/components/github-project/ProjectPicker.tsx +++ b/src/renderer/src/components/github-project/ProjectPicker.tsx @@ -814,6 +814,7 @@ function AuthErrorBanner({ error }: { error: GitHubProjectViewError }): React.JS ) } diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index d2879454b14..62281ce599e 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -947,6 +947,7 @@ export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JS { if (selectedViewUrl) { void window.api.shell.openUrl(selectedViewUrl) @@ -1320,10 +1321,12 @@ function ViewTabStrip({ function ErrorState({ error, totalCount, + host, onOpenInGitHub }: { error: GitHubProjectViewError totalCount?: number + host?: string onOpenInGitHub: () => void }): React.JSX.Element { // Auth/scope errors get a richer remediation UI driven by `gh auth @@ -1334,6 +1337,7 @@ function ErrorState({