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.
+
+
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));
+ }
+}