From 5aa55c22b3c797f74b45972c4c1bb0d12aa65ef2 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 3 May 2026 22:53:41 +0300 Subject: [PATCH] Fixed digit input routing into text field in choice menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In planning and permission-prompt menus with a "Type something." or "Yes, and tell Claude what to do next" option, Claude Code routes all digit keystrokes to the text field regardless of which option the ❯ cursor is on. The plugin now detects any text-input option via findTextInputIdx() and uses arrow-key navigation (ESC[A/ESC[B + Enter) instead of sending the raw digit. Also fixed: "Yes, and tell Claude what to do next" was not recognized as a text-input option because its menu type lacks the shift+tab hint line. Added AND_TELL_CLAUDE_PATTERN in ScreenContentDetector.extractOption() to split such options into display="Yes" + description + hasTextInput=true. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 + pom.xml | 2 +- .../controller/ClaudeSessionController.java | 94 ++++++++++++++++--- .../process/ScreenContentDetector.java | 16 ++++ .../permission-prompt-bash.expectedRes.json | 9 ++ .../permission-prompt-bash.src.txt | 12 +++ 6 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.expectedRes.json create mode 100644 src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.src.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b07065c..6dc83af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- Fixed digits going into the "Type something." text field instead of selecting the intended numbered option in planning and permission-prompt choice menus +- Fixed "Yes, and tell Claude what to do next" option not showing a text input field in bash permission prompts - Fixed session tab showing the old project display name after IDE restart (tab now refreshes its name once all open projects finish loading) - Fixed model discovery failing when Enter was absorbed as a newline in the prompt instead of submitting the /model command (race condition under load) diff --git a/pom.xml b/pom.xml index a9243be5..51d807c5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.39-SNAPSHOT + 0.22.43-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/nbclaudecodegui/controller/ClaudeSessionController.java b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java index 7f4e92da..17efdbe4 100644 --- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java @@ -515,14 +515,36 @@ public void writePtyAnswer(String answer) { int sep = answer.indexOf(':', 5); String digit = answer.substring(5, sep); String text = answer.substring(sep + 1); - LOG.fine("[PTY write] type-input digit=" + digit + " text=" + text); + ChoiceMenuModel current = model.getActiveChoiceMenu(); + int targetIdx = Integer.parseInt(digit) - 1; + // Scan for the text-input option index (same logic as plain-digit branch) + int typeIdx = findTextInputIdx(current); + final int currentIdx = typeIdx >= 0 ? typeIdx : targetIdx; + boolean currentHasTextInput = typeIdx >= 0; + LOG.fine("[PTY write] type-input digit=" + digit + " text=" + text + + " currentHasTextInput=" + currentHasTextInput); Thread t = new Thread(() -> { try { - connector.write(digit); - Thread.sleep(200); - connector.write(text); - Thread.sleep(200); - connector.write("\r"); + if (currentHasTextInput) { + int delta = targetIdx - currentIdx; + if (delta != 0) { + byte[] arrow = delta < 0 ? new byte[]{0x1b, '[', 'A'} : new byte[]{0x1b, '[', 'B'}; + for (int i = 0; i < Math.abs(delta); i++) { + connector.write(arrow); + Thread.sleep(80); + } + Thread.sleep(100); + } + connector.write(text); + Thread.sleep(200); + connector.write("\r"); + } else { + connector.write(digit); + Thread.sleep(200); + connector.write(text); + Thread.sleep(200); + connector.write("\r"); + } } catch (IOException ex) { LOG.warning("writePtyAnswer TYPE write failed: " + ex.getMessage()); } catch (InterruptedException ie) { @@ -560,11 +582,47 @@ public void writePtyAnswer(String answer) { t.start(); } } else { - boolean isMenuDigit = answer.matches("[0-9]"); - String toWrite = isMenuDigit ? answer : answer + "\r"; - LOG.fine("[PTY write] " + toWrite.replace("\r", "\\r").replace("\n", "\\n")); - connector.write(toWrite); - model.clearChoiceMenu(); + ChoiceMenuModel current = model.getActiveChoiceMenu(); + // Find the text-input option index (if any). Claude Code routes ALL digit input + // to the text field regardless of which option the ❯ cursor is on, so we must + // check the whole option list, not just defaultOptionIndex. + int textInputIdx = findTextInputIdx(current); + boolean currentHasTextInput = textInputIdx >= 0; + LOG.fine("[PTY write] else-branch answer=" + answer + " textInputIdx=" + textInputIdx); + if (currentHasTextInput && answer.matches("[0-9]")) { + // Claude Code routes digits to the text-input field — navigate with arrow keys instead + int targetIdx = answer.charAt(0) - '1'; + int currentIdx = textInputIdx; // assume cursor is at the text-input option + int delta = targetIdx - currentIdx; + LOG.fine("[PTY write] ARROW-digit targetIdx=" + targetIdx + " currentIdx=" + currentIdx + " delta=" + delta); + model.clearChoiceMenu(); + if (delta == 0) { + connector.write("\r"); + } else { + byte[] arrow = delta < 0 ? new byte[]{0x1b, '[', 'A'} : new byte[]{0x1b, '[', 'B'}; + Thread t = new Thread(() -> { + try { + for (int i = 0; i < Math.abs(delta); i++) { + connector.write(arrow); + Thread.sleep(80); + } + connector.write("\r"); + } catch (IOException ex) { + LOG.warning("writePtyAnswer ARROW-digit write failed: " + ex.getMessage()); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + }, "pty-arrow-digit"); + t.setDaemon(true); + t.start(); + } + } else { + boolean isMenuDigit = answer.matches("[0-9]"); + String toWrite = isMenuDigit ? answer : answer + "\r"; + LOG.fine("[PTY write] " + toWrite.replace("\r", "\\r").replace("\n", "\\n")); + connector.write(toWrite); + model.clearChoiceMenu(); + } } } catch (IOException ex) { LOG.warning("writePtyAnswer failed: " + ex.getMessage()); @@ -1198,6 +1256,20 @@ void flushPendingPrompt() { * *

For numbered menus, full {@link java.util.Objects#equals} semantics apply. */ + private static boolean isTextInputOption(ChoiceMenuModel.Option opt) { + return opt.hasTextInput() || opt.display().trim().toLowerCase().startsWith("type"); + } + + /** Returns the 0-based index of the first text-input option in the menu, or -1 if none. */ + private static int findTextInputIdx(ChoiceMenuModel menu) { + if (menu == null) return -1; + List opts = menu.options(); + for (int i = 0; i < opts.size(); i++) { + if (isTextInputOption(opts.get(i))) return i; + } + return -1; + } + private static boolean optionsEqual(List a, List b) { if (a.size() != b.size()) return false; if (!a.isEmpty() && a.get(0).response().startsWith("ARROW:")) { diff --git a/src/main/java/io/github/nbclaudecodegui/process/ScreenContentDetector.java b/src/main/java/io/github/nbclaudecodegui/process/ScreenContentDetector.java index 4bf4a3a4..d0931722 100644 --- a/src/main/java/io/github/nbclaudecodegui/process/ScreenContentDetector.java +++ b/src/main/java/io/github/nbclaudecodegui/process/ScreenContentDetector.java @@ -71,6 +71,15 @@ public void setSessionTag(String tag) { private static final Pattern PLAN_NAME_DASHES_PATTERN = Pattern.compile("\u2500{2,}\\s+([\\w][\\w\\-]*)\\s+\u2500+"); + /** + * Matches permission-prompt options that embed a text-input hint after a comma: + * "Yes, and tell Claude what to do next" \u2192 group 1 = "Yes", group 2 = "and tell Claude what to do next". + * The phrase "tell Claude" is the key discriminator: it indicates the user must supply text, + * unlike "Yes, and don't ask again for: ..." which is a normal boolean option. + */ + private static final Pattern AND_TELL_CLAUDE_PATTERN = + Pattern.compile("^(.*?),\\s+(and tell Claude\\b.*)$", Pattern.CASE_INSENSITIVE); + // ------------------------------------------------------------------------- // Session state // ------------------------------------------------------------------------- @@ -736,6 +745,13 @@ static ChoiceMenuModel.Option extractOption(String line, int index) { checked = true; afterDot = afterDot.substring(3).stripLeading(); } + // "Yes, and tell Claude what to do next" → label + text-input hint + Matcher tellMatcher = AND_TELL_CLAUDE_PATTERN.matcher(afterDot); + if (tellMatcher.matches()) { + return new ChoiceMenuModel.Option( + tellMatcher.group(1).strip(), String.valueOf(num), + tellMatcher.group(2).strip(), checked, hasCheckbox, true); + } return new ChoiceMenuModel.Option(afterDot.strip(), String.valueOf(num), null, checked, hasCheckbox); } diff --git a/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.expectedRes.json b/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.expectedRes.json new file mode 100644 index 00000000..ff97953a --- /dev/null +++ b/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.expectedRes.json @@ -0,0 +1,9 @@ +{ + "text": "Do you want to proceed?", + "options": [ + {"display": "Yes", "response": "1", "description": "and tell Claude what to do next", "hasTextInput": true}, + {"display": "Yes, and don't ask again for: javap -classpath lib/intellij-core-1.0-PATCHED.jar -c com.intellij.util.containers.ContainerUtil 2>&1 | grep -A 10 \"createConcurrentWeakMap\"", "response": "2"}, + {"display": "No", "response": "3"} + ], + "defaultOptionIndex": 0 +} \ No newline at end of file diff --git a/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.src.txt b/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.src.txt new file mode 100644 index 00000000..bc023839 --- /dev/null +++ b/src/test/resources/io/github/nbclaudecodegui/process/screen-content-detector/detect-choice-menu/permission-prompt-bash.src.txt @@ -0,0 +1,12 @@ +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Bash command + + javap -classpath lib/intellij-core-1.0-PATCHED.jar -c com.intellij.util.containers.ContainerUtil 2>&1 | grep -A 10 "createConcurrentWeakMap" + Run shell command + + Do you want to proceed? + ❯ 1. Yes, and tell Claude what to do next + 2. Yes, and don't ask again for: javap -classpath lib/intellij-core-1.0-PATCHED.jar -c com.intellij.util.containers.ContainerUtil 2>&1 | grep -A 10 "createConcurrentWeakMap" + 3. No + + Esc to cancel · ctrl+e to explain \ No newline at end of file