Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.22.34-SNAPSHOT</version>
<version>0.22.39-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Claude Code GUI</name>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
<url>https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui</url>

Expand Down
85 changes: 85 additions & 0 deletions src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -1386,9 +1411,69 @@ private List<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading