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"); } }