From a3b688565bc1968f542a5136527c9495e1bdc801 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 4 Oct 2026 19:25:11 +0300 Subject: [PATCH] Fixed the choice dialog (e.g. /model) flickering when the scrolled menu does not start from the first item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scroll indicator row (e.g. "↑ 8. Fable 5") is detected as the question and changes with the scroll position, so a shorter window of the same list was seen as a different menu. A menu is now treated as a shrunk view of the active one when both questions are scroll indicators (or equal) and every option matches an option of the active menu by response and display. Co-Authored-By: Claude Sonnet 5.5 --- pom.xml | 2 +- .../controller/ClaudeSessionController.java | 32 +++++++++++++------ .../ClaudeSessionControllerTest.java | 23 +++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 55df72d..63d2ccb 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.3.38-SNAPSHOT + 1.3.39-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 839c9c6..ae40e52 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java @@ -1454,26 +1454,40 @@ 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 list as {@code current}: same question (or both "question" lines are + * scroll indicators such as {@code ↑ 8. Fable 5}, which change with the scroll + * position) and every option matches an option of {@code current} by response and + * display, with fewer options in total. * *

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. + * The choice panel shrinks the terminal, which makes Claude draw fewer items (and + * move the scroll window), 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; + if (n.size() >= c.size()) return false; + boolean sameText = newMenu.text().equals(current.text()) + || (isScrollIndicator(newMenu.text()) && isScrollIndicator(current.text())); + if (!sameText) return false; + for (ChoiceMenuModel.Option no : n) { + boolean found = false; + for (ChoiceMenuModel.Option co : c) { + if (no.display().equals(co.display()) && no.response().equals(co.response())) { + found = true; + break; + } } + if (!found) return false; } return true; } + private static boolean isScrollIndicator(String text) { + return text.startsWith("\u2191") || text.startsWith("\u2193"); + } + 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 b91b6dc..7da432e 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionControllerTest.java @@ -68,6 +68,29 @@ void isShrunkViewOf_differentTextOrOptions_false() { assertFalse(ClaudeSessionController.isShrunkViewOf(menu("Select model", "a", "x", "c"), full)); } + /** Menu whose options are numbered from {@code first} (a scrolled window of a long list). */ + private static ChoiceMenuModel scrolled(String text, int first, 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(first + i))); + } + return new ChoiceMenuModel(text, opts, 0); + } + + @Test + void isShrunkViewOf_scrolledWindowWithDifferentScrollIndicator_true() { + ChoiceMenuModel full = scrolled("\u2191 8. Fable 5", 9, "a", "b", "c", "d", "e"); + ChoiceMenuModel shrunk = scrolled("\u2191 10. b", 11, "c", "d", "e"); + assertTrue(ClaudeSessionController.isShrunkViewOf(shrunk, full)); + } + + @Test + void isShrunkViewOf_scrolledWindowMismatchingOption_false() { + ChoiceMenuModel full = scrolled("\u2191 8. Fable 5", 9, "a", "b", "c", "d", "e"); + assertFalse(ClaudeSessionController.isShrunkViewOf(scrolled("\u2191 10. b", 11, "c", "x", "e"), full)); + assertFalse(ClaudeSessionController.isShrunkViewOf(scrolled("Question", 11, "c", "d", "e"), full)); + } + @Test void onClaudeIdleSetsLifecycleToReady() { model.setLifecycle(SessionLifecycle.WORKING);