diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3f2aa9b..ce97ff50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,5 @@
+- Fixed model discovery failing when Enter was absorbed as a newline in the prompt instead of submitting the /model command (race condition under load)
+
# 0.22.24 (2026-04-29)
- Fixed model name detection for new Claude CLI format where the default item has a long label ("Default (recommended)") with only 2-space alignment, and for model names that include a space (e.g. "Sonnet 4")
diff --git a/pom.xml b/pom.xml
index 72996e5a..34903f32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.22.33-SNAPSHOT
+ 0.22.34-SNAPSHOT
nbm
Netbeans 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/nbclaudecodegui/controller/ClaudeSessionController.java b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
index d504edbc..7f4e92da 100644
--- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
+++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
@@ -59,6 +59,9 @@ public class ClaudeSessionController {
/** ESC byte sent to the PTY (dismiss autocomplete, close menus). */
private static final byte[] PTY_ESC = {0x1b};
+ /** DEL character (0x7F) — acts as BackSpace in xterm-256color terminals. */
+ private static final byte[] PTY_BACKSPACE = {0x7f};
+
private static final int MAX_MODEL_DISCOVERY_ATTEMPTS = 1;
private final ClaudeSessionModel model;
@@ -803,6 +806,14 @@ void discoverModels() {
LOG.fine("[discoverModels] ModelMenuParser found models on poll " + (poll + 1));
break;
}
+ // If ❯ /model is still in the prompt with a blank line below it, CR was
+ // absorbed as a newline — retry: BackSpace to remove the blank line, then CR.
+ if (poll < 12 && isModelCommandPendingWithBlankLine(lines)) {
+ LOG.fine("[discoverModels] /model pending with blank line on poll " + (poll + 1) + ", retrying CR");
+ connector.write(PTY_BACKSPACE);
+ Thread.sleep(200);
+ connector.write("\r");
+ }
}
// Scan the rolling PTY line buffer for the /model hint line, which is
@@ -1261,6 +1272,30 @@ private void startStatusPollTimer() {
statusTimer.start();
}
+ /**
+ * Returns {@code true} when the screen shows {@code ❯ /model} followed by a blank line
+ * and then a separator — meaning CR was absorbed as a newline instead of submitting the
+ * command. Only the active input area (cursor line) is checked to avoid false positives
+ * when "/model" appears in Claude's output history.
+ */
+ boolean isModelCommandPendingWithBlankLine(List lines) {
+ for (int i = 0; i < lines.size() - 2; i++) {
+ String trimmed = lines.get(i).trim();
+ if ((trimmed.startsWith("❯") || trimmed.startsWith(">")) && trimmed.contains("/model")) {
+ if (lines.get(i + 1).isBlank() && isSeparatorLine(lines.get(i + 2))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isSeparatorLine(String line) {
+ String t = line.trim();
+ return t.length() >= 4 && t.chars().allMatch(c ->
+ c == '─' || c == '━' || c == '—' || c == '-' || c == '=');
+ }
+
/**
* Opens the {@code /model} selection menu via a safe three-step sequence:
* type {@code /model}, wait, ESC to dismiss autocomplete (keeping the
diff --git a/src/test/java/io/github/nbclaudecodegui/controller/ClaudeSessionControllerTest.java b/src/test/java/io/github/nbclaudecodegui/controller/ClaudeSessionControllerTest.java
index b11dd065..9f82b6e9 100644
--- a/src/test/java/io/github/nbclaudecodegui/controller/ClaudeSessionControllerTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/controller/ClaudeSessionControllerTest.java
@@ -661,6 +661,47 @@ void discoverModels() {
"Blocking dialog must be shown to the user while discovery is deferred");
}
+ // -------------------------------------------------------------------------
+ // isModelCommandPendingWithBlankLine
+ // -------------------------------------------------------------------------
+
+ @Test
+ void isModelCommandPendingWithBlankLine_detectsNewlineInsertedByEnter() {
+ List lines = List.of(
+ " ● Built successfully. Install target/nbm/foo.nbm and test.",
+ "",
+ " ─────────────────────────────────────────────────────────────",
+ " ──────── fix-kotlin-extensions-nosuchmethoderror ──",
+ " ❯ /model",
+ "",
+ " ─────────────────────────────────────────────────────────────",
+ " ─────────────────────────────────────────────────────────────",
+ " ─────────────────────────────────",
+ " ⏵⏵ bypass permissions on (shift+tab to cycle)"
+ );
+ assertTrue(controller.isModelCommandPendingWithBlankLine(lines));
+ }
+
+ @Test
+ void isModelCommandPendingWithBlankLine_falseWhenNoBlankAfterModel() {
+ List lines = List.of(
+ " ❯ /model",
+ " ─────────────────────────────────────────────────────────────"
+ );
+ assertFalse(controller.isModelCommandPendingWithBlankLine(lines));
+ }
+
+ @Test
+ void isModelCommandPendingWithBlankLine_falseWhenModelInHistory() {
+ List lines = List.of(
+ " You used /model earlier",
+ "",
+ " ─────────────────────────────────────────────────────────────",
+ " ❯ "
+ );
+ assertFalse(controller.isModelCommandPendingWithBlankLine(lines));
+ }
+
// -------------------------------------------------------------------------
// No-op listener base class
// -------------------------------------------------------------------------