From bca2d052f65564fa3c9d9c7106908b41ef66c906 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 4 Oct 2026 16:36:34 +0300 Subject: [PATCH] Fixed the choice dialog (e.g. /model) endlessly flickering and resizing when not all menu items fit on screen Scrolling pickers draw as many items as fit the terminal; the choice panel shrinks the terminal, so Claude drew fewer items, which shrank the panel and grew the terminal again. A newly detected menu with the same question whose options are a strict prefix of the active menu's options is now treated as the same menu, so the larger option list is kept. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../controller/ClaudeSessionController.java | 24 ++++++++++++++- .../ClaudeSessionControllerTest.java | 30 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c3747..65b97bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed the choice dialog (e.g. /model) endlessly flickering and resizing when not all menu items fit on screen # 1.3.20 (2026-10-04) - Fixed switching the edit mode (e.g. to Auto Mode) while a prompt was still running sometimes cycling through several modes and landing on the wrong one instead of the one selected diff --git a/pom.xml b/pom.xml index 28f763b..55df72d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.3.37-SNAPSHOT + 1.3.38-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/nbplugins/claudecodegui/controller/ClaudeSessionController.java b/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java index 4616523..839c9c6 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java @@ -1438,7 +1438,7 @@ private void applyDetectedChoiceMenu(ChoiceMenuModel newMenu, String logTag) { model.setActiveChoiceMenu(newMenu); return; } - if (sameMenu(newMenu, current)) { + if (sameMenu(newMenu, current) || isShrunkViewOf(newMenu, current)) { pendingChoiceMenuCandidate = null; return; } @@ -1452,6 +1452,28 @@ private void applyDetectedChoiceMenu(ChoiceMenuModel newMenu, String logTag) { } } + /** + * Returns {@code true} when {@code newMenu} is just a shorter window onto the same + * scrolling list as {@code current} (same question, options are a strict prefix). + * + *

Scrolling pickers such as {@code /model} draw as many items as fit the terminal. + * The choice panel shrinks the terminal, which makes Claude draw fewer items, which + * shrinks the panel, which grows the terminal again — an endless flicker. Keeping + * the larger option list breaks that loop. + */ + static boolean isShrunkViewOf(ChoiceMenuModel newMenu, ChoiceMenuModel current) { + List n = newMenu.options(); + List c = current.options(); + if (!newMenu.text().equals(current.text()) || n.size() >= c.size()) return false; + for (int i = 0; i < n.size(); i++) { + if (!n.get(i).display().equals(c.get(i).display()) + || !n.get(i).response().equals(c.get(i).response())) { + return false; + } + } + return true; + } + private static boolean sameMenu(ChoiceMenuModel a, ChoiceMenuModel b) { return a.text().equals(b.text()) && optionsEqual(a.options(), b.options()); } diff --git a/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java b/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java index b8bffab..b91b6dc 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java @@ -38,6 +38,36 @@ void setUp() { // onClaudeIdle // ------------------------------------------------------------------------- + private static ChoiceMenuModel menu(String text, String... displays) { + List opts = new java.util.ArrayList<>(); + for (int i = 0; i < displays.length; i++) { + opts.add(new ChoiceMenuModel.Option(displays[i], String.valueOf(i + 1))); + } + return new ChoiceMenuModel(text, opts, 0); + } + + @Test + void isShrunkViewOf_prefixOfScrollingList_true() { + ChoiceMenuModel full = menu("Select model", "a", "b", "c", "d"); + ChoiceMenuModel shrunk = menu("Select model", "a", "b", "c"); + assertTrue(ClaudeSessionController.isShrunkViewOf(shrunk, full)); + } + + @Test + void isShrunkViewOf_sameSizeOrGrown_false() { + ChoiceMenuModel three = menu("Select model", "a", "b", "c"); + ChoiceMenuModel four = menu("Select model", "a", "b", "c", "d"); + assertFalse(ClaudeSessionController.isShrunkViewOf(three, three)); + assertFalse(ClaudeSessionController.isShrunkViewOf(four, three)); + } + + @Test + void isShrunkViewOf_differentTextOrOptions_false() { + ChoiceMenuModel full = menu("Select model", "a", "b", "c", "d"); + assertFalse(ClaudeSessionController.isShrunkViewOf(menu("Other", "a", "b", "c"), full)); + assertFalse(ClaudeSessionController.isShrunkViewOf(menu("Select model", "a", "x", "c"), full)); + } + @Test void onClaudeIdleSetsLifecycleToReady() { model.setLifecycle(SessionLifecycle.WORKING);