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 stale model aliases from a "Claude Compatible API" profile still showing in the model selector after switching the profile to "Managed by Claude"
- Fixed ClassNotFoundException on plugin installation/upgrade when the session tab had been previously saved under the old package name (https://github.com/nbplugins/NetbeansPluginClaudeCodeGui/issues/146)
- Fixed a Warning dialog appearing on NetBeans exit when a diff tab had been opened during the session

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.nbplugins</groupId>
<artifactId>netbeans-plugin-claude-code-gui</artifactId>
<version>1.2.15-SNAPSHOT</version>
<version>1.2.16-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 @@ -308,8 +308,11 @@ public Map<String, String> toEnvVars() {
default -> { /* SYSTEM_MANAGED — inherit from parent process */ }
}

// Model aliases (ANTHROPIC_DEFAULT_*_MODEL)
if (modelAliases != null) {
// Model aliases (ANTHROPIC_DEFAULT_*_MODEL) — only for connection types
// that use a custom endpoint; irrelevant (and potentially harmful) otherwise.
ConnectionType ct = computeConnectionType();
if ((ct == ConnectionType.OTHER_API || ct == ConnectionType.OPENAI_PROXY)
&& modelAliases != null) {
for (int i = 0; i < ALIAS_NAMES.length; i++) {
String modelId = modelAliases.get(ALIAS_NAMES[i]);
if (modelId != null && !modelId.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,13 @@ private void flushFormToCurrentProfile() {
p.setOpenaiProxy(false);
}

// Clear model aliases/custom models when switching to a connection type that does not
// support them — prevents stale aliases from the old type appearing in the model combo box.
if (!rbOtherApi.isSelected() && !rbOpenAIProxy.isSelected()) {
p.setModelAliases(null);
p.setCustomModels(null);
}

// Proxy
if (rbProxyNone.isSelected()) {
p.setProxyMode(ProxyMode.NO_PROXY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,36 @@ void storageDir_jacksonRoundTrip() throws Exception {
ClaudeProfile loaded = mapper.readValue(json, ClaudeProfile.class);
assertEquals("/custom/path", loaded.getStorageDir());
}

// -------------------------------------------------------------------------
// toEnvVars — model aliases (regression for stale-aliases-after-type-switch)
// -------------------------------------------------------------------------

@Test
void toEnvVars_doesNotInjectModelAliasesForManagedConnection() {
ClaudeProfile p = ClaudeProfile.createNamed("Test");
// no apiKey / token → CLAUDE_MANAGED
p.setModelAliases(Map.of("sonnet", "old-model-id"));
p.setCustomModels(List.of("old-custom-model"));

assertEquals(ConnectionType.CLAUDE_MANAGED, p.computeConnectionType());
Map<String, String> env = p.toEnvVars();

assertFalse(env.containsKey("ANTHROPIC_DEFAULT_SONNET_MODEL"),
"Model alias env vars must not be injected for CLAUDE_MANAGED connection");
}

@Test
void toEnvVars_injectsModelAliasesForOtherApiConnection() {
ClaudeProfile p = ClaudeProfile.createNamed("Test");
p.setApiKey("sk-test");
p.setBaseUrl("https://example.com");
p.setModelAliases(Map.of("sonnet", "my-sonnet-model"));

assertEquals(ConnectionType.OTHER_API, p.computeConnectionType());
Map<String, String> env = p.toEnvVars();

assertEquals("my-sonnet-model", env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
"Model alias env vars must be injected for OTHER_API connection");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.swing.JComboBox;
import javax.swing.JPasswordField;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -64,6 +66,79 @@ void flushWritesToFormProfile_notComboSelection() throws Exception {
"Default profile must not receive Work's apiKey");
}

/**
* Regression for stale model aliases after profile type switch:
* when a profile had model aliases set under "Claude Compatible API" and then
* the user switches to "Managed by Claude", flushFormToCurrentProfile must
* clear the stored modelAliases and customModels so they don't appear in the
* model combo box in the next session.
*/
@Test
void flush_clearsModelAliasesWhenSwitchingToManaged() throws Exception {
ClaudeProfile work = ClaudeProfile.createNamed("Work");
work.setApiKey("sk-old");
work.setBaseUrl("https://old.example.com");
work.setModelAliases(Map.of("sonnet", "old-model"));
work.setCustomModels(List.of("old-custom-1", "old-custom-2"));

ClaudeProfilesPanel panel = new ClaudeProfilesPanel();
setField(panel, "profiles", new ArrayList<>(List.of(ClaudeProfile.createDefault(), work)));
rebuildCombo(panel);
invoke(panel, "loadProfileIntoForm", ClaudeProfile.class, work);

// Switch to "Managed by Claude" — clear API key and base URL
JPasswordField apiKeyField = getField(panel, "apiKeyField", JPasswordField.class);
apiKeyField.setText("");
JTextField baseUrlField = getField(panel, "baseUrlField", JTextField.class);
baseUrlField.setText("");
JRadioButton rbManaged = getField(panel, "rbManaged", JRadioButton.class);
rbManaged.setSelected(true);
// deselect others
for (String rb : new String[]{"rbSubscription", "rbClaudeApi", "rbOtherApi", "rbOpenAIProxy"}) {
getField(panel, rb, JRadioButton.class).setSelected(false);
}

invoke(panel, "flushFormToCurrentProfile");

assertTrue(work.getModelAliases().isEmpty(),
"modelAliases must be cleared when switching to Managed by Claude");
assertTrue(work.getCustomModels().isEmpty(),
"customModels must be cleared when switching to Managed by Claude");
}

@Test
void flush_keepsModelAliasesForOtherApiConnection() throws Exception {
ClaudeProfile work = ClaudeProfile.createNamed("Work");
work.setApiKey("sk-test");
work.setBaseUrl("https://example.com");
work.setModelAliases(Map.of("sonnet", "my-sonnet"));
work.setCustomModels(List.of("custom-1"));

ClaudeProfilesPanel panel = new ClaudeProfilesPanel();
setField(panel, "profiles", new ArrayList<>(List.of(ClaudeProfile.createDefault(), work)));
rebuildCombo(panel);
invoke(panel, "loadProfileIntoForm", ClaudeProfile.class, work);

// Keep "Other API" selected
JPasswordField apiKeyField = getField(panel, "apiKeyField", JPasswordField.class);
apiKeyField.setText("sk-test");
JTextField baseUrlField = getField(panel, "baseUrlField", JTextField.class);
baseUrlField.setText("https://example.com");
JRadioButton rbOtherApi = getField(panel, "rbOtherApi", JRadioButton.class);
rbOtherApi.setSelected(true);
for (String rb : new String[]{"rbManaged", "rbSubscription", "rbClaudeApi", "rbOpenAIProxy"}) {
getField(panel, rb, JRadioButton.class).setSelected(false);
}

invoke(panel, "flushFormToCurrentProfile");

// Model aliases must NOT be cleared when staying on Other API
assertFalse(work.getModelAliases().isEmpty(),
"modelAliases must be kept when connection type supports them");
assertFalse(work.getCustomModels().isEmpty(),
"customModels must be kept when connection type supports them");
}

// -------------------------------------------------------------------------
// Reflection helpers
// -------------------------------------------------------------------------
Expand Down
Loading