extra = parseArgs(extraCliArgs);
- if (!extra.isEmpty()) {
- cmd.addAll(extra);
- LOG.info("Extra CLI args appended: " + extra);
- }
-
- // Session mode flags
- if (mode == SessionMode.CONTINUE_LAST) {
- cmd.add("--continue");
- LOG.info("Session mode: --continue");
- } else if (mode == SessionMode.RESUME_SPECIFIC
- && resumeSessionId != null && !resumeSessionId.isBlank()) {
- cmd.add("--resume");
- cmd.add(resumeSessionId);
- LOG.info("Session mode: --resume " + resumeSessionId);
- }
+ appendSessionFlags(cmd, workingDir, env, extraCliArgs, mode, resumeSessionId);
lastCommand = String.join(" ", cmd);
LOG.info("Claude command: " + lastCommand);
@@ -227,6 +212,45 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl
/** Returns the last command attempted to start, as a space-joined string. */
public String getLastCommand() { return lastCommand; }
+ /**
+ * Appends extra CLI args and session-mode flags to {@code cmd}.
+ *
+ * Package-private for unit testing.
+ *
+ * @param cmd command list to append to (already contains executable and MCP flags)
+ * @param workingDir working directory (used to check for existing sessions)
+ * @param env environment map (used to resolve {@code CLAUDE_CONFIG_DIR})
+ * @param extraCliArgs extra CLI arguments string (may be blank)
+ * @param mode session mode
+ * @param resumeSessionId session ID for RESUME_SPECIFIC mode
+ */
+ void appendSessionFlags(List cmd, String workingDir, Map env,
+ String extraCliArgs, SessionMode mode, String resumeSessionId) {
+ List extra = parseArgs(extraCliArgs);
+ if (!extra.isEmpty()) {
+ cmd.addAll(extra);
+ LOG.info("Extra CLI args appended: " + extra);
+ }
+
+ if (mode == SessionMode.CONTINUE_LAST) {
+ String configDirStr = env.get("CLAUDE_CONFIG_DIR");
+ Path claudeConfigDir = configDirStr != null
+ ? Path.of(configDirStr)
+ : Path.of(System.getProperty("user.home"), ".claude");
+ if (ClaudeSessionStore.hasAnySessions(Path.of(workingDir), claudeConfigDir)) {
+ cmd.add("--continue");
+ LOG.fine("Session mode: --continue (existing session found)");
+ } else {
+ LOG.fine("Session mode: CONTINUE_LAST — no sessions found, starting new");
+ }
+ } else if (mode == SessionMode.RESUME_SPECIFIC
+ && resumeSessionId != null && !resumeSessionId.isBlank()) {
+ cmd.add("--resume");
+ cmd.add(resumeSessionId);
+ LOG.fine("Session mode: --resume " + resumeSessionId);
+ }
+ }
+
/**
* Stops the current PTY process and cleans up {@code settings.local.json}.
*
diff --git a/src/main/java/io/github/nbclaudecodegui/process/ClaudeSessionStore.java b/src/main/java/io/github/nbclaudecodegui/process/ClaudeSessionStore.java
index 0908b4d7..47657820 100644
--- a/src/main/java/io/github/nbclaudecodegui/process/ClaudeSessionStore.java
+++ b/src/main/java/io/github/nbclaudecodegui/process/ClaudeSessionStore.java
@@ -69,6 +69,31 @@ static Path sessionsDir(Path workingDir, Path claudeConfigDir) {
return configDir.resolve("projects").resolve(computeHash(workingDir));
}
+ // -------------------------------------------------------------------------
+ // Existence check
+ // -------------------------------------------------------------------------
+
+ /**
+ * Returns {@code true} if at least one session JSONL file exists for the
+ * given working directory. Does not parse any file content.
+ *
+ * @param workingDir working directory to check
+ * @param claudeConfigDir Claude config base dir; {@code null} means {@code ~/.claude}
+ * @return {@code true} if any {@code .jsonl} file is found
+ */
+ public static boolean hasAnySessions(Path workingDir, Path claudeConfigDir) {
+ Path dir = sessionsDir(workingDir, claudeConfigDir);
+ if (!Files.isDirectory(dir)) {
+ return false;
+ }
+ try (Stream files = Files.list(dir)) {
+ return files.anyMatch(p -> p.getFileName().toString().endsWith(".jsonl"));
+ } catch (IOException e) {
+ LOG.fine("Could not check sessions in " + dir + ": " + e.getMessage());
+ return false;
+ }
+ }
+
// -------------------------------------------------------------------------
// List
// -------------------------------------------------------------------------
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java
index e76c6e7c..bf13069b 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionSelectorPanel.java
@@ -160,6 +160,7 @@ public ClaudeSessionSelectorPanel(File initialDirectory, OpenListener openListen
if (initialDirectory != null) {
pathCombo.setSelectedItem(initialDirectory.getAbsolutePath());
}
+ pathCombo.addActionListener(e -> reloadSessionModePanel());
// --- browse button ---
browseButton = new JButton(NbBundle.getMessage(ClaudeSessionSelectorPanel.class, "BTN_Browse"));
@@ -395,8 +396,7 @@ private void onProjectSelected() {
}
private void reloadSessionModePanel() {
- if (sessionModePanel != null
- && sessionModePanel.getSelectedMode() == SessionMode.RESUME_SPECIFIC) {
+ if (sessionModePanel != null) {
sessionModePanel.reload(
resolveDir() != null ? resolveDir().toPath() : null,
resolveClaudeConfigDir(),
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
index 0245ae89..f53516de 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
@@ -938,16 +938,25 @@ public void onSaveAndSwitch(String sessionName, SessionMode mode, String resumeI
cleanupChatLayout();
if (mode == SessionMode.CLOSE_ONLY) {
- // Just stop — do nothing more
+ doCloseTab();
return;
}
+ if (mode == SessionMode.RESTART_ADVANCED) {
+ return; // selector already shown by cleanupChatLayout()
+ }
+
// Restart with new mode
if (dir != null) {
startSession(dir, profileName, extraCliArgs, mode, resumeId);
}
}
+ /** Closes this tab. Protected to allow overriding in tests. */
+ protected void doCloseTab() {
+ close();
+ }
+
private void stopProcess() {
controller.stopProcess();
cleanupChatLayout();
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java b/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java
index f76f1b0e..17852f8e 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/SessionModePanel.java
@@ -59,6 +59,7 @@ public final class SessionModePanel extends JPanel {
// -------------------------------------------------------------------------
private final JRadioButton closeOnlyRadio;
+ private final JRadioButton restartAdvancedRadio;
private final JRadioButton newRadio;
private final JRadioButton continueRadio;
private final JRadioButton resumeRadio;
@@ -94,12 +95,14 @@ public SessionModePanel(Path workingDir, Path claudeConfigDir,
ButtonGroup group = new ButtonGroup();
- closeOnlyRadio = new JRadioButton("Close only");
- newRadio = new JRadioButton("New session");
- continueRadio = new JRadioButton("Continue last");
- resumeRadio = new JRadioButton("Resume specific");
+ closeOnlyRadio = new JRadioButton("Close only");
+ restartAdvancedRadio = new JRadioButton("Restart Advanced...");
+ newRadio = new JRadioButton("New session");
+ continueRadio = new JRadioButton("Continue last");
+ resumeRadio = new JRadioButton("Resume specific");
group.add(closeOnlyRadio);
+ group.add(restartAdvancedRadio);
group.add(newRadio);
group.add(continueRadio);
group.add(resumeRadio);
@@ -110,6 +113,7 @@ public SessionModePanel(Path workingDir, Path claudeConfigDir,
if (showCloseOnly) {
radioPanel.add(row(closeOnlyRadio));
+ radioPanel.add(row(restartAdvancedRadio));
}
radioPanel.add(row(newRadio));
radioPanel.add(row(continueRadio));
@@ -169,6 +173,7 @@ public void componentResized(java.awt.event.ComponentEvent e) {
// Radio listeners
closeOnlyRadio.addActionListener(e -> onRadioChanged());
+ restartAdvancedRadio.addActionListener(e -> onRadioChanged());
newRadio.addActionListener(e -> onRadioChanged());
continueRadio.addActionListener(e -> onRadioChanged());
resumeRadio.addActionListener(e -> onRadioChanged());
@@ -206,9 +211,10 @@ public void mouseClicked(MouseEvent e) {
* @return selected {@link SessionMode}
*/
public SessionMode getSelectedMode() {
- if (closeOnlyRadio.isSelected()) return SessionMode.CLOSE_ONLY;
- if (newRadio.isSelected()) return SessionMode.NEW;
- if (continueRadio.isSelected()) return SessionMode.CONTINUE_LAST;
+ if (closeOnlyRadio.isSelected()) return SessionMode.CLOSE_ONLY;
+ if (restartAdvancedRadio.isSelected()) return SessionMode.RESTART_ADVANCED;
+ if (newRadio.isSelected()) return SessionMode.NEW;
+ if (continueRadio.isSelected()) return SessionMode.CONTINUE_LAST;
return SessionMode.RESUME_SPECIFIC;
}
@@ -233,10 +239,11 @@ public String getSelectedSessionId() {
*/
public void setMode(SessionMode mode) {
switch (mode) {
- case CLOSE_ONLY -> closeOnlyRadio.setSelected(true);
- case NEW -> newRadio.setSelected(true);
- case CONTINUE_LAST -> continueRadio.setSelected(true);
- case RESUME_SPECIFIC -> resumeRadio.setSelected(true);
+ 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);
}
onRadioChanged();
}
diff --git a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
index 3acac4d3..092a59a0 100644
--- a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
@@ -342,6 +342,66 @@ private static void resetPrefs(File script) {
if (script != null) script.delete();
}
+ // -------------------------------------------------------------------------
+ // appendSessionFlags — --continue only when sessions exist
+ // -------------------------------------------------------------------------
+
+ @Test
+ void appendSessionFlags_continueLast_noSessionsDir_noFlag(@TempDir Path tmpDir) {
+ ClaudeProcess cp = new ClaudeProcess();
+ java.util.List cmd = new java.util.ArrayList<>(java.util.List.of("claude"));
+ // tmpDir has no sessions subdirectory → no --continue
+ cp.appendSessionFlags(cmd, tmpDir.toString(), java.util.Map.of(),
+ "", io.github.nbclaudecodegui.model.SessionMode.CONTINUE_LAST, null);
+ assertFalse(cmd.contains("--continue"),
+ "should NOT add --continue when no sessions exist");
+ }
+
+ @Test
+ void appendSessionFlags_continueLast_sessionsExist_addsFlag(@TempDir Path tmpDir) throws Exception {
+ // Create a fake sessions dir with one .jsonl file
+ String hash = tmpDir.toString().replace('/', '-');
+ Path sessionsDir = tmpDir.resolve(".claude").resolve("projects").resolve(hash);
+ Files.createDirectories(sessionsDir);
+ Files.writeString(sessionsDir.resolve("fake-session.jsonl"), "{}\n");
+
+ ClaudeProcess cp = new ClaudeProcess();
+ java.util.List cmd = new java.util.ArrayList<>(java.util.List.of("claude"));
+ cp.appendSessionFlags(cmd, tmpDir.toString(),
+ java.util.Map.of("CLAUDE_CONFIG_DIR", tmpDir.resolve(".claude").toString()),
+ "", io.github.nbclaudecodegui.model.SessionMode.CONTINUE_LAST, null);
+ assertTrue(cmd.contains("--continue"),
+ "should add --continue when sessions exist");
+ }
+
+ @Test
+ void appendSessionFlags_newMode_noFlag(@TempDir Path tmpDir) throws Exception {
+ // Even if sessions exist, NEW mode should not add any session flag
+ String hash = tmpDir.toString().replace('/', '-');
+ Path sessionsDir = tmpDir.resolve(".claude").resolve("projects").resolve(hash);
+ Files.createDirectories(sessionsDir);
+ Files.writeString(sessionsDir.resolve("fake-session.jsonl"), "{}\n");
+
+ ClaudeProcess cp = new ClaudeProcess();
+ java.util.List cmd = new java.util.ArrayList<>(java.util.List.of("claude"));
+ cp.appendSessionFlags(cmd, tmpDir.toString(),
+ java.util.Map.of("CLAUDE_CONFIG_DIR", tmpDir.resolve(".claude").toString()),
+ "", io.github.nbclaudecodegui.model.SessionMode.NEW, null);
+ assertFalse(cmd.contains("--continue"), "NEW mode should not add --continue");
+ assertFalse(cmd.contains("--resume"), "NEW mode should not add --resume");
+ }
+
+ @Test
+ void appendSessionFlags_resumeSpecific_addsResumeFlag(@TempDir Path tmpDir) {
+ ClaudeProcess cp = new ClaudeProcess();
+ java.util.List cmd = new java.util.ArrayList<>(java.util.List.of("claude"));
+ cp.appendSessionFlags(cmd, tmpDir.toString(), java.util.Map.of(),
+ "", io.github.nbclaudecodegui.model.SessionMode.RESUME_SPECIFIC, "abc-123");
+ assertTrue(cmd.contains("--resume"), "should add --resume");
+ assertTrue(cmd.contains("abc-123"), "should add the session id");
+ assertFalse(cmd.contains("--continue"), "should not add --continue");
+ }
+
// -------------------------------------------------------------------------
// parseArgs
// -------------------------------------------------------------------------
diff --git a/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabCloseOnlyTest.java b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabCloseOnlyTest.java
new file mode 100644
index 00000000..ba0c7757
--- /dev/null
+++ b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabCloseOnlyTest.java
@@ -0,0 +1,61 @@
+package io.github.nbclaudecodegui.ui;
+
+import io.github.nbclaudecodegui.model.SessionMode;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Verifies that {@link ClaudeSessionTab#onSaveAndSwitch} closes the tab when
+ * {@link SessionMode#CLOSE_ONLY} is selected.
+ */
+class ClaudeSessionTabCloseOnlyTest {
+
+ @Test
+ void onSaveAndSwitch_closeOnly_closesTab() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean(false);
+
+ ClaudeSessionTab tab = new ClaudeSessionTab() {
+ @Override
+ protected void doCloseTab() {
+ closed.set(true);
+ }
+ };
+
+ tab.onSaveAndSwitch("", SessionMode.CLOSE_ONLY, null);
+
+ assertTrue(closed.get(), "Tab must be closed when CLOSE_ONLY mode is selected");
+ }
+
+ @Test
+ void onSaveAndSwitch_newMode_doesNotCloseTab() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean(false);
+
+ ClaudeSessionTab tab = new ClaudeSessionTab() {
+ @Override
+ protected void doCloseTab() {
+ closed.set(true);
+ }
+ };
+
+ tab.onSaveAndSwitch("", SessionMode.NEW, null);
+
+ assertFalse(closed.get(), "Tab must not be closed when NEW mode is selected");
+ }
+
+ @Test
+ void onSaveAndSwitch_restartAdvanced_doesNotCloseTab() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean(false);
+
+ ClaudeSessionTab tab = new ClaudeSessionTab() {
+ @Override
+ protected void doCloseTab() {
+ closed.set(true);
+ }
+ };
+
+ tab.onSaveAndSwitch("", SessionMode.RESTART_ADVANCED, null);
+
+ assertFalse(closed.get(), "Tab must not be closed when RESTART_ADVANCED mode is selected");
+ }
+}
diff --git a/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java b/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java
index 2548bc6a..c3046042 100644
--- a/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/ui/SessionModePanelTest.java
@@ -81,4 +81,18 @@ 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());
+ }
}