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