diff --git a/CHANGELOG.md b/CHANGELOG.md index ab00cfa6..ee8647ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added ability to manually add, rename, and sort models in the Model Aliases dialog - Fixed missing Claude Code icon in Tools → Options dialog # 1.0.4 (2026-05-18) diff --git a/docs/screenshots/model-aliases.png b/docs/screenshots/model-aliases.png new file mode 100644 index 00000000..c754a5aa Binary files /dev/null and b/docs/screenshots/model-aliases.png differ diff --git a/docs/user-manual.md b/docs/user-manual.md index 53d39180..d1f61b82 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -495,6 +495,8 @@ If your provider names models differently from Anthropic's standard names, the p Click **Model Aliases…** to open the Model Aliases dialog. +![Model Aliases dialog](screenshots/model-aliases.png) + The dialog shows a table with three columns: | Column | Description | @@ -508,7 +510,10 @@ The dialog shows a table with three columns: | Button | Effect | |--------|--------| | **Fetch** | Queries the provider's models endpoint using the configured API key, fills the table with discovered model IDs, and marks each as available (✓) or unavailable (✗) | +| **Add** | Opens an input dialog; enter a model ID to append a new row to the table (with blank alias and no availability mark) | +| **Rename** | Opens an input dialog pre-filled with the selected row's ID; confirm to update the model ID in place | | **↑ / ↓** | Reorder rows — the order determines how models appear in the status bar model selector | +| **Sort** | Sort all rows alphabetically by model ID (case-insensitive) | | **Delete** | Remove the selected row | | **Prune** | Remove all rows marked as unavailable (✗) | diff --git a/pom.xml b/pom.xml index 68b9763f..602c5df6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.1.1-SNAPSHOT + 1.1.3-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/ModelAliasesDialog.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialog.java index df81cb36..1d2a5c9f 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialog.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialog.java @@ -153,19 +153,25 @@ public Component getTableCellRendererComponent(JTable t, Object value, // --- Button panel (right, vertical) --- JButton fetchBtn = new JButton("Fetch"); + JButton addBtn = new JButton("Add"); + JButton renameBtn = new JButton("Rename"); JButton upBtn = new JButton("\u2191"); JButton downBtn = new JButton("\u2193"); + JButton sortBtn = new JButton("Sort"); JButton deleteBtn = new JButton("Delete"); JButton pruneBtn = new JButton("Prune"); - for (JButton b : new JButton[]{fetchBtn, upBtn, downBtn, deleteBtn, pruneBtn}) { + for (JButton b : new JButton[]{fetchBtn, addBtn, renameBtn, upBtn, downBtn, sortBtn, deleteBtn, pruneBtn}) { b.setAlignmentX(Component.CENTER_ALIGNMENT); b.setMaximumSize(new Dimension(80, 28)); } fetchBtn .addActionListener(e -> onFetch()); + addBtn .addActionListener(e -> onAdd()); + renameBtn.addActionListener(e -> onRename()); upBtn .addActionListener(e -> moveRow(-1)); downBtn .addActionListener(e -> moveRow(+1)); + sortBtn .addActionListener(e -> onSort()); deleteBtn.addActionListener(e -> onDelete()); pruneBtn .addActionListener(e -> onPrune()); @@ -173,10 +179,16 @@ public Component getTableCellRendererComponent(JTable t, Object value, btnPanel.setLayout(new BoxLayout(btnPanel, BoxLayout.Y_AXIS)); btnPanel.add(fetchBtn); btnPanel.add(Box.createVerticalStrut(4)); + btnPanel.add(addBtn); + btnPanel.add(Box.createVerticalStrut(2)); + btnPanel.add(renameBtn); + btnPanel.add(Box.createVerticalStrut(4)); btnPanel.add(upBtn); btnPanel.add(Box.createVerticalStrut(2)); btnPanel.add(downBtn); btnPanel.add(Box.createVerticalStrut(4)); + btnPanel.add(sortBtn); + btnPanel.add(Box.createVerticalStrut(4)); btnPanel.add(deleteBtn); btnPanel.add(Box.createVerticalStrut(4)); btnPanel.add(pruneBtn); @@ -326,6 +338,16 @@ private void moveRow(int delta) { table.setRowSelectionInterval(target, target); } + private void onSort() { + stopEditing(); + List rows = collectModels(); + rows.sort((a, b) -> a.id().compareToIgnoreCase(b.id())); + tableModel.setRowCount(0); + for (ModelAlias m : rows) { + tableModel.addRow(new Object[]{m.id(), m.available(), m.alias()}); + } + } + private void onDelete() { stopEditing(); int sel = table.getSelectedRow(); @@ -341,6 +363,58 @@ private void onPrune() { } } + private void onAdd() { + String id = JOptionPane.showInputDialog(this, "Model ID:", "Add Model", JOptionPane.PLAIN_MESSAGE); + if (id == null) return; + id = id.trim(); + String error = validateModelId(id, -1); + if (error != null) { statusLabel.setText(error); return; } + tableModel.addRow(new Object[]{id, null, ""}); + int newRow = tableModel.getRowCount() - 1; + table.setRowSelectionInterval(newRow, newRow); + table.scrollRectToVisible(table.getCellRect(newRow, 0, true)); + statusLabel.setText(" "); + } + + private void onRename() { + stopEditing(); + int sel = table.getSelectedRow(); + if (sel < 0) return; + String current = (String) tableModel.getValueAt(sel, 0); + String newId = (String) JOptionPane.showInputDialog( + this, "Model ID:", "Rename Model", JOptionPane.PLAIN_MESSAGE, null, null, current); + if (newId == null) return; + newId = newId.trim(); + if (newId.equals(current)) return; + String error = validateModelId(newId, sel); + if (error != null) { statusLabel.setText(error); return; } + tableModel.setValueAt(newId, sel, 0); + tableModel.setValueAt(null, sel, 1); + statusLabel.setText(" "); + } + + String validateModelId(String id, int skipRow) { + List ids = new ArrayList<>(); + for (int i = 0; i < tableModel.getRowCount(); i++) { + ids.add((String) tableModel.getValueAt(i, 0)); + } + return validateModelId(id, ids, skipRow); + } + + /** + * Returns an error message if {@code id} is blank or already exists in + * {@code existingIds} (excluding {@code skipRow}), or {@code null} if valid. + */ + static String validateModelId(String id, List existingIds, int skipRow) { + if (id.isBlank()) return "Model ID must not be blank."; + for (int i = 0; i < existingIds.size(); i++) { + if (i != skipRow && id.equals(existingIds.get(i))) { + return "Model already in the list: " + id; + } + } + return null; + } + private void onOk() { stopEditing(); List models = collectModels(); diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialogTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialogTest.java new file mode 100644 index 00000000..be9be2a0 --- /dev/null +++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ModelAliasesDialogTest.java @@ -0,0 +1,78 @@ +package io.github.nbplugins.claudecodegui.settings; + +import java.util.List; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link ModelAliasesDialog} helpers. + */ +class ModelAliasesDialogTest { + + // ------------------------------------------------------------------------- + // parseModelIds + // ------------------------------------------------------------------------- + + @Test + void parseModelIds_emptyString_returnsEmpty() { + assertTrue(ModelAliasesDialog.parseModelIds("").isEmpty()); + } + + @Test + void parseModelIds_nullInput_returnsEmpty() { + assertTrue(ModelAliasesDialog.parseModelIds(null).isEmpty()); + } + + @Test + void parseModelIds_validResponse_returnsIds() { + String json = "{\"data\":[{\"id\":\"claude-sonnet-4-5\"},{\"id\":\"claude-opus-4\"}]}"; + List ids = ModelAliasesDialog.parseModelIds(json); + assertEquals(List.of("claude-sonnet-4-5", "claude-opus-4"), ids); + } + + @Test + void parseModelIds_blankId_skipped() { + String json = "{\"data\":[{\"id\":\"\"},{\"id\":\"claude-haiku-4-5\"}]}"; + List ids = ModelAliasesDialog.parseModelIds(json); + assertEquals(List.of("claude-haiku-4-5"), ids); + } + + @Test + void parseModelIds_noDataKey_stillFindsIds() { + String json = "{\"id\":\"model-x\"}"; + List ids = ModelAliasesDialog.parseModelIds(json); + assertEquals(List.of("model-x"), ids); + } + + // ------------------------------------------------------------------------- + // validateModelId + // ------------------------------------------------------------------------- + + @Test + void validateModelId_blank_returnsError() { + assertNotNull(ModelAliasesDialog.validateModelId("", List.of(), -1)); + assertNotNull(ModelAliasesDialog.validateModelId(" ", List.of(), -1)); + } + + @Test + void validateModelId_uniqueId_returnsNull() { + assertNull(ModelAliasesDialog.validateModelId("new-model", List.of("existing"), -1)); + } + + @Test + void validateModelId_duplicateId_returnsError() { + assertNotNull(ModelAliasesDialog.validateModelId("existing", List.of("existing"), -1)); + } + + @Test + void validateModelId_skipRowAllowsSameId() { + // Rename: skipping row 0 means the same name at row 0 is not a conflict + assertNull(ModelAliasesDialog.validateModelId("existing", List.of("existing"), 0)); + } + + @Test + void validateModelId_skipRowStillCatchesDuplicateInOtherRow() { + // Renaming row 0 to "b" (which is at row 1) → conflict + assertNotNull(ModelAliasesDialog.validateModelId("b", List.of("a", "b"), 0)); + } +}