From 8745b76a64a85d9c879bc9deabb2e437d44d100d Mon Sep 17 00:00:00 2001 From: Marc Kassubeck Date: Wed, 20 May 2026 23:11:07 +0200 Subject: [PATCH 1/2] refactor: lazy-load webview panel modules via shared cache Decouple PlanningPanel, PrDetailsPanel, PipelineRunDetailsPanel from the activation import graph. Each is now reached via a shared lazyPanels.ts loader that caches the dynamic import promise so all callers observe the same loaded-or-not state. This matters for static refresh methods (refreshAllOpenPanels, refreshOpenPanels) which must no-op when the module was never opened; they consult *PanelLoaded() instead of forcing a load. Honest scoping note: the extension host bundle is currently CJS without splitting, so esbuild inlines dynamic imports as wrapped require() calls and the modules are still parsed at activation. This change is purely structural and unblocks a future move to ESM + splitting (or manual external chunks) where the deferred parse would actually take effect. --- src/commands/pipelineCommands.ts | 3 ++- src/commands/pullRequestCommands.ts | 3 ++- src/extension.ts | 23 ++++++++++++++--------- src/views/lazyPanels.ts | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/views/lazyPanels.ts diff --git a/src/commands/pipelineCommands.ts b/src/commands/pipelineCommands.ts index 8d458e9..d7e5a70 100644 --- a/src/commands/pipelineCommands.ts +++ b/src/commands/pipelineCommands.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import type { AdoClient } from '../api/adoClient'; import type { ConfigManager } from '../config/configManager'; import type { PipelineRunNode } from '../providers/pipelinesProvider'; -import { PipelineRunDetailsPanel } from '../views/pipelineRunDetailsPanel'; +import { loadPipelineRunDetailsPanel } from '../views/lazyPanels'; import { showErrorMessage, showInformationMessage } from '../utils/notifications'; import { pipelineRunUrl } from '../utils/pipelineUrls'; @@ -17,6 +17,7 @@ export async function viewPipelineRunDetails( return; } + const { PipelineRunDetailsPanel } = await loadPipelineRunDetailsPanel(); await PipelineRunDetailsPanel.show(context, client, config, node.build.id, { organization: node.organization, project: node.project diff --git a/src/commands/pullRequestCommands.ts b/src/commands/pullRequestCommands.ts index 50dae0f..8f6dea1 100644 --- a/src/commands/pullRequestCommands.ts +++ b/src/commands/pullRequestCommands.ts @@ -7,9 +7,9 @@ import type { import type { AdoClient, GitPullRequest, PullRequestReviewVote } from '../api/adoClient'; import { PullRequestReviewVotes } from '../api/adoClient'; import type { ConfigManager } from '../config/configManager'; -import { PrDetailsPanel } from '../views/prDetailsPanel'; import type { PrCommentController } from '../views/prCommentController'; import type { PrDiffCache } from '../views/prContentProvider'; +import { loadPrDetailsPanel } from '../views/lazyPanels'; import { parseAdoRemoteUrl } from '../utils/repoContext'; import { showErrorMessage, showInformationMessage, showWarningMessage } from '../utils/notifications'; @@ -102,6 +102,7 @@ export async function viewPullRequestDetails( client: AdoClient, config: ConfigManager ): Promise { + const { PrDetailsPanel } = await loadPrDetailsPanel(); await PrDetailsPanel.show(context, client, config, node.pr, { organization: node.organization, project: node.project diff --git a/src/extension.ts b/src/extension.ts index 6f9128a..da87679 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,10 +13,8 @@ import { import { PipelinesProvider, type PipelineRunNode, type PipelineStepLogNode } from './providers/pipelinesProvider'; import { BacklogProvider, SprintProvider, BoardProvider } from './providers/planningProviders'; import { WorkItemIconResolver } from './providers/workItemIconResolver'; -import { PlanningPanel } from './views/planningPanel'; import { PrCommentController, type CommentReply } from './views/prCommentController'; import { PrDiffCache, PrDiffContentProvider, PR_DIFF_SCHEME } from './views/prContentProvider'; -import { PrDetailsPanel } from './views/prDetailsPanel'; import { PipelineLogContentProvider, PIPELINE_LOG_SCHEME } from './views/pipelineLogContentProvider'; import { NotificationService } from './notifications/notificationService'; import { PrCommentHandler } from './notifications/handlers/prCommentHandler'; @@ -68,12 +66,17 @@ import { } from './commands/pipelineCommands'; import { McpServerManager } from './mcp/mcpServerManager'; import { TodoCodeActionProvider } from './views/todoCodeActionProvider'; -import { PipelineRunDetailsPanel } from './views/pipelineRunDetailsPanel'; import { AdoCompletionProvider } from './providers/completionProvider'; import { installNotificationMirroring, showErrorMessage, showInformationMessage, showOutputChannel, showWarningMessage } from './utils/notifications'; import { WorkItemHoverProvider, PullRequestHoverProvider } from './providers/hoverProvider'; import { adoErrorFingerprint, classifyAdoAuthError } from './utils/adoErrors'; import type { AuthRecoveryResult } from './utils/authRecovery'; +import { + loadPlanningPanel, + planningPanelLoaded, + prDetailsPanelLoaded, + loadPipelineRunDetailsPanel +} from './views/lazyPanels'; export async function activate(context: vscode.ExtensionContext): Promise { installNotificationMirroring(); @@ -465,21 +468,21 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push( vscode.commands.registerCommand('adoext.openBacklogView', async () => { if (!(await ensureSignedIn())) { return; } - await PlanningPanel.show(context, 'backlog', client, config, refreshAllViews); + await (await loadPlanningPanel()).PlanningPanel.show(context, 'backlog', client, config, refreshAllViews); }) ); context.subscriptions.push( vscode.commands.registerCommand('adoext.openBoardView', async () => { if (!(await ensureSignedIn())) { return; } - await PlanningPanel.show(context, 'board', client, config, refreshAllViews); + await (await loadPlanningPanel()).PlanningPanel.show(context, 'board', client, config, refreshAllViews); }) ); context.subscriptions.push( vscode.commands.registerCommand('adoext.openSprintView', async () => { if (!(await ensureSignedIn())) { return; } - await PlanningPanel.show(context, 'sprint', client, config, refreshAllViews); + await (await loadPlanningPanel()).PlanningPanel.show(context, 'sprint', client, config, refreshAllViews); }) ); @@ -826,7 +829,8 @@ export async function activate(context: vscode.ExtensionContext): Promise async () => { await toggleResolvedPullRequestThreads(config); pullRequestProvider.refresh(); - await PrDetailsPanel.refreshAllOpenPanels(); + const prMod = prDetailsPanelLoaded(); + if (prMod) { await (await prMod).PrDetailsPanel.refreshAllOpenPanels(); } } ) ); @@ -897,7 +901,7 @@ export async function activate(context: vscode.ExtensionContext): Promise if (!(await ensureSignedIn())) { return; } const newId = await rerunPipelineRun(node, client, config); if (typeof newId === 'number' && newId > 0) { - await PipelineRunDetailsPanel.show(context, client, config, newId, { + await (await loadPipelineRunDetailsPanel()).PipelineRunDetailsPanel.show(context, client, config, newId, { organization: node?.organization, project: node?.project }); @@ -1163,7 +1167,8 @@ export async function activate(context: vscode.ExtensionContext): Promise } refreshAllViews(); if (e.affectsConfiguration('adoext.planningAssignedFilter')) { - void PlanningPanel.refreshOpenPanels(); + const planningMod = planningPanelLoaded(); + if (planningMod) { void planningMod.then(m => m.PlanningPanel.refreshOpenPanels()); } } if ( e.affectsConfiguration('adoext.notifyOnNewPullRequestComments') || diff --git a/src/views/lazyPanels.ts b/src/views/lazyPanels.ts new file mode 100644 index 0000000..7b02607 --- /dev/null +++ b/src/views/lazyPanels.ts @@ -0,0 +1,17 @@ +// Shared lazy loaders for heavy webview panel modules. +// Caching the import promise here ensures all callers (extension.ts and +// command modules) observe the same loaded-or-not state, which matters for +// static refresh methods that must no-op when the module was never opened. + +let _planning: Promise | undefined; +export const loadPlanningPanel = () => (_planning ??= import('./planningPanel')); +export const planningPanelLoaded = () => _planning; + +let _prDetails: Promise | undefined; +export const loadPrDetailsPanel = () => (_prDetails ??= import('./prDetailsPanel')); +export const prDetailsPanelLoaded = () => _prDetails; + +let _pipelineRun: Promise | undefined; +export const loadPipelineRunDetailsPanel = () => + (_pipelineRun ??= import('./pipelineRunDetailsPanel')); +export const pipelineRunDetailsPanelLoaded = () => _pipelineRun; From de60210c223f0ee7bbc7f3acf43bce627e3d7528 Mon Sep 17 00:00:00 2001 From: Marc Kassubeck Date: Thu, 21 May 2026 00:38:19 +0200 Subject: [PATCH 2/2] review: rename *PanelLoaded() to loadedXPanel() The reviewer flagged the old name as misleading: it returns the cached import promise iff loadXPanel() was previously called, not a boolean 'is loaded' state. New names follow the loadX / loadedX convention used elsewhere for lazy module accessors. --- src/extension.ts | 8 ++++---- src/views/lazyPanels.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index da87679..a71e4df 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -73,8 +73,8 @@ import { adoErrorFingerprint, classifyAdoAuthError } from './utils/adoErrors'; import type { AuthRecoveryResult } from './utils/authRecovery'; import { loadPlanningPanel, - planningPanelLoaded, - prDetailsPanelLoaded, + loadedPlanningPanel, + loadedPrDetailsPanel, loadPipelineRunDetailsPanel } from './views/lazyPanels'; @@ -829,7 +829,7 @@ export async function activate(context: vscode.ExtensionContext): Promise async () => { await toggleResolvedPullRequestThreads(config); pullRequestProvider.refresh(); - const prMod = prDetailsPanelLoaded(); + const prMod = loadedPrDetailsPanel(); if (prMod) { await (await prMod).PrDetailsPanel.refreshAllOpenPanels(); } } ) @@ -1167,7 +1167,7 @@ export async function activate(context: vscode.ExtensionContext): Promise } refreshAllViews(); if (e.affectsConfiguration('adoext.planningAssignedFilter')) { - const planningMod = planningPanelLoaded(); + const planningMod = loadedPlanningPanel(); if (planningMod) { void planningMod.then(m => m.PlanningPanel.refreshOpenPanels()); } } if ( diff --git a/src/views/lazyPanels.ts b/src/views/lazyPanels.ts index 7b02607..75b80b4 100644 --- a/src/views/lazyPanels.ts +++ b/src/views/lazyPanels.ts @@ -2,16 +2,20 @@ // Caching the import promise here ensures all callers (extension.ts and // command modules) observe the same loaded-or-not state, which matters for // static refresh methods that must no-op when the module was never opened. +// +// `loadX()` triggers (or reuses) the import. +// `loadedX()` returns the cached import promise iff loadX() was ever called, +// otherwise undefined - used to skip work on modules that were never opened. let _planning: Promise | undefined; export const loadPlanningPanel = () => (_planning ??= import('./planningPanel')); -export const planningPanelLoaded = () => _planning; +export const loadedPlanningPanel = () => _planning; let _prDetails: Promise | undefined; export const loadPrDetailsPanel = () => (_prDetails ??= import('./prDetailsPanel')); -export const prDetailsPanelLoaded = () => _prDetails; +export const loadedPrDetailsPanel = () => _prDetails; let _pipelineRun: Promise | undefined; export const loadPipelineRunDetailsPanel = () => (_pipelineRun ??= import('./pipelineRunDetailsPanel')); -export const pipelineRunDetailsPanelLoaded = () => _pipelineRun; +export const loadedPipelineRunDetailsPanel = () => _pipelineRun;