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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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")
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.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.22.33-SNAPSHOT</version>
<version>0.22.34-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans 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 @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,47 @@ void discoverModels() {
"Blocking dialog must be shown to the user while discovery is deferred");
}

// -------------------------------------------------------------------------
// isModelCommandPendingWithBlankLine
// -------------------------------------------------------------------------

@Test
void isModelCommandPendingWithBlankLine_detectsNewlineInsertedByEnter() {
List<String> 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<String> lines = List.of(
" ❯ /model",
" ─────────────────────────────────────────────────────────────"
);
assertFalse(controller.isModelCommandPendingWithBlankLine(lines));
}

@Test
void isModelCommandPendingWithBlankLine_falseWhenModelInHistory() {
List<String> lines = List.of(
" You used /model earlier",
"",
" ─────────────────────────────────────────────────────────────",
" ❯ "
);
assertFalse(controller.isModelCommandPendingWithBlankLine(lines));
}

// -------------------------------------------------------------------------
// No-op listener base class
// -------------------------------------------------------------------------
Expand Down
Loading