diff --git a/CHANGELOG.md b/CHANGELOG.md
index b773b546..f2294249 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed terminal appearing blank after dismissing the model selection menu
- Fixed MCP connection to localhost being routed through an active HTTP_PROXY/HTTPS_PROXY; NO_PROXY is now automatically extended with localhost and 127.0.0.1 when a proxy is configured
- Fixed terminal scrollbar disappearing when Claude shows a choice/confirmation prompt (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/22)
- Fixed Cancel button sending Ctrl+C instead of Esc, making it impossible to exit /usage and similar screens
diff --git a/pom.xml b/pom.xml
index b64609c2..0bb4f553 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.20.9-SNAPSHOT
+ 0.20.19-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/controller/ClaudeSessionController.java b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
index 6cdc65e8..3a4de787 100644
--- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
+++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java
@@ -55,7 +55,7 @@ public final class ClaudeSessionController {
/** ESC byte sent to the PTY (dismiss autocomplete, close menus). */
private static final byte[] PTY_ESC = {0x1b};
- private static final int MAX_MODEL_DISCOVERY_ATTEMPTS = 3;
+ private static final int MAX_MODEL_DISCOVERY_ATTEMPTS = 1;
private final ClaudeSessionModel model;
@@ -70,6 +70,7 @@ public final class ClaudeSessionController {
private ClaudeProcess claudeProcess;
private PtyTtyConnector connector;
+ private volatile JediTermWidget terminalWidget;
/** Polls screen state every 500 ms; owned here to allow proper teardown. */
private javax.swing.Timer statusTimer;
@@ -211,6 +212,7 @@ public void startProcess(java.io.File dir, String profileName, String extraCliAr
});
});
+ this.terminalWidget = widget;
widget.setTtyConnector(connector);
widget.start();
@@ -264,6 +266,7 @@ public void stopProcess() {
}
connector = null;
claudeProcess = null;
+ terminalWidget = null;
modelComboPopulated = false;
modelDiscoveryInProgress = false;
modelDiscoveryAttempts = 0;
@@ -721,7 +724,36 @@ private void discoverModels() {
Thread t = new Thread(() -> {
try {
openModelMenu();
- List lines = screenLines.get();
+
+ // Poll the screen until the model selection menu appears.
+ // openModelMenu() types /model + Enter, but Claude CLI needs
+ // time to render the numbered menu. Poll up to ~5 s.
+ List lines = null;
+ Optional menuOpt = Optional.empty();
+ for (int poll = 0; poll < 16; poll++) {
+ Thread.sleep(300);
+ lines = screenLines.get();
+ if (LOG.isLoggable(java.util.logging.Level.FINE)) {
+ StringBuilder sb = new StringBuilder("[discoverModels] poll ")
+ .append(poll + 1).append(" screen (")
+ .append(lines.size()).append(" lines):");
+ for (int li = 0; li < lines.size(); li++) {
+ String l = lines.get(li);
+ if (!l.isBlank()) sb.append("\n ").append(li).append(": ").append(l);
+ }
+ LOG.fine(sb.toString());
+ }
+ menuOpt = screenContentDetector.detectChoiceMenu(lines);
+ if (menuOpt.isPresent() && !menuOpt.get().options().isEmpty()) {
+ LOG.fine("[discoverModels] detectChoiceMenu found menu on poll " + (poll + 1));
+ break;
+ }
+ ModelMenuParser.ModelDiscovery probe = new ModelMenuParser().parse(lines);
+ if (!probe.models().isEmpty()) {
+ LOG.fine("[discoverModels] ModelMenuParser found models on poll " + (poll + 1));
+ break;
+ }
+ }
// Scan the rolling PTY line buffer for the /model hint line, which is
// shorter and never truncated (e.g. "/model … (currently anthropic/claude-sonnet-4.6)").
@@ -736,7 +768,6 @@ private void discoverModels() {
List models;
int selIdx;
- Optional menuOpt = screenContentDetector.detectChoiceMenu(lines);
if (menuOpt.isPresent() && !menuOpt.get().options().isEmpty()) {
List opts = menuOpt.get().options();
Pattern descPat = Pattern.compile("(?:\u2714\\s+|\\s{3,})(.+?)(?:\\s*[·\u00b7].*)?$");
@@ -786,18 +817,27 @@ private void discoverModels() {
final int finalSelIdx = selIdx;
if (!finalModels.isEmpty()) {
model.setModelList(finalModels, finalSelIdx);
- onClaudeIdle();
} else {
SwingUtilities.invokeLater(() -> modelComboPopulated = false);
- onClaudeIdle();
}
} catch (IOException | InterruptedException ex) {
LOG.warning("discoverModels failed: " + ex.getMessage());
SwingUtilities.invokeLater(() -> modelComboPopulated = false);
- onClaudeIdle();
} finally {
+ // Wait for Claude CLI to finish its normal redraw after Esc,
+ // then send a single SIGWINCH to force a complete TUI redraw
+ // (the Esc-dismiss redraw often leaves stale /model fragments).
+ // Finally wait for the resize-triggered redraw to complete
+ // before transitioning to READY — setting READY too early
+ // causes pollScreenState to see mid-redraw garbage → WORKING,
+ // and the session gets stuck.
try { Thread.sleep(600); } catch (InterruptedException ignored) {}
- SwingUtilities.invokeLater(() -> modelDiscoveryInProgress = false);
+ forceTerminalRedraw();
+ try { Thread.sleep(400); } catch (InterruptedException ignored) {}
+ SwingUtilities.invokeLater(() -> {
+ modelDiscoveryInProgress = false;
+ onClaudeIdle();
+ });
}
}, "claude-model-discovery");
t.setDaemon(true);
@@ -1137,4 +1177,21 @@ private void sendCancelToPty(byte[] bytes) throws IOException {
connector.write(bytes);
onClaudeIdle();
}
+
+ /**
+ * Sends a single SIGWINCH pair (shrink by one row, then restore) to force
+ * Claude CLI to fully redraw the TUI. Used after model discovery Esc
+ * dismiss, which often leaves stale content on screen.
+ */
+ private void forceTerminalRedraw() {
+ JediTermWidget w = terminalWidget;
+ if (connector == null || w == null) return;
+ com.jediterm.core.util.TermSize ts = w.getTerminalPanel()
+ .getTerminalSizeFromComponent();
+ if (ts == null || ts.getColumns() <= 0 || ts.getRows() <= 1) return;
+ connector.resize(new java.awt.Dimension(ts.getColumns(), ts.getRows() - 1));
+ connector.resize(new java.awt.Dimension(ts.getColumns(), ts.getRows()));
+ LOG.fine("[forceTerminalRedraw] cols=" + ts.getColumns() + " rows=" + ts.getRows());
+ }
+
}
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
index 53a0d2ae..62630c7e 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
@@ -825,11 +825,6 @@ private void switchSouthCard(String card) {
}
southCard.revalidate();
southCard.repaint();
- // Keep the terminal scrolled to the bottom after layout changes
- if (terminalWidget != null) {
- SwingUtilities.invokeLater(
- () -> terminalWidget.getTerminalPanel().scrollToShowAllOutput());
- }
}
private void stopProcess() {