Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-claude-code-gui</artifactId>
<version>1.3.38-SNAPSHOT</version>
<version>1.3.39-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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<ChoiceMenuModel.Option> n = newMenu.options();
List<ChoiceMenuModel.Option> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChoiceMenuModel.Option> 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);
Expand Down
Loading