diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a38e42..df282b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pom.xml b/pom.xml index ae7485d2..15626ccf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.2.15-SNAPSHOT + 1.2.16-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/nbplugins/claudecodegui/settings/ClaudeProfile.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java index d4fb3743..acd51275 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java @@ -308,8 +308,11 @@ public Map 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()) { diff --git a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java index 16c8b1b0..3c07ced0 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java @@ -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); diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java index 472bb73a..a545999e 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java @@ -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 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 env = p.toEnvVars(); + + assertEquals("my-sonnet-model", env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"), + "Model alias env vars must be injected for OTHER_API connection"); + } } diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java index 2f6b81d9..df6f7704 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java @@ -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.*; @@ -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 // -------------------------------------------------------------------------