From b4bc4d1ccd6ecd717bb86d40390e4479318f17b1 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Tue, 14 Apr 2026 21:03:14 +0300 Subject: [PATCH] Fixed terminal appearing blank after dismissing the model selection menu --- CHANGELOG.md | 1 + pom.xml | 2 +- .../controller/ClaudeSessionController.java | 71 +++++++++++++++++-- .../nbclaudecodegui/ui/ClaudeSessionTab.java | 5 -- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351e6da8..09add7ca 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 6981348a..8d4a9c3a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.47-SNAPSHOT + 0.21.48-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 215c1fbf..9dfc0686 100644 --- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java @@ -58,7 +58,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; @@ -74,6 +74,7 @@ public final class ClaudeSessionController { private ClaudeProcess claudeProcess; private PtyTtyConnector connector; private java.io.File workingDir; + private volatile JediTermWidget terminalWidget; /** Polls screen state every 500 ms; owned here to allow proper teardown. */ private javax.swing.Timer statusTimer; @@ -236,6 +237,7 @@ public void startProcess(java.io.File dir, String profileName, String extraCliAr }); }); + this.terminalWidget = widget; widget.setTtyConnector(connector); widget.start(); @@ -289,6 +291,7 @@ public void stopProcess() { } connector = null; claudeProcess = null; + terminalWidget = null; modelComboPopulated = false; modelDiscoveryInProgress = false; modelDiscoveryAttempts = 0; @@ -770,7 +773,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)"). @@ -785,7 +817,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].*)?$"); @@ -835,18 +866,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); @@ -1225,4 +1265,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 e30308e8..5e207e89 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -932,11 +932,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()); - } } /**