From 4dbb690808a2f78db0056736412b5f15401a2455 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 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() {