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 @@
- Fixed choice menu not recognizing the Ultraplan "Tell Claude what to change" option as a text input field
- 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
Expand Down
131 changes: 131 additions & 0 deletions claude-launch-tests/test_ultraplan_tell_claude.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Investigate: when Claude shows the Ultraplan "Would you like to proceed?" menu
with option 4 "Tell Claude what to change", what byte sequence correctly:
1. Selects option 4 and activates text-entry mode
2. Submits the typed text back to Claude

Standard type-input menus use: digit → text → \r
But option 4 says "shift+tab to approve with this feedback",
suggesting shift+tab (\x1b[Z) might be the submit key.

Test variants:
A. 4 → text → \r
B. 4 → text → shift+tab (\x1b[Z)
"""

import pexpect
import re
import time
import sys
import os
import signal

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

SHIFT_TAB = '\x1b[Z'
ENTER = '\r'

MARKER = 'XMARKERX42' # unique string to identify our feedback


def safe_buf(child):
before = child.before if isinstance(child.before, str) else ''
after = child.after if isinstance(child.after, str) else ''
buf = child.buffer if isinstance(child.buffer, str) else ''
return before + after + buf


def run_test(label, submit_key_name, submit_key, logpath, script_suffix):
print(f'\n=== {label} ===', flush=True)
logfile = open(logpath, 'w')
child = pexpect.spawn(
CLAUDE,
['--permission-mode', 'plan',
f'Write a hello world Python script to /tmp/hello_plan_{script_suffix}.py'],
cwd=CWD,
encoding='utf-8',
timeout=120,
logfile=logfile,
)
try:
# Wait for the plan-review menu
print(' Waiting for plan-review menu (up to 120s)...', flush=True)
child.expect(r'proceed\?', timeout=120)
print(' "proceed?" detected', flush=True)

# Drain remaining menu output (wait for menu to finish drawing)
try:
child.expect(pexpect.TIMEOUT, timeout=2.0)
except pexpect.TIMEOUT:
pass

buf_plain = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', safe_buf(child))
has4 = 'Tell' in buf_plain or '4.' in buf_plain
print(f' Option 4 visible: {has4}', flush=True)

# Step 1: Send "4" to select option 4
print(f' Sending "4"...', flush=True)
child.send('4')
# Wait for Claude to respond (redraw menu or enter text mode)
try:
child.expect(pexpect.TIMEOUT, timeout=1.5)
except pexpect.TIMEOUT:
pass
buf_after4 = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', safe_buf(child))
print(f' After "4" (plain): {repr(buf_after4[-200:])}', flush=True)

# Step 2: Type the feedback
test_text = f'{MARKER} add error handling'
print(f' Typing: {repr(test_text)}', flush=True)
child.send(test_text)
time.sleep(0.3)

# Step 3: Submit with tested key
print(f' Submitting with {submit_key_name}...', flush=True)
child.send(submit_key)

# Wait for Claude to react — look for:
# - New ❯ prompt (main prompt, not menu cursor)
# - "proceed?" again (Claude re-planned with our feedback)
# - "written" or file path (Claude executed with our feedback)
# - timeout = no reaction
print(' Waiting for reaction (up to 60s)...', flush=True)
idx = child.expect(
[r'❯\xa0', r'proceed\?', r'written|Written|hello_plan', pexpect.TIMEOUT],
timeout=60
)
result_buf = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', safe_buf(child))
if idx == 0:
print(f' RESULT: Main prompt appeared — Claude accepted feedback', flush=True)
elif idx == 1:
print(f' RESULT: New plan-review menu — Claude re-planned with feedback', flush=True)
elif idx == 2:
print(f' RESULT: Claude executed/wrote the script', flush=True)
else:
print(f' RESULT: TIMEOUT — no recognizable reaction', flush=True)
print(f' Final excerpt: {repr(result_buf[-300:])}', flush=True)

except pexpect.TIMEOUT as e:
buf = re.sub(r'\x1b\[[0-9;]*[A-Za-z]', '', child.buffer or '')
print(f' OUTER TIMEOUT. Buffer: {repr(buf[-400:])}', flush=True)
except pexpect.EOF:
print(' EOF', flush=True)
except Exception as e:
print(f' ERROR: {e}', flush=True)
finally:
try:
os.kill(child.pid, signal.SIGTERM)
except Exception:
pass
try:
child.close()
except Exception:
pass
logfile.close()


run_test('Test A: 4→text→\\r', 'enter', ENTER, '/tmp/pexpect_ultraplan_a.log', 'enter')
run_test('Test B: 4→text→shift+tab', 'shifttab', SHIFT_TAB, '/tmp/pexpect_ultraplan_b.log', 'shifttab')

print('\nDone.', flush=True)
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.22-SNAPSHOT</version>
<version>0.22.23-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
43 changes: 31 additions & 12 deletions src/main/java/io/github/nbclaudecodegui/model/ChoiceMenuModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ public final class ChoiceMenuModel {
* below the label — present when Claude renders a description line under the option.
* {@code checked} is set when the option has a checked checkbox marker ({@code [x]}).
* {@code hasCheckbox} is set when the option has any checkbox marker ({@code [ ]} or {@code [x]}).
* {@code hasTextInput} is set when the option requires free-text entry, detected by a
* {@code "shift+tab to approve with this feedback"} hint line below the option.
*
* @param display user-facing label shown in the menu
* @param response string sent back to Claude when this option is selected
* @param description optional subtitle shown below the label; may be {@code null}
* @param checked whether the checkbox is pre-checked
* @param hasCheckbox whether the option has a checkbox marker at all
* @param display user-facing label shown in the menu
* @param response string sent back to Claude when this option is selected
* @param description optional subtitle shown below the label; may be {@code null}
* @param checked whether the checkbox is pre-checked
* @param hasCheckbox whether the option has a checkbox marker at all
* @param hasTextInput whether the option expects free-text input (submitted with \r)
*/
public record Option(
@JsonProperty("display") String display,
Expand All @@ -36,44 +39,60 @@ public record Option(
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@JsonProperty("checked") boolean checked,
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@JsonProperty("hasCheckbox") boolean hasCheckbox) {
@JsonProperty("hasCheckbox") boolean hasCheckbox,
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@JsonProperty("hasTextInput") boolean hasTextInput) {

/** Jackson deserialization constructor. */
@JsonCreator
public Option {
}

/**
* Convenience constructor without description, checked or hasCheckbox state.
* Convenience constructor without description, checked, hasCheckbox, or hasTextInput.
*
* @param display user-facing label
* @param response string sent back to Claude
*/
public Option(String display, String response) {
this(display, response, null, false, false);
this(display, response, null, false, false, false);
}

/**
* Convenience constructor without checked or hasCheckbox state.
* Convenience constructor without checked, hasCheckbox, or hasTextInput.
*
* @param display user-facing label
* @param response string sent back to Claude
* @param description optional subtitle; may be {@code null}
*/
public Option(String display, String response, String description) {
this(display, response, description, false, false);
this(display, response, description, false, false, false);
}

/**
* Convenience constructor without hasCheckbox state.
* Convenience constructor without hasCheckbox or hasTextInput.
*
* @param display user-facing label
* @param response string sent back to Claude
* @param description optional subtitle; may be {@code null}
* @param checked whether the checkbox is pre-checked
*/
public Option(String display, String response, String description, boolean checked) {
this(display, response, description, checked, false);
this(display, response, description, checked, false, false);
}

/**
* Convenience constructor without hasTextInput.
*
* @param display user-facing label
* @param response string sent back to Claude
* @param description optional subtitle; may be {@code null}
* @param checked whether the checkbox is pre-checked
* @param hasCheckbox whether the option has a checkbox marker
*/
public Option(String display, String response, String description, boolean checked,
boolean hasCheckbox) {
this(display, response, description, checked, hasCheckbox, false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,17 @@ public Optional<ChoiceMenuModel> detectChoiceMenu(List<String> screenLines) {
String line = raw.trim();
if (line.isBlank()) break;
if (OPTION_LINE.matcher(line).matches()) break;
// Skip keyboard-hint lines
if (isHintLine(line)) break;
// Skip keyboard-hint lines; "shift+tab to approve" also marks the option
// as requiring free-text input (hasTextInput=true).
if (isHintLine(line)) {
if (line.toLowerCase().startsWith("shift+tab")) {
ChoiceMenuModel.Option prev = options.get(oi);
options.set(oi, new ChoiceMenuModel.Option(
prev.display(), prev.response(), prev.description(),
prev.checked(), prev.hasCheckbox(), true));
}
break;
}
// Skip separator lines
if (isSeparatorLine(line)) break;
// Description must be indented (at least 1 leading space) and not too deeply
Expand Down Expand Up @@ -741,7 +750,8 @@ private static boolean isHintLine(String trimmed) {
// or start with typical shortcut keywords.
if (trimmed.contains("\u00B7")) return true; // ·
String lower = trimmed.toLowerCase();
return lower.startsWith("esc ") || lower.startsWith("enter to") || lower.startsWith("tab to");
return lower.startsWith("esc ") || lower.startsWith("enter to") || lower.startsWith("tab to")
|| lower.startsWith("shift+tab");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ private void cancel() {
* (display text starts with "type", case-insensitive).
*/
private static boolean isTypeInputOption(ChoiceMenuModel.Option opt) {
return opt.display().trim().toLowerCase().startsWith("type");
return opt.display().trim().toLowerCase().startsWith("type") || opt.hasTextInput();
}

/** Shows {@code hint} as greyed-out placeholder text; clears on focus, restores on blur if empty. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"text": "Claude has written up a plan and is ready to execute. Would you like to proceed?",
"options": [
{"display": "Yes, auto-accept edits", "response": "1"},
{"display": "Yes, manually approve edits", "response": "2"},
{"display": "No, refine with Ultraplan on Claude Code on the web", "response": "3"},
{"display": "Tell Claude what to change", "response": "4", "hasTextInput": true}
],
"defaultOptionIndex": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌

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/write-a-hello-world-serialized-kazoo.md
Loading