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 @@
- Improved error panel on process start failure: shows Working Directory and CLAUDE_CONFIG_DIR fields (before Command and Error) for easier diagnosis; all fields have a right-click Copy menu; fixed "Restart Advanced…" not dismissing the error panel
- Added Advanced tab in Tools → Options → Claude Code with Debug mode, Hang timeout, and a new "Enable MCP integration" toggle (disabling it skips the --mcp-config flag while hooks remain active)
- Fixed process start errors and hang timeouts now logged to the IDE log file at SEVERE level
- Added hang detection: configurable timeout (default 60 s) kills the process and shows an error if no PTY output is received after launch; set to 0 to disable
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.21.36-SNAPSHOT</version>
<version>0.21.38-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
60 changes: 56 additions & 4 deletions src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.github.nbclaudecodegui.settings.ClaudeProfileStore;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.FlowLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
Expand Down Expand Up @@ -281,7 +282,6 @@ public ClaudeSessionTab(File dir) {
setIcon(ImageUtilities.loadImage(ICON, true));
updateDisplayName(dir);
model.addListener(this);
controller.setHangCallback(this::showStartError);
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -653,6 +653,17 @@ private void startSession(File dir, String profileName, String extraCliArgs) {
private void startSession(File dir, String profileName, String extraCliArgs,
SessionMode mode, String resumeSessionId) {
this.activeResumeSessionId = resumeSessionId;
Path configDir = null;
if (!ClaudeProfile.DEFAULT_NAME.equals(profileName)) {
ClaudeProfile profile = ClaudeProfileStore.findByName(profileName);
if (profile != null && !profile.isDefault()) {
configDir = ClaudeProfileStore.resolveStorageDir(
profile, ClaudeCodePreferences.getProfilesDir());
}
}
final Path capturedConfigDir = configDir;
controller.setHangCallback(
(cmd, err) -> showStartError(cmd, err, model.getWorkingDirectory(), capturedConfigDir));
updateDisplayName(dir);
sessionTag = "[" + dir.getName() + "] ";
JediTermWidget widget = new JediTermWidget(new NetBeansSettingsProvider());
Expand All @@ -669,7 +680,7 @@ private void startSession(File dir, String profileName, String extraCliArgs,
LOG.log(Level.SEVERE, "Claude process failed to start in " + dir, ex);
String command = controller.getLastAttemptedCommand();
String errorMsg = ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName();
showStartError(command, errorMsg);
showStartError(command, errorMsg, dir, capturedConfigDir);
}
}

Expand Down Expand Up @@ -764,12 +775,31 @@ private void showSelectorLayout() {
repaint();
}

private void showStartError(String command, String error) {
/**
* Replaces the current layout with a diagnostics panel describing why the
* Claude Code process could not be started.
*
* @param command the command that was attempted (may be empty)
* @param error the error message
* @param workingDir the working directory that was used (may be {@code null})
* @param claudeConfigDir the {@code CLAUDE_CONFIG_DIR} value in effect;
* {@code null} means the default profile (no override)
*/
private void showStartError(String command, String error,
File workingDir, Path claudeConfigDir) {
if (splitPane != null) {
remove(splitPane);
splitPane = null;
}

JTextField dirField = new JTextField(workingDir != null ? workingDir.getAbsolutePath() : "");
dirField.setEditable(false);
BasicTextContextMenu.attach(dirField, BasicTextContextMenu.createReadOnly(dirField));

JTextField cfgDirField = new JTextField(claudeConfigDir != null ? claudeConfigDir.toString() : "");
cfgDirField.setEditable(false);
BasicTextContextMenu.attach(cfgDirField, BasicTextContextMenu.createReadOnly(cfgDirField));

JTextField cmdField = new JTextField(command);
cmdField.setEditable(false);
BasicTextContextMenu.attach(cmdField, BasicTextContextMenu.createReadOnly(cmdField));
Expand All @@ -784,10 +814,28 @@ private void showStartError(String command, String error) {
JPanel fieldsPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(4, 0, 4, 8); gbc.anchor = GridBagConstraints.WEST;
fieldsPanel.add(new JLabel("Working Directory:"), gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
fieldsPanel.add(dirField, gbc);
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
JTextField cfgDirLabelField = new JTextField("CLAUDE_CONFIG_DIR");
cfgDirLabelField.setEditable(false);
cfgDirLabelField.setBorder(null);
cfgDirLabelField.setBackground(null);
cfgDirLabelField.setOpaque(false);
BasicTextContextMenu.attach(cfgDirLabelField, BasicTextContextMenu.createReadOnly(cfgDirLabelField));
JPanel cfgDirLabelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
cfgDirLabelPanel.setOpaque(false);
cfgDirLabelPanel.add(cfgDirLabelField);
cfgDirLabelPanel.add(new JLabel(":"));
fieldsPanel.add(cfgDirLabelPanel, gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
fieldsPanel.add(cfgDirField, gbc);
gbc.gridx = 0; gbc.gridy = 2; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
fieldsPanel.add(new JLabel("Command:"), gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
fieldsPanel.add(cmdField, gbc);
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
gbc.gridx = 0; gbc.gridy = 3; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0;
fieldsPanel.add(new JLabel("Error:"), gbc);
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0;
fieldsPanel.add(errField, gbc);
Expand Down Expand Up @@ -984,6 +1032,10 @@ private void cleanupChatLayout() {
southCardLayout = null;
activeCard = null;
}
if (errorPanel != null) {
remove(errorPanel);
errorPanel = null;
}
selectorPanel.setVisible(true);
selectorPanel.unlock();
add(selectorPanel, BorderLayout.CENTER);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package io.github.nbclaudecodegui.ui;

import io.github.nbclaudecodegui.model.SessionMode;
import java.awt.BorderLayout;
import java.awt.Component;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Path;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

/**
* Unit tests for the error panel in {@link ClaudeSessionTab}.
*/
class ClaudeSessionTabErrorPanelTest {

/** Reflectively invoke {@code showStartError} with all four parameters. */
private static void showStartError(ClaudeSessionTab tab,
String command, String error,
File workingDir, Path claudeConfigDir)
throws Exception {
Method m = ClaudeSessionTab.class.getDeclaredMethod(
"showStartError", String.class, String.class, File.class, Path.class);
m.setAccessible(true);
m.invoke(tab, command, error, workingDir, claudeConfigDir);
}

/** Reflectively read the {@code errorPanel} field. */
private static JPanel getErrorPanel(ClaudeSessionTab tab) throws Exception {
Field f = ClaudeSessionTab.class.getDeclaredField("errorPanel");
f.setAccessible(true);
return (JPanel) f.get(tab);
}

/** Collect all JTextFields recursively from a container. */
private static java.util.List<JTextField> collectFields(Component c) {
java.util.List<JTextField> result = new java.util.ArrayList<>();
collectFields(c, result);
return result;
}

private static void collectFields(Component c, java.util.List<JTextField> result) {
if (c instanceof JTextField tf) {
result.add(tf);
} else if (c instanceof java.awt.Container container) {
for (Component child : container.getComponents()) {
collectFields(child, result);
}
}
}

/** Collect all JLabels recursively from a container. */
private static java.util.List<JLabel> collectLabels(Component c) {
java.util.List<JLabel> result = new java.util.ArrayList<>();
collectLabels(c, result);
return result;
}

private static void collectLabels(Component c, java.util.List<JLabel> result) {
if (c instanceof JLabel lbl) {
result.add(lbl);
} else if (c instanceof java.awt.Container container) {
for (Component child : container.getComponents()) {
collectLabels(child, result);
}
}
}

@Test
void showStartError_errorPanelIsCenterComponent() throws Exception {
ClaudeSessionTab tab = new ClaudeSessionTab();
File dir = new File("/tmp/test-dir");
Path cfgDir = Path.of("/home/user/.config/claude/profiles/myprofile");

showStartError(tab, "claude --arg", "Connection refused", dir, cfgDir);

BorderLayout layout = (BorderLayout) tab.getLayout();
Component center = layout.getLayoutComponent(BorderLayout.CENTER);
JPanel errorPanel = getErrorPanel(tab);

assertNotNull(errorPanel, "errorPanel field must be set");
assertSame(errorPanel, center, "errorPanel must be the CENTER component");
}

@Test
void showStartError_fieldOrderAndValues() throws Exception {
ClaudeSessionTab tab = new ClaudeSessionTab();
File dir = new File("/tmp/my-project");
Path cfgDir = Path.of("/home/user/.config/claude/profiles/work");

showStartError(tab, "claude --verbose", "No such file", dir, cfgDir);

JPanel errorPanel = getErrorPanel(tab);
assertNotNull(errorPanel);

java.util.List<JLabel> labels = collectLabels(errorPanel);
// Filter out the title label (it has no colon)
java.util.List<String> fieldLabels = labels.stream()
.map(JLabel::getText)
.filter(t -> t.contains(":"))
.toList();

// 3 colon-containing labels: "Working Directory:", ":", "Command:", "Error:"
// Note: CLAUDE_CONFIG_DIR row uses a JTextField for the env var name + a ":" JLabel
assertEquals(4, fieldLabels.size(), "Must have 4 colon-containing labels");
assertEquals("Working Directory:", fieldLabels.get(0));
assertEquals(":", fieldLabels.get(1));
assertEquals("Command:", fieldLabels.get(2));
assertEquals("Error:", fieldLabels.get(3));

// 5 JTextFields: dirField, cfgDirLabelField("CLAUDE_CONFIG_DIR"), cfgDirField, cmdField, errField
java.util.List<JTextField> fields = collectFields(errorPanel);
assertEquals(5, fields.size(), "Must have 5 text fields");
assertEquals(dir.getAbsolutePath(), fields.get(0).getText());
assertEquals("CLAUDE_CONFIG_DIR", fields.get(1).getText());
assertEquals(cfgDir.toString(), fields.get(2).getText());
assertEquals("claude --verbose", fields.get(3).getText());
assertEquals("No such file", fields.get(4).getText());
}

@Test
void showStartError_defaultProfile_configDirIsBlank() throws Exception {
ClaudeSessionTab tab = new ClaudeSessionTab();
File dir = new File("/tmp/proj");

// null claudeConfigDir → default profile
showStartError(tab, "claude", "error", dir, null);

JPanel errorPanel = getErrorPanel(tab);
assertNotNull(errorPanel);

java.util.List<JTextField> fields = collectFields(errorPanel);
assertEquals(5, fields.size());
assertEquals("", fields.get(2).getText(), "CLAUDE_CONFIG_DIR value field must be empty for default profile");
}

@Test
void restartAdvanced_afterError_clearsErrorPanel() throws Exception {
ClaudeSessionTab tab = new ClaudeSessionTab();
File dir = new File("/tmp/proj");

showStartError(tab, "claude", "fail", dir, null);
assertNotNull(getErrorPanel(tab), "errorPanel must be set after showStartError");

tab.onSaveAndSwitch("", SessionMode.RESTART_ADVANCED, null);

assertNull(getErrorPanel(tab), "errorPanel must be null after RESTART_ADVANCED");

BorderLayout layout = (BorderLayout) tab.getLayout();
Component center = layout.getLayoutComponent(BorderLayout.CENTER);
assertNotNull(center, "A CENTER component must be present after RESTART_ADVANCED");
assertFalse(center instanceof JPanel ep && ep == getErrorPanel(tab),
"CENTER must not be the error panel");
}
}
Loading