From f4d5be7bc7644561379bf586eef350edd75fd082 Mon Sep 17 00:00:00 2001 From: Ben Scholtens Date: Tue, 20 Jan 2026 13:57:14 -0500 Subject: [PATCH] Add webview tracking functionality and debug command - Introduced `trackWebviewPanel` and `trackWebviewView` functions in `webviewTracker.ts` to log the creation and disposal of webviews and webview views. - Updated various providers and components to utilize the new tracking functions for better monitoring of webview usage. - Added a debug command `codex-editor.debugWebviews` to dump active webview information to the output channel. - Enhanced existing webview management by ensuring proper disposal of resources and tracking of active instances. --- .../contentIndexes/indexes/index.ts | 2 + src/bookNameSettings/bookNameSettings.ts | 12 +++ src/cellLabelImporter/cellLabelImporter.ts | 14 +++ src/copilotSettings/copilotSettings.ts | 12 +++ src/extension.ts | 7 ++ src/globalProvider.ts | 24 ++++- src/licenseSettings/licenseSettings.ts | 12 +++ src/projectManager/projectExportView.ts | 12 +++ .../EditAnalysisViewProvider.ts | 2 + .../NewSourceUploaderProvider.ts | 2 + .../SplashScreen/SplashScreenProvider.ts | 2 + .../StartupFlow/StartupFlowProvider.ts | 2 + .../VideoEditor/VideoEditorProvider.ts | 2 + .../VideoPlayer/VideoPlayerProvider.ts | 2 + .../WelcomeView/welcomeViewProvider.ts | 2 + src/providers/WordsView/WordsViewProvider.ts | 2 + .../codexCellEditorProvider.ts | 2 + .../DictionaryEditorProvider.ts | 2 + .../publishProjectView/PublishProjectView.ts | 2 + .../cellLabelImporter.integration.test.ts | 87 +++++++++++++++++ src/utils/webviewTracker.ts | 93 +++++++++++++++++++ 21 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/test/suite/integration/cellLabelImporter.integration.test.ts create mode 100644 src/utils/webviewTracker.ts diff --git a/src/activationHelpers/contextAware/contentIndexes/indexes/index.ts b/src/activationHelpers/contextAware/contentIndexes/indexes/index.ts index 32b984eb0..eed7af192 100644 --- a/src/activationHelpers/contextAware/contentIndexes/indexes/index.ts +++ b/src/activationHelpers/contextAware/contentIndexes/indexes/index.ts @@ -1,5 +1,6 @@ "use strict"; import * as vscode from "vscode"; +import { trackWebviewPanel } from "../../../../utils/webviewTracker"; import { getWorkSpaceFolder, getWorkSpaceUri } from "../../../../utils"; import { IndexingStatusBarHandler } from "../statusBarHandler"; @@ -1013,6 +1014,7 @@ export async function createIndexWithContext(context: vscode.ExtensionContext) { vscode.ViewColumn.One, {} ); + trackWebviewPanel(panel, "fileInfo", "contentIndexes.getFileInfo"); // Generate HTML content panel.webview.html = ` diff --git a/src/bookNameSettings/bookNameSettings.ts b/src/bookNameSettings/bookNameSettings.ts index 7f84936a3..cd07ab66f 100644 --- a/src/bookNameSettings/bookNameSettings.ts +++ b/src/bookNameSettings/bookNameSettings.ts @@ -3,8 +3,15 @@ import * as vscode from "vscode"; import * as xml2js from "xml2js"; import { addMetadataEdit } from "@/utils/editMapUtils"; import { getCorrespondingSourceUri } from "@/utils/codexNotebookUtils"; +import { trackWebviewPanel } from "../utils/webviewTracker"; + +let currentPanel: vscode.WebviewPanel | undefined; export async function openBookNameEditor() { + if (currentPanel) { + currentPanel.reveal(); + return; + } const panel = vscode.window.createWebviewPanel( "bookNameEditor", "Edit Book Names", @@ -14,6 +21,11 @@ export async function openBookNameEditor() { retainContextWhenHidden: true, } ); + trackWebviewPanel(panel, "bookNameEditor", "openBookNameEditor"); + currentPanel = panel; + panel.onDidDispose(() => { + currentPanel = undefined; + }); // Dynamic import for fs and path const fs = await import("fs"); diff --git a/src/cellLabelImporter/cellLabelImporter.ts b/src/cellLabelImporter/cellLabelImporter.ts index c3dccfa28..2e6cafa7e 100644 --- a/src/cellLabelImporter/cellLabelImporter.ts +++ b/src/cellLabelImporter/cellLabelImporter.ts @@ -13,6 +13,7 @@ import { copyToTempStorage, getColumnHeaders } from "./utils"; import { updateCellLabels } from "./updater"; import { getNonce } from "../providers/dictionaryTable/utilities/getNonce"; import { safePostMessageToPanel } from "../utils/webviewUtils"; +import { trackWebviewPanel } from "../utils/webviewTracker"; const DEBUG_CELL_LABEL_IMPORTER = false; function debug(message: string, ...args: any[]): void { @@ -21,6 +22,8 @@ function debug(message: string, ...args: any[]): void { } } +let currentPanel: vscode.WebviewPanel | undefined; + // Interface for the cell label data interface CellLabelData { cellId: string; @@ -142,6 +145,10 @@ async function getHtmlForCellLabelImporterView( } export async function openCellLabelImporter(context: vscode.ExtensionContext) { + if (currentPanel) { + currentPanel.reveal(); + return; + } const panel = vscode.window.createWebviewPanel( "cellLabelImporter", "Import Cell Labels (React)", @@ -155,9 +162,16 @@ export async function openCellLabelImporter(context: vscode.ExtensionContext) { ], } ); + trackWebviewPanel(panel, "cellLabelImporter", "openCellLabelImporter"); context.subscriptions.push(panel); let disposables: vscode.Disposable[] = []; + currentPanel = panel; + panel.onDidDispose(() => { + currentPanel = undefined; + disposables.forEach((d) => d.dispose()); + disposables = []; + }); // --- Variables to manage temporary files and import sources for the current session --- let currentSessionTempFileUris: vscode.Uri[] = []; diff --git a/src/copilotSettings/copilotSettings.ts b/src/copilotSettings/copilotSettings.ts index 4da7b213a..9df08a6dc 100644 --- a/src/copilotSettings/copilotSettings.ts +++ b/src/copilotSettings/copilotSettings.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { callLLM } from "../utils/llmUtils"; import { CompletionConfig } from "@/utils/llmUtils"; import { MetadataManager } from "../utils/metadataManager"; +import { trackWebviewPanel } from "../utils/webviewTracker"; interface ProjectLanguage { tag: string; @@ -16,6 +17,8 @@ function debug(message: string, ...args: any[]): void { } } +let currentPanel: vscode.WebviewPanel | undefined; + export async function debugValidationSetting() { const config = vscode.workspace.getConfiguration("codex-editor-extension"); const useOnlyValidatedExamples = config.get("useOnlyValidatedExamples"); @@ -34,6 +37,10 @@ export async function debugValidationSetting() { } export async function openSystemMessageEditor() { + if (currentPanel) { + currentPanel.reveal(); + return; + } const panel = vscode.window.createWebviewPanel( "systemMessageEditor", "Copilot Settings", @@ -43,6 +50,11 @@ export async function openSystemMessageEditor() { retainContextWhenHidden: true, } ); + trackWebviewPanel(panel, "systemMessageEditor", "openSystemMessageEditor"); + currentPanel = panel; + panel.onDidDispose(() => { + currentPanel = undefined; + }); const scriptUri = panel.webview.asWebviewUri( vscode.Uri.joinPath( diff --git a/src/extension.ts b/src/extension.ts index f342a3303..26f220c98 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -59,6 +59,7 @@ import { checkIfMetadataAndGitIsInitialized } from "./projectManager/utils/proje import { CommentsMigrator } from "./utils/commentsMigrationUtils"; import { registerTestingCommands } from "./evaluation/testingCommands"; import { initializeABTesting } from "./utils/abTestingSetup"; +import { dumpActiveWebviews } from "./utils/webviewTracker"; import { migration_addValidationsForUserEdits, migration_moveTimestampsToMetadataData, @@ -561,6 +562,12 @@ export async function activate(context: vscode.ExtensionContext) { (async () => registerTestingCommands(context))(), ]); + context.subscriptions.push( + vscode.commands.registerCommand("codex-editor.debugWebviews", () => { + dumpActiveWebviews(); + }) + ); + // Initialize A/B testing registry (always-on) initializeABTesting(); diff --git a/src/globalProvider.ts b/src/globalProvider.ts index d89f8a519..084c60412 100644 --- a/src/globalProvider.ts +++ b/src/globalProvider.ts @@ -4,6 +4,7 @@ import { CustomWebviewProvider } from "./providers/parallelPassagesWebview/custo import { GlobalContentType, GlobalMessage } from "../types"; import { getNonce } from "./providers/dictionaryTable/utilities/getNonce"; import { safePostMessageToView } from "./utils/webviewUtils"; +import { trackWebviewView } from "./utils/webviewTracker"; @@ -11,6 +12,7 @@ import { safePostMessageToView } from "./utils/webviewUtils"; export abstract class BaseWebviewProvider implements vscode.WebviewViewProvider { protected _view?: vscode.WebviewView; protected _context: vscode.ExtensionContext; + private viewDisposables: vscode.Disposable[] = []; constructor(context: vscode.ExtensionContext) { this._context = context; @@ -28,6 +30,12 @@ export abstract class BaseWebviewProvider implements vscode.WebviewViewProvider // Common webview resolution public resolveWebviewView(webviewView: vscode.WebviewView) { + trackWebviewView(webviewView, this.getWebviewId(), "BaseWebviewProvider.resolveWebviewView"); + // Clean up any previous view-specific disposables before replacing the view + if (this.viewDisposables.length > 0) { + this.viewDisposables.forEach((d) => d.dispose()); + this.viewDisposables = []; + } this._view = webviewView; webviewView.webview.options = { @@ -38,7 +46,7 @@ export abstract class BaseWebviewProvider implements vscode.WebviewViewProvider webviewView.webview.html = this.getHtmlForWebview(webviewView); // Set up message handling with common handlers - webviewView.webview.onDidReceiveMessage(async (message: any) => { + const messageDisposable = webviewView.webview.onDidReceiveMessage(async (message: any) => { // Handle global messages first if ("destination" in message) { GlobalProvider.getInstance().handleMessage(message); @@ -54,6 +62,15 @@ export abstract class BaseWebviewProvider implements vscode.WebviewViewProvider // Pass to child class for specific handling await this.handleMessage(message); }); + this.viewDisposables.push(messageDisposable); + + const disposeDisposable = webviewView.onDidDispose(() => { + this._view = undefined; + this.viewDisposables.forEach((d) => d.dispose()); + this.viewDisposables = []; + this.onWebviewDisposed(); + }); + this.viewDisposables.push(disposeDisposable); // Call child class initialization if needed this.onWebviewResolved(webviewView); @@ -64,6 +81,11 @@ export abstract class BaseWebviewProvider implements vscode.WebviewViewProvider // Child classes can override this for additional initialization } + // Optional hook for cleanup when the webview is disposed + protected onWebviewDisposed(): void { + // Child classes can override this for cleanup + } + // Common message handlers protected async handleCommonMessage(message: any): Promise { switch (message.command) { diff --git a/src/licenseSettings/licenseSettings.ts b/src/licenseSettings/licenseSettings.ts index 7567e1787..8ffa00946 100644 --- a/src/licenseSettings/licenseSettings.ts +++ b/src/licenseSettings/licenseSettings.ts @@ -1,8 +1,15 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; +import { trackWebviewPanel } from "../utils/webviewTracker"; + +let currentPanel: vscode.WebviewPanel | undefined; export async function openLicenseEditor() { + if (currentPanel) { + currentPanel.reveal(); + return; + } const panel = vscode.window.createWebviewPanel( "licenseEditor", "Project License", @@ -12,6 +19,11 @@ export async function openLicenseEditor() { retainContextWhenHidden: true, } ); + trackWebviewPanel(panel, "licenseEditor", "openLicenseEditor"); + currentPanel = panel; + panel.onDidDispose(() => { + currentPanel = undefined; + }); // Get workspace folder const workspaceFolders = vscode.workspace.workspaceFolders; diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 48c6b13da..219f128c5 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -1,8 +1,15 @@ import { CodexExportFormat } from "../exportHandler/exportHandler"; import * as vscode from "vscode"; import { safePostMessageToPanel } from "../utils/webviewUtils"; +import { trackWebviewPanel } from "../utils/webviewTracker"; + +let currentPanel: vscode.WebviewPanel | undefined; export async function openProjectExportView(context: vscode.ExtensionContext) { + if (currentPanel) { + currentPanel.reveal(); + return; + } const panel = vscode.window.createWebviewPanel( "projectExportView", "Export Project", @@ -12,6 +19,11 @@ export async function openProjectExportView(context: vscode.ExtensionContext) { retainContextWhenHidden: true, } ); + trackWebviewPanel(panel, "projectExportView", "openProjectExportView"); + currentPanel = panel; + panel.onDidDispose(() => { + currentPanel = undefined; + }); // Get project configuration const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); diff --git a/src/providers/EditAnalysisView/EditAnalysisViewProvider.ts b/src/providers/EditAnalysisView/EditAnalysisViewProvider.ts index 19a4fa9ee..2bb93b0bb 100644 --- a/src/providers/EditAnalysisView/EditAnalysisViewProvider.ts +++ b/src/providers/EditAnalysisView/EditAnalysisViewProvider.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { analyzeEditHistory } from "../../activationHelpers/contextAware/contentIndexes/indexes/editHistory"; import { readLocalProjectSettings, writeLocalProjectSettings } from "../../utils/localProjectSettings"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; export class EditAnalysisProvider implements vscode.Disposable { public static readonly viewType = "codex-editor.editAnalysis"; @@ -28,6 +29,7 @@ export class EditAnalysisProvider implements vscode.Disposable { retainContextWhenHidden: true, } ); + trackWebviewPanel(this._panel, EditAnalysisProvider.viewType, "EditAnalysisProvider.show"); this._panel.onDidDispose(() => { this._panel = undefined; diff --git a/src/providers/NewSourceUploader/NewSourceUploaderProvider.ts b/src/providers/NewSourceUploader/NewSourceUploaderProvider.ts index b8e5a743f..c7fb4df27 100644 --- a/src/providers/NewSourceUploader/NewSourceUploaderProvider.ts +++ b/src/providers/NewSourceUploader/NewSourceUploaderProvider.ts @@ -26,6 +26,7 @@ import { getNotebookMetadataManager } from "../../utils/notebookMetadataManager" import { migrateLocalizedBooksToMetadata as migrateLocalizedBooks } from "./localizedBooksMigration/localizedBooksMigration"; import { removeLocalizedBooksJsonIfPresent as removeLocalizedBooksJson } from "./localizedBooksMigration/removeLocalizedBooksJson"; import { getAttachmentDocumentSegmentFromUri } from "../../utils/attachmentFolderUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; // import { parseRtfWithPandoc as parseRtfNode } from "../../../webviews/codex-webviews/src/NewSourceUploader/importers/rtf/pandocNodeBridge"; const execAsync = promisify(exec); @@ -102,6 +103,7 @@ export class NewSourceUploaderProvider implements vscode.CustomTextEditorProvide webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken ): Promise { + trackWebviewPanel(webviewPanel, NewSourceUploaderProvider.viewType, "NewSourceUploaderProvider.resolveCustomTextEditor"); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; webviewPanel.webview.options = { enableScripts: true, diff --git a/src/providers/SplashScreen/SplashScreenProvider.ts b/src/providers/SplashScreen/SplashScreenProvider.ts index 9395fe298..aadc7cb1c 100644 --- a/src/providers/SplashScreen/SplashScreenProvider.ts +++ b/src/providers/SplashScreen/SplashScreenProvider.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { ActivationTiming } from "../../extension"; import { getWebviewHtml } from "../../utils/webviewTemplate"; import { safePostMessageToPanel } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; const DEBUG_SPLASH_SCREEN_PROVIDER = false; function debug(message: string, ...args: any[]): void { @@ -61,6 +62,7 @@ export class SplashScreenProvider { retainContextWhenHidden: true, } ); + trackWebviewPanel(this._panel, SplashScreenProvider.viewType, "SplashScreenProvider.show"); debug("[SplashScreen] Panel created successfully"); // Set webview options diff --git a/src/providers/StartupFlow/StartupFlowProvider.ts b/src/providers/StartupFlow/StartupFlowProvider.ts index 8e7e15046..2db022995 100644 --- a/src/providers/StartupFlow/StartupFlowProvider.ts +++ b/src/providers/StartupFlow/StartupFlowProvider.ts @@ -26,6 +26,7 @@ import JSZip from "jszip"; import { getWebviewHtml } from "../../utils/webviewTemplate"; import { safePostMessageToPanel, safeIsVisible, safeSetHtml, safeSetOptions } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; import * as path from "path"; import * as fs from "fs"; import git from "isomorphic-git"; @@ -197,6 +198,7 @@ export class StartupFlowProvider implements vscode.CustomTextEditorProvider { _token: vscode.CancellationToken ): void | Thenable { this.webviewPanel = webviewPanel; + trackWebviewPanel(webviewPanel, StartupFlowProvider.viewType, "StartupFlowProvider.resolveCustomTextEditor"); this.disposables.push(webviewPanel); // Set options before content diff --git a/src/providers/VideoEditor/VideoEditorProvider.ts b/src/providers/VideoEditor/VideoEditorProvider.ts index c505dd227..5af715beb 100644 --- a/src/providers/VideoEditor/VideoEditorProvider.ts +++ b/src/providers/VideoEditor/VideoEditorProvider.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; export class VideoEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "codex.videoEditor"; @@ -10,6 +11,7 @@ export class VideoEditorProvider implements vscode.CustomTextEditorProvider { webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise { + trackWebviewPanel(webviewPanel, VideoEditorProvider.viewType, "VideoEditorProvider.resolveCustomTextEditor"); // Set the HTML content for the webview webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview); diff --git a/src/providers/VideoPlayer/VideoPlayerProvider.ts b/src/providers/VideoPlayer/VideoPlayerProvider.ts index 31513e93b..598431df4 100644 --- a/src/providers/VideoPlayer/VideoPlayerProvider.ts +++ b/src/providers/VideoPlayer/VideoPlayerProvider.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { getWebviewHtml } from "../../utils/webviewTemplate"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; export class VideoPlayerProvider implements vscode.TextDocumentContentProvider, vscode.CustomTextEditorProvider { @@ -30,6 +31,7 @@ export class VideoPlayerProvider webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise { + trackWebviewPanel(webviewPanel, VideoPlayerProvider.viewType, "VideoPlayerProvider.resolveCustomTextEditor"); webviewPanel.webview.options = { enableScripts: true, localResourceRoots: [this.context.extensionUri], diff --git a/src/providers/WelcomeView/welcomeViewProvider.ts b/src/providers/WelcomeView/welcomeViewProvider.ts index b345c51ce..3d178708b 100644 --- a/src/providers/WelcomeView/welcomeViewProvider.ts +++ b/src/providers/WelcomeView/welcomeViewProvider.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { getAuthApi } from "../../extension"; import { safePostMessageToPanel } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; import { StartupFlowGlobalState } from "../StartupFlow/StartupFlowProvider"; const DEBUG_MODE = false; @@ -278,6 +279,7 @@ export class WelcomeViewProvider { retainContextWhenHidden: true, } ); + trackWebviewPanel(this._panel, WelcomeViewProvider.viewType, "WelcomeViewProvider.show"); // Set the webview's html content this._panel.webview.html = this._getHtmlForWebview(); diff --git a/src/providers/WordsView/WordsViewProvider.ts b/src/providers/WordsView/WordsViewProvider.ts index 792c8bb01..e58373ad0 100644 --- a/src/providers/WordsView/WordsViewProvider.ts +++ b/src/providers/WordsView/WordsViewProvider.ts @@ -7,6 +7,7 @@ import { } from "../../activationHelpers/contextAware/contentIndexes/indexes/wordsIndex"; import { readSourceAndTargetFiles } from "../../activationHelpers/contextAware/contentIndexes/indexes/fileReaders"; import { safePostMessageToPanel } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; export class WordsViewProvider implements vscode.Disposable { public static readonly viewType = "frontier.wordsView"; @@ -43,6 +44,7 @@ export class WordsViewProvider implements vscode.Disposable { retainContextWhenHidden: true, } ); + trackWebviewPanel(this._panel, WordsViewProvider.viewType, "WordsViewProvider.show"); this._panel.onDidDispose(() => { this._panel = undefined; diff --git a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts index 1a08283c8..1d40d3cc6 100644 --- a/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts +++ b/src/providers/codexCellEditorProvider/codexCellEditorProvider.ts @@ -28,6 +28,7 @@ import { SyncManager } from "../../projectManager/syncManager"; import bibleData from "../../../webviews/codex-webviews/src/assets/bible-books-lookup.json"; import { getNonce } from "../dictionaryTable/utilities/getNonce"; import { safePostMessageToPanel } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; import path from "path"; import * as fs from "fs"; import { getAuthApi } from "@/extension"; @@ -488,6 +489,7 @@ export class CodexCellEditorProvider implements vscode.CustomEditorProvider { debug("Resolving custom editor for:", document.uri.toString()); + trackWebviewPanel(webviewPanel, CodexCellEditorProvider.viewType, "CodexCellEditorProvider.resolveCustomEditor"); // Store the webview panel with its document URI as the key this.webviewPanels.set(document.uri.toString(), webviewPanel); diff --git a/src/providers/dictionaryTable/DictionaryEditorProvider.ts b/src/providers/dictionaryTable/DictionaryEditorProvider.ts index 980dd8f5d..089eb94f1 100644 --- a/src/providers/dictionaryTable/DictionaryEditorProvider.ts +++ b/src/providers/dictionaryTable/DictionaryEditorProvider.ts @@ -18,6 +18,7 @@ import { deleteWord, updateWord, } from "../../sqldb"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; type FetchPageResult = { entries: DictionaryEntry[]; @@ -60,6 +61,7 @@ export class DictionaryEditorProvider implements vscode.CustomTextEditorProvider webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise { + trackWebviewPanel(webviewPanel, DictionaryEditorProvider.viewType, "DictionaryEditorProvider.resolveCustomTextEditor"); this.document = await this.handleFetchPage(this.page, this.pageSize, this.searchQuery); webviewPanel.webview.options = { enableScripts: true, diff --git a/src/providers/publishProjectView/PublishProjectView.ts b/src/providers/publishProjectView/PublishProjectView.ts index 6c012072f..9feff24b9 100644 --- a/src/providers/publishProjectView/PublishProjectView.ts +++ b/src/providers/publishProjectView/PublishProjectView.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { getWebviewHtml } from "../../utils/webviewTemplate"; import { safePostMessageToPanel } from "../../utils/webviewUtils"; +import { trackWebviewPanel } from "../../utils/webviewTracker"; import { GlobalProvider } from "../../globalProvider"; import { getAuthApi } from "../../extension"; import { updateProjectSettings, updateMetadataFile } from "../../projectManager/utils/projectUtils"; @@ -253,6 +254,7 @@ export class PublishProjectView { ], } ); + trackWebviewPanel(panel, "frontierPublishProject", "PublishProjectView.createOrShow"); PublishProjectView.currentPanel = new PublishProjectView( panel, diff --git a/src/test/suite/integration/cellLabelImporter.integration.test.ts b/src/test/suite/integration/cellLabelImporter.integration.test.ts new file mode 100644 index 000000000..d600944f3 --- /dev/null +++ b/src/test/suite/integration/cellLabelImporter.integration.test.ts @@ -0,0 +1,87 @@ +import * as assert from "assert"; +import * as vscode from "vscode"; +import { matchCellLabels } from "../../../cellLabelImporter/matcher"; +import type { FileData } from "../../../cellLabelImporter/types"; + +suite("Cell Label Importer Integration", () => { + test("matches using mapped start time column to metadata.data.startTime", async () => { + const sourceFiles: FileData[] = [ + { + uri: vscode.Uri.file("/tmp/sample.codex"), + cells: [ + { + value: "Line 1\n", + metadata: { + id: "uuid-1", + data: { startTime: 10.5, endTime: 11.2 }, + cellLabel: "OLD", + }, + }, + ], + }, + ]; + + const importedRows = [ + { + LABEL: "NEW_LABEL", + BEGIN_TS: "00:00:10.500", + }, + ]; + + const result = await matchCellLabels( + importedRows, + sourceFiles, + [], + "LABEL", + { + matchColumn: "BEGIN_TS", + matchFieldPath: "metadata.data.startTime", + } + ); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].matched, true); + assert.strictEqual(result[0].cellId, "uuid-1"); + assert.strictEqual(result[0].newLabel, "NEW_LABEL"); + }); + + test("matches using mapped column to metadata.id string", async () => { + const sourceFiles: FileData[] = [ + { + uri: vscode.Uri.file("/tmp/sample-2.codex"), + cells: [ + { + value: "Line 2\n", + metadata: { + id: "uuid-2", + data: { startTime: 20.1, endTime: 21.2 }, + }, + }, + ], + }, + ]; + + const importedRows = [ + { + LABEL: "NEW_LABEL_2", + CELL_ID: "uuid-2", + }, + ]; + + const result = await matchCellLabels( + importedRows, + sourceFiles, + [], + "LABEL", + { + matchColumn: "CELL_ID", + matchFieldPath: "metadata.id", + } + ); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].matched, true); + assert.strictEqual(result[0].cellId, "uuid-2"); + assert.strictEqual(result[0].newLabel, "NEW_LABEL_2"); + }); +}); diff --git a/src/utils/webviewTracker.ts b/src/utils/webviewTracker.ts new file mode 100644 index 000000000..e15d4f94e --- /dev/null +++ b/src/utils/webviewTracker.ts @@ -0,0 +1,93 @@ +import * as vscode from "vscode"; + +type WebviewKind = "panel" | "view"; + +type WebviewEntry = { + id: string; + kind: WebviewKind; + viewType: string; + title?: string; + createdAt: number; + source?: string; +}; + +const entries = new Map(); +let counter = 0; +let outputChannel: vscode.OutputChannel | undefined; + +function getOutputChannel(): vscode.OutputChannel { + if (!outputChannel) { + outputChannel = vscode.window.createOutputChannel("Codex Webviews"); + } + return outputChannel; +} + +function log(line: string): void { + getOutputChannel().appendLine(line); +} + +export function trackWebviewPanel( + panel: vscode.WebviewPanel, + viewType: string, + source?: string +): string { + const id = `${viewType}:${++counter}`; + const entry: WebviewEntry = { + id, + kind: "panel", + viewType, + title: panel.title, + createdAt: Date.now(), + source, + }; + entries.set(id, entry); + log(`[create][panel] ${entry.viewType} id=${id} title="${entry.title}" source=${source ?? "unknown"}`); + panel.onDidDispose(() => { + entries.delete(id); + log(`[dispose][panel] ${entry.viewType} id=${id}`); + }); + return id; +} + +export function trackWebviewView( + view: vscode.WebviewView, + viewType: string, + source?: string +): string { + const id = `${viewType}:${++counter}`; + const entry: WebviewEntry = { + id, + kind: "view", + viewType, + createdAt: Date.now(), + source, + }; + entries.set(id, entry); + log(`[create][view] ${entry.viewType} id=${id} source=${source ?? "unknown"}`); + view.onDidDispose(() => { + entries.delete(id); + log(`[dispose][view] ${entry.viewType} id=${id}`); + }); + return id; +} + +export function dumpActiveWebviews(): void { + const channel = getOutputChannel(); + channel.appendLine("---- Active webviews ----"); + if (entries.size === 0) { + channel.appendLine("(none)"); + channel.show(true); + return; + } + const sorted = Array.from(entries.values()).sort((a, b) => a.createdAt - b.createdAt); + for (const entry of sorted) { + const ageMs = Date.now() - entry.createdAt; + const ageSec = Math.round(ageMs / 1000); + channel.appendLine( + `[active][${entry.kind}] ${entry.viewType} id=${entry.id} age=${ageSec}s` + + (entry.title ? ` title="${entry.title}"` : "") + + (entry.source ? ` source=${entry.source}` : "") + ); + } + channel.show(true); +}