From 2a2d7159fa1afd5904a38f67424627e6947e0a34 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Thu, 7 May 2026 18:46:21 +0300 Subject: [PATCH] Fixed "Continue last" and "Resume specific" staying enabled when no sessions exist When a directory or profile with no previous sessions is selected in the session selector, the "Continue last" and "Resume specific" radio buttons are now disabled and the selection falls back to "New session". When switching to a directory that has sessions, the radios re-enable and the preferred mode (from settings) is restored. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../ui/ClaudeSessionSelectorPanel.java | 1 + .../nbclaudecodegui/ui/SessionModePanel.java | 46 ++++- .../ui/SessionModePanelTest.java | 187 +++++++++++++++--- 5 files changed, 203 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e6c5fe8..d2709898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed "Continue last" and "Resume specific" staying enabled (and selected) when a directory or profile with no sessions is chosen in the session selector - Fixed model list showing garbled names (e.g. "Mtoket 4.6 with 1M context") when the /model menu includes 1M-context variants and cost annotations # 0.22.30 (2026-05-05) diff --git a/pom.xml b/pom.xml index 2356e96b..3ce8db4b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.47-SNAPSHOT + 0.22.48-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/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java index 6de68d64..abd0fb8d 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java @@ -403,6 +403,7 @@ private void reloadSessionModePanel() { resolveDir() != null ? resolveDir().toPath() : null, resolveClaudeConfigDir(), null); + sessionModePanel.setMode(ClaudeCodePreferences.getContextMenuSessionMode()); } } diff --git a/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java b/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java index 17852f8e..ab2535f4 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java @@ -199,6 +199,7 @@ public void mouseClicked(MouseEvent e) { }); refreshTable(); + updateHistoryRadiosEnabled(); } // ------------------------------------------------------------------------- @@ -235,6 +236,10 @@ public String getSelectedSessionId() { /** * Programmatically sets the selected mode. * + *

If the target radio is disabled (no sessions available), + * {@link SessionMode#CONTINUE_LAST} and {@link SessionMode#RESUME_SPECIFIC} + * fall back to {@link SessionMode#NEW}. + * * @param mode mode to select */ public void setMode(SessionMode mode) { @@ -242,8 +247,14 @@ public void setMode(SessionMode mode) { case CLOSE_ONLY -> closeOnlyRadio.setSelected(true); case RESTART_ADVANCED -> restartAdvancedRadio.setSelected(true); case NEW -> newRadio.setSelected(true); - case CONTINUE_LAST -> continueRadio.setSelected(true); - case RESUME_SPECIFIC -> resumeRadio.setSelected(true); + case CONTINUE_LAST -> { + if (continueRadio.isEnabled()) continueRadio.setSelected(true); + else newRadio.setSelected(true); + } + case RESUME_SPECIFIC -> { + if (resumeRadio.isEnabled()) resumeRadio.setSelected(true); + else newRadio.setSelected(true); + } } onRadioChanged(); } @@ -259,7 +270,10 @@ public void setOnDoubleClick(Runnable action) { } /** - * Reloads the session list for a new directory / profile combination. + * Reloads the session list for a new directory / profile combination and + * updates the enabled state of "Continue last" and "Resume specific" radios. + * If no sessions exist, those radios are disabled and the selection falls + * back to "New session". * * @param newWorkingDir new working directory * @param newClaudeConfigDir new config dir (null → ~/.claude) @@ -270,6 +284,7 @@ public void reload(Path newWorkingDir, Path newClaudeConfigDir, String newExclud this.claudeConfigDir = newClaudeConfigDir; this.excludeSession = newExcludeSession; refreshTable(); + updateHistoryRadiosEnabled(); } /** @@ -289,10 +304,35 @@ boolean isRenameButtonVisible() { return renameButton.isVisible(); } + boolean isContinueRadioEnabled() { + return continueRadio.isEnabled(); + } + + boolean isResumeRadioEnabled() { + return resumeRadio.isEnabled(); + } + // ------------------------------------------------------------------------- // Internal // ------------------------------------------------------------------------- + /** + * Enables or disables "Continue last" and "Resume specific" based on whether + * any sessions exist for the current directory/profile. If neither is available + * and one is currently selected, switches the selection to "New session" to + * avoid a misleading {@code --continue} or {@code --resume} launch. + */ + private void updateHistoryRadiosEnabled() { + boolean hasSessions = workingDir != null + && ClaudeSessionStore.hasAnySessions(workingDir, claudeConfigDir); + continueRadio.setEnabled(hasSessions); + resumeRadio.setEnabled(hasSessions); + if (!hasSessions && (continueRadio.isSelected() || resumeRadio.isSelected())) { + newRadio.setSelected(true); + onRadioChanged(); + } + } + private JPanel row(JRadioButton radio) { JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); p.setAlignmentX(Component.LEFT_ALIGNMENT); diff --git a/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java b/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java index c3046042..9f03f891 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java @@ -1,20 +1,129 @@ package io.github.nbclaudecodegui.ui; import io.github.nbclaudecodegui.model.SessionMode; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; class SessionModePanelTest { + @TempDir Path emptyDir; + @TempDir Path sessionDir; + + /** Creates a fake sessions directory with one .jsonl stub so hasAnySessions() returns true. */ + private Path withSession(Path workingDir) throws IOException { + // ClaudeSessionStore hashes the path by replacing / with - + String hash = workingDir.toAbsolutePath().toString().replace("/", "-"); + Path sessionsSubDir = workingDir.resolve(".claude").resolve("projects").resolve(hash); + Files.createDirectories(sessionsSubDir); + Files.writeString(sessionsSubDir.resolve("fake-session.jsonl"), "{}"); + return workingDir.resolve(".claude"); + } + + // ------------------------------------------------------------------------- + // No-sessions: radios disabled and selection falls back to NEW + // ------------------------------------------------------------------------- + @Test - void isSelectionValid_falseForResumeSpecificWithNoRowSelected() { - SessionModePanel panel = new SessionModePanel(null, null, null, false); + void noSessions_continueAndResumeDisabled() { + SessionModePanel panel = new SessionModePanel(emptyDir, null, null, false); + assertFalse(panel.isContinueRadioEnabled(), "Continue last should be disabled when no sessions"); + assertFalse(panel.isResumeRadioEnabled(), "Resume specific should be disabled when no sessions"); + } + + @Test + void noSessions_defaultSelectionIsNew() { + SessionModePanel panel = new SessionModePanel(emptyDir, null, null, false); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); + } + + @Test + void noSessions_setModeContinueLast_fallsBackToNew() { + SessionModePanel panel = new SessionModePanel(emptyDir, null, null, false); + panel.setMode(SessionMode.CONTINUE_LAST); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); + } + + @Test + void noSessions_setModeResumeSpecific_fallsBackToNew() { + SessionModePanel panel = new SessionModePanel(emptyDir, null, null, false); panel.setMode(SessionMode.RESUME_SPECIFIC); - assertFalse(panel.isSelectionValid(), - "RESUME_SPECIFIC with no row selected should be invalid"); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); } + @Test + void noSessions_reload_switchesFromContinueToNew() throws IOException { + // Start with a dir that has sessions, select CONTINUE_LAST + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); + panel.setMode(SessionMode.CONTINUE_LAST); + assertEquals(SessionMode.CONTINUE_LAST, panel.getSelectedMode()); + + // Reload with an empty dir → should fall back to NEW + panel.reload(emptyDir, null, null); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); + assertFalse(panel.isContinueRadioEnabled()); + } + + // ------------------------------------------------------------------------- + // With sessions: radios enabled + // ------------------------------------------------------------------------- + + @Test + void withSessions_radiosEnabled() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); + assertTrue(panel.isContinueRadioEnabled(), "Continue last should be enabled when sessions exist"); + assertTrue(panel.isResumeRadioEnabled(), "Resume specific should be enabled when sessions exist"); + } + + @Test + void withSessions_setModeContinueLast_selects() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); + panel.setMode(SessionMode.CONTINUE_LAST); + assertEquals(SessionMode.CONTINUE_LAST, panel.getSelectedMode()); + } + + @Test + void withSessions_setModeResumeSpecific_selects() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); + panel.setMode(SessionMode.RESUME_SPECIFIC); + assertEquals(SessionMode.RESUME_SPECIFIC, panel.getSelectedMode()); + } + + @Test + void withSessions_reload_restoresContinueLast() throws IOException { + // Start with empty dir + SessionModePanel panel = new SessionModePanel(emptyDir, null, null, false); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); + + // Reload with sessions, then apply preferred mode + Path configDir = withSession(sessionDir); + panel.reload(sessionDir, configDir, null); + panel.setMode(SessionMode.CONTINUE_LAST); + assertEquals(SessionMode.CONTINUE_LAST, panel.getSelectedMode()); + } + + // ------------------------------------------------------------------------- + // Null workingDir (selector opened with no directory chosen yet) + // ------------------------------------------------------------------------- + + @Test + void nullWorkingDir_defaultIsNew() { + SessionModePanel panel = new SessionModePanel(null, null, null, false); + assertEquals(SessionMode.NEW, panel.getSelectedMode()); + } + + // ------------------------------------------------------------------------- + // Selection validity + // ------------------------------------------------------------------------- + @Test void isSelectionValid_trueForNewMode() { SessionModePanel panel = new SessionModePanel(null, null, null, false); @@ -23,12 +132,22 @@ void isSelectionValid_trueForNewMode() { } @Test - void isSelectionValid_trueForContinueLast() { - SessionModePanel panel = new SessionModePanel(null, null, null, false); + void isSelectionValid_trueForContinueLast() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); panel.setMode(SessionMode.CONTINUE_LAST); assertTrue(panel.isSelectionValid()); } + @Test + void isSelectionValid_falseForResumeSpecificWithNoRowSelected() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); + panel.setMode(SessionMode.RESUME_SPECIFIC); + assertFalse(panel.isSelectionValid(), + "RESUME_SPECIFIC with no row selected should be invalid"); + } + @Test void isSelectionValid_trueForCloseOnly() { SessionModePanel panel = new SessionModePanel(null, null, null, true); @@ -36,11 +155,9 @@ void isSelectionValid_trueForCloseOnly() { assertTrue(panel.isSelectionValid()); } - @Test - void getSelectedMode_defaultIsContinueLast_whenShowCloseOnlyFalse() { - SessionModePanel panel = new SessionModePanel(null, null, null, false); - assertEquals(SessionMode.CONTINUE_LAST, panel.getSelectedMode()); - } + // ------------------------------------------------------------------------- + // Default selection + // ------------------------------------------------------------------------- @Test void getSelectedMode_defaultIsCloseOnly_whenShowCloseOnlyTrue() { @@ -48,6 +165,24 @@ void getSelectedMode_defaultIsCloseOnly_whenShowCloseOnlyTrue() { assertEquals(SessionMode.CLOSE_ONLY, panel.getSelectedMode()); } + @Test + void getSelectedMode_restartAdvanced_whenSet() { + SessionModePanel panel = new SessionModePanel(null, null, null, true); + panel.setMode(SessionMode.RESTART_ADVANCED); + assertEquals(SessionMode.RESTART_ADVANCED, panel.getSelectedMode()); + } + + @Test + void isSelectionValid_trueForRestartAdvanced() { + SessionModePanel panel = new SessionModePanel(null, null, null, true); + panel.setMode(SessionMode.RESTART_ADVANCED); + assertTrue(panel.isSelectionValid()); + } + + // ------------------------------------------------------------------------- + // getSelectedSessionId + // ------------------------------------------------------------------------- + @Test void getSelectedSessionId_nullWhenNotResumeSpecific() { SessionModePanel panel = new SessionModePanel(null, null, null, false); @@ -55,6 +190,10 @@ void getSelectedSessionId_nullWhenNotResumeSpecific() { assertNull(panel.getSelectedSessionId()); } + // ------------------------------------------------------------------------- + // Rename button visibility + // ------------------------------------------------------------------------- + @Test void renameButton_hiddenWhenNewModeSelected() { SessionModePanel panel = new SessionModePanel(null, null, null, false); @@ -63,15 +202,17 @@ void renameButton_hiddenWhenNewModeSelected() { } @Test - void renameButton_hiddenWhenContinueLastSelected() { - SessionModePanel panel = new SessionModePanel(null, null, null, false); + void renameButton_hiddenWhenContinueLastSelected() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); panel.setMode(SessionMode.CONTINUE_LAST); assertFalse(panel.isRenameButtonVisible(), "Rename button should be hidden when CONTINUE_LAST mode is selected"); } @Test - void renameButton_visibleWhenResumeSpecificSelected() { - SessionModePanel panel = new SessionModePanel(null, null, null, false); + void renameButton_visibleWhenResumeSpecificSelected() throws IOException { + Path configDir = withSession(sessionDir); + SessionModePanel panel = new SessionModePanel(sessionDir, configDir, null, false); panel.setMode(SessionMode.RESUME_SPECIFIC); assertTrue(panel.isRenameButtonVisible(), "Rename button should be visible when RESUME_SPECIFIC mode is selected"); } @@ -79,20 +220,6 @@ void renameButton_visibleWhenResumeSpecificSelected() { @Test void renameButton_hiddenByDefault() { SessionModePanel panel = new SessionModePanel(null, null, null, false); - assertFalse(panel.isRenameButtonVisible(), "Rename button should be hidden by default (CONTINUE_LAST is default)"); - } - - @Test - void getSelectedMode_restartAdvanced_whenSet() { - SessionModePanel panel = new SessionModePanel(null, null, null, true); - panel.setMode(SessionMode.RESTART_ADVANCED); - assertEquals(SessionMode.RESTART_ADVANCED, panel.getSelectedMode()); - } - - @Test - void isSelectionValid_trueForRestartAdvanced() { - SessionModePanel panel = new SessionModePanel(null, null, null, true); - panel.setMode(SessionMode.RESTART_ADVANCED); - assertTrue(panel.isSelectionValid()); + assertFalse(panel.isRenameButtonVisible(), "Rename button should be hidden by default"); } }