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 "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)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.22.47-SNAPSHOT</version>
<version>0.22.48-SNAPSHOT</version>
<packaging>nbm</packaging>
<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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ private void reloadSessionModePanel() {
resolveDir() != null ? resolveDir().toPath() : null,
resolveClaudeConfigDir(),
null);
sessionModePanel.setMode(ClaudeCodePreferences.getContextMenuSessionMode());
}
}

Expand Down
46 changes: 43 additions & 3 deletions src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public void mouseClicked(MouseEvent e) {
});

refreshTable();
updateHistoryRadiosEnabled();
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -235,15 +236,25 @@ public String getSelectedSessionId() {
/**
* Programmatically sets the selected mode.
*
* <p>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) {
switch (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();
}
Expand All @@ -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)
Expand All @@ -270,6 +284,7 @@ public void reload(Path newWorkingDir, Path newClaudeConfigDir, String newExclud
this.claudeConfigDir = newClaudeConfigDir;
this.excludeSession = newExcludeSession;
refreshTable();
updateHistoryRadiosEnabled();
}

/**
Expand All @@ -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);
Expand Down
187 changes: 157 additions & 30 deletions src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -23,38 +132,68 @@ 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);
panel.setMode(SessionMode.CLOSE_ONLY);
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() {
SessionModePanel panel = new SessionModePanel(null, null, null, true);
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);
panel.setMode(SessionMode.NEW);
assertNull(panel.getSelectedSessionId());
}

// -------------------------------------------------------------------------
// Rename button visibility
// -------------------------------------------------------------------------

@Test
void renameButton_hiddenWhenNewModeSelected() {
SessionModePanel panel = new SessionModePanel(null, null, null, false);
Expand All @@ -63,36 +202,24 @@ 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");
}

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