diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 08c39f6..f454fc9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -96,6 +96,9 @@ jobs: run: > ./gradlew test --tests "org.zoocode.jetbrains.terminal.RawOutputTtyConnectorTest" + --tests "org.zoocode.jetbrains.util.JcefSupportTest" + --tests "org.zoocode.jetbrains.descriptor.PluginDescriptorTest" + --tests "org.zoocode.jetbrains.terminal.TerminalPtyBuilderTest" --no-configuration-cache - name: Build the plugin diff --git a/.gitignore b/.gitignore index 18bd871..ffafd14 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,11 @@ deps/vscode/* jetbrains_plugin/plugins jetbrains_plugin/build .gradle +/.gradle-user-home/ deps/vsix/package/* deps/vsix/plugin/* -e # Roo build artifacts .zoo-build/ dist-with-roo/ -plans/ \ No newline at end of file +plans/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f550b04 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +### Terminal on Windows + +- The plugin requests ConPTY explicitly through the public pty4j setter. The JetBrains registry key `terminal.use.conpty.on.windows` is no longer read. +- If the ConPTY native library fails to load, the process falls back to winpty and the session continues. +- Other process start failures propagate to the terminal error handling. diff --git a/README.md b/README.md index 7c61409..9748cbe 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ The plugin targets IntelliJ Platform build **233** (JetBrains 2023.3) or newer. - Android Studio 2026.1.4 or newer when using Android Studio; install a preview build until this version reaches the stable channel - A JCEF-enabled IDE runtime, which Zoo Code requires to render its interface +Where JCEF support comes from depends on the IDE version. From IntelliJ Platform 2026.2 (build 262), JCEF ships as a separate bundled plugin. If that plugin is missing or disabled, Zoo Code shows a warning notification and the Zoo Code tool window cannot render. Enable the bundled JCEF plugin under **Settings β Plugins β Installed** and restart the IDE. On IDEs older than 2026.2, JCEF comes from the IDE runtime; see [Known Issues](docs/KNOWN_ISSUES.md). + On first launch the plugin automatically downloads a pinned, platform-specific Node.js runtime from [nodejs.org](https://nodejs.org) (checksum-verified) and uses it to run the extension host, so no local Node.js installation is required. If the download is unavailable, the plugin falls back to a bundled Node.js or a Node.js **20.6.0 or newer** found on your `PATH`. ### Building from source diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index a5593eb..9f9beb3 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -20,18 +20,18 @@ Android Studio's default startup runtime does not include JCEF capabilities, req 1. **Download a JCEF-Compatible JetBrains Runtime** Download a JetBrains Runtime (JBR) with JCEF support: - + π https://github.com/JetBrains/JetBrainsRuntime/releases - + Choose a release with: - `jbr_jcef` in the name - Correct architecture (osx-aarch64, linux-x64, or windows-x64) - + Example: ``` jbr_jcef-17.0.11-osx-aarch64-b1063.2.tar.gz ``` - + Unpack it somewhere, e.g.: ``` ~/jbr/jbr_jcef @@ -47,15 +47,15 @@ Android Studio's default startup runtime does not include JCEF capabilities, req 4. Select it and choose the folder where you extracted the JBR (e.g., ~/jbr/jbr_jcef). 3. **Restart Android Studio** - + After selecting the new runtime, Android Studio will prompt you to restart. Confirm, and it will relaunch with the new runtime. 4. **Verify the Runtime** - + Go to Help β About to confirm the IDE is now running with the new JBR. The version should indicate JCEF support (e.g., JetBrains Runtime jbr-17.0.11+7-b1238.56-jcef). 5. **Revert if Needed** - + You can always use the same "Choose Boot Java Runtime for the IDE" action to switch back to the default runtime if needed. ##### Method 2: Using Edit Custom VM Options @@ -91,6 +91,27 @@ On Linux ARM platforms, JCEF may fail to initialize native processes properly, l #### Solutions No solution available at this time. +### 1.3 JCEF Is Not Available on IDE 2026.2 or Newer + +#### Problem Description + +From IntelliJ Platform 2026.2 (build 262), JetBrains packages JCEF as a separate bundled plugin with the id `com.intellij.modules.jcef`. JCEF is no longer part of the IDE platform. When this bundled plugin is not present, the Zoo Code view cannot render. + +#### Behavior + +The plugin does not crash. It logs the reason and shows a warning notification. The Zoo Code tool window stays empty. The IDE log shows the state in the startup line `JCEF: CLASSES_MISSING`. + +#### Affected Platforms +- IDEs based on IntelliJ Platform build 262 or newer without the bundled JCEF plugin + +#### Solutions + +1. Open **Settings β Plugins β Installed** and search for **JCEF**. +2. Enable the bundled JCEF plugin if it is disabled. +3. Restart the IDE. + +If the bundled JCEF plugin is not in the list, this IDE build does not ship JCEF, and the Zoo Code view cannot run in it. IDEs older than 2026.2 get JCEF from the runtime; see section 1.1. + --- ## 2. Node Related Issues diff --git a/jetbrains_plugin/build.gradle.kts b/jetbrains_plugin/build.gradle.kts index 809ff92..d58013b 100644 --- a/jetbrains_plugin/build.gradle.kts +++ b/jetbrains_plugin/build.gradle.kts @@ -154,7 +154,7 @@ fun Sync.prepareSandbox() { } depfile.readLines().let { it.forEach { line -> - depPatterns.add(line.substringAfterLast("node_modules/") + "/**") + depPatterns.add(line.replace('\\', '/').substringAfterLast("node_modules/") + "/**") } } @@ -363,17 +363,29 @@ tasks { // linked against the 233 platform jars already on the compile classpath; this task // never needs the 2026.3 IDE. The scan target is the instrumented jar in the // sandbox, the exact bytes that the Plugin Verifier checks and that the published - // zip packages. The marker output makes the task up-to-date when the sandbox and - // the checker are unchanged. + // zip packages. The task first runs the checker --self-test, which feeds it + // synthetic clean and banned jars, so a scanner regression fails this task. The + // marker output makes the task up-to-date when the sandbox and the checker are + // unchanged. register("verifyPackagedBytecode") { group = "verification" - description = "Reject packaged references to terminal APIs removed in IntelliJ 2026.3" + description = + "Reject packaged references to terminal APIs removed in IntelliJ 2026.3 and to internal LocalPtyOptions API" val checker = layout.projectDirectory.file("smoke/Smoke263DispatchCheck.java") inputs.files(prepareSandbox.map { it.destinationDir }) inputs.file(checker) outputs.file(layout.buildDirectory.file("verification/packaged-bytecode.ok")) dependsOn(prepareSandbox) doLast { + project.exec { + commandLine( + verificationJavaLauncher.get().executablePath.asFile.absolutePath, + "-cp", + configurations.getByName("compileClasspath").asPath, + checker.asFile.absolutePath, + "--self-test", + ) + } val pluginJar = packagedPluginJar() project.exec { commandLine( diff --git a/jetbrains_plugin/smoke/PowerShellIntegrationCheck.ps1 b/jetbrains_plugin/smoke/PowerShellIntegrationCheck.ps1 new file mode 100644 index 0000000..0b91f6c --- /dev/null +++ b/jetbrains_plugin/smoke/PowerShellIntegrationCheck.ps1 @@ -0,0 +1,47 @@ +$ErrorActionPreference = 'Stop' +$profilePath = Join-Path $PSScriptRoot '../src/main/resources/run-vs-agent-shell-integrations/vscode-powershell/profile.ps1' +$tokens = $null +$parseErrors = $null +[System.Management.Automation.Language.Parser]::ParseFile($profilePath, [ref]$tokens, [ref]$parseErrors) | Out-Null +if ($parseErrors.Count) { throw ($parseErrors.Message -join '; ') } +if ([IO.File]::ReadAllText($profilePath) -match '[^\x00-\x7F]') { throw 'Profile must remain ASCII for Windows PowerShell 5.1.' } + +# Mock only console input/history; load the real profile in a child script scope. +Import-Module PSReadLine +$global:testHistoryId = 1 +function global:Get-History { [pscustomobject]@{ Id = $global:testHistoryId } } +function global:PSConsoleHostReadLine { 'Write-Output "hello;world"' } +$env:VSCODE_NONCE = 'test-nonce' +$originalConsole = [Console]::Out +$capture = New-Object IO.StringWriter +try { + [Console]::SetOut($capture) + & $profilePath + $unicodeValue = 'caf' + [char]0xe9 + ' ' + [char]0x65e5 + [char]0x672c + [char]0x8a9e + ' ' + [char]::ConvertFromUtf32(0x1f680) + $escapedUnicode = __VSCode-Escape-Value $unicodeValue + if ($escapedUnicode -match '[^\x00-\x7F]') { throw 'Unicode marker is not ASCII-safe.' } + if ($escapedUnicode -ne 'caf\xc3\xa9 \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e \xf0\x9f\x9a\x80') { throw 'Incorrect UTF-8 Unicode escapes.' } + $initialPrompt = prompt + $command = PSConsoleHostReadLine + $global:testHistoryId = 2 + $finishedPrompt = prompt + $null = PSConsoleHostReadLine + $global:testHistoryId = 3 + cmd.exe /c exit 7 + $failedPrompt = prompt + $null = PSConsoleHostReadLine + $global:testHistoryId = 4 + Write-Output 'alive' | Out-Null + $recoveredPrompt = prompt +} finally { [Console]::SetOut($originalConsole) } +$esc = [char]27 +$bell = [char]7 +$markers = $capture.ToString() +if ($command -ne 'Write-Output "hello;world"') { throw 'ReadLine did not preserve the command.' } +if (-not $markers.Contains("${esc}]633;E;Write-Output `"hello\x3bworld`";test-nonce${bell}")) { throw 'Missing command/nonce marker after script scope exited.' } +if (-not $markers.Contains("${esc}]633;C${bell}")) { throw 'Missing execution start marker.' } +if (-not $initialPrompt.Contains("${esc}]633;P;Cwd=")) { throw 'Missing working directory marker.' } +if (-not $finishedPrompt.Contains("${esc}]633;D;0${bell}")) { throw 'Missing successful completion marker.' } +if (-not $failedPrompt.Contains("${esc}]633;D;7${bell}")) { throw 'Native exit code 7 was not preserved.' } +if (-not $recoveredPrompt.Contains("${esc}]633;D;0${bell}")) { throw 'Stale native exit code reused for successful command.' } +'PASS: Windows PowerShell parsing, script-scope lifetime, command escaping, nonce, start, cwd and completion markers.' diff --git a/jetbrains_plugin/smoke/Smoke263DispatchCheck.java b/jetbrains_plugin/smoke/Smoke263DispatchCheck.java index 1a59340..e03ff41 100644 --- a/jetbrains_plugin/smoke/Smoke263DispatchCheck.java +++ b/jetbrains_plugin/smoke/Smoke263DispatchCheck.java @@ -3,15 +3,18 @@ import java.lang.reflect.Proxy; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; /** * Headless proof for the 2026.3 terminal compatibility, run by the @@ -29,7 +32,8 @@ * valid override; it only cannot itself be overridden, which nothing * needs to do, *
β οΈ JCEF Not Supported
") append("Please use a JCEF-enabled runtime.
") @@ -211,7 +211,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { val osVersion = System.getProperty("os.version") val osArch = System.getProperty("os.arch") val javaVersion = System.getProperty("java.version") - val jcefSupported = JBCefApp.isSupported() + val jcefRenderable = JcefSupport.current().isRenderable // Check for Linux ARM system val isLinuxArm = osName.lowercase().contains("linux") && (osArch.lowercase().contains("aarch64") || osArch.lowercase().contains("arm")) @@ -225,14 +225,14 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { append(" IDE: ${appInfo.fullApplicationName} (build ${appInfo.build})\n") append(" Plugin: v$pluginVersion\n") append(" Java: $javaVersion\n") - append(" JCEF Support: ${if (jcefSupported) "Yes" else "No"}\n") + append(" JCEF Support: ${if (jcefRenderable) "Yes" else "No"}\n") // Add warning messages if (isLinuxArm) { append("\nWarning: Linux ARM systems are currently not supported.\n") } - if (!jcefSupported) { + if (!jcefRenderable) { append("\nError: Your IDE runtime does not support JCEF. Please use a runtime with JCEF support.\n") } } @@ -274,7 +274,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { // Check configuration status and show appropriate content if (configManager.isConfigurationLoaded() && configManager.isConfigurationValid()) { - + val initPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(20, 20, 20, 20) add(placeholderLabel, BorderLayout.CENTER) @@ -301,10 +301,10 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { add(buttonPanel, BorderLayout.SOUTH) } contentPanel.add(initPanel, BorderLayout.CENTER) - + // Don't auto-start here - WecoderPlugin will handle startup // The plugin will start automatically if configuration is valid - + // Start configuration monitoring startConfigurationMonitoring() @@ -528,15 +528,15 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { showDebugInfo() } } - + buttonPanel.add(debugButton, BorderLayout.WEST) - + // Add all components panel.add(titleLabel, BorderLayout.NORTH) panel.add(descLabel, BorderLayout.CENTER) panel.add(pluginListPanel, BorderLayout.CENTER) panel.add(buttonPanel, BorderLayout.SOUTH) - + return panel } @@ -556,7 +556,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { val extensionId = provider.getExtensionId() val isCurrent = provider.getExtensionId() == currentExtensionId val isAvailable = provider.isAvailable(project) - + PluginInfo( id = extensionId, displayName = provider.getDisplayName(), @@ -580,12 +580,12 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { */ private fun createPluginRow(pluginInfo: PluginInfo): JPanel { val rowPanel = JPanel(BorderLayout()) - + // Main content panel - use default IDE styling val contentPanel = JPanel(BorderLayout()).apply { // Use default panel background from IDE theme isOpaque = true - + // Simple border without custom colors val borderWidth = if (pluginInfo.isCurrent) 2 else 1 border = BorderFactory.createCompoundBorder( @@ -630,7 +630,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { font = JBFont.label() isEnabled = true isFocusPainted = false - + addActionListener { uploadVsixForPlugin(pluginInfo.id, pluginInfo.displayName) } @@ -671,7 +671,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { applyPluginSelection(pluginInfo.id) } } - + override fun mouseEntered(e: java.awt.event.MouseEvent) { contentPanel.cursor = java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR) // Simple hover border effect using IDE theme colors @@ -681,7 +681,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { ) contentPanel.repaint() } - + override fun mouseExited(e: java.awt.event.MouseEvent) { contentPanel.cursor = java.awt.Cursor.getDefaultCursor() // Restore normal border @@ -730,7 +730,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { try { // Use VsixUploadDialog directly val success = VsixUploadDialog.show(project, pluginId, pluginName) - + if (success) { javax.swing.JOptionPane.showMessageDialog( contentPanel, @@ -749,7 +749,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { ) } } - + /** * Create configuration status panel */ @@ -757,33 +757,33 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { val panel = JPanel() panel.layout = BorderLayout() panel.border = javax.swing.BorderFactory.createEmptyBorder(20, 20, 20, 20) - + // Status label val statusLabel = JLabel().apply { font = font.deriveFont(14f) horizontalAlignment = javax.swing.SwingConstants.CENTER } - + // Update status updateConfigStatus(statusLabel) - + panel.add(statusLabel, BorderLayout.CENTER) return panel } - + /** * Update configuration status */ private fun updateConfigStatus(statusLabel: JLabel) { // Detect current theme for status colors val isDarkTheme = detectCurrentTheme() - + if (configManager.isConfigurationLoaded()) { if (configManager.isConfigurationValid()) { val extensionId = configManager.getCurrentExtensionId() // Check if plugin is actually running val isPluginRunning = isPluginActuallyRunning() - + if (isPluginRunning) { statusLabel.text = "β Plugin Running - Current Plugin: $extensionId" statusLabel.foreground = getThemeAdaptiveColor(isDarkTheme, "success") @@ -814,22 +814,22 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { else -> javax.swing.UIManager.getColor("Label.foreground") ?: java.awt.Color(128, 128, 128) } } - + /** * Apply plugin selection and create configuration */ private fun applyPluginSelection(pluginId: String) { try { logger.info("Applying plugin selection: $pluginId") - + // Create configuration with selected plugin configManager.setCurrentExtensionId(pluginId) - + // Verify configuration was saved successfully if (configManager.isConfigurationValid()) { // Start the plugin directly instead of just saving configuration startPluginAfterSelection(pluginId) - + logger.info("Plugin selection applied successfully: $pluginId") } else { // Configuration is still invalid after setting @@ -841,7 +841,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { "Configuration Update Failed", javax.swing.JOptionPane.ERROR_MESSAGE ) - + logger.error("Configuration is still invalid after setting extension ID: $pluginId, error: $errorMsg") } } catch (e: Exception) { @@ -855,40 +855,40 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { ) } } - + /** * Start plugin after plugin selection */ private fun startPluginAfterSelection(pluginId: String) { try { logger.info("Starting plugin after selection: $pluginId") - + // Set plugin starting state isPluginStarting = true - + // Update status to show plugin is starting updateConfigStatus(configStatusPanel.getComponent(0) as JLabel) - + // Get extension manager and set the selected provider val extensionManager = ExtensionManager.getInstance(project) extensionManager.initialize(pluginId) - + // Initialize the current provider extensionManager.initializeCurrentProvider() - + // Start plugin service val pluginService = WecoderPlugin.getInstance(project) pluginService.initialize(project) - + // Initialize WebViewManager val webViewManager = project.getService(WebViewManager::class.java) if (webViewManager != null) { // Register to project Disposer com.intellij.openapi.util.Disposer.register(project, webViewManager) - + // Start configuration monitoring startConfigurationMonitoring() - + // Register project-level resource disposal com.intellij.openapi.util.Disposer.register(project, com.intellij.openapi.Disposable { logger.info("Disposing Zoo Code JetBrains plugin for project: ${project.name}") @@ -899,26 +899,26 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { isPluginRunning = false isPluginStarting = false }) - + logger.info("Plugin started successfully after selection: $pluginId") - + // Set plugin running state isPluginRunning = true isPluginStarting = false - + // Update UI to show plugin is running updateUIContent() } else { logger.error("WebViewManager not available") throw IllegalStateException("WebViewManager not available") } - + } catch (e: Exception) { logger.error("Failed to start plugin after selection", e) // Reset state on failure isPluginStarting = false isPluginRunning = false - + val message = "β Plugin Startup Failed\nError: ${e.message}\n\nPlease check plugin configuration or try restarting the IDE." javax.swing.JOptionPane.showMessageDialog( contentPanel, @@ -928,7 +928,7 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { ) } } - + /** * Update UI content based on configuration status */ @@ -938,20 +938,20 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { logger.info("Plugin is starting or running, skipping UI update") return } - + // Check if plugin is actually running val isPluginRunning = isPluginActuallyRunning() - + // If plugin is running, don't change the main UI content if (isPluginRunning) { logger.info("Plugin is running, keeping current UI content") return } - + contentPanel.removeAll() - + // Always show system info panel (Zoo Code is always configured) - + val initPanel = JPanel(BorderLayout()).apply { border = BorderFactory.createEmptyBorder(20, 20, 20, 20) add(placeholderLabel, BorderLayout.CENTER) @@ -959,33 +959,33 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { } contentPanel.add(initPanel, BorderLayout.CENTER) logger.info("Showing system info panel - Zoo Code auto-configured") - + contentPanel.revalidate() contentPanel.repaint() } - - + + /** * Show manual configuration instructions */ private fun showManualConfigInstructions() { val instructions = """ π Manual Configuration Instructions - + 1. Create configuration file in user home directory: ${PluginConstants.ConfigFiles.getMainConfigPath()} 2. Add the following content: ${PluginConstants.ConfigFiles.EXTENSION_TYPE_KEY}=zoo-code - + 3. Supported plugin types: - zoo-code: Zoo Code AI Assistant - cline: Cline AI Assistant - custom: Custom Plugin - + 4. Save the file and restart IDE - + Configuration file path: ${configManager.getConfigurationFilePath()} """.trimIndent() - + javax.swing.JOptionPane.showMessageDialog( contentPanel, instructions, @@ -1001,18 +1001,18 @@ class ZooCodeJetBrainsToolWindowFactory : ToolWindowFactory { val debugText = """ Zoo Code Debug Information ========================== - + π Plugin Status: ${if (configManager.isConfigurationLoaded() && configManager.isConfigurationValid()) "Loaded and Valid" else "Not Loaded or Invalid"} - + π Current Configuration: ${configManager.getCurrentExtensionId() ?: "Not Set"} - + βοΈ Configuration File Path: ${configManager.getConfigurationFilePath()} - + π Configuration Load Time: ${configManager.getConfigurationLoadTime()?.let { it.toString() } ?: "Unknown"} - + π‘ Tip: If configuration is invalid, please check the configuration file content or try manual configuration. """.trimIndent() - + javax.swing.JOptionPane.showMessageDialog( contentPanel, debugText, diff --git a/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/util/JcefSupport.kt b/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/util/JcefSupport.kt new file mode 100644 index 0000000..4443f4c --- /dev/null +++ b/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/util/JcefSupport.kt @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Weibo, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.zoocode.jetbrains.util + +import com.intellij.ui.jcef.JBCefApp + +/** + * Outcome of the JBCefApp availability probe. + * + * [AVAILABLE] means JBCefApp.isSupported() returned true, so JCEF views can + * render. [RUNTIME_UNAVAILABLE] means the JBCefApp class is present but the + * probe returned false, which a runtime without JCEF support causes. + * [CLASSES_MISSING] means loading JBCefApp itself failed with a LinkageError; + * on IDE builds 2026.2 (262) and newer that means the bundled + * com.intellij.modules.jcef plugin is absent. + */ +enum class JcefSupport { + AVAILABLE, + RUNTIME_UNAVAILABLE, + CLASSES_MISSING; + + /** Only [AVAILABLE] can render JCEF views. */ + val isRenderable: Boolean + get() = this == AVAILABLE + + companion object { + /** Runs the JBCefApp.isSupported() probe and maps the outcome. */ + fun current(): JcefSupport = classify { JBCefApp.isSupported() } + + /** + * Test seam; production code calls [current]. Without the JCEF plugin + * the probe fails with NoClassDefFoundError, so only LinkageError maps + * to [CLASSES_MISSING] and every other probe failure propagates. + */ + internal fun classify(isSupported: () -> Boolean): JcefSupport = + try { + if (isSupported()) AVAILABLE else RUNTIME_UNAVAILABLE + } catch (_: LinkageError) { + CLASSES_MISSING + } + } +} diff --git a/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/webview/WebViewManager.kt b/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/webview/WebViewManager.kt index 73178d5..e1dfebb 100644 --- a/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/webview/WebViewManager.kt +++ b/jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/webview/WebViewManager.kt @@ -17,6 +17,8 @@ import com.intellij.ui.jcef.JBCefBrowser import com.intellij.ui.jcef.JBCefJSQuery import org.zoocode.jetbrains.core.PluginContext import org.zoocode.jetbrains.core.ServiceProxyRegistry +import org.zoocode.jetbrains.util.JcefSupport +import org.zoocode.jetbrains.util.NotificationUtil import org.zoocode.jetbrains.events.WebviewHtmlUpdateData import org.zoocode.jetbrains.events.WebviewViewProviderData import org.zoocode.jetbrains.ipc.proxy.SerializableObjectWithBuffers @@ -66,20 +68,20 @@ class WebViewManager(var project: Project) : Disposable, ThemeChangeListener { // Latest created WebView instance @Volatile private var latestWebView: WebViewInstance? = null - + // Store WebView creation callbacks private val creationCallbacks = mutableListOf