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 @@
- Added "Bypass Permissions" edit mode: automatically detected when Claude is launched with `--dangerously-skip-permissions`; auto-allows all file edits without showing a diff dialog
- Fixed PreToolUse and Stop hook HTTP 502 errors (regression in claude-code 2.1.118)
- Fixed toolbar button focusing an existing session tab instead of opening a new one when all open tabs have active sessions
- Reorganized General and Advanced settings tabs: moved technical settings (hang timeout, MCP port, Enable MCP integration, session list limit, debug mode) to Advanced
Expand Down
89 changes: 89 additions & 0 deletions claude-launch-tests/test_skip_permission_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Discover the screen text for the "bypass permissions" (4th) edit mode.

Claude Code cycles edit modes via Shift+Tab (ESC[Z):
default → plan → acceptEdits → bypassPermissions → default → ...

This test:
1. Launches Claude interactively.
2. Waits for the READY state.
3. Sends Shift+Tab up to 5 times, printing bottom 3 lines after each press.
4. Exits after all presses so you can see every status-line variant.

Usage:
cd claude-launch-tests && python3 test_skip_permission_mode.py

Expected output: bottom status lines for each mode, including the
4th "bypass permissions" mode text — use it to update ScreenContentDetector.
"""

import pexpect
import time
import sys

CLAUDE = '/usr/local/bin/claude'
CWD = '/tmp'
SHIFT_TAB = '\x1b[Z'


def bottom_lines(text: str, n: int = 5) -> list[str]:
"""Return the last n non-blank lines from terminal output."""
lines = text.splitlines()
non_blank = [l.rstrip() for l in lines if l.strip()]
return non_blank[-n:] if len(non_blank) >= n else non_blank


def main():
print("Launching Claude to discover bypass-permissions mode screen text...", flush=True)
child = pexpect.spawn(
CLAUDE, [],
cwd=CWD, encoding='utf-8', timeout=40,
logfile=open('/tmp/test_skip_perm_mode.log', 'w')
)

try:
# Wait for READY: input cursor ❯ or shift+tab text
print("Waiting for Claude READY state...", flush=True)
idx = child.expect([r'shift\+tab', r'❯', pexpect.TIMEOUT], timeout=30)
if idx == 2:
print("FAIL: Claude did not reach READY state", flush=True)
print("Last output:", repr(child.before[-500:]), flush=True)
sys.exit(1)

time.sleep(0.8)
# Read remaining buffered output
child.expect([pexpect.TIMEOUT], timeout=1.0)
buf = child.before
print("\n=== INITIAL STATE ===", flush=True)
for line in bottom_lines(buf):
print(" " + repr(line), flush=True)

# Cycle through modes via Shift+Tab
for i in range(1, 6):
print(f"\n=== Shift+Tab #{i} ===", flush=True)
child.send(SHIFT_TAB)
time.sleep(0.5)

# Read whatever came out
child.expect([pexpect.TIMEOUT], timeout=0.8)
buf = child.before
lines = bottom_lines(buf)
if lines:
for line in lines:
print(" " + repr(line), flush=True)
else:
print(" (no output)", flush=True)

print("\nDone. Check lines above for the bypass-permissions status line text.", flush=True)

except pexpect.EOF:
print("EOF — Claude exited unexpectedly", flush=True)
except Exception as e:
print(f"Error: {e}", flush=True)
finally:
child.close(force=True)


if __name__ == '__main__':
main()
36 changes: 36 additions & 0 deletions claude-launch-tests/test_skip_permission_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
Capture the screen text when Claude is launched with --dangerously-skip-permissions.
Prints the bottom status lines so we know what to detect.
"""

import pexpect
import time

CLAUDE = '/usr/local/bin/claude'
CWD = '/tmp'


def bottom_lines(text: str, n: int = 5) -> list:
lines = text.splitlines()
non_blank = [l.rstrip() for l in lines if l.strip()]
return non_blank[-n:] if len(non_blank) >= n else non_blank


child = pexpect.spawn(
CLAUDE, ['--dangerously-skip-permissions'],
cwd=CWD, encoding='utf-8', timeout=30,
logfile=open('/tmp/test_skip_screen.log', 'w')
)

try:
idx = child.expect([r'shift\+tab', r'❯', r'bypass', r'skip', pexpect.TIMEOUT], timeout=20)
print(f"Matched index: {idx}", flush=True)
time.sleep(1.0)
child.expect([pexpect.TIMEOUT], timeout=1.0)
buf = child.before
print("=== Bottom lines after ready ===")
for line in bottom_lines(buf, 8):
print(" " + repr(line))
finally:
child.close(force=True)
39 changes: 39 additions & 0 deletions docs/to-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [ ] Bug 1

Choice menu не распознаёт 4 как вариант для теустового ввода

```
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌

Claude has written up a plan and is ready to execute. Would you like to proceed?

❯ 1. Yes, auto-accept edits
2. Yes, manually approve edits
3. No, refine with Ultraplan on Claude Code on the web
4. Tell Claude what to change
shift+tab to approve with this feedback

ctrl-g to edit in nano · ~/.claude/plans/https-github-com-nbclaudecodegui-netbean-serialized-kazoo.md
```

# [x] Bug 2

```
● Ran 1 stop hook
⎿  http://localhost:28991/stop
⎿  Stop hook error: HTTP 502 from http://localhost:28991/stop
```

# [ ] Feature 3

Добавить возможность навигации стрелками в ChoiceMenuPanel:
- Right, Left
- между Yes и No
- Right - из checkbox в текстовое поле ввода, соответствующее этому чекбоксу, если оно есть
- Up, Down
- С Yes, No - на последний/первый radiobox
- Из текстового поля ввода - на предыдущий/последующий пункт

# [ ] Bug 4

Не поддерживает skip permition mode в комбо.
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.22.19-SNAPSHOT</version>
<version>0.22.22-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 @@ -5,6 +5,7 @@
import com.pty4j.PtyProcess;
import io.github.nbclaudecodegui.model.ChoiceMenuModel;
import io.github.nbclaudecodegui.model.ClaudeSessionModel;
import io.github.nbclaudecodegui.model.EditMode;
import io.github.nbclaudecodegui.model.SavedSession;
import io.github.nbclaudecodegui.model.SessionLifecycle;
import io.github.nbclaudecodegui.model.SessionMode;
Expand Down Expand Up @@ -242,7 +243,7 @@ public void startProcess(java.io.File dir, String profileName, String extraCliAr
widget.start();

// Ensure the initial edit mode is registered so the hook can read it
String initialMode = model.getEditMode() != null ? model.getEditMode() : "default";
EditMode initialMode = model.getEditMode() != null ? model.getEditMode() : EditMode.DEFAULT;
model.setEditMode(initialMode);

modelDiscoveryAttempts = 0;
Expand Down Expand Up @@ -640,52 +641,52 @@ public void sendShiftTab() {
* <p>If the selected mode differs from the current model state, updates the
* model and triggers a Shift+Tab sequence to switch Claude's built-in mode.
*
* @param newMode the newly selected mode string (one of {@code "plan"},
* {@code "default"}, or {@code "acceptEdits"})
* @param newMode the newly selected mode
*/
public void onEditModeComboChanged(String newMode) {
public void onEditModeComboChanged(EditMode newMode) {
if (newMode == null) return;
if (newMode.equals(model.getEditMode())) return;
if (newMode == model.getEditMode()) return;
model.setEditMode(newMode);
sendShiftTabsUntilMode(newMode);
}

/**
* Sends Shift+Tab (ESC[Z) presses until the terminal screen shows
* {@code targetMode}, or until 3 attempts are exhausted.
* {@code targetMode}, or until 5 attempts are exhausted.
* 5 attempts cover the full 4-mode cycle plus one extra in case of lag.
*
* <p>Runs on a daemon thread; sets lifecycle to WORKING at start and
* READY on completion.
*
* @param targetMode the edit mode to reach
*/
private void sendShiftTabsUntilMode(String targetMode) {
private void sendShiftTabsUntilMode(EditMode targetMode) {
if (connector == null) return;
model.setLifecycle(SessionLifecycle.WORKING);
modeSwitchInProgress = true;
Thread t = new Thread(() -> {
try {
LOG.info("sendShiftTabsUntilMode: target=" + targetMode
+ " currentModel=" + model.getEditMode());
for (int attempt = 0; attempt < 3; attempt++) {
for (int attempt = 0; attempt < 5; attempt++) {
connector.write(new byte[]{0x1b, '[', 'Z'});
Thread.sleep(200);
Optional<String> detected =
Optional<EditMode> detected =
screenContentDetector.detectEditMode(screenLines.get());
LOG.fine("sendShiftTabsUntilMode: attempt=" + attempt
+ " detected=" + detected.orElse("(empty)"));
if (detected.isPresent() && detected.get().equals(targetMode)) {
+ " detected=" + detected.map(EditMode::key).orElse("(empty)"));
if (detected.isPresent() && detected.get() == targetMode) {
LOG.fine("sendShiftTabsUntilMode: reached target on attempt=" + attempt);
return;
}
// "default" mode has no idle-screen marker — empty detection means we are in default
if ("default".equals(targetMode) && !detected.isPresent()) {
// DEFAULT mode has no idle-screen marker — empty detection means we are in default
if (targetMode == EditMode.DEFAULT && detected.isEmpty()) {
LOG.fine("sendShiftTabsUntilMode: no mode marker → treating as default on attempt=" + attempt);
return;
}
}
LOG.warning("sendShiftTabsUntilMode: did not reach " + targetMode
+ " after 3 attempts");
+ " after 5 attempts");
} catch (IOException | InterruptedException ex) {
LOG.warning("sendShiftTabsUntilMode failed: " + ex.getMessage());
} finally {
Expand Down Expand Up @@ -988,17 +989,18 @@ void pollScreenState() {

// Continuously sync CC screen mode → model (skip during switches and discovery)
if (modelComboPopulated && !modeSwitchInProgress && !modelDiscoveryInProgress) {
Optional<String> detected = screenContentDetector.detectEditMode(lines);
Optional<EditMode> detected = screenContentDetector.detectEditMode(lines);
LOG.fine("[pollScreenState] editMode sync: detected=" + detected.orElse(null) + " current=" + model.getEditMode());
if (detected.isPresent()) {
String mode = detected.get();
if (!mode.equals(model.getEditMode())) {
EditMode mode = detected.get();
if (mode != model.getEditMode()) {
model.setEditMode(mode);
}
} else if (model.getLifecycle() != SessionLifecycle.WORKING) {
// Unknown mode outside WORKING (screen transitioning or idle with no indicator)
// → treat as Ask/default. During WORKING: preserve current registry value.
if (!"default".equals(model.getEditMode())) {
model.setEditMode("default");
if (model.getEditMode() != EditMode.DEFAULT) {
model.setEditMode(EditMode.DEFAULT);
}
}
}
Expand Down Expand Up @@ -1072,7 +1074,13 @@ private void handleHang(int timeoutSec) {
}

private void detectAndApplyInitialEditMode(List<String> lines) {
screenContentDetector.detectEditMode(lines).ifPresent(m -> model.setEditMode(m));
Optional<EditMode> detected = screenContentDetector.detectEditMode(lines);
List<String> bottom = new java.util.ArrayList<>();
for (int i = lines.size() - 1; i >= 0 && bottom.size() < 3; i--) {
if (!lines.get(i).isBlank()) bottom.add(lines.get(i));
}
LOG.fine("[detectAndApplyInitialEditMode] detected=" + detected.orElse(null) + " bottom3=" + bottom);
detected.ifPresent(m -> model.setEditMode(m));
}

/**
Expand Down
23 changes: 13 additions & 10 deletions src/main/java/io/github/nbclaudecodegui/mcp/NetBeansMCPHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package io.github.nbclaudecodegui.mcp;

import com.fasterxml.jackson.databind.JsonNode;
import io.github.nbclaudecodegui.model.EditMode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
Expand Down Expand Up @@ -501,15 +502,17 @@ public CompletableFuture<String> handlePreToolUse(String hookJson) {

// cwd from hook JSON == confirmedDirectory of the session that launched Claude
String cwd = hook.has("cwd") ? hook.get("cwd").asText() : null;
String editMode = getEditModeForCwd(cwd);
EditMode editMode = getEditModeForCwd(cwd);
LOGGER.info("handlePreToolUse: editMode=" + editMode + " cwd=" + cwd);

String filePath = getFilePath(toolInput);

// bypassPermissions: auto-allow everything regardless of location
// acceptEdits: auto-allow only if file is inside the session's confirmed directory
if ("acceptEdits".equals(editMode)
&& io.github.nbclaudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, cwd)) {
LOGGER.info("acceptEdits mode — file inside project, auto-allowing: " + filePath);
if (editMode == EditMode.BYPASS_PERMISSIONS
|| (editMode == EditMode.ACCEPT_EDITS
&& io.github.nbclaudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, cwd))) {
LOGGER.info(editMode.key() + " mode — auto-allowing: " + filePath);
return CompletableFuture.completedFuture(hookAllowJson());
}
// plan / ask / acceptEdits-outside: fall through to show diff dialog
Expand Down Expand Up @@ -681,16 +684,16 @@ private static java.util.Optional<ClaudeSessionTab> findSessionByCwd(String cwd)
}

/**
* Returns the edit mode of the session whose working directory is {@code cwd},
* or {@code "default"} if the session cannot be found.
* Returns the {@link EditMode} of the session whose working directory is {@code cwd},
* or {@link EditMode#DEFAULT} if the session cannot be found.
*
* <p>Safe to call from any thread — uses the thread-safe registry in
* {@link io.github.nbclaudecodegui.model.ClaudeSessionModel#EDIT_MODE_REGISTRY}.
*/
private static String getEditModeForCwd(String cwd) {
if (cwd == null) return "default";
String mode = io.github.nbclaudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd);
return mode != null ? mode : "default";
private static EditMode getEditModeForCwd(String cwd) {
if (cwd == null) return EditMode.DEFAULT;
EditMode mode = io.github.nbclaudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd);
return mode != null ? mode : EditMode.DEFAULT;
}

/**
Expand Down
Loading
Loading