diff --git a/CHANGELOG.md b/CHANGELOG.md index d74fb390..3515a80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pom.xml b/pom.xml index d806dc45..7a1e19f9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.38-SNAPSHOT + 0.21.39-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/controller/ClaudeSessionController.java b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java index dfe80c14..3bfb3228 100644 --- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java @@ -974,6 +974,45 @@ public void setHangCallback(java.util.function.BiConsumer 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. + * + *

{@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. @@ -983,7 +1022,7 @@ public void setHangCallback(java.util.function.BiConsumer 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 cb = hangCallback; if (cb != null) { String msg = "No output received within " + timeoutSec diff --git a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java index 813dae4b..84b32fec 100644 --- a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java +++ b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java @@ -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). @@ -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"); } @@ -264,7 +277,14 @@ void appendSessionFlags(List cmd, String workingDir, Map * {@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()}. + * + *

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(); @@ -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; @@ -651,6 +685,17 @@ static String toShellCommand(List 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. */ diff --git a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java index 849ef7ac..2d1b2bf4 100644 --- a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java +++ b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java @@ -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 // -------------------------------------------------------------------------