diff --git a/CHANGELOG.md b/CHANGELOG.md index 92db04e9..0ef03a0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed diff panel appearing for file edits despite bypassPermissions or acceptEdits mode when Claude's working directory was a subdirectory of the session root - Fixed occasional ConcurrentModificationException when the screen-poll timer reads the terminal buffer concurrently with PTY writes - Fixed tab title showing "Claude Code" for sessions opened in non-project directories — now shows the directory name - Added OpenAI-compatible proxy: profiles can now route Claude Code through any OpenAI-compatible API provider (OpenAI, Azure, Groq, DeepSeek, Ollama, local models, etc.) diff --git a/pom.xml b/pom.xml index e31a8c64..d8beefbd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.2.10-SNAPSHOT + 1.2.11-SNAPSHOT nbm Netbeans Plugin 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/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java b/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java index f14e104c..4cf6e68e 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java @@ -526,10 +526,12 @@ public CompletableFuture handlePreToolUse(String hookJson) { return CompletableFuture.completedFuture(hookAllowJson()); } - // cwd from hook JSON == confirmedDirectory of the session that launched Claude + // cwd from hook JSON == Claude's current working directory (may be a subdirectory of session root) String cwd = hook.has("cwd") ? hook.get("cwd").asText() : null; EditMode editMode = getEditModeForCwd(cwd); - LOGGER.info("handlePreToolUse: editMode=" + editMode + " cwd=" + cwd); + // Session root is the key that matched in the registry (may differ from hook cwd when Claude cd'd into a subdir) + String sessionRoot = getSessionRootForCwd(cwd); + LOGGER.info("handlePreToolUse: editMode=" + editMode + " cwd=" + cwd + " sessionRoot=" + sessionRoot); String filePath = getFilePath(toolInput); @@ -537,7 +539,7 @@ public CompletableFuture handlePreToolUse(String hookJson) { // acceptEdits: auto-allow only if file is inside the session's confirmed directory if (editMode == EditMode.BYPASS_PERMISSIONS || (editMode == EditMode.ACCEPT_EDITS - && io.github.nbplugins.claudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, cwd))) { + && io.github.nbplugins.claudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, sessionRoot))) { LOGGER.info(editMode.key() + " mode — auto-allowing: " + filePath); return CompletableFuture.completedFuture(hookAllowJson()); } @@ -749,8 +751,44 @@ private static java.util.Optional findSessionByCwd(String cwd) */ private static EditMode getEditModeForCwd(String cwd) { if (cwd == null) return EditMode.DEFAULT; - EditMode mode = io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd); - return mode != null ? mode : EditMode.DEFAULT; + // Exact match (common fast path) + EditMode exact = io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd); + if (exact != null) return exact; + // Prefix match: Claude may have cd'd into a subdirectory of the session root. + // Find the longest registered key that is an ancestor of the hook's cwd. + java.nio.file.Path cwdPath = java.nio.file.Path.of(cwd); + EditMode best = EditMode.DEFAULT; + int bestLen = -1; + for (var entry : io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.entrySet()) { + java.nio.file.Path keyPath = java.nio.file.Path.of(entry.getKey()); + if (cwdPath.startsWith(keyPath) && entry.getKey().length() > bestLen) { + best = entry.getValue(); + bestLen = entry.getKey().length(); + } + } + return best; + } + + /** + * Returns the session root directory (registry key) that best matches {@code cwd}, + * using the same longest-prefix logic as {@link #getEditModeForCwd}. + * Used to evaluate the {@code acceptEdits} directory boundary against the session root + * rather than Claude's current working directory (which may be a subdirectory). + */ + private static String getSessionRootForCwd(String cwd) { + if (cwd == null) return null; + if (io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.containsKey(cwd)) return cwd; + java.nio.file.Path cwdPath = java.nio.file.Path.of(cwd); + String best = cwd; + int bestLen = -1; + for (String key : io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.keySet()) { + java.nio.file.Path keyPath = java.nio.file.Path.of(key); + if (cwdPath.startsWith(keyPath) && key.length() > bestLen) { + best = key; + bestLen = key.length(); + } + } + return best; } /** diff --git a/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java b/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java index d94bc3be..8091231a 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java @@ -118,6 +118,37 @@ void noRegistryEntryDoesNotAutoAllow() throws Exception { "Without a registry entry the handler must not auto-allow, got: " + result); } + // ------------------------------------------------------------------------- + // bypassPermissions — hook cwd is a subdirectory of the registered session root + // ------------------------------------------------------------------------- + + @Test + void bypassPermissionsAutoAllowsWhenHookCwdIsSubdirectory() throws Exception { + // Session was started at CWD; Claude cd'd into a subdirectory and sends hook with that cwd + ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.BYPASS_PERMISSIONS); + String subCwd = CWD + "/submodules/Kotlin"; + + CompletableFuture future = handler.handlePreToolUse(editJsonWithCwd(FILE_INSIDE, subCwd)); + + assertTrue(future.isDone(), "Future must be completed immediately in bypassPermissions mode even with subdirectory cwd"); + String result = future.get(1, TimeUnit.SECONDS); + assertTrue(result.contains("\"allow\""), + "bypassPermissions must auto-allow when hook cwd is a subdirectory of registered session root, got: " + result); + } + + @Test + void acceptEditsAutoAllowsInsideProjectWhenHookCwdIsSubdirectory() throws Exception { + ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.ACCEPT_EDITS); + String subCwd = CWD + "/submodules/Kotlin"; + + CompletableFuture future = handler.handlePreToolUse(editJsonWithCwd(FILE_INSIDE, subCwd)); + + assertTrue(future.isDone(), "Future must be completed immediately for file inside project when hook cwd is subdirectory"); + String result = future.get(1, TimeUnit.SECONDS); + assertTrue(result.contains("\"allow\""), + "acceptEdits must auto-allow files inside project even when hook cwd is a subdirectory, got: " + result); + } + // ------------------------------------------------------------------------- // Non-file-edit tools are always auto-allowed // ------------------------------------------------------------------------- @@ -137,9 +168,13 @@ void nonFileEditToolAutoAllowed() throws Exception { // ------------------------------------------------------------------------- private static String editJson(String filePath) { + return editJsonWithCwd(filePath, CWD); + } + + private static String editJsonWithCwd(String filePath, String cwd) { return "{\"tool_name\":\"Edit\"," + "\"tool_input\":{\"file_path\":\"" + filePath + "\"," + "\"old_string\":\"x\",\"new_string\":\"y\"}," - + "\"cwd\":\"" + CWD + "\"}"; + + "\"cwd\":\"" + cwd + "\"}"; } }