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());