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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Fixed prompt area being collapsible to zero height by dragging the divider
- Fixed terminal scrolling to the beginning and locking when a choice/confirmation menu appears (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/22)

# 0.21.50 (2026-04-19)
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/github/nbclaudecodegui/ui/ClaudePromptPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ public ClaudePromptPanel(Consumer<String> onSend,
});
}

@Override
public java.awt.Dimension getMinimumSize() {
BorderLayout layout = (BorderLayout) getLayout();
java.awt.Component east = layout.getLayoutComponent(BorderLayout.EAST);
java.awt.Component west = layout.getLayoutComponent(BorderLayout.WEST);
int minH = 0;
if (east != null) minH = Math.max(minH, east.getMinimumSize().height);
if (west != null) minH = Math.max(minH, west.getMinimumSize().height);
java.awt.Insets ins = getInsets();
return new java.awt.Dimension(0, minH + ins.top + ins.bottom);
}

// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
Expand Down
17 changes: 9 additions & 8 deletions src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -701,16 +701,17 @@ private void showChatLayout(JediTermWidget widget) {
southCardLayout = new CardLayout();
southCard = new JPanel(southCardLayout) {
/**
* Returns (0, 0) so that JSplitPane can collapse the bottom panel
* freely when the tab is docked as a narrow side panel.
*
* Without this override, getMinimumSize() delegates to the visible
* child (e.g. promptPanel with a 3-row DecoratedTextArea + buttons),
* which reports a tall minimum height (~600 px). JSplitPane enforces
* that minimum, causing the divider to bounce back when the user tries
* to shrink the tab below that height (issue #19).
* Returns (0, 0) for non-prompt cards so that JSplitPane can collapse
* the bottom panel freely when the tab is docked as a narrow side panel
* (issue #19). For the prompt card, delegates to the visible child's
* minimum height so the divider cannot be dragged below the button column.
*/
@Override public java.awt.Dimension getMinimumSize() {
if (CARD_PROMPT.equals(activeCard)) {
for (java.awt.Component c : getComponents()) {
if (c.isVisible()) return new java.awt.Dimension(0, c.getMinimumSize().height);
}
}
return new java.awt.Dimension(0, 0);
}
@Override public java.awt.Dimension getPreferredSize() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.nbclaudecodegui.ui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JSplitPane;
import javax.swing.JPanel;
Expand All @@ -8,49 +9,130 @@
import static org.junit.jupiter.api.Assertions.*;

/**
* Regression test for issue #19: the session tab must be resizable below
* ~600 px when docked as a side panel.
* Regression tests for prompt-area resize behaviour.
*
* <p>Root cause: {@code southCard.getMinimumSize()} was delegating to the
* visible child component (e.g. promptPanel with a 3-row text area + buttons),
* which reported a tall minimum height. {@link JSplitPane} enforces the
* bottom component's minimum size, so the divider bounced back when the user
* tried to shrink the tab below that height.
* <p>Issue #19: the session tab must be resizable below ~600 px when docked as
* a side panel — fixed by returning (0, 0) from {@code southCard.getMinimumSize()}.
*
* <p>Fix: {@code southCard.getMinimumSize()} now always returns (0, 0),
* letting JSplitPane collapse the bottom panel freely.
* <p>Follow-up fix (scroll-lock): the prompt area must not be collapsible to
* zero height — fixed by having {@code ClaudePromptPanel.getMinimumSize()}
* return the height of its button columns, and by having {@code southCard}
* delegate to the visible child's minimum when {@code CARD_PROMPT} is active.
*/
class ClaudeSessionTabMinSizeTest {

/**
* Verifies that {@code southCard}'s minimum size is (0, 0) so that
* JSplitPane does not enforce a large lower bound on the tab height.
* Verifies that {@code southCard}'s minimum size is (0, 0) when a non-prompt
* card is active (e.g. CARD_CHOICE), so JSplitPane does not enforce a large
* lower bound on the tab height (issue #19).
*/
@Test
void southCardMinimumSizeIsZero() throws Exception {
void southCardMinimumSizeIsZeroForChoiceCard() throws Exception {
SwingUtilities.invokeAndWait(() -> {
// Simulate the southCard construction from showChatLayout():
// an anonymous JPanel subclass with getMinimumSize() returning (0,0).
String[] activeCard = {"choice"};
java.awt.CardLayout cardLayout = new java.awt.CardLayout();
JPanel southCard = new JPanel(cardLayout) {
@Override public Dimension getMinimumSize() {
if ("prompt".equals(activeCard[0])) {
for (java.awt.Component c : getComponents()) {
if (c.isVisible()) return new Dimension(0, c.getMinimumSize().height);
}
}
return new Dimension(0, 0);
}
};

// Add a child with a large preferred/minimum size (mirrors promptPanel).
JPanel tallChild = new JPanel() {
JPanel choiceChild = new JPanel() {
@Override public Dimension getMinimumSize() { return new Dimension(400, 600); }
@Override public Dimension getPreferredSize() { return new Dimension(400, 600); }
};
southCard.add(tallChild, "prompt");
southCard.add(choiceChild, "choice");
cardLayout.show(southCard, "choice");

Dimension min = southCard.getMinimumSize();
assertEquals(0, min.width, "southCard min width must be 0 for choice card (issue #19)");
assertEquals(0, min.height, "southCard min height must be 0 for choice card (issue #19)");
});
}

/**
* Verifies that {@code southCard}'s minimum height is driven by the visible
* child when {@code CARD_PROMPT} is active — so the divider cannot be
* dragged below the button column height.
*/
@Test
void southCardMinimumHeightIsFromChildWhenPromptCardActive() throws Exception {
SwingUtilities.invokeAndWait(() -> {
String[] activeCard = {"prompt"};
java.awt.CardLayout cardLayout = new java.awt.CardLayout();
JPanel southCard = new JPanel(cardLayout) {
@Override public Dimension getMinimumSize() {
if ("prompt".equals(activeCard[0])) {
for (java.awt.Component c : getComponents()) {
if (c.isVisible()) return new Dimension(0, c.getMinimumSize().height);
}
}
return new Dimension(0, 0);
}
};

JPanel promptChild = new JPanel() {
@Override public Dimension getMinimumSize() { return new Dimension(400, 42); }
@Override public Dimension getPreferredSize() { return new Dimension(400, 100); }
};
southCard.add(promptChild, "prompt");
cardLayout.show(southCard, "prompt");

Dimension min = southCard.getMinimumSize();
assertEquals(0, min.width,
"southCard minimum width must be 0 so JSplitPane can resize freely (issue #19)");
assertEquals(0, min.height,
"southCard minimum height must be 0 so JSplitPane can resize freely (issue #19)");
assertEquals(0, min.width, "southCard min width must be 0 even for prompt card");
assertEquals(42, min.height, "southCard min height must match button column height when prompt card active");
});
}

/**
* Verifies that {@code ClaudePromptPanel.getMinimumSize()} returns a height
* driven by the EAST/WEST button columns, not the CENTER scroll pane.
*/
@Test
void promptPanel_minimumHeight_isDrivenByButtonColumns() throws Exception {
SwingUtilities.invokeAndWait(() -> {
JPanel promptPanel = new JPanel(new BorderLayout()) {
@Override
public Dimension getMinimumSize() {
BorderLayout layout = (BorderLayout) getLayout();
java.awt.Component east = layout.getLayoutComponent(BorderLayout.EAST);
java.awt.Component west = layout.getLayoutComponent(BorderLayout.WEST);
int minH = 0;
if (east != null) minH = Math.max(minH, east.getMinimumSize().height);
if (west != null) minH = Math.max(minH, west.getMinimumSize().height);
java.awt.Insets ins = getInsets();
return new Dimension(0, minH + ins.top + ins.bottom);
}
};

// CENTER: large textarea (should NOT drive minimum)
JPanel center = new JPanel() {
@Override public Dimension getMinimumSize() { return new Dimension(400, 600); }
};
// EAST: button column
JPanel east = new JPanel() {
@Override public Dimension getMinimumSize() { return new Dimension(30, 55); }
};
// WEST: button column
JPanel west = new JPanel() {
@Override public Dimension getMinimumSize() { return new Dimension(30, 40); }
};

promptPanel.add(center, BorderLayout.CENTER);
promptPanel.add(east, BorderLayout.EAST);
promptPanel.add(west, BorderLayout.WEST);

Dimension min = promptPanel.getMinimumSize();
assertEquals(0, min.width, "promptPanel minimum width must be 0");
assertTrue(min.height >= 55,
"promptPanel minimum height must be >= east button column height (55), got " + min.height);
assertTrue(min.height < 600,
"promptPanel minimum height must not be driven by CENTER textarea (600), got " + min.height);
});
}

Expand Down
Loading