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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
- 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
- Fixed MCP config not working on Windows due to quote stripping in CreateProcess (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/13)
- Fixed authentication for profiles with the "Claude API" connection type: the API key is now correctly recognised by Claude Code on session start

# 0.20.4 (2026-04-09)
Expand Down
Binary file added docs/screenshots/settings-advanced.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/settings-favorites.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/settings-general.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 20 additions & 3 deletions docs/user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ Open **Tools → Options → Claude Code** in NetBeans.

### General tab

![Settings — General tab](screenshots/settings-general.png)

| Setting | Default | Description |
|---------|---------|-------------|
| Claude CLI path | (empty) | Absolute path to the `claude` executable. Leave empty to use the system `PATH`. |
Expand All @@ -338,12 +340,23 @@ Open **Tools → Options → Claude Code** in NetBeans.
| History TTL (days) | 0 | Number of days after which history entries expire. 0 = keep forever. |
| Send prompt key | Ctrl+Enter | Key combination that sends the prompt from the input area. |
| Insert newline key | Enter | Key combination that inserts a newline in the input area. The send and newline keys are configured independently but cannot be set to the same value. |
| Debug mode | Off | Enables verbose logging of all Claude I/O to the NetBeans log file and the Output window. |
| Open diff in a separate tab | Off | Opens the diff panel in a new IDE tab instead of embedding it in the session tab. |
| Show markdown preview for .md files in diff | On | Shows a rendered markdown preview alongside the raw diff for `.md` files. |

### Advanced tab

![Settings — Advanced tab](screenshots/settings-advanced.png)

| Setting | Default | Description |
|---------|---------|-------------|
| Debug mode | Off | Enables verbose logging of all Claude I/O to the NetBeans log file and the Output window. |
| Hang timeout (seconds) | 60 | Kill the process if no PTY output is received within this many seconds after launch. Set to 0 to disable. |
| Enable MCP integration | On | When unchecked, the `--mcp-config` flag is not passed to `claude`. Hooks (PreToolUse) are always configured regardless of this setting. Disable only if MCP causes issues. |

### Favorites tab

![Settings — Favorites tab](screenshots/settings-favorites.png)

Manages global favorites — prompts available in every project.

The table shows all global favorites with columns: ☐ (checkbox for multi-select), **Text**, **Shortcut**.
Expand All @@ -360,6 +373,10 @@ The table shows all global favorites with columns: ☐ (checkbox for multi-selec

The search field filters entries by text.

### Profiles tab

See [Section 12 — Profiles](#12-profiles) for full details on creating and managing profiles.

---

## 12. Profiles
Expand Down Expand Up @@ -544,14 +561,14 @@ As you move the cursor or change the selection in any open editor, the plugin au
If Claude says a tool is unavailable (e.g. "I don't have access to `getWorkspaceFolders`"):

1. Check that no other application is using the MCP server port (**Tools → Options → Claude Code → General → MCP server port**, default: 28991). Change the port if needed and restart the IDE.
2. Enable **Debug mode** and check the log file (`~/.netbeans/<version>/var/log/messages.log`) for `MCP SSE server started on port`.
2. Enable **Debug mode** (**Tools → Options → Claude Code → Advanced**) and check the log file (`~/.netbeans/<version>/var/log/messages.log`) for `MCP SSE server started on port`.

---

## 14. Troubleshooting

### Enable debug mode
Go to **Tools → Options → Claude Code → General** and check **Debug mode**. This writes detailed logs of all Claude I/O (PTY bytes, MCP messages, hook calls) to the NetBeans log file.
Go to **Tools → Options → Claude Code → Advanced** and check **Debug mode**. This writes detailed logs of all Claude I/O (PTY bytes, MCP messages, hook calls) to the NetBeans log file.

### Log file location
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ public final class ClaudeSessionController {
new javax.swing.Timer(400, e -> flushPendingPrompt());
{ promptFlushTimer.setRepeats(false); }

/** Timestamp (ms) when the PTY process was launched; used for hang detection. */
private volatile long processStartedAt = 0;

/** {@code true} once the first byte/line from the PTY has been received. */
private volatile boolean firstOutputReceived = false;

/**
* Called when a hang is detected: receives (command, errorMessage).
* Invoked on the EDT after the process is stopped.
*/
private java.util.function.BiConsumer<String, String> hangCallback;

/** {@code true} while a shift-tab thread is actively switching the CC edit mode. */
private volatile boolean modeSwitchInProgress = false;

Expand Down Expand Up @@ -113,6 +125,12 @@ public final class ClaudeSessionController {
private final java.util.Deque<String> recentPtyLines = new java.util.ArrayDeque<>();
private static final int PTY_LINE_BUFFER_SIZE = 40;

/** Package-private for tests: sets hang-detection state as if the process just started. */
void simulateProcessStart(long startedAt, boolean outputReceived) {
processStartedAt = startedAt;
firstOutputReceived = outputReceived;
}

/** Package-private for tests: injects a stripped PTY line into the rolling buffer. */
void simulatePtyLine(String line) {
synchronized (recentPtyLines) {
Expand Down Expand Up @@ -167,11 +185,15 @@ public void startProcess(java.io.File dir, String profileName, String extraCliAr
customModelIds = profile != null ? new ArrayList<>(profile.getCustomModels()) : List.of();
PtyProcess process = claudeProcess.start(dir.getAbsolutePath(), profile, extraCliArgs != null ? extraCliArgs : "");

processStartedAt = System.currentTimeMillis();
firstOutputReceived = false;

connector = new PtyTtyConnector(process);
connector.setSessionTag(tag);
screenContentDetector.setSessionTag(tag);

connector.setLineListener(line -> {
firstOutputReceived = true;
LOG.fine(tag + "[PTY line] " + line);
synchronized (recentPtyLines) {
recentPtyLines.addLast(line);
Expand Down Expand Up @@ -248,6 +270,8 @@ public void stopProcess() {
customModelIds = List.of();
standardModelCount = 0;
synchronized (recentPtyLines) { recentPtyLines.clear(); }
processStartedAt = 0;
firstOutputReceived = false;

model.setModelList(List.of(), -1);
model.clearChoiceMenu();
Expand Down Expand Up @@ -861,6 +885,18 @@ void pollScreenState() {
// UNKNOWN → no transition
}

// Hang detection: if no PTY output received within the configured timeout, kill the process
if (!firstOutputReceived && processStartedAt > 0) {
int timeoutSec = ClaudeCodePreferences.getHangTimeoutSeconds();
if (timeoutSec > 0) {
long elapsed = System.currentTimeMillis() - processStartedAt;
if (elapsed > timeoutSec * 1000L) {
handleHang(timeoutSec);
return;
}
}
}

// Continuously sync CC screen mode → model (skip during switches and discovery)
if (modelComboPopulated && !modeSwitchInProgress && !modelDiscoveryInProgress) {
Optional<String> detected = screenContentDetector.detectEditMode(lines);
Expand All @@ -879,6 +915,34 @@ void pollScreenState() {
}
}

/**
* Registers a callback invoked on the EDT when a hang is detected and the
* process has been stopped.
*
* @param callback receives (command, errorMessage); may be {@code null} to clear
*/
public void setHangCallback(java.util.function.BiConsumer<String, String> callback) {
this.hangCallback = callback;
}

/**
* Called from {@link #pollScreenState()} when the hang watchdog fires.
* Stops the process and notifies the registered callback on the EDT.
*
* @param timeoutSec the configured timeout that elapsed (for the error message)
*/
private void handleHang(int timeoutSec) {
LOG.warning("Hang detected: no PTY output received within " + timeoutSec + " s — killing process");
String command = claudeProcess != null ? claudeProcess.getLastCommand() : "";
stopProcess();
java.util.function.BiConsumer<String, String> cb = hangCallback;
if (cb != null) {
String msg = "No output received within " + timeoutSec
+ " seconds. Process was killed.";
cb.accept(command, msg);
}
}

private void detectAndApplyInitialEditMode(List<String> lines) {
screenContentDetector.detectEditMode(lines).ifPresent(m -> model.setEditMode(m));
}
Expand Down
92 changes: 86 additions & 6 deletions src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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 @@ -143,11 +146,25 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl
+ ", port=" + (mcp != null ? mcp.getServerPort() : -1));
if (mcp != null && mcp.isServerRunning()) {
int port = mcp.getServerPort();
// Pass MCP server config via --mcp-config (Claude 2.x no longer reads
// mcpServers from settings.local.json; --mcp-config works in TUI mode).
cmd.add("--mcp-config");
cmd.add(buildMcpConfigJson(port));
LOG.info("Passing --mcp-config with netbeans SSE server on port " + port);
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");
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");
}
try {
writeSettingsLocalJson(workingDir, port, profile);
} catch (IOException e) {
Expand Down Expand Up @@ -205,7 +222,14 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl
* {@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 @@ -219,6 +243,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 @@ -556,6 +594,48 @@ static Map<String, String> buildEnv(ClaudeProfile profile, java.nio.file.Path pr
* Builds a minimal {@code settings.local.json} from scratch for the given port.
* Used as a fallback when the existing file cannot be parsed.
*/
/**
* Converts a command list to a shell-pasteable string.
* Arguments that contain spaces, quotes, or other shell-special characters
* are quoted so the result can be pasted directly into a terminal.
* On Windows, double-quotes inside an argument are doubled ("").
* On other platforms, they are backslash-escaped (\").
*/
static String toShellCommand(List<String> cmd) {
boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win");
return toShellCommand(cmd, windows);
}

static String toShellCommand(List<String> cmd, boolean windows) {
StringBuilder sb = new StringBuilder();
for (String arg : cmd) {
if (sb.length() > 0) sb.append(' ');
boolean needsQuoting = arg.isEmpty() || arg.chars().anyMatch(c ->
c == ' ' || c == '\t' || c == '"' || c == '\'' || c == '\\' ||
c == '{' || c == '}' || c == '(' || c == ')' ||
c == '&' || c == '|' || c == '<' || c == '>');
if (!needsQuoting) {
sb.append(arg);
} else if (windows) {
sb.append('"').append(arg.replace("\"", "\"\"")).append('"');
} else {
sb.append('"').append(arg.replace("\"", "\\\"")).append('"');
}
}
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
Loading
Loading