diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index 1497b530478eb..2e6497b92db8b 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -792,29 +792,29 @@ export class IntegrationService implements Disposable { } const results = await Promise.allSettled(promises); - + const successfulResults = [ + ...flatten( + filterMap(results, r => + r.status === 'fulfilled' && r.value?.value != null ? r.value.value : undefined, + ), + ), + ]; const errors = [ ...filterMap(results, r => r.status === 'fulfilled' && r.value?.error != null ? r.value.error : undefined, ), ]; - if (errors.length) { - return { - error: errors.length === 1 ? errors[0] : new AggregateError(errors), - duration: Date.now() - start, - }; - } + + const error = + errors.length === 0 + ? undefined + : errors.length === 1 + ? errors[0] + : new AggregateError(errors, 'Failed to get some pull requests'); return { - value: [ - ...flatten( - filterMap(results, r => - r.status === 'fulfilled' && r.value != null && r.value?.error == null - ? r.value.value - : undefined, - ), - ), - ], + value: successfulResults, + error: error, duration: Date.now() - start, }; } diff --git a/src/plus/integrations/models/integration.ts b/src/plus/integrations/models/integration.ts index fc7294407158b..20b2b4833354c 100644 --- a/src/plus/integrations/models/integration.ts +++ b/src/plus/integrations/models/integration.ts @@ -46,7 +46,7 @@ export type IntegrationKey = T extend export type IntegrationConnectedKey = `connected:${IntegrationKey}`; export type IntegrationResult = - | { value: T; duration?: number; error?: never } + | { value: T; duration?: number; error?: Error } | { error: Error; duration?: number; value?: never } | undefined; diff --git a/src/plus/launchpad/launchpad.ts b/src/plus/launchpad/launchpad.ts index e623efa428164..1fe78b34bbc59 100644 --- a/src/plus/launchpad/launchpad.ts +++ b/src/plus/launchpad/launchpad.ts @@ -41,6 +41,7 @@ import type { IntegrationIds } from '../../constants.integrations'; import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../../constants.integrations'; import type { LaunchpadTelemetryContext, Source, Sources, TelemetryEvents } from '../../constants.telemetry'; import type { Container } from '../../container'; +import { AuthenticationError } from '../../errors'; import type { QuickPickItemOfT } from '../../quickpicks/items/common'; import { createQuickPickItemOfT, createQuickPickSeparator } from '../../quickpicks/items/common'; import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive'; @@ -52,6 +53,8 @@ import { openUrl } from '../../system/-webview/vscode/uris'; import { getScopedCounter } from '../../system/counter'; import { fromNow } from '../../system/date'; import { some } from '../../system/iterable'; +import { Logger } from '../../system/logger'; +import { AggregateError } from '../../system/promise'; import { interpolate, pluralize } from '../../system/string'; import { ProviderBuildStatusState, ProviderPullRequestReviewState } from '../integrations/providers/models'; import type { LaunchpadCategorizedResult, LaunchpadItem } from './launchpadProvider'; @@ -157,6 +160,11 @@ const instanceCounter = getScopedCounter(); const defaultCollapsedGroups: LaunchpadGroup[] = ['draft', 'other', 'snoozed']; +const OpenLogsQuickInputButton: QuickInputButton = { + iconPath: new ThemeIcon('output'), + tooltip: 'Open Logs', +}; + export class LaunchpadCommand extends QuickCommand { private readonly source: Source; private readonly telemetryContext: LaunchpadTelemetryContext | undefined; @@ -565,10 +573,10 @@ export class LaunchpadCommand extends QuickCommand { return groupedAndSorted; }; - function getItemsAndQuickpickProps(isFiltering?: boolean) { + const getItemsAndQuickpickProps = (isFiltering?: boolean) => { const result = context.inSearch ? context.searchResult : context.result; - if (result?.error != null) { + if (result?.error != null && !result?.items?.length) { return { title: `${context.title} \u00a0\u2022\u00a0 Unable to Load Items`, placeholder: `Unable to load items (${ @@ -582,7 +590,7 @@ export class LaunchpadCommand extends QuickCommand { }; } - if (!result?.items.length) { + if (!result?.items?.length) { if (context.inSearch === 'mode') { return { title: `Search For Pull Request \u00a0\u2022\u00a0 ${context.title}`, @@ -616,6 +624,11 @@ export class LaunchpadCommand extends QuickCommand { } const items = getLaunchpadQuickPickItems(result.items, isFiltering); + + // Add error information item if there's an error but items were still loaded + const errorItem: DirectiveQuickPickItem | undefined = + result?.error != null ? this.createErrorQuickPickItem(result.error) : undefined; + const hasPicked = items.some(i => i.picked); if (context.inSearch === 'mode') { const offItem: ToggleSearchModeQuickPickItem = { @@ -630,7 +643,9 @@ export class LaunchpadCommand extends QuickCommand { return { title: `Search For Pull Request \u00a0\u2022\u00a0 ${context.title}`, placeholder: 'Enter a term to search for a pull request to act on', - items: isFiltering ? [...items, offItem] : [offItem, ...items], + items: isFiltering + ? [...(errorItem != null ? [errorItem] : []), ...items, offItem] + : [offItem, ...(errorItem != null ? [errorItem] : []), ...items], }; } @@ -646,10 +661,14 @@ export class LaunchpadCommand extends QuickCommand { title: context.title, placeholder: 'Choose a pull request or paste a pull request URL to act on', items: isFiltering - ? [...items, onItem] - : [onItem, ...getLaunchpadQuickPickItems(result.items, isFiltering)], + ? [...(errorItem != null ? [errorItem] : []), ...items, onItem] + : [ + onItem, + ...(errorItem != null ? [errorItem] : []), + ...getLaunchpadQuickPickItems(result.items, isFiltering), + ], }; - } + }; const updateItems = async ( quickpick: QuickPick< @@ -830,6 +849,16 @@ export class LaunchpadCommand extends QuickCommand { return; } + if (button === OpenLogsQuickInputButton) { + Logger.showOutputChannel(); + return; + } + + if (button === ConnectIntegrationButton) { + await this.container.integrations.manageCloudIntegrations({ source: 'launchpad' }); + return; + } + if (!item) return; switch (button) { @@ -1403,6 +1432,25 @@ export class LaunchpadCommand extends QuickCommand { this.source, ); } + + private createErrorQuickPickItem(error: Error): DirectiveQuickPickItem { + if (error instanceof AggregateError) { + const firstAuthError = error.errors.find(e => e instanceof AuthenticationError); + error = firstAuthError ?? error.errors[0] ?? error; + } + + const isAuthError = error instanceof AuthenticationError; + + return createDirectiveQuickPickItem(Directive.Noop, false, { + label: isAuthError ? '$(warning) Authentication Required' : '$(warning) Unable to fully load items', + detail: isAuthError + ? `${String(error)} — Click to reconnect your integration` + : error.name === 'HttpError' && 'status' in error && typeof error.status === 'number' + ? `${error.status}: ${String(error)}` + : String(error), + buttons: isAuthError ? [ConnectIntegrationButton, OpenLogsQuickInputButton] : [OpenLogsQuickInputButton], + }); + } } function getLaunchpadItemInformationRows( @@ -1657,10 +1705,10 @@ function updateTelemetryContext(context: Context) { if (context.telemetryContext == null) return; let updatedContext: NonNullable<(typeof context)['telemetryContext']>; - if (context.result.error != null) { + if (context.result.error != null || !context.result.items) { updatedContext = { ...context.telemetryContext, - 'items.error': String(context.result.error), + 'items.error': String(context.result.error ?? 'items not loaded'), }; } else { const grouped = countLaunchpadItemGroups(context.result.items); diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts index 11cf6af7f2e4e..c3d29e204c53f 100644 --- a/src/plus/launchpad/launchpadProvider.ts +++ b/src/plus/launchpad/launchpadProvider.ts @@ -149,7 +149,7 @@ export type LaunchpadCategorizedResult = | { items: LaunchpadItem[]; timings?: LaunchpadCategorizedTimings; - error?: never; + error?: Error; } | { error: Error; @@ -221,11 +221,6 @@ export class LaunchpadProvider implements Disposable { } const prs = getSettledValue(prsResult)?.value; - if (prs?.error != null) { - Logger.error(prs.error, scope, 'Failed to get pull requests'); - throw prs.error; - } - const subscription = getSettledValue(subscriptionResult); let suggestionCounts; @@ -252,7 +247,7 @@ export class LaunchpadProvider implements Disposable { search, connectedIntegrations, ); - const result: { readonly value: PullRequest[]; duration: number } = { + const result: { readonly value: PullRequest[]; duration: number; error?: Error } = { value: [], duration: 0, }; @@ -690,7 +685,7 @@ export class LaunchpadProvider implements Disposable { isSearching ? typeof options.search === 'string' ? this.getSearchedPullRequests(options.search, cancellation) - : { prs: { value: options.search, duration: 0 }, suggestionCounts: undefined } + : { prs: { value: options.search, duration: 0, error: undefined }, suggestionCounts: undefined } : this.getPullRequestsWithSuggestionCounts({ force: options?.force, cancellation: cancellation }), ]); @@ -719,6 +714,7 @@ export class LaunchpadProvider implements Disposable { codeSuggestionCounts: prsWithSuggestionCounts?.suggestionCounts?.duration, enrichedItems: enrichedItems?.duration, }, + error: prsWithSuggestionCounts?.prs?.error, }; return result; } @@ -848,6 +844,7 @@ export class LaunchpadProvider implements Disposable { codeSuggestionCounts: prsWithSuggestionCounts?.suggestionCounts?.duration, enrichedItems: enrichedItems?.duration, }, + error: prsWithSuggestionCounts?.prs?.error, }; return result; } finally { diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 538e08ae2fbb3..352c48bae0313 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -1927,13 +1927,13 @@ async function getLaunchpadItemInfo( ): Promise { launchpadPromise ??= container.launchpad.getCategorizedItems(); let result = await launchpadPromise; - if (result.error != null) return undefined; + if (result.error != null || !result.items) return undefined; let lpi = result.items.find(i => i.url === pr.url); if (lpi == null) { // result = await container.launchpad.getCategorizedItems({ search: pr.url }); result = await container.launchpad.getCategorizedItems({ search: [pr] }); - if (result.error != null) return undefined; + if (result.error != null || !result.items) return undefined; lpi = result.items.find(i => i.url === pr.url); }