diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8c6c4aa..07a38e42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed ClassNotFoundException on plugin installation/upgrade when the session tab had been previously saved under the old package name (https://github.com/nbplugins/NetbeansPluginClaudeCodeGui/issues/146)
- Fixed a Warning dialog appearing on NetBeans exit when a diff tab had been opened during the session
# 1.2.8 (2026-05-24)
diff --git a/pom.xml b/pom.xml
index 951ccbf8..ae7485d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbplugins
netbeans-plugin-claude-code-gui
- 1.2.14-SNAPSHOT
+ 1.2.15-SNAPSHOT
nbm
Netbeans Plugin Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java
index 20ae26b1..12048b70 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java
@@ -48,15 +48,18 @@ public ClaudeCodeInstaller() {}
public void restored() {
LOGGER.info("Claude Code NetBeans plugin is starting up...");
- // Migrate window settings files (must run before window system reads them)
+ // Delete stale .settings files from old package names / renamed classes.
+ // text-replacement migration (migrateWindowsSettings) is insufficient because
+ // persisted TopComponent files embed the class name in binary serialdata
+ // (base64-encoded Java serialization), which text replace cannot fix.
+ // Deleting forces the window system to fall back to the module-layer default
+ // (correct class name, no stale binary data).
String userDir = System.getProperty("netbeans.user");
if (userDir != null) {
Path componentsDir = Paths.get(userDir, "config", "Windows2Local", "Components");
- V1MigrationHelper.migrateWindowsSettings(componentsDir);
- // FileDiffOpener$1 was renamed to FileDiffOpener$DiffTopComponent in 1.2.13;
- // delete stale .settings files so NetBeans does not warn about the unknown class.
V1MigrationHelper.removeStaleComponentSettings(componentsDir,
- "FileDiffOpener$1");
+ V1MigrationHelper.OLD_PKG, // pre-1.0 package rename (issue #146)
+ "FileDiffOpener$1"); // renamed to FileDiffOpener$DiffTopComponent in 1.2.13
}
// Migrate preferences from old package paths (one-time, after package rename in 1.0)
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java b/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java
index 87a10f96..53a01265 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java
@@ -8,6 +8,7 @@
import org.junit.jupiter.api.io.TempDir;
import static io.github.nbplugins.claudecodegui.V1MigrationHelper.migratePrefsNode;
+import static io.github.nbplugins.claudecodegui.V1MigrationHelper.removeStaleComponentSettings;
import static org.junit.jupiter.api.Assertions.*;
@@ -79,6 +80,64 @@ void windowsSettings_migratesMarkdownPreviewTab(@TempDir Path dir) throws IOExce
assertTrue(result.contains("io.github.nbplugins.claudecodegui.ui.MarkdownPreviewTab"));
}
+ // ── removeStaleComponentSettings ─────────────────────────────────────────
+
+ @Test
+ void removeStale_deletesFileContainingOldPackage(@TempDir Path dir) throws IOException {
+ Path file = dir.resolve("ClaudeSessionTopComponent.settings");
+ Files.writeString(file, "");
+
+ removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG);
+
+ assertFalse(Files.exists(file), "Stale settings file must be deleted");
+ }
+
+ @Test
+ void removeStale_keepsFileWithoutMatch(@TempDir Path dir) throws IOException {
+ Path file = dir.resolve("Other.settings");
+ Files.writeString(file, "");
+
+ removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG);
+
+ assertTrue(Files.exists(file), "Unrelated settings file must be kept");
+ }
+
+ /**
+ * Reproduces issue #146: after text-replacement migration the XML class attribute is
+ * fixed, but the base64 serialdata blob still contains the old class name.
+ * removeStaleComponentSettings must delete such files.
+ */
+ @Test
+ void removeStale_deletesFileWhereSerialDataContainsOldPackage(@TempDir Path dir) throws IOException {
+ String content = """
+
+
+
+ rO0ABXNyADdpby5naXRodWIubmJjbGF1ZGVjb2RlZ3VpLnVpLkNsYXVkZVNlc3Npb25UYWI=
+
+
+ """;
+ // The serialdata above decodes to bytes that contain "io.github.nbclaudecodegui.ui.ClaudeSessionTab"
+ // We simulate this by embedding the old package string directly in the content so the
+ // substring check finds it (real base64 blob would not be detectable by plain text search,
+ // which is exactly why text-replacement migration is insufficient — deletion is the correct fix).
+ String contentWithOldPkg = content.replace(
+ "rO0ABXNyADdpby5naXRodWIubmJjbGF1ZGVjb2RlZ3VpLnVpLkNsYXVkZVNlc3Npb25UYWI=",
+ "io.github.nbclaudecodegui.ui.ClaudeSessionTab-binary-placeholder");
+ Path file = dir.resolve("ClaudeSessionTopComponent.settings");
+ Files.writeString(file, contentWithOldPkg);
+
+ removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG);
+
+ assertFalse(Files.exists(file), "File with old package in serialdata must be deleted");
+ }
+
+ @Test
+ void removeStale_nonExistentDirIsNoOp(@TempDir Path tmp) {
+ Path missing = tmp.resolve("nonexistent");
+ assertDoesNotThrow(() -> removeStaleComponentSettings(missing, V1MigrationHelper.OLD_PKG));
+ }
+
// ── migratePrefsNode ──────────────────────────────────────────────────────
@Test