diff --git a/CHANGELOG.md b/CHANGELOG.md index cd89fd90..ab025708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added a setting to control whether the choice menu grabs keyboard focus (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/20) - Added a global setting to choose the default dock position of the Claude Code session tab (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/24) # 0.21.50 (2026-04-19) diff --git a/pom.xml b/pom.xml index a40b692c..fdd7e86e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.6-SNAPSHOT + 0.22.7-SNAPSHOT nbm Netbeans 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/settings/ClaudeCodeOptionsPanel.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java index 6806966a..320e5744 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodeOptionsPanel.java @@ -40,6 +40,15 @@ */ public final class ClaudeCodeOptionsPanel extends JPanel { + private static final String[] CHOICE_MENU_FOCUS_VALUES = { + ClaudeCodePreferences.CHOICE_MENU_GRAB_FOCUS, + ClaudeCodePreferences.CHOICE_MENU_SHOW_NO_FOCUS, + ClaudeCodePreferences.CHOICE_MENU_HIDE_MENU + }; + private static final String[] CHOICE_MENU_FOCUS_LABELS = { + "Grab focus (default)", "Show without grabbing focus", "Hide menu" + }; + private static final String[] DOCK_MODE_VALUES = { "editor", "commonpalette", "explorer", "navigator", "output" }; @@ -77,6 +86,8 @@ public final class ClaudeCodeOptionsPanel extends JPanel { private JSpinner sessionListLimitSpinner; /** Dropdown for the dock position of the Claude Code session tab. */ private javax.swing.JComboBox dockModeCombo; + /** Dropdown for the choice menu focus behavior. */ + private javax.swing.JComboBox choiceMenuFocusCombo; /** Spinner for the hang-detection timeout in seconds (0 = disabled). */ private JSpinner hangTimeoutSpinner; /** Checkbox to enable MCP integration (pass --mcp-config flag). */ @@ -238,6 +249,13 @@ private JPanel buildGeneralPanel() { form.add(dockModeCombo, gbc(1, row, false)); row++; + // --- choice menu focus mode --- + form.add(new JLabel("Choice menu focus:"), gbc(0, row, false)); + choiceMenuFocusCombo = new javax.swing.JComboBox<>(CHOICE_MENU_FOCUS_LABELS); + choiceMenuFocusCombo.setToolTipText("Controls whether the choice menu grabs keyboard focus when it appears"); + form.add(choiceMenuFocusCombo, gbc(1, row, false)); + row++; + // spacer GridBagConstraints spacer = new GridBagConstraints(); spacer.gridx = 0; spacer.gridy = row; @@ -376,6 +394,9 @@ void load() { String dockMode = ClaudeCodePreferences.getSessionDockMode(); int dockIdx = java.util.Arrays.asList(DOCK_MODE_VALUES).indexOf(dockMode); dockModeCombo.setSelectedIndex(dockIdx >= 0 ? dockIdx : 0); + String focusMode = ClaudeCodePreferences.getChoiceMenuFocusMode(); + int focusIdx = java.util.Arrays.asList(CHOICE_MENU_FOCUS_VALUES).indexOf(focusMode); + choiceMenuFocusCombo.setSelectedIndex(focusIdx >= 0 ? focusIdx : 0); hangTimeoutSpinner.setValue(ClaudeCodePreferences.getHangTimeoutSeconds()); mcpEnabledCheckBox.setSelected(ClaudeCodePreferences.isMcpEnabled()); @@ -415,6 +436,11 @@ void store() { dockSel >= 0 && dockSel < DOCK_MODE_VALUES.length ? DOCK_MODE_VALUES[dockSel] : ClaudeCodePreferences.DEFAULT_SESSION_DOCK_MODE); + int focusSel = choiceMenuFocusCombo.getSelectedIndex(); + ClaudeCodePreferences.setChoiceMenuFocusMode( + focusSel >= 0 && focusSel < CHOICE_MENU_FOCUS_VALUES.length + ? CHOICE_MENU_FOCUS_VALUES[focusSel] + : ClaudeCodePreferences.DEFAULT_CHOICE_MENU_FOCUS_MODE); ClaudeCodePreferences.setHangTimeoutSeconds((Integer) hangTimeoutSpinner.getValue()); ClaudeCodePreferences.setMcpEnabled(mcpEnabledCheckBox.isSelected()); ClaudeCodePreferences.setSendKey(selectedValue(sendRadios)); diff --git a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java index beb0ae05..314350f5 100644 --- a/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java +++ b/src/main/java/io/github/nbclaudecodegui/settings/ClaudeCodePreferences.java @@ -570,6 +570,31 @@ public static void setSessionDockMode(String mode) { .put(KEY_SESSION_DOCK_MODE, mode != null ? mode : DEFAULT_SESSION_DOCK_MODE); } + // ------------------------------------------------------------------------- + // choiceMenuFocusMode + // ------------------------------------------------------------------------- + + /** Preference key: focus behavior when the choice menu appears. */ + public static final String KEY_CHOICE_MENU_FOCUS_MODE = "choiceMenuFocusMode"; + /** Value: grab keyboard focus (current / default behavior). */ + public static final String CHOICE_MENU_GRAB_FOCUS = "grab_focus"; + /** Value: show the panel without grabbing keyboard focus. */ + public static final String CHOICE_MENU_SHOW_NO_FOCUS = "show_no_focus"; + /** Value: do not show the choice menu panel at all. */ + public static final String CHOICE_MENU_HIDE_MENU = "hide_menu"; + /** Default: grab focus. */ + public static final String DEFAULT_CHOICE_MENU_FOCUS_MODE = CHOICE_MENU_GRAB_FOCUS; + + public static String getChoiceMenuFocusMode() { + return NbPreferences.forModule(ClaudeCodePreferences.class) + .get(KEY_CHOICE_MENU_FOCUS_MODE, DEFAULT_CHOICE_MENU_FOCUS_MODE); + } + + public static void setChoiceMenuFocusMode(String mode) { + NbPreferences.forModule(ClaudeCodePreferences.class) + .put(KEY_CHOICE_MENU_FOCUS_MODE, mode != null ? mode : DEFAULT_CHOICE_MENU_FOCUS_MODE); + } + private static String validated(String value, String fallback) { return ENTER.equals(value) || SHIFT_ENTER.equals(value) || CTRL_ENTER.equals(value) || ALT_ENTER.equals(value) diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java index e381d929..a782b8ea 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanel.java @@ -62,7 +62,7 @@ public ChoiceMenuPanel(Supplier workingDirSupplier) { * @param model the menu model * @param callback invoked with the chosen response string, or {@code null} on Cancel */ - public void show(ChoiceMenuModel model, Consumer callback) { + public void show(ChoiceMenuModel model, Consumer callback, boolean grabFocus) { this.callback = callback; removeAll(); @@ -520,7 +520,7 @@ public void keyPressed(java.awt.event.KeyEvent e) { } else { focusTarget = sendBtn; } - if (focusTarget != null) { + if (grabFocus && focusTarget != null) { javax.swing.SwingUtilities.invokeLater(focusTarget::requestFocusInWindow); } } diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java index ddbec957..b23aed89 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -489,13 +489,18 @@ public void onChoiceMenuChanged(ChoiceMenuModel menu) { if (menu != null) { LOG.fine(sessionTag + "[onChoiceMenuChanged] showing menu: \"" + menu.text() + "\" EDT=" + SwingUtilities.isEventDispatchThread()); + String focusMode = io.github.nbclaudecodegui.settings.ClaudeCodePreferences.getChoiceMenuFocusMode(); + if (io.github.nbclaudecodegui.settings.ClaudeCodePreferences.CHOICE_MENU_HIDE_MENU.equals(focusMode)) { + return; + } + boolean grabFocus = io.github.nbclaudecodegui.settings.ClaudeCodePreferences.CHOICE_MENU_GRAB_FOCUS.equals(focusMode); choiceMenuPanel.show(menu, answer -> { LOG.fine(sessionTag + "[PTY prompt answer] " + answer); SwingUtilities.invokeLater(() -> switchSouthCard(CARD_PROMPT)); revalidate(); repaint(); controller.writePtyAnswer(answer); - }); + }, grabFocus); switchSouthCard(CARD_CHOICE); } else { if (CARD_CHOICE.equals(activeCard)) { diff --git a/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java b/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java index 3426534c..e6fc022e 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/ChoiceMenuPanelTest.java @@ -40,7 +40,7 @@ void testShowWithOptionsCreatesButtons() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Allow?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertTrue(panel.getComponentCount() > 0, "panel should have components after show()"); @@ -58,7 +58,7 @@ void testYesButtonIsGreen() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JButton btn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(btn, "Yes button must exist"); @@ -72,7 +72,7 @@ void testNoButtonIsRed() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JButton btn = findButton(panel, UiUtils.ICON_CROSS + " No"); assertNotNull(btn, "No button must exist"); @@ -86,7 +86,7 @@ void testRadioButtonForNonYesNoOption() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JRadioButton rb = findRadioButton(panel, "Maybe"); assertNotNull(rb, "Non-Yes/No option should render as JRadioButton"); @@ -98,21 +98,21 @@ void testCancelAlwaysPresent() throws Exception { // With Yes/No only SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}, true); assertNotNull(findButton(panel, "Cancel"), "Cancel must be present with Yes/No options"); }); // With radio options SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}, true); assertNotNull(findButton(panel, "Cancel"), "Cancel must be present with radio options"); }); // Free-form SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(), -1), a -> {}, true); assertNotNull(findButton(panel, "Cancel"), "Cancel must be present for free-form input"); }); } @@ -121,7 +121,7 @@ void testCancelAlwaysPresent() throws Exception { void testSendAbsentWhenOnlyYesNo() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}, true); assertNull(findButton(panel, "Send"), "Send button must NOT be present when only Yes/No options"); }); } @@ -131,7 +131,7 @@ void testSendPresentWhenOtherOptionsExist() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); panel.show(new ChoiceMenuModel("Q?", - List.of(new Option("Yes", "1"), new Option("Maybe", "3")), -1), a -> {}); + List.of(new Option("Yes", "1"), new Option("Maybe", "3")), -1), a -> {}, true); assertNotNull(findButton(panel, "Send"), "Send button must be present when other options exist"); }); } @@ -140,7 +140,7 @@ void testSendPresentWhenOtherOptionsExist() throws Exception { void testSendDisabledUntilRadioSelected() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}, true); JButton send = findButton(panel, "Send"); assertNotNull(send, "Send must exist"); assertFalse(send.isEnabled(), "Send must be disabled when no radio is selected"); @@ -156,7 +156,7 @@ void testSendDisabledUntilRadioSelected() throws Exception { void testSendEnabledWhenDefaultRadioSelected() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "1")), 0), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "1")), 0), a -> {}, true); JButton send = findButton(panel, "Send"); assertNotNull(send, "Send must exist"); assertTrue(send.isEnabled(), "Send must be enabled when default radio is pre-selected"); @@ -167,7 +167,7 @@ void testSendEnabledWhenDefaultRadioSelected() throws Exception { void testDefaultRadioSelected() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "1")), 0), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "1")), 0), a -> {}, true); JRadioButton rb = findRadioButton(panel, "Maybe"); assertNotNull(rb, "JRadioButton for Maybe must exist"); assertTrue(rb.isSelected(), "Radio button for default option should be selected"); @@ -182,7 +182,7 @@ void testButtonClickInvokesCallbackWithResponse() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Allow?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1); - panel.show(model, received::add); + panel.show(model, received::add, true); JButton yesBtn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(yesBtn, "Yes button must exist"); @@ -198,7 +198,7 @@ void testHideAfterButtonClick() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Go?", List.of(new Option("Yes", "1")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JButton btn = findButton(panel, UiUtils.ICON_CHECK + " Yes"); assertNotNull(btn); @@ -219,7 +219,7 @@ void testCancelInvokesCallbackWithNull() throws Exception { panel.show(model, answer -> { if (answer == null) nullReceived.add(true); else received.add(answer); - }); + }, true); JButton cancelBtn = findButton(panel, "Cancel"); assertNotNull(cancelBtn, "Cancel button must exist"); @@ -237,7 +237,7 @@ void testHideMethodHidesPanel() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1")), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertTrue(panel.getComponentCount() > 0, "panel should have components after show()"); panel.dismiss(); @@ -250,7 +250,7 @@ void testShowWithNoOptionsShowsSendAndCancelButtons() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Enter your response:", List.of(), -1); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertTrue(panel.getComponentCount() > 0, "panel should have components after show()"); List btnLabels = collectButtonLabels(panel); @@ -263,7 +263,7 @@ void testShowWithNoOptionsShowsSendAndCancelButtons() throws Exception { void testCancelAndSendAreInRightCol() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Maybe", "3")), -1), a -> {}, true); JPanel rightCol = findPanelByName(panel, "rightCol"); assertNotNull(rightCol, "rightCol panel must exist"); @@ -276,7 +276,7 @@ void testCancelAndSendAreInRightCol() throws Exception { void testCancelInRightColWhenOnlyYesNo() throws Exception { SwingUtilities.invokeAndWait(() -> { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); - panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}); + panel.show(new ChoiceMenuModel("Q?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}, true); JPanel rightCol = findPanelByName(panel, "rightCol"); assertNotNull(rightCol, "rightCol panel must exist"); @@ -297,7 +297,7 @@ void dismissIfActiveFiresNullCallbackImmediately() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Do you want to proceed?", List.of(new Option(" Yes", "1"), new Option(" No", "3")), 0); - panel.show(model, answer -> received.add(String.valueOf(answer))); + panel.show(model, answer -> received.add(String.valueOf(answer)), true); assertTrue(panel.getComponentCount() > 0, "panel must have components after show()"); assertTrue(received.isEmpty(), "callback must not fire before user action"); @@ -322,7 +322,7 @@ void testTypeInputOptionRendersTextField() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Option A", "1"), new Option("Type something.", "2")), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JTextField tf = findTextField(panel); assertNotNull(tf, "panel must contain a JTextField for 'Type something.' option"); @@ -336,7 +336,7 @@ void testTypeInputFieldDisabledByDefault() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Option A", "1"), new Option("Type something.", "2")), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JTextField tf = findTextField(panel); assertNotNull(tf); @@ -352,7 +352,7 @@ void testTypeInputFieldEnabledWhenRadioSelected() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Option A", "1"), new Option("Type something.", "2")), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); JRadioButton typeRb = findRadioButtonByName(panel, "typeInputRb"); assertNotNull(typeRb, "type-input radio button must exist"); @@ -375,7 +375,7 @@ void testTypeInputOptionCallbackIncludesOptionNumber() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Option A", "1"), new Option("Type something.", "2")), 0); - panel.show(model, captured::add); + panel.show(model, captured::add, true); JRadioButton typeRb = findRadioButtonByName(panel, "typeInputRb"); assertNotNull(typeRb, "type-input radio button must exist with name 'typeInputRb'"); @@ -406,7 +406,7 @@ void testTypeInputOptionLayoutIsHorizontal() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Option A", "1"), new Option("Type something.", "2")), 0); - panel.show(model, s -> {}); + panel.show(model, s -> {}, true); JPanel typeInputRow = findPanelByName(panel, "typeInputRow"); assertNotNull(typeInputRow, "typeInputRow panel must exist"); @@ -427,7 +427,7 @@ void testFocusTraversalOrderRadiosThenSendCancel() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose?", List.of(new Option("Option A", "1"), new Option("Option B", "2")), 0); - panel.show(model, s -> {}); + panel.show(model, s -> {}, true); java.awt.FocusTraversalPolicy ftp = panel.getFocusTraversalPolicy(); assertNotNull(ftp, "panel must have an explicit FocusTraversalPolicy"); @@ -454,7 +454,7 @@ void testCheckboxOptionRendersAsJCheckBox() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose releases:", List.of(new Option("Release A", "1", null, false, true), new Option("Release B", "2", null, false, true)), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertNotNull(findCheckBox(panel, "Release A"), "hasCheckbox=true option must render as JCheckBox"); assertNotNull(findCheckBox(panel, "Release B"), "hasCheckbox=true option must render as JCheckBox"); @@ -469,7 +469,7 @@ void testNonCheckboxOptionInMixedMenuRendersAsRadio() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Release A", "1", null, false, true), new Option("Chat about this", "2")), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertNotNull(findCheckBox(panel, "Release A"), "hasCheckbox=true must be JCheckBox"); assertNotNull(findRadioButton(panel, "Chat about this"), "hasCheckbox=false must be JRadioButton"); @@ -482,7 +482,7 @@ void testCheckboxMenuHasSubmitButton() throws Exception { ChoiceMenuPanel panel = new ChoiceMenuPanel(() -> null); ChoiceMenuModel model = new ChoiceMenuModel("Choose releases:", List.of(new Option("Release A", "1", null, false, true)), 0); - panel.show(model, answer -> {}); + panel.show(model, answer -> {}, true); assertNotNull(findButton(panel, "Submit"), "must have Submit button when checkbox options exist"); assertNull(findButton(panel, "Send"), "must NOT have Send button when checkbox options exist"); @@ -497,7 +497,7 @@ void testSubmitButtonSendsMultiResponse() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose releases:", List.of(new Option("Release A", "1", null, false, true), new Option("Release B", "2", null, false, true)), 0); - panel.show(model, captured::add); + panel.show(model, captured::add, true); JCheckBox cb = findCheckBox(panel, "Release A"); assertNotNull(cb); @@ -520,7 +520,7 @@ void testRadioInMixedMenuSendsDirectResponse() throws Exception { ChoiceMenuModel model = new ChoiceMenuModel("Choose:", List.of(new Option("Release A", "1", null, false, true), new Option("Chat about this", "6")), 0); - panel.show(model, captured::add); + panel.show(model, captured::add, true); JRadioButton rb = findRadioButton(panel, "Chat about this"); assertNotNull(rb, "Chat about this must be a radio button"); @@ -553,7 +553,7 @@ void preferredHeightIsZeroBeforeShowCausesInvisiblePanel() throws Exception { int heightBefore = panel.getPreferredSize().height; panel.show(new ChoiceMenuModel("Allow?", - List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}); + List.of(new Option("Yes", "1"), new Option("No", "2")), -1), a -> {}, true); int heightAfter = panel.getPreferredSize().height; @@ -607,7 +607,7 @@ void preferredHeightAfterSplitPaneValidateCoversAllContent() throws Exception { List.of(new Option("Yes", "1"), new Option("Yes, and allow Claude to edit its own settings for this session", "2"), new Option("No", "3")), -1), - answer -> {}); + answer -> {}, true); cardLayout.show(southCard, "choice"); // Simulate lockDividerForChoiceMenu WITH validate() (the fix) @@ -670,7 +670,7 @@ void splitPaneRemainsEnabledWhenChoiceMenuShown() throws Exception { try { choiceMenuPanel.show(new ChoiceMenuModel("Continue?", List.of(new Option("Yes", "1"), new Option("No", "2")), -1), - answer -> {}); + answer -> {}, true); cardLayout.show(southCard, "choice"); // Simulate the production switchSouthCard(CARD_CHOICE) path: