diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dc83af4..9e90ce75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +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 "Yes, and tell Claude what to do next" option in bash permission prompts: now shows radio label "Yes" with input hint in the text field (not as a subtitle), and allows sending with empty text
- 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 51d807c5..3822d397 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.22.43-SNAPSHOT
+ 0.22.46-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 17efdbe4..22dfc5e6 100644
--- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
+++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
@@ -515,12 +515,13 @@ public void writePtyAnswer(String answer) {
int sep = answer.indexOf(':', 5);
String digit = answer.substring(5, sep);
String text = answer.substring(sep + 1);
- ChoiceMenuModel current = model.getActiveChoiceMenu();
+ ChoiceMenuModel current = detectCurrentMenu();
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;
+ // currentIdx = actual PTY cursor position (re-read from screen); currentHasTextInput
+ // = cursor is on a text-input option (digits go to its text field, not to navigation).
+ final int currentIdx = current != null ? current.defaultOptionIndex() : targetIdx;
+ boolean currentHasTextInput = current != null
+ && isTextInputOption(current.options().get(currentIdx));
LOG.fine("[PTY write] type-input digit=" + digit + " text=" + text
+ " currentHasTextInput=" + currentHasTextInput);
Thread t = new Thread(() -> {
@@ -582,17 +583,16 @@ public void writePtyAnswer(String answer) {
t.start();
}
} else {
- 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);
+ ChoiceMenuModel current = detectCurrentMenu();
+ // currentHasTextInput = PTY cursor (❯) is on a text-input option (re-read from
+ // screen); digits go into its text field, so use arrow-key navigation instead.
+ int currentIdx2 = current != null ? current.defaultOptionIndex() : 0;
+ boolean currentHasTextInput = current != null
+ && isTextInputOption(current.options().get(currentIdx2));
+ LOG.fine("[PTY write] else-branch answer=" + answer + " currentIdx=" + currentIdx2 + " currentHasTextInput=" + currentHasTextInput);
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 currentIdx = currentIdx2;
int delta = targetIdx - currentIdx;
LOG.fine("[PTY write] ARROW-digit targetIdx=" + targetIdx + " currentIdx=" + currentIdx + " delta=" + delta);
model.clearChoiceMenu();
@@ -1260,14 +1260,10 @@ 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;
+ /** Re-reads the current screen to get the up-to-date choice menu (PTY cursor may have moved). */
+ private ChoiceMenuModel detectCurrentMenu() {
+ List lines = screenLines.get();
+ return screenContentDetector.detectChoiceMenu(lines).orElse(model.getActiveChoiceMenu());
}
private static boolean optionsEqual(List a, List b) {
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java
index d103965e..f66d51c6 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java
@@ -177,8 +177,12 @@ public void show(ChoiceMenuModel model, Consumer callback, boolean grabF
leftCol.add(typeRow);
} else if (isTypeInputOption(opt)) {
- // Type-input: radio button + adjacent text field
- JRadioButton rb = new JRadioButton(" ");
+ // Type-input: radio button + adjacent text field.
+ // When description is set, display is a label ("Yes") and description is
+ // the input hint ("and tell Claude what to do next"). Otherwise display
+ // itself is the hint ("Type something.") and the radio gets a blank label.
+ boolean hasLabel = opt.description() != null && !opt.description().isBlank();
+ JRadioButton rb = new JRadioButton(hasLabel ? opt.display().trim() : " ");
rb.setAlignmentX(Component.LEFT_ALIGNMENT);
rb.setName("typeInputRb");
group.add(rb);
@@ -188,7 +192,8 @@ public void show(ChoiceMenuModel model, Consumer callback, boolean grabF
tf.setMaximumSize(new java.awt.Dimension(
Integer.MAX_VALUE, tf.getPreferredSize().height));
tf.setEnabled(false);
- setPlaceholder(tf, opt.display().trim());
+ String hintText = hasLabel ? opt.description().trim() : opt.display().trim();
+ setPlaceholder(tf, hintText);
rb.addChangeListener(e -> tf.setEnabled(rb.isSelected()));
tf.addFocusListener(new java.awt.event.FocusAdapter() {
@Override public void focusGained(java.awt.event.FocusEvent e) {
@@ -221,8 +226,8 @@ public void show(ChoiceMenuModel model, Consumer callback, boolean grabF
leftCol.add(rb);
}
- // Description subtitle
- if (opt.description() != null && !opt.description().isBlank()) {
+ // Description subtitle — skip for type-input options (description is the field hint)
+ if (opt.description() != null && !opt.description().isBlank() && !isTypeInputOption(opt)) {
JLabel desc = new JLabel("" + escapeHtml(opt.description()) + "");
desc.setForeground(Color.GRAY);
desc.setAlignmentX(Component.LEFT_ALIGNMENT);
@@ -313,14 +318,16 @@ public void keyPressed(java.awt.event.KeyEvent e) {
if (rb != null && rb.isSelected()) {
ChoiceMenuModel.Option opt = otherOptions.get(i);
if (isTypeInputOption(opt) && finalTypeFields[i] != null) {
- String hint = opt.display().trim();
+ boolean hasLabel = opt.description() != null && !opt.description().isBlank();
+ String hint = hasLabel ? opt.description().trim() : opt.display().trim();
String typed = finalTypeFields[i].getText().trim();
if (!typed.isEmpty() && !typed.equals(hint)) {
- // Bug 4: Claude needs the option number sent first to activate
- // text-entry mode, then the typed text + \r.
// Encode as "TYPE:N:text" for writePtyAnswer to handle.
int optNum = finalModel.options().indexOf(opt) + 1;
submitAnswer("TYPE:" + optNum + ":" + typed);
+ } else if (hasLabel) {
+ // "Yes" label with no text typed — select the option with no text.
+ submitAnswer(opt.response());
}
} else {
submitAnswer(opt.response());