diff --git a/CHANGELOG.md b/CHANGELOG.md index ce97ff50..0b07065c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed session tab showing the old project display name after IDE restart (tab now refreshes its name once all open projects finish loading) - Fixed model discovery failing when Enter was absorbed as a newline in the prompt instead of submitting the /model command (race condition under load) # 0.22.24 (2026-04-29) diff --git a/pom.xml b/pom.xml index 34903f32..a9243be5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,9 +6,9 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.34-SNAPSHOT + 0.22.39-SNAPSHOT nbm - Netbeans Claude Code GUI + Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java index e6d7ad64..cc407472 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -44,7 +44,16 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; +import java.beans.PropertyChangeListener; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.netbeans.api.options.OptionsDisplayer; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ui.OpenProjects; +import org.openide.filesystems.FileUtil; +import org.openide.util.RequestProcessor; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; @@ -201,6 +210,15 @@ public class ClaudeSessionTab extends TopComponent /** Guards programmatic combo updates to prevent feedback through the action listener. */ private boolean updatingCombo = false; + /** Set to true when the tab is closed, to suppress deferred display-name updates. */ + private volatile boolean tabClosed = false; + + /** Direct ProjectInformation (not proxy) for the current session dir; used to listen for name changes. */ + private ProjectInformation trackedProjectInfo; + private PropertyChangeListener projectNameListener; + /** Counter to cancel superseded polling rounds when updateDisplayName is called again. */ + private final AtomicInteger displayNameGeneration = new AtomicInteger(0); + // ------------------------------------------------------------------------- // Constructors // ------------------------------------------------------------------------- @@ -404,6 +422,13 @@ protected void componentClosed() { try { pendingDiffOnClose.run(); } catch (Exception ex) { /* ignore */ } pendingDiffOnClose = null; } + tabClosed = true; + displayNameGeneration.incrementAndGet(); // cancel pending polls + if (trackedProjectInfo != null && projectNameListener != null) { + trackedProjectInfo.removePropertyChangeListener(projectNameListener); + trackedProjectInfo = null; + projectNameListener = null; + } stopProcess(); } @@ -1386,9 +1411,69 @@ private List getScreenLines() { } } + private static final RequestProcessor DISPLAY_NAME_RP = + new RequestProcessor("ClaudeSessionTab-displayName", 1, true); + private void updateDisplayName(File dir) { + // Cancel any previous generation of polling / listeners. + int gen = displayNameGeneration.incrementAndGet(); + if (trackedProjectInfo != null && projectNameListener != null) { + trackedProjectInfo.removePropertyChangeListener(projectNameListener); + trackedProjectInfo = null; + projectNameListener = null; + } + String name = ClaudeSessionSelectorPanel.resolveTabLabel(dir); setDisplayName(name); setToolTipText(dir != null ? dir.getAbsolutePath() : "New Claude Code session"); + + if (dir == null) return; + + // Subscribe directly to the ProjectInformation from the project's own Lookup + // (bypassing the proxy returned by ProjectUtils.getInformation) so we hear + // PROP_DISPLAY_NAME when NbMavenProject reloads the pom.xml from disk. + for (Project p : OpenProjects.getDefault().getOpenProjects()) { + File projDir = FileUtil.toFile(p.getProjectDirectory()); + if (dir.equals(projDir)) { + ProjectInformation pi = p.getLookup().lookup(ProjectInformation.class); + if (pi != null) { + trackedProjectInfo = pi; + projectNameListener = evt -> { + if (ProjectInformation.PROP_DISPLAY_NAME.equals(evt.getPropertyName()) + && !tabClosed) { + SwingUtilities.invokeLater(() -> { + if (!tabClosed) { + String fresh = ClaudeSessionSelectorPanel.resolveTabLabel(dir); + if (!fresh.equals(getDisplayName())) { + setDisplayName(fresh); + } + } + }); + } + }; + pi.addPropertyChangeListener(projectNameListener); + } + break; + } + } + + // Fallback: poll periodically (no time limit) in case PROP_DISPLAY_NAME is never fired. + // Maven projects load from cache initially; the real name may arrive minutes later. + schedulePoll(dir, gen); + } + + private void schedulePoll(File dir, int gen) { + DISPLAY_NAME_RP.schedule(() -> { + if (tabClosed || displayNameGeneration.get() != gen) return; + SwingUtilities.invokeLater(() -> { + if (tabClosed || displayNameGeneration.get() != gen) return; + String fresh = ClaudeSessionSelectorPanel.resolveTabLabel(dir); + if (!fresh.equals(getDisplayName())) { + setDisplayName(fresh); + LOG.fine("Tab name updated by poll: " + fresh); + } + }); + schedulePoll(dir, gen); + }, 15, TimeUnit.SECONDS); } } diff --git a/src/test/java/io/github/nbclaudecodegui/ui/ClaudeCodeTopComponentTest.java b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeCodeTopComponentTest.java index 282bafaa..7b274761 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/ClaudeCodeTopComponentTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeCodeTopComponentTest.java @@ -68,6 +68,24 @@ void testTabTooltipSetImmediatelyOnStartSession() throws IOException { } } + @Test + void testTabNameRemainsStableWhenProjectsAlreadyLoaded() throws IOException { + // When openProjects() Future is already done (projects fully loaded), + // updateDisplayName must not schedule a background refresh — the name is final. + // Verify this by checking that a tab created for a non-project dir keeps "Claude Code". + File tmpDir = Files.createTempDirectory("proj-loaded-test").toFile(); + tmpDir.deleteOnExit(); + try { + ClaudeSessionTab tc = new ClaudeSessionTab(); + tc.autoStart(tmpDir, "default"); + // openProjects() is done in test env (no real projects) — name must be "Claude Code" + assertEquals("Claude Code", tc.getDisplayName(), + "Tab name for non-project dir must be 'Claude Code'"); + } finally { + tmpDir.delete(); + } + } + @Test void testCanCloseWithNoActiveSession() { ClaudeSessionTab tc = new ClaudeSessionTab();