diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b6493eb..d74fb390 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/pom.xml b/pom.xml
index 84b47bf7..d806dc45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.21.36-SNAPSHOT
+ 0.21.38-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 8938baa3..93886df9 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/ClaudeSessionTab.java
@@ -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;
@@ -281,7 +282,6 @@ public ClaudeSessionTab(File dir) {
setIcon(ImageUtilities.loadImage(ICON, true));
updateDisplayName(dir);
model.addListener(this);
- controller.setHangCallback(this::showStartError);
}
// -------------------------------------------------------------------------
@@ -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());
@@ -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);
}
}
@@ -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));
@@ -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);
@@ -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);
diff --git a/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabErrorPanelTest.java b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabErrorPanelTest.java
new file mode 100644
index 00000000..fd95f90a
--- /dev/null
+++ b/src/test/java/io/github/nbclaudecodegui/ui/ClaudeSessionTabErrorPanelTest.java
@@ -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 collectFields(Component c) {
+ java.util.List result = new java.util.ArrayList<>();
+ collectFields(c, result);
+ return result;
+ }
+
+ private static void collectFields(Component c, java.util.List 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 collectLabels(Component c) {
+ java.util.List result = new java.util.ArrayList<>();
+ collectLabels(c, result);
+ return result;
+ }
+
+ private static void collectLabels(Component c, java.util.List 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 labels = collectLabels(errorPanel);
+ // Filter out the title label (it has no colon)
+ java.util.List 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 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 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");
+ }
+}