diff --git a/CHANGELOG.md b/CHANGELOG.md index ab025708..97bd214a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed terminal scrolling to the beginning and locking when a choice/confirmation menu appears (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/22) - Added a setting to control whether the choice menu grabs keyboard focus (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/20) - Added a global setting to choose the default dock position of the Claude Code session tab (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/24) diff --git a/pom.xml b/pom.xml index fdd7e86e..7a9b161a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.7-SNAPSHOT + 0.22.9-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/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java index b23aed89..33c7b7ae 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java @@ -501,7 +501,9 @@ public void onChoiceMenuChanged(ChoiceMenuModel menu) { repaint(); controller.writePtyAnswer(answer); }, grabFocus); + LOG.fine(sessionTag + "[onChoiceMenuChanged pre-switch] " + termScrollState()); switchSouthCard(CARD_CHOICE); + LOG.fine(sessionTag + "[onChoiceMenuChanged post-switch] " + termScrollState()); } else { if (CARD_CHOICE.equals(activeCard)) { choiceMenuPanel.dismiss(); @@ -984,19 +986,46 @@ void switchSouthCard(String card) { + " choiceVisible=" + choiceMenuPanel.isVisible() + " choiceBounds=" + choiceMenuPanel.getBounds() + " southCardVisible=" + southCard.isVisible() - + " southCardBounds=" + southCard.getBounds()); + + " southCardBounds=" + southCard.getBounds() + + " divLoc=" + splitPane.getDividerLocation() + + " " + termScrollState()); SwingUtilities.invokeLater(() -> LOG.fine(sessionTag + "[switchSouthCard CHOICE post-EDT]" + " choiceShowing=" + choiceMenuPanel.isShowing() + " choiceBounds=" + choiceMenuPanel.getBounds() - + " componentCount=" + choiceMenuPanel.getComponentCount())); + + " componentCount=" + choiceMenuPanel.getComponentCount() + + " " + termScrollState())); } southCard.revalidate(); southCard.repaint(); // Keep the terminal scrolled to the bottom after layout changes caused by // setDividerLocation() — JediTerm resets scroll position to top on resize. if (terminalWidget != null) { - SwingUtilities.invokeLater( - () -> terminalWidget.getTerminalPanel().scrollToShowAllOutput()); + SwingUtilities.invokeLater(() -> { + LOG.fine(sessionTag + "[scrollTerminalToBottom pre] " + termScrollState()); + scrollTerminalToBottom(); + LOG.fine(sessionTag + "[scrollTerminalToBottom post] " + termScrollState()); + SwingUtilities.invokeLater(() -> + LOG.fine(sessionTag + "[scrollTerminalToBottom post-post] " + termScrollState())); + }); + } + } + + /** Scrolls the JediTerm widget to show the latest output (bottom of scrollback). */ + private void scrollTerminalToBottom() { + if (terminalWidget == null) return; + scrollBottomOfComponents(terminalWidget); + } + + private void scrollBottomOfComponents(java.awt.Container c) { + for (java.awt.Component child : c.getComponents()) { + if (child instanceof javax.swing.JScrollBar bar + && bar.getOrientation() == javax.swing.JScrollBar.VERTICAL) { + bar.setValue(bar.getMaximum() - bar.getVisibleAmount()); + return; + } + if (child instanceof java.awt.Container sub) { + scrollBottomOfComponents(sub); + } } } @@ -1263,6 +1292,32 @@ private static JPanel makeSep() { // Helpers // ------------------------------------------------------------------------- + /** Collects scroll-related state of the JediTerm terminal widget for diagnostics. */ + private String termScrollState() { + if (terminalWidget == null) return "terminalWidget=null"; + StringBuilder sb = new StringBuilder(); + sb.append("termSize=").append(terminalWidget.getSize()); + sb.append(" termEnabled=").append(terminalWidget.isEnabled()); + walkScrollComponents(terminalWidget, sb, 0); + return sb.toString(); + } + + private void walkScrollComponents(java.awt.Container c, StringBuilder sb, int depth) { + for (java.awt.Component child : c.getComponents()) { + if (child instanceof javax.swing.JScrollBar bar) { + sb.append(" scrollBar[enabled=").append(bar.isEnabled()) + .append(",val=").append(bar.getValue()) + .append(",min=").append(bar.getMinimum()) + .append(",max=").append(bar.getMaximum()) + .append(",vis=").append(bar.getVisibleAmount()) + .append("]"); + } + if (child instanceof java.awt.Container sub && depth < 5) { + walkScrollComponents(sub, sb, depth + 1); + } + } + } + private List getScreenLines() { if (terminalWidget == null) return java.util.Collections.emptyList(); try {