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 MCP config not working on Windows due to quote stripping in CreateProcess (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/13)
- 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
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.38-SNAPSHOT</version>
<version>0.21.39-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
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,45 @@ public void setHangCallback(java.util.function.BiConsumer<String, String> callba
this.hangCallback = callback;
}

/**
* Kills the PTY process and resets timers/state, but does NOT call
* {@link ClaudeProcess#stop()} — so settings.local.json and the temp MCP
* config file are preserved until the error panel is dismissed and
* {@link #stopProcess()} is called.
*
* <p>{@code claudeProcess} is intentionally kept non-null so that the
* subsequent {@code stopProcess()} on panel dismiss can still call
* {@code claudeProcess.stop()} and clean up files.
*/
private void killProcessForErrorPanel() {
if (statusTimer != null) {
statusTimer.stop();
statusTimer = null;
}
promptFlushTimer.stop();
if (connector != null) {
connector.setLineListener(null);
}
if (claudeProcess != null) {
claudeProcess.killOnly();
}
connector = null;
// claudeProcess kept — file cleanup deferred to stopProcess() on panel dismiss
modelComboPopulated = false;
modelDiscoveryInProgress = false;
modelDiscoveryAttempts = 0;
customModelIds = List.of();
standardModelCount = 0;
synchronized (recentPtyLines) { recentPtyLines.clear(); }
processStartedAt = 0;
firstOutputReceived = false;

model.setModelList(List.of(), -1);
model.clearChoiceMenu();
model.clearEditModeRegistry();
model.setLifecycle(SessionLifecycle.STARTING);
}

/**
* Called from {@link #pollScreenState()} when the hang watchdog fires.
* Stops the process and notifies the registered callback on the EDT.
Expand All @@ -983,7 +1022,7 @@ public void setHangCallback(java.util.function.BiConsumer<String, String> callba
private void handleHang(int timeoutSec) {
LOG.severe("Hang detected in " + workingDir + ": no PTY output received within " + timeoutSec + " s — killing process");
String command = claudeProcess != null ? claudeProcess.getLastCommand() : "";
stopProcess();
killProcessForErrorPanel(); // kill PTY; file cleanup deferred to panel dismiss
java.util.function.BiConsumer<String, String> cb = hangCallback;
if (cb != null) {
String msg = "No output received within " + timeoutSec
Expand Down
51 changes: 48 additions & 3 deletions src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public ClaudeProcess() {}
/** Working directory of the current session; {@code null} when stopped. */
private volatile String workingDir;

/** Temp file holding the --mcp-config JSON; {@code null} when not in use. */
private volatile Path mcpConfigTempFile;

/**
* Starts a Claude CLI PTY process in the given working directory using
* the Default profile (no extra env vars injected).
Expand Down Expand Up @@ -166,9 +169,19 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl
if (io.github.nbclaudecodegui.settings.ClaudeCodePreferences.isMcpEnabled()) {
// Pass MCP server config via --mcp-config (Claude 2.x no longer reads
// mcpServers from settings.local.json; --mcp-config works in TUI mode).
// On Windows, inline JSON gets quote-stripped by CreateProcess, so we
// write to a temp file and pass the file path instead.
cmd.add("--mcp-config");
cmd.add(buildMcpConfigJson(port));
LOG.info("Passing --mcp-config with netbeans SSE server on port " + port);
try {
Path tmpCfg = writeMcpConfigTempFile(port);
mcpConfigTempFile = tmpCfg;
cmd.add(tmpCfg.toAbsolutePath().toString());
LOG.info("Passing --mcp-config via temp file: " + tmpCfg);
} catch (IOException e) {
LOG.warning("Could not write --mcp-config temp file, falling back to inline JSON: " + e.getMessage());
cmd.add(buildMcpConfigJson(port));
LOG.info("Passing --mcp-config with netbeans SSE server on port " + port);
}
} else {
LOG.info("MCP integration disabled by user preference; skipping --mcp-config");
}
Expand Down Expand Up @@ -264,7 +277,14 @@ void appendSessionFlags(List<String> cmd, String workingDir, Map<String, String>
* {@value #OUR_HOOK_MATCHER}). User-provided keys are left untouched.
* If the file becomes empty after cleanup it is deleted.
*/
public void stop() {
/**
* Kills the PTY process only — does NOT delete settings.local.json or the
* temp MCP config file. File cleanup is deferred to {@link #stop()}.
*
* <p>Use this when the error panel is about to be shown and files must stay
* alive until the user dismisses the panel.
*/
public void killOnly() {
PtyProcess p = ptyProcess;
if (p != null && p.isAlive()) {
p.destroy();
Expand All @@ -278,6 +298,20 @@ public void stop() {
}
}
ptyProcess = null;
// workingDir and mcpConfigTempFile intentionally NOT cleared — deferred to stop()
}

public void stop() {
killOnly();

Path tmp = mcpConfigTempFile;
mcpConfigTempFile = null;
if (tmp != null) {
try {
Files.deleteIfExists(tmp);
LOG.fine("Deleted --mcp-config temp file: " + tmp);
} catch (IOException e) { /* ignore */ }
}

String dir = workingDir;
workingDir = null;
Expand Down Expand Up @@ -651,6 +685,17 @@ static String toShellCommand(List<String> cmd, boolean windows) {
return sb.toString();
}

/**
* Writes the MCP config JSON to a temp file and returns its path.
* The file is registered for deletion on JVM exit as a safety net.
*/
static Path writeMcpConfigTempFile(int port) throws IOException {
Path tmp = Files.createTempFile("claude-mcp-config-", ".json");
tmp.toFile().deleteOnExit();
Files.writeString(tmp, buildMcpConfigJson(port), StandardCharsets.UTF_8);
return tmp;
}

/**
* Builds the JSON string to pass as {@code --mcp-config} to the Claude CLI.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,27 @@ void testToShellCommandQuotesJsonArgWindows() {
assertTrue(inner.contains("\"\""), "Windows quoting must double double-quotes");
}

// -------------------------------------------------------------------------
// writeMcpConfigTempFile
// -------------------------------------------------------------------------

@Test
void testWriteMcpConfigTempFile() throws Exception {
Path tmp = ClaudeProcess.writeMcpConfigTempFile(28991);
try {
assertTrue(Files.exists(tmp), "temp file should be created");
String content = Files.readString(tmp, StandardCharsets.UTF_8);
assertTrue(content.contains("netbeans"), "temp file should contain netbeans key");
assertTrue(content.contains("28991"), "temp file should contain port");
assertTrue(content.startsWith("{"), "temp file should be valid JSON object");
// File path should contain no double-quotes (safe to pass as process arg)
assertFalse(tmp.toAbsolutePath().toString().contains("\""),
"temp file path should contain no double-quotes");
} finally {
Files.deleteIfExists(tmp);
}
}

// -------------------------------------------------------------------------
// buildMcpConfigJson
// -------------------------------------------------------------------------
Expand Down
Loading