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 @@
- 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)
Expand Down
Binary file added docs/screenshots/model-aliases.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions docs/user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 (✗) |

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.1.1-SNAPSHOT</version>
<version>1.1.3-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 @@ -153,30 +153,42 @@ 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());

JPanel btnPanel = new JPanel();
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);
Expand Down Expand Up @@ -326,6 +338,16 @@ private void moveRow(int delta) {
table.setRowSelectionInterval(target, target);
}

private void onSort() {
stopEditing();
List<ModelAlias> 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();
Expand All @@ -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<String> 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<String> 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<ModelAlias> models = collectModels();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> ids = ModelAliasesDialog.parseModelIds(json);
assertEquals(List.of("claude-haiku-4-5"), ids);
}

@Test
void parseModelIds_noDataKey_stillFindsIds() {
String json = "{\"id\":\"model-x\"}";
List<String> 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));
}
}
Loading