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
// -------------------------------------------------------------------------