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 "Open with Claude Code" context menu not applying Default profile settings (API key, proxy, Extra CLI args, etc.) (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/23)
- Added alphabetical sorting to the "Select Project" dropdown (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/21)
- Fixed Default profile settings (API key, connection type, proxy, etc.) not being saved when edited in Tools → Options → Claude Code → Profiles
- Fixed MCP config not working on Windows due to quote stripping in CreateProcess (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/13)
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.21.41-SNAPSHOT</version>
<version>0.21.45-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans 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 @@ -100,6 +100,22 @@ public static List<ClaudeProfile> getProfiles() {
return result;
}

/**
* Returns the preference-backed Default profile.
*
* <p>Unlike {@link ClaudeProfile#createDefault()}, this method goes through
* {@link #getProfiles()}, which loads stored settings (API key, proxy,
* extraCliArgs, etc.) from {@code NbPreferences}. Callers that need the
* Default profile should use this method so the user's configuration is
* honoured rather than silently discarded.
*
* @return the Default profile with settings loaded from preferences
*/
public static ClaudeProfile getDefaultProfile() {
return getProfiles().stream().filter(ClaudeProfile::isDefault).findFirst()
.orElseGet(ClaudeProfile::createDefault);
}

/**
* Looks up a profile by name.
*
Expand All @@ -108,14 +124,10 @@ public static List<ClaudeProfile> getProfiles() {
*/
public static ClaudeProfile findByName(String name) {
if (name == null || name.isBlank()) {
return ClaudeProfile.createDefault();
}
for (ClaudeProfile p : getProfiles()) {
if (p.getName().equals(name)) {
return p;
}
return getDefaultProfile();
}
return ClaudeProfile.createDefault();
return getProfiles().stream().filter(p -> p.getName().equals(name)).findFirst()
.orElseGet(ClaudeProfileStore::getDefaultProfile);
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
* // Assign a profile to a project
* ClaudeProjectProperties.setProfileName(projectDir, "MyProfile");
*
* // Retrieve the assigned profile (falls back to Default)
* ClaudeProfile profile = ClaudeProjectProperties.resolveProfile(projectDir);
* // Retrieve the assigned profile name (falls back to Default when blank)
* String profileName = ClaudeProjectProperties.getProfileName(projectDir);
* ClaudeProfile profile = ClaudeProfileStore.findByName(profileName);
* }</pre>
*/
public final class ClaudeProjectProperties {
Expand Down Expand Up @@ -51,31 +52,6 @@ public static String getProfileName(File projectDir) {
.get(KEY_PREFIX + projectDir.getAbsolutePath(), "");
}

/**
* Resolves the {@link ClaudeProfile} for the given project directory.
*
* <p>If no profile name is stored, or the stored name is blank, the
* Default profile is returned. If the stored name cannot be found in
* {@link ClaudeProfileStore}, the Default profile is returned and a
* warning is logged.
*
* @param projectDir the project root directory; may be {@code null}
* @return the assigned profile, or the Default profile if not set / not found
*/
public static ClaudeProfile resolveProfile(File projectDir) {
String name = getProfileName(projectDir);
if (name.isBlank()) {
return ClaudeProfile.createDefault();
}
ClaudeProfile found = ClaudeProfileStore.findByName(name);
if (found.isDefault() && !name.isBlank()) {
LOG.warning("Profile '" + name + "' assigned to project "
+ (projectDir != null ? projectDir.getAbsolutePath() : "null")
+ " not found — using Default.");
}
return found;
}

// -------------------------------------------------------------------------
// Write
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,17 @@ public boolean isSessionActive() {
/**
* Starts a session for {@code dir} with {@code profileName} without showing any dialogs.
*
* <p>The profile's {@code extraCliArgs} are resolved here so that the global
* "Extra CLI args" setting in Tools → Options is honoured when a session is
* launched from the project context menu ("Open with Claude Code").
* Previously passing {@code null} meant the setting was silently ignored.
*
* @param dir working directory
* @param profileName profile name, or {@code null} for Default
*/
public void autoStart(File dir, String profileName) {
autoStart(dir, profileName, null);
ClaudeProfile profile = ClaudeProfileStore.findByName(profileName);
autoStart(dir, profileName, profile.getExtraCliArgs());
}

public void autoStart(File dir, String profileName, String extraCliArgs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,49 @@ void findByName_nullName_returnsDefault() {
assertTrue(found.isDefault());
}

@Test
void findByName_unknownName_returnsDefault() {
ClaudeProfile found = ClaudeProfileStore.findByName("nonexistent-profile-xyz");
assertTrue(found.isDefault(),
"findByName with unknown name must fall back to the Default profile");
}

// -------------------------------------------------------------------------
// getDefaultProfile (issue #23)
// -------------------------------------------------------------------------

/**
* Regression test for issue #23: {@code getDefaultProfile()} must return
* the preference-backed Default profile, matching what {@code getProfiles()}
* returns — not a bare {@code createDefault()} with empty fields.
*
* <p>{@code createDefault()} sets {@code extraCliArgs = ""} unconditionally,
* so using it in {@code findByName(null)} would silently discard any Extra
* CLI args the user configured in Tools → Options.
*/
@Test
void getDefaultProfile_isDefault() {
ClaudeProfile p = ClaudeProfileStore.getDefaultProfile();
assertTrue(p.isDefault(), "getDefaultProfile() must return the Default profile");
assertEquals("Default", p.getName());
}

@Test
void getDefaultProfile_matchesGetProfiles() {
ClaudeProfile fromList = ClaudeProfileStore.getProfiles().get(0);
ClaudeProfile fromMethod = ClaudeProfileStore.getDefaultProfile();
assertEquals(fromList.getExtraCliArgs(), fromMethod.getExtraCliArgs(),
"getDefaultProfile() must return the same extraCliArgs as getProfiles().get(0)");
}

@Test
void findByName_null_delegatesToGetDefaultProfile() {
ClaudeProfile fromFind = ClaudeProfileStore.findByName(null);
ClaudeProfile fromMethod = ClaudeProfileStore.getDefaultProfile();
assertEquals(fromMethod.getExtraCliArgs(), fromFind.getExtraCliArgs(),
"findByName(null) must delegate to getDefaultProfile()");
}

// -------------------------------------------------------------------------
// saveProfile / Default cannot be saved
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,6 @@ void setProfileName_nullName_clearsEntry() {
} catch (Exception ignored) {}
}

// -------------------------------------------------------------------------
// resolveProfile — null dir falls back to Default
// -------------------------------------------------------------------------

@Test
void resolveProfile_nullDir_returnsDefault() {
ClaudeProfile profile = ClaudeProjectProperties.resolveProfile(null);
assertTrue(profile.isDefault());
}

// -------------------------------------------------------------------------
// renameAssignments
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.github.nbclaudecodegui.ui;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

/**
* Regression tests for issue #23: {@link ClaudeSessionTab#autoStart(File, String)}
* must forward the profile's {@code extraCliArgs} to the underlying session start,
* not pass {@code null}.
*
* <p>Before the fix, {@code autoStart(File, String)} called
* {@code autoStart(dir, profileName, null)}, silently discarding any Extra CLI
* args configured for the profile in Tools → Options → Profiles.
*/
class ClaudeSessionTabAutoStartTest {

/**
* Verifies that {@code autoStart(File, String)} passes a non-null
* {@code extraCliArgs} string to the three-argument overload.
*
* <p>A non-null value guarantees that the profile's CLI args setting
* (even if empty) is forwarded rather than dropped.
*/
@Test
void autoStart_twoArg_passesNonNullExtraCliArgs() throws IOException {
AtomicReference<String> capturedExtra = new AtomicReference<>("UNSET");

ClaudeSessionTab tab = new ClaudeSessionTab() {
@Override
public void autoStart(File dir, String profileName, String extraCliArgs) {
capturedExtra.set(extraCliArgs);
}
};

File tmpDir = Files.createTempDirectory("autostart-test").toFile();
try {
tab.autoStart(tmpDir, null);
assertNotNull(capturedExtra.get(),
"autoStart(File,String) must not pass null extraCliArgs to the " +
"three-argument overload — profile extraCliArgs would be silently discarded");
} finally {
tmpDir.delete();
}
}
}
Loading