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 // -------------------------------------------------------------------------